@dxos/plugin-navtree 0.8.4-main.67995b8 → 0.8.4-main.69d29f4

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 (178) hide show
  1. package/dist/lib/browser/app-graph-builder-RSMA6I22.mjs +43 -0
  2. package/dist/lib/browser/app-graph-builder-RSMA6I22.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-6YPQ625I.mjs +1293 -0
  4. package/dist/lib/browser/chunk-6YPQ625I.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-AGQDZBPL.mjs +23 -0
  6. package/dist/lib/browser/chunk-AGQDZBPL.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-YKTYPINZ.mjs +19 -0
  8. package/dist/lib/browser/chunk-YKTYPINZ.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +82 -95
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/{keyboard-QRS6WOQH.mjs → keyboard-32EH3N4K.mjs} +20 -19
  12. package/dist/lib/browser/keyboard-32EH3N4K.mjs.map +7 -0
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs +52 -0
  15. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs.map +7 -0
  16. package/dist/lib/browser/react-surface-F6CVMS3Y.mjs +58 -0
  17. package/dist/lib/browser/react-surface-F6CVMS3Y.mjs.map +7 -0
  18. package/dist/lib/browser/state-EVYQOUI3.mjs +120 -0
  19. package/dist/lib/browser/state-EVYQOUI3.mjs.map +7 -0
  20. package/dist/types/src/NavTreePlugin.d.ts +2 -1
  21. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  22. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  23. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  25. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  26. package/dist/types/src/capabilities/index.d.ts +5 -105
  27. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  28. package/dist/types/src/capabilities/keyboard/index.d.ts +3 -0
  29. package/dist/types/src/capabilities/keyboard/index.d.ts.map +1 -0
  30. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +5 -0
  31. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  33. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  35. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  36. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  37. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  38. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  39. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/state/index.d.ts +3 -0
  41. package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/state/state.d.ts +6 -0
  43. package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
  44. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +3 -2
  45. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -1
  46. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -1
  47. package/dist/types/src/components/NavTree/NavTree.d.ts +30 -1
  48. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  49. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +29 -4
  50. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/NavTreeContainer.d.ts +8 -1
  52. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  53. package/dist/types/src/components/NavTreeDocumentTitle.d.ts +1 -1
  54. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -1
  55. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +6 -6
  56. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  57. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +33 -4
  58. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  60. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  61. package/dist/types/src/components/Sidebar/L0Menu.d.ts +6 -4
  62. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  63. package/dist/types/src/components/Sidebar/L1Panel.d.ts +1 -1
  64. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +2 -2
  66. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  67. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  68. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  69. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  70. package/dist/types/src/components/index.d.ts.map +1 -1
  71. package/dist/types/src/components/types.d.ts +8 -8
  72. package/dist/types/src/components/types.d.ts.map +1 -1
  73. package/dist/types/src/experimental/Tree.d.ts +1 -2
  74. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  75. package/dist/types/src/experimental/Tree.stories.d.ts +7 -4
  76. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  77. package/dist/types/src/hooks.d.ts +32 -1
  78. package/dist/types/src/hooks.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +1 -2
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/meta.d.ts +3 -4
  82. package/dist/types/src/meta.d.ts.map +1 -1
  83. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  84. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  85. package/dist/types/src/testing/generator.d.ts +5 -6
  86. package/dist/types/src/testing/generator.d.ts.map +1 -1
  87. package/dist/types/src/testing/update-state.d.ts +2 -2
  88. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  89. package/dist/types/src/types/capabilities.d.ts +16 -0
  90. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  91. package/dist/types/src/types/events.d.ts +5 -0
  92. package/dist/types/src/types/events.d.ts.map +1 -0
  93. package/dist/types/src/types/index.d.ts +4 -0
  94. package/dist/types/src/types/index.d.ts.map +1 -0
  95. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  96. package/dist/types/src/types/schema.d.ts.map +1 -0
  97. package/dist/types/src/util.d.ts +8 -9
  98. package/dist/types/src/util.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +56 -55
  101. package/src/NavTreePlugin.tsx +65 -83
  102. package/src/capabilities/app-graph-builder/app-graph-builder.ts +41 -0
  103. package/src/capabilities/app-graph-builder/index.ts +7 -0
  104. package/src/capabilities/index.ts +5 -9
  105. package/src/capabilities/keyboard/index.ts +7 -0
  106. package/src/capabilities/keyboard/keyboard.ts +65 -0
  107. package/src/capabilities/operation-resolver/index.ts +7 -0
  108. package/src/capabilities/operation-resolver/operation-resolver.ts +41 -0
  109. package/src/capabilities/react-surface/index.ts +7 -0
  110. package/src/capabilities/react-surface/react-surface.tsx +54 -0
  111. package/src/capabilities/state/index.ts +7 -0
  112. package/src/capabilities/state/state.ts +121 -0
  113. package/src/components/CommandsDialog/CommandsDialog.tsx +109 -98
  114. package/src/components/CommandsDialog/CommandsTrigger.tsx +6 -15
  115. package/src/components/NavTree/NavTree.stories.tsx +97 -92
  116. package/src/components/NavTree/NavTree.tsx +9 -15
  117. package/src/components/NavTreeContainer.tsx +280 -256
  118. package/src/components/NavTreeDocumentTitle.tsx +5 -4
  119. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +20 -12
  120. package/src/components/NavTreeItem/NavTreeItemAction.tsx +25 -18
  121. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +36 -29
  122. package/src/components/Sidebar/L0Menu.tsx +114 -139
  123. package/src/components/Sidebar/L1Panel.tsx +32 -35
  124. package/src/components/Sidebar/L1Tabs.tsx +7 -31
  125. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -8
  126. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  127. package/src/components/index.ts +1 -0
  128. package/src/components/types.ts +9 -8
  129. package/src/experimental/Tree.stories.tsx +28 -30
  130. package/src/experimental/Tree.tsx +7 -24
  131. package/src/hooks.ts +54 -2
  132. package/src/index.ts +1 -2
  133. package/src/meta.ts +11 -8
  134. package/src/testing/app-graph-builder.ts +159 -234
  135. package/src/testing/generator.ts +20 -18
  136. package/src/testing/update-state.ts +6 -6
  137. package/src/types/capabilities.ts +21 -0
  138. package/src/types/events.ts +11 -0
  139. package/src/types/index.ts +7 -0
  140. package/src/{types.ts → types/schema.ts} +9 -8
  141. package/src/util.ts +14 -35
  142. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs +0 -45
  143. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-ERDGQTH5.mjs +0 -16
  145. package/dist/lib/browser/chunk-ERDGQTH5.mjs.map +0 -7
  146. package/dist/lib/browser/chunk-WCARAZ7J.mjs +0 -15
  147. package/dist/lib/browser/chunk-WCARAZ7J.mjs.map +0 -7
  148. package/dist/lib/browser/chunk-WEMCSQI7.mjs +0 -1469
  149. package/dist/lib/browser/chunk-WEMCSQI7.mjs.map +0 -7
  150. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs +0 -47
  151. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs.map +0 -7
  152. package/dist/lib/browser/keyboard-QRS6WOQH.mjs.map +0 -7
  153. package/dist/lib/browser/react-surface-JSJ5SQX5.mjs +0 -51
  154. package/dist/lib/browser/react-surface-JSJ5SQX5.mjs.map +0 -7
  155. package/dist/lib/browser/state-54DY3DOY.mjs +0 -117
  156. package/dist/lib/browser/state-54DY3DOY.mjs.map +0 -7
  157. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  158. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  159. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  160. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  161. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  162. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  163. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  164. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  165. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  166. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  167. package/dist/types/src/capabilities/state.d.ts +0 -20
  168. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  169. package/dist/types/src/events.d.ts +0 -4
  170. package/dist/types/src/events.d.ts.map +0 -1
  171. package/dist/types/src/types.d.ts.map +0 -1
  172. package/src/capabilities/app-graph-builder.ts +0 -52
  173. package/src/capabilities/capabilities.ts +0 -20
  174. package/src/capabilities/intent-resolver.ts +0 -37
  175. package/src/capabilities/keyboard.ts +0 -59
  176. package/src/capabilities/react-surface.tsx +0 -45
  177. package/src/capabilities/state.ts +0 -104
  178. package/src/events.ts +0 -11
@@ -0,0 +1,121 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { Capability, Common } from '@dxos/app-framework';
9
+ import { Path } from '@dxos/react-ui-list';
10
+
11
+ import { meta } from '../../meta';
12
+ import { type NavTreeCapabilities as NC, NavTreeCapabilities } from '../../types';
13
+
14
+ const KEY = `${meta.id}/state/v1`;
15
+
16
+ /** Default item state for new entries. */
17
+ const defaultItemState: NC.NavTreeItemState = { open: false, current: false, alternateTree: false };
18
+
19
+ /** Default state entries for initial tree structure. */
20
+ // TODO(thure): Initialize these dynamically.
21
+ const defaultStateEntries: [string, NC.NavTreeItemState][] = [
22
+ ['root', { open: true, current: false }],
23
+ ['root~dxos.org/plugin/space-spaces', { open: true, current: false }],
24
+ ['root~dxos.org/plugin/files', { open: true, current: false }],
25
+ ];
26
+
27
+ const getInitialState = (): Map<string, NC.NavTreeItemState> => {
28
+ const stringified = localStorage.getItem(KEY);
29
+ if (!stringified) {
30
+ return new Map(defaultStateEntries);
31
+ }
32
+
33
+ try {
34
+ const cached: [string, { open: boolean; current: boolean }][] = JSON.parse(stringified);
35
+ return new Map(cached.map(([key, value]) => [key, { open: value.open, current: false }]));
36
+ } catch {
37
+ return new Map(defaultStateEntries);
38
+ }
39
+ };
40
+
41
+ export default Capability.makeModule(
42
+ Effect.fnUntraced(function* () {
43
+ const registry = yield* Capability.get(Common.Capability.AtomRegistry);
44
+ const layoutAtom = yield* Capability.get(Common.Capability.Layout);
45
+
46
+ // Backing state for persistence (not reactive).
47
+ const backingState = getInitialState();
48
+
49
+ // Per-path atom family for fine-grained reactivity.
50
+ // keepAlive prevents atoms from being garbage collected when components unmount,
51
+ // ensuring state is preserved across deletion/restoration cycles.
52
+ const itemAtomFamily = Atom.family((pathString: string) =>
53
+ Atom.make<NC.NavTreeItemState>(backingState.get(pathString) ?? { ...defaultItemState }).pipe(Atom.keepAlive),
54
+ );
55
+
56
+ const getItemAtom = (path: string[]): Atom.Atom<NC.NavTreeItemState> => {
57
+ const pathString = Path.create(...path);
58
+ return itemAtomFamily(pathString);
59
+ };
60
+
61
+ const getItem = (path: string[]): NC.NavTreeItemState => {
62
+ return registry.get(getItemAtom(path));
63
+ };
64
+
65
+ const setItem = (path: string[], key: 'open' | 'current' | 'alternateTree', next: boolean) => {
66
+ const pathString = Path.create(...path);
67
+ const atom = itemAtomFamily(pathString);
68
+ const currentValue = registry.get(atom);
69
+ const newValue = { ...currentValue, [key]: next };
70
+
71
+ // Update the atom.
72
+ registry.set(atom, newValue);
73
+
74
+ // Update backing state and persist to localStorage.
75
+ backingState.set(pathString, newValue);
76
+ localStorage.setItem(KEY, JSON.stringify(Array.from(backingState.entries())));
77
+ };
78
+
79
+ // Subscribe to layout changes to update current state.
80
+ let previous: string[] = [];
81
+ const unsubscribe = registry.subscribe(layoutAtom, (layout) => {
82
+ const removed = previous.filter((id) => !layout.active.includes(id));
83
+ previous = layout.active;
84
+
85
+ const handleUpdate = () => {
86
+ // Mark removed items as not current.
87
+ removed.forEach((id) => {
88
+ const keys = Array.from(backingState.keys()).filter((key) => Path.last(key) === id);
89
+ keys.forEach((key) => {
90
+ setItem(Path.parts(key), 'current', false);
91
+ });
92
+ });
93
+
94
+ // Mark active items as current.
95
+ layout.active.forEach((id: string) => {
96
+ const keys = Array.from(new Set([...backingState.keys(), id])).filter((key) => Path.last(key) === id);
97
+ keys.forEach((key) => {
98
+ setItem(Path.parts(key), 'current', true);
99
+ });
100
+ });
101
+ };
102
+
103
+ // TODO(wittjosiah): This is setTimeout because there's a race between the keys being initialized.
104
+ // Keys are initialized on the first render of an item in the navtree.
105
+ // This could be avoided if the location was a path as well and not just an id.
106
+ const timeout = setTimeout(handleUpdate, 500);
107
+ handleUpdate();
108
+ return () => clearTimeout(timeout);
109
+ });
110
+
111
+ return Capability.contributes(
112
+ NavTreeCapabilities.State,
113
+ {
114
+ getItem,
115
+ getItemAtom,
116
+ setItem,
117
+ },
118
+ () => Effect.sync(() => unsubscribe()),
119
+ );
120
+ }),
121
+ );
@@ -2,117 +2,128 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo, useState } from 'react';
5
+ import React, { forwardRef, useMemo, useState } from 'react';
6
6
 
7
- import { createIntent, LayoutAction, useAppGraph, useIntentDispatcher } from '@dxos/app-framework';
8
- import { type ActionLike, isAction, isActionGroup } from '@dxos/app-graph';
7
+ import { Common } from '@dxos/app-framework';
8
+ import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
9
9
  import { Keyboard, keySymbols } from '@dxos/keyboard';
10
+ import { Graph, Node, useActionRunner } from '@dxos/plugin-graph';
10
11
  import { useActions } from '@dxos/plugin-graph';
11
- import { Button, Dialog, Icon, useTranslation, toLocalizedString } from '@dxos/react-ui';
12
- import { SearchList } from '@dxos/react-ui-searchlist';
13
- import {
14
- cardDialogContent,
15
- cardDialogHeader,
16
- cardDialogPaddedOverflow,
17
- cardDialogSearchListRoot,
18
- } from '@dxos/react-ui-stack';
19
- import { descriptionText, mx } from '@dxos/react-ui-theme';
12
+ import { Button, Dialog, toLocalizedString, useTranslation } from '@dxos/react-ui';
13
+ import { SearchList, useSearchListResults } from '@dxos/react-ui-searchlist';
14
+ import { osTranslations } from '@dxos/ui-theme';
20
15
  import { getHostPlatform } from '@dxos/util';
21
16
 
22
- import { KEY_BINDING, NAVTREE_PLUGIN } from '../../meta';
17
+ import { KEY_BINDING, meta } from '../../meta';
18
+
19
+ export type CommandsDialogContentProps = {
20
+ selected?: string;
21
+ };
23
22
 
24
23
  // TODO(wittjosiah): This probably deserves its own plugin but for now it lives here w/ other navigation UI.
25
- export const CommandsDialogContent = ({ selected: initial }: { selected?: string }) => {
26
- const { t } = useTranslation(NAVTREE_PLUGIN);
27
- const [selected, setSelected] = useState<string | undefined>(initial);
28
- const { dispatchPromise: dispatch } = useIntentDispatcher();
29
- const { graph } = useAppGraph();
24
+ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogContentProps>(
25
+ ({ selected: initial }, forwardedRef) => {
26
+ const { t } = useTranslation(meta.id);
27
+ const { invokeSync } = useOperationInvoker();
28
+ const runAction = useActionRunner();
29
+ const { graph } = useAppGraph();
30
+ const [selected, setSelected] = useState<string | undefined>(initial);
30
31
 
31
- // Traverse graph.
32
- // TODO(burdon): Factor out commonality with shortcut dialog.
33
- const allActions = useMemo(() => {
34
- // TODO(burdon): Get from navtree (not keyboard).
35
- const current = Keyboard.singleton.getCurrentContext();
36
- const actionMap = new Set<string>();
37
- const actions: ActionLike[] = [];
38
- graph.traverse({
39
- visitor: (node, path) => {
40
- if (
41
- (isAction(node) || isActionGroup(node)) &&
42
- !actionMap.has(node.id) &&
43
- current.startsWith(path.slice(0, -1).join('/'))
44
- ) {
45
- actionMap.add(node.id);
46
- actions.push(node);
47
- }
48
- },
49
- });
32
+ // Traverse graph.
33
+ // TODO(burdon): Factor out commonality with shortcut dialog.
34
+ const allActions = useMemo(() => {
35
+ // TODO(burdon): Get from navtree (not keyboard).
36
+ const current = Keyboard.singleton.getCurrentContext();
37
+ const actionMap = new Set<string>();
38
+ const actions: Node.ActionLike[] = [];
39
+ Graph.traverse(graph, {
40
+ visitor: (node, path) => {
41
+ if (
42
+ (Node.isAction(node) || Node.isActionGroup(node)) &&
43
+ !actionMap.has(node.id) &&
44
+ current.startsWith(path.slice(0, -1).join('/'))
45
+ ) {
46
+ actionMap.add(node.id);
47
+ actions.push(node);
48
+ }
49
+ },
50
+ });
50
51
 
51
- actions.sort((a, b) => {
52
- return toLocalizedString(a.properties.label, t)
53
- ?.toLowerCase()
54
- .localeCompare(toLocalizedString(b.properties.label, t)?.toLowerCase());
55
- });
52
+ actions.sort((a, b) => {
53
+ return toLocalizedString(a.properties.label, t)
54
+ ?.toLowerCase()
55
+ .localeCompare(toLocalizedString(b.properties.label, t)?.toLowerCase());
56
+ });
56
57
 
57
- return actions;
58
- }, [graph]);
58
+ return actions;
59
+ }, [graph]);
59
60
 
60
- const group = allActions.find(({ id }) => id === selected);
61
- const groupActions = useActions(graph, group?.id);
62
- const actions = isActionGroup(group) ? groupActions : allActions;
61
+ const group = allActions.find(({ id }) => id === selected);
62
+ const groupActions = useActions(graph, group?.id);
63
+ const actions = Node.isActionGroup(group) ? groupActions : allActions;
63
64
 
64
- return (
65
- <Dialog.Content classNames={cardDialogContent}>
66
- <Dialog.Title classNames={cardDialogHeader}>{t('commands dialog title', { ns: NAVTREE_PLUGIN })}</Dialog.Title>
65
+ const { results, handleSearch } = useSearchListResults({
66
+ items: actions,
67
+ extract: (action) => toLocalizedString(action.properties.label, t),
68
+ });
67
69
 
68
- {/* TODO(burdon): BUG: Overscrolls container. */}
69
- <SearchList.Root label={t('command list input placeholder')} classNames={cardDialogSearchListRoot}>
70
- <SearchList.Input placeholder={t('command list input placeholder')} />
71
- <SearchList.Content classNames={cardDialogPaddedOverflow}>
72
- {actions?.map((action) => {
73
- const label = toLocalizedString(action.properties.label, t);
74
- const shortcut =
75
- typeof action.properties.keyBinding === 'string'
76
- ? action.properties.keyBinding
77
- : action.properties.keyBinding?.[getHostPlatform()];
78
- return (
79
- <SearchList.Item
80
- value={label}
81
- key={action.id}
82
- onSelect={() => {
83
- if (action.properties.disabled) {
84
- return;
85
- }
70
+ return (
71
+ <Dialog.Content ref={forwardedRef}>
72
+ <Dialog.Title>{t('commands dialog title', { ns: meta.id })}</Dialog.Title>
73
+ <SearchList.Root onSearch={handleSearch}>
74
+ <div aria-label={t('command list input placeholder')} role='combobox' aria-expanded='true'>
75
+ <SearchList.Input placeholder={t('command list input placeholder')} />
76
+ <SearchList.Content>
77
+ <SearchList.Viewport>
78
+ {results.map((action) => {
79
+ const shortcut =
80
+ typeof action.properties.keyBinding === 'string'
81
+ ? action.properties.keyBinding
82
+ : action.properties.keyBinding?.[getHostPlatform()];
86
83
 
87
- if (isActionGroup(action)) {
88
- setSelected(action.id);
89
- return;
90
- }
84
+ return (
85
+ <SearchList.Item
86
+ value={action.id}
87
+ key={action.id}
88
+ label={toLocalizedString(action.properties.label, t)}
89
+ icon={action.properties.icon}
90
+ suffix={shortcut ? keySymbols(shortcut).join('') : undefined}
91
+ onSelect={() => {
92
+ if (action.properties.disabled) {
93
+ return;
94
+ }
91
95
 
92
- void dispatch(createIntent(LayoutAction.UpdateDialog, { part: 'dialog', options: { state: false } }));
93
- setTimeout(() => {
94
- const node = graph.getConnections(group?.id ?? action.id, 'inbound')[0];
95
- void (node && isAction(action) && action.data({ parent: node, caller: KEY_BINDING }));
96
- });
97
- }}
98
- classNames='flex items-center gap-2'
99
- disabled={action.properties.disabled}
100
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
101
- >
102
- <Icon icon={action.properties.icon} size={4} />
103
- <span className='grow truncate'>{label}</span>
104
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
105
- </SearchList.Item>
106
- );
107
- })}
108
- </SearchList.Content>
109
- </SearchList.Root>
96
+ if (Node.isActionGroup(action)) {
97
+ setSelected(action.id);
98
+ return;
99
+ }
110
100
 
111
- <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
112
- <Dialog.Close asChild>
113
- <Button classNames='is-full'>{t('close label', { ns: 'os' })}</Button>
114
- </Dialog.Close>
115
- </div>
116
- </Dialog.Content>
117
- );
118
- };
101
+ invokeSync(Common.LayoutOperation.UpdateDialog, { state: false });
102
+ setTimeout(() => {
103
+ const node = Graph.getConnections(graph, group?.id ?? action.id, 'inbound')[0];
104
+ if (node && Node.isAction(action)) {
105
+ void runAction(action, { parent: node, caller: KEY_BINDING });
106
+ }
107
+ });
108
+ }}
109
+ classNames='flex items-center gap-2'
110
+ disabled={action.properties.disabled}
111
+ {...(action.properties?.testId && {
112
+ 'data-testid': action.properties.testId,
113
+ })}
114
+ />
115
+ );
116
+ })}
117
+ </SearchList.Viewport>
118
+ </SearchList.Content>
119
+ </div>
120
+ </SearchList.Root>
121
+ <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
122
+ <Dialog.Close asChild>
123
+ <Button classNames='is-full'>{t('close label', { ns: osTranslations })}</Button>
124
+ </Dialog.Close>
125
+ </div>
126
+ </Dialog.Content>
127
+ );
128
+ },
129
+ );
@@ -4,29 +4,20 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
7
+ import { Common } from '@dxos/app-framework';
8
+ import { useOperationInvoker } from '@dxos/app-framework/react';
8
9
  import { Button, Icon, useTranslation } from '@dxos/react-ui';
9
10
 
10
- import { COMMANDS_DIALOG, NAVTREE_PLUGIN } from '../../meta';
11
+ import { COMMANDS_DIALOG, meta } from '../../meta';
11
12
 
12
13
  // TODO(thure): Refactor to be handled by a more appropriate plugin.
13
14
  export const CommandsTrigger = () => {
14
- const { dispatchPromise: dispatch } = useIntentDispatcher();
15
- const { t } = useTranslation(NAVTREE_PLUGIN);
15
+ const { invokeSync } = useOperationInvoker();
16
+ const { t } = useTranslation(meta.id);
16
17
  return (
17
18
  <Button
18
19
  classNames='m-1 pli-1 lg:pli-2'
19
- onClick={() =>
20
- dispatch(
21
- createIntent(LayoutAction.UpdateDialog, {
22
- part: 'dialog',
23
- subject: COMMANDS_DIALOG,
24
- options: {
25
- blockAlign: 'start',
26
- },
27
- }),
28
- )
29
- }
20
+ onClick={() => invokeSync(Common.LayoutOperation.UpdateDialog, { subject: COMMANDS_DIALOG, blockAlign: 'start' })}
30
21
  >
31
22
  <span className='text-description font-normal grow text-start'>{t('command list input placeholder')}</span>
32
23
  <Icon icon='ph--magnifying-glass--regular' size={5} />
@@ -2,36 +2,23 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
9
- import React from 'react';
10
-
11
- import {
12
- Capabilities,
13
- IntentPlugin,
14
- LayoutAction,
15
- SettingsPlugin,
16
- contributes,
17
- createResolver,
18
- defineCapability,
19
- useCapability,
20
- } from '@dxos/app-framework';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
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 { Capability, Common } from '@dxos/app-framework';
12
+ import { useAtomCapability } from '@dxos/app-framework/react';
21
13
  import { withPluginManager } from '@dxos/app-framework/testing';
22
- import { live } from '@dxos/live-object';
23
- import { AttentionPlugin } from '@dxos/plugin-attention';
24
- import { ClientPlugin } from '@dxos/plugin-client';
25
- import { GraphPlugin } from '@dxos/plugin-graph';
26
- import { SpacePlugin } from '@dxos/plugin-space';
27
- import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
28
- import { ThemePlugin } from '@dxos/plugin-theme';
14
+ import { OperationResolver } from '@dxos/operation';
15
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
29
16
  import { faker } from '@dxos/random';
30
17
  import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
31
- import { useAttentionAttributes, useAttention } from '@dxos/react-ui-attention';
18
+ import { withTheme } from '@dxos/react-ui/testing';
19
+ import { useAttention, useAttentionAttributes } from '@dxos/react-ui-attention';
32
20
  import { Stack, StackItem } from '@dxos/react-ui-stack';
33
- import { defaultTx, mx } from '@dxos/react-ui-theme';
34
- import { withLayout } from '@dxos/storybook-utils';
21
+ import { mx } from '@dxos/ui-theme';
35
22
 
36
23
  import { NavTreePlugin } from '../../NavTreePlugin';
37
24
  import { storybookGraphBuilders } from '../../testing';
@@ -40,7 +27,7 @@ import { NavTreeContainer } from '../NavTreeContainer';
40
27
 
41
28
  faker.seed(1234);
42
29
 
43
- const StoryState = defineCapability<{ tab: string }>('story-state');
30
+ const StoryState = Capability.make<Atom.Atom<{ tab: string }>>('story-state');
44
31
 
45
32
  // TODO(burdon): Fix outline (e.g., button in sidebar nav is clipped when focused).
46
33
  // TODO(burdon): Consider similar containment of: Table, Sheet, Kanban Column, Form, etc.
@@ -52,7 +39,7 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
52
39
  const { hasAttention } = useAttention(attendableId);
53
40
  console.log('hasAttention', hasAttention);
54
41
  return (
55
- <div className='flex p-1 items-center border-b border-separator'>
42
+ <div className='flex p-1 items-center border-be border-separator'>
56
43
  <IconButton
57
44
  density='coarse'
58
45
  icon='ph--atom--regular'
@@ -68,6 +55,15 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
68
55
 
69
56
  const StoryPlank = ({ attendableId }: { attendableId: string }) => {
70
57
  const attentionAttrs = useAttentionAttributes(attendableId);
58
+ const rootElement = useRef<HTMLDivElement | null>(null);
59
+
60
+ // NOTE(thure): This is the same workaround as in Plank, but that component is out of scope for this story.
61
+ // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
62
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
63
+ if (event.target === event.currentTarget && event.key === 'Escape') {
64
+ rootElement.current?.closest('main')?.focus();
65
+ }
66
+ }, []);
71
67
 
72
68
  return (
73
69
  <StackItem.Root
@@ -75,10 +71,12 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
75
71
  {...attentionAttrs}
76
72
  classNames='bg-baseSurface border-ie border-separator'
77
73
  size={30}
74
+ onKeyDown={handleKeyDown}
75
+ ref={rootElement}
78
76
  >
79
77
  <StoryPlankHeading attendableId={attendableId} />
80
78
  <StackItem.Content toolbar>
81
- <Toolbar.Root classNames='border-b border-subduedSeparator'>
79
+ <Toolbar.Root classNames='border-be border-subduedSeparator'>
82
80
  <Toolbar.Button>Test</Toolbar.Button>
83
81
  </Toolbar.Root>
84
82
 
@@ -99,7 +97,7 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
99
97
  };
100
98
 
101
99
  const DefaultStory = () => {
102
- const state = useCapability(StoryState);
100
+ const state = useAtomCapability(StoryState);
103
101
 
104
102
  return (
105
103
  <Main.Root complementarySidebarState='closed'>
@@ -116,85 +114,92 @@ const DefaultStory = () => {
116
114
  );
117
115
  };
118
116
 
119
- const meta: Meta<typeof NavTreeContainer> = {
117
+ const meta = {
120
118
  title: 'plugins/plugin-navtree/NavTree',
121
119
  component: NavTreeContainer,
122
120
  render: DefaultStory,
123
121
  decorators: [
122
+ withTheme,
124
123
  withPluginManager({
125
124
  plugins: [
126
- ThemePlugin({ tx: defaultTx }),
127
- StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
128
- GraphPlugin(),
129
- IntentPlugin(),
130
- SettingsPlugin(),
131
- AttentionPlugin(),
125
+ ...corePlugins(),
126
+ StorybookPlugin({
127
+ initialState: { sidebarState: 'expanded' },
128
+ }),
129
+
132
130
  NavTreePlugin(),
133
131
  ],
134
- capabilities: (context) => [
135
- contributes(StoryState, live({ tab: 'space-0' })),
136
- contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders(context)),
137
- contributes(Capabilities.IntentResolver, [
138
- createResolver({
139
- intent: LayoutAction.UpdateLayout,
140
- filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
141
- Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
142
- resolve: ({ subject }) => {
143
- const state = context.getCapability(StoryState);
144
- state.tab = subject;
145
- },
146
- }),
147
- ]),
148
- ],
132
+ capabilities: () => {
133
+ const storyStateAtom = Atom.make({ tab: 'space-0' }).pipe(Atom.keepAlive);
134
+ return [
135
+ Capability.contributes(StoryState, storyStateAtom),
136
+ Capability.contributes(Common.Capability.AppGraphBuilder, storybookGraphBuilders()),
137
+ Capability.contributes(Common.Capability.OperationResolver, [
138
+ OperationResolver.make({
139
+ operation: Common.LayoutOperation.SwitchWorkspace,
140
+ handler: ({ subject }) =>
141
+ Effect.gen(function* () {
142
+ const registry: Registry.Registry = yield* Capability.get(Common.Capability.AtomRegistry);
143
+ registry.set(storyStateAtom, { tab: subject });
144
+ }),
145
+ }),
146
+ ]),
147
+ ];
148
+ },
149
149
  }),
150
- withLayout({ fullscreen: true }),
151
150
  ],
152
151
  parameters: {
152
+ layout: 'fullscreen',
153
153
  translations,
154
154
  },
155
- };
155
+ } satisfies Meta<typeof NavTreeContainer>;
156
156
 
157
157
  export default meta;
158
158
 
159
159
  type Story = StoryObj<typeof NavTreeContainer>;
160
160
 
161
- export const Default: Story = {};
161
+ export const Default: Story = {
162
+ play: async ({ canvasElement }) => {
163
+ const canvas = within(canvasElement);
162
164
 
163
- // TODO(wittjosiah): Deduplicate plugins/capabilities with default story.
164
- export const WithClient: Story = {
165
- decorators: [
166
- withPluginManager({
167
- plugins: [
168
- ThemePlugin({ tx: defaultTx }),
169
- GraphPlugin(),
170
- IntentPlugin(),
171
- SettingsPlugin(),
172
- AttentionPlugin(),
173
- NavTreePlugin(),
174
- ClientPlugin({
175
- onClientInitialized: async (_, client) => {
176
- await client.halo.createIdentity();
177
- },
178
- }),
179
- SpacePlugin(),
180
- // Needs to be last so that the dialog and popovers have access to all contexts.
181
- StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
182
- ],
183
- capabilities: (context) => [
184
- contributes(StoryState, live({ tab: 'space-0' })),
185
- contributes(Capabilities.IntentResolver, [
186
- createResolver({
187
- intent: LayoutAction.UpdateLayout,
188
- filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
189
- Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
190
- resolve: ({ subject }) => {
191
- const state = context.getCapability(StoryState);
192
- state.tab = subject;
193
- },
194
- }),
195
- ]),
196
- ],
197
- }),
198
- withLayout({ fullscreen: true }),
199
- ],
165
+ // Find the element with treegrid role and click on its parent
166
+ const treegridElement = await canvas.findByRole('treegrid');
167
+ const treegridParent = treegridElement.parentElement;
168
+ if (treegridParent) {
169
+ await userEvent.click(treegridParent);
170
+ }
171
+
172
+ // Press Escape
173
+ await userEvent.keyboard('{Escape}');
174
+
175
+ // Confirm that focus is on an element with attribute data-main-landmark="0"
176
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
177
+
178
+ // Press Tab
179
+ await userEvent.keyboard('{Tab}');
180
+
181
+ // Confirm that focus is now on an element with data-main-landmark="1"
182
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
183
+
184
+ // Press Tab
185
+ await userEvent.keyboard('{Tab}');
186
+
187
+ // Confirm that focus is now on an element with data-main-landmark="0"
188
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
189
+
190
+ // Press Shift-Tab
191
+ await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
192
+
193
+ // Press Enter
194
+ await userEvent.keyboard('{Enter}');
195
+
196
+ // Confirm that focus is now on an element with data-attendable-id="space-0:object-0"
197
+ await expect(document.activeElement).toHaveAttribute('data-attendable-id', 'space-0:object-0');
198
+
199
+ // Press Escape
200
+ await userEvent.keyboard('{Escape}');
201
+
202
+ // Confirm that focus is now on an element with data-main-landmark="1"
203
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
204
+ },
200
205
  };