@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
@@ -1,16 +1,46 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
- import {radii} from '@duro-app/tokens/tokens/spacing.css'
3
+ import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
5
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
4
6
 
5
7
  export const styles = css.create({
6
8
  root: {
7
9
  display: 'inline-flex',
8
10
  borderRadius: radii.sm,
9
- borderWidth: 1,
11
+ borderWidth: borders.hairline,
10
12
  borderStyle: 'solid',
11
13
  borderColor: colors.border,
12
14
  overflow: 'hidden',
13
15
  },
16
+ wrap: {
17
+ display: 'flex',
18
+ flexDirection: 'row',
19
+ flexWrap: 'wrap',
20
+ gap: spacing.xs,
21
+ borderWidth: 0,
22
+ borderRadius: 0,
23
+ overflow: 'visible',
24
+ },
25
+ // the scroll content: room for focus rings to draw
26
+ scrollContent: {
27
+ position: 'relative',
28
+ padding: spacing.xs,
29
+ },
30
+ // shows rows + 0.5 rows so the cut-off row hints at more; shorter content
31
+ // sizes itself (max, not fixed). Row heights match the wrapped Toggles.
32
+ maxRowsSmall: (rows: number) => ({
33
+ maxHeight: {
34
+ default: `calc((${rows} + 0.5) * ${sizes.controlSm} + ${rows} * ${spacing.xs} + 2 * ${spacing.xs})`,
35
+ '@media (pointer: coarse)': `calc((${rows} + 0.5) * ${sizes.touchTarget} + ${rows} * ${spacing.xs} + 2 * ${spacing.xs})`,
36
+ },
37
+ }),
38
+ maxRowsDefault: (rows: number) => ({
39
+ maxHeight: {
40
+ default: `calc((${rows} + 0.5) * ${sizes.controlMd} + ${rows} * ${spacing.xs} + 2 * ${spacing.xs})`,
41
+ '@media (pointer: coarse)': `calc((${rows} + 0.5) * ${sizes.touchTarget} + ${rows} * ${spacing.xs} + 2 * ${spacing.xs})`,
42
+ },
43
+ }),
14
44
  vertical: {
15
45
  flexDirection: 'column',
16
46
  },
@@ -0,0 +1,121 @@
1
+ import {useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject} from 'react'
2
+
3
+ const ROW_TOLERANCE = 2
4
+
5
+ function sameOrder(a: string[], b: string[]) {
6
+ return a.length === b.length && a.every((v, i) => v === b[i])
7
+ }
8
+
9
+ /**
10
+ * Roving focus for a wrapping ToggleGroup (WAI-ARIA toolbar pattern): one tab
11
+ * stop — the focused item, else the first pressed one, else the first — with
12
+ * arrow keys in reading order, Up/Down by visual row, Home/End.
13
+ */
14
+ export function useRovingFocus({
15
+ enabled,
16
+ pressed,
17
+ rootRef,
18
+ }: {
19
+ enabled: boolean
20
+ pressed: string[]
21
+ rootRef: RefObject<HTMLElement | null>
22
+ }) {
23
+ const items = useRef(new Map<string, HTMLElement>())
24
+ const [order, setOrder] = useState<string[]>([])
25
+ const [focusedValue, setFocusedValue] = useState<string | null>(null)
26
+
27
+ const sync = useCallback(() => {
28
+ const entries = [...items.current.entries()].sort(([, a], [, b]) =>
29
+ a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,
30
+ )
31
+ const next = entries.map(([v]) => v)
32
+ setOrder((prev) => (sameOrder(prev, next) ? prev : next))
33
+ }, [])
34
+
35
+ const register = useCallback(
36
+ (value: string, el: HTMLElement) => {
37
+ items.current.set(value, el)
38
+ sync()
39
+ return () => {
40
+ if (items.current.get(value) === el) items.current.delete(value)
41
+ sync()
42
+ }
43
+ },
44
+ [sync],
45
+ )
46
+
47
+ // a focus holder that unmounted no longer holds the tab stop
48
+ useLayoutEffect(() => {
49
+ if (focusedValue !== null && !order.includes(focusedValue)) setFocusedValue(null)
50
+ }, [order, focusedValue])
51
+
52
+ const tabStopValue = !enabled
53
+ ? null
54
+ : focusedValue !== null && order.includes(focusedValue)
55
+ ? focusedValue
56
+ : (pressed.find((v) => order.includes(v)) ?? order[0] ?? null)
57
+
58
+ // native listener: RSD's onKeyDown event is a reduced shape (as in Tree)
59
+ useEffect(() => {
60
+ const root = rootRef.current
61
+ if (!enabled || !root) return
62
+ function onKeyDown(e: KeyboardEvent) {
63
+ if (e.altKey || e.ctrlKey || e.metaKey) return
64
+ const els = order
65
+ .map((v) => items.current.get(v))
66
+ .filter((el): el is HTMLElement => !!el && !(el as HTMLButtonElement).disabled)
67
+ const i = els.indexOf(e.target as HTMLElement)
68
+ if (i < 0) return
69
+ let target: HTMLElement | undefined
70
+ switch (e.key) {
71
+ case 'ArrowRight':
72
+ target = els[Math.min(i + 1, els.length - 1)]
73
+ break
74
+ case 'ArrowLeft':
75
+ target = els[Math.max(i - 1, 0)]
76
+ break
77
+ case 'Home':
78
+ target = els[0]
79
+ break
80
+ case 'End':
81
+ target = els[els.length - 1]
82
+ break
83
+ case 'ArrowDown':
84
+ case 'ArrowUp': {
85
+ const down = e.key === 'ArrowDown'
86
+ const cur = els[i].getBoundingClientRect()
87
+ const cx = cur.left + cur.width / 2
88
+ const rects = els.map((el) => el.getBoundingClientRect())
89
+ // rows, by distinct top, in the direction of travel
90
+ const tops = [...new Set(rects.map((r) => Math.round(r.top)))].sort((a, b) => a - b)
91
+ const rowIdx = tops.findIndex((t) => Math.abs(t - cur.top) <= ROW_TOLERANCE)
92
+ const nextTop = tops[rowIdx + (down ? 1 : -1)]
93
+ if (nextTop === undefined) break
94
+ let best = Infinity
95
+ rects.forEach((r, k) => {
96
+ if (Math.abs(r.top - nextTop) > ROW_TOLERANCE) return
97
+ const d = Math.abs(r.left + r.width / 2 - cx)
98
+ if (d < best) {
99
+ best = d
100
+ target = els[k]
101
+ }
102
+ })
103
+ break
104
+ }
105
+ default:
106
+ return
107
+ }
108
+ e.preventDefault()
109
+ target?.focus()
110
+ }
111
+ root.addEventListener('keydown', onKeyDown)
112
+ return () => root.removeEventListener('keydown', onKeyDown)
113
+ }, [enabled, order, rootRef])
114
+
115
+ return {
116
+ tabStopValue,
117
+ register,
118
+ onItemFocus: setFocusedValue,
119
+ items,
120
+ }
121
+ }
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  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
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  root: {
@@ -22,7 +23,7 @@ export const styles = css.create({
22
23
  fontSize: typography.fontSizeXs,
23
24
  lineHeight: typography.lineHeight,
24
25
  borderRadius: radii.sm,
25
- borderWidth: 1,
26
+ borderWidth: borders.hairline,
26
27
  borderStyle: 'solid',
27
28
  borderColor: colors.border,
28
29
  boxShadow: shadows.md,
@@ -1,8 +1,10 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
- import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
3
+ import {microSpacing, radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
6
8
 
7
9
  export const styles = css.create({
8
10
  root: {
@@ -32,8 +34,8 @@ export const styles = css.create({
32
34
  display: 'flex',
33
35
  alignItems: 'center',
34
36
  gap: spacing.xs,
35
- paddingTop: '5px',
36
- paddingBottom: '5px',
37
+ paddingTop: microSpacing.px5,
38
+ paddingBottom: microSpacing.px5,
37
39
  paddingRight: spacing.sm,
38
40
  fontFamily: typography.fontFamily,
39
41
  fontSize: typography.fontSizeSm,
@@ -53,10 +55,10 @@ export const styles = css.create({
53
55
  paddingLeft: `calc(${spacing.sm} + ${Math.max(0, level - 1)} * ${spacing.lg})`,
54
56
  }),
55
57
  rowFocused: {
56
- outlineWidth: 2,
58
+ outlineWidth: borders.focusRing,
57
59
  outlineStyle: 'solid',
58
60
  outlineColor: colors.accent,
59
- outlineOffset: -2,
61
+ outlineOffset: `calc(-1 * ${borders.focusOffset})`,
60
62
  },
61
63
  rowSelected: {
62
64
  color: colors.accent,
@@ -67,7 +69,7 @@ export const styles = css.create({
67
69
  display: 'inline-flex',
68
70
  alignItems: 'center',
69
71
  justifyContent: 'center',
70
- width: spacing.md,
72
+ width: sizes.glyphMd,
71
73
  flexShrink: 0,
72
74
  color: colors.textMuted,
73
75
  transitionProperty: 'transform',
@@ -3,10 +3,11 @@ import {expect, fn, waitFor} from 'storybook/test'
3
3
  import {createColumnHelper} from '@tanstack/react-table'
4
4
  import {css, html} from 'react-strict-dom'
5
5
  import {VirtualTable} from './VirtualTable'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
6
7
 
7
8
  const storyStyles = css.create({
8
9
  // Phone-width wrapper: below VirtualTable's 640px stackBelow default.
9
- narrow: {width: 400},
10
+ narrow: {width: sizes.dialogSm},
10
11
  })
11
12
 
12
13
  interface Item {
@@ -49,7 +50,7 @@ export const SmallList: Story = {
49
50
  export const Virtualized: Story = {
50
51
  args: {
51
52
  data: makeData(500),
52
- maxHeight: 300,
53
+ maxHeight: 'listMaxH',
53
54
  rangeLabel: ({from, to, total}) => `${from}-${to} of ${total}`,
54
55
  onVisiblePageChange: fn(),
55
56
  },
@@ -69,7 +70,7 @@ export const Virtualized: Story = {
69
70
  export const ScrollReportsPage: Story = {
70
71
  args: {
71
72
  data: makeData(500),
72
- maxHeight: 300,
73
+ maxHeight: 'listMaxH',
73
74
  pageSize: 50,
74
75
  rangeLabel: ({page, pages}) => `page ${page}/${pages}`,
75
76
  onVisiblePageChange: fn(),
@@ -13,6 +13,8 @@ import {
13
13
  import {useVirtualizer} from '@tanstack/react-virtual'
14
14
  import {breakpointsPx, type Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
15
15
  import {styles} from './styles.css'
16
+ import {SIZES_PX} from '@duro-app/tokens/keys'
17
+ import {resolveLength, type Length} from '../../shared/length'
16
18
 
17
19
  // Measure + commit before paint on the client; no-op-safe on the server.
18
20
  const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
@@ -56,7 +58,7 @@ interface VirtualTableProps<TData> {
56
58
  /** Estimated row height for the virtualizer. Default 44. */
57
59
  estimateRowHeight?: number
58
60
  /** Max height of the scroll viewport once virtualized. Default '70vh'. */
59
- maxHeight?: number | string
61
+ maxHeight?: Length
60
62
  /** Render every row (no windowing) at or below this count. Default 150. */
61
63
  virtualizeThreshold?: number
62
64
  /**
@@ -94,7 +96,7 @@ export function VirtualTable<TData>({
94
96
  onVisiblePageChange,
95
97
  rangeLabel,
96
98
  pageSize = 50,
97
- estimateRowHeight = 44,
99
+ estimateRowHeight = SIZES_PX.touchTarget,
98
100
  maxHeight = '70vh',
99
101
  virtualizeThreshold = 150,
100
102
  stackBelow = 'sm',
@@ -288,7 +290,7 @@ export function VirtualTable<TData>({
288
290
  role="table"
289
291
  style={[
290
292
  stacked ? styles.stackScroll : styles.scroll,
291
- virtualize && styles.scrollMax(maxHeight),
293
+ virtualize && styles.scrollMax(resolveLength(maxHeight)),
292
294
  ]}
293
295
  >
294
296
  {stacked ? null : (
@@ -1,7 +1,8 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
- import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
3
+ import {microSpacing, radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  wrap: {
@@ -11,7 +12,7 @@ export const styles = css.create({
11
12
  scroll: {
12
13
  position: 'relative',
13
14
  width: '100%',
14
- borderWidth: 1,
15
+ borderWidth: borders.hairline,
15
16
  borderStyle: 'solid',
16
17
  borderColor: colors.border,
17
18
  borderRadius: radii.md,
@@ -32,7 +33,7 @@ export const styles = css.create({
32
33
  headRow: {
33
34
  display: 'flex',
34
35
  flexDirection: 'row',
35
- borderBottomWidth: 1,
36
+ borderBottomWidth: borders.hairline,
36
37
  borderBottomStyle: 'solid',
37
38
  borderBottomColor: colors.border,
38
39
  },
@@ -71,7 +72,7 @@ export const styles = css.create({
71
72
  flexDirection: 'row',
72
73
  alignItems: 'center',
73
74
  width: '100%',
74
- borderBottomWidth: 1,
75
+ borderBottomWidth: borders.hairline,
75
76
  borderBottomStyle: 'solid',
76
77
  borderBottomColor: colors.border,
77
78
  },
@@ -124,7 +125,7 @@ export const styles = css.create({
124
125
  gap: spacing.xs,
125
126
  width: '100%',
126
127
  padding: spacing.sm,
127
- borderWidth: 1,
128
+ borderWidth: borders.hairline,
128
129
  borderStyle: 'solid',
129
130
  borderColor: colors.border,
130
131
  borderRadius: radii.sm,
@@ -161,13 +162,13 @@ export const styles = css.create({
161
162
  position: 'absolute',
162
163
  right: spacing.sm,
163
164
  bottom: spacing.sm,
164
- paddingTop: 3,
165
- paddingBottom: 3,
165
+ paddingTop: microSpacing.px3,
166
+ paddingBottom: microSpacing.px3,
166
167
  paddingLeft: spacing.sm,
167
168
  paddingRight: spacing.sm,
168
169
  borderRadius: radii.full,
169
170
  backgroundColor: colors.bgCardHover,
170
- borderWidth: 1,
171
+ borderWidth: borders.hairline,
171
172
  borderStyle: 'solid',
172
173
  borderColor: colors.border,
173
174
  color: colors.textMuted,
@@ -7,6 +7,8 @@ import {Inline} from '../components/Inline/Inline'
7
7
  import {useContainerQuery} from '../hooks/useContainerQuery'
8
8
  import {TokenTable} from './helpers'
9
9
  import {typography, typeScale} from '@duro-app/tokens/tokens/typography.css'
10
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
11
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
10
12
 
11
13
  const primitiveTokens: Record<string, string> = {
12
14
  xs: '4px',
@@ -33,7 +35,7 @@ export default meta
33
35
 
34
36
  const styles = css.create({
35
37
  wrapper: {
36
- maxWidth: 640,
38
+ maxWidth: sizes.panelLg,
37
39
  },
38
40
  hint: {
39
41
  fontSize: typography.fontSizeXs,
@@ -41,10 +43,10 @@ const styles = css.create({
41
43
  fontStyle: 'italic',
42
44
  },
43
45
  baselineRow: {
44
- height: 24,
46
+ height: sizes.iconLg,
45
47
  display: 'flex',
46
48
  alignItems: 'center',
47
- borderBottomWidth: 1,
49
+ borderBottomWidth: borders.hairline,
48
50
  borderBottomStyle: 'dashed',
49
51
  borderBottomColor: colors.border,
50
52
  },
@@ -52,11 +54,10 @@ const styles = css.create({
52
54
  fontSize: typography.fontSizeXs,
53
55
  fontFamily: 'monospace',
54
56
  color: colors.textMuted,
55
- width: 50,
57
+ width: sizes.iconXxl,
56
58
  },
57
59
  baselineBlock: {
58
- flexGrow: 1,
59
- height: 24,
60
+ height: sizes.iconLg,
60
61
  backgroundColor: colors.accent,
61
62
  opacity: 0.15,
62
63
  borderRadius: radii.xs,
@@ -64,13 +65,13 @@ const styles = css.create({
64
65
  resizableContainer: {
65
66
  resize: 'horizontal',
66
67
  overflow: 'auto',
67
- borderWidth: 2,
68
+ borderWidth: borders.strong,
68
69
  borderStyle: 'dashed',
69
70
  borderColor: colors.border,
70
71
  padding: spacing.md,
71
- minWidth: 200,
72
+ minWidth: sizes.listMaxHSm,
72
73
  maxWidth: '100%',
73
- width: 600,
74
+ width: sizes.pageSm,
74
75
  },
75
76
  sizeLabel: {
76
77
  fontSize: typography.fontSizeHeading,
@@ -87,7 +88,7 @@ const styles = css.create({
87
88
  color: colors.successText,
88
89
  },
89
90
  mappingRow: {
90
- borderBottomWidth: 1,
91
+ borderBottomWidth: borders.hairline,
91
92
  borderBottomStyle: 'solid',
92
93
  borderBottomColor: colors.border,
93
94
  fontFamily: 'monospace',
@@ -8,6 +8,8 @@ import {Text} from '../components/Text/Text'
8
8
  import {Heading} from '../components/Heading/Heading'
9
9
  import {TypeScaleTable} from './helpers'
10
10
  import {typography} from '@duro-app/tokens/tokens/typography.css'
11
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
12
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
11
13
 
12
14
  const meta: Meta = {
13
15
  title: 'Foundations/Typography',
@@ -169,7 +171,7 @@ export const TextVariants: StoryObj = {
169
171
 
170
172
  const localStyles = css.create({
171
173
  wrapper: {
172
- maxWidth: 720,
174
+ maxWidth: sizes.dialogLg,
173
175
  },
174
176
  hint: {
175
177
  fontSize: typography.fontSizeXs,
@@ -187,8 +189,8 @@ const localStyles = css.create({
187
189
  position: 'relative',
188
190
  },
189
191
  gridLine: {
190
- height: 24,
191
- borderBottomWidth: 1,
192
+ height: sizes.iconLg,
193
+ borderBottomWidth: borders.hairline,
192
194
  borderBottomStyle: 'dashed',
193
195
  borderBottomColor: colors.border,
194
196
  },
@@ -201,12 +203,12 @@ const localStyles = css.create({
201
203
  resizableContainer: {
202
204
  resize: 'horizontal',
203
205
  overflow: 'auto',
204
- borderWidth: 2,
206
+ borderWidth: borders.strong,
205
207
  borderStyle: 'dashed',
206
208
  borderColor: colors.border,
207
209
  padding: spacing.md,
208
- minWidth: 280,
210
+ minWidth: sizes.gridColMd,
209
211
  maxWidth: '100%',
210
- width: 700,
212
+ width: sizes.dialogLg,
211
213
  },
212
214
  })
@@ -1,12 +1,13 @@
1
1
  import {css, html} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
- import {typography, typeScale} from '@duro-app/tokens/tokens/typography.css'
3
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
4
4
  import {Table} from '../components/Table/Table'
5
5
  import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
6
7
 
7
8
  const styles = css.create({
8
9
  bar: {
9
- height: 12,
10
+ height: sizes.glyphSm,
10
11
  borderRadius: radii.xs,
11
12
  backgroundColor: colors.accent,
12
13
  },
@@ -0,0 +1,51 @@
1
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
2
+ import {SIZE_KEYS, type SizeToken} from '@duro-app/tokens/keys'
3
+
4
+ /**
5
+ * A length a measure prop accepts without a token: zero, a relative unit or a
6
+ * keyword. These are the exemptions of design-system.every-measure-is-a-token
7
+ * (ADR-0027); a px value is not one of them.
8
+ */
9
+ export type RelativeLength =
10
+ | 0
11
+ | '0'
12
+ | 'auto'
13
+ | 'none'
14
+ | 'min-content'
15
+ | 'max-content'
16
+ | 'fit-content'
17
+ | `${number}%`
18
+ | `${number}vh`
19
+ | `${number}vw`
20
+ | `${number}dvh`
21
+ | `${number}svh`
22
+ | `${number}lvh`
23
+ | `${number}em`
24
+ | `${number}rem`
25
+ | `${number}ch`
26
+
27
+ /** A measure prop: a `sizes` token key (`"gridColMd"`) or a relative length. */
28
+ export type Length = SizeToken | RelativeLength
29
+
30
+ /**
31
+ * A CSS grid track: a Length, a fraction, or a track function. The arguments
32
+ * of `minmax()` / `fit-content()` are not checked by the type — pass tokens
33
+ * through `resolveLength` or `sizes.*` when building one.
34
+ *
35
+ * holds-until: a structured track type (`{min: Length, max: GridTrack}`)
36
+ * replaces the template-literal functions; a type cannot see inside
37
+ * `minmax(${string})`, so a px there is the one measure lint and types miss.
38
+ */
39
+ export type GridTrack = Length | `${number}fr` | `minmax(${string})` | `fit-content(${string})`
40
+
41
+ const sizeKeys: ReadonlySet<string> = new Set(SIZE_KEYS)
42
+
43
+ /** The CSS value for a Length: a size token key becomes its variable. */
44
+ export function resolveLength(value: Length): string | number {
45
+ return typeof value === 'string' && sizeKeys.has(value) ? sizes[value as SizeToken] : value
46
+ }
47
+
48
+ /** The CSS value for a GridTrack: a size token key becomes its variable. */
49
+ export function resolveTrack(track: GridTrack): string | number {
50
+ return typeof track === 'string' && sizeKeys.has(track) ? sizes[track as SizeToken] : track
51
+ }
@@ -0,0 +1,47 @@
1
+ // Compile-only fixtures for measure props (ADR-0027, plan 2c′). `pnpm typecheck`
2
+ // fails if a raw px becomes assignable to a measure prop, or if a token key or
3
+ // a relative length stops being. Never imported at runtime.
4
+
5
+ import type {Grid} from '../components/Grid/Grid'
6
+ import type {InputGroup} from '../components/InputGroup/InputGroup'
7
+ import type {ScrollArea} from '../components/ScrollArea/ScrollArea'
8
+ import type {Table} from '../components/Table/Table'
9
+ import type {VirtualTable} from '../components/VirtualTable/VirtualTable'
10
+ import type {GridTrack, Length} from './length'
11
+
12
+ /** `true` when every member of T is assignable to U. */
13
+ type Assignable<T, U> = [T] extends [U] ? true : false
14
+
15
+ /** The props a function component takes. */
16
+ type PropsOf<C> = C extends (props: infer P) => unknown ? P : never
17
+ type Prop<C, K extends keyof PropsOf<C>> = NonNullable<PropsOf<C>[K]>
18
+
19
+ // Accepted: token keys, zero, relative lengths, keywords.
20
+ export const lengthTokens: Assignable<'gridColMd' | 'touchTarget', Length> = true
21
+ export const lengthRelative: Assignable<0 | '0' | '100%' | '70vh' | '2rem' | 'auto', Length> = true
22
+ export const trackForms: Assignable<
23
+ 'iconButton' | '2fr' | 'max-content' | 'minmax(0, 2fr)',
24
+ GridTrack
25
+ > = true
26
+
27
+ // Refused: px as a string or a number.
28
+ export const lengthPxString: Assignable<'280px', Length> = false
29
+ export const lengthPxNumber: Assignable<44, Length> = false
30
+ export const trackPx: Assignable<'40px', GridTrack> = false
31
+
32
+ // The props themselves.
33
+ export const gridMin: Assignable<'280px', Prop<typeof Grid, 'minColumnWidth'>> = false
34
+ export const gridMinToken: Assignable<'gridColMd', Prop<typeof Grid, 'minColumnWidth'>> = true
35
+ export const addonMin: Assignable<80, Prop<typeof InputGroup.Addon, 'minWidth'>> = false
36
+ export const viewportMax: Assignable<300, Prop<typeof ScrollArea.Viewport, 'maxHeight'>> = false
37
+ export const viewportMaxToken: Assignable<
38
+ 'listMaxH',
39
+ Prop<typeof ScrollArea.Viewport, 'maxHeight'>
40
+ > = true
41
+ export const tableMin: Assignable<120, Prop<typeof Table.Root, 'minColumnWidth'>> = false
42
+ export const headerWidth: Assignable<'40px', Prop<typeof Table.HeaderCell, 'width'>> = false
43
+ export const headerWidthToken: Assignable<
44
+ 'iconButton',
45
+ Prop<typeof Table.HeaderCell, 'width'>
46
+ > = true
47
+ export const virtualMax: Assignable<300, Prop<typeof VirtualTable, 'maxHeight'>> = false
@@ -0,0 +1,16 @@
1
+ import {css} from 'react-strict-dom'
2
+
3
+ // The visually-hidden technique: a 1×1 box clipped by overflow, still in the
4
+ // accessibility tree and still focusable where the element is. The 1px is the
5
+ // technique, not a design measure, so this module is the one file the
6
+ // every-measure-is-a-token rule exempts (ADR-0027; `exemptFiles` in
7
+ // eslint.config.js). Components layer their own extras on top (`opacity: 0`
8
+ // for a native input, `clip` for a label).
9
+ export const visuallyHidden = css.create({
10
+ base: {
11
+ position: 'absolute',
12
+ width: 1,
13
+ height: 1,
14
+ overflow: 'hidden',
15
+ },
16
+ })