@boostdev/design-system-components 0.1.1

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 (233) hide show
  1. package/AGENTS.md +72 -0
  2. package/README.md +396 -0
  3. package/dist/index.cjs +2273 -0
  4. package/dist/index.css +2543 -0
  5. package/dist/index.d.cts +453 -0
  6. package/dist/index.d.ts +453 -0
  7. package/dist/index.js +2221 -0
  8. package/package.json +143 -0
  9. package/src/components/interaction/Button/Button.module.css +136 -0
  10. package/src/components/interaction/Button/Button.spec.tsx +50 -0
  11. package/src/components/interaction/Button/Button.stories.tsx +43 -0
  12. package/src/components/interaction/Button/Button.tsx +68 -0
  13. package/src/components/interaction/Button/index.ts +1 -0
  14. package/src/components/interaction/Command/Command.module.css +128 -0
  15. package/src/components/interaction/Command/Command.spec.tsx +60 -0
  16. package/src/components/interaction/Command/Command.stories.tsx +35 -0
  17. package/src/components/interaction/Command/Command.tsx +161 -0
  18. package/src/components/interaction/Command/index.ts +2 -0
  19. package/src/components/interaction/Dialog/Dialog.module.css +39 -0
  20. package/src/components/interaction/Dialog/Dialog.spec.tsx +43 -0
  21. package/src/components/interaction/Dialog/Dialog.stories.tsx +36 -0
  22. package/src/components/interaction/Dialog/Dialog.tsx +42 -0
  23. package/src/components/interaction/Dialog/index.ts +1 -0
  24. package/src/components/interaction/Drawer/Drawer.module.css +98 -0
  25. package/src/components/interaction/Drawer/Drawer.spec.tsx +43 -0
  26. package/src/components/interaction/Drawer/Drawer.stories.tsx +46 -0
  27. package/src/components/interaction/Drawer/Drawer.tsx +71 -0
  28. package/src/components/interaction/Drawer/index.ts +1 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +68 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +74 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +68 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +137 -0
  33. package/src/components/interaction/DropdownMenu/index.ts +1 -0
  34. package/src/components/interaction/Popover/Popover.module.css +39 -0
  35. package/src/components/interaction/Popover/Popover.spec.tsx +72 -0
  36. package/src/components/interaction/Popover/Popover.stories.tsx +47 -0
  37. package/src/components/interaction/Popover/Popover.tsx +78 -0
  38. package/src/components/interaction/Popover/index.ts +1 -0
  39. package/src/components/interaction/Rating/Rating.module.css +16 -0
  40. package/src/components/interaction/Rating/Rating.spec.tsx +30 -0
  41. package/src/components/interaction/Rating/Rating.stories.tsx +29 -0
  42. package/src/components/interaction/Rating/Rating.tsx +30 -0
  43. package/src/components/interaction/Rating/index.ts +1 -0
  44. package/src/components/interaction/Toast/Toast.module.css +48 -0
  45. package/src/components/interaction/Toast/Toast.spec.tsx +41 -0
  46. package/src/components/interaction/Toast/Toast.stories.tsx +57 -0
  47. package/src/components/interaction/Toast/Toast.tsx +64 -0
  48. package/src/components/interaction/Toast/index.ts +1 -0
  49. package/src/components/interaction/form/Checkbox/Checkbox.module.css +61 -0
  50. package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +39 -0
  51. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +17 -0
  52. package/src/components/interaction/form/Checkbox/Checkbox.tsx +39 -0
  53. package/src/components/interaction/form/Checkbox/index.ts +1 -0
  54. package/src/components/interaction/form/Combobox/Combobox.module.css +104 -0
  55. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +81 -0
  56. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +25 -0
  57. package/src/components/interaction/form/Combobox/Combobox.tsx +182 -0
  58. package/src/components/interaction/form/Combobox/index.ts +1 -0
  59. package/src/components/interaction/form/FileInput/FileInput.module.css +79 -0
  60. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +53 -0
  61. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +17 -0
  62. package/src/components/interaction/form/FileInput/FileInput.tsx +99 -0
  63. package/src/components/interaction/form/FileInput/index.ts +1 -0
  64. package/src/components/interaction/form/FormInput/FormInput.module.css +37 -0
  65. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +43 -0
  66. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +17 -0
  67. package/src/components/interaction/form/FormInput/FormInput.tsx +47 -0
  68. package/src/components/interaction/form/FormInput/index.ts +1 -0
  69. package/src/components/interaction/form/NumberInput/NumberInput.module.css +78 -0
  70. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +49 -0
  71. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +17 -0
  72. package/src/components/interaction/form/NumberInput/NumberInput.tsx +106 -0
  73. package/src/components/interaction/form/NumberInput/index.ts +1 -0
  74. package/src/components/interaction/form/Radio/Radio.module.css +62 -0
  75. package/src/components/interaction/form/Radio/Radio.spec.tsx +38 -0
  76. package/src/components/interaction/form/Radio/Radio.stories.tsx +26 -0
  77. package/src/components/interaction/form/Radio/Radio.tsx +39 -0
  78. package/src/components/interaction/form/Radio/index.ts +1 -0
  79. package/src/components/interaction/form/Select/Select.module.css +64 -0
  80. package/src/components/interaction/form/Select/Select.spec.tsx +61 -0
  81. package/src/components/interaction/form/Select/Select.stories.tsx +24 -0
  82. package/src/components/interaction/form/Select/Select.tsx +72 -0
  83. package/src/components/interaction/form/Select/index.ts +1 -0
  84. package/src/components/interaction/form/Slider/Slider.module.css +99 -0
  85. package/src/components/interaction/form/Slider/Slider.spec.tsx +53 -0
  86. package/src/components/interaction/form/Slider/Slider.stories.tsx +18 -0
  87. package/src/components/interaction/form/Slider/Slider.tsx +71 -0
  88. package/src/components/interaction/form/Slider/index.ts +1 -0
  89. package/src/components/interaction/form/Switch/Switch.module.css +114 -0
  90. package/src/components/interaction/form/Switch/Switch.spec.tsx +48 -0
  91. package/src/components/interaction/form/Switch/Switch.stories.tsx +31 -0
  92. package/src/components/interaction/form/Switch/Switch.tsx +54 -0
  93. package/src/components/interaction/form/Switch/index.ts +1 -0
  94. package/src/components/interaction/form/Textarea/Textarea.module.css +44 -0
  95. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +53 -0
  96. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +18 -0
  97. package/src/components/interaction/form/Textarea/Textarea.tsx +44 -0
  98. package/src/components/interaction/form/Textarea/index.ts +1 -0
  99. package/src/components/interaction/form/atoms/InputContainer.module.css +9 -0
  100. package/src/components/interaction/form/atoms/InputContainer.tsx +9 -0
  101. package/src/components/interaction/form/atoms/Label.module.css +10 -0
  102. package/src/components/interaction/form/atoms/Label.tsx +15 -0
  103. package/src/components/interaction/form/atoms/Message.module.css +11 -0
  104. package/src/components/interaction/form/atoms/Message.tsx +17 -0
  105. package/src/components/layout/ButtonGroup/ButtonGroup.module.css +59 -0
  106. package/src/components/layout/ButtonGroup/ButtonGroup.spec.tsx +20 -0
  107. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +28 -0
  108. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +17 -0
  109. package/src/components/layout/ButtonGroup/index.ts +1 -0
  110. package/src/components/layout/Card/Card.module.css +72 -0
  111. package/src/components/layout/Card/Card.spec.tsx +33 -0
  112. package/src/components/layout/Card/Card.stories.tsx +32 -0
  113. package/src/components/layout/Card/Card.tsx +45 -0
  114. package/src/components/layout/Card/index.ts +1 -0
  115. package/src/components/layout/IconWrapper/IconWrapper.module.css +24 -0
  116. package/src/components/layout/IconWrapper/IconWrapper.spec.tsx +19 -0
  117. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +22 -0
  118. package/src/components/layout/IconWrapper/IconWrapper.tsx +14 -0
  119. package/src/components/layout/IconWrapper/index.ts +1 -0
  120. package/src/components/layout/SectionHeader/SectionHeader.module.css +75 -0
  121. package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +31 -0
  122. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +21 -0
  123. package/src/components/layout/SectionHeader/SectionHeader.tsx +32 -0
  124. package/src/components/layout/SectionHeader/index.ts +1 -0
  125. package/src/components/ui/Accordion/Accordion.module.css +87 -0
  126. package/src/components/ui/Accordion/Accordion.spec.tsx +78 -0
  127. package/src/components/ui/Accordion/Accordion.stories.tsx +34 -0
  128. package/src/components/ui/Accordion/Accordion.tsx +82 -0
  129. package/src/components/ui/Accordion/index.ts +1 -0
  130. package/src/components/ui/Alert/Alert.module.css +91 -0
  131. package/src/components/ui/Alert/Alert.spec.tsx +63 -0
  132. package/src/components/ui/Alert/Alert.stories.tsx +53 -0
  133. package/src/components/ui/Alert/Alert.tsx +54 -0
  134. package/src/components/ui/Alert/index.ts +1 -0
  135. package/src/components/ui/Avatar/Avatar.module.css +42 -0
  136. package/src/components/ui/Avatar/Avatar.spec.tsx +49 -0
  137. package/src/components/ui/Avatar/Avatar.stories.tsx +44 -0
  138. package/src/components/ui/Avatar/Avatar.tsx +45 -0
  139. package/src/components/ui/Avatar/index.ts +1 -0
  140. package/src/components/ui/Badge/Badge.module.css +46 -0
  141. package/src/components/ui/Badge/Badge.spec.tsx +19 -0
  142. package/src/components/ui/Badge/Badge.stories.tsx +29 -0
  143. package/src/components/ui/Badge/Badge.tsx +13 -0
  144. package/src/components/ui/Badge/index.ts +1 -0
  145. package/src/components/ui/Breadcrumb/Breadcrumb.module.css +50 -0
  146. package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +44 -0
  147. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +48 -0
  148. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +41 -0
  149. package/src/components/ui/Breadcrumb/index.ts +1 -0
  150. package/src/components/ui/Calendar/Calendar.module.css +120 -0
  151. package/src/components/ui/Calendar/Calendar.spec.tsx +64 -0
  152. package/src/components/ui/Calendar/Calendar.stories.tsx +59 -0
  153. package/src/components/ui/Calendar/Calendar.tsx +184 -0
  154. package/src/components/ui/Calendar/index.ts +1 -0
  155. package/src/components/ui/Carousel/Carousel.module.css +66 -0
  156. package/src/components/ui/Carousel/Carousel.spec.tsx +29 -0
  157. package/src/components/ui/Carousel/Carousel.stories.tsx +30 -0
  158. package/src/components/ui/Carousel/Carousel.tsx +64 -0
  159. package/src/components/ui/Carousel/index.ts +1 -0
  160. package/src/components/ui/DescriptionList/DescriptionList.module.css +43 -0
  161. package/src/components/ui/DescriptionList/DescriptionList.spec.tsx +31 -0
  162. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +21 -0
  163. package/src/components/ui/DescriptionList/DescriptionList.tsx +30 -0
  164. package/src/components/ui/DescriptionList/index.ts +1 -0
  165. package/src/components/ui/Link/Link.module.css +64 -0
  166. package/src/components/ui/Link/Link.spec.tsx +43 -0
  167. package/src/components/ui/Link/Link.stories.tsx +55 -0
  168. package/src/components/ui/Link/Link.tsx +42 -0
  169. package/src/components/ui/Link/index.ts +1 -0
  170. package/src/components/ui/Loading/Loading.module.css +33 -0
  171. package/src/components/ui/Loading/Loading.spec.tsx +19 -0
  172. package/src/components/ui/Loading/Loading.stories.tsx +27 -0
  173. package/src/components/ui/Loading/Loading.tsx +15 -0
  174. package/src/components/ui/Loading/index.ts +1 -0
  175. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +79 -0
  176. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +42 -0
  177. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +30 -0
  178. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +45 -0
  179. package/src/components/ui/NotificationBanner/index.ts +1 -0
  180. package/src/components/ui/Pagination/Pagination.module.css +78 -0
  181. package/src/components/ui/Pagination/Pagination.spec.tsx +67 -0
  182. package/src/components/ui/Pagination/Pagination.stories.tsx +40 -0
  183. package/src/components/ui/Pagination/Pagination.tsx +87 -0
  184. package/src/components/ui/Pagination/index.ts +1 -0
  185. package/src/components/ui/Progress/Progress.module.css +51 -0
  186. package/src/components/ui/Progress/Progress.spec.tsx +55 -0
  187. package/src/components/ui/Progress/Progress.stories.tsx +30 -0
  188. package/src/components/ui/Progress/Progress.tsx +43 -0
  189. package/src/components/ui/Progress/index.ts +1 -0
  190. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +40 -0
  191. package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +34 -0
  192. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +18 -0
  193. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +75 -0
  194. package/src/components/ui/ProgressCircle/index.ts +1 -0
  195. package/src/components/ui/Separator/Separator.module.css +23 -0
  196. package/src/components/ui/Separator/Separator.spec.tsx +30 -0
  197. package/src/components/ui/Separator/Separator.stories.tsx +40 -0
  198. package/src/components/ui/Separator/Separator.tsx +21 -0
  199. package/src/components/ui/Separator/index.ts +1 -0
  200. package/src/components/ui/Skeleton/Skeleton.module.css +24 -0
  201. package/src/components/ui/Skeleton/Skeleton.spec.tsx +19 -0
  202. package/src/components/ui/Skeleton/Skeleton.stories.tsx +25 -0
  203. package/src/components/ui/Skeleton/Skeleton.tsx +12 -0
  204. package/src/components/ui/Skeleton/index.ts +1 -0
  205. package/src/components/ui/SkipLink/SkipLink.module.css +30 -0
  206. package/src/components/ui/SkipLink/SkipLink.spec.tsx +24 -0
  207. package/src/components/ui/SkipLink/SkipLink.stories.tsx +24 -0
  208. package/src/components/ui/SkipLink/SkipLink.tsx +14 -0
  209. package/src/components/ui/SkipLink/index.ts +1 -0
  210. package/src/components/ui/Table/Table.module.css +111 -0
  211. package/src/components/ui/Table/Table.spec.tsx +69 -0
  212. package/src/components/ui/Table/Table.stories.tsx +53 -0
  213. package/src/components/ui/Table/Table.tsx +98 -0
  214. package/src/components/ui/Table/index.ts +1 -0
  215. package/src/components/ui/Tabs/Tabs.module.css +61 -0
  216. package/src/components/ui/Tabs/Tabs.spec.tsx +91 -0
  217. package/src/components/ui/Tabs/Tabs.stories.tsx +59 -0
  218. package/src/components/ui/Tabs/Tabs.tsx +100 -0
  219. package/src/components/ui/Tabs/index.ts +1 -0
  220. package/src/components/ui/Tooltip/Tooltip.module.css +69 -0
  221. package/src/components/ui/Tooltip/Tooltip.spec.tsx +46 -0
  222. package/src/components/ui/Tooltip/Tooltip.stories.tsx +69 -0
  223. package/src/components/ui/Tooltip/Tooltip.tsx +38 -0
  224. package/src/components/ui/Tooltip/index.ts +1 -0
  225. package/src/components/ui/Typography/Typography.module.css +41 -0
  226. package/src/components/ui/Typography/Typography.spec.tsx +39 -0
  227. package/src/components/ui/Typography/Typography.stories.tsx +31 -0
  228. package/src/components/ui/Typography/Typography.tsx +28 -0
  229. package/src/components/ui/Typography/index.ts +1 -0
  230. package/src/css/index.css +55 -0
  231. package/src/index.ts +54 -0
  232. package/src/test/setup.ts +1 -0
  233. package/src/typings.d.ts +4 -0
@@ -0,0 +1,78 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Accordion } from './Accordion';
4
+
5
+ const items = [
6
+ { id: 'a', title: 'First question', content: <p>First answer</p> },
7
+ { id: 'b', title: 'Second question', content: <p>Second answer</p> },
8
+ { id: 'c', title: 'Third question', content: <p>Third answer</p>, disabled: true },
9
+ ];
10
+
11
+ describe('Accordion', () => {
12
+ it('renders all trigger buttons', () => {
13
+ render(<Accordion items={items} />);
14
+ expect(screen.getAllByRole('button')).toHaveLength(3);
15
+ });
16
+
17
+ it('renders all panels hidden by default', () => {
18
+ render(<Accordion items={items} />);
19
+ expect(screen.getByText('First answer').closest('[role="region"]')).not.toBeVisible();
20
+ });
21
+
22
+ it('opens a panel when its trigger is clicked', async () => {
23
+ const user = userEvent.setup();
24
+ render(<Accordion items={items} />);
25
+ await user.click(screen.getByRole('button', { name: 'First question' }));
26
+ expect(screen.getByText('First answer')).toBeVisible();
27
+ });
28
+
29
+ it('sets aria-expanded to true on the active trigger', async () => {
30
+ const user = userEvent.setup();
31
+ render(<Accordion items={items} />);
32
+ const trigger = screen.getByRole('button', { name: 'First question' });
33
+ await user.click(trigger);
34
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
35
+ });
36
+
37
+ it('closes an open panel when its trigger is clicked again', async () => {
38
+ const user = userEvent.setup();
39
+ render(<Accordion items={items} defaultOpen={['a']} />);
40
+ await user.click(screen.getByRole('button', { name: 'First question' }));
41
+ expect(screen.getByText('First answer').closest('[role="region"]')).not.toBeVisible();
42
+ });
43
+
44
+ it('closes the previous panel when allowMultiple is false', async () => {
45
+ const user = userEvent.setup();
46
+ render(<Accordion items={items} />);
47
+ await user.click(screen.getByRole('button', { name: 'First question' }));
48
+ await user.click(screen.getByRole('button', { name: 'Second question' }));
49
+ expect(screen.getByText('First answer').closest('[role="region"]')).not.toBeVisible();
50
+ expect(screen.getByText('Second answer')).toBeVisible();
51
+ });
52
+
53
+ it('keeps multiple panels open when allowMultiple is true', async () => {
54
+ const user = userEvent.setup();
55
+ render(<Accordion items={items} allowMultiple />);
56
+ await user.click(screen.getByRole('button', { name: 'First question' }));
57
+ await user.click(screen.getByRole('button', { name: 'Second question' }));
58
+ expect(screen.getByText('First answer')).toBeVisible();
59
+ expect(screen.getByText('Second answer')).toBeVisible();
60
+ });
61
+
62
+ it('opens defaultOpen panels on mount', () => {
63
+ render(<Accordion items={items} defaultOpen={['b']} />);
64
+ expect(screen.getByText('Second answer')).toBeVisible();
65
+ });
66
+
67
+ it('disables a trigger when disabled is set', () => {
68
+ render(<Accordion items={items} />);
69
+ expect(screen.getByRole('button', { name: 'Third question' })).toBeDisabled();
70
+ });
71
+
72
+ it('links trigger to panel via aria-controls', () => {
73
+ render(<Accordion items={items} />);
74
+ const trigger = screen.getByRole('button', { name: 'First question' });
75
+ const panelId = trigger.getAttribute('aria-controls');
76
+ expect(document.getElementById(panelId!)).toBeInTheDocument();
77
+ });
78
+ });
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Accordion } from './Accordion';
3
+
4
+ const meta = {
5
+ title: 'UI/Accordion',
6
+ component: Accordion,
7
+ tags: ['autodocs'],
8
+ } satisfies Meta<typeof Accordion>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ const defaultItems = [
14
+ { id: 'q1', title: 'What is your return policy?', content: 'You can return any item within 30 days of purchase for a full refund.' },
15
+ { id: 'q2', title: 'Do you offer free shipping?', content: 'Free shipping is available on all orders over €50.' },
16
+ { id: 'q3', title: 'How do I track my order?', content: 'Once your order ships, you will receive an email with a tracking link.' },
17
+ ];
18
+
19
+ export const Default: Story = { args: { items: defaultItems } };
20
+
21
+ export const WithDefaultOpen: Story = { args: { items: defaultItems, defaultOpen: ['q1'] } };
22
+
23
+ export const AllowMultiple: Story = {
24
+ args: { items: defaultItems, allowMultiple: true, defaultOpen: ['q1', 'q2'] },
25
+ };
26
+
27
+ export const WithDisabledItem: Story = {
28
+ args: {
29
+ items: [
30
+ ...defaultItems,
31
+ { id: 'q4', title: 'Unavailable section', content: 'This content is not available.', disabled: true },
32
+ ],
33
+ },
34
+ };
@@ -0,0 +1,82 @@
1
+ import { ReactNode, useId, useState } from 'react';
2
+ import css from './Accordion.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ interface AccordionItem {
6
+ id: string;
7
+ title: ReactNode;
8
+ content: ReactNode;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ interface AccordionProps {
13
+ items: AccordionItem[];
14
+ allowMultiple?: boolean;
15
+ defaultOpen?: string[];
16
+ className?: string;
17
+ }
18
+
19
+ export function Accordion({
20
+ items,
21
+ allowMultiple = false,
22
+ defaultOpen = [],
23
+ className,
24
+ }: Readonly<AccordionProps>) {
25
+ const baseId = useId();
26
+ const [openIds, setOpenIds] = useState<string[]>(defaultOpen);
27
+
28
+ const toggle = (id: string) => {
29
+ setOpenIds(prev => {
30
+ const isOpen = prev.includes(id);
31
+ if (isOpen) return prev.filter(i => i !== id);
32
+ return allowMultiple ? [...prev, id] : [id];
33
+ });
34
+ };
35
+
36
+ return (
37
+ <div className={cn(css.accordion, className)}>
38
+ {items.map(item => {
39
+ const isOpen = openIds.includes(item.id);
40
+ const triggerId = `${baseId}-trigger-${item.id}`;
41
+ const panelId = `${baseId}-panel-${item.id}`;
42
+
43
+ return (
44
+ <div key={item.id} className={cn(css.item, isOpen ? css['--open'] : undefined)}>
45
+ <h3 className={css.heading}>
46
+ <button
47
+ type="button"
48
+ id={triggerId}
49
+ aria-expanded={isOpen}
50
+ aria-controls={panelId}
51
+ disabled={item.disabled}
52
+ className={css.trigger}
53
+ onClick={() => toggle(item.id)}
54
+ >
55
+ <span className={css.triggerLabel}>{item.title}</span>
56
+ <svg
57
+ aria-hidden="true"
58
+ className={css.chevron}
59
+ viewBox="0 0 24 24"
60
+ fill="none"
61
+ stroke="currentColor"
62
+ strokeWidth="2"
63
+ >
64
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 9l6 6 6-6" />
65
+ </svg>
66
+ </button>
67
+ </h3>
68
+ <div
69
+ id={panelId}
70
+ role="region"
71
+ aria-labelledby={triggerId}
72
+ hidden={!isOpen}
73
+ className={css.panel}
74
+ >
75
+ <div className={css.panelContent}>{item.content}</div>
76
+ </div>
77
+ </div>
78
+ );
79
+ })}
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1 @@
1
+ export { Accordion } from './Accordion';
@@ -0,0 +1,91 @@
1
+ @layer component {
2
+ .alert {
3
+ --alert_bg: var(--color_blue--subtle);
4
+ --alert_text: var(--color_on-blue--subtle);
5
+ --alert_border: var(--color_blue);
6
+
7
+ display: flex;
8
+ align-items: flex-start;
9
+ gap: var(--space_s);
10
+ padding: var(--space_m);
11
+ border-radius: var(--border_radius--s);
12
+ border-inline-start: var(--space_s) solid var(--alert_border);
13
+ background-color: var(--alert_bg);
14
+ color: var(--alert_text);
15
+ font-size: var(--font_size--body);
16
+ line-height: var(--font_line-height--body);
17
+ }
18
+
19
+ .--variant_info {
20
+ --alert_bg: var(--color_blue--subtle);
21
+ --alert_text: var(--color_on-blue--subtle);
22
+ --alert_border: var(--color_blue);
23
+ }
24
+
25
+ .--variant_success {
26
+ --alert_bg: var(--color_green--subtle);
27
+ --alert_text: var(--color_on-green--subtle);
28
+ --alert_border: var(--color_green);
29
+ }
30
+
31
+ .--variant_warning {
32
+ --alert_bg: var(--color_orange--subtle);
33
+ --alert_text: var(--color_on-orange--subtle);
34
+ --alert_border: var(--color_warning);
35
+ }
36
+
37
+ .--variant_error {
38
+ --alert_bg: rgb(from var(--color_error) r g b / 12%);
39
+ --alert_text: var(--color_on-bg);
40
+ --alert_border: var(--color_error);
41
+ }
42
+
43
+ .icon {
44
+ display: flex;
45
+ align-items: center;
46
+ flex-shrink: 0;
47
+ margin-block-start: 0.1em;
48
+ }
49
+
50
+ .content {
51
+ flex: 1;
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--space_xxs);
55
+ }
56
+
57
+ .title {
58
+ font-weight: var(--font_weight--semibold);
59
+ font-size: var(--font_size--body);
60
+ }
61
+
62
+ .dismiss {
63
+ all: unset;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ flex-shrink: 0;
68
+ cursor: pointer;
69
+ color: currentcolor;
70
+ transition: var(--animation_transition);
71
+ border: 1px solid currentcolor;
72
+ border-radius: 50%;
73
+
74
+ }
75
+
76
+ .dismiss svg {
77
+ width: 1rem;
78
+ height: 1rem;
79
+ }
80
+
81
+ .dismiss:focus-visible {
82
+ outline: var(--outline_default);
83
+ outline-offset: var(--outline_offset);
84
+ }
85
+
86
+ @media (hover: hover) and (pointer: fine) {
87
+ .dismiss:hover {
88
+ opacity: 0.7;
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,63 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Alert } from './Alert';
4
+
5
+ describe('Alert', () => {
6
+ it('renders children', () => {
7
+ render(<Alert>Something went wrong</Alert>);
8
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders with role="status" for info variant by default', () => {
12
+ render(<Alert>Info message</Alert>);
13
+ expect(screen.getByRole('status')).toBeInTheDocument();
14
+ });
15
+
16
+ it('renders with role="alert" for error variant', () => {
17
+ render(<Alert variant="error">Error message</Alert>);
18
+ expect(screen.getByRole('alert')).toBeInTheDocument();
19
+ });
20
+
21
+ it('renders with role="alert" for warning variant', () => {
22
+ render(<Alert variant="warning">Warning message</Alert>);
23
+ expect(screen.getByRole('alert')).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders with role="status" for success variant', () => {
27
+ render(<Alert variant="success">Success message</Alert>);
28
+ expect(screen.getByRole('status')).toBeInTheDocument();
29
+ });
30
+
31
+ it('renders a title when provided', () => {
32
+ render(<Alert title="Heads up">Something happened</Alert>);
33
+ expect(screen.getByText('Heads up')).toBeInTheDocument();
34
+ });
35
+
36
+ it('renders an icon when provided', () => {
37
+ render(<Alert icon={<svg data-testid="icon" />}>Message</Alert>);
38
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders a dismiss button when onDismiss is provided', () => {
42
+ render(<Alert onDismiss={() => {}}>Message</Alert>);
43
+ expect(screen.getByRole('button', { name: 'Dismiss alert' })).toBeInTheDocument();
44
+ });
45
+
46
+ it('does not render a dismiss button without onDismiss', () => {
47
+ render(<Alert>Message</Alert>);
48
+ expect(screen.queryByRole('button', { name: 'Dismiss alert' })).not.toBeInTheDocument();
49
+ });
50
+
51
+ it('calls onDismiss when dismiss button is clicked', async () => {
52
+ const user = userEvent.setup();
53
+ const onDismiss = vi.fn();
54
+ render(<Alert onDismiss={onDismiss}>Message</Alert>);
55
+ await user.click(screen.getByRole('button', { name: 'Dismiss alert' }));
56
+ expect(onDismiss).toHaveBeenCalledOnce();
57
+ });
58
+
59
+ it('accepts a custom className', () => {
60
+ render(<Alert className="custom">Message</Alert>);
61
+ expect(screen.getByRole('status')).toHaveClass('custom');
62
+ });
63
+ });
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { Alert } from './Alert';
4
+
5
+ const meta = {
6
+ title: 'UI/Alert',
7
+ component: Alert,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: { control: 'select', options: ['info', 'success', 'warning', 'error'] },
11
+ },
12
+ } satisfies Meta<typeof Alert>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Info: Story = { args: { variant: 'info', children: 'Your session will expire in 10 minutes.' } };
18
+ export const Success: Story = { args: { variant: 'success', children: 'Your changes have been saved.' } };
19
+ export const Warning: Story = { args: { variant: 'warning', children: 'This action cannot be undone.' } };
20
+ export const Error: Story = { args: { variant: 'error', children: 'Something went wrong. Please try again.' } };
21
+
22
+ export const WithTitle: Story = {
23
+ args: {
24
+ variant: 'error',
25
+ title: 'Payment failed',
26
+ children: 'Your card was declined. Please check your details and try again.',
27
+ },
28
+ };
29
+
30
+ export const Dismissible: Story = {
31
+ render: () => {
32
+ const [visible, setVisible] = useState(true);
33
+ return visible ? (
34
+ <Alert variant="info" onDismiss={() => setVisible(false)}>
35
+ This alert can be dismissed.
36
+ </Alert>
37
+ ) : (
38
+ <p>Alert dismissed.</p>
39
+ );
40
+ },
41
+ };
42
+
43
+ export const AllVariants: Story = {
44
+ render: () => (
45
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
46
+ {(['info', 'success', 'warning', 'error'] as const).map(v => (
47
+ <Alert key={v} variant={v} title={v.charAt(0).toUpperCase() + v.slice(1)}>
48
+ This is a {v} alert message.
49
+ </Alert>
50
+ ))}
51
+ </div>
52
+ ),
53
+ };
@@ -0,0 +1,54 @@
1
+ import { ReactNode } from 'react';
2
+ import css from './Alert.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ interface AlertProps {
6
+ variant?: 'info' | 'success' | 'warning' | 'error';
7
+ icon?: ReactNode;
8
+ title?: string;
9
+ children: ReactNode;
10
+ onDismiss?: () => void;
11
+ className?: string;
12
+ }
13
+
14
+ export function Alert({
15
+ variant = 'info',
16
+ icon,
17
+ title,
18
+ children,
19
+ onDismiss,
20
+ className,
21
+ }: Readonly<AlertProps>) {
22
+ const isUrgent = variant === 'error' || variant === 'warning';
23
+
24
+ return (
25
+ <div
26
+ role={isUrgent ? 'alert' : 'status'}
27
+ aria-live={isUrgent ? 'assertive' : 'polite'}
28
+ aria-atomic="true"
29
+ className={cn(css.alert, css[`--variant_${variant}`], className)}
30
+ >
31
+ {icon && (
32
+ <span className={css.icon} aria-hidden="true">
33
+ {icon}
34
+ </span>
35
+ )}
36
+ <div className={css.content}>
37
+ {title && <strong className={css.title}>{title}</strong>}
38
+ <div className={css.body}>{children}</div>
39
+ </div>
40
+ {onDismiss && (
41
+ <button
42
+ type="button"
43
+ className={css.dismiss}
44
+ onClick={onDismiss}
45
+ aria-label="Dismiss alert"
46
+ >
47
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
48
+ <path strokeLinecap="round" strokeLinejoin="round" d="M18 6L6 18M6 6l12 12" />
49
+ </svg>
50
+ </button>
51
+ )}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1 @@
1
+ export { Alert } from './Alert';
@@ -0,0 +1,42 @@
1
+ @layer component {
2
+ .avatar {
3
+ --avatar_size: 3em;
4
+ --avatar_bg: var(--color_blue);
5
+ --avatar_text: var(--color_on-blue);
6
+
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ width: var(--avatar_size);
11
+ height: var(--avatar_size);
12
+ border-radius: 50%;
13
+ overflow: hidden;
14
+ flex-shrink: 0;
15
+ }
16
+
17
+ .avatar.--fallback {
18
+ background-color: var(--avatar_bg);
19
+ color: var(--avatar_text);
20
+ font-weight: var(--font_weight--semibold);
21
+ }
22
+
23
+ .avatar.--size_small { --avatar_size: 2em; }
24
+ .avatar.--size_medium { --avatar_size: 3em; }
25
+ .avatar.--size_large { --avatar_size: 4.5em; }
26
+
27
+ .image {
28
+ width: 100%;
29
+ height: 100%;
30
+ object-fit: cover;
31
+ display: block;
32
+ }
33
+
34
+ .initials {
35
+ line-height: 1;
36
+ user-select: none;
37
+ font-size: var(--font_size--body);
38
+ }
39
+
40
+ .avatar.--size_small .initials { font-size: var(--font_size--body--s); }
41
+ .avatar.--size_large .initials { font-size: var(--font_size--heading-3); }
42
+ }
@@ -0,0 +1,49 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Avatar } from './Avatar';
3
+
4
+ describe('Avatar', () => {
5
+ it('renders an image when src is provided', () => {
6
+ render(<Avatar src="https://example.com/photo.jpg" name="Jane Doe" />);
7
+ expect(screen.getByRole('img')).toBeInTheDocument();
8
+ });
9
+
10
+ it('uses name as alt text on the image', () => {
11
+ render(<Avatar src="https://example.com/photo.jpg" name="Jane Doe" />);
12
+ expect(screen.getByRole('img')).toHaveAttribute('alt', 'Jane Doe');
13
+ });
14
+
15
+ it('uses explicit alt prop over name for the image', () => {
16
+ render(<Avatar src="https://example.com/photo.jpg" name="Jane Doe" alt="Profile photo" />);
17
+ expect(screen.getByRole('img')).toHaveAttribute('alt', 'Profile photo');
18
+ });
19
+
20
+ it('renders initials fallback when no src is provided', () => {
21
+ render(<Avatar name="Jane Doe" />);
22
+ expect(screen.getByText('JD')).toBeInTheDocument();
23
+ });
24
+
25
+ it('renders at most 2 initials', () => {
26
+ render(<Avatar name="Jane Marie Doe" />);
27
+ expect(screen.getByText('JM')).toBeInTheDocument();
28
+ });
29
+
30
+ it('has role="img" on the fallback element', () => {
31
+ render(<Avatar name="Jane Doe" />);
32
+ expect(screen.getByRole('img')).toBeInTheDocument();
33
+ });
34
+
35
+ it('uses name as aria-label on the fallback', () => {
36
+ render(<Avatar name="Jane Doe" />);
37
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'Jane Doe');
38
+ });
39
+
40
+ it('falls back to "User avatar" aria-label when no name is given', () => {
41
+ render(<Avatar />);
42
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'User avatar');
43
+ });
44
+
45
+ it('accepts a custom className', () => {
46
+ render(<Avatar name="Jane Doe" className="custom" />);
47
+ expect(screen.getByRole('img')).toHaveClass('custom');
48
+ });
49
+ });
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Avatar } from './Avatar';
3
+
4
+ const meta = {
5
+ title: 'UI/Avatar',
6
+ component: Avatar,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ size: { control: 'radio', options: ['small', 'medium', 'large'] },
10
+ },
11
+ } satisfies Meta<typeof Avatar>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const WithImage: Story = {
17
+ args: {
18
+ src: 'https://i.pravatar.cc/150?img=3',
19
+ name: 'Jane Doe',
20
+ size: 'medium',
21
+ },
22
+ };
23
+
24
+ export const WithInitials: Story = {
25
+ args: { name: 'Jane Doe', size: 'medium' },
26
+ };
27
+
28
+ export const SingleName: Story = {
29
+ args: { name: 'Bjorn', size: 'medium' },
30
+ };
31
+
32
+ export const NoName: Story = {
33
+ args: { size: 'medium' },
34
+ };
35
+
36
+ export const AllSizes: Story = {
37
+ render: () => (
38
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
39
+ <Avatar name="Jane Doe" size="small" />
40
+ <Avatar name="Jane Doe" size="medium" />
41
+ <Avatar name="Jane Doe" size="large" />
42
+ </div>
43
+ ),
44
+ };
@@ -0,0 +1,45 @@
1
+ import css from './Avatar.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface AvatarProps {
5
+ src?: string;
6
+ alt?: string;
7
+ name?: string;
8
+ size?: 'small' | 'medium' | 'large';
9
+ className?: string;
10
+ }
11
+
12
+ function getInitials(name: string): string {
13
+ return name
14
+ .split(' ')
15
+ .filter(Boolean)
16
+ .slice(0, 2)
17
+ .map(word => word[0].toUpperCase())
18
+ .join('');
19
+ }
20
+
21
+ export function Avatar({ src, alt, name, size = 'medium', className }: Readonly<AvatarProps>) {
22
+ const sizeClass = css[`--size_${size}`];
23
+
24
+ if (src) {
25
+ return (
26
+ <span className={cn(css.avatar, sizeClass, className)}>
27
+ <img src={src} alt={alt ?? name ?? ''} className={css.image} />
28
+ </span>
29
+ );
30
+ }
31
+
32
+ const initials = name ? getInitials(name) : '';
33
+
34
+ return (
35
+ <span
36
+ role="img"
37
+ aria-label={name ?? 'User avatar'}
38
+ className={cn(css.avatar, css['--fallback'], sizeClass, className)}
39
+ >
40
+ <span className={css.initials} aria-hidden="true">
41
+ {initials}
42
+ </span>
43
+ </span>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export { Avatar } from './Avatar';