@fragments-sdk/ui 0.7.2 → 0.7.4

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 (46) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +6 -2
  3. package/src/components/Accordion/Accordion.module.scss +11 -7
  4. package/src/components/AppShell/AppShell.module.scss +5 -5
  5. package/src/components/Avatar/index.tsx +11 -1
  6. package/src/components/Button/Button.module.scss +3 -3
  7. package/src/components/Chart/Chart.module.scss +5 -0
  8. package/src/components/Chart/index.tsx +34 -0
  9. package/src/components/Checkbox/index.tsx +29 -4
  10. package/src/components/Chip/Chip.module.scss +20 -7
  11. package/src/components/Chip/index.tsx +36 -26
  12. package/src/components/CodeBlock/CodeBlock.module.scss +6 -6
  13. package/src/components/CodeBlock/index.tsx +4 -1
  14. package/src/components/Collapsible/Collapsible.module.scss +1 -1
  15. package/src/components/Combobox/Combobox.module.scss +4 -4
  16. package/src/components/ConversationList/ConversationList.module.scss +4 -4
  17. package/src/components/Dialog/Dialog.module.scss +4 -4
  18. package/src/components/Dialog/index.tsx +14 -6
  19. package/src/components/EmptyState/EmptyState.module.scss +3 -3
  20. package/src/components/Grid/Grid.module.scss +3 -3
  21. package/src/components/Header/Header.module.scss +4 -4
  22. package/src/components/Input/index.tsx +15 -1
  23. package/src/components/Listbox/Listbox.module.scss +18 -8
  24. package/src/components/Listbox/index.tsx +163 -13
  25. package/src/components/Markdown/Markdown.module.scss +3 -3
  26. package/src/components/Menu/Menu.module.scss +3 -3
  27. package/src/components/Message/Message.module.scss +9 -3
  28. package/src/components/Popover/index.tsx +4 -1
  29. package/src/components/Prompt/Prompt.module.scss +6 -5
  30. package/src/components/RadioGroup/index.tsx +37 -4
  31. package/src/components/Select/Select.module.scss +3 -3
  32. package/src/components/Select/index.tsx +3 -2
  33. package/src/components/Sidebar/Sidebar.module.scss +32 -22
  34. package/src/components/Sidebar/index.tsx +31 -9
  35. package/src/components/Slider/index.tsx +13 -3
  36. package/src/components/Table/Table.module.scss +27 -17
  37. package/src/components/Table/index.tsx +52 -30
  38. package/src/components/Tabs/Tabs.module.scss +2 -4
  39. package/src/components/Textarea/index.tsx +22 -2
  40. package/src/components/Toast/index.tsx +114 -20
  41. package/src/components/ToggleGroup/index.tsx +55 -1
  42. package/src/components/Tooltip/Tooltip.module.scss +1 -1
  43. package/src/tokens/_computed.scss +2 -0
  44. package/src/tokens/_density.scss +4 -0
  45. package/src/tokens/_derive.scss +16 -16
  46. package/src/tokens/_variables.scss +8 -2
@@ -41,10 +41,21 @@ export interface RadioItemProps {
41
41
  description?: string;
42
42
  /** Whether this item is disabled */
43
43
  disabled?: boolean;
44
+ /** Accessible name for icon-only mode */
45
+ 'aria-label'?: string;
46
+ /** Accessible labelled-by relationship */
47
+ 'aria-labelledby'?: string;
48
+ /** Accessible described-by relationship */
49
+ 'aria-describedby'?: string;
44
50
  /** Additional class name */
45
51
  className?: string;
46
52
  }
47
53
 
54
+ function mergeAriaIds(...ids: Array<string | undefined>): string | undefined {
55
+ const merged = ids.filter(Boolean).join(' ').trim();
56
+ return merged.length > 0 ? merged : undefined;
57
+ }
58
+
48
59
  // ============================================
49
60
  // Context for size
50
61
  // ============================================
@@ -60,9 +71,15 @@ function RadioItem({
60
71
  label,
61
72
  description,
62
73
  disabled = false,
74
+ 'aria-label': ariaLabel,
75
+ 'aria-labelledby': ariaLabelledBy,
76
+ 'aria-describedby': ariaDescribedBy,
63
77
  className,
64
78
  }: RadioItemProps) {
65
79
  const size = React.useContext(RadioSizeContext);
80
+ const id = React.useId();
81
+ const labelId = label ? `radio-label-${id}` : undefined;
82
+ const descriptionId = description ? `radio-desc-${id}` : undefined;
66
83
 
67
84
  const radioClasses = [
68
85
  styles.radio,
@@ -84,6 +101,9 @@ function RadioItem({
84
101
  <BaseRadio.Root
85
102
  value={value}
86
103
  disabled={disabled}
104
+ aria-label={ariaLabel}
105
+ aria-labelledby={ariaLabelledBy}
106
+ aria-describedby={ariaDescribedBy}
87
107
  className={[radioClasses, className].filter(Boolean).join(' ')}
88
108
  >
89
109
  <BaseRadio.Indicator className={styles.indicator} />
@@ -96,14 +116,17 @@ function RadioItem({
96
116
  <BaseRadio.Root
97
117
  value={value}
98
118
  disabled={disabled}
119
+ aria-label={ariaLabel}
120
+ aria-labelledby={mergeAriaIds(ariaLabelledBy, labelId)}
121
+ aria-describedby={mergeAriaIds(ariaDescribedBy, descriptionId)}
99
122
  className={radioClasses}
100
123
  >
101
124
  <BaseRadio.Indicator className={styles.indicator} />
102
125
  </BaseRadio.Root>
103
126
  <div className={styles.content}>
104
- <span className={labelClasses}>{label}</span>
127
+ <span id={labelId} className={labelClasses}>{label}</span>
105
128
  {description && (
106
- <span className={styles.description}>{description}</span>
129
+ <span id={descriptionId} className={styles.description}>{description}</span>
107
130
  )}
108
131
  </div>
109
132
  </label>
@@ -126,8 +149,15 @@ function RadioGroupRoot({
126
149
  size = 'md',
127
150
  children,
128
151
  className,
152
+ 'aria-label': ariaLabel,
153
+ 'aria-labelledby': ariaLabelledBy,
154
+ 'aria-describedby': ariaDescribedBy,
129
155
  ...htmlProps
130
156
  }: RadioGroupProps) {
157
+ const groupId = React.useId();
158
+ const labelId = label ? `radio-group-label-${groupId}` : undefined;
159
+ const errorId = error ? `radio-group-error-${groupId}` : undefined;
160
+
131
161
  const groupClasses = [
132
162
  styles.group,
133
163
  styles[orientation],
@@ -137,18 +167,21 @@ function RadioGroupRoot({
137
167
  return (
138
168
  <RadioSizeContext.Provider value={size}>
139
169
  <div {...htmlProps} className={styles.wrapper}>
140
- {label && <span className={styles.groupLabel}>{label}</span>}
170
+ {label && <span id={labelId} className={styles.groupLabel}>{label}</span>}
141
171
  <BaseRadioGroup
142
172
  value={value}
143
173
  defaultValue={defaultValue}
144
174
  onValueChange={onValueChange}
145
175
  disabled={disabled}
146
176
  name={name}
177
+ aria-label={ariaLabel}
178
+ aria-labelledby={mergeAriaIds(ariaLabelledBy, labelId)}
179
+ aria-describedby={mergeAriaIds(ariaDescribedBy, errorId)}
147
180
  className={groupClasses}
148
181
  >
149
182
  {children}
150
183
  </BaseRadioGroup>
151
- {error && <span className={styles.error}>{error}</span>}
184
+ {error && <span id={errorId} className={styles.error}>{error}</span>}
152
185
  </div>
153
186
  </RadioSizeContext.Provider>
154
187
  );
@@ -96,7 +96,7 @@
96
96
  var(--fui-space-1, #{$fui-space-1}) * 2
97
97
  ) !important;
98
98
  overflow-y: auto !important;
99
- padding: var(--fui-space-1, $fui-space-1);
99
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
100
100
  box-shadow: var(--fui-shadow-md, $fui-shadow-md);
101
101
 
102
102
  // Animation
@@ -128,7 +128,7 @@
128
128
  align-items: center;
129
129
  gap: var(--fui-space-2, $fui-space-2);
130
130
  width: 100%;
131
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
131
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
132
132
  border-radius: var(--fui-radius-sm, $fui-radius-sm);
133
133
  cursor: pointer;
134
134
  outline: none;
@@ -178,7 +178,7 @@
178
178
 
179
179
  // Group label
180
180
  .groupLabel {
181
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
181
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
182
182
  font-size: var(--fui-font-size-xs, $fui-font-size-xs);
183
183
  font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
184
184
  color: var(--fui-text-tertiary, $fui-text-tertiary);
@@ -268,11 +268,12 @@ function SelectItem({ children, value, disabled, className }: SelectItemProps) {
268
268
 
269
269
  // Register this item's children in the registry so the trigger can display them
270
270
  React.useEffect(() => {
271
- itemsRef.current.set(value, children);
271
+ const items = itemsRef.current;
272
+ items.set(value, children);
272
273
  // Trigger re-render of trigger to show the registered content
273
274
  incrementItemsVersion();
274
275
  return () => {
275
- itemsRef.current.delete(value);
276
+ items.delete(value);
276
277
  };
277
278
  }, [itemsRef, incrementItemsVersion, value, children]);
278
279
 
@@ -31,7 +31,7 @@
31
31
 
32
32
  .header {
33
33
  justify-content: center;
34
- padding: var(--fui-space-3, $fui-space-3);
34
+ padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
35
35
  }
36
36
 
37
37
  .sectionLabel {
@@ -46,7 +46,7 @@
46
46
 
47
47
  .item {
48
48
  justify-content: center;
49
- padding: var(--fui-space-3, $fui-space-3);
49
+ padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
50
50
  }
51
51
 
52
52
  .itemIcon {
@@ -55,7 +55,7 @@
55
55
 
56
56
  .footer {
57
57
  justify-content: center;
58
- padding: var(--fui-space-3, $fui-space-3);
58
+ padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
59
59
  }
60
60
 
61
61
  .collapseToggle {
@@ -92,7 +92,7 @@
92
92
  display: flex;
93
93
  align-items: center;
94
94
  gap: var(--fui-space-3, $fui-space-3);
95
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-2, $fui-space-2);
95
+ padding: var(--fui-padding-item-sm, $fui-padding-item-sm) var(--fui-padding-item-sm, $fui-padding-item-sm);
96
96
  flex-shrink: 0;
97
97
  height: var(--appshell-header-height, 56px);
98
98
  }
@@ -105,7 +105,7 @@
105
105
  flex: 1;
106
106
  overflow-y: auto;
107
107
  overflow-x: hidden;
108
- padding: var(--fui-space-2, $fui-space-2);
108
+ padding: var(--fui-padding-item-sm, $fui-padding-item-sm);
109
109
  }
110
110
 
111
111
  // ============================================
@@ -121,13 +121,19 @@
121
121
  .sectionHeader {
122
122
  display: flex;
123
123
  align-items: center;
124
- justify-content: space-between;
125
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
124
+ gap: var(--fui-space-2, $fui-space-2);
125
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
126
+ }
127
+
128
+ .sectionTrigger {
129
+ flex: 1;
126
130
  }
127
131
 
128
132
  .sectionLabel {
129
133
  @include helper-text;
130
134
 
135
+ flex: 1;
136
+ min-width: 0;
131
137
  font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
132
138
  text-transform: uppercase;
133
139
  letter-spacing: 0.05em;
@@ -170,11 +176,14 @@
170
176
  // Collapsible section styles (using Collapsible component)
171
177
  .sectionCollapsible {
172
178
  // Override Collapsible trigger styles for sidebar context
173
- :global(.collapsible-trigger),
174
- button[aria-expanded] {
179
+ .sectionTrigger {
175
180
  background: transparent;
181
+ display: flex;
182
+ align-items: center;
176
183
  justify-content: space-between;
177
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
184
+ width: 100%;
185
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
186
+ border-radius: var(--fui-radius-md, $fui-radius-md);
178
187
 
179
188
  &:hover {
180
189
  background-color: var(--fui-bg-hover, $fui-bg-hover);
@@ -199,11 +208,10 @@
199
208
  @include interactive-base;
200
209
  @include text-base;
201
210
 
202
- display: flex;
211
+ display: inline-flex;
203
212
  align-items: center;
204
213
  gap: var(--fui-space-3, $fui-space-3);
205
- width: 100%;
206
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
214
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
207
215
  border-radius: var(--fui-radius-md, $fui-radius-md);
208
216
  color: var(--fui-text-secondary, $fui-text-secondary);
209
217
  text-decoration: none;
@@ -212,6 +220,7 @@
212
220
  &:hover:not(.itemDisabled) {
213
221
  background-color: var(--fui-bg-hover, $fui-bg-hover);
214
222
  color: var(--fui-text-primary, $fui-text-primary);
223
+ text-decoration: none;
215
224
  }
216
225
 
217
226
  &:active:not(.itemDisabled) {
@@ -225,8 +234,9 @@
225
234
  color: var(--fui-text-inverse, $fui-text-inverse);
226
235
 
227
236
  &:hover {
228
- background-color: var(--fui-color-accent-active, $fui-color-accent-active);
229
- color: var(--fui-text-inverse, $fui-text-inverse);
237
+ background-color: var(--fui-color-accent-hover, $fui-color-accent-hover) !important;
238
+ color: var(--fui-text-inverse, $fui-text-inverse) !important;
239
+ text-decoration: none;
230
240
  }
231
241
 
232
242
  .itemIcon {
@@ -270,7 +280,7 @@
270
280
  display: flex;
271
281
  align-items: center;
272
282
  justify-content: center;
273
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
283
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
274
284
  font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
275
285
  font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
276
286
  // Use accent-hover for WCAG AA contrast (4.5:1 minimum)
@@ -341,10 +351,9 @@
341
351
  @include interactive-base;
342
352
  @include text-base;
343
353
 
344
- display: flex;
354
+ display: inline-flex;
345
355
  align-items: center;
346
- width: 100%;
347
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
356
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
348
357
  border-radius: var(--fui-radius-md, $fui-radius-md);
349
358
  color: var(--fui-text-secondary, $fui-text-secondary);
350
359
  text-decoration: none;
@@ -369,6 +378,7 @@
369
378
  &:hover:not(.subItemDisabled) {
370
379
  background-color: var(--fui-bg-hover, $fui-bg-hover);
371
380
  color: var(--fui-text-primary, $fui-text-primary);
381
+ text-decoration: none;
372
382
  }
373
383
 
374
384
  &:active:not(.subItemDisabled) {
@@ -399,7 +409,7 @@
399
409
  display: flex;
400
410
  flex-direction: column;
401
411
  gap: var(--fui-space-2, $fui-space-2);
402
- padding: var(--fui-space-4, $fui-space-4);
412
+ padding: var(--fui-padding-container-md, $fui-padding-container-md);
403
413
  margin-top: auto;
404
414
  flex-shrink: 0;
405
415
  }
@@ -563,14 +573,14 @@
563
573
  display: flex;
564
574
  flex-direction: column;
565
575
  gap: var(--fui-space-2, $fui-space-2);
566
- padding: var(--fui-space-2, $fui-space-2);
576
+ padding: var(--fui-padding-item-sm, $fui-padding-item-sm);
567
577
  }
568
578
 
569
579
  .skeletonItem {
570
580
  display: flex;
571
581
  align-items: center;
572
582
  gap: var(--fui-space-3, $fui-space-3);
573
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
583
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
574
584
  min-height: $fui-sidebar-item-height;
575
585
  }
576
586
 
@@ -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 { useFocusTrap } from '../../utils/a11y';
6
7
  // Import globals to ensure CSS variables are defined
7
8
  import '../../styles/globals.scss';
8
9
 
@@ -271,6 +272,7 @@ interface SidebarContextValue {
271
272
  collapsedWidth: string;
272
273
  collapsible: SidebarCollapsible;
273
274
  toggleSidebar: () => void;
275
+ sidebarId: string;
274
276
  }
275
277
 
276
278
  const SidebarContext = React.createContext<SidebarContextValue | null>(null);
@@ -294,6 +296,7 @@ function useSidebar() {
294
296
  collapsedWidth: '64px',
295
297
  collapsible: 'icon' as SidebarCollapsible,
296
298
  toggleSidebar: () => {},
299
+ sidebarId: 'sidebar',
297
300
  state: 'expanded' as 'expanded' | 'collapsed' | 'open' | 'closed',
298
301
  };
299
302
  }
@@ -379,6 +382,7 @@ function SidebarProvider({
379
382
  enableKeyboardShortcut = true,
380
383
  }: SidebarProviderProps) {
381
384
  const isMobile = useIsMobile();
385
+ const sidebarId = React.useId();
382
386
 
383
387
  const [collapsed, setCollapsed] = useControllableState(
384
388
  controlledCollapsed,
@@ -456,6 +460,7 @@ function SidebarProvider({
456
460
  collapsedWidth,
457
461
  collapsible,
458
462
  toggleSidebar,
463
+ sidebarId,
459
464
  };
460
465
 
461
466
  return (
@@ -479,6 +484,7 @@ function SidebarRoot({
479
484
  collapsible = 'icon',
480
485
  className,
481
486
  style: styleProp,
487
+ 'aria-label': ariaLabel,
482
488
  ...htmlProps
483
489
  }: SidebarProps) {
484
490
  // Check if we're inside a SidebarProvider
@@ -506,6 +512,11 @@ function SidebarRoot({
506
512
  const resolvedWidth = existingContext ? existingContext.width : width;
507
513
  const resolvedCollapsedWidth = existingContext ? existingContext.collapsedWidth : collapsedWidth;
508
514
  const resolvedCollapsible = existingContext ? existingContext.collapsible : collapsible;
515
+ const sidebarId = React.useId();
516
+ const resolvedSidebarId = existingContext ? existingContext.sidebarId : sidebarId;
517
+ const sidebarRef = React.useRef<HTMLElement>(null);
518
+
519
+ useFocusTrap(sidebarRef, isMobile && open);
509
520
 
510
521
  const toggleSidebar = React.useCallback(() => {
511
522
  if (resolvedCollapsible === 'none') return;
@@ -558,6 +569,7 @@ function SidebarRoot({
558
569
  collapsedWidth: resolvedCollapsedWidth,
559
570
  collapsible: resolvedCollapsible,
560
571
  toggleSidebar,
572
+ sidebarId: resolvedSidebarId,
561
573
  };
562
574
 
563
575
  const isCollapsedForStyle = resolvedCollapsible === 'icon' && collapsed;
@@ -580,9 +592,15 @@ function SidebarRoot({
580
592
 
581
593
  const content = (
582
594
  <aside
595
+ ref={sidebarRef}
596
+ id={resolvedSidebarId}
583
597
  {...htmlProps}
584
598
  className={classes}
585
599
  style={style}
600
+ role={isMobile ? 'dialog' : undefined}
601
+ aria-modal={isMobile && open ? true : undefined}
602
+ aria-hidden={isMobile && !open ? true : undefined}
603
+ aria-label={isMobile ? (ariaLabel || 'Sidebar navigation') : ariaLabel}
586
604
  data-state={isMobile ? (open ? 'open' : 'closed') : (collapsed ? 'collapsed' : 'expanded')}
587
605
  data-position={resolvedPosition}
588
606
  data-collapsible={resolvedCollapsible}
@@ -663,13 +681,15 @@ function SidebarSection({
663
681
  return (
664
682
  <div className={classes} role="group" aria-label={label}>
665
683
  <Collapsible defaultOpen={defaultOpen} className={styles.sectionCollapsible}>
666
- <Collapsible.Trigger
667
- className={styles.sectionHeader}
668
- chevronPosition="end"
669
- >
670
- <span className={styles.sectionLabel}>{label}</span>
671
- {showAction && <span className={styles.sectionActionWrapper} onClick={(e) => e.stopPropagation()}>{action}</span>}
672
- </Collapsible.Trigger>
684
+ <div className={styles.sectionHeader}>
685
+ <Collapsible.Trigger
686
+ className={styles.sectionTrigger}
687
+ chevronPosition="end"
688
+ >
689
+ <span className={styles.sectionLabel}>{label}</span>
690
+ </Collapsible.Trigger>
691
+ {showAction && <div className={styles.sectionActionWrapper}>{action}</div>}
692
+ </div>
673
693
  <Collapsible.Content className={styles.sectionContent}>
674
694
  <ul className={styles.sectionList}>
675
695
  {children}
@@ -887,7 +907,7 @@ function SidebarFooter({ children, className }: SidebarFooterProps) {
887
907
  }
888
908
 
889
909
  function SidebarTrigger({ children, 'aria-label': ariaLabel = 'Toggle navigation', className }: SidebarTriggerProps) {
890
- const { open, setOpen, isMobile } = useSidebarContext();
910
+ const { open, setOpen, isMobile, sidebarId } = useSidebarContext();
891
911
 
892
912
  // Only render trigger on mobile
893
913
  if (!isMobile) {
@@ -903,6 +923,7 @@ function SidebarTrigger({ children, 'aria-label': ariaLabel = 'Toggle navigation
903
923
  onClick={() => setOpen(!open)}
904
924
  aria-label={ariaLabel}
905
925
  aria-expanded={open}
926
+ aria-controls={sidebarId}
906
927
  >
907
928
  {children || (open ? <CloseIcon /> : <MenuIcon />)}
908
929
  </button>
@@ -989,6 +1010,7 @@ function SidebarMenuSkeleton({
989
1010
  const isCollapsed = collapsed && !isMobile;
990
1011
 
991
1012
  const classes = [styles.menuSkeleton, className].filter(Boolean).join(' ');
1013
+ const labelWidths = ['64%', '72%', '68%', '79%', '74%', '66%', '83%', '70%'];
992
1014
 
993
1015
  return (
994
1016
  <div className={classes} aria-hidden="true">
@@ -999,7 +1021,7 @@ function SidebarMenuSkeleton({
999
1021
  <Skeleton
1000
1022
  variant="text"
1001
1023
  className={styles.skeletonLabel}
1002
- width={`${60 + Math.random() * 30}%`}
1024
+ width={labelWidths[i % labelWidths.length]}
1003
1025
  />
1004
1026
  )}
1005
1027
  </div>
@@ -16,6 +16,12 @@ export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
16
16
  valueSuffix?: string;
17
17
  disabled?: boolean;
18
18
  name?: string;
19
+ /** Accessible label when visible label is omitted */
20
+ 'aria-label'?: string;
21
+ /** Accessible labelled-by relationship */
22
+ 'aria-labelledby'?: string;
23
+ /** Accessible described-by relationship */
24
+ 'aria-describedby'?: string;
19
25
  }
20
26
 
21
27
  export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
@@ -34,6 +40,9 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
34
40
  className,
35
41
  name,
36
42
  id,
43
+ 'aria-label': ariaLabel,
44
+ 'aria-labelledby': ariaLabelledBy,
45
+ 'aria-describedby': ariaDescribedBy,
37
46
  ...htmlProps
38
47
  },
39
48
  ref
@@ -48,8 +57,6 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
48
57
  onChange?.(val);
49
58
  };
50
59
 
51
- const sliderValue = value !== undefined ? [value] : (defaultValue !== undefined ? [defaultValue] : undefined);
52
-
53
60
  return (
54
61
  <Field.Root {...htmlProps} disabled={disabled} className={[styles.wrapper, className].filter(Boolean).join(' ')}>
55
62
  {(label || showValue) && (
@@ -64,7 +71,7 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
64
71
  )}
65
72
  <BaseSlider.Root
66
73
  ref={ref}
67
- value={sliderValue}
74
+ value={value !== undefined ? [value] : undefined}
68
75
  defaultValue={defaultValue !== undefined ? [defaultValue] : undefined}
69
76
  onValueChange={handleChange}
70
77
  min={min}
@@ -73,6 +80,9 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
73
80
  disabled={disabled}
74
81
  name={name}
75
82
  id={id}
83
+ aria-label={ariaLabel || (label ? undefined : 'Slider')}
84
+ aria-labelledby={ariaLabelledBy}
85
+ aria-describedby={ariaDescribedBy}
76
86
  className={styles.root}
77
87
  >
78
88
  <BaseSlider.Control className={styles.control}>
@@ -33,7 +33,7 @@
33
33
  .sm {
34
34
  .th,
35
35
  .td {
36
- padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
36
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
37
37
  font-size: var(--fui-font-size-xs, $fui-font-size-xs);
38
38
  }
39
39
  }
@@ -41,7 +41,7 @@
41
41
  .md {
42
42
  .th,
43
43
  .td {
44
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
44
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
45
45
  font-size: var(--fui-font-size-sm, $fui-font-size-sm);
46
46
  }
47
47
  }
@@ -51,7 +51,6 @@
51
51
  position: sticky;
52
52
  top: 0;
53
53
  z-index: 1;
54
- background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
55
54
  }
56
55
 
57
56
  .headerRow {
@@ -83,16 +82,21 @@
83
82
 
84
83
  // Sortable header cell (for focus styles)
85
84
  .thSortable {
86
- cursor: pointer;
87
-
88
- &:focus-visible {
89
- @include focus-ring;
90
- outline-offset: -2px;
91
- }
85
+ padding: 0;
92
86
  }
93
87
 
94
- .sortable {
95
- cursor: pointer;
88
+ .sortButton {
89
+ @include button-reset;
90
+ @include interactive-base;
91
+
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ gap: var(--fui-space-1, $fui-space-1);
96
+ width: 100%;
97
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
98
+ color: inherit;
99
+ text-align: left;
96
100
  transition: color var(--fui-transition-fast, $fui-transition-fast);
97
101
 
98
102
  &:hover {
@@ -106,15 +110,13 @@
106
110
  color: var(--fui-text-tertiary, $fui-text-tertiary);
107
111
  flex-shrink: 0;
108
112
 
109
- .sortable:hover & {
113
+ .sortButton:hover & {
110
114
  color: var(--fui-text-secondary, $fui-text-secondary);
111
115
  }
112
116
  }
113
117
 
114
118
  // Body
115
- .tbody {
116
- background-color: var(--fui-bg-primary, $fui-bg-primary);
117
- }
119
+ .tbody {}
118
120
 
119
121
  .row {
120
122
  border-bottom: 1px solid var(--fui-border, $fui-border);
@@ -123,6 +125,10 @@
123
125
  &:last-child {
124
126
  border-bottom: none;
125
127
  }
128
+
129
+ &:hover {
130
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
131
+ }
126
132
  }
127
133
 
128
134
  .clickable {
@@ -132,6 +138,10 @@
132
138
  background-color: var(--fui-bg-hover, $fui-bg-hover);
133
139
  }
134
140
 
141
+ &:focus-visible {
142
+ @include focus-ring;
143
+ }
144
+
135
145
  &:active {
136
146
  background-color: var(--fui-bg-active, $fui-bg-active);
137
147
  }
@@ -219,7 +229,7 @@
219
229
  border-bottom-width: 2px;
220
230
  }
221
231
 
222
- .thSortable:focus-visible {
232
+ .sortButton:focus-visible {
223
233
  outline-width: 3px;
224
234
  }
225
235
  }
@@ -233,7 +243,7 @@
233
243
  transition: none;
234
244
  }
235
245
 
236
- .sortable {
246
+ .sortButton {
237
247
  transition: none;
238
248
  }
239
249
  }