@fragments-sdk/ui 0.3.0 → 0.4.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 (133) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +9 -4
  3. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  4. package/src/components/Accordion/Accordion.module.scss +111 -0
  5. package/src/components/Accordion/index.tsx +271 -0
  6. package/src/components/Alert/Alert.fragment.tsx +66 -41
  7. package/src/components/Alert/Alert.module.scss +31 -21
  8. package/src/components/Alert/index.tsx +202 -73
  9. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  10. package/src/components/AppShell/AppShell.module.scss +213 -0
  11. package/src/components/AppShell/index.tsx +398 -0
  12. package/src/components/Avatar/index.tsx +8 -9
  13. package/src/components/Badge/Badge.module.scss +16 -10
  14. package/src/components/Badge/index.tsx +20 -6
  15. package/src/components/Box/Box.fragment.tsx +168 -0
  16. package/src/components/Box/Box.module.scss +84 -0
  17. package/src/components/Box/index.tsx +78 -0
  18. package/src/components/Button/Button.module.scss +42 -0
  19. package/src/components/Button/index.tsx +67 -33
  20. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  21. package/src/components/ButtonGroup/index.tsx +40 -0
  22. package/src/components/Card/Card.fragment.tsx +51 -25
  23. package/src/components/Card/Card.module.scss +52 -5
  24. package/src/components/Card/index.tsx +154 -53
  25. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  26. package/src/components/Checkbox/index.tsx +3 -4
  27. package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
  28. package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
  29. package/src/components/CodeBlock/index.tsx +385 -0
  30. package/src/components/ColorChip/ColorChip.module.scss +165 -0
  31. package/src/components/ColorChip/index.tsx +157 -0
  32. package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
  33. package/src/components/ColorPicker/index.tsx +107 -0
  34. package/src/components/Dialog/Dialog.fragment.tsx +9 -0
  35. package/src/components/Dialog/Dialog.module.scss +26 -7
  36. package/src/components/Dialog/index.tsx +12 -15
  37. package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
  38. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  39. package/src/components/EmptyState/index.tsx +104 -69
  40. package/src/components/Field/Field.fragment.tsx +165 -0
  41. package/src/components/Field/Field.module.scss +31 -0
  42. package/src/components/Field/index.tsx +143 -0
  43. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  44. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  45. package/src/components/Fieldset/index.tsx +47 -0
  46. package/src/components/Form/Form.fragment.tsx +286 -0
  47. package/src/components/Form/Form.module.scss +8 -0
  48. package/src/components/Form/index.tsx +53 -0
  49. package/src/components/Grid/Grid.fragment.tsx +17 -17
  50. package/src/components/Grid/index.tsx +6 -1
  51. package/src/components/Header/Header.fragment.tsx +192 -0
  52. package/src/components/Header/Header.module.scss +209 -0
  53. package/src/components/Header/index.tsx +363 -0
  54. package/src/components/Icon/Icon.fragment.tsx +138 -0
  55. package/src/components/Icon/Icon.module.scss +38 -0
  56. package/src/components/Icon/index.tsx +58 -0
  57. package/src/components/Image/Image.fragment.tsx +195 -0
  58. package/src/components/Image/Image.module.scss +77 -0
  59. package/src/components/Image/index.tsx +95 -0
  60. package/src/components/Input/Input.module.scss +75 -2
  61. package/src/components/Input/index.tsx +60 -21
  62. package/src/components/Link/Link.fragment.tsx +132 -0
  63. package/src/components/Link/Link.module.scss +67 -0
  64. package/src/components/Link/index.tsx +57 -0
  65. package/src/components/List/List.fragment.tsx +152 -0
  66. package/src/components/List/List.module.scss +71 -0
  67. package/src/components/List/index.tsx +106 -0
  68. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  69. package/src/components/Listbox/Listbox.module.scss +97 -0
  70. package/src/components/Listbox/index.tsx +121 -0
  71. package/src/components/Menu/Menu.fragment.tsx +9 -0
  72. package/src/components/Menu/Menu.module.scss +17 -1
  73. package/src/components/Menu/index.tsx +3 -3
  74. package/src/components/Popover/Popover.fragment.tsx +9 -0
  75. package/src/components/Popover/Popover.module.scss +33 -10
  76. package/src/components/Popover/index.tsx +9 -11
  77. package/src/components/Progress/Progress.module.scss +11 -11
  78. package/src/components/Progress/index.tsx +34 -7
  79. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  80. package/src/components/Prompt/Prompt.module.scss +243 -0
  81. package/src/components/Prompt/index.tsx +439 -0
  82. package/src/components/RadioGroup/RadioGroup.module.scss +3 -3
  83. package/src/components/RadioGroup/index.tsx +3 -4
  84. package/src/components/Select/Select.fragment.tsx +9 -0
  85. package/src/components/Select/index.tsx +6 -7
  86. package/src/components/Separator/index.tsx +7 -3
  87. package/src/components/Sidebar/Sidebar.fragment.tsx +9 -0
  88. package/src/components/Sidebar/Sidebar.module.scss +72 -47
  89. package/src/components/Sidebar/index.tsx +5 -3
  90. package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
  91. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  92. package/src/components/Slider/Slider.module.scss +87 -0
  93. package/src/components/Slider/index.tsx +88 -0
  94. package/src/components/Stack/Stack.module.scss +120 -0
  95. package/src/components/Stack/index.tsx +148 -0
  96. package/src/components/Table/Table.fragment.tsx +7 -0
  97. package/src/components/Table/Table.module.scss +57 -0
  98. package/src/components/Table/index.tsx +44 -6
  99. package/src/components/Tabs/Tabs.fragment.tsx +9 -0
  100. package/src/components/Tabs/Tabs.module.scss +25 -10
  101. package/src/components/Tabs/index.tsx +11 -8
  102. package/src/components/Text/Text.module.scss +82 -0
  103. package/src/components/Text/index.tsx +58 -0
  104. package/src/components/Textarea/index.tsx +3 -7
  105. package/src/components/Theme/Theme.fragment.tsx +128 -0
  106. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  107. package/src/components/Theme/index.tsx +343 -0
  108. package/src/components/Toast/Toast.fragment.tsx +5 -5
  109. package/src/components/Toast/Toast.module.scss +16 -1
  110. package/src/components/Toast/index.tsx +27 -11
  111. package/src/components/Toggle/Toggle.module.scss +25 -10
  112. package/src/components/Toggle/index.tsx +12 -0
  113. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  114. package/src/components/ToggleGroup/index.tsx +144 -0
  115. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  116. package/src/components/Tooltip/index.tsx +4 -2
  117. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  118. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  119. package/src/components/VisuallyHidden/index.tsx +29 -0
  120. package/src/index.ts +195 -3
  121. package/src/recipes/AppShell.recipe.ts +175 -0
  122. package/src/recipes/CardGrid.recipe.ts +6 -2
  123. package/src/recipes/ChatInterface.recipe.ts +87 -0
  124. package/src/recipes/CodeExamples.recipe.ts +66 -0
  125. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  126. package/src/recipes/DashboardNav.recipe.ts +183 -0
  127. package/src/recipes/LoginForm.recipe.ts +8 -1
  128. package/src/recipes/SettingsPage.recipe.ts +37 -20
  129. package/src/styles/globals.scss +31 -0
  130. package/src/tokens/_index.scss +3 -0
  131. package/src/tokens/_mixins.scss +54 -1
  132. package/src/tokens/_variables.scss +429 -64
  133. package/src/utils/a11y.tsx +439 -0
@@ -12,18 +12,11 @@
12
12
  width: var(--sidebar-width);
13
13
  height: 100vh;
14
14
  background-color: var(--fui-bg-primary, $fui-bg-primary);
15
- border-right: 1px solid var(--fui-border, $fui-border);
16
15
  transition: width var(--fui-transition-normal, $fui-transition-normal);
17
16
  overflow: hidden;
18
17
  flex-shrink: 0;
19
18
  }
20
19
 
21
- // Right position variant
22
- .positionRight {
23
- border-right: none;
24
- border-left: 1px solid var(--fui-border, $fui-border);
25
- }
26
-
27
20
  // Desktop collapsed state
28
21
  .collapsed {
29
22
  width: var(--sidebar-collapsed-width);
@@ -91,10 +84,9 @@
91
84
  display: flex;
92
85
  align-items: center;
93
86
  gap: var(--fui-space-3, $fui-space-3);
94
- padding: var(--fui-space-4, $fui-space-4) var(--fui-space-4, $fui-space-4);
95
- border-bottom: 1px solid var(--fui-border, $fui-border);
96
- min-height: 60px;
87
+ padding: var(--fui-space-2, $fui-space-2) var(--fui-space-2, $fui-space-2);
97
88
  flex-shrink: 0;
89
+ height: var(--appshell-header-height, 56px);
98
90
  }
99
91
 
100
92
  // ============================================
@@ -122,7 +114,7 @@
122
114
  display: flex;
123
115
  align-items: center;
124
116
  justify-content: space-between;
125
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
117
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
126
118
  }
127
119
 
128
120
  .sectionLabel {
@@ -145,8 +137,8 @@
145
137
  display: flex;
146
138
  align-items: center;
147
139
  justify-content: center;
148
- width: 24px;
149
- height: 24px;
140
+ width: $fui-touch-sm;
141
+ height: $fui-touch-sm;
150
142
  border-radius: var(--fui-radius-sm, $fui-radius-sm);
151
143
  color: var(--fui-text-tertiary, $fui-text-tertiary);
152
144
 
@@ -156,8 +148,8 @@
156
148
  }
157
149
 
158
150
  svg {
159
- width: 16px;
160
- height: 16px;
151
+ width: $fui-icon-md;
152
+ height: $fui-icon-md;
161
153
  }
162
154
  }
163
155
 
@@ -184,11 +176,11 @@
184
176
  align-items: center;
185
177
  gap: var(--fui-space-3, $fui-space-3);
186
178
  width: 100%;
187
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
179
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
188
180
  border-radius: var(--fui-radius-md, $fui-radius-md);
189
181
  color: var(--fui-text-secondary, $fui-text-secondary);
190
182
  text-decoration: none;
191
- min-height: 40px;
183
+ min-height: $fui-sidebar-item-height;
192
184
 
193
185
  &:hover:not(.itemDisabled) {
194
186
  background-color: var(--fui-bg-hover, $fui-bg-hover);
@@ -224,14 +216,14 @@
224
216
  display: flex;
225
217
  align-items: center;
226
218
  justify-content: center;
227
- width: 20px;
228
- height: 20px;
219
+ width: $fui-icon-lg;
220
+ height: $fui-icon-lg;
229
221
  flex-shrink: 0;
230
222
  color: var(--fui-text-tertiary, $fui-text-tertiary);
231
223
 
232
224
  svg {
233
- width: 20px;
234
- height: 20px;
225
+ width: $fui-icon-lg;
226
+ height: $fui-icon-lg;
235
227
  }
236
228
 
237
229
  .item:hover:not(.itemDisabled) & {
@@ -258,7 +250,7 @@
258
250
  background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
259
251
  color: var(--fui-text-inverse, $fui-text-inverse);
260
252
  border-radius: var(--fui-radius-full, $fui-radius-full);
261
- min-width: 20px;
253
+ min-width: $fui-icon-lg;
262
254
  line-height: 1;
263
255
 
264
256
  .itemActive & {
@@ -271,14 +263,14 @@
271
263
  display: flex;
272
264
  align-items: center;
273
265
  justify-content: center;
274
- width: 16px;
275
- height: 16px;
266
+ width: $fui-icon-md;
267
+ height: $fui-icon-md;
276
268
  flex-shrink: 0;
277
269
  transition: transform var(--fui-transition-fast, $fui-transition-fast);
278
270
 
279
271
  svg {
280
- width: 16px;
281
- height: 16px;
272
+ width: $fui-icon-md;
273
+ height: $fui-icon-md;
282
274
  }
283
275
  }
284
276
 
@@ -325,12 +317,12 @@
325
317
  display: flex;
326
318
  align-items: center;
327
319
  width: 100%;
328
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
320
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
329
321
  border-radius: var(--fui-radius-md, $fui-radius-md);
330
322
  color: var(--fui-text-secondary, $fui-text-secondary);
331
323
  text-decoration: none;
332
324
  font-size: var(--fui-font-size-sm, $fui-font-size-sm);
333
- min-height: 36px;
325
+ min-height: $fui-sidebar-subitem-height;
334
326
  position: relative;
335
327
 
336
328
  // Left border indicator
@@ -340,8 +332,8 @@
340
332
  left: 0;
341
333
  top: 50%;
342
334
  transform: translateY(-50%);
343
- width: 2px;
344
- height: 16px;
335
+ width: $fui-sidebar-indicator-width;
336
+ height: $fui-sidebar-indicator-height;
345
337
  background-color: transparent;
346
338
  border-radius: var(--fui-radius-full, $fui-radius-full);
347
339
  transition: background-color var(--fui-transition-fast, $fui-transition-fast);
@@ -381,7 +373,6 @@
381
373
  flex-direction: column;
382
374
  gap: var(--fui-space-2, $fui-space-2);
383
375
  padding: var(--fui-space-4, $fui-space-4);
384
- border-top: 1px solid var(--fui-border, $fui-border);
385
376
  margin-top: auto;
386
377
  flex-shrink: 0;
387
378
  }
@@ -397,8 +388,8 @@
397
388
  display: flex;
398
389
  align-items: center;
399
390
  justify-content: center;
400
- width: 44px;
401
- height: 44px;
391
+ width: $fui-touch-lg;
392
+ height: $fui-touch-lg;
402
393
  border-radius: var(--fui-radius-md, $fui-radius-md);
403
394
  color: var(--fui-text-primary, $fui-text-primary);
404
395
 
@@ -407,8 +398,8 @@
407
398
  }
408
399
 
409
400
  svg {
410
- width: 24px;
411
- height: 24px;
401
+ width: $fui-icon-xl;
402
+ height: $fui-icon-xl;
412
403
  }
413
404
  }
414
405
 
@@ -440,8 +431,8 @@
440
431
  display: flex;
441
432
  align-items: center;
442
433
  justify-content: center;
443
- width: 32px;
444
- height: 32px;
434
+ width: $fui-touch-md;
435
+ height: $fui-touch-md;
445
436
  border-radius: var(--fui-radius-md, $fui-radius-md);
446
437
  color: var(--fui-text-secondary, $fui-text-secondary);
447
438
  margin-left: auto;
@@ -452,8 +443,8 @@
452
443
  }
453
444
 
454
445
  svg {
455
- width: 20px;
456
- height: 20px;
446
+ width: $fui-icon-lg;
447
+ height: $fui-icon-lg;
457
448
  }
458
449
  }
459
450
 
@@ -480,7 +471,7 @@
480
471
  position: absolute;
481
472
  top: 0;
482
473
  right: 0;
483
- width: 16px;
474
+ width: $fui-sidebar-rail-width;
484
475
  height: 100%;
485
476
  cursor: pointer;
486
477
  z-index: 10;
@@ -502,9 +493,9 @@
502
493
  position: absolute;
503
494
  top: 50%;
504
495
  transform: translateY(-50%);
505
- right: 4px;
506
- width: 4px;
507
- height: 40px;
496
+ right: $fui-sidebar-rail-indicator-width;
497
+ width: $fui-sidebar-rail-indicator-width;
498
+ height: $fui-sidebar-rail-indicator-height;
508
499
  border-radius: var(--fui-radius-full, $fui-radius-full);
509
500
  background-color: var(--fui-border, $fui-border);
510
501
  opacity: 0;
@@ -515,14 +506,14 @@
515
506
 
516
507
  .positionRight & {
517
508
  right: auto;
518
- left: 4px;
509
+ left: $fui-sidebar-rail-indicator-width;
519
510
  }
520
511
  }
521
512
 
522
513
  // Show indicator on hover
523
514
  &:hover::before {
524
515
  opacity: 1;
525
- height: 56px;
516
+ height: $fui-sidebar-rail-indicator-height-hover;
526
517
  background-color: var(--fui-color-accent, $fui-color-accent);
527
518
  }
528
519
 
@@ -552,10 +543,44 @@
552
543
  display: flex;
553
544
  align-items: center;
554
545
  gap: var(--fui-space-3, $fui-space-3);
555
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
556
- min-height: 40px;
546
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
547
+ min-height: $fui-sidebar-item-height;
557
548
  }
558
549
 
559
550
  .skeletonLabel {
560
551
  flex: 1;
561
552
  }
553
+
554
+ // ============================================
555
+ // Accessibility: Reduced Motion
556
+ // ============================================
557
+
558
+ @media (prefers-reduced-motion: reduce) {
559
+ .root {
560
+ transition: none;
561
+ }
562
+
563
+ .mobile {
564
+ transition: none;
565
+ }
566
+
567
+ .overlay {
568
+ transition: none;
569
+ }
570
+
571
+ .submenu {
572
+ transition: none;
573
+ }
574
+
575
+ .itemChevron {
576
+ transition: none;
577
+ }
578
+
579
+ .rail::before {
580
+ transition: none;
581
+ }
582
+
583
+ .subItem::before {
584
+ transition: none;
585
+ }
586
+ }
@@ -38,7 +38,7 @@ export interface SidebarProviderProps {
38
38
  enableKeyboardShortcut?: boolean;
39
39
  }
40
40
 
41
- export interface SidebarProps {
41
+ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
42
42
  children: React.ReactNode;
43
43
  /** Icon-only mode (desktop) - controlled */
44
44
  collapsed?: boolean;
@@ -60,8 +60,6 @@ export interface SidebarProps {
60
60
  position?: 'left' | 'right';
61
61
  /** Collapse behavior: 'icon' (default), 'offcanvas', or 'none' */
62
62
  collapsible?: SidebarCollapsible;
63
- /** Additional class name */
64
- className?: string;
65
63
  }
66
64
 
67
65
  export interface SidebarHeaderProps {
@@ -475,6 +473,8 @@ function SidebarRoot({
475
473
  position = 'left',
476
474
  collapsible = 'icon',
477
475
  className,
476
+ style: styleProp,
477
+ ...htmlProps
478
478
  }: SidebarProps) {
479
479
  // Check if we're inside a SidebarProvider
480
480
  const existingContext = React.useContext(SidebarContext);
@@ -570,10 +570,12 @@ function SidebarRoot({
570
570
  const style: React.CSSProperties = {
571
571
  '--sidebar-width': resolvedWidth,
572
572
  '--sidebar-collapsed-width': resolvedCollapsedWidth,
573
+ ...styleProp,
573
574
  } as React.CSSProperties;
574
575
 
575
576
  const content = (
576
577
  <aside
578
+ {...htmlProps}
577
579
  className={classes}
578
580
  style={style}
579
581
  data-state={isMobile ? (open ? 'open' : 'closed') : (collapsed ? 'collapsed' : 'expanded')}
@@ -110,7 +110,7 @@ export default defineSegment({
110
110
  name: 'Semantic Variants',
111
111
  description: 'Pre-configured shapes for common elements',
112
112
  render: () => (
113
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
113
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--fui-space-2)' }}>
114
114
  <Skeleton variant="heading" width={200} />
115
115
  <Skeleton variant="text" width="100%" />
116
116
  <Skeleton variant="text" width="80%" />
@@ -121,7 +121,7 @@ export default defineSegment({
121
121
  name: 'Avatar Skeleton',
122
122
  description: 'Circular placeholder for avatars',
123
123
  render: () => (
124
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
124
+ <div style={{ display: 'flex', gap: 'var(--fui-space-1)', alignItems: 'center' }}>
125
125
  <Skeleton.Circle size="sm" />
126
126
  <Skeleton.Circle size="md" />
127
127
  <Skeleton.Circle size="lg" />
@@ -132,12 +132,12 @@ export default defineSegment({
132
132
  name: 'Card Skeleton',
133
133
  description: 'Composed skeleton for a card layout',
134
134
  render: () => (
135
- <div style={{ width: 300, padding: 16, border: '1px solid #e5e7eb', borderRadius: 8 }}>
135
+ <div style={{ width: 300, padding: 'var(--fui-space-2)', border: '1px solid var(--fui-border)', borderRadius: 'var(--fui-radius-lg)' }}>
136
136
  <Skeleton variant="rect" height={120} radius="md" />
137
- <div style={{ marginTop: 12 }}>
137
+ <div style={{ marginTop: 'var(--fui-space-2)' }}>
138
138
  <Skeleton variant="heading" width="60%" />
139
139
  </div>
140
- <div style={{ marginTop: 8 }}>
140
+ <div style={{ marginTop: 'var(--fui-space-1)' }}>
141
141
  <Skeleton.Text lines={2} />
142
142
  </div>
143
143
  </div>
@@ -164,3 +164,14 @@
164
164
  background-color: var(--fui-bg-tertiary, #{$fui-dark-bg-tertiary});
165
165
  }
166
166
  }
167
+
168
+ // ============================================
169
+ // Accessibility: Reduced Motion
170
+ // ============================================
171
+
172
+ @media (prefers-reduced-motion: reduce) {
173
+ .skeleton,
174
+ .textLine {
175
+ animation: none;
176
+ }
177
+ }
@@ -0,0 +1,87 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ .wrapper {
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
8
+
9
+ .header {
10
+ display: flex;
11
+ justify-content: space-between;
12
+ align-items: center;
13
+ margin-bottom: var(--fui-space-2, $fui-space-2);
14
+ }
15
+
16
+ .label {
17
+ @include label-text;
18
+ }
19
+
20
+ .value {
21
+ font-family: var(--fui-font-mono, $fui-font-mono);
22
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
23
+ color: var(--fui-text-secondary, $fui-text-secondary);
24
+ min-width: 60px;
25
+ text-align: right;
26
+ }
27
+
28
+ .root {
29
+ width: 100%;
30
+ }
31
+
32
+ .control {
33
+ display: flex;
34
+ align-items: center;
35
+ width: 100%;
36
+ }
37
+
38
+ .track {
39
+ position: relative;
40
+ height: $fui-slider-track-height;
41
+ width: 100%;
42
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
43
+ border-radius: var(--fui-radius-full, $fui-radius-full);
44
+ cursor: pointer;
45
+
46
+ &[data-disabled] {
47
+ opacity: 0.5;
48
+ cursor: not-allowed;
49
+ }
50
+ }
51
+
52
+ .indicator {
53
+ position: absolute;
54
+ height: 100%;
55
+ background-color: var(--fui-color-accent, $fui-color-accent);
56
+ border-radius: var(--fui-radius-full, $fui-radius-full);
57
+ }
58
+
59
+ .thumb {
60
+ width: $fui-slider-thumb-size;
61
+ height: $fui-slider-thumb-size;
62
+ background-color: var(--fui-bg-primary, $fui-bg-primary);
63
+ border: $fui-slider-thumb-border solid var(--fui-color-accent, $fui-color-accent);
64
+ border-radius: var(--fui-radius-full, $fui-radius-full);
65
+ cursor: grab;
66
+ transition:
67
+ transform var(--fui-transition-fast, $fui-transition-fast),
68
+ box-shadow var(--fui-transition-fast, $fui-transition-fast);
69
+
70
+ &:hover {
71
+ transform: scale(1.1);
72
+ }
73
+
74
+ &:active {
75
+ cursor: grabbing;
76
+ transform: scale(1.05);
77
+ }
78
+
79
+ &:focus-visible {
80
+ @include focus-ring;
81
+ }
82
+
83
+ &[data-disabled] {
84
+ cursor: not-allowed;
85
+ opacity: 0.5;
86
+ }
87
+ }
@@ -0,0 +1,88 @@
1
+ import * as React from 'react';
2
+ import { Field } from '@base-ui/react/field';
3
+ import { Slider as BaseSlider } from '@base-ui/react/slider';
4
+ import styles from './Slider.module.scss';
5
+ import '../../styles/globals.scss';
6
+
7
+ export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
8
+ label?: string;
9
+ value?: number;
10
+ defaultValue?: number;
11
+ onChange?: (value: number) => void;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number;
15
+ showValue?: boolean;
16
+ valueSuffix?: string;
17
+ disabled?: boolean;
18
+ name?: string;
19
+ }
20
+
21
+ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
22
+ function Slider(
23
+ {
24
+ label,
25
+ value,
26
+ defaultValue,
27
+ onChange,
28
+ min = 0,
29
+ max = 100,
30
+ step = 1,
31
+ showValue = false,
32
+ valueSuffix = '',
33
+ disabled = false,
34
+ className,
35
+ name,
36
+ id,
37
+ ...htmlProps
38
+ },
39
+ ref
40
+ ) {
41
+ // For controlled component, use value; otherwise track internal state for display
42
+ const [internalValue, setInternalValue] = React.useState(defaultValue ?? min);
43
+ const displayValue = value !== undefined ? value : internalValue;
44
+
45
+ const handleChange = (newValue: number | number[]) => {
46
+ const val = Array.isArray(newValue) ? newValue[0] : newValue;
47
+ setInternalValue(val);
48
+ onChange?.(val);
49
+ };
50
+
51
+ const sliderValue = value !== undefined ? [value] : (defaultValue !== undefined ? [defaultValue] : undefined);
52
+
53
+ return (
54
+ <Field.Root {...htmlProps} disabled={disabled} className={[styles.wrapper, className].filter(Boolean).join(' ')}>
55
+ {(label || showValue) && (
56
+ <div className={styles.header}>
57
+ {label && <Field.Label className={styles.label}>{label}</Field.Label>}
58
+ {showValue && (
59
+ <span className={styles.value}>
60
+ {displayValue}{valueSuffix}
61
+ </span>
62
+ )}
63
+ </div>
64
+ )}
65
+ <BaseSlider.Root
66
+ ref={ref}
67
+ value={sliderValue}
68
+ defaultValue={defaultValue !== undefined ? [defaultValue] : undefined}
69
+ onValueChange={handleChange}
70
+ min={min}
71
+ max={max}
72
+ step={step}
73
+ disabled={disabled}
74
+ name={name}
75
+ id={id}
76
+ className={styles.root}
77
+ >
78
+ <BaseSlider.Control className={styles.control}>
79
+ <BaseSlider.Track className={styles.track}>
80
+ <BaseSlider.Indicator className={styles.indicator} />
81
+ <BaseSlider.Thumb className={styles.thumb} />
82
+ </BaseSlider.Track>
83
+ </BaseSlider.Control>
84
+ </BaseSlider.Root>
85
+ </Field.Root>
86
+ );
87
+ }
88
+ );
@@ -0,0 +1,120 @@
1
+ @use '../../tokens/variables' as *;
2
+
3
+ .stack {
4
+ display: flex;
5
+ }
6
+
7
+ // Direction
8
+ .row {
9
+ flex-direction: row;
10
+ }
11
+
12
+ .column {
13
+ flex-direction: column;
14
+ }
15
+
16
+ // Responsive direction
17
+ .directionResponsive {
18
+ flex-direction: var(--fui-stack-direction, column);
19
+
20
+ @media (min-width: 640px) {
21
+ flex-direction: var(--fui-stack-direction-sm, var(--fui-stack-direction, column));
22
+ }
23
+
24
+ @media (min-width: 768px) {
25
+ flex-direction: var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column)));
26
+ }
27
+
28
+ @media (min-width: 1024px) {
29
+ flex-direction: var(--fui-stack-direction-lg, var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column))));
30
+ }
31
+
32
+ @media (min-width: 1280px) {
33
+ flex-direction: var(--fui-stack-direction-xl, var(--fui-stack-direction-lg, var(--fui-stack-direction-md, var(--fui-stack-direction-sm, var(--fui-stack-direction, column)))));
34
+ }
35
+ }
36
+
37
+ // Gap sizes
38
+ .gap-xs {
39
+ gap: var(--fui-space-1, $fui-space-1);
40
+ }
41
+
42
+ .gap-sm {
43
+ gap: var(--fui-space-2, $fui-space-2);
44
+ }
45
+
46
+ .gap-md {
47
+ gap: var(--fui-space-3, $fui-space-3);
48
+ }
49
+
50
+ .gap-lg {
51
+ gap: var(--fui-space-4, $fui-space-4);
52
+ }
53
+
54
+ .gap-xl {
55
+ gap: var(--fui-space-6, $fui-space-6);
56
+ }
57
+
58
+ // Responsive gap
59
+ .gapResponsive {
60
+ gap: var(--fui-stack-gap, 0);
61
+
62
+ @media (min-width: 640px) {
63
+ gap: var(--fui-stack-gap-sm, var(--fui-stack-gap, 0));
64
+ }
65
+
66
+ @media (min-width: 768px) {
67
+ gap: var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0)));
68
+ }
69
+
70
+ @media (min-width: 1024px) {
71
+ gap: var(--fui-stack-gap-lg, var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0))));
72
+ }
73
+
74
+ @media (min-width: 1280px) {
75
+ gap: var(--fui-stack-gap-xl, var(--fui-stack-gap-lg, var(--fui-stack-gap-md, var(--fui-stack-gap-sm, var(--fui-stack-gap, 0)))));
76
+ }
77
+ }
78
+
79
+ // Align (cross-axis)
80
+ .align-start {
81
+ align-items: flex-start;
82
+ }
83
+
84
+ .align-center {
85
+ align-items: center;
86
+ }
87
+
88
+ .align-end {
89
+ align-items: flex-end;
90
+ }
91
+
92
+ .align-stretch {
93
+ align-items: stretch;
94
+ }
95
+
96
+ .align-baseline {
97
+ align-items: baseline;
98
+ }
99
+
100
+ // Justify (main-axis)
101
+ .justify-start {
102
+ justify-content: flex-start;
103
+ }
104
+
105
+ .justify-center {
106
+ justify-content: center;
107
+ }
108
+
109
+ .justify-end {
110
+ justify-content: flex-end;
111
+ }
112
+
113
+ .justify-between {
114
+ justify-content: space-between;
115
+ }
116
+
117
+ // Wrap
118
+ .wrap {
119
+ flex-wrap: wrap;
120
+ }