@dxos/plugin-navtree 0.6.8-main.046e6cf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +15 -0
  3. package/dist/lib/browser/chunk-U7MTKDFQ.mjs +13 -0
  4. package/dist/lib/browser/chunk-U7MTKDFQ.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1055 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/meta.mjs +11 -0
  9. package/dist/lib/browser/meta.mjs.map +7 -0
  10. package/dist/types/src/NavTreePlugin.d.ts +4 -0
  11. package/dist/types/src/NavTreePlugin.d.ts.map +1 -0
  12. package/dist/types/src/components/CommandsDialog.d.ts +5 -0
  13. package/dist/types/src/components/CommandsDialog.d.ts.map +1 -0
  14. package/dist/types/src/components/CommandsTrigger.d.ts +3 -0
  15. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -0
  16. package/dist/types/src/components/HaloButton.d.ts +12 -0
  17. package/dist/types/src/components/HaloButton.d.ts.map +1 -0
  18. package/dist/types/src/components/HaloButton.stories.d.ts +12 -0
  19. package/dist/types/src/components/HaloButton.stories.d.ts.map +1 -0
  20. package/dist/types/src/components/NavBarStart.d.ts +7 -0
  21. package/dist/types/src/components/NavBarStart.d.ts.map +1 -0
  22. package/dist/types/src/components/NavTreeContainer.d.ts +12 -0
  23. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -0
  24. package/dist/types/src/components/NavTreeDocumentTitle.d.ts +5 -0
  25. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -0
  26. package/dist/types/src/components/NavTreeFooter.d.ts +6 -0
  27. package/dist/types/src/components/NavTreeFooter.d.ts.map +1 -0
  28. package/dist/types/src/components/NotchStart.d.ts +3 -0
  29. package/dist/types/src/components/NotchStart.d.ts.map +1 -0
  30. package/dist/types/src/components/index.d.ts +6 -0
  31. package/dist/types/src/components/index.d.ts.map +1 -0
  32. package/dist/types/src/experimental/Tree.d.ts +56 -0
  33. package/dist/types/src/experimental/Tree.d.ts.map +1 -0
  34. package/dist/types/src/experimental/Tree.stories.d.ts +12 -0
  35. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -0
  36. package/dist/types/src/index.d.ts +5 -0
  37. package/dist/types/src/index.d.ts.map +1 -0
  38. package/dist/types/src/meta.d.ts +7 -0
  39. package/dist/types/src/meta.d.ts.map +1 -0
  40. package/dist/types/src/stories/Graph.stories.d.ts +17 -0
  41. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -0
  42. package/dist/types/src/translations.d.ts +27 -0
  43. package/dist/types/src/translations.d.ts.map +1 -0
  44. package/dist/types/src/util.d.ts +32 -0
  45. package/dist/types/src/util.d.ts.map +1 -0
  46. package/package.json +77 -0
  47. package/src/NavTreePlugin.tsx +262 -0
  48. package/src/components/CommandsDialog.tsx +114 -0
  49. package/src/components/CommandsTrigger.tsx +34 -0
  50. package/src/components/HaloButton.stories.tsx +19 -0
  51. package/src/components/HaloButton.tsx +41 -0
  52. package/src/components/NavBarStart.tsx +48 -0
  53. package/src/components/NavTreeContainer.tsx +375 -0
  54. package/src/components/NavTreeDocumentTitle.tsx +17 -0
  55. package/src/components/NavTreeFooter.tsx +155 -0
  56. package/src/components/NotchStart.tsx +29 -0
  57. package/src/components/index.ts +9 -0
  58. package/src/experimental/Tree.stories.tsx +229 -0
  59. package/src/experimental/Tree.tsx +282 -0
  60. package/src/index.ts +10 -0
  61. package/src/meta.ts +12 -0
  62. package/src/stories/Graph.stories.tsx +162 -0
  63. package/src/translations.ts +28 -0
  64. package/src/util.ts +231 -0
@@ -0,0 +1,229 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { House, List, Planet, PlusCircle, Sailboat } from '@phosphor-icons/react';
8
+ import React, { type JSX, type PropsWithChildren, useEffect, useState } from 'react';
9
+
10
+ import { faker } from '@dxos/random';
11
+ import { modalSurface, mx } from '@dxos/react-ui-theme';
12
+ import { withFullscreen, withTheme } from '@dxos/storybook-utils';
13
+
14
+ import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitNodes, visitor } from './Tree';
15
+
16
+ faker.seed(1234);
17
+
18
+ export default {
19
+ title: 'plugin-navtree/Tree',
20
+ component: Tree,
21
+ decorators: [withTheme, withFullscreen({ classNames: modalSurface })],
22
+ };
23
+
24
+ /**
25
+ * Space:
26
+ * - Space panels are separated from each other to give them more weight than folders.
27
+ * - Private space isn't different (just first, different icon).
28
+ * - Edit titles in place.
29
+ *
30
+ * Navigation:
31
+ * - up/down across spaces/folder peers
32
+ * - right to enter space/folder children
33
+ * - left to parent
34
+ */
35
+ const Container = ({ children, sidebar }: PropsWithChildren<{ sidebar: JSX.Element }>) => {
36
+ return (
37
+ <div className='flex'>
38
+ {/* TODO(burdon): Custom thin scrollbar. */}
39
+ {/* TODO(burdon): Horizontal scrolling within navtree? */}
40
+ <div className='flex flex-col overflow-y-auto w-[300px] bg-neutral-100 dark:bg-neutral-950'>{sidebar}</div>
41
+ <div className='flex flex-col grow overflow-hidden'>{children}</div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ // TODO(burdon): Generate.
47
+ const data: TreeNodeData[] = [
48
+ {
49
+ id: 'root',
50
+ title: 'Root',
51
+ children: [
52
+ {
53
+ id: faker.string.uuid(),
54
+ title: 'Personal Space',
55
+ color: 'text-green-400',
56
+ Icon: House,
57
+ children: [
58
+ {
59
+ id: faker.string.uuid(),
60
+ title: faker.commerce.productName(),
61
+ },
62
+ {
63
+ id: faker.string.uuid(),
64
+ title: faker.commerce.productName(),
65
+ },
66
+ {
67
+ id: faker.string.uuid(),
68
+ title: faker.commerce.productName(),
69
+ children: [
70
+ {
71
+ id: faker.string.uuid(),
72
+ title: faker.commerce.productName(),
73
+ children: [
74
+ {
75
+ id: faker.string.uuid(),
76
+ title: faker.commerce.productName(),
77
+ },
78
+ {
79
+ id: faker.string.uuid(),
80
+ title: faker.commerce.productName(),
81
+ },
82
+ ],
83
+ },
84
+ ],
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ id: faker.string.uuid(),
90
+ title: faker.commerce.productName(),
91
+ Icon: Planet,
92
+ children: [
93
+ {
94
+ id: faker.string.uuid(),
95
+ title: faker.commerce.productName(),
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ id: faker.string.uuid(),
101
+ title: faker.commerce.productName(),
102
+ Icon: Sailboat,
103
+ },
104
+ {
105
+ id: faker.string.uuid(),
106
+ title: faker.commerce.productName(),
107
+ Icon: Planet,
108
+ },
109
+ ],
110
+ },
111
+ ];
112
+
113
+ export const Default = () => {
114
+ return <Container sidebar={<Sidebar mutate />} />;
115
+ };
116
+
117
+ export const Visitor = () => {
118
+ const [items] = useState(data);
119
+ const root = items[0];
120
+ const [openItems] = useState<ItemMap>({ [root.children![0].id]: true });
121
+ return (
122
+ <div className='flex flex-col'>
123
+ {Array.from(visitor(root, openItems)).map(({ node: { id, title }, depth }) => (
124
+ <div key={id} className='grid grid-cols-[40px_400px_400px] font-mono'>
125
+ <div>{depth}</div>
126
+ <div>{id}</div>
127
+ <div>{title}</div>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ );
132
+ };
133
+
134
+ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
135
+ const [items, setItems] = useState(data);
136
+ const root = items[0];
137
+ const [openItems, setOpenItems] = useState<ItemMap>({ [root.children![0].id]: true });
138
+ const [selectedItems, setSelectedItems] = useState<ItemMap>({});
139
+ const [activeItems, setActiveItems] = useState<ItemMap>(() => {
140
+ const active: ItemMap = {};
141
+ visitNodes(root, (node, depth) => {
142
+ if (depth > 1 && Math.random() > 0.6) {
143
+ active[node.id] = true;
144
+ }
145
+ });
146
+ return active;
147
+ });
148
+
149
+ useEffect(() => {
150
+ if (mutate) {
151
+ const t = setInterval(() => {
152
+ setActiveItems((active) => {
153
+ visitNodes(root, (node, depth) => {
154
+ if (depth > 1) {
155
+ active[node.id] = active[node.id] ? Math.random() > 0.4 : Math.random() > 0.7;
156
+ }
157
+ });
158
+ return { ...active };
159
+ });
160
+ }, 5_000);
161
+ return () => clearInterval(t);
162
+ }
163
+ }, []);
164
+
165
+ const handleCreateItem: TreeProps['onMenuAction'] = (id, action) => {
166
+ setItems((items) => {
167
+ let parent: TreeNodeData | undefined;
168
+ visitNodes(root, (node) => {
169
+ if (node.id === id) {
170
+ parent = node;
171
+ }
172
+ });
173
+
174
+ (parent!.children ??= []).push({
175
+ id: faker.string.uuid(),
176
+ title: faker.commerce.productName(),
177
+ });
178
+
179
+ setOpenItems((open) => ({ ...open, [parent!.id]: true }));
180
+
181
+ return { ...items };
182
+ });
183
+ };
184
+
185
+ const handleCreateSpace = () => {
186
+ setItems((items) => {
187
+ items[0].children?.push({
188
+ id: faker.string.uuid(),
189
+ Icon: Planet,
190
+ title: faker.commerce.productName(),
191
+ });
192
+
193
+ return { ...items };
194
+ });
195
+ };
196
+
197
+ return (
198
+ <div className='flex flex-col overflow-hidden'>
199
+ <div className='flex flex-col overflow-y-auto'>
200
+ <Tree
201
+ className='p-0.5 gap-1'
202
+ node={root}
203
+ open={openItems}
204
+ selected={selectedItems}
205
+ active={activeItems}
206
+ onChangeOpen={(id, open) => setOpenItems((items) => ({ ...items, [id]: open }))}
207
+ onChangeSelected={(id, open) => setSelectedItems((items) => ({ ...items, [id]: open }))}
208
+ onMenuAction={handleCreateItem}
209
+ getSlots={(node, open, depth, ancestors) => {
210
+ if (depth === 1) {
211
+ return {
212
+ root: 'rounded bg-white dark:bg-neutral-850',
213
+ header: mx('rounded-t bg-neutral-50 dark:bg-neutral-900', !open && 'rounded-b'),
214
+ };
215
+ }
216
+ }}
217
+ />
218
+ </div>
219
+
220
+ <div className='flex items-center my-2 px-2 gap-2'>
221
+ <PlusCircle onClick={handleCreateSpace} />
222
+ <span className='grow text-sm' onClick={handleCreateSpace}>
223
+ New space
224
+ </span>
225
+ <List />
226
+ </div>
227
+ </div>
228
+ );
229
+ };
@@ -0,0 +1,282 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { CaretRight, Circle, File, Folder, type Icon, type IconProps, Plus, UserCircle } from '@phosphor-icons/react';
6
+ import React, { type HTMLAttributes, type PropsWithChildren } from 'react';
7
+
8
+ import { type ClassNameValue, type Size } from '@dxos/react-ui';
9
+ import { getSize, mx } from '@dxos/react-ui-theme';
10
+
11
+ // TODO(burdon): Rounded border if first/last.
12
+ const styles = {
13
+ hover: 'hover:bg-slate-400 dark:hover:bg-slate-800',
14
+ selected: '!bg-slate-300 dark:!bg-slate-700',
15
+ };
16
+
17
+ export const IconButton = ({
18
+ Icon,
19
+ classNames,
20
+ size = 4,
21
+ onClick,
22
+ ...props
23
+ }: {
24
+ Icon: Icon;
25
+ classNames?: ClassNameValue;
26
+ size?: Size;
27
+ } & Pick<IconProps, 'weight'> &
28
+ Pick<HTMLAttributes<HTMLDivElement>, 'onClick'>) => {
29
+ // TODO(burdon): Density aware.
30
+ return (
31
+ <div className={mx('flex w-6 h-6 items-center justify-center select-none', classNames)} onClick={onClick}>
32
+ <Icon className={mx(getSize(size), 'cursor-pointer')} {...props} />
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export type TreeNodeData = {
38
+ id: string;
39
+ title: string;
40
+ Icon?: Icon;
41
+ color?: string;
42
+ children?: TreeNodeData[];
43
+ };
44
+
45
+ export type TreeIteratorNode = {
46
+ node: TreeNodeData;
47
+ depth: number;
48
+ };
49
+
50
+ export function* visitor(node: TreeNodeData, open: ItemMap): Generator<TreeIteratorNode> {
51
+ const stack: TreeIteratorNode[] = [{ node, depth: 0 }];
52
+ while (stack.length > 0) {
53
+ const { node, depth } = stack.pop()!;
54
+ if (depth > 0) {
55
+ yield { node, depth };
56
+ }
57
+
58
+ const children = Array.from(node.children ?? []);
59
+ if (depth === 0 || open[node.id]) {
60
+ for (let i = children.length - 1; i >= 0; i--) {
61
+ const child = children[i];
62
+ stack.push({ node: child, depth: depth + 1 });
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ export const visitNodes = <T,>(
69
+ node: TreeNodeData,
70
+ callback: (node: TreeNodeData, depth: number) => T,
71
+ depth = 0,
72
+ ): T | undefined => {
73
+ const result = callback(node, depth);
74
+ if (result) {
75
+ return result;
76
+ }
77
+
78
+ for (const child of node.children ?? []) {
79
+ const result = visitNodes(child, callback, depth + 1);
80
+ if (result) {
81
+ return result;
82
+ }
83
+ }
84
+ };
85
+
86
+ export type ItemMap<T = boolean> = Record<TreeNodeData['id'], T>;
87
+
88
+ export type TreeNodeProps = {
89
+ node: TreeNodeData;
90
+ depth?: number;
91
+ getSlots?: (
92
+ node: TreeNodeData,
93
+ open: boolean,
94
+ depth: number,
95
+ ancestors: TreeNodeData[],
96
+ ) => { root?: string; header?: string } | undefined;
97
+
98
+ // Component state is separate from the data (stored separately).
99
+ open?: ItemMap;
100
+ selected?: ItemMap;
101
+ active?: ItemMap;
102
+
103
+ onChangeOpen?: (id: string, open: boolean) => void;
104
+ onChangeSelected?: (id: string, select: boolean) => void;
105
+ onMenuAction?: (id: string, action: string) => void;
106
+ };
107
+
108
+ const StateIcon = ({ node, open, selected, active }: TreeNodeProps) => {
109
+ const { id } = node;
110
+
111
+ // Check if any children are active.
112
+ let isChildActive = false;
113
+ if (!active?.[id] && !open?.[id]) {
114
+ isChildActive = !!visitNodes(node, ({ id }) => {
115
+ if (active?.[id]) {
116
+ return true;
117
+ }
118
+ });
119
+ }
120
+
121
+ const isActive = active?.[id] || isChildActive;
122
+
123
+ // TODO(burdon): No animation if opening/closing folder.
124
+ return (
125
+ <IconButton
126
+ Icon={isActive ? UserCircle : Circle}
127
+ size={4}
128
+ weight={selected?.[id] ? 'fill' : 'duotone'}
129
+ classNames={mx(
130
+ 'text-slate-500',
131
+ !isChildActive && 'opacity-0 transition duration-500',
132
+ (isActive || selected?.[id]) && 'opacity-100',
133
+ )}
134
+ />
135
+ );
136
+ };
137
+
138
+ const OpenIcon = ({
139
+ node: { id, children },
140
+ open,
141
+ onChangeOpen,
142
+ }: Pick<TreeNodeProps, 'node' | 'open' | 'onChangeOpen'>) => {
143
+ return (
144
+ (children?.length && open && (
145
+ <IconButton
146
+ Icon={CaretRight}
147
+ size={3}
148
+ classNames={mx('transition duration-200', open?.[id] ? 'rotate-90' : 'transform-none')}
149
+ onClick={(ev) => {
150
+ ev.stopPropagation();
151
+ onChangeOpen?.(id, !open[id]);
152
+ }}
153
+ />
154
+ )) || <div />
155
+ );
156
+ };
157
+
158
+ // TODO(burdon): Drag handle,
159
+ const ItemIcon = ({
160
+ node: { children, Icon = children?.length ? Folder : File, color },
161
+ }: Pick<TreeNodeProps, 'node'>) => {
162
+ return (
163
+ (Icon && (
164
+ <IconButton
165
+ Icon={Icon}
166
+ classNames={color ?? 'text-neutral-700 dark:text-neutral-300'}
167
+ weight={color ? 'duotone' : 'regular'}
168
+ />
169
+ )) || <div />
170
+ );
171
+ };
172
+
173
+ const MenuItem = ({ node: { id }, onMenuAction }: Pick<TreeNodeProps, 'node' | 'onMenuAction'>) => {
174
+ return (
175
+ <IconButton
176
+ Icon={Plus}
177
+ classNames='invisible group-hover:visible'
178
+ onClick={(ev) => {
179
+ ev.stopPropagation();
180
+ onMenuAction?.(id, 'create');
181
+ }}
182
+ />
183
+ );
184
+ };
185
+
186
+ // TODO(burdon): Editable.
187
+ export const Title = ({ node: { title } }: Pick<TreeNodeProps, 'node'>) => {
188
+ return (
189
+ <div className='grow p-1 text-sm whitespace-nowrap truncate text-neutral-800 dark:text-neutral-200'>{title}</div>
190
+ );
191
+ };
192
+
193
+ export const Grid = ({
194
+ className,
195
+ ...props
196
+ }: PropsWithChildren<{ className?: string }> & HTMLAttributes<HTMLDivElement>) => {
197
+ // TODO(burdon): Density aware.
198
+ return <div className={mx('grid grid-cols-[24px_24px_1fr_24px_24px]', className)} {...props} />;
199
+ };
200
+
201
+ export const TreeNodeRow = (props: TreeNodeProps & { className?: string }) => {
202
+ const {
203
+ node: { id },
204
+ className,
205
+ depth = 0,
206
+ selected,
207
+ onChangeSelected,
208
+ } = props;
209
+
210
+ return (
211
+ <Grid
212
+ style={{ paddingLeft: `${(depth - 1) * 24}px` }}
213
+ className={mx(
214
+ 'group w-full items-center cursor-pointer',
215
+ styles.hover,
216
+ selected?.[id] && styles.selected,
217
+ className,
218
+ )}
219
+ onClick={() => selected && onChangeSelected?.(id, !selected[id])}
220
+ >
221
+ <OpenIcon {...props} />
222
+ <ItemIcon {...props} />
223
+ <Title {...props} />
224
+ <MenuItem {...props} />
225
+ <StateIcon {...props} />
226
+ </Grid>
227
+ );
228
+ };
229
+
230
+ export const TreeNode = (props: TreeNodeProps & { ancestors?: TreeNodeData[] }) => {
231
+ const {
232
+ node: { id, children },
233
+ depth = 0,
234
+ getSlots,
235
+ open,
236
+ ancestors = [],
237
+ } = props;
238
+ const { root, header } = getSlots?.(props.node, open?.[id] ?? false, depth, ancestors) ?? {};
239
+
240
+ return (
241
+ <div role='none' className={mx('flex flex-col', root)}>
242
+ <TreeNodeRow {...props} className={header} />
243
+ {(children?.length ?? 0) > 0 && open?.[id] && <TreeChildNodes {...props} ancestors={ancestors} />}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ export const TreeChildNodes = ({
249
+ className,
250
+ ...props
251
+ }: TreeNodeProps & { ancestors?: TreeNodeData[]; className?: string }) => {
252
+ const {
253
+ node: { children },
254
+ depth = 0,
255
+ ancestors = [],
256
+ } = props;
257
+ return (
258
+ <div className={mx('flex flex-col mt-0.5 gap-0.5', className)}>
259
+ {children?.map((child, i) => (
260
+ <TreeNode key={child.id} {...props} depth={depth + 1} node={child} ancestors={[...ancestors, props.node]} />
261
+ ))}
262
+ </div>
263
+ );
264
+ };
265
+
266
+ export type TreeProps = { className?: string; showRoot?: boolean } & TreeNodeProps;
267
+
268
+ export const Tree = ({ className, showRoot, ...props }: TreeProps) => {
269
+ if (showRoot) {
270
+ return (
271
+ <div className={className}>
272
+ <TreeNode {...props} depth={0} />
273
+ </div>
274
+ );
275
+ } else {
276
+ return (
277
+ <div className={className}>
278
+ <TreeChildNodes {...props} className={className} depth={0} node={props.node} />
279
+ </div>
280
+ );
281
+ }
282
+ };
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { NavTreePlugin } from './NavTreePlugin';
6
+
7
+ export default NavTreePlugin;
8
+
9
+ export * from './NavTreePlugin';
10
+ export * from './util';
package/src/meta.ts ADDED
@@ -0,0 +1,12 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export const NAVTREE_PLUGIN = 'dxos.org/plugin/navtree';
6
+
7
+ // TODO(wittjosiah): Factor out.
8
+ export const KEY_BINDING = 'KeyBinding';
9
+
10
+ export default {
11
+ id: NAVTREE_PLUGIN,
12
+ };
@@ -0,0 +1,162 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { batch } from '@preact/signals-core';
8
+ import React, { useMemo, useState } from 'react';
9
+
10
+ import { Graph } from '@dxos/app-graph';
11
+ import { registerSignalRuntime } from '@dxos/echo-signals/react';
12
+ import { faker } from '@dxos/random';
13
+ import { DensityProvider, Tooltip, Treegrid } from '@dxos/react-ui';
14
+ import { Mosaic, Path, type MosaicDropEvent, type MosaicMoveEvent, type MosaicOperation } from '@dxos/react-ui-mosaic';
15
+ import { NavTree, type NavTreeItemNode, type OpenItemIds } from '@dxos/react-ui-navtree';
16
+ import { withTheme } from '@dxos/storybook-utils';
17
+ import { arrayMove } from '@dxos/util';
18
+
19
+ import { type NavTreeItemGraphNode, treeItemsFromRootNode } from '../util';
20
+
21
+ faker.seed(3);
22
+ registerSignalRuntime();
23
+
24
+ const createGraph = () => {
25
+ const graph = new Graph();
26
+
27
+ batch(() => {
28
+ (graph as any)._addNode({
29
+ id: ROOT_ID,
30
+ nodes: [...Array(2)].map((_, i) => ({
31
+ id: faker.string.hexadecimal({ length: 4 }).slice(2).toUpperCase(),
32
+ properties: { index: `a${i}`, label: faker.lorem.words(2), description: faker.lorem.sentence() },
33
+ nodes: [...Array(2)].map((_, j) => ({
34
+ id: faker.string.hexadecimal({ length: 4 }).slice(2).toUpperCase(),
35
+ properties: { index: `a${j}`, label: faker.lorem.words(2), description: faker.lorem.sentence() },
36
+ nodes: [...Array(2)].map((_, k) => ({
37
+ id: faker.string.hexadecimal({ length: 4 }).slice(2).toUpperCase(),
38
+ properties: { index: `a${k}`, label: faker.lorem.words(2), description: faker.lorem.sentence() },
39
+ })),
40
+ })),
41
+ })),
42
+ });
43
+ });
44
+
45
+ return graph;
46
+ };
47
+
48
+ const ROOT_ID = 'root';
49
+ const graph = createGraph();
50
+
51
+ const StorybookNavTree = ({ id = ROOT_ID }: { id?: string }) => {
52
+ const [openItemIds, setopenItemIds] = useState<OpenItemIds>({});
53
+ const items = useMemo(
54
+ () => treeItemsFromRootNode(graph, graph.root as NavTreeItemGraphNode, openItemIds),
55
+ [openItemIds],
56
+ );
57
+ const handleopenItemIdsChange = (item: NavTreeItemNode, nextOpen: boolean) => {
58
+ const itemId = item.path?.join(Treegrid.PATH_SEPARATOR) ?? 'never';
59
+ setopenItemIds((prevOpenItemIds) => {
60
+ if (nextOpen) {
61
+ return { ...prevOpenItemIds, [itemId]: true };
62
+ } else {
63
+ const { [itemId]: _, ...nextOpenItemIds } = prevOpenItemIds;
64
+ return nextOpenItemIds;
65
+ }
66
+ });
67
+ };
68
+
69
+ const handleOver = ({ active, over }: MosaicMoveEvent<number>): MosaicOperation => {
70
+ // Reject all operations that don’t match the graph’s root id
71
+ if (Path.first(active.path) !== id || Path.first(over.path) !== Path.first(active.path)) {
72
+ return 'reject';
73
+ }
74
+ // Rearrange if rearrange is supported and active and over are siblings
75
+ else if (Path.siblings(over.path, active.path)) {
76
+ return graph.findNode(Path.last(Path.parent(over.path)))?.properties.onRearrangeChildren ? 'rearrange' : 'reject';
77
+ }
78
+ // Rearrange if rearrange is supported and active is or would be a child of over
79
+ else if (Path.hasChild(over.path, active.path)) {
80
+ return graph.findNode(Path.last(over.path))?.properties.onRearrangeChildren ? 'rearrange' : 'reject';
81
+ }
82
+ // Check if transfer is supported
83
+ else {
84
+ const overNode = graph.findNode(Path.last(over.path));
85
+ const activeNode = graph.findNode(Path.last(active.path));
86
+ if (overNode && activeNode) {
87
+ return 'transfer';
88
+ } else {
89
+ return 'reject';
90
+ }
91
+ }
92
+ };
93
+
94
+ const handleDrop = ({ operation, active, over }: MosaicDropEvent<number>) => {
95
+ batch(() => {
96
+ const activeNode = graph.findNode(active.item.id);
97
+ const overNode = graph.findNode(over.item.id);
98
+ const activeParent = activeNode && graph.nodes(activeNode, { relation: 'inbound' })[0];
99
+ const overParent = overNode && graph.nodes(overNode, { relation: 'inbound' })[0];
100
+ if (
101
+ activeNode &&
102
+ overNode &&
103
+ activeParent &&
104
+ overParent &&
105
+ activeParent.id === overParent.id &&
106
+ activeNode.id !== overNode.id &&
107
+ operation === 'rearrange'
108
+ ) {
109
+ const ids = graph.nodes(activeParent).map((node) => node.id);
110
+ const activeIndex = ids.indexOf(activeNode.id);
111
+ const overIndex = ids.indexOf(overNode.id);
112
+ (graph as any)._sortEdges(
113
+ activeParent.id,
114
+ 'outbound',
115
+ arrayMove(ids, activeIndex, overIndex > -1 ? overIndex : ids.length - 1),
116
+ );
117
+ } else if (activeNode && activeParent && overParent && operation === 'transfer') {
118
+ (graph as any)._removeEdges([{ source: activeParent.id, target: activeNode.id }]);
119
+ (graph as any)._addEdges([{ source: overNode.id, target: activeNode.id }]);
120
+ }
121
+ });
122
+ };
123
+
124
+ return (
125
+ <NavTree
126
+ id='storybook navtree'
127
+ items={items}
128
+ onItemOpenChange={handleopenItemIdsChange}
129
+ onDrop={handleDrop}
130
+ onOver={handleOver}
131
+ />
132
+ );
133
+ };
134
+
135
+ export default {
136
+ title: 'react-ui-navtree/Graph',
137
+ component: NavTree,
138
+ parameters: {
139
+ layout: 'fullscreen',
140
+ },
141
+ decorators: [
142
+ withTheme,
143
+ (Story: any) => (
144
+ <Tooltip.Provider>
145
+ <DensityProvider density='fine'>
146
+ <div role='none' className='p-2'>
147
+ <Story />
148
+ </div>
149
+ </DensityProvider>
150
+ </Tooltip.Provider>
151
+ ),
152
+ ],
153
+ };
154
+
155
+ export const Default = {
156
+ render: ({ debug }: { debug?: boolean }) => (
157
+ <Mosaic.Root debug={debug}>
158
+ <StorybookNavTree />
159
+ <Mosaic.DragOverlay />
160
+ </Mosaic.Root>
161
+ ),
162
+ };