@boostdev/design-system-components 1.0.3 → 1.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 (262) hide show
  1. package/AGENTS.md +51 -10
  2. package/dist/client.cjs +350 -174
  3. package/dist/client.css +751 -647
  4. package/dist/client.d.cts +62 -97
  5. package/dist/client.d.ts +62 -97
  6. package/dist/client.js +377 -211
  7. package/dist/index.cjs +350 -174
  8. package/dist/index.css +751 -647
  9. package/dist/index.d.cts +62 -97
  10. package/dist/index.d.ts +62 -97
  11. package/dist/index.js +377 -211
  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 +304 -0
  17. package/dist/web-components.js +1978 -0
  18. package/package.json +27 -5
  19. package/src/components/interaction/Button/Button.module.css +11 -8
  20. package/src/components/interaction/Button/Button.native.mdx +72 -0
  21. package/src/components/interaction/Button/Button.native.spec.tsx +35 -0
  22. package/src/components/interaction/Button/Button.native.stories.tsx +42 -0
  23. package/src/components/interaction/Button/Button.native.tsx +95 -0
  24. package/src/components/interaction/Button/Button.stories.tsx +1 -1
  25. package/src/components/interaction/Button/Button.tsx +2 -2
  26. package/src/components/interaction/Command/Command.mdx +16 -0
  27. package/src/components/interaction/Command/Command.module.css +7 -7
  28. package/src/components/interaction/Command/Command.spec.tsx +37 -0
  29. package/src/components/interaction/Command/Command.stories.tsx +1 -1
  30. package/src/components/interaction/Command/Command.tsx +39 -4
  31. package/src/components/interaction/Dialog/Dialog.module.css +10 -13
  32. package/src/components/interaction/Dialog/Dialog.spec.tsx +53 -1
  33. package/src/components/interaction/Dialog/Dialog.stories.tsx +18 -1
  34. package/src/components/interaction/Dialog/Dialog.tsx +52 -18
  35. package/src/components/interaction/Drawer/Drawer.mdx +14 -0
  36. package/src/components/interaction/Drawer/Drawer.module.css +11 -11
  37. package/src/components/interaction/Drawer/Drawer.spec.tsx +48 -1
  38. package/src/components/interaction/Drawer/Drawer.stories.tsx +1 -1
  39. package/src/components/interaction/Drawer/Drawer.tsx +40 -6
  40. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -6
  41. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +1 -1
  42. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +2 -2
  43. package/src/components/interaction/Popover/Popover.mdx +17 -0
  44. package/src/components/interaction/Popover/Popover.module.css +132 -16
  45. package/src/components/interaction/Popover/Popover.spec.tsx +119 -20
  46. package/src/components/interaction/Popover/Popover.stories.tsx +1 -1
  47. package/src/components/interaction/Popover/Popover.tsx +69 -29
  48. package/src/components/interaction/Rating/Rating.stories.tsx +1 -1
  49. package/src/components/interaction/Rating/Rating.tsx +2 -2
  50. package/src/components/interaction/Toast/Toast.module.css +13 -8
  51. package/src/components/interaction/Toast/Toast.spec.tsx +21 -0
  52. package/src/components/interaction/Toast/Toast.stories.tsx +1 -1
  53. package/src/components/interaction/Toast/Toast.tsx +22 -2
  54. package/src/components/interaction/form/Checkbox/Checkbox.mdx +2 -2
  55. package/src/components/interaction/form/Checkbox/Checkbox.module.css +9 -9
  56. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +74 -0
  57. package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +42 -0
  58. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +36 -0
  59. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +80 -0
  60. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +1 -1
  61. package/src/components/interaction/form/Checkbox/Checkbox.tsx +2 -2
  62. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +44 -0
  63. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +1 -1
  64. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +2 -2
  65. package/src/components/interaction/form/Combobox/Combobox.mdx +21 -0
  66. package/src/components/interaction/form/Combobox/Combobox.module.css +8 -9
  67. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +1 -1
  68. package/src/components/interaction/form/Combobox/Combobox.tsx +2 -2
  69. package/src/components/interaction/form/FileInput/FileInput.mdx +14 -0
  70. package/src/components/interaction/form/FileInput/FileInput.module.css +5 -5
  71. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +1 -1
  72. package/src/components/interaction/form/FileInput/FileInput.tsx +2 -2
  73. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +1 -1
  74. package/src/components/interaction/form/FormInput/FormInput.tsx +2 -2
  75. package/src/components/interaction/form/NumberInput/NumberInput.mdx +16 -0
  76. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  77. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +1 -1
  78. package/src/components/interaction/form/NumberInput/NumberInput.tsx +2 -2
  79. package/src/components/interaction/form/Radio/Radio.mdx +3 -3
  80. package/src/components/interaction/form/Radio/Radio.module.css +9 -9
  81. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +46 -0
  82. package/src/components/interaction/form/Radio/Radio.native.tsx +79 -0
  83. package/src/components/interaction/form/Radio/Radio.stories.tsx +1 -1
  84. package/src/components/interaction/form/Radio/Radio.tsx +2 -2
  85. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +45 -0
  86. package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +1 -1
  87. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +2 -2
  88. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -6
  89. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +1 -1
  90. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +2 -2
  91. package/src/components/interaction/form/Select/Select.module.css +3 -3
  92. package/src/components/interaction/form/Select/Select.stories.tsx +1 -1
  93. package/src/components/interaction/form/Select/Select.tsx +2 -2
  94. package/src/components/interaction/form/Slider/Slider.mdx +1 -1
  95. package/src/components/interaction/form/Slider/Slider.module.css +10 -10
  96. package/src/components/interaction/form/Slider/Slider.stories.tsx +1 -1
  97. package/src/components/interaction/form/Slider/Slider.tsx +2 -2
  98. package/src/components/interaction/form/Switch/Switch.mdx +4 -4
  99. package/src/components/interaction/form/Switch/Switch.module.css +11 -11
  100. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +60 -0
  101. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +35 -0
  102. package/src/components/interaction/form/Switch/Switch.native.tsx +59 -0
  103. package/src/components/interaction/form/Switch/Switch.stories.tsx +1 -1
  104. package/src/components/interaction/form/Switch/Switch.tsx +2 -2
  105. package/src/components/interaction/form/Textarea/Textarea.mdx +17 -0
  106. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -1
  107. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +1 -1
  108. package/src/components/interaction/form/Textarea/Textarea.tsx +2 -2
  109. package/src/components/interaction/form/atoms/InputContainer.tsx +2 -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.native.stories.tsx +53 -0
  122. package/src/components/layout/Card/Card.native.tsx +89 -0
  123. package/src/components/layout/Card/Card.stories.tsx +1 -1
  124. package/src/components/layout/Card/Card.tsx +2 -2
  125. package/src/components/layout/IconWrapper/IconWrapper.mdx +2 -2
  126. package/src/components/layout/IconWrapper/IconWrapper.module.css +2 -2
  127. package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +39 -0
  128. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +41 -0
  129. package/src/components/layout/IconWrapper/IconWrapper.native.tsx +20 -0
  130. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +1 -1
  131. package/src/components/layout/IconWrapper/IconWrapper.tsx +2 -2
  132. package/src/components/layout/SectionHeader/SectionHeader.mdx +13 -0
  133. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +38 -0
  134. package/src/components/layout/SectionHeader/SectionHeader.native.tsx +79 -0
  135. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +1 -1
  136. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -2
  137. package/src/components/ui/Accordion/Accordion.mdx +14 -0
  138. package/src/components/ui/Accordion/Accordion.module.css +5 -5
  139. package/src/components/ui/Accordion/Accordion.stories.tsx +1 -1
  140. package/src/components/ui/Accordion/Accordion.tsx +2 -2
  141. package/src/components/ui/Alert/Alert.native.mdx +62 -0
  142. package/src/components/ui/Alert/Alert.native.stories.tsx +43 -0
  143. package/src/components/ui/Alert/Alert.native.tsx +94 -0
  144. package/src/components/ui/Alert/Alert.stories.tsx +1 -1
  145. package/src/components/ui/Alert/Alert.tsx +2 -2
  146. package/src/components/ui/Avatar/Avatar.native.mdx +50 -0
  147. package/src/components/ui/Avatar/Avatar.native.spec.tsx +47 -0
  148. package/src/components/ui/Avatar/Avatar.native.stories.tsx +34 -0
  149. package/src/components/ui/Avatar/Avatar.native.tsx +85 -0
  150. package/src/components/ui/Avatar/Avatar.stories.tsx +1 -1
  151. package/src/components/ui/Avatar/Avatar.tsx +2 -2
  152. package/src/components/ui/Badge/Badge.mdx +2 -2
  153. package/src/components/ui/Badge/Badge.native.mdx +54 -0
  154. package/src/components/ui/Badge/Badge.native.stories.tsx +36 -0
  155. package/src/components/ui/Badge/Badge.native.tsx +50 -0
  156. package/src/components/ui/Badge/Badge.stories.tsx +1 -1
  157. package/src/components/ui/Badge/Badge.tsx +2 -2
  158. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +1 -1
  159. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -2
  160. package/src/components/ui/Calendar/Calendar.mdx +16 -0
  161. package/src/components/ui/Calendar/Calendar.module.css +7 -7
  162. package/src/components/ui/Calendar/Calendar.stories.tsx +1 -1
  163. package/src/components/ui/Calendar/Calendar.tsx +2 -2
  164. package/src/components/ui/Carousel/Carousel.module.css +5 -5
  165. package/src/components/ui/Carousel/Carousel.stories.tsx +1 -1
  166. package/src/components/ui/Carousel/Carousel.tsx +2 -2
  167. package/src/components/ui/Collapsible/Collapsible.module.css +4 -4
  168. package/src/components/ui/Collapsible/Collapsible.stories.tsx +1 -1
  169. package/src/components/ui/Collapsible/Collapsible.tsx +2 -2
  170. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +1 -1
  171. package/src/components/ui/DescriptionList/DescriptionList.tsx +2 -2
  172. package/src/components/ui/Link/Link.mdx +14 -0
  173. package/src/components/ui/Link/Link.module.css +2 -2
  174. package/src/components/ui/Link/Link.stories.tsx +1 -1
  175. package/src/components/ui/Link/Link.tsx +2 -2
  176. package/src/components/ui/Loading/Loading.module.css +7 -7
  177. package/src/components/ui/Loading/Loading.native.spec.tsx +24 -0
  178. package/src/components/ui/Loading/Loading.native.stories.tsx +33 -0
  179. package/src/components/ui/Loading/Loading.native.tsx +29 -0
  180. package/src/components/ui/Loading/Loading.stories.tsx +1 -1
  181. package/src/components/ui/Loading/Loading.tsx +2 -2
  182. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +3 -3
  183. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +39 -0
  184. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +76 -0
  185. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +1 -1
  186. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +2 -2
  187. package/src/components/ui/Pagination/Pagination.module.css +7 -7
  188. package/src/components/ui/Pagination/Pagination.stories.tsx +1 -1
  189. package/src/components/ui/Pagination/Pagination.tsx +2 -2
  190. package/src/components/ui/Progress/Progress.mdx +1 -1
  191. package/src/components/ui/Progress/Progress.module.css +6 -6
  192. package/src/components/ui/Progress/Progress.native.stories.tsx +34 -0
  193. package/src/components/ui/Progress/Progress.native.tsx +84 -0
  194. package/src/components/ui/Progress/Progress.stories.tsx +1 -1
  195. package/src/components/ui/Progress/Progress.tsx +2 -2
  196. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +1 -1
  197. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +1 -1
  198. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +2 -2
  199. package/src/components/ui/Separator/Separator.mdx +14 -0
  200. package/src/components/ui/Separator/Separator.module.css +3 -3
  201. package/src/components/ui/Separator/Separator.native.stories.tsx +42 -0
  202. package/src/components/ui/Separator/Separator.native.tsx +32 -0
  203. package/src/components/ui/Separator/Separator.stories.tsx +1 -1
  204. package/src/components/ui/Separator/Separator.tsx +2 -2
  205. package/src/components/ui/Skeleton/Skeleton.module.css +1 -1
  206. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +33 -0
  207. package/src/components/ui/Skeleton/Skeleton.native.tsx +41 -0
  208. package/src/components/ui/Skeleton/Skeleton.stories.tsx +1 -1
  209. package/src/components/ui/Skeleton/Skeleton.tsx +2 -3
  210. package/src/components/ui/SkipLink/SkipLink.stories.tsx +1 -1
  211. package/src/components/ui/SkipLink/SkipLink.tsx +5 -3
  212. package/src/components/ui/Table/Table.mdx +14 -0
  213. package/src/components/ui/Table/Table.module.css +9 -9
  214. package/src/components/ui/Table/Table.stories.tsx +1 -1
  215. package/src/components/ui/Table/Table.tsx +2 -2
  216. package/src/components/ui/Tabs/Tabs.module.css +3 -3
  217. package/src/components/ui/Tabs/Tabs.stories.tsx +1 -1
  218. package/src/components/ui/Tabs/Tabs.tsx +2 -2
  219. package/src/components/ui/Tooltip/Tooltip.mdx +14 -0
  220. package/src/components/ui/Tooltip/Tooltip.module.css +8 -8
  221. package/src/components/ui/Tooltip/Tooltip.stories.tsx +1 -1
  222. package/src/components/ui/Tooltip/Tooltip.tsx +2 -2
  223. package/src/components/ui/Typography/Typography.mdx +13 -0
  224. package/src/components/ui/Typography/Typography.native.mdx +56 -0
  225. package/src/components/ui/Typography/Typography.native.stories.tsx +38 -0
  226. package/src/components/ui/Typography/Typography.native.tsx +65 -0
  227. package/src/components/ui/Typography/Typography.stories.tsx +1 -1
  228. package/src/components/ui/Typography/Typography.tsx +2 -2
  229. package/src/css/bdc.css +8 -0
  230. package/src/index.ts +1 -0
  231. package/src/native/ThemeContext.tsx +28 -0
  232. package/src/native/tokens.ts +13 -0
  233. package/src/native.ts +39 -0
  234. package/src/polyfill-invoker-commands.ts +68 -0
  235. package/src/react-augment.d.ts +32 -0
  236. package/src/stories/DesignSystem/DarkMode.mdx +130 -0
  237. package/src/stories/ReactNative.mdx +121 -0
  238. package/src/types.ts +2 -0
  239. package/src/typings.d.ts +3 -0
  240. package/src/web-components/globals.ts +61 -0
  241. package/src/web-components/index.ts +12 -0
  242. package/src/web-components/interaction/BdsButton.mdx +106 -0
  243. package/src/web-components/interaction/BdsButton.stories.tsx +60 -0
  244. package/src/web-components/interaction/BdsPopover.mdx +120 -0
  245. package/src/web-components/interaction/BdsPopover.stories.tsx +70 -0
  246. package/src/web-components/interaction/BdsToastProvider.mdx +94 -0
  247. package/src/web-components/interaction/BdsToastProvider.stories.tsx +73 -0
  248. package/src/web-components/interaction/bds-button.spec.ts +95 -0
  249. package/src/web-components/interaction/bds-button.ts +293 -0
  250. package/src/web-components/interaction/bds-popover.spec.ts +174 -0
  251. package/src/web-components/interaction/bds-popover.ts +229 -0
  252. package/src/web-components/interaction/bds-toast-provider.spec.ts +122 -0
  253. package/src/web-components/interaction/bds-toast-provider.ts +211 -0
  254. package/src/web-components/test/helpers.ts +22 -0
  255. package/src/web-components/ui/BdsAlert.mdx +90 -0
  256. package/src/web-components/ui/BdsAlert.stories.tsx +60 -0
  257. package/src/web-components/ui/BdsBadge.mdx +74 -0
  258. package/src/web-components/ui/BdsBadge.stories.tsx +37 -0
  259. package/src/web-components/ui/bds-alert.spec.ts +109 -0
  260. package/src/web-components/ui/bds-alert.ts +209 -0
  261. package/src/web-components/ui/bds-badge.spec.ts +51 -0
  262. package/src/web-components/ui/bds-badge.ts +88 -0
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import type { ToastVariant } from './bds-toast-provider';
4
+ import { BdsToastProvider } from './bds-toast-provider';
5
+ import '../index'; // auto-registers all custom elements
6
+
7
+ // Render the bds-toast-provider as a wrapper around demo content.
8
+ // Trigger buttons call BdsToastProvider.show() — the static convenience API.
9
+ function ToastDemo() {
10
+ const variants: { label: string; variant: ToastVariant }[] = [
11
+ { label: 'Info', variant: 'info' },
12
+ { label: 'Success', variant: 'success' },
13
+ { label: 'Warning', variant: 'warning' },
14
+ { label: 'Error', variant: 'error' },
15
+ ];
16
+
17
+ return React.createElement(
18
+ 'bds-toast-provider',
19
+ null,
20
+ React.createElement(
21
+ 'div',
22
+ { style: { display: 'flex', gap: '0.75rem', flexWrap: 'wrap' } },
23
+ ...variants.map(({ label, variant }) =>
24
+ React.createElement(
25
+ 'button',
26
+ {
27
+ key: variant,
28
+ onClick: () => BdsToastProvider.show(`This is a ${variant} toast`, variant),
29
+ },
30
+ `Show ${label}`,
31
+ ),
32
+ ),
33
+ ),
34
+ );
35
+ }
36
+
37
+ const meta = {
38
+ title: 'Web Components/Interaction/ToastProvider',
39
+ component: ToastDemo,
40
+ tags: ['!stable', 'experimental'],
41
+ parameters: { layout: 'centered' },
42
+ } satisfies Meta<typeof ToastDemo>;
43
+
44
+ export default meta;
45
+ type Story = StoryObj<typeof meta>;
46
+
47
+ export const Default: Story = {};
48
+
49
+ export const EventAPI: Story = {
50
+ render: () =>
51
+ React.createElement(
52
+ 'bds-toast-provider',
53
+ null,
54
+ React.createElement(
55
+ 'div',
56
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.5rem' } },
57
+ React.createElement('p', { style: { margin: 0, fontSize: '0.875rem', color: '#666' } },
58
+ 'Uses the document event API — no import required.'),
59
+ React.createElement(
60
+ 'button',
61
+ {
62
+ onClick: () =>
63
+ document.dispatchEvent(
64
+ new CustomEvent('bds-show-toast', {
65
+ detail: { message: 'Fired via bds-show-toast event', variant: 'info' },
66
+ }),
67
+ ),
68
+ },
69
+ 'Dispatch bds-show-toast',
70
+ ),
71
+ ),
72
+ ),
73
+ };
@@ -0,0 +1,95 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsButton } from './bds-button';
3
+
4
+ describe('bds-button', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-button')).toBe(BdsButton);
7
+ });
8
+
9
+ it('renders a <button> by default', async () => {
10
+ const el = await fixture('<bds-button>Click</bds-button>');
11
+ const btn = el.shadowRoot!.querySelector('button');
12
+ expect(btn).not.toBeNull();
13
+ cleanup(el);
14
+ });
15
+
16
+ it('renders an <a> when href is set', async () => {
17
+ const el = await fixture('<bds-button href="/home">Home</bds-button>');
18
+ const a = el.shadowRoot!.querySelector('a');
19
+ expect(a).not.toBeNull();
20
+ expect(a!.getAttribute('href')).toBe('/home');
21
+ cleanup(el);
22
+ });
23
+
24
+ it('defaults to variant="default" and size="medium"', async () => {
25
+ const el = await fixture('<bds-button>Click</bds-button>');
26
+ expect(el.getAttribute('variant')).toBe('default');
27
+ expect(el.getAttribute('size')).toBe('medium');
28
+ cleanup(el);
29
+ });
30
+
31
+ it('reflects variant attribute', async () => {
32
+ const el = await fixture('<bds-button variant="ghost">Click</bds-button>');
33
+ expect(el.getAttribute('variant')).toBe('ghost');
34
+ const btn = el.shadowRoot!.querySelector('button');
35
+ expect(btn!.className).toContain('ghost');
36
+ cleanup(el);
37
+ });
38
+
39
+ it('reflects size attribute', async () => {
40
+ const el = await fixture('<bds-button size="small">Click</bds-button>');
41
+ const btn = el.shadowRoot!.querySelector('button');
42
+ expect(btn!.className).toContain('small');
43
+ cleanup(el);
44
+ });
45
+
46
+ it('sets disabled on the inner button', async () => {
47
+ const el = await fixture('<bds-button disabled>Click</bds-button>');
48
+ const btn = el.shadowRoot!.querySelector('button');
49
+ expect(btn!.disabled).toBe(true);
50
+ cleanup(el);
51
+ });
52
+
53
+ it('sets aria-disabled and removes href when disabled + href', async () => {
54
+ const el = await fixture('<bds-button href="/home" disabled>Home</bds-button>');
55
+ const a = el.shadowRoot!.querySelector('a');
56
+ expect(a!.getAttribute('aria-disabled')).toBe('true');
57
+ expect(a!.hasAttribute('href')).toBe(false);
58
+ cleanup(el);
59
+ });
60
+
61
+ it('applies has-pulse class when has-pulse is set', async () => {
62
+ const el = await fixture('<bds-button has-pulse>Click</bds-button>');
63
+ const btn = el.shadowRoot!.querySelector('button');
64
+ expect(btn!.className).toContain('has-pulse');
65
+ cleanup(el);
66
+ });
67
+
68
+ it('renders default slot content in light DOM', async () => {
69
+ const el = await fixture('<bds-button>Save</bds-button>');
70
+ expect(el.textContent?.trim()).toBe('Save');
71
+ cleanup(el);
72
+ });
73
+
74
+ it('renders icon-start slot when content is provided', async () => {
75
+ const el = await fixture(`
76
+ <bds-button>
77
+ <svg slot="icon-start" data-testid="icon"></svg>
78
+ Label
79
+ </bds-button>
80
+ `);
81
+ await (el as unknown as { updateComplete: Promise<void> }).updateComplete;
82
+ // Icon span should be present in shadow DOM
83
+ const iconStart = el.shadowRoot!.querySelector('.icon-start');
84
+ expect(iconStart).not.toBeNull();
85
+ cleanup(el);
86
+ });
87
+
88
+ it('forwards target and rel to anchor', async () => {
89
+ const el = await fixture('<bds-button href="/ext" target="_blank" rel="noopener">Ext</bds-button>');
90
+ const a = el.shadowRoot!.querySelector('a');
91
+ expect(a!.getAttribute('target')).toBe('_blank');
92
+ expect(a!.getAttribute('rel')).toBe('noopener');
93
+ cleanup(el);
94
+ });
95
+ });
@@ -0,0 +1,293 @@
1
+ import { LitElement, css, html, nothing } from 'lit';
2
+
3
+ export type ButtonVariant = 'default' | 'ghost';
4
+ export type ButtonSize = 'small' | 'medium' | 'large';
5
+
6
+ /**
7
+ * `<bds-button>` — framework-agnostic Button custom element.
8
+ *
9
+ * Attributes:
10
+ * variant — "default" (filled, default) | "ghost" (transparent + outlined)
11
+ * size — "small" | "medium" (default) | "large"
12
+ * disabled — boolean
13
+ * href — renders an <a> tag instead of <button>
14
+ * target — forwarded to <a> when href is set
15
+ * rel — forwarded to <a> when href is set
16
+ * has-pulse — boolean, enables the pulse ring animation
17
+ * type — "button" (default) | "submit" | "reset"
18
+ *
19
+ * Slots:
20
+ * (default) — button label
21
+ * icon-start — icon placed before the label
22
+ * icon-end — icon placed after the label
23
+ *
24
+ * Events:
25
+ * click — standard DOM click (disabled buttons suppress it and set aria-disabled)
26
+ *
27
+ * @example
28
+ * <bds-button variant="ghost" size="small">
29
+ * <svg slot="icon-start">…</svg>
30
+ * Save
31
+ * </bds-button>
32
+ */
33
+ export class BdsButton extends LitElement {
34
+ static styles = css`
35
+ @keyframes fadeZoom {
36
+ 0%, 100% { opacity: 1; transform: scale(1); }
37
+ 50% { opacity: 0.75; transform: scale(1.15); }
38
+ }
39
+
40
+ @keyframes pulse {
41
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 rgb(from var(--button_pulse-color, var(--button_color, var(--bds-color_green))) r g b / 50%); }
42
+ 70% { transform: scale(1.01); box-shadow: 0 0 0 var(--bds-space_m) rgb(from var(--button_pulse-color, var(--button_color, var(--bds-color_green))) r g b / 0%); }
43
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgb(from var(--button_pulse-color, var(--button_color, var(--bds-color_green))) r g b / 0%); }
44
+ }
45
+
46
+ :host {
47
+ display: inline-flex;
48
+ }
49
+
50
+ .button,
51
+ .button[href] {
52
+ all: unset;
53
+ position: relative;
54
+ font: inherit;
55
+ padding-inline: var(--bds-space_l);
56
+ display: inline-flex;
57
+ gap: var(--bds-space_xs);
58
+ border-radius: var(--button_border_radius--s, var(--bds-border_radius--s));
59
+ align-items: center;
60
+ cursor: pointer;
61
+ justify-content: center;
62
+ height: var(--button_height, 3em);
63
+ font-size: var(--button_font-size, inherit);
64
+ white-space: nowrap;
65
+ line-height: 1;
66
+
67
+ --button_color: var(--bds-color_green);
68
+ --button_on-color: var(--bds-color_on-green);
69
+
70
+ background-color: var(--button_bg, var(--button_color));
71
+ color: var(--button_text, var(--button_on-color));
72
+ border: var(--button_border, none);
73
+ --bdc_color: transparent;
74
+
75
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
76
+ outline-offset: var(--bdc-outline_offset);
77
+ transition: var(--bds-animation_transition),
78
+ --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing);
79
+ }
80
+
81
+ .button[href] {
82
+ text-decoration: none;
83
+ }
84
+
85
+ /* Variants */
86
+ .button.ghost {
87
+ --button_bg: transparent;
88
+ --button_text: var(--button_color);
89
+ --bdc_color: var(--button_color);
90
+ }
91
+
92
+ /* Sizes */
93
+ .button.small {
94
+ --button_height: 2.25em;
95
+ --button_font-size: var(--bds-font_size--body--s);
96
+ --button_border_radius--s: var(--bds-border_radius--xs);
97
+ padding-inline: var(--bds-space_m);
98
+ }
99
+
100
+ .button.medium {
101
+ --button_height: 3em;
102
+ padding-inline: var(--bds-space_l);
103
+ }
104
+
105
+ .button.large {
106
+ --button_font-size: var(--bds-font_size--heading-3);
107
+ --button_border_radius--s: var(--bds-border_radius--m);
108
+ padding-inline: var(--bds-space_xl);
109
+ }
110
+
111
+ /* Pulse */
112
+ .button.has-pulse {
113
+ animation: pulse 3s infinite;
114
+ }
115
+
116
+ /* SVG icon colour */
117
+ .button svg {
118
+ --icon__stroke: currentcolor;
119
+ fill: currentcolor;
120
+ color: currentcolor;
121
+ flex-shrink: 0;
122
+ }
123
+
124
+ /* Icon slots — hidden by default, shown when slot has content */
125
+ .icon-start,
126
+ .icon-end {
127
+ display: none;
128
+ align-items: center;
129
+ }
130
+
131
+ .icon-start.has-content {
132
+ display: flex;
133
+ margin-inline-start: calc(var(--bds-space_s) * -1);
134
+ transition: var(--bds-animation_transition);
135
+ }
136
+
137
+ .icon-end.has-content {
138
+ display: flex;
139
+ margin-inline-end: calc(var(--bds-space_s) * -1);
140
+ transition: var(--bds-animation_transition);
141
+ }
142
+
143
+ /* Hover icon animation */
144
+ @media (hover: hover) and (pointer: fine) {
145
+ .button:hover .icon-start svg,
146
+ .button:hover .icon-end svg {
147
+ animation: fadeZoom 2s ease-out infinite;
148
+ }
149
+
150
+ /* Default hover */
151
+ .button.default:hover {
152
+ --button_bg: var(--bds-color_bg);
153
+ --button_text: var(--bds-color_interactive);
154
+ --button_pulse-color: var(--bds-color_interactive);
155
+ --bdc_color: currentcolor;
156
+ }
157
+
158
+ /* Ghost hover */
159
+ .button.ghost:hover {
160
+ --button_bg: var(--button_color);
161
+ --button_text: var(--button_on-color);
162
+ --bdc_color: transparent;
163
+ }
164
+ }
165
+
166
+ /* Focus visible */
167
+ .button:focus-visible {
168
+ outline: var(--bds-outline_default);
169
+ outline-offset: var(--bds-outline_offset);
170
+ border-radius: var(--bds-border_radius--s);
171
+ }
172
+
173
+ /* Disabled */
174
+ .button:disabled,
175
+ .button[aria-disabled='true'] {
176
+ opacity: 0.5;
177
+ cursor: not-allowed;
178
+ pointer-events: none;
179
+ }
180
+ `;
181
+
182
+ static properties = {
183
+ variant: { type: String, reflect: true },
184
+ size: { type: String, reflect: true },
185
+ disabled: { type: Boolean, reflect: true },
186
+ href: { type: String },
187
+ target: { type: String },
188
+ rel: { type: String },
189
+ hasPulse: { type: Boolean, attribute: 'has-pulse', reflect: true },
190
+ type: { type: String },
191
+ ariaLabel: { type: String, attribute: 'aria-label' },
192
+ // Internal state
193
+ _iconStartFilled: { state: true },
194
+ _iconEndFilled: { state: true },
195
+ };
196
+
197
+ // `declare` annotates the type without emitting a class field initializer
198
+ // that would shadow Lit's reactive accessors on the prototype.
199
+ declare variant: ButtonVariant;
200
+ declare size: ButtonSize;
201
+ declare disabled: boolean;
202
+ declare href: string | undefined;
203
+ declare target: string | undefined;
204
+ declare rel: string | undefined;
205
+ declare hasPulse: boolean;
206
+ declare type: 'button' | 'submit' | 'reset';
207
+ declare ariaLabel: string | null;
208
+ declare private _iconStartFilled: boolean;
209
+ declare private _iconEndFilled: boolean;
210
+
211
+ constructor() {
212
+ super();
213
+ this.variant = 'default';
214
+ this.size = 'medium';
215
+ this.disabled = false;
216
+ this.hasPulse = false;
217
+ this.type = 'button';
218
+ this.ariaLabel = null;
219
+ this._iconStartFilled = false;
220
+ this._iconEndFilled = false;
221
+ }
222
+
223
+ private _onIconSlotChange(e: Event, position: 'start' | 'end') {
224
+ const slot = e.target as HTMLSlotElement;
225
+ const filled = slot.assignedNodes({ flatten: true }).length > 0;
226
+ if (position === 'start') {
227
+ this._iconStartFilled = filled;
228
+ } else {
229
+ this._iconEndFilled = filled;
230
+ }
231
+ }
232
+
233
+ private get _classes() {
234
+ return `button ${this.variant} ${this.size}${this.hasPulse ? ' has-pulse' : ''}`;
235
+ }
236
+
237
+ private _handleAnchorClick(e: MouseEvent) {
238
+ if (this.disabled) {
239
+ e.preventDefault();
240
+ }
241
+ }
242
+
243
+ private _iconSlot(position: 'start' | 'end') {
244
+ const filled = position === 'start' ? this._iconStartFilled : this._iconEndFilled;
245
+ return html`
246
+ <span class="icon-${position} ${filled ? 'has-content' : ''}">
247
+ <slot name="icon-${position}" @slotchange=${(e: Event) => this._onIconSlotChange(e, position)}></slot>
248
+ </span>
249
+ `;
250
+ }
251
+
252
+ render() {
253
+ if (this.href) {
254
+ return html`
255
+ <a
256
+ class=${this._classes}
257
+ href=${this.disabled ? nothing : this.href}
258
+ target=${this.target ?? nothing}
259
+ rel=${this.rel ?? nothing}
260
+ aria-disabled=${this.disabled ? 'true' : nothing}
261
+ tabindex=${this.disabled ? '-1' : nothing}
262
+ aria-label=${this.ariaLabel ?? nothing}
263
+ @click=${this._handleAnchorClick}
264
+ >
265
+ ${this._iconSlot('start')}
266
+ <slot></slot>
267
+ ${this._iconSlot('end')}
268
+ </a>
269
+ `;
270
+ }
271
+
272
+ return html`
273
+ <button
274
+ class=${this._classes}
275
+ type=${this.type}
276
+ ?disabled=${this.disabled}
277
+ aria-label=${this.ariaLabel ?? nothing}
278
+ >
279
+ ${this._iconSlot('start')}
280
+ <slot></slot>
281
+ ${this._iconSlot('end')}
282
+ </button>
283
+ `;
284
+ }
285
+ }
286
+
287
+ customElements.define('bds-button', BdsButton);
288
+
289
+ declare global {
290
+ interface HTMLElementTagNameMap {
291
+ 'bds-button': BdsButton;
292
+ }
293
+ }
@@ -0,0 +1,174 @@
1
+ import { fixture, cleanup, makeCommandEvent } from '../test/helpers';
2
+ import { BdsPopover } from './bds-popover';
3
+
4
+ describe('bds-popover', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-popover')).toBe(BdsPopover);
7
+ });
8
+
9
+ it('defaults to placement="bottom" and open=false', async () => {
10
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button><p>Content</p></bds-popover>');
11
+ expect(el.getAttribute('placement')).toBe('bottom');
12
+ expect(el.hasAttribute('open')).toBe(false);
13
+ cleanup(el);
14
+ });
15
+
16
+ it('panel is hidden by default (no .open class)', async () => {
17
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button><p>Content</p></bds-popover>');
18
+ const panel = el.shadowRoot!.querySelector('.panel');
19
+ expect(panel!.classList.contains('open')).toBe(false);
20
+ cleanup(el);
21
+ });
22
+
23
+ it('sets data-placement on the panel', async () => {
24
+ const el = await fixture('<bds-popover placement="right"><button slot="trigger">Open</button></bds-popover>');
25
+ const panel = el.shadowRoot!.querySelector('.panel');
26
+ expect(panel!.getAttribute('data-placement')).toBe('right');
27
+ cleanup(el);
28
+ });
29
+
30
+ it('opens when trigger slot is clicked', async () => {
31
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
32
+ const trigger = el.shadowRoot!.querySelector('slot[name="trigger"]')!;
33
+ trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
34
+ await el.updateComplete;
35
+ expect(el.open).toBe(true);
36
+ expect(el.hasAttribute('open')).toBe(true);
37
+ const panel = el.shadowRoot!.querySelector('.panel');
38
+ expect(panel!.classList.contains('open')).toBe(true);
39
+ cleanup(el);
40
+ });
41
+
42
+ it('closes when trigger is clicked again', async () => {
43
+ const el = await fixture('<bds-popover open><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
44
+ await el.updateComplete;
45
+ const trigger = el.shadowRoot!.querySelector('slot[name="trigger"]')!;
46
+ trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
47
+ await el.updateComplete;
48
+ expect(el.open).toBe(false);
49
+ cleanup(el);
50
+ });
51
+
52
+ it('closes on Escape key', async () => {
53
+ const el = await fixture('<bds-popover open><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
54
+ await el.updateComplete;
55
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
56
+ await el.updateComplete;
57
+ expect(el.open).toBe(false);
58
+ cleanup(el);
59
+ });
60
+
61
+ it('closes on outside mousedown', async () => {
62
+ const el = await fixture('<bds-popover open><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
63
+ await el.updateComplete;
64
+ document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
65
+ await el.updateComplete;
66
+ expect(el.open).toBe(false);
67
+ cleanup(el);
68
+ });
69
+
70
+ it('does not close on inside mousedown', async () => {
71
+ const el = await fixture('<bds-popover open><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
72
+ await el.updateComplete;
73
+ el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
74
+ await el.updateComplete;
75
+ expect(el.open).toBe(true);
76
+ cleanup(el);
77
+ });
78
+
79
+ it('fires bds-open event when opened', async () => {
80
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
81
+ const handler = vi.fn();
82
+ el.addEventListener('bds-open', handler);
83
+ el.shadowRoot!.querySelector('slot[name="trigger"]')!
84
+ .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
85
+ await el.updateComplete;
86
+ expect(handler).toHaveBeenCalledOnce();
87
+ cleanup(el);
88
+ });
89
+
90
+ it('fires bds-close event when closed', async () => {
91
+ const el = await fixture('<bds-popover open><button slot="trigger">Open</button></bds-popover>') as BdsPopover;
92
+ await el.updateComplete;
93
+ const handler = vi.fn();
94
+ el.addEventListener('bds-close', handler);
95
+ el.shadowRoot!.querySelector('slot[name="trigger"]')!
96
+ .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
97
+ await el.updateComplete;
98
+ expect(handler).toHaveBeenCalledOnce();
99
+ cleanup(el);
100
+ });
101
+
102
+ it('injects a unique anchor-name CSS custom property on :host', async () => {
103
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button></bds-popover>');
104
+ const anchorValue = el.style.getPropertyValue('--bds-popover-anchor');
105
+ expect(anchorValue).toMatch(/^--bds-popover-[a-z0-9]+$/);
106
+ cleanup(el);
107
+ });
108
+
109
+ it('panel has role="region" and aria-label="Popover"', async () => {
110
+ const el = await fixture('<bds-popover><button slot="trigger">Open</button></bds-popover>');
111
+ const panel = el.shadowRoot!.querySelector('.panel');
112
+ expect(panel!.getAttribute('role')).toBe('region');
113
+ expect(panel!.getAttribute('aria-label')).toBe('Popover');
114
+ cleanup(el);
115
+ });
116
+
117
+ describe('Invoker Commands API (command event on host)', () => {
118
+ it('opens on --open command', async () => {
119
+ const el = await fixture('<bds-popover id="p1"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
120
+ el.dispatchEvent(makeCommandEvent('--open'));
121
+ await el.updateComplete;
122
+ expect(el.open).toBe(true);
123
+ cleanup(el);
124
+ });
125
+
126
+ it('closes on --close command', async () => {
127
+ const el = await fixture('<bds-popover id="p2" open><button slot="trigger">T</button></bds-popover>') as BdsPopover;
128
+ await el.updateComplete;
129
+ el.dispatchEvent(makeCommandEvent('--close'));
130
+ await el.updateComplete;
131
+ expect(el.open).toBe(false);
132
+ cleanup(el);
133
+ });
134
+
135
+ it('toggles on --toggle command', async () => {
136
+ const el = await fixture('<bds-popover id="p3"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
137
+ el.dispatchEvent(makeCommandEvent('--toggle'));
138
+ await el.updateComplete;
139
+ expect(el.open).toBe(true);
140
+ el.dispatchEvent(makeCommandEvent('--toggle'));
141
+ await el.updateComplete;
142
+ expect(el.open).toBe(false);
143
+ cleanup(el);
144
+ });
145
+
146
+ it('fires bds-open event on --open command', async () => {
147
+ const el = await fixture('<bds-popover id="p4"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
148
+ const handler = vi.fn();
149
+ el.addEventListener('bds-open', handler);
150
+ el.dispatchEvent(makeCommandEvent('--open'));
151
+ await el.updateComplete;
152
+ expect(handler).toHaveBeenCalledOnce();
153
+ cleanup(el);
154
+ });
155
+
156
+ it('ignores unknown commands', async () => {
157
+ const el = await fixture('<bds-popover id="p5"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
158
+ el.dispatchEvent(makeCommandEvent('--unknown'));
159
+ await el.updateComplete;
160
+ expect(el.open).toBe(false);
161
+ cleanup(el);
162
+ });
163
+ });
164
+
165
+ it('two instances get different anchor names', async () => {
166
+ const a = await fixture('<bds-popover><button slot="trigger">A</button></bds-popover>');
167
+ const b = await fixture('<bds-popover><button slot="trigger">B</button></bds-popover>');
168
+ const anchorA = a.style.getPropertyValue('--bds-popover-anchor');
169
+ const anchorB = b.style.getPropertyValue('--bds-popover-anchor');
170
+ expect(anchorA).not.toBe(anchorB);
171
+ cleanup(a);
172
+ cleanup(b);
173
+ });
174
+ });