@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.
Files changed (64) hide show
  1. package/dist/lib/browser/{chunk-I3M5WM6C.mjs → chunk-OBVIYZLR.mjs} +182 -43
  2. package/dist/lib/browser/chunk-OBVIYZLR.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +20 -17
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +13 -9
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node-esm/{chunk-DVJLDZLE.mjs → chunk-4IEMCMIV.mjs} +182 -43
  9. package/dist/lib/node-esm/chunk-4IEMCMIV.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +20 -17
  11. package/dist/lib/node-esm/index.mjs.map +3 -3
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs +13 -9
  14. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  15. package/dist/types/src/components/Board/Board.d.ts +1 -1
  16. package/dist/types/src/components/Board/Board.d.ts.map +1 -1
  17. package/dist/types/src/components/Board/Board.stories.d.ts +2 -2
  18. package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Board/Item.d.ts.map +1 -1
  20. package/dist/types/src/components/Focus/Focus.d.ts +1 -1
  21. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
  22. package/dist/types/src/components/Mosaic/Container.d.ts +10 -1
  23. package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -1
  24. package/dist/types/src/components/Mosaic/DragHandle.d.ts +28 -0
  25. package/dist/types/src/components/Mosaic/DragHandle.d.ts.map +1 -0
  26. package/dist/types/src/components/Mosaic/Mosaic.d.ts +18 -2
  27. package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
  28. package/dist/types/src/components/Mosaic/Placeholder.d.ts +2 -2
  29. package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -1
  30. package/dist/types/src/components/Mosaic/ResizeHandle.d.ts +14 -0
  31. package/dist/types/src/components/Mosaic/ResizeHandle.d.ts.map +1 -0
  32. package/dist/types/src/components/Mosaic/ResizeHandle.stories.d.ts +12 -0
  33. package/dist/types/src/components/Mosaic/ResizeHandle.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Mosaic/Root.d.ts +1 -1
  35. package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -1
  36. package/dist/types/src/components/Mosaic/Stack.d.ts +1 -1
  37. package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -1
  38. package/dist/types/src/components/Mosaic/Tile.d.ts +34 -0
  39. package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -1
  40. package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
  41. package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
  42. package/dist/types/src/testing/types.d.ts +10 -12
  43. package/dist/types/src/testing/types.d.ts.map +1 -1
  44. package/dist/types/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +33 -33
  46. package/src/components/Board/Board.stories.tsx +3 -3
  47. package/src/components/Board/Board.tsx +1 -1
  48. package/src/components/Board/Item.tsx +20 -12
  49. package/src/components/Focus/Focus.tsx +1 -1
  50. package/src/components/Mosaic/Container.tsx +21 -7
  51. package/src/components/Mosaic/DragHandle.tsx +75 -0
  52. package/src/components/Mosaic/Mosaic.tsx +12 -4
  53. package/src/components/Mosaic/Placeholder.tsx +4 -2
  54. package/src/components/Mosaic/ResizeHandle.stories.tsx +119 -0
  55. package/src/components/Mosaic/ResizeHandle.tsx +56 -0
  56. package/src/components/Mosaic/Root.tsx +3 -1
  57. package/src/components/Mosaic/Stack.tsx +23 -2
  58. package/src/components/Mosaic/Tile.tsx +82 -4
  59. package/src/components/Mosaic/styles.ts +4 -3
  60. package/src/components/SearchStack/SearchStack.tsx +1 -1
  61. package/src/testing/DefaultStackTile.tsx +6 -4
  62. package/src/testing/types.ts +14 -14
  63. package/dist/lib/browser/chunk-I3M5WM6C.mjs.map +0 -7
  64. 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.0",
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.0",
66
- "@dxos/keys": "0.9.0",
67
- "@dxos/echo": "0.9.0",
68
- "@dxos/log": "0.9.0",
69
- "@dxos/react-client": "0.9.0",
70
- "@dxos/invariant": "0.9.0",
71
- "@dxos/react-ui-dnd": "0.9.0",
72
- "@dxos/react-ui-attention": "0.9.0",
73
- "@dxos/react-ui-list": "0.9.0",
74
- "@dxos/react-ui-menu": "0.9.0",
75
- "@dxos/react-ui-search": "0.9.0",
76
- "@dxos/ui-theme": "0.9.0",
77
- "@dxos/react-ui-syntax-highlighter": "0.9.0",
78
- "@dxos/util": "0.9.0"
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.7",
82
+ "@types/react": "~19.2.17",
83
83
  "@types/react-dom": "~19.2.3",
84
- "effect": "3.21.3",
85
- "react": "~19.2.3",
86
- "react-dom": "~19.2.3",
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.0",
89
- "@dxos/react-ui": "0.9.0",
90
- "@dxos/schema": "0.9.0",
91
- "@dxos/test-utils": "0.9.0",
92
- "@dxos/react-ui-syntax-highlighter": "0.9.0",
93
- "@dxos/types": "0.9.0",
94
- "@dxos/ui-theme": "0.9.0",
95
- "@dxos/storybook-utils": "0.9.0"
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.3",
100
- "react": "~19.2.3",
101
- "react-dom": "~19.2.3",
102
- "@dxos/random": "0.9.0",
103
- "@dxos/react-ui": "0.9.0",
104
- "@dxos/ui-theme": "0.9.0"
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 DefaultStoryProps = {
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 }: DefaultStoryProps) => {
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 DefaultStoryProps;
202
+ const args = context.args as StoryArgs;
203
203
  createTestData(space.db, args.columns ?? 0, args.items);
204
204
  },
205
205
  }),
@@ -188,4 +188,4 @@ export const Board = {
188
188
 
189
189
  export { useBoard, useBoardColumn };
190
190
 
191
- export type { BoardRootProps, BoardContentProps, BoardColumnProps, BoardItemProps };
191
+ export type { BoardColumnProps, BoardContentProps, BoardItemProps, BoardRootProps };
@@ -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, Tag, Toolbar, useTranslation } from '@dxos/react-ui';
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
- <Menu.Trigger asChild disabled={!items?.length}>
82
- <Toolbar.IconButton
83
- iconOnly
84
- variant='ghost'
85
- icon='ph--dots-three-vertical--regular'
86
- label={t('action-menu.label')}
87
- />
88
- </Menu.Trigger>
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 icon='ph--note--regular' classNames='text-description'>
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 icon='ph--tag--regular'>
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 palette={getHashStyles(label).hue}>{label}</Tag>
106
+ <Tag hue={getHashStyles(label).hue}>{label}</Tag>
99
107
  </div>
100
108
  )}
101
109
  </Card.Row>
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  // Re-export from @dxos/react-ui.
6
- export { Focus, useFocus, type FocusGroupProps, type FocusItemProps } from '@dxos/react-ui';
6
+ export { Focus, type FocusGroupProps, type FocusItemProps, useFocus } from '@dxos/react-ui';
@@ -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
- // TODO(burdon): Provide semantic intent to onDrop.
223
- // getDropEffect: () => {
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
- // canScroll: ({ element: _ }) => {
267
- // return true;
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
- MosaicTileProps,
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 { MosaicPlaceholder, MosaicDropIndicator };
139
+ export { MosaicDropIndicator, MosaicPlaceholder };
138
140
 
139
- export type { MosaicPlaceholderProps, MosaicDropIndicatorProps };
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 { MosaicRootProps, MosaicDraggingState };
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 { MosaicStackTileComponent, MosaicStackProps, MosaicVirtualStackProps };
377
+ export type { MosaicStackProps, MosaicStackTileComponent, MosaicVirtualStackProps };