@duro-app/ui 5.11.0 → 5.13.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 (331) hide show
  1. package/dist/Table-Bms2ftt3.js +1694 -0
  2. package/dist/Table-Bms2ftt3.js.map +1 -0
  3. package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
  4. package/dist/components/Alert/Alert.d.ts.map +1 -1
  5. package/dist/components/AppShell/native.css.d.ts +53 -0
  6. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  7. package/dist/components/Button/Button.d.ts +2 -1
  8. package/dist/components/Button/Button.d.ts.map +1 -1
  9. package/dist/components/Button/styles.css.d.ts +1 -0
  10. package/dist/components/Button/styles.css.d.ts.map +1 -1
  11. package/dist/components/Callout/Callout.d.ts.map +1 -1
  12. package/dist/components/Callout/styles.css.d.ts +3 -0
  13. package/dist/components/Callout/styles.css.d.ts.map +1 -1
  14. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  15. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
  16. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  17. package/dist/components/Dialog/Dialog.d.ts +2 -17
  18. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  19. package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
  20. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  21. package/dist/components/Dialog/native.css.d.ts +82 -0
  22. package/dist/components/Dialog/native.css.d.ts.map +1 -0
  23. package/dist/components/Dialog/parts.css.d.ts +82 -0
  24. package/dist/components/Dialog/parts.css.d.ts.map +1 -0
  25. package/dist/components/Dialog/parts.d.ts +35 -0
  26. package/dist/components/Dialog/parts.d.ts.map +1 -0
  27. package/dist/components/Dialog/styles.css.d.ts +1 -74
  28. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  29. package/dist/components/Drawer/Drawer.d.ts +7 -35
  30. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  31. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  32. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  33. package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
  34. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  35. package/dist/components/Drawer/native.css.d.ts +55 -0
  36. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  37. package/dist/components/Drawer/parts.css.d.ts +96 -0
  38. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  39. package/dist/components/Drawer/parts.d.ts +56 -0
  40. package/dist/components/Drawer/parts.d.ts.map +1 -0
  41. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  42. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  43. package/dist/components/Drawer/styles.css.d.ts +15 -58
  44. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  45. package/dist/components/Drawer/touchInput.d.ts +35 -0
  46. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  47. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  48. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  49. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  50. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  51. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  52. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  53. package/dist/components/Grid/styles.css.d.ts +1 -0
  54. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  55. package/dist/components/Icon/Icon.d.ts +3 -9
  56. package/dist/components/Icon/Icon.d.ts.map +1 -1
  57. package/dist/components/Icon/geometry.d.ts +60 -0
  58. package/dist/components/Icon/geometry.d.ts.map +1 -0
  59. package/dist/components/Icon/types.d.ts +15 -0
  60. package/dist/components/Icon/types.d.ts.map +1 -0
  61. package/dist/components/Input/Input.d.ts +5 -1
  62. package/dist/components/Input/Input.d.ts.map +1 -1
  63. package/dist/components/Input/styles.css.d.ts +1 -0
  64. package/dist/components/Input/styles.css.d.ts.map +1 -1
  65. package/dist/components/LinkButton/styles.css.d.ts +1 -0
  66. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  67. package/dist/components/Menu/Menu.d.ts +7 -1
  68. package/dist/components/Menu/Menu.d.ts.map +1 -1
  69. package/dist/components/Menu/native.css.d.ts +14 -0
  70. package/dist/components/Menu/native.css.d.ts.map +1 -0
  71. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  72. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  73. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  74. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  75. package/dist/components/Meter/styles.css.d.ts +1 -0
  76. package/dist/components/Meter/styles.css.d.ts.map +1 -1
  77. package/dist/components/PageShell/styles.css.d.ts +2 -0
  78. package/dist/components/PageShell/styles.css.d.ts.map +1 -1
  79. package/dist/components/Popover/native.css.d.ts +11 -0
  80. package/dist/components/Popover/native.css.d.ts.map +1 -0
  81. package/dist/components/RadioGroup/RadioGroup.d.ts +1 -17
  82. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  83. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +3 -0
  84. package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -1
  85. package/dist/components/RadioGroup/group.d.ts +31 -0
  86. package/dist/components/RadioGroup/group.d.ts.map +1 -0
  87. package/dist/components/RadioGroup/styles.css.d.ts +10 -0
  88. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
  89. package/dist/components/ScrollArea/styles.css.d.ts +1 -0
  90. package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
  91. package/dist/components/SideNav/Chevron.d.ts +4 -0
  92. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  93. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  94. package/dist/components/SideNav/styles.css.d.ts +6 -0
  95. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  96. package/dist/components/SideNav/types.d.ts +6 -0
  97. package/dist/components/SideNav/types.d.ts.map +1 -0
  98. package/dist/components/Spinner/Spinner.d.ts +2 -6
  99. package/dist/components/Spinner/Spinner.d.ts.map +1 -1
  100. package/dist/components/Spinner/Spinner.types.d.ts +6 -0
  101. package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
  102. package/dist/components/Spinner/ring.css.d.ts +39 -0
  103. package/dist/components/Spinner/ring.css.d.ts.map +1 -0
  104. package/dist/components/Spinner/styles.css.d.ts +1 -28
  105. package/dist/components/Spinner/styles.css.d.ts.map +1 -1
  106. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  107. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  108. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  109. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  110. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  111. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  112. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  113. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  114. package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
  115. package/dist/components/StatusIcon/styles.css.d.ts +3 -0
  116. package/dist/components/StatusIcon/styles.css.d.ts.map +1 -1
  117. package/dist/components/Switch/Switch.d.ts +2 -18
  118. package/dist/components/Switch/Switch.d.ts.map +1 -1
  119. package/dist/components/Switch/Switch.types.d.ts +18 -0
  120. package/dist/components/Switch/Switch.types.d.ts.map +1 -0
  121. package/dist/components/TableOfContents/styles.css.d.ts +1 -0
  122. package/dist/components/TableOfContents/styles.css.d.ts.map +1 -1
  123. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  124. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  125. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  126. package/dist/components/Tabs/indicator.d.ts +28 -0
  127. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  128. package/dist/components/Tabs/styles.css.d.ts +4 -0
  129. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  130. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  131. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  132. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  133. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  134. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  135. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  136. package/dist/components/Tag/Tag.d.ts +2 -24
  137. package/dist/components/Tag/Tag.d.ts.map +1 -1
  138. package/dist/components/Tag/styles.css.d.ts +8 -0
  139. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  140. package/dist/components/Tag/types.d.ts +39 -0
  141. package/dist/components/Tag/types.d.ts.map +1 -0
  142. package/dist/components/Textarea/styles.css.d.ts +1 -0
  143. package/dist/components/Textarea/styles.css.d.ts.map +1 -1
  144. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  145. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  146. package/dist/components/Toast/native.css.d.ts +81 -0
  147. package/dist/components/Toast/native.css.d.ts.map +1 -0
  148. package/dist/components/Toast/store.d.ts +47 -0
  149. package/dist/components/Toast/store.d.ts.map +1 -0
  150. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  151. package/dist/components/Toggle/styles.css.d.ts +11 -2
  152. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  153. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  154. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  155. package/dist/components/Tree/Tree.d.ts.map +1 -1
  156. package/dist/components/Tree/styles.css.d.ts +6 -0
  157. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  158. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  159. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  160. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  161. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  162. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  163. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  164. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  165. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  166. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  167. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  168. package/dist/components/VirtualTable/styles.css.d.ts +6 -0
  169. package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
  170. package/dist/index.css +1 -1
  171. package/dist/index.d.ts +3 -1
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +6202 -4794
  174. package/dist/index.js.map +1 -1
  175. package/dist/shared/ButtonHost.d.ts +11 -0
  176. package/dist/shared/ButtonHost.d.ts.map +1 -0
  177. package/dist/shared/portal/hostRegistry.d.ts +33 -0
  178. package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
  179. package/dist/shared/pressableChild.d.ts +14 -0
  180. package/dist/shared/pressableChild.d.ts.map +1 -0
  181. package/dist/shared/useOverlayOpenState.d.ts +40 -0
  182. package/dist/shared/useOverlayOpenState.d.ts.map +1 -0
  183. package/dist/table.js +141 -135
  184. package/dist/table.js.map +1 -1
  185. package/package.json +34 -4
  186. package/src/components/Alert/Alert.tsx +14 -2
  187. package/src/components/AppShell/AppShell.meta.ts +1 -1
  188. package/src/components/AppShell/AppShell.native.tsx +174 -0
  189. package/src/components/AppShell/native.css.ts +56 -0
  190. package/src/components/Button/Button.tsx +6 -5
  191. package/src/components/Button/styles.css.ts +2 -0
  192. package/src/components/Callout/Callout.tsx +17 -3
  193. package/src/components/Callout/styles.css.ts +5 -0
  194. package/src/components/Checkbox/Checkbox.native.tsx +67 -0
  195. package/src/components/Checkbox/Checkbox.tsx +1 -50
  196. package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -0
  197. package/src/components/DetailPanel/DetailPanel.tsx +9 -45
  198. package/src/components/Dialog/Dialog.native.tsx +296 -0
  199. package/src/components/Dialog/Dialog.stories.tsx +71 -0
  200. package/src/components/Dialog/Dialog.tsx +41 -130
  201. package/src/components/Dialog/native.css.ts +78 -0
  202. package/src/components/Dialog/parts.css.ts +101 -0
  203. package/src/components/Dialog/parts.tsx +61 -0
  204. package/src/components/Dialog/styles.css.ts +2 -89
  205. package/src/components/Drawer/Drawer.meta.ts +2 -1
  206. package/src/components/Drawer/Drawer.native.tsx +469 -0
  207. package/src/components/Drawer/Drawer.tsx +63 -179
  208. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  209. package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
  210. package/src/components/Drawer/native.css.ts +58 -0
  211. package/src/components/Drawer/parts.css.ts +125 -0
  212. package/src/components/Drawer/parts.tsx +110 -0
  213. package/src/components/Drawer/sheetPhysics.ts +103 -0
  214. package/src/components/Drawer/styles.css.ts +19 -74
  215. package/src/components/Drawer/touchInput.ts +81 -0
  216. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  217. package/src/components/Drawer/useSheetHeights.ts +42 -0
  218. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  219. package/src/components/Grid/styles.css.ts +2 -0
  220. package/src/components/Icon/Icon.native.tsx +54 -0
  221. package/src/components/Icon/Icon.tsx +19 -430
  222. package/src/components/Icon/geometry.ts +456 -0
  223. package/src/components/Icon/types.ts +74 -0
  224. package/src/components/Input/Input.tsx +8 -0
  225. package/src/components/Input/styles.css.ts +2 -0
  226. package/src/components/LinkButton/styles.css.ts +2 -0
  227. package/src/components/Menu/Menu.meta.ts +1 -1
  228. package/src/components/Menu/Menu.native.tsx +268 -0
  229. package/src/components/Menu/Menu.tsx +11 -2
  230. package/src/components/Menu/native.css.ts +18 -0
  231. package/src/components/Menu/useMenuOpen.ts +33 -0
  232. package/src/components/Menu/useMenuRoot.ts +7 -26
  233. package/src/components/Meter/styles.css.ts +2 -0
  234. package/src/components/PageShell/styles.css.ts +4 -0
  235. package/src/components/Popover/Popover.meta.ts +1 -1
  236. package/src/components/Popover/Popover.native.tsx +205 -0
  237. package/src/components/Popover/native.css.ts +13 -0
  238. package/src/components/RadioGroup/RadioGroup.meta.ts +1 -1
  239. package/src/components/RadioGroup/RadioGroup.native.tsx +48 -0
  240. package/src/components/RadioGroup/RadioGroup.stories.tsx +64 -1
  241. package/src/components/RadioGroup/RadioGroup.tsx +8 -83
  242. package/src/components/RadioGroup/group.tsx +85 -0
  243. package/src/components/RadioGroup/styles.css.ts +13 -0
  244. package/src/components/ScrollArea/styles.css.ts +2 -0
  245. package/src/components/SideNav/Chevron.native.tsx +20 -0
  246. package/src/components/SideNav/Chevron.tsx +20 -0
  247. package/src/components/SideNav/SideNav.meta.ts +1 -1
  248. package/src/components/SideNav/SideNav.tsx +57 -41
  249. package/src/components/SideNav/styles.css.ts +8 -0
  250. package/src/components/SideNav/types.ts +6 -0
  251. package/src/components/Spinner/Spinner.native.tsx +37 -0
  252. package/src/components/Spinner/Spinner.tsx +6 -15
  253. package/src/components/Spinner/Spinner.types.ts +6 -0
  254. package/src/components/Spinner/ring.css.ts +45 -0
  255. package/src/components/Spinner/styles.css.ts +2 -32
  256. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  257. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  258. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  259. package/src/components/SplitPane/SplitPane.tsx +218 -0
  260. package/src/components/SplitPane/styles.css.ts +81 -0
  261. package/src/components/StatusIcon/StatusIcon.tsx +13 -2
  262. package/src/components/StatusIcon/styles.css.ts +5 -0
  263. package/src/components/Switch/Switch.native.tsx +45 -0
  264. package/src/components/Switch/Switch.tsx +22 -44
  265. package/src/components/Switch/Switch.types.ts +18 -0
  266. package/src/components/TableOfContents/styles.css.ts +2 -0
  267. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  268. package/src/components/Tabs/TabLayout.tsx +6 -0
  269. package/src/components/Tabs/Tabs.meta.ts +1 -1
  270. package/src/components/Tabs/Tabs.tsx +38 -136
  271. package/src/components/Tabs/indicator.ts +36 -0
  272. package/src/components/Tabs/styles.css.ts +7 -0
  273. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  274. package/src/components/Tabs/useTabIndicator.ts +49 -0
  275. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  276. package/src/components/Tabs/useTabKeys.ts +99 -0
  277. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  278. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  279. package/src/components/Tag/Tag.meta.ts +1 -1
  280. package/src/components/Tag/Tag.native.tsx +70 -0
  281. package/src/components/Tag/Tag.tsx +9 -37
  282. package/src/components/Tag/styles.css.ts +9 -0
  283. package/src/components/Tag/types.ts +43 -0
  284. package/src/components/Textarea/styles.css.ts +2 -0
  285. package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
  286. package/src/components/Toast/Toast.meta.ts +1 -1
  287. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  288. package/src/components/Toast/ToastProvider.tsx +22 -84
  289. package/src/components/Toast/native.css.ts +78 -0
  290. package/src/components/Toast/store.ts +96 -0
  291. package/src/components/Toggle/Toggle.tsx +5 -3
  292. package/src/components/Toggle/styles.css.ts +14 -2
  293. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  294. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  295. package/src/components/Tooltip/TooltipHint.ts +13 -0
  296. package/src/components/Tree/Tree.meta.ts +1 -1
  297. package/src/components/Tree/Tree.tsx +23 -135
  298. package/src/components/Tree/styles.css.ts +6 -0
  299. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  300. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  301. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  302. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  303. package/src/components/VirtualList/VirtualList.tsx +103 -0
  304. package/src/components/VirtualList/styles.css.ts +31 -0
  305. package/src/components/VirtualTable/styles.css.ts +12 -0
  306. package/src/hooks/useContainerBelow.native.ts +30 -0
  307. package/src/index.native.ts +61 -1
  308. package/src/index.ts +8 -1
  309. package/src/native-guard/globals.d.ts +10 -0
  310. package/src/native-guard/react-dom.d.ts +3 -0
  311. package/src/shared/ButtonHost.native.tsx +70 -0
  312. package/src/shared/ButtonHost.tsx +12 -0
  313. package/src/shared/accessibilityFocus.native.ts +23 -0
  314. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  315. package/src/shared/portal/OverlayModal.native.tsx +51 -0
  316. package/src/shared/portal/PortalHost.native.tsx +102 -0
  317. package/src/shared/portal/hostRegistry.ts +76 -0
  318. package/src/shared/portal/themeScope.native.ts +18 -0
  319. package/src/shared/pressableChild.ts +33 -0
  320. package/src/shared/safeArea.native.ts +22 -0
  321. package/src/shared/useKeyboardHeight.native.ts +18 -0
  322. package/src/shared/useOverlayOpenState.ts +128 -0
  323. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  324. package/dist/Table-ColfpgKV.js +0 -1552
  325. package/dist/Table-ColfpgKV.js.map +0 -1
  326. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts +0 -10
  327. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts.map +0 -1
  328. package/dist/index.native.d.ts +0 -33
  329. package/dist/index.native.d.ts.map +0 -1
  330. package/dist/platform.native.d.ts +0 -2
  331. package/dist/platform.native.d.ts.map +0 -1
@@ -14,83 +14,15 @@ import {html} from 'react-strict-dom'
14
14
  import {Icon, type IconName} from '../Icon'
15
15
  import {usePortalMount} from '../ThemeProvider/ThemeProvider'
16
16
  import {styles} from './styles.css'
17
+ import {
18
+ createToastStore,
19
+ type ToastContextValue,
20
+ type ToastEntry,
21
+ type ToastStore,
22
+ type ToastVariant,
23
+ } from './store'
17
24
 
18
- export type ToastVariant = 'success' | 'error' | 'info' | 'warning'
19
-
20
- export interface ToastAction {
21
- label: string
22
- onClick: () => void
23
- }
24
-
25
- export interface ToastOptions {
26
- /** Visual + semantic tone. Errors announce assertively; errors and
27
- * warnings linger longer. */
28
- variant?: ToastVariant
29
- message: ReactNode
30
- /** Optional single action, e.g. Undo. Runs, then dismisses the toast. */
31
- action?: ToastAction
32
- /** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
33
- * The countdown pauses while the toast is hovered or holds focus. */
34
- duration?: number
35
- }
36
-
37
- export interface ToastContextValue {
38
- /** Show a toast; returns its id so it can be dismissed programmatically. */
39
- toast: (opts: ToastOptions) => string
40
- dismiss: (id: string) => void
41
- }
42
-
43
- interface ToastEntry {
44
- id: string
45
- variant: ToastVariant
46
- message: ReactNode
47
- action?: ToastAction
48
- duration: number
49
- }
50
-
51
- interface ToastStore {
52
- subscribe: (cb: () => void) => () => void
53
- getSnapshot: () => readonly ToastEntry[]
54
- add: (opts: ToastOptions) => string
55
- remove: (id: string) => void
56
- }
57
-
58
- const DEFAULT_DURATION: Record<ToastVariant, number> = {
59
- success: 5000,
60
- info: 5000,
61
- error: 8000,
62
- warning: 8000,
63
- }
64
-
65
- function createToastStore(): ToastStore {
66
- let entries: ToastEntry[] = []
67
- let seq = 0
68
- const listeners = new Set<() => void>()
69
- const notify = () => listeners.forEach((l) => l())
70
-
71
- return {
72
- subscribe: (cb) => {
73
- listeners.add(cb)
74
- return () => listeners.delete(cb)
75
- },
76
- getSnapshot: () => entries,
77
- add: (opts) => {
78
- const variant = opts.variant ?? 'info'
79
- const id = `toast-${++seq}`
80
- const duration = opts.duration ?? DEFAULT_DURATION[variant]
81
- entries = [...entries, {id, variant, message: opts.message, action: opts.action, duration}]
82
- notify()
83
- return id
84
- },
85
- remove: (id) => {
86
- const next = entries.filter((e) => e.id !== id)
87
- if (next.length !== entries.length) {
88
- entries = next
89
- notify()
90
- }
91
- },
92
- }
93
- }
25
+ export type {ToastAction, ToastContextValue, ToastOptions, ToastVariant} from './store'
94
26
 
95
27
  const variantIcon: Record<ToastVariant, IconName> = {
96
28
  success: 'check-circle-filled',
@@ -115,10 +47,12 @@ export function useToast(): ToastContextValue {
115
47
 
116
48
  function ToastItem({
117
49
  entry,
50
+ store,
118
51
  onDismiss,
119
52
  dismissLabel,
120
53
  }: {
121
54
  entry: ToastEntry
55
+ store: ToastStore
122
56
  onDismiss: (id: string) => void
123
57
  dismissLabel: string
124
58
  }) {
@@ -126,19 +60,17 @@ function ToastItem({
126
60
  const [hovered, setHovered] = useState(false)
127
61
  const [focused, setFocused] = useState(false)
128
62
  const paused = hovered || focused
129
- // What is left of the countdown; a pause keeps it, a resume restarts the
130
- // timer with what was left (WCAG 2.2.1: the reader controls the timing).
131
- const remaining = useRef(duration)
132
-
63
+ // The store keeps what is left of the countdown; a pause keeps it, a resume
64
+ // restarts the timer with what was left (WCAG 2.2.1: the reader controls the timing).
133
65
  useEffect(() => {
134
66
  if (duration <= 0 || paused) return
135
67
  const started = Date.now()
136
- const timer = setTimeout(() => onDismiss(id), Math.max(remaining.current, 0))
68
+ const timer = setTimeout(() => onDismiss(id), Math.max(store.remaining(id), 0))
137
69
  return () => {
138
70
  clearTimeout(timer)
139
- remaining.current -= Date.now() - started
71
+ store.spend(id, Date.now() - started)
140
72
  }
141
- }, [id, duration, paused, onDismiss])
73
+ }, [id, duration, paused, onDismiss, store])
142
74
 
143
75
  return (
144
76
  <html.div
@@ -213,7 +145,13 @@ export function ToastProvider({dismissLabel = 'Dismiss', children}: ToastProvide
213
145
  const region = (
214
146
  <html.div role="region" aria-label="Notifications" style={styles.region}>
215
147
  {entries.map((entry) => (
216
- <ToastItem key={entry.id} entry={entry} onDismiss={dismiss} dismissLabel={dismissLabel} />
148
+ <ToastItem
149
+ key={entry.id}
150
+ entry={entry}
151
+ store={store}
152
+ onDismiss={dismiss}
153
+ dismissLabel={dismissLabel}
154
+ />
217
155
  ))}
218
156
  </html.div>
219
157
  )
@@ -0,0 +1,78 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
8
+
9
+ /**
10
+ * The native toast, the picked B0 direction (docs/mockups/native-toast/
11
+ * Tinted.dc.html): a card with a status-coloured edge, one row of message,
12
+ * action and close. Motion is Reanimated's.
13
+ */
14
+ export const nativeStyles = css.create({
15
+ region: {
16
+ position: 'absolute',
17
+ left: 0,
18
+ right: 0,
19
+ display: 'flex',
20
+ flexDirection: 'column',
21
+ alignItems: 'center',
22
+ gap: spacing.sm,
23
+ paddingLeft: spacing.md,
24
+ paddingRight: spacing.md,
25
+ },
26
+ // The bottom safe-area inset and the keyboard, plus the edge spacing.
27
+ regionBottom: (bottom: number) => ({bottom}),
28
+ toast: {
29
+ boxSizing: 'border-box',
30
+ width: '100%',
31
+ maxWidth: sizes.toastMaxW,
32
+ display: 'flex',
33
+ flexDirection: 'row',
34
+ alignItems: 'center',
35
+ gap: spacing.sm,
36
+ paddingTop: spacing.sm,
37
+ paddingBottom: spacing.sm,
38
+ paddingRight: spacing.sm,
39
+ paddingLeft: spacing.md,
40
+ borderRadius: radii.md,
41
+ borderWidth: borders.hairline,
42
+ borderStyle: 'solid',
43
+ borderColor: colors.border,
44
+ borderLeftWidth: borders.accent,
45
+ backgroundColor: colors.bgCard,
46
+ boxShadow: shadows.md,
47
+ },
48
+ success: {borderLeftColor: colors.successBorder},
49
+ error: {borderLeftColor: colors.errorBorder},
50
+ info: {borderLeftColor: colors.infoBorder},
51
+ warning: {borderLeftColor: colors.warningBorder},
52
+ message: {
53
+ flexGrow: 1,
54
+ flexShrink: 1,
55
+ color: colors.text,
56
+ fontSize: typography.fontSizeSm,
57
+ lineHeight: typography.lineHeight,
58
+ },
59
+ action: {
60
+ display: 'flex',
61
+ padding: spacing.sm,
62
+ borderWidth: 0,
63
+ backgroundColor: 'transparent',
64
+ color: colors.accent,
65
+ fontSize: typography.fontSizeSm,
66
+ fontWeight: typography.fontWeightSemibold,
67
+ },
68
+ close: {
69
+ display: 'flex',
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ width: sizes.touchTarget,
73
+ height: sizes.touchTarget,
74
+ borderRadius: radii.sm,
75
+ borderWidth: 0,
76
+ backgroundColor: 'transparent',
77
+ },
78
+ })
@@ -0,0 +1,96 @@
1
+ import type {ReactNode} from 'react'
2
+
3
+ /** The toast queue both platforms' ToastProvider render (plan N0, Slice B). */
4
+
5
+ export type ToastVariant = 'success' | 'error' | 'info' | 'warning'
6
+
7
+ export interface ToastAction {
8
+ label: string
9
+ onClick: () => void
10
+ }
11
+
12
+ export interface ToastOptions {
13
+ /** Visual + semantic tone. Errors announce assertively; errors and
14
+ * warnings linger longer. */
15
+ variant?: ToastVariant
16
+ message: ReactNode
17
+ /** Optional single action, e.g. Undo. Runs, then dismisses the toast. */
18
+ action?: ToastAction
19
+ /** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
20
+ * The countdown pauses while the toast is hovered or holds focus. */
21
+ duration?: number
22
+ }
23
+
24
+ export interface ToastContextValue {
25
+ /** Show a toast; returns its id so it can be dismissed programmatically. */
26
+ toast: (opts: ToastOptions) => string
27
+ dismiss: (id: string) => void
28
+ }
29
+
30
+ export interface ToastEntry {
31
+ id: string
32
+ variant: ToastVariant
33
+ message: ReactNode
34
+ action?: ToastAction
35
+ duration: number
36
+ }
37
+
38
+ export interface ToastStore {
39
+ subscribe: (cb: () => void) => () => void
40
+ getSnapshot: () => readonly ToastEntry[]
41
+ add: (opts: ToastOptions) => string
42
+ remove: (id: string) => void
43
+ /**
44
+ * What is left of a toast's countdown, in ms. Kept here, not in the toast's
45
+ * component: on native a toast re-renders into another host when a Dialog
46
+ * above it closes, and its countdown carries on.
47
+ */
48
+ remaining: (id: string) => number
49
+ /** Count `ms` of shown time against a toast's countdown. */
50
+ spend: (id: string, ms: number) => void
51
+ }
52
+
53
+ export const DEFAULT_DURATION: Record<ToastVariant, number> = {
54
+ success: 5000,
55
+ info: 5000,
56
+ error: 8000,
57
+ warning: 8000,
58
+ }
59
+
60
+ export function createToastStore(): ToastStore {
61
+ let entries: ToastEntry[] = []
62
+ let seq = 0
63
+ const left = new Map<string, number>()
64
+ const listeners = new Set<() => void>()
65
+ const notify = () => listeners.forEach((l) => l())
66
+
67
+ return {
68
+ subscribe: (cb) => {
69
+ listeners.add(cb)
70
+ return () => listeners.delete(cb)
71
+ },
72
+ getSnapshot: () => entries,
73
+ add: (opts) => {
74
+ const variant = opts.variant ?? 'info'
75
+ const id = `toast-${++seq}`
76
+ const duration = opts.duration ?? DEFAULT_DURATION[variant]
77
+ entries = [...entries, {id, variant, message: opts.message, action: opts.action, duration}]
78
+ left.set(id, duration)
79
+ notify()
80
+ return id
81
+ },
82
+ remove: (id) => {
83
+ left.delete(id)
84
+ const next = entries.filter((e) => e.id !== id)
85
+ if (next.length !== entries.length) {
86
+ entries = next
87
+ notify()
88
+ }
89
+ },
90
+ remaining: (id) => left.get(id) ?? 0,
91
+ spend: (id, ms) => {
92
+ const current = left.get(id)
93
+ if (current !== undefined) left.set(id, current - ms)
94
+ },
95
+ }
96
+ }
@@ -1,10 +1,11 @@
1
1
  import {type ReactNode, useCallback, useLayoutEffect, useRef} from 'react'
2
- import {html} from 'react-strict-dom'
3
2
  import {useControllableValue} from '../../hooks/useControllableValue'
4
3
  import {useToggleGroup} from '../ToggleGroup/ToggleGroupContext'
5
4
  import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
6
5
  import {mergeRefs} from '../../shared/mergeRefs'
6
+ import {ButtonHost} from '../../shared/ButtonHost'
7
7
  import type {ControlSize} from '../../shared/types'
8
+ import {isNative} from '../../platform'
8
9
  import {styles} from './styles.css'
9
10
 
10
11
  export type ToggleSize = ControlSize
@@ -83,7 +84,7 @@ export function Toggle({
83
84
  }, [disabled, isGrouped, value, group, pressed, setStandalonePressed])
84
85
 
85
86
  return (
86
- <html.button
87
+ <ButtonHost
87
88
  ref={mergeRefs(ref, grouped.ref)}
88
89
  type="button"
89
90
  tabIndex={
@@ -103,6 +104,7 @@ export function Toggle({
103
104
  data-pressed={pressed ? '' : undefined}
104
105
  style={[
105
106
  styles.base,
107
+ isNative && styles.nativeFlex,
106
108
  sizeMap[size],
107
109
  pressed ? styles.pressed : styles.unpressed,
108
110
  wrap && wrappedSizeMap[size],
@@ -114,6 +116,6 @@ export function Toggle({
114
116
  ]}
115
117
  >
116
118
  {children}
117
- </html.button>
119
+ </ButtonHost>
118
120
  )
119
121
  }
@@ -27,13 +27,20 @@ export const styles = css.create({
27
27
  minHeight: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
28
28
  },
29
29
  sizeDefault: {
30
- padding: `${spacing.sm} ${spacing.md}`,
30
+ // Longhands: RSD-native rejects a multi-value shorthand.
31
+ paddingTop: spacing.sm,
32
+ paddingBottom: spacing.sm,
33
+ paddingLeft: spacing.md,
34
+ paddingRight: spacing.md,
31
35
  fontSize: typography.fontSizeSm,
32
36
  borderRadius: radii.sm,
33
37
  gap: spacing.sm,
34
38
  },
35
39
  sizeSmall: {
36
- padding: `${spacing.xs} ${spacing.sm}`,
40
+ paddingTop: spacing.xs,
41
+ paddingBottom: spacing.xs,
42
+ paddingLeft: spacing.sm,
43
+ paddingRight: spacing.sm,
37
44
  fontSize: typography.fontSizeXs,
38
45
  borderRadius: radii.sm,
39
46
  gap: spacing.xs,
@@ -87,4 +94,9 @@ export const styles = css.create({
87
94
  opacity: 0.5,
88
95
  cursor: 'not-allowed',
89
96
  },
97
+ // RSD-native only honors display:'flex' (not 'inline-flex'). Layered on
98
+ // native only.
99
+ nativeFlex: {
100
+ display: 'flex',
101
+ },
90
102
  })
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it).',
5
+ 'Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it). On touch (React Native) no bubble is drawn: a string content becomes the accessibility hint of the Duro trigger inside (Button, Toggle, Menu.Trigger, Popover.Trigger), read after its name. Keep tooltip text non-essential, and keep aria-label on icon-only triggers.',
6
6
  whenToUse: [
7
7
  'Providing extra context on hover for icon-only buttons',
8
8
  'Explaining abbreviations or truncated text',
@@ -0,0 +1,36 @@
1
+ import type {ReactNode} from 'react'
2
+ import {TooltipHintContext} from './TooltipHint'
3
+
4
+ type Placement = 'top' | 'bottom' | 'left' | 'right'
5
+
6
+ interface RootProps {
7
+ children: ReactNode
8
+ /** On native, a string becomes the trigger's accessibility hint; anything else is dropped. */
9
+ content: ReactNode
10
+ /** Web only. */
11
+ placement?: Placement
12
+ /** Web only. */
13
+ delay?: number
14
+ }
15
+
16
+ /**
17
+ * Native Tooltip (plan N0, Decision 5): touch has no hover, so no bubble is
18
+ * drawn. A string `content` becomes the accessibility hint of the Duro
19
+ * trigger inside (Button, Toggle, Menu.Trigger, Popover.Trigger), read after
20
+ * its name. Keep tooltip text non-essential: sighted touch users never see
21
+ * it, and an icon-only trigger still needs its `aria-label`.
22
+ */
23
+ function Root({children, content}: RootProps) {
24
+ const hint = typeof content === 'string' ? content : undefined
25
+ return <TooltipHintContext.Provider value={hint}>{children}</TooltipHintContext.Provider>
26
+ }
27
+
28
+ /** Renders its child in place; the hint reaches it through context. */
29
+ function Trigger({children}: {children: ReactNode}) {
30
+ return children
31
+ }
32
+
33
+ export const Tooltip = {
34
+ Root,
35
+ Trigger,
36
+ }
@@ -0,0 +1,13 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ /**
4
+ * Native: a Tooltip's string content, read by the Duro trigger inside it
5
+ * (Button, Toggle, Menu.Trigger, Popover.Trigger) and set as that trigger's
6
+ * own accessibility hint (plan N0, Decision 5). Touch has no hover, so the
7
+ * text is never drawn; screen readers read it after the trigger's name.
8
+ */
9
+ export const TooltipHintContext = createContext<string | undefined>(undefined)
10
+
11
+ export function useTooltipHint(): string | undefined {
12
+ return useContext(TooltipHintContext)
13
+ }
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required.',
5
+ 'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required. On React Native selection, expansion and aria-level are the same; the keyboard model (arrows, Home/End, typeahead) is web only.',
6
6
  whenToUse: [
7
7
  'Browsing data that nests to any depth — a file tree, zones › services › capabilities, namespace › resource',
8
8
  'Picking one node of a hierarchy to show its details beside the tree',
@@ -1,17 +1,10 @@
1
- import {
2
- Children,
3
- type ReactNode,
4
- useCallback,
5
- useContext,
6
- useEffect,
7
- useId,
8
- useLayoutEffect,
9
- useRef,
10
- useState,
11
- } from 'react'
1
+ import {Children, type ReactNode, useCallback, useContext, useId, useRef, useState} from 'react'
12
2
  import {html} from 'react-strict-dom'
3
+ import {SPACING_PX} from '@duro-app/tokens/keys'
13
4
  import {styles} from './styles.css'
5
+ import {isNative} from '../../platform'
14
6
  import {TreeContext, TreeLevelContext, useTree} from './TreeContext'
7
+ import {useTreeKeyboard} from './useTreeKeyboard'
15
8
  import {isPlainClick, type LinkClickEvent, type OnNavigate} from '../../shared/navigate'
16
9
 
17
10
  /* Tree — a hierarchy of items (the WAI-ARIA tree pattern): single selection,
@@ -43,8 +36,6 @@ interface RootProps {
43
36
  onExpandedChange?: (expanded: string[]) => void
44
37
  }
45
38
 
46
- const TYPEAHEAD_RESET_MS = 500
47
-
48
39
  function Root({
49
40
  children,
50
41
  'aria-label': ariaLabel,
@@ -86,126 +77,16 @@ function Root({
86
77
  const isExpanded = useCallback((v: string) => open.includes(v), [open])
87
78
  const toggle = useCallback((v: string) => setExpanded(v, !open.includes(v)), [open, setExpanded])
88
79
 
89
- // Exactly one item is tabbable: the focus holder, else the selection, else
90
- // the first item — re-checked when an item appears, closes or goes away.
91
- useLayoutEffect(() => {
92
- const root = rootRef.current
93
- if (!root) return
94
- const tabbable = root.querySelector('[role="treeitem"][tabindex="0"]')
95
- if (tabbable) return
96
- const first = root.querySelector<HTMLElement>('[role="treeitem"]')
97
- const next = first?.dataset.treeValue ?? null
98
- if (next !== focusValue) setFocusValue(next)
99
- }, [focusValue, open, children])
100
-
101
- // Keyboard: DOM order over the rendered items IS the visible order (a
102
- // closed branch renders no children), so navigation reads it directly.
103
- const typeahead = useRef({text: '', at: 0})
104
- useEffect(() => {
105
- const root = rootRef.current
106
- if (!root) return
107
- const rootEl = root
108
- const items = () => Array.from(rootEl.querySelectorAll<HTMLElement>('[role="treeitem"]'))
109
- const focusItem = (el: HTMLElement | null | undefined) => {
110
- if (!el) return
111
- el.focus()
112
- }
113
- function onKeyDown(e: KeyboardEvent) {
114
- const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
115
- if (!current || !rootEl.contains(current)) return
116
- const list = items()
117
- const i = list.indexOf(current)
118
- const val = current.dataset.treeValue ?? ''
119
- const isOpen = current.getAttribute('aria-expanded')
120
- switch (e.key) {
121
- case 'ArrowDown':
122
- e.preventDefault()
123
- focusItem(list[i + 1])
124
- return
125
- case 'ArrowUp':
126
- e.preventDefault()
127
- focusItem(list[i - 1])
128
- return
129
- case 'ArrowRight':
130
- e.preventDefault()
131
- if (isOpen === 'false') setExpanded(val, true)
132
- else if (isOpen === 'true') focusItem(list[i + 1])
133
- return
134
- case 'ArrowLeft': {
135
- e.preventDefault()
136
- if (isOpen === 'true') {
137
- setExpanded(val, false)
138
- return
139
- }
140
- const parent = current.parentElement?.closest<HTMLElement>('[role="treeitem"]')
141
- if (parent && rootEl.contains(parent)) focusItem(parent)
142
- return
143
- }
144
- case 'Home':
145
- e.preventDefault()
146
- focusItem(list[0])
147
- return
148
- case 'End':
149
- e.preventDefault()
150
- focusItem(list[list.length - 1])
151
- return
152
- case 'Enter':
153
- case ' ': {
154
- e.preventDefault()
155
- // an item with href: follow its link (the click selects it and
156
- // runs onNavigate, or the browser navigates)
157
- const link = current.querySelector<HTMLElement>(':scope > [data-tree-link]')
158
- if (link) link.click()
159
- else onSelect(val)
160
- return
161
- }
162
- }
163
- // typeahead: a printable character, no modifier
164
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
165
- const now = Date.now()
166
- const t = typeahead.current
167
- t.text = now - t.at > TYPEAHEAD_RESET_MS ? e.key : t.text + e.key
168
- t.at = now
169
- // the same letter typed again cycles through the items it starts;
170
- // a real prefix ("fi") narrows, and may match the current item
171
- const typed = t.text.toLocaleLowerCase()
172
- const repeat = [...typed].every((c) => c === typed[0])
173
- const needle = repeat ? typed[0] : typed
174
- const start = repeat ? i + 1 : i
175
- for (let k = 0; k < list.length; k++) {
176
- const el = list[(start + k) % list.length]
177
- if ((el.dataset.treeText ?? '').toLocaleLowerCase().startsWith(needle)) {
178
- focusItem(el)
179
- break
180
- }
181
- }
182
- }
183
- }
184
- // the roving tab stop follows focus, however it moved (keys or pointer)
185
- function onFocusIn(e: FocusEvent) {
186
- const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
187
- const v = item?.dataset.treeValue
188
- if (v) setFocusValue(v)
189
- setKeyboardFocus(item?.matches(':focus-visible') ?? false)
190
- }
191
- function onFocusOut(e: FocusEvent) {
192
- if (!rootEl.contains(e.relatedTarget as Node | null)) setKeyboardFocus(false)
193
- }
194
- const onPointerDown = () => setKeyboardFocus(false)
195
- const onKey = () => setKeyboardFocus(true)
196
- root.addEventListener('keydown', onKeyDown)
197
- root.addEventListener('keydown', onKey)
198
- root.addEventListener('focusin', onFocusIn)
199
- root.addEventListener('focusout', onFocusOut)
200
- root.addEventListener('pointerdown', onPointerDown)
201
- return () => {
202
- root.removeEventListener('keydown', onKeyDown)
203
- root.removeEventListener('keydown', onKey)
204
- root.removeEventListener('focusin', onFocusIn)
205
- root.removeEventListener('focusout', onFocusOut)
206
- root.removeEventListener('pointerdown', onPointerDown)
207
- }
208
- }, [onSelect, setExpanded])
80
+ useTreeKeyboard({
81
+ rootRef,
82
+ focusValue,
83
+ setFocusValue,
84
+ setKeyboardFocus,
85
+ open,
86
+ children,
87
+ onSelect,
88
+ setExpanded,
89
+ })
209
90
 
210
91
  return (
211
92
  <TreeContext.Provider
@@ -272,7 +153,9 @@ function Item({value, label, textValue, children, meta, href, onNavigate}: ItemP
272
153
  // and a name computed from content would read the whole subtree
273
154
  const labelId = useId()
274
155
  const rowStyles = [
275
- styles.indent(level),
156
+ isNative
157
+ ? styles.indentPx(SPACING_PX.sm + Math.max(0, level - 1) * SPACING_PX.lg)
158
+ : styles.indent(level),
276
159
  selected && styles.rowSelected,
277
160
  keyboardFocus && focusValue === value && styles.rowFocused,
278
161
  ]
@@ -287,7 +170,12 @@ function Item({value, label, textValue, children, meta, href, onNavigate}: ItemP
287
170
  e.preventDefault()
288
171
  toggle(value)
289
172
  }}
290
- style={[styles.chevron, open && styles.chevronOpen, !hasChildren && styles.chevronLeaf]}
173
+ style={[
174
+ styles.chevron,
175
+ isNative && styles.nativeFlex,
176
+ open && styles.chevronOpen,
177
+ !hasChildren && styles.chevronLeaf,
178
+ ]}
291
179
  >
292
180
  ▸
293
181
  </html.span>
@@ -56,6 +56,8 @@ export const styles = css.create({
56
56
  outlineWidth: 0,
57
57
  outlineStyle: 'none',
58
58
  },
59
+ // Native has no calc(): the same steps, in px from the spacing scale.
60
+ indentPx: (px: number) => ({paddingLeft: px}),
59
61
  // one indent step per level below the top
60
62
  indent: (level: number) => ({
61
63
  paddingLeft: `calc(${spacing.sm} + ${Math.max(0, level - 1)} * ${spacing.lg})`,
@@ -71,6 +73,10 @@ export const styles = css.create({
71
73
  fontWeight: typography.fontWeightMedium,
72
74
  backgroundColor: colors.bgCardHover,
73
75
  },
76
+ // RSD on native lays out only display: flex, not the web's inline-flex.
77
+ nativeFlex: {
78
+ display: 'flex',
79
+ },
74
80
  chevron: {
75
81
  display: 'inline-flex',
76
82
  alignItems: 'center',
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Native has no hardware-keyboard tree navigation (plan N0, Decision 6):
3
+ * each item is a screen-reader stop with its role, level and expanded state.
4
+ */
5
+ export function useTreeKeyboard(_input: unknown) {}