@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
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it).',
5
+ 'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it). On React Native the same API draws through the portal host above every open overlay and the keyboard, as a card with a status-coloured edge, its action and its close in one row; a sideways swipe dismisses, a press holds the countdown, and so does an active screen reader (WCAG 2.2.1).',
6
6
  whenToUse: [
7
7
  'Confirming an action succeeded (e.g. "Grant revoked")',
8
8
  'Reporting an action failed, with the reason',
@@ -0,0 +1,226 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ useSyncExternalStore,
11
+ } from 'react'
12
+ import {
13
+ AccessibilityInfo,
14
+ type LayoutChangeEvent,
15
+ Platform,
16
+ Pressable,
17
+ StyleSheet,
18
+ } from 'react-native'
19
+ import {Gesture, GestureDetector} from 'react-native-gesture-handler'
20
+ import Animated, {
21
+ Easing,
22
+ useAnimatedStyle,
23
+ useSharedValue,
24
+ withTiming,
25
+ } from 'react-native-reanimated'
26
+ import {scheduleOnRN} from 'react-native-worklets'
27
+ import {html} from 'react-strict-dom'
28
+ import {DURATION_MS, LAYERS, SPACING_PX} from '@duro-app/tokens/keys'
29
+ import {Icon} from '../Icon/Icon'
30
+ import {Portal} from '../../shared/portal/PortalHost'
31
+ import {useSafeAreaInsets} from '../../shared/safeArea'
32
+ import {useKeyboardHeight} from '../../shared/useKeyboardHeight'
33
+ import {useScreenReaderEnabled} from '../../shared/useScreenReaderEnabled'
34
+ import {DISMISS_DURATION, DRAG_SLOP, SNAP_DURATION, shouldDismissEdge} from '../Drawer/sheetPhysics'
35
+ import {nativeStyles} from './native.css'
36
+ import {createToastStore, type ToastContextValue, type ToastEntry, type ToastStore} from './store'
37
+
38
+ export type {ToastAction, ToastContextValue, ToastOptions, ToastVariant} from './store'
39
+
40
+ const ToastContext = createContext<ToastContextValue | null>(null)
41
+
42
+ export function useToast(): ToastContextValue {
43
+ const ctx = useContext(ToastContext)
44
+ if (!ctx) throw new Error('useToast must be used within a <ToastProvider>')
45
+ return ctx
46
+ }
47
+
48
+ const easeOut = Easing.out(Easing.cubic)
49
+
50
+ function ToastItem({
51
+ entry,
52
+ store,
53
+ onDismiss,
54
+ dismissLabel,
55
+ screenReader,
56
+ }: {
57
+ entry: ToastEntry
58
+ store: ToastStore
59
+ onDismiss: (id: string) => void
60
+ dismissLabel: string
61
+ screenReader: boolean
62
+ }) {
63
+ const {id, variant, message, action, duration} = entry
64
+ const [pressed, setPressed] = useState(false)
65
+ // A press holds the toast as hover does on the web; a screen reader holds
66
+ // it for as long as it is on (WCAG 2.2.1).
67
+ const paused = pressed || screenReader
68
+
69
+ // The store keeps what is left, so a toast that moves to another host
70
+ // (a Dialog above it closed) carries on where it was.
71
+ useEffect(() => {
72
+ if (duration <= 0 || paused) return
73
+ const started = Date.now()
74
+ const timer = setTimeout(() => onDismiss(id), Math.max(store.remaining(id), 0))
75
+ return () => {
76
+ clearTimeout(timer)
77
+ store.spend(id, Date.now() - started)
78
+ }
79
+ }, [id, duration, paused, onDismiss, store])
80
+
81
+ // iOS has no live regions: announce the message once. Android reads the
82
+ // live region itself, so announcing there too would read it twice.
83
+ const announced = useRef(false)
84
+ useEffect(() => {
85
+ if (Platform.OS !== 'ios' || announced.current || typeof message !== 'string') return
86
+ announced.current = true
87
+ AccessibilityInfo.announceForAccessibility(message)
88
+ }, [message])
89
+
90
+ const x = useSharedValue(0)
91
+ const shown = useSharedValue(0)
92
+ const width = useSharedValue(1)
93
+ useEffect(() => {
94
+ shown.value = withTiming(1, {duration: DURATION_MS.brisk, easing: easeOut})
95
+ }, [shown])
96
+
97
+ const pan = useMemo(
98
+ () =>
99
+ Gesture.Pan()
100
+ .activeOffsetX([-DRAG_SLOP, DRAG_SLOP])
101
+ .failOffsetY([-DRAG_SLOP, DRAG_SLOP])
102
+ .onUpdate((e) => {
103
+ x.value = e.translationX
104
+ })
105
+ .onEnd((e) => {
106
+ const distance = Math.abs(e.translationX)
107
+ if (shouldDismissEdge(distance, width.value, Math.abs(e.velocityX) / 1000)) {
108
+ const off = Math.sign(e.translationX || e.velocityX) * width.value
109
+ x.value = withTiming(off, {duration: DISMISS_DURATION}, (done) => {
110
+ if (done) scheduleOnRN(onDismiss, id)
111
+ })
112
+ } else {
113
+ x.value = withTiming(0, {duration: SNAP_DURATION, easing: easeOut})
114
+ }
115
+ }),
116
+ [id, onDismiss, x, width],
117
+ )
118
+
119
+ const motion = useAnimatedStyle(() => ({
120
+ opacity: shown.value * (1 - Math.min(1, Math.abs(x.value) / width.value)),
121
+ transform: [{translateX: x.value}, {translateY: (1 - shown.value) * SPACING_PX.sm}],
122
+ }))
123
+
124
+ const onLayout = (e: LayoutChangeEvent) => {
125
+ width.value = e.nativeEvent.layout.width
126
+ }
127
+
128
+ return (
129
+ <GestureDetector gesture={pan}>
130
+ <Animated.View onLayout={onLayout} style={[layout.row, motion]}>
131
+ <Pressable
132
+ onPressIn={() => setPressed(true)}
133
+ onPressOut={() => setPressed(false)}
134
+ accessible={false}
135
+ style={layout.row}
136
+ >
137
+ <html.div
138
+ role={variant === 'error' ? 'alert' : 'status'}
139
+ aria-live={variant === 'error' ? 'assertive' : 'polite'}
140
+ style={[nativeStyles.toast, nativeStyles[variant]]}
141
+ >
142
+ <html.div style={nativeStyles.message}>{message}</html.div>
143
+ {action ? (
144
+ <html.button
145
+ type="button"
146
+ style={nativeStyles.action}
147
+ onClick={() => {
148
+ action.onClick()
149
+ onDismiss(id)
150
+ }}
151
+ >
152
+ {action.label}
153
+ </html.button>
154
+ ) : null}
155
+ <html.button
156
+ type="button"
157
+ aria-label={dismissLabel}
158
+ style={nativeStyles.close}
159
+ onClick={() => onDismiss(id)}
160
+ >
161
+ <Icon name="x" size="sm" color="textMuted" />
162
+ </html.button>
163
+ </html.div>
164
+ </Pressable>
165
+ </Animated.View>
166
+ </GestureDetector>
167
+ )
168
+ }
169
+
170
+ // Layout-only: each toast row spans the region and centres its card.
171
+ const layout = StyleSheet.create({row: {width: '100%', alignItems: 'center'}})
172
+
173
+ export interface ToastProviderProps {
174
+ /** Accessible name of each toast's close button, e.g. from the app's message catalog. */
175
+ dismissLabel?: string
176
+ children: ReactNode
177
+ }
178
+
179
+ /**
180
+ * Native ToastProvider (plan N0, Slice B): the same store and countdown as
181
+ * the web, drawn through the portal host at `LAYERS.toast`, so a toast shows
182
+ * above the app, every open Duro overlay (a sheet included) and the
183
+ * keyboard. A sideways swipe or the close button dismisses it.
184
+ */
185
+ export function ToastProvider({dismissLabel = 'Dismiss', children}: ToastProviderProps) {
186
+ const storeRef = useRef<ToastStore>(null)
187
+ if (storeRef.current === null) storeRef.current = createToastStore()
188
+ const store = storeRef.current
189
+ const entries = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
190
+ const insets = useSafeAreaInsets()
191
+ const keyboard = useKeyboardHeight()
192
+ const screenReader = useScreenReaderEnabled()
193
+
194
+ const value = useMemo<ToastContextValue>(
195
+ () => ({toast: (opts) => store.add(opts), dismiss: (id) => store.remove(id)}),
196
+ [store],
197
+ )
198
+ const dismiss = useCallback((id: string) => store.remove(id), [store])
199
+ const bottom = Math.max(insets.bottom, keyboard) + SPACING_PX.md
200
+
201
+ return (
202
+ <ToastContext.Provider value={value}>
203
+ {children}
204
+ {entries.length > 0 && (
205
+ <Portal layer={LAYERS.toast}>
206
+ <html.div
207
+ role="region"
208
+ aria-label="Notifications"
209
+ style={[nativeStyles.region, nativeStyles.regionBottom(bottom)]}
210
+ >
211
+ {entries.map((entry) => (
212
+ <ToastItem
213
+ key={entry.id}
214
+ entry={entry}
215
+ store={store}
216
+ onDismiss={dismiss}
217
+ dismissLabel={dismissLabel}
218
+ screenReader={screenReader}
219
+ />
220
+ ))}
221
+ </html.div>
222
+ </Portal>
223
+ )}
224
+ </ToastContext.Provider>
225
+ )
226
+ }
@@ -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,9 +1,9 @@
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
8
  import {isNative} from '../../platform'
9
9
  import {styles} from './styles.css'
@@ -84,7 +84,7 @@ export function Toggle({
84
84
  }, [disabled, isGrouped, value, group, pressed, setStandalonePressed])
85
85
 
86
86
  return (
87
- <html.button
87
+ <ButtonHost
88
88
  ref={mergeRefs(ref, grouped.ref)}
89
89
  type="button"
90
90
  tabIndex={
@@ -116,6 +116,6 @@ export function Toggle({
116
116
  ]}
117
117
  >
118
118
  {children}
119
- </html.button>
119
+ </ButtonHost>
120
120
  )
121
121
  }
@@ -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',