initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
));
|
||||
Reference in New Issue
Block a user