@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.
Files changed (175) hide show
  1. package/README.md +98 -2
  2. package/fragments.json +1 -1
  3. package/package.json +11 -5
  4. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  5. package/src/components/Accordion/Accordion.module.scss +111 -0
  6. package/src/components/Accordion/index.tsx +271 -0
  7. package/src/components/Alert/Alert.fragment.tsx +67 -42
  8. package/src/components/Alert/Alert.module.scss +31 -21
  9. package/src/components/Alert/index.tsx +202 -73
  10. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  11. package/src/components/AppShell/AppShell.module.scss +213 -0
  12. package/src/components/AppShell/index.tsx +398 -0
  13. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  14. package/src/components/Avatar/index.tsx +8 -9
  15. package/src/components/Badge/Badge.fragment.tsx +2 -2
  16. package/src/components/Badge/Badge.module.scss +16 -10
  17. package/src/components/Badge/index.tsx +20 -6
  18. package/src/components/Box/Box.fragment.tsx +168 -0
  19. package/src/components/Box/Box.module.scss +84 -0
  20. package/src/components/Box/index.tsx +78 -0
  21. package/src/components/Button/Button.fragment.tsx +2 -2
  22. package/src/components/Button/Button.module.scss +42 -0
  23. package/src/components/Button/index.tsx +67 -33
  24. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
  25. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  26. package/src/components/ButtonGroup/index.tsx +40 -0
  27. package/src/components/Card/Card.fragment.tsx +52 -26
  28. package/src/components/Card/Card.module.scss +52 -5
  29. package/src/components/Card/index.tsx +154 -53
  30. package/src/components/Chart/Chart.fragment.tsx +213 -0
  31. package/src/components/Chart/Chart.module.scss +123 -0
  32. package/src/components/Chart/index.tsx +267 -0
  33. package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
  34. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  35. package/src/components/Checkbox/index.tsx +3 -4
  36. package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
  37. package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
  38. package/src/components/CodeBlock/index.tsx +599 -0
  39. package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
  40. package/src/components/Collapsible/Collapsible.module.scss +117 -0
  41. package/src/components/Collapsible/index.tsx +219 -0
  42. package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
  43. package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
  44. package/src/components/ColorPicker/index.tsx +129 -0
  45. package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
  46. package/src/components/ConversationList/ConversationList.module.scss +160 -0
  47. package/src/components/ConversationList/index.tsx +254 -0
  48. package/src/components/Dialog/Dialog.fragment.tsx +12 -3
  49. package/src/components/Dialog/Dialog.module.scss +26 -7
  50. package/src/components/Dialog/index.tsx +12 -15
  51. package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
  52. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  53. package/src/components/EmptyState/index.tsx +104 -69
  54. package/src/components/Field/Field.fragment.tsx +165 -0
  55. package/src/components/Field/Field.module.scss +31 -0
  56. package/src/components/Field/index.tsx +143 -0
  57. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  58. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  59. package/src/components/Fieldset/index.tsx +47 -0
  60. package/src/components/Form/Form.fragment.tsx +286 -0
  61. package/src/components/Form/Form.module.scss +8 -0
  62. package/src/components/Form/index.tsx +53 -0
  63. package/src/components/Grid/Grid.fragment.tsx +18 -18
  64. package/src/components/Grid/index.tsx +6 -1
  65. package/src/components/Header/Header.fragment.tsx +192 -0
  66. package/src/components/Header/Header.module.scss +208 -0
  67. package/src/components/Header/index.tsx +363 -0
  68. package/src/components/Icon/Icon.fragment.tsx +138 -0
  69. package/src/components/Icon/Icon.module.scss +38 -0
  70. package/src/components/Icon/index.tsx +58 -0
  71. package/src/components/Image/Image.fragment.tsx +195 -0
  72. package/src/components/Image/Image.module.scss +77 -0
  73. package/src/components/Image/index.tsx +95 -0
  74. package/src/components/Input/Input.fragment.tsx +1 -1
  75. package/src/components/Input/Input.module.scss +75 -2
  76. package/src/components/Input/index.tsx +60 -21
  77. package/src/components/Link/Link.fragment.tsx +132 -0
  78. package/src/components/Link/Link.module.scss +67 -0
  79. package/src/components/Link/index.tsx +57 -0
  80. package/src/components/List/List.fragment.tsx +152 -0
  81. package/src/components/List/List.module.scss +71 -0
  82. package/src/components/List/index.tsx +106 -0
  83. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  84. package/src/components/Listbox/Listbox.module.scss +97 -0
  85. package/src/components/Listbox/index.tsx +121 -0
  86. package/src/components/Loading/Loading.fragment.tsx +153 -0
  87. package/src/components/Loading/Loading.module.scss +256 -0
  88. package/src/components/Loading/index.tsx +236 -0
  89. package/src/components/Menu/Menu.fragment.tsx +12 -3
  90. package/src/components/Menu/Menu.module.scss +17 -1
  91. package/src/components/Menu/index.tsx +3 -3
  92. package/src/components/Message/Message.fragment.tsx +200 -0
  93. package/src/components/Message/Message.module.scss +224 -0
  94. package/src/components/Message/index.tsx +278 -0
  95. package/src/components/Popover/Popover.fragment.tsx +13 -4
  96. package/src/components/Popover/Popover.module.scss +33 -10
  97. package/src/components/Popover/index.tsx +9 -11
  98. package/src/components/Progress/Progress.fragment.tsx +1 -1
  99. package/src/components/Progress/Progress.module.scss +11 -11
  100. package/src/components/Progress/index.tsx +34 -7
  101. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  102. package/src/components/Prompt/Prompt.module.scss +243 -0
  103. package/src/components/Prompt/index.tsx +439 -0
  104. package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
  105. package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
  106. package/src/components/RadioGroup/index.tsx +3 -4
  107. package/src/components/Select/Select.fragment.tsx +10 -1
  108. package/src/components/Select/Select.module.scss +8 -0
  109. package/src/components/Select/index.tsx +91 -12
  110. package/src/components/Separator/Separator.fragment.tsx +1 -1
  111. package/src/components/Separator/index.tsx +7 -3
  112. package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
  113. package/src/components/Sidebar/Sidebar.module.scss +91 -47
  114. package/src/components/Sidebar/index.tsx +57 -14
  115. package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
  116. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  117. package/src/components/Slider/Slider.fragment.tsx +201 -0
  118. package/src/components/Slider/Slider.module.scss +87 -0
  119. package/src/components/Slider/index.tsx +88 -0
  120. package/src/components/Stack/Stack.fragment.tsx +194 -0
  121. package/src/components/Stack/Stack.module.scss +120 -0
  122. package/src/components/Stack/index.tsx +148 -0
  123. package/src/components/Table/Table.fragment.tsx +10 -3
  124. package/src/components/Table/Table.module.scss +57 -0
  125. package/src/components/Table/index.tsx +44 -6
  126. package/src/components/Tabs/Tabs.fragment.tsx +10 -1
  127. package/src/components/Tabs/Tabs.module.scss +25 -10
  128. package/src/components/Tabs/index.tsx +11 -8
  129. package/src/components/Text/Text.fragment.tsx +188 -0
  130. package/src/components/Text/Text.module.scss +82 -0
  131. package/src/components/Text/index.tsx +58 -0
  132. package/src/components/Textarea/Textarea.fragment.tsx +1 -1
  133. package/src/components/Textarea/index.tsx +3 -7
  134. package/src/components/Theme/Theme.fragment.tsx +128 -0
  135. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  136. package/src/components/Theme/index.tsx +343 -0
  137. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
  138. package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
  139. package/src/components/ThinkingIndicator/index.tsx +258 -0
  140. package/src/components/Toast/Toast.fragment.tsx +6 -6
  141. package/src/components/Toast/Toast.module.scss +16 -1
  142. package/src/components/Toast/index.tsx +27 -11
  143. package/src/components/Toggle/Toggle.fragment.tsx +1 -1
  144. package/src/components/Toggle/Toggle.module.scss +25 -10
  145. package/src/components/Toggle/index.tsx +12 -0
  146. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
  147. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  148. package/src/components/ToggleGroup/index.tsx +144 -0
  149. package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
  150. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  151. package/src/components/Tooltip/index.tsx +4 -2
  152. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  153. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  154. package/src/components/VisuallyHidden/index.tsx +29 -0
  155. package/src/index.ts +278 -3
  156. package/src/recipes/AIChat.recipe.ts +266 -0
  157. package/src/recipes/AppShell.recipe.ts +175 -0
  158. package/src/recipes/CardGrid.recipe.ts +6 -2
  159. package/src/recipes/ChatInterface.recipe.ts +87 -0
  160. package/src/recipes/CodeExamples.recipe.ts +66 -0
  161. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  162. package/src/recipes/DashboardNav.recipe.ts +183 -0
  163. package/src/recipes/LoginForm.recipe.ts +8 -1
  164. package/src/recipes/SettingsPage.recipe.ts +37 -20
  165. package/src/styles/globals.scss +31 -0
  166. package/src/tokens/_computed.scss +212 -0
  167. package/src/tokens/_density.scss +171 -0
  168. package/src/tokens/_derive.scss +287 -0
  169. package/src/tokens/_index.scss +41 -0
  170. package/src/tokens/_mixins.scss +95 -1
  171. package/src/tokens/_palettes.scss +185 -0
  172. package/src/tokens/_radius.scss +107 -0
  173. package/src/tokens/_seeds.scss +59 -0
  174. package/src/tokens/_variables.scss +507 -101
  175. 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-space-3, $fui-space-3) var(--fui-space-4, $fui-space-4);
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
- }: ToastProps) {
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 className={toastClasses} role="alert">
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
- if (toasts.length === 0) return null;
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 className={containerClasses}>
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 './index.js';
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: 2px;
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: 2px;
49
- left: 2px;
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: 14px;
63
- height: 14px;
62
+ width: $fui-toggle-thumb-sm;
63
+ height: $fui-toggle-thumb-sm;
64
64
 
65
65
  [data-checked] > & {
66
- transform: translateX(14px);
66
+ transform: translateX($fui-toggle-thumb-sm);
67
67
  }
68
68
  }
69
69
 
70
70
  .thumbMd {
71
- width: 18px;
72
- height: 18px;
71
+ width: $fui-toggle-thumb-md;
72
+ height: $fui-toggle-thumb-md;
73
73
 
74
74
  [data-checked] > & {
75
- transform: translateX(18px);
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: 2px;
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
+ }