@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,55 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Progress } from './Progress';
3
+
4
+ describe('Progress', () => {
5
+ it('renders a progressbar', () => {
6
+ render(<Progress value={50} label="Loading" />);
7
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
8
+ });
9
+
10
+ it('sets aria-valuenow', () => {
11
+ render(<Progress value={60} label="Loading" />);
12
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '60');
13
+ });
14
+
15
+ it('sets aria-valuemin to 0', () => {
16
+ render(<Progress value={60} label="Loading" />);
17
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
18
+ });
19
+
20
+ it('sets aria-valuemax to 100 by default', () => {
21
+ render(<Progress value={60} label="Loading" />);
22
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '100');
23
+ });
24
+
25
+ it('sets aria-valuemax to custom max', () => {
26
+ render(<Progress value={3} max={10} label="Steps" />);
27
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '10');
28
+ });
29
+
30
+ it('sets aria-label', () => {
31
+ render(<Progress value={50} label="Uploading file" />);
32
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-label', 'Uploading file');
33
+ });
34
+
35
+ it('shows label and percentage when showLabel is true', () => {
36
+ render(<Progress value={75} label="Progress" showLabel />);
37
+ expect(screen.getByText('Progress')).toBeInTheDocument();
38
+ expect(screen.getByText('75%')).toBeInTheDocument();
39
+ });
40
+
41
+ it('does not show label text when showLabel is false', () => {
42
+ render(<Progress value={75} label="Progress" />);
43
+ expect(screen.queryByText('Progress')).not.toBeInTheDocument();
44
+ });
45
+
46
+ it('clamps value above max to 100%', () => {
47
+ render(<Progress value={150} label="Overloaded" showLabel />);
48
+ expect(screen.getByText('100%')).toBeInTheDocument();
49
+ });
50
+
51
+ it('clamps negative value to 0%', () => {
52
+ render(<Progress value={-10} label="Empty" showLabel />);
53
+ expect(screen.getByText('0%')).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Progress } from './Progress';
3
+
4
+ const meta = {
5
+ title: 'UI/Progress',
6
+ component: Progress,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ size: { control: 'radio', options: ['small', 'medium', 'large'] },
10
+ value: { control: { type: 'range', min: 0, max: 100 } },
11
+ },
12
+ } satisfies Meta<typeof Progress>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = { args: { value: 60, label: 'Loading' } };
18
+ export const WithLabel: Story = { args: { value: 60, label: 'Uploading file', showLabel: true } };
19
+ export const Empty: Story = { args: { value: 0, label: 'Not started', showLabel: true } };
20
+ export const Complete: Story = { args: { value: 100, label: 'Complete', showLabel: true } };
21
+
22
+ export const AllSizes: Story = {
23
+ render: () => (
24
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
25
+ <Progress value={60} label="Small" size="small" showLabel />
26
+ <Progress value={60} label="Medium" size="medium" showLabel />
27
+ <Progress value={60} label="Large" size="large" showLabel />
28
+ </div>
29
+ ),
30
+ };
@@ -0,0 +1,43 @@
1
+ import css from './Progress.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface ProgressProps {
5
+ value: number;
6
+ max?: number;
7
+ label: string;
8
+ showLabel?: boolean;
9
+ size?: 'small' | 'medium' | 'large';
10
+ className?: string;
11
+ }
12
+
13
+ export function Progress({
14
+ value,
15
+ max = 100,
16
+ label,
17
+ showLabel = false,
18
+ size = 'medium',
19
+ className,
20
+ }: Readonly<ProgressProps>) {
21
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
22
+
23
+ return (
24
+ <div className={cn(css.container, className)}>
25
+ {showLabel && (
26
+ <div className={css.labelRow}>
27
+ <span className={css.label}>{label}</span>
28
+ <span className={css.value}>{Math.round(percentage)}%</span>
29
+ </div>
30
+ )}
31
+ <div
32
+ role="progressbar"
33
+ aria-label={label}
34
+ aria-valuenow={value}
35
+ aria-valuemin={0}
36
+ aria-valuemax={max}
37
+ className={cn(css.track, css[`--size_${size}`])}
38
+ >
39
+ <div className={css.fill} style={{ width: `${percentage}%` }} />
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+ export { Progress } from './Progress';
@@ -0,0 +1,40 @@
1
+ @layer component {
2
+ .wrapper {
3
+ position: relative;
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ }
8
+
9
+ .svg {
10
+ display: block;
11
+ }
12
+
13
+ .track {
14
+ stroke: var(--color_bg--subtle);
15
+ }
16
+
17
+ .fill {
18
+ stroke: var(--color_green);
19
+ transition: stroke-dashoffset var(--animation_transition-duration) var(--animation_easing);
20
+ }
21
+
22
+ .value {
23
+ position: absolute;
24
+ font-size: var(--font_size--body--s);
25
+ font-weight: var(--font_weight--semibold);
26
+ font-variant-numeric: tabular-nums;
27
+ color: var(--color_on-bg);
28
+ line-height: 1;
29
+ }
30
+
31
+ .--size_small .value { font-size: 0.5rem; }
32
+ .--size_medium .value { font-size: var(--font_size--body--s); }
33
+ .--size_large .value { font-size: var(--font_size--body); }
34
+
35
+ @media (prefers-reduced-motion: reduce) {
36
+ .fill {
37
+ transition: none;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ProgressCircle } from './ProgressCircle';
3
+
4
+ describe('ProgressCircle', () => {
5
+ it('renders a progressbar', () => {
6
+ render(<ProgressCircle value={50} label="Loading" />);
7
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
8
+ });
9
+
10
+ it('sets aria-label from label prop', () => {
11
+ render(<ProgressCircle value={50} label="Upload progress" />);
12
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-label', 'Upload progress');
13
+ });
14
+
15
+ it('sets aria-valuenow', () => {
16
+ render(<ProgressCircle value={75} label="Progress" />);
17
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '75');
18
+ });
19
+
20
+ it('clamps values above max', () => {
21
+ render(<ProgressCircle value={150} label="Progress" />);
22
+ expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '150');
23
+ });
24
+
25
+ it('shows percentage value when showValue is true', () => {
26
+ render(<ProgressCircle value={50} label="Progress" showValue />);
27
+ expect(screen.getByText('50%')).toBeInTheDocument();
28
+ });
29
+
30
+ it('does not show value by default', () => {
31
+ render(<ProgressCircle value={50} label="Progress" />);
32
+ expect(screen.queryByText('50%')).not.toBeInTheDocument();
33
+ });
34
+ });
@@ -0,0 +1,18 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ProgressCircle } from './ProgressCircle';
3
+
4
+ const meta = {
5
+ title: 'UI/ProgressCircle',
6
+ component: ProgressCircle,
7
+ tags: ['autodocs'],
8
+ } satisfies Meta<typeof ProgressCircle>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = { args: { value: 65, label: 'Upload progress' } };
14
+ export const WithValue: Story = { args: { value: 65, label: 'Upload progress', showValue: true } };
15
+ export const Small: Story = { args: { value: 40, label: 'Progress', size: 'small', showValue: true } };
16
+ export const Large: Story = { args: { value: 80, label: 'Progress', size: 'large', showValue: true } };
17
+ export const Complete: Story = { args: { value: 100, label: 'Complete', showValue: true } };
18
+ export const Empty: Story = { args: { value: 0, label: 'Not started', showValue: true } };
@@ -0,0 +1,75 @@
1
+ import css from './ProgressCircle.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface ProgressCircleProps {
5
+ value: number;
6
+ max?: number;
7
+ label: string;
8
+ showValue?: boolean;
9
+ size?: 'small' | 'medium' | 'large';
10
+ className?: string;
11
+ }
12
+
13
+ const SIZE_PX = { small: 40, medium: 64, large: 96 };
14
+ const STROKE_WIDTH = 4;
15
+
16
+ export function ProgressCircle({
17
+ value,
18
+ max = 100,
19
+ label,
20
+ showValue = false,
21
+ size = 'medium',
22
+ className,
23
+ }: Readonly<ProgressCircleProps>) {
24
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
25
+ const px = SIZE_PX[size];
26
+ const radius = (px - STROKE_WIDTH * 2) / 2;
27
+ const circumference = 2 * Math.PI * radius;
28
+ const offset = circumference - (percentage / 100) * circumference;
29
+ const cx = px / 2;
30
+
31
+ return (
32
+ <div
33
+ role="progressbar"
34
+ aria-label={label}
35
+ aria-valuenow={value}
36
+ aria-valuemin={0}
37
+ aria-valuemax={max}
38
+ className={cn(css.wrapper, css[`--size_${size}`], className)}
39
+ >
40
+ <svg
41
+ width={px}
42
+ height={px}
43
+ viewBox={`0 0 ${px} ${px}`}
44
+ aria-hidden="true"
45
+ className={css.svg}
46
+ >
47
+ <circle
48
+ className={css.track}
49
+ cx={cx}
50
+ cy={cx}
51
+ r={radius}
52
+ strokeWidth={STROKE_WIDTH}
53
+ fill="none"
54
+ />
55
+ <circle
56
+ className={css.fill}
57
+ cx={cx}
58
+ cy={cx}
59
+ r={radius}
60
+ strokeWidth={STROKE_WIDTH}
61
+ fill="none"
62
+ strokeDasharray={circumference}
63
+ strokeDashoffset={offset}
64
+ strokeLinecap="round"
65
+ transform={`rotate(-90 ${cx} ${cx})`}
66
+ />
67
+ </svg>
68
+ {showValue && (
69
+ <span className={css.value} aria-hidden="true">
70
+ {Math.round(percentage)}%
71
+ </span>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1 @@
1
+ export { ProgressCircle } from './ProgressCircle';
@@ -0,0 +1,23 @@
1
+ @layer component {
2
+ .separator {
3
+ --separator_color: var(--color_border);
4
+ --separator_thickness: 1px;
5
+
6
+ border: none;
7
+ background-color: var(--separator_color);
8
+ }
9
+
10
+ .separator.--horizontal {
11
+ display: block;
12
+ width: 100%;
13
+ height: var(--separator_thickness);
14
+ margin-block: var(--space_m);
15
+ }
16
+
17
+ .separator.--vertical {
18
+ display: inline-block;
19
+ width: var(--separator_thickness);
20
+ align-self: stretch;
21
+ margin-inline: var(--space_m);
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Separator } from './Separator';
3
+
4
+ describe('Separator', () => {
5
+ it('renders as an hr element by default', () => {
6
+ const { container } = render(<Separator />);
7
+ expect(container.querySelector('hr')).toBeInTheDocument();
8
+ });
9
+
10
+ it('renders as a hr for horizontal orientation', () => {
11
+ const { container } = render(<Separator orientation="horizontal" />);
12
+ expect(container.querySelector('hr')).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders as a div with role="separator" for vertical orientation', () => {
16
+ render(<Separator orientation="vertical" />);
17
+ const separator = screen.getByRole('separator');
18
+ expect(separator.tagName).toBe('DIV');
19
+ });
20
+
21
+ it('sets aria-orientation="vertical" on the vertical separator', () => {
22
+ render(<Separator orientation="vertical" />);
23
+ expect(screen.getByRole('separator')).toHaveAttribute('aria-orientation', 'vertical');
24
+ });
25
+
26
+ it('accepts a custom className', () => {
27
+ const { container } = render(<Separator className="custom" />);
28
+ expect(container.querySelector('hr')).toHaveClass('custom');
29
+ });
30
+ });
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Separator } from './Separator';
3
+
4
+ const meta = {
5
+ title: 'UI/Separator',
6
+ component: Separator,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ orientation: { control: 'radio', options: ['horizontal', 'vertical'] },
10
+ },
11
+ } satisfies Meta<typeof Separator>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Horizontal: Story = {
17
+ args: { orientation: 'horizontal' },
18
+ decorators: [
19
+ Story => (
20
+ <div>
21
+ <p>Content above</p>
22
+ <Story />
23
+ <p>Content below</p>
24
+ </div>
25
+ ),
26
+ ],
27
+ };
28
+
29
+ export const Vertical: Story = {
30
+ args: { orientation: 'vertical' },
31
+ decorators: [
32
+ Story => (
33
+ <div style={{ display: 'flex', alignItems: 'center', height: '2rem' }}>
34
+ <span>Left</span>
35
+ <Story />
36
+ <span>Right</span>
37
+ </div>
38
+ ),
39
+ ],
40
+ };
@@ -0,0 +1,21 @@
1
+ import css from './Separator.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface SeparatorProps {
5
+ orientation?: 'horizontal' | 'vertical';
6
+ className?: string;
7
+ }
8
+
9
+ export function Separator({ orientation = 'horizontal', className }: Readonly<SeparatorProps>) {
10
+ if (orientation === 'vertical') {
11
+ return (
12
+ <div
13
+ role="separator"
14
+ aria-orientation="vertical"
15
+ className={cn(css.separator, css['--vertical'], className)}
16
+ />
17
+ );
18
+ }
19
+
20
+ return <hr className={cn(css.separator, css['--horizontal'], className)} />;
21
+ }
@@ -0,0 +1 @@
1
+ export { Separator } from './Separator';
@@ -0,0 +1,24 @@
1
+ @layer component {
2
+ .skeleton {
3
+ background: linear-gradient(
4
+ 90deg,
5
+ var(--color_grey--subtle) 25%,
6
+ var(--color_bg) 50%,
7
+ var(--color_grey--subtle) 75%
8
+ );
9
+ background-size: 200% 100%;
10
+ animation: shimmer 1.5s infinite;
11
+ border-radius: var(--border_radius--xs);
12
+ min-height: var(--space_m);
13
+ }
14
+ }
15
+
16
+ @keyframes shimmer {
17
+ 0% {
18
+ background-position: 200% 0;
19
+ }
20
+
21
+ 100% {
22
+ background-position: -200% 0;
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ import { render } from '@testing-library/react';
2
+ import { Skeleton } from './Skeleton';
3
+
4
+ describe('Skeleton', () => {
5
+ it('renders with aria-hidden', () => {
6
+ const { container } = render(<Skeleton />);
7
+ expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
8
+ });
9
+
10
+ it('renders a div', () => {
11
+ const { container } = render(<Skeleton />);
12
+ expect(container.firstChild?.nodeName).toBe('DIV');
13
+ });
14
+
15
+ it('accepts a custom className', () => {
16
+ const { container } = render(<Skeleton className="custom" />);
17
+ expect(container.firstChild).toHaveClass('custom');
18
+ });
19
+ });
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Skeleton } from './Skeleton';
3
+
4
+ const meta = {
5
+ title: 'UI/Skeleton',
6
+ component: Skeleton,
7
+ tags: ['autodocs'],
8
+ } satisfies Meta<typeof Skeleton>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = { args: {} };
14
+ export const TextLine: Story = {
15
+ render: () => <Skeleton className="skeleton-text" />,
16
+ };
17
+ export const Card: Story = {
18
+ render: () => (
19
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width: '300px' }}>
20
+ <Skeleton className="skeleton-image" />
21
+ <Skeleton className="skeleton-title" />
22
+ <Skeleton className="skeleton-subtitle" />
23
+ </div>
24
+ ),
25
+ };
@@ -0,0 +1,12 @@
1
+ import { cn } from '@boostdev/design-system-foundation';
2
+ import css from './Skeleton.module.css';
3
+
4
+ interface SkeletonProps {
5
+ className?: string;
6
+ }
7
+
8
+ export function Skeleton({ className }: SkeletonProps) {
9
+ return (
10
+ <div aria-hidden="true" className={cn(css.skeleton, className)} />
11
+ );
12
+ }
@@ -0,0 +1 @@
1
+ export { Skeleton } from './Skeleton';
@@ -0,0 +1,30 @@
1
+ @layer component {
2
+ .skipLink {
3
+ position: absolute;
4
+ inset-block-start: var(--space_s);
5
+ inset-inline-start: var(--space_s);
6
+ z-index: var(--z_overlay);
7
+ padding: var(--space_xs) var(--space_m);
8
+ background-color: var(--color_bg);
9
+ color: var(--color_on-bg);
10
+ font-size: var(--font_size--body);
11
+ font-weight: var(--font_weight--semibold);
12
+ border-radius: var(--border_radius--s);
13
+ border: 2px solid var(--color_focus);
14
+ text-decoration: none;
15
+ transform: translateY(-200%);
16
+ transition: transform var(--animation_transition-duration) var(--animation_easing);
17
+ }
18
+
19
+ .skipLink:focus-visible {
20
+ transform: translateY(0);
21
+ outline: var(--outline_default);
22
+ outline-offset: var(--outline_offset);
23
+ }
24
+
25
+ @media (prefers-reduced-motion: reduce) {
26
+ .skipLink {
27
+ transition: none;
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { SkipLink } from './SkipLink';
3
+
4
+ describe('SkipLink', () => {
5
+ it('renders a link with default text', () => {
6
+ render(<SkipLink />);
7
+ expect(screen.getByRole('link', { name: 'Skip to main content' })).toBeInTheDocument();
8
+ });
9
+
10
+ it('renders with custom text', () => {
11
+ render(<SkipLink>Skip to navigation</SkipLink>);
12
+ expect(screen.getByRole('link', { name: 'Skip to navigation' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('links to #main by default', () => {
16
+ render(<SkipLink />);
17
+ expect(screen.getByRole('link')).toHaveAttribute('href', '#main');
18
+ });
19
+
20
+ it('links to custom href', () => {
21
+ render(<SkipLink href="#content" />);
22
+ expect(screen.getByRole('link')).toHaveAttribute('href', '#content');
23
+ });
24
+ });
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SkipLink } from './SkipLink';
3
+
4
+ const meta = {
5
+ title: 'UI/SkipLink',
6
+ component: SkipLink,
7
+ tags: ['autodocs'],
8
+ decorators: [
9
+ Story => (
10
+ <div style={{ minHeight: '4rem', position: 'relative' }}>
11
+ <p style={{ fontSize: '0.75rem', color: 'gray', marginBottom: '0.5rem' }}>
12
+ Tab into the preview to reveal the skip link.
13
+ </p>
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ } satisfies Meta<typeof SkipLink>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ export const Default: Story = {};
24
+ export const CustomTarget: Story = { args: { href: '#content', children: 'Skip to content' } };
@@ -0,0 +1,14 @@
1
+ import css from './SkipLink.module.css';
2
+
3
+ interface SkipLinkProps {
4
+ href?: string;
5
+ children?: string;
6
+ }
7
+
8
+ export function SkipLink({ href = '#main', children = 'Skip to main content' }: SkipLinkProps) {
9
+ return (
10
+ <a href={href} className={css.skipLink}>
11
+ {children}
12
+ </a>
13
+ );
14
+ }
@@ -0,0 +1 @@
1
+ export { SkipLink } from './SkipLink';