@dxos/react-ui-mosaic 0.9.1-staging.ee54ba693a → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/lib/chunk-Mosaic.mjs +1024 -0
  2. package/dist/lib/chunk-Mosaic.mjs.map +1 -0
  3. package/dist/lib/index.mjs +653 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/playwright.mjs +200 -0
  6. package/dist/lib/playwright.mjs.map +1 -0
  7. package/dist/lib/testing.mjs +127 -0
  8. package/dist/lib/testing.mjs.map +1 -0
  9. package/dist/lib/translations.mjs +11 -0
  10. package/dist/lib/translations.mjs.map +1 -0
  11. package/dist/types/src/components/Board/Board.d.ts +7 -27
  12. package/dist/types/src/components/Board/Board.d.ts.map +1 -1
  13. package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Board/BoardColumnContext.d.ts +10 -0
  15. package/dist/types/src/components/Board/BoardColumnContext.d.ts.map +1 -0
  16. package/dist/types/src/components/Board/BoardContext.d.ts +26 -0
  17. package/dist/types/src/components/Board/BoardContext.d.ts.map +1 -0
  18. package/dist/types/src/components/Board/Column.d.ts +2 -7
  19. package/dist/types/src/components/Board/Column.d.ts.map +1 -1
  20. package/dist/types/src/components/Board/index.d.ts +2 -0
  21. package/dist/types/src/components/Board/index.d.ts.map +1 -1
  22. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
  23. package/dist/types/src/components/Mosaic/Container.d.ts +6 -34
  24. package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -1
  25. package/dist/types/src/components/Mosaic/Mosaic.d.ts +10 -15
  26. package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
  27. package/dist/types/src/components/Mosaic/MosaicContainerContext.d.ts +34 -0
  28. package/dist/types/src/components/Mosaic/MosaicContainerContext.d.ts.map +1 -0
  29. package/dist/types/src/components/Mosaic/MosaicTileContext.d.ts +31 -0
  30. package/dist/types/src/components/Mosaic/MosaicTileContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Mosaic/Placeholder.d.ts +3 -3
  32. package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -1
  33. package/dist/types/src/components/Mosaic/Stack.d.ts +8 -2
  34. package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -1
  35. package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Mosaic/Tile.d.ts +6 -32
  37. package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -1
  38. package/dist/types/src/components/Mosaic/VirtualStackPagination.stories.d.ts +13 -0
  39. package/dist/types/src/components/Mosaic/VirtualStackPagination.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/Mosaic/index.d.ts +0 -1
  41. package/dist/types/src/components/Mosaic/index.d.ts.map +1 -1
  42. package/dist/types/src/components/SearchStack/SearchStack.d.ts +1 -1
  43. package/dist/types/src/components/SearchStack/SearchStack.d.ts.map +1 -1
  44. package/dist/types/src/hooks/index.d.ts +1 -0
  45. package/dist/types/src/hooks/index.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts +2 -2
  47. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts +3 -3
  49. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useVirtualizerPagination.d.ts +60 -0
  51. package/dist/types/src/hooks/useVirtualizerPagination.d.ts.map +1 -0
  52. package/dist/types/src/hooks/useVirtualizerPagination.test.d.ts +2 -0
  53. package/dist/types/src/hooks/useVirtualizerPagination.test.d.ts.map +1 -0
  54. package/dist/types/src/hooks/useVisibleItems.d.ts +2 -2
  55. package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +35 -37
  58. package/src/components/Board/Board.stories.tsx +7 -6
  59. package/src/components/Board/Board.tsx +9 -54
  60. package/src/components/Board/BoardColumnContext.ts +25 -0
  61. package/src/components/Board/BoardContext.ts +43 -0
  62. package/src/components/Board/Column.tsx +13 -27
  63. package/src/components/Board/Item.tsx +2 -2
  64. package/src/components/Board/index.ts +3 -0
  65. package/src/components/Mosaic/Container.tsx +31 -53
  66. package/src/components/Mosaic/DragHandle.tsx +1 -1
  67. package/src/components/Mosaic/{Mosaic.tsx → Mosaic.ts} +5 -16
  68. package/src/components/Mosaic/MosaicContainerContext.ts +44 -0
  69. package/src/components/Mosaic/MosaicTileContext.ts +34 -0
  70. package/src/components/Mosaic/Placeholder.tsx +7 -8
  71. package/src/components/Mosaic/ResizeHandle.stories.tsx +4 -4
  72. package/src/components/Mosaic/ResizeHandle.tsx +2 -2
  73. package/src/components/Mosaic/Stack.stories.tsx +49 -13
  74. package/src/components/Mosaic/Stack.tsx +30 -9
  75. package/src/components/Mosaic/Tile.tsx +33 -33
  76. package/src/components/Mosaic/VirtualStackPagination.stories.tsx +423 -0
  77. package/src/components/Mosaic/index.ts +0 -1
  78. package/src/hooks/index.ts +1 -0
  79. package/src/hooks/useDefaultColumnEventHandler.ts +2 -2
  80. package/src/hooks/useEventHandlerAdapter.ts +4 -5
  81. package/src/hooks/useVirtualizerPagination.test.tsx +105 -0
  82. package/src/hooks/useVirtualizerPagination.ts +473 -0
  83. package/src/hooks/useVisibleItems.ts +2 -2
  84. package/src/playwright/smoke.spec.ts +4 -1
  85. package/src/testing/DefaultStackTile.tsx +1 -1
  86. package/src/testing/decorator.tsx +3 -3
  87. package/dist/lib/browser/chunk-OBVIYZLR.mjs +0 -1119
  88. package/dist/lib/browser/chunk-OBVIYZLR.mjs.map +0 -7
  89. package/dist/lib/browser/index.mjs +0 -701
  90. package/dist/lib/browser/index.mjs.map +0 -7
  91. package/dist/lib/browser/meta.json +0 -1
  92. package/dist/lib/browser/playwright/index.mjs +0 -256
  93. package/dist/lib/browser/playwright/index.mjs.map +0 -7
  94. package/dist/lib/browser/testing/index.mjs +0 -136
  95. package/dist/lib/browser/testing/index.mjs.map +0 -7
  96. package/dist/lib/browser/translations.mjs +0 -18
  97. package/dist/lib/browser/translations.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-4IEMCMIV.mjs +0 -1121
  99. package/dist/lib/node-esm/chunk-4IEMCMIV.mjs.map +0 -7
  100. package/dist/lib/node-esm/index.mjs +0 -702
  101. package/dist/lib/node-esm/index.mjs.map +0 -7
  102. package/dist/lib/node-esm/meta.json +0 -1
  103. package/dist/lib/node-esm/playwright/index.mjs +0 -258
  104. package/dist/lib/node-esm/playwright/index.mjs.map +0 -7
  105. package/dist/lib/node-esm/testing/index.mjs +0 -137
  106. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  107. package/dist/lib/node-esm/translations.mjs +0 -20
  108. package/dist/lib/node-esm/translations.mjs.map +0 -7
  109. package/dist/types/src/components/Mosaic/Root.d.ts +0 -35
  110. package/dist/types/src/components/Mosaic/Root.d.ts.map +0 -1
  111. package/dist/types/src/components/Mosaic/types.d.ts +0 -86
  112. package/dist/types/src/components/Mosaic/types.d.ts.map +0 -1
  113. package/src/components/Mosaic/Root.tsx +0 -288
  114. package/src/components/Mosaic/types.ts +0 -94
  115. /package/src/components/Focus/{Focus.tsx → Focus.ts} +0 -0
@@ -2,14 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import {
6
- MosaicContainer,
7
- type MosaicContainerProps,
8
- type MosaicContainerState,
9
- type MosaicScrollController,
10
- useMosaicContainerContext,
11
- } from './Container';
5
+ import { MosaicContainer, type MosaicContainerProps, type MosaicScrollController } from './Container';
12
6
  import { MosaicDragHandle, type MosaicDragHandleProps } from './DragHandle';
7
+ import { type MosaicContainerState, useMosaicContainerContext } from './MosaicContainerContext';
8
+ import { type MosaicTileState, useMosaicTileContext } from './MosaicTileContext';
13
9
  import {
14
10
  MosaicDropIndicator,
15
11
  type MosaicDropIndicatorProps,
@@ -17,16 +13,14 @@ import {
17
13
  type MosaicPlaceholderProps,
18
14
  } from './Placeholder';
19
15
  import { MosaicResizeHandle, type MosaicResizeHandleProps } from './ResizeHandle';
20
- import { MosaicRoot, type MosaicRootProps, useMosaicRootContext } from './Root';
21
16
  import { MosaicStack, type MosaicStackProps, type MosaicStackTileComponent, MosaicVirtualStack } from './Stack';
22
- import { MosaicTile, type MosaicTileProps, type MosaicTileState, useMosaicTileContext } from './Tile';
17
+ import { MosaicTile, type MosaicTileProps } from './Tile';
23
18
 
24
19
  //
25
20
  // Mosaic
26
21
  //
27
22
 
28
23
  export const Mosaic = {
29
- Root: MosaicRoot,
30
24
  Container: MosaicContainer,
31
25
  Tile: MosaicTile,
32
26
  DragHandle: MosaicDragHandle,
@@ -44,7 +38,6 @@ export type {
44
38
  MosaicDropIndicatorProps,
45
39
  MosaicPlaceholderProps,
46
40
  MosaicResizeHandleProps,
47
- MosaicRootProps,
48
41
  MosaicScrollController,
49
42
  MosaicStackProps,
50
43
  MosaicStackTileComponent,
@@ -52,8 +45,4 @@ export type {
52
45
  MosaicTileState,
53
46
  };
54
47
 
55
- export {
56
- useMosaicRootContext as useMosaic,
57
- useMosaicContainerContext as useMosaicContainer,
58
- useMosaicTileContext as useMosaicTile,
59
- };
48
+ export { useMosaicContainerContext as useMosaicContainer, useMosaicTileContext as useMosaicTile };
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+
7
+ import { type AllowedAxis } from '@dxos/react-ui';
8
+ import { type DndContainerHandler, type DndDraggingState, type DndLocation } from '@dxos/react-ui-dnd';
9
+
10
+ // Kept out of `Container.tsx`: react-refresh only fast-refreshes a module whose exports are all
11
+ // components, so the context and hook exported beside them force a full page reload on every edit.
12
+
13
+ export const MOSAIC_CONTAINER_NAME = 'Mosaic.Container';
14
+
15
+ export type MosaicContainerState = { type: 'idle' } | { type: 'active'; bounds?: DOMRect };
16
+
17
+ export type MosaicContainerContextValue<TData = any, Location = DndLocation> = {
18
+ id: string;
19
+ eventHandler: DndContainerHandler<TData>;
20
+ orientation?: AllowedAxis;
21
+ dragging?: DndDraggingState;
22
+ scrolling?: boolean;
23
+ state: MosaicContainerState;
24
+
25
+ /** Active drop location. */
26
+ activeLocation?: Location;
27
+ setActiveLocation: (location: Location | undefined) => void;
28
+
29
+ /** ID of the current (aria-current) item. */
30
+ currentId?: string;
31
+ /** Set the current item by ID. */
32
+ setCurrentId: (id: string | undefined) => void;
33
+
34
+ /** IDs of selected (aria-selected) items. */
35
+ selectedIds?: ReadonlySet<string>;
36
+ /** Request to set or unset selection on an item by ID. */
37
+ setSelected: (id: string, selected: boolean) => void;
38
+
39
+ /** Register a scroll-to-item callback (provided by Stack/VirtualStack). */
40
+ registerScrollTo: (fn: ((id: string) => void) | undefined) => void;
41
+ };
42
+
43
+ export const [MosaicContainerContextProvider, useMosaicContainerContext] =
44
+ createContext<MosaicContainerContextValue>('MosaicContainer');
@@ -0,0 +1,34 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
6
+ import { createContext } from '@radix-ui/react-context';
7
+
8
+ import { type Size } from '@dxos/react-ui-dnd';
9
+
10
+ // Kept out of `Tile.tsx`: react-refresh only fast-refreshes a module whose exports are all
11
+ // components, so the context and hook exported beside them force a full page reload on every edit.
12
+
13
+ export const MOSAIC_TILE_NAME = 'Mosaic.Tile';
14
+
15
+ export type MosaicTileState =
16
+ | { type: 'idle' }
17
+ | { type: 'preview'; container: HTMLElement; rect: DOMRect }
18
+ | { type: 'dragging' }
19
+ | { type: 'target'; closestEdge: Edge | null };
20
+
21
+ export type MosaicTileContextValue = {
22
+ state: MosaicTileState;
23
+ /** Register the element that initiates dragging; set by a child `Mosaic.DragHandle`. */
24
+ setDragHandle: (element: HTMLElement | null) => void;
25
+ /** Current extent (rem) during/after resize; undefined when the tile is not sized. */
26
+ size?: Size;
27
+ /** Update the tile extent. A `commit` (drop) propagates to the consumer's `onSizeChange`. */
28
+ setSize: (size: Size, commit?: boolean) => void;
29
+ /** Resize bounds (rem) declared by the tile; consumed by `Mosaic.ResizeHandle`. */
30
+ minSize?: number;
31
+ maxSize?: number;
32
+ };
33
+
34
+ export const [MosaicTileContextProvider, useMosaicTileContext] = createContext<MosaicTileContextValue>('MosaicTile');
@@ -12,11 +12,11 @@ import { Slot } from '@radix-ui/react-slot';
12
12
  import React, { type PropsWithChildren, useLayoutEffect, useMemo, useRef } from 'react';
13
13
 
14
14
  import { type Axis, type ThemedClassName } from '@dxos/react-ui';
15
+ import { type DndLocation, type DndPlaceholderData, getSourceData } from '@dxos/react-ui-dnd';
15
16
  import { mx } from '@dxos/ui-theme';
16
17
 
17
- import { useMosaicContainerContext } from './Container';
18
- import { useMosaicTileContext } from './Tile';
19
- import { type LocationType, type MosaicPlaceholderData, getSourceData } from './types';
18
+ import { useMosaicContainerContext } from './MosaicContainerContext';
19
+ import { useMosaicTileContext } from './MosaicTileContext';
20
20
 
21
21
  //
22
22
  // Placeholder
@@ -35,7 +35,7 @@ const MOSAIC_PLACEHOLDER_STATE_ATTR = 'mosaic-placeholder-state';
35
35
  // a specific gap unambiguously without relying on layout-dependent indices.
36
36
  const MOSAIC_PLACEHOLDER_LOCATION_ATTR = 'mosaic-placeholder-location';
37
37
 
38
- type MosaicPlaceholderProps<Location = LocationType> = ThemedClassName<
38
+ type MosaicPlaceholderProps<Location = DndLocation> = ThemedClassName<
39
39
  PropsWithChildren<{
40
40
  asChild?: boolean;
41
41
  orientation?: Axis;
@@ -43,7 +43,7 @@ type MosaicPlaceholderProps<Location = LocationType> = ThemedClassName<
43
43
  }>
44
44
  >;
45
45
 
46
- const MosaicPlaceholder = <Location extends LocationType = LocationType>({
46
+ const MosaicPlaceholder = <Location extends DndLocation = DndLocation>({
47
47
  classNames,
48
48
  children,
49
49
  asChild,
@@ -59,13 +59,13 @@ const MosaicPlaceholder = <Location extends LocationType = LocationType>({
59
59
  activeLocation,
60
60
  setActiveLocation,
61
61
  } = useMosaicContainerContext(MOSAIC_PLACEHOLDER_NAME);
62
- const data = useMemo<MosaicPlaceholderData<Location>>(
62
+ const data = useMemo<DndPlaceholderData<Location>>(
63
63
  () =>
64
64
  ({
65
65
  type: 'placeholder',
66
66
  containerId,
67
67
  location,
68
- }) satisfies MosaicPlaceholderData<Location>,
68
+ }) satisfies DndPlaceholderData<Location>,
69
69
  [containerId, location],
70
70
  );
71
71
 
@@ -103,7 +103,6 @@ const MosaicPlaceholder = <Location extends LocationType = LocationType>({
103
103
  [`data-${MOSAIC_PLACEHOLDER_STATE_ATTR}`]: data.location === activeLocation ? 'active' : 'idle',
104
104
  [`data-${MOSAIC_PLACEHOLDER_LOCATION_ATTR}`]: String(location),
105
105
  }}
106
- role='none'
107
106
  className={mx('relative', classNames)}
108
107
  ref={rootRef}
109
108
  >
@@ -6,7 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useCallback, useMemo, useState } from 'react';
7
7
 
8
8
  import { type Axis, ScrollArea } from '@dxos/react-ui';
9
- import { type Size } from '@dxos/react-ui-dnd';
9
+ import { Dnd, type Size } from '@dxos/react-ui-dnd';
10
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { Mosaic, type MosaicTileProps } from './Mosaic';
@@ -40,7 +40,7 @@ const ResizableTile = ({ data, size, onResize, ...tileProps }: ResizableTileProp
40
40
  return (
41
41
  <Mosaic.Tile
42
42
  {...tileProps}
43
- classNames='flex flex-col p-2 m-1 border border-separator rounded-sm bg-base-surface overflow-hidden'
43
+ classNames='flex flex-col p-2 m-1 border border-separator rounded-sm dx-base-surface overflow-hidden'
44
44
  data={data}
45
45
  size={size}
46
46
  onSizeChange={handleSizeChange}
@@ -70,7 +70,7 @@ const DefaultStory = ({ orientation = 'vertical' }: { orientation?: Axis }) => {
70
70
  );
71
71
 
72
72
  return (
73
- <Mosaic.Root>
73
+ <Dnd.Root>
74
74
  <Mosaic.Container
75
75
  asChild
76
76
  orientation={orientation}
@@ -89,7 +89,7 @@ const DefaultStory = ({ orientation = 'vertical' }: { orientation?: Axis }) => {
89
89
  </ScrollArea.Viewport>
90
90
  </ScrollArea.Root>
91
91
  </Mosaic.Container>
92
- </Mosaic.Root>
92
+ </Dnd.Root>
93
93
  );
94
94
  };
95
95
 
@@ -6,8 +6,8 @@ import React from 'react';
6
6
 
7
7
  import { ResizeHandle } from '@dxos/react-ui-dnd';
8
8
 
9
- import { useMosaicContainerContext } from './Container';
10
- import { useMosaicTileContext } from './Tile';
9
+ import { useMosaicContainerContext } from './MosaicContainerContext';
10
+ import { useMosaicTileContext } from './MosaicTileContext';
11
11
 
12
12
  const MOSAIC_RESIZE_HANDLE_NAME = 'Mosaic.ResizeHandle';
13
13
 
@@ -9,7 +9,9 @@ import { useMemo } from 'react';
9
9
  import { Obj } from '@dxos/echo';
10
10
  import { random } from '@dxos/random';
11
11
  import { Panel, ScrollArea, Toolbar } from '@dxos/react-ui';
12
+ import { Dnd, type DndContainerHandler } from '@dxos/react-ui-dnd';
12
13
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
14
+ import { arrayMove } from '@dxos/util';
13
15
 
14
16
  import { useContainerDebug } from '../../hooks';
15
17
  import { DefaultStackTile, TestItem } from '../../testing';
@@ -30,17 +32,50 @@ const createTestItems = (n: number) =>
30
32
  }),
31
33
  );
32
34
 
33
- const DefaultStackStory = (props: MosaicStackProps<Obj.Any>) => {
35
+ // Stateful items plus a same-container reorder handler so the stories actually apply drops
36
+ // (the container is headless — reordering is the consumer's responsibility).
37
+ const useReorderableStack = () => {
34
38
  // Create items at render time to avoid Storybook serialization issues with ECHO objects.
35
- const items = useMemo(() => createTestItems(NUM_ITEMS), []);
39
+ const [items, setItems] = useState(() => createTestItems(NUM_ITEMS));
40
+ const eventHandler = useMemo<DndContainerHandler<Obj.Any>>(
41
+ () => ({
42
+ id: 'test',
43
+ canDrop: () => true,
44
+ onDrop: ({ source, target }) => {
45
+ const to =
46
+ (target?.type === 'tile' || target?.type === 'placeholder') && typeof target.location === 'number'
47
+ ? Math.floor(target.location)
48
+ : undefined;
49
+ if (to === undefined || to < 0) {
50
+ return;
51
+ }
52
+ setItems((prev) => {
53
+ const from = prev.findIndex((item) => item.id === source.id);
54
+ if (from === -1) {
55
+ return prev;
56
+ }
57
+ const next = prev.slice();
58
+ arrayMove(next, from, to);
59
+ return next;
60
+ });
61
+ },
62
+ }),
63
+ [],
64
+ );
65
+
66
+ return { items, eventHandler };
67
+ };
68
+
69
+ const DefaultStackStory = (props: MosaicStackProps<Obj.Any>) => {
70
+ const { items, eventHandler } = useReorderableStack();
36
71
  const [DebugInfo, debugHandler] = useContainerDebug(props.debug);
37
72
  const [viewport, setViewport] = useState<HTMLElement | null>(null);
38
73
  return (
39
- <Mosaic.Root debug={props.debug}>
74
+ <Dnd.Root>
40
75
  <Panel.Root>
41
76
  <Panel.Toolbar asChild>
42
77
  <Toolbar.Root>
43
- <div className='flex grow justify-center'>Items: {items.length}</div>
78
+ <Toolbar.Text>Items: {items.length}</Toolbar.Text>
44
79
  </Toolbar.Root>
45
80
  </Panel.Toolbar>
46
81
  <Panel.Content asChild>
@@ -49,8 +84,9 @@ const DefaultStackStory = (props: MosaicStackProps<Obj.Any>) => {
49
84
  asChild
50
85
  orientation='vertical'
51
86
  autoScroll={viewport}
52
- eventHandler={{ id: 'test', canDrop: () => true }}
87
+ eventHandler={eventHandler}
53
88
  debug={debugHandler}
89
+ placeholderDebug={props.debug}
54
90
  >
55
91
  <ScrollArea.Root orientation='vertical'>
56
92
  <ScrollArea.Viewport ref={setViewport}>
@@ -61,23 +97,22 @@ const DefaultStackStory = (props: MosaicStackProps<Obj.Any>) => {
61
97
  </Focus.Group>
62
98
  </Panel.Content>
63
99
  {props.debug && (
64
- <Panel.Statusbar className='h-[40dvh]'>
100
+ <Panel.Statusbar classNames='h-[40dvh]'>
65
101
  <DebugInfo />
66
102
  </Panel.Statusbar>
67
103
  )}
68
104
  </Panel.Root>
69
- </Mosaic.Root>
105
+ </Dnd.Root>
70
106
  );
71
107
  };
72
108
 
73
109
  const VirtualStackStory = (props: MosaicStackProps<Obj.Any>) => {
74
- // Create items at render time to avoid Storybook serialization issues with ECHO objects.
75
- const items = useMemo(() => createTestItems(NUM_ITEMS), []);
110
+ const { items, eventHandler } = useReorderableStack();
76
111
  const [info, setInfo] = useState<any>(null);
77
112
  const [DebugInfo, debugHandler] = useContainerDebug(props.debug);
78
113
  const [viewport, setViewport] = useState<HTMLElement | null>(null);
79
114
  return (
80
- <Mosaic.Root debug={props.debug}>
115
+ <Dnd.Root>
81
116
  <Panel.Root>
82
117
  <Panel.Toolbar asChild>
83
118
  <Toolbar.Root>
@@ -89,8 +124,9 @@ const VirtualStackStory = (props: MosaicStackProps<Obj.Any>) => {
89
124
  asChild
90
125
  orientation='vertical'
91
126
  autoScroll={viewport}
92
- eventHandler={{ id: 'test', canDrop: () => true }}
127
+ eventHandler={eventHandler}
93
128
  debug={debugHandler}
129
+ placeholderDebug={props.debug}
94
130
  >
95
131
  <ScrollArea.Root orientation='vertical'>
96
132
  <ScrollArea.Viewport ref={setViewport}>
@@ -108,12 +144,12 @@ const VirtualStackStory = (props: MosaicStackProps<Obj.Any>) => {
108
144
  </Mosaic.Container>
109
145
  </Panel.Content>
110
146
  {props.debug && (
111
- <Panel.Statusbar className='h-[40dvh]'>
147
+ <Panel.Statusbar classNames='h-[40dvh]'>
112
148
  <DebugInfo />
113
149
  </Panel.Statusbar>
114
150
  )}
115
151
  </Panel.Root>
116
- </Mosaic.Root>
152
+ </Dnd.Root>
117
153
  );
118
154
  };
119
155
 
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { useComposedRefs } from '@radix-ui/react-compose-refs';
6
- import { type ReactVirtualizerOptions, useVirtualizer } from '@tanstack/react-virtual';
6
+ import { type ReactVirtualizerOptions, type Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
7
7
  import React, {
8
8
  type FC,
9
9
  Fragment,
@@ -19,14 +19,14 @@ import React, {
19
19
 
20
20
  import { invariant } from '@dxos/invariant';
21
21
  import { type Axis, type ThemedClassName, composable, composableProps } from '@dxos/react-ui';
22
+ import { type GetId } from '@dxos/react-ui-dnd';
22
23
  import { mx } from '@dxos/ui-theme';
23
24
 
24
- import { useVisibleItems } from '../../hooks';
25
- import { useMosaicContainerContext } from './Container';
25
+ import { type VirtualizerPaginationController, useVirtualizerPagination, useVisibleItems } from '../../hooks';
26
+ import { useMosaicContainerContext } from './MosaicContainerContext';
26
27
  import { MosaicPlaceholder, type MosaicPlaceholderProps } from './Placeholder';
27
28
  import { styles } from './styles';
28
29
  import { type MosaicTileProps } from './Tile';
29
- import { type GetId } from './types';
30
30
 
31
31
  //
32
32
  // Mosaic Drag-and-drop
@@ -179,7 +179,12 @@ type MosaicVirtualStackProps<TData = any> = MosaicStackProps<TData> &
179
179
  Pick<
180
180
  ReactVirtualizerOptions<HTMLElement, HTMLDivElement>,
181
181
  'estimateSize' | 'gap' | 'getScrollElement' | 'overscan' | 'onChange'
182
- >;
182
+ > & {
183
+ /** Enables paginated loading; see `useVirtualizerPagination`. Requires `draggable={false}`. */
184
+ pagination?: VirtualizerPaginationController;
185
+ /** Rows from either loaded edge at which the adjacent page is requested. @default 12. */
186
+ paginationThreshold?: number;
187
+ };
183
188
 
184
189
  const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackProps>(
185
190
  (
@@ -193,6 +198,8 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
193
198
  getScrollElement,
194
199
  overscan = 8,
195
200
  gap,
201
+ pagination,
202
+ paginationThreshold,
196
203
  onChange,
197
204
  draggable = true,
198
205
  debug,
@@ -201,6 +208,7 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
201
208
  forwardedRef,
202
209
  ) => {
203
210
  invariant(Tile);
211
+ invariant(!pagination || !draggable, 'pagination requires draggable={false}');
204
212
  const { id, dragging, currentId, selectedIds, registerScrollTo } =
205
213
  useMosaicContainerContext(MOSAIC_VIRTUAL_STACK_NAME);
206
214
  const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
@@ -212,14 +220,27 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
212
220
  ? (index: number) => (index % 2 === 0 ? 0 : estimateSize(Math.floor(index / 2)))
213
221
  : estimateSize;
214
222
 
223
+ const { onChange: handlePaginationChange, leadingSpace } = useVirtualizerPagination({
224
+ items: visibleItems,
225
+ getId,
226
+ pagination,
227
+ threshold: paginationThreshold,
228
+ });
229
+ const handleChange = useCallback(
230
+ (nextVirtualizer: Virtualizer<any, any>, sync: boolean) => {
231
+ handlePaginationChange(nextVirtualizer);
232
+ onChange?.(nextVirtualizer, sync);
233
+ },
234
+ [handlePaginationChange, onChange],
235
+ );
236
+
215
237
  const virtualizer = useVirtualizer({
216
238
  // NOTE: When draggable we double the number of items to allow for placeholders.
217
239
  count: draggable ? visibleItems.length * 2 + 1 : visibleItems.length,
218
240
  estimateSize: wrappedEstimateSize,
219
241
  gap,
220
- // Inset the stack from both ends by `gap` (matching the inter-item spacing). The virtualizer
221
- // folds this into item offsets, getTotalSize(), and scrollToIndex, so no manual compensation.
222
- paddingStart: gap,
242
+ // Inset by `gap` (inter-item spacing) plus any pagination spacer; see `useVirtualizerPagination`.
243
+ paddingStart: (gap ?? 0) + leadingSpace,
223
244
  paddingEnd: gap,
224
245
  // Key measurements by stable item ID so the size cache survives scrolling;
225
246
  // without this, measurements are indexed by position and are lost when items reorder.
@@ -228,7 +249,7 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
228
249
  : (index) => getId(visibleItems![index]),
229
250
  getScrollElement,
230
251
  overscan,
231
- onChange,
252
+ onChange: handleChange,
232
253
  });
233
254
 
234
255
  // Register scroll-to-item via virtualizer index lookup.
@@ -17,50 +17,41 @@ import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/elemen
17
17
  import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
18
18
  import { type DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types';
19
19
  import { composeRefs } from '@radix-ui/react-compose-refs';
20
- import { createContext } from '@radix-ui/react-context';
21
20
  import { Primitive } from '@radix-ui/react-primitive';
22
21
  import { Slot } from '@radix-ui/react-slot';
23
- import React, { type PropsWithChildren, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
22
+ import React, {
23
+ type PropsWithChildren,
24
+ useCallback,
25
+ useEffect,
26
+ useLayoutEffect,
27
+ useMemo,
28
+ useRef,
29
+ useState,
30
+ } from 'react';
24
31
  import { createPortal } from 'react-dom';
25
32
 
26
33
  import { type ThemedClassName } from '@dxos/react-ui';
27
34
  import { composableProps, slottable } from '@dxos/react-ui';
28
- import { type Size, resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
35
+ import {
36
+ type DndLocation,
37
+ type DndTileData,
38
+ type Size,
39
+ getSourceData,
40
+ resizeAttributes,
41
+ sizeStyle,
42
+ } from '@dxos/react-ui-dnd';
29
43
 
30
- import { useMosaicContainerContext } from './Container';
31
- import { type LocationType, type MosaicTileData, getSourceData } from './types';
44
+ import { useMosaicContainerContext } from './MosaicContainerContext';
45
+ import { MOSAIC_TILE_NAME, MosaicTileContextProvider, type MosaicTileState } from './MosaicTileContext';
32
46
 
33
47
  //
34
48
  // Tile
35
49
  //
36
50
 
37
- const MOSAIC_TILE_NAME = 'Mosaic.Tile';
38
-
39
- type MosaicTileState =
40
- | { type: 'idle' }
41
- | { type: 'preview'; container: HTMLElement; rect: DOMRect }
42
- | { type: 'dragging' }
43
- | { type: 'target'; closestEdge: Edge | null };
44
-
45
- type MosaicTileContextValue = {
46
- state: MosaicTileState;
47
- /** Register the element that initiates dragging; set by a child `Mosaic.DragHandle`. */
48
- setDragHandle: (element: HTMLElement | null) => void;
49
- /** Current extent (rem) during/after resize; undefined when the tile is not sized. */
50
- size?: Size;
51
- /** Update the tile extent. A `commit` (drop) propagates to the consumer's `onSizeChange`. */
52
- setSize: (size: Size, commit?: boolean) => void;
53
- /** Resize bounds (rem) declared by the tile; consumed by `Mosaic.ResizeHandle`. */
54
- minSize?: number;
55
- maxSize?: number;
56
- };
57
-
58
- const [MosaicTileContextProvider, useMosaicTileContext] = createContext<MosaicTileContextValue>('MosaicTile');
59
-
60
51
  // State attribute: data-[mosaic-tile-state=dragging]
61
52
  const MOSAIC_TILE_STATE_ATTR = 'mosaic-tile-state';
62
53
 
63
- type MosaicTileProps<TData = any, TLocation = LocationType> = ThemedClassName<
54
+ type MosaicTileProps<TData = any, TLocation = DndLocation> = ThemedClassName<
64
55
  PropsWithChildren<{
65
56
  className?: string;
66
57
  dragHandle?: HTMLElement | null;
@@ -141,13 +132,22 @@ const MosaicTile = slottable<HTMLDivElement, MosaicTileProps>(
141
132
  },
142
133
  [onSizeChange],
143
134
  );
135
+ // The `size` prop is the source of truth; internal state exists only to reflect the live extent
136
+ // during a drag (which commits back through `onSizeChange`). Re-sync when the prop changes so a
137
+ // tile that first rendered without a size — e.g. before the breakpoint settled, or a
138
+ // mobile/fullbleed → sliding branch switch that swaps `size` in — applies it once it arrives,
139
+ // instead of staying stuck at its initial (often `undefined`) extent. A live drag never changes
140
+ // the prop (only the committed drop does), so this does not fight the drag.
141
+ useEffect(() => {
142
+ setInternalSize(sizeProp);
143
+ }, [sizeProp]);
144
144
 
145
145
  const allowedEdges = useMemo<Edge[]>(
146
146
  () => allowedEdgesProp || (orientation === 'vertical' ? ['top', 'bottom'] : ['left', 'right']),
147
147
  [allowedEdgesProp, orientation],
148
148
  );
149
149
 
150
- const data = useMemo<MosaicTileData>(
150
+ const data = useMemo<DndTileData>(
151
151
  () =>
152
152
  ({
153
153
  type: 'tile',
@@ -155,7 +155,7 @@ const MosaicTile = slottable<HTMLDivElement, MosaicTileProps>(
155
155
  containerId,
156
156
  data: dataProp,
157
157
  location,
158
- }) satisfies MosaicTileData,
158
+ }) satisfies DndTileData,
159
159
  [containerId, location, dataProp],
160
160
  );
161
161
 
@@ -329,6 +329,6 @@ const MosaicTile = slottable<HTMLDivElement, MosaicTileProps>(
329
329
 
330
330
  MosaicTile.displayName = MOSAIC_TILE_NAME;
331
331
 
332
- export { MosaicTile, useMosaicTileContext };
332
+ export { MosaicTile };
333
333
 
334
- export type { MosaicTileProps, MosaicTileState };
334
+ export type { MosaicTileProps };