@dxos/react-ui 0.8.4-main.40e3dcdf1b → 0.8.4-main.422d1c7879

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 (250) hide show
  1. package/dist/lib/browser/{chunk-2FKSMWNY.mjs → chunk-OCVRIJCH.mjs} +79 -5
  2. package/dist/lib/browser/chunk-OCVRIJCH.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +905 -700
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +30 -18
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node-esm/{chunk-ZNBLTSHI.mjs → chunk-QUD5P3RU.mjs} +79 -5
  9. package/dist/lib/node-esm/chunk-QUD5P3RU.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +905 -700
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs +30 -18
  14. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  15. package/dist/types/src/components/Avatars/Avatar.d.ts +1 -1
  16. package/dist/types/src/components/Avatars/Avatar.d.ts.map +1 -1
  17. package/dist/types/src/components/Avatars/Avatar.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Avatars/AvatarGroup.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Breadcrumb/Breadcrumb.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Button/Button.d.ts +2 -2
  21. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  22. package/dist/types/src/components/Button/Button.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
  24. package/dist/types/src/components/Button/IconButton.stories.d.ts +3 -0
  25. package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Button/Toggle.d.ts +2 -2
  27. package/dist/types/src/components/Button/Toggle.d.ts.map +1 -1
  28. package/dist/types/src/components/Button/Toggle.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Button/ToggleGroup.d.ts +6 -6
  30. package/dist/types/src/components/Button/ToggleGroup.d.ts.map +1 -1
  31. package/dist/types/src/components/Button/ToggleGroup.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/Card/Card.d.ts +69 -49
  33. package/dist/types/src/components/Card/Card.d.ts.map +1 -1
  34. package/dist/types/src/components/Card/Card.stories.d.ts +2 -2
  35. package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Clipboard/CopyButton.d.ts.map +1 -1
  37. package/dist/types/src/components/Clipboard/index.d.ts +10 -1
  38. package/dist/types/src/components/Clipboard/index.d.ts.map +1 -1
  39. package/dist/types/src/components/Dialog/AlertDialog.d.ts +39 -28
  40. package/dist/types/src/components/Dialog/AlertDialog.d.ts.map +1 -1
  41. package/dist/types/src/components/Dialog/AlertDialog.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/Dialog/Dialog.d.ts +44 -27
  43. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  44. package/dist/types/src/components/Dialog/Dialog.stories.d.ts +3 -2
  45. package/dist/types/src/components/Dialog/Dialog.stories.d.ts.map +1 -1
  46. package/dist/types/src/components/ErrorFallback/ErrorFallback.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/ErrorFallback/ErrorStack.d.ts +14 -3
  48. package/dist/types/src/components/ErrorFallback/ErrorStack.d.ts.map +1 -1
  49. package/dist/types/src/components/Focus/Focus.d.ts +36 -0
  50. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -0
  51. package/dist/types/src/components/Focus/Focus.stories.d.ts +9 -0
  52. package/dist/types/src/components/Focus/Focus.stories.d.ts.map +1 -0
  53. package/dist/types/src/components/Focus/index.d.ts +2 -0
  54. package/dist/types/src/components/Focus/index.d.ts.map +1 -0
  55. package/dist/types/src/components/Icon/Icon.d.ts +3 -0
  56. package/dist/types/src/components/Icon/Icon.d.ts.map +1 -1
  57. package/dist/types/src/components/Icon/Icon.stories.d.ts +11 -3
  58. package/dist/types/src/components/Icon/Icon.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Image/Image.d.ts +2 -1
  60. package/dist/types/src/components/Image/Image.d.ts.map +1 -1
  61. package/dist/types/src/components/Image/Image.stories.d.ts +2 -1
  62. package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -1
  63. package/dist/types/src/components/Input/Input.d.ts +2 -2
  64. package/dist/types/src/components/Input/Input.d.ts.map +1 -1
  65. package/dist/types/src/components/Input/Input.stories.d.ts +3 -3
  66. package/dist/types/src/components/Input/Input.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/Link/Link.stories.d.ts.map +1 -1
  68. package/dist/types/src/components/List/List.d.ts +5 -3
  69. package/dist/types/src/components/List/List.d.ts.map +1 -1
  70. package/dist/types/src/components/List/List.stories.d.ts +3 -1
  71. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  72. package/dist/types/src/components/List/Tree.d.ts.map +1 -1
  73. package/dist/types/src/components/List/Tree.stories.d.ts.map +1 -1
  74. package/dist/types/src/components/List/Treegrid.d.ts +5 -9
  75. package/dist/types/src/components/List/Treegrid.d.ts.map +1 -1
  76. package/dist/types/src/components/List/Treegrid.stories.d.ts.map +1 -1
  77. package/dist/types/src/components/Main/Main.d.ts.map +1 -1
  78. package/dist/types/src/components/Main/Main.stories.d.ts.map +1 -1
  79. package/dist/types/src/components/Menu/ContextMenu.stories.d.ts.map +1 -1
  80. package/dist/types/src/components/Menu/DropdownMenu.d.ts +51 -50
  81. package/dist/types/src/components/Menu/DropdownMenu.d.ts.map +1 -1
  82. package/dist/types/src/components/Menu/DropdownMenu.stories.d.ts +14 -1
  83. package/dist/types/src/components/Menu/DropdownMenu.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/Message/Message.d.ts.map +1 -1
  85. package/dist/types/src/components/Message/Message.stories.d.ts +2 -2
  86. package/dist/types/src/components/Message/Message.stories.d.ts.map +1 -1
  87. package/dist/types/src/components/Popover/Popover.d.ts +31 -23
  88. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  89. package/dist/types/src/components/Popover/Popover.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts +12 -9
  91. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  92. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts +18 -5
  93. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  94. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts +34 -13
  95. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
  96. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts +5 -6
  97. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts.map +1 -1
  98. package/dist/types/src/components/Select/Select.stories.d.ts +2 -2
  99. package/dist/types/src/components/Select/Select.stories.d.ts.map +1 -1
  100. package/dist/types/src/components/Separator/Separator.d.ts +3 -3
  101. package/dist/types/src/components/Separator/Separator.d.ts.map +1 -1
  102. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/Splitter/Splitter.d.ts +19 -17
  104. package/dist/types/src/components/Splitter/Splitter.d.ts.map +1 -1
  105. package/dist/types/src/components/Splitter/Splitter.stories.d.ts.map +1 -1
  106. package/dist/types/src/components/Status/Status.d.ts +3 -4
  107. package/dist/types/src/components/Status/Status.d.ts.map +1 -1
  108. package/dist/types/src/components/Status/Status.stories.d.ts.map +1 -1
  109. package/dist/types/src/components/Tag/Tag.stories.d.ts.map +1 -1
  110. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts +1 -1
  111. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  112. package/dist/types/src/components/ThemeProvider/ThemeProvider.stories.d.ts +1 -1
  113. package/dist/types/src/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
  114. package/dist/types/src/components/Toast/Toast.d.ts +15 -15
  115. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  116. package/dist/types/src/components/Toast/Toast.stories.d.ts.map +1 -1
  117. package/dist/types/src/components/Toolbar/Toolbar.d.ts +8 -16
  118. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  119. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  120. package/dist/types/src/components/Tooltip/Tooltip.d.ts +9 -9
  121. package/dist/types/src/components/Tooltip/Tooltip.d.ts.map +1 -1
  122. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts +2 -2
  123. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts.map +1 -1
  124. package/dist/types/src/components/index.d.ts +1 -0
  125. package/dist/types/src/components/index.d.ts.map +1 -1
  126. package/dist/types/src/exemplars/generics.stories.d.ts +6 -5
  127. package/dist/types/src/exemplars/generics.stories.d.ts.map +1 -1
  128. package/dist/types/src/exemplars/slot.stories.d.ts +1 -0
  129. package/dist/types/src/exemplars/slot.stories.d.ts.map +1 -1
  130. package/dist/types/src/hooks/index.d.ts +1 -0
  131. package/dist/types/src/hooks/index.d.ts.map +1 -1
  132. package/dist/types/src/hooks/useDensityContext.d.ts +1 -1
  133. package/dist/types/src/hooks/useDensityContext.d.ts.map +1 -1
  134. package/dist/types/src/playground/Custom.stories.d.ts.map +1 -1
  135. package/dist/types/src/primitives/Column/Column.d.ts +21 -14
  136. package/dist/types/src/primitives/Column/Column.d.ts.map +1 -1
  137. package/dist/types/src/primitives/Column/Column.stories.d.ts +19 -0
  138. package/dist/types/src/primitives/Column/Column.stories.d.ts.map +1 -1
  139. package/dist/types/src/primitives/Container/Container.d.ts +4 -5
  140. package/dist/types/src/primitives/Container/Container.d.ts.map +1 -1
  141. package/dist/types/src/primitives/Container/Container.stories.d.ts.map +1 -1
  142. package/dist/types/src/primitives/Flex/Flex.d.ts +5 -7
  143. package/dist/types/src/primitives/Flex/Flex.d.ts.map +1 -1
  144. package/dist/types/src/primitives/Flex/Flex.stories.d.ts.map +1 -1
  145. package/dist/types/src/primitives/Grid/Grid.d.ts +3 -8
  146. package/dist/types/src/primitives/Grid/Grid.d.ts.map +1 -1
  147. package/dist/types/src/primitives/Grid/Grid.stories.d.ts.map +1 -1
  148. package/dist/types/src/primitives/Panel/Panel.d.ts +24 -15
  149. package/dist/types/src/primitives/Panel/Panel.d.ts.map +1 -1
  150. package/dist/types/src/primitives/Panel/Panel.stories.d.ts.map +1 -1
  151. package/dist/types/src/testing/Loading.d.ts.map +1 -1
  152. package/dist/types/src/testing/decorators/withTheme.d.ts +1 -1
  153. package/dist/types/src/testing/decorators/withTheme.d.ts.map +1 -1
  154. package/dist/types/src/translations.d.ts +3 -3
  155. package/dist/types/src/util/usePx.d.ts.map +1 -1
  156. package/dist/types/tsconfig.tsbuildinfo +1 -1
  157. package/package.json +18 -24
  158. package/src/components/Avatars/Avatar.stories.tsx +2 -3
  159. package/src/components/Avatars/Avatar.tsx +1 -2
  160. package/src/components/Avatars/AvatarGroup.stories.tsx +0 -1
  161. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -2
  162. package/src/components/Button/Button.stories.tsx +0 -1
  163. package/src/components/Button/Button.tsx +5 -13
  164. package/src/components/Button/IconButton.stories.tsx +6 -4
  165. package/src/components/Button/IconButton.tsx +1 -3
  166. package/src/components/Button/Toggle.stories.tsx +0 -1
  167. package/src/components/Button/Toggle.tsx +4 -4
  168. package/src/components/Button/ToggleGroup.stories.tsx +0 -1
  169. package/src/components/Button/ToggleGroup.tsx +12 -16
  170. package/src/components/Card/Card.stories.tsx +12 -12
  171. package/src/components/Card/Card.tsx +266 -108
  172. package/src/components/Clipboard/CopyButton.tsx +3 -4
  173. package/src/components/Dialog/AlertDialog.stories.tsx +5 -6
  174. package/src/components/Dialog/AlertDialog.tsx +67 -126
  175. package/src/components/Dialog/Dialog.stories.tsx +64 -9
  176. package/src/components/Dialog/Dialog.tsx +84 -88
  177. package/src/components/ErrorFallback/ErrorFallback.stories.tsx +3 -8
  178. package/src/components/ErrorFallback/ErrorStack.tsx +36 -2
  179. package/src/components/Focus/AUDIT.md +43 -0
  180. package/src/components/Focus/Focus.stories.tsx +230 -0
  181. package/src/components/Focus/Focus.tsx +201 -0
  182. package/src/components/Focus/index.ts +5 -0
  183. package/src/components/Icon/Icon.stories.tsx +43 -13
  184. package/src/components/Icon/Icon.tsx +5 -1
  185. package/src/components/Image/Image.stories.tsx +3 -3
  186. package/src/components/Image/Image.tsx +16 -3
  187. package/src/components/Input/Input.stories.tsx +3 -4
  188. package/src/components/Input/Input.tsx +7 -7
  189. package/src/components/Link/Link.stories.tsx +0 -1
  190. package/src/components/List/List.stories.tsx +0 -1
  191. package/src/components/List/List.tsx +6 -5
  192. package/src/components/List/Tree.stories.tsx +1 -2
  193. package/src/components/List/Tree.tsx +0 -1
  194. package/src/components/List/Treegrid.stories.tsx +26 -27
  195. package/src/components/List/Treegrid.tsx +14 -14
  196. package/src/components/Main/Main.stories.tsx +0 -1
  197. package/src/components/Main/Main.tsx +1 -2
  198. package/src/components/Menu/ContextMenu.stories.tsx +0 -1
  199. package/src/components/Menu/DropdownMenu.stories.tsx +0 -1
  200. package/src/components/Menu/DropdownMenu.tsx +48 -42
  201. package/src/components/Message/Message.stories.tsx +6 -7
  202. package/src/components/Message/Message.tsx +1 -5
  203. package/src/components/Popover/Popover.stories.tsx +4 -5
  204. package/src/components/Popover/Popover.tsx +42 -42
  205. package/src/components/ScrollArea/ScrollArea.stories.tsx +93 -30
  206. package/src/components/ScrollArea/ScrollArea.tsx +39 -23
  207. package/src/components/ScrollContainer/ScrollContainer.stories.tsx +20 -18
  208. package/src/components/ScrollContainer/ScrollContainer.tsx +200 -91
  209. package/src/components/Select/Select.stories.tsx +5 -6
  210. package/src/components/Separator/Separator.tsx +4 -7
  211. package/src/components/Skeleton/Skeleton.stories.tsx +0 -1
  212. package/src/components/Splitter/Splitter.stories.tsx +29 -29
  213. package/src/components/Splitter/Splitter.tsx +35 -34
  214. package/src/components/Status/Status.stories.tsx +0 -1
  215. package/src/components/Status/Status.tsx +8 -5
  216. package/src/components/Tag/Tag.stories.tsx +0 -1
  217. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +0 -1
  218. package/src/components/ThemeProvider/ThemeProvider.tsx +5 -4
  219. package/src/components/Toast/Toast.stories.tsx +0 -1
  220. package/src/components/Toast/Toast.tsx +16 -31
  221. package/src/components/Toolbar/Toolbar.stories.tsx +0 -1
  222. package/src/components/Toolbar/Toolbar.tsx +19 -34
  223. package/src/components/Tooltip/Tooltip.stories.tsx +6 -7
  224. package/src/components/Tooltip/Tooltip.tsx +15 -16
  225. package/src/components/index.ts +1 -0
  226. package/src/exemplars/generics.stories.tsx +7 -15
  227. package/src/exemplars/slot.stories.tsx +67 -57
  228. package/src/exemplars/tabster.stories.tsx +1 -1
  229. package/src/exemplars/virtualizer.stories.tsx +4 -4
  230. package/src/hooks/index.ts +1 -0
  231. package/src/hooks/useDensityContext.ts +2 -2
  232. package/src/playground/Custom.stories.tsx +6 -9
  233. package/src/primitives/Column/AUDIT.md +148 -0
  234. package/src/primitives/Column/Column.stories.tsx +122 -19
  235. package/src/primitives/Column/Column.tsx +73 -42
  236. package/src/primitives/Container/Container.stories.tsx +0 -1
  237. package/src/primitives/Container/Container.tsx +5 -8
  238. package/src/primitives/Flex/Flex.stories.tsx +0 -1
  239. package/src/primitives/Flex/Flex.tsx +10 -12
  240. package/src/primitives/Grid/Grid.stories.tsx +0 -1
  241. package/src/primitives/Grid/Grid.tsx +4 -9
  242. package/src/primitives/Panel/Panel.stories.tsx +9 -8
  243. package/src/primitives/Panel/Panel.tsx +64 -63
  244. package/src/testing/Loading.tsx +25 -4
  245. package/src/testing/decorators/withLayout.tsx +6 -6
  246. package/src/testing/decorators/withTheme.tsx +10 -7
  247. package/src/translations.ts +3 -3
  248. package/src/util/usePx.ts +1 -0
  249. package/dist/lib/browser/chunk-2FKSMWNY.mjs.map +0 -7
  250. package/dist/lib/node-esm/chunk-ZNBLTSHI.mjs.map +0 -7
@@ -0,0 +1,201 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import {
6
+ useArrowNavigationGroup,
7
+ useFocusableGroup,
8
+ useMergedTabsterAttributes_unstable,
9
+ } from '@fluentui/react-tabster';
10
+ import { useComposedRefs } from '@radix-ui/react-compose-refs';
11
+ import { Primitive } from '@radix-ui/react-primitive';
12
+ import { Slot } from '@radix-ui/react-slot';
13
+ import React, {
14
+ type FocusEvent,
15
+ type KeyboardEvent,
16
+ type MouseEvent,
17
+ createContext,
18
+ useCallback,
19
+ useContext,
20
+ useRef,
21
+ useState,
22
+ } from 'react';
23
+
24
+ import { composableProps, slottable } from '@dxos/ui-theme';
25
+ import { type Axis } from '@dxos/ui-types';
26
+
27
+ import { useThemeContext } from '../../hooks';
28
+
29
+ //
30
+ // Context
31
+ //
32
+
33
+ type FocusState = 'active' | 'error';
34
+
35
+ const FOCUS_STATE_ATTR = 'focus-state';
36
+
37
+ type ContextValue = {
38
+ setFocus?: (state: FocusState | undefined) => void;
39
+ /** True when any item within the group has DOM focus. */
40
+ groupHasFocus?: boolean;
41
+ };
42
+
43
+ const FocusContext = createContext<ContextValue>({});
44
+
45
+ const useFocus = () => useContext(FocusContext);
46
+
47
+ //
48
+ // Group
49
+ //
50
+
51
+ type GroupProps = {
52
+ orientation?: Axis;
53
+ /** Show a subdued ring when unfocused (e.g., as a cell border). */
54
+ border?: boolean;
55
+ onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
56
+ };
57
+
58
+ /**
59
+ * Provides arrow-key navigation across focusable children via tabster.
60
+ * Does not manage `aria-current` — use `Focus.Item` on each child for that.
61
+ */
62
+ // TODO(wittjosiah): Consider how this could integrate with with react-ui-attention.
63
+ // Perhaps react-ui-attention comes under the mosaic umbrella as it supports selection?
64
+ const Group = slottable<HTMLDivElement, GroupProps>(
65
+ ({ children, asChild, orientation = 'vertical', border = false, ...props }, forwardedRef) => {
66
+ const Comp = asChild ? Slot : Primitive.div;
67
+ const { tx } = useThemeContext();
68
+ const rootRef = useRef<HTMLDivElement>(null);
69
+ const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' });
70
+ const arrowNavigationAttrs = useArrowNavigationGroup({ axis: orientation, memorizeCurrent: true });
71
+ const tabsterAttrs = useMergedTabsterAttributes_unstable(focusableGroupAttrs, arrowNavigationAttrs);
72
+ const [state, setState] = useState<FocusState | undefined>();
73
+ const [groupHasFocus, setGroupHasFocus] = useState(false);
74
+
75
+ const handleFocusIn = useCallback(() => setGroupHasFocus(true), []);
76
+ const handleFocusOut = useCallback((event: FocusEvent<HTMLDivElement>) => {
77
+ const related = event.relatedTarget as HTMLElement | null;
78
+ if (!related || !rootRef.current?.contains(related)) {
79
+ setGroupHasFocus(false);
80
+ }
81
+ }, []);
82
+
83
+ const { className, ...rest } = composableProps(props);
84
+ return (
85
+ <FocusContext.Provider value={{ setFocus: setState, groupHasFocus }}>
86
+ <Comp
87
+ {...rest}
88
+ tabIndex={0}
89
+ className={tx('focus.group', { border }, className)}
90
+ {...tabsterAttrs}
91
+ {...(state && {
92
+ [`data-${FOCUS_STATE_ATTR}`]: state,
93
+ })}
94
+ onBlur={handleFocusOut}
95
+ onFocus={handleFocusIn}
96
+ ref={useComposedRefs<HTMLDivElement>(rootRef, forwardedRef)}
97
+ >
98
+ {children}
99
+ </Comp>
100
+ </FocusContext.Provider>
101
+ );
102
+ },
103
+ );
104
+
105
+ //
106
+ // Item
107
+ //
108
+
109
+ type ItemProps = {
110
+ current?: boolean;
111
+ /** Show a subdued ring when unfocused (e.g., as a cell border). */
112
+ border?: boolean;
113
+ onCurrentChange?: () => void;
114
+ };
115
+
116
+ /**
117
+ * Focusable item within a `Focus.Group`.
118
+ * Uses `useFocusableGroup` so the parent Group's arrow navigation treats this as a single unit
119
+ * (internal buttons are not arrow-navigation targets; Enter/Escape to go in/out).
120
+ * Supports controlled (`current` prop) and uncontrolled (focus-driven) `aria-current`.
121
+ */
122
+ const Item = slottable<HTMLDivElement, ItemProps>(
123
+ (
124
+ { children, asChild, current, border = false, onCurrentChange, onClick, onFocus, onBlur, ...props },
125
+ forwardedRef,
126
+ ) => {
127
+ const Comp = asChild ? Slot : Primitive.div;
128
+ const { tx } = useThemeContext();
129
+ // Tell tabster's groupper to ignore Enter so it doesn't move focus into the group.
130
+ const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true } });
131
+ const [focused, setFocused] = useState(false);
132
+
133
+ const handleClick = useCallback(
134
+ (event: MouseEvent<HTMLDivElement>) => {
135
+ onCurrentChange?.();
136
+ onClick?.(event);
137
+ },
138
+ [onCurrentChange, onClick],
139
+ );
140
+
141
+ const handleKeyDown = useCallback(
142
+ (event: KeyboardEvent<HTMLDivElement>) => {
143
+ if (event.key === 'Enter') {
144
+ onCurrentChange?.();
145
+ }
146
+ },
147
+ [onCurrentChange],
148
+ );
149
+
150
+ const handleFocus = useCallback(
151
+ (event: FocusEvent<HTMLDivElement>) => {
152
+ setFocused(true);
153
+ onFocus?.(event);
154
+ },
155
+ [onFocus],
156
+ );
157
+
158
+ const handleBlur = useCallback(
159
+ (event: FocusEvent<HTMLDivElement>) => {
160
+ setFocused(false);
161
+ onBlur?.(event);
162
+ },
163
+ [onBlur],
164
+ );
165
+
166
+ // Controlled `current` prop takes precedence (e.g., virtualized items that scroll back into view).
167
+ // Otherwise fall back to DOM focus state.
168
+ const isCurrent = current ?? focused;
169
+
170
+ const { className, ...rest } = composableProps(props);
171
+ return (
172
+ <Comp
173
+ {...rest}
174
+ tabIndex={0}
175
+ className={tx('focus.item', { border }, className)}
176
+ {...focusableGroupAttrs}
177
+ aria-current={isCurrent || undefined}
178
+ onClick={handleClick}
179
+ onKeyDown={handleKeyDown}
180
+ onFocus={handleFocus}
181
+ onBlur={handleBlur}
182
+ ref={forwardedRef}
183
+ >
184
+ {children}
185
+ </Comp>
186
+ );
187
+ },
188
+ );
189
+
190
+ //
191
+ // Focus
192
+ //
193
+
194
+ export const Focus = {
195
+ Group,
196
+ Item,
197
+ };
198
+
199
+ export type { GroupProps as FocusGroupProps, ItemProps as FocusItemProps };
200
+
201
+ export { useFocus };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './Focus';
@@ -6,10 +6,9 @@ import { IconBase, type IconProps, type IconWeight } from '@phosphor-icons/react
6
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
7
7
  import React, { type FC, type ReactElement, type SVGProps, forwardRef } from 'react';
8
8
 
9
- import { getSize, mx } from '@dxos/ui-theme';
9
+ import { getSize, iconSize, mx } from '@dxos/ui-theme';
10
10
 
11
11
  import { withTheme } from '../../testing';
12
-
13
12
  import { Icon } from './Icon';
14
13
 
15
14
  /**
@@ -50,29 +49,60 @@ const createIcon = ({
50
49
  return CustomIcon;
51
50
  };
52
51
 
53
- const DefaultStory = ({ CustomIcon }: { CustomIcon: FC<IconProps> }) => {
54
- return (
55
- <div className='grid grid-cols-2 gap-8'>
56
- <CustomIcon weight={'regular'} className={mx(getSize(16))} />
57
- <Icon icon='ph--github-logo--regular' classNames={mx(getSize(16))} />
58
- </div>
59
- );
60
- };
61
-
62
52
  const meta = {
63
53
  title: 'ui/react-ui-core/components/Icon',
64
- render: DefaultStory,
54
+ component: Icon,
65
55
  decorators: [withTheme()],
66
56
  parameters: {
67
57
  layout: 'centered',
68
58
  },
69
- } satisfies Meta<typeof DefaultStory>;
59
+ } satisfies Meta<typeof Icon>;
70
60
 
71
61
  export default meta;
72
62
 
73
63
  type Story = StoryObj<typeof meta>;
74
64
 
75
65
  export const Default: Story = {
66
+ args: {
67
+ icon: 'ph--github-logo--regular',
68
+ },
69
+ };
70
+
71
+ export const Static: Story = {
72
+ args: {
73
+ icon: 'ph--github-logo--regular',
74
+ classNames: getSize(8),
75
+ },
76
+ };
77
+
78
+ export const Dynamic: Story = {
79
+ args: {
80
+ icon: 'ph--github-logo--regular',
81
+ },
82
+ render: (args) => {
83
+ return (
84
+ <div className='flex gap-4'>
85
+ <Icon {...args} />
86
+ <div className='flex gap-4' style={iconSize(8)}>
87
+ <Icon {...args} />
88
+ <div className='flex gap-4' style={iconSize(null)}>
89
+ <Icon {...args} />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ );
94
+ },
95
+ };
96
+
97
+ export const Custom = {
98
+ render: ({ CustomIcon }: { CustomIcon: FC<IconProps> }) => {
99
+ return (
100
+ <div className='grid grid-cols-2 gap-8'>
101
+ <CustomIcon weight={'regular'} className={mx(getSize(16))} />
102
+ <Icon icon='ph--github-logo--regular' classNames={mx(getSize(16))} />
103
+ </div>
104
+ );
105
+ },
76
106
  args: {
77
107
  CustomIcon: createIcon({
78
108
  name: 'GithubLogo',
@@ -15,10 +15,14 @@ export type IconProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.s
15
15
  size?: Size;
16
16
  };
17
17
 
18
+ /**
19
+ * The Icon's size can be set directly or inherited from the `--dx-icon-size` CSS variable.
20
+ */
18
21
  export const Icon = memo(
19
- forwardRef<SVGSVGElement, IconProps>(({ icon, classNames, size = 4, ...props }, forwardedRef) => {
22
+ forwardRef<SVGSVGElement, IconProps>(({ icon, classNames, size, ...props }, forwardedRef) => {
20
23
  const { tx } = useThemeContext();
21
24
  const href = useIconHref(icon);
25
+
22
26
  return (
23
27
  <svg {...props} className={tx('icon.root', { size }, classNames)} ref={forwardedRef}>
24
28
  <use href={href} />
@@ -5,14 +5,14 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useMemo } from 'react';
7
7
 
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
  import { withTheme } from '@dxos/react-ui/testing';
10
10
 
11
11
  import { Image } from './Image';
12
12
 
13
13
  const seed = Math.random();
14
14
 
15
- faker.seed(seed);
15
+ random.seed(seed);
16
16
 
17
17
  const meta = {
18
18
  title: 'ui/react-ui-mosaic/Image',
@@ -34,7 +34,7 @@ type Story = StoryObj<typeof meta>;
34
34
 
35
35
  export const Default: Story = {
36
36
  args: {
37
- src: faker.image.url(),
37
+ src: random.image.url(),
38
38
  },
39
39
  };
40
40
 
@@ -13,15 +13,16 @@ export type ImageProps = ThemedClassName<
13
13
  {
14
14
  src: string;
15
15
  alt?: string;
16
+ fit?: 'contain' | 'cover';
16
17
  crossOrigin?: 'anonymous' | 'use-credentials' | '';
17
18
  } & ColorOptions
18
19
  >;
19
20
 
20
- // TODO(burdon): Option for neutral background color.
21
21
  export const Image = ({
22
22
  classNames,
23
23
  src,
24
24
  alt = '',
25
+ fit = 'contain',
25
26
  crossOrigin = 'anonymous',
26
27
  sampleSize = 64,
27
28
  contrast = 0.9,
@@ -68,7 +69,16 @@ export const Image = ({
68
69
 
69
70
  return (
70
71
  <div
71
- className={mx(`relative flex w-full justify-center overflow-hidden transition-all duration-700`, classNames)}
72
+ // `isolate` (`isolation: isolate`) creates a new stacking context so
73
+ // the inner <img>'s `z-10` stays scoped to this wrapper. Without it
74
+ // the z-10 leaks into the parent's stacking context and elevates the
75
+ // image above any pseudo-element rings (e.g. Focus.Item's
76
+ // `dx-ring-pseudo` `::after`) painted on ancestors — most visibly,
77
+ // the focus ring on a Card containing a Card.Poster.
78
+ className={mx(
79
+ `relative flex w-full justify-center overflow-hidden transition-all duration-700 isolate`,
80
+ classNames,
81
+ )}
72
82
  style={{
73
83
  backgroundColor: dominantColor,
74
84
  }}
@@ -94,7 +104,10 @@ export const Image = ({
94
104
  crossOrigin={crossOriginState}
95
105
  onError={handleImageError}
96
106
  onLoad={handleImageLoad}
97
- className='z-10 object-contain transition-opacity duration-500'
107
+ className={mx(
108
+ 'z-10 transition-opacity duration-500',
109
+ fit === 'cover' ? 'w-full h-full object-cover' : 'object-contain',
110
+ )}
98
111
  style={{
99
112
  opacity: imageLoaded ? 1 : 0,
100
113
  }}
@@ -8,7 +8,6 @@ import React from 'react';
8
8
  import { type MessageValence } from '@dxos/ui-types';
9
9
 
10
10
  import { withLayoutVariants, withTheme } from '../../testing';
11
-
12
11
  import {
13
12
  type CheckboxProps,
14
13
  Input,
@@ -28,7 +27,7 @@ type VariantMap = {
28
27
 
29
28
  type Variant = { [K in keyof VariantMap]: { type: K } & VariantMap[K] }[keyof VariantMap];
30
29
 
31
- type StoryProps = Partial<{
30
+ type DefaultStoryProps = Partial<{
32
31
  kind: keyof VariantMap;
33
32
  label: string;
34
33
  labelVisuallyHidden: boolean;
@@ -47,7 +46,7 @@ const DefaultStory = ({
47
46
  validationValence,
48
47
  validationMessage,
49
48
  ...props
50
- }: StoryProps) => {
49
+ }: DefaultStoryProps) => {
51
50
  return (
52
51
  <Input.Root {...{ validationValence }}>
53
52
  <Input.Label srOnly={labelVisuallyHidden}>{label}</Input.Label>
@@ -75,7 +74,7 @@ const meta = {
75
74
 
76
75
  export default meta;
77
76
 
78
- type Story = StoryObj<StoryProps & Variant>;
77
+ type Story = StoryObj<DefaultStoryProps & Variant>;
79
78
 
80
79
  export const DensityCoarse: Story = {
81
80
  args: {
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Root as CheckboxPrimitive, type CheckboxProps as CheckboxPrimitiveProps } from '@radix-ui/react-checkbox';
5
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
6
6
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
7
  import React, { type ComponentPropsWithRef, type ForwardRefExoticComponent, forwardRef } from 'react';
8
8
 
@@ -130,13 +130,13 @@ type TextInputProps = InputSharedProps & ThemedClassName<TextInputPrimitiveProps
130
130
 
131
131
  const TextInput = forwardRef<HTMLInputElement, InputScopedProps<TextInputProps>>(
132
132
  (
133
- { __inputScope, classNames, density: propsDensity, elevation: propsElevation, variant, noAutoFill, ...props },
133
+ { __inputScope, classNames, density: densityProp, elevation: elevationProp, variant, noAutoFill, ...props },
134
134
  forwardedRef,
135
135
  ) => {
136
136
  const { hasIosKeyboard } = useThemeContext();
137
137
  const { tx } = useThemeContext();
138
- const density = useDensityContext(propsDensity);
139
- const elevation = useElevationContext(propsElevation);
138
+ const density = useDensityContext(densityProp);
139
+ const elevation = useElevationContext(elevationProp);
140
140
  const { validationValence } = useInputContext(INPUT_NAME, __inputScope);
141
141
 
142
142
  return (
@@ -193,7 +193,7 @@ const TextArea = forwardRef<HTMLTextAreaElement, InputScopedProps<TextAreaProps>
193
193
  },
194
194
  );
195
195
 
196
- type CheckboxProps = ThemedClassName<Omit<CheckboxPrimitiveProps, 'children'>> & {
196
+ type CheckboxProps = ThemedClassName<Omit<CheckboxPrimitive.CheckboxProps, 'children'>> & {
197
197
  size?: Size;
198
198
  };
199
199
 
@@ -222,7 +222,7 @@ const Checkbox: ForwardRefExoticComponent<CheckboxProps> = forwardRef<
222
222
  const { tx } = useThemeContext();
223
223
 
224
224
  return (
225
- <CheckboxPrimitive
225
+ <CheckboxPrimitive.Root
226
226
  {...{
227
227
  ...props,
228
228
  checked,
@@ -241,7 +241,7 @@ const Checkbox: ForwardRefExoticComponent<CheckboxProps> = forwardRef<
241
241
  icon={checked === 'indeterminate' ? 'ph--minus--regular' : 'ph--check--regular'}
242
242
  classNames={tx('input.checkboxIndicator', { size, checked })}
243
243
  />
244
- </CheckboxPrimitive>
244
+ </CheckboxPrimitive.Root>
245
245
  );
246
246
  },
247
247
  );
@@ -5,7 +5,6 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
7
  import { withTheme } from '../../testing';
8
-
9
8
  import { Link } from './Link';
10
9
 
11
10
  const meta = {
@@ -13,7 +13,6 @@ import { getSize, ghostHover, mx, surfaceShadow } from '@dxos/ui-theme';
13
13
 
14
14
  import { withTheme } from '../../testing';
15
15
  import { Icon } from '../Icon';
16
-
17
16
  import { List, ListItem, type ListScopedProps } from './List';
18
17
 
19
18
  const meta = {
@@ -24,24 +24,25 @@ import {
24
24
  useListContext,
25
25
  useListItemContext,
26
26
  } from '@dxos/react-list';
27
+ import { composable, composableProps } from '@dxos/ui-theme';
27
28
  import { type Density } from '@dxos/ui-types';
28
29
 
29
30
  import { useDensityContext, useThemeContext } from '../../hooks';
30
31
  import { type ThemedClassName } from '../../util';
31
32
  import { DensityProvider } from '../DensityProvider';
32
33
  import { Icon } from '../Icon';
33
-
34
34
  import { ListDropIndicator } from './ListDropIndicator';
35
35
 
36
- type ListProps = ThemedClassName<ListPrimitiveProps> & { density?: Density };
36
+ type ListProps = ThemedClassName<ListPrimitiveProps & { density?: Density }>;
37
37
 
38
- const List = forwardRef<HTMLOListElement, ListProps>(({ classNames, children, ...props }, forwardedRef) => {
38
+ const List = composable<HTMLOListElement, ListProps>(({ children, ...props }, forwardedRef) => {
39
39
  const { tx } = useThemeContext();
40
40
  const density = useDensityContext(props.density);
41
+ const { className, ...rest } = composableProps(props);
41
42
 
42
43
  return (
43
44
  <DensityProvider density={density}>
44
- <ListPrimitive {...props} className={tx('list.root', {}, classNames)} ref={forwardedRef}>
45
+ <ListPrimitive {...rest} className={tx('list.root', {}, className)} ref={forwardedRef}>
45
46
  {children}
46
47
  </ListPrimitive>
47
48
  </DensityProvider>
@@ -89,7 +90,7 @@ const ListItemHeading = forwardRef<HTMLParagraphElement, ListItemHeadingProps>(
89
90
  className={tx('list.item.heading', { density }, classNames)}
90
91
  ref={forwardedRef}
91
92
  >
92
- {children}
93
+ <span>{children}</span>
93
94
  </ListPrimitiveItemHeading>
94
95
  );
95
96
  },
@@ -6,7 +6,6 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
8
  import { withTheme } from '../../testing';
9
-
10
9
  import { Tree, TreeItem } from './Tree';
11
10
 
12
11
  type StorybookTreeProps = {
@@ -25,7 +24,7 @@ const createKey = (key: string, prefix?: string) => (prefix === undefined ? key
25
24
  const StorybookTreeItem = ({ data, prefix }: StorybookTreeItemProps) => {
26
25
  const keys = Array.isArray(data) ? Array.from(data.keys()) : Object.keys(data);
27
26
  return (
28
- <Tree.Root density='fine'>
27
+ <Tree.Root>
29
28
  {keys.map((key) => {
30
29
  const id = createKey(String(key), prefix);
31
30
  const value = data[key as keyof typeof data];
@@ -5,7 +5,6 @@
5
5
  import React, { type ComponentPropsWithoutRef, type FC, type ForwardRefExoticComponent, forwardRef } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '../../util';
8
-
9
8
  import {
10
9
  LIST_ITEM_NAME,
11
10
  List,
@@ -5,14 +5,13 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
 
10
10
  import { withTheme } from '../../testing';
11
11
  import { Icon } from '../Icon';
12
+ import { Treegrid, TREEGRID_PARENT_OF_SEPARATOR, TREEGRID_PATH_SEPARATOR } from './Treegrid';
12
13
 
13
- import { Treegrid } from './Treegrid';
14
-
15
- faker.seed(1234);
14
+ random.seed(1234);
16
15
 
17
16
  type StorybookNode = {
18
17
  id: string;
@@ -32,33 +31,33 @@ const content = {
32
31
  title: 'Root',
33
32
  nodes: [
34
33
  {
35
- id: faker.string.uuid(),
34
+ id: random.string.uuid(),
36
35
  title: 'Personal Space',
37
36
  icon: 'ph--house--regular',
38
37
  nodes: [
39
38
  {
40
- id: faker.string.uuid(),
41
- title: faker.commerce.productName(),
39
+ id: random.string.uuid(),
40
+ title: random.commerce.productName(),
42
41
  },
43
42
  {
44
- id: faker.string.uuid(),
45
- title: faker.commerce.productName(),
43
+ id: random.string.uuid(),
44
+ title: random.commerce.productName(),
46
45
  },
47
46
  {
48
- id: faker.string.uuid(),
49
- title: faker.commerce.productName(),
47
+ id: random.string.uuid(),
48
+ title: random.commerce.productName(),
50
49
  nodes: [
51
50
  {
52
- id: faker.string.uuid(),
53
- title: faker.commerce.productName(),
51
+ id: random.string.uuid(),
52
+ title: random.commerce.productName(),
54
53
  nodes: [
55
54
  {
56
- id: faker.string.uuid(),
57
- title: faker.commerce.productName(),
55
+ id: random.string.uuid(),
56
+ title: random.commerce.productName(),
58
57
  },
59
58
  {
60
- id: faker.string.uuid(),
61
- title: faker.commerce.productName(),
59
+ id: random.string.uuid(),
60
+ title: random.commerce.productName(),
62
61
  },
63
62
  ],
64
63
  },
@@ -67,24 +66,24 @@ const content = {
67
66
  ],
68
67
  },
69
68
  {
70
- id: faker.string.uuid(),
71
- title: faker.commerce.productName(),
69
+ id: random.string.uuid(),
70
+ title: random.commerce.productName(),
72
71
  icon: 'ph--planet--regular',
73
72
  nodes: [
74
73
  {
75
- id: faker.string.uuid(),
76
- title: faker.commerce.productName(),
74
+ id: random.string.uuid(),
75
+ title: random.commerce.productName(),
77
76
  },
78
77
  ],
79
78
  },
80
79
  {
81
- id: faker.string.uuid(),
82
- title: faker.commerce.productName(),
80
+ id: random.string.uuid(),
81
+ title: random.commerce.productName(),
83
82
  icon: 'ph--sailboat--regular',
84
83
  },
85
84
  {
86
- id: faker.string.uuid(),
87
- title: faker.commerce.productName(),
85
+ id: random.string.uuid(),
86
+ title: random.commerce.productName(),
88
87
  icon: 'ph--planet--regular',
89
88
  },
90
89
  ],
@@ -129,8 +128,8 @@ const DefaultStory = () => {
129
128
  return (
130
129
  <Treegrid.Row
131
130
  key={node.id}
132
- id={path.join(Treegrid.PATH_SEPARATOR)}
133
- {...(parentOf && { parentOf: parentOf.join(Treegrid.PARENT_OF_SEPARATOR) })}
131
+ id={path.join(TREEGRID_PATH_SEPARATOR)}
132
+ {...(parentOf && { parentOf: parentOf.join(TREEGRID_PARENT_OF_SEPARATOR) })}
134
133
  >
135
134
  <Treegrid.Cell indent classNames='flex items-center'>
136
135
  {node.icon && <Icon icon={node.icon} classNames='w-[1em] h-[1em] my-1' />}