initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.buttonsWrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {
BookmarkIcon,
CheckIcon,
DotsThreeOutlineIcon,
GearIcon,
HeartIcon,
LinkSimpleIcon,
MegaphoneIcon,
PaperPlaneRightIcon,
PlayIcon,
PlusIcon,
ShareFatIcon,
TrashIcon,
} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as ToastActionCreators from '~/actions/ToastActionCreators';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabSection,
} from '~/components/modals/shared/SettingsTabLayout';
import {Button} from '~/components/uikit/Button/Button';
import styles from './ButtonsTab.module.css';
import {SubsectionTitle} from './shared';
interface ButtonsTabProps {
openContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
}
export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}) => {
const {t} = useLingui();
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsTabSection
title={<Trans>Button Variants</Trans>}
description={<Trans>Click any button to see toast notifications with feedback.</Trans>}
>
<div className={styles.buttonsWrapper}>
<Button
leftIcon={<PlusIcon size={16} weight="bold" />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Primary button clicked!`})}
>
<Trans>Primary</Trans>
</Button>
<Button
variant="secondary"
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Secondary button clicked!`})}
>
<Trans>Secondary</Trans>
</Button>
<Button
variant="danger-primary"
onClick={() => ToastActionCreators.createToast({type: 'error', children: t`Danger primary clicked!`})}
>
<Trans>Danger Primary</Trans>
</Button>
<Button
variant="danger-secondary"
onClick={() => ToastActionCreators.createToast({type: 'error', children: t`Danger secondary clicked!`})}
>
<Trans>Danger Secondary</Trans>
</Button>
<Button
variant="inverted"
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Inverted button clicked!`})}
>
<Trans>Inverted</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Disabled States</Trans>}>
<div className={styles.buttonsWrapper}>
<Button disabled>
<Trans>Primary (Disabled)</Trans>
</Button>
<Button variant="secondary" disabled>
<Trans>Secondary (Disabled)</Trans>
</Button>
<Button variant="danger-primary" disabled>
<Trans>Danger (Disabled)</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Button Sizes</Trans>}>
<div className={styles.buttonsWrapper}>
<Button
small
leftIcon={<MegaphoneIcon size={14} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Small button clicked!`})}
>
<Trans>Small Button</Trans>
</Button>
<Button
leftIcon={<MegaphoneIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Regular button clicked!`})}
>
<Trans>Regular Button</Trans>
</Button>
<Button
small
variant="secondary"
leftIcon={<GearIcon size={14} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Small secondary clicked!`})}
>
<Trans>Small Secondary</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Buttons with Icons</Trans>}>
<SubsectionTitle>
<Trans>Left Icon</Trans>
</SubsectionTitle>
<div className={styles.buttonsWrapper}>
<Button
leftIcon={<PlusIcon size={16} weight="bold" />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Add action!`})}
>
<Trans>Add Item</Trans>
</Button>
<Button
variant="secondary"
leftIcon={<GearIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Settings opened!`})}
>
<Trans>Settings</Trans>
</Button>
<Button
variant="danger-primary"
leftIcon={<TrashIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'error', children: t`Delete action!`})}
>
<Trans>Delete</Trans>
</Button>
<Button
leftIcon={<ShareFatIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Share action!`})}
>
<Trans>Share</Trans>
</Button>
</div>
<SubsectionTitle>
<Trans>Right Icon</Trans>
</SubsectionTitle>
<div className={styles.buttonsWrapper}>
<Button
rightIcon={<PaperPlaneRightIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Message sent!`})}
>
<Trans>Send</Trans>
</Button>
<Button
variant="secondary"
rightIcon={<LinkSimpleIcon size={16} weight="bold" />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Link copied!`})}
>
<Trans>Copy Link</Trans>
</Button>
</div>
<SubsectionTitle>
<Trans>Both Sides</Trans>
</SubsectionTitle>
<div className={styles.buttonsWrapper}>
<Button
leftIcon={<PlusIcon size={16} weight="bold" />}
rightIcon={<ShareFatIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Action with both icons!`})}
>
<Trans>Create & Share</Trans>
</Button>
<Button
variant="secondary"
leftIcon={<HeartIcon size={16} />}
rightIcon={<CheckIcon size={16} weight="bold" />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Saved!`})}
>
<Trans>Save Favorite</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Square Icon Buttons</Trans>}
description={<Trans>Compact buttons with just an icon, perfect for toolbars and action bars.</Trans>}
>
<div className={styles.buttonsWrapper}>
<Button
square
aria-label={t`Play`}
icon={<PlayIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Play!`})}
/>
<Button
square
aria-label={t`Settings`}
icon={<GearIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Settings!`})}
/>
<Button
square
variant="secondary"
aria-label={t`Bookmark`}
icon={<BookmarkIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Bookmarked!`})}
/>
<Button
square
variant="secondary"
aria-label={t`Heart`}
icon={<HeartIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Liked!`})}
/>
<Button
square
variant="danger-primary"
aria-label={t`Delete`}
icon={<TrashIcon size={16} />}
onClick={() => ToastActionCreators.createToast({type: 'error', children: t`Deleted!`})}
/>
<Button square aria-label={t`More`} icon={<DotsThreeOutlineIcon size={16} />} onClick={openContextMenu} />
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Loading States</Trans>}
description={<Trans>Buttons show a loading indicator when submitting is true.</Trans>}
>
<div className={styles.buttonsWrapper}>
<Button submitting>
<Trans>Submitting</Trans>
</Button>
<Button variant="secondary" submitting>
<Trans>Loading</Trans>
</Button>
<Button small submitting leftIcon={<MegaphoneIcon size={14} />}>
<Trans>Small Submitting</Trans>
</Button>
<Button variant="danger-primary" submitting>
<Trans>Processing</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Button with Context Menu</Trans>}
description={
<Trans>
Buttons can trigger context menus on click by passing the onClick event directly to openContextMenu.
</Trans>
}
>
<div className={styles.buttonsWrapper}>
<Button leftIcon={<DotsThreeOutlineIcon size={16} />} onClick={openContextMenu}>
<Trans>Open Menu</Trans>
</Button>
<Button
square
icon={<DotsThreeOutlineIcon size={16} />}
aria-label={t`Open Menu (icon)`}
onClick={openContextMenu}
/>
</div>
</SettingsTabSection>
</SettingsTabContent>
</SettingsTabContainer>
);
});

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.itemsWrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.5rem;
}
.itemWithLabel {
display: flex;
align-items: center;
gap: 0.5rem;
}
.itemText {
font-size: 0.875rem;
color: var(--text-primary);
}
.itemTextSmall {
font-size: 0.75rem;
color: var(--text-primary);
}
.itemTextBase {
font-size: 1rem;
color: var(--text-primary);
}
.itemTextTertiary {
font-size: 0.75rem;
color: var(--text-tertiary);
}
.avatarGroup {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.avatarShapes {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stacksWrapper {
display: flex;
flex-direction: column;
gap: 1rem;
}
.stackItem {
display: flex;
align-items: center;
gap: 1rem;
}
.badgesWrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {ChannelTypes, getStatusTypeLabel, StatusTypes} from '~/Constants';
import {GroupDMAvatar} from '~/components/common/GroupDMAvatar';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabSection,
} from '~/components/modals/shared/SettingsTabLayout';
import {AvatarStack} from '~/components/uikit/avatars/AvatarStack';
import {MentionBadge} from '~/components/uikit/MentionBadge';
import {MockAvatar} from '~/components/uikit/MockAvatar';
import {ChannelRecord} from '~/records/ChannelRecord';
import type {UserPartial} from '~/records/UserRecord';
import styles from './IndicatorsTab.module.css';
import {SubsectionTitle} from './shared';
const AVATAR_SIZES_WITH_STATUS: Array<16 | 24 | 32 | 36 | 40 | 48 | 80> = [16, 24, 32, 36, 40, 48, 80];
const AVATAR_STATUSES: Array<string> = [
StatusTypes.ONLINE,
StatusTypes.IDLE,
StatusTypes.DND,
StatusTypes.INVISIBLE,
StatusTypes.OFFLINE,
];
const createMockRecipient = (id: string): UserPartial => ({
id,
username: id,
discriminator: '0000',
avatar: null,
flags: 0,
});
const createMockGroupDMChannel = (id: string, recipientIds: Array<string>): ChannelRecord =>
new ChannelRecord({
id,
type: ChannelTypes.GROUP_DM,
recipients: recipientIds.map(createMockRecipient),
});
const getMockGroupDMChannels = (): Array<ChannelRecord> => [
createMockGroupDMChannel('1000000000000000001', ['1000000000000000002', '1000000000000000003']),
createMockGroupDMChannel('1000000000000000004', [
'1000000000000000005',
'1000000000000000006',
'1000000000000000007',
]),
];
export const IndicatorsTab: React.FC = observer(() => {
const {i18n} = useLingui();
const mockGroupDMChannels = getMockGroupDMChannels();
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsTabSection
title={<Trans>Status Indicators</Trans>}
description={
<Trans>
Visual indicators showing user status, rendered using the same masked status badges as avatars: online,
idle, do not disturb, invisible, and offline.
</Trans>
}
>
<SubsectionTitle>
<Trans>Single User (All Statuses)</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_STATUSES.map((status) => (
<div key={status} className={styles.avatarGroup}>
<MockAvatar size={40} status={status} />
<span className={styles.itemTextTertiary}>{getStatusTypeLabel(i18n, status) ?? status}</span>
</div>
))}
</div>
<SubsectionTitle>
<Trans>Mobile Online Status on Avatars</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_SIZES_WITH_STATUS.map((size) => (
<div key={`mobile-avatar-size-${size}`} className={styles.avatarGroup}>
<MockAvatar size={size} status={StatusTypes.ONLINE} isMobileStatus />
<span className={styles.itemTextTertiary}>{size}px</span>
</div>
))}
</div>
<SubsectionTitle>
<Trans>Different Sizes (Status Supported)</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_SIZES_WITH_STATUS.map((size) => (
<div key={size} className={styles.avatarGroup}>
<MockAvatar size={size} status={StatusTypes.ONLINE} />
<span className={styles.itemTextTertiary}>{size}px</span>
</div>
))}
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Mention Badges</Trans>}
description={<Trans>Notification badges showing unread mention counts in different sizes.</Trans>}
>
<SubsectionTitle>
<Trans>Medium Size (Default)</Trans>
</SubsectionTitle>
<div className={styles.badgesWrapper}>
<MentionBadge mentionCount={1} />
<MentionBadge mentionCount={5} />
<MentionBadge mentionCount={12} />
<MentionBadge mentionCount={99} />
<MentionBadge mentionCount={150} />
<MentionBadge mentionCount={1000} />
<MentionBadge mentionCount={9999} />
</div>
<SubsectionTitle>
<Trans>Small Size</Trans>
</SubsectionTitle>
<div className={styles.badgesWrapper}>
<MentionBadge mentionCount={1} size="small" />
<MentionBadge mentionCount={5} size="small" />
<MentionBadge mentionCount={12} size="small" />
<MentionBadge mentionCount={99} size="small" />
<MentionBadge mentionCount={150} size="small" />
<MentionBadge mentionCount={1000} size="small" />
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Mock Avatars</Trans>}
description={<Trans>Mock user avatars in various sizes and all status permutations.</Trans>}
>
<SubsectionTitle>
<Trans>Different Sizes (Online)</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_SIZES_WITH_STATUS.map((size) => (
<div key={size} className={styles.avatarGroup}>
<MockAvatar size={size} status={StatusTypes.ONLINE} />
<span className={styles.itemTextTertiary}>{size}px</span>
</div>
))}
</div>
<SubsectionTitle>
<Trans>All Status Types</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_STATUSES.map((status) => (
<div key={status} className={styles.avatarGroup}>
<MockAvatar size={48} status={status} />
<span className={styles.itemTextTertiary}>{getStatusTypeLabel(i18n, status) ?? status}</span>
</div>
))}
</div>
<SubsectionTitle>
<Trans>Typing State</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
{AVATAR_STATUSES.map((status) => (
<div key={status} className={styles.avatarGroup}>
<MockAvatar size={48} status={status} isTyping />
<span className={styles.itemTextTertiary}>{getStatusTypeLabel(i18n, status) ?? status}</span>
</div>
))}
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Group DM Avatars</Trans>}
description={
<Trans>
Group DM avatars using the same status masks as regular avatars, including stacked layouts and typing
states.
</Trans>
}
>
<SubsectionTitle>
<Trans>Different Sizes & Member Counts</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[0]} size={32} />
<span className={styles.itemTextTertiary}>32px · 2 members</span>
</div>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[1]} size={40} />
<span className={styles.itemTextTertiary}>40px · 3 members</span>
</div>
</div>
<SubsectionTitle>
<Trans>Group Online Status</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[0]} size={32} statusOverride={StatusTypes.ONLINE} />
<span className={styles.itemTextTertiary}>
<Trans>2 members (online)</Trans>
</span>
</div>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[1]} size={40} statusOverride={StatusTypes.ONLINE} />
<span className={styles.itemTextTertiary}>
<Trans>3 members (online)</Trans>
</span>
</div>
</div>
<SubsectionTitle>
<Trans>Group Typing States</Trans>
</SubsectionTitle>
<div className={styles.itemsWrapper}>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[0]} size={32} isTyping />
<span className={styles.itemTextTertiary}>
<Trans>2 members (typing)</Trans>
</span>
</div>
<div className={styles.avatarGroup}>
<GroupDMAvatar channel={mockGroupDMChannels[1]} size={40} isTyping />
<span className={styles.itemTextTertiary}>
<Trans>3 members (typing)</Trans>
</span>
</div>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Avatar Stacks</Trans>}
description={<Trans>Overlapping avatar groups showing multiple users with automatic overflow counts.</Trans>}
>
<SubsectionTitle>
<Trans>Different Sizes</Trans>
</SubsectionTitle>
<div className={styles.stacksWrapper}>
<div className={styles.stackItem}>
<AvatarStack size={24}>
{[1, 2, 3, 4].map((i) => (
<MockAvatar key={i} size={24} userTag={`User ${i}`} />
))}
</AvatarStack>
<span className={styles.itemTextTertiary}>24px</span>
</div>
<div className={styles.stackItem}>
<AvatarStack size={32}>
{[1, 2, 3, 4].map((i) => (
<MockAvatar key={i} size={32} userTag={`User ${i}`} />
))}
</AvatarStack>
<span className={styles.itemTextTertiary}>32px</span>
</div>
<div className={styles.stackItem}>
<AvatarStack size={40}>
{[1, 2, 3, 4].map((i) => (
<MockAvatar key={i} size={40} userTag={`User ${i}`} />
))}
</AvatarStack>
<span className={styles.itemTextTertiary}>40px</span>
</div>
</div>
<SubsectionTitle>
<Trans>Max Visible Count</Trans>
</SubsectionTitle>
<div className={styles.stacksWrapper}>
<div className={styles.stackItem}>
<AvatarStack size={32} maxVisible={3}>
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<MockAvatar key={i} size={32} userTag={`User ${i}`} />
))}
</AvatarStack>
<span className={styles.itemTextTertiary}>
<Trans>Show max 3 (+5 badge)</Trans>
</span>
</div>
<div className={styles.stackItem}>
<AvatarStack size={32} maxVisible={5}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (
<MockAvatar key={i} size={32} userTag={`User ${i}`} />
))}
</AvatarStack>
<span className={styles.itemTextTertiary}>
<Trans>Show max 5 (+5 badge)</Trans>
</span>
</div>
</div>
</SettingsTabSection>
</SettingsTabContent>
</SettingsTabContainer>
);
});

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.section {
border-bottom: 1px solid var(--background-modifier-accent);
padding-bottom: 2rem;
}
.sectionTitle {
margin-bottom: 1rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {
DotsThreeOutlineIcon,
GearIcon,
LinkSimpleIcon,
PlayIcon,
PlusIcon,
ShareFatIcon,
TrashIcon,
} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import React from 'react';
import {openFromEvent} from '~/actions/ContextMenuActionCreators';
import {MenuGroup} from '~/components/uikit/ContextMenu/MenuGroup';
import {MenuGroups} from '~/components/uikit/ContextMenu/MenuGroups';
import {MenuItem} from '~/components/uikit/ContextMenu/MenuItem';
import {MenuItemCheckbox} from '~/components/uikit/ContextMenu/MenuItemCheckbox';
import {MenuItemRadio} from '~/components/uikit/ContextMenu/MenuItemRadio';
import {MenuItemSlider} from '~/components/uikit/ContextMenu/MenuItemSlider';
import {MenuItemSubmenu} from '~/components/uikit/ContextMenu/MenuItemSubmenu';
import type {RadioOption} from '~/components/uikit/RadioGroup/RadioGroup';
import {ButtonsTab} from './ButtonsTab';
import {IndicatorsTab} from './IndicatorsTab';
import styles from './Inline.module.css';
import {InputsTab} from './InputsTab';
import {OverlaysTab} from './OverlaysTab';
import {SelectionsTab} from './SelectionsTab';
export const ComponentGalleryInlineTab: React.FC = observer(() => {
const {t} = useLingui();
const [primarySwitch, setPrimarySwitch] = React.useState(true);
const [dangerSwitch, setDangerSwitch] = React.useState(false);
const [selectValue, setSelectValue] = React.useState('opt1');
const [selectValue2, setSelectValue2] = React.useState('size-md');
const [sliderValue, setSliderValue] = React.useState(42);
const [sliderValue2, setSliderValue2] = React.useState(75);
const [sliderValue3, setSliderValue3] = React.useState(50);
const [sliderValue4, setSliderValue4] = React.useState(75);
const [sliderValue5, setSliderValue5] = React.useState(60);
const [color, setColor] = React.useState(0x3b82f6);
const [color2, setColor2] = React.useState(0xff5733);
const [radioValue, setRadioValue] = React.useState<'a' | 'b' | 'c'>('a');
const [checkOne, setCheckOne] = React.useState(true);
const [checkTwo, setCheckTwo] = React.useState(false);
const [checkboxChecked, setCheckboxChecked] = React.useState(false);
const [checkboxChecked2, setCheckboxChecked2] = React.useState(true);
const [radioGroupValue, setRadioGroupValue] = React.useState<string>('option1');
const [inputValue1, setInputValue1] = React.useState('');
const [inputValue2, setInputValue2] = React.useState('');
const [inputValue3, setInputValue3] = React.useState('');
const [searchValue, setSearchValue] = React.useState('');
const [emailValue, setEmailValue] = React.useState('');
const [passwordValue, setPasswordValue] = React.useState('');
const [textareaValue1, setTextareaValue1] = React.useState('');
const [textareaValue2, setTextareaValue2] = React.useState('This is some example text in the textarea.');
const [inlineEditValue, setInlineEditValue] = React.useState('EditableText');
const radioOptions: Array<RadioOption<string>> = [
{value: 'option1', name: t`First Option`, desc: t`This is the first option description`},
{value: 'option2', name: t`Second Option`, desc: t`This is the second option description`},
{value: 'option3', name: t`Third Option`, desc: t`This is the third option description`},
];
const handleOpenContextMenu = React.useCallback(
(event: React.MouseEvent<HTMLElement>) => {
openFromEvent(event, ({onClose}) => (
<MenuGroups>
<MenuGroup>
<MenuItem icon={<GearIcon size={16} />} onClick={() => onClose()}>
<Trans>Settings</Trans>
</MenuItem>
<MenuItem icon={<ShareFatIcon size={16} />} onClick={() => onClose()}>
<Trans>Share</Trans>
</MenuItem>
<MenuItem icon={<LinkSimpleIcon size={16} weight="bold" />} onClick={() => onClose()}>
<Trans>Copy Link</Trans>
</MenuItem>
</MenuGroup>
<MenuGroup>
<MenuItemCheckbox icon={<PlusIcon size={16} weight="bold" />} checked={checkOne} onChange={setCheckOne}>
<Trans>Enable Extra Option</Trans>
</MenuItemCheckbox>
<MenuItemCheckbox icon={<PlusIcon size={16} weight="bold" />} checked={checkTwo} onChange={setCheckTwo}>
<Trans>Enable Beta Feature</Trans>
</MenuItemCheckbox>
</MenuGroup>
<MenuGroup>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'a'}
onSelect={() => setRadioValue('a')}
>
<Trans>Mode A</Trans>
</MenuItemRadio>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'b'}
onSelect={() => setRadioValue('b')}
>
<Trans>Mode B</Trans>
</MenuItemRadio>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'c'}
onSelect={() => setRadioValue('c')}
>
<Trans>Mode C</Trans>
</MenuItemRadio>
</MenuGroup>
<MenuGroup>
<MenuItemSlider
label={t`Opacity`}
value={sliderValue}
minValue={0}
maxValue={100}
onChange={(v: number) => setSliderValue(Math.round(v))}
onFormat={(v: number) => `${Math.round(v)}%`}
/>
</MenuGroup>
<MenuGroup>
<MenuItemSubmenu
label={t`More Actions`}
icon={<DotsThreeOutlineIcon size={16} />}
render={() => (
<>
<MenuItem onClick={() => onClose()}>
<Trans>Duplicate</Trans>
</MenuItem>
<MenuItem onClick={() => onClose()}>
<Trans>Archive</Trans>
</MenuItem>
</>
)}
/>
<MenuItem icon={<TrashIcon size={16} />} danger onClick={() => onClose()}>
<Trans>Delete</Trans>
</MenuItem>
</MenuGroup>
</MenuGroups>
));
},
[checkOne, checkTwo, radioValue, sliderValue],
);
return (
<div className={styles.container}>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t`Buttons`}</h3>
<ButtonsTab openContextMenu={handleOpenContextMenu} />
</div>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t`Inputs & Text`}</h3>
<InputsTab
inputValue1={inputValue1}
setInputValue1={setInputValue1}
inputValue2={inputValue2}
setInputValue2={setInputValue2}
inputValue3={inputValue3}
setInputValue3={setInputValue3}
searchValue={searchValue}
setSearchValue={setSearchValue}
emailValue={emailValue}
setEmailValue={setEmailValue}
passwordValue={passwordValue}
setPasswordValue={setPasswordValue}
textareaValue1={textareaValue1}
setTextareaValue1={setTextareaValue1}
textareaValue2={textareaValue2}
setTextareaValue2={setTextareaValue2}
inlineEditValue={inlineEditValue}
setInlineEditValue={setInlineEditValue}
color={color}
setColor={setColor}
color2={color2}
setColor2={setColor2}
/>
</div>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t`Selections`}</h3>
<SelectionsTab
selectValue={selectValue}
setSelectValue={setSelectValue}
selectValue2={selectValue2}
setSelectValue2={setSelectValue2}
primarySwitch={primarySwitch}
setPrimarySwitch={setPrimarySwitch}
dangerSwitch={dangerSwitch}
setDangerSwitch={setDangerSwitch}
checkboxChecked={checkboxChecked}
setCheckboxChecked={setCheckboxChecked}
checkboxChecked2={checkboxChecked2}
setCheckboxChecked2={setCheckboxChecked2}
radioGroupValue={radioGroupValue}
setRadioGroupValue={setRadioGroupValue}
radioOptions={radioOptions}
sliderValue={sliderValue}
setSliderValue={setSliderValue}
sliderValue2={sliderValue2}
setSliderValue2={setSliderValue2}
sliderValue3={sliderValue3}
setSliderValue3={setSliderValue3}
sliderValue4={sliderValue4}
setSliderValue4={setSliderValue4}
sliderValue5={sliderValue5}
setSliderValue5={setSliderValue5}
/>
</div>
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t`Overlays & Menus`}</h3>
<OverlaysTab openContextMenu={handleOpenContextMenu} />
</div>
<div>
<h3 className={styles.sectionTitle}>{t`Indicators & Status`}</h3>
<IndicatorsTab />
</div>
</div>
);
});

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
.gridSingle {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.inlineEditWrapper {
display: flex;
align-items: center;
gap: 1rem;
}
.inlineEditLabel {
font-size: 0.875rem;
color: var(--text-tertiary);
}
.inlineEditWrapper {
display: flex;
align-items: center;
gap: 1rem;
}
.inlineEditCaption {
font-size: 0.875rem;
color: var(--text-tertiary);
}
.colorPickersGrid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.colorPickersGrid {
grid-template-columns: repeat(2, 1fr);
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {MagnifyingGlassIcon, UserIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as ToastActionCreators from '~/actions/ToastActionCreators';
import {ColorPickerField} from '~/components/form/ColorPickerField';
import {Input, Textarea} from '~/components/form/Input';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabSection,
} from '~/components/modals/shared/SettingsTabLayout';
import {InlineEdit} from '~/components/uikit/InlineEdit';
import styles from './InputsTab.module.css';
interface InputsTabProps {
inputValue1: string;
setInputValue1: (value: string) => void;
inputValue2: string;
setInputValue2: (value: string) => void;
inputValue3: string;
setInputValue3: (value: string) => void;
searchValue: string;
setSearchValue: (value: string) => void;
emailValue: string;
setEmailValue: (value: string) => void;
passwordValue: string;
setPasswordValue: (value: string) => void;
textareaValue1: string;
setTextareaValue1: (value: string) => void;
textareaValue2: string;
setTextareaValue2: (value: string) => void;
inlineEditValue: string;
setInlineEditValue: (value: string) => void;
color: number;
setColor: (value: number) => void;
color2: number;
setColor2: (value: number) => void;
}
export const InputsTab: React.FC<InputsTabProps> = observer(
({
inputValue1,
setInputValue1,
inputValue2,
setInputValue2,
inputValue3,
setInputValue3,
searchValue,
setSearchValue,
emailValue,
setEmailValue,
passwordValue,
setPasswordValue,
textareaValue1,
setTextareaValue1,
textareaValue2,
setTextareaValue2,
inlineEditValue,
setInlineEditValue,
color,
setColor,
color2,
setColor2,
}) => {
const {t} = useLingui();
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsTabSection
title={<Trans>Basic Text Inputs</Trans>}
description={<Trans>All inputs are fully interactive - type to test them out!</Trans>}
>
<div className={styles.grid}>
<Input
label={t`Display Name`}
placeholder={t`Enter your name`}
value={inputValue1}
onChange={(e) => setInputValue1(e.target.value)}
/>
<Input
label={t`Username`}
placeholder={t`@username`}
value={inputValue2}
onChange={(e) => setInputValue2(e.target.value)}
/>
<Input
label={t`Email Address`}
type="email"
placeholder={t`your@email.com`}
value={emailValue}
onChange={(e) => setEmailValue(e.target.value)}
/>
<Input
label={t`Password`}
type="password"
placeholder={t`Enter a secure password`}
value={passwordValue}
onChange={(e) => setPasswordValue(e.target.value)}
/>
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Inputs with Icons</Trans>}>
<div className={styles.grid}>
<Input
label={t`Search`}
placeholder={t`Search for anything...`}
leftIcon={<MagnifyingGlassIcon size={16} weight="bold" />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
<Input
label={t`User Profile`}
placeholder={t`Enter username`}
leftIcon={<UserIcon size={16} />}
value={inputValue3}
onChange={(e) => setInputValue3(e.target.value)}
/>
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Input States</Trans>}>
<div className={styles.grid}>
<Input
label={t`With Error`}
placeholder={t`This field has an error`}
error={t`This is an error message`}
/>
<Input label={t`Disabled Input`} placeholder={t`Cannot be edited`} disabled value="Disabled value" />
</div>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Textarea</Trans>}>
<div className={styles.grid}>
<Textarea
label={t`About You`}
placeholder={t`Write a short bio (max 280 characters)`}
maxLength={280}
showCharacterCount
value={textareaValue1}
onChange={(e) => setTextareaValue1(e.target.value)}
/>
<Textarea
label={t`Message`}
placeholder={t`Type your message here...`}
minRows={3}
value={textareaValue2}
onChange={(e) => setTextareaValue2(e.target.value)}
/>
</div>
<div className={styles.gridSingle}>
<Textarea
label={t`Long Form Content`}
placeholder={t`Write your content here... This textarea expands as you type.`}
minRows={4}
maxRows={12}
value={textareaValue1}
onChange={(e) => setTextareaValue1(e.target.value)}
footer={
<p className={styles.inlineEditLabel}>
<Trans>This textarea auto-expands between 4-12 rows as you type.</Trans>
</p>
}
/>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Inline Edit</Trans>}
description={
<Trans>Click the text below to edit it inline. Press Enter to save or Escape to cancel.</Trans>
}
>
<div className={styles.inlineEditWrapper}>
<span className={styles.inlineEditCaption}>
<Trans>Editable Text:</Trans>
</span>
<InlineEdit
value={inlineEditValue}
onSave={(newValue) => {
setInlineEditValue(newValue);
ToastActionCreators.createToast({type: 'success', children: t`Value saved: ${newValue}`});
}}
placeholder={t`Enter text`}
maxLength={50}
/>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Color Pickers</Trans>}
description={<Trans>Click to open the color picker and choose a new color.</Trans>}
>
<div className={styles.colorPickersGrid}>
<ColorPickerField label={t`Primary Accent Color`} value={color} onChange={setColor} />
<ColorPickerField label={t`Secondary Accent Color`} value={color2} onChange={setColor2} />
</div>
</SettingsTabSection>
</SettingsTabContent>
</SettingsTabContainer>
);
},
);

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.sectionsContainer {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.sectionHeader {
display: flex;
flex-direction: column;
gap: 0.25rem;
border-top: 1px solid var(--background-header-secondary);
padding-top: 1rem;
}
.item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.itemHeader {
display: flex;
align-items: center;
gap: 0.5rem;
}
.itemLabel {
display: inline-block;
white-space: nowrap;
font-family: monospace;
font-size: 0.875rem;
color: var(--text-tertiary);
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import React from 'react';
import {MessagePreviewContext, MessageStates, MessageTypes} from '~/Constants';
import {Message} from '~/components/channel/Message';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabHeader,
} from '~/components/modals/shared/SettingsTabLayout';
import {Scroller} from '~/components/uikit/Scroller';
import {ChannelRecord} from '~/records/ChannelRecord';
import {MessageRecord} from '~/records/MessageRecord';
import UserStore from '~/stores/UserStore';
import appearanceStyles from '../AppearanceTab.module.css';
import styles from './MarkdownTab.module.css';
import {SubsectionTitle} from './shared';
export const MarkdownTab: React.FC = observer(() => {
const {t} = useLingui();
const createMarkdownPreviewMessages = React.useCallback(() => {
const currentUser = UserStore.getCurrentUser();
const author = currentUser?.toJSON() || {
id: 'markdown-preview-user',
username: 'MarkdownUser',
discriminator: '0000',
global_name: 'Markdown Preview User',
avatar: null,
bot: false,
system: false,
flags: 0,
};
const fakeChannel = new ChannelRecord({
id: 'markdown-preview-channel',
type: 0,
name: 'markdown-preview',
position: 0,
parent_id: null,
topic: null,
url: null,
nsfw: false,
last_message_id: null,
last_pin_timestamp: null,
bitrate: null,
user_limit: null,
permission_overwrites: [],
});
const tabOpenedAt = new Date();
const markdownSections = [
{
title: t`Text Formatting`,
items: [
{label: '**bold**', content: '**bold text**'},
{label: '*italic*', content: '*italic text*'},
{label: '***bold italic***', content: '***bold italic***'},
{label: '__underline__', content: '__underline text__'},
{label: '~~strikethrough~~', content: '~~strikethrough text~~'},
{label: '`code`', content: '`inline code`'},
{label: '||spoiler||', content: '||spoiler text||'},
{label: '\\*escaped\\*', content: '\\*escaped asterisks\\*'},
],
},
{
title: t`Headings`,
items: [
{label: '#', content: '# Heading 1'},
{label: '##', content: '## Heading 2'},
{label: '###', content: '### Heading 3'},
{label: '####', content: '#### Heading 4'},
],
},
{
title: t`Links`,
items: [
{label: '[text](url)', content: '[Masked Link](https://fluxer.app)'},
{label: '<url>', content: '<https://fluxer.app>'},
{label: 'url', content: 'https://fluxer.app'},
{label: '<email>', content: '<contact@fluxer.app>'},
],
},
{
title: t`Lists`,
items: [
{label: 'Unordered', content: '- First item\n- Second item\n- Third item'},
{label: 'Ordered', content: '1. First item\n2. Second item\n3. Third item'},
{
label: 'Nested',
content: '- Parent item\n - Nested item\n - Another nested\n- Another parent',
},
],
},
{
title: t`Blockquotes`,
items: [
{label: 'Single line', content: '> Single line quote'},
{label: 'Multi-line', content: '> Multi-line quote\n> Spans multiple lines\n> Continues here'},
{
label: 'Alternative',
content: '>>> Multi-line quote\nContinues without > on each line\nUntil the message ends',
},
],
},
{
title: t`Code Blocks`,
items: [
{label: 'Plain', content: '```\nfunction example() {\n return "Hello";\n}\n```'},
{
label: 'JavaScript',
// biome-ignore lint/suspicious/noTemplateCurlyInString: this is intended
content: '```js\nfunction greet(name) {\n console.log(`Hello, ${name}!`);\n}\n```',
},
{
label: 'Python',
content: '```py\ndef factorial(n):\n return 1 if n <= 1 else n * factorial(n-1)\n```',
},
],
},
{
title: t`Tables`,
items: [
{
label: 'Basic',
content: '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |',
},
{
label: 'Aligned',
content: '| Left | Center | Right |\n|:-----|:------:|------:|\n| L1 | C1 | R1 |',
},
],
},
{
title: t`Alerts / Callouts`,
items: [
{label: '[!NOTE]', content: '> [!NOTE]\n> Helpful information here'},
{label: '[!TIP]', content: '> [!TIP]\n> Useful suggestion here'},
{label: '[!IMPORTANT]', content: '> [!IMPORTANT]\n> Critical information here'},
{label: '[!WARNING]', content: '> [!WARNING]\n> Exercise caution here'},
{label: '[!CAUTION]', content: '> [!CAUTION]\n> Potential risks here'},
],
},
{
title: t`Special Features`,
items: [
{label: 'Subtext', content: '-# This is subtext that appears smaller and dimmed'},
{label: 'Block Spoiler', content: '||\nBlock spoiler content\nClick to reveal!\n||'},
{label: 'Unicode Emojis', content: '🎉 🚀 ❤️ 👍 😀'},
{label: 'Shortcodes', content: ':tm: :copyright: :registered:'},
],
},
{
title: t`Mentions & Timestamps`,
items: [
{label: '@everyone', content: '@everyone'},
{label: '@here', content: '@here'},
{label: 'Short Time', content: '<t:1618936830:t>'},
{label: 'Long Time', content: '<t:1618936830:T>'},
{label: 'Short Date', content: '<t:1618936830:d>'},
{label: 'Long Date', content: '<t:1618936830:D>'},
{label: 'Default', content: '<t:1618936830:f>'},
{label: 'Full', content: '<t:1618936830:F>'},
{label: 'Short Date/Time', content: '<t:1618936830:s>'},
{label: 'Relative', content: '<t:1618936830:R>'},
],
},
];
return {
fakeChannel,
createMessage: (content: string, id: string) => {
return new MessageRecord(
{
id: `markdown-preview-${id}`,
channel_id: 'markdown-preview-channel',
author,
type: MessageTypes.DEFAULT,
flags: 0,
pinned: false,
mention_everyone: false,
content,
timestamp: tabOpenedAt.toISOString(),
state: MessageStates.SENT,
},
{skipUserCache: true},
);
},
markdownSections,
};
}, [t]);
const {fakeChannel, createMessage, markdownSections} = createMarkdownPreviewMessages();
return (
<SettingsTabContainer>
<SettingsTabHeader
title={<Trans>Markdown Preview</Trans>}
description={<Trans>Each message below demonstrates a single markdown feature with live preview.</Trans>}
/>
<SettingsTabContent>
<div className={styles.sectionsContainer}>
{markdownSections.map((section, sectionIndex) => (
<div key={sectionIndex} className={styles.section}>
<div className={styles.sectionHeader}>
<SubsectionTitle>{section.title}</SubsectionTitle>
</div>
{section.items.map((item, itemIndex) => {
const message = createMessage(item.content, `${sectionIndex}-${itemIndex}`);
return (
<div key={itemIndex} className={styles.item}>
<div className={styles.itemHeader}>
<code className={styles.itemLabel}>{item.label}</code>
</div>
<div className={appearanceStyles.previewWrapper}>
<div
className={clsx(appearanceStyles.previewContainer, appearanceStyles.previewContainerCozy)}
style={{
height: 'auto',
minHeight: '60px',
maxHeight: '300px',
}}
>
<Scroller
className={appearanceStyles.previewMessagesContainer}
style={{
height: 'auto',
minHeight: '60px',
maxHeight: '280px',
pointerEvents: 'auto',
paddingTop: '16px',
paddingBottom: '16px',
}}
>
<Message
channel={fakeChannel}
message={message}
previewContext={MessagePreviewContext.SETTINGS}
shouldGroup={false}
/>
</Scroller>
<div className={appearanceStyles.previewOverlay} style={{pointerEvents: 'none'}} />
</div>
</div>
</div>
);
})}
</div>
))}
</div>
</SettingsTabContent>
</SettingsTabContainer>
);
});

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.buttonsWrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.demoArea {
border-radius: 0.5rem;
border: 1px solid var(--background-modifier-accent);
background-color: var(--background-secondary);
padding: 1rem;
font-size: 0.875rem;
color: var(--text-primary);
cursor: pointer;
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {DotsThreeOutlineIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as ToastActionCreators from '~/actions/ToastActionCreators';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabSection,
} from '~/components/modals/shared/SettingsTabLayout';
import {Button} from '~/components/uikit/Button/Button';
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
import styles from './OverlaysTab.module.css';
interface OverlaysTabProps {
openContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
}
export const OverlaysTab: React.FC<OverlaysTabProps> = observer(({openContextMenu}) => {
const {t} = useLingui();
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsTabSection
title={<Trans>Tooltips</Trans>}
description={<Trans>Hover over buttons to see tooltips in different positions.</Trans>}
>
<div className={styles.buttonsWrapper}>
<Tooltip text={t`I am a tooltip`}>
<Button>
<Trans>Hover Me</Trans>
</Button>
</Tooltip>
<Tooltip text={t`Top Tooltip`} position="top">
<Button variant="secondary">
<Trans>Top</Trans>
</Button>
</Tooltip>
<Tooltip text={t`Right Tooltip`} position="right">
<Button variant="secondary">
<Trans>Right</Trans>
</Button>
</Tooltip>
<Tooltip text={t`Bottom Tooltip`} position="bottom">
<Button variant="secondary">
<Trans>Bottom</Trans>
</Button>
</Tooltip>
<Tooltip text={t`Left Tooltip`} position="left">
<Button variant="secondary">
<Trans>Left</Trans>
</Button>
</Tooltip>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Toasts</Trans>}
description={<Trans>Toasts appear in the top-center of the screen.</Trans>}
>
<div className={styles.buttonsWrapper}>
<Button onClick={() => ToastActionCreators.createToast({type: 'success', children: t`Great Success!`})}>
<Trans>Success</Trans>
</Button>
<Button
variant="danger-primary"
onClick={() => ToastActionCreators.createToast({type: 'error', children: t`Something went wrong.`})}
>
<Trans>Error</Trans>
</Button>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Context Menus</Trans>}
description={
<Trans>
Context menus can be opened with left-click (on buttons) or right-click (on other elements). This
demonstrates various menu items including checkboxes, radio buttons, sliders, and submenus.
</Trans>
}
>
<div className={styles.buttonsWrapper}>
<Button leftIcon={<DotsThreeOutlineIcon size={16} />} onClick={openContextMenu}>
<Trans>Open Menu</Trans>
</Button>
<Button
square
icon={<DotsThreeOutlineIcon size={16} />}
aria-label={t`Open Menu (icon)`}
onClick={openContextMenu}
/>
<div role="button" tabIndex={0} onContextMenu={openContextMenu} className={styles.demoArea}>
<Trans>Right-click here to open the context menu</Trans>
</div>
</div>
</SettingsTabSection>
</SettingsTabContent>
</SettingsTabContainer>
);
});

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.descriptionSmall {
font-size: 0.75rem;
color: var(--text-secondary);
}
.gridDouble {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.gridDouble {
grid-template-columns: 1fr 1fr;
}
}
.gridSingle {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.contentList {
display: flex;
flex-direction: column;
gap: 1rem;
}
.sliderRow {
display: flex;
align-items: center;
gap: 1rem;
}
.sliderContainer {
width: 100%;
}
.sliderValue {
min-width: 56px;
text-align: right;
font-family: monospace;
font-size: 0.875rem;
color: var(--text-primary);
}
.sliderValueDisabled {
min-width: 56px;
text-align: right;
font-family: monospace;
font-size: 0.875rem;
color: var(--text-tertiary);
}

View File

@@ -0,0 +1,351 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {t} from '@lingui/core/macro';
import {Trans} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {Select} from '~/components/form/Select';
import {Switch} from '~/components/form/Switch';
import {
SettingsTabContainer,
SettingsTabContent,
SettingsTabSection,
} from '~/components/modals/shared/SettingsTabLayout';
import {Checkbox} from '~/components/uikit/Checkbox/Checkbox';
import {RadioGroup, type RadioOption} from '~/components/uikit/RadioGroup/RadioGroup';
import {Slider} from '~/components/uikit/Slider';
import styles from './SelectionsTab.module.css';
import {SubsectionTitle} from './shared';
interface SelectionsTabProps {
selectValue: string;
setSelectValue: (value: string) => void;
selectValue2: string;
setSelectValue2: (value: string) => void;
primarySwitch: boolean;
setPrimarySwitch: (value: boolean) => void;
dangerSwitch: boolean;
setDangerSwitch: (value: boolean) => void;
checkboxChecked: boolean;
setCheckboxChecked: (value: boolean) => void;
checkboxChecked2: boolean;
setCheckboxChecked2: (value: boolean) => void;
radioGroupValue: string;
setRadioGroupValue: (value: string) => void;
radioOptions: Array<RadioOption<string>>;
sliderValue: number;
setSliderValue: (value: number) => void;
sliderValue2: number;
setSliderValue2: (value: number) => void;
sliderValue3: number;
setSliderValue3: (value: number) => void;
sliderValue4: number;
setSliderValue4: (value: number) => void;
sliderValue5: number;
setSliderValue5: (value: number) => void;
}
export const SelectionsTab: React.FC<SelectionsTabProps> = observer(
({
selectValue,
setSelectValue,
selectValue2,
setSelectValue2,
primarySwitch,
setPrimarySwitch,
dangerSwitch,
setDangerSwitch,
checkboxChecked,
setCheckboxChecked,
checkboxChecked2,
setCheckboxChecked2,
radioGroupValue,
setRadioGroupValue,
radioOptions,
sliderValue,
setSliderValue,
sliderValue2,
setSliderValue2,
sliderValue3,
setSliderValue3,
sliderValue4,
setSliderValue4,
sliderValue5,
}) => {
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsTabSection
title={<Trans>Select Dropdown</Trans>}
description={<Trans>Click to open the dropdown menu and select different options.</Trans>}
>
<div className={styles.gridDouble}>
<Select<string>
label={t`Choose an option`}
value={selectValue}
onChange={(value) => {
setSelectValue(value);
}}
options={[
{value: 'opt1', label: t`Option One`},
{value: 'opt2', label: t`Option Two`},
{value: 'opt3', label: t`Option Three`},
{value: 'opt4', label: t`Option Four`},
]}
/>
<Select<string>
label={t`Size Selection`}
value={selectValue2}
onChange={(value) => {
setSelectValue2(value);
}}
options={[
{value: 'size-sm', label: t`Small`},
{value: 'size-md', label: t`Medium`},
{value: 'size-lg', label: t`Large`},
{value: 'size-xl', label: t`Extra Large`},
]}
/>
</div>
<div className={styles.gridSingle}>
<Select
label={t`Disabled Select`}
value="disabled-opt"
onChange={() => {}}
disabled
options={[{value: 'disabled-opt', label: t`This is disabled`}]}
/>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Switches</Trans>}
description={<Trans>Toggle switches on and off to see state changes.</Trans>}
>
<div className={styles.contentList}>
<Switch
label={t`Enable Notifications`}
description={t`Receive notifications when someone mentions you`}
value={primarySwitch}
onChange={(value) => {
setPrimarySwitch(value);
}}
/>
<Switch
label={t`Dark Mode`}
description={t`Use dark theme across the application`}
value={dangerSwitch}
onChange={(value) => {
setDangerSwitch(value);
}}
/>
<Switch
label={t`Disabled Switch`}
description={t`This switch is disabled and cannot be toggled`}
value={false}
onChange={() => {}}
disabled
/>
<Switch
label={t`Disabled (Checked)`}
description={t`This switch is disabled in the checked state`}
value={true}
onChange={() => {}}
disabled
/>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Checkboxes</Trans>}
description={<Trans>Click to check and uncheck. Available in square and round styles.</Trans>}
>
<SubsectionTitle>
<Trans>Square Checkboxes</Trans>
</SubsectionTitle>
<div className={styles.contentList}>
<Checkbox
checked={checkboxChecked}
onChange={(checked) => {
setCheckboxChecked(checked);
}}
>
<Trans>Interactive Checkbox</Trans>
</Checkbox>
<Checkbox
checked={checkboxChecked2}
onChange={(checked) => {
setCheckboxChecked2(checked);
}}
>
<Trans>Another Checkbox</Trans>
</Checkbox>
<Checkbox checked={true} disabled>
<Trans>Disabled (Checked)</Trans>
</Checkbox>
<Checkbox checked={false} disabled>
<Trans>Disabled (Unchecked)</Trans>
</Checkbox>
</div>
<SubsectionTitle>
<Trans>Round Checkboxes</Trans>
</SubsectionTitle>
<div className={styles.contentList}>
<Checkbox checked={checkboxChecked} onChange={(checked) => setCheckboxChecked(checked)} type="round">
<Trans>Round Style Checkbox</Trans>
</Checkbox>
<Checkbox checked={checkboxChecked2} onChange={(checked) => setCheckboxChecked2(checked)} type="round">
<Trans>Another Round Checkbox</Trans>
</Checkbox>
<Checkbox checked={true} disabled type="round">
<Trans>Disabled Round (Checked)</Trans>
</Checkbox>
</div>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Radio Group</Trans>}
description={<Trans>Radio buttons allow selecting one option from a group.</Trans>}
>
<RadioGroup
aria-label={t`Select an option from the radio group`}
options={radioOptions}
value={radioGroupValue}
onChange={(value) => {
setRadioGroupValue(value);
}}
/>
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Sliders</Trans>}
description={
<Trans>Drag the slider handles to adjust values. Click markers to jump to specific values.</Trans>
}
>
<SubsectionTitle>
<Trans>Standard Slider with Markers</Trans>
</SubsectionTitle>
<div className={styles.sliderRow}>
<div className={styles.sliderContainer}>
<Slider
defaultValue={sliderValue}
factoryDefaultValue={42}
minValue={0}
maxValue={100}
onValueChange={(v) => setSliderValue(Math.round(v))}
onValueRender={(v) => `${Math.round(v)}%`}
markers={[0, 25, 50, 75, 100]}
onMarkerRender={(m) => `${m}%`}
/>
</div>
<div className={styles.sliderValue}>{sliderValue}%</div>
</div>
<SubsectionTitle>
<Trans>Slider with Fewer Markers</Trans>
</SubsectionTitle>
<div className={styles.sliderRow}>
<div className={styles.sliderContainer}>
<Slider
defaultValue={sliderValue2}
factoryDefaultValue={75}
minValue={0}
maxValue={100}
onValueChange={(v) => setSliderValue2(Math.round(v))}
onValueRender={(v) => `${Math.round(v)}%`}
markers={[0, 50, 100]}
onMarkerRender={(m) => `${m}%`}
/>
</div>
<div className={styles.sliderValue}>{sliderValue2}%</div>
</div>
<SubsectionTitle>
<Trans>Slider with Step Values</Trans>
</SubsectionTitle>
<p className={styles.descriptionSmall}>
<Trans>Snaps to increments of 5.</Trans>
</p>
<div className={styles.sliderRow}>
<div className={styles.sliderContainer}>
<Slider
defaultValue={sliderValue3}
factoryDefaultValue={50}
minValue={0}
maxValue={100}
step={5}
onValueChange={(v) => setSliderValue3(Math.round(v))}
onValueRender={(v) => `${Math.round(v)}%`}
markers={[0, 25, 50, 75, 100]}
onMarkerRender={(m) => `${m}%`}
/>
</div>
<div className={styles.sliderValue}>{sliderValue3}%</div>
</div>
<SubsectionTitle>
<Trans>Markers Below Slider</Trans>
</SubsectionTitle>
<p className={styles.descriptionSmall}>
<Trans>Alternative marker positioning.</Trans>
</p>
<div className={styles.sliderRow}>
<div className={styles.sliderContainer}>
<Slider
defaultValue={sliderValue4}
factoryDefaultValue={75}
minValue={0}
maxValue={100}
markerPosition="below"
onValueChange={(v) => setSliderValue4(Math.round(v))}
onValueRender={(v) => `${Math.round(v)}%`}
markers={[0, 25, 50, 75, 100]}
onMarkerRender={(m) => `${m}%`}
/>
</div>
<div className={styles.sliderValue}>{sliderValue4}%</div>
</div>
<SubsectionTitle>
<Trans>Disabled Slider</Trans>
</SubsectionTitle>
<div className={styles.sliderRow}>
<div className={styles.sliderContainer}>
<Slider
defaultValue={sliderValue5}
factoryDefaultValue={60}
minValue={0}
maxValue={100}
disabled
onValueRender={(v) => `${Math.round(v)}%`}
markers={[0, 25, 50, 75, 100]}
onMarkerRender={(m) => `${m}%`}
/>
</div>
<div className={styles.sliderValueDisabled}>{sliderValue5}%</div>
</div>
</SettingsTabSection>
</SettingsTabContent>
</SettingsTabContainer>
);
},
);

View File

@@ -0,0 +1,281 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Trans, useLingui} from '@lingui/react/macro';
import {
DotsThreeOutlineIcon,
GearIcon,
LinkSimpleIcon,
PlayIcon,
PlusIcon,
ShareFatIcon,
TrashIcon,
WarningCircleIcon,
} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import React from 'react';
import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators';
import {SettingsSection} from '~/components/modals/shared/SettingsSection';
import {SettingsTabContainer, SettingsTabContent} from '~/components/modals/shared/SettingsTabLayout';
import {StatusSlate} from '~/components/modals/shared/StatusSlate';
import {MenuGroup} from '~/components/uikit/ContextMenu/MenuGroup';
import {MenuGroups} from '~/components/uikit/ContextMenu/MenuGroups';
import {MenuItem} from '~/components/uikit/ContextMenu/MenuItem';
import {MenuItemCheckbox} from '~/components/uikit/ContextMenu/MenuItemCheckbox';
import {MenuItemRadio} from '~/components/uikit/ContextMenu/MenuItemRadio';
import {MenuItemSlider} from '~/components/uikit/ContextMenu/MenuItemSlider';
import {MenuItemSubmenu} from '~/components/uikit/ContextMenu/MenuItemSubmenu';
import type {RadioOption} from '~/components/uikit/RadioGroup/RadioGroup';
import {ButtonsTab} from './ButtonsTab';
import {IndicatorsTab} from './IndicatorsTab';
import {InputsTab} from './InputsTab';
import {MarkdownTab} from './MarkdownTab';
import {OverlaysTab} from './OverlaysTab';
import {SelectionsTab} from './SelectionsTab';
const ComponentGalleryTab: React.FC = observer(() => {
const {t} = useLingui();
const [primarySwitch, setPrimarySwitch] = React.useState(true);
const [dangerSwitch, setDangerSwitch] = React.useState(false);
const [selectValue, setSelectValue] = React.useState('opt1');
const [selectValue2, setSelectValue2] = React.useState('size-md');
const [sliderValue, setSliderValue] = React.useState(42);
const [sliderValue2, setSliderValue2] = React.useState(75);
const [sliderValue3, setSliderValue3] = React.useState(50);
const [sliderValue4, setSliderValue4] = React.useState(75);
const [sliderValue5, setSliderValue5] = React.useState(60);
const [color, setColor] = React.useState(0x3b82f6);
const [color2, setColor2] = React.useState(0xff5733);
const [radioValue, setRadioValue] = React.useState<'a' | 'b' | 'c'>('a');
const [checkOne, setCheckOne] = React.useState(true);
const [checkTwo, setCheckTwo] = React.useState(false);
const [checkboxChecked, setCheckboxChecked] = React.useState(false);
const [checkboxChecked2, setCheckboxChecked2] = React.useState(true);
const [radioGroupValue, setRadioGroupValue] = React.useState<string>('option1');
const [inputValue1, setInputValue1] = React.useState('');
const [inputValue2, setInputValue2] = React.useState('');
const [inputValue3, setInputValue3] = React.useState('');
const [searchValue, setSearchValue] = React.useState('');
const [emailValue, setEmailValue] = React.useState('');
const [passwordValue, setPasswordValue] = React.useState('');
const [textareaValue1, setTextareaValue1] = React.useState('');
const [textareaValue2, setTextareaValue2] = React.useState('This is some example text in the textarea.');
const [inlineEditValue, setInlineEditValue] = React.useState('EditableText');
const radioOptions: Array<RadioOption<string>> = [
{value: 'option1', name: t`First Option`, desc: t`This is the first option description`},
{value: 'option2', name: t`Second Option`, desc: t`This is the second option description`},
{value: 'option3', name: t`Third Option`, desc: t`This is the third option description`},
];
const openContextMenu = React.useCallback(
(event: React.MouseEvent<HTMLElement>) => {
ContextMenuActionCreators.openFromEvent(event, ({onClose}) => (
<MenuGroups>
<MenuGroup>
<MenuItem icon={<GearIcon size={16} />} onClick={() => onClose()}>
<Trans>Settings</Trans>
</MenuItem>
<MenuItem icon={<ShareFatIcon size={16} />} onClick={() => onClose()}>
<Trans>Share</Trans>
</MenuItem>
<MenuItem icon={<LinkSimpleIcon size={16} weight="bold" />} onClick={() => onClose()}>
<Trans>Copy Link</Trans>
</MenuItem>
</MenuGroup>
<MenuGroup>
<MenuItemCheckbox icon={<PlusIcon size={16} weight="bold" />} checked={checkOne} onChange={setCheckOne}>
<Trans>Enable Extra Option</Trans>
</MenuItemCheckbox>
<MenuItemCheckbox icon={<PlusIcon size={16} weight="bold" />} checked={checkTwo} onChange={setCheckTwo}>
<Trans>Enable Beta Feature</Trans>
</MenuItemCheckbox>
</MenuGroup>
<MenuGroup>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'a'}
onSelect={() => setRadioValue('a')}
>
<Trans>Mode A</Trans>
</MenuItemRadio>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'b'}
onSelect={() => setRadioValue('b')}
>
<Trans>Mode B</Trans>
</MenuItemRadio>
<MenuItemRadio
icon={<PlayIcon size={16} />}
selected={radioValue === 'c'}
onSelect={() => setRadioValue('c')}
>
<Trans>Mode C</Trans>
</MenuItemRadio>
</MenuGroup>
<MenuGroup>
<MenuItemSlider
label={t`Opacity`}
value={sliderValue}
minValue={0}
maxValue={100}
onChange={(v: number) => setSliderValue(Math.round(v))}
onFormat={(v: number) => `${Math.round(v)}%`}
/>
</MenuGroup>
<MenuGroup>
<MenuItemSubmenu
label={t`More Actions`}
icon={<DotsThreeOutlineIcon size={16} />}
render={() => (
<>
<MenuItem onClick={() => onClose()}>
<Trans>Duplicate</Trans>
</MenuItem>
<MenuItem onClick={() => onClose()}>
<Trans>Archive</Trans>
</MenuItem>
</>
)}
/>
<MenuItem icon={<TrashIcon size={16} />} danger onClick={() => onClose()}>
<Trans>Delete</Trans>
</MenuItem>
</MenuGroup>
</MenuGroups>
));
},
[checkOne, checkTwo, radioValue, sliderValue],
);
return (
<SettingsTabContainer>
<SettingsTabContent>
<SettingsSection id="buttons" title={t`Buttons`}>
<ButtonsTab openContextMenu={openContextMenu} />
</SettingsSection>
<SettingsSection id="inputs" title={t`Inputs & Text`}>
<InputsTab
inputValue1={inputValue1}
setInputValue1={setInputValue1}
inputValue2={inputValue2}
setInputValue2={setInputValue2}
inputValue3={inputValue3}
setInputValue3={setInputValue3}
searchValue={searchValue}
setSearchValue={setSearchValue}
emailValue={emailValue}
setEmailValue={setEmailValue}
passwordValue={passwordValue}
setPasswordValue={setPasswordValue}
textareaValue1={textareaValue1}
setTextareaValue1={setTextareaValue1}
textareaValue2={textareaValue2}
setTextareaValue2={setTextareaValue2}
inlineEditValue={inlineEditValue}
setInlineEditValue={setInlineEditValue}
color={color}
setColor={setColor}
color2={color2}
setColor2={setColor2}
/>
</SettingsSection>
<SettingsSection id="selections" title={t`Selections`}>
<SelectionsTab
selectValue={selectValue}
setSelectValue={setSelectValue}
selectValue2={selectValue2}
setSelectValue2={setSelectValue2}
primarySwitch={primarySwitch}
setPrimarySwitch={setPrimarySwitch}
dangerSwitch={dangerSwitch}
setDangerSwitch={setDangerSwitch}
checkboxChecked={checkboxChecked}
setCheckboxChecked={setCheckboxChecked}
checkboxChecked2={checkboxChecked2}
setCheckboxChecked2={setCheckboxChecked2}
radioGroupValue={radioGroupValue}
setRadioGroupValue={setRadioGroupValue}
radioOptions={radioOptions}
sliderValue={sliderValue}
setSliderValue={setSliderValue}
sliderValue2={sliderValue2}
setSliderValue2={setSliderValue2}
sliderValue3={sliderValue3}
setSliderValue3={setSliderValue3}
sliderValue4={sliderValue4}
setSliderValue4={setSliderValue4}
sliderValue5={sliderValue5}
setSliderValue5={setSliderValue5}
/>
</SettingsSection>
<SettingsSection id="overlays" title={t`Overlays & Menus`}>
<OverlaysTab openContextMenu={openContextMenu} />
</SettingsSection>
<SettingsSection id="indicators" title={t`Indicators & Status`}>
<IndicatorsTab />
</SettingsSection>
<SettingsSection
id="status"
title={t`Status Slate`}
description={t`A reusable component for empty states, errors, and status messages.`}
>
<StatusSlate
Icon={WarningCircleIcon}
title={<Trans>Lorem ipsum dolor sit amet</Trans>}
description={
<Trans>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua.
</Trans>
}
actions={[
{
text: <Trans>Primary Action</Trans>,
onClick: () => {},
},
{
text: <Trans>Secondary Action</Trans>,
onClick: () => {},
variant: 'secondary',
},
]}
/>
</SettingsSection>
<SettingsSection id="markdown" title={t`Markdown`}>
<MarkdownTab />
</SettingsSection>
</SettingsTabContent>
</SettingsTabContainer>
);
});
export default ComponentGalleryTab;

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
.sectionTitle {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.subsectionTitle {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {observer} from 'mobx-react-lite';
import type React from 'react';
import styles from './shared.module.css';
export const SubsectionTitle = observer(({children}: {children: React.ReactNode}) => (
<h4 className={styles.subsectionTitle}>{children}</h4>
));