@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.
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/dist/client.cjs +406 -362
- package/dist/client.css +560 -511
- package/dist/client.d.cts +27 -5
- package/dist/client.d.ts +27 -5
- package/dist/client.js +405 -362
- package/dist/index.cjs +406 -362
- package/dist/index.css +560 -511
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +405 -362
- package/package.json +4 -2
- package/src/components/interaction/Button/Button.mdx +73 -0
- package/src/components/interaction/Button/Button.module.css +12 -20
- package/src/components/interaction/Button/Button.stories.tsx +0 -1
- package/src/components/interaction/Command/Command.mdx +28 -0
- package/src/components/interaction/Command/Command.module.css +2 -1
- package/src/components/interaction/Command/Command.stories.tsx +3 -3
- package/src/components/interaction/Dialog/Dialog.mdx +57 -0
- package/src/components/interaction/Dialog/Dialog.module.css +1 -1
- package/src/components/interaction/Dialog/Dialog.stories.tsx +0 -1
- package/src/components/interaction/Drawer/Drawer.mdx +35 -0
- package/src/components/interaction/Drawer/Drawer.module.css +3 -9
- package/src/components/interaction/Drawer/Drawer.stories.tsx +3 -3
- package/src/components/interaction/Drawer/Drawer.tsx +5 -3
- package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +40 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +5 -5
- package/src/components/interaction/Popover/Popover.mdx +34 -0
- package/src/components/interaction/Popover/Popover.stories.tsx +3 -3
- package/src/components/interaction/Rating/Rating.mdx +38 -0
- package/src/components/interaction/Rating/Rating.stories.tsx +0 -1
- package/src/components/interaction/Toast/Toast.mdx +78 -0
- package/src/components/interaction/Toast/Toast.module.css +21 -7
- package/src/components/interaction/Toast/Toast.stories.tsx +0 -1
- package/src/components/interaction/form/Checkbox/Checkbox.mdx +57 -0
- package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +0 -1
- package/src/components/interaction/form/Combobox/Combobox.mdx +37 -0
- package/src/components/interaction/form/Combobox/Combobox.stories.tsx +0 -1
- package/src/components/interaction/form/FileInput/FileInput.mdx +38 -0
- package/src/components/interaction/form/FileInput/FileInput.stories.tsx +0 -1
- package/src/components/interaction/form/FormInput/FormInput.mdx +54 -0
- package/src/components/interaction/form/FormInput/FormInput.stories.tsx +0 -1
- package/src/components/interaction/form/NumberInput/NumberInput.mdx +38 -0
- package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +0 -1
- package/src/components/interaction/form/Radio/Radio.mdx +60 -0
- package/src/components/interaction/form/Radio/Radio.module.css +24 -11
- package/src/components/interaction/form/Radio/Radio.stories.tsx +5 -5
- package/src/components/interaction/form/Radio/Radio.tsx +6 -2
- package/src/components/interaction/form/Select/Select.mdx +58 -0
- package/src/components/interaction/form/Select/Select.stories.tsx +0 -1
- package/src/components/interaction/form/Slider/Slider.mdx +58 -0
- package/src/components/interaction/form/Slider/Slider.stories.tsx +0 -1
- package/src/components/interaction/form/Switch/Switch.mdx +61 -0
- package/src/components/interaction/form/Switch/Switch.module.css +6 -10
- package/src/components/interaction/form/Switch/Switch.stories.tsx +0 -1
- package/src/components/interaction/form/Textarea/Textarea.mdx +41 -0
- package/src/components/interaction/form/Textarea/Textarea.stories.tsx +0 -1
- package/src/components/layout/ButtonGroup/ButtonGroup.mdx +47 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.module.css +10 -10
- package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +1 -2
- package/src/components/layout/Card/Card.mdx +58 -0
- package/src/components/layout/Card/Card.stories.tsx +0 -1
- package/src/components/layout/IconWrapper/IconWrapper.mdx +39 -0
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +0 -1
- package/src/components/layout/SectionHeader/SectionHeader.mdx +38 -0
- package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +0 -1
- package/src/components/ui/Accordion/Accordion.mdx +40 -0
- package/src/components/ui/Accordion/Accordion.stories.tsx +0 -1
- package/src/components/ui/Alert/Alert.mdx +63 -0
- package/src/components/ui/Alert/Alert.module.css +3 -7
- package/src/components/ui/Alert/Alert.stories.tsx +0 -1
- package/src/components/ui/Avatar/Avatar.mdx +53 -0
- package/src/components/ui/Avatar/Avatar.module.css +4 -8
- package/src/components/ui/Avatar/Avatar.stories.tsx +0 -1
- package/src/components/ui/Badge/Badge.mdx +62 -0
- package/src/components/ui/Badge/Badge.module.css +4 -10
- package/src/components/ui/Badge/Badge.stories.tsx +0 -1
- package/src/components/ui/Breadcrumb/Breadcrumb.mdx +36 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +0 -1
- package/src/components/ui/Calendar/Calendar.mdx +34 -0
- package/src/components/ui/Calendar/Calendar.stories.tsx +0 -1
- package/src/components/ui/Carousel/Carousel.mdx +31 -0
- package/src/components/ui/Carousel/Carousel.stories.tsx +0 -1
- package/src/components/ui/Collapsible/Collapsible.mdx +59 -0
- package/src/components/ui/Collapsible/Collapsible.module.css +76 -0
- package/src/components/ui/Collapsible/Collapsible.spec.tsx +75 -0
- package/src/components/ui/Collapsible/Collapsible.stories.tsx +57 -0
- package/src/components/ui/Collapsible/Collapsible.tsx +55 -0
- package/src/components/ui/Collapsible/index.ts +2 -0
- package/src/components/ui/DescriptionList/DescriptionList.mdx +28 -0
- package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +0 -1
- package/src/components/ui/Link/Link.mdx +44 -0
- package/src/components/ui/Link/Link.module.css +2 -5
- package/src/components/ui/Link/Link.stories.tsx +0 -1
- package/src/components/ui/Loading/Loading.mdx +52 -0
- package/src/components/ui/Loading/Loading.stories.tsx +0 -1
- package/src/components/ui/NotificationBanner/NotificationBanner.mdx +45 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.module.css +3 -7
- package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +2 -2
- package/src/components/ui/Pagination/Pagination.mdx +39 -0
- package/src/components/ui/Pagination/Pagination.module.css +3 -7
- package/src/components/ui/Pagination/Pagination.stories.tsx +0 -1
- package/src/components/ui/Progress/Progress.mdx +52 -0
- package/src/components/ui/Progress/Progress.module.css +1 -3
- package/src/components/ui/Progress/Progress.stories.tsx +0 -1
- package/src/components/ui/ProgressCircle/ProgressCircle.mdx +46 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +0 -1
- package/src/components/ui/Separator/Separator.mdx +29 -0
- package/src/components/ui/Separator/Separator.stories.tsx +0 -1
- package/src/components/ui/Skeleton/Skeleton.mdx +36 -0
- package/src/components/ui/Skeleton/Skeleton.stories.tsx +0 -1
- package/src/components/ui/SkipLink/SkipLink.mdx +34 -0
- package/src/components/ui/SkipLink/SkipLink.stories.tsx +0 -1
- package/src/components/ui/Table/Table.mdx +38 -0
- package/src/components/ui/Table/Table.stories.tsx +0 -1
- package/src/components/ui/Tabs/Tabs.mdx +40 -0
- package/src/components/ui/Tabs/Tabs.module.css +2 -5
- package/src/components/ui/Tabs/Tabs.stories.tsx +0 -1
- package/src/components/ui/Tooltip/Tooltip.mdx +43 -0
- package/src/components/ui/Tooltip/Tooltip.module.css +2 -5
- package/src/components/ui/Tooltip/Tooltip.stories.tsx +6 -6
- package/src/components/ui/Typography/Typography.mdx +41 -0
- package/src/components/ui/Typography/Typography.stories.tsx +0 -1
- package/src/index.ts +2 -0
- package/src/static/logo.svg +8 -0
- package/src/stories/Introduction.css +17 -0
- 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
|
|
|
@@ -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 {
|
|
@@ -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"`
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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,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
|