@boostdev/design-system-components 0.1.13 → 0.1.15

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 (128) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/client.cjs +406 -362
  4. package/dist/client.css +560 -511
  5. package/dist/client.d.cts +27 -5
  6. package/dist/client.d.ts +27 -5
  7. package/dist/client.js +405 -362
  8. package/dist/index.cjs +406 -362
  9. package/dist/index.css +560 -511
  10. package/dist/index.d.cts +27 -5
  11. package/dist/index.d.ts +27 -5
  12. package/dist/index.js +405 -362
  13. package/package.json +4 -2
  14. package/src/components/interaction/Button/Button.mdx +73 -0
  15. package/src/components/interaction/Button/Button.module.css +12 -20
  16. package/src/components/interaction/Button/Button.stories.tsx +0 -1
  17. package/src/components/interaction/Command/Command.mdx +28 -0
  18. package/src/components/interaction/Command/Command.module.css +2 -1
  19. package/src/components/interaction/Command/Command.stories.tsx +3 -3
  20. package/src/components/interaction/Dialog/Dialog.mdx +57 -0
  21. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  22. package/src/components/interaction/Dialog/Dialog.stories.tsx +0 -1
  23. package/src/components/interaction/Drawer/Drawer.mdx +35 -0
  24. package/src/components/interaction/Drawer/Drawer.module.css +3 -9
  25. package/src/components/interaction/Drawer/Drawer.stories.tsx +3 -3
  26. package/src/components/interaction/Drawer/Drawer.tsx +5 -3
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +40 -0
  28. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +5 -5
  29. package/src/components/interaction/Popover/Popover.mdx +34 -0
  30. package/src/components/interaction/Popover/Popover.stories.tsx +3 -3
  31. package/src/components/interaction/Rating/Rating.mdx +38 -0
  32. package/src/components/interaction/Rating/Rating.stories.tsx +0 -1
  33. package/src/components/interaction/Toast/Toast.mdx +78 -0
  34. package/src/components/interaction/Toast/Toast.module.css +21 -7
  35. package/src/components/interaction/Toast/Toast.stories.tsx +0 -1
  36. package/src/components/interaction/form/Checkbox/Checkbox.mdx +57 -0
  37. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +0 -1
  38. package/src/components/interaction/form/Combobox/Combobox.mdx +37 -0
  39. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +0 -1
  40. package/src/components/interaction/form/FileInput/FileInput.mdx +38 -0
  41. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +0 -1
  42. package/src/components/interaction/form/FormInput/FormInput.mdx +54 -0
  43. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +0 -1
  44. package/src/components/interaction/form/NumberInput/NumberInput.mdx +38 -0
  45. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +0 -1
  46. package/src/components/interaction/form/Radio/Radio.mdx +60 -0
  47. package/src/components/interaction/form/Radio/Radio.module.css +24 -11
  48. package/src/components/interaction/form/Radio/Radio.stories.tsx +5 -5
  49. package/src/components/interaction/form/Radio/Radio.tsx +6 -2
  50. package/src/components/interaction/form/Select/Select.mdx +58 -0
  51. package/src/components/interaction/form/Select/Select.stories.tsx +0 -1
  52. package/src/components/interaction/form/Slider/Slider.mdx +58 -0
  53. package/src/components/interaction/form/Slider/Slider.stories.tsx +0 -1
  54. package/src/components/interaction/form/Switch/Switch.mdx +61 -0
  55. package/src/components/interaction/form/Switch/Switch.module.css +6 -10
  56. package/src/components/interaction/form/Switch/Switch.stories.tsx +0 -1
  57. package/src/components/interaction/form/Textarea/Textarea.mdx +41 -0
  58. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +0 -1
  59. package/src/components/layout/ButtonGroup/ButtonGroup.mdx +47 -0
  60. package/src/components/layout/ButtonGroup/ButtonGroup.module.css +10 -10
  61. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +1 -2
  62. package/src/components/layout/Card/Card.mdx +58 -0
  63. package/src/components/layout/Card/Card.stories.tsx +0 -1
  64. package/src/components/layout/IconWrapper/IconWrapper.mdx +39 -0
  65. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +0 -1
  66. package/src/components/layout/SectionHeader/SectionHeader.mdx +38 -0
  67. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +0 -1
  68. package/src/components/ui/Accordion/Accordion.mdx +40 -0
  69. package/src/components/ui/Accordion/Accordion.stories.tsx +0 -1
  70. package/src/components/ui/Alert/Alert.mdx +63 -0
  71. package/src/components/ui/Alert/Alert.module.css +3 -7
  72. package/src/components/ui/Alert/Alert.stories.tsx +0 -1
  73. package/src/components/ui/Avatar/Avatar.mdx +53 -0
  74. package/src/components/ui/Avatar/Avatar.module.css +4 -8
  75. package/src/components/ui/Avatar/Avatar.stories.tsx +0 -1
  76. package/src/components/ui/Badge/Badge.mdx +62 -0
  77. package/src/components/ui/Badge/Badge.module.css +4 -10
  78. package/src/components/ui/Badge/Badge.stories.tsx +0 -1
  79. package/src/components/ui/Breadcrumb/Breadcrumb.mdx +36 -0
  80. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +0 -1
  81. package/src/components/ui/Calendar/Calendar.mdx +34 -0
  82. package/src/components/ui/Calendar/Calendar.stories.tsx +0 -1
  83. package/src/components/ui/Carousel/Carousel.mdx +31 -0
  84. package/src/components/ui/Carousel/Carousel.stories.tsx +0 -1
  85. package/src/components/ui/Collapsible/Collapsible.mdx +59 -0
  86. package/src/components/ui/Collapsible/Collapsible.module.css +76 -0
  87. package/src/components/ui/Collapsible/Collapsible.spec.tsx +75 -0
  88. package/src/components/ui/Collapsible/Collapsible.stories.tsx +57 -0
  89. package/src/components/ui/Collapsible/Collapsible.tsx +55 -0
  90. package/src/components/ui/Collapsible/index.ts +2 -0
  91. package/src/components/ui/DescriptionList/DescriptionList.mdx +28 -0
  92. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +0 -1
  93. package/src/components/ui/Link/Link.mdx +44 -0
  94. package/src/components/ui/Link/Link.module.css +2 -5
  95. package/src/components/ui/Link/Link.stories.tsx +0 -1
  96. package/src/components/ui/Loading/Loading.mdx +52 -0
  97. package/src/components/ui/Loading/Loading.stories.tsx +0 -1
  98. package/src/components/ui/NotificationBanner/NotificationBanner.mdx +45 -0
  99. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +3 -7
  100. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +2 -2
  101. package/src/components/ui/Pagination/Pagination.mdx +39 -0
  102. package/src/components/ui/Pagination/Pagination.module.css +3 -7
  103. package/src/components/ui/Pagination/Pagination.stories.tsx +0 -1
  104. package/src/components/ui/Progress/Progress.mdx +52 -0
  105. package/src/components/ui/Progress/Progress.module.css +1 -3
  106. package/src/components/ui/Progress/Progress.stories.tsx +0 -1
  107. package/src/components/ui/ProgressCircle/ProgressCircle.mdx +46 -0
  108. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +0 -1
  109. package/src/components/ui/Separator/Separator.mdx +29 -0
  110. package/src/components/ui/Separator/Separator.stories.tsx +0 -1
  111. package/src/components/ui/Skeleton/Skeleton.mdx +36 -0
  112. package/src/components/ui/Skeleton/Skeleton.stories.tsx +0 -1
  113. package/src/components/ui/SkipLink/SkipLink.mdx +34 -0
  114. package/src/components/ui/SkipLink/SkipLink.stories.tsx +0 -1
  115. package/src/components/ui/Table/Table.mdx +38 -0
  116. package/src/components/ui/Table/Table.stories.tsx +0 -1
  117. package/src/components/ui/Tabs/Tabs.mdx +40 -0
  118. package/src/components/ui/Tabs/Tabs.module.css +2 -5
  119. package/src/components/ui/Tabs/Tabs.stories.tsx +0 -1
  120. package/src/components/ui/Tooltip/Tooltip.mdx +43 -0
  121. package/src/components/ui/Tooltip/Tooltip.module.css +2 -5
  122. package/src/components/ui/Tooltip/Tooltip.stories.tsx +6 -6
  123. package/src/components/ui/Typography/Typography.mdx +41 -0
  124. package/src/components/ui/Typography/Typography.stories.tsx +0 -1
  125. package/src/index.ts +2 -0
  126. package/src/static/logo.svg +8 -0
  127. package/src/stories/Introduction.css +17 -0
  128. package/src/stories/Introduction.mdx +129 -0
@@ -0,0 +1,53 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Avatar.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Avatar
7
+
8
+ Circular user representation. Displays a profile image when available, falling back to initials derived from `name`, then a generic placeholder.
9
+
10
+ ## When to use
11
+ - User identity in comments, messages, or list items
12
+ - Profile headers
13
+
14
+ ## Examples
15
+
16
+ ### With image
17
+ <Canvas of={Stories.WithImage} />
18
+
19
+ ### With initials
20
+ <Canvas of={Stories.WithInitials} />
21
+
22
+ ### All sizes
23
+ <Canvas of={Stories.AllSizes} />
24
+
25
+ ### Single name (one initial)
26
+ <Canvas of={Stories.SingleName} />
27
+
28
+ ### No name fallback
29
+ <Canvas of={Stories.NoName} />
30
+
31
+ ## Props
32
+
33
+ <ArgTypes of={Stories} />
34
+
35
+ ## CSS variables
36
+
37
+
38
+ <table>
39
+ <thead>
40
+ <tr><th>Variable</th><th>Description</th></tr>
41
+ </thead>
42
+ <tbody>
43
+ <tr><td>`--avatar_color`</td><td>Background colour for initials fallback</td></tr>
44
+ <tr><td>`--avatar_on-color`</td><td>Text colour for initials</td></tr>
45
+ </tbody>
46
+ </table>
47
+
48
+
49
+ ## Accessibility
50
+
51
+ - Always provide a meaningful `alt` when passing `src`
52
+ - When no image is shown, `aria-label` is derived from `name` to describe the avatar to screen readers
53
+ - Touch target meets 44×44px minimum at `medium` size and above
@@ -1,22 +1,18 @@
1
1
  @layer component {
2
2
  .avatar {
3
- --avatar_size: 3em;
4
- --avatar_bg: var(--color_blue);
5
- --avatar_text: var(--color_on-blue);
6
-
7
3
  display: inline-flex;
8
4
  align-items: center;
9
5
  justify-content: center;
10
- width: var(--avatar_size);
11
- height: var(--avatar_size);
6
+ width: var(--avatar_size, 3em);
7
+ height: var(--avatar_size, 3em);
12
8
  border-radius: 50%;
13
9
  overflow: hidden;
14
10
  flex-shrink: 0;
15
11
  }
16
12
 
17
13
  .avatar.--fallback {
18
- background-color: var(--avatar_bg);
19
- color: var(--avatar_text);
14
+ background-color: var(--avatar_bg, var(--color_blue));
15
+ color: var(--avatar_text, var(--color_on-blue));
20
16
  font-weight: var(--font_weight--semibold);
21
17
  }
22
18
 
@@ -4,7 +4,6 @@ import { Avatar } from './Avatar';
4
4
  const meta = {
5
5
  title: 'UI/Avatar',
6
6
  component: Avatar,
7
- tags: ['autodocs'],
8
7
  argTypes: {
9
8
  size: { control: 'radio', options: ['small', 'medium', 'large'] },
10
9
  },
@@ -0,0 +1,62 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Badge.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Badge
7
+
8
+ Compact inline label for status, category, or count. Renders a `<span>` with a semantic variant colour.
9
+
10
+ ## When to use
11
+ - Highlight status on a record (active, error, draft)
12
+ - Show a count alongside a navigation item
13
+ - Label items in a list by category
14
+
15
+ ## When not to use
16
+ - As a call-to-action — use `Button` instead
17
+ - For long text — badges are single words or short phrases
18
+
19
+ ## Examples
20
+
21
+ ### Variants
22
+ <Canvas of={Stories.AllVariants} />
23
+
24
+ ### Primary
25
+ <Canvas of={Stories.Primary} />
26
+
27
+ ### Secondary
28
+ <Canvas of={Stories.Secondary} />
29
+
30
+ ### Success
31
+ <Canvas of={Stories.Success} />
32
+
33
+ ### Error
34
+ <Canvas of={Stories.Error} />
35
+
36
+ ### Warning
37
+ <Canvas of={Stories.Warning} />
38
+
39
+ ## Props
40
+
41
+ <ArgTypes of={Stories} />
42
+
43
+ ## CSS variables
44
+
45
+ Override these on a parent element to customise the badge without touching global tokens:
46
+
47
+
48
+ <table>
49
+ <thead>
50
+ <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
51
+ </thead>
52
+ <tbody>
53
+ <tr><td>`--badge_color`</td><td>`var(--color_cta--subtle)`</td><td>Background colour</td></tr>
54
+ <tr><td>`--badge_on-color`</td><td>`var(--color_on-cta--subtle)`</td><td>Text colour</td></tr>
55
+ </tbody>
56
+ </table>
57
+
58
+
59
+ ## Accessibility
60
+
61
+ - Renders as `<span>` — purely decorative/informational, no interactive role
62
+ - Ensure the badge text is meaningful without surrounding context, or supplement it with a visually-hidden description for screen readers
@@ -1,22 +1,16 @@
1
1
  @layer component {
2
2
  .badge {
3
- /* Component tokens — override from a parent context or inline style */
4
- --badge_bg: var(--color_blue);
5
- --badge_text: var(--color_on-blue);
6
- --badge_radius: 2em;
7
- --badge_font-size: var(--font_size--body--s);
8
-
9
3
  display: inline-flex;
10
4
  align-items: center;
11
5
  justify-content: center;
12
6
  padding: var(--space_xxs) var(--space_xs);
13
- border-radius: var(--badge_radius);
14
- font-size: var(--badge_font-size);
7
+ border-radius: var(--badge_radius, 2em);
8
+ font-size: var(--badge_font-size, var(--font_size--body--s));
15
9
  font-weight: var(--font_weight--semibold);
16
10
  line-height: 1;
17
11
  white-space: nowrap;
18
- background-color: var(--badge_bg);
19
- color: var(--badge_text);
12
+ background-color: var(--badge_bg, var(--color_blue));
13
+ color: var(--badge_text, var(--color_on-blue));
20
14
  }
21
15
 
22
16
  .--variant_primary {
@@ -4,7 +4,6 @@ import { Badge } from './Badge';
4
4
  const meta = {
5
5
  title: 'UI/Badge',
6
6
  component: Badge,
7
- tags: ['autodocs'],
8
7
  argTypes: {
9
8
  variant: { control: 'select', options: ['primary', 'secondary', 'success', 'error', 'warning'] },
10
9
  },
@@ -0,0 +1,36 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Breadcrumb.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Breadcrumb
7
+
8
+ Hierarchical navigation trail showing the user's location within the site structure.
9
+
10
+ ## When to use
11
+ - Multi-level site structures (3+ levels deep)
12
+ - Pages where users may arrive directly and benefit from context
13
+
14
+ ## Examples
15
+
16
+ ### Default
17
+ <Canvas of={Stories.Default} />
18
+
19
+ ### Single level
20
+ <Canvas of={Stories.SingleLevel} />
21
+
22
+ ### Two levels
23
+ <Canvas of={Stories.TwoLevels} />
24
+
25
+ ### Deep nested
26
+ <Canvas of={Stories.DeepNested} />
27
+
28
+ ## Props
29
+
30
+ <ArgTypes of={Stories} />
31
+
32
+ ## Accessibility
33
+
34
+ - Wraps in `<nav aria-label="Breadcrumb">`
35
+ - The current page item has `aria-current="page"`
36
+ - Separator glyphs are `aria-hidden="true"`
@@ -4,7 +4,6 @@ import { Breadcrumb } from './Breadcrumb';
4
4
  const meta = {
5
5
  title: 'UI/Breadcrumb',
6
6
  component: Breadcrumb,
7
- tags: ['autodocs'],
8
7
  } satisfies Meta<typeof Breadcrumb>;
9
8
 
10
9
  export default meta;
@@ -0,0 +1,34 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Calendar.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Calendar
7
+
8
+ Date picker calendar grid. Use standalone or as the popover content inside a date input.
9
+
10
+ ## When to use
11
+ - Date selection in forms where a visual calendar improves comprehension
12
+ - Booking flows, scheduling tools, date range pickers
13
+
14
+ ## Examples
15
+
16
+ ### Default (uncontrolled)
17
+ <Canvas of={Stories.Default} />
18
+
19
+ ### With default value
20
+ <Canvas of={Stories.WithDefaultValue} />
21
+
22
+ ### With min/max constraints
23
+ <Canvas of={Stories.WithMinMax} />
24
+
25
+ ## Props
26
+
27
+ <ArgTypes of={Stories} />
28
+
29
+ ## Accessibility
30
+
31
+ - Grid uses `role="grid"` with `role="gridcell"` for each day
32
+ - Selected date has `aria-selected="true"`; disabled dates have `aria-disabled="true"`
33
+ - Month navigation buttons have descriptive `aria-label` values
34
+ - Arrow keys move focus within the grid; `Enter`/`Space` select a date
@@ -5,7 +5,6 @@ import { Calendar } from './Calendar';
5
5
  const meta = {
6
6
  title: 'UI/Calendar',
7
7
  component: Calendar,
8
- tags: ['autodocs'],
9
8
  decorators: [
10
9
  Story => (
11
10
  <div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
@@ -0,0 +1,31 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Carousel.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Carousel
7
+
8
+ Horizontally scrollable slide container with previous/next navigation.
9
+
10
+ ## When to use
11
+ - Image galleries, featured content, or step-by-step flows where all items share equal priority
12
+ - When screen space is limited and items benefit from a browse interaction
13
+
14
+ ## When not to use
15
+ - Critical content — most users do not interact with carousels; place key content outside
16
+ - More than 5–6 slides — consider a grid or tabbed layout
17
+
18
+ ## Examples
19
+
20
+ ### Default
21
+ <Canvas of={Stories.Default} />
22
+
23
+ ## Props
24
+
25
+ <ArgTypes of={Stories} />
26
+
27
+ ## Accessibility
28
+
29
+ - Prev/next buttons have `aria-label="Previous"` / `"Next"`
30
+ - The slide container has `aria-live="polite"` to announce slide changes
31
+ - Fully keyboard navigable: buttons receive focus, slides are accessible via arrow keys
@@ -4,7 +4,6 @@ import { Carousel } from './Carousel';
4
4
  const meta = {
5
5
  title: 'UI/Carousel',
6
6
  component: Carousel,
7
- tags: ['autodocs'],
8
7
  } satisfies Meta<typeof Carousel>;
9
8
 
10
9
  export default meta;
@@ -0,0 +1,59 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Collapsible.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Collapsible
7
+
8
+ Disclosure widget that shows or hides content. Built on the native `<details>`/`<summary>` HTML elements — no JavaScript required for basic toggle behaviour.
9
+
10
+ ## When to use
11
+ - A single section of content that users may or may not need (FAQs, help text, optional details)
12
+ - When you want progressive disclosure without routing to a new page
13
+
14
+ ## When not to use
15
+ - Multiple interrelated sections with complex state — use `Accordion` instead
16
+ - Navigation or tabbed views — use `Tabs`
17
+
18
+ ## Examples
19
+
20
+ ### Default
21
+ <Canvas of={Stories.Default} />
22
+
23
+ ### Starts open
24
+ <Canvas of={Stories.DefaultOpen} />
25
+
26
+ ### Grouped (accordion behaviour)
27
+ Use the `name` prop to link multiple Collapsibles so only one can be open at a time — this uses the native `<details name="…">` attribute.
28
+ <Canvas of={Stories.Group} />
29
+
30
+ ### Rich summary content
31
+ <Canvas of={Stories.RichSummary} />
32
+
33
+ ## Props
34
+
35
+ <ArgTypes of={Stories} />
36
+
37
+ ## CSS variables
38
+
39
+ <table>
40
+ <thead>
41
+ <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
42
+ </thead>
43
+ <tbody>
44
+ <tr><td>`--collapsible_border-color`</td><td>`var(--color_bg--subtle)`</td><td>Border and separator colour</td></tr>
45
+ <tr><td>`--collapsible_border-width`</td><td>`1px`</td><td>Border and separator width</td></tr>
46
+ <tr><td>`--collapsible_border-radius`</td><td>`var(--border_radius--m)`</td><td>Corner radius</td></tr>
47
+ <tr><td>`--collapsible_bg`</td><td>`var(--color_bg)`</td><td>Summary background</td></tr>
48
+ <tr><td>`--collapsible_bg--hover`</td><td>`var(--color_bg--subtle)`</td><td>Summary background on hover</td></tr>
49
+ <tr><td>`--collapsible_color`</td><td>`var(--color_on-bg)`</td><td>Summary text colour</td></tr>
50
+ <tr><td>`--collapsible_on-color`</td><td>`var(--color_on-bg)`</td><td>Content text colour</td></tr>
51
+ </tbody>
52
+ </table>
53
+
54
+ ## Accessibility
55
+
56
+ - Uses semantic `<details>`/`<summary>` — the browser applies `role="group"` implicitly
57
+ - Keyboard: `Enter` or `Space` on the focused summary toggles the panel
58
+ - Arrow keys do **not** move between grouped Collapsibles (unlike `Accordion`); use `Tab` to navigate
59
+ - No `aria-expanded` needed — the native element communicates state to screen readers
@@ -0,0 +1,76 @@
1
+ @layer component {
2
+ .collapsible {
3
+ border: var(--collapsible_border-width, 1px) solid var(--collapsible_border-color, var(--color_bg--subtle));
4
+ border-radius: var(--collapsible_border-radius, var(--border_radius--m));
5
+ overflow: hidden;
6
+ }
7
+
8
+ .summary {
9
+ list-style: none; /* removes native disclosure triangle */
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: var(--space_m);
14
+ padding: var(--space_m);
15
+ cursor: pointer;
16
+ font-weight: var(--font_weight--semibold);
17
+ font-size: var(--font_size--body);
18
+ color: var(--collapsible_color, var(--color_on-bg));
19
+ background-color: var(--collapsible_bg, var(--color_bg));
20
+ user-select: none;
21
+ transition: var(--animation_transition);
22
+ }
23
+
24
+ /* Remove WebKit disclosure triangle */
25
+ .summary::-webkit-details-marker {
26
+ display: none;
27
+ }
28
+
29
+ .summary:focus-visible {
30
+ outline: var(--outline_default);
31
+ outline-offset: calc(var(--outline_offset) * -1);
32
+ }
33
+
34
+ @media (hover: hover) and (pointer: fine) {
35
+ .summary:hover {
36
+ background-color: var(--collapsible_bg--hover, var(--color_bg--subtle));
37
+ }
38
+ }
39
+
40
+ /* Separator when open */
41
+ .collapsible[open] > .summary {
42
+ border-block-end: var(--collapsible_border-width, 1px) solid var(--collapsible_border-color, var(--color_bg--subtle));
43
+ }
44
+
45
+ .summaryContent {
46
+ flex: 1;
47
+ }
48
+
49
+ /* Chevron drawn with CSS borders */
50
+ .icon {
51
+ width: 0.5rem;
52
+ height: 0.5rem;
53
+ border-right: 2px solid currentcolor;
54
+ border-bottom: 2px solid currentcolor;
55
+ transform: rotate(45deg);
56
+ flex-shrink: 0;
57
+ transition: transform var(--animation_transition-duration) var(--animation_easing);
58
+ }
59
+
60
+ .collapsible[open] > .summary .icon {
61
+ transform: rotate(-135deg);
62
+ }
63
+
64
+ .content {
65
+ padding: var(--space_m);
66
+ color: var(--collapsible_on-color, var(--color_on-bg));
67
+ font-size: var(--font_size--body);
68
+ line-height: var(--font_line-height--body);
69
+ }
70
+
71
+ @media (prefers-reduced-motion: reduce) {
72
+ .icon {
73
+ transition: none;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,75 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Collapsible } from './Collapsible';
4
+
5
+ describe('Collapsible', () => {
6
+ it('renders summary text', () => {
7
+ render(<Collapsible summary="Toggle me">Content</Collapsible>);
8
+ expect(screen.getByText('Toggle me')).toBeInTheDocument();
9
+ });
10
+
11
+ it('hides content by default', () => {
12
+ render(<Collapsible summary="Toggle">Hidden content</Collapsible>);
13
+ expect(screen.getByRole('group')).not.toHaveAttribute('open');
14
+ });
15
+
16
+ it('shows content when defaultOpen is true', () => {
17
+ render(<Collapsible summary="Toggle" defaultOpen>Visible content</Collapsible>);
18
+ expect(screen.getByRole('group')).toHaveAttribute('open');
19
+ });
20
+
21
+ it('expands on summary click', async () => {
22
+ const user = userEvent.setup();
23
+ render(<Collapsible summary="Toggle">Content</Collapsible>);
24
+ const details = screen.getByRole('group');
25
+ expect(details).not.toHaveAttribute('open');
26
+ await user.click(screen.getByText('Toggle'));
27
+ expect(details).toHaveAttribute('open');
28
+ });
29
+
30
+ it('collapses on second click', async () => {
31
+ const user = userEvent.setup();
32
+ render(<Collapsible summary="Toggle" defaultOpen>Content</Collapsible>);
33
+ const details = screen.getByRole('group');
34
+ await user.click(screen.getByText('Toggle'));
35
+ expect(details).not.toHaveAttribute('open');
36
+ });
37
+
38
+ it('calls onToggle with new open state when expanded', async () => {
39
+ const user = userEvent.setup();
40
+ const onToggle = vi.fn();
41
+ render(<Collapsible summary="Toggle" onToggle={onToggle}>Content</Collapsible>);
42
+ await user.click(screen.getByText('Toggle'));
43
+ expect(onToggle).toHaveBeenCalledWith(true);
44
+ });
45
+
46
+ it('calls onToggle with false when collapsed', async () => {
47
+ const user = userEvent.setup();
48
+ const onToggle = vi.fn();
49
+ render(<Collapsible summary="Toggle" defaultOpen onToggle={onToggle}>Content</Collapsible>);
50
+ await user.click(screen.getByText('Toggle'));
51
+ expect(onToggle).toHaveBeenCalledWith(false);
52
+ });
53
+
54
+ it('respects controlled open prop', () => {
55
+ render(<Collapsible summary="Toggle" open>Content</Collapsible>);
56
+ expect(screen.getByRole('group')).toHaveAttribute('open');
57
+ });
58
+
59
+ it('renders with name attribute for grouping', () => {
60
+ render(
61
+ <>
62
+ <Collapsible summary="A" name="group">Content A</Collapsible>
63
+ <Collapsible summary="B" name="group">Content B</Collapsible>
64
+ </>,
65
+ );
66
+ const groups = screen.getAllByRole('group');
67
+ expect(groups[0]).toHaveAttribute('name', 'group');
68
+ expect(groups[1]).toHaveAttribute('name', 'group');
69
+ });
70
+
71
+ it('accepts className', () => {
72
+ render(<Collapsible summary="Toggle" className="custom">Content</Collapsible>);
73
+ expect(screen.getByRole('group')).toHaveClass('custom');
74
+ });
75
+ });
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Collapsible } from './Collapsible';
3
+
4
+ const meta = {
5
+ title: 'UI/Collapsible',
6
+ component: Collapsible,
7
+ } satisfies Meta<typeof Collapsible>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ summary: 'What is the return policy?',
15
+ children: 'You can return any item within 30 days of purchase for a full refund.',
16
+ },
17
+ };
18
+
19
+ export const DefaultOpen: Story = {
20
+ args: {
21
+ summary: 'Expanded by default',
22
+ children: 'This panel starts open. Click the summary to collapse it.',
23
+ defaultOpen: true,
24
+ },
25
+ };
26
+
27
+ export const Group: Story = {
28
+ render: () => (
29
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: '480px' }}>
30
+ <Collapsible name="faq" summary="What is your return policy?">
31
+ You can return any item within 30 days of purchase for a full refund.
32
+ </Collapsible>
33
+ <Collapsible name="faq" summary="How long does shipping take?" defaultOpen>
34
+ Standard shipping takes 3–5 business days. Express shipping is available at checkout.
35
+ </Collapsible>
36
+ <Collapsible name="faq" summary="Do you ship internationally?">
37
+ Yes, we ship to over 50 countries. International shipping times vary by destination.
38
+ </Collapsible>
39
+ </div>
40
+ ),
41
+ };
42
+
43
+ export const RichSummary: Story = {
44
+ args: {
45
+ summary: (
46
+ <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
47
+ <strong>Advanced settings</strong>
48
+ <span style={{ fontSize: '0.75rem', opacity: 0.6 }}>optional</span>
49
+ </span>
50
+ ),
51
+ children: (
52
+ <p style={{ margin: 0 }}>
53
+ Configure advanced options here. These settings are optional and have sensible defaults.
54
+ </p>
55
+ ),
56
+ },
57
+ };
@@ -0,0 +1,55 @@
1
+ import { HTMLAttributes, ReactNode, SyntheticEvent } from 'react';
2
+ import css from './Collapsible.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ export interface CollapsibleProps extends Omit<HTMLAttributes<HTMLDetailsElement>, 'onToggle'> {
6
+ /** The trigger label — always visible */
7
+ summary: ReactNode;
8
+ /** The content revealed when expanded */
9
+ children: ReactNode;
10
+ /** Controlled open state */
11
+ open?: boolean;
12
+ /** Uncontrolled initial open state */
13
+ defaultOpen?: boolean;
14
+ /** Called after each toggle with the new open state */
15
+ onToggle?: (open: boolean) => void;
16
+ /**
17
+ * Groups multiple Collapsible elements so only one is open at a time
18
+ * (native <details name="…"> behaviour).
19
+ */
20
+ name?: string;
21
+ className?: string;
22
+ }
23
+
24
+ export function Collapsible({
25
+ summary,
26
+ children,
27
+ open,
28
+ defaultOpen,
29
+ onToggle,
30
+ name,
31
+ className,
32
+ ...rest
33
+ }: Readonly<CollapsibleProps>) {
34
+ const handleToggle = (e: SyntheticEvent<HTMLDetailsElement>) => {
35
+ onToggle?.(e.currentTarget.open);
36
+ };
37
+
38
+ return (
39
+ <details
40
+ className={cn(css.collapsible, className)}
41
+ open={open ?? defaultOpen}
42
+ name={name}
43
+ onToggle={handleToggle}
44
+ {...rest}
45
+ >
46
+ <summary className={css.summary}>
47
+ <span className={css.summaryContent}>{summary}</span>
48
+ <span className={css.icon} aria-hidden="true" />
49
+ </summary>
50
+ <div className={css.content}>
51
+ {children}
52
+ </div>
53
+ </details>
54
+ );
55
+ }
@@ -0,0 +1,2 @@
1
+ export { Collapsible } from './Collapsible';
2
+ export type { CollapsibleProps } from './Collapsible';
@@ -0,0 +1,28 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './DescriptionList.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # DescriptionList
7
+
8
+ Key-value pairs rendered as a semantic `<dl>` element. Supports stacked and inline layouts.
9
+
10
+ ## When to use
11
+ - Product details, order summaries, profile information
12
+ - Any list of labelled values where semantic key-value association matters
13
+
14
+ ## Examples
15
+
16
+ ### Stacked (default)
17
+ <Canvas of={Stories.Stacked} />
18
+
19
+ ### Inline
20
+ <Canvas of={Stories.Inline} />
21
+
22
+ ## Props
23
+
24
+ <ArgTypes of={Stories} />
25
+
26
+ ## Accessibility
27
+
28
+ - Uses native `<dl>`, `<dt>`, `<dd>` — correct semantics without extra ARIA