@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-components",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "React",
@@ -26,6 +26,7 @@
26
26
  "type": "module",
27
27
  "exports": {
28
28
  ".": {
29
+ "react-native": "./dist/native/index.js",
29
30
  "types": "./dist/index.d.ts",
30
31
  "import": "./dist/index.js",
31
32
  "require": "./dist/index.cjs"
@@ -35,6 +36,10 @@
35
36
  "import": "./dist/client.js",
36
37
  "require": "./dist/client.cjs"
37
38
  },
39
+ "./web-components": {
40
+ "types": "./dist/web-components.d.ts",
41
+ "import": "./dist/web-components.js"
42
+ },
38
43
  "./css": "./dist/index.css",
39
44
  "./css/bdc": "./src/css/bdc.css",
40
45
  "./css/accordion": "./src/components/ui/Accordion/Accordion.module.css",
@@ -89,6 +94,7 @@
89
94
  "./css/section-header": "./src/components/layout/SectionHeader/SectionHeader.module.css"
90
95
  },
91
96
  "main": "./dist/index.js",
97
+ "react-native": "./dist/native/index.js",
92
98
  "types": "./dist/index.d.ts",
93
99
  "files": [
94
100
  "dist",
@@ -121,15 +127,19 @@
121
127
  "@testing-library/react": "^16.3.2",
122
128
  "@testing-library/user-event": "^14.6.1",
123
129
  "@types/node": "^25.5.0",
124
- "@types/react": "^18",
125
- "@types/react-dom": "^18",
130
+ "@types/react": "^19",
131
+ "@types/react-dom": "^19",
132
+ "@types/react-native": "^0.73.0",
126
133
  "@vitejs/plugin-react": "^5.1.4",
127
134
  "eslint": "^9",
128
135
  "git-cliff": "^2.12.0",
129
136
  "identity-obj-proxy": "^3.0.0",
130
137
  "jsdom": "^28.1.0",
138
+ "lit": "^3.3.2",
139
+ "react-native-web": "^0.21.2",
131
140
  "remark-gfm": "^4.0.1",
132
141
  "storybook": "^8.6.18",
142
+ "storybook-addon-tag-badges": "^1.4.0",
133
143
  "stylelint": "^16",
134
144
  "stylelint-config-standard": "^37",
135
145
  "tsup": "^8",
@@ -142,7 +152,16 @@
142
152
  "peerDependencies": {
143
153
  "@boostdev/design-system-foundation": ">=1.0.4",
144
154
  "react": ">=18",
145
- "react-dom": ">=18"
155
+ "react-dom": ">=18",
156
+ "react-native": ">=0.70"
157
+ },
158
+ "peerDependenciesMeta": {
159
+ "react-dom": {
160
+ "optional": true
161
+ },
162
+ "react-native": {
163
+ "optional": true
164
+ }
146
165
  },
147
166
  "publishConfig": {
148
167
  "access": "public",
@@ -150,5 +169,8 @@
150
169
  },
151
170
  "sideEffects": [
152
171
  "src/css/**/*.css"
153
- ]
172
+ ],
173
+ "dependencies": {
174
+ "@oddbird/css-anchor-positioning": "^0.9.0"
175
+ }
154
176
  }
@@ -41,14 +41,17 @@
41
41
  cursor: pointer;
42
42
  scroll-padding-block-end: var(--bds-space_l);
43
43
  justify-content: center;
44
- height: var(--button_height, 3em);
44
+ block-size: var(--button_height, 3em);
45
45
  font-size: var(--button_font-size, inherit);
46
46
  white-space: nowrap;
47
47
  line-height: 1;
48
48
 
49
49
  /* Colour model: override --button_color + --button_on-color to re-theme */
50
- --button_color: var(--bds-color_green);
51
- --button_on-color: var(--bds-color_on-green);
50
+ --button_color: var(--bds-color_green);
51
+ --button_on-color: var(--bds-color_on-green);
52
+
53
+ /* Text-safe variant of --button_color for use on page background (ghost text/border) */
54
+ --button_color--on-bg: var(--bds-color_green--strong);
52
55
 
53
56
  background-color: var(--button_bg, var(--button_color));
54
57
  color: var(--button_text, var(--button_on-color));
@@ -63,11 +66,11 @@
63
66
  /* Default: filled with --button_color, no visible border */
64
67
  .button.--default { /* inherits base — no overrides needed */ }
65
68
 
66
- /* Ghost: transparent background, border and text use --button_color */
69
+ /* Ghost: transparent background, border and text use text-safe --button_color--on-bg */
67
70
  .button.--ghost {
68
71
  --button_bg: transparent;
69
- --button_text: var(--button_color);
70
- --bdc_color: var(--button_color);
72
+ --button_text: var(--button_color--on-bg);
73
+ --bdc_color: var(--button_color--on-bg);
71
74
  }
72
75
 
73
76
  .button.--size_small {
@@ -141,8 +144,8 @@
141
144
  @media (hover: hover) and (pointer: fine) {
142
145
  .button.--default:hover {
143
146
  --button_bg: var(--bds-color_bg);
144
- --button_text: var(--bds-color_interactive);
145
- --button_pulse-color: var(--bds-color_interactive);
147
+ --button_text: var(--bds-color_interactive_on-bg);
148
+ --button_pulse-color: var(--bds-color_interactive_on-bg);
146
149
  --bdc_color: currentcolor;
147
150
  }
148
151
  }
@@ -0,0 +1,72 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Button.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Button (React Native)
7
+
8
+ React Native button component using `Pressable`. Matches the web `Button` API as closely as possible, replacing `onClick` with `onPress` and `aria-label` with `accessibilityLabel`.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Button } from '@boostdev/components';
16
+
17
+ <Button variant="default" size="medium" onPress={() => console.log('pressed')}>
18
+ Save
19
+ </Button>
20
+ ```
21
+
22
+ Wrap your tree in `ThemeProvider` (once, at the app root):
23
+
24
+ ```tsx
25
+ import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
26
+
27
+ export default function App() {
28
+ return <ThemeProvider><YourApp /></ThemeProvider>;
29
+ }
30
+ ```
31
+
32
+ ## Examples
33
+
34
+ ### All variants & sizes
35
+ <Canvas of={Stories.AllVariants} />
36
+
37
+ ### Default
38
+ <Canvas of={Stories.Default} />
39
+
40
+ ### Ghost
41
+ <Canvas of={Stories.Ghost} />
42
+
43
+ ### Sizes
44
+ <Canvas of={Stories.Small} />
45
+ <Canvas of={Stories.Medium} />
46
+ <Canvas of={Stories.Large} />
47
+
48
+ ### Disabled
49
+ <Canvas of={Stories.Disabled} />
50
+
51
+ ## Props
52
+
53
+ <ArgTypes of={Stories} />
54
+
55
+ ## Props reference
56
+
57
+ | Prop | Type | Default | Description |
58
+ |------|------|---------|-------------|
59
+ | `variant` | `"default" \| "ghost"` | `"default"` | Visual style |
60
+ | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Height and padding scale |
61
+ | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
62
+ | `onPress` | `() => void` | — | Called when the button is pressed |
63
+ | `iconStart` | `ReactNode` | — | Icon before the label |
64
+ | `iconEnd` | `ReactNode` | — | Icon after the label |
65
+ | `accessibilityLabel` | string | — | Screen reader label (required for icon-only buttons) |
66
+ | `style` | `StyleProp<ViewStyle>` | — | Override container styles |
67
+
68
+ ## Accessibility
69
+
70
+ - Provide `accessibilityLabel` for icon-only buttons
71
+ - `disabled` communicates the disabled state to assistive technologies
72
+ - Pressed state reduces opacity to 0.75 for visual feedback
@@ -0,0 +1,35 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Button } from './Button.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Button (native)', () => {
10
+ it('has button role', () => {
11
+ renderWithTheme(<Button>Save</Button>);
12
+ expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('exposes disabled state', () => {
16
+ renderWithTheme(<Button disabled>Save</Button>);
17
+ expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('aria-disabled', 'true');
18
+ });
19
+
20
+ it('renders with icon-start without throwing', () => {
21
+ // Icon wrappers have importantForAccessibility="no-hide-descendants" (Android)
22
+ // and accessibilityElementsHidden={true} (iOS) so decorative icons are hidden on device.
23
+ const icon = <span data-testid="icon">★</span>;
24
+ renderWithTheme(<Button iconStart={icon}>Save</Button>);
25
+ expect(screen.getByRole('button')).toBeInTheDocument();
26
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders with icon-end without throwing', () => {
30
+ const icon = <span data-testid="icon-end">→</span>;
31
+ renderWithTheme(<Button iconEnd={icon}>Next</Button>);
32
+ expect(screen.getByRole('button')).toBeInTheDocument();
33
+ expect(screen.getByTestId('icon-end')).toBeInTheDocument();
34
+ });
35
+ });
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Button } from './Button.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Interaction/Button',
9
+ component: Button,
10
+ decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ variant: { control: 'select', options: ['default', 'ghost'] },
14
+ size: { control: 'select', options: ['small', 'medium', 'large'] },
15
+ onPress: { action: 'pressed' },
16
+ },
17
+ } satisfies Meta<typeof Button>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = { args: { children: 'Default', variant: 'default' } };
23
+ export const Ghost: Story = { args: { children: 'Ghost', variant: 'ghost' } };
24
+ export const Small: Story = { args: { children: 'Small', size: 'small' } };
25
+ export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
26
+ export const Large: Story = { args: { children: 'Large', size: 'large' } };
27
+ export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
28
+ export const AllVariants: Story = {
29
+ render: () => (
30
+ <ThemeProvider>
31
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
32
+ {(['default', 'ghost'] as const).map(v => (
33
+ <div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
34
+ {(['small', 'medium', 'large'] as const).map(s => (
35
+ <Button key={s} variant={v} size={s}>{v} {s}</Button>
36
+ ))}
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </ThemeProvider>
41
+ ),
42
+ };
@@ -0,0 +1,95 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleSheet, Pressable, Text, View, StyleProp, ViewStyle } from 'react-native';
3
+ import { spacing, font, border } from '../../../native/tokens';
4
+ import { useTheme } from '../../../native/ThemeContext';
5
+
6
+ type ButtonVariant = 'default' | 'ghost';
7
+ type ButtonSize = 'small' | 'medium' | 'large';
8
+
9
+ export interface ButtonProps {
10
+ children?: ReactNode;
11
+ variant?: ButtonVariant;
12
+ size?: ButtonSize;
13
+ iconStart?: ReactNode;
14
+ iconEnd?: ReactNode;
15
+ disabled?: boolean;
16
+ onPress?: () => void;
17
+ accessibilityLabel?: string;
18
+ style?: StyleProp<ViewStyle>;
19
+ }
20
+
21
+ const sizeTokens: Record<ButtonSize, { height: number; px: number; fontSize: number; radius: number }> = {
22
+ small: { height: 36, px: spacing.m, fontSize: font.size.bodyS, radius: border.radius.xs },
23
+ medium: { height: 48, px: spacing.m, fontSize: font.size.body, radius: border.radius.s },
24
+ large: { height: 56, px: spacing.xxl, fontSize: font.size.heading3, radius: border.radius.m },
25
+ };
26
+
27
+ const styles = StyleSheet.create({
28
+ pressable: {
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ gap: spacing.xs,
33
+ alignSelf: 'flex-start',
34
+ },
35
+ label: {
36
+ fontFamily: font.family.body,
37
+ fontWeight: font.weight.semibold,
38
+ includeFontPadding: false,
39
+ },
40
+ });
41
+
42
+ export function Button({
43
+ children,
44
+ variant = 'default',
45
+ size = 'medium',
46
+ iconStart,
47
+ iconEnd,
48
+ disabled = false,
49
+ onPress,
50
+ accessibilityLabel,
51
+ style,
52
+ }: Readonly<ButtonProps>) {
53
+ const { colors } = useTheme();
54
+ const { height, px, fontSize, radius } = sizeTokens[size];
55
+
56
+ // Color model: filled = green bg, ghost = transparent bg + darker green border/text
57
+ const primaryColor = colors.colorGreen;
58
+ const onPrimaryColor = colors.colorOnGreen;
59
+ // Ghost uses colorGreenStrong for text/border — colorGreen (#00c950) fails 3:1 on white (2.15:1)
60
+ const ghostTextColor = colors.colorGreenStrong;
61
+
62
+ const containerStyle: ViewStyle = {
63
+ height,
64
+ paddingHorizontal: px,
65
+ borderRadius: radius,
66
+ opacity: disabled ? 0.4 : 1,
67
+ ...(variant === 'default' && { backgroundColor: primaryColor }),
68
+ ...(variant === 'ghost' && {
69
+ backgroundColor: 'transparent',
70
+ borderWidth: 1,
71
+ borderColor: ghostTextColor,
72
+ }),
73
+ };
74
+
75
+ const textColor = variant === 'default' ? onPrimaryColor : ghostTextColor;
76
+
77
+ return (
78
+ <Pressable
79
+ onPress={onPress}
80
+ disabled={disabled}
81
+ accessibilityRole="button"
82
+ accessibilityLabel={accessibilityLabel}
83
+ accessibilityState={{ disabled }}
84
+ style={({ pressed }) => [styles.pressable, containerStyle, pressed && { opacity: 0.75 }, style]}
85
+ >
86
+ {iconStart && <View importantForAccessibility="no-hide-descendants" accessibilityElementsHidden={true}>{iconStart}</View>}
87
+ {children && (
88
+ <Text style={[styles.label, { fontSize, color: textColor }]}>
89
+ {children}
90
+ </Text>
91
+ )}
92
+ {iconEnd && <View importantForAccessibility="no-hide-descendants">{iconEnd}</View>}
93
+ </Pressable>
94
+ );
95
+ }
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { Button } from './Button';
3
3
 
4
4
  const meta = {
5
- title: 'Interaction/Button',
5
+ title: 'React/Interaction/Button',
6
6
  component: Button,
7
7
  argTypes: {
8
8
  variant: { control: 'select', options: ['default', 'ghost'] },
@@ -1,8 +1,9 @@
1
1
  import {ButtonHTMLAttributes, MouseEventHandler, ReactNode} from 'react';
2
2
  import css from './Button.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
+ import type { WithClassName } from '../../../types';
4
5
 
5
- export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
6
+ export interface ButtonProps extends WithClassName, ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
6
7
  href?: string;
7
8
  variant?: 'default' | 'ghost';
8
9
  type?: 'button' | 'submit' | 'reset';
@@ -10,7 +11,6 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement | HT
10
11
  iconStart?: ReactNode;
11
12
  iconEnd?: ReactNode;
12
13
  children?: ReactNode;
13
- className?: string;
14
14
  disabled?: boolean;
15
15
  hasPulse?: boolean;
16
16
  onClick?: MouseEventHandler<HTMLElement>;
@@ -20,6 +20,22 @@ Keyboard-first command palette for searching and executing actions. Commonly tri
20
20
 
21
21
  <ArgTypes of={Stories} />
22
22
 
23
+ ## CSS variables
24
+
25
+
26
+ <table>
27
+ <thead>
28
+ <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
29
+ </thead>
30
+ <tbody>
31
+ <tr><td>`--command_color`</td><td>`var(--bds-color_bg)`</td><td>Panel background colour</td></tr>
32
+ <tr><td>`--command_on-color`</td><td>`var(--bds-color_on-bg)`</td><td>Text and icon colour</td></tr>
33
+ <tr><td>`--command_shadow`</td><td>`var(--bds-shadow_xl)`</td><td>Panel box shadow</td></tr>
34
+ <tr><td>`--command_border`</td><td>`none`</td><td>Panel border</td></tr>
35
+ </tbody>
36
+ </table>
37
+
38
+
23
39
  ## Accessibility
24
40
 
25
41
  - Input has `role="combobox"` with `aria-controls` pointing to the results list
@@ -4,8 +4,8 @@
4
4
  border: none;
5
5
  border-radius: var(--bds-border_radius--m);
6
6
  background: transparent;
7
- width: min(40rem, 90vw);
8
- max-height: 70vh;
7
+ inline-size: min(40rem, 90vw);
8
+ max-block-size: 70vh;
9
9
  overflow: visible;
10
10
  margin-block-start: 10vh;
11
11
  }
@@ -23,7 +23,7 @@
23
23
  border-radius: var(--bds-border_radius--m);
24
24
  box-shadow: var(--command_shadow, var(--bds-shadow_xl));
25
25
  overflow: hidden;
26
- max-height: 70vh;
26
+ max-block-size: 70vh;
27
27
  border: var(--command_border, none);
28
28
  --bdc_color: currentcolor;
29
29
  --bdc-outline_radius: var(--bds-border_radius--m);
@@ -42,8 +42,8 @@
42
42
  }
43
43
 
44
44
  .searchIcon {
45
- width: 1.25rem;
46
- height: 1.25rem;
45
+ inline-size: 1.25rem;
46
+ block-size: 1.25rem;
47
47
  color: var(--bds-color_on-bg);
48
48
  flex-shrink: 0;
49
49
  }
@@ -56,7 +56,7 @@
56
56
  font-size: var(--bds-font_size--body);
57
57
  color: var(--bds-color_on-bg);
58
58
  background: transparent;
59
- min-width: 0;
59
+ min-inline-size: 0;
60
60
  }
61
61
 
62
62
  .search::placeholder {
@@ -78,7 +78,7 @@
78
78
  }
79
79
 
80
80
  .list {
81
- overflow-y: auto;
81
+ overflow-block: auto;
82
82
  list-style: none;
83
83
  margin: 0;
84
84
  padding: var(--bds-space_xs) 0;
@@ -59,6 +59,16 @@ describe('Command', () => {
59
59
  expect(screen.getByText('Save current file')).toBeInTheDocument();
60
60
  });
61
61
 
62
+ it('dialog has an id for invoker targeting', () => {
63
+ render(<Command isOpen items={items} onClose={() => {}} />);
64
+ expect(document.querySelector('dialog')).toHaveAttribute('id');
65
+ });
66
+
67
+ it('uses provided id on dialog', () => {
68
+ render(<Command id="cmd-palette" isOpen items={items} onClose={() => {}} />);
69
+ expect(document.querySelector('dialog')).toHaveAttribute('id', 'cmd-palette');
70
+ });
71
+
62
72
  it('locks body scroll when open', () => {
63
73
  render(<Command isOpen items={items} onClose={() => {}} />);
64
74
  expect(document.body.style.overflow).toBe('hidden');
@@ -99,4 +109,31 @@ describe('Command', () => {
99
109
 
100
110
  document.body.removeChild(trigger);
101
111
  });
112
+
113
+ describe('command event (Invoker Commands API)', () => {
114
+ function makeCommandEvent(command: string) {
115
+ const event = new Event('command');
116
+ Object.defineProperty(event, 'command', { value: command, configurable: true });
117
+ return event;
118
+ }
119
+
120
+ it('locks scroll and calls onOpen on show-modal command', () => {
121
+ const onOpen = vi.fn();
122
+ render(<Command isOpen={false} items={items} onOpen={onOpen} onClose={() => {}} />);
123
+ const dialog = document.querySelector('dialog')!;
124
+ dialog.dispatchEvent(makeCommandEvent('show-modal'));
125
+ expect(document.body.style.overflow).toBe('hidden');
126
+ expect(onOpen).toHaveBeenCalled();
127
+ });
128
+
129
+ it('restores scroll and calls onClose on close command', () => {
130
+ const onClose = vi.fn();
131
+ render(<Command isOpen items={items} onClose={onClose} />);
132
+ document.body.style.overflow = 'hidden';
133
+ const dialog = document.querySelector('dialog')!;
134
+ dialog.dispatchEvent(makeCommandEvent('close'));
135
+ expect(document.body.style.overflow).toBe('');
136
+ expect(onClose).toHaveBeenCalled();
137
+ });
138
+ });
102
139
  });
@@ -5,7 +5,7 @@ import type { CommandItem } from './Command';
5
5
  import { Button } from '../Button/Button';
6
6
 
7
7
  const meta = {
8
- title: 'Interaction/Command',
8
+ title: 'React/Interaction/Command',
9
9
  component: Command,
10
10
  } satisfies Meta<typeof Command>;
11
11
 
@@ -1,6 +1,12 @@
1
1
  import { useState, useEffect, useRef, useId, KeyboardEvent, useMemo } from 'react';
2
+ import { installInvokerCommandsPolyfill } from '../../../polyfill-invoker-commands';
3
+
4
+ // Install once at module load — no-op if native support is present.
5
+ installInvokerCommandsPolyfill();
6
+
2
7
  import css from './Command.module.css';
3
8
  import { cn } from '@boostdev/design-system-foundation';
9
+ import type { WithClassName } from '../../../types';
4
10
 
5
11
  export interface CommandItem {
6
12
  id: string;
@@ -11,16 +17,19 @@ export interface CommandItem {
11
17
  onSelect: () => void;
12
18
  }
13
19
 
14
- interface CommandProps {
20
+ interface CommandProps extends WithClassName {
21
+ id?: string;
15
22
  isOpen: boolean;
23
+ onOpen?: () => void;
16
24
  onClose: () => void;
17
25
  items: CommandItem[];
18
26
  placeholder?: string;
19
- className?: string;
20
27
  }
21
28
 
22
29
  export function Command({
30
+ id: idProp,
23
31
  isOpen,
32
+ onOpen,
24
33
  onClose,
25
34
  items,
26
35
  placeholder = 'Search commands…',
@@ -28,6 +37,8 @@ export function Command({
28
37
  }: Readonly<CommandProps>) {
29
38
  const [query, setQuery] = useState('');
30
39
  const [activeIndex, setActiveIndex] = useState(0);
40
+ const generatedId = useId();
41
+ const dialogId = idProp ?? generatedId;
31
42
  const dialogRef = useRef<HTMLDialogElement>(null);
32
43
  const inputRef = useRef<HTMLInputElement>(null);
33
44
  const listboxId = useId();
@@ -56,14 +67,14 @@ export function Command({
56
67
  useEffect(() => {
57
68
  const dialog = dialogRef.current;
58
69
  if (!dialog) return;
59
- if (isOpen) {
70
+ if (isOpen && !dialog.open) {
60
71
  triggerRef.current = document.activeElement;
61
72
  dialog.showModal();
62
73
  document.body.style.overflow = 'hidden';
63
74
  setQuery('');
64
75
  setActiveIndex(0);
65
76
  setTimeout(() => inputRef.current?.focus(), 0);
66
- } else if (dialog.open) {
77
+ } else if (!isOpen && dialog.open) {
67
78
  dialog.close();
68
79
  document.body.style.overflow = '';
69
80
  (triggerRef.current as HTMLElement | null)?.focus();
@@ -77,6 +88,29 @@ export function Command({
77
88
  setActiveIndex(0);
78
89
  }, [query]);
79
90
 
91
+ // Invoker Commands API: sync state when opened/closed via commandfor/command.
92
+ useEffect(() => {
93
+ const dialog = dialogRef.current;
94
+ if (!dialog) return;
95
+ const handleCommand = (e: CommandEvent) => {
96
+ if (e.command === 'show-modal') {
97
+ triggerRef.current = document.activeElement;
98
+ document.body.style.overflow = 'hidden';
99
+ setQuery('');
100
+ setActiveIndex(0);
101
+ setTimeout(() => inputRef.current?.focus(), 0);
102
+ onOpen?.();
103
+ } else if (e.command === 'close') {
104
+ document.body.style.overflow = '';
105
+ const trigger = triggerRef.current;
106
+ setTimeout(() => (trigger as HTMLElement | null)?.focus(), 0);
107
+ onClose();
108
+ }
109
+ };
110
+ dialog.addEventListener('command', handleCommand);
111
+ return () => dialog.removeEventListener('command', handleCommand);
112
+ }, [onOpen, onClose]);
113
+
80
114
  const handleCancel = (e: React.SyntheticEvent) => {
81
115
  e.preventDefault();
82
116
  onClose();
@@ -106,6 +140,7 @@ export function Command({
106
140
  return (
107
141
  <dialog
108
142
  ref={dialogRef}
143
+ id={dialogId}
109
144
  className={cn(css.dialog, className)}
110
145
  aria-label="Command palette"
111
146
  aria-modal="true"