@dxos/react-ui-mosaic 0.9.0 → 0.9.1-staging.ee54ba693a
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-I3M5WM6C.mjs → chunk-OBVIYZLR.mjs} +182 -43
- package/dist/lib/browser/chunk-OBVIYZLR.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +20 -17
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +13 -9
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/{chunk-DVJLDZLE.mjs → chunk-4IEMCMIV.mjs} +182 -43
- package/dist/lib/node-esm/chunk-4IEMCMIV.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +20 -17
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +13 -9
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/components/Board/Board.d.ts +1 -1
- package/dist/types/src/components/Board/Board.d.ts.map +1 -1
- package/dist/types/src/components/Board/Board.stories.d.ts +2 -2
- package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -1
- package/dist/types/src/components/Board/Item.d.ts.map +1 -1
- package/dist/types/src/components/Focus/Focus.d.ts +1 -1
- package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Container.d.ts +10 -1
- package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/DragHandle.d.ts +28 -0
- package/dist/types/src/components/Mosaic/DragHandle.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Mosaic.d.ts +18 -2
- package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Placeholder.d.ts +2 -2
- package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/ResizeHandle.d.ts +14 -0
- package/dist/types/src/components/Mosaic/ResizeHandle.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/ResizeHandle.stories.d.ts +12 -0
- package/dist/types/src/components/Mosaic/ResizeHandle.stories.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Root.d.ts +1 -1
- package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Stack.d.ts +1 -1
- package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Tile.d.ts +34 -0
- package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
- package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
- package/dist/types/src/testing/types.d.ts +10 -12
- package/dist/types/src/testing/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -33
- package/src/components/Board/Board.stories.tsx +3 -3
- package/src/components/Board/Board.tsx +1 -1
- package/src/components/Board/Item.tsx +20 -12
- package/src/components/Focus/Focus.tsx +1 -1
- package/src/components/Mosaic/Container.tsx +21 -7
- package/src/components/Mosaic/DragHandle.tsx +75 -0
- package/src/components/Mosaic/Mosaic.tsx +12 -4
- package/src/components/Mosaic/Placeholder.tsx +4 -2
- package/src/components/Mosaic/ResizeHandle.stories.tsx +119 -0
- package/src/components/Mosaic/ResizeHandle.tsx +56 -0
- package/src/components/Mosaic/Root.tsx +3 -1
- package/src/components/Mosaic/Stack.tsx +23 -2
- package/src/components/Mosaic/Tile.tsx +82 -4
- package/src/components/Mosaic/styles.ts +4 -3
- package/src/components/SearchStack/SearchStack.tsx +1 -1
- package/src/testing/DefaultStackTile.tsx +6 -4
- package/src/testing/types.ts +14 -14
- package/dist/lib/browser/chunk-I3M5WM6C.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-DVJLDZLE.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-mosaic",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "Modular UI components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -62,46 +62,46 @@
|
|
|
62
62
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
63
63
|
"@tanstack/react-virtual": "^3.13.18",
|
|
64
64
|
"bind-event-listener": "^3.0.0",
|
|
65
|
-
"@dxos/async": "0.9.
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/react-client": "0.9.
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/react-ui-
|
|
72
|
-
"@dxos/react-ui-attention": "0.9.
|
|
73
|
-
"@dxos/react-ui-
|
|
74
|
-
"@dxos/react-ui-menu": "0.9.
|
|
75
|
-
"@dxos/react-ui-
|
|
76
|
-
"@dxos/ui-theme": "0.9.
|
|
77
|
-
"@dxos/react-ui-
|
|
78
|
-
"@dxos/util": "0.9.
|
|
65
|
+
"@dxos/async": "0.9.1-staging.ee54ba693a",
|
|
66
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
67
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a",
|
|
68
|
+
"@dxos/keys": "0.9.1-staging.ee54ba693a",
|
|
69
|
+
"@dxos/react-client": "0.9.1-staging.ee54ba693a",
|
|
70
|
+
"@dxos/echo": "0.9.1-staging.ee54ba693a",
|
|
71
|
+
"@dxos/react-ui-list": "0.9.1-staging.ee54ba693a",
|
|
72
|
+
"@dxos/react-ui-attention": "0.9.1-staging.ee54ba693a",
|
|
73
|
+
"@dxos/react-ui-dnd": "0.9.1-staging.ee54ba693a",
|
|
74
|
+
"@dxos/react-ui-menu": "0.9.1-staging.ee54ba693a",
|
|
75
|
+
"@dxos/react-ui-syntax-highlighter": "0.9.1-staging.ee54ba693a",
|
|
76
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
77
|
+
"@dxos/react-ui-search": "0.9.1-staging.ee54ba693a",
|
|
78
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@effect-atom/atom-react": "^0.5.0",
|
|
82
|
-
"@types/react": "~19.2.
|
|
82
|
+
"@types/react": "~19.2.17",
|
|
83
83
|
"@types/react-dom": "~19.2.3",
|
|
84
|
-
"effect": "3.21.
|
|
85
|
-
"react": "~19.2.
|
|
86
|
-
"react-dom": "~19.2.
|
|
84
|
+
"effect": "3.21.4",
|
|
85
|
+
"react": "~19.2.7",
|
|
86
|
+
"react-dom": "~19.2.7",
|
|
87
87
|
"vite": "^8.0.16",
|
|
88
|
-
"@dxos/random": "0.9.
|
|
89
|
-
"@dxos/react-ui": "0.9.
|
|
90
|
-
"@dxos/
|
|
91
|
-
"@dxos/
|
|
92
|
-
"@dxos/
|
|
93
|
-
"@dxos/
|
|
94
|
-
"@dxos/
|
|
95
|
-
"@dxos/
|
|
88
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
89
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a",
|
|
90
|
+
"@dxos/react-ui-syntax-highlighter": "0.9.1-staging.ee54ba693a",
|
|
91
|
+
"@dxos/schema": "0.9.1-staging.ee54ba693a",
|
|
92
|
+
"@dxos/test-utils": "0.9.1-staging.ee54ba693a",
|
|
93
|
+
"@dxos/storybook-utils": "0.9.1-staging.ee54ba693a",
|
|
94
|
+
"@dxos/types": "0.9.1-staging.ee54ba693a",
|
|
95
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
96
96
|
},
|
|
97
97
|
"peerDependencies": {
|
|
98
98
|
"@effect-atom/atom-react": "^0.5.0",
|
|
99
|
-
"effect": "3.21.
|
|
100
|
-
"react": "~19.2.
|
|
101
|
-
"react-dom": "~19.2.
|
|
102
|
-
"@dxos/
|
|
103
|
-
"@dxos/
|
|
104
|
-
"@dxos/ui
|
|
99
|
+
"effect": "3.21.4",
|
|
100
|
+
"react": "~19.2.7",
|
|
101
|
+
"react-dom": "~19.2.7",
|
|
102
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
103
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
104
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a"
|
|
105
105
|
},
|
|
106
106
|
"publishConfig": {
|
|
107
107
|
"access": "public"
|
|
@@ -49,7 +49,7 @@ const createTestData = (db: Database.Database, columns: number, items?: (column:
|
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
type
|
|
52
|
+
type StoryArgs = {
|
|
53
53
|
columns?: number;
|
|
54
54
|
items?: (column: number) => number;
|
|
55
55
|
debug?: boolean;
|
|
@@ -164,7 +164,7 @@ const useTestBoardModel = (): TestBoardModelResult => {
|
|
|
164
164
|
return { model, eventHandler };
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
const DefaultStory = ({ debug = false, columns: columnsProp = 0 }:
|
|
167
|
+
const DefaultStory = ({ debug = false, columns: columnsProp = 0 }: StoryArgs) => {
|
|
168
168
|
const { model, eventHandler } = useTestBoardModel();
|
|
169
169
|
const columns = useAtomValue(model.columns);
|
|
170
170
|
if (columnsProp > 0 && columns.length === 0) {
|
|
@@ -199,7 +199,7 @@ const meta = {
|
|
|
199
199
|
createIdentity: true,
|
|
200
200
|
createSpace: true,
|
|
201
201
|
onCreateSpace: ({ space }, context) => {
|
|
202
|
-
const args = context.args as
|
|
202
|
+
const args = context.args as StoryArgs;
|
|
203
203
|
createTestData(space.db, args.columns ?? 0, args.items);
|
|
204
204
|
},
|
|
205
205
|
}),
|
|
@@ -6,7 +6,7 @@ import { useComposedRefs } from '@radix-ui/react-compose-refs';
|
|
|
6
6
|
import React, { type ReactElement, type Ref as ReactRef, forwardRef, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { Obj } from '@dxos/echo';
|
|
9
|
-
import { Card,
|
|
9
|
+
import { Card, Icon, IconButton, Tag, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { Menu, createMenuAction } from '@dxos/react-ui-menu';
|
|
11
11
|
import { getHashStyles } from '@dxos/ui-theme';
|
|
12
12
|
|
|
@@ -78,24 +78,32 @@ const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
|
|
|
78
78
|
<Card.DragHandle ref={setDragHandle} testId='mosaicBoard.cardDragHandle' />
|
|
79
79
|
<Card.Title data-testid='mosaicBoard.cardTitle'>{label}</Card.Title>
|
|
80
80
|
{/* TODO(wittjosiah): Reconcile with Card.Menu. */}
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
<Card.Block end>
|
|
82
|
+
<Menu.Trigger asChild disabled={!items?.length}>
|
|
83
|
+
<IconButton
|
|
84
|
+
iconOnly
|
|
85
|
+
variant='ghost'
|
|
86
|
+
icon='ph--dots-three-vertical--regular'
|
|
87
|
+
label={t('action-menu.label')}
|
|
88
|
+
/>
|
|
89
|
+
</Menu.Trigger>
|
|
90
|
+
</Card.Block>
|
|
89
91
|
<Menu.Content items={items} />
|
|
90
92
|
</Card.Header>
|
|
91
93
|
{/* TODO(burdon): Replace with surface. */}
|
|
92
|
-
<Card.Row
|
|
94
|
+
<Card.Row classNames='text-description'>
|
|
95
|
+
<Card.Block>
|
|
96
|
+
<Icon icon='ph--note--regular' />
|
|
97
|
+
</Card.Block>
|
|
93
98
|
<Card.Text>{description}</Card.Text>
|
|
94
99
|
</Card.Row>
|
|
95
|
-
<Card.Row
|
|
100
|
+
<Card.Row>
|
|
101
|
+
<Card.Block>
|
|
102
|
+
<Icon icon='ph--tag--regular' />
|
|
103
|
+
</Card.Block>
|
|
96
104
|
{label && (
|
|
97
105
|
<div className='shrink-0 flex gap-1 items-center text-xs'>
|
|
98
|
-
<Tag
|
|
106
|
+
<Tag hue={getHashStyles(label).hue}>{label}</Tag>
|
|
99
107
|
</div>
|
|
100
108
|
)}
|
|
101
109
|
</Card.Row>
|
|
@@ -14,8 +14,10 @@ import React, {
|
|
|
14
14
|
type CSSProperties,
|
|
15
15
|
type PropsWithChildren,
|
|
16
16
|
type ReactNode,
|
|
17
|
+
type Ref,
|
|
17
18
|
useCallback,
|
|
18
19
|
useEffect,
|
|
20
|
+
useImperativeHandle,
|
|
19
21
|
useMemo,
|
|
20
22
|
useRef,
|
|
21
23
|
useState,
|
|
@@ -80,12 +82,19 @@ const MOSAIC_CONTAINER_PLACEHOLDER_HEIGHT = '--mosaic-placeholder-height';
|
|
|
80
82
|
|
|
81
83
|
let counter = 0;
|
|
82
84
|
|
|
85
|
+
/** Imperative handle for scrolling a stack to an item without changing the current/selected item. */
|
|
86
|
+
export type MosaicScrollController = {
|
|
87
|
+
scrollToItem: (id: string) => void;
|
|
88
|
+
};
|
|
89
|
+
|
|
83
90
|
type MosaicContainerProps = PropsWithChildren<
|
|
84
91
|
Partial<Pick<MosaicContainerContextValue, 'eventHandler' | 'orientation'>> & {
|
|
85
92
|
asChild?: boolean;
|
|
86
93
|
/** Support autoscrolling container when dragging. */
|
|
87
94
|
autoScroll?: HTMLElement | null;
|
|
88
95
|
withFocus?: boolean;
|
|
96
|
+
/** Imperative handle to scroll the stack to an item (decoupled from selection). */
|
|
97
|
+
controllerRef?: Ref<MosaicScrollController>;
|
|
89
98
|
/** Controlled current-item ID. */
|
|
90
99
|
currentId?: string;
|
|
91
100
|
/** Called when a tile requests to become current. */
|
|
@@ -98,6 +107,9 @@ type MosaicContainerProps = PropsWithChildren<
|
|
|
98
107
|
}
|
|
99
108
|
>;
|
|
100
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Container for a Mosaic layout.
|
|
112
|
+
*/
|
|
101
113
|
// TODO(burdon): Make generic.
|
|
102
114
|
const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
103
115
|
(
|
|
@@ -108,6 +120,7 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
108
120
|
asChild,
|
|
109
121
|
autoScroll: autoscrollElement,
|
|
110
122
|
withFocus,
|
|
123
|
+
controllerRef,
|
|
111
124
|
currentId,
|
|
112
125
|
onCurrentChange,
|
|
113
126
|
selectedIds,
|
|
@@ -147,6 +160,9 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
147
160
|
scrollToRef.current = fn;
|
|
148
161
|
}, []);
|
|
149
162
|
|
|
163
|
+
// Imperative scroll-to-item, decoupled from selection (e.g. a calendar scrolling the stack to a day).
|
|
164
|
+
useImperativeHandle(controllerRef, () => ({ scrollToItem: (id: string) => scrollToRef.current?.(id) }), []);
|
|
165
|
+
|
|
150
166
|
// When currentId changes, scroll the matching item into view.
|
|
151
167
|
useEffect(() => {
|
|
152
168
|
if (currentId) {
|
|
@@ -219,10 +235,8 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
219
235
|
return (data && eventHandler.canDrop?.({ source: data })) || false;
|
|
220
236
|
},
|
|
221
237
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
// return 'move';
|
|
225
|
-
// },
|
|
238
|
+
// Reorder is a move, not a copy — otherwise the browser shows the green "+" copy cursor.
|
|
239
|
+
getDropEffect: () => 'move',
|
|
226
240
|
|
|
227
241
|
/**
|
|
228
242
|
* Dragging started in this container.
|
|
@@ -263,9 +277,9 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
263
277
|
autoscrollElement && [
|
|
264
278
|
autoScrollForElements({
|
|
265
279
|
element: autoscrollElement,
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
280
|
+
// Only autoscroll for tile (reorder) drags. Resize-handle drags carry no tile data, so
|
|
281
|
+
// resizing a tile near a viewport edge must not scroll the container.
|
|
282
|
+
canScroll: ({ source }) => getSourceData(source) != null,
|
|
269
283
|
getAllowedAxis: () => orientation,
|
|
270
284
|
getConfiguration: () => ({
|
|
271
285
|
maxScrollSpeed: 'fast',
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
6
|
+
import React, { type ReactNode, forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Button, IconButton, type IconButtonProps, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { useMosaicTileContext } from './Tile';
|
|
11
|
+
|
|
12
|
+
const MOSAIC_DRAG_HANDLE_NAME = 'Mosaic.DragHandle';
|
|
13
|
+
|
|
14
|
+
const REACT_UI_TRANSLATION_KEY = '@dxos/react-ui';
|
|
15
|
+
|
|
16
|
+
export type MosaicDragHandleProps = ThemedClassName<
|
|
17
|
+
Partial<Pick<IconButtonProps, 'icon' | 'label' | 'variant'>> & {
|
|
18
|
+
testId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Inline glyph rendered in place of the sprite icon. The browser does not rasterize external SVG
|
|
21
|
+
* sprite `<use>` icons into a native drag image, so a tile that appears in its own drag preview
|
|
22
|
+
* (the clone follows the cursor) must use an inline-SVG handle here to stay visible while dragging.
|
|
23
|
+
*/
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Affordance that initiates dragging the enclosing {@link MosaicTile}. Renders a button and
|
|
30
|
+
* registers itself with the tile (via context), so consumers only need to drop it inside a
|
|
31
|
+
* `Mosaic.Tile` — no `dragHandle` ref plumbing required.
|
|
32
|
+
*/
|
|
33
|
+
export const MosaicDragHandle = forwardRef<HTMLButtonElement, MosaicDragHandleProps>(
|
|
34
|
+
(
|
|
35
|
+
{ icon = 'ph--dots-six-vertical--regular', label, variant = 'ghost', classNames, testId, children },
|
|
36
|
+
forwardedRef,
|
|
37
|
+
) => {
|
|
38
|
+
const { t } = useTranslation(REACT_UI_TRANSLATION_KEY);
|
|
39
|
+
const { setDragHandle } = useMosaicTileContext(MOSAIC_DRAG_HANDLE_NAME);
|
|
40
|
+
const ref = composeRefs(forwardedRef, setDragHandle);
|
|
41
|
+
const testIdProps = testId ? { 'data-testid': testId } : {};
|
|
42
|
+
|
|
43
|
+
// Inline-glyph variant: stays visible in the tile's own native drag image (sprite icons do not).
|
|
44
|
+
if (children) {
|
|
45
|
+
return (
|
|
46
|
+
<Button
|
|
47
|
+
ref={ref}
|
|
48
|
+
variant={variant}
|
|
49
|
+
tabIndex={-1}
|
|
50
|
+
classNames={['grid place-items-center cursor-grab', classNames]}
|
|
51
|
+
{...testIdProps}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
<span className='sr-only'>{label ?? t('toolbar-drag-handle.label')}</span>
|
|
55
|
+
</Button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<IconButton
|
|
61
|
+
ref={ref}
|
|
62
|
+
iconOnly
|
|
63
|
+
noTooltip
|
|
64
|
+
tabIndex={-1}
|
|
65
|
+
variant={variant}
|
|
66
|
+
icon={icon}
|
|
67
|
+
label={label ?? t('toolbar-drag-handle.label')}
|
|
68
|
+
classNames={['cursor-grab', classNames]}
|
|
69
|
+
{...testIdProps}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
MosaicDragHandle.displayName = MOSAIC_DRAG_HANDLE_NAME;
|
|
@@ -6,14 +6,17 @@ import {
|
|
|
6
6
|
MosaicContainer,
|
|
7
7
|
type MosaicContainerProps,
|
|
8
8
|
type MosaicContainerState,
|
|
9
|
+
type MosaicScrollController,
|
|
9
10
|
useMosaicContainerContext,
|
|
10
11
|
} from './Container';
|
|
12
|
+
import { MosaicDragHandle, type MosaicDragHandleProps } from './DragHandle';
|
|
11
13
|
import {
|
|
12
14
|
MosaicDropIndicator,
|
|
13
15
|
type MosaicDropIndicatorProps,
|
|
14
16
|
MosaicPlaceholder,
|
|
15
17
|
type MosaicPlaceholderProps,
|
|
16
18
|
} from './Placeholder';
|
|
19
|
+
import { MosaicResizeHandle, type MosaicResizeHandleProps } from './ResizeHandle';
|
|
17
20
|
import { MosaicRoot, type MosaicRootProps, useMosaicRootContext } from './Root';
|
|
18
21
|
import { MosaicStack, type MosaicStackProps, type MosaicStackTileComponent, MosaicVirtualStack } from './Stack';
|
|
19
22
|
import { MosaicTile, type MosaicTileProps, type MosaicTileState, useMosaicTileContext } from './Tile';
|
|
@@ -26,6 +29,8 @@ export const Mosaic = {
|
|
|
26
29
|
Root: MosaicRoot,
|
|
27
30
|
Container: MosaicContainer,
|
|
28
31
|
Tile: MosaicTile,
|
|
32
|
+
DragHandle: MosaicDragHandle,
|
|
33
|
+
ResizeHandle: MosaicResizeHandle,
|
|
29
34
|
Placeholder: MosaicPlaceholder,
|
|
30
35
|
DropIndicator: MosaicDropIndicator,
|
|
31
36
|
Stack: MosaicStack,
|
|
@@ -33,15 +38,18 @@ export const Mosaic = {
|
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
export type {
|
|
36
|
-
MosaicRootProps,
|
|
37
41
|
MosaicContainerProps,
|
|
38
42
|
MosaicContainerState,
|
|
39
|
-
|
|
40
|
-
MosaicTileState,
|
|
41
|
-
MosaicPlaceholderProps,
|
|
43
|
+
MosaicDragHandleProps,
|
|
42
44
|
MosaicDropIndicatorProps,
|
|
45
|
+
MosaicPlaceholderProps,
|
|
46
|
+
MosaicResizeHandleProps,
|
|
47
|
+
MosaicRootProps,
|
|
48
|
+
MosaicScrollController,
|
|
43
49
|
MosaicStackProps,
|
|
44
50
|
MosaicStackTileComponent,
|
|
51
|
+
MosaicTileProps,
|
|
52
|
+
MosaicTileState,
|
|
45
53
|
};
|
|
46
54
|
|
|
47
55
|
export {
|
|
@@ -82,6 +82,8 @@ const MosaicPlaceholder = <Location extends LocationType = LocationType>({
|
|
|
82
82
|
const data = getSourceData(source);
|
|
83
83
|
return (data && eventHandler.canDrop?.({ source: data })) || false;
|
|
84
84
|
},
|
|
85
|
+
// Reorder is a move, not a copy — otherwise the browser shows the green "+" copy cursor.
|
|
86
|
+
getDropEffect: () => 'move',
|
|
85
87
|
onDragEnter: () => {
|
|
86
88
|
setActiveLocation(data.location);
|
|
87
89
|
},
|
|
@@ -134,6 +136,6 @@ MosaicDropIndicator.displayName = DROP_INDICATOR_NAME;
|
|
|
134
136
|
// Exports
|
|
135
137
|
//
|
|
136
138
|
|
|
137
|
-
export {
|
|
139
|
+
export { MosaicDropIndicator, MosaicPlaceholder };
|
|
138
140
|
|
|
139
|
-
export type {
|
|
141
|
+
export type { MosaicDropIndicatorProps, MosaicPlaceholderProps };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type Axis, ScrollArea } from '@dxos/react-ui';
|
|
9
|
+
import { type Size } from '@dxos/react-ui-dnd';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { Mosaic, type MosaicTileProps } from './Mosaic';
|
|
13
|
+
|
|
14
|
+
type Item = { id: string; label: string };
|
|
15
|
+
|
|
16
|
+
const ITEMS: Item[] = Array.from({ length: 6 }, (_, index) => ({ id: `tile-${index}`, label: `Tile ${index + 1}` }));
|
|
17
|
+
|
|
18
|
+
const DEFAULT_HORIZONTAL: Size = 24;
|
|
19
|
+
const DEFAULT_VERTICAL: Size = 8;
|
|
20
|
+
|
|
21
|
+
// Per-axis resize bounds (rem) declared on the tile.
|
|
22
|
+
const BOUNDS = {
|
|
23
|
+
horizontal: {
|
|
24
|
+
minSize: 12,
|
|
25
|
+
maxSize: 40,
|
|
26
|
+
},
|
|
27
|
+
vertical: {
|
|
28
|
+
minSize: 4,
|
|
29
|
+
maxSize: 16,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
type ResizableTileProps = MosaicTileProps<Item> & {
|
|
34
|
+
onResize: (id: string, size: Size) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ResizableTile = ({ data, size, onResize, ...tileProps }: ResizableTileProps) => {
|
|
38
|
+
const handleSizeChange = useCallback((next: Size) => onResize(data.id, next), [data.id, onResize]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Mosaic.Tile
|
|
42
|
+
{...tileProps}
|
|
43
|
+
classNames='flex flex-col p-2 m-1 border border-separator rounded-sm bg-base-surface overflow-hidden'
|
|
44
|
+
data={data}
|
|
45
|
+
size={size}
|
|
46
|
+
onSizeChange={handleSizeChange}
|
|
47
|
+
>
|
|
48
|
+
<div className='font-medium'>{data.label}</div>
|
|
49
|
+
<div className='text-xs text-description'>{typeof size === 'number' ? `${size}rem` : 'intrinsic'}</div>
|
|
50
|
+
<Mosaic.ResizeHandle />
|
|
51
|
+
</Mosaic.Tile>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const DefaultStory = ({ orientation = 'vertical' }: { orientation?: Axis }) => {
|
|
56
|
+
const initial = orientation === 'horizontal' ? DEFAULT_HORIZONTAL : DEFAULT_VERTICAL;
|
|
57
|
+
const [viewport, setViewport] = useState<HTMLElement | null>(null);
|
|
58
|
+
const [sizes, setSizes] = useState<Record<string, Size>>(() =>
|
|
59
|
+
Object.fromEntries(ITEMS.map((item) => [item.id, initial])),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleSizeChange = useCallback((id: string, size: Size) => setSizes((prev) => ({ ...prev, [id]: size })), []);
|
|
63
|
+
|
|
64
|
+
const bounds = BOUNDS[orientation];
|
|
65
|
+
const Tile = useMemo(
|
|
66
|
+
() => (tileProps: MosaicTileProps<Item>) => (
|
|
67
|
+
<ResizableTile {...tileProps} {...bounds} size={sizes[tileProps.data.id]} onResize={handleSizeChange} />
|
|
68
|
+
),
|
|
69
|
+
[sizes, handleSizeChange, bounds],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Mosaic.Root>
|
|
74
|
+
<Mosaic.Container
|
|
75
|
+
asChild
|
|
76
|
+
orientation={orientation}
|
|
77
|
+
autoScroll={viewport}
|
|
78
|
+
eventHandler={{ id: 'resize-demo', canDrop: () => false }}
|
|
79
|
+
>
|
|
80
|
+
<ScrollArea.Root orientation={orientation}>
|
|
81
|
+
<ScrollArea.Viewport ref={setViewport}>
|
|
82
|
+
<Mosaic.Stack
|
|
83
|
+
orientation={orientation}
|
|
84
|
+
getId={(item) => item.id}
|
|
85
|
+
items={ITEMS}
|
|
86
|
+
draggable={false}
|
|
87
|
+
Tile={Tile}
|
|
88
|
+
/>
|
|
89
|
+
</ScrollArea.Viewport>
|
|
90
|
+
</ScrollArea.Root>
|
|
91
|
+
</Mosaic.Container>
|
|
92
|
+
</Mosaic.Root>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const meta: Meta = {
|
|
97
|
+
title: 'ui/react-ui-mosaic/ResizeHandle',
|
|
98
|
+
decorators: [withLayout({ layout: 'fullscreen' }), withTheme()],
|
|
99
|
+
render: (args) => <DefaultStory {...args} />,
|
|
100
|
+
parameters: {
|
|
101
|
+
layout: 'fullscreen',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default meta;
|
|
106
|
+
|
|
107
|
+
type Story = StoryObj<typeof DefaultStory>;
|
|
108
|
+
|
|
109
|
+
export const Horizontal: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
orientation: 'horizontal',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const Vertical: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
orientation: 'vertical',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { ResizeHandle } from '@dxos/react-ui-dnd';
|
|
8
|
+
|
|
9
|
+
import { useMosaicContainerContext } from './Container';
|
|
10
|
+
import { useMosaicTileContext } from './Tile';
|
|
11
|
+
|
|
12
|
+
const MOSAIC_RESIZE_HANDLE_NAME = 'Mosaic.ResizeHandle';
|
|
13
|
+
|
|
14
|
+
// Lower bounds for a tile's extent (rem); below these the content stops being usable.
|
|
15
|
+
const MIN_WIDTH = 20;
|
|
16
|
+
const MIN_HEIGHT = 3;
|
|
17
|
+
// Extent assumed when the subject hasn't been measured yet (matches the deck plank default).
|
|
18
|
+
const DEFAULT_EXTRINSIC_SIZE = 50;
|
|
19
|
+
|
|
20
|
+
export type MosaicResizeHandleProps = {
|
|
21
|
+
/** Lower bound in rem; overrides the tile's `minSize`, else a sensible per-axis minimum. */
|
|
22
|
+
minSize?: number;
|
|
23
|
+
/** Upper bound in rem; overrides the tile's `maxSize`. */
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
/** Extent assumed before the subject is first measured. */
|
|
26
|
+
fallbackSize?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Edge affordance that resizes the enclosing {@link MosaicTile}. The axis tracks the container
|
|
31
|
+
* orientation (width when horizontal, height when vertical); the tile must be given a `size` so the
|
|
32
|
+
* dragged extent is reflected and persisted via its `onSizeChange`. Resize bounds default to the
|
|
33
|
+
* tile's `minSize`/`maxSize` and can be overridden per handle.
|
|
34
|
+
*/
|
|
35
|
+
export const MosaicResizeHandle = ({
|
|
36
|
+
minSize,
|
|
37
|
+
maxSize,
|
|
38
|
+
fallbackSize = DEFAULT_EXTRINSIC_SIZE,
|
|
39
|
+
}: MosaicResizeHandleProps) => {
|
|
40
|
+
const { orientation = 'vertical' } = useMosaicContainerContext(MOSAIC_RESIZE_HANDLE_NAME);
|
|
41
|
+
const { size, setSize, minSize: tileMinSize, maxSize: tileMaxSize } = useMosaicTileContext(MOSAIC_RESIZE_HANDLE_NAME);
|
|
42
|
+
const horizontal = orientation === 'horizontal';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ResizeHandle
|
|
46
|
+
side={horizontal ? 'inline-end' : 'block-end'}
|
|
47
|
+
fallbackSize={fallbackSize}
|
|
48
|
+
minSize={minSize ?? tileMinSize ?? (horizontal ? MIN_WIDTH : MIN_HEIGHT)}
|
|
49
|
+
maxSize={maxSize ?? tileMaxSize}
|
|
50
|
+
size={size}
|
|
51
|
+
onSizeChange={setSize}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
MosaicResizeHandle.displayName = MOSAIC_RESIZE_HANDLE_NAME;
|
|
@@ -137,6 +137,8 @@ const MosaicRoot = forwardRef<HTMLDivElement, MosaicRootProps>(({ children, asCh
|
|
|
137
137
|
// Main controller.
|
|
138
138
|
return combine(
|
|
139
139
|
monitorForElements({
|
|
140
|
+
// Only monitor Mosaic tile drags; other pragmatic-dnd sources under this root are not ours to handle.
|
|
141
|
+
canMonitor: ({ source }) => source.data.type === 'tile',
|
|
140
142
|
/**
|
|
141
143
|
* Dragging started within any container.
|
|
142
144
|
*/
|
|
@@ -283,4 +285,4 @@ MosaicRoot.displayName = MOSAIC_ROOT_NAME;
|
|
|
283
285
|
|
|
284
286
|
export { MosaicRoot, useMosaicRootContext };
|
|
285
287
|
|
|
286
|
-
export type {
|
|
288
|
+
export type { MosaicDraggingState, MosaicRootProps };
|
|
@@ -6,12 +6,14 @@ import { useComposedRefs } from '@radix-ui/react-compose-refs';
|
|
|
6
6
|
import { type ReactVirtualizerOptions, useVirtualizer } from '@tanstack/react-virtual';
|
|
7
7
|
import React, {
|
|
8
8
|
type FC,
|
|
9
|
-
forwardRef,
|
|
10
9
|
Fragment,
|
|
11
10
|
type ReactElement,
|
|
12
11
|
type Ref,
|
|
12
|
+
forwardRef,
|
|
13
13
|
useCallback,
|
|
14
14
|
useEffect,
|
|
15
|
+
useLayoutEffect,
|
|
16
|
+
useMemo,
|
|
15
17
|
useRef,
|
|
16
18
|
} from 'react';
|
|
17
19
|
|
|
@@ -87,10 +89,29 @@ const MosaicStackInner = composable<HTMLDivElement, MosaicStackProps>(
|
|
|
87
89
|
currentId,
|
|
88
90
|
selectedIds,
|
|
89
91
|
registerScrollTo,
|
|
92
|
+
setActiveLocation,
|
|
90
93
|
} = useMosaicContainerContext(MOSAIC_STACK_NAME);
|
|
91
94
|
const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
|
|
92
95
|
invariant(orientation === 'vertical' || orientation === 'horizontal', `Invalid orientation: ${orientation}`);
|
|
93
96
|
|
|
97
|
+
// Reserve the dragged item's slot the instant it leaves `visibleItems`. The source is removed a frame
|
|
98
|
+
// before any placeholder becomes active (the pointer still sits over the source, so no `onDragEnter`
|
|
99
|
+
// has fired), which collapses the stack by the item's height and flickers the following tiles upward.
|
|
100
|
+
// Activating the source-slot placeholder (location `index + 0.5`) in a layout effect fills the gap in
|
|
101
|
+
// the same commit, before paint.
|
|
102
|
+
const draggingSource = dragging?.source.data;
|
|
103
|
+
const sourceIndex = useMemo(() => {
|
|
104
|
+
if (!items || !draggingSource || draggingSource.containerId !== id) {
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
return items.findIndex((item) => getId(item) === draggingSource.id);
|
|
108
|
+
}, [items, draggingSource, id, getId]);
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (sourceIndex >= 0) {
|
|
111
|
+
setActiveLocation(sourceIndex + 0.5);
|
|
112
|
+
}
|
|
113
|
+
}, [sourceIndex, setActiveLocation]);
|
|
114
|
+
|
|
94
115
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
95
116
|
const scrollToId = useCallback(
|
|
96
117
|
(targetId: string) => {
|
|
@@ -353,4 +374,4 @@ InternalPlaceholder.displayName = 'InternalPlaceholder';
|
|
|
353
374
|
|
|
354
375
|
export { MosaicStack, MosaicVirtualStack };
|
|
355
376
|
|
|
356
|
-
export type {
|
|
377
|
+
export type { MosaicStackProps, MosaicStackTileComponent, MosaicVirtualStackProps };
|