@dxos/react-ui-mosaic 0.10.0 → 0.11.1

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
@@ -0,0 +1,1024 @@
1
+ import { createContext } from "@radix-ui/react-context";
2
+ import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { Button, Focus, IconButton, composable, composableProps, slottable, useFocus, useTranslation } from "@dxos/react-ui";
4
+ import { ResizeHandle, getSourceData, resizeAttributes, sizeStyle, useDndRootContext } from "@dxos/react-ui-dnd";
5
+ import { mx } from "@dxos/ui-theme";
6
+ import { createPortal } from "react-dom";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { isTruthy } from "@dxos/util";
9
+ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
10
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
11
+ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
12
+ import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs";
13
+ import { Primitive } from "@radix-ui/react-primitive";
14
+ import { Slot } from "@radix-ui/react-slot";
15
+ import { bind } from "bind-event-listener";
16
+ import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box";
17
+ import { useVirtualizer } from "@tanstack/react-virtual";
18
+ import { invariant } from "@dxos/invariant";
19
+ import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
20
+ import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source";
21
+ import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
22
+ //#region src/hooks/useVirtualizerPagination.ts
23
+ /** Rows from either loaded edge at which the next/previous page is requested. */
24
+ var DEFAULT_THRESHOLD = 12;
25
+ /** Index at/above which `getNext` fires; small windows clamp to the last row only. */
26
+ var nextPageIndexThreshold = (itemCount, threshold) => itemCount > threshold ? itemCount - threshold : itemCount - 1;
27
+ /** True when loaded content exceeds the scroll viewport (user can actually scroll). */
28
+ var isScrollable = (virtualizer) => {
29
+ const viewportHeight = virtualizer.scrollElement?.clientHeight ?? 0;
30
+ return viewportHeight > 0 && virtualizer.getTotalSize() > viewportHeight + 1;
31
+ };
32
+ var readEdgeGeometry = (virtualizer) => {
33
+ const virtualItems = virtualizer.getVirtualItems();
34
+ const measurements = virtualizer.measurementsCache;
35
+ return {
36
+ scrollOffset: virtualizer.scrollOffset ?? 0,
37
+ viewportHeight: virtualizer.scrollElement?.clientHeight ?? 0,
38
+ firstVisibleIndex: virtualItems.at(0)?.index,
39
+ lastVisibleIndex: virtualItems.at(-1)?.index,
40
+ windowStart: measurements[0]?.start,
41
+ windowEnd: measurements.at(-1)?.end
42
+ };
43
+ };
44
+ var isNearBottomEdge = (geometry, itemCount, threshold) => geometry.lastVisibleIndex !== void 0 ? geometry.lastVisibleIndex >= nextPageIndexThreshold(itemCount, threshold) : geometry.windowEnd != null && geometry.scrollOffset + geometry.viewportHeight > geometry.windowEnd;
45
+ var isNearTopEdge = (geometry, threshold) => geometry.firstVisibleIndex !== void 0 ? geometry.firstVisibleIndex < threshold : geometry.windowStart != null && geometry.scrollOffset < geometry.windowStart;
46
+ /** Loaded content doesn't reach the viewport's top edge (e.g. scrolled into the leading spacer). */
47
+ var isBlankAbove = (geometry) => geometry.windowStart != null && geometry.windowStart > geometry.scrollOffset;
48
+ /** Loaded content doesn't reach the viewport's bottom edge. */
49
+ var isBlankBelow = (geometry) => geometry.windowEnd != null && geometry.windowEnd < geometry.scrollOffset + geometry.viewportHeight;
50
+ /** Not fired at this exact scroll offset since the edge was last left, unless blank re-arms it. */
51
+ var canRequestNext = (state, geometry) => state.lastGetNextScrollRef.current === null || geometry.scrollOffset !== state.lastGetNextScrollRef.current || isBlankBelow(geometry);
52
+ var canRequestPrevious = (state, geometry) => state.lastGetPreviousScrollRef.current === null || geometry.scrollOffset !== state.lastGetPreviousScrollRef.current || isBlankAbove(geometry);
53
+ var requestNext = (state, items, scrollOffset) => {
54
+ state.lastNextRequestedItemsRef.current = items;
55
+ state.lastGetNextScrollRef.current = scrollOffset;
56
+ const getNext = state.paginationRef.current?.getNext;
57
+ queueMicrotask(() => getNext?.());
58
+ };
59
+ var requestPrevious = (state, items, scrollOffset) => {
60
+ state.lastPreviousRequestedItemsRef.current = items;
61
+ state.lastGetPreviousScrollRef.current = scrollOffset;
62
+ const getPrevious = state.paginationRef.current?.getPrevious;
63
+ queueMicrotask(() => getPrevious?.());
64
+ };
65
+ /**
66
+ * Requests `getNext`/`getPrevious` once the loaded window's edge nears the viewport. Deduped per
67
+ * direction and per `items` snapshot; re-armed while the viewport sits in blank space so a chain
68
+ * of pages loads back-to-back without further user scrolling.
69
+ */
70
+ var evaluateTriggers = ({ virtualizer, items, pagination, itemCount, threshold, state }) => {
71
+ const geometry = readEdgeGeometry(virtualizer);
72
+ const scrollable = isScrollable(virtualizer);
73
+ const smallWindow = itemCount <= threshold;
74
+ const nearBottom = isNearBottomEdge(geometry, itemCount, threshold);
75
+ const nearTop = isNearTopEdge(geometry, threshold);
76
+ if (!nearBottom) state.lastGetNextScrollRef.current = null;
77
+ if (!nearTop) state.lastGetPreviousScrollRef.current = null;
78
+ const triggerNext = !!pagination?.getNext && !pagination.isLoading && scrollable && nearBottom && (!smallWindow || geometry.scrollOffset > 0) && canRequestNext(state, geometry);
79
+ const triggerPrevious = !!pagination?.getPrevious && !pagination.isLoading && scrollable && nearTop && pagination.atHead !== true && canRequestPrevious(state, geometry);
80
+ if (!triggerNext) state.lastNextRequestedItemsRef.current = void 0;
81
+ if (!triggerPrevious) state.lastPreviousRequestedItemsRef.current = void 0;
82
+ if (triggerNext && state.lastNextRequestedItemsRef.current !== items) requestNext(state, items, geometry.scrollOffset);
83
+ else if (triggerPrevious && state.lastPreviousRequestedItemsRef.current !== items) requestPrevious(state, items, geometry.scrollOffset);
84
+ };
85
+ /** Total height (px) of `prevItems[0, oldIndexOfNewFirst)`, per its outgoing measurements. */
86
+ var evictedPrefixHeight = (virtualizer, oldIndexOfNewFirst) => {
87
+ const measurements = virtualizer.measurementsCache;
88
+ return (measurements[oldIndexOfNewFirst]?.start ?? 0) - (measurements[0]?.start ?? 0);
89
+ };
90
+ /**
91
+ * True when `items` is `prevItems` sliced from `oldIndexOfNewFirst` (a genuine slide/eviction/append).
92
+ * False for a reorder -- e.g. a conversation bumped to the head by a mid-sync reply -- whose old
93
+ * mid-list offset would otherwise be misread as evicted height and grow the spacer for good.
94
+ */
95
+ var isContiguousSlide = (prevItems, items, oldIndexOfNewFirst, getId) => {
96
+ const overlap = Math.min(items.length, prevItems.length - oldIndexOfNewFirst);
97
+ for (let index = 0; index < overlap; index++) if (getId(items[index]) !== getId(prevItems[oldIndexOfNewFirst + index])) return false;
98
+ return true;
99
+ };
100
+ /**
101
+ * Finds an item retained across a prepend, anchored on its pre-change offset. Scans from the tail
102
+ * (the common case -- a prepend leaves the rest of the window untouched) and doesn't require the
103
+ * item to be currently rendered, since `measurementsCache` covers the whole array.
104
+ */
105
+ var findRetainedAnchor = (prevItems, newIds, getId, virtualizer) => {
106
+ for (let index = prevItems.length - 1; index >= 0; index--) {
107
+ const id = getId(prevItems[index]);
108
+ if (newIds.has(id)) {
109
+ const start = virtualizer.measurementsCache[index]?.start;
110
+ return start != null ? {
111
+ id,
112
+ start
113
+ } : null;
114
+ }
115
+ }
116
+ return null;
117
+ };
118
+ /**
119
+ * Classifies how `items` changed at the front, relative to `prevItems`, using only the outgoing
120
+ * window's own measurements (no need to wait for the incoming render). `'evicted'` covers both a
121
+ * prefix eviction (the window slid forward) and a pure append (the window grew) -- either way the
122
+ * item now at the front was already loaded, so its height is already known, *and* the retained
123
+ * items keep their old relative order (`isContiguousSlide`). `'prepended'` covers everything else:
124
+ * a `getPrevious` prepend, a reset to a disjoint range, or an existing item reordered to the head
125
+ * (e.g. conversation grouping bumping a thread on a new reply) -- despite the new head having been
126
+ * loaded before, nothing was actually evicted.
127
+ */
128
+ var classifyFrontEdgeChange = (prevItems, items, getId, virtualizer) => {
129
+ if (!virtualizer || !prevItems) return { kind: "none" };
130
+ const firstNewId = items?.[0] != null ? getId(items[0]) : void 0;
131
+ const oldIndexOfNewFirst = firstNewId != null ? prevItems.findIndex((item) => getId(item) === firstNewId) : -1;
132
+ if (oldIndexOfNewFirst >= 0 && items && isContiguousSlide(prevItems, items, oldIndexOfNewFirst, getId)) return {
133
+ kind: "evicted",
134
+ evictedHeight: evictedPrefixHeight(virtualizer, oldIndexOfNewFirst)
135
+ };
136
+ return {
137
+ kind: "prepended",
138
+ anchor: findRetainedAnchor(prevItems, new Set(items?.map((item) => getId(item))), getId, virtualizer)
139
+ };
140
+ };
141
+ /**
142
+ * `'no-overlap'`: the anchor isn't in the new `items` (e.g. a reset) -- the spacer no longer
143
+ * describes anything. `'pending'`: the incoming render hasn't measured the anchor yet. `'absorbed'`:
144
+ * the shift is fully absorbed by the spacer, no scroll rewrite needed. `'snap'`: growing past an
145
+ * empty spacer (or the `atHead` reset) leaves a remainder that has to move the scroll offset.
146
+ */
147
+ var computePrependCorrection = (virtualizer, items, getId, anchor, currentSpace, atHead) => {
148
+ const newIndex = items.findIndex((item) => getId(item) === anchor.id);
149
+ if (newIndex < 0) return { kind: "no-overlap" };
150
+ const newStart = virtualizer.measurementsCache[newIndex]?.start;
151
+ if (newStart == null) return { kind: "pending" };
152
+ const shift = newStart - anchor.start;
153
+ const nextSpace = atHead ? 0 : Math.max(0, currentSpace - shift);
154
+ const scrollAdjust = shift + (nextSpace - currentSpace);
155
+ return scrollAdjust === 0 ? {
156
+ kind: "absorbed",
157
+ nextSpace
158
+ } : {
159
+ kind: "snap",
160
+ nextSpace,
161
+ scrollAdjust
162
+ };
163
+ };
164
+ /**
165
+ * Wires a paginated data source into a `Mosaic.VirtualStack`: requests the next/previous page near
166
+ * either loaded edge (`evaluateTriggers`), and keeps the scrollbar stable across the resulting
167
+ * `items` change by maintaining a leading spacer instead of rewriting the scroll offset.
168
+ *
169
+ * Called internally by `Mosaic.VirtualStack` when its `pagination` prop is set -- not normally
170
+ * called directly.
171
+ *
172
+ * `@tanstack/react-virtual` recomputes every item's pixel offset from the current array on each
173
+ * render, with no compensation for insertion/removal at the front -- rewriting the scroll offset
174
+ * to chase that shift visibly jumps the scrollbar thumb. Instead, `leadingSpace` (fed back into
175
+ * `paddingStart`) grows by exactly the evicted height and shrinks by exactly the prepended height,
176
+ * so retained items keep their absolute position and the thumb only scales as the total grows.
177
+ *
178
+ * Eviction (`classifyFrontEdgeChange`'s `'evicted'`) is corrected synchronously during the same
179
+ * render that observes the new `items`, since the evicted height is already known -- deferring it
180
+ * would let the browser observe (and clamp `scrollTop` against) a momentary, too-short frame.
181
+ * Prepending (`'prepended'`) is corrected one render later, from a layout effect
182
+ * (`computePrependCorrection`), since the prepended items' heights aren't known until the incoming
183
+ * render measures them; that lag is safe because prepending only ever grows content before the
184
+ * spacer shrinks to match.
185
+ *
186
+ * The layout effect also re-runs `evaluateTriggers` on every `items` change, evicted/appended or
187
+ * prepended alike -- `@tanstack/react-virtual` only calls `onChange` when the visible range (or
188
+ * `isScrolling`) changes, not merely when `items` does. Once the loaded window's tail is already
189
+ * fully rendered and the scrollbar sits at its physical max, further scrolling can't move that
190
+ * range at all, so `onChange` stops firing -- relying on it alone would permanently stall a
191
+ * `getNext` chain right after its own page lands, before the user scrolls again.
192
+ *
193
+ * Assumes the stack renders with `draggable={false}` (virtual indices map 1:1 onto `items`).
194
+ */
195
+ var useVirtualizerPagination = ({ items, getId, pagination, threshold = DEFAULT_THRESHOLD }) => {
196
+ const itemCount = items?.length ?? 0;
197
+ const [leadingSpace, setLeadingSpace] = useState(0);
198
+ const leadingSpaceRef = useRef(leadingSpace);
199
+ const setSpacer = useCallback((nextSpace) => {
200
+ if (nextSpace !== leadingSpaceRef.current) {
201
+ leadingSpaceRef.current = nextSpace;
202
+ setLeadingSpace(nextSpace);
203
+ }
204
+ }, []);
205
+ const virtualizerRef = useRef(null);
206
+ const anchorRef = useRef(null);
207
+ const prevItemsRef = useRef(items);
208
+ const restoringRef = useRef(false);
209
+ const triggerState = {
210
+ lastNextRequestedItemsRef: useRef(void 0),
211
+ lastPreviousRequestedItemsRef: useRef(void 0),
212
+ lastGetNextScrollRef: useRef(null),
213
+ lastGetPreviousScrollRef: useRef(null),
214
+ paginationRef: useRef(pagination)
215
+ };
216
+ triggerState.paginationRef.current = pagination;
217
+ const rearmTriggers = useCallback((virtualizer) => {
218
+ evaluateTriggers({
219
+ virtualizer,
220
+ items,
221
+ pagination: triggerState.paginationRef.current,
222
+ itemCount,
223
+ threshold,
224
+ state: triggerState
225
+ });
226
+ }, [
227
+ items,
228
+ itemCount,
229
+ threshold
230
+ ]);
231
+ if (prevItemsRef.current !== items) {
232
+ if (pagination) {
233
+ const change = classifyFrontEdgeChange(prevItemsRef.current, items, getId, virtualizerRef.current);
234
+ if (change.kind === "evicted") {
235
+ setSpacer(leadingSpaceRef.current + change.evictedHeight);
236
+ anchorRef.current = null;
237
+ } else if (change.kind === "prepended") {
238
+ anchorRef.current = change.anchor;
239
+ if (!change.anchor) setSpacer(0);
240
+ }
241
+ }
242
+ prevItemsRef.current = items;
243
+ }
244
+ const onChange = useCallback((virtualizer) => {
245
+ virtualizerRef.current = virtualizer;
246
+ if (restoringRef.current) {
247
+ restoringRef.current = false;
248
+ return;
249
+ }
250
+ rearmTriggers(virtualizer);
251
+ }, [rearmTriggers]);
252
+ useLayoutEffect(() => {
253
+ const virtualizer = virtualizerRef.current;
254
+ const anchor = anchorRef.current;
255
+ anchorRef.current = null;
256
+ if (!virtualizer || !items) return;
257
+ if (!anchor) {
258
+ rearmTriggers(virtualizer);
259
+ return;
260
+ }
261
+ const atHead = triggerState.paginationRef.current?.atHead === true;
262
+ const correction = computePrependCorrection(virtualizer, items, getId, anchor, leadingSpaceRef.current, atHead);
263
+ switch (correction.kind) {
264
+ case "pending": return;
265
+ case "no-overlap":
266
+ setSpacer(0);
267
+ rearmTriggers(virtualizer);
268
+ return;
269
+ case "absorbed":
270
+ setSpacer(correction.nextSpace);
271
+ rearmTriggers(virtualizer);
272
+ return;
273
+ case "snap":
274
+ setSpacer(correction.nextSpace);
275
+ restoringRef.current = true;
276
+ virtualizer.scrollToOffset((virtualizer.scrollOffset ?? 0) + correction.scrollAdjust);
277
+ return;
278
+ }
279
+ }, [
280
+ items,
281
+ getId,
282
+ setSpacer,
283
+ rearmTriggers
284
+ ]);
285
+ return {
286
+ onChange,
287
+ leadingSpace
288
+ };
289
+ };
290
+ //#endregion
291
+ //#region src/hooks/useVisibleItems.ts
292
+ /**
293
+ * Returns items with the dragging item removed.
294
+ */
295
+ var useVisibleItems = ({ id, items, dragging, getId }) => {
296
+ return useMemo(() => {
297
+ if (!items || !dragging || id !== dragging.containerId) return items ?? [];
298
+ const idx = items.findIndex((item) => getId(item) === dragging.id);
299
+ if (idx === -1) return items;
300
+ const visibleItems = items.slice();
301
+ visibleItems.splice(idx, 1);
302
+ return visibleItems;
303
+ }, [
304
+ id,
305
+ items,
306
+ dragging,
307
+ getId
308
+ ]);
309
+ };
310
+ //#endregion
311
+ //#region src/components/Mosaic/styles.ts
312
+ var styles = { placeholder: {
313
+ root: [
314
+ "group opacity-0",
315
+ "group-data-[mosaic-debug=\"true\"]:opacity-100",
316
+ "group-data-[mosaic-debug=\"true\"]:bg-orange-500",
317
+ "data-[mosaic-placeholder-state=active]:opacity-100",
318
+ "data-[mosaic-placeholder-orientation=vertical]:snap-start",
319
+ "data-[mosaic-placeholder-orientation=vertical]:py-1",
320
+ "data-[mosaic-placeholder-orientation=vertical]:data-[mosaic-placeholder-state=active]:py-2.5",
321
+ "data-[mosaic-placeholder-orientation=horizontal]:px-1",
322
+ "data-[mosaic-placeholder-orientation=horizontal]:data-[mosaic-placeholder-state=active]:px-2.5"
323
+ ],
324
+ content: [
325
+ "transition-all duration-0",
326
+ "group-data-[mosaic-placeholder-orientation=vertical]:h-0",
327
+ "group-data-[mosaic-placeholder-orientation=vertical]:group-data-[mosaic-placeholder-state=active]:h-[var(--mosaic-placeholder-height)]",
328
+ "group-data-[mosaic-placeholder-orientation=horizontal]:w-0",
329
+ "group-data-[mosaic-placeholder-orientation=horizontal]:group-data-[mosaic-placeholder-state=active]:w-[var(--mosaic-placeholder-width)]"
330
+ ]
331
+ } };
332
+ //#endregion
333
+ //#region src/components/Mosaic/MosaicContainerContext.ts
334
+ var MOSAIC_CONTAINER_NAME = "Mosaic.Container";
335
+ var [MosaicContainerContextProvider, useMosaicContainerContext] = createContext("MosaicContainer");
336
+ //#endregion
337
+ //#region src/components/Mosaic/Container.tsx
338
+ var MOSAIC_CONTAINER_STATE_ATTR = "mosaic-container-state";
339
+ var MOSAIC_CONTAINER_DEBUG_ATTR = "mosaic-debug";
340
+ var MOSAIC_CONTAINER_PLACEHOLDER_WIDTH = "--mosaic-placeholder-width";
341
+ var MOSAIC_CONTAINER_PLACEHOLDER_HEIGHT = "--mosaic-placeholder-height";
342
+ var counter = 0;
343
+ /**
344
+ * Container for a Mosaic layout.
345
+ */
346
+ var MosaicContainer = composable(({ children, eventHandler: eventHandlerProp, orientation = "vertical", asChild, autoScroll: autoscrollElement, withFocus, controllerRef, currentId, onCurrentChange, selectedIds, onSelectionChange, debug, placeholderDebug, ...props }, forwardedRef) => {
347
+ const Comp = asChild ? Slot : Primitive.div;
348
+ const rootRef = useRef(null);
349
+ const composedRef = useComposedRefs(rootRef, forwardedRef);
350
+ const eventHandler = useMemo(() => eventHandlerProp ?? { id: `mosaic-container-${counter++}` }, [eventHandlerProp]);
351
+ const { dragging } = useDndRootContext(MOSAIC_CONTAINER_NAME);
352
+ const [state, setState] = useState({ type: "idle" });
353
+ const [activeLocation, setActiveLocation] = useState();
354
+ const [scrolling, setScrolling] = useState(false);
355
+ const setCurrentId = useCallback((id) => onCurrentChange?.(id), [onCurrentChange]);
356
+ const setSelected = useCallback((id, selected) => onSelectionChange?.(id, selected), [onSelectionChange]);
357
+ const scrollToRef = useRef(void 0);
358
+ const registerScrollTo = useCallback((fn) => {
359
+ scrollToRef.current = fn;
360
+ }, []);
361
+ useImperativeHandle(controllerRef, () => ({ scrollToItem: (id) => scrollToRef.current?.(id) }), []);
362
+ useEffect(() => {
363
+ if (currentId) scrollToRef.current?.(currentId);
364
+ }, [currentId]);
365
+ const { setFocus } = useFocus();
366
+ useEffect(() => {
367
+ if (withFocus) setFocus?.(state.type === "active" ? "active" : void 0);
368
+ }, [
369
+ setFocus,
370
+ withFocus,
371
+ state
372
+ ]);
373
+ const { addContainer, removeContainer } = useDndRootContext(eventHandler.id);
374
+ useEffect(() => {
375
+ addContainer(eventHandler);
376
+ return () => removeContainer(eventHandler.id);
377
+ }, [eventHandler]);
378
+ const data = useMemo(() => ({
379
+ type: "container",
380
+ id: eventHandler.id
381
+ }), [eventHandler.id]);
382
+ useEffect(() => {
383
+ if (!rootRef.current) return;
384
+ let timeout;
385
+ const handleScroll = () => {
386
+ setScrolling(true);
387
+ clearTimeout(timeout);
388
+ timeout = setTimeout(() => {
389
+ setScrolling(false);
390
+ }, 500);
391
+ };
392
+ return combine(...[
393
+ bind(rootRef.current, {
394
+ type: "dnd:cancel",
395
+ listener: () => {
396
+ setState({ type: "idle" });
397
+ }
398
+ }),
399
+ dropTargetForElements({
400
+ element: rootRef.current,
401
+ getData: () => data,
402
+ /**
403
+ * Test if permitted to drop here.
404
+ * NOTE: Contained Tile and Placeholder elements do the same.
405
+ */
406
+ canDrop: ({ source }) => {
407
+ const data = getSourceData(source);
408
+ return data && eventHandler.canDrop?.({ source: data }) || false;
409
+ },
410
+ getDropEffect: () => "move",
411
+ /**
412
+ * Dragging started in this container.
413
+ */
414
+ onDragStart: ({ source }) => {
415
+ const sourceData = source.data;
416
+ setState({
417
+ type: "active",
418
+ bounds: sourceData.bounds
419
+ });
420
+ },
421
+ /**
422
+ * Dragging entered this container.
423
+ */
424
+ onDragEnter: ({ source }) => {
425
+ const sourceData = source.data;
426
+ setState({
427
+ type: "active",
428
+ bounds: sourceData.bounds
429
+ });
430
+ },
431
+ /**
432
+ * Dragging left this container.
433
+ * NOTE: If the container isn't full-height, then when the dragged item is temporarily removed the container will shrink,
434
+ * triggering `onDragLeave`, which then causes the item to be added and removed continually (flickering).
435
+ */
436
+ onDragLeave: ({ source }) => {
437
+ if (source.data.containerId !== eventHandler.id) setState({ type: "idle" });
438
+ },
439
+ /**
440
+ * Dropped in this container.
441
+ */
442
+ onDrop: () => {
443
+ setState({ type: "idle" });
444
+ }
445
+ }),
446
+ autoscrollElement && [
447
+ autoScrollForElements({
448
+ element: autoscrollElement,
449
+ canScroll: ({ source }) => getSourceData(source) != null,
450
+ getAllowedAxis: () => orientation,
451
+ getConfiguration: () => ({ maxScrollSpeed: "fast" })
452
+ }),
453
+ bind(autoscrollElement, {
454
+ type: "scroll",
455
+ listener: handleScroll
456
+ }),
457
+ () => setScrolling(false)
458
+ ]
459
+ ].filter(isTruthy).flatMap((x) => x));
460
+ }, [
461
+ rootRef,
462
+ eventHandler,
463
+ data,
464
+ autoscrollElement
465
+ ]);
466
+ return /* @__PURE__ */ jsxs(MosaicContainerContextProvider, {
467
+ id: eventHandler.id,
468
+ eventHandler,
469
+ orientation,
470
+ state,
471
+ dragging: state.type === "active" ? dragging : void 0,
472
+ scrolling,
473
+ activeLocation,
474
+ setActiveLocation,
475
+ currentId,
476
+ setCurrentId,
477
+ selectedIds,
478
+ setSelected,
479
+ registerScrollTo,
480
+ children: [/* @__PURE__ */ jsx(Comp, {
481
+ ...composableProps(props, {
482
+ classNames: "h-full group",
483
+ style: {
484
+ [MOSAIC_CONTAINER_PLACEHOLDER_WIDTH]: state.type === "active" && state.bounds ? `${state.bounds.width}px` : "0px",
485
+ [MOSAIC_CONTAINER_PLACEHOLDER_HEIGHT]: state.type === "active" && state.bounds ? `${state.bounds.height}px` : "0px"
486
+ }
487
+ }),
488
+ [`data-${MOSAIC_CONTAINER_STATE_ATTR}`]: state.type,
489
+ [`data-${MOSAIC_CONTAINER_DEBUG_ATTR}`]: placeholderDebug,
490
+ ref: composedRef,
491
+ children
492
+ }), debug?.()]
493
+ });
494
+ });
495
+ MosaicContainer.displayName = MOSAIC_CONTAINER_NAME;
496
+ //#endregion
497
+ //#region src/components/Mosaic/MosaicTileContext.ts
498
+ var MOSAIC_TILE_NAME = "Mosaic.Tile";
499
+ var [MosaicTileContextProvider, useMosaicTileContext] = createContext("MosaicTile");
500
+ //#endregion
501
+ //#region src/components/Mosaic/DragHandle.tsx
502
+ var MOSAIC_DRAG_HANDLE_NAME = "Mosaic.DragHandle";
503
+ var REACT_UI_TRANSLATION_KEY = "@dxos/react-ui";
504
+ /**
505
+ * Affordance that initiates dragging the enclosing {@link MosaicTile}. Renders a button and
506
+ * registers itself with the tile (via context), so consumers only need to drop it inside a
507
+ * `Mosaic.Tile` — no `dragHandle` ref plumbing required.
508
+ */
509
+ var MosaicDragHandle = forwardRef(({ icon = "ph--dots-six-vertical--regular", label, variant = "ghost", classNames, testId, children }, forwardedRef) => {
510
+ const { t } = useTranslation(REACT_UI_TRANSLATION_KEY);
511
+ const { setDragHandle } = useMosaicTileContext(MOSAIC_DRAG_HANDLE_NAME);
512
+ const ref = composeRefs(forwardedRef, setDragHandle);
513
+ const testIdProps = testId ? { "data-testid": testId } : {};
514
+ if (children) return /* @__PURE__ */ jsxs(Button, {
515
+ ref,
516
+ variant,
517
+ tabIndex: -1,
518
+ classNames: ["grid place-items-center cursor-grab", classNames],
519
+ ...testIdProps,
520
+ children: [children, /* @__PURE__ */ jsx("span", {
521
+ className: "sr-only",
522
+ children: label ?? t("toolbar-drag-handle.label")
523
+ })]
524
+ });
525
+ return /* @__PURE__ */ jsx(IconButton, {
526
+ ref,
527
+ iconOnly: true,
528
+ noTooltip: true,
529
+ tabIndex: -1,
530
+ variant,
531
+ icon,
532
+ label: label ?? t("toolbar-drag-handle.label"),
533
+ classNames: ["cursor-grab", classNames],
534
+ ...testIdProps
535
+ });
536
+ });
537
+ MosaicDragHandle.displayName = MOSAIC_DRAG_HANDLE_NAME;
538
+ //#endregion
539
+ //#region src/components/Mosaic/Placeholder.tsx
540
+ var MOSAIC_PLACEHOLDER_NAME = "Mosaic.Placeholder";
541
+ var MOSAIC_PLACEHOLDER_ORIENTATION_ATTR = "mosaic-placeholder-orientation";
542
+ var MOSAIC_PLACEHOLDER_STATE_ATTR = "mosaic-placeholder-state";
543
+ var MOSAIC_PLACEHOLDER_LOCATION_ATTR = "mosaic-placeholder-location";
544
+ var MosaicPlaceholder = ({ classNames, children, asChild, orientation = "vertical", location }) => {
545
+ const rootRef = useRef(null);
546
+ const Comp = asChild ? Slot : Primitive.div;
547
+ const { id: containerId, eventHandler, scrolling, activeLocation, setActiveLocation } = useMosaicContainerContext(MOSAIC_PLACEHOLDER_NAME);
548
+ const data = useMemo(() => ({
549
+ type: "placeholder",
550
+ containerId,
551
+ location
552
+ }), [containerId, location]);
553
+ useLayoutEffect(() => {
554
+ const root = rootRef.current;
555
+ if (!root || scrolling) return;
556
+ return dropTargetForElements({
557
+ element: root,
558
+ getData: () => data,
559
+ canDrop: ({ source }) => {
560
+ const data = getSourceData(source);
561
+ return data && eventHandler.canDrop?.({ source: data }) || false;
562
+ },
563
+ getDropEffect: () => "move",
564
+ onDragEnter: () => {
565
+ setActiveLocation(data.location);
566
+ },
567
+ onDragLeave: () => {
568
+ setActiveLocation(void 0);
569
+ },
570
+ onDrop: () => {
571
+ setActiveLocation(void 0);
572
+ }
573
+ });
574
+ }, [
575
+ rootRef,
576
+ data,
577
+ scrolling,
578
+ setActiveLocation
579
+ ]);
580
+ return /* @__PURE__ */ jsx(Comp, {
581
+ [`data-${MOSAIC_PLACEHOLDER_ORIENTATION_ATTR}`]: orientation,
582
+ [`data-${MOSAIC_PLACEHOLDER_STATE_ATTR}`]: data.location === activeLocation ? "active" : "idle",
583
+ [`data-${MOSAIC_PLACEHOLDER_LOCATION_ATTR}`]: String(location),
584
+ className: mx("relative", classNames),
585
+ ref: rootRef,
586
+ children
587
+ });
588
+ };
589
+ MosaicPlaceholder.displayName = MOSAIC_PLACEHOLDER_NAME;
590
+ var DROP_INDICATOR_NAME = "Mosaic.DropIndicator";
591
+ var MosaicDropIndicator = (props) => {
592
+ const { state } = useMosaicTileContext(DROP_INDICATOR_NAME);
593
+ return state.type === "target" && state.closestEdge ? /* @__PURE__ */ jsx(DropIndicator, {
594
+ ...props,
595
+ edge: state.closestEdge
596
+ }) : null;
597
+ };
598
+ MosaicDropIndicator.displayName = DROP_INDICATOR_NAME;
599
+ //#endregion
600
+ //#region src/components/Mosaic/ResizeHandle.tsx
601
+ var MOSAIC_RESIZE_HANDLE_NAME = "Mosaic.ResizeHandle";
602
+ var MIN_WIDTH = 20;
603
+ var MIN_HEIGHT = 3;
604
+ var DEFAULT_EXTRINSIC_SIZE = 50;
605
+ /**
606
+ * Edge affordance that resizes the enclosing {@link MosaicTile}. The axis tracks the container
607
+ * orientation (width when horizontal, height when vertical); the tile must be given a `size` so the
608
+ * dragged extent is reflected and persisted via its `onSizeChange`. Resize bounds default to the
609
+ * tile's `minSize`/`maxSize` and can be overridden per handle.
610
+ */
611
+ var MosaicResizeHandle = ({ minSize, maxSize, fallbackSize = DEFAULT_EXTRINSIC_SIZE }) => {
612
+ const { orientation = "vertical" } = useMosaicContainerContext(MOSAIC_RESIZE_HANDLE_NAME);
613
+ const { size, setSize, minSize: tileMinSize, maxSize: tileMaxSize } = useMosaicTileContext(MOSAIC_RESIZE_HANDLE_NAME);
614
+ const horizontal = orientation === "horizontal";
615
+ return /* @__PURE__ */ jsx(ResizeHandle, {
616
+ side: horizontal ? "inline-end" : "block-end",
617
+ fallbackSize,
618
+ minSize: minSize ?? tileMinSize ?? (horizontal ? MIN_WIDTH : MIN_HEIGHT),
619
+ maxSize: maxSize ?? tileMaxSize,
620
+ size,
621
+ onSizeChange: setSize
622
+ });
623
+ };
624
+ MosaicResizeHandle.displayName = MOSAIC_RESIZE_HANDLE_NAME;
625
+ //#endregion
626
+ //#region src/components/Mosaic/Stack.tsx
627
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-mosaic/src/components/Mosaic/Stack.tsx";
628
+ var MOSAIC_STACK_NAME = "MosaicStack";
629
+ /**
630
+ * Linear layout of Mosaic tiles.
631
+ * NOTE: This is a low-level component and should be wrapped by a scrollable container.
632
+ */
633
+ var MosaicStackInner = composable(({ orientation: orientationProp = "vertical", getId, items, scrollIntoView = true, Tile, draggable = true, debug, ...props }, forwardedRef) => {
634
+ const { id, orientation = orientationProp, dragging, currentId, selectedIds, registerScrollTo, setActiveLocation } = useMosaicContainerContext(MOSAIC_STACK_NAME);
635
+ const visibleItems = useVisibleItems({
636
+ id,
637
+ items,
638
+ dragging: dragging?.source.data,
639
+ getId
640
+ });
641
+ invariant(orientation === "vertical" || orientation === "horizontal", `Invalid orientation: ${orientation}`, {
642
+ "~LogMeta": "~LogMeta",
643
+ F: __dxlog_file,
644
+ L: 95,
645
+ S: void 0,
646
+ A: ["orientation === 'vertical' || orientation === 'horizontal'", "`Invalid orientation: ${orientation}`"]
647
+ });
648
+ const draggingSource = dragging?.source.data;
649
+ const sourceIndex = useMemo(() => {
650
+ if (!items || !draggingSource || draggingSource.containerId !== id) return -1;
651
+ return items.findIndex((item) => getId(item) === draggingSource.id);
652
+ }, [
653
+ items,
654
+ draggingSource,
655
+ id,
656
+ getId
657
+ ]);
658
+ useLayoutEffect(() => {
659
+ if (sourceIndex >= 0) setActiveLocation(sourceIndex + .5);
660
+ }, [sourceIndex, setActiveLocation]);
661
+ const rootRef = useRef(null);
662
+ const scrollToId = useCallback((targetId) => {
663
+ if (!scrollIntoView) return;
664
+ (rootRef.current?.querySelector(`[data-object-id="${CSS.escape(targetId)}"]`))?.scrollIntoView({
665
+ block: "nearest",
666
+ behavior: "smooth"
667
+ });
668
+ }, [scrollIntoView]);
669
+ useEffect(() => {
670
+ registerScrollTo(scrollToId);
671
+ return () => registerScrollTo(void 0);
672
+ }, [registerScrollTo, scrollToId]);
673
+ const composedRef = useComposedRefs(rootRef, forwardedRef);
674
+ return /* @__PURE__ */ jsxs("div", {
675
+ ...composableProps(props, {
676
+ role: "list",
677
+ classNames: [
678
+ "flex",
679
+ orientation === "horizontal" && "h-full [&>*]:shrink-0",
680
+ orientation === "vertical" && "flex-col"
681
+ ]
682
+ }),
683
+ ref: composedRef,
684
+ children: [draggable && /* @__PURE__ */ jsx(InternalPlaceholder, {
685
+ orientation,
686
+ location: .5
687
+ }), visibleItems?.map((item, index) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tile, {
688
+ id: getId(item),
689
+ data: item,
690
+ location: index + 1,
691
+ draggable,
692
+ current: getId(item) === currentId,
693
+ selected: selectedIds?.has(getId(item)),
694
+ debug
695
+ }), draggable && /* @__PURE__ */ jsx(InternalPlaceholder, {
696
+ orientation,
697
+ location: index + 1.5
698
+ })] }, getId(item)))]
699
+ });
700
+ });
701
+ MosaicStackInner.displayName = MOSAIC_STACK_NAME;
702
+ var MosaicStack = MosaicStackInner;
703
+ var MOSAIC_VIRTUAL_STACK_NAME = "MosaicVirtualStack";
704
+ var MosaicVirtualStackInner = forwardRef(({ orientation = "vertical", getId, items, scrollIntoView = true, Tile, estimateSize, getScrollElement, overscan = 8, gap, pagination, paginationThreshold, onChange, draggable = true, debug, ...props }, forwardedRef) => {
705
+ invariant(Tile, void 0, {
706
+ "~LogMeta": "~LogMeta",
707
+ F: __dxlog_file,
708
+ L: 210,
709
+ S: void 0,
710
+ A: ["Tile", ""]
711
+ });
712
+ invariant(!pagination || !draggable, "pagination requires draggable={false}", {
713
+ "~LogMeta": "~LogMeta",
714
+ F: __dxlog_file,
715
+ L: 211,
716
+ S: void 0,
717
+ A: ["!pagination || !draggable", "'pagination requires draggable={false}'"]
718
+ });
719
+ const { id, dragging, currentId, selectedIds, registerScrollTo } = useMosaicContainerContext(MOSAIC_VIRTUAL_STACK_NAME);
720
+ const visibleItems = useVisibleItems({
721
+ id,
722
+ items,
723
+ dragging: dragging?.source.data,
724
+ getId
725
+ });
726
+ const wrappedEstimateSize = draggable ? (index) => index % 2 === 0 ? 0 : estimateSize(Math.floor(index / 2)) : estimateSize;
727
+ const { onChange: handlePaginationChange, leadingSpace } = useVirtualizerPagination({
728
+ items: visibleItems,
729
+ getId,
730
+ pagination,
731
+ threshold: paginationThreshold
732
+ });
733
+ const handleChange = useCallback((nextVirtualizer, sync) => {
734
+ handlePaginationChange(nextVirtualizer);
735
+ onChange?.(nextVirtualizer, sync);
736
+ }, [handlePaginationChange, onChange]);
737
+ const virtualizer = useVirtualizer({
738
+ count: draggable ? visibleItems.length * 2 + 1 : visibleItems.length,
739
+ estimateSize: wrappedEstimateSize,
740
+ gap,
741
+ paddingStart: (gap ?? 0) + leadingSpace,
742
+ paddingEnd: gap,
743
+ getItemKey: draggable ? (index) => index % 2 === 0 ? `ph-${index}` : getId(visibleItems[Math.floor(index / 2)]) : (index) => getId(visibleItems[index]),
744
+ getScrollElement,
745
+ overscan,
746
+ onChange: handleChange
747
+ });
748
+ const scrollToId = useCallback((targetId) => {
749
+ const itemIndex = visibleItems.findIndex((item) => getId(item) === targetId);
750
+ if (itemIndex >= 0) {
751
+ if (!scrollIntoView) return;
752
+ const virtualIndex = draggable ? itemIndex * 2 + 1 : itemIndex;
753
+ const [offset] = virtualizer.getOffsetForIndex(virtualIndex, "start") ?? [];
754
+ if (offset != null) virtualizer.scrollToOffset(Math.max(0, offset - (gap ?? 0)), { behavior: "smooth" });
755
+ }
756
+ }, [
757
+ visibleItems,
758
+ getId,
759
+ draggable,
760
+ virtualizer,
761
+ scrollIntoView,
762
+ gap
763
+ ]);
764
+ useEffect(() => {
765
+ registerScrollTo(scrollToId);
766
+ return () => registerScrollTo(void 0);
767
+ }, [registerScrollTo, scrollToId]);
768
+ const virtualItems = virtualizer.getVirtualItems();
769
+ const getData = (index) => {
770
+ if (draggable) if (index % 2 === 0) return { location: Math.floor(index / 2) + .5 };
771
+ else return {
772
+ data: visibleItems[Math.floor(index / 2)],
773
+ location: Math.floor(index / 2) + 1
774
+ };
775
+ else return {
776
+ data: visibleItems[index],
777
+ location: index + 1
778
+ };
779
+ };
780
+ return /* @__PURE__ */ jsx("div", {
781
+ ...composableProps(props, {
782
+ role: "list",
783
+ classNames: [
784
+ "flex shrink-0",
785
+ orientation === "horizontal" && "h-full [&>*]:shrink-0",
786
+ orientation === "vertical" && "flex-col"
787
+ ]
788
+ }),
789
+ style: {
790
+ position: "relative",
791
+ ...orientation === "vertical" ? {
792
+ width: "100%",
793
+ height: virtualizer.getTotalSize()
794
+ } : {
795
+ width: virtualizer.getTotalSize(),
796
+ height: "100%"
797
+ }
798
+ },
799
+ ref: forwardedRef,
800
+ children: virtualItems.map(({ key, index, start }) => {
801
+ const { data, location } = getData(index);
802
+ return /* @__PURE__ */ jsx("div", {
803
+ "data-index": index,
804
+ style: {
805
+ position: "absolute",
806
+ top: 0,
807
+ left: 0,
808
+ ...orientation === "vertical" ? {
809
+ width: "100%",
810
+ transform: `translateY(${start}px)`
811
+ } : {
812
+ height: "100%",
813
+ transform: `translateX(${start}px)`
814
+ }
815
+ },
816
+ ref: virtualizer.measureElement,
817
+ children: data ? /* @__PURE__ */ jsx(Tile, {
818
+ id: getId(data),
819
+ data,
820
+ location,
821
+ draggable,
822
+ current: getId(data) === currentId,
823
+ selected: selectedIds?.has(getId(data)),
824
+ debug
825
+ }) : /* @__PURE__ */ jsx(InternalPlaceholder, {
826
+ orientation,
827
+ location
828
+ })
829
+ }, key);
830
+ })
831
+ });
832
+ });
833
+ MosaicVirtualStackInner.displayName = MOSAIC_VIRTUAL_STACK_NAME;
834
+ var MosaicVirtualStack = MosaicVirtualStackInner;
835
+ MosaicVirtualStack[Symbol.for("dxos.composable")] = true;
836
+ var InternalPlaceholder = (props) => {
837
+ return /* @__PURE__ */ jsx(MosaicPlaceholder, {
838
+ ...props,
839
+ classNames: styles.placeholder.root,
840
+ children: /* @__PURE__ */ jsx("div", { className: mx("flex h-full bg-base-surface border border-dashed border-separator rounded-xs", styles.placeholder.content) })
841
+ });
842
+ };
843
+ InternalPlaceholder.displayName = "InternalPlaceholder";
844
+ //#endregion
845
+ //#region src/components/Mosaic/Tile.tsx
846
+ var MOSAIC_TILE_STATE_ATTR = "mosaic-tile-state";
847
+ var MosaicTile = slottable(({ children, asChild, dragHandle: dragHandleProp, allowedEdges: allowedEdgesProp, location, id, data: dataProp, draggable: draggableProp, current, selected, size: sizeProp, onSizeChange, minSize, maxSize, debug: _, ...props }, forwardedRef) => {
848
+ const Comp = asChild ? Slot : Primitive.div;
849
+ const rootRef = useRef(null);
850
+ const composedRef = composeRefs(rootRef, forwardedRef);
851
+ const { id: containerId, eventHandler, orientation, scrolling, setActiveLocation } = useMosaicContainerContext(MOSAIC_TILE_NAME);
852
+ const [state, setState] = useState({ type: "idle" });
853
+ const [registeredDragHandle, setRegisteredDragHandle] = useState(null);
854
+ const dragHandle = dragHandleProp ?? registeredDragHandle;
855
+ const [size, setInternalSize] = useState(sizeProp);
856
+ const setSize = useCallback((nextSize, commit) => {
857
+ setInternalSize(nextSize);
858
+ if (commit) onSizeChange?.(nextSize);
859
+ }, [onSizeChange]);
860
+ useEffect(() => {
861
+ setInternalSize(sizeProp);
862
+ }, [sizeProp]);
863
+ const allowedEdges = useMemo(() => allowedEdgesProp || (orientation === "vertical" ? ["top", "bottom"] : ["left", "right"]), [allowedEdgesProp, orientation]);
864
+ const data = useMemo(() => ({
865
+ type: "tile",
866
+ id,
867
+ containerId,
868
+ data: dataProp,
869
+ location
870
+ }), [
871
+ containerId,
872
+ location,
873
+ dataProp
874
+ ]);
875
+ useLayoutEffect(() => {
876
+ const root = rootRef.current;
877
+ if (!draggableProp || !root || !containerId || scrolling) return;
878
+ const handleChange = ({ self, source }) => {
879
+ if (source.data.id !== dataProp.id) {
880
+ const closestEdge = extractClosestEdge(self.data);
881
+ const location = data.location + (closestEdge === "top" || closestEdge === "left" ? -.5 : .5);
882
+ setActiveLocation(location);
883
+ setState({
884
+ type: "target",
885
+ closestEdge
886
+ });
887
+ }
888
+ };
889
+ const handleNativeDragStart = (event) => {
890
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
891
+ };
892
+ root.addEventListener("dragstart", handleNativeDragStart);
893
+ return combine(() => root.removeEventListener("dragstart", handleNativeDragStart), draggable({
894
+ element: root,
895
+ dragHandle: dragHandle && state.type !== "preview" ? dragHandle : void 0,
896
+ canDrag: () => true,
897
+ getInitialData: () => data,
898
+ onGenerateDragPreview: ({ location, nativeSetDragImage }) => {
899
+ setCustomNativeDragPreview({
900
+ nativeSetDragImage,
901
+ getOffset: preserveOffsetOnSource({
902
+ element: root,
903
+ input: location.current.input
904
+ }),
905
+ render: ({ container }) => {
906
+ data.bounds = root.getBoundingClientRect();
907
+ setState({
908
+ type: "preview",
909
+ container,
910
+ rect: root.getBoundingClientRect()
911
+ });
912
+ return () => setState({ type: "dragging" });
913
+ }
914
+ });
915
+ },
916
+ onDragStart: () => {
917
+ setState({ type: "dragging" });
918
+ },
919
+ onDrop: () => {
920
+ setState({ type: "idle" });
921
+ }
922
+ }), dropTargetForElements({
923
+ element: root,
924
+ getData: ({ input, element }) => attachClosestEdge(data, {
925
+ input,
926
+ element,
927
+ allowedEdges
928
+ }),
929
+ canDrop: ({ source }) => {
930
+ const data = getSourceData(source);
931
+ return data && eventHandler.canDrop?.({ source: data }) || false;
932
+ },
933
+ getDropEffect: () => "move",
934
+ onDragEnter: ({ self, source }) => {
935
+ handleChange({
936
+ self,
937
+ source
938
+ });
939
+ },
940
+ onDragLeave: () => {
941
+ setState({ type: "idle" });
942
+ setActiveLocation(void 0);
943
+ },
944
+ onDrag: ({ self, source }) => {
945
+ handleChange({
946
+ self,
947
+ source
948
+ });
949
+ },
950
+ onDrop: () => {
951
+ setState({ type: "idle" });
952
+ setActiveLocation(void 0);
953
+ }
954
+ }));
955
+ }, [
956
+ rootRef,
957
+ forwardedRef,
958
+ containerId,
959
+ dragHandle,
960
+ eventHandler,
961
+ data,
962
+ scrolling,
963
+ allowedEdges,
964
+ setActiveLocation
965
+ ]);
966
+ const { className, ...rest } = composableProps(props, { classNames: "relative outline-none" });
967
+ const vertical = orientation === "vertical";
968
+ const sized = size != null;
969
+ const bounded = sized || minSize != null || maxSize != null;
970
+ const sizeStyles = {
971
+ ...sized && sizeStyle(size, vertical ? "vertical" : "horizontal"),
972
+ ...minSize != null && { [vertical ? "minBlockSize" : "minInlineSize"]: `${minSize}rem` },
973
+ ...maxSize != null && { [vertical ? "maxBlockSize" : "maxInlineSize"]: `${maxSize}rem` }
974
+ };
975
+ return /* @__PURE__ */ jsxs(MosaicTileContextProvider, {
976
+ state,
977
+ setDragHandle: setRegisteredDragHandle,
978
+ size,
979
+ setSize,
980
+ minSize,
981
+ maxSize,
982
+ children: [/* @__PURE__ */ jsx(Comp, {
983
+ ...rest,
984
+ ...bounded && resizeAttributes,
985
+ ...bounded && { style: {
986
+ ...rest.style,
987
+ ...sizeStyles
988
+ } },
989
+ "data-object-id": id,
990
+ [`data-${MOSAIC_TILE_STATE_ATTR}`]: state.type,
991
+ role: "listitem",
992
+ "aria-current": current ? "true" : void 0,
993
+ "aria-selected": selected ? "true" : void 0,
994
+ className,
995
+ ref: composedRef,
996
+ children
997
+ }), state.type === "preview" && createPortal(/* @__PURE__ */ jsx(Comp, {
998
+ [`data-${MOSAIC_TILE_STATE_ATTR}`]: state.type,
999
+ className,
1000
+ style: {
1001
+ width: `${state.rect.width}px`,
1002
+ height: `${state.rect.height}px`
1003
+ },
1004
+ children
1005
+ }), state.container)]
1006
+ });
1007
+ });
1008
+ MosaicTile.displayName = MOSAIC_TILE_NAME;
1009
+ //#endregion
1010
+ //#region src/components/Mosaic/Mosaic.ts
1011
+ var Mosaic = {
1012
+ Container: MosaicContainer,
1013
+ Tile: MosaicTile,
1014
+ DragHandle: MosaicDragHandle,
1015
+ ResizeHandle: MosaicResizeHandle,
1016
+ Placeholder: MosaicPlaceholder,
1017
+ DropIndicator: MosaicDropIndicator,
1018
+ Stack: MosaicStack,
1019
+ VirtualStack: MosaicVirtualStack
1020
+ };
1021
+ //#endregion
1022
+ export { Focus as a, useVirtualizerPagination as c, styles as i, useMosaicTileContext as n, useFocus as o, useMosaicContainerContext as r, useVisibleItems as s, Mosaic as t };
1023
+
1024
+ //# sourceMappingURL=chunk-Mosaic.mjs.map