@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,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,126 @@
1
+ import { fixture, cleanup } 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
+ it('two instances get different anchor names', async () => {
118
+ const a = await fixture('<bds-popover><button slot="trigger">A</button></bds-popover>');
119
+ const b = await fixture('<bds-popover><button slot="trigger">B</button></bds-popover>');
120
+ const anchorA = a.style.getPropertyValue('--bds-popover-anchor');
121
+ const anchorB = b.style.getPropertyValue('--bds-popover-anchor');
122
+ expect(anchorA).not.toBe(anchorB);
123
+ cleanup(a);
124
+ cleanup(b);
125
+ });
126
+ });
@@ -0,0 +1,217 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ export type PopoverPlacement = 'bottom' | 'top' | 'left' | 'right';
4
+
5
+ /**
6
+ * `<bds-popover>` — framework-agnostic Popover custom element.
7
+ *
8
+ * Uses CSS Anchor Positioning within the shadow DOM so the panel stays
9
+ * anchored to the trigger and automatically repositions when it would
10
+ * overflow the viewport (`@position-try` fallbacks).
11
+ *
12
+ * Attributes:
13
+ * placement — "bottom" (default) | "top" | "left" | "right"
14
+ * open — boolean; reflects whether the panel is visible
15
+ *
16
+ * Slots:
17
+ * trigger — the element that opens/closes the panel on click
18
+ * (default) — panel content
19
+ *
20
+ * Events:
21
+ * bds-open — fired (bubbles, composed) when the panel opens
22
+ * bds-close — fired (bubbles, composed) when the panel closes
23
+ *
24
+ * @example
25
+ * <bds-popover placement="bottom">
26
+ * <button slot="trigger">Open</button>
27
+ * <p>Panel content here</p>
28
+ * </bds-popover>
29
+ */
30
+ export class BdsPopover extends LitElement {
31
+ static styles = css`
32
+ :host {
33
+ display: inline-flex;
34
+ position: relative;
35
+ }
36
+
37
+ /* The wrapper IS the CSS anchor — anchor-name resolves via the custom
38
+ property injected on :host in connectedCallback(), so each instance
39
+ gets its own unique name without colliding with siblings. */
40
+ .wrapper {
41
+ display: inline-flex;
42
+ anchor-name: var(--bds-popover-anchor);
43
+ }
44
+
45
+ .panel {
46
+ display: none;
47
+ position: absolute;
48
+ position-anchor: var(--bds-popover-anchor);
49
+ margin: 0;
50
+ overflow-y: auto;
51
+ z-index: var(--popover_z-index, 100);
52
+ min-width: 12rem;
53
+ padding: var(--bds-space_m);
54
+ border-radius: var(--bds-border_radius--s);
55
+ background-color: var(--popover_color, var(--bds-color_bg));
56
+ box-shadow: var(--popover_shadow, var(--bds-shadow_m));
57
+ color: var(--popover_on-color, var(--bds-color_on-bg));
58
+ font-size: var(--bds-font_size--body);
59
+ }
60
+
61
+ .panel.open {
62
+ display: block;
63
+ }
64
+
65
+ /* Primary placements */
66
+
67
+ .panel[data-placement='bottom'] {
68
+ top: anchor(bottom);
69
+ left: anchor(left);
70
+ margin-top: var(--bds-space_xs, 8px);
71
+ position-try-fallbacks: --bds-popover-top, --bds-popover-right, --bds-popover-left;
72
+ }
73
+
74
+ .panel[data-placement='top'] {
75
+ bottom: anchor(top);
76
+ left: anchor(left);
77
+ margin-bottom: var(--bds-space_xs, 8px);
78
+ position-try-fallbacks: --bds-popover-bottom, --bds-popover-right, --bds-popover-left;
79
+ }
80
+
81
+ .panel[data-placement='right'] {
82
+ top: anchor(top);
83
+ left: anchor(right);
84
+ margin-left: var(--bds-space_xs, 8px);
85
+ position-try-fallbacks: --bds-popover-left, --bds-popover-bottom, --bds-popover-top;
86
+ }
87
+
88
+ .panel[data-placement='left'] {
89
+ top: anchor(top);
90
+ right: anchor(left);
91
+ margin-right: var(--bds-space_xs, 8px);
92
+ position-try-fallbacks: --bds-popover-right, --bds-popover-bottom, --bds-popover-top;
93
+ }
94
+
95
+ /* Fallback placements */
96
+
97
+ @position-try --bds-popover-bottom {
98
+ top: anchor(bottom);
99
+ right: unset;
100
+ bottom: unset;
101
+ left: anchor(left);
102
+ margin: 0;
103
+ margin-top: var(--bds-space_xs, 8px);
104
+ }
105
+
106
+ @position-try --bds-popover-top {
107
+ top: unset;
108
+ right: unset;
109
+ bottom: anchor(top);
110
+ left: anchor(left);
111
+ margin: 0;
112
+ margin-bottom: var(--bds-space_xs, 8px);
113
+ }
114
+
115
+ @position-try --bds-popover-right {
116
+ top: anchor(top);
117
+ right: unset;
118
+ bottom: unset;
119
+ left: anchor(right);
120
+ margin: 0;
121
+ margin-left: var(--bds-space_xs, 8px);
122
+ }
123
+
124
+ @position-try --bds-popover-left {
125
+ top: anchor(top);
126
+ right: anchor(left);
127
+ bottom: unset;
128
+ left: unset;
129
+ margin: 0;
130
+ margin-right: var(--bds-space_xs, 8px);
131
+ }
132
+ `;
133
+
134
+ static properties = {
135
+ placement: { type: String, reflect: true },
136
+ open: { type: Boolean, reflect: true },
137
+ };
138
+
139
+ declare placement: PopoverPlacement;
140
+ declare open: boolean;
141
+
142
+ private _anchorName: string;
143
+
144
+ constructor() {
145
+ super();
146
+ this.placement = 'bottom';
147
+ this.open = false;
148
+ const uid = Math.random().toString(36).slice(2, 8);
149
+ this._anchorName = `--bds-popover-${uid}`;
150
+ }
151
+
152
+ connectedCallback() {
153
+ super.connectedCallback();
154
+ // Inject the unique anchor-name as a CSS custom property so both
155
+ // `.wrapper` (anchor-name) and `.panel` (position-anchor) reference
156
+ // the same value without hard-coding it in the shared static styles.
157
+ this.style.setProperty('--bds-popover-anchor', this._anchorName);
158
+ document.addEventListener('keydown', this._onKeydown);
159
+ document.addEventListener('mousedown', this._onOutsideClick);
160
+ }
161
+
162
+ disconnectedCallback() {
163
+ super.disconnectedCallback();
164
+ document.removeEventListener('keydown', this._onKeydown);
165
+ document.removeEventListener('mousedown', this._onOutsideClick);
166
+ }
167
+
168
+ private _onKeydown = (e: KeyboardEvent) => {
169
+ if (this.open && e.key === 'Escape') this._close();
170
+ };
171
+
172
+ private _onOutsideClick = (e: MouseEvent) => {
173
+ if (this.open && !this.contains(e.target as Node)) this._close();
174
+ };
175
+
176
+ private _toggle() {
177
+ if (this.open) {
178
+ this._close();
179
+ } else {
180
+ this._open();
181
+ }
182
+ }
183
+
184
+ private _open() {
185
+ this.open = true;
186
+ this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
187
+ }
188
+
189
+ private _close() {
190
+ this.open = false;
191
+ this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
192
+ }
193
+
194
+ render() {
195
+ return html`
196
+ <span class="wrapper">
197
+ <slot name="trigger" @click=${this._toggle}></slot>
198
+ </span>
199
+ <div
200
+ class="panel ${this.open ? 'open' : ''}"
201
+ data-placement=${this.placement}
202
+ role="region"
203
+ aria-label="Popover"
204
+ >
205
+ <slot></slot>
206
+ </div>
207
+ `;
208
+ }
209
+ }
210
+
211
+ customElements.define('bds-popover', BdsPopover);
212
+
213
+ declare global {
214
+ interface HTMLElementTagNameMap {
215
+ 'bds-popover': BdsPopover;
216
+ }
217
+ }