@dxos/react-ui-mosaic 0.8.4-main.bc674ce → 0.8.4-main.c85a9c8dae
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.
- package/dist/lib/browser/chunk-7S4PIFKU.mjs +953 -0
- package/dist/lib/browser/chunk-7S4PIFKU.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +596 -70
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +138 -0
- package/dist/lib/browser/playwright/index.mjs.map +7 -0
- package/dist/lib/browser/testing/index.mjs +61 -232
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/chunk-MK6KU66S.mjs +955 -0
- package/dist/lib/node-esm/chunk-MK6KU66S.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +596 -70
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/playwright/index.mjs +140 -0
- package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
- package/dist/lib/node-esm/testing/index.mjs +61 -232
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/components/Board/Board.d.ts +81 -0
- package/dist/types/src/components/Board/Board.d.ts.map +1 -0
- package/dist/types/src/components/{Mosaic/Mosaic.stories.d.ts → Board/Board.stories.d.ts} +15 -1
- package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -0
- package/dist/types/src/components/Board/Column.d.ts +40 -0
- package/dist/types/src/components/Board/Column.d.ts.map +1 -0
- package/dist/types/src/components/Board/Item.d.ts +9 -0
- package/dist/types/src/components/Board/Item.d.ts.map +1 -0
- package/dist/types/src/components/Board/index.d.ts +2 -0
- package/dist/types/src/components/Board/index.d.ts.map +1 -0
- package/dist/types/src/components/Focus/Focus.d.ts +5 -4
- package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
- package/dist/types/src/components/Focus/index.d.ts +0 -1
- package/dist/types/src/components/Focus/index.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Container.d.ts +42 -0
- package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Mosaic.d.ts +35 -118
- package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Placeholder.d.ts +21 -0
- package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Root.d.ts +36 -0
- package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Stack.d.ts +23 -0
- package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -0
- package/dist/types/src/components/{Stack → Mosaic}/Stack.stories.d.ts +2 -2
- package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Tile.d.ts +48 -0
- package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/types.d.ts +9 -2
- package/dist/types/src/components/Mosaic/types.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -4
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts +15 -0
- package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEventHandlerAdapter.d.ts +1 -1
- package/dist/types/src/hooks/useEventHandlerAdapter.d.ts.map +1 -1
- package/dist/types/src/hooks/useVisibleItems.d.ts +2 -2
- package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
- package/dist/types/src/playwright/board-manager.d.ts +45 -0
- package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
- package/dist/types/src/playwright/index.d.ts +9 -0
- package/dist/types/src/playwright/index.d.ts.map +1 -0
- package/dist/types/src/playwright/playwright.config.d.ts +3 -0
- package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
- package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
- package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
- package/dist/types/src/testing/CardContainer.d.ts +5 -4
- package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
- package/dist/types/src/testing/DefaultStackTile.d.ts +2 -2
- package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/types.d.ts +21 -0
- package/dist/types/src/testing/types.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -28
- package/src/components/Board/Board.stories.tsx +273 -0
- package/src/components/Board/Board.tsx +191 -0
- package/src/components/Board/Column.tsx +300 -0
- package/src/components/Board/Item.tsx +116 -0
- package/src/components/{Layout → Board}/index.ts +1 -1
- package/src/components/Focus/Focus.tsx +26 -17
- package/src/components/Focus/index.ts +0 -2
- package/src/components/Mosaic/Container.tsx +289 -0
- package/src/components/Mosaic/Mosaic.tsx +35 -913
- package/src/components/Mosaic/Placeholder.tsx +133 -0
- package/src/components/Mosaic/Root.tsx +292 -0
- package/src/components/{Stack → Mosaic}/Stack.stories.tsx +30 -26
- package/src/components/Mosaic/Stack.tsx +246 -0
- package/src/components/Mosaic/Tile.tsx +242 -0
- package/src/components/Mosaic/extension.ts +1 -1
- package/src/components/Mosaic/styles.ts +9 -8
- package/src/components/Mosaic/types.ts +15 -3
- package/src/components/index.ts +1 -4
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useContainerDebug.tsx +4 -2
- package/src/hooks/useDefaultColumnEventHandler.ts +35 -0
- package/src/hooks/useEventHandlerAdapter.ts +15 -6
- package/src/hooks/useVisibleItems.ts +7 -2
- package/src/playwright/board-manager.ts +154 -0
- package/src/playwright/index.ts +13 -0
- package/src/playwright/playwright.config.ts +17 -0
- package/src/playwright/smoke.spec.ts +138 -0
- package/src/testing/CardContainer.tsx +30 -18
- package/src/testing/DefaultStackTile.tsx +27 -16
- package/src/testing/index.ts +1 -1
- package/src/testing/types.ts +38 -0
- package/src/translations.ts +5 -1
- package/dist/lib/browser/chunk-UMBPEG4F.mjs +0 -1637
- package/dist/lib/browser/chunk-UMBPEG4F.mjs.map +0 -7
- package/dist/lib/browser/index.css +0 -13
- package/dist/lib/browser/index.css.map +0 -7
- package/dist/lib/browser/testing/index.css +0 -13
- package/dist/lib/browser/testing/index.css.map +0 -7
- package/dist/lib/node-esm/chunk-X6GV7SMK.mjs +0 -1639
- package/dist/lib/node-esm/chunk-X6GV7SMK.mjs.map +0 -7
- package/dist/lib/node-esm/index.css +0 -13
- package/dist/lib/node-esm/index.css.map +0 -7
- package/dist/lib/node-esm/testing/index.css +0 -13
- package/dist/lib/node-esm/testing/index.css.map +0 -7
- package/dist/types/src/components/Card/Card.d.ts +0 -121
- package/dist/types/src/components/Card/Card.d.ts.map +0 -1
- package/dist/types/src/components/Card/Card.stories.d.ts +0 -21
- package/dist/types/src/components/Card/Card.stories.d.ts.map +0 -1
- package/dist/types/src/components/Card/CardDragPreview.d.ts +0 -6
- package/dist/types/src/components/Card/CardDragPreview.d.ts.map +0 -1
- package/dist/types/src/components/Card/index.d.ts +0 -4
- package/dist/types/src/components/Card/index.d.ts.map +0 -1
- package/dist/types/src/components/Card/styles.d.ts +0 -13
- package/dist/types/src/components/Card/styles.d.ts.map +0 -1
- package/dist/types/src/components/Focus/styles.d.ts +0 -6
- package/dist/types/src/components/Focus/styles.d.ts.map +0 -1
- package/dist/types/src/components/Image/Image.d.ts +0 -14
- package/dist/types/src/components/Image/Image.d.ts.map +0 -1
- package/dist/types/src/components/Image/Image.stories.d.ts +0 -33
- package/dist/types/src/components/Image/Image.stories.d.ts.map +0 -1
- package/dist/types/src/components/Image/index.d.ts +0 -2
- package/dist/types/src/components/Image/index.d.ts.map +0 -1
- package/dist/types/src/components/Layout/Layout.d.ts +0 -33
- package/dist/types/src/components/Layout/Layout.d.ts.map +0 -1
- package/dist/types/src/components/Layout/Layout.stories.d.ts +0 -10
- package/dist/types/src/components/Layout/Layout.stories.d.ts.map +0 -1
- package/dist/types/src/components/Layout/index.d.ts +0 -2
- package/dist/types/src/components/Layout/index.d.ts.map +0 -1
- package/dist/types/src/components/Mosaic/Mosaic.stories.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/Scrollable.d.ts +0 -24
- package/dist/types/src/components/Scrollable/Scrollable.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts +0 -10
- package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/index.d.ts +0 -2
- package/dist/types/src/components/Scrollable/index.d.ts.map +0 -1
- package/dist/types/src/components/Stack/Stack.d.ts +0 -22
- package/dist/types/src/components/Stack/Stack.d.ts.map +0 -1
- package/dist/types/src/components/Stack/Stack.stories.d.ts.map +0 -1
- package/dist/types/src/components/Stack/index.d.ts +0 -2
- package/dist/types/src/components/Stack/index.d.ts.map +0 -1
- package/dist/types/src/testing/Board.d.ts +0 -45
- package/dist/types/src/testing/Board.d.ts.map +0 -1
- package/dist/types/src/testing/Slot.stories.d.ts +0 -14
- package/dist/types/src/testing/Slot.stories.d.ts.map +0 -1
- package/dist/types/src/testing/Virtualizer.stories.d.ts +0 -12
- package/dist/types/src/testing/Virtualizer.stories.d.ts.map +0 -1
- package/src/components/Card/Card.stories.tsx +0 -148
- package/src/components/Card/Card.tsx +0 -458
- package/src/components/Card/CardDragPreview.tsx +0 -23
- package/src/components/Card/index.ts +0 -8
- package/src/components/Card/styles.ts +0 -23
- package/src/components/Focus/styles.ts +0 -24
- package/src/components/Image/Image.stories.tsx +0 -86
- package/src/components/Image/Image.tsx +0 -223
- package/src/components/Image/index.ts +0 -5
- package/src/components/Layout/Layout.stories.tsx +0 -59
- package/src/components/Layout/Layout.tsx +0 -108
- package/src/components/Mosaic/Mosaic.stories.tsx +0 -113
- package/src/components/Scrollable/Scrollable.stories.tsx +0 -59
- package/src/components/Scrollable/Scrollable.tsx +0 -88
- package/src/components/Scrollable/index.ts +0 -5
- package/src/components/Scrollable/scrollable.css +0 -20
- package/src/components/Stack/Stack.tsx +0 -210
- package/src/components/Stack/index.ts +0 -5
- package/src/testing/Board.tsx +0 -278
- package/src/testing/Slot.stories.tsx +0 -101
- package/src/testing/Virtualizer.stories.tsx +0 -133
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ReactVirtualizerOptions, useVirtualizer } from '@tanstack/react-virtual';
|
|
6
|
+
import React, { type FC, Fragment, type ReactElement, type Ref, forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { type Axis, type ComposableProps } from '@dxos/react-ui';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
|
+
|
|
12
|
+
import { useVisibleItems } from '../../hooks';
|
|
13
|
+
|
|
14
|
+
import { useMosaicContainerContext } from './Container';
|
|
15
|
+
import { MosaicPlaceholder, type MosaicPlaceholderProps } from './Placeholder';
|
|
16
|
+
import { styles } from './styles';
|
|
17
|
+
import { type MosaicTileProps } from './Tile';
|
|
18
|
+
import { type GetId } from './types';
|
|
19
|
+
|
|
20
|
+
//
|
|
21
|
+
// Mosaic Drag-and-drop
|
|
22
|
+
//
|
|
23
|
+
// Drop targets:
|
|
24
|
+
// - Placeholders exist to allow gaps but prevent deadspace when dragging within a container.
|
|
25
|
+
// - When dragging over a Tile, the placeholder next to the closest edge is activated.
|
|
26
|
+
// - Placeholders expand when active; event handlers are disabled while the container is scrolling.
|
|
27
|
+
//
|
|
28
|
+
// [Container]
|
|
29
|
+
// - [Placeholder 0.5]
|
|
30
|
+
// - [Tile 1 ]
|
|
31
|
+
// - [Placeholder 1.5]
|
|
32
|
+
// - [Tile 2 ]
|
|
33
|
+
// - [Placeholder 2.5]
|
|
34
|
+
// - [Tile 3 ]
|
|
35
|
+
// - [Placeholder 3.5]
|
|
36
|
+
//
|
|
37
|
+
// Implementation Notes
|
|
38
|
+
// - We use [Radix composition](https://www.radix-ui.com/primitives/docs/guides/composition) to factor out composible aspects (e.g., Focus, Mosaic, etc.)
|
|
39
|
+
// - NOTE: Use Slottable only if needed to disambiguate; otherwise a suspected Radix bug causes compositional problems.
|
|
40
|
+
|
|
41
|
+
const MOSAIC_STACK_NAME = 'MosaicStack';
|
|
42
|
+
|
|
43
|
+
type MosaicStackTileComponent<TData = any> = FC<MosaicTileProps<TData>>;
|
|
44
|
+
|
|
45
|
+
type MosaicStackProps<TData = any> = ComposableProps<
|
|
46
|
+
{
|
|
47
|
+
Tile: MosaicStackTileComponent<TData>;
|
|
48
|
+
getId: GetId<TData>;
|
|
49
|
+
role?: string;
|
|
50
|
+
orientation?: Axis;
|
|
51
|
+
items?: readonly TData[];
|
|
52
|
+
} & Pick<MosaicTileProps<TData>, 'draggable' | 'debug'>
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Linear layout of Mosaic tiles.
|
|
57
|
+
* NOTE: This is a low-level component and should be wrapped by a scrollable container.
|
|
58
|
+
*/
|
|
59
|
+
const MosaicStackInner = forwardRef<HTMLDivElement, MosaicStackProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
classNames,
|
|
63
|
+
className,
|
|
64
|
+
role = 'list',
|
|
65
|
+
orientation: orientationProp = 'vertical',
|
|
66
|
+
draggable = true,
|
|
67
|
+
items,
|
|
68
|
+
getId,
|
|
69
|
+
Tile,
|
|
70
|
+
debug,
|
|
71
|
+
...props
|
|
72
|
+
},
|
|
73
|
+
forwardedRef,
|
|
74
|
+
) => {
|
|
75
|
+
invariant(Tile);
|
|
76
|
+
const { id, orientation = orientationProp, dragging } = useMosaicContainerContext(MOSAIC_STACK_NAME);
|
|
77
|
+
const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
|
|
78
|
+
invariant(orientation === 'vertical' || orientation === 'horizontal', `Invalid orientation: ${orientation}`);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
{...props}
|
|
83
|
+
role={role}
|
|
84
|
+
className={mx(
|
|
85
|
+
'flex',
|
|
86
|
+
orientation === 'horizontal' && 'h-full [&>*]:shrink-0',
|
|
87
|
+
orientation === 'vertical' && 'flex-col',
|
|
88
|
+
classNames,
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
ref={forwardedRef}
|
|
92
|
+
>
|
|
93
|
+
<InternalPlaceholder orientation={orientation} location={0.5} />
|
|
94
|
+
{visibleItems?.map((item, index) => (
|
|
95
|
+
<Fragment key={getId(item)}>
|
|
96
|
+
<Tile id={getId(item)} data={item} location={index + 1} draggable={draggable} debug={debug} />
|
|
97
|
+
<InternalPlaceholder orientation={orientation} location={index + 1.5} />
|
|
98
|
+
</Fragment>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
MosaicStackInner.displayName = MOSAIC_STACK_NAME;
|
|
106
|
+
|
|
107
|
+
const MosaicStack = MosaicStackInner as <TData = any>(
|
|
108
|
+
props: MosaicStackProps<TData> & { ref?: Ref<HTMLDivElement> },
|
|
109
|
+
) => ReactElement;
|
|
110
|
+
|
|
111
|
+
//
|
|
112
|
+
// VirtualStack
|
|
113
|
+
//
|
|
114
|
+
|
|
115
|
+
const MOSAIC_VIRTUAL_STACK_NAME = 'MosaicVirtualStack';
|
|
116
|
+
|
|
117
|
+
type MosaicVirtualStackProps<TData = any> = MosaicStackProps<TData> &
|
|
118
|
+
Pick<
|
|
119
|
+
ReactVirtualizerOptions<HTMLElement, HTMLDivElement>,
|
|
120
|
+
'estimateSize' | 'getScrollElement' | 'overscan' | 'onChange'
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackProps>(
|
|
124
|
+
(
|
|
125
|
+
{
|
|
126
|
+
classNames,
|
|
127
|
+
className,
|
|
128
|
+
role = 'list',
|
|
129
|
+
orientation = 'vertical',
|
|
130
|
+
items,
|
|
131
|
+
getId,
|
|
132
|
+
Tile,
|
|
133
|
+
estimateSize,
|
|
134
|
+
getScrollElement,
|
|
135
|
+
overscan = 8,
|
|
136
|
+
onChange,
|
|
137
|
+
debug,
|
|
138
|
+
...props
|
|
139
|
+
},
|
|
140
|
+
forwardedRef,
|
|
141
|
+
) => {
|
|
142
|
+
invariant(Tile);
|
|
143
|
+
const { id, dragging } = useMosaicContainerContext(MOSAIC_VIRTUAL_STACK_NAME);
|
|
144
|
+
const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
|
|
145
|
+
const virtualizer = useVirtualizer({
|
|
146
|
+
count: visibleItems.length * 2 + 1,
|
|
147
|
+
estimateSize,
|
|
148
|
+
getScrollElement,
|
|
149
|
+
overscan,
|
|
150
|
+
onChange,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
{...props}
|
|
158
|
+
role={role}
|
|
159
|
+
className={mx(
|
|
160
|
+
'flex',
|
|
161
|
+
orientation === 'horizontal' && 'h-full [&>*]:shrink-0',
|
|
162
|
+
orientation === 'vertical' && 'flex-col',
|
|
163
|
+
classNames,
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
style={{
|
|
167
|
+
position: 'relative',
|
|
168
|
+
...(orientation === 'vertical'
|
|
169
|
+
? {
|
|
170
|
+
width: '100%',
|
|
171
|
+
height: virtualizer.getTotalSize(),
|
|
172
|
+
}
|
|
173
|
+
: {
|
|
174
|
+
width: virtualizer.getTotalSize(),
|
|
175
|
+
height: '100%',
|
|
176
|
+
}),
|
|
177
|
+
}}
|
|
178
|
+
ref={forwardedRef}
|
|
179
|
+
>
|
|
180
|
+
{virtualItems.map((virtualItem) => {
|
|
181
|
+
const data = visibleItems![Math.floor(virtualItem.index / 2)];
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
key={virtualItem.key}
|
|
185
|
+
data-index={virtualItem.index}
|
|
186
|
+
style={{
|
|
187
|
+
position: 'absolute',
|
|
188
|
+
top: 0,
|
|
189
|
+
left: 0,
|
|
190
|
+
...(orientation === 'vertical'
|
|
191
|
+
? {
|
|
192
|
+
width: '100%',
|
|
193
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
height: '100%',
|
|
197
|
+
transform: `translateX(${virtualItem.start}px)`,
|
|
198
|
+
}),
|
|
199
|
+
}}
|
|
200
|
+
ref={virtualizer.measureElement}
|
|
201
|
+
>
|
|
202
|
+
{virtualItem.index % 2 === 0 ? (
|
|
203
|
+
<InternalPlaceholder orientation={orientation} location={Math.floor(virtualItem.index / 2) + 0.5} />
|
|
204
|
+
) : (
|
|
205
|
+
<Tile id={getId(data)} data={data} location={Math.floor(virtualItem.index / 2) + 1} debug={debug} />
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
MosaicVirtualStackInner.displayName = MOSAIC_VIRTUAL_STACK_NAME;
|
|
216
|
+
|
|
217
|
+
const MosaicVirtualStack = MosaicVirtualStackInner as <TData = any>(
|
|
218
|
+
props: MosaicVirtualStackProps<TData> & { ref?: Ref<HTMLDivElement> },
|
|
219
|
+
) => ReactElement;
|
|
220
|
+
|
|
221
|
+
//
|
|
222
|
+
// InternalPlaceholder
|
|
223
|
+
//
|
|
224
|
+
|
|
225
|
+
const InternalPlaceholder = (props: MosaicPlaceholderProps<number>) => {
|
|
226
|
+
return (
|
|
227
|
+
<MosaicPlaceholder {...props} classNames={styles.placeholder.root}>
|
|
228
|
+
<div
|
|
229
|
+
className={mx(
|
|
230
|
+
'flex h-full bg-base-surface border border-dashed border-separator rounded-xs',
|
|
231
|
+
styles.placeholder.content,
|
|
232
|
+
)}
|
|
233
|
+
/>
|
|
234
|
+
</MosaicPlaceholder>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
InternalPlaceholder.displayName = 'InternalPlaceholder';
|
|
239
|
+
|
|
240
|
+
//
|
|
241
|
+
// Exports
|
|
242
|
+
//
|
|
243
|
+
|
|
244
|
+
export { MosaicStack, MosaicVirtualStack };
|
|
245
|
+
|
|
246
|
+
export type { MosaicStackTileComponent, MosaicStackProps, MosaicVirtualStackProps };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
6
|
+
import {
|
|
7
|
+
type ElementDragPayload,
|
|
8
|
+
draggable,
|
|
9
|
+
dropTargetForElements,
|
|
10
|
+
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
11
|
+
import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
|
12
|
+
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
13
|
+
import { type DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types';
|
|
14
|
+
import {
|
|
15
|
+
type Edge,
|
|
16
|
+
attachClosestEdge,
|
|
17
|
+
extractClosestEdge,
|
|
18
|
+
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
19
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
20
|
+
import { createContext } from '@radix-ui/react-context';
|
|
21
|
+
import { Primitive } from '@radix-ui/react-primitive';
|
|
22
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
23
|
+
import React, {
|
|
24
|
+
type CSSProperties,
|
|
25
|
+
type PropsWithChildren,
|
|
26
|
+
forwardRef,
|
|
27
|
+
useLayoutEffect,
|
|
28
|
+
useMemo,
|
|
29
|
+
useRef,
|
|
30
|
+
useState,
|
|
31
|
+
} from 'react';
|
|
32
|
+
import { createPortal } from 'react-dom';
|
|
33
|
+
|
|
34
|
+
import { type ComposableProps } from '@dxos/react-ui';
|
|
35
|
+
import { mx } from '@dxos/ui-theme';
|
|
36
|
+
|
|
37
|
+
import { useMosaicContainerContext } from './Container';
|
|
38
|
+
import { type LocationType, type MosaicTileData, getSourceData } from './types';
|
|
39
|
+
|
|
40
|
+
//
|
|
41
|
+
// Tile
|
|
42
|
+
//
|
|
43
|
+
|
|
44
|
+
const MOSAIC_TILE_NAME = 'Mosaic.Tile';
|
|
45
|
+
|
|
46
|
+
type MosaicTileState =
|
|
47
|
+
| { type: 'idle' }
|
|
48
|
+
| { type: 'preview'; container: HTMLElement; rect: DOMRect }
|
|
49
|
+
| { type: 'dragging' }
|
|
50
|
+
| { type: 'target'; closestEdge: Edge | null };
|
|
51
|
+
|
|
52
|
+
type MosaicTileContextValue = {
|
|
53
|
+
state: MosaicTileState;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const [MosaicTileContextProvider, useMosaicTileContext] = createContext<MosaicTileContextValue>('MosaicTile');
|
|
57
|
+
|
|
58
|
+
// State attribute: data-[mosaic-tile-state=dragging]
|
|
59
|
+
const MOSAIC_TILE_STATE_ATTR = 'mosaic-tile-state';
|
|
60
|
+
|
|
61
|
+
type MosaicTileProps<TData = any, TLocation = LocationType> = ComposableProps<
|
|
62
|
+
PropsWithChildren<{
|
|
63
|
+
asChild?: boolean;
|
|
64
|
+
dragHandle?: HTMLElement | null;
|
|
65
|
+
allowedEdges?: Edge[];
|
|
66
|
+
id: string;
|
|
67
|
+
data: TData;
|
|
68
|
+
location: TLocation;
|
|
69
|
+
draggable?: boolean; // TODO(burdon): Not currently implemented.
|
|
70
|
+
debug?: boolean;
|
|
71
|
+
}>
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
const MosaicTile = forwardRef<HTMLDivElement, MosaicTileProps>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
classNames,
|
|
78
|
+
className,
|
|
79
|
+
children,
|
|
80
|
+
asChild,
|
|
81
|
+
dragHandle,
|
|
82
|
+
allowedEdges: allowedEdgesProp,
|
|
83
|
+
location,
|
|
84
|
+
id,
|
|
85
|
+
data: dataProp,
|
|
86
|
+
debug: _,
|
|
87
|
+
...props
|
|
88
|
+
}: MosaicTileProps,
|
|
89
|
+
forwardedRef,
|
|
90
|
+
) => {
|
|
91
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
92
|
+
const composedRef = composeRefs<HTMLDivElement>(rootRef, forwardedRef);
|
|
93
|
+
const Comp = asChild ? Slot : Primitive.div;
|
|
94
|
+
|
|
95
|
+
// State.
|
|
96
|
+
const {
|
|
97
|
+
id: containerId,
|
|
98
|
+
eventHandler,
|
|
99
|
+
orientation,
|
|
100
|
+
scrolling,
|
|
101
|
+
setActiveLocation,
|
|
102
|
+
} = useMosaicContainerContext(MOSAIC_TILE_NAME);
|
|
103
|
+
const [state, setState] = useState<MosaicTileState>({ type: 'idle' });
|
|
104
|
+
|
|
105
|
+
const allowedEdges = useMemo<Edge[]>(
|
|
106
|
+
() => allowedEdgesProp || (orientation === 'vertical' ? ['top', 'bottom'] : ['left', 'right']),
|
|
107
|
+
[allowedEdgesProp, orientation],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const data = useMemo<MosaicTileData>(
|
|
111
|
+
() =>
|
|
112
|
+
({
|
|
113
|
+
type: 'tile',
|
|
114
|
+
id,
|
|
115
|
+
containerId,
|
|
116
|
+
data: dataProp,
|
|
117
|
+
location,
|
|
118
|
+
}) satisfies MosaicTileData,
|
|
119
|
+
[containerId, location, dataProp],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
const root = rootRef.current;
|
|
124
|
+
if (!root || !containerId || scrolling) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handleChange = ({ self, source }: { self: DropTargetRecord; source: ElementDragPayload }) => {
|
|
129
|
+
if (source.data.id !== dataProp.id) {
|
|
130
|
+
const closestEdge = extractClosestEdge(self.data);
|
|
131
|
+
const location = data.location + (closestEdge === 'top' || closestEdge === 'left' ? -0.5 : 0.5);
|
|
132
|
+
setActiveLocation(location);
|
|
133
|
+
setState({ type: 'target', closestEdge });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return combine(
|
|
138
|
+
// Source.
|
|
139
|
+
draggable({
|
|
140
|
+
element: root,
|
|
141
|
+
dragHandle: dragHandle && state.type !== 'preview' ? dragHandle : undefined,
|
|
142
|
+
canDrag: () => true,
|
|
143
|
+
getInitialData: () => data,
|
|
144
|
+
onGenerateDragPreview: ({ location, nativeSetDragImage }) => {
|
|
145
|
+
setCustomNativeDragPreview({
|
|
146
|
+
nativeSetDragImage,
|
|
147
|
+
getOffset: preserveOffsetOnSource({
|
|
148
|
+
element: root,
|
|
149
|
+
input: location.current.input,
|
|
150
|
+
}),
|
|
151
|
+
render: ({ container }) => {
|
|
152
|
+
data.bounds = root.getBoundingClientRect();
|
|
153
|
+
setState({
|
|
154
|
+
type: 'preview',
|
|
155
|
+
container,
|
|
156
|
+
rect: root.getBoundingClientRect(),
|
|
157
|
+
});
|
|
158
|
+
return () => setState({ type: 'dragging' });
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
onDragStart: () => {
|
|
164
|
+
setState({ type: 'dragging' });
|
|
165
|
+
},
|
|
166
|
+
onDrop: () => {
|
|
167
|
+
setState({ type: 'idle' });
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// Target.
|
|
172
|
+
dropTargetForElements({
|
|
173
|
+
element: root,
|
|
174
|
+
getData: ({ input, element }) => attachClosestEdge(data, { input, element, allowedEdges }),
|
|
175
|
+
canDrop: ({ source }) => {
|
|
176
|
+
const data = getSourceData(source);
|
|
177
|
+
return (data && eventHandler.canDrop?.({ source: data })) || false;
|
|
178
|
+
},
|
|
179
|
+
onDragEnter: ({ self, source }) => {
|
|
180
|
+
handleChange({ self, source });
|
|
181
|
+
},
|
|
182
|
+
onDragLeave: () => {
|
|
183
|
+
setState({ type: 'idle' });
|
|
184
|
+
setActiveLocation(undefined);
|
|
185
|
+
},
|
|
186
|
+
onDrag: ({ self, source }) => {
|
|
187
|
+
handleChange({ self, source });
|
|
188
|
+
},
|
|
189
|
+
onDrop: () => {
|
|
190
|
+
setState({ type: 'idle' });
|
|
191
|
+
setActiveLocation(undefined);
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
}, [rootRef, dragHandle, eventHandler, data, scrolling, allowedEdges, setActiveLocation]);
|
|
196
|
+
|
|
197
|
+
// NOTE: Ensure no gaps between cells (prevent drop indicators flickering).
|
|
198
|
+
// NOTE: Ensure padding doesn't change position of cursor when dragging (no margins).
|
|
199
|
+
return (
|
|
200
|
+
<MosaicTileContextProvider state={state}>
|
|
201
|
+
<Comp
|
|
202
|
+
{...props}
|
|
203
|
+
{...{
|
|
204
|
+
[`data-${MOSAIC_TILE_STATE_ATTR}`]: state.type,
|
|
205
|
+
}}
|
|
206
|
+
role='listitem'
|
|
207
|
+
className={mx('relative', className, classNames)}
|
|
208
|
+
ref={composedRef}
|
|
209
|
+
>
|
|
210
|
+
{children}
|
|
211
|
+
</Comp>
|
|
212
|
+
|
|
213
|
+
{state.type === 'preview' &&
|
|
214
|
+
createPortal(
|
|
215
|
+
<Comp
|
|
216
|
+
{...{
|
|
217
|
+
// NOTE: Use to control appearance while dragging.
|
|
218
|
+
[`data-${MOSAIC_TILE_STATE_ATTR}`]: state.type,
|
|
219
|
+
}}
|
|
220
|
+
// TODO(burdon): Configure drop animation.
|
|
221
|
+
className={mx('relative', className, classNames)}
|
|
222
|
+
style={
|
|
223
|
+
{
|
|
224
|
+
width: `${state.rect.width}px`,
|
|
225
|
+
height: `${state.rect.height}px`,
|
|
226
|
+
} as CSSProperties
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
{children}
|
|
230
|
+
</Comp>,
|
|
231
|
+
state.container,
|
|
232
|
+
)}
|
|
233
|
+
</MosaicTileContextProvider>
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
MosaicTile.displayName = MOSAIC_TILE_NAME;
|
|
239
|
+
|
|
240
|
+
export { MosaicTile, useMosaicTileContext };
|
|
241
|
+
|
|
242
|
+
export type { MosaicTileProps, MosaicTileState };
|
|
@@ -123,7 +123,7 @@ const dropField = StateField.define<DecorationSet>({
|
|
|
123
123
|
toDOM() {
|
|
124
124
|
const dom = document.createElement('span');
|
|
125
125
|
dom.className = 'cm-drop-cursor';
|
|
126
|
-
dom.style.borderLeft = '2px solid var(--
|
|
126
|
+
dom.style.borderLeft = '2px solid var(--color-accent-surface)';
|
|
127
127
|
dom.style.display = 'inline-block';
|
|
128
128
|
dom.style.height = '1em';
|
|
129
129
|
dom.style.verticalAlign = 'middle';
|
|
@@ -11,10 +11,11 @@ export const styles = {
|
|
|
11
11
|
'group-data-[mosaic-debug="true"]:opacity-100',
|
|
12
12
|
'group-data-[mosaic-debug="true"]:bg-orange-500',
|
|
13
13
|
'data-[mosaic-placeholder-state=active]:opacity-100',
|
|
14
|
-
'data-[mosaic-placeholder-
|
|
15
|
-
'data-[mosaic-placeholder-
|
|
16
|
-
'data-[mosaic-placeholder-
|
|
17
|
-
'data-[mosaic-placeholder-
|
|
14
|
+
'data-[mosaic-placeholder-orientation=vertical]:snap-start', // TODO(burdon): Option.
|
|
15
|
+
'data-[mosaic-placeholder-orientation=vertical]:py-1',
|
|
16
|
+
'data-[mosaic-placeholder-orientation=vertical]:data-[mosaic-placeholder-state=active]:py-2.5',
|
|
17
|
+
'data-[mosaic-placeholder-orientation=horizontal]:px-1',
|
|
18
|
+
'data-[mosaic-placeholder-orientation=horizontal]:data-[mosaic-placeholder-state=active]:px-2.5',
|
|
18
19
|
],
|
|
19
20
|
content: [
|
|
20
21
|
'transition-all duration-0',
|
|
@@ -22,10 +23,10 @@ export const styles = {
|
|
|
22
23
|
// TODO(burdon): Flickers when start to drag since source is removed and then the placeholder shows up.
|
|
23
24
|
// - Use absolute positioning?
|
|
24
25
|
// 'group-data-[mosaic-placeholder-state=active]:duration-200',
|
|
25
|
-
'group-data-[mosaic-placeholder-
|
|
26
|
-
'group-data-[mosaic-placeholder-
|
|
27
|
-
'group-data-[mosaic-placeholder-
|
|
28
|
-
'group-data-[mosaic-placeholder-
|
|
26
|
+
'group-data-[mosaic-placeholder-orientation=vertical]:h-0',
|
|
27
|
+
'group-data-[mosaic-placeholder-orientation=vertical]:group-data-[mosaic-placeholder-state=active]:h-[var(--mosaic-placeholder-height)]',
|
|
28
|
+
'group-data-[mosaic-placeholder-orientation=horizontal]:w-0',
|
|
29
|
+
'group-data-[mosaic-placeholder-orientation=horizontal]:group-data-[mosaic-placeholder-state=active]:w-[var(--mosaic-placeholder-width)]',
|
|
29
30
|
],
|
|
30
31
|
},
|
|
31
32
|
} satisfies Record<string, Record<string, ClassNameValue>>;
|
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export type Axis = 'vertical' | 'horizontal';
|
|
7
|
-
export type AllowedAxis = Axis | 'all';
|
|
5
|
+
import { type ElementDragPayload } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
8
6
|
|
|
9
7
|
export type GetId<TData = any> = (data: TData) => string;
|
|
10
8
|
|
|
9
|
+
/**
|
|
10
|
+
* NOTE: Must implement value equivalence.
|
|
11
|
+
*/
|
|
12
|
+
export type LocationType = string | number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the source data from the drag payload.
|
|
16
|
+
*/
|
|
17
|
+
export const getSourceData = <TData = any, TLocation = any>(
|
|
18
|
+
source: ElementDragPayload,
|
|
19
|
+
): MosaicTileData<TData, TLocation> | null => {
|
|
20
|
+
return source.data.type === 'tile' ? (source.data as MosaicTileData<TData, TLocation>) : null;
|
|
21
|
+
};
|
|
22
|
+
|
|
11
23
|
/**
|
|
12
24
|
* Draggable item.
|
|
13
25
|
* Tiles may contain arbitrary data (which may not be an ECHO object; e.g., search result).
|
package/src/components/index.ts
CHANGED
package/src/hooks/index.ts
CHANGED
|
@@ -31,8 +31,10 @@ export const useContainerDebug = (debug?: boolean): UseContainerDebug => {
|
|
|
31
31
|
}, [debug, debugRef]);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
const CONTAINER_INFO_NAME = 'ContainerInfo';
|
|
35
|
+
|
|
34
36
|
const ContainerInfo = forwardRef<HTMLDivElement, ThemedClassName>(({ classNames }, forwardedRef) => {
|
|
35
|
-
const { id, state, activeLocation, scrolling } = useMosaicContainer(
|
|
37
|
+
const { id, state, activeLocation, scrolling } = useMosaicContainer(CONTAINER_INFO_NAME);
|
|
36
38
|
const counter = useRef(0);
|
|
37
39
|
return (
|
|
38
40
|
<Json
|
|
@@ -43,4 +45,4 @@ const ContainerInfo = forwardRef<HTMLDivElement, ThemedClassName>(({ classNames
|
|
|
43
45
|
);
|
|
44
46
|
});
|
|
45
47
|
|
|
46
|
-
ContainerInfo.displayName =
|
|
48
|
+
ContainerInfo.displayName = CONTAINER_INFO_NAME;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Obj } from '@dxos/echo';
|
|
6
|
+
|
|
7
|
+
import { type GetId, type MosaicEventHandler } from '../components';
|
|
8
|
+
|
|
9
|
+
import { useEventHandlerAdapter } from './useEventHandlerAdapter';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal model shape needed to build the default column handler.
|
|
13
|
+
*/
|
|
14
|
+
export type DefaultColumnEventHandlerModel<TColumn = unknown> = {
|
|
15
|
+
getColumnId: GetId<TColumn>;
|
|
16
|
+
isColumn: (obj: unknown) => obj is TColumn;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the default column drag/drop handler (no persistence).
|
|
21
|
+
* Use this when no custom eventHandler is passed to Board.Content.
|
|
22
|
+
*/
|
|
23
|
+
export const useDefaultColumnEventHandler = <TColumn extends Obj.Unknown = Obj.Unknown>(
|
|
24
|
+
id: string,
|
|
25
|
+
model: DefaultColumnEventHandlerModel<TColumn>,
|
|
26
|
+
items: TColumn[],
|
|
27
|
+
): MosaicEventHandler<TColumn> =>
|
|
28
|
+
useEventHandlerAdapter<TColumn, TColumn>({
|
|
29
|
+
id,
|
|
30
|
+
items,
|
|
31
|
+
getId: model.getColumnId,
|
|
32
|
+
get: (data) => data,
|
|
33
|
+
make: (object) => object,
|
|
34
|
+
canDrop: ({ source }) => model.isColumn(source.data),
|
|
35
|
+
});
|
|
@@ -10,7 +10,10 @@ import { arrayMove } from '@dxos/util';
|
|
|
10
10
|
|
|
11
11
|
import { type GetId, type MosaicEventHandler } from '../components';
|
|
12
12
|
|
|
13
|
-
export type UseEventHandlerProps<TItem = any, TObject =
|
|
13
|
+
export type UseEventHandlerProps<TItem = any, TObject extends Obj.Unknown = Obj.Unknown> = Pick<
|
|
14
|
+
MosaicEventHandler<TItem>,
|
|
15
|
+
'id' | 'canDrop'
|
|
16
|
+
> & {
|
|
14
17
|
/**
|
|
15
18
|
* The items to manage.
|
|
16
19
|
*/
|
|
@@ -72,17 +75,23 @@ export const useEventHandlerAdapter = <TItem = any, TObject extends Obj.Unknown
|
|
|
72
75
|
void cb(source.data);
|
|
73
76
|
},
|
|
74
77
|
onDrop: ({ source, target }) => {
|
|
75
|
-
|
|
76
|
-
log.info('onDrop', { source, target, to });
|
|
78
|
+
log.info('onDrop', { source, target });
|
|
77
79
|
|
|
78
80
|
const mutate = (items: TItem[]) => {
|
|
81
|
+
const to =
|
|
82
|
+
target?.type === 'tile' || target?.type === 'placeholder'
|
|
83
|
+
? target.location
|
|
84
|
+
: target?.type === 'container'
|
|
85
|
+
? items.length
|
|
86
|
+
: -1;
|
|
79
87
|
const from = items.findIndex((item) => getId(item) === source.id);
|
|
80
|
-
|
|
88
|
+
const insertIndex = typeof to === 'number' && to >= 0 ? Math.floor(to) : -1;
|
|
89
|
+
if (insertIndex !== -1) {
|
|
81
90
|
if (from !== -1) {
|
|
82
|
-
arrayMove(items, from,
|
|
91
|
+
arrayMove(items, from, insertIndex);
|
|
83
92
|
} else {
|
|
84
93
|
// TODO(burdon): This should be the responsibility of the source container.
|
|
85
|
-
items.splice(
|
|
94
|
+
items.splice(insertIndex, 0, make(get(source.data)));
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
};
|