@boostdev/design-system-components 0.1.17 → 1.0.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 (137) hide show
  1. package/AGENTS.md +65 -27
  2. package/README.md +64 -29
  3. package/dist/client.cjs +412 -241
  4. package/dist/client.css +1380 -1186
  5. package/dist/client.d.cts +43 -16
  6. package/dist/client.d.ts +43 -16
  7. package/dist/client.js +414 -245
  8. package/dist/index.cjs +412 -241
  9. package/dist/index.css +1380 -1186
  10. package/dist/index.d.cts +43 -16
  11. package/dist/index.d.ts +43 -16
  12. package/dist/index.js +414 -245
  13. package/package.json +9 -4
  14. package/src/components/interaction/Button/Button.mdx +2 -5
  15. package/src/components/interaction/Button/Button.module.css +74 -44
  16. package/src/components/interaction/Button/Button.stories.tsx +4 -4
  17. package/src/components/interaction/Button/Button.tsx +5 -5
  18. package/src/components/interaction/Button/index.ts +1 -0
  19. package/src/components/interaction/Command/Command.module.css +53 -38
  20. package/src/components/interaction/Command/Command.spec.tsx +24 -0
  21. package/src/components/interaction/Command/Command.tsx +5 -0
  22. package/src/components/interaction/Dialog/Dialog.mdx +2 -2
  23. package/src/components/interaction/Dialog/Dialog.module.css +20 -10
  24. package/src/components/interaction/Dialog/Dialog.spec.tsx +19 -0
  25. package/src/components/interaction/Dialog/Dialog.tsx +22 -0
  26. package/src/components/interaction/Drawer/Drawer.module.css +13 -11
  27. package/src/components/interaction/Drawer/Drawer.spec.tsx +30 -0
  28. package/src/components/interaction/Drawer/Drawer.tsx +7 -3
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +24 -19
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
  31. package/src/components/interaction/Popover/Popover.module.css +17 -12
  32. package/src/components/interaction/Popover/Popover.spec.tsx +25 -4
  33. package/src/components/interaction/Popover/Popover.stories.tsx +1 -1
  34. package/src/components/interaction/Popover/Popover.tsx +5 -2
  35. package/src/components/interaction/Rating/Rating.module.css +3 -3
  36. package/src/components/interaction/Toast/Toast.module.css +19 -19
  37. package/src/components/interaction/Toast/Toast.tsx +1 -1
  38. package/src/components/interaction/form/Checkbox/Checkbox.mdx +2 -2
  39. package/src/components/interaction/form/Checkbox/Checkbox.module.css +22 -18
  40. package/src/components/interaction/form/Checkbox/Checkbox.tsx +2 -2
  41. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +29 -0
  42. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +87 -0
  43. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +62 -0
  44. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +46 -0
  45. package/src/components/interaction/form/CheckboxGroup/index.ts +2 -0
  46. package/src/components/interaction/form/Combobox/Combobox.module.css +44 -34
  47. package/src/components/interaction/form/Combobox/Combobox.tsx +0 -1
  48. package/src/components/interaction/form/FileInput/FileInput.module.css +24 -16
  49. package/src/components/interaction/form/FileInput/FileInput.tsx +5 -2
  50. package/src/components/interaction/form/FormInput/FormInput.module.css +21 -16
  51. package/src/components/interaction/form/FormInput/FormInput.tsx +3 -0
  52. package/src/components/interaction/form/NumberInput/NumberInput.module.css +25 -19
  53. package/src/components/interaction/form/NumberInput/NumberInput.tsx +5 -2
  54. package/src/components/interaction/form/Radio/Radio.mdx +3 -3
  55. package/src/components/interaction/form/Radio/Radio.module.css +28 -24
  56. package/src/components/interaction/form/Radio/Radio.stories.tsx +1 -1
  57. package/src/components/interaction/form/Radio/Radio.tsx +2 -2
  58. package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +29 -0
  59. package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +75 -0
  60. package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +64 -0
  61. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +46 -0
  62. package/src/components/interaction/form/RadioGroup/index.ts +2 -0
  63. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +5 -5
  64. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +26 -26
  65. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +4 -0
  66. package/src/components/interaction/form/Select/Select.module.css +24 -19
  67. package/src/components/interaction/form/Select/Select.tsx +3 -0
  68. package/src/components/interaction/form/Slider/Slider.mdx +1 -1
  69. package/src/components/interaction/form/Slider/Slider.module.css +24 -24
  70. package/src/components/interaction/form/Slider/Slider.tsx +1 -0
  71. package/src/components/interaction/form/Switch/Switch.mdx +4 -4
  72. package/src/components/interaction/form/Switch/Switch.module.css +22 -16
  73. package/src/components/interaction/form/Switch/Switch.tsx +1 -1
  74. package/src/components/interaction/form/Textarea/Textarea.module.css +23 -18
  75. package/src/components/interaction/form/Textarea/Textarea.tsx +3 -0
  76. package/src/components/interaction/form/atoms/InputContainer.module.css +2 -2
  77. package/src/components/interaction/form/atoms/Label.module.css +2 -2
  78. package/src/components/interaction/form/atoms/Message.module.css +2 -2
  79. package/src/components/layout/ButtonGroup/ButtonGroup.module.css +2 -2
  80. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +2 -2
  81. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +8 -3
  82. package/src/components/layout/ButtonGroup/index.ts +1 -0
  83. package/src/components/layout/Card/Card.module.css +30 -16
  84. package/src/components/layout/Card/Card.tsx +1 -1
  85. package/src/components/layout/IconWrapper/IconWrapper.mdx +2 -2
  86. package/src/components/layout/IconWrapper/IconWrapper.module.css +5 -5
  87. package/src/components/layout/IconWrapper/IconWrapper.tsx +4 -3
  88. package/src/components/layout/IconWrapper/index.ts +1 -0
  89. package/src/components/layout/SectionHeader/SectionHeader.module.css +23 -23
  90. package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +2 -2
  91. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -2
  92. package/src/components/ui/Accordion/Accordion.module.css +23 -18
  93. package/src/components/ui/Alert/Alert.module.css +31 -27
  94. package/src/components/ui/Avatar/Avatar.module.css +6 -6
  95. package/src/components/ui/Avatar/Avatar.tsx +1 -1
  96. package/src/components/ui/Badge/Badge.mdx +2 -2
  97. package/src/components/ui/Badge/Badge.module.css +15 -15
  98. package/src/components/ui/Breadcrumb/Breadcrumb.module.css +11 -11
  99. package/src/components/ui/Calendar/Calendar.module.css +39 -29
  100. package/src/components/ui/Calendar/Calendar.tsx +6 -2
  101. package/src/components/ui/Carousel/Carousel.module.css +15 -10
  102. package/src/components/ui/Carousel/Carousel.stories.tsx +1 -1
  103. package/src/components/ui/Collapsible/Collapsible.mdx +6 -6
  104. package/src/components/ui/Collapsible/Collapsible.module.css +18 -18
  105. package/src/components/ui/DescriptionList/DescriptionList.module.css +8 -8
  106. package/src/components/ui/Link/Link.module.css +14 -14
  107. package/src/components/ui/Loading/Loading.module.css +8 -2
  108. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +27 -23
  109. package/src/components/ui/Pagination/Pagination.module.css +22 -17
  110. package/src/components/ui/Progress/Progress.mdx +1 -1
  111. package/src/components/ui/Progress/Progress.module.css +10 -10
  112. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +8 -8
  113. package/src/components/ui/Separator/Separator.module.css +1 -1
  114. package/src/components/ui/Separator/Separator.tsx +1 -1
  115. package/src/components/ui/Skeleton/Skeleton.module.css +5 -5
  116. package/src/components/ui/SkipLink/SkipLink.module.css +17 -12
  117. package/src/components/ui/Table/Table.module.css +35 -30
  118. package/src/components/ui/Table/Table.tsx +2 -1
  119. package/src/components/ui/Tabs/Tabs.module.css +17 -17
  120. package/src/components/ui/Tabs/Tabs.tsx +0 -1
  121. package/src/components/ui/Tooltip/Tooltip.module.css +12 -12
  122. package/src/components/ui/Tooltip/Tooltip.spec.tsx +3 -3
  123. package/src/components/ui/Tooltip/Tooltip.tsx +10 -2
  124. package/src/components/ui/Typography/Typography.module.css +18 -18
  125. package/src/css/bdc.css +61 -0
  126. package/src/css/index.css +5 -0
  127. package/src/index.ts +7 -0
  128. package/src/stories/DesignSystem/Borders.mdx +158 -0
  129. package/src/stories/DesignSystem/Colors.mdx +196 -0
  130. package/src/stories/DesignSystem/Elevation.mdx +127 -0
  131. package/src/stories/DesignSystem/Grid.mdx +140 -0
  132. package/src/stories/DesignSystem/Motion.mdx +96 -0
  133. package/src/stories/DesignSystem/Overview.mdx +99 -0
  134. package/src/stories/DesignSystem/Spacing.mdx +74 -0
  135. package/src/stories/DesignSystem/Typography.mdx +110 -0
  136. package/src/stories/Introduction.css +2 -2
  137. package/src/stories/Introduction.mdx +7 -7
@@ -1,4 +1,6 @@
1
1
  import { ReactNode, useEffect, useRef } from 'react';
2
+
3
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
2
4
  import css from './Dialog.module.css';
3
5
  import { cn } from '@boostdev/design-system-foundation';
4
6
 
@@ -11,16 +13,19 @@ interface DialogProps {
11
13
 
12
14
  export function Dialog({ children, isOpen = false, className, onClose }: DialogProps) {
13
15
  const dialogRef = useRef<HTMLDialogElement | null>(null);
16
+ const triggerRef = useRef<Element | null>(null);
14
17
 
15
18
  useEffect(() => {
16
19
  const dialog = dialogRef.current;
17
20
  if (!dialog) return;
18
21
  if (isOpen) {
22
+ triggerRef.current = document.activeElement;
19
23
  dialog.showModal();
20
24
  document.body.style.overflow = 'hidden';
21
25
  } else if (dialog.open) {
22
26
  dialog.close();
23
27
  document.body.style.overflow = '';
28
+ (triggerRef.current as HTMLElement | null)?.focus();
24
29
  }
25
30
  return () => {
26
31
  document.body.style.overflow = '';
@@ -36,12 +41,29 @@ export function Dialog({ children, isOpen = false, className, onClose }: DialogP
36
41
  onClose?.();
37
42
  };
38
43
 
44
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDialogElement>) => {
45
+ if (e.key !== 'Tab') return;
46
+ const dialog = dialogRef.current;
47
+ if (!dialog) return;
48
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
49
+ if (focusable.length === 0) return;
50
+ const first = focusable[0];
51
+ const last = focusable[focusable.length - 1];
52
+ if (e.shiftKey) {
53
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
54
+ } else {
55
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
56
+ }
57
+ };
58
+
39
59
  return (
40
60
  <dialog
41
61
  ref={dialogRef}
42
62
  className={cn(className, css.dialog)}
63
+ aria-modal="true"
43
64
  onClick={handleBackdropClick}
44
65
  onCancel={handleCancel}
66
+ onKeyDown={handleKeyDown}
45
67
  >
46
68
  <form method="dialog" className={css.closeForm}>
47
69
  <button
@@ -14,7 +14,7 @@
14
14
  }
15
15
 
16
16
  .drawer::backdrop {
17
- background-color: var(--drawer_backdrop-color, rgb(0 0 0 / 50%));
17
+ background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
18
18
  backdrop-filter: blur(3px);
19
19
  }
20
20
 
@@ -25,11 +25,11 @@
25
25
  flex-direction: column;
26
26
  width: min(28rem, 90vw);
27
27
  height: 100%;
28
- background-color: var(--drawer_color, var(--color_bg));
29
- color: var(--drawer_on-color, var(--color_on-bg));
30
- box-shadow: var(--shadow_xl);
28
+ background-color: var(--drawer_color, var(--bds-bg));
29
+ color: var(--drawer_on-color, var(--bds-on-bg));
30
+ box-shadow: var(--bds-shadow_xl);
31
31
  overflow: hidden;
32
- transition: var(--animation_transition);
32
+ transition: var(--bds-animation_transition);
33
33
  }
34
34
 
35
35
  .--side_right .panel {
@@ -44,7 +44,7 @@
44
44
  display: flex;
45
45
  align-items: center;
46
46
  justify-content: space-between;
47
- padding: var(--space_m) var(--space_l);
47
+ padding: var(--bds-space_m) var(--bds-space_l);
48
48
  border-block-end: 1px solid currentcolor;
49
49
  flex-shrink: 0;
50
50
  }
@@ -58,7 +58,7 @@
58
58
  height: 2rem;
59
59
  border-radius: 50%;
60
60
  cursor: pointer;
61
- transition: var(--animation_transition);
61
+ transition: var(--bds-animation_transition);
62
62
  }
63
63
 
64
64
  .closeButton svg {
@@ -67,20 +67,22 @@
67
67
  }
68
68
 
69
69
  .closeButton:focus-visible {
70
- outline: var(--outline_default);
71
- outline-offset: var(--outline_offset);
70
+ outline: var(--bds-outline_default);
71
+ outline-offset: var(--bds-outline_offset);
72
72
  }
73
73
 
74
74
  @media (hover: hover) and (pointer: fine) {
75
75
  .closeButton:hover {
76
- background-color: var(--color_bg--subtle);
76
+ background-color: var(--bds-bg--subtle);
77
77
  }
78
78
  }
79
79
 
80
80
  .body {
81
81
  flex: 1;
82
82
  overflow-y: auto;
83
- padding: var(--space_l);
83
+ padding: var(--bds-space_l);
84
+ display: flex;
85
+ flex-direction: column;
84
86
  }
85
87
 
86
88
  @media (prefers-reduced-motion: reduce) {
@@ -58,4 +58,34 @@ describe('Drawer', () => {
58
58
  unmount();
59
59
  expect(document.body.style.overflow).toBe('');
60
60
  });
61
+
62
+ it('has aria-modal="true" when open', () => {
63
+ render(<Drawer isOpen title="Settings" onClose={() => {}}>Content</Drawer>);
64
+ const dialog = document.querySelector('dialog');
65
+ expect(dialog).toHaveAttribute('aria-modal', 'true');
66
+ });
67
+
68
+ it('renders aria-label on the dialog when ariaLabel prop is provided', () => {
69
+ render(<Drawer isOpen title="Settings" ariaLabel="Settings panel" onClose={() => {}}>Content</Drawer>);
70
+ const dialog = document.querySelector('dialog');
71
+ expect(dialog).toHaveAttribute('aria-label', 'Settings panel');
72
+ });
73
+
74
+ it('does not render aria-label when ariaLabel prop is not provided', () => {
75
+ render(<Drawer isOpen title="Settings" onClose={() => {}}>Content</Drawer>);
76
+ const dialog = document.querySelector('dialog');
77
+ expect(dialog).not.toHaveAttribute('aria-label');
78
+ });
79
+
80
+ it('restores focus to trigger element on close', () => {
81
+ const trigger = document.createElement('button');
82
+ document.body.appendChild(trigger);
83
+ trigger.focus();
84
+
85
+ const { rerender } = render(<Drawer isOpen title="Menu" onClose={() => {}}>Body</Drawer>);
86
+ rerender(<Drawer isOpen={false} title="Menu" onClose={() => {}}>Body</Drawer>);
87
+ expect(document.activeElement).toBe(trigger);
88
+
89
+ document.body.removeChild(trigger);
90
+ });
61
91
  });
@@ -9,7 +9,7 @@ interface DrawerProps {
9
9
  children: ReactNode;
10
10
  side?: 'left' | 'right';
11
11
  className?: string;
12
- arialLabel?: string;
12
+ ariaLabel?: string;
13
13
  }
14
14
 
15
15
  export function Drawer({
@@ -18,20 +18,23 @@ export function Drawer({
18
18
  title,
19
19
  children,
20
20
  side = 'right',
21
- arialLabel = '',
21
+ ariaLabel,
22
22
  className,
23
23
  }: Readonly<DrawerProps>) {
24
24
  const dialogRef = useRef<HTMLDialogElement>(null);
25
+ const triggerRef = useRef<Element | null>(null);
25
26
 
26
27
  useEffect(() => {
27
28
  const dialog = dialogRef.current;
28
29
  if (!dialog) return;
29
30
  if (isOpen) {
31
+ triggerRef.current = document.activeElement;
30
32
  dialog.showModal();
31
33
  document.body.style.overflow = 'hidden';
32
34
  } else if (dialog.open) {
33
35
  dialog.close();
34
36
  document.body.style.overflow = '';
37
+ (triggerRef.current as HTMLElement | null)?.focus();
35
38
  }
36
39
  return () => {
37
40
  document.body.style.overflow = '';
@@ -53,7 +56,8 @@ export function Drawer({
53
56
  <dialog
54
57
  ref={dialogRef}
55
58
  className={cn(css.drawer, css[`--side_${side}`], className)}
56
- aria-label={arialLabel}
59
+ aria-label={ariaLabel}
60
+ aria-modal="true"
57
61
  onClick={handleClick}
58
62
  onCancel={handleCancel}
59
63
  >
@@ -6,15 +6,20 @@
6
6
 
7
7
  .menu {
8
8
  position: absolute;
9
- top: calc(100% + var(--space_xxs));
10
- z-index: var(--z-index_dropdown);
9
+ top: calc(100% + var(--bds-space_xxs));
10
+ z-index: var(--bds-z-index_dropdown);
11
11
  min-width: 10rem;
12
- padding: var(--space_xxs) 0;
13
- border-radius: var(--border_radius--s);
14
- border: 1px solid currentcolor;
15
- background-color: var(--dropdown_color, var(--color_bg));
16
- color: var(--dropdown_on-color, var(--color_on-bg));
17
- box-shadow: var(--shadow_m);
12
+ padding: var(--bds-space_xxs) 0;
13
+ border: var(--dropdown_border, none);
14
+ --bdc_color: currentcolor;
15
+ --bdc_radius: var(--bds-border_radius--s);
16
+
17
+ outline: var(--bdc_width) solid var(--bdc_color);
18
+ outline-offset: var(--bdc_offset);
19
+ border-radius: var(--dropdown_radius, var(--bdc_radius));
20
+ background-color: var(--dropdown_color, var(--bds-bg));
21
+ color: var(--dropdown_on-color, var(--bds-on-bg));
22
+ box-shadow: var(--dropdown_shadow, var(--bds-shadow_m));
18
23
  list-style: none;
19
24
  margin: 0;
20
25
  }
@@ -25,7 +30,7 @@
25
30
  .separator {
26
31
  border: none;
27
32
  border-top: 1px solid currentcolor;
28
- margin: var(--space_xxs) 0;
33
+ margin: var(--bds-space_xxs) 0;
29
34
  opacity: 0.15;
30
35
  }
31
36
 
@@ -33,15 +38,15 @@
33
38
  all: unset;
34
39
  display: flex;
35
40
  align-items: center;
36
- gap: var(--space_xs);
41
+ gap: var(--bds-space_xs);
37
42
  width: 100%;
38
- padding: var(--space_xs) var(--space_m);
39
- font-family: var(--font_family--body);
40
- font-size: var(--font_size--body);
43
+ padding: var(--bds-space_xs) var(--bds-space_m);
44
+ font-family: var(--bds-font_family--body);
45
+ font-size: var(--bds-font_size--body);
41
46
  color: currentcolor;
42
47
  cursor: pointer;
43
48
  box-sizing: border-box;
44
- transition: var(--animation_transition);
49
+ transition: var(--bds-animation_transition);
45
50
  }
46
51
 
47
52
  .item:disabled {
@@ -50,15 +55,15 @@
50
55
  }
51
56
 
52
57
  .item:focus-visible {
53
- outline: var(--outline_default);
54
- outline-offset: calc(var(--outline_offset) * -1);
55
- border-radius: var(--border_radius--xs);
58
+ outline: var(--bds-outline_default);
59
+ outline-offset: calc(var(--bds-outline_offset) * -1);
60
+ border-radius: var(--bds-border_radius--xs);
56
61
  }
57
62
 
58
63
  @media (hover: hover) and (pointer: fine) {
59
64
  .item:not(:disabled):hover {
60
- background-color: var(--color_bg--subtle);
61
- color: var(--color_interactive);
65
+ background-color: var(--bds-bg--subtle);
66
+ color: var(--bds-interactive);
62
67
  }
63
68
  }
64
69
 
@@ -26,14 +26,14 @@ export const Default: Story = {
26
26
  ],
27
27
  },
28
28
  render: args => (
29
- <DropdownMenu {...args} trigger={<Button variant="secondary">Actions ▾</Button>} />
29
+ <DropdownMenu {...args} trigger={<Button variant="ghost">Actions ▾</Button>} />
30
30
  ),
31
31
  };
32
32
 
33
33
  export const WithDisabledItem: Story = {
34
34
  render: () => (
35
35
  <DropdownMenu
36
- trigger={<Button variant="secondary">Options ▾</Button>}
36
+ trigger={<Button variant="ghost">Options ▾</Button>}
37
37
  items={[
38
38
  { id: 'view', label: 'View' },
39
39
  { id: 'edit', label: 'Edit' },
@@ -46,7 +46,7 @@ export const WithDisabledItem: Story = {
46
46
  export const WithSeparator: Story = {
47
47
  render: () => (
48
48
  <DropdownMenu
49
- trigger={<Button variant="secondary">File ▾</Button>}
49
+ trigger={<Button variant="ghost">File ▾</Button>}
50
50
  items={[
51
51
  { id: 'new', label: 'New' },
52
52
  { id: 'open', label: 'Open' },
@@ -60,7 +60,7 @@ export const WithSeparator: Story = {
60
60
  export const AlignedEnd: Story = {
61
61
  render: () => (
62
62
  <DropdownMenu
63
- trigger={<Button variant="secondary">⋮</Button>}
63
+ trigger={<Button variant="ghost">⋮</Button>}
64
64
  items={[{ id: 'copy', label: 'Copy link' }, { id: 'report', label: 'Report' }]}
65
65
  placement="bottom-end"
66
66
  />
@@ -6,34 +6,39 @@
6
6
 
7
7
  .panel {
8
8
  position: absolute;
9
- z-index: var(--z-index_popover);
9
+ z-index: var(--bds-z-index_popover);
10
10
  min-width: 12rem;
11
- padding: var(--space_m);
12
- border-radius: var(--border_radius--s);
13
- border: 1px solid currentcolor;
14
- background-color: var(--popover_color, var(--color_bg));
15
- box-shadow: var(--shadow_m);
16
- color: var(--popover_on-color, var(--color_on-bg));
17
- font-size: var(--font_size--body);
11
+ padding: var(--bds-space_m);
12
+ border: var(--popover_border, none);
13
+ --bdc_color: currentcolor;
14
+ --bdc_radius: var(--bds-border_radius--s);
15
+
16
+ outline: var(--bdc_width) solid var(--bdc_color);
17
+ outline-offset: var(--bdc_offset);
18
+ border-radius: var(--popover_radius, var(--bdc_radius));
19
+ background-color: var(--popover_color, var(--bds-bg));
20
+ box-shadow: var(--popover_shadow, var(--bds-shadow_m));
21
+ color: var(--popover_on-color, var(--bds-on-bg));
22
+ font-size: var(--bds-font_size--body);
18
23
  }
19
24
 
20
25
  .--placement_bottom {
21
- top: calc(100% + var(--space_xs));
26
+ top: calc(100% + var(--bds-space_xs));
22
27
  left: 0;
23
28
  }
24
29
 
25
30
  .--placement_top {
26
- bottom: calc(100% + var(--space_xs));
31
+ bottom: calc(100% + var(--bds-space_xs));
27
32
  left: 0;
28
33
  }
29
34
 
30
35
  .--placement_right {
31
- left: calc(100% + var(--space_xs));
36
+ left: calc(100% + var(--bds-space_xs));
32
37
  top: 0;
33
38
  }
34
39
 
35
40
  .--placement_left {
36
- right: calc(100% + var(--space_xs));
41
+ right: calc(100% + var(--bds-space_xs));
37
42
  top: 0;
38
43
  }
39
44
  }
@@ -18,7 +18,7 @@ describe('Popover', () => {
18
18
  <button type="button">Open</button>
19
19
  </Popover>
20
20
  );
21
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
21
+ expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
22
22
  });
23
23
 
24
24
  it('shows the panel when trigger is clicked', async () => {
@@ -29,7 +29,6 @@ describe('Popover', () => {
29
29
  </Popover>
30
30
  );
31
31
  await user.click(screen.getByRole('button', { name: 'Open' }));
32
- expect(screen.getByRole('dialog')).toBeInTheDocument();
33
32
  expect(screen.getByText('Panel content')).toBeInTheDocument();
34
33
  });
35
34
 
@@ -42,7 +41,7 @@ describe('Popover', () => {
42
41
  );
43
42
  await user.click(screen.getByRole('button', { name: 'Toggle' }));
44
43
  await user.click(screen.getByRole('button', { name: 'Toggle' }));
45
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
44
+ expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
46
45
  });
47
46
 
48
47
  it('closes the panel when Escape is pressed', async () => {
@@ -54,7 +53,7 @@ describe('Popover', () => {
54
53
  );
55
54
  await user.click(screen.getByRole('button', { name: 'Open' }));
56
55
  await user.keyboard('{Escape}');
57
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
56
+ expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
58
57
  });
59
58
 
60
59
  it('sets aria-expanded on the trigger when open', async () => {
@@ -69,4 +68,26 @@ describe('Popover', () => {
69
68
  await user.click(trigger);
70
69
  expect(trigger).toHaveAttribute('aria-expanded', 'true');
71
70
  });
71
+
72
+ it('renders role="region" with aria-label when aria-label prop is provided', async () => {
73
+ const user = userEvent.setup();
74
+ render(
75
+ <Popover content={<p>Filters</p>} aria-label="Filter options">
76
+ <button type="button">Open</button>
77
+ </Popover>
78
+ );
79
+ await user.click(screen.getByRole('button', { name: 'Open' }));
80
+ expect(screen.getByRole('region', { name: 'Filter options' })).toBeInTheDocument();
81
+ });
82
+
83
+ it('does not render role="region" when no aria-label is provided', async () => {
84
+ const user = userEvent.setup();
85
+ render(
86
+ <Popover content={<p>Panel</p>}>
87
+ <button type="button">Open</button>
88
+ </Popover>
89
+ );
90
+ await user.click(screen.getByRole('button', { name: 'Open' }));
91
+ expect(screen.queryByRole('region')).not.toBeInTheDocument();
92
+ });
72
93
  });
@@ -41,7 +41,7 @@ export const WithForm: Story = {
41
41
  </div>
42
42
  }
43
43
  >
44
- <Button variant="secondary">Filters</Button>
44
+ <Button variant="ghost">Filters</Button>
45
45
  </Popover>
46
46
  ),
47
47
  };
@@ -16,6 +16,7 @@ interface PopoverProps {
16
16
  content: ReactNode;
17
17
  placement?: 'top' | 'bottom' | 'left' | 'right';
18
18
  className?: string;
19
+ 'aria-label'?: string;
19
20
  }
20
21
 
21
22
  export function Popover({
@@ -23,6 +24,7 @@ export function Popover({
23
24
  content,
24
25
  placement = 'bottom',
25
26
  className,
27
+ 'aria-label': ariaLabel,
26
28
  }: Readonly<PopoverProps>) {
27
29
  const [isOpen, setIsOpen] = useState(false);
28
30
  const containerRef = useRef<HTMLSpanElement>(null);
@@ -52,6 +54,7 @@ export function Popover({
52
54
  ? cloneElement(children as ReactElement<Record<string, unknown>>, {
53
55
  'aria-expanded': isOpen,
54
56
  'aria-controls': panelId,
57
+ 'aria-haspopup': true,
55
58
  onClick: (e: MouseEvent) => {
56
59
  setIsOpen(prev => !prev);
57
60
  const existingOnClick = (children.props as Record<string, unknown>).onClick;
@@ -66,8 +69,8 @@ export function Popover({
66
69
  {isOpen && (
67
70
  <div
68
71
  id={panelId}
69
- role="dialog"
70
- aria-modal="false"
72
+ role={ariaLabel ? 'region' : undefined}
73
+ aria-label={ariaLabel}
71
74
  className={cn(css.panel, css[`--placement_${placement}`])}
72
75
  >
73
76
  {content}
@@ -1,8 +1,8 @@
1
1
  @layer component {
2
2
  .rating {
3
3
  display: inline-flex;
4
- gap: var(--space_xxxs);
5
- color: var(--color_bg);
4
+ gap: var(--bds-space_xxxs);
5
+ color: var(--bds-bg);
6
6
  }
7
7
 
8
8
  .star {
@@ -11,6 +11,6 @@
11
11
  }
12
12
 
13
13
  .--filled {
14
- color: var(--color_warning);
14
+ color: var(--bds-warning);
15
15
  }
16
16
  }
@@ -1,45 +1,45 @@
1
1
  @layer component {
2
2
  .toastContainer {
3
3
  position: fixed;
4
- bottom: var(--space_xl);
5
- right: var(--space_xl);
4
+ bottom: var(--bds-space_xl);
5
+ right: var(--bds-space_xl);
6
6
  display: flex;
7
7
  flex-direction: column;
8
- gap: var(--space_m);
9
- z-index: var(--z-index_toast);
8
+ gap: var(--bds-space_m);
9
+ z-index: var(--bds-z-index_toast);
10
10
  }
11
11
 
12
12
  .toast {
13
- padding: var(--space_m) var(--space_l);
14
- background-color: var(--toast_color, var(--color_bg));
15
- color: var(--toast_on-color, var(--color_on-bg));
16
- border-radius: var(--border_radius--s);
17
- box-shadow: var(--shadow_s);
13
+ padding: var(--bds-space_m) var(--bds-space_l);
14
+ background-color: var(--toast_color, var(--bds-bg));
15
+ color: var(--toast_on-color, var(--bds-on-bg));
16
+ border-radius: var(--bds-border_radius--s);
17
+ box-shadow: var(--bds-shadow_s);
18
18
  display: flex;
19
19
  align-items: center;
20
- gap: var(--space_m);
20
+ gap: var(--bds-space_m);
21
21
  min-width: 300px;
22
- animation: slideIn var(--animation_duration--fast) var(--animation_easing);
22
+ animation: slideIn var(--bds-animation_duration--fast) var(--bds-animation_easing);
23
23
  }
24
24
 
25
25
  .toast.--variant_success {
26
- --toast_color: var(--color_success);
27
- --toast_on-color: var(--color_on-success);
26
+ --toast_color: var(--bds-success);
27
+ --toast_on-color: var(--bds-on-success);
28
28
  }
29
29
 
30
30
  .toast.--variant_warning {
31
- --toast_color: var(--color_warning);
32
- --toast_on-color: var(--color_on-warning);
31
+ --toast_color: var(--bds-warning);
32
+ --toast_on-color: var(--bds-on-warning);
33
33
  }
34
34
 
35
35
  .toast.--variant_info {
36
- --toast_color: var(--color_bg--subtle);
37
- --toast_on-color: var(--color_on-bg--subtle);
36
+ --toast_color: var(--bds-bg--subtle);
37
+ --toast_on-color: var(--bds-on-bg--subtle);
38
38
  }
39
39
 
40
40
  .toast.--variant_error {
41
- --toast_color: var(--color_error);
42
- --toast_on-color: var(--color_on-error);
41
+ --toast_color: var(--bds-error);
42
+ --toast_on-color: var(--bds-on-error);
43
43
  }
44
44
 
45
45
  .message {
@@ -53,7 +53,7 @@ function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: () => void })
53
53
  }, [onRemove]);
54
54
 
55
55
  return (
56
- <div className={cn(css.toast, css[`--variant_${toast.variant}`])} role="status">
56
+ <div className={cn(css.toast, css[`--variant_${toast.variant}`])} role="status" aria-live="polite" aria-atomic="true">
57
57
  <span className={css.message}>{toast.message}</span>
58
58
  <button type="button" className={css.closeButton} onClick={onRemove} aria-label="Dismiss">
59
59
  <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -44,8 +44,8 @@ Binary toggle for opt-in/opt-out choices. Renders a styled native `<input type="
44
44
  <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
45
45
  </thead>
46
46
  <tbody>
47
- <tr><td>`--checkbox_color-active`</td><td>`var(--color_active)`</td><td>Checked background</td></tr>
48
- <tr><td>`--checkbox_color-on-active`</td><td>`var(--color_on-active)`</td><td>Checkmark colour</td></tr>
47
+ <tr><td>`--checkbox_color-active`</td><td>`var(--bds-active)`</td><td>Checked background</td></tr>
48
+ <tr><td>`--checkbox_color-on-active`</td><td>`var(--bds-on-active)`</td><td>Checkmark colour</td></tr>
49
49
  </tbody>
50
50
  </table>
51
51
 
@@ -1,6 +1,6 @@
1
1
  @layer component {
2
2
  .checkboxGroup {
3
- --inputSize: calc(var(--space_s) * 2);
3
+ --inputSize: calc(var(--bds-space_s) * 2);
4
4
 
5
5
  display: flex;
6
6
  flex-direction: column;
@@ -9,26 +9,32 @@
9
9
  .inputWrapper {
10
10
  display: flex;
11
11
  align-items: stretch;
12
- gap: var(--space_xs);
12
+ gap: var(--bds-space_xs);
13
13
  }
14
14
 
15
15
  .checkbox {
16
16
  margin-block-start: 0.25em;
17
17
  width: var(--inputSize);
18
18
  height: var(--inputSize);
19
- color: var(--checkbox_color, var(--color_on-bg));
20
- border: 1px solid currentcolor;
21
- border-radius: var(--border_radius--xs);
19
+ color: var(--checkbox_color, var(--bds-on-bg));
20
+ border: var(--checkbox_border, none);
21
+ --bdc_color: currentcolor;
22
+ --bdc_radius: var(--bds-border_radius--xs);
23
+
24
+ outline: var(--bdc_width) solid var(--bdc_color);
25
+ outline-offset: var(--bdc_offset);
26
+ border-radius: var(--checkbox_radius, var(--bdc_radius));
27
+ box-shadow: var(--checkbox_shadow, var(--bdc_shadow));
22
28
  appearance: none;
23
- background-color: var(--checkbox_color_bg, var(--color_bg));
29
+ background-color: var(--checkbox_color_bg, var(--bds-bg));
24
30
  cursor: pointer;
25
31
  position: relative;
26
- transition: background-color 0.3s ease, border-color 0.3s ease;
32
+ transition: --bdc_color 0.3s ease, background-color 0.3s ease;
27
33
  }
28
34
 
29
35
  .checkbox:checked {
30
- background-color: var(--checkbox_color-active, var(--color_active));
31
- border-color: var(--checkbox_color-active, var(--color_active));
36
+ background-color: var(--checkbox_color-active, var(--bds-active));
37
+ --bdc_color: var(--checkbox_color-active, var(--bds-active));
32
38
  }
33
39
 
34
40
  .checkbox:checked::after {
@@ -38,23 +44,21 @@
38
44
  top: 0.2em;
39
45
  width: 0.5em;
40
46
  height: 1em;
41
- border: solid var(--checkbox_color-on-active, var(--color_on-active));
47
+ border: solid var(--checkbox_color-on-active, var(--bds-on-active));
42
48
  border-width: 0 2px 2px 0;
43
49
  transform: rotate(45deg);
44
50
  }
45
51
 
46
52
  .checkbox:focus {
47
- outline: none;
48
- border-color: var(--color_interactive);
49
- box-shadow: 0 0 0 2px rgb(from var(--color_interactive) r g b / 20%);
53
+ --bdc_color: var(--bdc_color--focus);
50
54
  }
51
55
 
52
- .checkboxError {
53
- border-color: var(--color_error);
56
+ .checkbox:focus-visible {
57
+ outline: var(--bds-outline_default);
58
+ outline-offset: var(--bds-outline_offset);
54
59
  }
55
60
 
56
- .checkboxError:focus {
57
- border-color: var(--color_error);
58
- box-shadow: 0 0 0 2px rgb(from var(--color_error) r g b / 20%);
61
+ .checkboxError {
62
+ --bdc_color: var(--bdc_color--error);
59
63
  }
60
64
  }