@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.
- package/LICENSE +8 -0
- package/README.md +45 -0
- package/dist/lib/browser/index.mjs +596 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/Container.d.ts +65 -0
- package/dist/types/src/mosaic/Container.d.ts.map +1 -0
- package/dist/types/src/mosaic/Debug.d.ts +9 -0
- package/dist/types/src/mosaic/Debug.d.ts.map +1 -0
- package/dist/types/src/mosaic/DefaultComponent.d.ts +4 -0
- package/dist/types/src/mosaic/DefaultComponent.d.ts.map +1 -0
- package/dist/types/src/mosaic/DragOverlay.d.ts +11 -0
- package/dist/types/src/mosaic/DragOverlay.d.ts.map +1 -0
- package/dist/types/src/mosaic/Mosaic.d.ts +32 -0
- package/dist/types/src/mosaic/Mosaic.d.ts.map +1 -0
- package/dist/types/src/mosaic/Root.d.ts +24 -0
- package/dist/types/src/mosaic/Root.d.ts.map +1 -0
- package/dist/types/src/mosaic/SortableContext.d.ts +14 -0
- package/dist/types/src/mosaic/SortableContext.d.ts.map +1 -0
- package/dist/types/src/mosaic/Tile.d.ts +41 -0
- package/dist/types/src/mosaic/Tile.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/index.d.ts +5 -0
- package/dist/types/src/mosaic/hooks/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useContainer.d.ts +4 -0
- package/dist/types/src/mosaic/hooks/useContainer.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts +10 -0
- package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useMosaic.d.ts +2 -0
- package/dist/types/src/mosaic/hooks/useMosaic.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useSortedItems.d.ts +6 -0
- package/dist/types/src/mosaic/hooks/useSortedItems.d.ts.map +1 -0
- package/dist/types/src/mosaic/index.d.ts +5 -0
- package/dist/types/src/mosaic/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/types.d.ts +9 -0
- package/dist/types/src/mosaic/types.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/data.d.ts +3 -0
- package/dist/types/src/mosaic/util/data.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/index.d.ts +4 -0
- package/dist/types/src/mosaic/util/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/path.d.ts +14 -0
- package/dist/types/src/mosaic/util/path.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/path.test.d.ts +2 -0
- package/dist/types/src/mosaic/util/path.test.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/styles.d.ts +3 -0
- package/dist/types/src/mosaic/util/styles.d.ts.map +1 -0
- package/dist/types/src/testing/Cards.d.ts +10 -0
- package/dist/types/src/testing/Cards.d.ts.map +1 -0
- package/dist/types/src/testing/decorators.d.ts +4 -0
- package/dist/types/src/testing/decorators.d.ts.map +1 -0
- package/dist/types/src/testing/generator.d.ts +43 -0
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/next-fractional-index.d.ts +5 -0
- package/dist/types/src/testing/next-fractional-index.d.ts.map +1 -0
- package/dist/types/src/views/Demo/Demo.stories.d.ts +13 -0
- package/dist/types/src/views/Demo/Demo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Demo/Echo.stories.d.ts +12 -0
- package/dist/types/src/views/Demo/Echo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Grid/Grid.d.ts +30 -0
- package/dist/types/src/views/Grid/Grid.d.ts.map +1 -0
- package/dist/types/src/views/Grid/Grid.stories.d.ts +13 -0
- package/dist/types/src/views/Grid/Grid.stories.d.ts.map +1 -0
- package/dist/types/src/views/Grid/index.d.ts +3 -0
- package/dist/types/src/views/Grid/index.d.ts.map +1 -0
- package/dist/types/src/views/Grid/layout.d.ts +27 -0
- package/dist/types/src/views/Grid/layout.d.ts.map +1 -0
- package/dist/types/src/views/Grid/testing.d.ts +10 -0
- package/dist/types/src/views/Grid/testing.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Echo.stories.d.ts +18 -0
- package/dist/types/src/views/Kanban/Echo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Kanban.d.ts +11 -0
- package/dist/types/src/views/Kanban/Kanban.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Kanban.stories.d.ts +28 -0
- package/dist/types/src/views/Kanban/Kanban.stories.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/index.d.ts +2 -0
- package/dist/types/src/views/Kanban/index.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/testing.d.ts +20 -0
- package/dist/types/src/views/Kanban/testing.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Columns.stories.d.ts +34 -0
- package/dist/types/src/views/Stack/Columns.stories.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Stack.d.ts +9 -0
- package/dist/types/src/views/Stack/Stack.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Stack.stories.d.ts +39 -0
- package/dist/types/src/views/Stack/Stack.stories.d.ts.map +1 -0
- package/dist/types/src/views/Stack/index.d.ts +2 -0
- package/dist/types/src/views/Stack/index.d.ts.map +1 -0
- package/dist/types/src/views/Stack/testing.d.ts +9 -0
- package/dist/types/src/views/Stack/testing.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Graph.stories.d.ts +21 -0
- package/dist/types/src/views/Tree/Graph.stories.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Tree.d.ts +13 -0
- package/dist/types/src/views/Tree/Tree.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Tree.stories.d.ts +41 -0
- package/dist/types/src/views/Tree/Tree.stories.d.ts.map +1 -0
- package/dist/types/src/views/Tree/index.d.ts +2 -0
- package/dist/types/src/views/Tree/index.d.ts.map +1 -0
- package/dist/types/src/views/Tree/testing.d.ts +15 -0
- package/dist/types/src/views/Tree/testing.d.ts.map +1 -0
- package/dist/types/src/views/index.d.ts +5 -0
- package/dist/types/src/views/index.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/index.ts +5 -0
- package/src/mosaic/Container.tsx +137 -0
- package/src/mosaic/Debug.tsx +24 -0
- package/src/mosaic/DefaultComponent.tsx +26 -0
- package/src/mosaic/DragOverlay.tsx +136 -0
- package/src/mosaic/Mosaic.ts +84 -0
- package/src/mosaic/Root.tsx +255 -0
- package/src/mosaic/SortableContext.tsx +60 -0
- package/src/mosaic/Tile.tsx +189 -0
- package/src/mosaic/hooks/index.ts +8 -0
- package/src/mosaic/hooks/useContainer.ts +16 -0
- package/src/mosaic/hooks/useItemsWithPreview.ts +64 -0
- package/src/mosaic/hooks/useMosaic.ts +11 -0
- package/src/mosaic/hooks/useSortedItems.ts +14 -0
- package/src/mosaic/index.ts +9 -0
- package/src/mosaic/types.ts +13 -0
- package/src/mosaic/util/data.ts +19 -0
- package/src/mosaic/util/index.ts +7 -0
- package/src/mosaic/util/path.test.ts +26 -0
- package/src/mosaic/util/path.ts +21 -0
- package/src/mosaic/util/styles.ts +10 -0
- package/src/testing/Cards.tsx +88 -0
- package/src/testing/decorators.tsx +20 -0
- package/src/testing/generator.ts +135 -0
- package/src/testing/index.ts +9 -0
- package/src/testing/next-fractional-index.ts +36 -0
- package/src/views/Demo/Demo.stories.tsx +79 -0
- package/src/views/Demo/Echo.stories.tsx +74 -0
- package/src/views/Grid/Grid.stories.tsx +48 -0
- package/src/views/Grid/Grid.tsx +248 -0
- package/src/views/Grid/index.ts +7 -0
- package/src/views/Grid/layout.ts +42 -0
- package/src/views/Grid/testing.tsx +91 -0
- package/src/views/Kanban/Echo.stories.tsx +67 -0
- package/src/views/Kanban/Kanban.stories.tsx +48 -0
- package/src/views/Kanban/Kanban.tsx +139 -0
- package/src/views/Kanban/index.ts +5 -0
- package/src/views/Kanban/testing.tsx +231 -0
- package/src/views/Stack/Columns.stories.tsx +61 -0
- package/src/views/Stack/Stack.stories.tsx +62 -0
- package/src/views/Stack/Stack.tsx +58 -0
- package/src/views/Stack/index.ts +5 -0
- package/src/views/Stack/testing.tsx +77 -0
- package/src/views/Tree/Graph.stories.tsx +36 -0
- package/src/views/Tree/Tree.stories.tsx +92 -0
- package/src/views/Tree/Tree.tsx +128 -0
- package/src/views/Tree/index.ts +5 -0
- package/src/views/Tree/testing.tsx +156 -0
- 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,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
|
+
);
|