@boostdev/design-system-components 1.0.2 → 1.1.0

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 (268) hide show
  1. package/AGENTS.md +20 -18
  2. package/dist/client.cjs +236 -153
  3. package/dist/client.css +785 -702
  4. package/dist/client.d.cts +53 -94
  5. package/dist/client.d.ts +53 -94
  6. package/dist/client.js +237 -163
  7. package/dist/index.cjs +236 -153
  8. package/dist/index.css +785 -702
  9. package/dist/index.d.cts +53 -94
  10. package/dist/index.d.ts +53 -94
  11. package/dist/index.js +237 -163
  12. package/dist/native/index.cjs +991 -0
  13. package/dist/native/index.d.cts +208 -0
  14. package/dist/native/index.d.ts +208 -0
  15. package/dist/native/index.js +968 -0
  16. package/dist/web-components.d.ts +303 -0
  17. package/dist/web-components.js +1968 -0
  18. package/package.json +29 -7
  19. package/src/components/interaction/Button/Button.module.css +14 -11
  20. package/src/components/interaction/Button/Button.native.spec.tsx +35 -0
  21. package/src/components/interaction/Button/Button.native.stories.tsx +42 -0
  22. package/src/components/interaction/Button/Button.native.tsx +95 -0
  23. package/src/components/interaction/Button/Button.stories.tsx +1 -1
  24. package/src/components/interaction/Button/Button.tsx +2 -2
  25. package/src/components/interaction/Command/Command.mdx +16 -0
  26. package/src/components/interaction/Command/Command.module.css +12 -12
  27. package/src/components/interaction/Command/Command.stories.tsx +1 -1
  28. package/src/components/interaction/Command/Command.tsx +2 -2
  29. package/src/components/interaction/Dialog/Dialog.module.css +3 -3
  30. package/src/components/interaction/Dialog/Dialog.stories.tsx +1 -1
  31. package/src/components/interaction/Dialog/Dialog.tsx +2 -2
  32. package/src/components/interaction/Drawer/Drawer.mdx +14 -0
  33. package/src/components/interaction/Drawer/Drawer.module.css +3 -3
  34. package/src/components/interaction/Drawer/Drawer.stories.tsx +1 -1
  35. package/src/components/interaction/Drawer/Drawer.tsx +2 -2
  36. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +3 -3
  37. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +1 -1
  38. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +2 -2
  39. package/src/components/interaction/Popover/Popover.mdx +17 -0
  40. package/src/components/interaction/Popover/Popover.module.css +72 -15
  41. package/src/components/interaction/Popover/Popover.spec.tsx +68 -4
  42. package/src/components/interaction/Popover/Popover.stories.tsx +1 -1
  43. package/src/components/interaction/Popover/Popover.tsx +76 -20
  44. package/src/components/interaction/Rating/Rating.module.css +2 -2
  45. package/src/components/interaction/Rating/Rating.stories.tsx +1 -1
  46. package/src/components/interaction/Rating/Rating.tsx +2 -2
  47. package/src/components/interaction/Toast/Toast.module.css +16 -11
  48. package/src/components/interaction/Toast/Toast.spec.tsx +21 -0
  49. package/src/components/interaction/Toast/Toast.stories.tsx +1 -1
  50. package/src/components/interaction/Toast/Toast.tsx +22 -2
  51. package/src/components/interaction/form/Checkbox/Checkbox.mdx +2 -2
  52. package/src/components/interaction/form/Checkbox/Checkbox.module.css +5 -5
  53. package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +42 -0
  54. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +36 -0
  55. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +80 -0
  56. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +1 -1
  57. package/src/components/interaction/form/Checkbox/Checkbox.tsx +2 -2
  58. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +44 -0
  59. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +2 -2
  60. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +1 -1
  61. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +2 -2
  62. package/src/components/interaction/form/Combobox/Combobox.mdx +21 -0
  63. package/src/components/interaction/form/Combobox/Combobox.module.css +8 -8
  64. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +1 -1
  65. package/src/components/interaction/form/Combobox/Combobox.tsx +2 -2
  66. package/src/components/interaction/form/FileInput/FileInput.mdx +14 -0
  67. package/src/components/interaction/form/FileInput/FileInput.module.css +8 -8
  68. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +1 -1
  69. package/src/components/interaction/form/FileInput/FileInput.tsx +2 -2
  70. package/src/components/interaction/form/FormInput/FormInput.module.css +2 -2
  71. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +1 -1
  72. package/src/components/interaction/form/FormInput/FormInput.tsx +2 -2
  73. package/src/components/interaction/form/NumberInput/NumberInput.mdx +16 -0
  74. package/src/components/interaction/form/NumberInput/NumberInput.module.css +4 -4
  75. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +1 -1
  76. package/src/components/interaction/form/NumberInput/NumberInput.tsx +2 -2
  77. package/src/components/interaction/form/Radio/Radio.mdx +3 -3
  78. package/src/components/interaction/form/Radio/Radio.module.css +6 -6
  79. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +46 -0
  80. package/src/components/interaction/form/Radio/Radio.native.tsx +79 -0
  81. package/src/components/interaction/form/Radio/Radio.stories.tsx +1 -1
  82. package/src/components/interaction/form/Radio/Radio.tsx +2 -2
  83. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +45 -0
  84. package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +2 -2
  85. package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +1 -1
  86. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +2 -2
  87. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
  88. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +1 -1
  89. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +2 -2
  90. package/src/components/interaction/form/Select/Select.module.css +3 -3
  91. package/src/components/interaction/form/Select/Select.stories.tsx +1 -1
  92. package/src/components/interaction/form/Select/Select.tsx +2 -2
  93. package/src/components/interaction/form/Slider/Slider.mdx +1 -1
  94. package/src/components/interaction/form/Slider/Slider.module.css +14 -14
  95. package/src/components/interaction/form/Slider/Slider.stories.tsx +1 -1
  96. package/src/components/interaction/form/Slider/Slider.tsx +2 -2
  97. package/src/components/interaction/form/Switch/Switch.mdx +4 -4
  98. package/src/components/interaction/form/Switch/Switch.module.css +8 -8
  99. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +60 -0
  100. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +35 -0
  101. package/src/components/interaction/form/Switch/Switch.native.tsx +59 -0
  102. package/src/components/interaction/form/Switch/Switch.stories.tsx +1 -1
  103. package/src/components/interaction/form/Switch/Switch.tsx +2 -2
  104. package/src/components/interaction/form/Textarea/Textarea.mdx +17 -0
  105. package/src/components/interaction/form/Textarea/Textarea.module.css +2 -2
  106. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +1 -1
  107. package/src/components/interaction/form/Textarea/Textarea.tsx +2 -2
  108. package/src/components/interaction/form/atoms/InputContainer.tsx +2 -1
  109. package/src/components/interaction/form/atoms/Label.module.css +1 -1
  110. package/src/components/interaction/form/atoms/Label.native.stories.tsx +18 -0
  111. package/src/components/interaction/form/atoms/Label.native.tsx +29 -0
  112. package/src/components/interaction/form/atoms/Label.tsx +5 -3
  113. package/src/components/interaction/form/atoms/Message.native.spec.tsx +30 -0
  114. package/src/components/interaction/form/atoms/Message.native.stories.tsx +22 -0
  115. package/src/components/interaction/form/atoms/Message.native.tsx +34 -0
  116. package/src/components/interaction/form/atoms/Message.tsx +5 -3
  117. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +50 -0
  118. package/src/components/layout/ButtonGroup/ButtonGroup.native.tsx +34 -0
  119. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +1 -1
  120. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +2 -2
  121. package/src/components/layout/Card/Card.module.css +2 -2
  122. package/src/components/layout/Card/Card.native.stories.tsx +53 -0
  123. package/src/components/layout/Card/Card.native.tsx +89 -0
  124. package/src/components/layout/Card/Card.stories.tsx +1 -1
  125. package/src/components/layout/Card/Card.tsx +2 -2
  126. package/src/components/layout/IconWrapper/IconWrapper.mdx +2 -2
  127. package/src/components/layout/IconWrapper/IconWrapper.module.css +3 -3
  128. package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +39 -0
  129. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +41 -0
  130. package/src/components/layout/IconWrapper/IconWrapper.native.tsx +20 -0
  131. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +1 -1
  132. package/src/components/layout/IconWrapper/IconWrapper.tsx +2 -2
  133. package/src/components/layout/SectionHeader/SectionHeader.mdx +13 -0
  134. package/src/components/layout/SectionHeader/SectionHeader.module.css +2 -2
  135. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +38 -0
  136. package/src/components/layout/SectionHeader/SectionHeader.native.tsx +79 -0
  137. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +1 -1
  138. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -2
  139. package/src/components/ui/Accordion/Accordion.mdx +14 -0
  140. package/src/components/ui/Accordion/Accordion.module.css +5 -5
  141. package/src/components/ui/Accordion/Accordion.stories.tsx +1 -1
  142. package/src/components/ui/Accordion/Accordion.tsx +2 -2
  143. package/src/components/ui/Alert/Alert.module.css +15 -15
  144. package/src/components/ui/Alert/Alert.native.stories.tsx +43 -0
  145. package/src/components/ui/Alert/Alert.native.tsx +94 -0
  146. package/src/components/ui/Alert/Alert.stories.tsx +1 -1
  147. package/src/components/ui/Alert/Alert.tsx +2 -2
  148. package/src/components/ui/Avatar/Avatar.module.css +2 -2
  149. package/src/components/ui/Avatar/Avatar.native.spec.tsx +47 -0
  150. package/src/components/ui/Avatar/Avatar.native.stories.tsx +34 -0
  151. package/src/components/ui/Avatar/Avatar.native.tsx +85 -0
  152. package/src/components/ui/Avatar/Avatar.stories.tsx +1 -1
  153. package/src/components/ui/Avatar/Avatar.tsx +2 -2
  154. package/src/components/ui/Badge/Badge.mdx +2 -2
  155. package/src/components/ui/Badge/Badge.module.css +12 -12
  156. package/src/components/ui/Badge/Badge.native.stories.tsx +36 -0
  157. package/src/components/ui/Badge/Badge.native.tsx +50 -0
  158. package/src/components/ui/Badge/Badge.stories.tsx +1 -1
  159. package/src/components/ui/Badge/Badge.tsx +2 -2
  160. package/src/components/ui/Breadcrumb/Breadcrumb.module.css +2 -2
  161. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +1 -1
  162. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -2
  163. package/src/components/ui/Calendar/Calendar.mdx +16 -0
  164. package/src/components/ui/Calendar/Calendar.module.css +11 -11
  165. package/src/components/ui/Calendar/Calendar.stories.tsx +1 -1
  166. package/src/components/ui/Calendar/Calendar.tsx +2 -2
  167. package/src/components/ui/Carousel/Carousel.module.css +4 -4
  168. package/src/components/ui/Carousel/Carousel.stories.tsx +1 -1
  169. package/src/components/ui/Carousel/Carousel.tsx +2 -2
  170. package/src/components/ui/Collapsible/Collapsible.module.css +6 -6
  171. package/src/components/ui/Collapsible/Collapsible.stories.tsx +1 -1
  172. package/src/components/ui/Collapsible/Collapsible.tsx +2 -2
  173. package/src/components/ui/DescriptionList/DescriptionList.module.css +3 -3
  174. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +1 -1
  175. package/src/components/ui/DescriptionList/DescriptionList.tsx +2 -2
  176. package/src/components/ui/Link/Link.mdx +14 -0
  177. package/src/components/ui/Link/Link.module.css +4 -4
  178. package/src/components/ui/Link/Link.stories.tsx +1 -1
  179. package/src/components/ui/Link/Link.tsx +2 -2
  180. package/src/components/ui/Loading/Loading.module.css +2 -2
  181. package/src/components/ui/Loading/Loading.native.spec.tsx +24 -0
  182. package/src/components/ui/Loading/Loading.native.stories.tsx +33 -0
  183. package/src/components/ui/Loading/Loading.native.tsx +29 -0
  184. package/src/components/ui/Loading/Loading.stories.tsx +1 -1
  185. package/src/components/ui/Loading/Loading.tsx +2 -2
  186. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +15 -15
  187. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +39 -0
  188. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +76 -0
  189. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +1 -1
  190. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +2 -2
  191. package/src/components/ui/Pagination/Pagination.module.css +8 -8
  192. package/src/components/ui/Pagination/Pagination.stories.tsx +1 -1
  193. package/src/components/ui/Pagination/Pagination.tsx +2 -2
  194. package/src/components/ui/Progress/Progress.mdx +1 -1
  195. package/src/components/ui/Progress/Progress.module.css +3 -3
  196. package/src/components/ui/Progress/Progress.native.stories.tsx +34 -0
  197. package/src/components/ui/Progress/Progress.native.tsx +84 -0
  198. package/src/components/ui/Progress/Progress.stories.tsx +1 -1
  199. package/src/components/ui/Progress/Progress.tsx +2 -2
  200. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +3 -3
  201. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +1 -1
  202. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +2 -2
  203. package/src/components/ui/Separator/Separator.mdx +14 -0
  204. package/src/components/ui/Separator/Separator.module.css +1 -1
  205. package/src/components/ui/Separator/Separator.native.stories.tsx +42 -0
  206. package/src/components/ui/Separator/Separator.native.tsx +32 -0
  207. package/src/components/ui/Separator/Separator.stories.tsx +1 -1
  208. package/src/components/ui/Separator/Separator.tsx +2 -2
  209. package/src/components/ui/Skeleton/Skeleton.module.css +3 -3
  210. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +33 -0
  211. package/src/components/ui/Skeleton/Skeleton.native.tsx +41 -0
  212. package/src/components/ui/Skeleton/Skeleton.stories.tsx +1 -1
  213. package/src/components/ui/Skeleton/Skeleton.tsx +2 -3
  214. package/src/components/ui/SkipLink/SkipLink.module.css +3 -3
  215. package/src/components/ui/SkipLink/SkipLink.stories.tsx +1 -1
  216. package/src/components/ui/SkipLink/SkipLink.tsx +5 -3
  217. package/src/components/ui/Table/Table.mdx +14 -0
  218. package/src/components/ui/Table/Table.module.css +11 -11
  219. package/src/components/ui/Table/Table.stories.tsx +1 -1
  220. package/src/components/ui/Table/Table.tsx +2 -2
  221. package/src/components/ui/Tabs/Tabs.module.css +3 -3
  222. package/src/components/ui/Tabs/Tabs.stories.tsx +1 -1
  223. package/src/components/ui/Tabs/Tabs.tsx +2 -2
  224. package/src/components/ui/Tooltip/Tooltip.mdx +14 -0
  225. package/src/components/ui/Tooltip/Tooltip.module.css +2 -2
  226. package/src/components/ui/Tooltip/Tooltip.stories.tsx +1 -1
  227. package/src/components/ui/Tooltip/Tooltip.tsx +2 -2
  228. package/src/components/ui/Typography/Typography.mdx +13 -0
  229. package/src/components/ui/Typography/Typography.module.css +1 -1
  230. package/src/components/ui/Typography/Typography.native.stories.tsx +38 -0
  231. package/src/components/ui/Typography/Typography.native.tsx +65 -0
  232. package/src/components/ui/Typography/Typography.stories.tsx +1 -1
  233. package/src/components/ui/Typography/Typography.tsx +2 -2
  234. package/src/css/bdc.css +10 -2
  235. package/src/index.ts +3 -0
  236. package/src/native/ThemeContext.tsx +28 -0
  237. package/src/native/tokens.ts +13 -0
  238. package/src/native.ts +39 -0
  239. package/src/react-augment.d.ts +13 -0
  240. package/src/stories/DesignSystem/Borders.mdx +7 -7
  241. package/src/stories/DesignSystem/Colors.mdx +28 -29
  242. package/src/stories/DesignSystem/DarkMode.mdx +130 -0
  243. package/src/stories/DesignSystem/Elevation.mdx +4 -4
  244. package/src/stories/DesignSystem/Grid.mdx +5 -5
  245. package/src/stories/DesignSystem/Motion.mdx +2 -2
  246. package/src/stories/DesignSystem/Overview.mdx +1 -1
  247. package/src/stories/DesignSystem/Spacing.mdx +3 -3
  248. package/src/stories/DesignSystem/Typography.mdx +6 -6
  249. package/src/types.ts +2 -0
  250. package/src/typings.d.ts +3 -0
  251. package/src/web-components/globals.ts +61 -0
  252. package/src/web-components/index.ts +12 -0
  253. package/src/web-components/interaction/BdsButton.stories.tsx +60 -0
  254. package/src/web-components/interaction/BdsPopover.stories.tsx +70 -0
  255. package/src/web-components/interaction/BdsToastProvider.stories.tsx +73 -0
  256. package/src/web-components/interaction/bds-button.spec.ts +95 -0
  257. package/src/web-components/interaction/bds-button.ts +293 -0
  258. package/src/web-components/interaction/bds-popover.spec.ts +126 -0
  259. package/src/web-components/interaction/bds-popover.ts +217 -0
  260. package/src/web-components/interaction/bds-toast-provider.spec.ts +122 -0
  261. package/src/web-components/interaction/bds-toast-provider.ts +211 -0
  262. package/src/web-components/test/helpers.ts +14 -0
  263. package/src/web-components/ui/BdsAlert.stories.tsx +60 -0
  264. package/src/web-components/ui/BdsBadge.stories.tsx +37 -0
  265. package/src/web-components/ui/bds-alert.spec.ts +109 -0
  266. package/src/web-components/ui/bds-alert.ts +209 -0
  267. package/src/web-components/ui/bds-badge.spec.ts +51 -0
  268. package/src/web-components/ui/bds-badge.ts +88 -0
@@ -0,0 +1,122 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsToastProvider } from './bds-toast-provider';
3
+
4
+ describe('bds-toast-provider', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-toast-provider')).toBe(BdsToastProvider);
7
+ });
8
+
9
+ it('renders a popover="manual" container in shadow DOM', async () => {
10
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>');
11
+ const container = el.shadowRoot!.querySelector('[popover]');
12
+ expect(container).not.toBeNull();
13
+ expect(container!.getAttribute('popover')).toBe('manual');
14
+ cleanup(el);
15
+ });
16
+
17
+ it('container has role="region" and aria-label="Notifications"', async () => {
18
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>');
19
+ const container = el.shadowRoot!.querySelector('[popover="manual"]');
20
+ expect(container!.getAttribute('role')).toBe('region');
21
+ expect(container!.getAttribute('aria-label')).toBe('Notifications');
22
+ cleanup(el);
23
+ });
24
+
25
+ it('shows no toasts initially', async () => {
26
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>');
27
+ const toasts = el.shadowRoot!.querySelectorAll('.toast');
28
+ expect(toasts.length).toBe(0);
29
+ cleanup(el);
30
+ });
31
+
32
+ it('showToast adds a toast to the shadow DOM', async () => {
33
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
34
+ el.showToast('Hello toast', 'info');
35
+ await el.updateComplete;
36
+ const toasts = el.shadowRoot!.querySelectorAll('.toast');
37
+ expect(toasts.length).toBe(1);
38
+ expect(toasts[0].querySelector('.message')!.textContent).toBe('Hello toast');
39
+ cleanup(el);
40
+ });
41
+
42
+ it('applies the correct variant class', async () => {
43
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
44
+ el.showToast('Success!', 'success');
45
+ await el.updateComplete;
46
+ const toast = el.shadowRoot!.querySelector('.toast');
47
+ expect(toast!.classList.contains('--variant_success')).toBe(true);
48
+ cleanup(el);
49
+ });
50
+
51
+ it('dismiss button click removes the toast', async () => {
52
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
53
+ el.showToast('Dismiss me', 'info');
54
+ await el.updateComplete;
55
+ const btn = el.shadowRoot!.querySelector<HTMLButtonElement>('.close')!;
56
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
57
+ await el.updateComplete;
58
+ expect(el.shadowRoot!.querySelectorAll('.toast').length).toBe(0);
59
+ cleanup(el);
60
+ });
61
+
62
+ it('auto-removes the toast after 5 seconds', async () => {
63
+ vi.useFakeTimers();
64
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
65
+ el.showToast('Timed out', 'warning');
66
+ await el.updateComplete;
67
+ expect(el.shadowRoot!.querySelectorAll('.toast').length).toBe(1);
68
+ vi.advanceTimersByTime(5100);
69
+ await el.updateComplete;
70
+ expect(el.shadowRoot!.querySelectorAll('.toast').length).toBe(0);
71
+ vi.useRealTimers();
72
+ cleanup(el);
73
+ });
74
+
75
+ it('static BdsToastProvider.show() adds a toast via the first provider', async () => {
76
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
77
+ BdsToastProvider.show('Static show', 'error');
78
+ await el.updateComplete;
79
+ const toast = el.shadowRoot!.querySelector('.toast');
80
+ expect(toast).not.toBeNull();
81
+ expect(toast!.querySelector('.message')!.textContent).toBe('Static show');
82
+ cleanup(el);
83
+ });
84
+
85
+ it('bds-show-toast document event adds a toast', async () => {
86
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
87
+ document.dispatchEvent(
88
+ new CustomEvent('bds-show-toast', { detail: { message: 'Event toast', variant: 'success' } }),
89
+ );
90
+ await el.updateComplete;
91
+ const toast = el.shadowRoot!.querySelector('.toast');
92
+ expect(toast).not.toBeNull();
93
+ expect(toast!.querySelector('.message')!.textContent).toBe('Event toast');
94
+ cleanup(el);
95
+ });
96
+
97
+ it('showToast defaults variant to info when not specified', async () => {
98
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
99
+ el.showToast('No variant');
100
+ await el.updateComplete;
101
+ const toast = el.shadowRoot!.querySelector('.toast');
102
+ expect(toast!.classList.contains('--variant_info')).toBe(true);
103
+ cleanup(el);
104
+ });
105
+
106
+ it('bds-show-toast event with no message is a no-op', async () => {
107
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
108
+ document.dispatchEvent(new CustomEvent('bds-show-toast', { detail: {} }));
109
+ await el.updateComplete;
110
+ expect(el.shadowRoot!.querySelectorAll('.toast').length).toBe(0);
111
+ cleanup(el);
112
+ });
113
+
114
+ it('multiple toasts stack in the container', async () => {
115
+ const el = await fixture('<bds-toast-provider></bds-toast-provider>') as BdsToastProvider;
116
+ el.showToast('First', 'info');
117
+ el.showToast('Second', 'success');
118
+ await el.updateComplete;
119
+ expect(el.shadowRoot!.querySelectorAll('.toast').length).toBe(2);
120
+ cleanup(el);
121
+ });
122
+ });
@@ -0,0 +1,211 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ export type ToastVariant = 'success' | 'error' | 'info' | 'warning';
4
+
5
+ interface ToastEntry {
6
+ id: string;
7
+ message: string;
8
+ variant: ToastVariant;
9
+ }
10
+
11
+ /**
12
+ * `<bds-toast-provider>` — framework-agnostic Toast provider custom element.
13
+ *
14
+ * Wraps page content and manages a `popover="manual"` toast container that
15
+ * renders in the browser top layer — above all fixed/sticky elements without
16
+ * needing a z-index.
17
+ *
18
+ * Usage:
19
+ * // Instance method (direct reference)
20
+ * provider.showToast('Saved!', 'success');
21
+ *
22
+ * // Static convenience (finds the first provider in the DOM)
23
+ * BdsToastProvider.show('Something went wrong', 'error');
24
+ *
25
+ * // Event-driven (no import required)
26
+ * document.dispatchEvent(new CustomEvent('bds-show-toast', {
27
+ * detail: { message: 'Hello', variant: 'info' }
28
+ * }));
29
+ *
30
+ * Slots:
31
+ * (default) — page content; the host uses display:contents so layout
32
+ * is unaffected
33
+ *
34
+ * @example
35
+ * <bds-toast-provider>
36
+ * <main>…</main>
37
+ * </bds-toast-provider>
38
+ */
39
+ export class BdsToastProvider extends LitElement {
40
+ static styles = css`
41
+ :host {
42
+ display: contents;
43
+ }
44
+
45
+ .container {
46
+ /* Reset UA popover defaults */
47
+ position: fixed;
48
+ inset: unset;
49
+ bottom: var(--bds-space_xl, 2rem);
50
+ right: var(--bds-space_xl, 2rem);
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--bds-space_m, 1rem);
54
+ border: none;
55
+ padding: 0;
56
+ background: transparent;
57
+ }
58
+
59
+ .toast {
60
+ padding: var(--bds-space_m) var(--bds-space_l);
61
+ background-color: var(--toast_color, var(--bds-color_bg));
62
+ color: var(--toast_on-color, var(--bds-color_on-bg));
63
+ border-radius: var(--bds-border_radius--s);
64
+ box-shadow: var(--bds-shadow_s);
65
+ display: flex;
66
+ align-items: center;
67
+ gap: var(--bds-space_m);
68
+ min-width: 300px;
69
+ }
70
+
71
+ .toast.--variant_success {
72
+ --toast_color: var(--bds-color_success);
73
+ --toast_on-color: var(--bds-color_on-success);
74
+ }
75
+
76
+ .toast.--variant_warning {
77
+ --toast_color: var(--bds-color_warning);
78
+ --toast_on-color: var(--bds-color_on-warning);
79
+ }
80
+
81
+ .toast.--variant_info {
82
+ --toast_color: var(--bds-color_bg--subtle);
83
+ --toast_on-color: var(--bds-color_on-bg--subtle);
84
+ }
85
+
86
+ .toast.--variant_error {
87
+ --toast_color: var(--bds-color_error);
88
+ --toast_on-color: var(--bds-color_on-error);
89
+ }
90
+
91
+ .message {
92
+ flex: 1;
93
+ }
94
+
95
+ .close {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ flex-shrink: 0;
100
+ width: 1.25rem;
101
+ height: 1.25rem;
102
+ padding: 0;
103
+ background: none;
104
+ border: none;
105
+ cursor: pointer;
106
+ color: var(--toast_on-color);
107
+ }
108
+
109
+ .close svg {
110
+ width: 1rem;
111
+ height: 1rem;
112
+ }
113
+ `;
114
+
115
+ static properties = {
116
+ _toasts: { state: true },
117
+ };
118
+
119
+ declare private _toasts: ToastEntry[];
120
+
121
+ constructor() {
122
+ super();
123
+ this._toasts = [];
124
+ }
125
+
126
+ connectedCallback() {
127
+ super.connectedCallback();
128
+ document.addEventListener('bds-show-toast', this._onShowToastEvent);
129
+ }
130
+
131
+ disconnectedCallback() {
132
+ super.disconnectedCallback();
133
+ document.removeEventListener('bds-show-toast', this._onShowToastEvent);
134
+ }
135
+
136
+ // Sync the popover's top-layer visibility with the toast queue —
137
+ // mirrors Toast.tsx's useEffect([toasts.length]).
138
+ updated() {
139
+ const container = this.shadowRoot?.querySelector<HTMLElement & {
140
+ showPopover?: () => void;
141
+ hidePopover?: () => void;
142
+ }>('[popover]');
143
+ if (!container) return;
144
+ if (this._toasts.length > 0) {
145
+ container.showPopover?.();
146
+ } else {
147
+ try { container.hidePopover?.(); } catch { /* already hidden on initial mount */ }
148
+ }
149
+ }
150
+
151
+ showToast(message: string, variant: ToastVariant = 'info') {
152
+ const id = Math.random().toString(36).slice(2, 9);
153
+ this._toasts = [...this._toasts, { id, message, variant }];
154
+ setTimeout(() => this._removeToast(id), 5000);
155
+ }
156
+
157
+ private _removeToast(id: string) {
158
+ this._toasts = this._toasts.filter(t => t.id !== id);
159
+ }
160
+
161
+ private _onShowToastEvent = (e: Event) => {
162
+ const { message, variant } = (e as CustomEvent<{ message: string; variant?: ToastVariant }>).detail ?? {};
163
+ if (message) this.showToast(message, variant);
164
+ };
165
+
166
+ static show(message: string, variant?: ToastVariant) {
167
+ const provider = document.querySelector<BdsToastProvider>('bds-toast-provider');
168
+ provider?.showToast(message, variant ?? 'info');
169
+ }
170
+
171
+ render() {
172
+ return html`
173
+ <slot></slot>
174
+ <div
175
+ popover="manual"
176
+ role="region"
177
+ aria-label="Notifications"
178
+ class="container"
179
+ >
180
+ ${this._toasts.map(t => html`
181
+ <div
182
+ class="toast --variant_${t.variant}"
183
+ role="status"
184
+ aria-live="polite"
185
+ aria-atomic="true"
186
+ >
187
+ <span class="message">${t.message}</span>
188
+ <button
189
+ type="button"
190
+ class="close"
191
+ @click=${() => this._removeToast(t.id)}
192
+ aria-label="Dismiss"
193
+ >
194
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
195
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
196
+ </svg>
197
+ </button>
198
+ </div>
199
+ `)}
200
+ </div>
201
+ `;
202
+ }
203
+ }
204
+
205
+ customElements.define('bds-toast-provider', BdsToastProvider);
206
+
207
+ declare global {
208
+ interface HTMLElementTagNameMap {
209
+ 'bds-toast-provider': BdsToastProvider;
210
+ }
211
+ }
@@ -0,0 +1,14 @@
1
+ /** Render an HTML string, upgrade the custom element, and await first paint. */
2
+ export async function fixture(html: string): Promise<HTMLElement> {
3
+ const template = document.createElement('template');
4
+ template.innerHTML = html;
5
+ const el = template.content.firstElementChild as HTMLElement;
6
+ document.body.appendChild(el);
7
+ await (el as unknown as { updateComplete: Promise<void> }).updateComplete;
8
+ return el;
9
+ }
10
+
11
+ /** Remove an element from the document after a test. */
12
+ export function cleanup(el: HTMLElement) {
13
+ el.remove();
14
+ }
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import type { AlertVariant } from './bds-alert';
4
+ import '../index'; // auto-registers all custom elements
5
+
6
+ // Thin React wrapper so Storybook controls work with the custom element's props
7
+ function BdsAlert({
8
+ variant = 'info',
9
+ title,
10
+ dismissible,
11
+ children,
12
+ onDismiss,
13
+ }: {
14
+ variant?: AlertVariant;
15
+ title?: string;
16
+ dismissible?: boolean;
17
+ children?: React.ReactNode;
18
+ onDismiss?: () => void;
19
+ }) {
20
+ return React.createElement(
21
+ 'bds-alert',
22
+ { variant, title, dismissible: dismissible || undefined, onbdsdismiss: onDismiss },
23
+ children,
24
+ );
25
+ }
26
+
27
+ const meta = {
28
+ title: 'Web Components/UI/Alert',
29
+ component: BdsAlert,
30
+ tags: ['!stable', 'experimental'],
31
+ parameters: { layout: 'padded' },
32
+ argTypes: {
33
+ variant: { control: 'select', options: ['info', 'success', 'warning', 'error'] },
34
+ },
35
+ } satisfies Meta<typeof BdsAlert>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Info: Story = { args: { children: 'This is an informational message.', variant: 'info' } };
41
+ export const Success: Story = { args: { children: 'Your changes have been saved.', variant: 'success' } };
42
+ export const Warning: Story = { args: { children: 'Please review before continuing.', variant: 'warning' } };
43
+ export const Error: Story = { args: { children: 'Something went wrong.', variant: 'error' } };
44
+ export const WithTitle: Story = {
45
+ args: { children: 'More details about the alert.', variant: 'info', title: 'Heads up' },
46
+ };
47
+ export const Dismissible: Story = {
48
+ args: { children: 'You can dismiss this alert.', variant: 'success', dismissible: true },
49
+ };
50
+ export const AllVariants: Story = {
51
+ render: () => (
52
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: 500 }}>
53
+ {(['info', 'success', 'warning', 'error'] as const).map(v => (
54
+ <BdsAlert key={v} variant={v} title={v.charAt(0).toUpperCase() + v.slice(1)}>
55
+ This is a {v} alert.
56
+ </BdsAlert>
57
+ ))}
58
+ </div>
59
+ ),
60
+ };
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import type { BadgeVariant } from './bds-badge';
4
+ import '../index'; // auto-registers all custom elements
5
+
6
+ // Thin React wrapper so Storybook controls work with the custom element's props
7
+ function BdsBadge({ variant = 'primary', children }: { variant?: BadgeVariant; children?: React.ReactNode }) {
8
+ return React.createElement('bds-badge', { variant }, children);
9
+ }
10
+
11
+ const meta = {
12
+ title: 'Web Components/UI/Badge',
13
+ component: BdsBadge,
14
+ tags: ['!stable', 'experimental'],
15
+ parameters: { layout: 'centered' },
16
+ argTypes: {
17
+ variant: { control: 'select', options: ['primary', 'secondary', 'success', 'error', 'warning'] },
18
+ },
19
+ } satisfies Meta<typeof BdsBadge>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Primary: Story = { args: { children: 'Primary', variant: 'primary' } };
25
+ export const Secondary: Story = { args: { children: 'Secondary', variant: 'secondary' } };
26
+ export const Success: Story = { args: { children: 'Success', variant: 'success' } };
27
+ export const Error: Story = { args: { children: 'Error', variant: 'error' } };
28
+ export const Warning: Story = { args: { children: 'Warning', variant: 'warning' } };
29
+ export const AllVariants: Story = {
30
+ render: () => (
31
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
32
+ {(['primary', 'secondary', 'success', 'error', 'warning'] as const).map(v => (
33
+ <BdsBadge key={v} variant={v}>{v}</BdsBadge>
34
+ ))}
35
+ </div>
36
+ ),
37
+ };
@@ -0,0 +1,109 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsAlert } from './bds-alert';
3
+
4
+ describe('bds-alert', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-alert')).toBe(BdsAlert);
7
+ });
8
+
9
+ it('renders the alert container in shadow DOM', async () => {
10
+ const el = await fixture('<bds-alert>Message</bds-alert>');
11
+ const div = el.shadowRoot!.querySelector('.alert');
12
+ expect(div).not.toBeNull();
13
+ cleanup(el);
14
+ });
15
+
16
+ it('defaults to variant="info"', async () => {
17
+ const el = await fixture('<bds-alert>Message</bds-alert>');
18
+ expect(el.getAttribute('variant')).toBe('info');
19
+ cleanup(el);
20
+ });
21
+
22
+ it('sets role="status" and aria-live="polite" for info variant', async () => {
23
+ const el = await fixture('<bds-alert variant="info">Info</bds-alert>');
24
+ const div = el.shadowRoot!.querySelector('.alert');
25
+ expect(div!.getAttribute('role')).toBe('status');
26
+ expect(div!.getAttribute('aria-live')).toBe('polite');
27
+ cleanup(el);
28
+ });
29
+
30
+ it('sets role="status" and aria-live="polite" for success variant', async () => {
31
+ const el = await fixture('<bds-alert variant="success">Done</bds-alert>');
32
+ const div = el.shadowRoot!.querySelector('.alert');
33
+ expect(div!.getAttribute('role')).toBe('status');
34
+ expect(div!.getAttribute('aria-live')).toBe('polite');
35
+ cleanup(el);
36
+ });
37
+
38
+ it('sets role="alert" and aria-live="assertive" for error variant', async () => {
39
+ const el = await fixture('<bds-alert variant="error">Error</bds-alert>');
40
+ const div = el.shadowRoot!.querySelector('.alert');
41
+ expect(div!.getAttribute('role')).toBe('alert');
42
+ expect(div!.getAttribute('aria-live')).toBe('assertive');
43
+ cleanup(el);
44
+ });
45
+
46
+ it('sets role="alert" and aria-live="assertive" for warning variant', async () => {
47
+ const el = await fixture('<bds-alert variant="warning">Warning</bds-alert>');
48
+ const div = el.shadowRoot!.querySelector('.alert');
49
+ expect(div!.getAttribute('role')).toBe('alert');
50
+ expect(div!.getAttribute('aria-live')).toBe('assertive');
51
+ cleanup(el);
52
+ });
53
+
54
+ it('renders title when title attribute is set', async () => {
55
+ const el = await fixture('<bds-alert title="Heads up">Message</bds-alert>');
56
+ const strong = el.shadowRoot!.querySelector('strong.title');
57
+ expect(strong).not.toBeNull();
58
+ expect(strong!.textContent).toBe('Heads up');
59
+ cleanup(el);
60
+ });
61
+
62
+ it('does not render title element when title is not set', async () => {
63
+ const el = await fixture('<bds-alert>Message</bds-alert>');
64
+ const strong = el.shadowRoot!.querySelector('strong.title');
65
+ expect(strong).toBeNull();
66
+ cleanup(el);
67
+ });
68
+
69
+ it('does not render dismiss button by default', async () => {
70
+ const el = await fixture('<bds-alert>Message</bds-alert>');
71
+ const btn = el.shadowRoot!.querySelector('button.dismiss');
72
+ expect(btn).toBeNull();
73
+ cleanup(el);
74
+ });
75
+
76
+ it('renders dismiss button when dismissible is set', async () => {
77
+ const el = await fixture('<bds-alert dismissible>Message</bds-alert>');
78
+ const btn = el.shadowRoot!.querySelector('button.dismiss');
79
+ expect(btn).not.toBeNull();
80
+ expect(btn!.getAttribute('aria-label')).toBe('Dismiss alert');
81
+ cleanup(el);
82
+ });
83
+
84
+ it('fires bds-dismiss event when dismiss button is clicked', async () => {
85
+ const el = await fixture('<bds-alert dismissible>Message</bds-alert>');
86
+ const btn = el.shadowRoot!.querySelector('button.dismiss')!;
87
+ const handler = vi.fn();
88
+ el.addEventListener('bds-dismiss', handler);
89
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
90
+ expect(handler).toHaveBeenCalledOnce();
91
+ cleanup(el);
92
+ });
93
+
94
+ it('renders slotted body content in light DOM', async () => {
95
+ const el = await fixture('<bds-alert>Something went wrong</bds-alert>');
96
+ expect(el.textContent?.trim()).toBe('Something went wrong');
97
+ cleanup(el);
98
+ });
99
+
100
+ it('reflects variant property updates', async () => {
101
+ const el = await fixture('<bds-alert>Message</bds-alert>') as BdsAlert;
102
+ el.variant = 'warning';
103
+ await el.updateComplete;
104
+ expect(el.getAttribute('variant')).toBe('warning');
105
+ const div = el.shadowRoot!.querySelector('.alert');
106
+ expect(div!.getAttribute('role')).toBe('alert');
107
+ cleanup(el);
108
+ });
109
+ });