@dxos/plugin-navtree 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef
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/dist/lib/browser/NavTreePlugin-53I3ONRE.mjs +74 -0
- package/dist/lib/browser/NavTreePlugin-53I3ONRE.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +95 -122
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +21 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/types/src/NavTreePlugin.d.ts +3 -1
- package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
- package/dist/types/src/NavTreePlugin.node.d.ts +4 -0
- package/dist/types/src/NavTreePlugin.node.d.ts.map +1 -0
- package/dist/types/src/NavTreePlugin.test.d.ts +2 -0
- package/dist/types/src/NavTreePlugin.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts +7 -105
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/keyboard.d.ts +3 -2
- package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
- package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +3 -2
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/state.d.ts +4 -18
- package/dist/types/src/capabilities/state.d.ts.map +1 -1
- package/dist/types/src/components/NavTree/NavTree.d.ts +11 -4
- package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
- package/dist/types/src/components/NavTree/NavTree.stories.d.ts +10 -13
- package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
- package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
- package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
- package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +13 -9
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +3 -2
- package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
- package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L0Menu.d.ts +8 -5
- package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -7
- package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
- package/dist/types/src/components/Sidebar/L1Tabs.d.ts +5 -4
- package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
- package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
- package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -3
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/types.d.ts +8 -17
- package/dist/types/src/components/types.d.ts.map +1 -1
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +28 -0
- package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
- package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
- package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
- package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
- package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
- package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
- package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
- package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
- package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
- package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
- package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
- package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
- package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
- package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +7 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/experimental/Tree.d.ts +2 -2
- package/dist/types/src/experimental/Tree.d.ts.map +1 -1
- package/dist/types/src/experimental/Tree.stories.d.ts +15 -4
- package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useActions.d.ts +5 -0
- package/dist/types/src/hooks/useActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
- package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
- package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
- package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useNavTreeState.d.ts +18 -0
- package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +2 -2
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/operations/expose.d.ts +5 -0
- package/dist/types/src/operations/expose.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
- package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +5 -6
- package/dist/types/src/testing/generator.d.ts.map +1 -1
- package/dist/types/src/testing/update-state.d.ts +2 -2
- package/dist/types/src/testing/update-state.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +8 -11
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/capabilities.d.ts +15 -0
- package/dist/types/src/types/capabilities.d.ts.map +1 -0
- package/dist/types/src/types/events.d.ts +5 -0
- package/dist/types/src/types/events.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +4 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
- package/dist/types/src/types/schema.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +12 -8
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +78 -58
- package/src/NavTreePlugin.node.ts +37 -0
- package/src/NavTreePlugin.test.ts +29 -0
- package/src/NavTreePlugin.tsx +59 -76
- package/src/capabilities/app-graph-builder.ts +34 -44
- package/src/capabilities/index.ts +10 -8
- package/src/capabilities/keyboard.ts +58 -51
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/capabilities/react-surface.tsx +46 -38
- package/src/capabilities/state.ts +128 -84
- package/src/components/NavTree/NavTree.stories.tsx +105 -100
- package/src/components/NavTree/NavTree.tsx +65 -33
- package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
- package/src/components/NavTreeContext/index.ts +5 -0
- package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +23 -15
- package/src/components/NavTreeItem/NavTreeItemAction.tsx +69 -46
- package/src/components/NavTreeItem/NavTreeItemColumns.tsx +67 -35
- package/src/components/Sidebar/L0Menu.tsx +148 -183
- package/src/components/Sidebar/L1Panel.tsx +202 -103
- package/src/components/Sidebar/L1Tabs.tsx +18 -34
- package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +2 -3
- package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
- package/src/components/index.ts +1 -3
- package/src/components/types.ts +13 -19
- package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +46 -0
- package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +127 -0
- package/src/containers/CommandsDialogContent/index.ts +5 -0
- package/src/containers/CommandsTrigger/CommandsTrigger.tsx +28 -0
- package/src/containers/CommandsTrigger/index.ts +5 -0
- package/src/containers/NavTreeContainer/NavTreeContainer.tsx +271 -0
- package/src/containers/NavTreeContainer/index.ts +6 -0
- package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +4 -3
- package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
- package/src/containers/index.ts +12 -0
- package/src/experimental/Tree.stories.tsx +81 -87
- package/src/experimental/Tree.tsx +3 -3
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useActions.ts +34 -0
- package/src/hooks/useLoadDescendents.ts +23 -0
- package/src/hooks/useNavTreeModel.ts +118 -0
- package/src/hooks/useNavTreeState.ts +35 -0
- package/src/index.ts +8 -3
- package/src/meta.ts +9 -4
- package/src/operations/expose.ts +42 -0
- package/src/operations/index.ts +7 -0
- package/src/testing/app-graph-builder.ts +167 -238
- package/src/testing/generator.ts +41 -39
- package/src/testing/update-state.ts +6 -6
- package/src/translations.ts +8 -10
- package/src/types/capabilities.ts +21 -0
- package/src/types/events.ts +12 -0
- package/src/types/index.ts +7 -0
- package/src/{types.ts → types/schema.ts} +9 -8
- package/src/util.ts +28 -17
- package/dist/lib/browser/app-graph-builder-BCXDEDN7.mjs +0 -45
- package/dist/lib/browser/app-graph-builder-BCXDEDN7.mjs.map +0 -7
- package/dist/lib/browser/chunk-6AZKY5WZ.mjs +0 -1470
- package/dist/lib/browser/chunk-6AZKY5WZ.mjs.map +0 -7
- package/dist/lib/browser/chunk-LPAREX6J.mjs +0 -15
- package/dist/lib/browser/chunk-LPAREX6J.mjs.map +0 -7
- package/dist/lib/browser/chunk-ZENLR72O.mjs +0 -14
- package/dist/lib/browser/chunk-ZENLR72O.mjs.map +0 -7
- package/dist/lib/browser/intent-resolver-G6COWRWW.mjs +0 -47
- package/dist/lib/browser/intent-resolver-G6COWRWW.mjs.map +0 -7
- package/dist/lib/browser/keyboard-AHSAWKXA.mjs +0 -51
- package/dist/lib/browser/keyboard-AHSAWKXA.mjs.map +0 -7
- package/dist/lib/browser/react-surface-FOCB4Y5R.mjs +0 -51
- package/dist/lib/browser/react-surface-FOCB4Y5R.mjs.map +0 -7
- package/dist/lib/browser/state-2Q623V7U.mjs +0 -117
- package/dist/lib/browser/state-2Q623V7U.mjs.map +0 -7
- package/dist/types/src/capabilities/capabilities.d.ts +0 -21
- package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
- package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
- package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
- package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
- package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
- package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
- package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
- package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
- package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
- package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
- package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
- package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
- package/dist/types/src/events.d.ts +0 -4
- package/dist/types/src/events.d.ts.map +0 -1
- package/dist/types/src/hooks.d.ts +0 -3
- package/dist/types/src/hooks.d.ts.map +0 -1
- package/dist/types/src/types.d.ts.map +0 -1
- package/src/capabilities/capabilities.ts +0 -20
- package/src/capabilities/intent-resolver.ts +0 -37
- package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
- package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -35
- package/src/components/CommandsDialog/index.ts +0 -6
- package/src/components/NavTreeContainer.tsx +0 -339
- package/src/events.ts +0 -11
- package/src/hooks.ts +0 -21
- /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
|
@@ -2,44 +2,52 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import React, { type ComponentProps } from 'react';
|
|
6
7
|
|
|
7
|
-
import { Capabilities,
|
|
8
|
-
import {
|
|
8
|
+
import { Capabilities, Capability } from '@dxos/app-framework';
|
|
9
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
10
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
11
|
+
import { Node } from '@dxos/plugin-graph';
|
|
9
12
|
|
|
10
|
-
import { CommandsDialogContent, CommandsTrigger, NavTreeContainer, NavTreeDocumentTitle } from '
|
|
11
|
-
import { COMMANDS_DIALOG
|
|
13
|
+
import { CommandsDialogContent, CommandsTrigger, NavTreeContainer, NavTreeDocumentTitle } from '#containers';
|
|
14
|
+
import { COMMANDS_DIALOG } from '#meta';
|
|
12
15
|
|
|
13
|
-
export default () =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
16
|
+
export default Capability.makeModule(() =>
|
|
17
|
+
Effect.succeed(
|
|
18
|
+
Capability.contributes(Capabilities.ReactSurface, [
|
|
19
|
+
Surface.create({
|
|
20
|
+
id: COMMANDS_DIALOG,
|
|
21
|
+
filter: AppSurface.component<ComponentProps<typeof CommandsDialogContent>>(AppSurface.Dialog, COMMANDS_DIALOG),
|
|
22
|
+
component: ({ data, ref }) => <CommandsDialogContent {...data.props} ref={ref} />,
|
|
23
|
+
}),
|
|
24
|
+
Surface.create({
|
|
25
|
+
id: 'navigation',
|
|
26
|
+
role: 'navigation',
|
|
27
|
+
filter: (data): data is { popoverAnchorId?: string; current: string } => typeof data.current === 'string',
|
|
28
|
+
component: ({ data, ref }) => {
|
|
29
|
+
return (
|
|
30
|
+
<NavTreeContainer
|
|
31
|
+
tab={data.current}
|
|
32
|
+
popoverAnchorId={data.popoverAnchorId as string | undefined}
|
|
33
|
+
ref={ref}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
Surface.create({
|
|
39
|
+
id: 'document-title',
|
|
40
|
+
role: 'document-title',
|
|
41
|
+
component: ({ data }) => (
|
|
42
|
+
<NavTreeDocumentTitle node={Node.isGraphNode(data.subject) ? data.subject : undefined} />
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
Surface.create({
|
|
46
|
+
id: 'search-input',
|
|
47
|
+
role: 'search-input',
|
|
48
|
+
position: 'fallback',
|
|
49
|
+
component: () => <CommandsTrigger />,
|
|
50
|
+
}),
|
|
51
|
+
]),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
@@ -2,104 +2,148 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
6
7
|
|
|
7
|
-
import { Capabilities,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { Capabilities, Capability } from '@dxos/app-framework';
|
|
9
|
+
import { AppCapabilities } from '@dxos/app-toolkit';
|
|
10
|
+
import { Graph, Node } from '@dxos/plugin-graph';
|
|
10
11
|
import { Path } from '@dxos/react-ui-list';
|
|
11
12
|
|
|
12
|
-
import { meta } from '
|
|
13
|
+
import { meta } from '#meta';
|
|
14
|
+
import { type NavTreeCapabilities as NC, NavTreeCapabilities } from '#types';
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
const KEY = `${meta.id}.state.v1`;
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
/** Default item state for new entries. */
|
|
19
|
+
const defaultItemState: NC.NavTreeItemState = { open: false, current: false };
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/** L0 (top-level workspace) paths are direct children of root — not part of the expandable tree model. */
|
|
22
|
+
const isTopLevelPath = (path: string[]): boolean => path.length === 2 && path[0] === Node.RootId;
|
|
23
|
+
|
|
24
|
+
/** Default state entries for initial tree structure. */
|
|
25
|
+
// TODO(thure): Initialize these dynamically.
|
|
26
|
+
const defaultStateEntries: [string, NC.NavTreeItemState][] = [['root', { open: true, current: false }]];
|
|
27
|
+
|
|
28
|
+
const getInitialState = (): Map<string, NC.NavTreeItemState> => {
|
|
19
29
|
const stringified = localStorage.getItem(KEY);
|
|
20
30
|
if (!stringified) {
|
|
21
|
-
return;
|
|
31
|
+
return new Map(defaultStateEntries);
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
try {
|
|
25
35
|
const cached: [string, { open: boolean; current: boolean }][] = JSON.parse(stringified);
|
|
26
|
-
return cached.map(([key, value])
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} catch {}
|
|
36
|
+
return new Map(cached.map(([key, value]) => [key, { open: value.open, current: false }]));
|
|
37
|
+
} catch {
|
|
38
|
+
return new Map(defaultStateEntries);
|
|
39
|
+
}
|
|
31
40
|
};
|
|
32
41
|
|
|
33
|
-
export default (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return value;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const setItem = (path: string[], key: 'open' | 'current' | 'alternateTree', next: boolean) => {
|
|
60
|
-
const value = getItem(path);
|
|
61
|
-
value[key] = next;
|
|
62
|
-
|
|
63
|
-
localStorage.setItem(KEY, JSON.stringify(Array.from(state.entries())));
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const isOpen = (path: string[]) => getItem(path).open;
|
|
67
|
-
const isCurrent = (path: string[]) => getItem(path).current;
|
|
68
|
-
const isAlternateTree = (path: string[]) => {
|
|
69
|
-
const item = getItem(path);
|
|
70
|
-
return item.alternateTree ?? false;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
let previous: string[] = [];
|
|
74
|
-
const unsubscribe = effect(() => {
|
|
75
|
-
const removed = previous.filter((id) => !layout.active.includes(id));
|
|
76
|
-
previous = layout.active;
|
|
77
|
-
|
|
78
|
-
const handleUpdate = () => {
|
|
79
|
-
removed.forEach((id) => {
|
|
80
|
-
const keys = Array.from(state.keys()).filter((key) => Path.last(key) === id);
|
|
81
|
-
keys.forEach((key) => {
|
|
82
|
-
setItem(Path.parts(key), 'current', false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
42
|
+
export default Capability.makeModule(
|
|
43
|
+
Effect.fnUntraced(function* () {
|
|
44
|
+
const registry = yield* Capability.get(Capabilities.AtomRegistry);
|
|
45
|
+
const layoutAtom = yield* Capability.get(AppCapabilities.Layout);
|
|
46
|
+
|
|
47
|
+
// Backing state for persistence (not reactive).
|
|
48
|
+
const backingState = getInitialState();
|
|
49
|
+
|
|
50
|
+
// Per-path atom family for fine-grained reactivity.
|
|
51
|
+
// keepAlive prevents atoms from being garbage collected when components unmount,
|
|
52
|
+
// ensuring state is preserved across deletion/restoration cycles.
|
|
53
|
+
const itemAtomFamily = Atom.family((pathString: string) =>
|
|
54
|
+
Atom.make<NC.NavTreeItemState>(backingState.get(pathString) ?? { ...defaultItemState }).pipe(Atom.keepAlive),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const getItemAtom = (path: string[]): Atom.Atom<NC.NavTreeItemState> => {
|
|
58
|
+
const pathString = Path.create(...path);
|
|
59
|
+
if (!backingState.has(pathString)) {
|
|
60
|
+
backingState.set(pathString, { ...defaultItemState });
|
|
61
|
+
}
|
|
62
|
+
return itemAtomFamily(pathString);
|
|
63
|
+
};
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
keys.forEach((key) => {
|
|
89
|
-
setItem(Path.parts(key), 'current', true);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
65
|
+
const getItem = (path: string[]): NC.NavTreeItemState => {
|
|
66
|
+
return registry.get(getItemAtom(path));
|
|
92
67
|
};
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
69
|
+
const setItem = (path: string[], key: 'open' | 'current', next: boolean) => {
|
|
70
|
+
const pathString = Path.create(...path);
|
|
71
|
+
const atom = itemAtomFamily(pathString);
|
|
72
|
+
const currentValue = registry.get(atom);
|
|
73
|
+
const newValue = { ...currentValue, [key]: next };
|
|
74
|
+
|
|
75
|
+
registry.set(atom, newValue);
|
|
76
|
+
|
|
77
|
+
if (!(key === 'open' && isTopLevelPath(path))) {
|
|
78
|
+
backingState.set(pathString, newValue);
|
|
79
|
+
localStorage.setItem(KEY, JSON.stringify(Array.from(backingState.entries())));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Subscribe to layout changes to update current state.
|
|
84
|
+
let previous: string[] = [];
|
|
85
|
+
const unsubscribe = registry.subscribe(layoutAtom, (layout) => {
|
|
86
|
+
const removed = previous.filter((id) => !layout.active.includes(id));
|
|
87
|
+
previous = layout.active;
|
|
88
|
+
|
|
89
|
+
const handleUpdate = () => {
|
|
90
|
+
// Mark removed items as not current.
|
|
91
|
+
removed.forEach((id) => {
|
|
92
|
+
const keys = Array.from(backingState.keys()).filter((key) => Path.last(key) === id);
|
|
93
|
+
keys.forEach((key) => {
|
|
94
|
+
setItem(Path.parts(key), 'current', false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Mark active items as current.
|
|
99
|
+
layout.active.forEach((id: string) => {
|
|
100
|
+
const keys = Array.from(new Set([...backingState.keys(), id])).filter((key) => Path.last(key) === id);
|
|
101
|
+
keys.forEach((key) => {
|
|
102
|
+
setItem(Path.parts(key), 'current', true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// TODO(wittjosiah): This is setTimeout because there's a race between the keys being initialized.
|
|
108
|
+
// Keys are initialized on the first render of an item in the navtree.
|
|
109
|
+
// This could be avoided if the location was a path as well and not just an id.
|
|
110
|
+
const timeout = setTimeout(handleUpdate, 500);
|
|
111
|
+
handleUpdate();
|
|
112
|
+
return () => clearTimeout(timeout);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Once graph is ready, expand every node marked open in state so the graph has children loaded for rendering.
|
|
116
|
+
yield* Effect.gen(function* () {
|
|
117
|
+
const { graph } = yield* Capability.waitFor(AppCapabilities.AppGraph);
|
|
118
|
+
|
|
119
|
+
// Always expand the active workspace so its subtree is initialized.
|
|
120
|
+
const layout = registry.get(layoutAtom);
|
|
121
|
+
if (layout.workspace) {
|
|
122
|
+
Graph.expand(graph, layout.workspace, 'child');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Expand persisted open nodes, skipping inactive workspace tabs.
|
|
126
|
+
const openPaths = Array.from(backingState.entries())
|
|
127
|
+
.filter(([, state]) => state.open)
|
|
128
|
+
.map(([pathString]) => Path.parts(pathString))
|
|
129
|
+
.filter((path) => !isTopLevelPath(path));
|
|
130
|
+
for (const path of openPaths) {
|
|
131
|
+
const nodeId = path[path.length - 1];
|
|
132
|
+
if (!nodeId) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
Graph.expand(graph, nodeId, 'child');
|
|
136
|
+
}
|
|
137
|
+
}).pipe(Effect.forkDaemon);
|
|
138
|
+
|
|
139
|
+
return Capability.contributes(
|
|
140
|
+
NavTreeCapabilities.State,
|
|
141
|
+
{
|
|
142
|
+
getItem,
|
|
143
|
+
getItemAtom,
|
|
144
|
+
setItem,
|
|
145
|
+
},
|
|
146
|
+
() => Effect.sync(() => unsubscribe()),
|
|
147
|
+
);
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
@@ -2,43 +2,34 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
5
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import
|
|
7
|
-
import React from 'react';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
IntentPlugin,
|
|
12
|
-
LayoutAction,
|
|
13
|
-
SettingsPlugin,
|
|
14
|
-
contributes,
|
|
15
|
-
createResolver,
|
|
16
|
-
defineCapability,
|
|
17
|
-
useCapability,
|
|
18
|
-
} from '@dxos/app-framework';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import React, { type KeyboardEvent, useCallback, useRef } from 'react';
|
|
9
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
10
|
+
|
|
11
|
+
import { Capabilities, Capability } from '@dxos/app-framework';
|
|
19
12
|
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
|
|
26
|
-
import { ThemePlugin } from '@dxos/plugin-theme';
|
|
27
|
-
import { faker } from '@dxos/random';
|
|
13
|
+
import { useAtomCapability } from '@dxos/app-framework/ui';
|
|
14
|
+
import { AppCapabilities, LayoutOperation } from '@dxos/app-toolkit';
|
|
15
|
+
import { Operation, OperationHandlerSet } from '@dxos/compute';
|
|
16
|
+
import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
|
|
17
|
+
import { random } from '@dxos/random';
|
|
28
18
|
import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
|
|
29
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
30
19
|
import { useAttention, useAttentionAttributes } from '@dxos/react-ui-attention';
|
|
31
20
|
import { Stack, StackItem } from '@dxos/react-ui-stack';
|
|
32
|
-
import {
|
|
21
|
+
import { withLayout } from '@dxos/react-ui/testing';
|
|
22
|
+
import { mx } from '@dxos/ui-theme';
|
|
23
|
+
|
|
24
|
+
import { NavTreeContainer } from '#containers';
|
|
25
|
+
import { storybookGraphBuilders } from '#testing';
|
|
26
|
+
import { translations } from '#translations';
|
|
33
27
|
|
|
34
28
|
import { NavTreePlugin } from '../../NavTreePlugin';
|
|
35
|
-
import { storybookGraphBuilders } from '../../testing';
|
|
36
|
-
import { translations } from '../../translations';
|
|
37
|
-
import { NavTreeContainer } from '../NavTreeContainer';
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
random.seed(1234);
|
|
40
31
|
|
|
41
|
-
const StoryState =
|
|
32
|
+
const StoryState = Capability.make<Atom.Atom<{ tab: string }>>('story-state');
|
|
42
33
|
|
|
43
34
|
// TODO(burdon): Fix outline (e.g., button in sidebar nav is clipped when focused).
|
|
44
35
|
// TODO(burdon): Consider similar containment of: Table, Sheet, Kanban Column, Form, etc.
|
|
@@ -53,11 +44,11 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
|
|
|
53
44
|
<div className='flex p-1 items-center border-b border-separator'>
|
|
54
45
|
<IconButton
|
|
55
46
|
density='coarse'
|
|
56
|
-
icon='ph--
|
|
47
|
+
icon='ph--circle--regular'
|
|
57
48
|
label='Test'
|
|
58
49
|
iconOnly
|
|
59
50
|
variant={hasAttention ? 'primary' : 'ghost'}
|
|
60
|
-
classNames='
|
|
51
|
+
classNames='w-(--dx-rail-action) h-(--dx-rail-action)'
|
|
61
52
|
/>
|
|
62
53
|
<StackItem.ResizeHandle />
|
|
63
54
|
</div>
|
|
@@ -66,25 +57,36 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
|
|
|
66
57
|
|
|
67
58
|
const StoryPlank = ({ attendableId }: { attendableId: string }) => {
|
|
68
59
|
const attentionAttrs = useAttentionAttributes(attendableId);
|
|
60
|
+
const rootElement = useRef<HTMLDivElement | null>(null);
|
|
61
|
+
|
|
62
|
+
// NOTE(thure): This is the same workaround as in Plank, but that component is out of scope for this story.
|
|
63
|
+
// TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
|
|
64
|
+
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
|
65
|
+
if (event.target === event.currentTarget && event.key === 'Escape') {
|
|
66
|
+
rootElement.current?.closest('main')?.focus();
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
69
|
|
|
70
70
|
return (
|
|
71
71
|
<StackItem.Root
|
|
72
72
|
item={{ id: attendableId }}
|
|
73
73
|
{...attentionAttrs}
|
|
74
|
-
classNames='bg-
|
|
74
|
+
classNames='bg-base-surface border-e border-separator'
|
|
75
75
|
size={30}
|
|
76
|
+
onKeyDown={handleKeyDown}
|
|
77
|
+
ref={rootElement}
|
|
76
78
|
>
|
|
77
79
|
<StoryPlankHeading attendableId={attendableId} />
|
|
78
80
|
<StackItem.Content toolbar>
|
|
79
|
-
<Toolbar.Root classNames='border-b border-
|
|
81
|
+
<Toolbar.Root classNames='border-b border-subdued-separator'>
|
|
80
82
|
<Toolbar.Button>Test</Toolbar.Button>
|
|
81
83
|
</Toolbar.Root>
|
|
82
84
|
|
|
83
|
-
<div className={mx(container, 'm-2 bg-
|
|
85
|
+
<div className={mx(container, 'm-2 bg-active-surface')}>
|
|
84
86
|
<Input.Root>
|
|
85
87
|
<Input.Label>Level 1 (group)</Input.Label>
|
|
86
88
|
</Input.Root>
|
|
87
|
-
<div className={mx(container, 'bg-
|
|
89
|
+
<div className={mx(container, 'bg-base-surface')}>
|
|
88
90
|
<Input.Root>
|
|
89
91
|
<Input.Label>Level 2 (base)</Input.Label>
|
|
90
92
|
<Input.TextArea placeholder='Enter text' />
|
|
@@ -97,10 +99,10 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
|
|
|
97
99
|
};
|
|
98
100
|
|
|
99
101
|
const DefaultStory = () => {
|
|
100
|
-
const state =
|
|
102
|
+
const state = useAtomCapability(StoryState);
|
|
101
103
|
|
|
102
104
|
return (
|
|
103
|
-
<Main.Root
|
|
105
|
+
<Main.Root navigationSidebarState='expanded'>
|
|
104
106
|
<Main.NavigationSidebar label='Navigation' classNames='grid'>
|
|
105
107
|
<NavTreeContainer tab={state.tab} />
|
|
106
108
|
</Main.NavigationSidebar>
|
|
@@ -115,36 +117,38 @@ const DefaultStory = () => {
|
|
|
115
117
|
};
|
|
116
118
|
|
|
117
119
|
const meta = {
|
|
118
|
-
title: 'plugins/plugin-navtree/NavTree',
|
|
120
|
+
title: 'plugins/plugin-navtree/components/NavTree',
|
|
119
121
|
component: NavTreeContainer,
|
|
120
122
|
render: DefaultStory,
|
|
121
123
|
decorators: [
|
|
122
|
-
|
|
124
|
+
withLayout({ layout: 'fullscreen' }),
|
|
123
125
|
withPluginManager({
|
|
124
126
|
plugins: [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
...corePlugins(),
|
|
128
|
+
StorybookPlugin({
|
|
129
|
+
initialState: { sidebarState: 'expanded' },
|
|
130
|
+
}),
|
|
131
|
+
|
|
130
132
|
NavTreePlugin(),
|
|
131
|
-
StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
|
|
132
|
-
],
|
|
133
|
-
capabilities: (context) => [
|
|
134
|
-
contributes(StoryState, live({ tab: 'space-0' })),
|
|
135
|
-
contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders(context)),
|
|
136
|
-
contributes(Capabilities.IntentResolver, [
|
|
137
|
-
createResolver({
|
|
138
|
-
intent: LayoutAction.UpdateLayout,
|
|
139
|
-
filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
|
|
140
|
-
Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
|
|
141
|
-
resolve: ({ subject }) => {
|
|
142
|
-
const state = context.getCapability(StoryState);
|
|
143
|
-
state.tab = subject;
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
]),
|
|
147
133
|
],
|
|
134
|
+
capabilities: () => {
|
|
135
|
+
const storyStateAtom = Atom.make({ tab: 'root/space-0' }).pipe(Atom.keepAlive);
|
|
136
|
+
return [
|
|
137
|
+
Capability.contributes(StoryState, storyStateAtom),
|
|
138
|
+
Capability.contributes(AppCapabilities.AppGraphBuilder, storybookGraphBuilders()),
|
|
139
|
+
Capability.contributes(
|
|
140
|
+
Capabilities.OperationHandler,
|
|
141
|
+
OperationHandlerSet.make(
|
|
142
|
+
Operation.withHandler(LayoutOperation.SwitchWorkspace, ({ subject }) =>
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const registry: Registry.Registry = yield* Capability.get(Capabilities.AtomRegistry);
|
|
145
|
+
registry.set(storyStateAtom, { tab: subject });
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
];
|
|
151
|
+
},
|
|
148
152
|
}),
|
|
149
153
|
],
|
|
150
154
|
parameters: {
|
|
@@ -157,47 +161,48 @@ export default meta;
|
|
|
157
161
|
|
|
158
162
|
type Story = StoryObj<typeof NavTreeContainer>;
|
|
159
163
|
|
|
160
|
-
export const Default: Story = {
|
|
164
|
+
export const Default: Story = {
|
|
165
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
166
|
+
const canvas = within(canvasElement);
|
|
161
167
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
translations,
|
|
168
|
+
// Find the element with treegrid role and click on its parent
|
|
169
|
+
const treegridElement = await canvas.findByRole('treegrid');
|
|
170
|
+
const treegridParent = treegridElement.parentElement;
|
|
171
|
+
if (treegridParent) {
|
|
172
|
+
await userEvent.click(treegridParent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Press Escape
|
|
176
|
+
await userEvent.keyboard('{Escape}');
|
|
177
|
+
|
|
178
|
+
// Confirm that focus is on an element with attribute data-main-landmark="0"
|
|
179
|
+
await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
|
|
180
|
+
|
|
181
|
+
// Press Tab
|
|
182
|
+
await userEvent.keyboard('{Tab}');
|
|
183
|
+
|
|
184
|
+
// Confirm that focus is now on an element with data-main-landmark="1"
|
|
185
|
+
await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
|
|
186
|
+
|
|
187
|
+
// Press Tab
|
|
188
|
+
await userEvent.keyboard('{Tab}');
|
|
189
|
+
|
|
190
|
+
// Confirm that focus is now on an element with data-main-landmark="0"
|
|
191
|
+
await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
|
|
192
|
+
|
|
193
|
+
// Press Shift-Tab
|
|
194
|
+
await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
|
|
195
|
+
|
|
196
|
+
// Press Enter
|
|
197
|
+
await userEvent.keyboard('{Enter}');
|
|
198
|
+
|
|
199
|
+
// Confirm that focus is now on an element with data-attendable-id="space-0:object-0"
|
|
200
|
+
await expect(document.activeElement).toHaveAttribute('data-attendable-id', 'space-0:object-0');
|
|
201
|
+
|
|
202
|
+
// Press Escape
|
|
203
|
+
await userEvent.keyboard('{Escape}');
|
|
204
|
+
|
|
205
|
+
// Confirm that focus is now on an element with data-main-landmark="1"
|
|
206
|
+
await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
|
|
202
207
|
},
|
|
203
208
|
};
|