@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
@@ -1,13 +1,25 @@
1
1
  @layer component {
2
2
  .wrapper {
3
- position: relative;
4
3
  display: inline-flex;
4
+
5
+ /* No position: relative — the panel is position: fixed so it doesn't need
6
+ the wrapper as a containing block. */
5
7
  }
6
8
 
7
9
  .panel {
8
- position: absolute;
9
- z-index: var(--bds-z-index_popover);
10
- min-width: 12rem;
10
+ /* Fixed positioning: containing block is the viewport.
11
+ - CSS anchor() values resolve in viewport space → polyfill applies them correctly.
12
+ - Ancestor overflow / stacking contexts cannot clip the panel. */
13
+ position: fixed;
14
+
15
+ /* Reset UA [popover] defaults (inset: 0, margin: auto, border, padding). */
16
+ inset: unset;
17
+ margin: 0;
18
+ overflow-block: auto;
19
+ z-index: var(--popover_z-index, 100);
20
+ min-inline-size: 12rem;
21
+ max-inline-size: var(--popover_max-width, 20rem);
22
+ max-block-size: var(--popover_max-height, 80dvh);
11
23
  padding: var(--bds-space_m);
12
24
  border: var(--popover_border, none);
13
25
  --bdc_color: currentcolor;
@@ -22,23 +34,127 @@
22
34
  font-size: var(--bds-font_size--body);
23
35
  }
24
36
 
25
- .--placement_bottom {
26
- top: calc(100% + var(--bds-space_xs));
27
- left: 0;
37
+ /* Primary placements via CSS anchor positioning.
38
+ anchor() resolves relative to the element tagged with the matching anchor-name.
39
+ @position-try fallbacks are tried in order when the primary placement overflows. */
40
+
41
+ .panel[data-placement='bottom'] {
42
+ top: anchor(bottom);
43
+ left: anchor(left);
44
+ margin-block-start: var(--bds-space_xs, 8px);
45
+ position-try-fallbacks: --popover-top, --popover-right, --popover-left;
46
+ }
47
+
48
+ .panel[data-placement='top'] {
49
+ bottom: anchor(top);
50
+ left: anchor(left);
51
+ margin-block-end: var(--bds-space_xs, 8px);
52
+ position-try-fallbacks: --popover-bottom, --popover-right, --popover-left;
53
+ }
54
+
55
+ .panel[data-placement='right'] {
56
+ top: anchor(top);
57
+ left: anchor(right);
58
+ margin-inline-start: var(--bds-space_xs, 8px);
59
+ position-try-fallbacks: --popover-left, --popover-bottom, --popover-top;
60
+ }
61
+
62
+ .panel[data-placement='left'] {
63
+ top: anchor(top);
64
+ right: anchor(left);
65
+ margin-inline-end: var(--bds-space_xs, 8px);
66
+ position-try-fallbacks: --popover-right, --popover-bottom, --popover-top;
67
+ }
68
+
69
+ /* Fallback placements — used by the browser (or polyfill) when the primary
70
+ placement would overflow the viewport. Each block resets all inset/margin
71
+ properties so nothing bleeds in from the primary placement. */
72
+
73
+ @position-try --popover-bottom {
74
+ top: anchor(bottom);
75
+ right: unset;
76
+ bottom: unset;
77
+ left: anchor(left);
78
+ margin: 0;
79
+ margin-block-start: var(--bds-space_xs, 8px);
80
+ }
81
+
82
+ @position-try --popover-top {
83
+ top: unset;
84
+ right: unset;
85
+ bottom: anchor(top);
86
+ left: anchor(left);
87
+ margin: 0;
88
+ margin-block-end: var(--bds-space_xs, 8px);
28
89
  }
29
90
 
30
- .--placement_top {
31
- bottom: calc(100% + var(--bds-space_xs));
32
- left: 0;
91
+ @position-try --popover-right {
92
+ top: anchor(top);
93
+ right: unset;
94
+ bottom: unset;
95
+ left: anchor(right);
96
+ margin: 0;
97
+ margin-inline-start: var(--bds-space_xs, 8px);
33
98
  }
34
99
 
35
- .--placement_right {
36
- left: calc(100% + var(--bds-space_xs));
37
- top: 0;
100
+ @position-try --popover-left {
101
+ top: anchor(top);
102
+ right: anchor(left);
103
+ bottom: unset;
104
+ left: unset;
105
+ margin: 0;
106
+ margin-inline-end: var(--bds-space_xs, 8px);
107
+ }
108
+
109
+ /* Full-width fallback blocks for narrow screens.
110
+ Defined at layer level (not inside @media) because @position-try cannot
111
+ be nested inside @media. These preserve left:8px/right:8px so the
112
+ panel stays edge-to-edge even when a @position-try fallback fires. */
113
+
114
+ @position-try --popover-bottom-fullwidth {
115
+ top: anchor(bottom);
116
+ right: var(--bds-space_xs, 8px);
117
+ bottom: unset;
118
+ left: var(--bds-space_xs, 8px);
119
+ margin: 0;
120
+ margin-block-start: var(--bds-space_xs, 8px);
38
121
  }
39
122
 
40
- .--placement_left {
41
- right: calc(100% + var(--bds-space_xs));
42
- top: 0;
123
+ @position-try --popover-top-fullwidth {
124
+ top: unset;
125
+ right: var(--bds-space_xs, 8px);
126
+ bottom: anchor(top);
127
+ left: var(--bds-space_xs, 8px);
128
+ margin: 0;
129
+ margin-block-end: var(--bds-space_xs, 8px);
130
+ }
131
+
132
+ /* Narrow screens (≤420px): span full viewport width and only flip vertically.
133
+ max-width: none overrides the base 20rem cap; left/right edge constraints
134
+ handle the width instead. The panel still touches the anchor on the
135
+ vertical axis (top/bottom via anchor() is not overridden here). */
136
+ @media (width <= 420px) {
137
+ .panel[data-placement='bottom'],
138
+ .panel[data-placement='top'],
139
+ .panel[data-placement='left'],
140
+ .panel[data-placement='right'] {
141
+ left: var(--bds-space_xs, 8px);
142
+ right: var(--bds-space_xs, 8px);
143
+ inline-size: 100%;
144
+ max-inline-size: calc(100svw - 2 * var(--bds-space_xs, 8px));
145
+ position-try-fallbacks: --popover-top-fullwidth, --popover-bottom-fullwidth;
146
+ }
147
+ }
148
+
149
+ /* Wide + short screens (e.g. landscape mobile, split-screen): when the
150
+ viewport has more horizontal room than vertical, prefer side placements. */
151
+ @media (aspect-ratio >= 2/1) {
152
+ .panel[data-placement='bottom'] {
153
+ position-try-fallbacks: --popover-top, --popover-right, --popover-left;
154
+ }
155
+
156
+ .panel[data-placement='top'] {
157
+ position-try-fallbacks: --popover-bottom, --popover-right, --popover-left;
158
+ }
43
159
  }
44
160
  }
@@ -2,6 +2,47 @@ import { render, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { Popover } from './Popover';
4
4
 
5
+ // jsdom doesn't implement CSS.supports — stub so the anchor polyfill guard doesn't throw.
6
+ beforeAll(() => {
7
+ Object.defineProperty(globalThis, 'CSS', {
8
+ value: { supports: () => true },
9
+ configurable: true,
10
+ });
11
+ });
12
+
13
+ // jsdom doesn't implement the Popover API — mock showPopover/hidePopover/togglePopover
14
+ // and dispatch the `toggle` event so our state-sync listener fires.
15
+ beforeEach(() => {
16
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
17
+ const event = new Event('toggle');
18
+ Object.defineProperty(event, 'newState', { value: 'open', configurable: true });
19
+ this.dispatchEvent(event);
20
+ });
21
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
22
+ const event = new Event('toggle');
23
+ Object.defineProperty(event, 'newState', { value: 'closed', configurable: true });
24
+ this.dispatchEvent(event);
25
+ });
26
+ HTMLElement.prototype.togglePopover = vi.fn(function (this: HTMLElement) {
27
+ const isCurrentlyOpen = this.getAttribute('data-popover-open') === 'true';
28
+ if (isCurrentlyOpen) {
29
+ this.setAttribute('data-popover-open', 'false');
30
+ const event = new Event('toggle');
31
+ Object.defineProperty(event, 'newState', { value: 'closed', configurable: true });
32
+ this.dispatchEvent(event);
33
+ } else {
34
+ this.setAttribute('data-popover-open', 'true');
35
+ const event = new Event('toggle');
36
+ Object.defineProperty(event, 'newState', { value: 'open', configurable: true });
37
+ this.dispatchEvent(event);
38
+ }
39
+ });
40
+ });
41
+
42
+ function getPanel() {
43
+ return document.querySelector<HTMLElement>('[popover]')!;
44
+ }
45
+
5
46
  describe('Popover', () => {
6
47
  it('renders the trigger', () => {
7
48
  render(
@@ -12,13 +53,23 @@ describe('Popover', () => {
12
53
  expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument();
13
54
  });
14
55
 
15
- it('does not show the panel by default', () => {
56
+ it('renders the panel as a [popover] element', () => {
16
57
  render(
17
58
  <Popover content={<p>Panel content</p>}>
18
59
  <button type="button">Open</button>
19
60
  </Popover>
20
61
  );
21
- expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
62
+ expect(getPanel()).toBeInTheDocument();
63
+ expect(getPanel()).toHaveAttribute('popover', 'auto');
64
+ });
65
+
66
+ it('panel is not open by default', () => {
67
+ render(
68
+ <Popover content={<p>Panel content</p>}>
69
+ <button type="button">Open</button>
70
+ </Popover>
71
+ );
72
+ expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false');
22
73
  });
23
74
 
24
75
  it('shows the panel when trigger is clicked', async () => {
@@ -29,7 +80,20 @@ describe('Popover', () => {
29
80
  </Popover>
30
81
  );
31
82
  await user.click(screen.getByRole('button', { name: 'Open' }));
32
- expect(screen.getByText('Panel content')).toBeInTheDocument();
83
+ expect(HTMLElement.prototype.togglePopover).toHaveBeenCalled();
84
+ });
85
+
86
+ it('sets aria-expanded true after opening', async () => {
87
+ const user = userEvent.setup();
88
+ render(
89
+ <Popover content={<p>Panel</p>}>
90
+ <button type="button">Open</button>
91
+ </Popover>
92
+ );
93
+ const trigger = screen.getByRole('button');
94
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
95
+ await user.click(trigger);
96
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
33
97
  });
34
98
 
35
99
  it('closes the panel when trigger is clicked again', async () => {
@@ -41,32 +105,68 @@ describe('Popover', () => {
41
105
  );
42
106
  await user.click(screen.getByRole('button', { name: 'Toggle' }));
43
107
  await user.click(screen.getByRole('button', { name: 'Toggle' }));
44
- expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
108
+ expect(screen.getByRole('button', { name: 'Toggle' })).toHaveAttribute('aria-expanded', 'false');
45
109
  });
46
110
 
47
- it('closes the panel when Escape is pressed', async () => {
48
- const user = userEvent.setup();
111
+ it('trigger uses commandfor/command="toggle-popover"', () => {
49
112
  render(
50
- <Popover content={<p>Panel content</p>}>
113
+ <Popover content={<p>Panel</p>}>
51
114
  <button type="button">Open</button>
52
115
  </Popover>
53
116
  );
54
- await user.click(screen.getByRole('button', { name: 'Open' }));
55
- await user.keyboard('{Escape}');
56
- expect(screen.queryByText('Panel content')).not.toBeInTheDocument();
117
+ const trigger = screen.getByRole('button');
118
+ expect(trigger).toHaveAttribute('commandfor');
119
+ expect(trigger).toHaveAttribute('command', 'toggle-popover');
120
+ expect(trigger.getAttribute('commandfor')).toBe(getPanel().id);
57
121
  });
58
122
 
59
- it('sets aria-expanded on the trigger when open', async () => {
60
- const user = userEvent.setup();
123
+ it('sets anchor-name on the trigger and position-anchor on the panel', () => {
61
124
  render(
62
125
  <Popover content={<p>Panel</p>}>
63
126
  <button type="button">Open</button>
64
127
  </Popover>
65
128
  );
66
129
  const trigger = screen.getByRole('button');
67
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
68
- await user.click(trigger);
69
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
130
+ const panel = getPanel();
131
+ const anchorName = trigger.style.getPropertyValue('anchor-name');
132
+ const positionAnchor = panel.style.getPropertyValue('position-anchor');
133
+ expect(anchorName).toMatch(/^--popover-/);
134
+ expect(positionAnchor).toBe(anchorName);
135
+ });
136
+
137
+ it('each instance gets a unique anchor-name', () => {
138
+ render(
139
+ <>
140
+ <Popover content={<p>A</p>}>
141
+ <button type="button">A</button>
142
+ </Popover>
143
+ <Popover content={<p>B</p>}>
144
+ <button type="button">B</button>
145
+ </Popover>
146
+ </>
147
+ );
148
+ const [btnA, btnB] = screen.getAllByRole('button');
149
+ expect(btnA.style.getPropertyValue('anchor-name')).not.toBe(
150
+ btnB.style.getPropertyValue('anchor-name')
151
+ );
152
+ });
153
+
154
+ it('sets data-placement from the placement prop', () => {
155
+ render(
156
+ <Popover content={<p>Panel</p>} placement="top">
157
+ <button type="button">Open</button>
158
+ </Popover>
159
+ );
160
+ expect(getPanel()).toHaveAttribute('data-placement', 'top');
161
+ });
162
+
163
+ it('defaults data-placement to "bottom"', () => {
164
+ render(
165
+ <Popover content={<p>Panel</p>}>
166
+ <button type="button">Open</button>
167
+ </Popover>
168
+ );
169
+ expect(getPanel()).toHaveAttribute('data-placement', 'bottom');
70
170
  });
71
171
 
72
172
  it('renders role="region" with aria-label when aria-label prop is provided', async () => {
@@ -77,17 +177,16 @@ describe('Popover', () => {
77
177
  </Popover>
78
178
  );
79
179
  await user.click(screen.getByRole('button', { name: 'Open' }));
80
- expect(screen.getByRole('region', { name: 'Filter options' })).toBeInTheDocument();
180
+ expect(getPanel()).toHaveAttribute('role', 'region');
181
+ expect(getPanel()).toHaveAttribute('aria-label', 'Filter options');
81
182
  });
82
183
 
83
- it('does not render role="region" when no aria-label is provided', async () => {
84
- const user = userEvent.setup();
184
+ it('does not render role="region" when no aria-label is provided', () => {
85
185
  render(
86
186
  <Popover content={<p>Panel</p>}>
87
187
  <button type="button">Open</button>
88
188
  </Popover>
89
189
  );
90
- await user.click(screen.getByRole('button', { name: 'Open' }));
91
- expect(screen.queryByRole('region')).not.toBeInTheDocument();
190
+ expect(getPanel()).not.toHaveAttribute('role');
92
191
  });
93
192
  });
@@ -3,7 +3,7 @@ import { Popover } from './Popover';
3
3
  import { Button } from '../Button/Button';
4
4
 
5
5
  const meta = {
6
- title: 'Interaction/Popover',
6
+ title: 'React/Interaction/Popover',
7
7
  component: Popover,
8
8
  argTypes: {
9
9
  placement: { control: 'radio', options: ['top', 'bottom', 'left', 'right'] },
@@ -8,14 +8,27 @@ import {
8
8
  useRef,
9
9
  useState,
10
10
  } from 'react';
11
+ import { installInvokerCommandsPolyfill } from '../../../polyfill-invoker-commands';
12
+
13
+ // Install once at module load — no-op if native support is present.
14
+ installInvokerCommandsPolyfill();
15
+
11
16
  import css from './Popover.module.css';
12
17
  import { cn } from '@boostdev/design-system-foundation';
18
+ import type { WithClassName } from '../../../types';
13
19
 
14
- interface PopoverProps {
20
+ // Load the CSS Anchor Positioning polyfill once, only in browsers that need it.
21
+ if (
22
+ typeof window !== 'undefined' &&
23
+ !(typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('anchor-name: --a'))
24
+ ) {
25
+ import('@oddbird/css-anchor-positioning').catch(() => {});
26
+ }
27
+
28
+ interface PopoverProps extends WithClassName {
15
29
  children: ReactElement;
16
30
  content: ReactNode;
17
31
  placement?: 'top' | 'bottom' | 'left' | 'right';
18
- className?: string;
19
32
  'aria-label'?: string;
20
33
  }
21
34
 
@@ -28,54 +41,81 @@ export function Popover({
28
41
  }: Readonly<PopoverProps>) {
29
42
  const [isOpen, setIsOpen] = useState(false);
30
43
  const containerRef = useRef<HTMLSpanElement>(null);
31
- const panelId = useId();
44
+ const panelRef = useRef<HTMLDivElement>(null);
45
+ const rawId = useId();
46
+ // CSS anchor-name must be a valid custom-ident: strip non-word characters.
47
+ const anchorName = `--popover-${rawId.replace(/\W/g, '') || 'a'}`;
48
+ const panelId = rawId;
32
49
 
50
+ // Sync isOpen state via the popover toggle event (fires on both native and
51
+ // polyfilled open/close, including light-dismiss and Escape).
33
52
  useEffect(() => {
34
- if (!isOpen) return;
53
+ const panel = panelRef.current;
54
+ if (!panel) return;
55
+ const handleToggle = (e: Event) => {
56
+ setIsOpen((e as ToggleEvent).newState === 'open');
57
+ };
58
+ panel.addEventListener('toggle', handleToggle);
59
+ return () => panel.removeEventListener('toggle', handleToggle);
60
+ }, []);
35
61
 
36
- const handlePointerDown = (e: PointerEvent) => {
37
- if (!containerRef.current?.contains(e.target as Node)) {
38
- setIsOpen(false);
62
+ // Close the popover when the anchor scrolls out of the viewport.
63
+ // Light-dismiss (outside click) and Escape are handled natively by popover="auto".
64
+ useEffect(() => {
65
+ if (!isOpen) return;
66
+ const handleScroll = () => {
67
+ const rect = containerRef.current?.getBoundingClientRect();
68
+ if (!rect) return;
69
+ const { innerWidth: vw, innerHeight: vh } = window;
70
+ if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
71
+ panelRef.current?.hidePopover();
39
72
  }
40
73
  };
41
- const handleKeyDown = (e: KeyboardEvent) => {
42
- if (e.key === 'Escape') setIsOpen(false);
43
- };
44
-
45
- document.addEventListener('pointerdown', handlePointerDown);
46
- document.addEventListener('keydown', handleKeyDown);
74
+ window.addEventListener('scroll', handleScroll, { capture: true, passive: true });
75
+ window.addEventListener('resize', handleScroll, { passive: true });
47
76
  return () => {
48
- document.removeEventListener('pointerdown', handlePointerDown);
49
- document.removeEventListener('keydown', handleKeyDown);
77
+ window.removeEventListener('scroll', handleScroll, { capture: true });
78
+ window.removeEventListener('resize', handleScroll);
50
79
  };
51
80
  }, [isOpen]);
52
81
 
82
+ // Inject invoker command attrs and accessibility props onto the trigger.
83
+ // commandfor/command="toggle-popover" replaces the onClick toggle handler —
84
+ // the browser (or polyfill) calls togglePopover() on the panel, which fires
85
+ // the toggle event above to sync state.
53
86
  const trigger = isValidElement(children)
54
87
  ? cloneElement(children as ReactElement<Record<string, unknown>>, {
55
88
  'aria-expanded': isOpen,
56
89
  'aria-controls': panelId,
57
90
  'aria-haspopup': true,
58
- onClick: (e: MouseEvent) => {
59
- setIsOpen(prev => !prev);
60
- const existingOnClick = (children.props as Record<string, unknown>).onClick;
61
- if (typeof existingOnClick === 'function') existingOnClick(e);
91
+ commandfor: panelId,
92
+ command: 'toggle-popover',
93
+ style: {
94
+ ...((children.props as Record<string, unknown>).style as object | undefined),
95
+ anchorName,
62
96
  },
97
+ // Forward any existing onClick on the child (for custom side-effects).
98
+ // Do NOT add our own toggle here — commandfor/command handles it to
99
+ // avoid a double-toggle when both onClick and invoker fire.
100
+ onClick: (children.props as Record<string, unknown>).onClick as (() => void) | undefined,
63
101
  })
64
102
  : children;
65
103
 
66
104
  return (
67
105
  <span ref={containerRef} className={cn(css.wrapper, className)}>
68
106
  {trigger}
69
- {isOpen && (
70
- <div
71
- id={panelId}
72
- role={ariaLabel ? 'region' : undefined}
73
- aria-label={ariaLabel}
74
- className={cn(css.panel, css[`--placement_${placement}`])}
75
- >
76
- {content}
77
- </div>
78
- )}
107
+ <div
108
+ ref={panelRef}
109
+ id={panelId}
110
+ popover="auto"
111
+ data-placement={placement}
112
+ role={ariaLabel ? 'region' : undefined}
113
+ aria-label={ariaLabel}
114
+ className={css.panel}
115
+ style={{ positionAnchor: anchorName }}
116
+ >
117
+ {content}
118
+ </div>
79
119
  </span>
80
120
  );
81
121
  }
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { Rating } from './Rating';
3
3
 
4
4
  const meta = {
5
- title: 'Interaction/Rating',
5
+ title: 'React/Interaction/Rating',
6
6
  component: Rating,
7
7
  argTypes: {
8
8
  value: { control: { type: 'range', min: 0, max: 5, step: 1 } },
@@ -1,10 +1,10 @@
1
1
  import css from './Rating.module.css';
2
2
  import { cn } from '@boostdev/design-system-foundation';
3
+ import type { WithClassName } from '../../../types';
3
4
 
4
- interface RatingProps {
5
+ interface RatingProps extends WithClassName {
5
6
  value: number;
6
7
  max?: number;
7
- className?: string;
8
8
  }
9
9
 
10
10
  export function Rating({ value, max = 5, className }: RatingProps) {
@@ -1,12 +1,17 @@
1
1
  @layer component {
2
2
  .toastContainer {
3
3
  position: fixed;
4
- bottom: var(--bds-space_xl);
5
- right: var(--bds-space_xl);
4
+ inset: unset; /* override UA popover default (inset: 0) */
5
+ inset-block-end: var(--bds-space_xl);
6
+ inset-inline-end: var(--bds-space_xl);
6
7
  display: flex;
7
8
  flex-direction: column;
8
9
  gap: var(--bds-space_m);
9
- z-index: var(--bds-z-index_toast);
10
+
11
+ /* z-index removed — top layer renders above all fixed/sticky elements */
12
+ border: none; /* override UA popover default */
13
+ padding: 0; /* override UA popover default */
14
+ background: transparent; /* override UA popover default */
10
15
  }
11
16
 
12
17
  .toast {
@@ -18,7 +23,7 @@
18
23
  display: flex;
19
24
  align-items: center;
20
25
  gap: var(--bds-space_m);
21
- min-width: 300px;
26
+ min-inline-size: 300px;
22
27
  animation: slideIn var(--bds-animation_duration--fast) var(--bds-animation_easing);
23
28
  }
24
29
 
@@ -51,8 +56,8 @@
51
56
  align-items: center;
52
57
  justify-content: center;
53
58
  flex-shrink: 0;
54
- width: 1.25rem;
55
- height: 1.25rem;
59
+ inline-size: 1.25rem;
60
+ block-size: 1.25rem;
56
61
  padding: 0;
57
62
  background: none;
58
63
  border: none;
@@ -61,8 +66,8 @@
61
66
  }
62
67
 
63
68
  .closeButton svg {
64
- width: 1rem;
65
- height: 1rem;
69
+ inline-size: 1rem;
70
+ block-size: 1rem;
66
71
  }
67
72
  }
68
73
 
@@ -32,6 +32,27 @@ describe('Toast', () => {
32
32
  vi.useRealTimers();
33
33
  });
34
34
 
35
+ it('renders the toast container as a manual popover', () => {
36
+ render(
37
+ <ToastProvider>
38
+ <div />
39
+ </ToastProvider>
40
+ );
41
+ const container = document.querySelector('[popover]');
42
+ expect(container).toHaveAttribute('popover', 'manual');
43
+ });
44
+
45
+ it('renders the toast container as a labelled notifications region', () => {
46
+ render(
47
+ <ToastProvider>
48
+ <div />
49
+ </ToastProvider>
50
+ );
51
+ const container = document.querySelector('[popover="manual"]');
52
+ expect(container).toHaveAttribute('role', 'region');
53
+ expect(container).toHaveAttribute('aria-label', 'Notifications');
54
+ });
55
+
35
56
  it('throws when useToast is used outside ToastProvider', () => {
36
57
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
37
58
  const Bad = () => { useToast(); return null; };
@@ -3,7 +3,7 @@ import { ToastProvider, useToast } from './Toast';
3
3
  import { Button } from '../Button/Button';
4
4
 
5
5
  const meta = {
6
- title: 'Interaction/Toast',
6
+ title: 'React/Interaction/Toast',
7
7
  component: ToastProvider,
8
8
  } satisfies Meta<typeof ToastProvider>;
9
9