@boostdev/design-system-components 2.7.1 → 2.11.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 (535) hide show
  1. package/AGENTS.md +54 -46
  2. package/README.md +34 -20
  3. package/dist/client.cjs +1317 -779
  4. package/dist/client.css +1029 -812
  5. package/dist/client.d.cts +88 -26
  6. package/dist/client.d.ts +88 -26
  7. package/dist/client.js +1278 -732
  8. package/dist/index.cjs +1317 -779
  9. package/dist/index.css +1029 -812
  10. package/dist/index.d.cts +88 -26
  11. package/dist/index.d.ts +88 -26
  12. package/dist/index.js +1278 -732
  13. package/dist/web-components/chunk-2OLVQUBU.js +193 -0
  14. package/dist/web-components/chunk-3AY2YKCM.js +223 -0
  15. package/dist/web-components/chunk-3WAV6MDN.js +212 -0
  16. package/dist/web-components/{chunk-CZBIVR4V.js → chunk-47AT5KZD.js} +88 -16
  17. package/dist/web-components/chunk-4A2YYTYN.js +93 -0
  18. package/dist/web-components/{chunk-Q4T5BJT6.js → chunk-62NK3ZSC.js} +18 -3
  19. package/dist/web-components/chunk-66M6ETWD.js +77 -0
  20. package/dist/web-components/{chunk-NCPQ7AF3.js → chunk-6G22I2CK.js} +18 -3
  21. package/dist/web-components/{chunk-QDST57JC.js → chunk-7ZOJ7N4E.js} +14 -4
  22. package/dist/web-components/{chunk-5R6IFELV.js → chunk-AIBOHNDU.js} +11 -1
  23. package/dist/web-components/{chunk-ITHLKR7Q.js → chunk-AP4RDUL2.js} +16 -5
  24. package/dist/web-components/{chunk-QWPACK5L.js → chunk-BPYKZVDK.js} +11 -1
  25. package/dist/web-components/{chunk-YLVTEY23.js → chunk-BTCMJQKB.js} +53 -21
  26. package/dist/web-components/chunk-CUKQ2L5B.js +245 -0
  27. package/dist/web-components/chunk-DIZDCL7Z.js +14 -0
  28. package/dist/web-components/{chunk-PC3IOQAH.js → chunk-DV5T43ZC.js} +27 -7
  29. package/dist/web-components/{chunk-2QA62WCQ.js → chunk-EF7IVYTY.js} +10 -0
  30. package/dist/web-components/{chunk-L3XHOMN6.js → chunk-F3ZDNHZZ.js} +26 -1
  31. package/dist/web-components/chunk-FEDABOR5.js +248 -0
  32. package/dist/web-components/{chunk-6I2DBFQ7.js → chunk-HGFCJXOQ.js} +36 -5
  33. package/dist/web-components/{chunk-SOQQ7RVJ.js → chunk-IPGHUCJT.js} +23 -8
  34. package/dist/web-components/chunk-ITD2MV5K.js +392 -0
  35. package/dist/web-components/{chunk-CM3KYZ6Z.js → chunk-JH6LQKLK.js} +78 -6
  36. package/dist/web-components/chunk-JMCXAAFK.js +120 -0
  37. package/dist/web-components/{chunk-KUI55GFB.js → chunk-JUPCZVVW.js} +50 -18
  38. package/dist/web-components/chunk-JV5QT2OU.js +146 -0
  39. package/dist/web-components/{chunk-3GFWLSLS.js → chunk-KA3VPKMY.js} +29 -6
  40. package/dist/web-components/{chunk-CR5PGUHV.js → chunk-KJGRFDFI.js} +26 -11
  41. package/dist/web-components/{chunk-PY62FTRW.js → chunk-KZPRFY5P.js} +68 -33
  42. package/dist/web-components/{chunk-X46KL5BK.js → chunk-LDIPCTRN.js} +18 -1
  43. package/dist/web-components/chunk-M3ECULRH.js +174 -0
  44. package/dist/web-components/chunk-NBN2ENWJ.js +90 -0
  45. package/dist/web-components/chunk-NIN3JO6Q.js +189 -0
  46. package/dist/web-components/{chunk-2FGATTGT.js → chunk-NTQ4TCUT.js} +76 -55
  47. package/dist/web-components/chunk-OY74D4X6.js +302 -0
  48. package/dist/web-components/{chunk-3S42DZ7W.js → chunk-P24JYC5V.js} +7 -0
  49. package/dist/web-components/{chunk-BKIHQYJR.js → chunk-PXPPKCLT.js} +17 -2
  50. package/dist/web-components/{chunk-TQ7IQ4GZ.js → chunk-QEF2IRID.js} +71 -29
  51. package/dist/web-components/chunk-QZAIRJ75.js +111 -0
  52. package/dist/web-components/{chunk-6I3CFC26.js → chunk-RSZ3BWBK.js} +20 -5
  53. package/dist/web-components/chunk-RVF7W653.js +101 -0
  54. package/dist/web-components/chunk-SEO2F5KJ.js +234 -0
  55. package/dist/web-components/{chunk-VMMVWLXL.js → chunk-SFZ6Q4BP.js} +10 -0
  56. package/dist/web-components/{chunk-LY62SAVN.js → chunk-SYNLFKD3.js} +12 -2
  57. package/dist/web-components/chunk-TMWXRHGF.js +192 -0
  58. package/dist/web-components/{chunk-65JXQOBA.js → chunk-U3YZE6R6.js} +51 -7
  59. package/dist/web-components/chunk-UO7ROSHD.js +424 -0
  60. package/dist/web-components/chunk-VANAKB6D.js +152 -0
  61. package/dist/web-components/{chunk-PXZWYO6Z.js → chunk-VED2DFJ6.js} +108 -29
  62. package/dist/web-components/chunk-W6H5ID2U.js +161 -0
  63. package/dist/web-components/{chunk-57TRNCWQ.js → chunk-WFVKU4GX.js} +22 -11
  64. package/dist/web-components/{chunk-OQTRTMAB.js → chunk-WIBAN7KP.js} +10 -0
  65. package/dist/web-components/chunk-XI7KRAH7.js +54 -0
  66. package/dist/web-components/{chunk-F5E3IT7G.js → chunk-XIZWAWMJ.js} +50 -11
  67. package/dist/web-components/{chunk-FXURM6D5.js → chunk-YFAXGRPR.js} +10 -0
  68. package/dist/web-components/globals.js +1 -1
  69. package/dist/web-components/index.d.ts +77 -707
  70. package/dist/web-components/index.js +339 -2066
  71. package/dist/web-components/interaction/bds-accordion-item.js +1 -1
  72. package/dist/web-components/interaction/bds-accordion.js +1 -1
  73. package/dist/web-components/interaction/bds-button.d.ts +10 -2
  74. package/dist/web-components/interaction/bds-button.js +1 -1
  75. package/dist/web-components/interaction/bds-collapsible.d.ts +7 -2
  76. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  77. package/dist/web-components/interaction/bds-dialog.d.ts +11 -3
  78. package/dist/web-components/interaction/bds-dialog.js +1 -1
  79. package/dist/web-components/interaction/bds-drawer.d.ts +7 -1
  80. package/dist/web-components/interaction/bds-drawer.js +1 -1
  81. package/dist/web-components/interaction/bds-dropdown-menu-item.d.ts +51 -0
  82. package/dist/web-components/interaction/bds-dropdown-menu-item.js +7 -0
  83. package/dist/web-components/interaction/bds-dropdown-menu.d.ts +70 -0
  84. package/dist/web-components/interaction/bds-dropdown-menu.js +7 -0
  85. package/dist/web-components/interaction/bds-popover.d.ts +14 -3
  86. package/dist/web-components/interaction/bds-popover.js +1 -1
  87. package/dist/web-components/interaction/bds-skip-link.js +1 -1
  88. package/dist/web-components/interaction/bds-tab-panel.js +1 -1
  89. package/dist/web-components/interaction/bds-tab.js +1 -1
  90. package/dist/web-components/interaction/bds-tabs.d.ts +1 -0
  91. package/dist/web-components/interaction/bds-tabs.js +1 -1
  92. package/dist/web-components/interaction/bds-toast-provider.js +1 -1
  93. package/dist/web-components/interaction/bds-tooltip.d.ts +19 -3
  94. package/dist/web-components/interaction/bds-tooltip.js +1 -1
  95. package/dist/web-components/interaction/form/bds-checkbox-group.d.ts +83 -0
  96. package/dist/web-components/interaction/form/bds-checkbox-group.js +7 -0
  97. package/dist/web-components/interaction/form/bds-checkbox.d.ts +6 -0
  98. package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
  99. package/dist/web-components/interaction/form/bds-combobox.d.ts +18 -0
  100. package/dist/web-components/interaction/form/bds-combobox.js +1 -1
  101. package/dist/web-components/interaction/form/bds-field-group.d.ts +89 -0
  102. package/dist/web-components/interaction/form/bds-field-group.js +7 -0
  103. package/dist/web-components/interaction/form/bds-file-input.d.ts +1 -1
  104. package/dist/web-components/interaction/form/bds-file-input.js +1 -1
  105. package/dist/web-components/interaction/form/bds-form-input.d.ts +92 -0
  106. package/dist/web-components/interaction/form/bds-form-input.js +7 -0
  107. package/dist/web-components/interaction/form/bds-number-input.d.ts +18 -1
  108. package/dist/web-components/interaction/form/bds-number-input.js +1 -1
  109. package/dist/web-components/interaction/form/bds-radio-group.d.ts +81 -0
  110. package/dist/web-components/interaction/form/bds-radio-group.js +7 -0
  111. package/dist/web-components/interaction/form/bds-radio.d.ts +34 -6
  112. package/dist/web-components/interaction/form/bds-radio.js +1 -1
  113. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +4 -4
  114. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  115. package/dist/web-components/interaction/form/bds-select.d.ts +22 -1
  116. package/dist/web-components/interaction/form/bds-select.js +1 -1
  117. package/dist/web-components/interaction/form/bds-slider.d.ts +42 -10
  118. package/dist/web-components/interaction/form/bds-slider.js +1 -1
  119. package/dist/web-components/interaction/form/bds-switch.d.ts +20 -0
  120. package/dist/web-components/interaction/form/bds-switch.js +1 -1
  121. package/dist/web-components/interaction/form/bds-textarea.d.ts +23 -3
  122. package/dist/web-components/interaction/form/bds-textarea.js +1 -1
  123. package/dist/web-components/layout/bds-grid-item.d.ts +63 -0
  124. package/dist/web-components/layout/bds-grid-item.js +7 -0
  125. package/dist/web-components/layout/bds-grid.d.ts +108 -0
  126. package/dist/web-components/layout/bds-grid.js +7 -0
  127. package/dist/web-components/ui/bds-alert.d.ts +16 -2
  128. package/dist/web-components/ui/bds-alert.js +1 -1
  129. package/dist/web-components/ui/bds-avatar.d.ts +2 -1
  130. package/dist/web-components/ui/bds-avatar.js +1 -1
  131. package/dist/web-components/ui/bds-badge.js +1 -1
  132. package/dist/web-components/ui/bds-breadcrumb.d.ts +3 -1
  133. package/dist/web-components/ui/bds-breadcrumb.js +1 -1
  134. package/dist/web-components/ui/bds-button-container.d.ts +52 -0
  135. package/dist/web-components/ui/bds-button-container.js +7 -0
  136. package/dist/web-components/ui/bds-button-group.d.ts +21 -0
  137. package/dist/web-components/ui/bds-button-group.js +8 -0
  138. package/dist/web-components/ui/bds-calendar.d.ts +121 -0
  139. package/dist/web-components/ui/bds-calendar.js +7 -0
  140. package/dist/web-components/ui/bds-card.d.ts +12 -3
  141. package/dist/web-components/ui/bds-card.js +1 -1
  142. package/dist/web-components/ui/bds-carousel.d.ts +98 -0
  143. package/dist/web-components/ui/bds-carousel.js +7 -0
  144. package/dist/web-components/ui/bds-description-list.js +1 -1
  145. package/dist/web-components/ui/bds-icon-wrapper.d.ts +0 -9
  146. package/dist/web-components/ui/bds-icon-wrapper.js +1 -1
  147. package/dist/web-components/ui/bds-link.js +1 -1
  148. package/dist/web-components/ui/bds-loading.js +1 -1
  149. package/dist/web-components/ui/bds-notification-banner.js +1 -1
  150. package/dist/web-components/ui/bds-pagination.d.ts +47 -0
  151. package/dist/web-components/ui/bds-pagination.js +7 -0
  152. package/dist/web-components/ui/bds-progress-circle.d.ts +3 -1
  153. package/dist/web-components/ui/bds-progress-circle.js +1 -1
  154. package/dist/web-components/ui/bds-progress.d.ts +3 -1
  155. package/dist/web-components/ui/bds-progress.js +1 -1
  156. package/dist/web-components/ui/bds-rating.js +1 -1
  157. package/dist/web-components/ui/bds-section-header.d.ts +18 -4
  158. package/dist/web-components/ui/bds-section-header.js +1 -1
  159. package/dist/web-components/ui/bds-separator.js +1 -1
  160. package/dist/web-components/ui/bds-skeleton.js +1 -1
  161. package/dist/web-components/ui/bds-table.d.ts +52 -0
  162. package/dist/web-components/ui/bds-table.js +7 -0
  163. package/dist/web-components/ui/bds-typography.d.ts +1 -0
  164. package/dist/web-components/ui/bds-typography.js +1 -1
  165. package/package.json +60 -3
  166. package/src/components/interaction/Button/Button.module.css +23 -14
  167. package/src/components/interaction/Button/Button.spec.tsx +20 -0
  168. package/src/components/interaction/Button/Button.tsx +2 -2
  169. package/src/components/interaction/Command/Command.mdx +5 -2
  170. package/src/components/interaction/Command/Command.module.css +23 -7
  171. package/src/components/interaction/Command/Command.spec.tsx +35 -0
  172. package/src/components/interaction/Command/Command.tsx +15 -4
  173. package/src/components/interaction/Command/index.ts +1 -1
  174. package/src/components/interaction/Dialog/Dialog.mdx +4 -4
  175. package/src/components/interaction/Dialog/Dialog.module.css +20 -0
  176. package/src/components/interaction/Dialog/Dialog.spec.tsx +20 -0
  177. package/src/components/interaction/Dialog/Dialog.tsx +8 -3
  178. package/src/components/interaction/Dialog/index.ts +1 -0
  179. package/src/components/interaction/Drawer/Drawer.mdx +5 -2
  180. package/src/components/interaction/Drawer/Drawer.module.css +34 -3
  181. package/src/components/interaction/Drawer/Drawer.spec.tsx +67 -1
  182. package/src/components/interaction/Drawer/Drawer.tsx +30 -12
  183. package/src/components/interaction/Drawer/index.ts +1 -0
  184. package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +2 -0
  185. package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +34 -0
  186. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +15 -5
  187. package/src/components/interaction/DropdownMenu/index.ts +1 -0
  188. package/src/components/interaction/Popover/Popover.mdx +6 -2
  189. package/src/components/interaction/Popover/Popover.module.css +26 -18
  190. package/src/components/interaction/Popover/Popover.spec.tsx +38 -0
  191. package/src/components/interaction/Popover/Popover.tsx +19 -5
  192. package/src/components/interaction/Popover/index.ts +1 -0
  193. package/src/components/interaction/Rating/Rating.mdx +1 -1
  194. package/src/components/interaction/Rating/Rating.module.css +2 -2
  195. package/src/components/interaction/Rating/Rating.tsx +1 -1
  196. package/src/components/interaction/Rating/index.ts +1 -0
  197. package/src/components/interaction/Toast/Toast.mdx +3 -0
  198. package/src/components/interaction/Toast/Toast.module.css +35 -5
  199. package/src/components/interaction/Toast/Toast.spec.tsx +69 -1
  200. package/src/components/interaction/Toast/Toast.tsx +15 -9
  201. package/src/components/interaction/form/Checkbox/Checkbox.mdx +3 -0
  202. package/src/components/interaction/form/Checkbox/Checkbox.module.css +13 -6
  203. package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +6 -0
  204. package/src/components/interaction/form/Checkbox/Checkbox.tsx +1 -1
  205. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +16 -1
  206. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +24 -8
  207. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +32 -2
  208. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +14 -2
  209. package/src/components/interaction/form/Combobox/Combobox.mdx +11 -4
  210. package/src/components/interaction/form/Combobox/Combobox.module.css +6 -6
  211. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +47 -0
  212. package/src/components/interaction/form/Combobox/Combobox.tsx +10 -5
  213. package/src/components/interaction/form/FieldGroup/FieldGroup.mdx +9 -10
  214. package/src/components/interaction/form/FieldGroup/FieldGroup.module.css +57 -23
  215. package/src/components/interaction/form/FieldGroup/FieldGroup.spec.tsx +129 -7
  216. package/src/components/interaction/form/FieldGroup/FieldGroup.tsx +72 -12
  217. package/src/components/interaction/form/FileInput/FileInput.mdx +9 -1
  218. package/src/components/interaction/form/FileInput/FileInput.module.css +8 -8
  219. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +89 -1
  220. package/src/components/interaction/form/FileInput/FileInput.tsx +6 -2
  221. package/src/components/interaction/form/FormInput/FormInput.mdx +9 -5
  222. package/src/components/interaction/form/FormInput/FormInput.module.css +17 -8
  223. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +20 -0
  224. package/src/components/interaction/form/FormInput/FormInput.tsx +7 -2
  225. package/src/components/interaction/form/NumberInput/NumberInput.mdx +5 -1
  226. package/src/components/interaction/form/NumberInput/NumberInput.module.css +6 -6
  227. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +51 -0
  228. package/src/components/interaction/form/NumberInput/NumberInput.tsx +36 -8
  229. package/src/components/interaction/form/Radio/Radio.mdx +8 -1
  230. package/src/components/interaction/form/Radio/Radio.module.css +20 -7
  231. package/src/components/interaction/form/Radio/Radio.spec.tsx +15 -0
  232. package/src/components/interaction/form/Radio/Radio.tsx +10 -4
  233. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +16 -1
  234. package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +24 -8
  235. package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +32 -2
  236. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +14 -2
  237. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +9 -3
  238. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +25 -12
  239. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +86 -5
  240. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +44 -4
  241. package/src/components/interaction/form/Select/Select.mdx +10 -4
  242. package/src/components/interaction/form/Select/Select.module.css +4 -4
  243. package/src/components/interaction/form/Select/Select.spec.tsx +34 -0
  244. package/src/components/interaction/form/Select/Select.tsx +21 -9
  245. package/src/components/interaction/form/Slider/Slider.mdx +4 -1
  246. package/src/components/interaction/form/Slider/Slider.module.css +32 -8
  247. package/src/components/interaction/form/Slider/Slider.spec.tsx +29 -0
  248. package/src/components/interaction/form/Slider/Slider.tsx +9 -7
  249. package/src/components/interaction/form/Switch/Switch.mdx +2 -2
  250. package/src/components/interaction/form/Switch/Switch.module.css +12 -13
  251. package/src/components/interaction/form/Switch/Switch.spec.tsx +20 -0
  252. package/src/components/interaction/form/Switch/Switch.tsx +2 -1
  253. package/src/components/interaction/form/Textarea/Textarea.mdx +5 -0
  254. package/src/components/interaction/form/Textarea/Textarea.module.css +5 -5
  255. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +13 -0
  256. package/src/components/interaction/form/Textarea/Textarea.tsx +3 -1
  257. package/src/components/interaction/form/atoms/Message.module.css +1 -1
  258. package/src/components/interaction/form/atoms/Message.spec.tsx +29 -0
  259. package/src/components/interaction/form/atoms/Message.tsx +1 -1
  260. package/src/components/layout/ButtonGroup/ButtonGroup.mdx +11 -0
  261. package/src/components/layout/ButtonGroup/ButtonGroup.module.css +1 -5
  262. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +14 -0
  263. package/src/components/layout/Card/Card.mdx +6 -1
  264. package/src/components/layout/Card/Card.module.css +26 -5
  265. package/src/components/layout/Card/Card.spec.tsx +7 -0
  266. package/src/components/layout/Card/Card.tsx +4 -3
  267. package/src/components/layout/Grid/Grid.spec.tsx +44 -0
  268. package/src/components/layout/Grid/Grid.tsx +5 -5
  269. package/src/components/layout/Grid/GridItem.tsx +6 -6
  270. package/src/components/layout/Grid/masonry.ts +31 -5
  271. package/src/components/layout/Grid/useIsomorphicLayoutEffect.ts +11 -0
  272. package/src/components/layout/IconWrapper/IconWrapper.mdx +8 -2
  273. package/src/components/layout/IconWrapper/IconWrapper.module.css +9 -6
  274. package/src/components/layout/SectionHeader/SectionHeader.mdx +4 -1
  275. package/src/components/layout/SectionHeader/SectionHeader.module.css +10 -9
  276. package/src/components/ui/Accordion/Accordion.mdx +4 -1
  277. package/src/components/ui/Accordion/Accordion.module.css +2 -1
  278. package/src/components/ui/Alert/Alert.mdx +21 -4
  279. package/src/components/ui/Alert/Alert.module.css +9 -4
  280. package/src/components/ui/Alert/Alert.spec.tsx +20 -0
  281. package/src/components/ui/Avatar/Avatar.mdx +8 -3
  282. package/src/components/ui/Avatar/Avatar.module.css +13 -8
  283. package/src/components/ui/Badge/Badge.mdx +3 -1
  284. package/src/components/ui/Breadcrumb/Breadcrumb.mdx +3 -0
  285. package/src/components/ui/Breadcrumb/Breadcrumb.module.css +4 -0
  286. package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +12 -0
  287. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +11 -3
  288. package/src/components/ui/Calendar/Calendar.mdx +6 -0
  289. package/src/components/ui/Calendar/Calendar.module.css +68 -19
  290. package/src/components/ui/Calendar/Calendar.spec.tsx +139 -24
  291. package/src/components/ui/Calendar/Calendar.stories.tsx +14 -0
  292. package/src/components/ui/Calendar/Calendar.tsx +143 -154
  293. package/src/components/ui/Calendar/CalendarRange.mdx +39 -0
  294. package/src/components/ui/Calendar/CalendarRange.spec.tsx +119 -0
  295. package/src/components/ui/Calendar/CalendarRange.stories.tsx +51 -0
  296. package/src/components/ui/Calendar/CalendarRange.tsx +213 -0
  297. package/src/components/ui/Calendar/MonthGrid.tsx +141 -0
  298. package/src/components/ui/Calendar/calendar-core.spec.ts +176 -0
  299. package/src/components/ui/Calendar/calendar-core.ts +164 -0
  300. package/src/components/ui/Calendar/index.ts +1 -0
  301. package/src/components/ui/Carousel/Carousel.mdx +8 -3
  302. package/src/components/ui/Carousel/Carousel.module.css +7 -5
  303. package/src/components/ui/Carousel/Carousel.spec.tsx +67 -0
  304. package/src/components/ui/Carousel/Carousel.tsx +19 -7
  305. package/src/components/ui/Collapsible/Collapsible.mdx +17 -6
  306. package/src/components/ui/Collapsible/Collapsible.module.css +7 -2
  307. package/src/components/ui/Collapsible/Collapsible.spec.tsx +40 -0
  308. package/src/components/ui/Collapsible/Collapsible.tsx +19 -2
  309. package/src/components/ui/DescriptionList/DescriptionList.mdx +2 -0
  310. package/src/components/ui/Link/Link.mdx +4 -2
  311. package/src/components/ui/Loading/Loading.mdx +6 -3
  312. package/src/components/ui/Loading/Loading.module.css +7 -7
  313. package/src/components/ui/Loading/Loading.spec.tsx +12 -0
  314. package/src/components/ui/Loading/Loading.tsx +8 -3
  315. package/src/components/ui/NotificationBanner/NotificationBanner.mdx +4 -0
  316. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +5 -0
  317. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +20 -0
  318. package/src/components/ui/Pagination/Pagination.mdx +29 -7
  319. package/src/components/ui/Pagination/Pagination.module.css +15 -14
  320. package/src/components/ui/Progress/Progress.mdx +6 -2
  321. package/src/components/ui/Progress/Progress.module.css +1 -1
  322. package/src/components/ui/Progress/Progress.spec.tsx +17 -0
  323. package/src/components/ui/Progress/Progress.tsx +2 -2
  324. package/src/components/ui/ProgressCircle/ProgressCircle.mdx +8 -4
  325. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +2 -2
  326. package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +11 -1
  327. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +1 -1
  328. package/src/components/ui/Separator/Separator.mdx +7 -0
  329. package/src/components/ui/Separator/Separator.module.css +1 -0
  330. package/src/components/ui/Skeleton/Skeleton.mdx +17 -0
  331. package/src/components/ui/Skeleton/Skeleton.module.css +12 -5
  332. package/src/components/ui/SkipLink/SkipLink.mdx +19 -0
  333. package/src/components/ui/SkipLink/SkipLink.module.css +8 -7
  334. package/src/components/ui/SkipLink/SkipLink.spec.tsx +28 -0
  335. package/src/components/ui/Table/Table.mdx +14 -0
  336. package/src/components/ui/Table/Table.module.css +13 -8
  337. package/src/components/ui/Table/Table.spec.tsx +56 -0
  338. package/src/components/ui/Table/Table.tsx +44 -6
  339. package/src/components/ui/Tabs/Tabs.mdx +16 -0
  340. package/src/components/ui/Tabs/Tabs.module.css +3 -3
  341. package/src/components/ui/Tabs/Tabs.spec.tsx +20 -0
  342. package/src/components/ui/Tabs/Tabs.tsx +5 -1
  343. package/src/components/ui/Tooltip/Tooltip.mdx +9 -0
  344. package/src/components/ui/Tooltip/Tooltip.module.css +32 -12
  345. package/src/components/ui/Tooltip/Tooltip.spec.tsx +56 -0
  346. package/src/components/ui/Tooltip/Tooltip.tsx +26 -7
  347. package/src/components/ui/Typography/Typography.mdx +7 -0
  348. package/src/components/ui/Typography/Typography.module.css +5 -5
  349. package/src/index.ts +7 -3
  350. package/src/stories/DesignSystem/Borders.mdx +14 -9
  351. package/src/stories/DesignSystem/Colors.mdx +43 -41
  352. package/src/stories/DesignSystem/Overview.mdx +4 -3
  353. package/src/stories/Introduction.mdx +5 -5
  354. package/src/web-components/globals.ts +7 -0
  355. package/src/web-components/index.ts +2 -0
  356. package/src/web-components/interaction/BdsAccordion.mdx +10 -1
  357. package/src/web-components/interaction/BdsButton.mdx +15 -4
  358. package/src/web-components/interaction/BdsButton.stories.tsx +1 -1
  359. package/src/web-components/interaction/BdsCollapsible.mdx +3 -3
  360. package/src/web-components/interaction/BdsDialog.mdx +12 -7
  361. package/src/web-components/interaction/BdsDialog.stories.tsx +2 -2
  362. package/src/web-components/interaction/BdsDrawer.mdx +9 -6
  363. package/src/web-components/interaction/BdsDropdownMenu.mdx +20 -1
  364. package/src/web-components/interaction/BdsPopover.mdx +7 -3
  365. package/src/web-components/interaction/BdsSkipLink.mdx +14 -1
  366. package/src/web-components/interaction/BdsTabs.mdx +18 -1
  367. package/src/web-components/interaction/BdsToastProvider.mdx +21 -3
  368. package/src/web-components/interaction/BdsTooltip.mdx +4 -4
  369. package/src/web-components/interaction/bds-accordion-item.ts +10 -0
  370. package/src/web-components/interaction/bds-accordion.spec.ts +41 -0
  371. package/src/web-components/interaction/bds-accordion.ts +28 -2
  372. package/src/web-components/interaction/bds-button.spec.ts +81 -0
  373. package/src/web-components/interaction/bds-button.ts +60 -13
  374. package/src/web-components/interaction/bds-collapsible.spec.ts +93 -2
  375. package/src/web-components/interaction/bds-collapsible.ts +49 -7
  376. package/src/web-components/interaction/bds-dialog.spec.ts +49 -0
  377. package/src/web-components/interaction/bds-dialog.ts +68 -11
  378. package/src/web-components/interaction/bds-drawer.spec.ts +93 -4
  379. package/src/web-components/interaction/bds-drawer.ts +54 -21
  380. package/src/web-components/interaction/bds-dropdown-menu-item.ts +12 -1
  381. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +63 -0
  382. package/src/web-components/interaction/bds-dropdown-menu.ts +45 -7
  383. package/src/web-components/interaction/bds-popover.spec.ts +80 -6
  384. package/src/web-components/interaction/bds-popover.ts +83 -32
  385. package/src/web-components/interaction/bds-skip-link.spec.ts +18 -0
  386. package/src/web-components/interaction/bds-skip-link.ts +29 -8
  387. package/src/web-components/interaction/bds-tab-panel.ts +10 -0
  388. package/src/web-components/interaction/bds-tab.spec.ts +25 -0
  389. package/src/web-components/interaction/bds-tab.ts +20 -5
  390. package/src/web-components/interaction/bds-tabs.spec.ts +92 -5
  391. package/src/web-components/interaction/bds-tabs.ts +42 -1
  392. package/src/web-components/interaction/bds-toast-provider.spec.ts +43 -0
  393. package/src/web-components/interaction/bds-toast-provider.ts +50 -21
  394. package/src/web-components/interaction/bds-tooltip.spec.ts +71 -0
  395. package/src/web-components/interaction/bds-tooltip.ts +100 -5
  396. package/src/web-components/interaction/form/BdsCheckbox.mdx +20 -1
  397. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +8 -0
  398. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +4 -3
  399. package/src/web-components/interaction/form/BdsCombobox.mdx +39 -2
  400. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +31 -0
  401. package/src/web-components/interaction/form/BdsFieldGroup.mdx +8 -3
  402. package/src/web-components/interaction/form/BdsFieldGroup.stories.tsx +1 -1
  403. package/src/web-components/interaction/form/BdsFileInput.mdx +26 -5
  404. package/src/web-components/interaction/form/BdsFormInput.mdx +16 -1
  405. package/src/web-components/interaction/form/BdsNumberInput.mdx +34 -2
  406. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +27 -3
  407. package/src/web-components/interaction/form/BdsRadio.mdx +39 -12
  408. package/src/web-components/interaction/form/BdsRadio.stories.tsx +26 -0
  409. package/src/web-components/interaction/form/BdsRadioGroup.mdx +9 -4
  410. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +37 -11
  411. package/src/web-components/interaction/form/BdsSelect.mdx +22 -4
  412. package/src/web-components/interaction/form/BdsSelect.stories.tsx +33 -0
  413. package/src/web-components/interaction/form/BdsSlider.mdx +36 -19
  414. package/src/web-components/interaction/form/BdsSlider.stories.tsx +24 -0
  415. package/src/web-components/interaction/form/BdsSwitch.mdx +38 -11
  416. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +16 -0
  417. package/src/web-components/interaction/form/BdsTextarea.mdx +42 -16
  418. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +21 -0
  419. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +99 -0
  420. package/src/web-components/interaction/form/bds-checkbox-group.ts +95 -9
  421. package/src/web-components/interaction/form/bds-checkbox.spec.ts +55 -0
  422. package/src/web-components/interaction/form/bds-checkbox.ts +30 -5
  423. package/src/web-components/interaction/form/bds-combobox.spec.ts +79 -0
  424. package/src/web-components/interaction/form/bds-combobox.ts +102 -17
  425. package/src/web-components/interaction/form/bds-field-group.spec.ts +158 -0
  426. package/src/web-components/interaction/form/bds-field-group.ts +167 -23
  427. package/src/web-components/interaction/form/bds-file-input.spec.ts +73 -5
  428. package/src/web-components/interaction/form/bds-file-input.ts +71 -34
  429. package/src/web-components/interaction/form/bds-form-input.spec.ts +22 -0
  430. package/src/web-components/interaction/form/bds-form-input.ts +27 -16
  431. package/src/web-components/interaction/form/bds-number-input.spec.ts +49 -0
  432. package/src/web-components/interaction/form/bds-number-input.ts +118 -29
  433. package/src/web-components/interaction/form/bds-radio-group.spec.ts +57 -0
  434. package/src/web-components/interaction/form/bds-radio-group.ts +60 -12
  435. package/src/web-components/interaction/form/bds-radio.spec.ts +123 -0
  436. package/src/web-components/interaction/form/bds-radio.ts +136 -23
  437. package/src/web-components/interaction/form/bds-segmented-control.spec.ts +55 -0
  438. package/src/web-components/interaction/form/bds-segmented-control.ts +81 -59
  439. package/src/web-components/interaction/form/bds-select.spec.ts +57 -0
  440. package/src/web-components/interaction/form/bds-select.ts +96 -8
  441. package/src/web-components/interaction/form/bds-slider.spec.ts +83 -0
  442. package/src/web-components/interaction/form/bds-slider.ts +181 -33
  443. package/src/web-components/interaction/form/bds-switch.spec.ts +68 -0
  444. package/src/web-components/interaction/form/bds-switch.ts +142 -41
  445. package/src/web-components/interaction/form/bds-textarea.spec.ts +71 -0
  446. package/src/web-components/interaction/form/bds-textarea.ts +109 -8
  447. package/src/web-components/layout/BdsGrid.mdx +6 -2
  448. package/src/web-components/layout/BdsGrid.stories.tsx +4 -0
  449. package/src/web-components/layout/bds-grid-item.spec.ts +21 -0
  450. package/src/web-components/layout/bds-grid-item.ts +17 -3
  451. package/src/web-components/layout/bds-grid.spec.ts +98 -0
  452. package/src/web-components/layout/bds-grid.ts +139 -19
  453. package/src/web-components/ui/BdsAlert.mdx +6 -5
  454. package/src/web-components/ui/BdsAlert.stories.tsx +6 -6
  455. package/src/web-components/ui/BdsAvatar.mdx +4 -2
  456. package/src/web-components/ui/BdsBreadcrumb.mdx +3 -2
  457. package/src/web-components/ui/BdsButtonContainer.mdx +64 -0
  458. package/src/web-components/ui/BdsButtonContainer.stories.tsx +84 -0
  459. package/src/web-components/ui/BdsCalendar.mdx +29 -4
  460. package/src/web-components/ui/BdsCalendar.stories.tsx +16 -18
  461. package/src/web-components/ui/BdsCalendarRange.mdx +71 -0
  462. package/src/web-components/ui/BdsCalendarRange.stories.tsx +31 -0
  463. package/src/web-components/ui/BdsCard.mdx +7 -3
  464. package/src/web-components/ui/BdsCard.stories.tsx +17 -1
  465. package/src/web-components/ui/BdsCarousel.mdx +40 -12
  466. package/src/web-components/ui/BdsDescriptionList.mdx +6 -1
  467. package/src/web-components/ui/BdsIconWrapper.mdx +3 -2
  468. package/src/web-components/ui/BdsLink.mdx +14 -2
  469. package/src/web-components/ui/BdsNotificationBanner.mdx +18 -0
  470. package/src/web-components/ui/BdsPagination.mdx +40 -1
  471. package/src/web-components/ui/BdsProgress.mdx +8 -2
  472. package/src/web-components/ui/BdsProgressCircle.mdx +5 -2
  473. package/src/web-components/ui/BdsRating.mdx +7 -0
  474. package/src/web-components/ui/BdsSectionHeader.mdx +22 -3
  475. package/src/web-components/ui/BdsSectionHeader.stories.tsx +15 -13
  476. package/src/web-components/ui/BdsTable.mdx +23 -2
  477. package/src/web-components/ui/BdsTable.stories.tsx +8 -2
  478. package/src/web-components/ui/BdsTypography.mdx +17 -1
  479. package/src/web-components/ui/bds-alert.spec.ts +17 -2
  480. package/src/web-components/ui/bds-alert.ts +31 -5
  481. package/src/web-components/ui/bds-avatar.spec.ts +29 -0
  482. package/src/web-components/ui/bds-avatar.ts +30 -12
  483. package/src/web-components/ui/bds-badge.spec.ts +5 -0
  484. package/src/web-components/ui/bds-badge.ts +11 -1
  485. package/src/web-components/ui/bds-breadcrumb.spec.ts +30 -0
  486. package/src/web-components/ui/bds-breadcrumb.ts +26 -4
  487. package/src/web-components/ui/bds-button-container.spec.ts +39 -0
  488. package/src/web-components/ui/bds-button-container.ts +123 -0
  489. package/src/web-components/ui/bds-button-group.spec.ts +14 -20
  490. package/src/web-components/ui/bds-button-group.ts +13 -64
  491. package/src/web-components/ui/bds-calendar-range.spec.ts +123 -0
  492. package/src/web-components/ui/bds-calendar-range.ts +307 -0
  493. package/src/web-components/ui/bds-calendar.spec.ts +176 -0
  494. package/src/web-components/ui/bds-calendar.ts +186 -344
  495. package/src/web-components/ui/bds-card.spec.ts +32 -0
  496. package/src/web-components/ui/bds-card.ts +34 -9
  497. package/src/web-components/ui/bds-carousel.spec.ts +147 -0
  498. package/src/web-components/ui/bds-carousel.ts +113 -36
  499. package/src/web-components/ui/bds-description-list.spec.ts +15 -0
  500. package/src/web-components/ui/bds-description-list.ts +37 -3
  501. package/src/web-components/ui/bds-icon-wrapper.spec.ts +12 -0
  502. package/src/web-components/ui/bds-icon-wrapper.ts +16 -15
  503. package/src/web-components/ui/bds-link.spec.ts +17 -0
  504. package/src/web-components/ui/bds-link.ts +26 -11
  505. package/src/web-components/ui/bds-loading.ts +11 -1
  506. package/src/web-components/ui/bds-notification-banner.spec.ts +13 -0
  507. package/src/web-components/ui/bds-notification-banner.ts +18 -1
  508. package/src/web-components/ui/bds-pagination.spec.ts +44 -0
  509. package/src/web-components/ui/bds-pagination.ts +39 -19
  510. package/src/web-components/ui/bds-progress-circle.spec.ts +21 -0
  511. package/src/web-components/ui/bds-progress-circle.ts +17 -5
  512. package/src/web-components/ui/bds-progress.spec.ts +21 -0
  513. package/src/web-components/ui/bds-progress.ts +19 -6
  514. package/src/web-components/ui/bds-rating.spec.ts +6 -0
  515. package/src/web-components/ui/bds-rating.ts +12 -2
  516. package/src/web-components/ui/bds-section-header.spec.ts +39 -8
  517. package/src/web-components/ui/bds-section-header.ts +47 -13
  518. package/src/web-components/ui/bds-separator.ts +10 -0
  519. package/src/web-components/ui/bds-skeleton.ts +10 -0
  520. package/src/web-components/ui/bds-table.spec.ts +63 -0
  521. package/src/web-components/ui/bds-table.ts +88 -28
  522. package/src/web-components/ui/bds-typography.spec.ts +47 -0
  523. package/src/web-components/ui/bds-typography.ts +28 -10
  524. package/src/web-components/ui/calendar-styles.ts +94 -0
  525. package/dist/web-components/chunk-5TFWOPLH.js +0 -49
  526. package/dist/web-components/chunk-7FMR53BW.js +0 -127
  527. package/dist/web-components/chunk-AGKJTMJI.js +0 -56
  528. package/dist/web-components/chunk-BQLZ7I4G.js +0 -56
  529. package/dist/web-components/chunk-IYGF6CD5.js +0 -118
  530. package/dist/web-components/chunk-QBB6ZX4R.js +0 -107
  531. package/dist/web-components/chunk-Y3POAKGS.js +0 -133
  532. package/dist/web-components/chunk-Y4MAXTMA.js +0 -80
  533. package/dist/web-components/chunk-Y7V7LM6R.js +0 -98
  534. package/src/web-components/ui/BdsButtonGroup.mdx +0 -69
  535. package/src/web-components/ui/BdsButtonGroup.stories.tsx +0 -66
@@ -0,0 +1,176 @@
1
+ import {
2
+ toISO, parseISO, addDays, addMonths, isSameDay, compareDay, clampToRange,
3
+ sortRange, isInRange, isDisabled, buildMonthGrid,
4
+ getMonthLabel, getDayLabel, getWeekdayNames, nextFocusDate,
5
+ } from './calendar-core';
6
+
7
+ describe('calendar-core', () => {
8
+ describe('toISO / parseISO', () => {
9
+ it('formats a date as YYYY-MM-DD in local time', () => {
10
+ expect(toISO(new Date(2024, 0, 5))).toBe('2024-01-05');
11
+ expect(toISO(new Date(2024, 11, 31))).toBe('2024-12-31');
12
+ });
13
+ it('round-trips through parseISO', () => {
14
+ const d = parseISO('2024-03-15')!;
15
+ expect(d.getFullYear()).toBe(2024);
16
+ expect(d.getMonth()).toBe(2);
17
+ expect(d.getDate()).toBe(15);
18
+ });
19
+ it('returns null for invalid input', () => {
20
+ expect(parseISO('')).toBeNull();
21
+ expect(parseISO('nope')).toBeNull();
22
+ expect(parseISO('2024-13-40')).toBeNull();
23
+ });
24
+ it('parses years below 100 without the Date two-digit-year remap', () => {
25
+ const d = parseISO('0099-01-15')!;
26
+ expect(d.getFullYear()).toBe(99);
27
+ expect(d.getMonth()).toBe(0);
28
+ expect(d.getDate()).toBe(15);
29
+ });
30
+ });
31
+
32
+ describe('addDays', () => {
33
+ it('crosses month and year boundaries', () => {
34
+ expect(toISO(addDays(new Date(2024, 0, 31), 1))).toBe('2024-02-01');
35
+ expect(toISO(addDays(new Date(2024, 11, 31), 1))).toBe('2025-01-01');
36
+ expect(toISO(addDays(new Date(2024, 0, 1), -1))).toBe('2023-12-31');
37
+ });
38
+ });
39
+
40
+ describe('addMonths', () => {
41
+ it('clamps the day to the target month length', () => {
42
+ expect(toISO(addMonths(new Date(2024, 0, 31), 1))).toBe('2024-02-29'); // leap Feb
43
+ expect(toISO(addMonths(new Date(2023, 2, 31), -1))).toBe('2023-02-28'); // non-leap Feb
44
+ expect(toISO(addMonths(new Date(2024, 0, 15), 13))).toBe('2025-02-15'); // spans >12 months
45
+ expect(toISO(addMonths(new Date(2024, 0, 15), -1))).toBe('2023-12-15'); // crosses year back
46
+ });
47
+ });
48
+
49
+ describe('isSameDay / compareDay', () => {
50
+ it('compares by calendar day only', () => {
51
+ expect(isSameDay(new Date(2024, 0, 1, 9), new Date(2024, 0, 1, 23))).toBe(true);
52
+ expect(compareDay(new Date(2024, 0, 1), new Date(2024, 0, 2))).toBe(-1);
53
+ expect(compareDay(new Date(2024, 0, 2), new Date(2024, 0, 1))).toBe(1);
54
+ expect(compareDay(new Date(2024, 0, 1, 8), new Date(2024, 0, 1, 20))).toBe(0);
55
+ });
56
+ });
57
+
58
+ describe('clampToRange', () => {
59
+ it('clamps below min and above max, passes through in-range', () => {
60
+ const min = new Date(2024, 0, 10);
61
+ const max = new Date(2024, 0, 20);
62
+ expect(toISO(clampToRange(new Date(2024, 0, 5), min, max))).toBe('2024-01-10');
63
+ expect(toISO(clampToRange(new Date(2024, 0, 25), min, max))).toBe('2024-01-20');
64
+ expect(toISO(clampToRange(new Date(2024, 0, 15), min, max))).toBe('2024-01-15');
65
+ });
66
+ it('always returns a fresh, midnight-normalized Date, even when in range', () => {
67
+ const input = new Date(2024, 0, 15, 9, 30);
68
+ const out = clampToRange(input, new Date(2024, 0, 10), new Date(2024, 0, 20));
69
+ expect(out).not.toBe(input);
70
+ expect(out.getHours()).toBe(0);
71
+ expect(toISO(out)).toBe('2024-01-15');
72
+ });
73
+ });
74
+
75
+ describe('sortRange / isInRange', () => {
76
+ it('sorts earliest first regardless of argument order', () => {
77
+ const [a, b] = sortRange(new Date(2024, 0, 20), new Date(2024, 0, 10));
78
+ expect(toISO(a)).toBe('2024-01-10');
79
+ expect(toISO(b)).toBe('2024-01-20');
80
+ });
81
+ it('is inclusive and order-independent', () => {
82
+ const s = new Date(2024, 0, 10);
83
+ const e = new Date(2024, 0, 20);
84
+ expect(isInRange(new Date(2024, 0, 10), s, e)).toBe(true);
85
+ expect(isInRange(new Date(2024, 0, 20), e, s)).toBe(true);
86
+ expect(isInRange(new Date(2024, 0, 15), s, e)).toBe(true);
87
+ expect(isInRange(new Date(2024, 0, 9), s, e)).toBe(false);
88
+ expect(isInRange(new Date(2024, 0, 21), s, e)).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('isDisabled', () => {
93
+ it('respects min, max, and the predicate', () => {
94
+ const min = new Date(2024, 0, 10);
95
+ const max = new Date(2024, 0, 20);
96
+ expect(isDisabled(new Date(2024, 0, 9), { min, max })).toBe(true);
97
+ expect(isDisabled(new Date(2024, 0, 21), { min, max })).toBe(true);
98
+ expect(isDisabled(new Date(2024, 0, 15), { min, max })).toBe(false);
99
+ const weekends = (d: Date) => d.getDay() === 0 || d.getDay() === 6;
100
+ expect(isDisabled(new Date(2024, 0, 13), { isDateDisallowed: weekends })).toBe(true); // Sat
101
+ expect(isDisabled(new Date(2024, 0, 15), { isDateDisallowed: weekends })).toBe(false); // Mon
102
+ });
103
+ });
104
+
105
+ describe('buildMonthGrid', () => {
106
+ it('always returns 42 cells', () => {
107
+ expect(buildMonthGrid(2024, 1, 1)).toHaveLength(42);
108
+ });
109
+ it('Monday-first January 2024 starts on the 1st (Jan 1 2024 is a Monday)', () => {
110
+ const cells = buildMonthGrid(2024, 0, 1);
111
+ expect(cells[0].iso).toBe('2024-01-01');
112
+ expect(cells[0].outside).toBe(false);
113
+ });
114
+ it('Sunday-first January 2024 leads with Dec 31 2023 (outside)', () => {
115
+ const cells = buildMonthGrid(2024, 0, 0);
116
+ expect(cells[0].iso).toBe('2023-12-31');
117
+ expect(cells[0].outside).toBe(true);
118
+ expect(cells[1].iso).toBe('2024-01-01');
119
+ expect(cells[1].outside).toBe(false);
120
+ });
121
+ it('marks in-month day count correctly for a leap February', () => {
122
+ const cells = buildMonthGrid(2024, 1, 1); // Feb 2024 = 29 days
123
+ expect(cells.filter(c => !c.outside)).toHaveLength(29);
124
+ });
125
+ });
126
+
127
+ describe('Intl helpers', () => {
128
+ it('getMonthLabel formats month + year (en)', () => {
129
+ expect(getMonthLabel(2024, 0, 'en-US')).toBe('January 2024');
130
+ });
131
+ it('getDayLabel formats a long date (en)', () => {
132
+ expect(getDayLabel(new Date(2024, 0, 15), 'en-US')).toBe('January 15, 2024');
133
+ });
134
+ it('getWeekdayNames returns 7 names starting from firstDayOfWeek (en, Monday)', () => {
135
+ const names = getWeekdayNames('en-US', 1);
136
+ expect(names).toHaveLength(7);
137
+ expect(names[0]).toBe('Mon');
138
+ expect(names[6]).toBe('Sun');
139
+ });
140
+ it('getWeekdayNames starts on Sunday when firstDayOfWeek=0', () => {
141
+ expect(getWeekdayNames('en-US', 0)[0]).toBe('Sun');
142
+ });
143
+ });
144
+
145
+ describe('nextFocusDate', () => {
146
+ const opts = { firstDayOfWeek: 1 };
147
+ it('moves by day and week with arrows', () => {
148
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'ArrowRight', opts)!)).toBe('2024-01-16');
149
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'ArrowLeft', opts)!)).toBe('2024-01-14');
150
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'ArrowDown', opts)!)).toBe('2024-01-22');
151
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'ArrowUp', opts)!)).toBe('2024-01-08');
152
+ });
153
+ it('moves to week bounds with Home/End honoring firstDayOfWeek', () => {
154
+ // 2024-01-17 is a Wednesday; Monday-first week → Home = Mon Jan 15, End = Sun Jan 21
155
+ expect(toISO(nextFocusDate(new Date(2024, 0, 17), 'Home', opts)!)).toBe('2024-01-15');
156
+ expect(toISO(nextFocusDate(new Date(2024, 0, 17), 'End', opts)!)).toBe('2024-01-21');
157
+ // Sunday-first week → Home = Sun Jan 14, End = Sat Jan 20
158
+ expect(toISO(nextFocusDate(new Date(2024, 0, 17), 'Home', { firstDayOfWeek: 0 })!)).toBe('2024-01-14');
159
+ });
160
+ it('pages by month and, with shift, by year', () => {
161
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'PageUp', opts)!)).toBe('2023-12-15');
162
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'PageDown', opts)!)).toBe('2024-02-15');
163
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'PageUp', { ...opts, shiftKey: true })!)).toBe('2023-01-15');
164
+ expect(toISO(nextFocusDate(new Date(2024, 0, 15), 'PageDown', { ...opts, shiftKey: true })!)).toBe('2025-01-15');
165
+ });
166
+ it('clamps the day when paging into a shorter month or year', () => {
167
+ expect(toISO(nextFocusDate(new Date(2024, 0, 31), 'PageDown', opts)!)).toBe('2024-02-29');
168
+ expect(toISO(nextFocusDate(new Date(2023, 2, 31), 'PageUp', opts)!)).toBe('2023-02-28');
169
+ expect(toISO(nextFocusDate(new Date(2024, 1, 29), 'PageUp', { ...opts, shiftKey: true })!)).toBe('2023-02-28');
170
+ });
171
+ it('returns null for non-navigation keys', () => {
172
+ expect(nextFocusDate(new Date(2024, 0, 15), 'Enter', opts)).toBeNull();
173
+ expect(nextFocusDate(new Date(2024, 0, 15), 'a', opts)).toBeNull();
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Framework-agnostic calendar logic shared by the React <Calendar>/<CalendarRange>
3
+ * components and the <bds-calendar>/<bds-calendar-range> web components.
4
+ * Pure functions only — no React, no Lit, no DOM.
5
+ */
6
+
7
+ export type ISODate = string; // 'YYYY-MM-DD'
8
+
9
+ /** Format a Date to ISO 8601 `YYYY-MM-DD` in local time. */
10
+ export function toISO(date: Date): ISODate {
11
+ const y = date.getFullYear();
12
+ const m = String(date.getMonth() + 1).padStart(2, '0');
13
+ const d = String(date.getDate()).padStart(2, '0');
14
+ return `${y}-${m}-${d}`;
15
+ }
16
+
17
+ /** Parse `YYYY-MM-DD` to a local-time Date at midnight, or null if invalid. */
18
+ export function parseISO(iso: string): Date | null {
19
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
20
+ if (!m) return null;
21
+ const year = Number(m[1]);
22
+ const month = Number(m[2]) - 1;
23
+ const day = Number(m[3]);
24
+ const date = new Date(year, month, day);
25
+ // The Date constructor remaps years 0–99 to 1900–1999; force the literal year.
26
+ date.setFullYear(year);
27
+ if (isNaN(date.getTime())) return null;
28
+ // Reject overflow (e.g. 2024-13-40 rolling into another month/day).
29
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
30
+ return date;
31
+ }
32
+
33
+ /** A new midnight Date `n` days after `date` (n may be negative). */
34
+ export function addDays(date: Date, n: number): Date {
35
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
36
+ }
37
+
38
+ /**
39
+ * A new midnight Date `n` months after `date` (n may be negative), with the
40
+ * day-of-month clamped to the target month's length so e.g. Jan 31 + 1 month
41
+ * lands on Feb 28/29, never rolling over into March.
42
+ */
43
+ export function addMonths(date: Date, n: number): Date {
44
+ const firstOfTarget = new Date(date.getFullYear(), date.getMonth() + n, 1);
45
+ const daysInTarget = new Date(firstOfTarget.getFullYear(), firstOfTarget.getMonth() + 1, 0).getDate();
46
+ return new Date(firstOfTarget.getFullYear(), firstOfTarget.getMonth(), Math.min(date.getDate(), daysInTarget));
47
+ }
48
+
49
+ export function isSameDay(a: Date, b: Date): boolean {
50
+ return a.getFullYear() === b.getFullYear()
51
+ && a.getMonth() === b.getMonth()
52
+ && a.getDate() === b.getDate();
53
+ }
54
+
55
+ /** -1 if a is before b, 1 if after, 0 if same calendar day. */
56
+ export function compareDay(a: Date, b: Date): number {
57
+ const am = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime();
58
+ const bm = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime();
59
+ return am < bm ? -1 : am > bm ? 1 : 0;
60
+ }
61
+
62
+ export function clampToRange(date: Date, min?: Date, max?: Date): Date {
63
+ if (min && compareDay(date, min) < 0) return new Date(min.getFullYear(), min.getMonth(), min.getDate());
64
+ if (max && compareDay(date, max) > 0) return new Date(max.getFullYear(), max.getMonth(), max.getDate());
65
+ // Always return a fresh midnight Date so the result is consistent whether or not
66
+ // clamping occurred (never leaks the caller's reference or its time component).
67
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
68
+ }
69
+
70
+ /** Earliest date first. */
71
+ export function sortRange(a: Date, b: Date): [Date, Date] {
72
+ return compareDay(a, b) <= 0 ? [a, b] : [b, a];
73
+ }
74
+
75
+ /** Inclusive, order-independent membership test. */
76
+ export function isInRange(date: Date, start: Date, end: Date): boolean {
77
+ const [lo, hi] = sortRange(start, end);
78
+ return compareDay(date, lo) >= 0 && compareDay(date, hi) <= 0;
79
+ }
80
+
81
+ export interface DisabledOpts {
82
+ min?: Date;
83
+ max?: Date;
84
+ isDateDisallowed?: (date: Date) => boolean;
85
+ }
86
+
87
+ export function isDisabled(date: Date, opts: DisabledOpts): boolean {
88
+ if (opts.min && compareDay(date, opts.min) < 0) return true;
89
+ if (opts.max && compareDay(date, opts.max) > 0) return true;
90
+ return !!opts.isDateDisallowed?.(date);
91
+ }
92
+
93
+ export interface DayCell {
94
+ iso: ISODate;
95
+ date: Date;
96
+ day: number; // 1–31
97
+ outside: boolean; // belongs to the previous/next month
98
+ }
99
+
100
+ /**
101
+ * Build a stable 6-row × 7-column month grid (42 cells) beginning on
102
+ * `firstDayOfWeek` (0=Sun … 6=Sat). Leading/trailing days from adjacent months
103
+ * are flagged `outside`. Always 42 cells so multi-month rows align and the grid
104
+ * height never jumps between months.
105
+ */
106
+ export function buildMonthGrid(year: number, month: number, firstDayOfWeek: number): DayCell[] {
107
+ const firstOfMonth = new Date(year, month, 1);
108
+ const lead = (firstOfMonth.getDay() - firstDayOfWeek + 7) % 7;
109
+ const start = addDays(firstOfMonth, -lead);
110
+ return Array.from({ length: 42 }, (_, i) => {
111
+ const date = addDays(start, i);
112
+ return { iso: toISO(date), date, day: date.getDate(), outside: date.getMonth() !== month };
113
+ });
114
+ }
115
+
116
+ /** Localized month + year heading, e.g. "January 2024". */
117
+ export function getMonthLabel(year: number, month: number, locale?: string): string {
118
+ return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' })
119
+ .format(new Date(year, month, 1));
120
+ }
121
+
122
+ /** Localized long date for a day cell's accessible label, e.g. "January 15, 2024". */
123
+ export function getDayLabel(date: Date, locale?: string): string {
124
+ return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' })
125
+ .format(date);
126
+ }
127
+
128
+ /**
129
+ * Seven localized short weekday names ordered from `firstDayOfWeek`.
130
+ * e.g. firstDayOfWeek=1 → ['Mon','Tue',…,'Sun']. 2023-01-01 is a Sunday.
131
+ */
132
+ export function getWeekdayNames(locale: string | undefined, firstDayOfWeek: number): string[] {
133
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: 'short' });
134
+ return Array.from({ length: 7 }, (_, i) =>
135
+ fmt.format(new Date(2023, 0, 1 + ((firstDayOfWeek + i) % 7))));
136
+ }
137
+
138
+ export interface FocusKeyOpts {
139
+ shiftKey?: boolean;
140
+ firstDayOfWeek: number;
141
+ }
142
+
143
+ /**
144
+ * WAI-ARIA date-grid keyboard model: given the focused date and a keydown key,
145
+ * return the date focus should move to, or null if the key is not a navigation
146
+ * key. Arrow keys move by day/week; Home/End snap to the week bounds (honoring
147
+ * firstDayOfWeek); PageUp/PageDown move by month; Shift+PageUp/PageDown by year.
148
+ * Shared by both React components and both web components.
149
+ */
150
+ export function nextFocusDate(current: Date, key: string, opts: FocusKeyOpts): Date | null {
151
+ const offsetInWeek = (current.getDay() - opts.firstDayOfWeek + 7) % 7;
152
+ switch (key) {
153
+ case 'ArrowRight': return addDays(current, 1);
154
+ case 'ArrowLeft': return addDays(current, -1);
155
+ case 'ArrowDown': return addDays(current, 7);
156
+ case 'ArrowUp': return addDays(current, -7);
157
+ case 'Home': return addDays(current, -offsetInWeek);
158
+ case 'End': return addDays(current, 6 - offsetInWeek);
159
+ case 'PageUp': return addMonths(current, opts.shiftKey ? -12 : -1);
160
+ case 'PageDown': return addMonths(current, opts.shiftKey ? 12 : 1);
161
+ default:
162
+ return null;
163
+ }
164
+ }
@@ -1 +1,2 @@
1
1
  export { Calendar } from './Calendar';
2
+ export { CalendarRange } from './CalendarRange';
@@ -22,6 +22,8 @@ Horizontally scrollable slide container with previous/next navigation.
22
22
 
23
23
  ## Props
24
24
 
25
+ > **HTML attributes** — all standard HTML attributes for the root element are forwarded via `{...rest}` (`id`, `className`, `style`, `data-*`, `aria-*`, event handlers, etc.). Component-controlled attributes take precedence.
26
+
25
27
  <ArgTypes of={Stories} />
26
28
 
27
29
  ## CSS variables
@@ -32,14 +34,17 @@ Horizontally scrollable slide container with previous/next navigation.
32
34
  <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
33
35
  </thead>
34
36
  <tbody>
35
- <tr><td>`--carousel_indicator-radius`</td><td>`var(--bds-border_radius--circle)`</td><td>Navigation button border radius</td></tr>
37
+ <tr><td>`--carousel_navBtn-radius`</td><td>`var(--bds-border_radius--circle)`</td><td>Navigation button border radius</td></tr>
38
+ <tr><td>`--carousel_navBtn-shadow`</td><td>`var(--bds-shadow_s)`</td><td>Navigation button elevation shadow</td></tr>
36
39
  <tr><td>`--carousel_navBtn-bg--hover`</td><td>`var(--bds-color_bg--subtle)`</td><td>Navigation button background on hover</td></tr>
37
40
  </tbody>
38
41
  </table>
39
42
 
43
+ `--carousel_indicator-radius` is a deprecated alias for `--carousel_navBtn-radius` — it still works but `--carousel_navBtn-radius` is the documented, forward-compatible name.
40
44
 
41
45
  ## Accessibility
42
46
 
43
47
  - Prev/next buttons have `aria-label="Previous"` / `"Next"`
44
- - The slide container has `aria-live="polite"` to announce slide changes
45
- - Fully keyboard navigable: buttons receive focus, slides are accessible via arrow keys
48
+ - Prev/next buttons carry `aria-controls` pointing at the slide list
49
+ - Scrolling respects `prefers-reduced-motion` (both the CSS `scroll-behavior` and the JS-driven prev/next scroll fall back to an instant jump)
50
+ - Prev/next scroll toward the logical end/start of content, so they work correctly in RTL layouts too
@@ -10,7 +10,7 @@
10
10
  display: flex;
11
11
  gap: var(--bds-space_m);
12
12
  overflow-inline: auto;
13
- scroll-snap-type: x mandatory;
13
+ scroll-snap-type: inline mandatory;
14
14
  scrollbar-width: none;
15
15
  flex: 1;
16
16
  scroll-behavior: smooth;
@@ -30,16 +30,18 @@
30
30
  display: flex;
31
31
  align-items: center;
32
32
  justify-content: center;
33
- inline-size: 2.5rem;
34
- block-size: 2.5rem;
35
- border-radius: var(--carousel_indicator-radius, var(--bds-border_radius--circle));
33
+ inline-size: 2.75rem;
34
+ block-size: 2.75rem;
35
+
36
+ /* --carousel_indicator-radius is the deprecated name for this token; kept as a fallback */
37
+ border-radius: var(--carousel_navBtn-radius, var(--carousel_indicator-radius, var(--bds-border_radius--circle)));
36
38
  background-color: var(--bds-color_bg);
37
39
  color: var(--bds-color_on-bg);
38
40
 
39
41
  --bdc_color: var(--bds-color_on-bg);
40
42
 
41
43
  border: none;
42
- box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--bds-shadow_s);
44
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--carousel_navBtn-shadow, var(--bds-shadow_s));
43
45
  cursor: pointer;
44
46
  flex-shrink: 0;
45
47
  transition: var(--bds-animation_transition);
@@ -1,9 +1,28 @@
1
1
  import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import { Carousel } from './Carousel';
3
4
 
4
5
  const items = [<div key="1">Slide 1</div>, <div key="2">Slide 2</div>, <div key="3">Slide 3</div>];
5
6
 
7
+ function setPrefersReducedMotion(matches: boolean) {
8
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
9
+ matches,
10
+ media: query,
11
+ addEventListener: vi.fn(),
12
+ removeEventListener: vi.fn(),
13
+ addListener: vi.fn(),
14
+ removeListener: vi.fn(),
15
+ dispatchEvent: vi.fn(),
16
+ }));
17
+ }
18
+
6
19
  describe('Carousel', () => {
20
+ beforeEach(() => {
21
+ // jsdom does not implement matchMedia; stub it so prefers-reduced-motion checks
22
+ // resolve deterministically ('no preference' by default) instead of throwing.
23
+ setPrefersReducedMotion(false);
24
+ });
25
+
7
26
  it('renders a section with accessible label', () => {
8
27
  render(<Carousel items={items} label="Featured items" />);
9
28
  expect(screen.getByRole('region', { name: 'Featured items' })).toBeInTheDocument();
@@ -26,4 +45,52 @@ describe('Carousel', () => {
26
45
  expect(screen.getByText('Slide 2')).toBeInTheDocument();
27
46
  expect(screen.getByText('Slide 3')).toBeInTheDocument();
28
47
  });
48
+
49
+ it('scrolls the track forward when Next is clicked', async () => {
50
+ render(<Carousel items={items} label="Slides" />);
51
+ const track = screen.getByRole('list');
52
+ Object.defineProperty(track, 'clientWidth', { configurable: true, value: 300 });
53
+ track.scrollBy = vi.fn();
54
+
55
+ await userEvent.click(screen.getByRole('button', { name: 'Next' }));
56
+
57
+ expect(track.scrollBy).toHaveBeenCalledWith({ left: 300, behavior: 'smooth' });
58
+ });
59
+
60
+ it('scrolls the track backward when Previous is clicked', async () => {
61
+ render(<Carousel items={items} label="Slides" />);
62
+ const track = screen.getByRole('list');
63
+ Object.defineProperty(track, 'clientWidth', { configurable: true, value: 300 });
64
+ track.scrollBy = vi.fn();
65
+
66
+ await userEvent.click(screen.getByRole('button', { name: 'Previous' }));
67
+
68
+ expect(track.scrollBy).toHaveBeenCalledWith({ left: -300, behavior: 'smooth' });
69
+ });
70
+
71
+ it('reverses the scroll direction in RTL layouts', async () => {
72
+ render(<Carousel items={items} label="Slides" />);
73
+ const track = screen.getByRole('list');
74
+ Object.defineProperty(track, 'clientWidth', { configurable: true, value: 300 });
75
+ track.style.direction = 'rtl';
76
+ track.scrollBy = vi.fn();
77
+
78
+ await userEvent.click(screen.getByRole('button', { name: 'Next' }));
79
+ expect(track.scrollBy).toHaveBeenCalledWith({ left: -300, behavior: 'smooth' });
80
+
81
+ await userEvent.click(screen.getByRole('button', { name: 'Previous' }));
82
+ expect(track.scrollBy).toHaveBeenCalledWith({ left: 300, behavior: 'smooth' });
83
+ });
84
+
85
+ it('scrolls instantly instead of smoothly when prefers-reduced-motion is set', async () => {
86
+ setPrefersReducedMotion(true);
87
+ render(<Carousel items={items} label="Slides" />);
88
+ const track = screen.getByRole('list');
89
+ Object.defineProperty(track, 'clientWidth', { configurable: true, value: 300 });
90
+ track.scrollBy = vi.fn();
91
+
92
+ await userEvent.click(screen.getByRole('button', { name: 'Next' }));
93
+
94
+ expect(track.scrollBy).toHaveBeenCalledWith({ left: 300, behavior: 'auto' });
95
+ });
29
96
  });
@@ -8,16 +8,28 @@ interface CarouselProps extends WithClassName, HTMLAttributes<HTMLElement> {
8
8
  label: string;
9
9
  }
10
10
 
11
+ function prefersReducedMotion(): boolean {
12
+ return (
13
+ typeof window !== 'undefined' &&
14
+ typeof window.matchMedia === 'function' &&
15
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
16
+ );
17
+ }
18
+
11
19
  export function Carousel({ items, label, className, ...rest }: Readonly<CarouselProps>) {
12
20
  const trackRef = useRef<HTMLDivElement>(null);
13
21
  const listId = useId();
14
22
 
15
23
  const scroll = (direction: 'prev' | 'next') => {
16
- if (!trackRef.current) return;
17
- const { scrollLeft, clientWidth } = trackRef.current;
18
- trackRef.current.scrollTo({
19
- left: scrollLeft + (direction === 'next' ? clientWidth : -clientWidth),
20
- behavior: 'smooth',
24
+ const track = trackRef.current;
25
+ if (!track) return;
26
+ // Determine the physical scroll sign from the computed writing direction so
27
+ // "next"/"prev" always move toward the logical end/start, in LTR and RTL alike.
28
+ const isRtl = window.getComputedStyle(track).direction === 'rtl';
29
+ const sign = direction === 'next' ? 1 : -1;
30
+ track.scrollBy({
31
+ left: (isRtl ? -sign : sign) * track.clientWidth,
32
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth',
21
33
  });
22
34
  };
23
35
 
@@ -25,7 +37,7 @@ export function Carousel({ items, label, className, ...rest }: Readonly<Carousel
25
37
  <section {...rest} aria-label={label} className={cn(css.carousel, className)}>
26
38
  <button
27
39
  type="button"
28
- className={cn(css.navBtn, css['--prev'])}
40
+ className={css.navBtn}
29
41
  aria-label="Previous"
30
42
  aria-controls={listId}
31
43
  onClick={() => scroll('prev')}
@@ -50,7 +62,7 @@ export function Carousel({ items, label, className, ...rest }: Readonly<Carousel
50
62
 
51
63
  <button
52
64
  type="button"
53
- className={cn(css.navBtn, css['--next'])}
65
+ className={css.navBtn}
54
66
  aria-label="Next"
55
67
  aria-controls={listId}
56
68
  onClick={() => scroll('next')}
@@ -30,8 +30,18 @@ Use the `name` prop to link multiple Collapsibles so only one can be open at a t
30
30
  ### Rich summary content
31
31
  <Canvas of={Stories.RichSummary} />
32
32
 
33
+ ## Controlled usage
34
+
35
+ Pass `open` to take full control of the expanded state. The component intercepts the summary
36
+ click/keyboard activation, cancels the native `<details>` toggle, and reports the intended next
37
+ state via `onToggle` — update your own `open` state in that callback to actually expand or
38
+ collapse the panel. Without `open` (i.e. `open === undefined`), the component is uncontrolled and
39
+ the native `<details>` element drives its own state (seeded once from `defaultOpen`).
40
+
33
41
  ## Props
34
42
 
43
+ > **HTML attributes** — all standard HTML attributes for the root element are forwarded via `{...rest}` (`id`, `className`, `style`, `data-*`, `aria-*`, event handlers, etc.). Component-controlled attributes take precedence.
44
+
35
45
  <ArgTypes of={Stories} />
36
46
 
37
47
  ## CSS variables
@@ -41,13 +51,14 @@ Use the `name` prop to link multiple Collapsibles so only one can be open at a t
41
51
  <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
42
52
  </thead>
43
53
  <tbody>
44
- <tr><td>`--collapsible_border-color`</td><td>`var(--bds-bg--subtle)`</td><td>Border and separator colour</td></tr>
45
- <tr><td>`--collapsible_border-width`</td><td>`1px`</td><td>Border and separator width</td></tr>
54
+ <tr><td>`--collapsible_border`</td><td>`none`</td><td>Optional additional CSS border drawn around the frame — the visible frame edge is drawn by the inset box-shadow border system below, so this stays `none` by default</td></tr>
55
+ <tr><td>`--collapsible_border-color`</td><td>`var(--bds-color_bg--subtle)`</td><td>Frame outline and separator colour</td></tr>
56
+ <tr><td>`--collapsible_border-width`</td><td>`1px`</td><td>Frame outline and separator width</td></tr>
46
57
  <tr><td>`--collapsible_border-radius`</td><td>`var(--bds-border_radius--m)`</td><td>Corner radius</td></tr>
47
- <tr><td>`--collapsible_bg`</td><td>`var(--bds-bg)`</td><td>Summary background</td></tr>
48
- <tr><td>`--collapsible_bg--hover`</td><td>`var(--bds-bg--subtle)`</td><td>Summary background on hover</td></tr>
49
- <tr><td>`--collapsible_color`</td><td>`var(--bds-on-bg)`</td><td>Summary text colour</td></tr>
50
- <tr><td>`--collapsible_on-color`</td><td>`var(--bds-on-bg)`</td><td>Content text colour</td></tr>
58
+ <tr><td>`--collapsible_bg`</td><td>`var(--bds-color_bg)`</td><td>Summary background</td></tr>
59
+ <tr><td>`--collapsible_bg--hover`</td><td>`var(--bds-color_bg--subtle)`</td><td>Summary background on hover</td></tr>
60
+ <tr><td>`--collapsible_color`</td><td>`var(--bds-color_on-bg)`</td><td>Summary text colour</td></tr>
61
+ <tr><td>`--collapsible_on-color`</td><td>`var(--bds-color_on-bg)`</td><td>Content text colour</td></tr>
51
62
  </tbody>
52
63
  </table>
53
64
 
@@ -1,7 +1,12 @@
1
1
  @layer boostdev.component {
2
2
  .collapsible {
3
- border: var(--collapsible_border-width, 1px) solid var(--collapsible_border-color, var(--bds-color_bg--subtle));
4
- border-radius: var(--collapsible_border-radius, var(--bds-border_radius--m));
3
+ border: var(--collapsible_border, none);
4
+ --bdc_color: var(--collapsible_border-color, var(--bds-color_bg--subtle));
5
+ --bdc-outline_width: var(--collapsible_border-width, 1px);
6
+ --bdc-outline_radius: var(--collapsible_border-radius, var(--bds-border_radius--m));
7
+
8
+ border-radius: var(--bdc-outline_radius);
9
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color);
5
10
  overflow: hidden;
6
11
  }
7
12
 
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react';
1
2
  import { render, screen } from '@testing-library/react';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import { Collapsible } from './Collapsible';
@@ -56,6 +57,45 @@ describe('Collapsible', () => {
56
57
  expect(screen.getByRole('group')).toHaveAttribute('open');
57
58
  });
58
59
 
60
+ it('does not change the open attribute on click when open is controlled and onToggle does not update it', async () => {
61
+ const user = userEvent.setup();
62
+ const onToggle = vi.fn();
63
+ render(<Collapsible summary="Toggle" open={false} onToggle={onToggle}>Content</Collapsible>);
64
+ const details = screen.getByRole('group');
65
+ await user.click(screen.getByText('Toggle'));
66
+ expect(details).not.toHaveAttribute('open');
67
+ expect(onToggle).toHaveBeenCalledWith(true);
68
+ });
69
+
70
+ it('reports the current open state, not a stale toggle, when controlled and already open', async () => {
71
+ const user = userEvent.setup();
72
+ const onToggle = vi.fn();
73
+ render(<Collapsible summary="Toggle" open onToggle={onToggle}>Content</Collapsible>);
74
+ const details = screen.getByRole('group');
75
+ await user.click(screen.getByText('Toggle'));
76
+ expect(details).toHaveAttribute('open');
77
+ expect(onToggle).toHaveBeenCalledWith(false);
78
+ });
79
+
80
+ it('expands and collapses a fully controlled instance when the consumer updates open from onToggle', async () => {
81
+ const user = userEvent.setup();
82
+ function Controlled() {
83
+ const [open, setOpen] = useState(false);
84
+ return (
85
+ <Collapsible summary="Toggle" open={open} onToggle={setOpen}>
86
+ Content
87
+ </Collapsible>
88
+ );
89
+ }
90
+ render(<Controlled />);
91
+ const details = screen.getByRole('group');
92
+ expect(details).not.toHaveAttribute('open');
93
+ await user.click(screen.getByText('Toggle'));
94
+ expect(details).toHaveAttribute('open');
95
+ await user.click(screen.getByText('Toggle'));
96
+ expect(details).not.toHaveAttribute('open');
97
+ });
98
+
59
99
  it('renders with name attribute for grouping', () => {
60
100
  render(
61
101
  <>