@duro-app/ui 4.4.0 → 5.0.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 (273) hide show
  1. package/dist/{Table-JTgcR_Fh.js → Table-BKN_d-mp.js} +506 -410
  2. package/dist/Table-BKN_d-mp.js.map +1 -0
  3. package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -1
  4. package/dist/components/ActionBar/styles.css.d.ts +7 -7
  5. package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
  6. package/dist/components/Alert/styles.css.d.ts +2 -2
  7. package/dist/components/Alert/styles.css.d.ts.map +1 -1
  8. package/dist/components/Badge/styles.css.d.ts +2 -2
  9. package/dist/components/Button/styles.css.d.ts +5 -5
  10. package/dist/components/Button/styles.css.d.ts.map +1 -1
  11. package/dist/components/Callout/styles.css.d.ts +1 -1
  12. package/dist/components/Callout/styles.css.d.ts.map +1 -1
  13. package/dist/components/Card/styles.css.d.ts +3 -3
  14. package/dist/components/Card/styles.css.d.ts.map +1 -1
  15. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  16. package/dist/components/Checkbox/styles.css.d.ts +7 -11
  17. package/dist/components/Checkbox/styles.css.d.ts.map +1 -1
  18. package/dist/components/Cluster/Cluster.stories.d.ts.map +1 -1
  19. package/dist/components/ColorInput/styles.css.d.ts +6 -6
  20. package/dist/components/ColorInput/styles.css.d.ts.map +1 -1
  21. package/dist/components/ColorMode/styles.css.d.ts +3 -3
  22. package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
  23. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  24. package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -1
  25. package/dist/components/Combobox/styles.css.d.ts +3 -3
  26. package/dist/components/Combobox/styles.css.d.ts.map +1 -1
  27. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  28. package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -1
  29. package/dist/components/DetailPanel/styles.css.d.ts +19 -10
  30. package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
  31. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  32. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  33. package/dist/components/Dialog/styles.css.d.ts +18 -9
  34. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  35. package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
  36. package/dist/components/DragDrop/DragDrop.stories.d.ts.map +1 -1
  37. package/dist/components/DragDrop/styles.css.d.ts +0 -6
  38. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  39. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  40. package/dist/components/Drawer/DrawerScroll.stories.d.ts +13 -0
  41. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -0
  42. package/dist/components/Drawer/styles.css.d.ts +19 -10
  43. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  44. package/dist/components/Drawer/useSwipeDismiss.d.ts +7 -2
  45. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  46. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  47. package/dist/components/Field/styles.css.d.ts +1 -1
  48. package/dist/components/Field/styles.css.d.ts.map +1 -1
  49. package/dist/components/Form/Form.stories.d.ts.map +1 -1
  50. package/dist/components/Grid/Grid.d.ts +6 -4
  51. package/dist/components/Grid/Grid.d.ts.map +1 -1
  52. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  53. package/dist/components/Grid/styles.css.d.ts +3 -3
  54. package/dist/components/Input/Input.stories.d.ts.map +1 -1
  55. package/dist/components/Input/styles.css.d.ts +3 -3
  56. package/dist/components/Input/styles.css.d.ts.map +1 -1
  57. package/dist/components/InputGroup/InputGroup.d.ts +4 -2
  58. package/dist/components/InputGroup/InputGroup.d.ts.map +1 -1
  59. package/dist/components/InputGroup/InputGroup.stories.d.ts.map +1 -1
  60. package/dist/components/InputGroup/styles.css.d.ts +3 -3
  61. package/dist/components/InputGroup/styles.css.d.ts.map +1 -1
  62. package/dist/components/LinkButton/styles.css.d.ts +1 -1
  63. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  64. package/dist/components/List/List.stories.d.ts.map +1 -1
  65. package/dist/components/List/styles.css.d.ts +3 -3
  66. package/dist/components/List/styles.css.d.ts.map +1 -1
  67. package/dist/components/Menu/styles.css.d.ts +4 -4
  68. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  69. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  70. package/dist/components/PageShell/styles.css.d.ts +3 -3
  71. package/dist/components/PageShell/styles.css.d.ts.map +1 -1
  72. package/dist/components/Panel/styles.css.d.ts +3 -3
  73. package/dist/components/Panel/styles.css.d.ts.map +1 -1
  74. package/dist/components/Popover/Popover.d.ts.map +1 -1
  75. package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
  76. package/dist/components/Popover/Popover.stories.d.ts.map +1 -1
  77. package/dist/components/Popover/PopoverLayerContext.d.ts +10 -0
  78. package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -0
  79. package/dist/components/Popover/PopoverLayers.stories.d.ts +13 -0
  80. package/dist/components/Popover/PopoverLayers.stories.d.ts.map +1 -0
  81. package/dist/components/Popover/styles.css.d.ts +2 -2
  82. package/dist/components/Popover/styles.css.d.ts.map +1 -1
  83. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  84. package/dist/components/RadioGroup/styles.css.d.ts +5 -9
  85. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
  86. package/dist/components/ScrollArea/ScrollArea.d.ts +10 -3
  87. package/dist/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  88. package/dist/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  89. package/dist/components/ScrollArea/styles.css.d.ts +8 -8
  90. package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
  91. package/dist/components/Select/Select.d.ts.map +1 -1
  92. package/dist/components/Select/Select.meta.d.ts.map +1 -1
  93. package/dist/components/Select/styles.css.d.ts +4 -4
  94. package/dist/components/Select/styles.css.d.ts.map +1 -1
  95. package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
  96. package/dist/components/SideNav/styles.css.d.ts +7 -7
  97. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  98. package/dist/components/Spinner/Spinner.d.ts.map +1 -1
  99. package/dist/components/Spinner/styles.css.d.ts +10 -14
  100. package/dist/components/Spinner/styles.css.d.ts.map +1 -1
  101. package/dist/components/Switch/Switch.d.ts.map +1 -1
  102. package/dist/components/Switch/styles.css.d.ts +10 -14
  103. package/dist/components/Switch/styles.css.d.ts.map +1 -1
  104. package/dist/components/Table/ColumnFilter.d.ts.map +1 -1
  105. package/dist/components/Table/FromTanstack.d.ts +2 -1
  106. package/dist/components/Table/FromTanstack.d.ts.map +1 -1
  107. package/dist/components/Table/Pagination.d.ts.map +1 -1
  108. package/dist/components/Table/SortIndicator.d.ts.map +1 -1
  109. package/dist/components/Table/Table.d.ts +11 -9
  110. package/dist/components/Table/Table.d.ts.map +1 -1
  111. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  112. package/dist/components/Table/styles.css.d.ts +6 -6
  113. package/dist/components/Table/styles.css.d.ts.map +1 -1
  114. package/dist/components/Tabs/styles.css.d.ts +8 -8
  115. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  116. package/dist/components/Tag/Tag.stories.d.ts.map +1 -1
  117. package/dist/components/Tag/styles.css.d.ts +4 -4
  118. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  119. package/dist/components/TagGroup/TagGroup.d.ts.map +1 -1
  120. package/dist/components/TagGroup/styles.css.d.ts +4 -10
  121. package/dist/components/TagGroup/styles.css.d.ts.map +1 -1
  122. package/dist/components/Textarea/Textarea.stories.d.ts.map +1 -1
  123. package/dist/components/Textarea/styles.css.d.ts +3 -3
  124. package/dist/components/Textarea/styles.css.d.ts.map +1 -1
  125. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
  126. package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
  127. package/dist/components/Toast/styles.css.d.ts +12 -12
  128. package/dist/components/Toast/styles.css.d.ts.map +1 -1
  129. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  130. package/dist/components/Toggle/styles.css.d.ts +14 -4
  131. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  132. package/dist/components/ToggleGroup/ToggleGroup.d.ts +7 -1
  133. package/dist/components/ToggleGroup/ToggleGroup.d.ts.map +1 -1
  134. package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts.map +1 -1
  135. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +4 -0
  136. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts.map +1 -1
  137. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +6 -0
  138. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts.map +1 -1
  139. package/dist/components/ToggleGroup/styles.css.d.ts +20 -1
  140. package/dist/components/ToggleGroup/styles.css.d.ts.map +1 -1
  141. package/dist/components/ToggleGroup/useRovingFocus.d.ts +17 -0
  142. package/dist/components/ToggleGroup/useRovingFocus.d.ts.map +1 -0
  143. package/dist/components/Tooltip/styles.css.d.ts +1 -1
  144. package/dist/components/Tooltip/styles.css.d.ts.map +1 -1
  145. package/dist/components/Tree/styles.css.d.ts +4 -4
  146. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  147. package/dist/components/VirtualTable/VirtualTable.d.ts +2 -1
  148. package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
  149. package/dist/components/VirtualTable/VirtualTable.stories.d.ts.map +1 -1
  150. package/dist/components/VirtualTable/styles.css.d.ts +7 -7
  151. package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
  152. package/dist/docs/Spacing.stories.d.ts.map +1 -1
  153. package/dist/docs/Typography.stories.d.ts.map +1 -1
  154. package/dist/docs/helpers.d.ts.map +1 -1
  155. package/dist/index.css +1 -1
  156. package/dist/index.js +3788 -3515
  157. package/dist/index.js.map +1 -1
  158. package/dist/shared/length.d.ts +24 -0
  159. package/dist/shared/length.d.ts.map +1 -0
  160. package/dist/shared/length.typecheck.d.ts +28 -0
  161. package/dist/shared/length.typecheck.d.ts.map +1 -0
  162. package/dist/styles/visually-hidden.css.d.ts +10 -0
  163. package/dist/styles/visually-hidden.css.d.ts.map +1 -0
  164. package/dist/table.js +166 -166
  165. package/dist/table.js.map +1 -1
  166. package/package.json +2 -2
  167. package/src/components/ActionBar/ActionBar.stories.tsx +4 -2
  168. package/src/components/ActionBar/styles.css.ts +11 -9
  169. package/src/components/Alert/styles.css.ts +4 -3
  170. package/src/components/Badge/styles.css.ts +3 -3
  171. package/src/components/Button/styles.css.ts +8 -7
  172. package/src/components/Callout/styles.css.ts +2 -1
  173. package/src/components/Card/styles.css.ts +4 -3
  174. package/src/components/Checkbox/Checkbox.tsx +2 -1
  175. package/src/components/Checkbox/styles.css.ts +10 -11
  176. package/src/components/Cluster/Cluster.stories.tsx +2 -1
  177. package/src/components/ColorInput/styles.css.ts +9 -7
  178. package/src/components/ColorMode/styles.css.ts +4 -3
  179. package/src/components/Combobox/Combobox.meta.ts +6 -0
  180. package/src/components/Combobox/Combobox.tsx +5 -0
  181. package/src/components/Combobox/styles.css.ts +5 -3
  182. package/src/components/DetailPanel/DetailPanel.stories.tsx +5 -3
  183. package/src/components/DetailPanel/DetailPanel.tsx +3 -0
  184. package/src/components/DetailPanel/styles.css.ts +17 -11
  185. package/src/components/Dialog/Dialog.stories.tsx +2 -1
  186. package/src/components/Dialog/Dialog.tsx +3 -0
  187. package/src/components/Dialog/styles.css.ts +14 -9
  188. package/src/components/DragDrop/DragDrop.stories.tsx +2 -1
  189. package/src/components/DragDrop/DragDrop.tsx +2 -1
  190. package/src/components/DragDrop/styles.css.ts +0 -7
  191. package/src/components/Drawer/Drawer.meta.ts +1 -1
  192. package/src/components/Drawer/Drawer.tsx +15 -2
  193. package/src/components/Drawer/DrawerScroll.stories.tsx +292 -0
  194. package/src/components/Drawer/styles.css.ts +15 -10
  195. package/src/components/Drawer/useSwipeDismiss.ts +53 -8
  196. package/src/components/Field/Field.stories.tsx +3 -2
  197. package/src/components/Field/styles.css.ts +2 -1
  198. package/src/components/Form/Form.stories.tsx +4 -3
  199. package/src/components/Grid/Grid.meta.ts +2 -2
  200. package/src/components/Grid/Grid.stories.tsx +3 -2
  201. package/src/components/Grid/Grid.tsx +8 -6
  202. package/src/components/Grid/styles.css.ts +2 -2
  203. package/src/components/Input/Input.stories.tsx +2 -1
  204. package/src/components/Input/styles.css.ts +4 -3
  205. package/src/components/InputGroup/InputGroup.stories.tsx +3 -2
  206. package/src/components/InputGroup/InputGroup.tsx +5 -3
  207. package/src/components/InputGroup/styles.css.ts +4 -3
  208. package/src/components/LinkButton/styles.css.ts +2 -1
  209. package/src/components/List/List.stories.tsx +4 -2
  210. package/src/components/List/styles.css.ts +6 -4
  211. package/src/components/Menu/styles.css.ts +6 -4
  212. package/src/components/Menu/useMenuRoot.ts +5 -1
  213. package/src/components/PageShell/styles.css.ts +4 -3
  214. package/src/components/Panel/styles.css.ts +4 -3
  215. package/src/components/Popover/Popover.meta.ts +7 -0
  216. package/src/components/Popover/Popover.stories.tsx +6 -4
  217. package/src/components/Popover/Popover.tsx +30 -24
  218. package/src/components/Popover/PopoverLayerContext.ts +20 -0
  219. package/src/components/Popover/PopoverLayers.stories.tsx +234 -0
  220. package/src/components/Popover/styles.css.ts +3 -2
  221. package/src/components/RadioGroup/RadioGroup.tsx +2 -1
  222. package/src/components/RadioGroup/styles.css.ts +8 -9
  223. package/src/components/ScrollArea/ScrollArea.meta.ts +2 -2
  224. package/src/components/ScrollArea/ScrollArea.stories.tsx +7 -5
  225. package/src/components/ScrollArea/ScrollArea.tsx +18 -7
  226. package/src/components/ScrollArea/styles.css.ts +10 -9
  227. package/src/components/Select/Select.meta.ts +6 -0
  228. package/src/components/Select/Select.tsx +8 -1
  229. package/src/components/Select/styles.css.ts +6 -4
  230. package/src/components/Select/useSelectRoot.ts +6 -2
  231. package/src/components/SideNav/styles.css.ts +10 -8
  232. package/src/components/Spinner/Spinner.tsx +2 -1
  233. package/src/components/Spinner/styles.css.ts +14 -14
  234. package/src/components/Switch/Switch.tsx +2 -1
  235. package/src/components/Switch/styles.css.ts +15 -15
  236. package/src/components/Table/ColumnFilter.tsx +5 -4
  237. package/src/components/Table/FromTanstack.tsx +2 -1
  238. package/src/components/Table/Pagination.tsx +6 -4
  239. package/src/components/Table/SortIndicator.tsx +4 -2
  240. package/src/components/Table/Table.stories.tsx +8 -8
  241. package/src/components/Table/Table.tsx +19 -15
  242. package/src/components/Table/styles.css.ts +9 -7
  243. package/src/components/Tabs/styles.css.ts +11 -9
  244. package/src/components/Tag/Tag.stories.tsx +4 -2
  245. package/src/components/Tag/styles.css.ts +6 -5
  246. package/src/components/TagGroup/TagGroup.tsx +2 -1
  247. package/src/components/TagGroup/styles.css.ts +6 -10
  248. package/src/components/Textarea/Textarea.stories.tsx +2 -1
  249. package/src/components/Textarea/styles.css.ts +4 -3
  250. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +7 -5
  251. package/src/components/Toast/Toast.stories.tsx +2 -1
  252. package/src/components/Toast/styles.css.ts +16 -14
  253. package/src/components/Toggle/Toggle.meta.ts +1 -1
  254. package/src/components/Toggle/Toggle.tsx +19 -2
  255. package/src/components/Toggle/styles.css.ts +18 -4
  256. package/src/components/ToggleGroup/ToggleGroup.meta.ts +11 -1
  257. package/src/components/ToggleGroup/ToggleGroup.stories.tsx +157 -0
  258. package/src/components/ToggleGroup/ToggleGroup.tsx +87 -10
  259. package/src/components/ToggleGroup/ToggleGroupContext.ts +6 -0
  260. package/src/components/ToggleGroup/styles.css.ts +32 -2
  261. package/src/components/ToggleGroup/useRovingFocus.ts +121 -0
  262. package/src/components/Tooltip/styles.css.ts +2 -1
  263. package/src/components/Tree/styles.css.ts +8 -6
  264. package/src/components/VirtualTable/VirtualTable.stories.tsx +4 -3
  265. package/src/components/VirtualTable/VirtualTable.tsx +5 -3
  266. package/src/components/VirtualTable/styles.css.ts +9 -8
  267. package/src/docs/Spacing.stories.tsx +11 -10
  268. package/src/docs/Typography.stories.tsx +8 -6
  269. package/src/docs/helpers.tsx +3 -2
  270. package/src/shared/length.ts +51 -0
  271. package/src/shared/length.typecheck.tsx +47 -0
  272. package/src/styles/visually-hidden.css.ts +16 -0
  273. package/dist/Table-JTgcR_Fh.js.map +0 -1
@@ -0,0 +1,292 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, waitFor} from 'storybook/test'
3
+ import {cdp, page} from 'vitest/browser'
4
+ import {Drawer} from './Drawer'
5
+ import {ToggleGroup} from '../ToggleGroup/ToggleGroup'
6
+ import {Toggle} from '../Toggle/Toggle'
7
+ import {Icon, type IconName} from '../Icon'
8
+ import {SIZES_PX} from '@duro-app/tokens/keys'
9
+
10
+ const meta: Meta = {
11
+ title: 'Components/Drawer/Scroll',
12
+ parameters: {layout: 'fullscreen'},
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj
17
+
18
+ const ICON_NAMES: IconName[] = [
19
+ 'x-circle',
20
+ 'check-circle',
21
+ 'check-done',
22
+ 'clock',
23
+ 'forbidden',
24
+ 'info-circle',
25
+ 'alert-triangle',
26
+ 'shield',
27
+ 'lock',
28
+ 'key',
29
+ 'map',
30
+ 'layers',
31
+ 'repeat',
32
+ 'database',
33
+ 'shield-check',
34
+ 'route',
35
+ 'git-branch',
36
+ 'menu',
37
+ 'pin',
38
+ 'server',
39
+ 'hard-drive',
40
+ 'box',
41
+ 'image',
42
+ 'tag',
43
+ 'pie-chart',
44
+ 'users',
45
+ 'user-plus',
46
+ 'mail',
47
+ 'file-text',
48
+ 'plug',
49
+ 'search',
50
+ 'mic',
51
+ 'sun',
52
+ 'moon',
53
+ 'monitor',
54
+ 'contrast',
55
+ 'info-circle-filled',
56
+ 'alert-triangle-filled',
57
+ 'check-circle-filled',
58
+ 'x-circle-filled',
59
+ 'shield-filled',
60
+ 'lock-filled',
61
+ ]
62
+
63
+ function Scene({pressed}: {pressed?: string} = {}) {
64
+ return (
65
+ <Drawer.Root anchor="bottom" defaultOpen>
66
+ <Drawer.Portal>
67
+ <Drawer.Header>
68
+ <Drawer.Title>Pick an icon</Drawer.Title>
69
+ </Drawer.Header>
70
+ <Drawer.Body>
71
+ <ToggleGroup
72
+ wrap
73
+ maxRows={3}
74
+ size="small"
75
+ aria-label="Icon"
76
+ defaultValue={pressed ? [pressed] : []}
77
+ >
78
+ {ICON_NAMES.map((name) => (
79
+ <Toggle key={name} value={name} aria-label={name}>
80
+ <Icon name={name} size="sm" />
81
+ </Toggle>
82
+ ))}
83
+ </ToggleGroup>
84
+ </Drawer.Body>
85
+ </Drawer.Portal>
86
+ </Drawer.Root>
87
+ )
88
+ }
89
+
90
+ type Point = {x: number; y: number}
91
+
92
+ /** `CDPSession` is empty until the provider's typings load; `send` exists at runtime. */
93
+ const cdpSend = (method: string, params: object) =>
94
+ (cdp() as unknown as {send: (method: string, params: object) => Promise<unknown>}).send(
95
+ method,
96
+ params,
97
+ )
98
+
99
+ const pause = (ms: number) => new Promise((r) => setTimeout(r, ms))
100
+
101
+ /**
102
+ * CDP input lands in top-level page coordinates; the story runs in an iframe
103
+ * the test runner offsets and may scale to fit. Map a point up every frame.
104
+ */
105
+ function toPage(p: Point): Point {
106
+ let {x, y} = p
107
+ let win: Window = window
108
+ while (win.frameElement && win.parent !== win) {
109
+ const rect = win.frameElement.getBoundingClientRect()
110
+ const scale = rect.width / win.innerWidth
111
+ x = rect.left + x * scale
112
+ y = rect.top + y * scale
113
+ win = win.parent
114
+ }
115
+ return {x, y}
116
+ }
117
+
118
+ /**
119
+ * A page-coordinate touch point with a 1px radius: Chrome's touch adjustment
120
+ * snaps a fat default touch onto the nearest control, which would turn every
121
+ * drag in the list into a drag on a toggle.
122
+ */
123
+ const touchAt = (p: Point) => ({...toPage(p), radiusX: 1, radiusY: 1})
124
+
125
+ /** Real touch drag through CDP; synthetic pointer events don't scroll. */
126
+ async function touchDrag(from: Point, to: Point) {
127
+ const steps = 12
128
+ const send = (type: string, touchPoints: Point[]) =>
129
+ cdpSend('Input.dispatchTouchEvent', {type, touchPoints: touchPoints.map(touchAt)})
130
+ await send('touchStart', [from])
131
+ for (let i = 1; i <= steps; i++) {
132
+ await send('touchMove', [
133
+ {x: from.x + ((to.x - from.x) * i) / steps, y: from.y + ((to.y - from.y) * i) / steps},
134
+ ])
135
+ await pause(16)
136
+ }
137
+ await send('touchEnd', [])
138
+ }
139
+
140
+ const getViewport = () => document.querySelector<HTMLElement>('[data-duro-scroll]')!
141
+ const getDialog = () => document.querySelector('[role="dialog"]')
142
+
143
+ /**
144
+ * A point inside the list that is not on a toggle, so a drag starting there
145
+ * is the case the deferral decides (a drag starting on a toggle is decided
146
+ * the same way, but on origin/main it was left to the native scroll).
147
+ */
148
+ function freePoint(el: HTMLElement, fy: number): Point {
149
+ const r = el.getBoundingClientRect()
150
+ const y = r.top + r.height * fy
151
+ for (let dy = 0; dy < 12; dy++) {
152
+ for (let x = r.left + 2; x < r.right; x += 2) {
153
+ const hit = document.elementFromPoint(x, y + dy)
154
+ if (hit && el.contains(hit) && !hit.closest('button')) return {x, y: y + dy}
155
+ }
156
+ }
157
+ throw new Error('no free point in the list')
158
+ }
159
+
160
+ async function setup() {
161
+ await page.viewport(390, 844)
162
+ await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
163
+ await waitFor(() => expect(getViewport()).toBeTruthy())
164
+ const viewport = getViewport()
165
+ await expect(viewport.scrollHeight).toBeGreaterThan(viewport.clientHeight)
166
+ // let the slide-in animation finish
167
+ await pause(500)
168
+ return viewport
169
+ }
170
+
171
+ export const DragUpAtTopScrolls: Story = {
172
+ render: () => <Scene />,
173
+ play: async () => {
174
+ const viewport = await setup()
175
+ await expect(viewport.scrollTop).toBe(0)
176
+ const from = freePoint(viewport, 0.7)
177
+ await touchDrag(from, {x: from.x, y: from.y - 60})
178
+ await pause(300)
179
+ await expect(viewport.scrollTop).toBeGreaterThan(0)
180
+ await expect(getDialog()).not.toBeNull()
181
+ },
182
+ }
183
+
184
+ export const DragDownMidListScrolls: Story = {
185
+ render: () => <Scene />,
186
+ play: async () => {
187
+ const viewport = await setup()
188
+ viewport.scrollTop = 40
189
+ await pause(100)
190
+ const before = viewport.scrollTop
191
+ await expect(before).toBeGreaterThan(0)
192
+ const from = freePoint(viewport, 0.5)
193
+ await touchDrag(from, {x: from.x, y: from.y + 20})
194
+ await pause(300)
195
+ await expect(viewport.scrollTop).toBeLessThan(before)
196
+ await expect(getDialog()).not.toBeNull()
197
+ },
198
+ }
199
+
200
+ export const DragDownAtTopDismisses: Story = {
201
+ render: () => <Scene />,
202
+ play: async () => {
203
+ const viewport = await setup()
204
+ await expect(viewport.scrollTop).toBe(0)
205
+ const from = freePoint(viewport, 0.5)
206
+ await touchDrag(from, {x: from.x, y: from.y + 300})
207
+ await waitFor(() => expect(getDialog()).toBeNull())
208
+ },
209
+ }
210
+
211
+ export const DragDownOnHeaderDismisses: Story = {
212
+ render: () => <Scene />,
213
+ play: async () => {
214
+ await setup()
215
+ const title = [...document.querySelectorAll('[role="dialog"] *')].find(
216
+ (el) => el.textContent === 'Pick an icon',
217
+ ) as HTMLElement
218
+ const r = title.getBoundingClientRect()
219
+ const from = {x: r.left + r.width / 2, y: r.top + r.height / 2}
220
+ await touchDrag(from, {x: from.x, y: from.y + 300})
221
+ await waitFor(() => expect(getDialog()).toBeNull())
222
+ },
223
+ }
224
+
225
+ export const TapOnToggleStillPresses: Story = {
226
+ render: () => <Scene />,
227
+ play: async () => {
228
+ await setup()
229
+ const key = document.querySelector<HTMLElement>('[role="dialog"] button[aria-label="key"]')!
230
+ const r = key.getBoundingClientRect()
231
+ const at = toPage({x: r.left + r.width / 2, y: r.top + r.height / 2})
232
+ await cdpSend('Input.dispatchTouchEvent', {
233
+ type: 'touchStart',
234
+ touchPoints: [{...at, radiusX: 1, radiusY: 1}],
235
+ })
236
+ await cdpSend('Input.dispatchTouchEvent', {type: 'touchEnd', touchPoints: []})
237
+ await waitFor(() => expect(key).toHaveAttribute('aria-pressed', 'true'))
238
+ await expect(getDialog()).not.toBeNull()
239
+ },
240
+ }
241
+
242
+ export const MountKeepsTheDrawerStill: Story = {
243
+ // size the page before the render: a drawer mounts at its final width
244
+ beforeEach: async () => {
245
+ await page.viewport(390, 844)
246
+ },
247
+ render: () => <Scene pressed={ICON_NAMES[39]} />,
248
+ play: async () => {
249
+ const pageScroll = document.scrollingElement?.scrollTop
250
+ await waitFor(() => expect(getViewport()).toBeTruthy())
251
+ await pause(500)
252
+ const viewport = getViewport()
253
+ const body = viewport.closest('[role="dialog"]')!
254
+ // the list scrolled itself to the pressed icon …
255
+ await expect(viewport.scrollTop).toBeGreaterThan(0)
256
+ const pressed = viewport.querySelector('[aria-pressed="true"]')!.getBoundingClientRect()
257
+ const v = viewport.getBoundingClientRect()
258
+ await expect(pressed.top).toBeGreaterThanOrEqual(v.top - 1)
259
+ await expect(pressed.bottom).toBeLessThanOrEqual(v.bottom + 1)
260
+ // … and nothing outside it moved
261
+ for (const el of body.querySelectorAll<HTMLElement>('*')) {
262
+ if (el !== viewport && !viewport.contains(el)) await expect(el.scrollTop).toBe(0)
263
+ }
264
+ await expect(document.scrollingElement?.scrollTop).toBe(pageScroll)
265
+ },
266
+ }
267
+
268
+ /** The coarse-pointer rows of a wrapping group, measured, not inferred. */
269
+ export const CoarsePointerRows: Story = {
270
+ render: () => (
271
+ <ToggleGroup wrap maxRows={3} size="small" aria-label="Icon">
272
+ {ICON_NAMES.map((name) => (
273
+ <Toggle key={name} value={name} aria-label={name}>
274
+ <Icon name={name} size="sm" />
275
+ </Toggle>
276
+ ))}
277
+ </ToggleGroup>
278
+ ),
279
+ play: async () => {
280
+ // phone width, so 42 icons overflow three rows
281
+ await page.viewport(390, 844)
282
+ await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
283
+ await waitFor(() => expect(matchMedia('(pointer: coarse)').matches).toBe(true))
284
+ const viewport = getViewport()
285
+ const expected = 3.5 * SIZES_PX.touchTarget + 3 * 4 + 2 * 4
286
+ await waitFor(() => expect(Math.abs(viewport.clientHeight - expected)).toBeLessThanOrEqual(2))
287
+ const toggle = viewport.querySelector('button')!.getBoundingClientRect()
288
+ // exact: Toggle's styles read the same size tokens, and a drift must fail here
289
+ await expect(Math.abs(toggle.height - SIZES_PX.touchTarget)).toBeLessThanOrEqual(0.5)
290
+ await expect(toggle.width).toBeGreaterThanOrEqual(SIZES_PX.touchTarget)
291
+ },
292
+ }
@@ -4,13 +4,15 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
6
  import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
7
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
8
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
7
9
 
8
10
  export const styles = css.create({
9
11
  // --- Backdrop ---
10
12
  backdrop: {
11
13
  position: 'fixed',
12
14
  inset: 0,
13
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
15
+ backgroundColor: colors.scrim,
14
16
  zIndex: 1000,
15
17
  // Portaled into the ThemeProvider mount, which is pointer-events: none:
16
18
  // the backdrop must take clicks back (outside click dismisses).
@@ -77,13 +79,13 @@ export const styles = css.create({
77
79
  panelHorizontal: {
78
80
  width: '100%',
79
81
  height: '100%',
80
- borderLeftWidth: 1,
82
+ borderLeftWidth: borders.hairline,
81
83
  borderLeftStyle: 'solid',
82
84
  borderLeftColor: colors.border,
83
85
  },
84
86
  panelVertical: {
85
87
  width: '100%',
86
- borderTopWidth: 1,
88
+ borderTopWidth: borders.hairline,
87
89
  borderTopStyle: 'solid',
88
90
  borderTopColor: colors.border,
89
91
  borderTopLeftRadius: radii.lg,
@@ -92,15 +94,15 @@ export const styles = css.create({
92
94
  },
93
95
  panelLeftBorder: {
94
96
  borderLeftWidth: 0,
95
- borderRightWidth: 1,
97
+ borderRightWidth: borders.hairline,
96
98
  borderRightStyle: 'solid',
97
99
  borderRightColor: colors.border,
98
100
  },
99
101
 
100
102
  // --- Panel widths (horizontal anchors) ---
101
- sm: {maxWidth: 360},
102
- md: {maxWidth: 480},
103
- lg: {maxWidth: 640},
103
+ sm: {maxWidth: sizes.panelSm},
104
+ md: {maxWidth: sizes.panelMd},
105
+ lg: {maxWidth: sizes.panelLg},
104
106
 
105
107
  // --- Slide animations: Right ---
106
108
  slideInRight: {
@@ -166,7 +168,10 @@ export const styles = css.create({
166
168
  // The closeAnimationDuration prop token drives both these and the unmount
167
169
  // timeout, so the exit animation can never race the unmount.
168
170
  closeDurationInstant: {animationDuration: duration.instant},
171
+ closeDurationMinimal: {animationDuration: duration.minimal},
172
+ closeDurationQuick: {animationDuration: duration.quick},
169
173
  closeDurationFast: {animationDuration: duration.fast},
174
+ closeDurationBrisk: {animationDuration: duration.brisk},
170
175
  closeDurationBase: {animationDuration: duration.base},
171
176
  closeDurationSlow: {animationDuration: duration.slow},
172
177
 
@@ -221,7 +226,7 @@ export const styles = css.create({
221
226
  paddingLeft: spacing.lg,
222
227
  paddingRight: spacing.lg,
223
228
  paddingBottom: spacing.lg,
224
- borderTopWidth: 1,
229
+ borderTopWidth: borders.hairline,
225
230
  borderTopStyle: 'solid',
226
231
  borderTopColor: colors.border,
227
232
  },
@@ -231,8 +236,8 @@ export const styles = css.create({
231
236
  display: 'flex',
232
237
  alignItems: 'center',
233
238
  justifyContent: 'center',
234
- width: 32,
235
- height: 32,
239
+ width: sizes.iconButton,
240
+ height: sizes.iconButton,
236
241
  borderRadius: radii.sm,
237
242
  borderWidth: 0,
238
243
  backgroundColor: {
@@ -21,7 +21,12 @@ interface UseSwipeDismissOptions {
21
21
  anchor: DrawerAnchor
22
22
  enabled: boolean
23
23
  onDismiss: () => void
24
- panelRef: React.RefObject<HTMLDivElement | null>
24
+ /**
25
+ * The panel element itself, not a ref: a Drawer open from its first render
26
+ * draws the panel inline, then remounts it into the portal once the mount
27
+ * exists, and listeners must follow the element that is actually shown.
28
+ */
29
+ panel: HTMLDivElement | null
25
30
  }
26
31
 
27
32
  interface SwipeState {
@@ -32,6 +37,9 @@ interface SwipeState {
32
37
  startTime: number
33
38
  swiping: boolean
34
39
  panelSize: number
40
+ /** Inner Duro scroll viewport the gesture started in; capture is deferred until the axis is decided */
41
+ deferredScroll: HTMLElement | null
42
+ pointerId: number
35
43
  }
36
44
 
37
45
  /**
@@ -41,7 +49,7 @@ interface SwipeState {
41
49
  * Opposite direction: iOS-style — panel stays anchored, shows subtle
42
50
  * overscroll feedback (box-shadow intensifies), then snaps back.
43
51
  */
44
- export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipeDismissOptions) {
52
+ export function useSwipeDismiss({anchor, enabled, onDismiss, panel}: UseSwipeDismissOptions) {
45
53
  const stateRef = useRef<SwipeState>({
46
54
  startX: 0,
47
55
  startY: 0,
@@ -50,6 +58,8 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
50
58
  startTime: 0,
51
59
  swiping: false,
52
60
  panelSize: 0,
61
+ deferredScroll: null,
62
+ pointerId: 0,
53
63
  })
54
64
 
55
65
  const isHorizontal = anchor === 'left' || anchor === 'right'
@@ -169,14 +179,18 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
169
179
  }, [])
170
180
 
171
181
  useEffect(() => {
172
- if (!enabled) return
173
- const panel = panelRef.current
174
- if (!panel) return
182
+ if (!enabled || !panel) return
175
183
 
176
184
  function handlePointerDown(e: PointerEvent) {
177
185
  if (e.button !== 0) return
178
186
  const target = e.target as HTMLElement
179
- if (target.closest('button, a, input, textarea, select, [role="button"]')) {
187
+ const scrollEl = target.closest<HTMLElement>('[data-duro-scroll]')
188
+ // Outside a ScrollArea a control keeps its press. Inside one the gesture
189
+ // is decided on the first move even from a control: a list of toggles is
190
+ // all buttons, and touch adjustment snaps a finger onto one. A tap
191
+ // (under the 8px slop) still presses it; a drag never captures before
192
+ // the decision, so the release lands on the panel, not the control.
193
+ if (!scrollEl && target.closest('button, a, input, textarea, select, [role="button"]')) {
180
194
  return
181
195
  }
182
196
 
@@ -190,14 +204,30 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
190
204
  state.swiping = false
191
205
  state.panelSize = isHorizontal ? rect.width : rect.height
192
206
 
207
+ state.pointerId = e.pointerId
208
+ state.deferredScroll = scrollEl
209
+
193
210
  panel!.dataset.panelSize = String(state.panelSize)
194
- panel!.setPointerCapture(e.pointerId)
211
+
212
+ // Inside a Duro ScrollArea the scroll may own the gesture: wait for the first move.
213
+ if (!state.deferredScroll) beginCapture(e.pointerId)
214
+ }
215
+
216
+ function beginCapture(pointerId: number) {
217
+ panel!.setPointerCapture(pointerId)
195
218
 
196
219
  // Freeze CSS animation — we take over positioning.
197
220
  // Never restore it; the panel is already at its final resting position.
198
221
  panel!.style.animation = 'none'
199
222
  }
200
223
 
224
+ /** True when the viewport can't scroll further against the dismiss direction */
225
+ function isAtDismissEdge(el: HTMLElement) {
226
+ if (!isHorizontal) return el.scrollTop <= 0
227
+ if (anchor === 'right') return el.scrollLeft <= 0
228
+ return el.scrollLeft + el.clientWidth >= el.scrollWidth - 1
229
+ }
230
+
201
231
  function handlePointerMove(e: PointerEvent) {
202
232
  const state = stateRef.current
203
233
  if (state.startTime === 0) return
@@ -219,8 +249,21 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
219
249
  } else {
220
250
  // Wrong axis — abort gesture
221
251
  state.startTime = 0
252
+ state.deferredScroll = null
222
253
  return
223
254
  }
255
+
256
+ const scrollEl = state.deferredScroll
257
+ if (scrollEl) {
258
+ state.deferredScroll = null
259
+ if (getMovement(state) <= 0 || !isAtDismissEdge(scrollEl)) {
260
+ // The scroll owns this gesture: no swiping, no capture, no preventDefault
261
+ state.swiping = false
262
+ state.startTime = 0
263
+ return
264
+ }
265
+ beginCapture(state.pointerId)
266
+ }
224
267
  }
225
268
 
226
269
  if (state.swiping) {
@@ -239,6 +282,7 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
239
282
  const velocity = movement / elapsed
240
283
 
241
284
  state.startTime = 0
285
+ state.deferredScroll = null
242
286
 
243
287
  if (!state.swiping) {
244
288
  return
@@ -266,6 +310,7 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
266
310
  const state = stateRef.current
267
311
  state.startTime = 0
268
312
  state.swiping = false
313
+ state.deferredScroll = null
269
314
  resetStyles(panel!)
270
315
  }
271
316
 
@@ -290,7 +335,7 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
290
335
  }
291
336
  }, [
292
337
  enabled,
293
- panelRef,
338
+ panel,
294
339
  anchor,
295
340
  isHorizontal,
296
341
  getMovement,
@@ -11,6 +11,7 @@ import {Checkbox} from '../Checkbox/Checkbox'
11
11
  import {Stack} from '../Stack/Stack'
12
12
  import {Fieldset} from '../Fieldset/Fieldset'
13
13
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
14
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
14
15
 
15
16
  const meta: Meta = {
16
17
  title: 'Components/Field',
@@ -70,8 +71,8 @@ export const WithError: Story = {
70
71
  }
71
72
 
72
73
  const stackStyles = css.create({
73
- stack: {display: 'flex', flexDirection: 'column', gap: spacing.md, maxWidth: 400},
74
- stackWide: {display: 'flex', flexDirection: 'column', gap: spacing.md, maxWidth: 600},
74
+ stack: {display: 'flex', flexDirection: 'column', gap: spacing.md, maxWidth: sizes.dialogSm},
75
+ stackWide: {display: 'flex', flexDirection: 'column', gap: spacing.md, maxWidth: sizes.pageSm},
75
76
  })
76
77
 
77
78
  export const FormExample: Story = {
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  root: {
@@ -21,7 +22,7 @@ export const styles = css.create({
21
22
  color: colors.text,
22
23
  },
23
24
  labelSide: {
24
- minWidth: 120,
25
+ minWidth: sizes.labelMinW,
25
26
  paddingTop: spacing.sm,
26
27
  flexShrink: 0,
27
28
  },
@@ -1,6 +1,6 @@
1
1
  import {useState} from 'react'
2
2
  import type {Meta, StoryObj} from '@storybook/react'
3
- import {expect, fn, userEvent, within} from 'storybook/test'
3
+ import {expect, fn, userEvent} from 'storybook/test'
4
4
  import {css, html} from 'react-strict-dom'
5
5
  import {Schema} from 'effect'
6
6
  import {Form} from './Form'
@@ -9,6 +9,7 @@ import {Input} from '../Input/Input'
9
9
  import {Textarea} from '../Textarea/Textarea'
10
10
  import {Fieldset} from '../Fieldset/Fieldset'
11
11
  import {Button} from '../Button/Button'
12
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
12
13
 
13
14
  const meta: Meta = {
14
15
  title: 'Components/Form',
@@ -48,8 +49,8 @@ const ProfileSchema = Schema.Struct({
48
49
  })
49
50
 
50
51
  const wrapStyles = css.create({
51
- wrap: {maxWidth: 400},
52
- wrapWide: {maxWidth: 600},
52
+ wrap: {maxWidth: sizes.dialogSm},
53
+ wrapWide: {maxWidth: sizes.pageSm},
53
54
  })
54
55
 
55
56
  // --- Stories ---
@@ -24,14 +24,14 @@ export const meta: ComponentMeta = {
24
24
  relationship: 'Grid layout="split" with a List and a Panel is the split-pane recipe',
25
25
  },
26
26
  ],
27
- example: `// Split: list ≥ 240px beside a detail pane, one column below md
27
+ example: `// Split: list ≥ sizes.gridColSm (240px) beside a detail pane, one column below md
28
28
  <Grid layout="split" gap="lg">
29
29
  <List.Root>…</List.Root>
30
30
  <Panel.Root>…</Panel.Root>
31
31
  </Grid>
32
32
 
33
33
  // Responsive: columns auto-fit based on min width
34
- <Grid minColumnWidth="280px" gap="md">
34
+ <Grid minColumnWidth="gridColMd" gap="md">
35
35
  <Card>A</Card>
36
36
  <Card>B</Card>
37
37
  <Card>C</Card>
@@ -8,6 +8,7 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
8
8
  import {SPACING_KEYS} from '@duro-app/tokens/keys'
9
9
  import {useContainerQuery} from '../../hooks/useContainerQuery'
10
10
  import {typography} from '@duro-app/tokens/tokens/typography.css'
11
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
11
12
 
12
13
  const meta: Meta<typeof Grid> = {
13
14
  title: 'Layout/Grid',
@@ -45,7 +46,7 @@ const localStyles = css.create({
45
46
  },
46
47
  frame: (width: number) => ({
47
48
  width,
48
- borderWidth: 1,
49
+ borderWidth: borders.hairline,
49
50
  borderStyle: 'dashed',
50
51
  borderColor: colors.border,
51
52
  }),
@@ -74,7 +75,7 @@ export const FixedColumns: Story = {
74
75
 
75
76
  export const AutoFit: Story = {
76
77
  render: () => (
77
- <Grid gap="md" minColumnWidth="200px">
78
+ <Grid gap="md" minColumnWidth="gridColSm">
78
79
  <Cell>Card A</Cell>
79
80
  <Cell>Card B</Cell>
80
81
  <Cell>Card C</Cell>
@@ -4,6 +4,7 @@ import {styles} from './styles.css'
4
4
  import {isNative} from '../../platform'
5
5
  import {cellBasis, cellPosition, columnWeights, gridTemplate, type GridColumns} from './columns'
6
6
  import type {SpacingToken} from '@duro-app/tokens/keys'
7
+ import {resolveLength, type Length} from '../../shared/length'
7
8
 
8
9
  export type GridLayout = 'split' | 'split-wide'
9
10
 
@@ -13,10 +14,11 @@ interface GridProps {
13
14
  * two-thirds split. Weights are the portable form of a track list — a CSS
14
15
  * template string would be silently ignored on native. */
15
16
  columns?: GridColumns
16
- /** Responsive auto-fit: as many columns of at least this width as fit.
17
- * Wins over `columns` when both are set. On native this approximates to
18
- * cells of that minimum width that grow and wrap. */
19
- minColumnWidth?: string
17
+ /** Responsive auto-fit: as many columns of at least this width as fit — a
18
+ * size token key (`"gridColMd"`) or a relative length. Wins over `columns`
19
+ * when both are set. On native this approximates to cells of that minimum
20
+ * width that grow and wrap. */
21
+ minColumnWidth?: Length
20
22
  /**
21
23
  * A named responsive layout. `split` is list/detail (1:2, list ≥ 240px),
22
24
  * `split-wide` is nav/content (1:3, nav ≥ 280px). Each collapses to one
@@ -101,7 +103,7 @@ export function Grid({gap = 'md', columns, minColumnWidth, layout, children}: Gr
101
103
  const columnStyle = layout
102
104
  ? layoutMap[layout]
103
105
  : minColumnWidth
104
- ? styles.autoFit(minColumnWidth)
106
+ ? styles.autoFit(resolveLength(minColumnWidth))
105
107
  : typeof columns === 'number'
106
108
  ? columnsMap[columns]
107
109
  : columns
@@ -136,7 +138,7 @@ function NativeGrid({gap = 'md', columns, minColumnWidth, children}: GridProps)
136
138
  return (
137
139
  <html.div style={[styles.nativeRow, gapMap[gap]]}>
138
140
  {items.map((child, index) => (
139
- <html.div key={index} style={styles.nativeMinCell(minColumnWidth)}>
141
+ <html.div key={index} style={styles.nativeMinCell(resolveLength(minColumnWidth))}>
140
142
  {child}
141
143
  </html.div>
142
144
  ))}
@@ -22,7 +22,7 @@ export const styles = css.create({
22
22
  col4: {gridTemplateColumns: 'repeat(4, 1fr)'},
23
23
  col5: {gridTemplateColumns: 'repeat(5, 1fr)'},
24
24
  col6: {gridTemplateColumns: 'repeat(6, 1fr)'},
25
- autoFit: (minWidth: string) => ({
25
+ autoFit: (minWidth: string | number) => ({
26
26
  gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}, 1fr))`,
27
27
  }),
28
28
  // Hosts the @container query for a named layout. A container cannot query
@@ -86,7 +86,7 @@ export const styles = css.create({
86
86
  // Auto-fit approximation: start at the minimum width, grow to fill. flexGrow
87
87
  // is fine here: this style is native-only, so RSD-web's forced flex-grow: 0
88
88
  // never applies.
89
- nativeMinCell: (minWidth: string) => ({
89
+ nativeMinCell: (minWidth: string | number) => ({
90
90
  flexBasis: minWidth,
91
91
  flexGrow: 1,
92
92
  boxSizing: 'border-box',