@fragments-sdk/ui 0.3.0 → 0.5.0
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/README.md +98 -2
- package/fragments.json +1 -1
- package/package.json +11 -5
- package/src/components/Accordion/Accordion.fragment.tsx +186 -0
- package/src/components/Accordion/Accordion.module.scss +111 -0
- package/src/components/Accordion/index.tsx +271 -0
- package/src/components/Alert/Alert.fragment.tsx +67 -42
- package/src/components/Alert/Alert.module.scss +31 -21
- package/src/components/Alert/index.tsx +202 -73
- package/src/components/AppShell/AppShell.fragment.tsx +315 -0
- package/src/components/AppShell/AppShell.module.scss +213 -0
- package/src/components/AppShell/index.tsx +398 -0
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Avatar/index.tsx +8 -9
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +16 -10
- package/src/components/Badge/index.tsx +20 -6
- package/src/components/Box/Box.fragment.tsx +168 -0
- package/src/components/Box/Box.module.scss +84 -0
- package/src/components/Box/index.tsx +78 -0
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/Button/Button.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +52 -26
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
- package/src/components/CodeBlock/index.tsx +599 -0
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
- package/src/components/ColorPicker/index.tsx +129 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +12 -3
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
- package/src/components/EmptyState/EmptyState.module.scss +9 -9
- package/src/components/EmptyState/index.tsx +104 -69
- package/src/components/Field/Field.fragment.tsx +165 -0
- package/src/components/Field/Field.module.scss +31 -0
- package/src/components/Field/index.tsx +143 -0
- package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
- package/src/components/Fieldset/Fieldset.module.scss +22 -0
- package/src/components/Fieldset/index.tsx +47 -0
- package/src/components/Form/Form.fragment.tsx +286 -0
- package/src/components/Form/Form.module.scss +8 -0
- package/src/components/Form/index.tsx +53 -0
- package/src/components/Grid/Grid.fragment.tsx +18 -18
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +208 -0
- package/src/components/Header/index.tsx +363 -0
- package/src/components/Icon/Icon.fragment.tsx +138 -0
- package/src/components/Icon/Icon.module.scss +38 -0
- package/src/components/Icon/index.tsx +58 -0
- package/src/components/Image/Image.fragment.tsx +195 -0
- package/src/components/Image/Image.module.scss +77 -0
- package/src/components/Image/index.tsx +95 -0
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +75 -2
- package/src/components/Input/index.tsx +60 -21
- package/src/components/Link/Link.fragment.tsx +132 -0
- package/src/components/Link/Link.module.scss +67 -0
- package/src/components/Link/index.tsx +57 -0
- package/src/components/List/List.fragment.tsx +152 -0
- package/src/components/List/List.module.scss +71 -0
- package/src/components/List/index.tsx +106 -0
- package/src/components/Listbox/Listbox.fragment.tsx +191 -0
- package/src/components/Listbox/Listbox.module.scss +97 -0
- package/src/components/Listbox/index.tsx +121 -0
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +12 -3
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +13 -4
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Progress/Progress.module.scss +11 -11
- package/src/components/Progress/index.tsx +34 -7
- package/src/components/Prompt/Prompt.fragment.tsx +231 -0
- package/src/components/Prompt/Prompt.module.scss +243 -0
- package/src/components/Prompt/index.tsx +439 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +10 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +91 -12
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
- package/src/components/Sidebar/Sidebar.module.scss +91 -47
- package/src/components/Sidebar/index.tsx +57 -14
- package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Stack/Stack.module.scss +120 -0
- package/src/components/Stack/index.tsx +148 -0
- package/src/components/Table/Table.fragment.tsx +10 -3
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +10 -1
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Textarea/index.tsx +3 -7
- package/src/components/Theme/Theme.fragment.tsx +128 -0
- package/src/components/Theme/ThemeToggle.module.scss +82 -0
- package/src/components/Theme/index.tsx +343 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +6 -6
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/Tooltip/Tooltip.module.scss +4 -4
- package/src/components/Tooltip/index.tsx +4 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
- package/src/components/VisuallyHidden/index.tsx +29 -0
- package/src/index.ts +278 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/recipes/AppShell.recipe.ts +175 -0
- package/src/recipes/CardGrid.recipe.ts +6 -2
- package/src/recipes/ChatInterface.recipe.ts +87 -0
- package/src/recipes/CodeExamples.recipe.ts +66 -0
- package/src/recipes/DashboardLayout.recipe.ts +46 -12
- package/src/recipes/DashboardNav.recipe.ts +183 -0
- package/src/recipes/LoginForm.recipe.ts +8 -1
- package/src/recipes/SettingsPage.recipe.ts +37 -20
- package/src/styles/globals.scss +31 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +41 -0
- package/src/tokens/_mixins.scss +95 -1
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +507 -101
- package/src/utils/a11y.tsx +439 -0
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
display: flex;
|
|
64
64
|
align-items: flex-start;
|
|
65
65
|
gap: var(--fui-space-3, $fui-space-3);
|
|
66
|
-
padding: var(--fui-
|
|
66
|
+
padding: var(--fui-padding-inline-sm, $fui-padding-inline-sm) var(--fui-padding-inline-md, $fui-padding-inline-md);
|
|
67
67
|
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
68
68
|
border: 1px solid var(--fui-border, $fui-border);
|
|
69
69
|
border-radius: var(--fui-radius-lg, $fui-radius-lg);
|
|
@@ -225,3 +225,18 @@
|
|
|
225
225
|
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
|
|
229
|
+
// ============================================
|
|
230
|
+
// Accessibility: Reduced Motion
|
|
231
|
+
// ============================================
|
|
232
|
+
|
|
233
|
+
@media (prefers-reduced-motion: reduce) {
|
|
234
|
+
.toast {
|
|
235
|
+
animation: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.action,
|
|
239
|
+
.close {
|
|
240
|
+
transition: none;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -28,11 +28,9 @@ export interface ToastData {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface ToastProps extends Omit<ToastData, 'id'> {
|
|
31
|
+
export interface ToastProps extends Omit<ToastData, 'id'>, Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
32
32
|
/** Callback when toast should be dismissed */
|
|
33
33
|
onDismiss?: () => void;
|
|
34
|
-
/** Additional class name */
|
|
35
|
-
className?: string;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
export interface ToastProviderProps {
|
|
@@ -165,14 +163,19 @@ const variantIcons: Record<ToastVariant, React.ComponentType | null> = {
|
|
|
165
163
|
// ============================================
|
|
166
164
|
|
|
167
165
|
function ToastItem({
|
|
166
|
+
id,
|
|
168
167
|
title,
|
|
169
168
|
description,
|
|
170
169
|
variant = 'default',
|
|
171
170
|
action,
|
|
172
171
|
onDismiss,
|
|
173
172
|
className,
|
|
174
|
-
|
|
173
|
+
...htmlProps
|
|
174
|
+
}: ToastProps & { id?: string }) {
|
|
175
175
|
const Icon = variantIcons[variant];
|
|
176
|
+
const uniqueId = React.useId();
|
|
177
|
+
const titleId = title ? `toast-title-${id || uniqueId}` : undefined;
|
|
178
|
+
const descId = description ? `toast-desc-${id || uniqueId}` : undefined;
|
|
176
179
|
|
|
177
180
|
const toastClasses = [
|
|
178
181
|
styles.toast,
|
|
@@ -181,15 +184,21 @@ function ToastItem({
|
|
|
181
184
|
].filter(Boolean).join(' ');
|
|
182
185
|
|
|
183
186
|
return (
|
|
184
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
{...htmlProps}
|
|
189
|
+
className={toastClasses}
|
|
190
|
+
role="alert"
|
|
191
|
+
aria-labelledby={titleId}
|
|
192
|
+
aria-describedby={descId}
|
|
193
|
+
>
|
|
185
194
|
{Icon && (
|
|
186
195
|
<span className={styles.icon}>
|
|
187
196
|
<Icon />
|
|
188
197
|
</span>
|
|
189
198
|
)}
|
|
190
199
|
<div className={styles.content}>
|
|
191
|
-
{title && <div className={styles.title}>{title}</div>}
|
|
192
|
-
{description && <div className={styles.description}>{description}</div>}
|
|
200
|
+
{title && <div id={titleId} className={styles.title}>{title}</div>}
|
|
201
|
+
{description && <div id={descId} className={styles.description}>{description}</div>}
|
|
193
202
|
</div>
|
|
194
203
|
{action && (
|
|
195
204
|
<button
|
|
@@ -205,7 +214,7 @@ function ToastItem({
|
|
|
205
214
|
type="button"
|
|
206
215
|
className={styles.close}
|
|
207
216
|
onClick={onDismiss}
|
|
208
|
-
aria-label="Dismiss"
|
|
217
|
+
aria-label="Dismiss notification"
|
|
209
218
|
>
|
|
210
219
|
<CloseIcon />
|
|
211
220
|
</button>
|
|
@@ -232,10 +241,17 @@ function ToastContainer({
|
|
|
232
241
|
styles[position.replace('-', '')],
|
|
233
242
|
].filter(Boolean).join(' ');
|
|
234
243
|
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
// Always render the container for screen reader live region to work properly
|
|
245
|
+
// The live region must exist before announcements are made
|
|
237
246
|
return (
|
|
238
|
-
<div
|
|
247
|
+
<div
|
|
248
|
+
className={containerClasses}
|
|
249
|
+
role="region"
|
|
250
|
+
aria-label="Notifications"
|
|
251
|
+
aria-live="polite"
|
|
252
|
+
aria-atomic="false"
|
|
253
|
+
aria-relevant="additions"
|
|
254
|
+
>
|
|
239
255
|
{toasts.map((toast) => (
|
|
240
256
|
<ToastItem
|
|
241
257
|
key={toast.id}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Toggle } from '
|
|
3
|
+
import { Toggle } from '.';
|
|
4
4
|
|
|
5
5
|
// Stateful wrapper for interactive demos
|
|
6
6
|
function StatefulToggle(props: React.ComponentProps<typeof Toggle>) {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
position: relative;
|
|
22
22
|
flex-shrink: 0;
|
|
23
23
|
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
24
|
-
margin-top:
|
|
24
|
+
margin-top: var(--fui-space-0-5, $fui-space-0-5);
|
|
25
25
|
background-color: var(--fui-border-strong, $fui-border-strong);
|
|
26
26
|
|
|
27
27
|
&[data-checked] {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
|
|
46
46
|
.thumb {
|
|
47
47
|
position: absolute;
|
|
48
|
-
top:
|
|
49
|
-
left:
|
|
48
|
+
top: $fui-toggle-thumb-offset;
|
|
49
|
+
left: $fui-toggle-thumb-offset;
|
|
50
50
|
border-radius: 50%;
|
|
51
51
|
background-color: var(--fui-bg-primary, $fui-bg-primary);
|
|
52
52
|
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
@@ -59,20 +59,20 @@
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
.thumbSm {
|
|
62
|
-
width:
|
|
63
|
-
height:
|
|
62
|
+
width: $fui-toggle-thumb-sm;
|
|
63
|
+
height: $fui-toggle-thumb-sm;
|
|
64
64
|
|
|
65
65
|
[data-checked] > & {
|
|
66
|
-
transform: translateX(
|
|
66
|
+
transform: translateX($fui-toggle-thumb-sm);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.thumbMd {
|
|
71
|
-
width:
|
|
72
|
-
height:
|
|
71
|
+
width: $fui-toggle-thumb-md;
|
|
72
|
+
height: $fui-toggle-thumb-md;
|
|
73
73
|
|
|
74
74
|
[data-checked] > & {
|
|
75
|
-
transform: translateX(
|
|
75
|
+
transform: translateX($fui-toggle-thumb-md);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -92,12 +92,27 @@
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
.description {
|
|
95
|
+
display: block;
|
|
95
96
|
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
96
97
|
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
97
|
-
margin-top:
|
|
98
|
+
margin-top: var(--fui-space-0-5, $fui-space-0-5);
|
|
98
99
|
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
.descriptionSm {
|
|
102
103
|
font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
|
|
103
104
|
}
|
|
105
|
+
|
|
106
|
+
// ============================================
|
|
107
|
+
// Accessibility: Reduced Motion
|
|
108
|
+
// ============================================
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
.thumb {
|
|
112
|
+
transition: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.track {
|
|
116
|
+
transition: none;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -14,6 +14,10 @@ export interface ToggleProps {
|
|
|
14
14
|
size?: 'sm' | 'md';
|
|
15
15
|
className?: string;
|
|
16
16
|
name?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
'aria-label'?: string;
|
|
19
|
+
'aria-labelledby'?: string;
|
|
20
|
+
'aria-describedby'?: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
@@ -28,6 +32,10 @@ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
28
32
|
size = 'md',
|
|
29
33
|
className,
|
|
30
34
|
name,
|
|
35
|
+
id,
|
|
36
|
+
'aria-label': ariaLabel,
|
|
37
|
+
'aria-labelledby': ariaLabelledBy,
|
|
38
|
+
'aria-describedby': ariaDescribedBy,
|
|
31
39
|
},
|
|
32
40
|
ref
|
|
33
41
|
) {
|
|
@@ -57,12 +65,16 @@ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
57
65
|
return (
|
|
58
66
|
<Switch.Root
|
|
59
67
|
ref={ref}
|
|
68
|
+
id={id}
|
|
60
69
|
checked={checked}
|
|
61
70
|
defaultChecked={defaultChecked}
|
|
62
71
|
onCheckedChange={onChange}
|
|
63
72
|
disabled={disabled}
|
|
64
73
|
name={name}
|
|
65
74
|
className={rootClasses}
|
|
75
|
+
aria-label={ariaLabel}
|
|
76
|
+
aria-labelledby={ariaLabelledBy}
|
|
77
|
+
aria-describedby={ariaDescribedBy}
|
|
66
78
|
>
|
|
67
79
|
<Switch.Thumb className={trackClasses}>
|
|
68
80
|
<span className={thumbClasses} aria-hidden="true" />
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { ToggleGroup } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: ToggleGroup,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'ToggleGroup',
|
|
10
|
+
description: 'A group of toggle buttons where only one can be selected at a time. Useful for switching between views, modes, or options.',
|
|
11
|
+
category: 'forms',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['toggle', 'group', 'segmented', 'control', 'tabs', 'switch'],
|
|
14
|
+
since: '0.2.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Switching between mutually exclusive views or modes',
|
|
20
|
+
'Selecting one option from a small set (2-5 options)',
|
|
21
|
+
'Segmented controls like view switchers',
|
|
22
|
+
'Filter or sort options',
|
|
23
|
+
],
|
|
24
|
+
whenNot: [
|
|
25
|
+
'Multiple selections allowed (use Checkbox group)',
|
|
26
|
+
'Many options (use Select or RadioGroup)',
|
|
27
|
+
'Navigation between pages (use Tabs)',
|
|
28
|
+
'On/off toggle (use Toggle component)',
|
|
29
|
+
],
|
|
30
|
+
guidelines: [
|
|
31
|
+
'Keep options to 2-5 items for clarity',
|
|
32
|
+
'Use clear, concise labels',
|
|
33
|
+
'Consider icons for common actions (grid/list view)',
|
|
34
|
+
'Ensure adequate touch targets on mobile',
|
|
35
|
+
],
|
|
36
|
+
accessibility: [
|
|
37
|
+
'Uses role="group" for semantic grouping',
|
|
38
|
+
'Each item has role="radio" with aria-checked',
|
|
39
|
+
'Keyboard navigable with Tab and arrow keys',
|
|
40
|
+
'Focus visible on active item',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
props: {
|
|
45
|
+
value: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Currently selected value',
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
onChange: {
|
|
51
|
+
type: 'function',
|
|
52
|
+
description: 'Called with new value when selection changes',
|
|
53
|
+
required: true,
|
|
54
|
+
},
|
|
55
|
+
children: {
|
|
56
|
+
type: 'node',
|
|
57
|
+
description: 'ToggleGroup.Item components',
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
variant: {
|
|
61
|
+
type: 'enum',
|
|
62
|
+
description: 'Visual style',
|
|
63
|
+
values: ['default', 'pills', 'outline'],
|
|
64
|
+
default: 'default',
|
|
65
|
+
},
|
|
66
|
+
size: {
|
|
67
|
+
type: 'enum',
|
|
68
|
+
description: 'Size variant',
|
|
69
|
+
values: ['sm', 'md'],
|
|
70
|
+
default: 'md',
|
|
71
|
+
},
|
|
72
|
+
gap: {
|
|
73
|
+
type: 'enum',
|
|
74
|
+
description: 'Gap between items (pills/outline variants)',
|
|
75
|
+
values: ['none', 'xs', 'sm'],
|
|
76
|
+
default: 'xs',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
relations: [
|
|
81
|
+
{ component: 'RadioGroup', relationship: 'alternative', note: 'RadioGroup for form-style single selection' },
|
|
82
|
+
{ component: 'Tabs', relationship: 'alternative', note: 'Tabs for content panel switching' },
|
|
83
|
+
{ component: 'Toggle', relationship: 'sibling', note: 'Toggle for single on/off control' },
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
contract: {
|
|
87
|
+
propsSummary: [
|
|
88
|
+
'value: string - selected value (required)',
|
|
89
|
+
'onChange: (value: string) => void - change handler (required)',
|
|
90
|
+
'children: ToggleGroup.Item[] - toggle items',
|
|
91
|
+
'variant: default|pills|outline - visual style',
|
|
92
|
+
'size: sm|md - size variant',
|
|
93
|
+
'gap: none|xs|sm - spacing',
|
|
94
|
+
],
|
|
95
|
+
scenarioTags: [
|
|
96
|
+
'forms.selection',
|
|
97
|
+
'input.toggle',
|
|
98
|
+
'control.segmented',
|
|
99
|
+
],
|
|
100
|
+
a11yRules: ['A11Y_GROUP_ROLE', 'A11Y_KEYBOARD_ACCESSIBLE'],
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
variants: [
|
|
104
|
+
{
|
|
105
|
+
name: 'Default',
|
|
106
|
+
description: 'Basic toggle group',
|
|
107
|
+
render: () => {
|
|
108
|
+
const [value, setValue] = React.useState('left');
|
|
109
|
+
return (
|
|
110
|
+
<ToggleGroup value={value} onChange={setValue}>
|
|
111
|
+
<ToggleGroup.Item value="left">Left</ToggleGroup.Item>
|
|
112
|
+
<ToggleGroup.Item value="center">Center</ToggleGroup.Item>
|
|
113
|
+
<ToggleGroup.Item value="right">Right</ToggleGroup.Item>
|
|
114
|
+
</ToggleGroup>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Pills Variant',
|
|
120
|
+
description: 'Pill-shaped toggle buttons',
|
|
121
|
+
render: () => {
|
|
122
|
+
const [value, setValue] = React.useState('all');
|
|
123
|
+
return (
|
|
124
|
+
<ToggleGroup value={value} onChange={setValue} variant="pills">
|
|
125
|
+
<ToggleGroup.Item value="all">All</ToggleGroup.Item>
|
|
126
|
+
<ToggleGroup.Item value="active">Active</ToggleGroup.Item>
|
|
127
|
+
<ToggleGroup.Item value="completed">Completed</ToggleGroup.Item>
|
|
128
|
+
</ToggleGroup>
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Outline Variant',
|
|
134
|
+
description: 'Outlined toggle buttons',
|
|
135
|
+
render: () => {
|
|
136
|
+
const [value, setValue] = React.useState('day');
|
|
137
|
+
return (
|
|
138
|
+
<ToggleGroup value={value} onChange={setValue} variant="outline">
|
|
139
|
+
<ToggleGroup.Item value="day">Day</ToggleGroup.Item>
|
|
140
|
+
<ToggleGroup.Item value="week">Week</ToggleGroup.Item>
|
|
141
|
+
<ToggleGroup.Item value="month">Month</ToggleGroup.Item>
|
|
142
|
+
</ToggleGroup>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Sizes',
|
|
148
|
+
description: 'Different size variants',
|
|
149
|
+
render: () => {
|
|
150
|
+
const [value1, setValue1] = React.useState('a');
|
|
151
|
+
const [value2, setValue2] = React.useState('a');
|
|
152
|
+
return (
|
|
153
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
154
|
+
<ToggleGroup value={value1} onChange={setValue1} size="sm">
|
|
155
|
+
<ToggleGroup.Item value="a">Small</ToggleGroup.Item>
|
|
156
|
+
<ToggleGroup.Item value="b">Size</ToggleGroup.Item>
|
|
157
|
+
</ToggleGroup>
|
|
158
|
+
<ToggleGroup value={value2} onChange={setValue2} size="md">
|
|
159
|
+
<ToggleGroup.Item value="a">Medium</ToggleGroup.Item>
|
|
160
|
+
<ToggleGroup.Item value="b">Size</ToggleGroup.Item>
|
|
161
|
+
</ToggleGroup>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'View Switcher',
|
|
168
|
+
description: 'Common pattern for switching between views',
|
|
169
|
+
render: () => {
|
|
170
|
+
const [view, setView] = React.useState('grid');
|
|
171
|
+
return (
|
|
172
|
+
<ToggleGroup value={view} onChange={setView} size="sm">
|
|
173
|
+
<ToggleGroup.Item value="grid">
|
|
174
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
175
|
+
<rect x="3" y="3" width="7" height="7" />
|
|
176
|
+
<rect x="14" y="3" width="7" height="7" />
|
|
177
|
+
<rect x="3" y="14" width="7" height="7" />
|
|
178
|
+
<rect x="14" y="14" width="7" height="7" />
|
|
179
|
+
</svg>
|
|
180
|
+
</ToggleGroup.Item>
|
|
181
|
+
<ToggleGroup.Item value="list">
|
|
182
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
183
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
184
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
185
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
186
|
+
</svg>
|
|
187
|
+
</ToggleGroup.Item>
|
|
188
|
+
</ToggleGroup>
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'With Disabled Item',
|
|
194
|
+
description: 'Toggle group with a disabled option',
|
|
195
|
+
render: () => {
|
|
196
|
+
const [value, setValue] = React.useState('basic');
|
|
197
|
+
return (
|
|
198
|
+
<ToggleGroup value={value} onChange={setValue}>
|
|
199
|
+
<ToggleGroup.Item value="basic">Basic</ToggleGroup.Item>
|
|
200
|
+
<ToggleGroup.Item value="pro">Pro</ToggleGroup.Item>
|
|
201
|
+
<ToggleGroup.Item value="enterprise" disabled>Enterprise</ToggleGroup.Item>
|
|
202
|
+
</ToggleGroup>
|
|
203
|
+
);
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Group Container
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
.group {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Gap variants
|
|
14
|
+
.gap-xs {
|
|
15
|
+
gap: 2px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.gap-sm {
|
|
19
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Default variant (connected buttons)
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
.default {
|
|
27
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
28
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
29
|
+
padding: 2px;
|
|
30
|
+
gap: 2px;
|
|
31
|
+
|
|
32
|
+
.item {
|
|
33
|
+
background-color: transparent;
|
|
34
|
+
border: none;
|
|
35
|
+
border-radius: calc(var(--fui-radius-md, $fui-radius-md) - 2px);
|
|
36
|
+
|
|
37
|
+
&.selected {
|
|
38
|
+
background-color: var(--fui-bg-primary, $fui-bg-primary);
|
|
39
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// Pills variant (separate pill buttons)
|
|
46
|
+
// ============================================
|
|
47
|
+
|
|
48
|
+
.pills {
|
|
49
|
+
.item {
|
|
50
|
+
background-color: transparent;
|
|
51
|
+
border: 1px solid transparent;
|
|
52
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
53
|
+
|
|
54
|
+
&:hover:not(:disabled):not(.selected) {
|
|
55
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&.selected {
|
|
59
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
60
|
+
border-color: var(--fui-border, $fui-border);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// Outline variant (bordered buttons)
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
.outline {
|
|
70
|
+
.item {
|
|
71
|
+
background-color: transparent;
|
|
72
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
73
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
74
|
+
|
|
75
|
+
&:hover:not(:disabled):not(.selected) {
|
|
76
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&.selected {
|
|
80
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
81
|
+
border-color: var(--fui-color-accent, $fui-color-accent);
|
|
82
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================
|
|
88
|
+
// Item base styles
|
|
89
|
+
// ============================================
|
|
90
|
+
|
|
91
|
+
.item {
|
|
92
|
+
@include button-reset;
|
|
93
|
+
@include interactive-base;
|
|
94
|
+
@include text-base;
|
|
95
|
+
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
100
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
101
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
user-select: none;
|
|
104
|
+
transition: all 150ms ease;
|
|
105
|
+
|
|
106
|
+
&.selected {
|
|
107
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&.disabled {
|
|
111
|
+
opacity: 0.5;
|
|
112
|
+
cursor: not-allowed;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// Size variants
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
.size-sm {
|
|
121
|
+
.item {
|
|
122
|
+
min-height: 28px;
|
|
123
|
+
padding: 0 var(--fui-space-2, $fui-space-2);
|
|
124
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.size-md {
|
|
129
|
+
.item {
|
|
130
|
+
min-height: 32px;
|
|
131
|
+
padding: 0 var(--fui-space-3, $fui-space-3);
|
|
132
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
133
|
+
}
|
|
134
|
+
}
|