@dxos/react-ui-mosaic 0.8.4-main.fcfe5033a5 → 0.9.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.
Files changed (91) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/{chunk-33NNDFD6.mjs → chunk-I3M5WM6C.mjs} +45 -73
  3. package/dist/lib/browser/chunk-I3M5WM6C.mjs.map +7 -0
  4. package/dist/lib/browser/index.mjs +125 -59
  5. package/dist/lib/browser/index.mjs.map +4 -4
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/playwright/index.mjs +118 -10
  8. package/dist/lib/browser/playwright/index.mjs.map +3 -3
  9. package/dist/lib/browser/testing/index.mjs +10 -19
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/lib/browser/translations.mjs +18 -0
  12. package/dist/lib/browser/translations.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-Y4LG7O67.mjs → chunk-DVJLDZLE.mjs} +45 -73
  14. package/dist/lib/node-esm/chunk-DVJLDZLE.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +125 -59
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/playwright/index.mjs +118 -10
  19. package/dist/lib/node-esm/playwright/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/testing/index.mjs +10 -19
  21. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/translations.mjs +20 -0
  23. package/dist/lib/node-esm/translations.mjs.map +7 -0
  24. package/dist/types/src/components/Board/Board.d.ts +12 -4
  25. package/dist/types/src/components/Board/Board.d.ts.map +1 -1
  26. package/dist/types/src/components/Board/Board.stories.d.ts +3 -3
  27. package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Board/Column.d.ts +5 -5
  29. package/dist/types/src/components/Board/Column.d.ts.map +1 -1
  30. package/dist/types/src/components/Board/Item.d.ts +1 -1
  31. package/dist/types/src/components/Board/Item.d.ts.map +1 -1
  32. package/dist/types/src/components/Mosaic/Container.d.ts +9 -1
  33. package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -1
  34. package/dist/types/src/components/Mosaic/Mosaic.d.ts +3 -2
  35. package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
  36. package/dist/types/src/components/Mosaic/Placeholder.d.ts +8 -8
  37. package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -1
  38. package/dist/types/src/components/Mosaic/Stack.d.ts +6 -1
  39. package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -1
  40. package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/Mosaic/Tile.d.ts +6 -2
  42. package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -1
  43. package/dist/types/src/components/Mosaic/extension.d.ts.map +1 -1
  44. package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
  45. package/dist/types/src/components/Mosaic/types.d.ts.map +1 -1
  46. package/dist/types/src/components/SearchStack/SearchStack.d.ts +20 -0
  47. package/dist/types/src/components/SearchStack/SearchStack.d.ts.map +1 -0
  48. package/dist/types/src/components/SearchStack/SearchStack.stories.d.ts +10 -0
  49. package/dist/types/src/components/SearchStack/SearchStack.stories.d.ts.map +1 -0
  50. package/dist/types/src/components/SearchStack/index.d.ts +2 -0
  51. package/dist/types/src/components/SearchStack/index.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +1 -0
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useContainerDebug.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts +1 -1
  57. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
  59. package/dist/types/src/playwright/board-manager.d.ts +20 -5
  60. package/dist/types/src/playwright/board-manager.d.ts.map +1 -1
  61. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  62. package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
  63. package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
  64. package/dist/types/src/testing/types.d.ts +7 -9
  65. package/dist/types/src/testing/types.d.ts.map +1 -1
  66. package/dist/types/src/translations.d.ts +3 -3
  67. package/dist/types/src/translations.d.ts.map +1 -1
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +42 -40
  70. package/src/components/Board/Board.stories.tsx +11 -10
  71. package/src/components/Board/Board.tsx +4 -3
  72. package/src/components/Board/Column.tsx +22 -18
  73. package/src/components/Board/Item.tsx +14 -10
  74. package/src/components/Mosaic/Container.tsx +18 -2
  75. package/src/components/Mosaic/Placeholder.tsx +6 -0
  76. package/src/components/Mosaic/Stack.tsx +20 -5
  77. package/src/components/Mosaic/Tile.tsx +11 -2
  78. package/src/components/Mosaic/extension.ts +1 -1
  79. package/src/components/SearchStack/SearchStack.stories.tsx +47 -0
  80. package/src/components/SearchStack/SearchStack.tsx +135 -0
  81. package/src/components/SearchStack/index.ts +5 -0
  82. package/src/components/index.ts +1 -0
  83. package/src/hooks/useContainerDebug.tsx +3 -3
  84. package/src/hooks/useEventHandlerAdapter.ts +1 -1
  85. package/src/playwright/board-manager.ts +168 -13
  86. package/src/playwright/smoke.spec.ts +3 -3
  87. package/src/testing/CardContainer.tsx +2 -5
  88. package/src/testing/DefaultStackTile.tsx +11 -5
  89. package/src/testing/types.ts +7 -17
  90. package/dist/lib/browser/chunk-33NNDFD6.mjs.map +0 -7
  91. package/dist/lib/node-esm/chunk-Y4LG7O67.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-mosaic",
3
- "version": "0.8.4-main.fcfe5033a5",
3
+ "version": "0.9.0",
4
4
  "description": "Modular UI components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,10 +8,13 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "DXOS.org",
13
13
  "sideEffects": false,
14
14
  "type": "module",
15
+ "imports": {
16
+ "#translations": "./src/translations.ts"
17
+ },
15
18
  "exports": {
16
19
  ".": {
17
20
  "source": "./src/index.ts",
@@ -30,19 +33,15 @@
30
33
  "types": "./dist/types/src/testing/index.d.ts",
31
34
  "browser": "./dist/lib/browser/testing/index.mjs",
32
35
  "node": "./dist/lib/node-esm/testing/index.mjs"
36
+ },
37
+ "./translations": {
38
+ "source": "./src/translations.ts",
39
+ "types": "./dist/types/src/translations.d.ts",
40
+ "browser": "./dist/lib/browser/translations.mjs",
41
+ "node": "./dist/lib/node-esm/translations.mjs"
33
42
  }
34
43
  },
35
44
  "types": "dist/types/src/index.d.ts",
36
- "typesVersions": {
37
- "*": {
38
- "playwright": [
39
- "dist/types/src/playwright/index.d.ts"
40
- ],
41
- "testing": [
42
- "dist/types/src/testing/index.d.ts"
43
- ]
44
- }
45
- },
46
45
  "files": [
47
46
  "dist",
48
47
  "src"
@@ -52,9 +51,10 @@
52
51
  "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
53
52
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
54
53
  "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "3.2.10",
55
- "@codemirror/state": "^6.5.2",
56
- "@codemirror/view": "^6.38.4",
54
+ "@codemirror/state": "^6.6.0",
55
+ "@codemirror/view": "^6.43.0",
57
56
  "@fluentui/react-tabster": "9.26.11",
57
+ "@playwright/test": "1.57.0",
58
58
  "@radix-ui/react-compose-refs": "1.1.1",
59
59
  "@radix-ui/react-context": "1.1.1",
60
60
  "@radix-ui/react-primitive": "2.0.2",
@@ -62,44 +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.8.4-main.fcfe5033a5",
66
- "@dxos/invariant": "0.8.4-main.fcfe5033a5",
67
- "@dxos/keys": "0.8.4-main.fcfe5033a5",
68
- "@dxos/react-client": "0.8.4-main.fcfe5033a5",
69
- "@dxos/log": "0.8.4-main.fcfe5033a5",
70
- "@dxos/echo": "0.8.4-main.fcfe5033a5",
71
- "@dxos/react-ui-menu": "0.8.4-main.fcfe5033a5",
72
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.fcfe5033a5",
73
- "@dxos/react-ui-dnd": "0.8.4-main.fcfe5033a5",
74
- "@dxos/util": "0.8.4-main.fcfe5033a5",
75
- "@dxos/ui-theme": "0.8.4-main.fcfe5033a5"
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"
76
79
  },
77
80
  "devDependencies": {
78
81
  "@effect-atom/atom-react": "^0.5.0",
79
82
  "@types/react": "~19.2.7",
80
83
  "@types/react-dom": "~19.2.3",
81
- "effect": "3.20.0",
84
+ "effect": "3.21.3",
82
85
  "react": "~19.2.3",
83
86
  "react-dom": "~19.2.3",
84
- "vite": "^7.1.11",
85
- "@dxos/react-ui": "0.8.4-main.fcfe5033a5",
86
- "@dxos/random": "0.8.4-main.fcfe5033a5",
87
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.fcfe5033a5",
88
- "@dxos/echo-atom": "0.8.4-main.fcfe5033a5",
89
- "@dxos/schema": "0.8.4-main.fcfe5033a5",
90
- "@dxos/storybook-utils": "0.8.4-main.fcfe5033a5",
91
- "@dxos/types": "0.8.4-main.fcfe5033a5",
92
- "@dxos/ui-theme": "0.8.4-main.fcfe5033a5",
93
- "@dxos/test-utils": "0.8.4-main.fcfe5033a5"
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"
94
96
  },
95
97
  "peerDependencies": {
96
98
  "@effect-atom/atom-react": "^0.5.0",
97
- "effect": "3.20.0",
99
+ "effect": "3.21.3",
98
100
  "react": "~19.2.3",
99
101
  "react-dom": "~19.2.3",
100
- "@dxos/react-ui": "0.8.4-main.fcfe5033a5",
101
- "@dxos/ui-theme": "0.8.4-main.fcfe5033a5",
102
- "@dxos/random": "0.8.4-main.fcfe5033a5"
102
+ "@dxos/random": "0.9.0",
103
+ "@dxos/react-ui": "0.9.0",
104
+ "@dxos/ui-theme": "0.9.0"
103
105
  },
104
106
  "publishConfig": {
105
107
  "access": "public"
@@ -8,7 +8,6 @@ import React, { useContext, useMemo } from 'react';
8
8
  import { expect, within } from 'storybook/test';
9
9
 
10
10
  import { type Database, Filter, Obj, Ref } from '@dxos/echo';
11
- import { AtomObj, AtomQuery } from '@dxos/echo-atom';
12
11
  import { invariant } from '@dxos/invariant';
13
12
  import { random } from '@dxos/random';
14
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
@@ -16,9 +15,10 @@ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
16
15
  import { withRegistry } from '@dxos/storybook-utils';
17
16
  import { mx } from '@dxos/ui-theme';
18
17
 
18
+ import { translations } from '#translations';
19
+
19
20
  import { useEventHandlerAdapter } from '../../hooks';
20
21
  import { TestColumn, TestItem } from '../../testing';
21
- import { translations } from '../../translations';
22
22
  import { Focus } from '../Focus';
23
23
  import { Mosaic, type MosaicEventHandler } from '../Mosaic';
24
24
  import { Board, type BoardModel } from './Board';
@@ -72,14 +72,15 @@ const useTestBoardModel = (): TestBoardModelResult => {
72
72
  const orderAtom = Atom.make<string[]>([]);
73
73
 
74
74
  // Source of truth for which columns exist; subscribed to ECHO query.
75
- const columnsAtom =
76
- space?.db != null ? AtomQuery.make(space.db, Filter.type(TestColumn)) : Atom.make<TestColumn[]>([]);
75
+ const columnsAtom = space?.db != null ? space.db.query(Filter.type(TestColumn)).atom : Atom.make<TestColumn[]>([]);
77
76
 
78
77
  // If orderAtom is empty, use Echo order as-is; else sort by orderAtom and append new columns.
79
78
  const orderedColumnsAtom = Atom.make((get) => {
80
79
  const cols = get(columnsAtom);
81
80
  const order = get(orderAtom);
82
- if (order.length === 0) return cols;
81
+ if (order.length === 0) {
82
+ return cols;
83
+ }
83
84
  const byId = new Map(cols.map((column) => [getColumnId(column), column]));
84
85
  const ordered = order.map((id) => byId.get(id)).filter((column): column is TestColumn => column != null);
85
86
  const appended = cols.filter((column) => !order.includes(getColumnId(column)));
@@ -90,10 +91,10 @@ const useTestBoardModel = (): TestBoardModelResult => {
90
91
  space?.db != null
91
92
  ? Atom.family((column: TestColumn) =>
92
93
  Atom.make((get) => {
93
- const refs = get(AtomObj.makeProperty(column, 'items'));
94
+ const refs = get(Obj.atomProperty(column, 'items'));
94
95
  return (
95
96
  refs
96
- ?.map((ref: Ref.Ref<TestItem>) => get(AtomObj.makeWithReactive(ref)))
97
+ ?.map((ref: Ref.Ref<TestItem>) => get(Obj.atomReactive(ref)))
97
98
  .filter((item): item is TestItem => item != null) ?? []
98
99
  );
99
100
  }),
@@ -118,14 +119,14 @@ const useTestBoardModel = (): TestBoardModelResult => {
118
119
  }),
119
120
  );
120
121
 
121
- Obj.change(column, (column) => {
122
+ Obj.update(column, (column) => {
122
123
  column.items.push(Ref.make(item));
123
124
  });
124
125
 
125
126
  return item;
126
127
  },
127
128
  onItemDelete: (column: TestColumn, current: TestItem) => {
128
- Obj.change(column, (column) => {
129
+ Obj.update(column, (column) => {
129
130
  if (!column.items) {
130
131
  return;
131
132
  }
@@ -172,7 +173,7 @@ const DefaultStory = ({ debug = false, columns: columnsProp = 0 }: DefaultStoryP
172
173
 
173
174
  return (
174
175
  <Mosaic.Root asChild debug={debug}>
175
- <div role='none' className={mx('grid md:p-2 overflow-hidden', debug && 'grid-cols-[1fr_20rem] gap-2')}>
176
+ <div className={mx('grid md:p-2 overflow-hidden', debug && 'grid-cols-[1fr_20rem] gap-2')}>
176
177
  <Board.Root model={model}>
177
178
  <Board.Content id='board' debug={debug} eventHandler={eventHandler} Tile={DefaultBoardColumn} />
178
179
  </Board.Root>
@@ -14,8 +14,9 @@ import React, {
14
14
  } from 'react';
15
15
 
16
16
  import { ScrollArea, type ThemedClassName } from '@dxos/react-ui';
17
- import { Json } from '@dxos/react-ui-syntax-highlighter';
18
- import { composable, composableProps, mx } from '@dxos/ui-theme';
17
+ import { composable, composableProps } from '@dxos/react-ui';
18
+ import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
19
+ import { mx } from '@dxos/ui-theme';
19
20
 
20
21
  import { useContainerDebug } from '../../hooks';
21
22
  import { Focus } from '../Focus';
@@ -162,7 +163,7 @@ export const BoardDebug = forwardRef<HTMLDivElement, ThemedClassName>(({ classNa
162
163
  const { containers, dragging } = useMosaic(BOARD_DEBUG_NAME);
163
164
  const counter = useRef(0);
164
165
  return (
165
- <Json.Data
166
+ <JsonHighlighter
166
167
  data={{ containers, dragging, count: counter.current++ }}
167
168
  classNames={mx('text-xs', classNames)}
168
169
  ref={forwardedRef}
@@ -8,21 +8,21 @@ import React, {
8
8
  type PropsWithChildren,
9
9
  type ReactElement,
10
10
  type Ref as ReactRef,
11
- type RefObject,
12
11
  forwardRef,
13
12
  useMemo,
14
- useRef,
15
13
  useState,
16
14
  } from 'react';
17
15
 
18
16
  import { Obj, Ref } from '@dxos/echo';
19
17
  import { useObject } from '@dxos/react-client/echo';
20
18
  import { IconButton, ScrollArea, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
19
+ import { composable, composableProps } from '@dxos/react-ui';
21
20
  import { Menu, createMenuAction } from '@dxos/react-ui-menu';
22
- import { composable, composableProps, mx } from '@dxos/ui-theme';
21
+ import { mx } from '@dxos/ui-theme';
22
+
23
+ import { translationKey } from '#translations';
23
24
 
24
25
  import { useContainerDebug, useEventHandlerAdapter } from '../../hooks';
25
- import { translationKey } from '../../translations';
26
26
  import { Focus } from '../Focus';
27
27
  import { Mosaic, type MosaicContainerProps, type MosaicStackProps, type MosaicTileProps } from '../Mosaic';
28
28
  import { useBoard } from './Board';
@@ -49,7 +49,10 @@ export function useBoardColumn<TColumn = unknown>(): TColumn | undefined {
49
49
  return value?.column as TColumn | undefined;
50
50
  }
51
51
 
52
- type BoardColumnProps<TColumn = any> = Pick<MosaicTileProps<TColumn>, 'classNames' | 'location' | 'data' | 'debug'>;
52
+ type BoardColumnProps<TColumn = any> = Pick<
53
+ MosaicTileProps<TColumn>,
54
+ 'classNames' | 'location' | 'data' | 'debug' | 'draggable'
55
+ >;
53
56
 
54
57
  //
55
58
  // Column Root
@@ -58,17 +61,16 @@ type BoardColumnProps<TColumn = any> = Pick<MosaicTileProps<TColumn>, 'className
58
61
  const BOARD_COLUMN_ROOT_NAME = 'Board.Column.Root';
59
62
 
60
63
  type BoardColumnRootProps<TColumn = any> = PropsWithChildren<BoardColumnProps<TColumn>> & {
61
- dragHandleRef?: RefObject<HTMLButtonElement | null>;
64
+ // Pass the actual button element (not a ref). Refs don't trigger re-renders, so reading
65
+ // `ref.current` at render time leaves Mosaic.Tile's `dragHandle` prop null forever.
66
+ // Consumers should track this with `useState` and a callback ref on `Board.Column.Header`.
67
+ dragHandle?: HTMLButtonElement | null;
62
68
  };
63
69
 
64
70
  const BoardColumnRootInner = composable<HTMLDivElement, BoardColumnRootProps>(
65
- ({ classNames, children, location, data, debug, dragHandleRef: dragHandleRefProp, ...rest }, forwardedRef) => {
71
+ ({ classNames, children, location, data, debug, draggable, dragHandle, ...rest }, forwardedRef) => {
66
72
  const { model } = useBoard(BOARD_COLUMN_ROOT_NAME);
67
73
 
68
- // TODO(burdon): Use merge ref hook (see react-hooks).
69
- const internalRef = useRef<HTMLButtonElement>(null);
70
- const dragHandleRef = dragHandleRefProp ?? internalRef;
71
-
72
74
  return (
73
75
  <Mosaic.Tile
74
76
  asChild
@@ -76,7 +78,8 @@ const BoardColumnRootInner = composable<HTMLDivElement, BoardColumnRootProps>(
76
78
  id={model.getColumnId(data)}
77
79
  data={data}
78
80
  debug={debug}
79
- dragHandle={dragHandleRef.current}
81
+ draggable={draggable}
82
+ dragHandle={dragHandle}
80
83
  >
81
84
  <Focus.Group
82
85
  {...rest}
@@ -239,16 +242,16 @@ const BOARD_DEFAULT_COLUMN_NAME = 'Board.DefaultColumn';
239
242
  type DefaultBoardColumnProps = BoardColumnProps & Pick<BoardColumnBodyProps, 'Tile'>;
240
243
 
241
244
  const DefaultBoardColumn = forwardRef<HTMLDivElement, DefaultBoardColumnProps>(
242
- ({ classNames, location, data, debug, Tile = BoardItem }, forwardedRef) => {
245
+ ({ classNames, location, data, debug, draggable, Tile = BoardItem }, forwardedRef) => {
243
246
  const { model } = useBoard(BOARD_DEFAULT_COLUMN_NAME);
244
247
  const [DebugInfo, debugHandler] = useContainerDebug(debug);
245
- const dragHandleRef = useRef<HTMLButtonElement>(null);
248
+ const [dragHandle, setDragHandle] = useState<HTMLButtonElement | null>(null);
246
249
  const [column, updateColumn] = useObject(data);
247
250
 
248
251
  const eventHandler = useEventHandlerAdapter<Ref.Unknown>({
249
252
  id: model.getColumnId(data),
250
253
  items: column?.items ?? [],
251
- getId: (ref) => ref.target?.id ?? ref.dxn.toString(),
254
+ getId: (ref) => ref.target?.id ?? ref.uri,
252
255
  get: (refOrObj) => (Ref.isRef(refOrObj) ? refOrObj.target! : refOrObj),
253
256
  make: (object) => Ref.make(object),
254
257
  canDrop: ({ source }) => {
@@ -269,12 +272,13 @@ const DefaultBoardColumn = forwardRef<HTMLDivElement, DefaultBoardColumnProps>(
269
272
  )}
270
273
  location={location}
271
274
  data={data}
272
- dragHandleRef={dragHandleRef}
275
+ draggable={draggable}
276
+ dragHandle={dragHandle}
273
277
  ref={forwardedRef}
274
278
  >
275
- <BoardColumnHeader label={Obj.getLabel(data) ?? data.id} dragHandleRef={dragHandleRef} />
279
+ <BoardColumnHeader label={Obj.getLabel(data) ?? data.id} dragHandleRef={setDragHandle} />
276
280
  <BoardColumnBody data={data} eventHandler={eventHandler} debug={debugHandler} Tile={Tile} />
277
- <div role='none' className='flex flex-col col-span-full'>
281
+ <div className='flex flex-col col-span-full'>
278
282
  <BoardColumnFooter data={data} />
279
283
  <DebugInfo />
280
284
  </div>
@@ -3,14 +3,15 @@
3
3
  //
4
4
 
5
5
  import { useComposedRefs } from '@radix-ui/react-compose-refs';
6
- import React, { type ReactElement, type Ref as ReactRef, forwardRef, useMemo, useRef } from 'react';
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
9
  import { Card, Tag, Toolbar, 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
 
13
- import { translationKey } from '../../translations';
13
+ import { translationKey } from '#translations';
14
+
14
15
  import { Focus } from '../Focus';
15
16
  import { Mosaic, type MosaicTileProps } from '../Mosaic';
16
17
  import { useBoard } from './Board';
@@ -20,15 +21,17 @@ const BOARD_ITEM_NAME = 'Board.Item';
20
21
 
21
22
  type BoardItemProps<TItem extends Obj.Unknown = any> = Pick<
22
23
  MosaicTileProps<TItem>,
23
- 'classNames' | 'location' | 'data' | 'debug'
24
+ 'classNames' | 'location' | 'data' | 'debug' | 'draggable'
24
25
  >;
25
26
 
26
27
  const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
27
- ({ classNames, data, location, debug }, forwardedRef) => {
28
+ ({ classNames, data, location, debug, draggable }, forwardedRef) => {
28
29
  const { t } = useTranslation(translationKey);
29
30
  const rootRef = useRef<HTMLDivElement>(null);
30
31
  const composedRef = useComposedRefs<HTMLDivElement>(rootRef, forwardedRef);
31
- const dragHandleRef = useRef<HTMLButtonElement>(null);
32
+ // Use state (callback ref) so the dragHandle prop updates when the button mounts.
33
+ // Refs don't trigger re-renders, so reading `.current` at render time leaves the prop null.
34
+ const [dragHandle, setDragHandle] = useState<HTMLButtonElement | null>(null);
32
35
 
33
36
  const { model } = useBoard(BOARD_ITEM_NAME);
34
37
  const column = useBoardColumn();
@@ -57,7 +60,8 @@ const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
57
60
  <Mosaic.Tile
58
61
  ref={rootRef}
59
62
  asChild
60
- dragHandle={dragHandleRef.current}
63
+ draggable={draggable}
64
+ dragHandle={dragHandle}
61
65
  id={data.id}
62
66
  data={data}
63
67
  location={location}
@@ -70,8 +74,8 @@ const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
70
74
  ref={composedRef}
71
75
  onClick={(event) => event.currentTarget.focus()}
72
76
  >
73
- <Card.Toolbar>
74
- <Card.DragHandle ref={dragHandleRef} testId='mosaicBoard.cardDragHandle' />
77
+ <Card.Header>
78
+ <Card.DragHandle ref={setDragHandle} testId='mosaicBoard.cardDragHandle' />
75
79
  <Card.Title data-testid='mosaicBoard.cardTitle'>{label}</Card.Title>
76
80
  {/* TODO(wittjosiah): Reconcile with Card.Menu. */}
77
81
  <Menu.Trigger asChild disabled={!items?.length}>
@@ -83,14 +87,14 @@ const BoardItemInner = forwardRef<HTMLDivElement, BoardItemProps>(
83
87
  />
84
88
  </Menu.Trigger>
85
89
  <Menu.Content items={items} />
86
- </Card.Toolbar>
90
+ </Card.Header>
87
91
  {/* TODO(burdon): Replace with surface. */}
88
92
  <Card.Row icon='ph--note--regular' classNames='text-description'>
89
93
  <Card.Text>{description}</Card.Text>
90
94
  </Card.Row>
91
95
  <Card.Row icon='ph--tag--regular'>
92
96
  {label && (
93
- <div role='none' className='shrink-0 flex gap-1 items-center text-xs'>
97
+ <div className='shrink-0 flex gap-1 items-center text-xs'>
94
98
  <Tag palette={getHashStyles(label).hue}>{label}</Tag>
95
99
  </div>
96
100
  )}
@@ -21,8 +21,7 @@ import React, {
21
21
  useState,
22
22
  } from 'react';
23
23
 
24
- import { type AllowedAxis } from '@dxos/react-ui';
25
- import { composable, composableProps } from '@dxos/ui-theme';
24
+ import { type AllowedAxis, composable, composableProps } from '@dxos/react-ui';
26
25
  import { isTruthy } from '@dxos/util';
27
26
 
28
27
  import { useFocus } from '../Focus';
@@ -60,6 +59,11 @@ type MosaicContainerContextValue<TData = any, Location = LocationType> = {
60
59
  /** Set the current item by ID. */
61
60
  setCurrentId: (id: string | undefined) => void;
62
61
 
62
+ /** IDs of selected (aria-selected) items. */
63
+ selectedIds?: ReadonlySet<string>;
64
+ /** Request to set or unset selection on an item by ID. */
65
+ setSelected: (id: string, selected: boolean) => void;
66
+
63
67
  /** Register a scroll-to-item callback (provided by Stack/VirtualStack). */
64
68
  registerScrollTo: (fn: ((id: string) => void) | undefined) => void;
65
69
  };
@@ -86,6 +90,10 @@ type MosaicContainerProps = PropsWithChildren<
86
90
  currentId?: string;
87
91
  /** Called when a tile requests to become current. */
88
92
  onCurrentChange?: (id: string | undefined) => void;
93
+ /** Controlled set of selected item IDs. */
94
+ selectedIds?: ReadonlySet<string>;
95
+ /** Called when a tile requests to toggle selection. */
96
+ onSelectionChange?: (id: string, selected: boolean) => void;
89
97
  debug?: () => ReactNode;
90
98
  }
91
99
  >;
@@ -102,6 +110,8 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
102
110
  withFocus,
103
111
  currentId,
104
112
  onCurrentChange,
113
+ selectedIds,
114
+ onSelectionChange,
105
115
  debug,
106
116
  ...props
107
117
  },
@@ -126,6 +136,10 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
126
136
  const [activeLocation, setActiveLocation] = useState<LocationType | undefined>();
127
137
  const [scrolling, setScrolling] = useState(false);
128
138
  const setCurrentId = useCallback((id: string | undefined) => onCurrentChange?.(id), [onCurrentChange]);
139
+ const setSelected = useCallback(
140
+ (id: string, selected: boolean) => onSelectionChange?.(id, selected),
141
+ [onSelectionChange],
142
+ );
129
143
 
130
144
  // Scroll-to-item: Stack/VirtualStack registers its implementation.
131
145
  const scrollToRef = useRef<((id: string) => void) | undefined>(undefined);
@@ -283,6 +297,8 @@ const MosaicContainer = composable<HTMLDivElement, MosaicContainerProps>(
283
297
  setActiveLocation={setActiveLocation}
284
298
  currentId={currentId}
285
299
  setCurrentId={setCurrentId}
300
+ selectedIds={selectedIds}
301
+ setSelected={setSelected}
286
302
  registerScrollTo={registerScrollTo}
287
303
  >
288
304
  <Comp
@@ -30,6 +30,11 @@ const MOSAIC_PLACEHOLDER_ORIENTATION_ATTR = 'mosaic-placeholder-orientation';
30
30
  // State attribute: data-[mosaic-placeholder-state=active]
31
31
  const MOSAIC_PLACEHOLDER_STATE_ATTR = 'mosaic-placeholder-state';
32
32
 
33
+ // Location attribute: data-[mosaic-placeholder-location=0.5]. Identifies the
34
+ // placeholder's slot in its container (0.5, 1.5, 2.5, …) so tests can target
35
+ // a specific gap unambiguously without relying on layout-dependent indices.
36
+ const MOSAIC_PLACEHOLDER_LOCATION_ATTR = 'mosaic-placeholder-location';
37
+
33
38
  type MosaicPlaceholderProps<Location = LocationType> = ThemedClassName<
34
39
  PropsWithChildren<{
35
40
  asChild?: boolean;
@@ -94,6 +99,7 @@ const MosaicPlaceholder = <Location extends LocationType = LocationType>({
94
99
  {...{
95
100
  [`data-${MOSAIC_PLACEHOLDER_ORIENTATION_ATTR}`]: orientation,
96
101
  [`data-${MOSAIC_PLACEHOLDER_STATE_ATTR}`]: data.location === activeLocation ? 'active' : 'idle',
102
+ [`data-${MOSAIC_PLACEHOLDER_LOCATION_ATTR}`]: String(location),
97
103
  }}
98
104
  role='none'
99
105
  className={mx('relative', classNames)}
@@ -16,8 +16,8 @@ import React, {
16
16
  } from 'react';
17
17
 
18
18
  import { invariant } from '@dxos/invariant';
19
- import { type Axis, type ThemedClassName } from '@dxos/react-ui';
20
- import { composable, composableProps, mx } from '@dxos/ui-theme';
19
+ import { type Axis, type ThemedClassName, composable, composableProps } from '@dxos/react-ui';
20
+ import { mx } from '@dxos/ui-theme';
21
21
 
22
22
  import { useVisibleItems } from '../../hooks';
23
23
  import { useMosaicContainerContext } from './Container';
@@ -85,6 +85,7 @@ const MosaicStackInner = composable<HTMLDivElement, MosaicStackProps>(
85
85
  orientation = orientationProp,
86
86
  dragging,
87
87
  currentId,
88
+ selectedIds,
88
89
  registerScrollTo,
89
90
  } = useMosaicContainerContext(MOSAIC_STACK_NAME);
90
91
  const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
@@ -130,6 +131,7 @@ const MosaicStackInner = composable<HTMLDivElement, MosaicStackProps>(
130
131
  location={index + 1}
131
132
  draggable={draggable}
132
133
  current={getId(item) === currentId}
134
+ selected={selectedIds?.has(getId(item))}
133
135
  debug={debug}
134
136
  />
135
137
  {draggable && <InternalPlaceholder orientation={orientation} location={index + 1.5} />}
@@ -178,7 +180,8 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
178
180
  forwardedRef,
179
181
  ) => {
180
182
  invariant(Tile);
181
- const { id, dragging, currentId, registerScrollTo } = useMosaicContainerContext(MOSAIC_VIRTUAL_STACK_NAME);
183
+ const { id, dragging, currentId, selectedIds, registerScrollTo } =
184
+ useMosaicContainerContext(MOSAIC_VIRTUAL_STACK_NAME);
182
185
  const visibleItems = useVisibleItems({ id, items, dragging: dragging?.source.data, getId });
183
186
  // In draggable mode virtual indices alternate: even=placeholder, odd=tile.
184
187
  // Wrap estimateSize so placeholders get 0 (their actual negligible height) and
@@ -193,6 +196,10 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
193
196
  count: draggable ? visibleItems.length * 2 + 1 : visibleItems.length,
194
197
  estimateSize: wrappedEstimateSize,
195
198
  gap,
199
+ // Inset the stack from both ends by `gap` (matching the inter-item spacing). The virtualizer
200
+ // folds this into item offsets, getTotalSize(), and scrollToIndex, so no manual compensation.
201
+ paddingStart: gap,
202
+ paddingEnd: gap,
196
203
  // Key measurements by stable item ID so the size cache survives scrolling;
197
204
  // without this, measurements are indexed by position and are lost when items reorder.
198
205
  getItemKey: draggable
@@ -213,10 +220,16 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
213
220
  }
214
221
 
215
222
  const virtualIndex = draggable ? itemIndex * 2 + 1 : itemIndex;
216
- virtualizer.scrollToIndex(virtualIndex, { align: 'start', behavior: 'smooth' });
223
+ // Align to the item's start, but offset by the inter-item `gap` so the tile lands
224
+ // one gap below the viewport edge rather than flush against it (a plain
225
+ // `scrollToIndex({ align: 'start' })` scrolls the preceding gap out of view).
226
+ const [offset] = virtualizer.getOffsetForIndex(virtualIndex, 'start') ?? [];
227
+ if (offset != null) {
228
+ virtualizer.scrollToOffset(Math.max(0, offset - (gap ?? 0)), { behavior: 'smooth' });
229
+ }
217
230
  }
218
231
  },
219
- [visibleItems, getId, draggable, virtualizer, scrollIntoView],
232
+ [visibleItems, getId, draggable, virtualizer, scrollIntoView, gap],
220
233
  );
221
234
 
222
235
  useEffect(() => {
@@ -293,6 +306,7 @@ const MosaicVirtualStackInner = forwardRef<HTMLDivElement, MosaicVirtualStackPro
293
306
  location={location}
294
307
  draggable={draggable}
295
308
  current={getId(data) === currentId}
309
+ selected={selectedIds?.has(getId(data))}
296
310
  debug={debug}
297
311
  />
298
312
  ) : (
@@ -311,6 +325,7 @@ MosaicVirtualStackInner.displayName = MOSAIC_VIRTUAL_STACK_NAME;
311
325
  const MosaicVirtualStack = MosaicVirtualStackInner as <TData = any>(
312
326
  props: MosaicVirtualStackProps<TData> & { ref?: Ref<HTMLDivElement> },
313
327
  ) => ReactElement;
328
+
314
329
  (MosaicVirtualStack as any)[Symbol.for('dxos.composable')] = true;
315
330
 
316
331
  //
@@ -24,7 +24,7 @@ import React, { type PropsWithChildren, useLayoutEffect, useMemo, useRef, useSta
24
24
  import { createPortal } from 'react-dom';
25
25
 
26
26
  import { type ThemedClassName } from '@dxos/react-ui';
27
- import { composableProps, slottable } from '@dxos/ui-theme';
27
+ import { composableProps, slottable } from '@dxos/react-ui';
28
28
 
29
29
  import { useMosaicContainerContext } from './Container';
30
30
  import { type LocationType, type MosaicTileData, getSourceData } from './types';
@@ -61,6 +61,8 @@ type MosaicTileProps<TData = any, TLocation = LocationType> = ThemedClassName<
61
61
  draggable?: boolean;
62
62
  /** Whether this tile is the current (aria-current) item. */
63
63
  current?: boolean;
64
+ /** Whether this tile is selected (aria-selected). */
65
+ selected?: boolean;
64
66
  debug?: boolean;
65
67
  }>
66
68
  >;
@@ -76,7 +78,8 @@ const MosaicTile = slottable<HTMLDivElement, MosaicTileProps>(
76
78
  id,
77
79
  data: dataProp,
78
80
  draggable: draggableProp,
79
- current: _current,
81
+ current,
82
+ selected,
80
83
  debug: _,
81
84
  ...props
82
85
  },
@@ -211,6 +214,12 @@ const MosaicTile = slottable<HTMLDivElement, MosaicTileProps>(
211
214
  [`data-${MOSAIC_TILE_STATE_ATTR}`]: state.type,
212
215
  }}
213
216
  role='listitem'
217
+ // Pair `current` / `selected` props with `aria-current` / `aria-selected`
218
+ // so the `dx-current` / `dx-selected` / `dx-hover` utilities (which key
219
+ // off those attributes) actually fire. Without this the props are purely
220
+ // advisory and the corresponding styling silently no-ops.
221
+ aria-current={current ? 'true' : undefined}
222
+ aria-selected={selected ? 'true' : undefined}
214
223
  className={className}
215
224
  ref={composedRef}
216
225
  >
@@ -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(--color-accent-surface)';
126
+ dom.style.borderLeft = '2px solid var(--color-accent-bg)';
127
127
  dom.style.display = 'inline-block';
128
128
  dom.style.height = '1em';
129
129
  dom.style.verticalAlign = 'middle';