@duro-app/ui 5.12.0 → 5.14.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 (270) hide show
  1. package/README.md +49 -0
  2. package/dist/Table-DnaoH-h5.js +1806 -0
  3. package/dist/Table-DnaoH-h5.js.map +1 -0
  4. package/dist/components/AppShell/native.css.d.ts +53 -0
  5. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  6. package/dist/components/Button/Button.d.ts.map +1 -1
  7. package/dist/components/ColorMode/ColorMode.d.ts +2 -1
  8. package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
  9. package/dist/components/ColorMode/storage.d.ts +4 -0
  10. package/dist/components/ColorMode/storage.d.ts.map +1 -0
  11. package/dist/components/ColorMode/styles.css.d.ts +4 -0
  12. package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
  13. package/dist/components/ColorMode/useSystemScheme.d.ts +7 -0
  14. package/dist/components/ColorMode/useSystemScheme.d.ts.map +1 -0
  15. package/dist/components/Combobox/Combobox.d.ts +7 -2
  16. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  17. package/dist/components/Combobox/comboboxState.d.ts +29 -0
  18. package/dist/components/Combobox/comboboxState.d.ts.map +1 -0
  19. package/dist/components/Combobox/native.css.d.ts +27 -0
  20. package/dist/components/Combobox/native.css.d.ts.map +1 -0
  21. package/dist/components/Combobox/useComboboxRoot.d.ts +6 -10
  22. package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -1
  23. package/dist/components/Drawer/Drawer.d.ts +12 -37
  24. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  25. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  26. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  27. package/dist/components/Drawer/DrawerSnap.stories.d.ts +22 -0
  28. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  29. package/dist/components/Drawer/native.css.d.ts +59 -0
  30. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  31. package/dist/components/Drawer/parts.css.d.ts +96 -0
  32. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  33. package/dist/components/Drawer/parts.d.ts +56 -0
  34. package/dist/components/Drawer/parts.d.ts.map +1 -0
  35. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  36. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  37. package/dist/components/Drawer/styles.css.d.ts +16 -57
  38. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  39. package/dist/components/Drawer/touchInput.d.ts +35 -0
  40. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  41. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  42. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  43. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  44. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  45. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  46. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  47. package/dist/components/Form/FormSubmitContext.d.ts +8 -0
  48. package/dist/components/Form/FormSubmitContext.d.ts.map +1 -0
  49. package/dist/components/Listbox/native.css.d.ts +84 -0
  50. package/dist/components/Listbox/native.css.d.ts.map +1 -0
  51. package/dist/components/Menu/Menu.d.ts +7 -1
  52. package/dist/components/Menu/Menu.d.ts.map +1 -1
  53. package/dist/components/Menu/native.css.d.ts +14 -0
  54. package/dist/components/Menu/native.css.d.ts.map +1 -0
  55. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  56. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  57. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  58. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  59. package/dist/components/Popover/native.css.d.ts +11 -0
  60. package/dist/components/Popover/native.css.d.ts.map +1 -0
  61. package/dist/components/Select/Select.d.ts +9 -2
  62. package/dist/components/Select/Select.d.ts.map +1 -1
  63. package/dist/components/Select/Select.stories.d.ts +11 -0
  64. package/dist/components/Select/Select.stories.d.ts.map +1 -1
  65. package/dist/components/Select/native.css.d.ts +11 -0
  66. package/dist/components/Select/native.css.d.ts.map +1 -0
  67. package/dist/components/Select/selectState.d.ts +21 -0
  68. package/dist/components/Select/selectState.d.ts.map +1 -0
  69. package/dist/components/Select/styles.css.d.ts +10 -0
  70. package/dist/components/Select/styles.css.d.ts.map +1 -1
  71. package/dist/components/Select/useSelectRoot.d.ts +6 -8
  72. package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
  73. package/dist/components/SideNav/Chevron.d.ts +4 -0
  74. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  75. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  76. package/dist/components/SideNav/styles.css.d.ts +6 -0
  77. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  78. package/dist/components/SideNav/types.d.ts +6 -0
  79. package/dist/components/SideNav/types.d.ts.map +1 -0
  80. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  81. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  82. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  83. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  84. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  85. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  86. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  87. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  88. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  89. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  90. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  91. package/dist/components/Tabs/indicator.d.ts +28 -0
  92. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  93. package/dist/components/Tabs/styles.css.d.ts +4 -0
  94. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  95. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  96. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  97. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  98. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  99. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  100. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  101. package/dist/components/Tag/Tag.d.ts +2 -24
  102. package/dist/components/Tag/Tag.d.ts.map +1 -1
  103. package/dist/components/Tag/styles.css.d.ts +8 -0
  104. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  105. package/dist/components/Tag/types.d.ts +39 -0
  106. package/dist/components/Tag/types.d.ts.map +1 -0
  107. package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts +13 -0
  108. package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts.map +1 -0
  109. package/dist/components/ThemeProvider/ThemeProvider.d.ts +9 -2
  110. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  111. package/dist/components/ThemeProvider/overrideRule.d.ts +7 -0
  112. package/dist/components/ThemeProvider/overrideRule.d.ts.map +1 -0
  113. package/dist/components/ThemeProvider/overrides.d.ts +16 -0
  114. package/dist/components/ThemeProvider/overrides.d.ts.map +1 -0
  115. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  116. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  117. package/dist/components/Toast/native.css.d.ts +81 -0
  118. package/dist/components/Toast/native.css.d.ts.map +1 -0
  119. package/dist/components/Toast/store.d.ts +47 -0
  120. package/dist/components/Toast/store.d.ts.map +1 -0
  121. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  122. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  123. package/dist/components/Tree/Tree.d.ts.map +1 -1
  124. package/dist/components/Tree/styles.css.d.ts +6 -0
  125. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  126. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  127. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  128. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  129. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  130. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  131. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  132. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  133. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  134. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  135. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  136. package/dist/index.css +1 -1
  137. package/dist/index.d.ts +4 -2
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +5090 -4381
  140. package/dist/index.js.map +1 -1
  141. package/dist/shared/ButtonHost.d.ts +11 -0
  142. package/dist/shared/ButtonHost.d.ts.map +1 -0
  143. package/dist/shared/pressableChild.d.ts +14 -0
  144. package/dist/shared/pressableChild.d.ts.map +1 -0
  145. package/dist/shared/textOf.d.ts +7 -0
  146. package/dist/shared/textOf.d.ts.map +1 -0
  147. package/dist/table.js +1 -1
  148. package/package.json +3 -2
  149. package/src/components/AppShell/AppShell.meta.ts +1 -1
  150. package/src/components/AppShell/AppShell.native.tsx +174 -0
  151. package/src/components/AppShell/native.css.ts +56 -0
  152. package/src/components/Button/Button.tsx +15 -4
  153. package/src/components/ColorMode/ColorMode.meta.ts +1 -1
  154. package/src/components/ColorMode/ColorMode.tsx +16 -37
  155. package/src/components/ColorMode/storage.native.ts +8 -0
  156. package/src/components/ColorMode/storage.ts +19 -0
  157. package/src/components/ColorMode/styles.css.ts +6 -0
  158. package/src/components/ColorMode/useSystemScheme.native.ts +6 -0
  159. package/src/components/ColorMode/useSystemScheme.ts +19 -0
  160. package/src/components/Combobox/Combobox.meta.ts +1 -1
  161. package/src/components/Combobox/Combobox.native.tsx +295 -0
  162. package/src/components/Combobox/Combobox.tsx +14 -8
  163. package/src/components/Combobox/comboboxState.ts +107 -0
  164. package/src/components/Combobox/native.css.ts +33 -0
  165. package/src/components/Combobox/useComboboxRoot.ts +17 -91
  166. package/src/components/Drawer/Drawer.meta.ts +2 -1
  167. package/src/components/Drawer/Drawer.native.tsx +491 -0
  168. package/src/components/Drawer/Drawer.tsx +65 -143
  169. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  170. package/src/components/Drawer/DrawerSnap.stories.tsx +225 -0
  171. package/src/components/Drawer/native.css.ts +62 -0
  172. package/src/components/Drawer/parts.css.ts +125 -0
  173. package/src/components/Drawer/parts.tsx +110 -0
  174. package/src/components/Drawer/sheetPhysics.ts +103 -0
  175. package/src/components/Drawer/styles.css.ts +18 -73
  176. package/src/components/Drawer/touchInput.ts +81 -0
  177. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  178. package/src/components/Drawer/useSheetHeights.ts +42 -0
  179. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  180. package/src/components/Form/Form.meta.ts +1 -1
  181. package/src/components/Form/Form.native.tsx +120 -0
  182. package/src/components/Form/FormSubmitContext.ts +12 -0
  183. package/src/components/Listbox/Listbox.meta.ts +1 -1
  184. package/src/components/Listbox/Listbox.native.tsx +153 -0
  185. package/src/components/Listbox/native.css.ts +91 -0
  186. package/src/components/Menu/Menu.meta.ts +1 -1
  187. package/src/components/Menu/Menu.native.tsx +268 -0
  188. package/src/components/Menu/Menu.tsx +11 -2
  189. package/src/components/Menu/native.css.ts +18 -0
  190. package/src/components/Menu/useMenuOpen.ts +33 -0
  191. package/src/components/Menu/useMenuRoot.ts +7 -26
  192. package/src/components/Popover/Popover.meta.ts +1 -1
  193. package/src/components/Popover/Popover.native.tsx +205 -0
  194. package/src/components/Popover/native.css.ts +13 -0
  195. package/src/components/ScrollArea/ScrollArea.meta.ts +1 -1
  196. package/src/components/ScrollArea/ScrollArea.native.tsx +61 -0
  197. package/src/components/Select/Select.meta.ts +1 -1
  198. package/src/components/Select/Select.native.tsx +224 -0
  199. package/src/components/Select/Select.stories.tsx +49 -1
  200. package/src/components/Select/Select.tsx +20 -3
  201. package/src/components/Select/native.css.ts +13 -0
  202. package/src/components/Select/selectState.ts +36 -0
  203. package/src/components/Select/styles.css.ts +11 -0
  204. package/src/components/Select/useSelectRoot.ts +9 -30
  205. package/src/components/SideNav/Chevron.native.tsx +20 -0
  206. package/src/components/SideNav/Chevron.tsx +20 -0
  207. package/src/components/SideNav/SideNav.meta.ts +1 -1
  208. package/src/components/SideNav/SideNav.tsx +57 -41
  209. package/src/components/SideNav/styles.css.ts +8 -0
  210. package/src/components/SideNav/types.ts +6 -0
  211. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  212. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  213. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  214. package/src/components/SplitPane/SplitPane.tsx +218 -0
  215. package/src/components/SplitPane/styles.css.ts +81 -0
  216. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  217. package/src/components/Tabs/TabLayout.tsx +6 -0
  218. package/src/components/Tabs/Tabs.meta.ts +1 -1
  219. package/src/components/Tabs/Tabs.tsx +38 -136
  220. package/src/components/Tabs/indicator.ts +36 -0
  221. package/src/components/Tabs/styles.css.ts +7 -0
  222. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  223. package/src/components/Tabs/useTabIndicator.ts +49 -0
  224. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  225. package/src/components/Tabs/useTabKeys.ts +99 -0
  226. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  227. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  228. package/src/components/Tag/Tag.meta.ts +1 -1
  229. package/src/components/Tag/Tag.native.tsx +70 -0
  230. package/src/components/Tag/Tag.tsx +9 -37
  231. package/src/components/Tag/styles.css.ts +9 -0
  232. package/src/components/Tag/types.ts +43 -0
  233. package/src/components/ThemeProvider/ThemeOverrides.stories.tsx +113 -0
  234. package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -1
  235. package/src/components/ThemeProvider/ThemeProvider.native.tsx +36 -7
  236. package/src/components/ThemeProvider/ThemeProvider.tsx +42 -6
  237. package/src/components/ThemeProvider/overrideRule.ts +39 -0
  238. package/src/components/ThemeProvider/overrides.ts +27 -0
  239. package/src/components/Toast/Toast.meta.ts +1 -1
  240. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  241. package/src/components/Toast/ToastProvider.tsx +22 -84
  242. package/src/components/Toast/native.css.ts +78 -0
  243. package/src/components/Toast/store.ts +96 -0
  244. package/src/components/Toggle/Toggle.tsx +3 -3
  245. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  246. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  247. package/src/components/Tooltip/TooltipHint.ts +13 -0
  248. package/src/components/Tree/Tree.meta.ts +1 -1
  249. package/src/components/Tree/Tree.tsx +23 -135
  250. package/src/components/Tree/styles.css.ts +6 -0
  251. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  252. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  253. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  254. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  255. package/src/components/VirtualList/VirtualList.tsx +103 -0
  256. package/src/components/VirtualList/styles.css.ts +31 -0
  257. package/src/form.native.ts +7 -0
  258. package/src/hooks/useContainerBelow.native.ts +30 -0
  259. package/src/index.native.ts +48 -0
  260. package/src/index.ts +9 -1
  261. package/src/shared/ButtonHost.native.tsx +70 -0
  262. package/src/shared/ButtonHost.tsx +12 -0
  263. package/src/shared/focusInput.native.ts +18 -0
  264. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  265. package/src/shared/pressableChild.ts +33 -0
  266. package/src/shared/textOf.ts +13 -0
  267. package/src/shared/useKeyboardHeight.native.ts +18 -0
  268. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  269. package/dist/Table-ColfpgKV.js +0 -1552
  270. package/dist/Table-ColfpgKV.js.map +0 -1
@@ -0,0 +1,91 @@
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 {sizes} from '@duro-app/tokens/tokens/sizes.css'
6
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
7
+
8
+ /**
9
+ * A picker sheet's rows on native (plan N0, C0 direction A "Check"): a
10
+ * touch-target row per option, the chosen one filled and checked.
11
+ */
12
+ export const sheetStyles = css.create({
13
+ list: {
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ },
17
+ option: {
18
+ display: 'flex',
19
+ flexDirection: 'row',
20
+ alignItems: 'center',
21
+ gap: spacing.sm,
22
+ minHeight: sizes.touchTarget,
23
+ paddingLeft: spacing.md,
24
+ paddingRight: spacing.md,
25
+ borderBottomWidth: borders.hairline,
26
+ borderBottomStyle: 'solid',
27
+ borderBottomColor: colors.border,
28
+ },
29
+ // The chosen option, and a row under the finger.
30
+ optionFilled: {
31
+ backgroundColor: colors.bgCardHover,
32
+ },
33
+ label: {
34
+ flex: 1,
35
+ color: colors.text,
36
+ fontFamily: typography.fontFamily,
37
+ fontSize: typography.fontSizeSm,
38
+ },
39
+ labelDisabled: {
40
+ color: colors.textMuted,
41
+ },
42
+ check: {
43
+ color: colors.accent,
44
+ fontFamily: typography.fontFamily,
45
+ fontSize: typography.fontSizeSm,
46
+ fontWeight: typography.fontWeightSemibold,
47
+ },
48
+ empty: {
49
+ display: 'flex',
50
+ flexDirection: 'column',
51
+ alignItems: 'center',
52
+ gap: spacing.xs,
53
+ paddingTop: spacing.xl,
54
+ paddingBottom: spacing.xl,
55
+ paddingLeft: spacing.md,
56
+ paddingRight: spacing.md,
57
+ color: colors.textMuted,
58
+ fontFamily: typography.fontFamily,
59
+ fontSize: typography.fontSizeSm,
60
+ textAlign: 'center',
61
+ },
62
+ // The search field pinned at a Combobox sheet's top, under its header.
63
+ search: {
64
+ display: 'flex',
65
+ flexDirection: 'row',
66
+ alignItems: 'center',
67
+ gap: spacing.sm,
68
+ minHeight: sizes.touchTarget,
69
+ marginLeft: spacing.md,
70
+ marginRight: spacing.md,
71
+ marginBottom: spacing.sm,
72
+ paddingLeft: spacing.sm,
73
+ paddingRight: spacing.sm,
74
+ borderRadius: radii.sm,
75
+ borderWidth: borders.hairline,
76
+ borderStyle: 'solid',
77
+ borderColor: colors.border,
78
+ backgroundColor: colors.bg,
79
+ },
80
+ searchInput: {
81
+ flex: 1,
82
+ color: colors.text,
83
+ fontFamily: typography.fontFamily,
84
+ fontSize: typography.fontSizeSm,
85
+ backgroundColor: 'transparent',
86
+ borderWidth: 0,
87
+ '::placeholder': {
88
+ color: colors.textMuted,
89
+ },
90
+ },
91
+ })
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars; size="small" matches small Toggles and Buttons (a small ghost trigger is iconButtonSm). The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.',
5
+ 'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars; size="small" matches small Toggles and Buttons (a small ghost trigger is iconButtonSm). The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog. On React Native the popup is placed against the trigger in a modal overlay; a press outside or the Android back button closes it, and an item runs its onClick after the menu has gone (so an item can open a Dialog). Menu.LinkItem takes onNavigate(href, event) on both platforms (ADR-0002); on native, without it or without preventDefault, the link opens with the system. Native has no arrow keys: each item is its own screen-reader stop.',
6
6
  whenToUse: [
7
7
  'Context menu or "more actions" dropdown',
8
8
  'Navigation links in a dropdown',
@@ -0,0 +1,268 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ type Ref,
5
+ type RefObject,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useId,
10
+ useRef,
11
+ } from 'react'
12
+ import {Linking, Platform, Pressable, ScrollView, StyleSheet} from 'react-native'
13
+ import {html} from 'react-strict-dom'
14
+ import {SIZES_PX} from '@duro-app/tokens/keys'
15
+ import {styles} from './styles.css'
16
+ import {nativeStyles} from './native.css'
17
+ import {useMenuOpen} from './useMenuOpen'
18
+ import {AnchoredOverlay} from '../../shared/portal/AnchoredOverlay'
19
+ import {focusForAccessibility} from '../../shared/accessibilityFocus'
20
+ import {mergeRefs} from '../../shared/mergeRefs'
21
+ import type {LinkClickEvent, OnNavigate} from '../../shared/navigate'
22
+ import type {ControlSize} from '../../shared/types'
23
+ import {useTooltipHint} from '../Tooltip/TooltipHint'
24
+ import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
25
+
26
+ interface NativeMenuContextValue {
27
+ open: boolean
28
+ toggle: () => void
29
+ /**
30
+ * Close, then run `after` once the menu's Modal is gone: an item that opens
31
+ * a Dialog would otherwise present during a dismissal, which iOS drops.
32
+ */
33
+ closeThen: (after?: () => void) => void
34
+ menuId: string
35
+ triggerRef: RefObject<unknown>
36
+ }
37
+
38
+ const MenuContext = createContext<NativeMenuContextValue | null>(null)
39
+
40
+ function useMenu() {
41
+ const ctx = useContext(MenuContext)
42
+ if (!ctx) throw new Error('Menu compound components must be used within Menu.Root')
43
+ return ctx
44
+ }
45
+
46
+ interface RootProps {
47
+ children: ReactNode
48
+ /** Controlled open state. */
49
+ open?: boolean
50
+ /** Initial open state (uncontrolled). */
51
+ defaultOpen?: boolean
52
+ /** Called when the menu opens or closes. */
53
+ onOpenChange?: (open: boolean) => void
54
+ }
55
+
56
+ /**
57
+ * Native Menu (plan N0, Slice B): the same compound API, its popup placed
58
+ * against the trigger in an OverlayModal. A press outside or Android's back
59
+ * button closes it; an item runs its action after the menu has gone. Native
60
+ * has no arrow keys or active descendant (Decision 6): each item is its own
61
+ * screen-reader stop.
62
+ */
63
+ function Root({children, ...options}: RootProps) {
64
+ const {open, setOpen} = useMenuOpen(options)
65
+ const menuId = useId()
66
+ const triggerRef = useRef<unknown>(null)
67
+ const pendingRef = useRef<(() => void) | null>(null)
68
+
69
+ const runPending = useCallback(() => {
70
+ const pending = pendingRef.current
71
+ pendingRef.current = null
72
+ pending?.()
73
+ }, [])
74
+
75
+ const closeThen = useCallback(
76
+ (after?: () => void) => {
77
+ pendingRef.current = after ?? null
78
+ setOpen(false)
79
+ },
80
+ [setOpen],
81
+ )
82
+
83
+ // Focus back on the trigger once the menu is gone; Android has no Modal
84
+ // onDismiss, so a pending action runs on the next frame there.
85
+ const wasOpen = useRef(open)
86
+ useEffect(() => {
87
+ if (wasOpen.current && !open) {
88
+ focusForAccessibility(triggerRef.current, 'Menu')
89
+ if (Platform.OS !== 'ios') requestAnimationFrame(runPending)
90
+ }
91
+ wasOpen.current = open
92
+ }, [open, runPending])
93
+
94
+ const toggle = useCallback(() => setOpen(!open), [open, setOpen])
95
+
96
+ return (
97
+ <MenuContext.Provider value={{open, toggle, closeThen, menuId, triggerRef}}>
98
+ <PendingContext.Provider value={runPending}>
99
+ <html.div style={[styles.root, nativeStyles.nativeFlex]}>{children}</html.div>
100
+ </PendingContext.Provider>
101
+ </MenuContext.Provider>
102
+ )
103
+ }
104
+
105
+ /** iOS: run the pending item action once the Modal has dismissed. */
106
+ const PendingContext = createContext<() => void>(() => {})
107
+
108
+ export type MenuTriggerVariant = 'default' | 'ghost'
109
+
110
+ interface TriggerProps {
111
+ children: ReactNode
112
+ /** Accessible name — required when the label is an icon only. */
113
+ 'aria-label'?: string
114
+ /** `ghost`: no border or fill until hover, for toolbars. Default: `default`. */
115
+ variant?: MenuTriggerVariant
116
+ /** `small` matches a small Toggle or Button beside it. Default: `default`. */
117
+ size?: ControlSize
118
+ ref?: Ref<unknown>
119
+ }
120
+
121
+ /**
122
+ * The trigger is an RN Pressable host, so it carries an enclosing Tooltip's
123
+ * text as its own accessibility hint (RSD drops `accessibilityHint` on html.*).
124
+ *
125
+ * holds-until: react-strict-dom maps aria-description to accessibilityHint
126
+ * (https://github.com/react/react-strict-dom/issues/524).
127
+ */
128
+ function Trigger({
129
+ children,
130
+ 'aria-label': ariaLabel,
131
+ variant = 'default',
132
+ size = 'default',
133
+ ref,
134
+ }: TriggerProps) {
135
+ const {open, toggle, triggerRef} = useMenu()
136
+ const hint = useTooltipHint()
137
+ return (
138
+ <Pressable
139
+ ref={mergeRefs(triggerRef, ref) as Ref<never>}
140
+ onPress={toggle}
141
+ accessibilityRole="button"
142
+ accessibilityLabel={ariaLabel}
143
+ accessibilityHint={hint}
144
+ accessibilityState={{expanded: open}}
145
+ >
146
+ <html.div
147
+ style={[
148
+ styles.trigger,
149
+ nativeStyles.nativeFlex,
150
+ variant === 'ghost'
151
+ ? [styles.triggerGhost, size === 'small' && styles.triggerGhostSmall]
152
+ : size === 'small' && styles.triggerSmall,
153
+ ]}
154
+ >
155
+ {children}
156
+ </html.div>
157
+ </Pressable>
158
+ )
159
+ }
160
+
161
+ interface PopupProps {
162
+ children: ReactNode
163
+ align?: 'start' | 'end'
164
+ }
165
+
166
+ function Popup({children, align = 'start'}: PopupProps) {
167
+ const {open, closeThen, menuId, triggerRef} = useMenu()
168
+ const runPending = useContext(PendingContext)
169
+ const popupRef = useRef<HTMLDivElement>(null)
170
+ return (
171
+ <AnchoredOverlay
172
+ visible={open}
173
+ anchorRef={triggerRef}
174
+ side="bottom"
175
+ align={align}
176
+ onRequestClose={() => closeThen()}
177
+ onShow={() => focusForAccessibility(popupRef.current, 'Menu')}
178
+ onDismiss={runPending}
179
+ >
180
+ <html.div
181
+ ref={popupRef}
182
+ id={menuId}
183
+ role="menu"
184
+ style={[styles.popup, nativeStyles.popupInPlace]}
185
+ >
186
+ <ScrollView style={layout.scroll} bounces={false}>
187
+ <ControlContextBoundary>{children}</ControlContextBoundary>
188
+ </ScrollView>
189
+ </html.div>
190
+ </AnchoredOverlay>
191
+ )
192
+ }
193
+
194
+ // The popup's height cap, `sizes.listMaxH`, on the view that scrolls.
195
+ const layout = StyleSheet.create({scroll: {maxHeight: SIZES_PX.listMaxH, flexGrow: 0}})
196
+
197
+ interface ItemProps {
198
+ onClick?: () => void
199
+ children: ReactNode
200
+ }
201
+
202
+ function Item({onClick, children}: ItemProps) {
203
+ const {closeThen} = useMenu()
204
+ return (
205
+ <Pressable accessibilityRole="menuitem" onPress={() => closeThen(onClick)}>
206
+ {({pressed}) => (
207
+ <html.div style={[styles.item, pressed && nativeStyles.itemPressed]}>{children}</html.div>
208
+ )}
209
+ </Pressable>
210
+ )
211
+ }
212
+
213
+ interface LinkItemProps {
214
+ href: string
215
+ /**
216
+ * Called with the href when the item is pressed (ADR-0002). Call
217
+ * `event.preventDefault()` to navigate in-app; otherwise, and without it,
218
+ * the URL opens with the system (`Linking.openURL`).
219
+ */
220
+ onNavigate?: OnNavigate
221
+ children: ReactNode
222
+ }
223
+
224
+ function LinkItem({href, onNavigate, children}: LinkItemProps) {
225
+ const {closeThen} = useMenu()
226
+ const follow = () => {
227
+ let prevented = false
228
+ const event: LinkClickEvent = {
229
+ button: 0,
230
+ metaKey: false,
231
+ ctrlKey: false,
232
+ shiftKey: false,
233
+ altKey: false,
234
+ get defaultPrevented() {
235
+ return prevented
236
+ },
237
+ preventDefault() {
238
+ prevented = true
239
+ },
240
+ }
241
+ onNavigate?.(href, event)
242
+ if (!prevented) {
243
+ Linking.openURL(href).catch((error: unknown) => {
244
+ console.warn(`[duro] Menu.LinkItem: could not open ${href}: ${String(error)}`)
245
+ })
246
+ }
247
+ }
248
+ return (
249
+ <Pressable accessibilityRole="link" onPress={() => closeThen(follow)}>
250
+ {({pressed}) => (
251
+ <html.div style={[styles.item, pressed && nativeStyles.itemPressed]}>{children}</html.div>
252
+ )}
253
+ </Pressable>
254
+ )
255
+ }
256
+
257
+ function Separator() {
258
+ return <html.div role="separator" aria-orientation="horizontal" style={styles.separator} />
259
+ }
260
+
261
+ export const Menu = {
262
+ Root,
263
+ Trigger,
264
+ Popup,
265
+ Item,
266
+ LinkItem,
267
+ Separator,
268
+ }
@@ -15,6 +15,7 @@ import {styles} from './styles.css'
15
15
  import {MenuContext, useMenu} from './MenuContext'
16
16
  import {useMenuRoot} from './useMenuRoot'
17
17
  import {devWarnOnce} from '../../shared/devWarnOnce'
18
+ import {isPlainClick, type LinkClickEvent, type OnNavigate} from '../../shared/navigate'
18
19
  import type {ControlSize} from '../../shared/types'
19
20
  import {usePortalMount} from '../ThemeProvider/ThemeProvider'
20
21
  import {computePopoverPosition} from '../Popover/position'
@@ -222,10 +223,15 @@ function Item({onClick, children}: ItemProps) {
222
223
  // --- LinkItem ---
223
224
  interface LinkItemProps {
224
225
  href: string
226
+ /**
227
+ * Called for a plain primary click (ADR-0002): call `event.preventDefault()`,
228
+ * then your router's navigate. Without it the browser follows the link.
229
+ */
230
+ onNavigate?: OnNavigate
225
231
  children: ReactNode
226
232
  }
227
233
 
228
- function LinkItem({href, children}: LinkItemProps) {
234
+ function LinkItem({href, onNavigate, children}: LinkItemProps) {
229
235
  const {close, highlightedId, setHighlightedId, registerItem} = useMenu()
230
236
  const id = useId()
231
237
  const ref = useRef<HTMLAnchorElement>(null)
@@ -242,7 +248,10 @@ function LinkItem({href, children}: LinkItemProps) {
242
248
  ref={ref}
243
249
  id={id}
244
250
  href={href}
245
- onClick={() => close()}
251
+ onClick={(event: LinkClickEvent) => {
252
+ if (onNavigate && isPlainClick(event)) onNavigate(href, event)
253
+ close()
254
+ }}
246
255
  role="menuitem"
247
256
  onPointerEnter={() => setHighlightedId(id)}
248
257
  style={[styles.item, styles.linkItem, isHighlighted && styles.itemHighlighted]}
@@ -0,0 +1,18 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+
4
+ /** Native Menu additions to the shared styles. */
5
+ export const nativeStyles = css.create({
6
+ // The AnchoredOverlay's view places the popup; it flows inside it.
7
+ // RSD on native lays out only display: flex, not the web's inline-flex.
8
+ nativeFlex: {
9
+ display: 'flex',
10
+ },
11
+ popupInPlace: {
12
+ position: 'relative',
13
+ },
14
+ // A press shows the highlight that hover and the arrow keys show on the web.
15
+ itemPressed: {
16
+ backgroundColor: colors.bgCardHover,
17
+ },
18
+ })
@@ -0,0 +1,33 @@
1
+ import {useCallback, useRef, useState} from 'react'
2
+
3
+ export interface MenuOpenOptions {
4
+ open?: boolean
5
+ defaultOpen?: boolean
6
+ onOpenChange?: (open: boolean) => void
7
+ }
8
+
9
+ /**
10
+ * The menu's open state, controlled or not, shared by both platforms (plan
11
+ * N0, Decision 10). Focus, highlight and keys are each platform's adapter:
12
+ * `useMenuRoot` on the web, Menu.native on native.
13
+ */
14
+ export function useMenuOpen({open: openProp, defaultOpen = false, onOpenChange}: MenuOpenOptions) {
15
+ const [inner, setInner] = useState(defaultOpen)
16
+ const controlled = openProp !== undefined
17
+ const open = controlled ? openProp : inner
18
+ const openRef = useRef(open)
19
+ openRef.current = open
20
+ const onOpenChangeRef = useRef(onOpenChange)
21
+ onOpenChangeRef.current = onOpenChange
22
+
23
+ const setOpen = useCallback(
24
+ (next: boolean) => {
25
+ if (next === openRef.current) return
26
+ if (!controlled) setInner(next)
27
+ onOpenChangeRef.current?.(next)
28
+ },
29
+ [controlled],
30
+ )
31
+
32
+ return {open, openRef, setOpen}
33
+ }
@@ -1,12 +1,9 @@
1
1
  import {useState, useCallback, useRef, useId, useEffect, useLayoutEffect} from 'react'
2
2
  import type {MenuContextValue} from './MenuContext'
3
3
  import {isOutsidePress} from '../Popover/outside'
4
+ import {useMenuOpen, type MenuOpenOptions} from './useMenuOpen'
4
5
 
5
- export interface MenuRootOptions {
6
- open?: boolean
7
- defaultOpen?: boolean
8
- onOpenChange?: (open: boolean) => void
9
- }
6
+ export type MenuRootOptions = MenuOpenOptions
10
7
 
11
8
  /**
12
9
  * The menu-button pattern (WAI-ARIA APG): on open, focus moves to the
@@ -16,10 +13,8 @@ export interface MenuRootOptions {
16
13
  * elsewhere (an editor) are never intercepted, and Escape stops there so an
17
14
  * enclosing Popover or Dialog stays open.
18
15
  */
19
- export function useMenuRoot({open: openProp, defaultOpen = false, onOpenChange}: MenuRootOptions) {
20
- const [inner, setInner] = useState(defaultOpen)
21
- const controlled = openProp !== undefined
22
- const open = controlled ? openProp : inner
16
+ export function useMenuRoot(options: MenuRootOptions) {
17
+ const {open, openRef, setOpen} = useMenuOpen(options)
23
18
  const [highlightedId, setHighlightedId] = useState<string | null>(null)
24
19
  // Read by the keydown handler, so activating an item is not a side effect
25
20
  // inside a state updater (React may run updaters twice).
@@ -34,38 +29,24 @@ export function useMenuRoot({open: openProp, defaultOpen = false, onOpenChange}:
34
29
  const popupRef = useRef<HTMLDivElement | null>(null)
35
30
  const itemsRef = useRef(new Map<string, HTMLElement>())
36
31
  const orderRef = useRef<string[]>([])
37
- const openRef = useRef(open)
38
- openRef.current = open
39
- const onOpenChangeRef = useRef(onOpenChange)
40
- onOpenChangeRef.current = onOpenChange
41
-
42
- const setOpen = useCallback(
43
- (next: boolean) => {
44
- if (next === openRef.current) return
45
- if (!controlled) setInner(next)
46
- onOpenChangeRef.current?.(next)
47
- },
48
- [controlled],
49
- )
50
-
51
32
  // Closes and leaves focus where it is (a press elsewhere put it there).
52
33
  const dismiss = useCallback(() => {
53
34
  if (!openRef.current) return
54
35
  setOpen(false)
55
36
  setHighlightedId(null)
56
- }, [setOpen])
37
+ }, [openRef, setOpen])
57
38
 
58
39
  // Closes and puts focus back on the trigger.
59
40
  const close = useCallback(() => {
60
41
  if (!openRef.current) return
61
42
  dismiss()
62
43
  triggerRef.current?.focus()
63
- }, [dismiss])
44
+ }, [openRef, dismiss])
64
45
 
65
46
  const toggle = useCallback(() => {
66
47
  if (openRef.current) close()
67
48
  else setOpen(true)
68
- }, [close, setOpen])
49
+ }, [openRef, close, setOpen])
69
50
 
70
51
  // On open: focus the menu and highlight the first item. Child effects (item
71
52
  // registration) run before this parent effect, so orderRef is populated.
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size="small" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal.',
5
+ 'Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size="small" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal. On React Native the popup is modal: it is placed against Popover.Trigger in an overlay, a press outside or the Android back button closes it, and screen-reader focus moves in on open and back to the trigger on close; anchor, ignore and raised are web only.',
6
6
  whenToUse: [
7
7
  'A small form or controls tied to one element that stay open while you work in them',
8
8
  'Anchoring to a spot that is not a button (a canvas point, a text selection) with a virtual anchor',