@dxos/react-ui-mosaic 0.9.1-main.c7dcc2e112 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-DZ5WMCI2.mjs → chunk-OBVIYZLR.mjs} +177 -41
- package/dist/lib/browser/chunk-OBVIYZLR.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +2 -2
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +7 -5
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/{chunk-4IOJ5T6K.mjs → chunk-4IEMCMIV.mjs} +177 -41
- package/dist/lib/node-esm/chunk-4IEMCMIV.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +2 -2
- 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 +7 -5
- 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/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.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 +17 -1
- 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/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 +1 -1
- package/src/components/Focus/Focus.tsx +1 -1
- package/src/components/Mosaic/Container.tsx +5 -7
- package/src/components/Mosaic/DragHandle.tsx +75 -0
- package/src/components/Mosaic/Mosaic.tsx +11 -5
- 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/testing/types.ts +14 -14
- package/dist/lib/browser/chunk-DZ5WMCI2.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4IOJ5T6K.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-mosaic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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/
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/keys": "0.
|
|
71
|
-
"@dxos/react-ui-
|
|
72
|
-
"@dxos/react-ui-
|
|
73
|
-
"@dxos/react-ui-
|
|
74
|
-
"@dxos/react-ui-
|
|
75
|
-
"@dxos/react-ui-
|
|
76
|
-
"@dxos/ui-
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
65
|
+
"@dxos/echo": "0.10.0",
|
|
66
|
+
"@dxos/async": "0.10.0",
|
|
67
|
+
"@dxos/log": "0.10.0",
|
|
68
|
+
"@dxos/react-client": "0.10.0",
|
|
69
|
+
"@dxos/invariant": "0.10.0",
|
|
70
|
+
"@dxos/keys": "0.10.0",
|
|
71
|
+
"@dxos/react-ui-attention": "0.10.0",
|
|
72
|
+
"@dxos/react-ui-dnd": "0.10.0",
|
|
73
|
+
"@dxos/react-ui-list": "0.10.0",
|
|
74
|
+
"@dxos/react-ui-menu": "0.10.0",
|
|
75
|
+
"@dxos/react-ui-search": "0.10.0",
|
|
76
|
+
"@dxos/react-ui-syntax-highlighter": "0.10.0",
|
|
77
|
+
"@dxos/util": "0.10.0",
|
|
78
|
+
"@dxos/ui-theme": "0.10.0"
|
|
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/react-ui": "0.
|
|
89
|
-
"@dxos/
|
|
90
|
-
"@dxos/
|
|
91
|
-
"@dxos/schema": "0.
|
|
92
|
-
"@dxos/storybook-utils": "0.
|
|
93
|
-
"@dxos/
|
|
94
|
-
"@dxos/types": "0.
|
|
95
|
-
"@dxos/
|
|
88
|
+
"@dxos/react-ui": "0.10.0",
|
|
89
|
+
"@dxos/random": "0.10.0",
|
|
90
|
+
"@dxos/react-ui-syntax-highlighter": "0.10.0",
|
|
91
|
+
"@dxos/schema": "0.10.0",
|
|
92
|
+
"@dxos/storybook-utils": "0.10.0",
|
|
93
|
+
"@dxos/ui-theme": "0.10.0",
|
|
94
|
+
"@dxos/types": "0.10.0",
|
|
95
|
+
"@dxos/test-utils": "0.10.0"
|
|
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/random": "0.
|
|
103
|
-
"@dxos/react-ui": "0.
|
|
104
|
-
"@dxos/ui-theme": "0.
|
|
99
|
+
"effect": "3.21.4",
|
|
100
|
+
"react": "~19.2.7",
|
|
101
|
+
"react-dom": "~19.2.7",
|
|
102
|
+
"@dxos/random": "0.10.0",
|
|
103
|
+
"@dxos/react-ui": "0.10.0",
|
|
104
|
+
"@dxos/ui-theme": "0.10.0"
|
|
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
|
}),
|
|
@@ -103,7 +103,7 @@ const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
|
|
|
103
103
|
</Card.Block>
|
|
104
104
|
{label && (
|
|
105
105
|
<div className='shrink-0 flex gap-1 items-center text-xs'>
|
|
106
|
-
<Tag
|
|
106
|
+
<Tag hue={getHashStyles(label).hue}>{label}</Tag>
|
|
107
107
|
</div>
|
|
108
108
|
)}
|
|
109
109
|
</Card.Row>
|
|
@@ -235,10 +235,8 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
235
235
|
return (data && eventHandler.canDrop?.({ source: data })) || false;
|
|
236
236
|
},
|
|
237
237
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
// return 'move';
|
|
241
|
-
// },
|
|
238
|
+
// Reorder is a move, not a copy — otherwise the browser shows the green "+" copy cursor.
|
|
239
|
+
getDropEffect: () => 'move',
|
|
242
240
|
|
|
243
241
|
/**
|
|
244
242
|
* Dragging started in this container.
|
|
@@ -279,9 +277,9 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
|
|
|
279
277
|
autoscrollElement && [
|
|
280
278
|
autoScrollForElements({
|
|
281
279
|
element: autoscrollElement,
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
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,
|
|
285
283
|
getAllowedAxis: () => orientation,
|
|
286
284
|
getConfiguration: () => ({
|
|
287
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;
|
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
type MosaicScrollController,
|
|
10
10
|
useMosaicContainerContext,
|
|
11
11
|
} from './Container';
|
|
12
|
+
import { MosaicDragHandle, type MosaicDragHandleProps } from './DragHandle';
|
|
12
13
|
import {
|
|
13
14
|
MosaicDropIndicator,
|
|
14
15
|
type MosaicDropIndicatorProps,
|
|
15
16
|
MosaicPlaceholder,
|
|
16
17
|
type MosaicPlaceholderProps,
|
|
17
18
|
} from './Placeholder';
|
|
19
|
+
import { MosaicResizeHandle, type MosaicResizeHandleProps } from './ResizeHandle';
|
|
18
20
|
import { MosaicRoot, type MosaicRootProps, useMosaicRootContext } from './Root';
|
|
19
21
|
import { MosaicStack, type MosaicStackProps, type MosaicStackTileComponent, MosaicVirtualStack } from './Stack';
|
|
20
22
|
import { MosaicTile, type MosaicTileProps, type MosaicTileState, useMosaicTileContext } from './Tile';
|
|
@@ -27,6 +29,8 @@ export const Mosaic = {
|
|
|
27
29
|
Root: MosaicRoot,
|
|
28
30
|
Container: MosaicContainer,
|
|
29
31
|
Tile: MosaicTile,
|
|
32
|
+
DragHandle: MosaicDragHandle,
|
|
33
|
+
ResizeHandle: MosaicResizeHandle,
|
|
30
34
|
Placeholder: MosaicPlaceholder,
|
|
31
35
|
DropIndicator: MosaicDropIndicator,
|
|
32
36
|
Stack: MosaicStack,
|
|
@@ -34,16 +38,18 @@ export const Mosaic = {
|
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
export type {
|
|
37
|
-
MosaicRootProps,
|
|
38
41
|
MosaicContainerProps,
|
|
39
42
|
MosaicContainerState,
|
|
40
|
-
|
|
41
|
-
MosaicTileProps,
|
|
42
|
-
MosaicTileState,
|
|
43
|
-
MosaicPlaceholderProps,
|
|
43
|
+
MosaicDragHandleProps,
|
|
44
44
|
MosaicDropIndicatorProps,
|
|
45
|
+
MosaicPlaceholderProps,
|
|
46
|
+
MosaicResizeHandleProps,
|
|
47
|
+
MosaicRootProps,
|
|
48
|
+
MosaicScrollController,
|
|
45
49
|
MosaicStackProps,
|
|
46
50
|
MosaicStackTileComponent,
|
|
51
|
+
MosaicTileProps,
|
|
52
|
+
MosaicTileState,
|
|
47
53
|
};
|
|
48
54
|
|
|
49
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 };
|