@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.
Files changed (208) hide show
  1. package/dist/lib/browser/NavTreePlugin-53I3ONRE.mjs +74 -0
  2. package/dist/lib/browser/NavTreePlugin-53I3ONRE.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +95 -122
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +21 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/types/src/NavTreePlugin.d.ts +3 -1
  9. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  10. package/dist/types/src/NavTreePlugin.node.d.ts +4 -0
  11. package/dist/types/src/NavTreePlugin.node.d.ts.map +1 -0
  12. package/dist/types/src/NavTreePlugin.test.d.ts +2 -0
  13. package/dist/types/src/NavTreePlugin.test.d.ts.map +1 -0
  14. package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
  15. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  16. package/dist/types/src/capabilities/index.d.ts +7 -105
  17. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  18. package/dist/types/src/capabilities/keyboard.d.ts +3 -2
  19. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  20. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  21. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  22. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  23. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/state.d.ts +4 -18
  25. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  26. package/dist/types/src/components/NavTree/NavTree.d.ts +11 -4
  27. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  28. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +10 -13
  29. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  31. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  32. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  33. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  34. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +13 -9
  35. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  36. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +3 -2
  37. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  39. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  40. package/dist/types/src/components/Sidebar/L0Menu.d.ts +8 -5
  41. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  42. package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -7
  43. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  44. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +5 -4
  45. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  46. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  47. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  48. package/dist/types/src/components/index.d.ts +0 -3
  49. package/dist/types/src/components/index.d.ts.map +1 -1
  50. package/dist/types/src/components/types.d.ts +8 -17
  51. package/dist/types/src/components/types.d.ts.map +1 -1
  52. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
  53. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  54. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +28 -0
  55. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
  56. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
  57. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  58. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  59. package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
  60. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  61. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  62. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  63. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  64. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  65. package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
  66. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  67. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
  68. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  69. package/dist/types/src/containers/index.d.ts +7 -0
  70. package/dist/types/src/containers/index.d.ts.map +1 -0
  71. package/dist/types/src/experimental/Tree.d.ts +2 -2
  72. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  73. package/dist/types/src/experimental/Tree.stories.d.ts +15 -4
  74. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  75. package/dist/types/src/hooks/index.d.ts +5 -0
  76. package/dist/types/src/hooks/index.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useActions.d.ts +5 -0
  78. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  79. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  80. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  81. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  82. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  83. package/dist/types/src/hooks/useNavTreeState.d.ts +18 -0
  84. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  85. package/dist/types/src/index.d.ts +3 -3
  86. package/dist/types/src/index.d.ts.map +1 -1
  87. package/dist/types/src/meta.d.ts +2 -2
  88. package/dist/types/src/meta.d.ts.map +1 -1
  89. package/dist/types/src/operations/expose.d.ts +5 -0
  90. package/dist/types/src/operations/expose.d.ts.map +1 -0
  91. package/dist/types/src/operations/index.d.ts +3 -0
  92. package/dist/types/src/operations/index.d.ts.map +1 -0
  93. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  94. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  95. package/dist/types/src/testing/generator.d.ts +5 -6
  96. package/dist/types/src/testing/generator.d.ts.map +1 -1
  97. package/dist/types/src/testing/update-state.d.ts +2 -2
  98. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  99. package/dist/types/src/translations.d.ts +8 -11
  100. package/dist/types/src/translations.d.ts.map +1 -1
  101. package/dist/types/src/types/capabilities.d.ts +15 -0
  102. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  103. package/dist/types/src/types/events.d.ts +5 -0
  104. package/dist/types/src/types/events.d.ts.map +1 -0
  105. package/dist/types/src/types/index.d.ts +4 -0
  106. package/dist/types/src/types/index.d.ts.map +1 -0
  107. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  108. package/dist/types/src/types/schema.d.ts.map +1 -0
  109. package/dist/types/src/util.d.ts +12 -8
  110. package/dist/types/src/util.d.ts.map +1 -1
  111. package/dist/types/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +78 -58
  113. package/src/NavTreePlugin.node.ts +37 -0
  114. package/src/NavTreePlugin.test.ts +29 -0
  115. package/src/NavTreePlugin.tsx +59 -76
  116. package/src/capabilities/app-graph-builder.ts +34 -44
  117. package/src/capabilities/index.ts +10 -8
  118. package/src/capabilities/keyboard.ts +58 -51
  119. package/src/capabilities/operation-handler.ts +16 -0
  120. package/src/capabilities/react-surface.tsx +46 -38
  121. package/src/capabilities/state.ts +128 -84
  122. package/src/components/NavTree/NavTree.stories.tsx +105 -100
  123. package/src/components/NavTree/NavTree.tsx +65 -33
  124. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  125. package/src/components/NavTreeContext/index.ts +5 -0
  126. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +23 -15
  127. package/src/components/NavTreeItem/NavTreeItemAction.tsx +69 -46
  128. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +67 -35
  129. package/src/components/Sidebar/L0Menu.tsx +148 -183
  130. package/src/components/Sidebar/L1Panel.tsx +202 -103
  131. package/src/components/Sidebar/L1Tabs.tsx +18 -34
  132. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +2 -3
  133. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
  134. package/src/components/index.ts +1 -3
  135. package/src/components/types.ts +13 -19
  136. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +46 -0
  137. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +127 -0
  138. package/src/containers/CommandsDialogContent/index.ts +5 -0
  139. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +28 -0
  140. package/src/containers/CommandsTrigger/index.ts +5 -0
  141. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +271 -0
  142. package/src/containers/NavTreeContainer/index.ts +6 -0
  143. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +4 -3
  144. package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
  145. package/src/containers/index.ts +12 -0
  146. package/src/experimental/Tree.stories.tsx +81 -87
  147. package/src/experimental/Tree.tsx +3 -3
  148. package/src/hooks/index.ts +8 -0
  149. package/src/hooks/useActions.ts +34 -0
  150. package/src/hooks/useLoadDescendents.ts +23 -0
  151. package/src/hooks/useNavTreeModel.ts +118 -0
  152. package/src/hooks/useNavTreeState.ts +35 -0
  153. package/src/index.ts +8 -3
  154. package/src/meta.ts +9 -4
  155. package/src/operations/expose.ts +42 -0
  156. package/src/operations/index.ts +7 -0
  157. package/src/testing/app-graph-builder.ts +167 -238
  158. package/src/testing/generator.ts +41 -39
  159. package/src/testing/update-state.ts +6 -6
  160. package/src/translations.ts +8 -10
  161. package/src/types/capabilities.ts +21 -0
  162. package/src/types/events.ts +12 -0
  163. package/src/types/index.ts +7 -0
  164. package/src/{types.ts → types/schema.ts} +9 -8
  165. package/src/util.ts +28 -17
  166. package/dist/lib/browser/app-graph-builder-BCXDEDN7.mjs +0 -45
  167. package/dist/lib/browser/app-graph-builder-BCXDEDN7.mjs.map +0 -7
  168. package/dist/lib/browser/chunk-6AZKY5WZ.mjs +0 -1470
  169. package/dist/lib/browser/chunk-6AZKY5WZ.mjs.map +0 -7
  170. package/dist/lib/browser/chunk-LPAREX6J.mjs +0 -15
  171. package/dist/lib/browser/chunk-LPAREX6J.mjs.map +0 -7
  172. package/dist/lib/browser/chunk-ZENLR72O.mjs +0 -14
  173. package/dist/lib/browser/chunk-ZENLR72O.mjs.map +0 -7
  174. package/dist/lib/browser/intent-resolver-G6COWRWW.mjs +0 -47
  175. package/dist/lib/browser/intent-resolver-G6COWRWW.mjs.map +0 -7
  176. package/dist/lib/browser/keyboard-AHSAWKXA.mjs +0 -51
  177. package/dist/lib/browser/keyboard-AHSAWKXA.mjs.map +0 -7
  178. package/dist/lib/browser/react-surface-FOCB4Y5R.mjs +0 -51
  179. package/dist/lib/browser/react-surface-FOCB4Y5R.mjs.map +0 -7
  180. package/dist/lib/browser/state-2Q623V7U.mjs +0 -117
  181. package/dist/lib/browser/state-2Q623V7U.mjs.map +0 -7
  182. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  183. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  184. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  185. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  186. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
  187. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  188. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  189. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  190. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  191. package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
  192. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  193. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  194. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  195. package/dist/types/src/events.d.ts +0 -4
  196. package/dist/types/src/events.d.ts.map +0 -1
  197. package/dist/types/src/hooks.d.ts +0 -3
  198. package/dist/types/src/hooks.d.ts.map +0 -1
  199. package/dist/types/src/types.d.ts.map +0 -1
  200. package/src/capabilities/capabilities.ts +0 -20
  201. package/src/capabilities/intent-resolver.ts +0 -37
  202. package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
  203. package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -35
  204. package/src/components/CommandsDialog/index.ts +0 -6
  205. package/src/components/NavTreeContainer.tsx +0 -339
  206. package/src/events.ts +0 -11
  207. package/src/hooks.ts +0 -21
  208. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
@@ -2,151 +2,250 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Option from 'effect/Option';
6
+ import React, { memo, useCallback, useMemo } from 'react';
6
7
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { Node } from '@dxos/app-graph';
9
+ import { isPinnedWorkspace } from '@dxos/app-toolkit';
10
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
11
+ import { Graph, useActionRunner, 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
 
13
- import { meta } from '../../meta';
18
+ import { useActions, useLoadDescendents } from '#hooks';
19
+ import { meta } from '#meta';
20
+
21
+ import { NAV_TREE_ITEM } from '../NavTree';
14
22
  import { useNavTreeContext } from '../NavTreeContext';
15
- import { NavTreeItemColumns } from '../NavTreeItem';
23
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
16
24
 
17
25
  export type L1PanelProps = {
18
26
  open?: boolean;
19
27
  path: string[];
20
- item: Node<any>;
21
- currentItemId: string;
28
+ item: Node.Node;
29
+ isCurrent: boolean;
22
30
  onBack?: () => void;
23
31
  };
24
32
 
25
33
  /**
26
34
  * Space or settings panel.
27
35
  */
28
- export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
36
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
29
37
  const { t } = useTranslation(meta.id);
30
- const { isAlternateTree, ...navTreeContext } = useNavTreeContext();
31
- // NOTE(burdon): I'm very uneasy about this pattern of declaring hooks as context props.
32
- const { useItems } = navTreeContext;
33
38
  const title = toLocalizedString(item.properties.label, t);
34
-
35
- // TODO(wittjosiah): Support multiple alternate trees.
36
- const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
37
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
38
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
39
- const useAlternateItems = useCallback(
40
- (node?: Node, { disposition }: { disposition?: string } = {}) => {
41
- // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
42
- return useItems(node, { disposition, sort: node?.id === alternateTree.id });
43
- },
44
- [alternateTree, useItems],
45
- );
39
+ const isActivated = useIsActivatedWorkspace(item);
40
+ const shouldRenderContent = isCurrent || isActivated;
46
41
 
47
42
  return (
48
- <Tabs.Tabpanel
43
+ <Tabs.Panel
49
44
  key={item.id}
50
45
  value={item.id}
51
46
  classNames={[
52
- 'absolute inset-block-0 inline-end-0',
53
- 'is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr]',
54
- 'pbs-[env(safe-area-inset-top)]',
55
- item.id === currentItemId && 'grid',
47
+ 'absolute inset-y-0 end-0',
48
+ 'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
49
+ 'py-[env(safe-area-inset-top)]',
50
+ isCurrent && 'grid',
56
51
  ]}
57
52
  tabIndex={-1}
58
53
  aria-label={title}
54
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
59
55
  {...(!open && { inert: true })}
60
56
  >
61
- {item.id === currentItemId && (
62
- <>
63
- <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
64
- <div role='none' className='overflow-y-auto'>
65
- {isAlternate ? (
66
- <Tree
67
- {...navTreeContext}
68
- useItems={useAlternateItems}
69
- id={alternateTree.id}
70
- root={alternateTree}
71
- path={alternatePath}
72
- levelOffset={5}
73
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
74
- renderColumns={NavTreeItemColumns}
75
- />
76
- ) : (
77
- <Tree
78
- {...navTreeContext}
79
- id={item.id}
80
- root={item}
81
- path={path}
82
- levelOffset={5}
83
- draggable
84
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
85
- renderColumns={NavTreeItemColumns}
86
- />
87
- )}
88
- </div>
89
- </>
90
- )}
91
- </Tabs.Tabpanel>
57
+ {shouldRenderContent && <L1PanelContent open={open} path={path} item={item} onBack={onBack} />}
58
+ </Tabs.Panel>
59
+ );
60
+ };
61
+
62
+ /** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
63
+ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
64
+ const { graph } = useAppGraph();
65
+ const edges = useEdges(graph, item.id);
66
+
67
+ return useMemo(() => {
68
+ const childIds = edges[Graph.relationKey('child')] ?? [];
69
+ return childIds.some((childId) => {
70
+ const child = Graph.getNode(graph, childId);
71
+ if (Option.isNone(child)) {
72
+ return false;
73
+ }
74
+ return child.value.properties.disposition === undefined;
75
+ });
76
+ }, [edges, graph]);
77
+ };
78
+
79
+ /**
80
+ * Mounted panel content for active or previously-visited tabs.
81
+ */
82
+ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
83
+ const navTreeContext = useNavTreeContext();
84
+
85
+ return (
86
+ <DensityProvider density='fine'>
87
+ <L1PanelHeader path={path} item={item} onBack={onBack} />
88
+ <ScrollArea.Root thin orientation='vertical'>
89
+ <ScrollArea.Viewport>
90
+ <Tree
91
+ model={navTreeContext.model}
92
+ id={item.id}
93
+ rootId={item.id}
94
+ path={path}
95
+ levelOffset={5}
96
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
97
+ draggable
98
+ renderColumns={NavTreeItemColumns}
99
+ blockInstruction={navTreeContext.blockInstruction}
100
+ canDrop={navTreeContext.canDrop}
101
+ canSelect={navTreeContext.canSelect}
102
+ onOpenChange={navTreeContext.onOpenChange}
103
+ onSelect={navTreeContext.onSelect}
104
+ onItemHover={navTreeContext.onItemHover}
105
+ />
106
+ </ScrollArea.Viewport>
107
+ </ScrollArea.Root>
108
+ </DensityProvider>
92
109
  );
93
110
  };
94
111
 
95
112
  /**
96
113
  * Header row.
97
114
  */
98
- const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
115
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
99
116
  const { t } = useTranslation(meta.id);
100
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
117
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
101
118
  const title = toLocalizedString(item.properties.label, t);
119
+ const backCapableWorkspace = isPinnedWorkspace(item.id);
120
+
121
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
122
+ useLoadDescendents(item);
102
123
 
103
- // TODO(wittjosiah): Support multiple alternate trees.
104
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
105
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
106
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
107
- const backCapable = item.id.startsWith('!') || isAlternate;
108
-
109
- const handleOpen = useCallback(() => {
110
- setAlternateTree?.(alternatePath, true);
111
- }, [alternatePath, setAlternateTree]);
112
-
113
- const handleBack = useCallback(() => {
114
- if (isAlternate) {
115
- setAlternateTree?.(alternatePath, false);
116
- } else {
117
- onBack?.();
118
- }
119
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
120
-
121
- // TODO(burdon): Reuse same grid as tree?
122
124
  return (
123
- <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
124
- <div className='is-6' />
125
- <h2 className='flex-1 truncate min-is-0'>{title}</h2>
126
- {(backCapable && (
125
+ <div
126
+ data-tauri-drag-region
127
+ className='flex w-full items-center border-b border-subdued-separator dx-app-drag dx-density-coarse pe-1'
128
+ >
129
+ {backCapableWorkspace ? (
127
130
  <IconButton
131
+ density='coarse'
132
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
128
133
  variant='ghost'
129
- icon='ph--arrow-u-down-left--regular'
134
+ icon='ph--caret-left--regular'
130
135
  iconOnly
131
- size={5}
132
- label={t('button back')}
133
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
134
- onClick={handleBack}
136
+ label={t('button-back.button')}
137
+ data-testid='treeView.primaryTreeButton'
138
+ onClick={() => onBack?.()}
135
139
  />
136
- )) ||
137
- (alternateTree && !isAlternate && (
138
- <IconButton
139
- data-testid='treeView.alternateTreeButton'
140
- variant='ghost'
141
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
142
- iconOnly
143
- size={5}
144
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
145
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
146
- onClick={handleOpen}
140
+ ) : (
141
+ <div data-tauri-drag-region className='w-6' />
142
+ )}
143
+ <h2 data-tauri-drag-region className='flex-1 truncate min-w-0'>
144
+ {title}
145
+ </h2>
146
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
147
+ <div role='none' className='contents dx-app-no-drag'>
148
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
149
+ <NavTreeItemAction
150
+ testId={primaryAction.properties?.testId}
151
+ label={toLocalizedString(primaryAction.properties?.label, t)}
152
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
153
+ parent={item}
154
+ path={path}
155
+ monolithic={Node.isAction(primaryAction)}
156
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
157
+ menuType={primaryAction.properties?.menuType}
158
+ caller={NAV_TREE_ITEM}
147
159
  />
148
- ))}
149
- <NavTreeItemColumns open path={path} item={item} density='coarse' />
160
+ )}
161
+ <MenuActions item={item} menuActions={menuActions} onAction={onAction} />
162
+ {ItemEnd && <ItemEnd node={item} open />}
163
+ </div>
150
164
  </div>
151
165
  );
152
166
  };
167
+
168
+ type L1MenuActions = {
169
+ primaryAction: Node.ActionLike;
170
+ groupedActions: Record<string, Node.Action[]>;
171
+ menuActions: Node.Action[];
172
+ onAction: (action: Node.Action, params?: Node.InvokeProps) => void;
173
+ };
174
+
175
+ /**
176
+ * Header menu actions for an L1 workspace tab. Renders nothing for an empty
177
+ * `menuActions`, a single inline icon button for one action, and a
178
+ * `…`-menu trigger for multiple.
179
+ */
180
+ const MenuActions = ({
181
+ item,
182
+ menuActions,
183
+ onAction,
184
+ }: {
185
+ item: Node.Node;
186
+ } & Pick<L1MenuActions, 'menuActions' | 'onAction'>) => {
187
+ const { t } = useTranslation(meta.id);
188
+
189
+ if (menuActions.length === 0) {
190
+ return null;
191
+ }
192
+
193
+ if (menuActions.length === 1) {
194
+ return (
195
+ <IconButton
196
+ density='coarse'
197
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
198
+ variant='ghost'
199
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
200
+ iconOnly
201
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
202
+ data-testid={menuActions[0].properties?.testId}
203
+ onClick={() => onAction(menuActions[0] as Node.Action)}
204
+ />
205
+ );
206
+ }
207
+
208
+ return (
209
+ <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
210
+ <Menu.Trigger asChild>
211
+ <IconButton
212
+ density='coarse'
213
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
214
+ variant='ghost'
215
+ icon='ph--dots-three-vertical--regular'
216
+ iconOnly
217
+ label={t('tree-item-actions.label')}
218
+ data-testid='navtree.treeItem.actionsLevel0'
219
+ />
220
+ </Menu.Trigger>
221
+ <Menu.Content group={item} items={menuActions as MenuItem[]} />
222
+ </Menu.Root>
223
+ );
224
+ };
225
+
226
+ /**
227
+ * Builds the menu actions for the L1 panel header.
228
+ */
229
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>): L1MenuActions => {
230
+ const runAction = useActionRunner();
231
+
232
+ const { actions: actionsProp, groupedActions } = useActions(item);
233
+ const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
234
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
235
+ );
236
+
237
+ const menuActions = (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp)
238
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
239
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
240
+
241
+ const onAction = useCallback(
242
+ (action: Node.Action, params?: Node.InvokeProps) => {
243
+ void runAction(action, { ...params, path });
244
+ },
245
+ [runAction, path],
246
+ );
247
+
248
+ return { primaryAction, groupedActions, menuActions, onAction };
249
+ };
250
+
251
+ export const L1Panel = memo(L1Panel$);
@@ -2,57 +2,41 @@
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
-
13
10
  import { L1Panel, type L1PanelProps } from './L1Panel';
14
11
 
15
- export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
12
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
13
+ currentItemId: string;
16
14
  path: string[];
17
- topLevelItems: Node<any>[];
15
+ topLevelItems: Node.Node[];
18
16
  };
19
17
 
20
18
  /**
21
- * Each space is an L1 tab.
19
+ * Each workspace is an L1 tab.
22
20
  */
23
- export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
21
+ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
24
22
  return (
25
23
  <>
26
24
  {topLevelItems.map((item) => {
27
25
  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;
26
+ if (type === 'tab') {
27
+ return (
28
+ <L1Panel
29
+ key={item.id}
30
+ item={item}
31
+ path={path}
32
+ open={open}
33
+ onBack={onBack}
34
+ isCurrent={item.id === currentItemId}
35
+ />
36
+ );
35
37
  }
38
+ return null;
36
39
  })}
37
40
  </>
38
41
  );
39
42
  };
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,28 @@
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
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
20
+ } & Pick<
21
+ TreeProps<NavTreeItemGraphNode>,
22
+ 'blockInstruction' | 'canDrop' | 'canSelect' | 'onOpenChange' | 'onSelect' | 'onItemHover'
19
23
  > &
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
- };
24
+ Pick<L1PanelProps, 'onBack'>;
31
25
 
32
26
  export type NavTreeItemColumnsProps = {
33
27
  path: string[];
34
- item: Node;
28
+ item: Node.Node;
35
29
  open: boolean;
36
30
  density?: Density;
37
31
  };
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { withPluginManager } from '@dxos/app-framework/testing';
9
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
10
+ import { Dialog } from '@dxos/react-ui';
11
+ import { withLayout } from '@dxos/react-ui/testing';
12
+
13
+ import { translations } from '#translations';
14
+
15
+ import { NavTreePlugin } from '../../NavTreePlugin';
16
+ import { CommandsDialogContent } from './CommandsDialogContent';
17
+
18
+ const DefaultStory = () => (
19
+ <Dialog.Root defaultOpen>
20
+ <Dialog.Overlay>
21
+ <CommandsDialogContent />
22
+ </Dialog.Overlay>
23
+ </Dialog.Root>
24
+ );
25
+
26
+ const meta = {
27
+ title: 'plugins/plugin-navtree/containers/CommandsDialogContent',
28
+ component: CommandsDialogContent,
29
+ render: DefaultStory,
30
+ decorators: [
31
+ withLayout({ layout: 'fullscreen' }),
32
+ withPluginManager({
33
+ plugins: [...corePlugins(), StorybookPlugin({}), NavTreePlugin()],
34
+ }),
35
+ ],
36
+ parameters: {
37
+ layout: 'fullscreen',
38
+ translations,
39
+ },
40
+ } satisfies Meta<typeof CommandsDialogContent>;
41
+
42
+ export default meta;
43
+
44
+ type Story = StoryObj<typeof meta>;
45
+
46
+ export const Default: Story = {};