@dxos/react-ui-mosaic 0.8.4-main.bc674ce → 0.8.4-main.c85a9c8dae

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/lib/browser/chunk-7S4PIFKU.mjs +953 -0
  2. package/dist/lib/browser/chunk-7S4PIFKU.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +596 -70
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/playwright/index.mjs +138 -0
  7. package/dist/lib/browser/playwright/index.mjs.map +7 -0
  8. package/dist/lib/browser/testing/index.mjs +61 -232
  9. package/dist/lib/browser/testing/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/chunk-MK6KU66S.mjs +955 -0
  11. package/dist/lib/node-esm/chunk-MK6KU66S.mjs.map +7 -0
  12. package/dist/lib/node-esm/index.mjs +596 -70
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/playwright/index.mjs +140 -0
  16. package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
  17. package/dist/lib/node-esm/testing/index.mjs +61 -232
  18. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  19. package/dist/types/src/components/Board/Board.d.ts +81 -0
  20. package/dist/types/src/components/Board/Board.d.ts.map +1 -0
  21. package/dist/types/src/components/{Mosaic/Mosaic.stories.d.ts → Board/Board.stories.d.ts} +15 -1
  22. package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Board/Column.d.ts +40 -0
  24. package/dist/types/src/components/Board/Column.d.ts.map +1 -0
  25. package/dist/types/src/components/Board/Item.d.ts +9 -0
  26. package/dist/types/src/components/Board/Item.d.ts.map +1 -0
  27. package/dist/types/src/components/Board/index.d.ts +2 -0
  28. package/dist/types/src/components/Board/index.d.ts.map +1 -0
  29. package/dist/types/src/components/Focus/Focus.d.ts +5 -4
  30. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
  31. package/dist/types/src/components/Focus/index.d.ts +0 -1
  32. package/dist/types/src/components/Focus/index.d.ts.map +1 -1
  33. package/dist/types/src/components/Mosaic/Container.d.ts +42 -0
  34. package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -0
  35. package/dist/types/src/components/Mosaic/Mosaic.d.ts +35 -118
  36. package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
  37. package/dist/types/src/components/Mosaic/Placeholder.d.ts +21 -0
  38. package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -0
  39. package/dist/types/src/components/Mosaic/Root.d.ts +36 -0
  40. package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -0
  41. package/dist/types/src/components/Mosaic/Stack.d.ts +23 -0
  42. package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -0
  43. package/dist/types/src/components/{Stack → Mosaic}/Stack.stories.d.ts +2 -2
  44. package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -0
  45. package/dist/types/src/components/Mosaic/Tile.d.ts +48 -0
  46. package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -0
  47. package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
  48. package/dist/types/src/components/Mosaic/types.d.ts +9 -2
  49. package/dist/types/src/components/Mosaic/types.d.ts.map +1 -1
  50. package/dist/types/src/components/index.d.ts +1 -4
  51. package/dist/types/src/components/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/index.d.ts +1 -0
  53. package/dist/types/src/hooks/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts +15 -0
  55. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -0
  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 +2 -2
  59. package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
  60. package/dist/types/src/playwright/board-manager.d.ts +45 -0
  61. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  62. package/dist/types/src/playwright/index.d.ts +9 -0
  63. package/dist/types/src/playwright/index.d.ts.map +1 -0
  64. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  65. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  66. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  67. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  68. package/dist/types/src/testing/CardContainer.d.ts +5 -4
  69. package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
  70. package/dist/types/src/testing/DefaultStackTile.d.ts +2 -2
  71. package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
  72. package/dist/types/src/testing/index.d.ts +1 -1
  73. package/dist/types/src/testing/index.d.ts.map +1 -1
  74. package/dist/types/src/testing/types.d.ts +21 -0
  75. package/dist/types/src/testing/types.d.ts.map +1 -0
  76. package/dist/types/src/translations.d.ts +5 -1
  77. package/dist/types/src/translations.d.ts.map +1 -1
  78. package/dist/types/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +43 -28
  80. package/src/components/Board/Board.stories.tsx +273 -0
  81. package/src/components/Board/Board.tsx +191 -0
  82. package/src/components/Board/Column.tsx +300 -0
  83. package/src/components/Board/Item.tsx +116 -0
  84. package/src/components/{Layout → Board}/index.ts +1 -1
  85. package/src/components/Focus/Focus.tsx +26 -17
  86. package/src/components/Focus/index.ts +0 -2
  87. package/src/components/Mosaic/Container.tsx +289 -0
  88. package/src/components/Mosaic/Mosaic.tsx +35 -913
  89. package/src/components/Mosaic/Placeholder.tsx +133 -0
  90. package/src/components/Mosaic/Root.tsx +292 -0
  91. package/src/components/{Stack → Mosaic}/Stack.stories.tsx +30 -26
  92. package/src/components/Mosaic/Stack.tsx +246 -0
  93. package/src/components/Mosaic/Tile.tsx +242 -0
  94. package/src/components/Mosaic/extension.ts +1 -1
  95. package/src/components/Mosaic/styles.ts +9 -8
  96. package/src/components/Mosaic/types.ts +15 -3
  97. package/src/components/index.ts +1 -4
  98. package/src/hooks/index.ts +1 -0
  99. package/src/hooks/useContainerDebug.tsx +4 -2
  100. package/src/hooks/useDefaultColumnEventHandler.ts +35 -0
  101. package/src/hooks/useEventHandlerAdapter.ts +15 -6
  102. package/src/hooks/useVisibleItems.ts +7 -2
  103. package/src/playwright/board-manager.ts +154 -0
  104. package/src/playwright/index.ts +13 -0
  105. package/src/playwright/playwright.config.ts +17 -0
  106. package/src/playwright/smoke.spec.ts +138 -0
  107. package/src/testing/CardContainer.tsx +30 -18
  108. package/src/testing/DefaultStackTile.tsx +27 -16
  109. package/src/testing/index.ts +1 -1
  110. package/src/testing/types.ts +38 -0
  111. package/src/translations.ts +5 -1
  112. package/dist/lib/browser/chunk-UMBPEG4F.mjs +0 -1637
  113. package/dist/lib/browser/chunk-UMBPEG4F.mjs.map +0 -7
  114. package/dist/lib/browser/index.css +0 -13
  115. package/dist/lib/browser/index.css.map +0 -7
  116. package/dist/lib/browser/testing/index.css +0 -13
  117. package/dist/lib/browser/testing/index.css.map +0 -7
  118. package/dist/lib/node-esm/chunk-X6GV7SMK.mjs +0 -1639
  119. package/dist/lib/node-esm/chunk-X6GV7SMK.mjs.map +0 -7
  120. package/dist/lib/node-esm/index.css +0 -13
  121. package/dist/lib/node-esm/index.css.map +0 -7
  122. package/dist/lib/node-esm/testing/index.css +0 -13
  123. package/dist/lib/node-esm/testing/index.css.map +0 -7
  124. package/dist/types/src/components/Card/Card.d.ts +0 -121
  125. package/dist/types/src/components/Card/Card.d.ts.map +0 -1
  126. package/dist/types/src/components/Card/Card.stories.d.ts +0 -21
  127. package/dist/types/src/components/Card/Card.stories.d.ts.map +0 -1
  128. package/dist/types/src/components/Card/CardDragPreview.d.ts +0 -6
  129. package/dist/types/src/components/Card/CardDragPreview.d.ts.map +0 -1
  130. package/dist/types/src/components/Card/index.d.ts +0 -4
  131. package/dist/types/src/components/Card/index.d.ts.map +0 -1
  132. package/dist/types/src/components/Card/styles.d.ts +0 -13
  133. package/dist/types/src/components/Card/styles.d.ts.map +0 -1
  134. package/dist/types/src/components/Focus/styles.d.ts +0 -6
  135. package/dist/types/src/components/Focus/styles.d.ts.map +0 -1
  136. package/dist/types/src/components/Image/Image.d.ts +0 -14
  137. package/dist/types/src/components/Image/Image.d.ts.map +0 -1
  138. package/dist/types/src/components/Image/Image.stories.d.ts +0 -33
  139. package/dist/types/src/components/Image/Image.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/Image/index.d.ts +0 -2
  141. package/dist/types/src/components/Image/index.d.ts.map +0 -1
  142. package/dist/types/src/components/Layout/Layout.d.ts +0 -33
  143. package/dist/types/src/components/Layout/Layout.d.ts.map +0 -1
  144. package/dist/types/src/components/Layout/Layout.stories.d.ts +0 -10
  145. package/dist/types/src/components/Layout/Layout.stories.d.ts.map +0 -1
  146. package/dist/types/src/components/Layout/index.d.ts +0 -2
  147. package/dist/types/src/components/Layout/index.d.ts.map +0 -1
  148. package/dist/types/src/components/Mosaic/Mosaic.stories.d.ts.map +0 -1
  149. package/dist/types/src/components/Scrollable/Scrollable.d.ts +0 -24
  150. package/dist/types/src/components/Scrollable/Scrollable.d.ts.map +0 -1
  151. package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts +0 -10
  152. package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts.map +0 -1
  153. package/dist/types/src/components/Scrollable/index.d.ts +0 -2
  154. package/dist/types/src/components/Scrollable/index.d.ts.map +0 -1
  155. package/dist/types/src/components/Stack/Stack.d.ts +0 -22
  156. package/dist/types/src/components/Stack/Stack.d.ts.map +0 -1
  157. package/dist/types/src/components/Stack/Stack.stories.d.ts.map +0 -1
  158. package/dist/types/src/components/Stack/index.d.ts +0 -2
  159. package/dist/types/src/components/Stack/index.d.ts.map +0 -1
  160. package/dist/types/src/testing/Board.d.ts +0 -45
  161. package/dist/types/src/testing/Board.d.ts.map +0 -1
  162. package/dist/types/src/testing/Slot.stories.d.ts +0 -14
  163. package/dist/types/src/testing/Slot.stories.d.ts.map +0 -1
  164. package/dist/types/src/testing/Virtualizer.stories.d.ts +0 -12
  165. package/dist/types/src/testing/Virtualizer.stories.d.ts.map +0 -1
  166. package/src/components/Card/Card.stories.tsx +0 -148
  167. package/src/components/Card/Card.tsx +0 -458
  168. package/src/components/Card/CardDragPreview.tsx +0 -23
  169. package/src/components/Card/index.ts +0 -8
  170. package/src/components/Card/styles.ts +0 -23
  171. package/src/components/Focus/styles.ts +0 -24
  172. package/src/components/Image/Image.stories.tsx +0 -86
  173. package/src/components/Image/Image.tsx +0 -223
  174. package/src/components/Image/index.ts +0 -5
  175. package/src/components/Layout/Layout.stories.tsx +0 -59
  176. package/src/components/Layout/Layout.tsx +0 -108
  177. package/src/components/Mosaic/Mosaic.stories.tsx +0 -113
  178. package/src/components/Scrollable/Scrollable.stories.tsx +0 -59
  179. package/src/components/Scrollable/Scrollable.tsx +0 -88
  180. package/src/components/Scrollable/index.ts +0 -5
  181. package/src/components/Scrollable/scrollable.css +0 -20
  182. package/src/components/Stack/Stack.tsx +0 -210
  183. package/src/components/Stack/index.ts +0 -5
  184. package/src/testing/Board.tsx +0 -278
  185. package/src/testing/Slot.stories.tsx +0 -101
  186. package/src/testing/Virtualizer.stories.tsx +0 -133
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-mosaic",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.c85a9c8dae",
4
4
  "description": "Modular UI components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -19,6 +19,12 @@
19
19
  "browser": "./dist/lib/browser/index.mjs",
20
20
  "node": "./dist/lib/node-esm/index.mjs"
21
21
  },
22
+ "./playwright": {
23
+ "source": "./src/playwright/index.ts",
24
+ "types": "./dist/types/src/playwright/index.d.ts",
25
+ "browser": "./dist/lib/browser/playwright/index.mjs",
26
+ "node": "./dist/lib/node-esm/playwright/index.mjs"
27
+ },
22
28
  "./testing": {
23
29
  "source": "./src/testing/index.ts",
24
30
  "types": "./dist/types/src/testing/index.d.ts",
@@ -28,7 +34,14 @@
28
34
  },
29
35
  "types": "dist/types/src/index.d.ts",
30
36
  "typesVersions": {
31
- "*": {}
37
+ "*": {
38
+ "playwright": [
39
+ "dist/types/src/playwright/index.d.ts"
40
+ ],
41
+ "testing": [
42
+ "dist/types/src/testing/index.d.ts"
43
+ ]
44
+ }
32
45
  },
33
46
  "files": [
34
47
  "dist",
@@ -49,42 +62,44 @@
49
62
  "@radix-ui/react-use-controllable-state": "1.1.0",
50
63
  "@tanstack/react-virtual": "^3.13.18",
51
64
  "bind-event-listener": "^3.0.0",
52
- "overlayscrollbars": "^2.13.0",
53
- "overlayscrollbars-react": "^0.5.6",
54
- "@dxos/async": "0.8.4-main.bc674ce",
55
- "@dxos/echo": "0.8.4-main.bc674ce",
56
- "@dxos/invariant": "0.8.4-main.bc674ce",
57
- "@dxos/keys": "0.8.4-main.bc674ce",
58
- "@dxos/log": "0.8.4-main.bc674ce",
59
- "@dxos/react-client": "0.8.4-main.bc674ce",
60
- "@dxos/react-ui-dnd": "0.8.4-main.bc674ce",
61
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.bc674ce",
62
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
63
- "@dxos/util": "0.8.4-main.bc674ce"
65
+ "@dxos/async": "0.8.4-main.c85a9c8dae",
66
+ "@dxos/echo": "0.8.4-main.c85a9c8dae",
67
+ "@dxos/invariant": "0.8.4-main.c85a9c8dae",
68
+ "@dxos/keys": "0.8.4-main.c85a9c8dae",
69
+ "@dxos/log": "0.8.4-main.c85a9c8dae",
70
+ "@dxos/react-client": "0.8.4-main.c85a9c8dae",
71
+ "@dxos/react-ui-menu": "0.8.4-main.c85a9c8dae",
72
+ "@dxos/react-ui-dnd": "0.8.4-main.c85a9c8dae",
73
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.c85a9c8dae",
74
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae",
75
+ "@dxos/util": "0.8.4-main.c85a9c8dae"
64
76
  },
65
77
  "devDependencies": {
78
+ "@effect-atom/atom-react": "^0.5.0",
66
79
  "@types/react": "~19.2.7",
67
80
  "@types/react-dom": "~19.2.3",
68
- "effect": "3.19.11",
81
+ "effect": "3.19.16",
69
82
  "react": "~19.2.3",
70
83
  "react-dom": "~19.2.3",
71
- "vite": "7.1.9",
72
- "@dxos/random": "0.8.4-main.bc674ce",
73
- "@dxos/react-ui": "0.8.4-main.bc674ce",
74
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.bc674ce",
75
- "@dxos/schema": "0.8.4-main.bc674ce",
76
- "@dxos/storybook-utils": "0.8.4-main.bc674ce",
77
- "@dxos/test-utils": "0.8.4-main.bc674ce",
78
- "@dxos/types": "0.8.4-main.bc674ce",
79
- "@dxos/ui-theme": "0.8.4-main.bc674ce"
84
+ "vite": "^7.1.11",
85
+ "@dxos/echo-atom": "0.8.4-main.c85a9c8dae",
86
+ "@dxos/random": "0.8.4-main.c85a9c8dae",
87
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.c85a9c8dae",
88
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
89
+ "@dxos/schema": "0.8.4-main.c85a9c8dae",
90
+ "@dxos/test-utils": "0.8.4-main.c85a9c8dae",
91
+ "@dxos/storybook-utils": "0.8.4-main.c85a9c8dae",
92
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae",
93
+ "@dxos/types": "0.8.4-main.c85a9c8dae"
80
94
  },
81
95
  "peerDependencies": {
82
- "effect": "3.19.11",
96
+ "@effect-atom/atom-react": "^0.5.0",
97
+ "effect": "3.19.16",
83
98
  "react": "~19.2.3",
84
99
  "react-dom": "~19.2.3",
85
- "@dxos/random": "0.8.4-main.bc674ce",
86
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
87
- "@dxos/react-ui": "0.8.4-main.bc674ce"
100
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
101
+ "@dxos/random": "0.8.4-main.c85a9c8dae",
102
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae"
88
103
  },
89
104
  "publishConfig": {
90
105
  "access": "public"
@@ -0,0 +1,273 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useContext, useMemo } from 'react';
8
+ import { expect, within } from 'storybook/test';
9
+
10
+ import { type Database, Filter, Obj, Ref } from '@dxos/echo';
11
+ import { AtomObj, AtomQuery } from '@dxos/echo-atom';
12
+ import { invariant } from '@dxos/invariant';
13
+ import { faker } from '@dxos/random';
14
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { withRegistry } from '@dxos/storybook-utils';
17
+ import { mx } from '@dxos/ui-theme';
18
+
19
+ import { useEventHandlerAdapter } from '../../hooks';
20
+ import { TestColumn, TestItem } from '../../testing';
21
+ import { translations } from '../../translations';
22
+ import { Focus } from '../Focus';
23
+ import { Mosaic, type MosaicEventHandler } from '../Mosaic';
24
+
25
+ import { Board, type BoardModel } from './Board';
26
+ import { DefaultBoardColumn } from './Column';
27
+
28
+ faker.seed(999);
29
+
30
+ const randomItems = () => faker.number.int({ min: 0, max: 20 });
31
+
32
+ const createTestData = (db: Database.Database, columns: number, items?: (column: number) => number) => {
33
+ Array.from({ length: columns }).forEach((_, i) => {
34
+ db.add(
35
+ Obj.make(TestColumn, {
36
+ name: `Column ${i}`,
37
+ items: Array.from({ length: items?.(i) ?? 0 }).map((_, j) => {
38
+ const item = db.add(
39
+ Obj.make(TestItem, {
40
+ name: faker.lorem.sentence(3),
41
+ description: faker.lorem.paragraph(1),
42
+ label: `${String.fromCharCode(65 + i)}-${j}`,
43
+ }),
44
+ );
45
+
46
+ return Ref.make(item);
47
+ }),
48
+ }),
49
+ );
50
+ });
51
+ };
52
+
53
+ type StoryProps = {
54
+ columns?: number;
55
+ items?: (column: number) => number;
56
+ debug?: boolean;
57
+ };
58
+
59
+ type TestBoardModelResult = {
60
+ model: BoardModel<TestColumn, TestItem>;
61
+ eventHandler: MosaicEventHandler<TestColumn>;
62
+ };
63
+
64
+ const useTestBoardModel = (): TestBoardModelResult => {
65
+ const { space } = useClientStory();
66
+ const registry = useContext(RegistryContext);
67
+
68
+ const { model, orderedColumnsAtom, orderAtom } = useMemo(() => {
69
+ const getColumnId = (data: TestColumn) => data.id;
70
+ const getItemId = (data: TestItem) => data.id;
71
+
72
+ // In-memory column order (ids only). Empty until the user first reorders; then persist drag/drop order.
73
+ const orderAtom = Atom.make<string[]>([]);
74
+
75
+ // Source of truth for which columns exist; subscribed to ECHO query.
76
+ const columnsAtom =
77
+ space?.db != null ? AtomQuery.make(space.db, Filter.type(TestColumn)) : Atom.make<TestColumn[]>([]);
78
+
79
+ // If orderAtom is empty, use Echo order as-is; else sort by orderAtom and append new columns.
80
+ const orderedColumnsAtom = Atom.make((get) => {
81
+ const cols = get(columnsAtom);
82
+ const order = get(orderAtom);
83
+ if (order.length === 0) return cols;
84
+ const byId = new Map(cols.map((column) => [getColumnId(column), column]));
85
+ const ordered = order.map((id) => byId.get(id)).filter((column): column is TestColumn => column != null);
86
+ const appended = cols.filter((column) => !order.includes(getColumnId(column)));
87
+ return [...ordered, ...appended];
88
+ });
89
+
90
+ const itemsAtomFamily =
91
+ space?.db != null
92
+ ? Atom.family((column: TestColumn) =>
93
+ Atom.make((get) => {
94
+ const refs = get(AtomObj.makeProperty(column, 'items'));
95
+ return (
96
+ refs
97
+ ?.map((ref: Ref.Ref<TestItem>) => get(AtomObj.makeWithReactive(ref)))
98
+ .filter((item): item is TestItem => item != null) ?? []
99
+ );
100
+ }),
101
+ )
102
+ : Atom.family((_column: TestColumn) => Atom.make<TestItem[]>([]));
103
+
104
+ const model = {
105
+ getColumnId,
106
+ getItemId,
107
+ isColumn: (obj: unknown): obj is TestColumn => Obj.isObject(obj) && Obj.instanceOf(TestColumn, obj),
108
+ isItem: (obj: unknown): obj is TestItem => Obj.isObject(obj) && Obj.instanceOf(TestItem, obj),
109
+ columns: orderedColumnsAtom,
110
+ items: (column: TestColumn) => itemsAtomFamily(column),
111
+ getColumns: () => registry.get(orderedColumnsAtom),
112
+ getItems: (column: TestColumn) => registry.get(itemsAtomFamily(column)),
113
+ onItemCreate: async (column: TestColumn) => {
114
+ invariant(space);
115
+ const item = space.db.add(
116
+ Obj.make(TestItem, {
117
+ name: faker.lorem.sentence(3),
118
+ description: faker.lorem.paragraph(1),
119
+ }),
120
+ );
121
+
122
+ Obj.change(column, (column) => {
123
+ column.items.push(Ref.make(item));
124
+ });
125
+
126
+ return item;
127
+ },
128
+ onItemDelete: (column: TestColumn, current: TestItem) => {
129
+ Obj.change(column, (mutableColumn) => {
130
+ if (!mutableColumn.items) {
131
+ return;
132
+ }
133
+ const idx = mutableColumn.items.findIndex((ref: Ref.Ref<TestItem>) => ref.target?.id === current?.id);
134
+ if (idx !== -1) {
135
+ mutableColumn.items.splice(idx, 1);
136
+ }
137
+ });
138
+ },
139
+ } satisfies BoardModel<TestColumn, TestItem>;
140
+
141
+ return { model, orderedColumnsAtom, orderAtom };
142
+ }, [space?.db, registry]);
143
+
144
+ const orderedColumns = useAtomValue(model.columns);
145
+ const eventHandler = useEventHandlerAdapter<TestColumn, TestColumn>({
146
+ id: 'board',
147
+ items: orderedColumns,
148
+ getId: model.getColumnId,
149
+ get: (data) => data,
150
+ make: (object) => object,
151
+ canDrop: ({ source }) => model.isColumn(source.data),
152
+ // On drag/drop, adapter mutates a copy of the list; we write the new order into orderAtom so it persists in memory.
153
+ onChange: (mutate) => {
154
+ const current = registry.get(orderedColumnsAtom);
155
+ const next = [...current];
156
+ mutate(next);
157
+ registry.set(
158
+ orderAtom,
159
+ next.map((column) => model.getColumnId(column)),
160
+ );
161
+ },
162
+ });
163
+
164
+ return { model, eventHandler };
165
+ };
166
+
167
+ const DefaultStory = ({ debug = false }: StoryProps) => {
168
+ const { model, eventHandler } = useTestBoardModel();
169
+ const columns = useAtomValue(model.columns);
170
+
171
+ if (columns.length === 0) {
172
+ return <></>;
173
+ }
174
+
175
+ return (
176
+ <Mosaic.Root asChild debug={debug}>
177
+ <div role='none' className={mx('grid md:p-2 overflow-hidden', debug && 'grid-cols-[1fr_20rem] gap-2')}>
178
+ <Board.Root model={model}>
179
+ <Board.Content id='board' debug={debug} eventHandler={eventHandler} Tile={DefaultBoardColumn} />
180
+ </Board.Root>
181
+ {debug && (
182
+ <Focus.Group classNames='flex flex-col gap-2 overflow-hidden'>
183
+ <Board.Debug classNames='p-2' />
184
+ </Focus.Group>
185
+ )}
186
+ </div>
187
+ </Mosaic.Root>
188
+ );
189
+ };
190
+
191
+ const meta = {
192
+ title: 'ui/react-ui-mosaic/Board',
193
+ render: DefaultStory,
194
+ decorators: [
195
+ withRegistry,
196
+ withTheme({ platform: 'desktop' }),
197
+ withLayout({ layout: 'fullscreen' }),
198
+ withClientProvider({
199
+ types: [TestColumn, TestItem],
200
+ createIdentity: true,
201
+ createSpace: true,
202
+ onCreateSpace: ({ space }, context) => {
203
+ const args = context.args as StoryProps;
204
+ createTestData(space.db, args.columns ?? 0, args.items);
205
+ },
206
+ }),
207
+ ],
208
+ parameters: {
209
+ layout: 'fullscreen',
210
+ translations,
211
+ },
212
+ argTypes: {
213
+ columns: {
214
+ control: 'number',
215
+ },
216
+ debug: {
217
+ control: 'boolean',
218
+ },
219
+ },
220
+ } satisfies Meta<typeof DefaultStory>;
221
+
222
+ export default meta;
223
+
224
+ type Story = StoryObj<typeof meta>;
225
+
226
+ export const Default: Story = {
227
+ args: {
228
+ columns: 8,
229
+ items: randomItems,
230
+ },
231
+ };
232
+
233
+ export const SingleColumn: Story = {
234
+ args: {
235
+ columns: 1,
236
+ items: () => 1,
237
+ },
238
+ };
239
+
240
+ export const Empty: Story = {};
241
+
242
+ export const EmptyColumn: Story = {
243
+ args: {
244
+ columns: 1,
245
+ },
246
+ };
247
+
248
+ export const Debug: Story = {
249
+ args: {
250
+ debug: true,
251
+ columns: 2,
252
+ },
253
+ };
254
+
255
+ export const Spec: Story = {
256
+ args: {
257
+ columns: 3,
258
+ items: (column) => (column < 2 ? 5 : 0),
259
+ },
260
+ play: async () => {
261
+ const body = within(document.body);
262
+ const columns = await body.findAllByTestId('board-column', undefined, { timeout: 30_000 });
263
+ await expect(columns).toHaveLength(3);
264
+
265
+ // First two columns have items, third is empty.
266
+ for (const column of columns.slice(0, 2)) {
267
+ const items = within(column).queryAllByTestId('board-item');
268
+ await expect(items.length).toBeGreaterThan(0);
269
+ }
270
+ const items = within(columns[2]).queryAllByTestId('board-item');
271
+ await expect(items).toHaveLength(0);
272
+ },
273
+ };
@@ -0,0 +1,191 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Atom, useAtomValue } from '@effect-atom/atom-react';
6
+ import { createContext } from '@radix-ui/react-context';
7
+ import React, {
8
+ type PropsWithChildren,
9
+ type ReactElement,
10
+ type Ref as ReactRef,
11
+ forwardRef,
12
+ useRef,
13
+ useState,
14
+ } from 'react';
15
+
16
+ import { ScrollArea, type ThemedClassName } from '@dxos/react-ui';
17
+ import { Json } from '@dxos/react-ui-syntax-highlighter';
18
+ import { mx } from '@dxos/ui-theme';
19
+
20
+ import { useContainerDebug } from '../../hooks';
21
+ import { Focus } from '../Focus';
22
+ import {
23
+ type GetId,
24
+ Mosaic,
25
+ type MosaicEventHandler,
26
+ type MosaicPlaceholderProps,
27
+ type MosaicStackProps,
28
+ mosaicStyles,
29
+ useMosaic,
30
+ } from '../Mosaic';
31
+
32
+ import { BoardColumn, type BoardColumnProps, DefaultBoardColumn, useBoardColumn } from './Column';
33
+ import { BoardItem, type BoardItemProps } from './Item';
34
+
35
+ //
36
+ // Model
37
+ //
38
+
39
+ export interface BoardModel<TColumn = any, TItem = any> {
40
+ getColumnId: GetId<TColumn>;
41
+ getItemId: GetId<TItem>;
42
+ isColumn: (obj: unknown) => obj is TColumn;
43
+ isItem: (obj: unknown) => obj is TItem;
44
+ columns: Atom.Atom<readonly TColumn[] | TColumn[]>;
45
+ items: (column: TColumn) => Atom.Atom<TItem[]>;
46
+ getColumns: () => TColumn[];
47
+ getItems: (column: TColumn) => TItem[];
48
+ onColumnCreate?: () => Promise<TColumn>;
49
+ onColumnDelete?: (column: TColumn) => void;
50
+ onItemCreate?: (column: TColumn) => Promise<TItem>;
51
+ onItemDelete?: (column: TColumn, item: TItem) => void;
52
+ }
53
+
54
+ //
55
+ // Context
56
+ //
57
+
58
+ const BOARD_NAME = 'Board';
59
+
60
+ type BoardContextValue<TColumn = any, TItem = any> = {
61
+ model: BoardModel<TColumn, TItem>;
62
+ };
63
+
64
+ const [BoardContextProvider, useBoardContext] = createContext<BoardContextValue>(BOARD_NAME);
65
+
66
+ /** Hook to read the board model from context (e.g. in custom column tiles). Pass TColumn and TItem for typed model. */
67
+ function useBoard<TColumn = any, TItem = any>(displayName?: string): BoardContextValue<TColumn, TItem> {
68
+ return useBoardContext(displayName ?? BOARD_NAME) as BoardContextValue<TColumn, TItem>;
69
+ }
70
+
71
+ //
72
+ // Root
73
+ //
74
+
75
+ const BOARD_ROOT_NAME = 'Board.Root';
76
+
77
+ type BoardRootProps<TColumn = any, TItem = any> = PropsWithChildren<BoardContextValue<TColumn, TItem>>;
78
+
79
+ const BoardRootInner = ({ model, children }: BoardRootProps) => {
80
+ return <BoardContextProvider model={model}>{children}</BoardContextProvider>;
81
+ };
82
+
83
+ BoardRootInner.displayName = BOARD_ROOT_NAME;
84
+
85
+ const BoardRoot = BoardRootInner as <TColumn = any, TItem = any>(props: BoardRootProps<TColumn, TItem>) => ReactElement;
86
+
87
+ //
88
+ // Content
89
+ //
90
+
91
+ const BOARD_CONTENT_NAME = 'Board.Content';
92
+
93
+ type BoardContentProps<TColumn = any> = ThemedClassName<{
94
+ id: string;
95
+ debug?: boolean;
96
+ eventHandler?: MosaicEventHandler<TColumn>;
97
+ Tile?: MosaicStackProps<TColumn>['Tile'];
98
+ }>;
99
+
100
+ const BoardContentInner = forwardRef<HTMLDivElement, BoardContentProps>(
101
+ ({ classNames, id: _id, debug, eventHandler, Tile = DefaultBoardColumn }, forwardedRef) => {
102
+ const { model } = useBoardContext(BOARD_CONTENT_NAME);
103
+ const [DebugInfo, debugHandler] = useContainerDebug(debug);
104
+ const [viewport, setViewport] = useState<HTMLElement | null>(null);
105
+
106
+ const items = useAtomValue(model.columns);
107
+
108
+ return (
109
+ <div ref={forwardedRef} className={mx('flex h-full w-full overflow-hidden', classNames)}>
110
+ <Focus.Group asChild orientation='horizontal'>
111
+ <Mosaic.Container
112
+ asChild
113
+ withFocus
114
+ orientation='horizontal'
115
+ autoScroll={viewport}
116
+ eventHandler={eventHandler}
117
+ debug={debugHandler}
118
+ >
119
+ <ScrollArea.Root orientation='horizontal' classNames='md:pt-3' margin padding>
120
+ <ScrollArea.Viewport classNames='snap-mandatory snap-x md:snap-none' ref={setViewport}>
121
+ <Mosaic.Stack items={items} getId={model.getColumnId} Tile={Tile} debug={debug} />
122
+ </ScrollArea.Viewport>
123
+ </ScrollArea.Root>
124
+ </Mosaic.Container>
125
+ </Focus.Group>
126
+ <DebugInfo />
127
+ </div>
128
+ );
129
+ },
130
+ );
131
+
132
+ BoardContentInner.displayName = BOARD_CONTENT_NAME;
133
+
134
+ const BoardContent = BoardContentInner as <TColumn = any>(
135
+ props: BoardContentProps<TColumn> & { ref?: ReactRef<HTMLDivElement> },
136
+ ) => ReactElement;
137
+
138
+ //
139
+ // Placeholder
140
+ //
141
+
142
+ const BOARD_PLACEHOLDER_NAME = 'Board.Placeholder';
143
+
144
+ const BoardPlaceholder = (props: MosaicPlaceholderProps<number>) => {
145
+ return (
146
+ <Mosaic.Placeholder {...props} classNames={mosaicStyles.placeholder.root}>
147
+ <div
148
+ className={mx('flex h-full border border-dashed border-separator rounded-xs', mosaicStyles.placeholder.content)}
149
+ />
150
+ </Mosaic.Placeholder>
151
+ );
152
+ };
153
+
154
+ BoardPlaceholder.displayName = BOARD_PLACEHOLDER_NAME;
155
+
156
+ //
157
+ // Debug
158
+ //
159
+
160
+ const BOARD_DEBUG_NAME = 'Board.Debug';
161
+
162
+ export const BoardDebug = forwardRef<HTMLDivElement, ThemedClassName>(({ classNames }, forwardedRef) => {
163
+ const { containers, dragging } = useMosaic(BOARD_DEBUG_NAME);
164
+ const counter = useRef(0);
165
+ return (
166
+ <Json
167
+ data={{ containers, dragging, count: counter.current++ }}
168
+ classNames={mx('text-xs', classNames)}
169
+ ref={forwardedRef}
170
+ />
171
+ );
172
+ });
173
+
174
+ BoardDebug.displayName = BOARD_DEBUG_NAME;
175
+
176
+ //
177
+ // Board
178
+ //
179
+
180
+ export const Board = {
181
+ Root: BoardRoot,
182
+ Content: BoardContent,
183
+ Column: BoardColumn,
184
+ Item: BoardItem,
185
+ Placeholder: BoardPlaceholder,
186
+ Debug: BoardDebug,
187
+ };
188
+
189
+ export { useBoard, useBoardColumn };
190
+
191
+ export type { BoardRootProps, BoardContentProps, BoardColumnProps, BoardItemProps };