@dxos/react-ui-mosaic 0.3.3-main.006ca4d

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 (153) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +45 -0
  3. package/dist/lib/browser/index.mjs +596 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/types/src/index.d.ts +2 -0
  7. package/dist/types/src/index.d.ts.map +1 -0
  8. package/dist/types/src/mosaic/Container.d.ts +65 -0
  9. package/dist/types/src/mosaic/Container.d.ts.map +1 -0
  10. package/dist/types/src/mosaic/Debug.d.ts +9 -0
  11. package/dist/types/src/mosaic/Debug.d.ts.map +1 -0
  12. package/dist/types/src/mosaic/DefaultComponent.d.ts +4 -0
  13. package/dist/types/src/mosaic/DefaultComponent.d.ts.map +1 -0
  14. package/dist/types/src/mosaic/DragOverlay.d.ts +11 -0
  15. package/dist/types/src/mosaic/DragOverlay.d.ts.map +1 -0
  16. package/dist/types/src/mosaic/Mosaic.d.ts +32 -0
  17. package/dist/types/src/mosaic/Mosaic.d.ts.map +1 -0
  18. package/dist/types/src/mosaic/Root.d.ts +24 -0
  19. package/dist/types/src/mosaic/Root.d.ts.map +1 -0
  20. package/dist/types/src/mosaic/SortableContext.d.ts +14 -0
  21. package/dist/types/src/mosaic/SortableContext.d.ts.map +1 -0
  22. package/dist/types/src/mosaic/Tile.d.ts +41 -0
  23. package/dist/types/src/mosaic/Tile.d.ts.map +1 -0
  24. package/dist/types/src/mosaic/hooks/index.d.ts +5 -0
  25. package/dist/types/src/mosaic/hooks/index.d.ts.map +1 -0
  26. package/dist/types/src/mosaic/hooks/useContainer.d.ts +4 -0
  27. package/dist/types/src/mosaic/hooks/useContainer.d.ts.map +1 -0
  28. package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts +10 -0
  29. package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts.map +1 -0
  30. package/dist/types/src/mosaic/hooks/useMosaic.d.ts +2 -0
  31. package/dist/types/src/mosaic/hooks/useMosaic.d.ts.map +1 -0
  32. package/dist/types/src/mosaic/hooks/useSortedItems.d.ts +6 -0
  33. package/dist/types/src/mosaic/hooks/useSortedItems.d.ts.map +1 -0
  34. package/dist/types/src/mosaic/index.d.ts +5 -0
  35. package/dist/types/src/mosaic/index.d.ts.map +1 -0
  36. package/dist/types/src/mosaic/types.d.ts +9 -0
  37. package/dist/types/src/mosaic/types.d.ts.map +1 -0
  38. package/dist/types/src/mosaic/util/data.d.ts +3 -0
  39. package/dist/types/src/mosaic/util/data.d.ts.map +1 -0
  40. package/dist/types/src/mosaic/util/index.d.ts +4 -0
  41. package/dist/types/src/mosaic/util/index.d.ts.map +1 -0
  42. package/dist/types/src/mosaic/util/path.d.ts +14 -0
  43. package/dist/types/src/mosaic/util/path.d.ts.map +1 -0
  44. package/dist/types/src/mosaic/util/path.test.d.ts +2 -0
  45. package/dist/types/src/mosaic/util/path.test.d.ts.map +1 -0
  46. package/dist/types/src/mosaic/util/styles.d.ts +3 -0
  47. package/dist/types/src/mosaic/util/styles.d.ts.map +1 -0
  48. package/dist/types/src/testing/Cards.d.ts +10 -0
  49. package/dist/types/src/testing/Cards.d.ts.map +1 -0
  50. package/dist/types/src/testing/decorators.d.ts +4 -0
  51. package/dist/types/src/testing/decorators.d.ts.map +1 -0
  52. package/dist/types/src/testing/generator.d.ts +43 -0
  53. package/dist/types/src/testing/generator.d.ts.map +1 -0
  54. package/dist/types/src/testing/index.d.ts +5 -0
  55. package/dist/types/src/testing/index.d.ts.map +1 -0
  56. package/dist/types/src/testing/next-fractional-index.d.ts +5 -0
  57. package/dist/types/src/testing/next-fractional-index.d.ts.map +1 -0
  58. package/dist/types/src/views/Demo/Demo.stories.d.ts +13 -0
  59. package/dist/types/src/views/Demo/Demo.stories.d.ts.map +1 -0
  60. package/dist/types/src/views/Demo/Echo.stories.d.ts +12 -0
  61. package/dist/types/src/views/Demo/Echo.stories.d.ts.map +1 -0
  62. package/dist/types/src/views/Grid/Grid.d.ts +30 -0
  63. package/dist/types/src/views/Grid/Grid.d.ts.map +1 -0
  64. package/dist/types/src/views/Grid/Grid.stories.d.ts +13 -0
  65. package/dist/types/src/views/Grid/Grid.stories.d.ts.map +1 -0
  66. package/dist/types/src/views/Grid/index.d.ts +3 -0
  67. package/dist/types/src/views/Grid/index.d.ts.map +1 -0
  68. package/dist/types/src/views/Grid/layout.d.ts +27 -0
  69. package/dist/types/src/views/Grid/layout.d.ts.map +1 -0
  70. package/dist/types/src/views/Grid/testing.d.ts +10 -0
  71. package/dist/types/src/views/Grid/testing.d.ts.map +1 -0
  72. package/dist/types/src/views/Kanban/Echo.stories.d.ts +18 -0
  73. package/dist/types/src/views/Kanban/Echo.stories.d.ts.map +1 -0
  74. package/dist/types/src/views/Kanban/Kanban.d.ts +11 -0
  75. package/dist/types/src/views/Kanban/Kanban.d.ts.map +1 -0
  76. package/dist/types/src/views/Kanban/Kanban.stories.d.ts +28 -0
  77. package/dist/types/src/views/Kanban/Kanban.stories.d.ts.map +1 -0
  78. package/dist/types/src/views/Kanban/index.d.ts +2 -0
  79. package/dist/types/src/views/Kanban/index.d.ts.map +1 -0
  80. package/dist/types/src/views/Kanban/testing.d.ts +20 -0
  81. package/dist/types/src/views/Kanban/testing.d.ts.map +1 -0
  82. package/dist/types/src/views/Stack/Columns.stories.d.ts +34 -0
  83. package/dist/types/src/views/Stack/Columns.stories.d.ts.map +1 -0
  84. package/dist/types/src/views/Stack/Stack.d.ts +9 -0
  85. package/dist/types/src/views/Stack/Stack.d.ts.map +1 -0
  86. package/dist/types/src/views/Stack/Stack.stories.d.ts +39 -0
  87. package/dist/types/src/views/Stack/Stack.stories.d.ts.map +1 -0
  88. package/dist/types/src/views/Stack/index.d.ts +2 -0
  89. package/dist/types/src/views/Stack/index.d.ts.map +1 -0
  90. package/dist/types/src/views/Stack/testing.d.ts +9 -0
  91. package/dist/types/src/views/Stack/testing.d.ts.map +1 -0
  92. package/dist/types/src/views/Tree/Graph.stories.d.ts +21 -0
  93. package/dist/types/src/views/Tree/Graph.stories.d.ts.map +1 -0
  94. package/dist/types/src/views/Tree/Tree.d.ts +13 -0
  95. package/dist/types/src/views/Tree/Tree.d.ts.map +1 -0
  96. package/dist/types/src/views/Tree/Tree.stories.d.ts +41 -0
  97. package/dist/types/src/views/Tree/Tree.stories.d.ts.map +1 -0
  98. package/dist/types/src/views/Tree/index.d.ts +2 -0
  99. package/dist/types/src/views/Tree/index.d.ts.map +1 -0
  100. package/dist/types/src/views/Tree/testing.d.ts +15 -0
  101. package/dist/types/src/views/Tree/testing.d.ts.map +1 -0
  102. package/dist/types/src/views/index.d.ts +5 -0
  103. package/dist/types/src/views/index.d.ts.map +1 -0
  104. package/package.json +67 -0
  105. package/src/index.ts +5 -0
  106. package/src/mosaic/Container.tsx +137 -0
  107. package/src/mosaic/Debug.tsx +24 -0
  108. package/src/mosaic/DefaultComponent.tsx +26 -0
  109. package/src/mosaic/DragOverlay.tsx +136 -0
  110. package/src/mosaic/Mosaic.ts +84 -0
  111. package/src/mosaic/Root.tsx +255 -0
  112. package/src/mosaic/SortableContext.tsx +60 -0
  113. package/src/mosaic/Tile.tsx +189 -0
  114. package/src/mosaic/hooks/index.ts +8 -0
  115. package/src/mosaic/hooks/useContainer.ts +16 -0
  116. package/src/mosaic/hooks/useItemsWithPreview.ts +64 -0
  117. package/src/mosaic/hooks/useMosaic.ts +11 -0
  118. package/src/mosaic/hooks/useSortedItems.ts +14 -0
  119. package/src/mosaic/index.ts +9 -0
  120. package/src/mosaic/types.ts +13 -0
  121. package/src/mosaic/util/data.ts +19 -0
  122. package/src/mosaic/util/index.ts +7 -0
  123. package/src/mosaic/util/path.test.ts +26 -0
  124. package/src/mosaic/util/path.ts +21 -0
  125. package/src/mosaic/util/styles.ts +10 -0
  126. package/src/testing/Cards.tsx +88 -0
  127. package/src/testing/decorators.tsx +20 -0
  128. package/src/testing/generator.ts +135 -0
  129. package/src/testing/index.ts +9 -0
  130. package/src/testing/next-fractional-index.ts +36 -0
  131. package/src/views/Demo/Demo.stories.tsx +79 -0
  132. package/src/views/Demo/Echo.stories.tsx +74 -0
  133. package/src/views/Grid/Grid.stories.tsx +48 -0
  134. package/src/views/Grid/Grid.tsx +248 -0
  135. package/src/views/Grid/index.ts +7 -0
  136. package/src/views/Grid/layout.ts +42 -0
  137. package/src/views/Grid/testing.tsx +91 -0
  138. package/src/views/Kanban/Echo.stories.tsx +67 -0
  139. package/src/views/Kanban/Kanban.stories.tsx +48 -0
  140. package/src/views/Kanban/Kanban.tsx +139 -0
  141. package/src/views/Kanban/index.ts +5 -0
  142. package/src/views/Kanban/testing.tsx +231 -0
  143. package/src/views/Stack/Columns.stories.tsx +61 -0
  144. package/src/views/Stack/Stack.stories.tsx +62 -0
  145. package/src/views/Stack/Stack.tsx +58 -0
  146. package/src/views/Stack/index.ts +5 -0
  147. package/src/views/Stack/testing.tsx +77 -0
  148. package/src/views/Tree/Graph.stories.tsx +36 -0
  149. package/src/views/Tree/Tree.stories.tsx +92 -0
  150. package/src/views/Tree/Tree.tsx +128 -0
  151. package/src/views/Tree/index.ts +5 -0
  152. package/src/views/Tree/testing.tsx +156 -0
  153. package/src/views/index.ts +8 -0
@@ -0,0 +1,248 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { useDroppable } from '@dnd-kit/core';
8
+ import { PlusCircle } from '@phosphor-icons/react';
9
+ import defaultsDeep from 'lodash.defaultsdeep';
10
+ import React, { type FC, useState, useMemo, useEffect } from 'react';
11
+ import { useResizeDetector } from 'react-resize-detector';
12
+
13
+ import { Button, useMediaQuery } from '@dxos/react-ui';
14
+ import { getSize, mx } from '@dxos/react-ui-theme';
15
+
16
+ import {
17
+ calculateCellWidth,
18
+ createMatrix,
19
+ getBounds,
20
+ getDimension,
21
+ getPanelBounds,
22
+ type Dimension,
23
+ type Position,
24
+ type Size,
25
+ } from './layout';
26
+ import { type MosaicContainerProps, type MosaicDataItem, Mosaic, Path, useMosaic } from '../../mosaic';
27
+
28
+ //
29
+ // Selection
30
+ //
31
+
32
+ // TODO(burdon): Factor out as mosaic standard.
33
+ type ControlledSelection = {
34
+ selected?: string;
35
+ onSelect?: (id: string) => void;
36
+ };
37
+
38
+ //
39
+ // Context.
40
+ //
41
+
42
+ type GridOptions = {
43
+ size: Size;
44
+ cellBounds: Dimension;
45
+ spacing?: number;
46
+ };
47
+
48
+ const defaultGridOptions: GridOptions = {
49
+ size: { x: 8, y: 8 },
50
+ cellBounds: { width: 280, height: 280 },
51
+ spacing: 8,
52
+ };
53
+
54
+ //
55
+ // Container.
56
+ //
57
+
58
+ export type GridLayout = { [id: string]: Position };
59
+
60
+ export type GridProps<TData extends MosaicDataItem = MosaicDataItem> = MosaicContainerProps<TData, Position> &
61
+ ControlledSelection & {
62
+ items?: TData[];
63
+ layout?: GridLayout;
64
+ options?: Partial<GridOptions>;
65
+ margin?: boolean;
66
+ square?: boolean;
67
+ debug?: boolean;
68
+ onCreate?: (position: Position) => void;
69
+ };
70
+
71
+ /**
72
+ * Grid content.
73
+ */
74
+ // TODO(burdon): Make generic (and forwardRef).
75
+ export const Grid = ({
76
+ id,
77
+ items = [],
78
+ layout = {},
79
+ options: opts,
80
+ margin,
81
+ square = true,
82
+ debug,
83
+ selected: controlledSelected,
84
+ Component = Mosaic.DefaultComponent,
85
+ className,
86
+ onDrop,
87
+ onSelect,
88
+ onCreate,
89
+ }: GridProps) => {
90
+ const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
91
+ const options = defaultsDeep({}, opts, defaultGridOptions);
92
+ const { matrix, bounds, cellBounds } = useMemo(() => {
93
+ // Change default cell bounds to screen width if mobile.
94
+ const cellWidth = calculateCellWidth(options.cellBounds.width, width ?? 0);
95
+ const cellBounds = {
96
+ width: cellWidth,
97
+ height: square ? cellWidth : options.cellBounds.height,
98
+ };
99
+
100
+ return {
101
+ matrix: createMatrix(options.size, ({ x, y }) => ({ x, y })),
102
+ bounds: getPanelBounds(options.size, cellBounds, options.spacing),
103
+ cellBounds,
104
+ };
105
+ }, [options, width]);
106
+
107
+ // No margin if mobile.
108
+ const [isNotMobile] = useMediaQuery('md');
109
+ const marginSize = margin && !isNotMobile ? Math.max(cellBounds.width, cellBounds.height) : 0;
110
+
111
+ const [selected, setSelected] = useState(controlledSelected);
112
+ useEffect(() => {
113
+ setSelected(controlledSelected);
114
+ if (controlledSelected) {
115
+ scrollToCenter(controlledSelected);
116
+ }
117
+ }, [controlledSelected]);
118
+
119
+ const scrollToCenter = (id: string) => {
120
+ const item = items.find((item) => item.id === id);
121
+ if (item && width && height) {
122
+ const pos = getBounds(layout[item.id], cellBounds);
123
+ const top = pos.top + marginSize - (height - cellBounds.height) / 2;
124
+ const left = pos.left + marginSize - (width - cellBounds.width) / 2;
125
+ containerRef.current!.scrollTo({ top, left, behavior: 'smooth' });
126
+ }
127
+ };
128
+
129
+ // TODO(burdon): Focus ring/navigation.
130
+ // TODO(burdon): Set center point on container (via translation?) Scale container to zoom.
131
+ const handleSelect = (id: string) => {
132
+ setSelected(id);
133
+ scrollToCenter(id);
134
+ onSelect?.(id);
135
+ };
136
+
137
+ useEffect(() => {
138
+ if (selected) {
139
+ scrollToCenter(selected);
140
+ }
141
+ }, [selected, width, height]);
142
+
143
+ return (
144
+ <Mosaic.Container
145
+ {...{
146
+ id,
147
+ Component,
148
+ getOverlayProps: () => ({ grow: true }),
149
+ getOverlayStyle: () => getDimension(cellBounds, options.spacing),
150
+ onDrop,
151
+ }}
152
+ >
153
+ <div className={mx('flex grow overflow-auto', className)}>
154
+ <div ref={containerRef} className={mx('grow overflow-auto snap-x snap-mandatory md:snap-none bg-neutral-600')}>
155
+ <div className='group block relative bg-neutral-500' style={{ ...bounds, margin: marginSize }}>
156
+ {matrix && (
157
+ <div style={{ padding: options.spacing }}>
158
+ <div className='relative'>
159
+ {matrix.map((row) =>
160
+ row.map(({ x, y }) => (
161
+ <GridCell
162
+ key={`${x}-${y}`}
163
+ path={id}
164
+ position={{ x, y }}
165
+ bounds={getBounds({ x, y }, cellBounds)}
166
+ padding={options.spacing}
167
+ onCreate={onCreate}
168
+ />
169
+ )),
170
+ )}
171
+ </div>
172
+ </div>
173
+ )}
174
+
175
+ {/* TODO(burdon): Events: onDoubleClick={() => handleSelect(id)} */}
176
+ <div>
177
+ {items.map((item) => {
178
+ const position = layout[item.id] ?? { x: 0, y: 0 };
179
+ return (
180
+ <Mosaic.DraggableTile
181
+ key={item.id}
182
+ item={item}
183
+ path={id}
184
+ position={position}
185
+ Component={Component}
186
+ draggableStyle={{
187
+ position: 'absolute',
188
+ ...getBounds(position, cellBounds, options.spacing),
189
+ }}
190
+ onSelect={() => handleSelect(item.id)}
191
+ // debug={debug}
192
+ />
193
+ );
194
+ })}
195
+ </div>
196
+ </div>
197
+
198
+ {debug && <Mosaic.Debug data={{ items: items?.length }} position='bottom-right' />}
199
+ </div>
200
+ </div>
201
+ </Mosaic.Container>
202
+ );
203
+ };
204
+
205
+ /**
206
+ * Grid cell.
207
+ */
208
+ const GridCell: FC<{
209
+ path: string;
210
+ position: Position;
211
+ bounds: Dimension;
212
+ padding?: number;
213
+ onCreate?: (position: Position) => void;
214
+ }> = ({ path, position, bounds, padding, onCreate }) => {
215
+ const { overItem } = useMosaic();
216
+ const isOverContainer = path === overItem?.path;
217
+ const { setNodeRef, isOver } = useDroppable({
218
+ id: Path.create(path, 'cell', `${position.x}-${position.y}`),
219
+ data: { path, position },
220
+ });
221
+
222
+ return (
223
+ <div
224
+ ref={setNodeRef}
225
+ style={{ ...bounds, padding }}
226
+ className='absolute flex justify-center items-center grow select-none cursor-pointer'
227
+ >
228
+ <div
229
+ className={mx(
230
+ 'group/cell hidden group-hover:flex w-full h-full items-center justify-center',
231
+ isOverContainer && 'flex',
232
+ 'box-border border-dashed border-4 border-neutral-600/50 rounded-lg',
233
+ 'transition ease-in-out duration-200 bg-neutral-500',
234
+ isOver && 'flex bg-neutral-600',
235
+ )}
236
+ >
237
+ <div className={mx('hidden group-hover/cell:flex', isOverContainer && 'hidden')}>
238
+ {onCreate && (
239
+ // TODO(burdon): Style button.
240
+ <Button variant='ghost' onClick={() => onCreate(position)}>
241
+ <PlusCircle className={getSize(8)} />
242
+ </Button>
243
+ )}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ );
248
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './layout';
6
+
7
+ export * from './Grid';
@@ -0,0 +1,42 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export type Size = { x: number; y: number };
6
+ export type Position = { x: number; y: number };
7
+ export type Dimension = { width: number; height: number };
8
+ export type Bounds = { left: number; top: number; width: number; height: number };
9
+
10
+ export const createMatrix = <TValue>(size: Size, value: (position: Position) => TValue): TValue[][] => {
11
+ const matrix: TValue[][] = [];
12
+ for (let x = 0; x < size.x; x++) {
13
+ matrix.push(Array.from({ length: size.y }, (_, y) => value({ x, y })));
14
+ }
15
+
16
+ return matrix;
17
+ };
18
+
19
+ export const getDimension = ({ width, height }: Dimension, spacing = 0): Dimension => ({
20
+ width: width - spacing * 2,
21
+ height: height - spacing * 2,
22
+ });
23
+
24
+ export const getBounds = ({ x, y }: Position, { width, height }: Dimension, spacing = 0): Bounds => ({
25
+ left: x * width + spacing * 2,
26
+ top: y * height + spacing * 2,
27
+ width: width - spacing * 2,
28
+ height: height - spacing * 2,
29
+ });
30
+
31
+ export const getPanelBounds = ({ x, y }: Position, { width, height }: Dimension, spacing = 0): Dimension => ({
32
+ width: x * width + spacing * 2,
33
+ height: y * height + spacing * 2,
34
+ });
35
+
36
+ export const calculateCellWidth = (
37
+ defaultWidth: number,
38
+ screenWidth: number,
39
+ options: { min: number; max: number } = { min: 260, max: 400 },
40
+ ) => {
41
+ return screenWidth > options.min && screenWidth < options.max ? screenWidth : defaultWidth;
42
+ };
@@ -0,0 +1,91 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { faker } from '@faker-js/faker';
6
+ import React, { useState } from 'react';
7
+
8
+ import { Grid, type GridLayout, type GridProps } from './Grid';
9
+ import { type Position } from './layout';
10
+ import { type MosaicDataItem, type MosaicDropEvent } from '../../mosaic';
11
+ import { ComplexCard, TestObjectGenerator } from '../../testing';
12
+
13
+ export type DemoGridProps = GridProps & {
14
+ initialItems?: MosaicDataItem[];
15
+ initialLayout?: GridLayout;
16
+ types?: string[];
17
+ debug?: boolean;
18
+ };
19
+
20
+ export const DemoGrid = ({
21
+ id = 'grid',
22
+ options = { size: { x: 8, y: 8 } },
23
+ Component = ComplexCard,
24
+ initialItems,
25
+ initialLayout,
26
+ types,
27
+ debug,
28
+ className,
29
+ }: DemoGridProps) => {
30
+ const [selected, setSelected] = useState<string>();
31
+ const [items, setItems] = useState<MosaicDataItem[]>(
32
+ initialItems ??
33
+ (() => {
34
+ const generator = new TestObjectGenerator({ types });
35
+ return generator.createObjects({ length: 12 });
36
+ }),
37
+ );
38
+ const [layout, setLayout] = useState<GridLayout>(
39
+ initialLayout ??
40
+ (() =>
41
+ items.reduce<GridLayout>((map, item, i) => {
42
+ map[item.id] = {
43
+ x: faker.number.int({ min: 0, max: (options.size?.x ?? 1) - 1 }),
44
+ y: faker.number.int({ min: 0, max: (options.size?.y ?? 1) - 1 }),
45
+ };
46
+ return map;
47
+ }, {})),
48
+ );
49
+
50
+ const handleDrop = ({ active, over }: MosaicDropEvent<Position>) => {
51
+ if (over.path !== id) {
52
+ setItems((items) => items.filter((item) => item.id !== active.item.id));
53
+ } else {
54
+ setItems((items) => {
55
+ if (items.findIndex((item) => item.id === active.item.id) === -1) {
56
+ return [active.item, ...items];
57
+ } else {
58
+ return items;
59
+ }
60
+ });
61
+ setLayout((layout) => ({ ...layout, [active.item.id]: over.position! }));
62
+ }
63
+ };
64
+
65
+ const handleCreate = (position: Position) => {
66
+ setItems((items) => {
67
+ const item = new TestObjectGenerator({ types }).createObject();
68
+ setTimeout(() => {
69
+ setSelected(item.id);
70
+ });
71
+ setLayout((layout) => ({ ...layout, [item.id]: position }));
72
+ return [item, ...items];
73
+ });
74
+ };
75
+
76
+ return (
77
+ <Grid
78
+ id={id}
79
+ items={items}
80
+ layout={layout}
81
+ options={options}
82
+ Component={Component}
83
+ className={className}
84
+ debug={debug}
85
+ selected={selected}
86
+ onSelect={setSelected}
87
+ onDrop={handleDrop}
88
+ onCreate={handleCreate}
89
+ />
90
+ );
91
+ };
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type PublicKey } from '@dxos/react-client';
8
+ import { Expando } from '@dxos/react-client/echo';
9
+ import { ClientSpaceDecorator } from '@dxos/react-client/testing';
10
+
11
+ import { Kanban } from './Kanban';
12
+ import { EchoKanban, columnValues } from './testing';
13
+ import { Mosaic } from '../../mosaic';
14
+ import { FullscreenDecorator, TestObjectGenerator, range } from '../../testing';
15
+
16
+ const generator = new TestObjectGenerator();
17
+
18
+ export default {
19
+ title: 'Views/Kanban',
20
+ component: Kanban,
21
+ render: ({
22
+ // TODO(wittjosiah): Can't use id because of ClientSpaceDecorator.
23
+ basePath = 'projects',
24
+ spaceKey,
25
+ debug,
26
+ }: {
27
+ basePath: string;
28
+ spaceKey: PublicKey;
29
+ debug: boolean;
30
+ }) => {
31
+ return (
32
+ <Mosaic.Root debug={debug}>
33
+ <Mosaic.DragOverlay />
34
+ <EchoKanban id={basePath} spaceKey={spaceKey} generator={generator} debug={debug} />
35
+ </Mosaic.Root>
36
+ );
37
+ },
38
+ decorators: [
39
+ FullscreenDecorator(),
40
+ ClientSpaceDecorator({
41
+ onCreateSpace: async (space) => {
42
+ const factory = generator.factories.project;
43
+ const objects = [
44
+ factory.schema,
45
+ ...range(factory.createObject, 10),
46
+ new Expando({
47
+ type: 'kanban',
48
+ title: 'Projects',
49
+ schema: factory.schema,
50
+ // TODO(burdon): Standardize with other story.
51
+ columnProp: 'status',
52
+ columnValues: columnValues.status,
53
+ objectPosition: {}, // TODO(burdon): Make this a CRDT.
54
+ }),
55
+ ];
56
+
57
+ // TODO(burdon): Batch API.
58
+ objects.forEach((object) => space.db.add(object));
59
+ },
60
+ }),
61
+ ],
62
+ parameters: {
63
+ layout: 'fullscreen',
64
+ },
65
+ };
66
+
67
+ export const ECHO = {};
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { faker } from '@faker-js/faker';
8
+ import React from 'react';
9
+
10
+ import { Kanban } from './Kanban';
11
+ import { DemoKanban, type DemoKanbanProps } from './testing';
12
+ import { Mosaic } from '../../mosaic';
13
+ import { ComplexCard, FullscreenDecorator, SimpleCard } from '../../testing';
14
+
15
+ faker.seed(3);
16
+
17
+ export default {
18
+ component: Kanban,
19
+ render: (args: DemoKanbanProps) => {
20
+ return (
21
+ <Mosaic.Root debug={args.debug}>
22
+ <Mosaic.DragOverlay />
23
+ <DemoKanban {...args} />
24
+ </Mosaic.Root>
25
+ );
26
+ },
27
+ decorators: [FullscreenDecorator()],
28
+ parameters: {
29
+ layout: 'fullscreen',
30
+ },
31
+ };
32
+
33
+ export const Default = {
34
+ args: {
35
+ Component: SimpleCard,
36
+ count: 3,
37
+ debug: true,
38
+ },
39
+ };
40
+
41
+ export const Complex = {
42
+ args: {
43
+ Component: ComplexCard,
44
+ types: ['document', 'image'],
45
+ count: 4,
46
+ debug: true,
47
+ },
48
+ };
@@ -0,0 +1,139 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef, useMemo } from 'react';
6
+
7
+ import { Card } from '@dxos/react-ui';
8
+ import { groupSurface, mx } from '@dxos/react-ui-theme';
9
+
10
+ import {
11
+ Mosaic,
12
+ type MosaicContainerProps,
13
+ type MosaicDataItem,
14
+ type MosaicTileComponent,
15
+ Path,
16
+ useItemsWithPreview,
17
+ useContainer,
18
+ } from '../../mosaic';
19
+
20
+ // Example:
21
+ // https://master--5fc05e08a4a65d0021ae0bf2.chromatic.com/?path=/story/presets-sortable-multiple-containers--basic-setup
22
+
23
+ export type KanbanColumn<TData extends MosaicDataItem = MosaicDataItem> = MosaicDataItem & {
24
+ title: string;
25
+ items: TData[];
26
+ };
27
+
28
+ export type KanbanProps<TData extends MosaicDataItem = MosaicDataItem> = MosaicContainerProps<TData, number> & {
29
+ columns: KanbanColumn<TData>[];
30
+ debug?: boolean;
31
+ };
32
+
33
+ export const Kanban = ({
34
+ id,
35
+ columns,
36
+ Component: TileComponent = Mosaic.DefaultComponent,
37
+ debug,
38
+ onDrop,
39
+ }: KanbanProps) => {
40
+ const Component = useMemo(() => OverlayComponent(id, TileComponent), [id, TileComponent]);
41
+
42
+ return (
43
+ <Mosaic.Container
44
+ {...{
45
+ id,
46
+ debug,
47
+ Component,
48
+ // Restrict columns to x-axis.
49
+ modifier: ({ path, item }, { transform }) =>
50
+ path === Path.create(id, item.id) ? { ...transform, y: 0 } : transform,
51
+ // Restrict to objects from other columns.
52
+ // TODO(burdon): Consider objects from other containers.
53
+ // TODO(wittjosiah): Allow override.
54
+ onOver: ({ active, over }) => (Path.length(active.path) >= Path.length(over.path) ? 'adopt' : 'reject'),
55
+ onDrop,
56
+ }}
57
+ >
58
+ <div className='grow flex overflow-y-hidden overflow-x-auto'>
59
+ <div className='flex'>
60
+ <Mosaic.SortableContext items={columns} direction='horizontal'>
61
+ {columns.map((column, index) => (
62
+ <Mosaic.SortableTile
63
+ key={column.id}
64
+ item={column}
65
+ path={id}
66
+ position={index}
67
+ Component={Component}
68
+ debug={debug}
69
+ />
70
+ ))}
71
+ </Mosaic.SortableContext>
72
+ </div>
73
+ </div>
74
+ </Mosaic.Container>
75
+ );
76
+ };
77
+
78
+ const OverlayComponent = (id: string, Component: MosaicTileComponent<any>): MosaicTileComponent<any> =>
79
+ forwardRef((props, ref) => {
80
+ const isColumn = Path.hasRoot(props.path, id) && Path.length(props.path) === 2;
81
+ if (isColumn && props.active === 'overlay') {
82
+ return (
83
+ // Needs to not override the main kanban path.
84
+ <Mosaic.Container {...{ id: `${id}-active`, Component }}>
85
+ <KanbanColumnComponent {...props} ref={ref} />
86
+ </Mosaic.Container>
87
+ );
88
+ }
89
+
90
+ return isColumn ? <KanbanColumnComponent {...props} ref={ref} /> : <Component {...props} ref={ref} />;
91
+ });
92
+
93
+ const KanbanColumnComponent: MosaicTileComponent<KanbanColumn> = forwardRef(
94
+ ({ path, item, active, draggableStyle, draggableProps, debug }, forwardRef) => {
95
+ const { id, title, items } = item;
96
+ const { Component } = useContainer();
97
+ const itemsWithPreview = useItemsWithPreview({ path, items });
98
+
99
+ return (
100
+ <div role='none' className='grow flex flex-col' ref={forwardRef}>
101
+ <div
102
+ className={mx(
103
+ groupSurface,
104
+ 'grow flex flex-col w-[300px] snap-center overflow-hidden m-1',
105
+ active === 'rearrange' && 'opacity-0',
106
+ )}
107
+ style={draggableStyle}
108
+ >
109
+ <Card.Root classNames='shrink-0 bg-cyan-200'>
110
+ <Card.Header>
111
+ <Card.DragHandle {...draggableProps} />
112
+ <Card.Title title={title} />
113
+ <Card.Menu />
114
+ </Card.Header>
115
+ </Card.Root>
116
+
117
+ <div className={mx('flex flex-col grow overflow-y-scroll')}>
118
+ <div className='flex flex-col my-1'>
119
+ <Mosaic.SortableContext id={path} items={itemsWithPreview} direction='vertical'>
120
+ {itemsWithPreview.map((item, index) => (
121
+ <Mosaic.SortableTile
122
+ key={item.id}
123
+ item={item}
124
+ path={path}
125
+ position={index}
126
+ Component={Component!}
127
+ className='m-1'
128
+ // debug={debug}
129
+ />
130
+ ))}
131
+ </Mosaic.SortableContext>
132
+ </div>
133
+ </div>
134
+ {debug && <Mosaic.Debug data={{ path, id, items: itemsWithPreview.length }} />}
135
+ </div>
136
+ </div>
137
+ );
138
+ },
139
+ );
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './Kanban';