@fragments-sdk/ui 0.8.2 → 0.8.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 (63) hide show
  1. package/README.md +13 -3
  2. package/fragments.json +1 -1
  3. package/package.json +4 -2
  4. package/src/blocks/ChatInterface.block.ts +3 -3
  5. package/src/blocks/DashboardLayout.block.ts +1 -1
  6. package/src/blocks/DashboardPage.block.ts +4 -3
  7. package/src/blocks/EmptyState.block.ts +4 -4
  8. package/src/blocks/SettingsPanel.block.ts +4 -4
  9. package/src/components/Accordion/Accordion.fragment.tsx +67 -1
  10. package/src/components/Alert/Alert.fragment.tsx +69 -1
  11. package/src/components/Alert/Alert.module.scss +7 -3
  12. package/src/components/AppShell/AppShell.fragment.tsx +341 -101
  13. package/src/components/AppShell/AppShell.module.scss +18 -19
  14. package/src/components/AppShell/index.tsx +37 -12
  15. package/src/components/Avatar/Avatar.fragment.tsx +35 -2
  16. package/src/components/Badge/Badge.fragment.tsx +47 -9
  17. package/src/components/Badge/Badge.module.scss +1 -0
  18. package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +1 -1
  19. package/src/components/Breadcrumbs/Breadcrumbs.module.scss +3 -0
  20. package/src/components/Button/Button.fragment.tsx +1 -1
  21. package/src/components/Checkbox/Checkbox.fragment.tsx +4 -4
  22. package/src/components/Checkbox/Checkbox.module.scss +7 -7
  23. package/src/components/Checkbox/index.tsx +6 -1
  24. package/src/components/Chip/Chip.fragment.tsx +1 -1
  25. package/src/components/CodeBlock/CodeBlock.fragment.tsx +10 -2
  26. package/src/components/CodeBlock/CodeBlock.module.scss +48 -11
  27. package/src/components/CodeBlock/CodeBlock.test.tsx +51 -1
  28. package/src/components/CodeBlock/index.tsx +221 -3
  29. package/src/components/ColorPicker/ColorPicker.fragment.tsx +1 -1
  30. package/src/components/ColorPicker/ColorPicker.module.scss +8 -7
  31. package/src/components/Combobox/Combobox.fragment.tsx +1 -1
  32. package/src/components/Combobox/Combobox.module.scss +1 -3
  33. package/src/components/Field/index.tsx +1 -1
  34. package/src/components/Form/Form.fragment.tsx +4 -4
  35. package/src/components/Header/Header.fragment.tsx +8 -8
  36. package/src/components/Input/Input.fragment.tsx +1 -1
  37. package/src/components/Message/Message.module.scss +3 -3
  38. package/src/components/Popover/Popover.fragment.tsx +1 -1
  39. package/src/components/Popover/Popover.module.scss +1 -3
  40. package/src/components/Prompt/Prompt.module.scss +6 -19
  41. package/src/components/Prompt/Prompt.test.tsx +8 -0
  42. package/src/components/Prompt/index.tsx +12 -1
  43. package/src/components/RadioGroup/RadioGroup.fragment.tsx +4 -3
  44. package/src/components/RadioGroup/RadioGroup.module.scss +9 -9
  45. package/src/components/RadioGroup/index.tsx +5 -1
  46. package/src/components/Sidebar/Sidebar.module.scss +9 -2
  47. package/src/components/Sidebar/Sidebar.test.tsx +6 -0
  48. package/src/components/Sidebar/index.tsx +4 -1
  49. package/src/components/Slider/Slider.fragment.tsx +2 -2
  50. package/src/components/Slider/Slider.module.scss +2 -0
  51. package/src/components/Switch/index.ts +1 -0
  52. package/src/components/Theme/Theme.fragment.tsx +16 -0
  53. package/src/components/Theme/ThemeToggle.module.scss +4 -3
  54. package/src/components/Toast/Toast.fragment.tsx +1 -0
  55. package/src/components/Toast/Toast.module.scss +9 -4
  56. package/src/components/Toggle/Toggle.fragment.tsx +32 -32
  57. package/src/components/Toggle/Toggle.module.scss +33 -26
  58. package/src/components/Toggle/Toggle.test.tsx +10 -10
  59. package/src/components/Toggle/index.tsx +23 -15
  60. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +2 -2
  61. package/src/index.ts +6 -1
  62. package/src/tokens/_mixins.scss +14 -0
  63. package/src/tokens/_variables.scss +12 -6
@@ -3,7 +3,7 @@
3
3
 
4
4
  .root {
5
5
  display: flex;
6
- align-items: flex-start;
6
+ align-items: center;
7
7
  gap: var(--fui-space-3, $fui-space-3);
8
8
  cursor: pointer;
9
9
  font-family: var(--fui-font-sans, $fui-font-sans);
@@ -14,21 +14,30 @@
14
14
  }
15
15
  }
16
16
 
17
+ .rootWithDescription {
18
+ align-items: flex-start;
19
+
20
+ .track {
21
+ margin-top: var(--fui-space-0-5, $fui-space-0-5);
22
+ }
23
+ }
24
+
17
25
  .track {
18
26
  @include button-reset;
19
27
  @include interactive-base;
28
+ @include min-target-size;
20
29
 
21
30
  position: relative;
22
31
  flex-shrink: 0;
23
32
  border-radius: var(--fui-radius-full, $fui-radius-full);
24
- margin-top: var(--fui-space-0-5, $fui-space-0-5);
25
33
  background-color: var(--fui-border-strong, $fui-border-strong);
34
+ transition: background-color var(--fui-transition-normal, $fui-transition-normal);
26
35
 
27
- &[data-checked] {
36
+ .root[data-checked] & {
28
37
  background-color: var(--fui-color-accent, $fui-color-accent);
29
38
  }
30
39
 
31
- &:hover:not(:disabled) {
40
+ .root:not([data-disabled]) &:hover {
32
41
  opacity: 0.9;
33
42
  }
34
43
  }
@@ -36,43 +45,41 @@
36
45
  .trackSm {
37
46
  width: var(--fui-toggle-width-sm, $fui-toggle-width-sm);
38
47
  height: var(--fui-toggle-height-sm, $fui-toggle-height-sm);
48
+ --_toggle-thumb-size: calc(var(--fui-toggle-height-sm, #{$fui-toggle-height-sm}) - 0.5rem);
49
+ --_toggle-inset: calc((var(--fui-toggle-height-sm, #{$fui-toggle-height-sm}) - var(--_toggle-thumb-size)) / 2);
50
+ --_toggle-translate: calc(
51
+ var(--fui-toggle-width-sm, #{$fui-toggle-width-sm}) -
52
+ var(--_toggle-thumb-size) -
53
+ (var(--_toggle-inset) * 2)
54
+ );
39
55
  }
40
56
 
41
57
  .trackMd {
42
58
  width: var(--fui-toggle-width-md, $fui-toggle-width-md);
43
59
  height: var(--fui-toggle-height-md, $fui-toggle-height-md);
60
+ --_toggle-thumb-size: calc(var(--fui-toggle-height-md, #{$fui-toggle-height-md}) - 0.5rem);
61
+ --_toggle-inset: calc((var(--fui-toggle-height-md, #{$fui-toggle-height-md}) - var(--_toggle-thumb-size)) / 2);
62
+ --_toggle-translate: calc(
63
+ var(--fui-toggle-width-md, #{$fui-toggle-width-md}) -
64
+ var(--_toggle-thumb-size) -
65
+ (var(--_toggle-inset) * 2)
66
+ );
44
67
  }
45
68
 
46
69
  .thumb {
47
70
  position: absolute;
48
- top: $fui-toggle-thumb-offset;
49
- left: $fui-toggle-thumb-offset;
71
+ top: var(--_toggle-inset, $fui-toggle-thumb-offset);
72
+ left: var(--_toggle-inset, $fui-toggle-thumb-offset);
73
+ width: var(--_toggle-thumb-size, $fui-toggle-thumb-md);
74
+ height: var(--_toggle-thumb-size, $fui-toggle-thumb-md);
50
75
  border-radius: 50%;
51
76
  background-color: var(--fui-bg-primary, $fui-bg-primary);
52
77
  box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
53
78
  transition: transform var(--fui-transition-normal, $fui-transition-normal);
54
79
 
55
- [data-checked] > & {
80
+ .root[data-checked] & {
56
81
  // Move thumb to the right when checked
57
- transform: translateX(calc(100%));
58
- }
59
- }
60
-
61
- .thumbSm {
62
- width: $fui-toggle-thumb-sm;
63
- height: $fui-toggle-thumb-sm;
64
-
65
- [data-checked] > & {
66
- transform: translateX($fui-toggle-thumb-sm);
67
- }
68
- }
69
-
70
- .thumbMd {
71
- width: $fui-toggle-thumb-md;
72
- height: $fui-toggle-thumb-md;
73
-
74
- [data-checked] > & {
75
- transform: translateX($fui-toggle-thumb-md);
82
+ transform: translateX(var(--_toggle-translate, 0));
76
83
  }
77
84
  }
78
85
 
@@ -1,49 +1,49 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
3
- import { Toggle } from './index';
3
+ import { Switch } from './index';
4
4
 
5
- describe('Toggle', () => {
5
+ describe('Switch', () => {
6
6
  it('renders a switch role', () => {
7
- render(<Toggle aria-label="Dark mode" />);
7
+ render(<Switch aria-label="Dark mode" />);
8
8
  expect(screen.getByRole('switch')).toBeInTheDocument();
9
9
  });
10
10
 
11
11
  it('is unchecked by default', () => {
12
- render(<Toggle aria-label="Dark mode" />);
12
+ render(<Switch aria-label="Dark mode" />);
13
13
  expect(screen.getByRole('switch')).not.toBeChecked();
14
14
  });
15
15
 
16
16
  it('renders as checked when checked prop is true', () => {
17
- render(<Toggle aria-label="Dark mode" checked onChange={() => {}} />);
17
+ render(<Switch aria-label="Dark mode" checked onChange={() => {}} />);
18
18
  expect(screen.getByRole('switch')).toBeChecked();
19
19
  });
20
20
 
21
21
  it('renders label text', () => {
22
- render(<Toggle label="Dark mode" />);
22
+ render(<Switch label="Dark mode" />);
23
23
  expect(screen.getByText('Dark mode')).toBeInTheDocument();
24
24
  });
25
25
 
26
26
  it('renders description text', () => {
27
- render(<Toggle label="Notifications" description="Enable push alerts" />);
27
+ render(<Switch label="Notifications" description="Enable push alerts" />);
28
28
  expect(screen.getByText('Enable push alerts')).toBeInTheDocument();
29
29
  });
30
30
 
31
31
  it('disables the switch', () => {
32
- render(<Toggle aria-label="Dark mode" disabled />);
32
+ render(<Switch aria-label="Dark mode" disabled />);
33
33
  expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true');
34
34
  });
35
35
 
36
36
  it('calls onChange with the new value on click', async () => {
37
37
  const handleChange = vi.fn();
38
38
  const user = userEvent.setup();
39
- render(<Toggle aria-label="Dark mode" onChange={handleChange} />);
39
+ render(<Switch aria-label="Dark mode" onChange={handleChange} />);
40
40
  await user.click(screen.getByRole('switch'));
41
41
  expect(handleChange).toHaveBeenCalled();
42
42
  expect(handleChange.mock.calls[0][0]).toBe(true);
43
43
  });
44
44
 
45
45
  it('has no accessibility violations', async () => {
46
- const { container } = render(<Toggle aria-label="Accessible toggle" />);
46
+ const { container } = render(<Switch aria-label="Accessible switch" />);
47
47
  await expectNoA11yViolations(container);
48
48
  });
49
49
  });
@@ -1,10 +1,10 @@
1
1
  import * as React from 'react';
2
- import { Switch } from '@base-ui/react/switch';
2
+ import { Switch as BaseSwitch } from '@base-ui/react/switch';
3
3
  import styles from './Toggle.module.scss';
4
4
  // Import globals to ensure CSS variables are defined
5
5
  import '../../styles/globals.scss';
6
6
 
7
- export interface ToggleProps {
7
+ export interface SwitchProps {
8
8
  checked?: boolean;
9
9
  defaultChecked?: boolean;
10
10
  onChange?: (checked: boolean) => void;
@@ -20,8 +20,10 @@ export interface ToggleProps {
20
20
  'aria-describedby'?: string;
21
21
  }
22
22
 
23
- const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
24
- function Toggle(
23
+ export type ToggleProps = SwitchProps;
24
+
25
+ const SwitchRoot = React.forwardRef<HTMLButtonElement, SwitchProps>(
26
+ function Switch(
25
27
  {
26
28
  checked,
27
29
  defaultChecked,
@@ -44,10 +46,7 @@ const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
44
46
  size === 'sm' ? styles.trackSm : styles.trackMd,
45
47
  ].join(' ');
46
48
 
47
- const thumbClasses = [
48
- styles.thumb,
49
- size === 'sm' ? styles.thumbSm : styles.thumbMd,
50
- ].join(' ');
49
+ const thumbClasses = styles.thumb;
51
50
 
52
51
  const labelClasses = [styles.label, size === 'sm' && styles.labelSm]
53
52
  .filter(Boolean)
@@ -60,10 +59,16 @@ const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
60
59
  .filter(Boolean)
61
60
  .join(' ');
62
61
 
63
- const rootClasses = [styles.root, className].filter(Boolean).join(' ');
62
+ const rootClasses = [
63
+ styles.root,
64
+ description && styles.rootWithDescription,
65
+ className,
66
+ ]
67
+ .filter(Boolean)
68
+ .join(' ');
64
69
 
65
70
  return (
66
- <Switch.Root
71
+ <BaseSwitch.Root
67
72
  ref={ref}
68
73
  id={id}
69
74
  checked={checked}
@@ -76,9 +81,9 @@ const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
76
81
  aria-labelledby={ariaLabelledBy}
77
82
  aria-describedby={ariaDescribedBy}
78
83
  >
79
- <Switch.Thumb className={trackClasses}>
84
+ <BaseSwitch.Thumb className={trackClasses}>
80
85
  <span className={thumbClasses} aria-hidden="true" />
81
- </Switch.Thumb>
86
+ </BaseSwitch.Thumb>
82
87
 
83
88
  {(label || description) && (
84
89
  <div className={styles.content}>
@@ -86,11 +91,14 @@ const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
86
91
  {description && <span className={descClasses}>{description}</span>}
87
92
  </div>
88
93
  )}
89
- </Switch.Root>
94
+ </BaseSwitch.Root>
90
95
  );
91
96
  }
92
97
  );
93
98
 
94
- export const Toggle = Object.assign(ToggleRoot, {
95
- Root: ToggleRoot,
99
+ export const Switch = Object.assign(SwitchRoot, {
100
+ Root: SwitchRoot,
96
101
  });
102
+
103
+ /** @deprecated Use `Switch` instead. */
104
+ export const Toggle = Switch;
@@ -115,7 +115,7 @@ export default defineFragment({
115
115
  'Multiple selections allowed (use Checkbox group)',
116
116
  'Many options (use Select or RadioGroup)',
117
117
  'Navigation between pages (use Tabs)',
118
- 'On/off toggle (use Toggle component)',
118
+ 'On/off toggle (use Switch component)',
119
119
  ],
120
120
  guidelines: [
121
121
  'Keep options to 2-5 items for clarity',
@@ -170,7 +170,7 @@ export default defineFragment({
170
170
  relations: [
171
171
  { component: 'RadioGroup', relationship: 'alternative', note: 'RadioGroup for form-style single selection' },
172
172
  { component: 'Tabs', relationship: 'alternative', note: 'Tabs for content panel switching' },
173
- { component: 'Toggle', relationship: 'sibling', note: 'Toggle for single on/off control' },
173
+ { component: 'Switch', relationship: 'sibling', note: 'Switch for single on/off control' },
174
174
  ],
175
175
 
176
176
  contract: {
package/src/index.ts CHANGED
@@ -23,7 +23,12 @@ export {
23
23
  type CardBodyProps,
24
24
  type CardFooterProps,
25
25
  } from './components/Card';
26
- export { Toggle, type ToggleProps } from './components/Toggle';
26
+ export {
27
+ Switch,
28
+ type SwitchProps,
29
+ Switch as Toggle,
30
+ type SwitchProps as ToggleProps,
31
+ } from './components/Switch';
27
32
  export {
28
33
  Alert,
29
34
  AlertRoot,
@@ -47,6 +47,20 @@
47
47
  }
48
48
  }
49
49
 
50
+ // Enforce minimum pointer target dimensions for interactive controls.
51
+ @mixin min-target-size($size: var(--fui-target-size-min, #{$fui-target-size-min})) {
52
+ min-width: $size;
53
+ min-height: $size;
54
+ }
55
+
56
+ // Centered interactive hit area with minimum dimensions.
57
+ @mixin touch-target($size: var(--fui-target-size-min, #{$fui-target-size-min})) {
58
+ @include min-target-size($size);
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ }
63
+
50
64
  // Typography base
51
65
  @mixin text-base {
52
66
  font-family: var(--fui-font-sans, #{$fui-font-sans});
@@ -270,11 +270,15 @@ $fui-touch-sm: computed.$computed-touch-sm !default;
270
270
  $fui-touch-md: computed.$computed-touch-md !default;
271
271
  $fui-touch-lg: computed.$computed-touch-lg !default;
272
272
 
273
+ // Minimum interactive target sizes (WCAG 2.2 SC 2.5.8)
274
+ $fui-target-size-min: 1.714rem !default; // 24px
275
+ $fui-target-size-comfortable: $fui-touch-md !default;
276
+
273
277
  // Toggle/Switch
274
278
  $fui-toggle-width-sm: 2.286rem !default; // 32px
275
279
  $fui-toggle-width-md: 2.857rem !default; // 40px
276
- $fui-toggle-height-sm: 1.286rem !default; // 18px
277
- $fui-toggle-height-md: 1.571rem !default; // 22px
280
+ $fui-toggle-height-sm: $fui-target-size-min !default; // 24px
281
+ $fui-toggle-height-md: 2rem !default; // 28px
278
282
  $fui-toggle-thumb-sm: 1rem !default; // 14px
279
283
  $fui-toggle-thumb-md: 1.286rem !default; // 18px
280
284
  $fui-toggle-thumb-offset: 2px !default; // Thumb inset from track edge
@@ -308,7 +312,7 @@ $fui-sidebar-rail-indicator-height-hover: 4rem !default; // 56px
308
312
 
309
313
  // Slider
310
314
  $fui-slider-track-height: 4px !default;
311
- $fui-slider-thumb-size: $fui-icon-md !default; // 16px
315
+ $fui-slider-thumb-size: $fui-target-size-min !default;
312
316
  $fui-slider-thumb-border: 2px !default;
313
317
 
314
318
  // EmptyState content max-widths
@@ -323,8 +327,8 @@ $fui-emptystate-icon-lg: 2.857rem !default; // 40px
323
327
 
324
328
  // ColorChip/ColorPicker
325
329
  $fui-colorpicker-size: 180px !default;
326
- $fui-colorpicker-hue-height: 10px !default;
327
- $fui-colorpicker-pointer-size: 1rem !default; // 14px
330
+ $fui-colorpicker-hue-height: $fui-target-size-min !default;
331
+ $fui-colorpicker-pointer-size: $fui-target-size-min !default;
328
332
  $fui-colorpicker-pointer-border: 2px !default;
329
333
 
330
334
  // AppShell Layout
@@ -336,7 +340,7 @@ $fui-header-z-index: 40 !default;
336
340
 
337
341
  // ThemeToggle sizes (derived from density)
338
342
  $fui-theme-toggle-sm-width: 26px !default;
339
- $fui-theme-toggle-sm-height: 22px !default;
343
+ $fui-theme-toggle-sm-height: 24px !default;
340
344
  $fui-theme-toggle-sm-icon: 12px !default;
341
345
  $fui-theme-toggle-md-width: 32px !default;
342
346
  $fui-theme-toggle-md-height: 28px !default;
@@ -528,6 +532,8 @@ $fui-dark-hero-gradient-color: rgba(120, 119, 198, 0.25) !default;
528
532
  --fui-input-height: #{$fui-input-height};
529
533
  --fui-input-height-sm: #{$fui-input-height-sm};
530
534
  --fui-input-height-lg: #{$fui-input-height-lg};
535
+ --fui-target-size-min: #{$fui-target-size-min};
536
+ --fui-target-size-comfortable: #{$fui-target-size-comfortable};
531
537
 
532
538
  // AppShell Layout
533
539
  --fui-appshell-header-height: #{$fui-appshell-header-height};