@dxos/react-ui 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44

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 (231) hide show
  1. package/dist/lib/browser/chunk-EJYV4HAH.mjs +774 -0
  2. package/dist/lib/browser/chunk-EJYV4HAH.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3197 -66
  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 +34 -45
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node-esm/chunk-YTLZCZ2M.mjs +776 -0
  9. package/dist/lib/node-esm/chunk-YTLZCZ2M.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +3197 -66
  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 +34 -45
  14. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  15. package/dist/types/src/components/AnchoredOverflow/AnchoredOverflow.d.ts +7 -0
  16. package/dist/types/src/components/AnchoredOverflow/AnchoredOverflow.d.ts.map +1 -1
  17. package/dist/types/src/components/Button/Button.d.ts +1 -1
  18. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  19. package/dist/types/src/components/Button/IconButton.d.ts +2 -1
  20. package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
  21. package/dist/types/src/components/Button/ToggleGroup.d.ts +4 -4
  22. package/dist/types/src/components/Button/ToggleGroup.stories.d.ts +4 -4
  23. package/dist/types/src/components/DensityProvider/DensityProvider.d.ts +1 -1
  24. package/dist/types/src/components/DensityProvider/DensityProvider.d.ts.map +1 -1
  25. package/dist/types/src/components/Dialog/Dialog.d.ts +24 -9
  26. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  27. package/dist/types/src/components/Dialog/Dialog.stories.d.ts +7 -5
  28. package/dist/types/src/components/Dialog/Dialog.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/ElevationProvider/ElevationProvider.d.ts +1 -1
  30. package/dist/types/src/components/ElevationProvider/ElevationProvider.d.ts.map +1 -1
  31. package/dist/types/src/components/Icon/Icon.d.ts +1 -1
  32. package/dist/types/src/components/Icon/Icon.d.ts.map +1 -1
  33. package/dist/types/src/components/Input/Input.d.ts +5 -2
  34. package/dist/types/src/components/Input/Input.d.ts.map +1 -1
  35. package/dist/types/src/components/Input/Input.stories.d.ts +2 -2
  36. package/dist/types/src/components/Input/Input.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/List/List.d.ts +1 -1
  38. package/dist/types/src/components/List/List.d.ts.map +1 -1
  39. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Main/Main.d.ts +8 -9
  41. package/dist/types/src/components/Main/Main.d.ts.map +1 -1
  42. package/dist/types/src/components/Main/Main.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/{Menus → Menu}/ContextMenu.d.ts +6 -6
  44. package/dist/types/src/components/Menu/ContextMenu.d.ts.map +1 -0
  45. package/dist/types/src/components/Menu/ContextMenu.stories.d.ts.map +1 -0
  46. package/dist/types/src/components/{Menus → Menu}/DropdownMenu.d.ts +3 -4
  47. package/dist/types/src/components/Menu/DropdownMenu.d.ts.map +1 -0
  48. package/dist/types/src/components/Menu/DropdownMenu.stories.d.ts.map +1 -0
  49. package/dist/types/src/components/Menu/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Message/Message.d.ts +1 -1
  51. package/dist/types/src/components/Message/Message.d.ts.map +1 -1
  52. package/dist/types/src/components/Message/Message.stories.d.ts +1 -1
  53. package/dist/types/src/components/Message/Message.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Popover/Popover.d.ts +1 -1
  55. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  56. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts +25 -26
  57. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  58. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts +46 -8
  59. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts +4 -2
  61. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
  62. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts +2 -1
  63. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts.map +1 -1
  64. package/dist/types/src/components/Select/Select.d.ts +9 -9
  65. package/dist/types/src/components/Separator/Separator.d.ts +1 -1
  66. package/dist/types/src/components/Skeleton/Skeleton.d.ts +12 -0
  67. package/dist/types/src/components/Skeleton/Skeleton.d.ts.map +1 -0
  68. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts +17 -0
  69. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts.map +1 -0
  70. package/dist/types/src/components/Skeleton/index.d.ts +2 -0
  71. package/dist/types/src/components/Skeleton/index.d.ts.map +1 -0
  72. package/dist/types/src/components/Splitter/Splitter.d.ts +26 -0
  73. package/dist/types/src/components/Splitter/Splitter.d.ts.map +1 -0
  74. package/dist/types/src/components/Splitter/Splitter.stories.d.ts +7 -0
  75. package/dist/types/src/components/Splitter/Splitter.stories.d.ts.map +1 -0
  76. package/dist/types/src/components/Splitter/index.d.ts +2 -0
  77. package/dist/types/src/components/Splitter/index.d.ts.map +1 -0
  78. package/dist/types/src/components/Tag/Tag.d.ts +1 -1
  79. package/dist/types/src/components/Tag/Tag.d.ts.map +1 -1
  80. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts +2 -2
  81. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  82. package/dist/types/src/components/ThemeProvider/TranslationsProvider.d.ts +1 -8
  83. package/dist/types/src/components/ThemeProvider/TranslationsProvider.d.ts.map +1 -1
  84. package/dist/types/src/components/ThemeProvider/index.d.ts +2 -1
  85. package/dist/types/src/components/ThemeProvider/index.d.ts.map +1 -1
  86. package/dist/types/src/components/Toast/Toast.d.ts +4 -4
  87. package/dist/types/src/components/Toolbar/Toolbar.d.ts +13 -12
  88. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  89. package/dist/types/src/components/Tooltip/Tooltip.d.ts.map +1 -1
  90. package/dist/types/src/components/index.d.ts +3 -1
  91. package/dist/types/src/components/index.d.ts.map +1 -1
  92. package/dist/types/src/exemplars/generics.stories.d.ts +17 -0
  93. package/dist/types/src/exemplars/generics.stories.d.ts.map +1 -0
  94. package/dist/types/src/exemplars/slot.stories.d.ts +14 -0
  95. package/dist/types/src/exemplars/slot.stories.d.ts.map +1 -0
  96. package/dist/types/src/exemplars/tabster.stories.d.ts +8 -0
  97. package/dist/types/src/exemplars/tabster.stories.d.ts.map +1 -0
  98. package/dist/types/src/hooks/useDensityContext.d.ts +1 -1
  99. package/dist/types/src/hooks/useDensityContext.d.ts.map +1 -1
  100. package/dist/types/src/hooks/useElevationContext.d.ts +1 -1
  101. package/dist/types/src/hooks/useElevationContext.d.ts.map +1 -1
  102. package/dist/types/src/index.d.ts +2 -1
  103. package/dist/types/src/index.d.ts.map +1 -1
  104. package/dist/types/src/playground/Controls.stories.d.ts.map +1 -1
  105. package/dist/types/src/primitives/Container/Container.d.ts +23 -0
  106. package/dist/types/src/primitives/Container/Container.d.ts.map +1 -0
  107. package/dist/types/src/primitives/Container/Container.stories.d.ts +11 -0
  108. package/dist/types/src/primitives/Container/Container.stories.d.ts.map +1 -0
  109. package/dist/types/src/primitives/Container/Layout.d.ts +18 -0
  110. package/dist/types/src/primitives/Container/Layout.d.ts.map +1 -0
  111. package/dist/types/src/primitives/Container/Layout.stories.d.ts +10 -0
  112. package/dist/types/src/primitives/Container/Layout.stories.d.ts.map +1 -0
  113. package/dist/types/src/primitives/Container/index.d.ts +3 -0
  114. package/dist/types/src/primitives/Container/index.d.ts.map +1 -0
  115. package/dist/types/src/primitives/Flex/Flex.d.ts +8 -0
  116. package/dist/types/src/primitives/Flex/Flex.d.ts.map +1 -0
  117. package/dist/types/src/primitives/Flex/index.d.ts +2 -0
  118. package/dist/types/src/primitives/Flex/index.d.ts.map +1 -0
  119. package/dist/types/src/primitives/index.d.ts +3 -0
  120. package/dist/types/src/primitives/index.d.ts.map +1 -0
  121. package/dist/types/src/testing/decorators/withLayout.d.ts +3 -3
  122. package/dist/types/src/testing/decorators/withLayout.d.ts.map +1 -1
  123. package/dist/types/src/testing/decorators/withLayoutVariants.d.ts +1 -1
  124. package/dist/types/src/testing/decorators/withLayoutVariants.d.ts.map +1 -1
  125. package/dist/types/src/testing/decorators/withTheme.d.ts +3 -2
  126. package/dist/types/src/testing/decorators/withTheme.d.ts.map +1 -1
  127. package/dist/types/src/util/index.d.ts +1 -2
  128. package/dist/types/src/util/index.d.ts.map +1 -1
  129. package/dist/types/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +37 -32
  131. package/src/components/AnchoredOverflow/AnchoredOverflow.tsx +8 -0
  132. package/src/components/Avatars/Avatar.stories.tsx +4 -4
  133. package/src/components/Avatars/Avatar.tsx +1 -1
  134. package/src/components/Avatars/AvatarGroup.stories.tsx +2 -2
  135. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +2 -2
  136. package/src/components/Button/Button.stories.tsx +2 -2
  137. package/src/components/Button/Button.tsx +1 -1
  138. package/src/components/Button/IconButton.stories.tsx +2 -2
  139. package/src/components/Button/IconButton.tsx +8 -3
  140. package/src/components/Button/Toggle.stories.tsx +2 -2
  141. package/src/components/Button/ToggleGroup.stories.tsx +2 -2
  142. package/src/components/Clipboard/CopyButton.tsx +3 -3
  143. package/src/components/DensityProvider/DensityProvider.tsx +1 -1
  144. package/src/components/Dialog/AlertDialog.stories.tsx +2 -2
  145. package/src/components/Dialog/Dialog.stories.tsx +57 -23
  146. package/src/components/Dialog/Dialog.tsx +181 -40
  147. package/src/components/ElevationProvider/ElevationProvider.tsx +1 -1
  148. package/src/components/Icon/Icon.stories.tsx +3 -3
  149. package/src/components/Icon/Icon.tsx +1 -1
  150. package/src/components/Input/Input.stories.tsx +12 -11
  151. package/src/components/Input/Input.tsx +13 -5
  152. package/src/components/Link/Link.stories.tsx +2 -2
  153. package/src/components/List/List.stories.tsx +18 -14
  154. package/src/components/List/List.tsx +1 -1
  155. package/src/components/List/Tree.stories.tsx +2 -2
  156. package/src/components/List/Treegrid.stories.tsx +2 -2
  157. package/src/components/List/Treegrid.tsx +1 -1
  158. package/src/components/Main/Main.stories.tsx +41 -20
  159. package/src/components/Main/Main.tsx +92 -45
  160. package/src/components/{Menus → Menu}/ContextMenu.stories.tsx +2 -2
  161. package/src/components/{Menus → Menu}/DropdownMenu.stories.tsx +2 -2
  162. package/src/components/{Menus → Menu}/DropdownMenu.tsx +61 -57
  163. package/src/components/Message/Message.stories.tsx +3 -3
  164. package/src/components/Message/Message.tsx +30 -5
  165. package/src/components/Popover/Popover.stories.tsx +2 -2
  166. package/src/components/Popover/Popover.tsx +35 -33
  167. package/src/components/ScrollArea/ScrollArea.stories.tsx +166 -40
  168. package/src/components/ScrollArea/ScrollArea.tsx +86 -80
  169. package/src/components/ScrollArea/index.ts +1 -1
  170. package/src/components/ScrollContainer/ScrollContainer.stories.tsx +3 -2
  171. package/src/components/ScrollContainer/ScrollContainer.tsx +99 -92
  172. package/src/components/Select/Select.stories.tsx +2 -2
  173. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  174. package/src/components/Skeleton/Skeleton.tsx +26 -0
  175. package/src/components/Skeleton/index.ts +5 -0
  176. package/src/components/Splitter/Splitter.stories.tsx +73 -0
  177. package/src/components/Splitter/Splitter.tsx +123 -0
  178. package/src/components/Splitter/index.ts +5 -0
  179. package/src/components/Status/Status.stories.tsx +2 -2
  180. package/src/components/Tag/Tag.stories.tsx +4 -4
  181. package/src/components/Tag/Tag.tsx +1 -1
  182. package/src/components/ThemeProvider/ThemeProvider.tsx +2 -3
  183. package/src/components/ThemeProvider/TranslationsProvider.tsx +1 -16
  184. package/src/components/ThemeProvider/index.ts +3 -3
  185. package/src/components/Toast/Toast.stories.tsx +2 -2
  186. package/src/components/Toolbar/Toolbar.stories.tsx +2 -2
  187. package/src/components/Toolbar/Toolbar.tsx +31 -12
  188. package/src/components/Tooltip/Tooltip.stories.tsx +2 -2
  189. package/src/components/Tooltip/Tooltip.tsx +22 -20
  190. package/src/components/index.ts +3 -1
  191. package/src/exemplars/generics.stories.tsx +44 -0
  192. package/src/exemplars/slot.stories.tsx +108 -0
  193. package/src/exemplars/tabster.stories.tsx +127 -0
  194. package/src/hooks/useDensityContext.ts +1 -1
  195. package/src/hooks/useElevationContext.ts +1 -1
  196. package/src/index.ts +2 -1
  197. package/src/playground/Controls.stories.tsx +3 -4
  198. package/src/playground/Custom.stories.tsx +2 -2
  199. package/src/playground/Typography.stories.tsx +2 -2
  200. package/src/primitives/Container/Container.stories.tsx +67 -0
  201. package/src/primitives/Container/Container.tsx +82 -0
  202. package/src/primitives/Container/Layout.stories.tsx +57 -0
  203. package/src/primitives/Container/Layout.tsx +61 -0
  204. package/src/primitives/Container/index.ts +6 -0
  205. package/src/primitives/Flex/Flex.tsx +26 -0
  206. package/src/primitives/Flex/index.ts +5 -0
  207. package/src/primitives/index.ts +6 -0
  208. package/src/testing/decorators/withLayout.tsx +22 -15
  209. package/src/testing/decorators/withLayoutVariants.tsx +3 -3
  210. package/src/testing/decorators/withTheme.tsx +21 -18
  211. package/src/util/index.ts +2 -2
  212. package/dist/lib/browser/chunk-53MI2QCM.mjs +0 -4707
  213. package/dist/lib/browser/chunk-53MI2QCM.mjs.map +0 -7
  214. package/dist/lib/node-esm/chunk-ID67AFFF.mjs +0 -4709
  215. package/dist/lib/node-esm/chunk-ID67AFFF.mjs.map +0 -7
  216. package/dist/types/src/components/Menus/ContextMenu.d.ts.map +0 -1
  217. package/dist/types/src/components/Menus/ContextMenu.stories.d.ts.map +0 -1
  218. package/dist/types/src/components/Menus/DropdownMenu.d.ts.map +0 -1
  219. package/dist/types/src/components/Menus/DropdownMenu.stories.d.ts.map +0 -1
  220. package/dist/types/src/components/Menus/index.d.ts.map +0 -1
  221. package/dist/types/src/util/ThemedClassName.d.ts +0 -5
  222. package/dist/types/src/util/ThemedClassName.d.ts.map +0 -1
  223. package/dist/types/src/util/domino.d.ts +0 -18
  224. package/dist/types/src/util/domino.d.ts.map +0 -1
  225. package/src/util/ThemedClassName.ts +0 -7
  226. package/src/util/domino.ts +0 -53
  227. /package/dist/types/src/components/{Menus → Menu}/ContextMenu.stories.d.ts +0 -0
  228. /package/dist/types/src/components/{Menus → Menu}/DropdownMenu.stories.d.ts +0 -0
  229. /package/dist/types/src/components/{Menus → Menu}/index.d.ts +0 -0
  230. /package/src/components/{Menus → Menu}/ContextMenu.tsx +0 -0
  231. /package/src/components/{Menus → Menu}/index.ts +0 -0
@@ -45,16 +45,17 @@ const DefaultStory = (props: ScrollContainerRootProps) => {
45
45
  </div>
46
46
  ))}
47
47
  </ScrollContainer.Viewport>
48
+ <ScrollContainer.ScrollDownButton />
48
49
  </ScrollContainer.Root>
49
50
  </div>
50
51
  );
51
52
  };
52
53
 
53
54
  const meta = {
54
- title: 'ui/react-ui-core/ScrollContainer',
55
+ title: 'ui/react-ui-core/components/ScrollContainer',
55
56
  component: ScrollContainer.Root,
56
57
  render: DefaultStory,
57
- decorators: [withTheme, withLayout({ container: 'column', classNames: 'is-[30rem]' })],
58
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'is-[30rem]' })],
58
59
  } satisfies Meta<typeof DefaultStory>;
59
60
 
60
61
  export default meta;
@@ -2,7 +2,6 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useState } from '@preact-signals/safe-react/react';
6
5
  import { createContext } from '@radix-ui/react-context';
7
6
  import React, {
8
7
  type HTMLAttributes,
@@ -13,16 +12,18 @@ import React, {
13
12
  useImperativeHandle,
14
13
  useMemo,
15
14
  useRef,
15
+ useState,
16
16
  } from 'react';
17
17
 
18
18
  // TODO(burdon): Move these deps to @dxos/dom-util.
19
19
  import { addEventListener, combine } from '@dxos/async';
20
20
  import { invariant } from '@dxos/invariant';
21
21
  import { useForwardedRef } from '@dxos/react-hooks';
22
- import { mx } from '@dxos/react-ui-theme';
22
+ import { mx } from '@dxos/ui-theme';
23
23
 
24
24
  import { type ThemedClassName } from '../../util';
25
25
  import { IconButton } from '../Button';
26
+ import { ScrollArea } from '../ScrollArea';
26
27
 
27
28
  const isBottom = (el: HTMLElement | null) => {
28
29
  return !!(el && el.scrollHeight - el.scrollTop === el.clientHeight);
@@ -51,98 +52,101 @@ type RootProps = ThemedClassName<
51
52
  PropsWithChildren<{
52
53
  pin?: boolean;
53
54
  fade?: boolean;
55
+ behavior?: ScrollBehavior;
54
56
  }>
55
57
  >;
56
58
 
57
59
  /**
58
60
  * Scroll container that automatically scrolls to the bottom when new content is added.
59
61
  */
60
- const Root = forwardRef<ScrollController, RootProps>(({ children, classNames, pin, fade }, forwardedRef) => {
61
- const scrollerRef = useRef<HTMLDivElement>(null);
62
- const autoScrollRef = useRef(false);
63
- const [overflow, setOverflow] = useState(false);
64
- const [pinned, setPinned] = useState(pin);
65
-
66
- const timeoutRef = useRef<NodeJS.Timeout>(undefined);
67
- const scrollToBottom = useCallback((behavior: ScrollBehavior = 'instant') => {
68
- if (scrollerRef.current) {
69
- // Temporarily hide scrollbar to prevent flicker.
70
- autoScrollRef.current = true;
71
- scrollerRef.current.classList.add('scrollbar-none');
72
- scrollerRef.current.scrollTo({
73
- top: scrollerRef.current.scrollHeight,
74
- behavior,
75
- });
76
-
77
- clearTimeout(timeoutRef.current);
78
- if (behavior !== 'instant') {
79
- timeoutRef.current = setTimeout(() => {
80
- scrollerRef.current?.classList.remove('scrollbar-none');
81
- autoScrollRef.current = false;
82
- }, 500);
62
+ const Root = forwardRef<ScrollController, RootProps>(
63
+ ({ children, classNames, pin, fade, behavior: behaviorProp = 'smooth' }, forwardedRef) => {
64
+ const scrollerRef = useRef<HTMLDivElement>(null);
65
+ const autoScrollRef = useRef(false);
66
+ const [overflow, setOverflow] = useState(false);
67
+ const [pinned, setPinned] = useState(pin);
68
+
69
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
70
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = behaviorProp) => {
71
+ if (scrollerRef.current) {
72
+ // Temporarily hide scrollbar to prevent flicker.
73
+ autoScrollRef.current = true;
74
+ scrollerRef.current.classList.add('scrollbar-none');
75
+ scrollerRef.current.scrollTo({
76
+ top: scrollerRef.current.scrollHeight,
77
+ behavior,
78
+ });
79
+
80
+ clearTimeout(timeoutRef.current);
81
+ if (behavior !== 'instant') {
82
+ timeoutRef.current = setTimeout(() => {
83
+ scrollerRef.current?.classList.remove('scrollbar-none');
84
+ autoScrollRef.current = false;
85
+ }, 500);
86
+ }
87
+ setPinned(true);
83
88
  }
84
- setPinned(true);
85
- }
86
- }, []);
87
-
88
- const controller = useMemo(
89
- () => ({
90
- viewport: scrollerRef.current,
91
- scrollToTop: () => {
92
- invariant(scrollerRef.current);
93
- scrollerRef.current.scrollTo({ top: 0, behavior: 'smooth' });
94
- setPinned(false);
95
- },
96
- scrollToBottom: () => {
97
- scrollToBottom('smooth');
98
- },
99
- }),
100
- [scrollToBottom, scrollerRef.current],
101
- );
102
-
103
- // Scroll controller imperative ref.
104
- useImperativeHandle(forwardedRef, () => controller, [controller]);
105
-
106
- // Listen for scroll events.
107
- useEffect(() => {
108
- if (!scrollerRef.current) {
109
- return;
110
- }
111
-
112
- return combine(
113
- // Check if user scrolls.
114
- addEventListener(scrollerRef.current, 'wheel', () => {
115
- setPinned(isBottom(scrollerRef.current));
116
- }),
117
- // Check if scrolls.
118
- addEventListener(scrollerRef.current, 'scroll', () => {
119
- setOverflow((scrollerRef.current?.scrollTop ?? 0) > 0);
89
+ }, []);
90
+
91
+ const controller = useMemo(
92
+ () => ({
93
+ viewport: scrollerRef.current,
94
+ scrollToTop: () => {
95
+ invariant(scrollerRef.current);
96
+ scrollerRef.current.scrollTo({ top: 0, behavior: 'smooth' });
97
+ setPinned(false);
98
+ },
99
+ scrollToBottom: () => {
100
+ scrollToBottom('smooth');
101
+ },
120
102
  }),
103
+ [scrollToBottom, scrollerRef.current],
121
104
  );
122
- }, []);
123
105
 
124
- return (
125
- <ScrollContainerProvider pinned={pinned} controller={controller} scrollToBottom={scrollToBottom}>
126
- <div className='relative grid flex-1 min-bs-0 overflow-hidden'>
127
- {fade && (
128
- <div
129
- role='none'
130
- data-visible={overflow}
131
- className={mx(
132
- // NOTE: Gradients may not be visible with dark reader extensions.
133
- 'z-10 absolute block-start-0 inset-inline-0 bs-24 is-full',
134
- 'opacity-0 duration-200 transition-opacity data-[visible="true"]:opacity-100',
135
- 'bg-gradient-to-b from-[--surface-bg] to-transparent pointer-events-none',
136
- )}
137
- />
138
- )}
139
- <div className={mx('flex flex-col min-bs-0 overflow-y-auto scrollbar-thin', classNames)} ref={scrollerRef}>
140
- {children}
106
+ // Scroll controller imperative ref.
107
+ useImperativeHandle(forwardedRef, () => controller, [controller]);
108
+
109
+ // Listen for scroll events.
110
+ useEffect(() => {
111
+ if (!scrollerRef.current) {
112
+ return;
113
+ }
114
+
115
+ return combine(
116
+ // Check if user scrolls.
117
+ addEventListener(scrollerRef.current, 'wheel', () => {
118
+ setPinned(isBottom(scrollerRef.current));
119
+ }),
120
+ // Check if scrolls.
121
+ addEventListener(scrollerRef.current, 'scroll', () => {
122
+ setOverflow((scrollerRef.current?.scrollTop ?? 0) > 0);
123
+ }),
124
+ );
125
+ }, []);
126
+
127
+ return (
128
+ <ScrollContainerProvider pinned={pinned} controller={controller} scrollToBottom={scrollToBottom}>
129
+ <div className='relative grid flex-1 min-bs-0 overflow-hidden'>
130
+ {fade && (
131
+ <div
132
+ role='none'
133
+ data-visible={overflow}
134
+ className={mx(
135
+ // NOTE: Gradients may not be visible with dark reader extensions.
136
+ 'z-10 absolute block-start-0 inset-inline-0 bs-24 is-full',
137
+ 'opacity-0 duration-200 transition-opacity data-[visible="true"]:opacity-100',
138
+ 'bg-gradient-to-b from-[--surface-bg] to-transparent pointer-events-none',
139
+ )}
140
+ />
141
+ )}
142
+ <ScrollArea.Root classNames={mx('min-bs-0', classNames)} ref={scrollerRef} thin>
143
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
144
+ </ScrollArea.Root>
141
145
  </div>
142
- </div>
143
- </ScrollContainerProvider>
144
- );
145
- });
146
+ </ScrollContainerProvider>
147
+ );
148
+ },
149
+ );
146
150
 
147
151
  Root.displayName = 'ScrollContainer.Root';
148
152
 
@@ -150,25 +154,26 @@ Root.displayName = 'ScrollContainer.Root';
150
154
  // Viewport
151
155
  //
152
156
 
157
+ const VIEWPORT_NAME = 'ScrollContainer.Viewport';
158
+
153
159
  type ViewportProps = ThemedClassName<PropsWithChildren<Omit<HTMLAttributes<HTMLDivElement>, 'className'>>>;
154
160
 
155
161
  const Viewport = forwardRef<HTMLDivElement, ViewportProps>(({ classNames, children, ...props }, forwardedRef) => {
156
162
  const contentRef = useForwardedRef(forwardedRef);
157
- const { pinned, scrollToBottom } = useScrollContainerContext(Viewport.displayName!);
163
+ const { pinned, scrollToBottom } = useScrollContainerContext(VIEWPORT_NAME);
158
164
 
159
165
  useEffect(() => {
160
166
  if (!pinned || !contentRef.current) {
161
167
  return;
162
168
  }
163
169
 
170
+ // Scroll instantly otherwise it might move while we're scrolling.
171
+ scrollToBottom();
172
+
164
173
  // Setup resize observer to detect content changes.
165
174
  const resizeObserver = new ResizeObserver(() => scrollToBottom());
166
- scrollToBottom('instant');
167
-
168
175
  resizeObserver.observe(contentRef.current);
169
- return () => {
170
- resizeObserver.disconnect();
171
- };
176
+ return () => resizeObserver.disconnect();
172
177
  }, [pinned, scrollToBottom]);
173
178
 
174
179
  return (
@@ -178,16 +183,18 @@ const Viewport = forwardRef<HTMLDivElement, ViewportProps>(({ classNames, childr
178
183
  );
179
184
  });
180
185
 
181
- Viewport.displayName = 'ScrollContainer.Viewport';
186
+ Viewport.displayName = VIEWPORT_NAME;
182
187
 
183
188
  //
184
189
  // ScrollDownButton
185
190
  //
186
191
 
192
+ const SCROLL_DOWN_BUTTON_NAME = 'ScrollContainer.ScrollDownButton';
193
+
187
194
  type ScrollDownButtonProps = ThemedClassName;
188
195
 
189
196
  const ScrollDownButton = ({ classNames }: ScrollDownButtonProps) => {
190
- const { pinned, scrollToBottom } = useScrollContainerContext(ScrollDownButton.displayName!);
197
+ const { pinned, scrollToBottom } = useScrollContainerContext(SCROLL_DOWN_BUTTON_NAME);
191
198
 
192
199
  return (
193
200
  <div
@@ -210,7 +217,7 @@ const ScrollDownButton = ({ classNames }: ScrollDownButtonProps) => {
210
217
  );
211
218
  };
212
219
 
213
- ScrollDownButton.displayName = 'ScrollContainer.ScrollDownButton';
220
+ ScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME;
214
221
 
215
222
  //
216
223
  // ScrollContainer
@@ -42,9 +42,9 @@ const DefaultStory = ({ items = [] }: StoryProps) => {
42
42
  };
43
43
 
44
44
  const meta = {
45
- title: 'ui/react-ui-core/Select',
45
+ title: 'ui/react-ui-core/components/Select',
46
46
  render: DefaultStory,
47
- decorators: [withTheme, withLayoutVariants()],
47
+ decorators: [withTheme(), withLayoutVariants()],
48
48
  } satisfies Meta<typeof DefaultStory>;
49
49
 
50
50
  export default meta;
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { withTheme } from '../../testing';
8
+
9
+ import { Skeleton } from './Skeleton';
10
+
11
+ export default {
12
+ title: 'ui/react-ui-core/components/Skeleton',
13
+ component: Skeleton,
14
+ decorators: [withTheme()],
15
+ parameters: {
16
+ layout: 'centered',
17
+ },
18
+ };
19
+
20
+ export const Default = {
21
+ render: () => (
22
+ <div className='flex flex-col gap-4 p-4 border border-separator rounded-sm'>
23
+ <div className='flex is-fit items-center gap-4'>
24
+ <Skeleton classNames='size-10 shrink-0 rounded-full' />
25
+ <div className='grid gap-2'>
26
+ <Skeleton classNames='bs-4 is-[150px]' />
27
+ <Skeleton classNames='bs-4 is-[100px]' />
28
+ </div>
29
+ </div>
30
+ </div>
31
+ ),
32
+ };
33
+
34
+ export const Card = {
35
+ render: () => (
36
+ <div className='flex flex-col gap-3 is-96 p-4 border border-separator rounded-sm'>
37
+ <div className='flex items-center gap-3'>
38
+ <Skeleton variant='circle' classNames='bs-12 is-12 rounded-full' />
39
+ <div className='flex flex-col gap-2 flex-1'>
40
+ <Skeleton classNames='bs-4 is-24' />
41
+ <Skeleton classNames='bs-3 is-32' />
42
+ </div>
43
+ </div>
44
+ <Skeleton classNames='bs-32 is-full rounded' />
45
+ <div className='flex flex-col gap-2'>
46
+ <Skeleton classNames='bs-3 is-full' />
47
+ <Skeleton classNames='bs-3 is-5/6' />
48
+ <Skeleton classNames='bs-3 is-4/6' />
49
+ </div>
50
+ </div>
51
+ ),
52
+ };
@@ -0,0 +1,26 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type ComponentPropsWithRef, forwardRef } from 'react';
6
+
7
+ import { useThemeContext } from '../../hooks';
8
+ import { type ThemedClassName } from '../../util';
9
+
10
+ type SkeletonProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
11
+ variant?: 'default' | 'circle' | 'text';
12
+ };
13
+
14
+ /**
15
+ * A skeleton loading component that displays a placeholder while content is loading.
16
+ */
17
+ const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
18
+ ({ classNames, variant = 'default', ...props }, forwardedRef) => {
19
+ const { tx } = useThemeContext();
20
+ return <div {...props} className={tx('skeleton.root', 'skeleton', { variant }, classNames)} ref={forwardedRef} />;
21
+ },
22
+ );
23
+
24
+ export { Skeleton };
25
+
26
+ export type { SkeletonProps };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Skeleton';
@@ -0,0 +1,73 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useState } from 'react';
7
+
8
+ import { Layout } from '../../primitives';
9
+ import { withLayout, withTheme } from '../../testing';
10
+ import { ScrollArea } from '../ScrollArea';
11
+ import { Toolbar } from '../Toolbar';
12
+
13
+ import { Splitter, type SplitterRootProps } from './Splitter';
14
+
15
+ const Panel = ({ label }: { label: string }) => {
16
+ return (
17
+ <Layout.Main toolbar>
18
+ <Toolbar.Root>{label}</Toolbar.Root>
19
+ <ScrollArea.Root orientation='vertical'>
20
+ <ScrollArea.Viewport>
21
+ {Array.from({ length: 100 }).map((_, i) => (
22
+ <div key={i} className='p-1'>
23
+ {label}-{i}
24
+ </div>
25
+ ))}
26
+ </ScrollArea.Viewport>
27
+ </ScrollArea.Root>
28
+ </Layout.Main>
29
+ );
30
+ };
31
+
32
+ const DefaultStory = (props: SplitterRootProps) => {
33
+ const [mode, setMode] = useState(props.mode ?? 'both');
34
+
35
+ return (
36
+ <div className='grid grid-rows-[min-content_1fr] bs-full overflow-hidden'>
37
+ <Toolbar.Root>
38
+ <Toolbar.Button onClick={() => setMode('upper')}>A</Toolbar.Button>
39
+ <Toolbar.Button onClick={() => setMode('both')}>A + B</Toolbar.Button>
40
+ <Toolbar.Button onClick={() => setMode('lower')}>B</Toolbar.Button>
41
+ </Toolbar.Root>
42
+ <Splitter.Root mode={mode} ratio={props.ratio} classNames='divide-y divide-subduedSeparator'>
43
+ <Splitter.Panel position='upper'>
44
+ <Panel label='A' />
45
+ </Splitter.Panel>
46
+ <Splitter.Panel position='lower'>
47
+ <Panel label='B' />
48
+ </Splitter.Panel>
49
+ </Splitter.Root>
50
+ </div>
51
+ );
52
+ };
53
+
54
+ const meta: Meta<SplitterRootProps> = {
55
+ title: 'ui/react-ui-core/components/Splitter',
56
+ component: Splitter.Root,
57
+ render: DefaultStory,
58
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
59
+ parameters: {
60
+ layout: 'fullscreen',
61
+ },
62
+ };
63
+
64
+ export default meta;
65
+
66
+ type Story = StoryObj<SplitterRootProps>;
67
+
68
+ export const Default: Story = {
69
+ args: {
70
+ mode: 'both',
71
+ ratio: 0.5,
72
+ },
73
+ };
@@ -0,0 +1,123 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createContextScope } from '@radix-ui/react-context';
6
+ import React, { type ComponentPropsWithoutRef, forwardRef } from 'react';
7
+
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ import { type ThemedClassName } from '../../util';
11
+
12
+ type ScopedProps<P> = P & { __scopeSplitter?: any };
13
+
14
+ // TODO(burdon): Generalize styles.
15
+ // TODO(burdon): Enalbe resize.
16
+ // TODO(burdon): Generalize horizontal/vertical and change to start/end.
17
+ type Mode = 'upper' | 'lower' | 'both';
18
+
19
+ type SplitterContextValue = {
20
+ mode: Mode;
21
+ ratio: number;
22
+ transition: number;
23
+ };
24
+
25
+ const SPLITTER_NAME = 'Splitter';
26
+
27
+ const [createSplitterContext, createSplitterScope] = createContextScope(SPLITTER_NAME);
28
+
29
+ const [SplitterProvider, useSplitterContext] = createSplitterContext<SplitterContextValue>(SPLITTER_NAME);
30
+
31
+ //
32
+ // Root
33
+ //
34
+
35
+ const ROOT_NAME = 'Splitter.Root';
36
+
37
+ type RootProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & Partial<SplitterContextValue>;
38
+
39
+ const Root = forwardRef<HTMLDivElement, ScopedProps<RootProps>>(
40
+ (
41
+ { __scopeSplitter, classNames, mode = 'upper', ratio = 0.5, transition = 250, children, ...rootProps },
42
+ forwardedRef,
43
+ ) => {
44
+ return (
45
+ <SplitterProvider scope={__scopeSplitter} mode={mode} ratio={ratio} transition={transition}>
46
+ <div
47
+ role='none'
48
+ {...rootProps}
49
+ ref={forwardedRef}
50
+ className={mx('relative bs-full overflow-hidden', classNames)}
51
+ >
52
+ {children}
53
+ </div>
54
+ </SplitterProvider>
55
+ );
56
+ },
57
+ );
58
+
59
+ Root.displayName = ROOT_NAME;
60
+
61
+ //
62
+ // Panel
63
+ //
64
+
65
+ const PANEL_NAME = 'Splitter.Panel';
66
+
67
+ interface PanelProps extends ThemedClassName<ComponentPropsWithoutRef<'div'>> {
68
+ position: 'upper' | 'lower';
69
+ }
70
+
71
+ const Panel = forwardRef<HTMLDivElement, ScopedProps<PanelProps>>(
72
+ ({ __scopeSplitter, classNames, children, position, style, ...panelProps }, forwardedRef) => {
73
+ const context = useSplitterContext(PANEL_NAME, __scopeSplitter);
74
+ const { mode, ratio, transition } = context;
75
+
76
+ // Calculate position and height based on mode and ratio.
77
+ const isUpper = position === 'upper';
78
+ const top = isUpper ? '0%' : mode === 'upper' ? '100%' : mode === 'lower' ? '0%' : `${ratio * 100}%`;
79
+
80
+ const height = isUpper
81
+ ? mode === 'upper'
82
+ ? '100%'
83
+ : mode === 'lower'
84
+ ? '0%'
85
+ : `${ratio * 100}%`
86
+ : mode === 'lower'
87
+ ? '100%'
88
+ : mode === 'upper'
89
+ ? '0%'
90
+ : `${(1 - ratio) * 100}%`;
91
+
92
+ return (
93
+ <div
94
+ {...panelProps}
95
+ ref={forwardedRef}
96
+ className={mx('absolute inset-inline-0 flex flex-col overflow-hidden', classNames)}
97
+ style={{
98
+ top,
99
+ height,
100
+ transition: `top ${transition}ms, height ${transition}ms ease-out`,
101
+ ...style,
102
+ }}
103
+ >
104
+ {children}
105
+ </div>
106
+ );
107
+ },
108
+ );
109
+
110
+ Panel.displayName = PANEL_NAME;
111
+
112
+ //
113
+ // Splitter
114
+ //
115
+
116
+ const Splitter = {
117
+ Root,
118
+ Panel,
119
+ };
120
+
121
+ export { Splitter, createSplitterScope };
122
+
123
+ export type { Mode as SplitterMode, RootProps as SplitterRootProps, PanelProps as SplitterPanelProps };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './Splitter';
@@ -10,9 +10,9 @@ import { withTheme } from '../../testing';
10
10
  import { Status } from './Status';
11
11
 
12
12
  const meta = {
13
- title: 'ui/react-ui-core/Status',
13
+ title: 'ui/react-ui-core/components/Status',
14
14
  component: Status,
15
- decorators: [withTheme],
15
+ decorators: [withTheme()],
16
16
  } satisfies Meta<typeof Status>;
17
17
 
18
18
  export default meta;
@@ -5,8 +5,8 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { hues } from '@dxos/react-ui-theme';
9
- import { type ChromaticPalette, type MessageValence } from '@dxos/react-ui-types';
8
+ import { hues } from '@dxos/ui-theme';
9
+ import { type ChromaticPalette, type MessageValence } from '@dxos/ui-types';
10
10
 
11
11
  import { withTheme } from '../../testing';
12
12
 
@@ -15,7 +15,7 @@ import { Tag } from './Tag';
15
15
  const palettes = ['neutral', 'success', 'info', 'warning', 'error', ...hues] as (ChromaticPalette | MessageValence)[];
16
16
 
17
17
  const meta = {
18
- title: 'ui/react-ui-core/Tag',
18
+ title: 'ui/react-ui-core/components/Tag',
19
19
  component: Tag,
20
20
  render: () => (
21
21
  <div role='grid' className='grid grid-cols-5 gap-2 max-is-screen-md'>
@@ -26,7 +26,7 @@ const meta = {
26
26
  ))}
27
27
  </div>
28
28
  ),
29
- decorators: [withTheme],
29
+ decorators: [withTheme()],
30
30
  parameters: {
31
31
  chromatic: {
32
32
  disableSnapshot: false,
@@ -6,7 +6,7 @@ import { Primitive } from '@radix-ui/react-primitive';
6
6
  import { Slot } from '@radix-ui/react-slot';
7
7
  import React, { type ComponentPropsWithRef, forwardRef } from 'react';
8
8
 
9
- import { type ChromaticPalette, type MessageValence, type NeutralPalette } from '@dxos/react-ui-types';
9
+ import { type ChromaticPalette, type MessageValence, type NeutralPalette } from '@dxos/ui-types';
10
10
 
11
11
  import { useThemeContext } from '../../hooks';
12
12
  import { type ThemedClassName } from '../../util';
@@ -5,7 +5,7 @@
5
5
  import { createKeyborg } from 'keyborg';
6
6
  import React, { type PropsWithChildren, createContext, useEffect, useMemo } from 'react';
7
7
 
8
- import { type Density, type Elevation, type ThemeFunction } from '@dxos/react-ui-types';
8
+ import { type Density, type Elevation, type ThemeFunction, type ThemeMode } from '@dxos/ui-types';
9
9
 
10
10
  import { type SafeAreaPadding, useSafeArea } from '../../hooks';
11
11
  import { hasIosKeyboard } from '../../util';
@@ -14,14 +14,13 @@ import { ElevationProvider } from '../ElevationProvider';
14
14
 
15
15
  import { TranslationsProvider, type TranslationsProviderProps } from './TranslationsProvider';
16
16
 
17
- export type ThemeMode = 'dark' | 'light';
18
-
19
17
  export type ThemeContextValue = {
20
18
  tx: ThemeFunction<any>;
21
19
  themeMode: ThemeMode;
22
20
  hasIosKeyboard: boolean;
23
21
  safeAreaPadding?: SafeAreaPadding;
24
22
  noCache?: boolean;
23
+ platform?: 'mobile' | 'desktop';
25
24
  };
26
25
 
27
26
  /**