@boostdev/design-system-components 0.1.16 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-components",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "React",
@@ -26,3 +26,4 @@ Keyboard-first command palette for searching and executing actions. Commonly tri
26
26
  - Results list uses `role="listbox"` with `role="option"` items
27
27
  - `aria-activedescendant` tracks the highlighted option
28
28
  - Keyboard: `↑`/`↓` to navigate, `Enter` to execute, `Escape` to dismiss
29
+ - Body scroll is locked while open (`overflow: hidden`) and restored on close or unmount
@@ -10,6 +10,7 @@ beforeEach(() => {
10
10
  HTMLDialogElement.prototype.close = vi.fn().mockImplementation(function (this: HTMLDialogElement) {
11
11
  this.removeAttribute('open');
12
12
  });
13
+ document.body.style.overflow = '';
13
14
  });
14
15
 
15
16
  const items: CommandItem[] = [
@@ -57,4 +58,21 @@ describe('Command', () => {
57
58
  render(<Command isOpen items={items} onClose={() => {}} />);
58
59
  expect(screen.getByText('Save current file')).toBeInTheDocument();
59
60
  });
61
+
62
+ it('locks body scroll when open', () => {
63
+ render(<Command isOpen items={items} onClose={() => {}} />);
64
+ expect(document.body.style.overflow).toBe('hidden');
65
+ });
66
+
67
+ it('restores body scroll when closed', () => {
68
+ const { rerender } = render(<Command isOpen items={items} onClose={() => {}} />);
69
+ rerender(<Command isOpen={false} items={items} onClose={() => {}} />);
70
+ expect(document.body.style.overflow).toBe('');
71
+ });
72
+
73
+ it('restores body scroll on unmount', () => {
74
+ const { unmount } = render(<Command isOpen items={items} onClose={() => {}} />);
75
+ unmount();
76
+ expect(document.body.style.overflow).toBe('');
77
+ });
60
78
  });
@@ -56,12 +56,17 @@ export function Command({
56
56
  if (!dialog) return;
57
57
  if (isOpen) {
58
58
  dialog.showModal();
59
+ document.body.style.overflow = 'hidden';
59
60
  setQuery('');
60
61
  setActiveIndex(0);
61
62
  setTimeout(() => inputRef.current?.focus(), 0);
62
63
  } else if (dialog.open) {
63
64
  dialog.close();
65
+ document.body.style.overflow = '';
64
66
  }
67
+ return () => {
68
+ document.body.style.overflow = '';
69
+ };
65
70
  }, [isOpen]);
66
71
 
67
72
  useEffect(() => {
@@ -10,6 +10,7 @@ beforeEach(() => {
10
10
  HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
11
11
  this.removeAttribute('open');
12
12
  });
13
+ document.body.style.overflow = '';
13
14
  });
14
15
 
15
16
  describe('Dialog', () => {
@@ -40,4 +41,21 @@ describe('Dialog', () => {
40
41
  render(<Dialog isOpen={false}><span>Content</span></Dialog>);
41
42
  expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled();
42
43
  });
44
+
45
+ it('locks body scroll when open', () => {
46
+ render(<Dialog isOpen><span>Content</span></Dialog>);
47
+ expect(document.body.style.overflow).toBe('hidden');
48
+ });
49
+
50
+ it('restores body scroll when closed', () => {
51
+ const { rerender } = render(<Dialog isOpen><span>Content</span></Dialog>);
52
+ rerender(<Dialog isOpen={false}><span>Content</span></Dialog>);
53
+ expect(document.body.style.overflow).toBe('');
54
+ });
55
+
56
+ it('restores body scroll on unmount', () => {
57
+ const { unmount } = render(<Dialog isOpen><span>Content</span></Dialog>);
58
+ unmount();
59
+ expect(document.body.style.overflow).toBe('');
60
+ });
43
61
  });
@@ -10,6 +10,7 @@ beforeEach(() => {
10
10
  HTMLDialogElement.prototype.close = vi.fn().mockImplementation(function (this: HTMLDialogElement) {
11
11
  this.removeAttribute('open');
12
12
  });
13
+ document.body.style.overflow = '';
13
14
  });
14
15
 
15
16
  describe('Drawer', () => {
@@ -40,4 +41,21 @@ describe('Drawer', () => {
40
41
  rerender(<Drawer isOpen={false} title="Test" onClose={() => {}}>Body</Drawer>);
41
42
  expect(HTMLDialogElement.prototype.close).toHaveBeenCalled();
42
43
  });
44
+
45
+ it('locks body scroll when open', () => {
46
+ render(<Drawer isOpen title="Settings" onClose={() => {}}>Content</Drawer>);
47
+ expect(document.body.style.overflow).toBe('hidden');
48
+ });
49
+
50
+ it('restores body scroll when closed', () => {
51
+ const { rerender } = render(<Drawer isOpen title="Test" onClose={() => {}}>Body</Drawer>);
52
+ rerender(<Drawer isOpen={false} title="Test" onClose={() => {}}>Body</Drawer>);
53
+ expect(document.body.style.overflow).toBe('');
54
+ });
55
+
56
+ it('restores body scroll on unmount', () => {
57
+ const { unmount } = render(<Drawer isOpen title="Test" onClose={() => {}}>Body</Drawer>);
58
+ unmount();
59
+ expect(document.body.style.overflow).toBe('');
60
+ });
43
61
  });
@@ -28,9 +28,14 @@ export function Drawer({
28
28
  if (!dialog) return;
29
29
  if (isOpen) {
30
30
  dialog.showModal();
31
+ document.body.style.overflow = 'hidden';
31
32
  } else if (dialog.open) {
32
33
  dialog.close();
34
+ document.body.style.overflow = '';
33
35
  }
36
+ return () => {
37
+ document.body.style.overflow = '';
38
+ };
34
39
  }, [isOpen]);
35
40
 
36
41
  // Close on backdrop click
@@ -0,0 +1,64 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './SegmentedControl.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # SegmentedControl
7
+
8
+ Single-select control where all options sit inside a shared track and a sliding thumb highlights the active choice. Based on radio inputs.
9
+
10
+ All options have **equal width** — the widest label determines the size for all. In the `large` variant, labels may wrap.
11
+
12
+ ## When to use
13
+ - Switching between a small set of mutually exclusive views or modes (2–5 options)
14
+ - When all options should be visible simultaneously with clear visual feedback
15
+
16
+ ## When not to use
17
+ - More than 5 options — use `Select` or `Tabs`
18
+ - Independent toggles — use `Switch` or `Checkbox`
19
+
20
+ ## Examples
21
+
22
+ ### Default
23
+ <Canvas of={Stories.Default} />
24
+
25
+ ### Small
26
+ <Canvas of={Stories.Small} />
27
+
28
+ ### Large (wrapping labels)
29
+ <Canvas of={Stories.WrappingLabels} />
30
+
31
+ ### With disabled option
32
+ <Canvas of={Stories.WithDisabledOption} />
33
+
34
+ ### All disabled
35
+ <Canvas of={Stories.AllDisabled} />
36
+
37
+ ### Controlled
38
+ <Canvas of={Stories.Controlled} />
39
+
40
+ ## Props
41
+
42
+ <ArgTypes of={Stories} />
43
+
44
+ ## CSS variables
45
+
46
+ <table>
47
+ <thead>
48
+ <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
49
+ </thead>
50
+ <tbody>
51
+ <tr><td>`--control_track-bg`</td><td>`var(--color_bg--subtle)`</td><td>Track background</td></tr>
52
+ <tr><td>`--control_thumb-bg`</td><td>`var(--color_bg)`</td><td>Thumb (active indicator) background</td></tr>
53
+ <tr><td>`--control_label-color`</td><td>`var(--color_on-bg--subtle)`</td><td>Unselected label colour</td></tr>
54
+ <tr><td>`--control_label-color--active`</td><td>`var(--color_on-bg)`</td><td>Selected label colour</td></tr>
55
+ <tr><td>`--control_label-color--hover`</td><td>`var(--color_on-bg)`</td><td>Hovered (unselected) label colour</td></tr>
56
+ </tbody>
57
+ </table>
58
+
59
+ ## Accessibility
60
+
61
+ - Each option is a native `<input type="radio">` — grouped by `name`, keyboard navigable with arrow keys
62
+ - The visual thumb is `aria-hidden` and purely decorative
63
+ - Labels are `<label>` elements with `htmlFor` pointing to each radio
64
+ - Disabled options remain focusable for screen reader discovery but cannot be activated
@@ -0,0 +1,99 @@
1
+ @layer component {
2
+ .control {
3
+ position: relative;
4
+ display: inline-grid;
5
+ grid-auto-flow: column;
6
+ grid-auto-columns: 1fr;
7
+ padding: var(--space_xxxs);
8
+ background-color: var(--control_track-bg, var(--color_bg--subtle));
9
+ border-radius: var(--border_radius--m);
10
+ gap: 0;
11
+ }
12
+
13
+ .thumb {
14
+ position: absolute;
15
+ top: var(--space_xxxs);
16
+ bottom: var(--space_xxxs);
17
+ left: var(--space_xxxs);
18
+ width: calc((100% - 2 * var(--space_xxxs)) / var(--control_count, 1));
19
+ border-radius: var(--border_radius--s);
20
+ background-color: var(--control_thumb-bg, var(--color_bg));
21
+ box-shadow: var(--shadow_s);
22
+ pointer-events: none;
23
+ transform: translateX(calc(var(--control_selected-index, 0) * 100%));
24
+ transition: transform var(--animation_transition-duration) var(--animation_easing);
25
+ }
26
+
27
+ .item {
28
+ position: relative;
29
+ z-index: 1;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ cursor: pointer;
34
+ border-radius: var(--border_radius--s);
35
+ transition: var(--animation_transition);
36
+ }
37
+
38
+ .item.--disabled {
39
+ opacity: 0.4;
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .radio {
44
+ position: absolute;
45
+ opacity: 0;
46
+ width: 0;
47
+ height: 0;
48
+ margin: 0;
49
+ }
50
+
51
+ .label {
52
+ display: block;
53
+ padding: var(--space_xs) var(--space_m);
54
+ white-space: nowrap;
55
+ font-size: var(--font_size--body);
56
+ line-height: var(--font_line-height--body);
57
+ color: var(--control_label-color, var(--color_on-bg--subtle));
58
+ transition: color var(--animation_transition-duration) var(--animation_easing);
59
+ pointer-events: none;
60
+ user-select: none;
61
+ }
62
+
63
+ .control.--size_small .label {
64
+ padding: var(--space_xxs) var(--space_s);
65
+ font-size: var(--font_size--body--s);
66
+ }
67
+
68
+ .control.--size_large .label {
69
+ padding: var(--space_s) var(--space_m);
70
+ white-space: normal;
71
+ text-align: center;
72
+ }
73
+
74
+ .item.--active .label {
75
+ color: var(--control_label-color--active, var(--color_on-bg));
76
+ }
77
+
78
+ .radio:focus-visible ~ .label {
79
+ outline: var(--outline_default);
80
+ outline-offset: calc(var(--outline_offset) * -1);
81
+ border-radius: var(--border_radius--s);
82
+ }
83
+
84
+ @media (hover: hover) and (pointer: fine) {
85
+ .item:not(.--active, .--disabled):hover .label {
86
+ color: var(--control_label-color--hover, var(--color_on-bg));
87
+ }
88
+ }
89
+
90
+ @media (prefers-reduced-motion: reduce) {
91
+ .thumb {
92
+ transition: none;
93
+ }
94
+
95
+ .label {
96
+ transition: none;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,87 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { SegmentedControl } from './SegmentedControl';
4
+
5
+ const options = [
6
+ { value: 'day', label: 'Day' },
7
+ { value: 'week', label: 'Week' },
8
+ { value: 'month', label: 'Month' },
9
+ ];
10
+
11
+ describe('SegmentedControl', () => {
12
+ it('renders all option labels', () => {
13
+ render(<SegmentedControl name="view" options={options} />);
14
+ expect(screen.getByText('Day')).toBeInTheDocument();
15
+ expect(screen.getByText('Week')).toBeInTheDocument();
16
+ expect(screen.getByText('Month')).toBeInTheDocument();
17
+ });
18
+
19
+ it('selects the first option by default', () => {
20
+ render(<SegmentedControl name="view" options={options} />);
21
+ expect(screen.getByRole('radio', { name: 'Day' })).toBeChecked();
22
+ expect(screen.getByRole('radio', { name: 'Week' })).not.toBeChecked();
23
+ });
24
+
25
+ it('selects defaultValue when provided', () => {
26
+ render(<SegmentedControl name="view" options={options} defaultValue="week" />);
27
+ expect(screen.getByRole('radio', { name: 'Week' })).toBeChecked();
28
+ expect(screen.getByRole('radio', { name: 'Day' })).not.toBeChecked();
29
+ });
30
+
31
+ it('selects an option on click', async () => {
32
+ const user = userEvent.setup();
33
+ render(<SegmentedControl name="view" options={options} />);
34
+ await user.click(screen.getByRole('radio', { name: 'Month' }));
35
+ expect(screen.getByRole('radio', { name: 'Month' })).toBeChecked();
36
+ expect(screen.getByRole('radio', { name: 'Day' })).not.toBeChecked();
37
+ });
38
+
39
+ it('calls onChange with the selected value', async () => {
40
+ const user = userEvent.setup();
41
+ const onChange = vi.fn();
42
+ render(<SegmentedControl name="view" options={options} onChange={onChange} />);
43
+ await user.click(screen.getByRole('radio', { name: 'Week' }));
44
+ expect(onChange).toHaveBeenCalledWith('week');
45
+ });
46
+
47
+ it('respects controlled value', () => {
48
+ render(<SegmentedControl name="view" options={options} value="month" onChange={() => {}} />);
49
+ expect(screen.getByRole('radio', { name: 'Month' })).toBeChecked();
50
+ });
51
+
52
+ it('disables a specific option', () => {
53
+ const opts = [
54
+ { value: 'a', label: 'A' },
55
+ { value: 'b', label: 'B', disabled: true },
56
+ ];
57
+ render(<SegmentedControl name="opt" options={opts} />);
58
+ expect(screen.getByRole('radio', { name: 'B' })).toBeDisabled();
59
+ expect(screen.getByRole('radio', { name: 'A' })).not.toBeDisabled();
60
+ });
61
+
62
+ it('disables all options when disabled prop is set', () => {
63
+ render(<SegmentedControl name="view" options={options} disabled />);
64
+ screen.getAllByRole('radio').forEach(radio => expect(radio).toBeDisabled());
65
+ });
66
+
67
+ it('sets --control_count on the container', () => {
68
+ const { container } = render(<SegmentedControl name="view" options={options} />);
69
+ const control = container.firstChild as HTMLElement;
70
+ expect(control.style.getPropertyValue('--control_count')).toBe('3');
71
+ });
72
+
73
+ it('sets --control_selected-index to the selected option index', async () => {
74
+ const user = userEvent.setup();
75
+ const { container } = render(<SegmentedControl name="view" options={options} defaultValue="day" />);
76
+ const control = container.firstChild as HTMLElement;
77
+ expect(control.style.getPropertyValue('--control_selected-index')).toBe('0');
78
+ await user.click(screen.getByRole('radio', { name: 'Month' }));
79
+ expect(control.style.getPropertyValue('--control_selected-index')).toBe('2');
80
+ });
81
+
82
+ it('renders all radios in the same name group', () => {
83
+ render(<SegmentedControl name="view" options={options} />);
84
+ const radios = screen.getAllByRole('radio');
85
+ radios.forEach(r => expect(r).toHaveAttribute('name', expect.stringContaining('view')));
86
+ });
87
+ });
@@ -0,0 +1,110 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { SegmentedControl } from './SegmentedControl';
4
+
5
+ const meta = {
6
+ title: 'Form/SegmentedControl',
7
+ component: SegmentedControl,
8
+ argTypes: {
9
+ size: { control: 'radio', options: ['small', 'medium', 'large'] },
10
+ },
11
+ } satisfies Meta<typeof SegmentedControl>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ args: {
18
+ name: 'view',
19
+ options: [
20
+ { value: 'day', label: 'Day' },
21
+ { value: 'week', label: 'Week' },
22
+ { value: 'month', label: 'Month' },
23
+ ],
24
+ defaultValue: 'week',
25
+ },
26
+ };
27
+
28
+ export const Small: Story = {
29
+ args: {
30
+ name: 'size-s',
31
+ size: 'small',
32
+ options: [
33
+ { value: 'list', label: 'List' },
34
+ { value: 'grid', label: 'Grid' },
35
+ ],
36
+ defaultValue: 'list',
37
+ },
38
+ };
39
+
40
+ export const Large: Story = {
41
+ args: {
42
+ name: 'size-l',
43
+ size: 'large',
44
+ options: [
45
+ { value: 'personal', label: 'Personal plan' },
46
+ { value: 'team', label: 'Team plan' },
47
+ { value: 'enterprise', label: 'Enterprise plan' },
48
+ ],
49
+ defaultValue: 'team',
50
+ },
51
+ };
52
+
53
+ export const WrappingLabels: Story = {
54
+ args: {
55
+ name: 'wrap',
56
+ size: 'large',
57
+ options: [
58
+ { value: 'solar', label: 'Solar energy only' },
59
+ { value: 'wind', label: 'Wind energy only' },
60
+ { value: 'mixed', label: 'Mix of sustainable sources' },
61
+ ],
62
+ defaultValue: 'mixed',
63
+ },
64
+ decorators: [Story => <div style={{ maxWidth: '360px' }}><Story /></div>],
65
+ };
66
+
67
+ export const WithDisabledOption: Story = {
68
+ args: {
69
+ name: 'disabled-opt',
70
+ options: [
71
+ { value: 'active', label: 'Active' },
72
+ { value: 'paused', label: 'Paused' },
73
+ { value: 'archived', label: 'Archived', disabled: true },
74
+ ],
75
+ defaultValue: 'active',
76
+ },
77
+ };
78
+
79
+ export const AllDisabled: Story = {
80
+ args: {
81
+ name: 'all-disabled',
82
+ options: [
83
+ { value: 'a', label: 'Option A' },
84
+ { value: 'b', label: 'Option B' },
85
+ ],
86
+ defaultValue: 'a',
87
+ disabled: true,
88
+ },
89
+ };
90
+
91
+ export const Controlled: Story = {
92
+ render: () => {
93
+ const [value, setValue] = useState('month');
94
+ return (
95
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
96
+ <SegmentedControl
97
+ name="controlled"
98
+ value={value}
99
+ onChange={setValue}
100
+ options={[
101
+ { value: 'day', label: 'Day' },
102
+ { value: 'week', label: 'Week' },
103
+ { value: 'month', label: 'Month' },
104
+ ]}
105
+ />
106
+ <span style={{ fontSize: '0.875rem', opacity: 0.6 }}>Selected: {value}</span>
107
+ </div>
108
+ );
109
+ },
110
+ };
@@ -0,0 +1,89 @@
1
+ import { InputHTMLAttributes, ReactNode, useId, useState } from 'react';
2
+ import css from './SegmentedControl.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ export interface SegmentedControlOption {
6
+ value: string;
7
+ label: ReactNode;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface SegmentedControlProps
12
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'size'> {
13
+ name: string;
14
+ options: SegmentedControlOption[];
15
+ /** Controlled selected value */
16
+ value?: string;
17
+ /** Uncontrolled initial value (defaults to first option) */
18
+ defaultValue?: string;
19
+ onChange?: (value: string) => void;
20
+ disabled?: boolean;
21
+ size?: 'small' | 'medium' | 'large';
22
+ className?: string;
23
+ }
24
+
25
+ export function SegmentedControl({
26
+ name,
27
+ options,
28
+ value,
29
+ defaultValue,
30
+ onChange,
31
+ disabled,
32
+ size = 'medium',
33
+ className,
34
+ ...rest
35
+ }: Readonly<SegmentedControlProps>) {
36
+ const baseId = name + useId();
37
+
38
+ const [internalValue, setInternalValue] = useState<string>(
39
+ defaultValue ?? options[0]?.value ?? '',
40
+ );
41
+
42
+ const activeValue = value ?? internalValue;
43
+ const selectedIndex = options.findIndex(o => o.value === activeValue);
44
+
45
+ const handleChange = (optionValue: string) => {
46
+ if (value === undefined) setInternalValue(optionValue);
47
+ onChange?.(optionValue);
48
+ };
49
+
50
+ return (
51
+ <div
52
+ className={cn(css.control, css[`--size_${size}`], className)}
53
+ style={{
54
+ '--control_count': options.length,
55
+ '--control_selected-index': Math.max(0, selectedIndex),
56
+ } as React.CSSProperties}
57
+ >
58
+ {/* Sliding thumb — purely visual */}
59
+ <span className={css.thumb} aria-hidden="true" />
60
+
61
+ {options.map((option, index) => {
62
+ const id = `${baseId}-${index}`;
63
+ const isChecked = option.value === activeValue;
64
+ const isDisabled = disabled || option.disabled;
65
+
66
+ return (
67
+ <label
68
+ key={option.value}
69
+ htmlFor={id}
70
+ className={cn(css.item, isChecked && css['--active'], isDisabled && css['--disabled'])}
71
+ >
72
+ <input
73
+ {...rest}
74
+ type="radio"
75
+ id={id}
76
+ name={name}
77
+ value={option.value}
78
+ checked={isChecked}
79
+ disabled={isDisabled}
80
+ className={css.radio}
81
+ onChange={() => handleChange(option.value)}
82
+ />
83
+ <span className={css.label}>{option.label}</span>
84
+ </label>
85
+ );
86
+ })}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,2 @@
1
+ export { SegmentedControl } from './SegmentedControl';
2
+ export type { SegmentedControlProps, SegmentedControlOption } from './SegmentedControl';
package/src/index.ts CHANGED
@@ -48,6 +48,8 @@ export { FileInput } from './components/interaction/form/FileInput';
48
48
  export { FormInput } from './components/interaction/form/FormInput';
49
49
  export { NumberInput } from './components/interaction/form/NumberInput';
50
50
  export { Radio } from './components/interaction/form/Radio';
51
+ export { SegmentedControl } from './components/interaction/form/SegmentedControl';
52
+ export type { SegmentedControlProps, SegmentedControlOption } from './components/interaction/form/SegmentedControl';
51
53
  export { Select } from './components/interaction/form/Select';
52
54
  export type { SelectOption } from './components/interaction/form/Select';
53
55
  export { Slider } from './components/interaction/form/Slider';
@@ -55,8 +55,8 @@ import {Card} from "../components/layout/Card/index.ts";
55
55
  <strong>UI</strong>
56
56
  <div className="cardContent">
57
57
  Badge · Typography · Alert · Avatar · Loading · Skeleton · Separator · Tooltip · Progress ·
58
- ProgressCircle · Tabs · Breadcrumb · Accordion · Pagination · Link · Table · DescriptionList ·
59
- NotificationBanner · SkipLink · Carousel · Calendar
58
+ ProgressCircle · Tabs · Breadcrumb · Accordion · Collapsible · Pagination · Link · Table ·
59
+ DescriptionList · NotificationBanner · SkipLink · Carousel · Calendar
60
60
  </div>
61
61
  </Card>
62
62
 
@@ -70,7 +70,8 @@ import {Card} from "../components/layout/Card/index.ts";
70
70
  <Card className="card">
71
71
  <strong>Form</strong>
72
72
  <div className="cardContent">
73
- FormInput · Checkbox · Radio · Switch · Select · Textarea · Slider · NumberInput · FileInput · Combobox
73
+ FormInput · Checkbox · Radio · Switch · SegmentedControl · Select · Textarea · Slider ·
74
+ NumberInput · FileInput · Combobox
74
75
  </div>
75
76
  </Card>
76
77