@fragments-sdk/ui 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +14 -2
- package/fragments.json +1 -1
- package/package.json +23 -2
- package/src/assets/fragments-logo.tsx +37 -0
- package/src/assets/fragments_logo.svg +1 -0
- package/src/assets/fragments_logo_text.svg +1 -0
- package/src/blocks/AccountSettings.block.ts +1 -1
- package/src/blocks/ActivityFeed.block.ts +7 -7
- package/src/blocks/ChatInterface.block.ts +36 -80
- package/src/blocks/DashboardLayout.block.ts +85 -66
- package/src/blocks/DashboardPage.block.ts +298 -0
- package/src/blocks/EmptyState.block.ts +6 -4
- package/src/blocks/FeatureGrid.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +21 -26
- package/src/blocks/PricingComparison.block.ts +1 -1
- package/src/blocks/SettingsPanel.block.ts +4 -4
- package/src/blocks/ShoppingCart.block.ts +2 -2
- package/src/components/Accordion/Accordion.fragment.tsx +67 -1
- package/src/components/Alert/Alert.fragment.tsx +69 -1
- package/src/components/Alert/Alert.module.scss +7 -3
- package/src/components/AppShell/AppShell.fragment.tsx +326 -87
- package/src/components/Avatar/Avatar.fragment.tsx +35 -2
- package/src/components/Badge/Badge.fragment.tsx +47 -9
- package/src/components/Badge/Badge.module.scss +1 -0
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +1 -1
- package/src/components/Breadcrumbs/Breadcrumbs.module.scss +3 -0
- package/src/components/Button/Button.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.fragment.tsx +4 -4
- package/src/components/Checkbox/Checkbox.module.scss +7 -7
- package/src/components/Checkbox/index.tsx +6 -1
- package/src/components/Chip/Chip.fragment.tsx +1 -1
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +10 -2
- package/src/components/CodeBlock/CodeBlock.module.scss +48 -11
- package/src/components/CodeBlock/CodeBlock.test.tsx +51 -1
- package/src/components/CodeBlock/index.tsx +221 -3
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +1 -1
- package/src/components/ColorPicker/ColorPicker.module.scss +8 -7
- package/src/components/Combobox/Combobox.fragment.tsx +1 -1
- package/src/components/Combobox/Combobox.module.scss +1 -3
- package/src/components/Field/index.tsx +1 -1
- package/src/components/Form/Form.fragment.tsx +4 -4
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.test.tsx +35 -0
- package/src/components/Input/index.tsx +47 -2
- package/src/components/Menu/Menu.module.scss +2 -0
- package/src/components/Message/Message.module.scss +3 -3
- package/src/components/Popover/Popover.fragment.tsx +1 -1
- package/src/components/Popover/Popover.module.scss +1 -3
- package/src/components/Prompt/Prompt.module.scss +6 -19
- package/src/components/Prompt/Prompt.test.tsx +8 -0
- package/src/components/Prompt/index.tsx +12 -1
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +4 -3
- package/src/components/RadioGroup/RadioGroup.module.scss +9 -9
- package/src/components/RadioGroup/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.module.scss +9 -2
- package/src/components/Sidebar/Sidebar.test.tsx +6 -0
- package/src/components/Sidebar/index.tsx +4 -1
- package/src/components/Slider/Slider.fragment.tsx +2 -2
- package/src/components/Slider/Slider.module.scss +2 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Table/Table.fragment.tsx +1 -1
- package/src/components/Theme/Theme.fragment.tsx +16 -0
- package/src/components/Theme/ThemeToggle.module.scss +4 -3
- package/src/components/Toast/Toast.fragment.tsx +1 -0
- package/src/components/Toast/Toast.module.scss +9 -4
- package/src/components/Toggle/Toggle.fragment.tsx +32 -32
- package/src/components/Toggle/Toggle.module.scss +33 -26
- package/src/components/Toggle/Toggle.test.tsx +10 -10
- package/src/components/Toggle/index.tsx +23 -15
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +34 -2
- package/src/index.ts +9 -1
- package/src/tokens/_derive.scss +32 -8
- package/src/tokens/_mixins.scss +14 -0
- package/src/tokens/_variables.scss +12 -6
- package/src/blocks/AIChat.block.ts +0 -266
- package/src/blocks/AppShell.block.ts +0 -175
- package/src/blocks/CTABanner.block.ts +0 -24
- package/src/blocks/CardGrid.block.ts +0 -22
- package/src/blocks/CodeExamples.block.ts +0 -66
- package/src/blocks/ConfirmDialog.block.ts +0 -19
- package/src/blocks/ConversationWithHistory.block.ts +0 -45
- package/src/blocks/DashboardNav.block.ts +0 -183
- package/src/blocks/ForgotPassword.block.ts +0 -26
- package/src/blocks/FormLayout.block.ts +0 -31
- package/src/blocks/InsetDashboardLayout.block.ts +0 -79
- package/src/blocks/MetricDashboard.block.ts +0 -38
- package/src/blocks/NewsletterSignup.block.ts +0 -26
- package/src/blocks/NotificationList.block.ts +0 -39
- package/src/blocks/NotificationPreferences.block.ts +0 -40
- package/src/blocks/OrderSummary.block.ts +0 -52
- package/src/blocks/ProfileEditForm.block.ts +0 -51
- package/src/blocks/SearchResults.block.ts +0 -39
- package/src/blocks/SettingsPage.block.ts +0 -58
- package/src/blocks/StreamingMessage.block.ts +0 -24
- package/src/blocks/TestimonialCard.block.ts +0 -27
- package/src/blocks/UserProfileCard.block.ts +0 -29
- package/src/recipes/AIChat.recipe.ts +0 -266
- package/src/recipes/AppShell.recipe.ts +0 -175
- package/src/recipes/CardGrid.recipe.ts +0 -22
- package/src/recipes/ChatInterface.recipe.ts +0 -87
- package/src/recipes/CodeExamples.recipe.ts +0 -66
- package/src/recipes/ConfirmDialog.recipe.ts +0 -19
- package/src/recipes/DashboardLayout.recipe.ts +0 -73
- package/src/recipes/DashboardNav.recipe.ts +0 -183
- package/src/recipes/FormLayout.recipe.ts +0 -31
- package/src/recipes/LoginForm.recipe.ts +0 -33
- package/src/recipes/SettingsPage.recipe.ts +0 -58
|
@@ -7,6 +7,7 @@ function renderPrompt(props: {
|
|
|
7
7
|
placeholder?: string;
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
defaultValue?: string;
|
|
10
|
+
loading?: boolean;
|
|
10
11
|
} = {}) {
|
|
11
12
|
return render(
|
|
12
13
|
<Prompt
|
|
@@ -14,6 +15,7 @@ function renderPrompt(props: {
|
|
|
14
15
|
onSubmit={props.onSubmit}
|
|
15
16
|
disabled={props.disabled}
|
|
16
17
|
defaultValue={props.defaultValue}
|
|
18
|
+
loading={props.loading}
|
|
17
19
|
>
|
|
18
20
|
<Prompt.Textarea />
|
|
19
21
|
<Prompt.Toolbar>
|
|
@@ -82,6 +84,12 @@ describe('Prompt', () => {
|
|
|
82
84
|
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
it('shows loading spinner icon in submit button when loading', () => {
|
|
88
|
+
renderPrompt({ loading: true, defaultValue: 'Submitting...' });
|
|
89
|
+
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
|
|
90
|
+
expect(screen.getByRole('status', { name: /submitting/i })).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
85
93
|
it('has no accessibility violations', async () => {
|
|
86
94
|
const { container } = renderPrompt();
|
|
87
95
|
await expectNoA11yViolations(container);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import styles from './Prompt.module.scss';
|
|
5
5
|
import '../../styles/globals.scss';
|
|
6
|
+
import { Loading } from '../Loading';
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
8
9
|
// Types
|
|
@@ -404,7 +405,17 @@ function PromptSubmit({
|
|
|
404
405
|
disabled={isDisabled}
|
|
405
406
|
aria-label={ariaLabel}
|
|
406
407
|
>
|
|
407
|
-
{
|
|
408
|
+
{loading ? (
|
|
409
|
+
<Loading
|
|
410
|
+
size="sm"
|
|
411
|
+
variant="spinner"
|
|
412
|
+
color="current"
|
|
413
|
+
label="Submitting"
|
|
414
|
+
className={styles.submitSpinner}
|
|
415
|
+
/>
|
|
416
|
+
) : (
|
|
417
|
+
children ?? <ArrowUpIcon />
|
|
418
|
+
)}
|
|
408
419
|
</button>
|
|
409
420
|
);
|
|
410
421
|
}
|
|
@@ -23,7 +23,7 @@ export default defineFragment({
|
|
|
23
23
|
whenNot: [
|
|
24
24
|
'Multiple selections allowed (use Checkbox group)',
|
|
25
25
|
'Many options (use Select)',
|
|
26
|
-
'Binary on/off choice (use
|
|
26
|
+
'Binary on/off choice (use Switch)',
|
|
27
27
|
'Options need to be searchable (use Combobox)',
|
|
28
28
|
],
|
|
29
29
|
guidelines: [
|
|
@@ -96,9 +96,9 @@ export default defineFragment({
|
|
|
96
96
|
note: 'Use Select for many options or limited space',
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
|
-
component: '
|
|
99
|
+
component: 'Switch',
|
|
100
100
|
relationship: 'alternative',
|
|
101
|
-
note: 'Use
|
|
101
|
+
note: 'Use Switch for binary on/off choices',
|
|
102
102
|
},
|
|
103
103
|
],
|
|
104
104
|
|
|
@@ -118,6 +118,7 @@ export default defineFragment({
|
|
|
118
118
|
a11yRules: [
|
|
119
119
|
'A11Y_RADIO_GROUP',
|
|
120
120
|
'A11Y_LABEL_REQUIRED',
|
|
121
|
+
'A11Y_TARGET_SIZE_MIN',
|
|
121
122
|
],
|
|
122
123
|
bans: [],
|
|
123
124
|
},
|
|
@@ -30,11 +30,15 @@
|
|
|
30
30
|
// Individual item wrapper
|
|
31
31
|
.itemWrapper {
|
|
32
32
|
display: inline-flex;
|
|
33
|
-
align-items:
|
|
33
|
+
align-items: center;
|
|
34
34
|
gap: var(--fui-space-2, $fui-space-2);
|
|
35
35
|
cursor: pointer;
|
|
36
36
|
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
37
37
|
|
|
38
|
+
&[data-has-description] {
|
|
39
|
+
align-items: flex-start;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
&[data-disabled] {
|
|
39
43
|
cursor: not-allowed;
|
|
40
44
|
opacity: 0.5;
|
|
@@ -44,15 +48,13 @@
|
|
|
44
48
|
// The radio circle
|
|
45
49
|
.radio {
|
|
46
50
|
@include interactive-base;
|
|
51
|
+
@include touch-target;
|
|
47
52
|
|
|
48
53
|
position: relative;
|
|
49
|
-
display: inline-flex;
|
|
50
|
-
align-items: center;
|
|
51
|
-
justify-content: center;
|
|
52
54
|
flex-shrink: 0;
|
|
53
55
|
width: 1rem;
|
|
54
56
|
height: 1rem;
|
|
55
|
-
margin-top:
|
|
57
|
+
margin-top: 0;
|
|
56
58
|
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
57
59
|
border: 1px solid var(--fui-border-strong, $fui-border-strong);
|
|
58
60
|
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
@@ -80,13 +82,11 @@
|
|
|
80
82
|
.radioSm {
|
|
81
83
|
width: 0.875rem;
|
|
82
84
|
height: 0.875rem;
|
|
83
|
-
margin-top: var(--fui-space-0-75, $fui-space-0-75);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
.radioLg {
|
|
87
88
|
width: 1.25rem;
|
|
88
89
|
height: 1.25rem;
|
|
89
|
-
margin-top: 0;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// The indicator dot - use absolute positioning for perfect centering
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
position: absolute;
|
|
95
95
|
top: 50%;
|
|
96
96
|
left: 50%;
|
|
97
|
-
width: 0.
|
|
98
|
-
height: 0.
|
|
97
|
+
width: 0.75rem;
|
|
98
|
+
height: 0.75rem;
|
|
99
99
|
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
100
100
|
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
101
101
|
|
|
@@ -112,7 +112,11 @@ function RadioItem({
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return (
|
|
115
|
-
<label
|
|
115
|
+
<label
|
|
116
|
+
className={wrapperClasses}
|
|
117
|
+
data-disabled={disabled || undefined}
|
|
118
|
+
data-has-description={description ? true : undefined}
|
|
119
|
+
>
|
|
116
120
|
<BaseRadio.Root
|
|
117
121
|
value={value}
|
|
118
122
|
disabled={disabled}
|
|
@@ -124,11 +124,18 @@
|
|
|
124
124
|
|
|
125
125
|
.nav {
|
|
126
126
|
flex: 1;
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
min-height: 0;
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
overflow: hidden;
|
|
129
131
|
padding: var(--fui-padding-item-sm, $fui-padding-item-sm);
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
.navScrollArea {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-height: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
// ============================================
|
|
133
140
|
// Section
|
|
134
141
|
// ============================================
|
|
@@ -55,6 +55,12 @@ describe('Sidebar', () => {
|
|
|
55
55
|
expect(screen.getByRole('navigation', { name: /main/i })).toBeInTheDocument();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('uses ScrollArea with fade indicators in nav content', () => {
|
|
59
|
+
renderSidebar();
|
|
60
|
+
const scrollAreaRoot = screen.getByRole('navigation', { name: /main/i }).querySelector('[data-orientation="vertical"]');
|
|
61
|
+
expect(scrollAreaRoot).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
58
64
|
it('renders section with label', () => {
|
|
59
65
|
renderSidebar();
|
|
60
66
|
expect(screen.getByText('Section One')).toBeInTheDocument();
|
|
@@ -3,6 +3,7 @@ import styles from './Sidebar.module.scss';
|
|
|
3
3
|
import { Tooltip } from '../Tooltip';
|
|
4
4
|
import { Skeleton } from '../Skeleton';
|
|
5
5
|
import { Collapsible } from '../Collapsible';
|
|
6
|
+
import { ScrollArea } from '../ScrollArea';
|
|
6
7
|
import { useFocusTrap } from '../../utils/a11y';
|
|
7
8
|
// Import globals to ensure CSS variables are defined
|
|
8
9
|
import '../../styles/globals.scss';
|
|
@@ -655,7 +656,9 @@ function SidebarNav({ children, 'aria-label': ariaLabel = 'Main navigation', cla
|
|
|
655
656
|
const classes = [styles.nav, className].filter(Boolean).join(' ');
|
|
656
657
|
return (
|
|
657
658
|
<nav className={classes} aria-label={ariaLabel}>
|
|
658
|
-
{
|
|
659
|
+
<ScrollArea orientation="vertical" showFades className={styles.navScrollArea}>
|
|
660
|
+
{children}
|
|
661
|
+
</ScrollArea>
|
|
659
662
|
</nav>
|
|
660
663
|
);
|
|
661
664
|
}
|
|
@@ -24,7 +24,7 @@ export default defineFragment({
|
|
|
24
24
|
whenNot: [
|
|
25
25
|
'Precise numeric input (use Input type="number")',
|
|
26
26
|
'Discrete options (use RadioGroup or Select)',
|
|
27
|
-
'Yes/no choices (use
|
|
27
|
+
'Yes/no choices (use Switch)',
|
|
28
28
|
],
|
|
29
29
|
guidelines: [
|
|
30
30
|
'Always provide a label describing what the slider controls',
|
|
@@ -109,7 +109,7 @@ export default defineFragment({
|
|
|
109
109
|
'input.numeric',
|
|
110
110
|
'control.slider',
|
|
111
111
|
],
|
|
112
|
-
a11yRules: ['A11Y_LABEL_REQUIRED', 'A11Y_KEYBOARD_ACCESSIBLE'],
|
|
112
|
+
a11yRules: ['A11Y_LABEL_REQUIRED', 'A11Y_KEYBOARD_ACCESSIBLE', 'A11Y_TARGET_SIZE_MIN'],
|
|
113
113
|
},
|
|
114
114
|
|
|
115
115
|
variants: [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Switch, type SwitchProps, Toggle, type ToggleProps } from '../Toggle';
|
|
@@ -58,7 +58,7 @@ export default defineFragment({
|
|
|
58
58
|
],
|
|
59
59
|
whenNot: [
|
|
60
60
|
'Simple lists (use List component)',
|
|
61
|
-
'Card-based layouts (use
|
|
61
|
+
'Card-based layouts (use Grid with Cards)',
|
|
62
62
|
'Heavily interactive data (consider DataGrid)',
|
|
63
63
|
'Small screens (consider card or list view)',
|
|
64
64
|
],
|
|
@@ -89,6 +89,22 @@ export default defineFragment({
|
|
|
89
89
|
{ component: 'AppShell', relationship: 'sibling', note: 'ThemeProvider typically wraps AppShell' },
|
|
90
90
|
],
|
|
91
91
|
|
|
92
|
+
contract: {
|
|
93
|
+
propsSummary: [
|
|
94
|
+
'defaultMode: light|dark|system - initial theme mode',
|
|
95
|
+
'mode: light|dark|system - controlled mode',
|
|
96
|
+
'onModeChange: (mode) => void - change handler',
|
|
97
|
+
'attribute: data-theme|class - DOM theme attribute',
|
|
98
|
+
'ThemeToggle size: sm|md|lg - toggle button size',
|
|
99
|
+
],
|
|
100
|
+
scenarioTags: [
|
|
101
|
+
'navigation.theme',
|
|
102
|
+
'settings.appearance',
|
|
103
|
+
'ui.mode-toggle',
|
|
104
|
+
],
|
|
105
|
+
a11yRules: ['A11Y_TARGET_SIZE_MIN'],
|
|
106
|
+
},
|
|
107
|
+
|
|
92
108
|
variants: [
|
|
93
109
|
{
|
|
94
110
|
name: 'Default',
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
.toggleButton {
|
|
17
17
|
@include button-reset;
|
|
18
18
|
@include interactive-base;
|
|
19
|
+
@include min-target-size;
|
|
19
20
|
|
|
20
21
|
display: flex;
|
|
21
22
|
align-items: center;
|
|
@@ -29,8 +30,8 @@
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
&:focus-visible {
|
|
32
|
-
outline:
|
|
33
|
-
outline-offset:
|
|
33
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
34
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
svg {
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
.toggleButtonActive {
|
|
42
43
|
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
43
44
|
color: var(--fui-text-primary, $fui-text-primary);
|
|
44
|
-
box-shadow:
|
|
45
|
+
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// Size variants
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
2
3
|
|
|
3
4
|
// ============================================
|
|
4
5
|
// Toast Container
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
@keyframes toastEnter {
|
|
78
79
|
from {
|
|
79
80
|
opacity: 0;
|
|
80
|
-
transform: translateY(
|
|
81
|
+
transform: translateY($fui-anim-offset-md) scale(0.96);
|
|
81
82
|
}
|
|
82
83
|
to {
|
|
83
84
|
opacity: 1;
|
|
@@ -168,6 +169,11 @@
|
|
|
168
169
|
|
|
169
170
|
.action {
|
|
170
171
|
flex-shrink: 0;
|
|
172
|
+
@include min-target-size;
|
|
173
|
+
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
171
177
|
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
|
|
172
178
|
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
173
179
|
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
@@ -194,9 +200,8 @@
|
|
|
194
200
|
|
|
195
201
|
.close {
|
|
196
202
|
flex-shrink: 0;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
justify-content: center;
|
|
203
|
+
@include touch-target;
|
|
204
|
+
|
|
200
205
|
width: 1.5rem;
|
|
201
206
|
height: 1.5rem;
|
|
202
207
|
margin: -2px -4px -2px 0;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { defineFragment } from '@fragments/core';
|
|
3
|
-
import {
|
|
3
|
+
import { Switch } from '.';
|
|
4
4
|
|
|
5
5
|
// Stateful wrapper for interactive demos
|
|
6
|
-
function
|
|
6
|
+
function StatefulSwitch(props: React.ComponentProps<typeof Switch>) {
|
|
7
7
|
const [checked, setChecked] = useState(props.checked ?? false);
|
|
8
|
-
return <
|
|
8
|
+
return <Switch {...props} checked={checked} onChange={setChecked} />;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export default defineFragment({
|
|
12
|
-
component:
|
|
12
|
+
component: Switch,
|
|
13
13
|
|
|
14
14
|
meta: {
|
|
15
|
-
name: '
|
|
15
|
+
name: 'Switch',
|
|
16
16
|
description: 'Binary on/off switch for settings and preferences. Provides immediate visual feedback and is ideal for options that take effect instantly.',
|
|
17
17
|
category: 'forms',
|
|
18
18
|
status: 'stable',
|
|
@@ -34,11 +34,11 @@ export default defineFragment({
|
|
|
34
34
|
'Complex multi-state options (use select or radio)',
|
|
35
35
|
],
|
|
36
36
|
guidelines: [
|
|
37
|
-
'
|
|
37
|
+
'Switch should always have a visible label explaining what it controls',
|
|
38
38
|
'The "on" state should be the positive/enabling action',
|
|
39
39
|
'Changes should take effect immediately - no save button needed',
|
|
40
|
-
'Include a description for
|
|
41
|
-
'Group related
|
|
40
|
+
'Include a description for switches whose effect isn\'t obvious from the label',
|
|
41
|
+
'Group related switches visually in settings panels',
|
|
42
42
|
],
|
|
43
43
|
accessibility: [
|
|
44
44
|
'Uses role="switch" with aria-checked for proper semantics',
|
|
@@ -51,7 +51,7 @@ export default defineFragment({
|
|
|
51
51
|
props: {
|
|
52
52
|
checked: {
|
|
53
53
|
type: 'boolean',
|
|
54
|
-
description: 'Whether the
|
|
54
|
+
description: 'Whether the switch is in the on state',
|
|
55
55
|
default: 'false',
|
|
56
56
|
},
|
|
57
57
|
onChange: {
|
|
@@ -68,19 +68,19 @@ export default defineFragment({
|
|
|
68
68
|
},
|
|
69
69
|
disabled: {
|
|
70
70
|
type: 'boolean',
|
|
71
|
-
description: 'Whether the
|
|
71
|
+
description: 'Whether the switch is non-interactive',
|
|
72
72
|
default: 'false',
|
|
73
73
|
},
|
|
74
74
|
size: {
|
|
75
75
|
type: 'enum',
|
|
76
|
-
description: '
|
|
76
|
+
description: 'Switch track size',
|
|
77
77
|
values: ['sm', 'md'],
|
|
78
78
|
default: 'md',
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
|
|
82
82
|
relations: [
|
|
83
|
-
{ component: 'Input', relationship: 'sibling', note: 'Input handles text/number entry;
|
|
83
|
+
{ component: 'Input', relationship: 'sibling', note: 'Input handles text/number entry; Switch handles boolean state' },
|
|
84
84
|
{ component: 'Checkbox', relationship: 'alternative', note: 'Use Checkbox when change requires form submission' },
|
|
85
85
|
],
|
|
86
86
|
|
|
@@ -91,35 +91,35 @@ export default defineFragment({
|
|
|
91
91
|
'label: string - visible label text',
|
|
92
92
|
'description: string - helper text below label',
|
|
93
93
|
'disabled: boolean - non-interactive state',
|
|
94
|
-
'size: sm|md -
|
|
94
|
+
'size: sm|md - switch size',
|
|
95
95
|
],
|
|
96
96
|
scenarioTags: [
|
|
97
97
|
'form.boolean',
|
|
98
|
-
'settings.
|
|
98
|
+
'settings.switch',
|
|
99
99
|
'settings.preference',
|
|
100
100
|
'form.switch',
|
|
101
101
|
],
|
|
102
|
-
a11yRules: ['A11Y_SWITCH_ROLE', 'A11Y_SWITCH_LABEL', 'A11Y_SWITCH_FOCUS'],
|
|
102
|
+
a11yRules: ['A11Y_SWITCH_ROLE', 'A11Y_SWITCH_LABEL', 'A11Y_SWITCH_FOCUS', 'A11Y_TARGET_SIZE_MIN'],
|
|
103
103
|
},
|
|
104
104
|
|
|
105
105
|
variants: [
|
|
106
106
|
{
|
|
107
107
|
name: 'Default Off',
|
|
108
|
-
description: '
|
|
109
|
-
render: () => <
|
|
108
|
+
description: 'Switch in the off state',
|
|
109
|
+
render: () => <StatefulSwitch label="Email notifications" />,
|
|
110
110
|
args: { label: 'Email notifications' },
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
name: 'Checked',
|
|
114
|
-
description: '
|
|
115
|
-
render: () => <
|
|
114
|
+
description: 'Switch in the on state',
|
|
115
|
+
render: () => <StatefulSwitch checked label="Dark mode" />,
|
|
116
116
|
args: { checked: true, label: 'Dark mode' },
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
name: 'With Description',
|
|
120
|
-
description: '
|
|
120
|
+
description: 'Switch with explanatory helper text',
|
|
121
121
|
render: () => (
|
|
122
|
-
<
|
|
122
|
+
<StatefulSwitch
|
|
123
123
|
checked
|
|
124
124
|
label="Auto-save"
|
|
125
125
|
description="Automatically save changes as you type"
|
|
@@ -129,41 +129,41 @@ export default defineFragment({
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
name: 'Small Size',
|
|
132
|
-
description: 'Compact
|
|
132
|
+
description: 'Compact switch for dense settings panels',
|
|
133
133
|
render: () => (
|
|
134
134
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
<
|
|
135
|
+
<StatefulSwitch size="sm" checked label="Show line numbers" />
|
|
136
|
+
<StatefulSwitch size="sm" label="Word wrap" />
|
|
137
|
+
<StatefulSwitch size="sm" checked label="Minimap" />
|
|
138
138
|
</div>
|
|
139
139
|
),
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
name: 'Disabled States',
|
|
143
|
-
description: 'Non-interactive
|
|
143
|
+
description: 'Non-interactive switches showing both states',
|
|
144
144
|
render: () => (
|
|
145
145
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
146
|
-
<
|
|
147
|
-
<
|
|
146
|
+
<Switch disabled label="Premium feature (upgrade required)" />
|
|
147
|
+
<Switch disabled checked label="System managed (read-only)" />
|
|
148
148
|
</div>
|
|
149
149
|
),
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: 'Settings Panel',
|
|
153
|
-
description: 'Multiple
|
|
153
|
+
description: 'Multiple switches in a realistic settings layout',
|
|
154
154
|
render: () => (
|
|
155
155
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '320px' }}>
|
|
156
|
-
<
|
|
156
|
+
<StatefulSwitch
|
|
157
157
|
checked
|
|
158
158
|
label="Push notifications"
|
|
159
159
|
description="Receive push notifications on your device"
|
|
160
160
|
/>
|
|
161
|
-
<
|
|
161
|
+
<StatefulSwitch
|
|
162
162
|
checked
|
|
163
163
|
label="Email digest"
|
|
164
164
|
description="Weekly summary of your activity"
|
|
165
165
|
/>
|
|
166
|
-
<
|
|
166
|
+
<StatefulSwitch
|
|
167
167
|
label="Marketing emails"
|
|
168
168
|
description="Product updates and promotional offers"
|
|
169
169
|
/>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
.root {
|
|
5
5
|
display: flex;
|
|
6
|
-
align-items:
|
|
6
|
+
align-items: center;
|
|
7
7
|
gap: var(--fui-space-3, $fui-space-3);
|
|
8
8
|
cursor: pointer;
|
|
9
9
|
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
@@ -14,21 +14,30 @@
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.rootWithDescription {
|
|
18
|
+
align-items: flex-start;
|
|
19
|
+
|
|
20
|
+
.track {
|
|
21
|
+
margin-top: var(--fui-space-0-5, $fui-space-0-5);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
.track {
|
|
18
26
|
@include button-reset;
|
|
19
27
|
@include interactive-base;
|
|
28
|
+
@include min-target-size;
|
|
20
29
|
|
|
21
30
|
position: relative;
|
|
22
31
|
flex-shrink: 0;
|
|
23
32
|
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
24
|
-
margin-top: var(--fui-space-0-5, $fui-space-0-5);
|
|
25
33
|
background-color: var(--fui-border-strong, $fui-border-strong);
|
|
34
|
+
transition: background-color var(--fui-transition-normal, $fui-transition-normal);
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
.root[data-checked] & {
|
|
28
37
|
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
.root:not([data-disabled]) &:hover {
|
|
32
41
|
opacity: 0.9;
|
|
33
42
|
}
|
|
34
43
|
}
|
|
@@ -36,43 +45,41 @@
|
|
|
36
45
|
.trackSm {
|
|
37
46
|
width: var(--fui-toggle-width-sm, $fui-toggle-width-sm);
|
|
38
47
|
height: var(--fui-toggle-height-sm, $fui-toggle-height-sm);
|
|
48
|
+
--_toggle-thumb-size: calc(var(--fui-toggle-height-sm, #{$fui-toggle-height-sm}) - 0.5rem);
|
|
49
|
+
--_toggle-inset: calc((var(--fui-toggle-height-sm, #{$fui-toggle-height-sm}) - var(--_toggle-thumb-size)) / 2);
|
|
50
|
+
--_toggle-translate: calc(
|
|
51
|
+
var(--fui-toggle-width-sm, #{$fui-toggle-width-sm}) -
|
|
52
|
+
var(--_toggle-thumb-size) -
|
|
53
|
+
(var(--_toggle-inset) * 2)
|
|
54
|
+
);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
.trackMd {
|
|
42
58
|
width: var(--fui-toggle-width-md, $fui-toggle-width-md);
|
|
43
59
|
height: var(--fui-toggle-height-md, $fui-toggle-height-md);
|
|
60
|
+
--_toggle-thumb-size: calc(var(--fui-toggle-height-md, #{$fui-toggle-height-md}) - 0.5rem);
|
|
61
|
+
--_toggle-inset: calc((var(--fui-toggle-height-md, #{$fui-toggle-height-md}) - var(--_toggle-thumb-size)) / 2);
|
|
62
|
+
--_toggle-translate: calc(
|
|
63
|
+
var(--fui-toggle-width-md, #{$fui-toggle-width-md}) -
|
|
64
|
+
var(--_toggle-thumb-size) -
|
|
65
|
+
(var(--_toggle-inset) * 2)
|
|
66
|
+
);
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
.thumb {
|
|
47
70
|
position: absolute;
|
|
48
|
-
top: $fui-toggle-thumb-offset;
|
|
49
|
-
left: $fui-toggle-thumb-offset;
|
|
71
|
+
top: var(--_toggle-inset, $fui-toggle-thumb-offset);
|
|
72
|
+
left: var(--_toggle-inset, $fui-toggle-thumb-offset);
|
|
73
|
+
width: var(--_toggle-thumb-size, $fui-toggle-thumb-md);
|
|
74
|
+
height: var(--_toggle-thumb-size, $fui-toggle-thumb-md);
|
|
50
75
|
border-radius: 50%;
|
|
51
76
|
background-color: var(--fui-bg-primary, $fui-bg-primary);
|
|
52
77
|
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
53
78
|
transition: transform var(--fui-transition-normal, $fui-transition-normal);
|
|
54
79
|
|
|
55
|
-
[data-checked]
|
|
80
|
+
.root[data-checked] & {
|
|
56
81
|
// Move thumb to the right when checked
|
|
57
|
-
transform: translateX(
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.thumbSm {
|
|
62
|
-
width: $fui-toggle-thumb-sm;
|
|
63
|
-
height: $fui-toggle-thumb-sm;
|
|
64
|
-
|
|
65
|
-
[data-checked] > & {
|
|
66
|
-
transform: translateX($fui-toggle-thumb-sm);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.thumbMd {
|
|
71
|
-
width: $fui-toggle-thumb-md;
|
|
72
|
-
height: $fui-toggle-thumb-md;
|
|
73
|
-
|
|
74
|
-
[data-checked] > & {
|
|
75
|
-
transform: translateX($fui-toggle-thumb-md);
|
|
82
|
+
transform: translateX(var(--_toggle-translate, 0));
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|