@dxos/react-ui 0.8.4-main.21d9917 → 0.8.4-main.2244d791bb

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 (183) hide show
  1. package/dist/lib/browser/{chunk-CEKVHJ27.mjs → chunk-6DTBPJE4.mjs} +4 -4
  2. package/dist/lib/browser/chunk-6DTBPJE4.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +456 -309
  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 +5 -4
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node-esm/{chunk-2NHEX4AD.mjs → chunk-JKHQSGNU.mjs} +4 -4
  9. package/dist/lib/node-esm/chunk-JKHQSGNU.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +456 -309
  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 +5 -4
  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/Avatars/Avatar.d.ts.map +1 -1
  18. package/dist/types/src/components/Breadcrumb/Breadcrumb.d.ts.map +1 -1
  19. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  20. package/dist/types/src/components/Dialog/AlertDialog.d.ts +4 -2
  21. package/dist/types/src/components/Dialog/AlertDialog.d.ts.map +1 -1
  22. package/dist/types/src/components/Dialog/AlertDialog.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Dialog/Dialog.d.ts +7 -1
  24. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  25. package/dist/types/src/components/Dialog/Dialog.stories.d.ts +1 -5
  26. package/dist/types/src/components/Dialog/Dialog.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Input/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Input/Input.stories.d.ts +1 -1
  29. package/dist/types/src/components/Input/Input.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/List/List.d.ts.map +1 -1
  31. package/dist/types/src/components/List/Treegrid.d.ts.map +1 -1
  32. package/dist/types/src/components/Main/Main.d.ts +8 -9
  33. package/dist/types/src/components/Main/Main.d.ts.map +1 -1
  34. package/dist/types/src/components/Main/Main.stories.d.ts +0 -3
  35. package/dist/types/src/components/Main/Main.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Menu/ContextMenu.d.ts.map +1 -1
  37. package/dist/types/src/components/Message/Message.d.ts.map +1 -1
  38. package/dist/types/src/components/Message/Message.stories.d.ts +2 -3
  39. package/dist/types/src/components/Message/Message.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts +23 -26
  41. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  42. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts +43 -9
  43. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
  45. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts +6 -1
  46. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/Select/Select.d.ts.map +1 -1
  48. package/dist/types/src/components/Skeleton/Skeleton.d.ts +12 -0
  49. package/dist/types/src/components/Skeleton/Skeleton.d.ts.map +1 -0
  50. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts +17 -0
  51. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts.map +1 -0
  52. package/dist/types/src/components/Skeleton/index.d.ts +2 -0
  53. package/dist/types/src/components/Skeleton/index.d.ts.map +1 -0
  54. package/dist/types/src/components/Splitter/Splitter.d.ts +26 -0
  55. package/dist/types/src/components/Splitter/Splitter.d.ts.map +1 -0
  56. package/dist/types/src/components/Splitter/Splitter.stories.d.ts +7 -0
  57. package/dist/types/src/components/Splitter/Splitter.stories.d.ts.map +1 -0
  58. package/dist/types/src/components/Splitter/index.d.ts +2 -0
  59. package/dist/types/src/components/Splitter/index.d.ts.map +1 -0
  60. package/dist/types/src/components/Tag/Tag.d.ts.map +1 -1
  61. package/dist/types/src/components/Tag/Tag.stories.d.ts +1 -5
  62. package/dist/types/src/components/Tag/Tag.stories.d.ts.map +1 -1
  63. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts +1 -0
  64. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  65. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  66. package/dist/types/src/components/Toolbar/Toolbar.d.ts +7 -6
  67. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  68. package/dist/types/src/components/index.d.ts +2 -0
  69. package/dist/types/src/components/index.d.ts.map +1 -1
  70. package/dist/types/src/exemplars/generics.stories.d.ts +17 -0
  71. package/dist/types/src/exemplars/generics.stories.d.ts.map +1 -0
  72. package/dist/types/src/exemplars/slot.stories.d.ts +14 -0
  73. package/dist/types/src/exemplars/slot.stories.d.ts.map +1 -0
  74. package/dist/types/src/exemplars/tabster.stories.d.ts +8 -0
  75. package/dist/types/src/exemplars/tabster.stories.d.ts.map +1 -0
  76. package/dist/types/src/exemplars/virtualizer.stories.d.ts +11 -0
  77. package/dist/types/src/exemplars/virtualizer.stories.d.ts.map +1 -0
  78. package/dist/types/src/index.d.ts +1 -0
  79. package/dist/types/src/index.d.ts.map +1 -1
  80. package/dist/types/src/playground/Controls.stories.d.ts.map +1 -1
  81. package/dist/types/src/primitives/Container/Container.d.ts +23 -0
  82. package/dist/types/src/primitives/Container/Container.d.ts.map +1 -0
  83. package/dist/types/src/primitives/Container/Container.stories.d.ts +11 -0
  84. package/dist/types/src/primitives/Container/Container.stories.d.ts.map +1 -0
  85. package/dist/types/src/primitives/Container/Layout.d.ts +18 -0
  86. package/dist/types/src/primitives/Container/Layout.d.ts.map +1 -0
  87. package/dist/types/src/primitives/Container/Layout.stories.d.ts +10 -0
  88. package/dist/types/src/primitives/Container/Layout.stories.d.ts.map +1 -0
  89. package/dist/types/src/primitives/Container/index.d.ts +3 -0
  90. package/dist/types/src/primitives/Container/index.d.ts.map +1 -0
  91. package/dist/types/src/primitives/Flex/Flex.d.ts +8 -0
  92. package/dist/types/src/primitives/Flex/Flex.d.ts.map +1 -0
  93. package/dist/types/src/primitives/Flex/index.d.ts +2 -0
  94. package/dist/types/src/primitives/Flex/index.d.ts.map +1 -0
  95. package/dist/types/src/primitives/index.d.ts +3 -0
  96. package/dist/types/src/primitives/index.d.ts.map +1 -0
  97. package/dist/types/src/testing/decorators/withTheme.d.ts +3 -2
  98. package/dist/types/src/testing/decorators/withTheme.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +23 -22
  101. package/src/components/AnchoredOverflow/AnchoredOverflow.tsx +10 -12
  102. package/src/components/Avatars/Avatar.stories.tsx +2 -2
  103. package/src/components/Avatars/Avatar.tsx +2 -9
  104. package/src/components/Avatars/AvatarGroup.stories.tsx +2 -2
  105. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +2 -2
  106. package/src/components/Breadcrumb/Breadcrumb.tsx +5 -31
  107. package/src/components/Button/Button.stories.tsx +3 -3
  108. package/src/components/Button/Button.tsx +1 -7
  109. package/src/components/Button/IconButton.stories.tsx +2 -2
  110. package/src/components/Button/IconButton.tsx +1 -1
  111. package/src/components/Button/Toggle.stories.tsx +2 -2
  112. package/src/components/Button/ToggleGroup.stories.tsx +2 -2
  113. package/src/components/Dialog/AlertDialog.stories.tsx +6 -7
  114. package/src/components/Dialog/AlertDialog.tsx +78 -9
  115. package/src/components/Dialog/Dialog.stories.tsx +11 -11
  116. package/src/components/Dialog/Dialog.tsx +44 -19
  117. package/src/components/Icon/Icon.stories.tsx +2 -2
  118. package/src/components/Icon/Icon.tsx +1 -1
  119. package/src/components/Input/Input.stories.tsx +11 -10
  120. package/src/components/Input/Input.tsx +10 -25
  121. package/src/components/Link/Link.stories.tsx +2 -2
  122. package/src/components/Link/Link.tsx +1 -1
  123. package/src/components/List/List.stories.tsx +2 -2
  124. package/src/components/List/List.tsx +7 -13
  125. package/src/components/List/Tree.stories.tsx +2 -2
  126. package/src/components/List/Treegrid.stories.tsx +2 -2
  127. package/src/components/List/Treegrid.tsx +4 -9
  128. package/src/components/Main/Main.stories.tsx +41 -23
  129. package/src/components/Main/Main.tsx +128 -71
  130. package/src/components/Menu/ContextMenu.stories.tsx +2 -2
  131. package/src/components/Menu/ContextMenu.tsx +7 -31
  132. package/src/components/Menu/DropdownMenu.stories.tsx +2 -2
  133. package/src/components/Menu/DropdownMenu.tsx +8 -8
  134. package/src/components/Message/Message.stories.tsx +23 -8
  135. package/src/components/Message/Message.tsx +8 -21
  136. package/src/components/Popover/Popover.stories.tsx +2 -2
  137. package/src/components/Popover/Popover.tsx +3 -3
  138. package/src/components/ScrollArea/ScrollArea.stories.tsx +152 -76
  139. package/src/components/ScrollArea/ScrollArea.tsx +68 -116
  140. package/src/components/ScrollArea/index.ts +1 -1
  141. package/src/components/ScrollContainer/ScrollContainer.stories.tsx +24 -9
  142. package/src/components/ScrollContainer/ScrollContainer.tsx +14 -9
  143. package/src/components/Select/Select.stories.tsx +2 -2
  144. package/src/components/Select/Select.tsx +9 -25
  145. package/src/components/Separator/Separator.tsx +1 -1
  146. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  147. package/src/components/Skeleton/Skeleton.tsx +26 -0
  148. package/src/components/Skeleton/index.ts +5 -0
  149. package/src/components/Splitter/Splitter.stories.tsx +73 -0
  150. package/src/components/Splitter/Splitter.tsx +123 -0
  151. package/src/components/Splitter/index.ts +5 -0
  152. package/src/components/Status/Status.stories.tsx +2 -2
  153. package/src/components/Status/Status.tsx +2 -2
  154. package/src/components/Tag/Tag.stories.tsx +3 -7
  155. package/src/components/Tag/Tag.tsx +1 -6
  156. package/src/components/ThemeProvider/ThemeProvider.tsx +2 -1
  157. package/src/components/Toast/Toast.stories.tsx +2 -2
  158. package/src/components/Toast/Toast.tsx +6 -10
  159. package/src/components/Toolbar/Toolbar.stories.tsx +2 -2
  160. package/src/components/Toolbar/Toolbar.tsx +13 -9
  161. package/src/components/Tooltip/Tooltip.stories.tsx +2 -2
  162. package/src/components/Tooltip/Tooltip.tsx +2 -2
  163. package/src/components/index.ts +2 -0
  164. package/src/exemplars/generics.stories.tsx +44 -0
  165. package/src/exemplars/slot.stories.tsx +108 -0
  166. package/src/exemplars/tabster.stories.tsx +127 -0
  167. package/src/exemplars/virtualizer.stories.tsx +133 -0
  168. package/src/index.ts +1 -0
  169. package/src/playground/Controls.stories.tsx +3 -4
  170. package/src/playground/Custom.stories.tsx +2 -2
  171. package/src/playground/Typography.stories.tsx +2 -2
  172. package/src/primitives/Container/Container.stories.tsx +67 -0
  173. package/src/primitives/Container/Container.tsx +79 -0
  174. package/src/primitives/Container/Layout.stories.tsx +57 -0
  175. package/src/primitives/Container/Layout.tsx +61 -0
  176. package/src/primitives/Container/index.ts +6 -0
  177. package/src/primitives/Flex/Flex.tsx +26 -0
  178. package/src/primitives/Flex/index.ts +5 -0
  179. package/src/primitives/index.ts +6 -0
  180. package/src/testing/decorators/withLayoutVariants.tsx +1 -1
  181. package/src/testing/decorators/withTheme.tsx +19 -17
  182. package/dist/lib/browser/chunk-CEKVHJ27.mjs.map +0 -7
  183. package/dist/lib/node-esm/chunk-2NHEX4AD.mjs.map +0 -7
@@ -485,7 +485,7 @@ const PopoverContentImpl = forwardRef<PopoverContentImplElement, PopoverContentI
485
485
  {...contentProps}
486
486
  collisionPadding={safeCollisionPadding}
487
487
  collisionBoundary={computedCollisionBoundary}
488
- className={tx('popover.content', 'popover', { elevation }, classNames)}
488
+ className={tx('popover.content', { elevation }, classNames)}
489
489
  ref={forwardedRef}
490
490
  style={{
491
491
  ...contentProps.style,
@@ -550,7 +550,7 @@ const PopoverArrow = forwardRef<PopoverArrowElement, PopoverArrowProps>(
550
550
  <PopperPrimitive.Arrow
551
551
  {...popperScope}
552
552
  {...arrowProps}
553
- className={tx('popover.arrow', 'popover__arrow', {}, classNames)}
553
+ className={tx('popover.arrow', {}, classNames)}
554
554
  ref={forwardedRef}
555
555
  />
556
556
  );
@@ -576,7 +576,7 @@ const PopoverViewport = forwardRef<HTMLDivElement, PopoverViewportProps>(
576
576
  return (
577
577
  <Root
578
578
  {...props}
579
- className={tx('popover.viewport', 'popover__viewport', { constrainInline, constrainBlock }, classNames)}
579
+ className={tx('popover.viewport', { constrainInline, constrainBlock }, classNames)}
580
580
  ref={forwardedRef}
581
581
  >
582
582
  {children}
@@ -1,101 +1,177 @@
1
1
  //
2
- // Copyright 2023 DXOS.org
2
+ // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { type PropsWithChildren } from 'react';
5
+ import React, { useMemo } from 'react';
7
6
 
8
7
  import { faker } from '@dxos/random';
9
- import { activeSurface, surfaceShadow } from '@dxos/ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
10
9
 
11
10
  import { withLayout, withTheme } from '../../testing';
12
11
 
13
12
  import { ScrollArea } from './ScrollArea';
14
13
 
15
- faker.seed(1234);
16
-
17
- const DefaultStory = ({ children }: PropsWithChildren<{}>) => {
18
- return (
19
- <ScrollArea.Root
20
- classNames={['is-[300px] bs-[400px] rounded', activeSurface, surfaceShadow({ elevation: 'positioned' })]}
21
- >
22
- <ScrollArea.Viewport classNames='rounded p-4'>
23
- <p>{children}</p>
24
- </ScrollArea.Viewport>
25
- <ScrollArea.Scrollbar orientation='horizontal'>
26
- <ScrollArea.Thumb />
27
- </ScrollArea.Scrollbar>
28
- <ScrollArea.Scrollbar orientation='vertical'>
29
- <ScrollArea.Thumb />
30
- </ScrollArea.Scrollbar>
31
- <ScrollArea.Corner />
32
- </ScrollArea.Root>
33
- );
34
- };
14
+ faker.seed(123);
35
15
 
36
- const meta = {
37
- title: 'ui/react-ui-core/ScrollArea',
38
- component: ScrollArea as any,
39
- render: DefaultStory,
40
- decorators: [withTheme, withLayout({ layout: 'fullscreen' })],
16
+ export default {
17
+ title: 'ui/react-ui-core/components/ScrollArea',
18
+ component: ScrollArea,
19
+ decorators: [withTheme()],
41
20
  parameters: {
42
- layout: 'fullscreen',
21
+ layout: 'centered',
43
22
  },
44
- } satisfies Meta<typeof DefaultStory>;
23
+ };
45
24
 
46
- export default meta;
25
+ const Column = () => (
26
+ <div>
27
+ {Array.from({ length: 50 }).map((_, index) => (
28
+ <div key={index} className='pli-1 text-sm cursor-pointer hover:bg-hoverSurface'>
29
+ Item {index + 1}
30
+ </div>
31
+ ))}
32
+ </div>
33
+ );
47
34
 
48
- type Story = StoryObj<typeof meta>;
35
+ const Row = () => (
36
+ <div className='flex gap-2 is-max'>
37
+ {Array.from({ length: 50 }).map((_, index) => (
38
+ <div
39
+ key={index}
40
+ className='shrink-0 bs-20 is-20 cursor-pointer border border-separator rounded-md flex items-center justify-center text-sm hover:bg-hoverSurface'
41
+ >
42
+ {index + 1}
43
+ </div>
44
+ ))}
45
+ </div>
46
+ );
49
47
 
50
- export const Default: Story = {
51
- args: {
52
- children: faker.lorem.paragraphs(5),
53
- },
48
+ export const Vertical = {
49
+ render: () => (
50
+ <div className='bs-72 is-48 p-2 border border-separator rounded-md'>
51
+ <ScrollArea.Root orientation='vertical' padding>
52
+ <ScrollArea.Viewport>
53
+ <Column />
54
+ </ScrollArea.Viewport>
55
+ </ScrollArea.Root>
56
+ </div>
57
+ ),
54
58
  };
55
59
 
56
- export const NestedScrollAreas: Story = {
57
- render: () => {
58
- const columns = Array.from({ length: 3 }).map((_, index) => ({
59
- id: String(index),
60
- itemCount: 20,
61
- }));
60
+ export const VerticalThin = {
61
+ render: () => (
62
+ <div className='bs-72 is-48 p-2 border border-separator rounded-md'>
63
+ <ScrollArea.Root orientation='vertical' padding thin>
64
+ <ScrollArea.Viewport>
65
+ <Column />
66
+ </ScrollArea.Viewport>
67
+ </ScrollArea.Root>
68
+ </div>
69
+ ),
70
+ };
62
71
 
63
- return (
64
- <div className='p-4 bs-full is-full overflow-hidden'>
65
- <div className='flex bs-full is-full overflow-hidden border border-sky-500'>
66
- <ScrollArea.Root>
67
- <ScrollArea.Viewport>
68
- <div className='flex gap-4 p-3 bs-full overflow-hidden'>
69
- {columns.map((column) => (
70
- <div key={column.id} className='flex flex-col gap-1 bs-full overflow-hidden is-[300px]'>
71
- <div className='flex shrink-0 p-2 border border-separator'>Column {column.id}</div>
72
- <ScrollArea.Expander classNames='border border-rose-500'>
73
- <ScrollArea.Root>
74
- <ScrollArea.Viewport>
75
- <div className='flex flex-col p-3 space-y-2'>
76
- {Array.from({ length: column.itemCount }, (_, i) => (
77
- <div key={i} className={`p-3 border border-separator rounded-sm`}>
78
- Item {i + 1}
79
- </div>
80
- ))}
81
- </div>
82
- </ScrollArea.Viewport>
83
- <ScrollArea.Scrollbar orientation='vertical'>
84
- <ScrollArea.Thumb />
85
- </ScrollArea.Scrollbar>
86
- </ScrollArea.Root>
87
- </ScrollArea.Expander>
88
- <div className={`p-2 border border-separator`}>Footer</div>
72
+ export const Horizontal = {
73
+ render: () => (
74
+ <div className='is-96 p-2 border border-separator rounded-md'>
75
+ <ScrollArea.Root orientation='horizontal' padding>
76
+ <ScrollArea.Viewport>
77
+ <Row />
78
+ </ScrollArea.Viewport>
79
+ </ScrollArea.Root>
80
+ </div>
81
+ ),
82
+ };
83
+
84
+ export const HorizontalThin = {
85
+ render: () => (
86
+ <div className='is-96 p-2 border border-separator rounded-md'>
87
+ <ScrollArea.Root orientation='horizontal' padding thin>
88
+ <ScrollArea.Viewport>
89
+ <Row />
90
+ </ScrollArea.Viewport>
91
+ </ScrollArea.Root>
92
+ </div>
93
+ ),
94
+ };
95
+
96
+ export const Both = {
97
+ render: () => (
98
+ <div className='bs-96 is-96 p-2 border border-separator rounded-md'>
99
+ <ScrollArea.Root orientation='all' padding>
100
+ <ScrollArea.Viewport>
101
+ <div className='flex flex-col gap-2'>
102
+ {Array.from({ length: 50 }).map((_, rowIndex) => (
103
+ <div key={rowIndex} className='flex gap-2'>
104
+ {Array.from({ length: 50 }).map((_, colIndex) => (
105
+ <div
106
+ key={colIndex}
107
+ className='shrink-0 bs-20 is-20 flex items-center justify-center text-sm border border-separator font-mono'
108
+ >
109
+ [{colIndex}:{rowIndex}]
89
110
  </div>
90
111
  ))}
91
112
  </div>
92
- </ScrollArea.Viewport>
93
- <ScrollArea.Scrollbar orientation='horizontal'>
94
- <ScrollArea.Thumb />
95
- </ScrollArea.Scrollbar>
96
- </ScrollArea.Root>
97
- </div>
98
- </div>
113
+ ))}
114
+ </div>
115
+ </ScrollArea.Viewport>
116
+ </ScrollArea.Root>
117
+ </div>
118
+ ),
119
+ };
120
+
121
+ export const NestedScrollAreas = {
122
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
123
+ render: () => {
124
+ const columns = useMemo(
125
+ () =>
126
+ Array.from({ length: 8 }).map((_, index) => ({
127
+ id: String(index),
128
+ count: faker.number.int({ min: 5, max: 20 }),
129
+ })),
130
+ [],
131
+ );
132
+
133
+ return (
134
+ <ScrollArea.Root thin orientation='horizontal'>
135
+ <ScrollArea.Viewport classNames='gap-4'>
136
+ {columns.map((column) => (
137
+ <section
138
+ key={column.id}
139
+ className='shrink-0 bs-full is-[16rem] grid grid-rows-[min-content_1fr_min-content] border border-separator'
140
+ >
141
+ <header className='flex shrink-0 p-2 border-be border-separator'>Column {column.id}</header>
142
+ <ScrollArea.Root thin orientation='vertical'>
143
+ <ScrollArea.Viewport classNames='plb-2 pli-2 gap-2'>
144
+ {Array.from({ length: column.count }, (_, i) => (
145
+ <div key={i} role='listitem' className={`shrink-0 p-2 text-sm border border-separator rounded-sm`}>
146
+ Item {i + 1}
147
+ </div>
148
+ ))}
149
+ </ScrollArea.Viewport>
150
+ </ScrollArea.Root>
151
+ <footer className={`p-2 text-subdued border-bs border-separator`}>{column.count}</footer>
152
+ </section>
153
+ ))}
154
+ </ScrollArea.Viewport>
155
+ </ScrollArea.Root>
99
156
  );
100
157
  },
101
158
  };
159
+
160
+ export const NativeScroll = {
161
+ render: () => (
162
+ <div className='group bs-48 is-48 border border-separator'>
163
+ <div
164
+ className={mx(
165
+ 'group bs-full is-full overflow-y-scroll',
166
+ '[&::-webkit-scrollbar]:is-3',
167
+ '[&::-webkit-scrollbar-thumb]:rounded-none',
168
+ '[&::-webkit-scrollbar-track]:bg-scrollbarTrack',
169
+ '[&::-webkit-scrollbar-thumb]:bg-scrollbarThumbSubdued',
170
+ 'group-hover:[&::-webkit-scrollbar-thumb]:bg-scrollbarThumb',
171
+ )}
172
+ >
173
+ <Column />
174
+ </div>
175
+ </div>
176
+ ),
177
+ };
@@ -1,138 +1,101 @@
1
1
  //
2
- // Copyright 2023 DXOS.org
2
+ // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import {
6
- Corner as ScrollAreaPrimitiveCorner,
7
- type ScrollAreaCornerProps as ScrollAreaPrimitiveCornerProps,
8
- Root as ScrollAreaPrimitiveRoot,
9
- type ScrollAreaProps as ScrollAreaPrimitiveRootProps,
10
- Scrollbar as ScrollAreaPrimitiveScrollbar,
11
- type ScrollAreaScrollbarProps as ScrollAreaPrimitiveScrollbarProps,
12
- Thumb as ScrollAreaPrimitiveThumb,
13
- type ScrollAreaThumbProps as ScrollAreaPrimitiveThumbProps,
14
- Viewport as ScrollAreaPrimitiveViewport,
15
- type ScrollAreaViewportProps as ScrollAreaPrimitiveViewportProps,
16
- } from '@radix-ui/react-scroll-area';
17
- import React, { type PropsWithChildren, forwardRef } from 'react';
18
-
19
- import { mx } from '@dxos/ui-theme';
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type HTMLAttributes, forwardRef } from 'react';
20
7
 
21
- import { useThemeContext } from '../../hooks';
22
- import { type ThemedClassName } from '../../util';
8
+ import { type AllowedAxis, type SlottableProps, type ThemedClassName } from '@dxos/ui-types';
23
9
 
24
- type ScrollAreaVariant = 'coarse' | 'fine';
10
+ import { useThemeContext } from '../../hooks';
25
11
 
26
12
  //
27
- // Root
13
+ // Context
28
14
  //
29
15
 
30
- type ScrollAreaRootProps = ThemedClassName<ScrollAreaPrimitiveRootProps>;
31
-
32
- const ScrollAreaRoot = forwardRef<HTMLDivElement, ScrollAreaRootProps>(({ classNames, ...props }, forwardedRef) => {
33
- const { tx } = useThemeContext();
34
- return (
35
- <ScrollAreaPrimitiveRoot
36
- {...props}
37
- className={tx('scrollArea.root', 'scroll-area', {}, classNames)}
38
- ref={forwardedRef}
39
- />
40
- );
41
- });
42
-
43
- //
44
- // Viewport
45
- //
16
+ const SCROLLAREA_NAME = 'ScrollArea';
46
17
 
47
- type ScrollAreaViewportProps = ThemedClassName<ScrollAreaPrimitiveViewportProps>;
18
+ type ScrollAreaContextType = {
19
+ /** Orientation of scrollbars. */
20
+ orientation: AllowedAxis;
21
+ /** Hide scrollbars when not scrolling. */
22
+ autoHide: boolean;
23
+ /** Apply padding to opposite side of scrollbar. */
24
+ margin?: boolean;
25
+ /** Apply padding. */
26
+ padding: boolean;
27
+ /** Use thin scrollbars. */
28
+ thin: boolean;
29
+ /** Enable snap scrolling. */
30
+ snap: boolean;
31
+ };
48
32
 
49
- const ScrollAreaViewport = forwardRef<HTMLDivElement, ScrollAreaViewportProps>(
50
- ({ classNames, ...props }, forwardedRef) => {
51
- const { tx } = useThemeContext();
52
- return (
53
- <ScrollAreaPrimitiveViewport
54
- {...props}
55
- className={tx('scrollArea.viewport', 'scroll-area', {}, classNames)}
56
- ref={forwardedRef}
57
- />
58
- );
59
- },
60
- );
33
+ const [ScrollAreaProvider, useScrollAreaContext] = createContext<ScrollAreaContextType>(SCROLLAREA_NAME);
61
34
 
62
35
  //
63
- // Scrollbar
36
+ // Root
64
37
  //
65
38
 
66
- type ScrollAreaScrollbarProps = ThemedClassName<ScrollAreaPrimitiveScrollbarProps> & { variant?: ScrollAreaVariant };
39
+ const SCROLLAREA_ROOT_NAME = 'ScrollArea.Root';
40
+
41
+ type ScrollAreaRootProps = SlottableProps<HTMLDivElement> & Partial<ScrollAreaContextType>;
67
42
 
68
- const ScrollAreaScrollbar = forwardRef<HTMLDivElement, ScrollAreaScrollbarProps>(
69
- ({ classNames, variant = 'fine', ...props }, forwardedRef) => {
43
+ /**
44
+ * ScrollArea provides native scrollbars with custom styling.
45
+ */
46
+ const ScrollAreaRoot = forwardRef<HTMLDivElement, ScrollAreaRootProps>(
47
+ (
48
+ {
49
+ classNames,
50
+ className,
51
+ children,
52
+ orientation = 'vertical',
53
+ autoHide = true,
54
+ margin = true, // TODO(burdon): Is this the right default?
55
+ padding = false,
56
+ thin = false,
57
+ snap = false,
58
+ ...props
59
+ },
60
+ forwardedRef,
61
+ ) => {
70
62
  const { tx } = useThemeContext();
63
+ const options = { orientation, autoHide, margin, padding, thin, snap };
64
+
71
65
  return (
72
- <ScrollAreaPrimitiveScrollbar
73
- data-variant={variant}
74
- {...props}
75
- className={tx('scrollArea.scrollbar', 'scroll-area__scrollbar', {}, classNames)}
76
- ref={forwardedRef}
77
- />
66
+ <ScrollAreaProvider {...options}>
67
+ <div {...props} className={tx('scrollArea.root', options, [className, classNames])} ref={forwardedRef}>
68
+ {children}
69
+ </div>
70
+ </ScrollAreaProvider>
78
71
  );
79
72
  },
80
73
  );
81
74
 
82
- //
83
- // Thumb
84
- //
85
-
86
- type ScrollAreaThumbProps = ThemedClassName<ScrollAreaPrimitiveThumbProps>;
87
-
88
- const ScrollAreaThumb = forwardRef<HTMLDivElement, ScrollAreaThumbProps>(({ classNames, ...props }, forwardedRef) => {
89
- const { tx } = useThemeContext();
90
- return (
91
- <ScrollAreaPrimitiveThumb
92
- {...props}
93
- className={tx('scrollArea.thumb', 'scroll-area__thumb', {}, classNames)}
94
- ref={forwardedRef}
95
- />
96
- );
97
- });
75
+ ScrollAreaRoot.displayName = SCROLLAREA_ROOT_NAME;
98
76
 
99
77
  //
100
- // Corner
78
+ // Viewport
101
79
  //
102
80
 
103
- type ScrollAreaCornerProps = ThemedClassName<ScrollAreaPrimitiveCornerProps>;
81
+ const SCROLLAREA_VIEWPORT_NAME = 'ScrollArea.Viewport';
104
82
 
105
- const ScrollAreaCorner = forwardRef<HTMLDivElement, ScrollAreaCornerProps>(({ classNames, ...props }, forwardedRef) => {
106
- const { tx } = useThemeContext();
107
- return (
108
- <ScrollAreaPrimitiveCorner
109
- {...props}
110
- className={tx('scrollArea.corner', 'scroll-area__corner', {}, classNames)}
111
- ref={forwardedRef}
112
- />
113
- );
114
- });
83
+ type ScrollAreaViewportProps = ThemedClassName<HTMLAttributes<HTMLDivElement>>;
115
84
 
116
- //
117
- // Expander
118
- //
119
-
120
- type ScrollAreaExpanderProps = ThemedClassName<PropsWithChildren>;
85
+ const ScrollAreaViewport = forwardRef<HTMLDivElement, ScrollAreaViewportProps>(
86
+ ({ classNames, children, ...props }, forwardedRef) => {
87
+ const { tx } = useThemeContext();
88
+ const options = useScrollAreaContext(SCROLLAREA_VIEWPORT_NAME);
121
89
 
122
- /**
123
- * Size-locking wrapper required for inner ScrollArea to function correctly.
124
- * NOTE: Radix ScrollArea.Viewport applies `display: table` to its immediate child,
125
- * causing the content to participate in table layout and escape the intended fixed-size viewport.
126
- */
127
- const ScrollAreaExpander = ({ classNames, children }: ScrollAreaExpanderProps) => {
128
- return (
129
- <div role='none' className={mx('relative bs-full is-full overflow-hidden', classNames)}>
130
- <div role='none' className='absolute inset-0 overflow-hidden'>
90
+ return (
91
+ <div {...props} className={tx('scrollArea.viewport', options, classNames)} ref={forwardedRef}>
131
92
  {children}
132
93
  </div>
133
- </div>
134
- );
135
- };
94
+ );
95
+ },
96
+ );
97
+
98
+ ScrollAreaViewport.displayName = SCROLLAREA_VIEWPORT_NAME;
136
99
 
137
100
  //
138
101
  // ScrollArea
@@ -141,17 +104,6 @@ const ScrollAreaExpander = ({ classNames, children }: ScrollAreaExpanderProps) =
141
104
  export const ScrollArea = {
142
105
  Root: ScrollAreaRoot,
143
106
  Viewport: ScrollAreaViewport,
144
- Scrollbar: ScrollAreaScrollbar,
145
- Thumb: ScrollAreaThumb,
146
- Corner: ScrollAreaCorner,
147
- Expander: ScrollAreaExpander,
148
107
  };
149
108
 
150
- export type {
151
- ScrollAreaRootProps,
152
- ScrollAreaViewportProps,
153
- ScrollAreaScrollbarProps,
154
- ScrollAreaThumbProps,
155
- ScrollAreaCornerProps,
156
- ScrollAreaExpanderProps,
157
- };
109
+ export type { ScrollAreaRootProps, ScrollAreaViewportProps };
@@ -1,5 +1,5 @@
1
1
  //
2
- // Copyright 2023 DXOS.org
2
+ // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './ScrollArea';
@@ -7,16 +7,22 @@ import React, { useEffect, useRef, useState } from 'react';
7
7
 
8
8
  import { faker } from '@dxos/random';
9
9
 
10
+ import { Layout } from '../../primitives';
10
11
  import { withLayout, withTheme } from '../../testing';
11
12
  import { Button } from '../Button';
12
13
  import { Toolbar } from '../Toolbar';
13
14
 
14
15
  import { ScrollContainer, type ScrollContainerRootProps, type ScrollController } from './ScrollContainer';
15
16
 
16
- const DefaultStory = (props: ScrollContainerRootProps) => {
17
+ type StoryProps = ScrollContainerRootProps & { running?: boolean; initialLines?: number };
18
+
19
+ const DefaultStory = ({ initialLines = 0, running: runningProp, ...props }: StoryProps) => {
17
20
  const [lines, setLines] = useState<string[]>([]);
18
- const [running, setRunning] = useState(true);
21
+ const [running, setRunning] = useState(runningProp);
19
22
  const scroller = useRef<ScrollController>(null);
23
+ useEffect(() => {
24
+ setLines(Array.from({ length: initialLines }, () => faker.lorem.paragraph()));
25
+ }, [initialLines]);
20
26
  useEffect(() => {
21
27
  if (!running) {
22
28
  return;
@@ -30,32 +36,32 @@ const DefaultStory = (props: ScrollContainerRootProps) => {
30
36
  }, [running]);
31
37
 
32
38
  return (
33
- <div className='flex flex-col bs-full overflow-hidden'>
39
+ <Layout.Main toolbar>
34
40
  <Toolbar.Root>
35
41
  <Button onClick={() => setRunning((running) => !running)}>{running ? 'Stop' : 'Start'}</Button>
36
42
  <Button onClick={() => scroller.current?.scrollToBottom()}>Scroll to bottom</Button>
37
- <div className='flex-1' />
38
- <div>{lines.length}</div>
43
+ <Toolbar.Separator variant='gap' />
44
+ <div className='pli-1'>{lines.length}</div>
39
45
  </Toolbar.Root>
40
46
  <ScrollContainer.Root {...props} ref={scroller}>
41
47
  <ScrollContainer.Viewport>
42
48
  {lines.map((line, index) => (
43
- <div key={index} className='p-2'>
49
+ <div key={index} className='p-2 text-description'>
44
50
  {line}
45
51
  </div>
46
52
  ))}
47
53
  </ScrollContainer.Viewport>
48
54
  <ScrollContainer.ScrollDownButton />
49
55
  </ScrollContainer.Root>
50
- </div>
56
+ </Layout.Main>
51
57
  );
52
58
  };
53
59
 
54
60
  const meta = {
55
- title: 'ui/react-ui-core/ScrollContainer',
61
+ title: 'ui/react-ui-core/components/ScrollContainer',
56
62
  component: ScrollContainer.Root,
57
63
  render: DefaultStory,
58
- decorators: [withTheme, withLayout({ layout: 'column', classNames: 'is-[30rem]' })],
64
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'is-[30rem]' })],
59
65
  } satisfies Meta<typeof DefaultStory>;
60
66
 
61
67
  export default meta;
@@ -66,5 +72,14 @@ export const Default: Story = {
66
72
  args: {
67
73
  pin: true,
68
74
  fade: true,
75
+ running: true,
76
+ },
77
+ };
78
+
79
+ export const Large: Story = {
80
+ args: {
81
+ pin: true,
82
+ fade: true,
83
+ initialLines: 100,
69
84
  },
70
85
  };