@dxos/plugin-navtree 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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 (226) hide show
  1. package/dist/lib/browser/CommandsDialogContent-A27AKTHH.mjs +111 -0
  2. package/dist/lib/browser/CommandsDialogContent-A27AKTHH.mjs.map +7 -0
  3. package/dist/lib/browser/CommandsTrigger-HBZYQCGV.mjs +33 -0
  4. package/dist/lib/browser/CommandsTrigger-HBZYQCGV.mjs.map +7 -0
  5. package/dist/lib/browser/NavTreeContainer-B5K4FUJS.mjs +12 -0
  6. package/dist/lib/browser/NavTreeContainer-B5K4FUJS.mjs.map +7 -0
  7. package/dist/lib/browser/NavTreeDocumentTitle-GEE3URWH.mjs +26 -0
  8. package/dist/lib/browser/NavTreeDocumentTitle-GEE3URWH.mjs.map +7 -0
  9. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs +44 -0
  10. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-DQCYMXEU.mjs +1426 -0
  12. package/dist/lib/browser/chunk-DQCYMXEU.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-H3OJ4WLF.mjs +14 -0
  14. package/dist/lib/browser/chunk-H3OJ4WLF.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-X2HDG62A.mjs +23 -0
  16. package/dist/lib/browser/chunk-X2HDG62A.mjs.map +7 -0
  17. package/dist/lib/browser/chunk-YKTYPINZ.mjs +19 -0
  18. package/dist/lib/browser/chunk-YKTYPINZ.mjs.map +7 -0
  19. package/dist/lib/browser/index.mjs +86 -91
  20. package/dist/lib/browser/index.mjs.map +4 -4
  21. package/dist/lib/browser/{keyboard-AHSAWKXA.mjs → keyboard-OJZC54PK.mjs} +22 -19
  22. package/dist/lib/browser/keyboard-OJZC54PK.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/operation-resolver-BNAMYOHF.mjs +53 -0
  25. package/dist/lib/browser/operation-resolver-BNAMYOHF.mjs.map +7 -0
  26. package/dist/lib/browser/react-surface-AYHOAJ4I.mjs +59 -0
  27. package/dist/lib/browser/react-surface-AYHOAJ4I.mjs.map +7 -0
  28. package/dist/lib/browser/state-AC3EETN7.mjs +131 -0
  29. package/dist/lib/browser/state-AC3EETN7.mjs.map +7 -0
  30. package/dist/types/src/NavTreePlugin.d.ts +2 -1
  31. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  32. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  33. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  35. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  36. package/dist/types/src/capabilities/index.d.ts +5 -108
  37. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  38. package/dist/types/src/capabilities/keyboard/index.d.ts +3 -0
  39. package/dist/types/src/capabilities/keyboard/index.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +5 -0
  41. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  43. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  44. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  45. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  46. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  47. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  48. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  49. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  50. package/dist/types/src/capabilities/state/index.d.ts +3 -0
  51. package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
  52. package/dist/types/src/capabilities/state/state.d.ts +6 -0
  53. package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
  54. package/dist/types/src/components/NavTree/NavTree.d.ts +11 -4
  55. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  56. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +2 -2
  57. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +6 -6
  59. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  60. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +2 -1
  61. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  63. package/dist/types/src/components/Sidebar/L0Menu.d.ts +8 -5
  64. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -7
  66. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  67. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +5 -4
  68. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  69. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  70. package/dist/types/src/components/index.d.ts +0 -3
  71. package/dist/types/src/components/index.d.ts.map +1 -1
  72. package/dist/types/src/components/types.d.ts +8 -16
  73. package/dist/types/src/components/types.d.ts.map +1 -1
  74. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
  75. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  76. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +3 -0
  77. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  78. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  79. package/dist/types/src/containers/CommandsTrigger/index.d.ts +3 -0
  80. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  81. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  82. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  83. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  84. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  85. package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
  86. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  87. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +3 -0
  88. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  89. package/dist/types/src/containers/index.d.ts +7 -0
  90. package/dist/types/src/containers/index.d.ts.map +1 -0
  91. package/dist/types/src/experimental/Tree.stories.d.ts +15 -4
  92. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  93. package/dist/types/src/hooks/index.d.ts +5 -0
  94. package/dist/types/src/hooks/index.d.ts.map +1 -0
  95. package/dist/types/src/hooks/useActions.d.ts +7 -0
  96. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  97. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  98. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  99. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  100. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  101. package/dist/types/src/hooks/useNavTreeState.d.ts +22 -0
  102. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  103. package/dist/types/src/index.d.ts +1 -2
  104. package/dist/types/src/index.d.ts.map +1 -1
  105. package/dist/types/src/meta.d.ts +2 -2
  106. package/dist/types/src/meta.d.ts.map +1 -1
  107. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  108. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  109. package/dist/types/src/testing/generator.d.ts +5 -6
  110. package/dist/types/src/testing/generator.d.ts.map +1 -1
  111. package/dist/types/src/testing/update-state.d.ts +2 -2
  112. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  113. package/dist/types/src/types/capabilities.d.ts +16 -0
  114. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  115. package/dist/types/src/types/events.d.ts +5 -0
  116. package/dist/types/src/types/events.d.ts.map +1 -0
  117. package/dist/types/src/types/index.d.ts +4 -0
  118. package/dist/types/src/types/index.d.ts.map +1 -0
  119. package/dist/types/src/{types.d.ts → types/schema.d.ts} +7 -7
  120. package/dist/types/src/types/schema.d.ts.map +1 -0
  121. package/dist/types/src/util.d.ts +11 -7
  122. package/dist/types/src/util.d.ts.map +1 -1
  123. package/dist/types/tsconfig.tsbuildinfo +1 -1
  124. package/package.json +55 -53
  125. package/src/NavTreePlugin.tsx +54 -73
  126. package/src/capabilities/app-graph-builder/app-graph-builder.ts +42 -0
  127. package/src/capabilities/app-graph-builder/index.ts +7 -0
  128. package/src/capabilities/index.ts +5 -9
  129. package/src/capabilities/keyboard/index.ts +7 -0
  130. package/src/capabilities/keyboard/keyboard.ts +66 -0
  131. package/src/capabilities/operation-resolver/index.ts +7 -0
  132. package/src/capabilities/operation-resolver/operation-resolver.ts +44 -0
  133. package/src/capabilities/react-surface/index.ts +7 -0
  134. package/src/capabilities/react-surface/react-surface.tsx +53 -0
  135. package/src/capabilities/state/index.ts +7 -0
  136. package/src/capabilities/state/state.ts +149 -0
  137. package/src/components/NavTree/NavTree.stories.tsx +44 -51
  138. package/src/components/NavTree/NavTree.tsx +63 -32
  139. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +18 -8
  140. package/src/components/NavTreeItem/NavTreeItemAction.tsx +33 -28
  141. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +39 -15
  142. package/src/components/Sidebar/L0Menu.tsx +145 -155
  143. package/src/components/Sidebar/L1Panel.tsx +231 -94
  144. package/src/components/Sidebar/L1Tabs.tsx +18 -33
  145. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +2 -3
  146. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
  147. package/src/components/index.ts +1 -3
  148. package/src/components/types.ts +14 -19
  149. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +133 -0
  150. package/src/containers/CommandsDialogContent/index.ts +7 -0
  151. package/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.tsx +5 -14
  152. package/src/containers/CommandsTrigger/index.ts +7 -0
  153. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +270 -0
  154. package/src/containers/NavTreeContainer/index.ts +7 -0
  155. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +4 -3
  156. package/src/containers/NavTreeDocumentTitle/index.ts +7 -0
  157. package/src/containers/index.ts +12 -0
  158. package/src/experimental/Tree.stories.tsx +54 -60
  159. package/src/experimental/Tree.tsx +3 -3
  160. package/src/hooks/index.ts +8 -0
  161. package/src/hooks/useActions.ts +38 -0
  162. package/src/hooks/useLoadDescendents.ts +23 -0
  163. package/src/hooks/useNavTreeModel.ts +112 -0
  164. package/src/hooks/useNavTreeState.ts +42 -0
  165. package/src/index.ts +1 -2
  166. package/src/meta.ts +7 -2
  167. package/src/testing/app-graph-builder.ts +159 -235
  168. package/src/testing/generator.ts +19 -17
  169. package/src/testing/update-state.ts +6 -6
  170. package/src/types/capabilities.ts +21 -0
  171. package/src/types/events.ts +12 -0
  172. package/src/types/index.ts +7 -0
  173. package/src/{types.ts → types/schema.ts} +6 -6
  174. package/src/util.ts +27 -16
  175. package/dist/lib/browser/app-graph-builder-YAPMENYT.mjs +0 -46
  176. package/dist/lib/browser/app-graph-builder-YAPMENYT.mjs.map +0 -7
  177. package/dist/lib/browser/chunk-IMFXKPSB.mjs +0 -1482
  178. package/dist/lib/browser/chunk-IMFXKPSB.mjs.map +0 -7
  179. package/dist/lib/browser/chunk-LPAREX6J.mjs +0 -15
  180. package/dist/lib/browser/chunk-LPAREX6J.mjs.map +0 -7
  181. package/dist/lib/browser/chunk-ZENLR72O.mjs +0 -14
  182. package/dist/lib/browser/chunk-ZENLR72O.mjs.map +0 -7
  183. package/dist/lib/browser/intent-resolver-TCQXGF6U.mjs +0 -47
  184. package/dist/lib/browser/intent-resolver-TCQXGF6U.mjs.map +0 -7
  185. package/dist/lib/browser/keyboard-AHSAWKXA.mjs.map +0 -7
  186. package/dist/lib/browser/react-surface-AH2TP73M.mjs +0 -51
  187. package/dist/lib/browser/react-surface-AH2TP73M.mjs.map +0 -7
  188. package/dist/lib/browser/state-2Q623V7U.mjs +0 -117
  189. package/dist/lib/browser/state-2Q623V7U.mjs.map +0 -7
  190. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  191. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  192. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  193. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  194. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  195. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  196. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  197. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  198. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  199. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  200. package/dist/types/src/capabilities/state.d.ts +0 -20
  201. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  202. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
  203. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  204. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  205. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  206. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  207. package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
  208. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  209. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  210. package/dist/types/src/events.d.ts +0 -4
  211. package/dist/types/src/events.d.ts.map +0 -1
  212. package/dist/types/src/hooks.d.ts +0 -3
  213. package/dist/types/src/hooks.d.ts.map +0 -1
  214. package/dist/types/src/types.d.ts.map +0 -1
  215. package/src/capabilities/app-graph-builder.ts +0 -53
  216. package/src/capabilities/capabilities.ts +0 -20
  217. package/src/capabilities/intent-resolver.ts +0 -37
  218. package/src/capabilities/keyboard.ts +0 -59
  219. package/src/capabilities/react-surface.tsx +0 -45
  220. package/src/capabilities/state.ts +0 -105
  221. package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
  222. package/src/components/CommandsDialog/index.ts +0 -6
  223. package/src/components/NavTreeContainer.tsx +0 -346
  224. package/src/events.ts +0 -11
  225. package/src/hooks.ts +0 -21
  226. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
@@ -2,150 +2,287 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Option from 'effect/Option';
7
+ import React, { memo, useCallback, useMemo } from 'react';
6
8
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { DensityProvider, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
+ import { Node } from '@dxos/app-graph';
10
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
11
+ import { Graph, useActionRunner, useConnections, useEdges } from '@dxos/plugin-graph';
12
+ import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
13
  import { Tree } from '@dxos/react-ui-list';
14
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
10
15
  import { Tabs } from '@dxos/react-ui-tabs';
11
- import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/react-ui-theme';
16
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
17
 
18
+ import { useActions, useIsAlternateTree, useLoadDescendents } from '../../hooks';
13
19
  import { meta } from '../../meta';
20
+ import { NAV_TREE_ITEM } from '../NavTree';
14
21
  import { useNavTreeContext } from '../NavTreeContext';
15
- import { NavTreeItemColumns } from '../NavTreeItem';
22
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
16
23
 
17
24
  export type L1PanelProps = {
18
25
  open?: boolean;
19
26
  path: string[];
20
- item: Node<any>;
21
- currentItemId: string;
27
+ item: Node.Node;
28
+ isCurrent: boolean;
22
29
  onBack?: () => void;
23
30
  };
24
31
 
25
32
  /**
26
33
  * Space or settings panel.
27
34
  */
28
- export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
35
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
29
36
  const { t } = useTranslation(meta.id);
30
- const { isAlternateTree, ...navTreeContext } = useNavTreeContext();
31
37
  const title = toLocalizedString(item.properties.label, t);
32
-
33
- // TODO(wittjosiah): Support multiple alternate trees.
34
- const { useItems } = navTreeContext;
35
- const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
36
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
37
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
38
- const useAlternateItems = useCallback(
39
- (node?: Node, { disposition }: { disposition?: string } = {}) => {
40
- // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
41
- return useItems(node, { disposition, sort: node?.id === alternateTree.id });
42
- },
43
- [alternateTree, useItems],
44
- );
38
+ const isActivated = useIsActivatedWorkspace(item);
39
+ const shouldRenderContent = isCurrent || isActivated;
45
40
 
46
41
  return (
47
42
  <Tabs.Tabpanel
48
43
  key={item.id}
49
44
  value={item.id}
50
45
  classNames={[
51
- 'absolute inset-block-0 inline-end-0',
52
- 'is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr]',
53
- 'pbs-[env(safe-area-inset-top)]',
54
- item.id === currentItemId && 'grid',
46
+ 'absolute inset-y-0 end-0',
47
+ 'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
48
+ 'py-[env(safe-area-inset-top)]',
49
+ isCurrent && 'grid',
55
50
  ]}
56
51
  tabIndex={-1}
57
52
  aria-label={title}
53
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
58
54
  {...(!open && { inert: true })}
59
55
  >
60
- {item.id === currentItemId && (
61
- <DensityProvider density='fine'>
62
- <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
63
- <div role='none' className='overflow-y-auto'>
64
- {isAlternate ? (
65
- <Tree
66
- {...navTreeContext}
67
- id={alternateTree.id}
68
- root={alternateTree}
69
- path={alternatePath}
70
- levelOffset={5}
71
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
72
- renderColumns={NavTreeItemColumns}
73
- useItems={useAlternateItems}
74
- />
75
- ) : (
76
- <Tree
77
- {...navTreeContext}
78
- id={item.id}
79
- root={item}
80
- path={path}
81
- levelOffset={5}
82
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
83
- renderColumns={NavTreeItemColumns}
84
- draggable
85
- />
86
- )}
87
- </div>
88
- </DensityProvider>
89
- )}
56
+ {shouldRenderContent && <L1PanelContent open={open} path={path} item={item} onBack={onBack} />}
90
57
  </Tabs.Tabpanel>
91
58
  );
92
59
  };
93
60
 
61
+ /** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
62
+ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
63
+ const { graph } = useAppGraph();
64
+ const edges = useEdges(graph, item.id);
65
+
66
+ return useMemo(() => {
67
+ const childIds = edges[Graph.relationKey('child')] ?? [];
68
+ return childIds.some((childId) => {
69
+ const child = Graph.getNode(graph, childId);
70
+ if (Option.isNone(child)) {
71
+ return false;
72
+ }
73
+ return child.value.properties.disposition === undefined;
74
+ });
75
+ }, [edges, graph]);
76
+ };
77
+
78
+ /**
79
+ * Mounted panel content for active or previously-visited tabs.
80
+ */
81
+ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
82
+ const navTreeContext = useNavTreeContext();
83
+
84
+ const alternateTree = useAlternateTreeItem(item);
85
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
86
+ const isAlternate = useIsAlternateTree(alternatePath, item);
87
+
88
+ return (
89
+ <DensityProvider density='fine'>
90
+ <L1PanelHeader path={path} item={item} onBack={onBack} />
91
+ <ScrollArea.Root thin orientation='vertical'>
92
+ <ScrollArea.Viewport>
93
+ {isAlternate && alternateTree ? (
94
+ <Tree
95
+ model={navTreeContext.model}
96
+ id={alternateTree.id}
97
+ rootId={alternateTree.id}
98
+ path={alternatePath}
99
+ levelOffset={5}
100
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
101
+ renderColumns={NavTreeItemColumns}
102
+ blockInstruction={navTreeContext.blockInstruction}
103
+ canDrop={navTreeContext.canDrop}
104
+ canSelect={navTreeContext.canSelect}
105
+ onOpenChange={navTreeContext.onOpenChange}
106
+ onSelect={navTreeContext.onSelect}
107
+ onItemHover={navTreeContext.onItemHover}
108
+ />
109
+ ) : (
110
+ <Tree
111
+ model={navTreeContext.model}
112
+ id={item.id}
113
+ rootId={item.id}
114
+ path={path}
115
+ levelOffset={5}
116
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
117
+ draggable
118
+ renderColumns={NavTreeItemColumns}
119
+ blockInstruction={navTreeContext.blockInstruction}
120
+ canDrop={navTreeContext.canDrop}
121
+ canSelect={navTreeContext.canSelect}
122
+ onOpenChange={navTreeContext.onOpenChange}
123
+ onSelect={navTreeContext.onSelect}
124
+ onItemHover={navTreeContext.onItemHover}
125
+ />
126
+ )}
127
+ </ScrollArea.Viewport>
128
+ </ScrollArea.Root>
129
+ </DensityProvider>
130
+ );
131
+ };
132
+
94
133
  /**
95
134
  * Header row.
96
135
  */
97
- const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
136
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
98
137
  const { t } = useTranslation(meta.id);
99
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
138
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
100
139
  const title = toLocalizedString(item.properties.label, t);
140
+ const backCapableWorkspace = item.id.startsWith('!');
101
141
 
102
- // TODO(wittjosiah): Support multiple alternate trees.
103
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
104
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
105
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
106
- const backCapable = item.id.startsWith('!') || isAlternate;
107
-
108
- const handleOpen = useCallback(() => {
109
- setAlternateTree?.(alternatePath, true);
110
- }, [alternatePath, setAlternateTree]);
111
-
112
- const handleBack = useCallback(() => {
113
- if (isAlternate) {
114
- setAlternateTree?.(alternatePath, false);
115
- } else {
116
- onBack?.();
117
- }
118
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
142
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
143
+ useLoadDescendents(item);
119
144
 
120
- // TODO(burdon): Reuse same grid as tree?
121
145
  return (
122
- <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
123
- <div className='is-6' />
124
- <h2 className='flex-1 truncate min-is-0'>{title}</h2>
125
- {(backCapable && (
146
+ <div className='flex w-full items-center border-b border-subdued-separator dx-app-drag dx-density-coarse pe-1'>
147
+ {backCapableWorkspace ? (
126
148
  <IconButton
149
+ size={5}
150
+ density='coarse'
151
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
127
152
  variant='ghost'
128
- icon='ph--arrow-u-down-left--regular'
153
+ icon='ph--caret-left--regular'
129
154
  iconOnly
130
- size={5}
131
155
  label={t('button back')}
132
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
133
- onClick={handleBack}
156
+ data-testid='treeView.primaryTreeButton'
157
+ onClick={() => onBack?.()}
134
158
  />
135
- )) ||
136
- (alternateTree && !isAlternate && (
159
+ ) : (
160
+ <div className='w-6' />
161
+ )}
162
+ <h2 className='flex-1 truncate min-w-0'>{title}</h2>
163
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
164
+ <div role='none' className='contents dx-app-no-drag'>
165
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
166
+ <NavTreeItemAction
167
+ testId={primaryAction.properties?.testId}
168
+ label={toLocalizedString(primaryAction.properties?.label, t)}
169
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
170
+ parent={item}
171
+ monolithic={Node.isAction(primaryAction)}
172
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
173
+ menuType={primaryAction.properties?.menuType}
174
+ caller={NAV_TREE_ITEM}
175
+ />
176
+ )}
177
+ {menuActions.length === 1 && (
137
178
  <IconButton
138
- data-testid='treeView.alternateTreeButton'
179
+ size={5}
180
+ density='coarse'
181
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
139
182
  variant='ghost'
140
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
183
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
141
184
  iconOnly
142
- size={5}
143
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
144
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
145
- onClick={handleOpen}
185
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
186
+ data-testid={menuActions[0].properties?.testId}
187
+ onClick={() => onAction(menuActions[0] as Node.Action)}
146
188
  />
147
- ))}
148
- <NavTreeItemColumns open path={path} item={item} density='coarse' />
189
+ )}
190
+ {menuActions.length > 1 && (
191
+ <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
192
+ <Menu.Trigger asChild>
193
+ <IconButton
194
+ size={5}
195
+ density='coarse'
196
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
197
+ variant='ghost'
198
+ icon='ph--dots-three-vertical--regular'
199
+ iconOnly
200
+ label={t('tree item actions label')}
201
+ data-testid='navtree.treeItem.actionsLevel0'
202
+ />
203
+ </Menu.Trigger>
204
+ <Menu.Content group={item} items={menuActions as MenuItem[]} />
205
+ </Menu.Root>
206
+ )}
207
+ {ItemEnd && <ItemEnd node={item} open />}
208
+ </div>
149
209
  </div>
150
210
  );
151
211
  };
212
+
213
+ /**
214
+ * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
215
+ */
216
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
217
+ const { t } = useTranslation(meta.id);
218
+ const { setAlternateTree } = useNavTreeContext();
219
+ const { graph } = useAppGraph();
220
+ const runAction = useActionRunner();
221
+
222
+ const alternateTree = useAlternateTreeItem(item);
223
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
224
+ const isAlternate = useIsAlternateTree(alternatePath, item);
225
+
226
+ // Graph actions.
227
+ const { actions: actionsProp, groupedActions } = useActions(item);
228
+ const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
229
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
230
+ );
231
+
232
+ const graphMenuActions = (
233
+ primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp
234
+ )
235
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
236
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
237
+
238
+ // Synthetic settings/back action.
239
+ const settingsActionId = `${item.id}~settings`;
240
+ const settingsAction = useMemo((): Node.Action | undefined => {
241
+ if (!alternateTree) {
242
+ return undefined;
243
+ }
244
+ return {
245
+ id: settingsActionId,
246
+ type: Node.ActionType,
247
+ data: () => Effect.void,
248
+ properties: {
249
+ label: isAlternate ? ['button back', { ns: meta.id }] : (alternateTree.properties.label ?? alternateTree.id),
250
+ icon: isAlternate
251
+ ? 'ph--arrow-u-down-left--regular'
252
+ : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
253
+ disposition: 'list-item',
254
+ testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
255
+ },
256
+ } as Node.Action;
257
+ }, [alternateTree, isAlternate, settingsActionId, t]);
258
+
259
+ const menuActions = useMemo(
260
+ () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
261
+ [graphMenuActions, settingsAction],
262
+ );
263
+
264
+ const onAction = useCallback(
265
+ (action: Node.Action, params?: Node.InvokeProps) => {
266
+ if (action.id === settingsActionId) {
267
+ if (alternateTree && !isAlternate) {
268
+ Graph.expand(graph, alternateTree.id, 'child');
269
+ }
270
+ setAlternateTree?.(alternatePath, !isAlternate);
271
+ } else {
272
+ void runAction(action, params);
273
+ }
274
+ },
275
+ [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction, graph, alternateTree],
276
+ );
277
+
278
+ return { primaryAction, groupedActions, menuActions, onAction };
279
+ };
280
+
281
+ /** Finds the first child with disposition 'alternate-tree' using graph connections directly. */
282
+ const useAlternateTreeItem = (item: Node.Node): Node.Node | undefined => {
283
+ const { graph } = useAppGraph();
284
+ const connections = useConnections(graph, item.id, 'child');
285
+ return connections.find((node) => node.properties.disposition === 'alternate-tree');
286
+ };
287
+
288
+ export const L1Panel = memo(L1Panel$);
@@ -2,57 +2,42 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo } from 'react';
5
+ import React from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/app-graph';
8
8
 
9
- import { useLoadDescendents } from '../../hooks';
10
9
  import { l0ItemType } from '../../util';
11
- import { useNavTreeContext } from '../NavTreeContext';
12
10
 
13
11
  import { L1Panel, type L1PanelProps } from './L1Panel';
14
12
 
15
- export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
13
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
14
+ currentItemId: string;
16
15
  path: string[];
17
- topLevelItems: Node<any>[];
16
+ topLevelItems: Node.Node[];
18
17
  };
19
18
 
20
19
  /**
21
- * Each space is an L1 tab.
20
+ * Each workspace is an L1 tab.
22
21
  */
23
- export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
22
+ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
24
23
  return (
25
24
  <>
26
25
  {topLevelItems.map((item) => {
27
26
  const type = l0ItemType(item);
28
- switch (type) {
29
- case 'tab':
30
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
31
- case 'collection':
32
- return <L1PanelCollection key={item.id} item={item} {...props} />;
33
- default:
34
- return null;
27
+ if (type === 'tab') {
28
+ return (
29
+ <L1Panel
30
+ key={item.id}
31
+ item={item}
32
+ path={path}
33
+ open={open}
34
+ onBack={onBack}
35
+ isCurrent={item.id === currentItemId}
36
+ />
37
+ );
35
38
  }
39
+ return null;
36
40
  })}
37
41
  </>
38
42
  );
39
43
  };
40
-
41
- /**
42
- *
43
- */
44
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
45
- const { useItems } = useNavTreeContext();
46
- useLoadDescendents(item);
47
- const collectionItems = useItems(item);
48
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
49
- return (
50
- <>
51
- {collectionItems
52
- .filter((item) => l0ItemType(item) === 'tab')
53
- .map((item) => (
54
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
55
- ))}
56
- </>
57
- );
58
- };
@@ -9,9 +9,8 @@ import { withTheme } from '@dxos/react-ui/testing';
9
9
  import { UserAccountAvatar } from './UserAccountAvatar';
10
10
 
11
11
  const meta = {
12
- title: 'plugins/plugin-navtree/UserAccountAvatar',
13
-
14
- decorators: [withTheme],
12
+ title: 'plugins/plugin-navtree/components/UserAccountAvatar',
13
+ decorators: [withTheme()],
15
14
  component: UserAccountAvatar,
16
15
  } satisfies Meta<typeof UserAccountAvatar>;
17
16
 
@@ -22,7 +22,7 @@ export const UserAccountAvatar = ({ size, userId, hue, emoji, status }: UserAcco
22
22
 
23
23
  return (
24
24
  <>
25
- <L0ItemActiveTabIndicator classNames='inset-block-6' />
25
+ <L0ItemActiveTabIndicator classNames='inset-y-6' />
26
26
  <div
27
27
  className='grid place-items-center dx-focus-ring-group-indicator rounded-full'
28
28
  data-joyride='welcome/account'
@@ -2,9 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './CommandsDialog';
6
5
  export * from './NavTree';
7
- export * from './NavTreeContainer';
8
6
  export * from './NavTreeContext';
9
- export * from './NavTreeDocumentTitle';
7
+
10
8
  export type * from './types';
@@ -4,34 +4,29 @@
4
4
 
5
5
  import type { FC } from 'react';
6
6
 
7
- import type { Node } from '@dxos/app-graph';
7
+ import { type Node } from '@dxos/app-graph';
8
8
  import type { Density } from '@dxos/react-ui';
9
- import type { TreeProps } from '@dxos/react-ui-list';
10
- import type { MaybePromise } from '@dxos/util';
9
+ import type { TreeModel, TreeProps } from '@dxos/react-ui-list';
11
10
 
12
- import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
11
+ import type { NavTreeItemGraphNode } from '../types';
13
12
 
14
13
  import type { L1PanelProps } from './Sidebar';
15
14
 
16
- export type NavTreeContextValue = Pick<
17
- TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
18
- 'useItems' | 'getProps' | 'isCurrent' | 'isOpen' | 'canDrop' | 'onOpenChange' | 'onSelect'
15
+ export type NavTreeContextValue = {
16
+ model: TreeModel<NavTreeItemGraphNode>;
17
+ popoverAnchorId?: string;
18
+ renderItemEnd?: FC<{ node: Node.Node; open: boolean }>;
19
+ setAlternateTree?: (path: string[], open: boolean) => void;
20
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
21
+ } & Pick<
22
+ TreeProps<NavTreeItemGraphNode>,
23
+ 'blockInstruction' | 'canDrop' | 'canSelect' | 'onOpenChange' | 'onSelect' | 'onItemHover'
19
24
  > &
20
- Pick<L1PanelProps, 'onBack'> & {
21
- tab: string;
22
- topbar?: boolean;
23
- popoverAnchorId?: string;
24
- renderItemEnd?: FC<{ node: Node; open: boolean }>;
25
- useActions: (node: Node) => FlattenedActions;
26
- loadDescendents?: (node: Node) => MaybePromise<void>;
27
- isAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;
28
- setAlternateTree?: (path: string[], open: boolean) => void;
29
- onTabChange?: (node: NavTreeItemGraphNode) => void;
30
- };
25
+ Pick<L1PanelProps, 'onBack'>;
31
26
 
32
27
  export type NavTreeItemColumnsProps = {
33
28
  path: string[];
34
- item: Node;
29
+ item: Node.Node;
35
30
  open: boolean;
36
31
  density?: Density;
37
32
  };
@@ -0,0 +1,133 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef, useMemo, useState } from 'react';
6
+
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
10
+ import { Keyboard, keySymbols } from '@dxos/keyboard';
11
+ import { Graph, Node, useActionRunner } from '@dxos/plugin-graph';
12
+ import { useActions } from '@dxos/plugin-graph';
13
+ import { Button, Dialog, toLocalizedString, useTranslation } from '@dxos/react-ui';
14
+ import { SearchList, useSearchListResults } from '@dxos/react-ui-searchlist';
15
+ import { osTranslations } from '@dxos/ui-theme';
16
+ import { getHostPlatform } from '@dxos/util';
17
+
18
+ import { KEY_BINDING, meta } from '../../meta';
19
+
20
+ export type CommandsDialogContentProps = {
21
+ selected?: string;
22
+ };
23
+
24
+ // TODO(wittjosiah): This probably deserves its own plugin but for now it lives here w/ other navigation UI.
25
+ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogContentProps>(
26
+ ({ selected: initial }, forwardedRef) => {
27
+ const { t } = useTranslation(meta.id);
28
+ const { invokeSync } = useOperationInvoker();
29
+ const runAction = useActionRunner();
30
+ const { graph } = useAppGraph();
31
+ const [selected, setSelected] = useState<string | undefined>(initial);
32
+
33
+ // Traverse graph.
34
+ // TODO(burdon): Factor out commonality with shortcut dialog.
35
+ const allActions = useMemo(() => {
36
+ // TODO(burdon): Get from navtree (not keyboard).
37
+ const current = Keyboard.singleton.getCurrentContext();
38
+ const actionMap = new Set<string>();
39
+ const actions: Node.ActionLike[] = [];
40
+ Graph.traverse(graph, {
41
+ relation: 'child',
42
+ visitor: (node, path) => {
43
+ if (
44
+ (Node.isAction(node) || Node.isActionGroup(node)) &&
45
+ !actionMap.has(node.id) &&
46
+ current.startsWith(path.slice(0, -1).join('/'))
47
+ ) {
48
+ actionMap.add(node.id);
49
+ actions.push(node);
50
+ }
51
+ },
52
+ });
53
+
54
+ actions.sort((a, b) => {
55
+ return toLocalizedString(a.properties.label, t)
56
+ ?.toLowerCase()
57
+ .localeCompare(toLocalizedString(b.properties.label, t)?.toLowerCase());
58
+ });
59
+
60
+ return actions;
61
+ }, [graph]);
62
+
63
+ const group = allActions.find(({ id }) => id === selected);
64
+ const groupActions = useActions(graph, group?.id);
65
+ const actions = Node.isActionGroup(group) ? groupActions : allActions;
66
+
67
+ const { results, handleSearch } = useSearchListResults({
68
+ items: actions,
69
+ extract: (action) => toLocalizedString(action.properties.label, t),
70
+ });
71
+
72
+ return (
73
+ <Dialog.Content ref={forwardedRef}>
74
+ <Dialog.Title>{t('commands dialog title', { ns: meta.id })}</Dialog.Title>
75
+ <SearchList.Root onSearch={handleSearch}>
76
+ <SearchList.Content>
77
+ <SearchList.Input placeholder={t('command list input placeholder')} />
78
+ <SearchList.Viewport>
79
+ {results.map((action) => {
80
+ const shortcut =
81
+ typeof action.properties.keyBinding === 'string'
82
+ ? action.properties.keyBinding
83
+ : action.properties.keyBinding?.[getHostPlatform()];
84
+
85
+ return (
86
+ <SearchList.Item
87
+ value={action.id}
88
+ key={action.id}
89
+ label={toLocalizedString(action.properties.label, t)}
90
+ icon={action.properties.icon}
91
+ suffix={shortcut ? keySymbols(shortcut).join('') : undefined}
92
+ onSelect={() => {
93
+ if (action.properties.disabled) {
94
+ return;
95
+ }
96
+
97
+ if (Node.isActionGroup(action)) {
98
+ setSelected(action.id);
99
+ return;
100
+ }
101
+
102
+ invokeSync(LayoutOperation.UpdateDialog, { state: false });
103
+ setTimeout(() => {
104
+ const node = Graph.getConnections(
105
+ graph,
106
+ group?.id ?? action.id,
107
+ Node.childRelation('inbound'),
108
+ )[0];
109
+ if (node && Node.isAction(action)) {
110
+ void runAction(action, { parent: node, caller: KEY_BINDING });
111
+ }
112
+ });
113
+ }}
114
+ classNames='flex items-center gap-2'
115
+ disabled={action.properties.disabled}
116
+ {...(action.properties?.testId && {
117
+ 'data-testid': action.properties.testId,
118
+ })}
119
+ />
120
+ );
121
+ })}
122
+ </SearchList.Viewport>
123
+ </SearchList.Content>
124
+ </SearchList.Root>
125
+ <div role='none' className='p-form-padding'>
126
+ <Dialog.Close asChild>
127
+ <Button classNames='w-full'>{t('close label', { ns: osTranslations })}</Button>
128
+ </Dialog.Close>
129
+ </div>
130
+ </Dialog.Content>
131
+ );
132
+ },
133
+ );