@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,42 @@
1
+ import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
2
+ import css from './Link.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ type LinkOwnProps<T extends ElementType> = {
6
+ as?: T;
7
+ children: ReactNode;
8
+ variant?: 'default' | 'subtle' | 'standalone';
9
+ external?: boolean;
10
+ className?: string;
11
+ };
12
+
13
+ type LinkProps<T extends ElementType = 'a'> = LinkOwnProps<T> &
14
+ Omit<ComponentPropsWithoutRef<T>, keyof LinkOwnProps<T>>;
15
+
16
+ export function Link<T extends ElementType = 'a'>({
17
+ as,
18
+ children,
19
+ variant = 'default',
20
+ external = false,
21
+ className,
22
+ ...props
23
+ }: LinkProps<T>) {
24
+ const Component = (as ?? 'a') as ElementType;
25
+
26
+ const externalProps = external
27
+ ? { target: '_blank', rel: 'noreferrer noopener' }
28
+ : {};
29
+
30
+ return (
31
+ <Component
32
+ className={cn(css.link, css[`--variant_${variant}`], className)}
33
+ {...externalProps}
34
+ {...props}
35
+ >
36
+ {children}
37
+ {external && (
38
+ <span className={css.externalLabel}> (opens in new tab)</span>
39
+ )}
40
+ </Component>
41
+ );
42
+ }
@@ -0,0 +1 @@
1
+ export { Link } from './Link';
@@ -0,0 +1,33 @@
1
+ @layer component {
2
+ .loading {
3
+ display: flex;
4
+ justify-content: center;
5
+ align-items: center;
6
+ }
7
+
8
+ .spinner {
9
+ width: 2.5em;
10
+ height: 2.5em;
11
+ border: 4px solid var(--color_bg);
12
+ border-top: 4px solid var(--color_interactive);
13
+ border-radius: 50%;
14
+ animation: spin 1s linear infinite;
15
+ }
16
+
17
+ .--size_small .spinner {
18
+ width: 1.5em;
19
+ height: 1.5em;
20
+ border-width: 2px;
21
+ }
22
+
23
+ .--size_large .spinner {
24
+ width: 4em;
25
+ height: 4em;
26
+ border-width: 6px;
27
+ }
28
+ }
29
+
30
+ @keyframes spin {
31
+ 0% { transform: rotate(0deg); }
32
+ 100% { transform: rotate(360deg); }
33
+ }
@@ -0,0 +1,19 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Loading } from './Loading';
3
+
4
+ describe('Loading', () => {
5
+ it('renders a status element', () => {
6
+ render(<Loading />);
7
+ expect(screen.getByRole('status')).toBeInTheDocument();
8
+ });
9
+
10
+ it('has an accessible aria-label', () => {
11
+ render(<Loading />);
12
+ expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Loading');
13
+ });
14
+
15
+ it('accepts a custom className', () => {
16
+ const { container } = render(<Loading className="custom" />);
17
+ expect(container.firstChild).toHaveClass('custom');
18
+ });
19
+ });
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Loading } from './Loading';
3
+
4
+ const meta = {
5
+ title: 'UI/Loading',
6
+ component: Loading,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ size: { control: 'select', options: ['small', 'medium', 'large'] },
10
+ },
11
+ } satisfies Meta<typeof Loading>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Small: Story = { args: { size: 'small' } };
17
+ export const Medium: Story = { args: { size: 'medium' } };
18
+ export const Large: Story = { args: { size: 'large' } };
19
+ export const AllSizes: Story = {
20
+ render: () => (
21
+ <div style={{ display: 'flex', gap: '32px', alignItems: 'center' }}>
22
+ <Loading size="small" />
23
+ <Loading size="medium" />
24
+ <Loading size="large" />
25
+ </div>
26
+ ),
27
+ };
@@ -0,0 +1,15 @@
1
+ import css from './Loading.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface LoadingProps {
5
+ size?: 'small' | 'medium' | 'large';
6
+ className?: string;
7
+ }
8
+
9
+ export function Loading({ size = 'medium', className }: LoadingProps) {
10
+ return (
11
+ <div className={cn(css.loading, css[`--size_${size}`], className)}>
12
+ <div className={css.spinner} role="status" aria-label="Loading" />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1 @@
1
+ export { Loading } from './Loading';
@@ -0,0 +1,79 @@
1
+ @layer component {
2
+ .banner {
3
+ --banner_bg: var(--color_blue--subtle);
4
+ --banner_text: var(--color_on-blue--subtle);
5
+ --banner_border: var(--color_blue);
6
+
7
+ display: flex;
8
+ align-items: center;
9
+ gap: var(--space_m);
10
+ padding: var(--space_s) var(--space_m);
11
+ background-color: var(--banner_bg);
12
+ color: var(--banner_text);
13
+ border-block-end: 3px solid var(--banner_border);
14
+ font-size: var(--font_size--body);
15
+ line-height: var(--font_line-height--body);
16
+ width: 100%;
17
+ }
18
+
19
+ .--variant_info {
20
+ --banner_bg: var(--color_blue--subtle);
21
+ --banner_text: var(--color_on-blue--subtle);
22
+ --banner_border: var(--color_blue);
23
+ }
24
+
25
+ .--variant_success {
26
+ --banner_bg: var(--color_green--subtle);
27
+ --banner_text: var(--color_on-green--subtle);
28
+ --banner_border: var(--color_green);
29
+ }
30
+
31
+ .--variant_warning {
32
+ --banner_bg: var(--color_orange--subtle);
33
+ --banner_text: var(--color_on-orange--subtle);
34
+ --banner_border: var(--color_warning);
35
+ }
36
+
37
+ .--variant_error {
38
+ --banner_bg: rgb(from var(--color_error) r g b / 12%);
39
+ --banner_text: var(--color_on-bg);
40
+ --banner_border: var(--color_error);
41
+ }
42
+
43
+ .content {
44
+ flex: 1;
45
+ }
46
+
47
+ .action {
48
+ flex-shrink: 0;
49
+ }
50
+
51
+ .dismiss {
52
+ all: unset;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ flex-shrink: 0;
57
+ cursor: pointer;
58
+ color: currentcolor;
59
+ border-radius: 50%;
60
+ border: 1px solid currentcolor;
61
+ transition: var(--animation_transition);
62
+ }
63
+
64
+ .dismiss svg {
65
+ width: 1rem;
66
+ height: 1rem;
67
+ }
68
+
69
+ .dismiss:focus-visible {
70
+ outline: var(--outline_default);
71
+ outline-offset: var(--outline_offset);
72
+ }
73
+
74
+ @media (hover: hover) and (pointer: fine) {
75
+ .dismiss:hover {
76
+ opacity: 0.7;
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,42 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { NotificationBanner } from './NotificationBanner';
4
+
5
+ describe('NotificationBanner', () => {
6
+ it('renders children', () => {
7
+ render(<NotificationBanner>System maintenance scheduled</NotificationBanner>);
8
+ expect(screen.getByText('System maintenance scheduled')).toBeInTheDocument();
9
+ });
10
+
11
+ it('uses status role for info variant', () => {
12
+ render(<NotificationBanner variant="info">Info</NotificationBanner>);
13
+ expect(screen.getByRole('status')).toBeInTheDocument();
14
+ });
15
+
16
+ it('uses alert role for error variant', () => {
17
+ render(<NotificationBanner variant="error">Error</NotificationBanner>);
18
+ expect(screen.getByRole('alert')).toBeInTheDocument();
19
+ });
20
+
21
+ it('uses alert role for warning variant', () => {
22
+ render(<NotificationBanner variant="warning">Warning</NotificationBanner>);
23
+ expect(screen.getByRole('alert')).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders dismiss button when onDismiss provided', () => {
27
+ render(<NotificationBanner onDismiss={() => {}}>Msg</NotificationBanner>);
28
+ expect(screen.getByRole('button', { name: 'Dismiss notification' })).toBeInTheDocument();
29
+ });
30
+
31
+ it('calls onDismiss when dismiss button clicked', async () => {
32
+ const onDismiss = vi.fn();
33
+ render(<NotificationBanner onDismiss={onDismiss}>Msg</NotificationBanner>);
34
+ await userEvent.click(screen.getByRole('button', { name: 'Dismiss notification' }));
35
+ expect(onDismiss).toHaveBeenCalledOnce();
36
+ });
37
+
38
+ it('renders action slot', () => {
39
+ render(<NotificationBanner action={<button>View details</button>}>Msg</NotificationBanner>);
40
+ expect(screen.getByRole('button', { name: 'View details' })).toBeInTheDocument();
41
+ });
42
+ });
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { NotificationBanner } from './NotificationBanner';
3
+
4
+ const meta = {
5
+ title: 'UI/NotificationBanner',
6
+ component: NotificationBanner,
7
+ tags: ['autodocs'],
8
+ } satisfies Meta<typeof NotificationBanner>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Info: Story = { args: { variant: 'info', children: 'A new version is available. Refresh to update.' } };
14
+ export const Success: Story = { args: { variant: 'success', children: 'Your changes have been saved successfully.' } };
15
+ export const Warning: Story = { args: { variant: 'warning', children: 'Your session will expire in 5 minutes.' } };
16
+ export const Error: Story = { args: { variant: 'error', children: 'Payment processing failed. Please try again.' } };
17
+ export const WithAction: Story = {
18
+ args: {
19
+ variant: 'info',
20
+ children: 'A new version is available.',
21
+ action: <button type="button">Update now</button>,
22
+ },
23
+ };
24
+ export const Dismissible: Story = {
25
+ args: {
26
+ variant: 'warning',
27
+ children: 'This is a dismissible banner.',
28
+ onDismiss: () => alert('dismissed'),
29
+ },
30
+ };
@@ -0,0 +1,45 @@
1
+ import { ReactNode } from 'react';
2
+ import css from './NotificationBanner.module.css';
3
+ import { cn } from '@boostdev/design-system-foundation';
4
+
5
+ interface NotificationBannerProps {
6
+ variant?: 'info' | 'success' | 'warning' | 'error';
7
+ children: ReactNode;
8
+ action?: ReactNode;
9
+ onDismiss?: () => void;
10
+ className?: string;
11
+ }
12
+
13
+ export function NotificationBanner({
14
+ variant = 'info',
15
+ children,
16
+ action,
17
+ onDismiss,
18
+ className,
19
+ }: Readonly<NotificationBannerProps>) {
20
+ const isUrgent = variant === 'error' || variant === 'warning';
21
+
22
+ return (
23
+ <div
24
+ role={isUrgent ? 'alert' : 'status'}
25
+ aria-live={isUrgent ? 'assertive' : 'polite'}
26
+ aria-atomic="true"
27
+ className={cn(css.banner, css[`--variant_${variant}`], className)}
28
+ >
29
+ <div className={css.content}>{children}</div>
30
+ {action && <div className={css.action}>{action}</div>}
31
+ {onDismiss && (
32
+ <button
33
+ type="button"
34
+ className={css.dismiss}
35
+ onClick={onDismiss}
36
+ aria-label="Dismiss notification"
37
+ >
38
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
39
+ <path strokeLinecap="round" strokeLinejoin="round" d="M18 6L6 18M6 6l12 12" />
40
+ </svg>
41
+ </button>
42
+ )}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export { NotificationBanner } from './NotificationBanner';
@@ -0,0 +1,78 @@
1
+ @layer component {
2
+ .pagination {
3
+ display: flex;
4
+ justify-content: center;
5
+ }
6
+
7
+ .list {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: var(--space_xxs);
11
+ list-style: none;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ .button {
17
+ --btn_bg: transparent;
18
+ --btn_color: var(--color_on-bg);
19
+ --btn_border: var(--color_bg--subtle);
20
+
21
+ all: unset;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ min-width: 2.25rem;
26
+ height: 2.25rem;
27
+ padding-inline: var(--space_xs);
28
+ border-radius: var(--border_radius--xs);
29
+ border: 1px solid var(--btn_border);
30
+ background-color: var(--btn_bg);
31
+ color: var(--btn_color);
32
+ font-family: var(--font_family--body);
33
+ font-size: var(--font_size--body);
34
+ font-variant-numeric: tabular-nums;
35
+ cursor: pointer;
36
+ transition: var(--animation_transition);
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ .button:disabled {
41
+ opacity: 0.4;
42
+ cursor: not-allowed;
43
+ }
44
+
45
+ .button:focus-visible {
46
+ outline: var(--outline_default);
47
+ outline-offset: var(--outline_offset);
48
+ }
49
+
50
+ @media (hover: hover) and (pointer: fine) {
51
+ .button:not(:disabled, .--active):hover {
52
+ --btn_bg: var(--color_bg--subtle);
53
+ --btn_color: var(--color_interactive);
54
+ }
55
+ }
56
+
57
+ .button.--active {
58
+ --btn_bg: var(--color_interactive);
59
+ --btn_color: var(--color_on-interactive);
60
+ --btn_border: var(--color_interactive);
61
+ }
62
+
63
+ .button.--nav svg {
64
+ width: 1rem;
65
+ height: 1rem;
66
+ }
67
+
68
+ .ellipsis {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ min-width: 2.25rem;
73
+ height: 2.25rem;
74
+ color: var(--color_on-bg--subtle);
75
+ font-size: var(--font_size--body);
76
+ user-select: none;
77
+ }
78
+ }
@@ -0,0 +1,67 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Pagination } from './Pagination';
4
+
5
+ describe('Pagination', () => {
6
+ it('renders a navigation landmark', () => {
7
+ render(<Pagination currentPage={1} totalPages={5} onPageChange={() => {}} />);
8
+ expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument();
9
+ });
10
+
11
+ it('renders a button for each page', () => {
12
+ render(<Pagination currentPage={1} totalPages={3} onPageChange={() => {}} />);
13
+ expect(screen.getByRole('button', { name: 'Page 1' })).toBeInTheDocument();
14
+ expect(screen.getByRole('button', { name: 'Page 2' })).toBeInTheDocument();
15
+ expect(screen.getByRole('button', { name: 'Page 3' })).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders previous and next buttons', () => {
19
+ render(<Pagination currentPage={2} totalPages={5} onPageChange={() => {}} />);
20
+ expect(screen.getByRole('button', { name: 'Previous page' })).toBeInTheDocument();
21
+ expect(screen.getByRole('button', { name: 'Next page' })).toBeInTheDocument();
22
+ });
23
+
24
+ it('marks the current page with aria-current="page"', () => {
25
+ render(<Pagination currentPage={2} totalPages={5} onPageChange={() => {}} />);
26
+ expect(screen.getByRole('button', { name: 'Page 2' })).toHaveAttribute('aria-current', 'page');
27
+ });
28
+
29
+ it('disables the previous button on the first page', () => {
30
+ render(<Pagination currentPage={1} totalPages={5} onPageChange={() => {}} />);
31
+ expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
32
+ });
33
+
34
+ it('disables the next button on the last page', () => {
35
+ render(<Pagination currentPage={5} totalPages={5} onPageChange={() => {}} />);
36
+ expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
37
+ });
38
+
39
+ it('calls onPageChange with the next page when next is clicked', async () => {
40
+ const user = userEvent.setup();
41
+ const onPageChange = vi.fn();
42
+ render(<Pagination currentPage={2} totalPages={5} onPageChange={onPageChange} />);
43
+ await user.click(screen.getByRole('button', { name: 'Next page' }));
44
+ expect(onPageChange).toHaveBeenCalledWith(3);
45
+ });
46
+
47
+ it('calls onPageChange with the previous page when previous is clicked', async () => {
48
+ const user = userEvent.setup();
49
+ const onPageChange = vi.fn();
50
+ render(<Pagination currentPage={3} totalPages={5} onPageChange={onPageChange} />);
51
+ await user.click(screen.getByRole('button', { name: 'Previous page' }));
52
+ expect(onPageChange).toHaveBeenCalledWith(2);
53
+ });
54
+
55
+ it('calls onPageChange with the correct page number when a page button is clicked', async () => {
56
+ const user = userEvent.setup();
57
+ const onPageChange = vi.fn();
58
+ render(<Pagination currentPage={1} totalPages={3} onPageChange={onPageChange} />);
59
+ await user.click(screen.getByRole('button', { name: 'Page 3' }));
60
+ expect(onPageChange).toHaveBeenCalledWith(3);
61
+ });
62
+
63
+ it('renders ellipsis for large page counts', () => {
64
+ render(<Pagination currentPage={5} totalPages={10} onPageChange={() => {}} />);
65
+ expect(screen.getAllByText('…').length).toBeGreaterThan(0);
66
+ });
67
+ });
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { Pagination } from './Pagination';
4
+
5
+ const meta = {
6
+ title: 'UI/Pagination',
7
+ component: Pagination,
8
+ tags: ['autodocs'],
9
+ } satisfies Meta<typeof Pagination>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: { currentPage: 3, totalPages: 7, onPageChange: () => {} },
16
+ };
17
+
18
+ export const FirstPage: Story = {
19
+ args: { currentPage: 1, totalPages: 5, onPageChange: () => {} },
20
+ };
21
+
22
+ export const LastPage: Story = {
23
+ args: { currentPage: 5, totalPages: 5, onPageChange: () => {} },
24
+ };
25
+
26
+ export const ManyPages: Story = {
27
+ args: { currentPage: 5, totalPages: 20, onPageChange: () => {} },
28
+ };
29
+
30
+ export const Interactive: Story = {
31
+ render: () => {
32
+ const [page, setPage] = useState(1);
33
+ return (
34
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'center' }}>
35
+ <p>Current page: {page}</p>
36
+ <Pagination currentPage={page} totalPages={10} onPageChange={setPage} />
37
+ </div>
38
+ );
39
+ },
40
+ };
@@ -0,0 +1,87 @@
1
+ import css from './Pagination.module.css';
2
+ import { cn } from '@boostdev/design-system-foundation';
3
+
4
+ interface PaginationProps {
5
+ currentPage: number;
6
+ totalPages: number;
7
+ onPageChange: (page: number) => void;
8
+ className?: string;
9
+ }
10
+
11
+ function getPageRange(current: number, total: number): (number | '...')[] {
12
+ const delta = 1;
13
+ const range: (number | '...')[] = [];
14
+
15
+ for (let i = 1; i <= total; i++) {
16
+ if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
17
+ range.push(i);
18
+ } else if (range[range.length - 1] !== '...') {
19
+ range.push('...');
20
+ }
21
+ }
22
+
23
+ return range;
24
+ }
25
+
26
+ export function Pagination({
27
+ currentPage,
28
+ totalPages,
29
+ onPageChange,
30
+ className,
31
+ }: Readonly<PaginationProps>) {
32
+ const pages = getPageRange(currentPage, totalPages);
33
+
34
+ return (
35
+ <nav aria-label="Pagination" className={cn(css.pagination, className)}>
36
+ <ul className={css.list}>
37
+ <li>
38
+ <button
39
+ type="button"
40
+ className={cn(css.button, css['--nav'])}
41
+ onClick={() => onPageChange(currentPage - 1)}
42
+ disabled={currentPage <= 1}
43
+ aria-label="Previous page"
44
+ >
45
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
46
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
47
+ </svg>
48
+ </button>
49
+ </li>
50
+
51
+ {pages.map((page, index) =>
52
+ page === '...' ? (
53
+ <li key={`ellipsis-${index}`}>
54
+ <span className={css.ellipsis} aria-hidden="true">…</span>
55
+ </li>
56
+ ) : (
57
+ <li key={page}>
58
+ <button
59
+ type="button"
60
+ className={cn(css.button, currentPage === page ? css['--active'] : undefined)}
61
+ onClick={() => onPageChange(page)}
62
+ aria-label={`Page ${page}`}
63
+ aria-current={currentPage === page ? 'page' : undefined}
64
+ >
65
+ {page}
66
+ </button>
67
+ </li>
68
+ )
69
+ )}
70
+
71
+ <li>
72
+ <button
73
+ type="button"
74
+ className={cn(css.button, css['--nav'])}
75
+ onClick={() => onPageChange(currentPage + 1)}
76
+ disabled={currentPage >= totalPages}
77
+ aria-label="Next page"
78
+ >
79
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
80
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
81
+ </svg>
82
+ </button>
83
+ </li>
84
+ </ul>
85
+ </nav>
86
+ );
87
+ }
@@ -0,0 +1 @@
1
+ export { Pagination } from './Pagination';
@@ -0,0 +1,51 @@
1
+ @layer component {
2
+ .container {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--space_xxs);
6
+ width: 100%;
7
+ }
8
+
9
+ .labelRow {
10
+ display: flex;
11
+ justify-content: space-between;
12
+ align-items: baseline;
13
+ font-size: var(--font_size--body--s);
14
+ color: var(--color_on-bg);
15
+ }
16
+
17
+ .label {
18
+ font-weight: var(--font_weight--semibold);
19
+ }
20
+
21
+ .value {
22
+ font-variant-numeric: tabular-nums;
23
+ }
24
+
25
+ .track {
26
+ --progress_height: var(--space_xs);
27
+
28
+ width: 100%;
29
+ height: var(--progress_height);
30
+ background-color: var(--color_bg--subtle);
31
+ border-radius: 999px;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .track.--size_small { --progress_height: var(--space_xxs); }
36
+ .track.--size_medium { --progress_height: var(--space_xs); }
37
+ .track.--size_large { --progress_height: var(--space_s); }
38
+
39
+ .fill {
40
+ height: 100%;
41
+ background-color: var(--color_green);
42
+ border-radius: 999px;
43
+ transition: width var(--animation_transition-duration) var(--animation_easing);
44
+ }
45
+
46
+ @media (prefers-reduced-motion: reduce) {
47
+ .fill {
48
+ transition: none;
49
+ }
50
+ }
51
+ }