@boostdev/design-system-components 0.1.14 → 0.1.16
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/dist/client.cjs +403 -361
- package/dist/client.css +485 -424
- package/dist/client.d.cts +25 -4
- package/dist/client.d.ts +25 -4
- package/dist/client.js +402 -361
- package/dist/index.cjs +403 -361
- package/dist/index.css +485 -424
- package/dist/index.d.cts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.js +402 -361
- package/package.json +1 -1
- package/src/components/interaction/form/Switch/Switch.mdx +7 -2
- package/src/components/interaction/form/Switch/Switch.module.css +6 -6
- package/src/components/interaction/form/Switch/Switch.spec.tsx +23 -0
- package/src/components/interaction/form/Switch/Switch.stories.tsx +8 -0
- package/src/components/interaction/form/Switch/Switch.tsx +5 -2
- package/src/components/interaction/form/atoms/Label.module.css +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/Progress/Progress.module.css +0 -4
- package/src/index.ts +2 -0
- package/src/stories/Introduction.mdx +2 -2
package/package.json
CHANGED
|
@@ -37,6 +37,9 @@ Toggle control for binary settings that take immediate effect — without requir
|
|
|
37
37
|
### Disabled + checked
|
|
38
38
|
<Canvas of={Stories.DisabledChecked} />
|
|
39
39
|
|
|
40
|
+
### With prefix label
|
|
41
|
+
<Canvas of={Stories.WithPrefix} />
|
|
42
|
+
|
|
40
43
|
## Props
|
|
41
44
|
|
|
42
45
|
<ArgTypes of={Stories} />
|
|
@@ -49,8 +52,10 @@ Toggle control for binary settings that take immediate effect — without requir
|
|
|
49
52
|
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
|
50
53
|
</thead>
|
|
51
54
|
<tbody>
|
|
52
|
-
<tr><td>`--
|
|
53
|
-
<tr><td>`--
|
|
55
|
+
<tr><td>`--switch_track-bg`</td><td>`var(--color_grey--subtle)`</td><td>Track background when off</td></tr>
|
|
56
|
+
<tr><td>`--switch_thumb-bg`</td><td>`var(--color_grey)`</td><td>Thumb colour when off (also track outline)</td></tr>
|
|
57
|
+
<tr><td>`--switch_track-bg--active`</td><td>`var(--color_active--subtle)`</td><td>Track background when on</td></tr>
|
|
58
|
+
<tr><td>`--switch_thumb-bg--active`</td><td>`var(--color_active--strong)`</td><td>Thumb colour when on (also track outline)</td></tr>
|
|
54
59
|
</tbody>
|
|
55
60
|
</table>
|
|
56
61
|
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
width: var(--switch_track-width);
|
|
54
54
|
height: var(--switch_track-height);
|
|
55
55
|
border-radius: 999px;
|
|
56
|
-
background-color: var(--
|
|
56
|
+
background-color: var(--switch_track-bg, var(--color_grey--subtle));
|
|
57
57
|
padding-inline: var(--switch_track-pad);
|
|
58
58
|
pointer-events: none;
|
|
59
59
|
transition: var(--animation_transition);
|
|
60
|
-
outline: 1px solid var(--
|
|
60
|
+
outline: 1px solid var(--switch_thumb-bg, var(--color_grey));
|
|
61
61
|
outline-offset: var(--outline_offset);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
width: var(--switch_thumb-size);
|
|
68
68
|
height: var(--switch_thumb-size);
|
|
69
69
|
border-radius: 50%;
|
|
70
|
-
background-color: var(--
|
|
70
|
+
background-color: var(--switch_thumb-bg, var(--color_grey));
|
|
71
71
|
box-shadow: var(--shadow_s);
|
|
72
72
|
transition: var(--animation_transition);
|
|
73
73
|
transform: translateX(0);
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
|
|
81
81
|
/* Checked state via CSS sibling combinator */
|
|
82
82
|
.switch:checked + .track {
|
|
83
|
-
background-color: var(--
|
|
84
|
-
outline-color: var(--
|
|
83
|
+
background-color: var(--switch_track-bg--active, var(--color_active--subtle));
|
|
84
|
+
outline-color: var(--switch_thumb-bg--active, var(--color_active--strong));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
.switch:checked + .track .thumb {
|
|
88
|
-
background-color: var(--
|
|
88
|
+
background-color: var(--switch_thumb-bg--active, var(--color_active--strong));
|
|
89
89
|
transform: translateX(var(--switch_thumb-size));
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -45,4 +45,27 @@ describe('Switch', () => {
|
|
|
45
45
|
render(<Switch label="Disabled" name="disabled" disabled />);
|
|
46
46
|
expect(screen.getByRole('switch')).toBeDisabled();
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it('renders a prefix label when provided', () => {
|
|
50
|
+
render(<Switch label="On" name="theme" prefix="Off" />);
|
|
51
|
+
expect(screen.getByText('Off')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('On')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('associates error message via aria-describedby', () => {
|
|
56
|
+
render(<Switch label="Accept" name="accept" error="Required" />);
|
|
57
|
+
const input = screen.getByRole('switch');
|
|
58
|
+
const error = screen.getByText('Required');
|
|
59
|
+
expect(input).toHaveAttribute('aria-describedby', expect.stringContaining(error.id));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders small size', () => {
|
|
63
|
+
render(<Switch label="Toggle" name="size-s" size="small" />);
|
|
64
|
+
expect(screen.getByRole('switch').closest('.switchGroup')).toHaveClass('--size_small');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders large size', () => {
|
|
68
|
+
render(<Switch label="Toggle" name="size-l" size="large" />);
|
|
69
|
+
expect(screen.getByRole('switch').closest('.switchGroup')).toHaveClass('--size_large');
|
|
70
|
+
});
|
|
48
71
|
});
|
|
@@ -19,6 +19,14 @@ export const WithError: Story = { args: { label: 'Required toggle', name: 'requi
|
|
|
19
19
|
export const Disabled: Story = { args: { label: 'Unavailable', name: 'unavailable', disabled: true } };
|
|
20
20
|
export const DisabledChecked: Story = { args: { label: 'Always on', name: 'always-on', defaultChecked: true, disabled: true } };
|
|
21
21
|
|
|
22
|
+
export const WithPrefix: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
label: 'On',
|
|
25
|
+
name: 'theme',
|
|
26
|
+
prefix: 'Off',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
22
30
|
export const AllSizes: Story = {
|
|
23
31
|
render: () => (
|
|
24
32
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {InputHTMLAttributes, ReactNode, useId} from 'react';
|
|
2
2
|
import css from './Switch.module.css';
|
|
3
3
|
import { cn } from '@boostdev/design-system-foundation';
|
|
4
4
|
import { InputContainer } from '../atoms/InputContainer';
|
|
5
5
|
import { Label } from '../atoms/Label';
|
|
6
6
|
import { Message } from '../atoms/Message';
|
|
7
7
|
|
|
8
|
-
interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
|
8
|
+
interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size' | 'prefix'> {
|
|
9
9
|
label: string;
|
|
10
10
|
name: string;
|
|
11
11
|
size?: 'small' | 'medium' | 'large';
|
|
12
12
|
error?: string;
|
|
13
13
|
hint?: string;
|
|
14
14
|
className?: string;
|
|
15
|
+
prefix?: ReactNode;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function Switch({
|
|
@@ -21,6 +22,7 @@ export function Switch({
|
|
|
21
22
|
error,
|
|
22
23
|
hint,
|
|
23
24
|
className,
|
|
25
|
+
prefix,
|
|
24
26
|
...props
|
|
25
27
|
}: Readonly<SwitchProps>) {
|
|
26
28
|
const id = name + useId();
|
|
@@ -31,6 +33,7 @@ export function Switch({
|
|
|
31
33
|
return (
|
|
32
34
|
<InputContainer className={cn(css.switchGroup, css[`--size_${size}`], className)}>
|
|
33
35
|
<div className={css.inputWrapper}>
|
|
36
|
+
{prefix && <Label id={id} label={prefix}/>}
|
|
34
37
|
<div className={css.trackWrapper}>
|
|
35
38
|
<input
|
|
36
39
|
type="checkbox"
|
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { Avatar } from './components/ui/Avatar';
|
|
|
6
6
|
export { Badge } from './components/ui/Badge';
|
|
7
7
|
export { Breadcrumb } from './components/ui/Breadcrumb';
|
|
8
8
|
export type { BreadcrumbItem } from './components/ui/Breadcrumb';
|
|
9
|
+
export { Collapsible } from './components/ui/Collapsible';
|
|
10
|
+
export type { CollapsibleProps } from './components/ui/Collapsible';
|
|
9
11
|
export { Calendar } from './components/ui/Calendar';
|
|
10
12
|
export { Carousel } from './components/ui/Carousel';
|
|
11
13
|
export { DescriptionList } from './components/ui/DescriptionList';
|
|
@@ -20,8 +20,8 @@ import {Card} from "../components/layout/Card/index.ts";
|
|
|
20
20
|
Accessible, token-driven React components. Zero extra runtime.
|
|
21
21
|
</p>
|
|
22
22
|
<ButtonGroup variant="content">
|
|
23
|
-
<Button variant="secondary" href="https://www.npmjs.com/package/@boostdev/foundation">DS Foundation</Button>
|
|
24
|
-
<Button variant="secondary" href="https://www.npmjs.com/package/@boostdev/components">DS Components</Button>
|
|
23
|
+
<Button variant="secondary" href="https://www.npmjs.com/package/@boostdev/design-system-foundation">DS Foundation</Button>
|
|
24
|
+
<Button variant="secondary" href="https://www.npmjs.com/package/@boostdev/design-system-components">DS Components</Button>
|
|
25
25
|
</ButtonGroup>
|
|
26
26
|
</div>
|
|
27
27
|
---
|