@dxos/plugin-navtree 0.8.4-main.1068cf700f → 0.8.4-main.1c7ec43d41

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 (207) hide show
  1. package/dist/lib/browser/NavTreePlugin-3KLPENH6.mjs +54 -0
  2. package/dist/lib/browser/NavTreePlugin-3KLPENH6.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +98 -114
  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 +1 -0
  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 +6 -0
  15. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  16. package/dist/types/src/capabilities/index.d.ts +7 -5
  17. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  18. package/dist/types/src/capabilities/keyboard.d.ts +5 -0
  19. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -0
  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 +5 -0
  23. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/state.d.ts +6 -0
  25. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  26. package/dist/types/src/components/NavTree/NavTree.d.ts +10 -32
  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 -15
  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 +7 -3
  35. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  36. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +1 -1
  37. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  39. package/dist/types/src/components/Sidebar/L0Menu.d.ts +4 -1
  40. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  41. package/dist/types/src/components/Sidebar/L1Panel.d.ts +2 -5
  42. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  43. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +3 -2
  44. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  45. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  46. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/index.d.ts +0 -3
  48. package/dist/types/src/components/index.d.ts.map +1 -1
  49. package/dist/types/src/components/types.d.ts +5 -14
  50. package/dist/types/src/components/types.d.ts.map +1 -1
  51. package/dist/types/src/{components/CommandsDialog/CommandsDialog.d.ts → containers/CommandsDialogContent/CommandsDialogContent.d.ts} +1 -1
  52. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  53. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +28 -0
  54. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
  55. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
  56. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  57. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  58. package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
  59. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  60. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  61. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  62. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  63. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  64. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  65. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
  66. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  67. package/dist/types/src/containers/index.d.ts +7 -0
  68. package/dist/types/src/containers/index.d.ts.map +1 -0
  69. package/dist/types/src/experimental/Tree.d.ts +2 -2
  70. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  71. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  72. package/dist/types/src/hooks/index.d.ts +5 -0
  73. package/dist/types/src/hooks/index.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useActions.d.ts +5 -0
  75. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  76. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  77. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  78. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  79. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  80. package/dist/types/src/hooks/useNavTreeState.d.ts +18 -0
  81. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  82. package/dist/types/src/index.d.ts +3 -2
  83. package/dist/types/src/index.d.ts.map +1 -1
  84. package/dist/types/src/meta.d.ts.map +1 -1
  85. package/dist/types/src/operations/expose.d.ts +5 -0
  86. package/dist/types/src/operations/expose.d.ts.map +1 -0
  87. package/dist/types/src/operations/index.d.ts +3 -0
  88. package/dist/types/src/operations/index.d.ts.map +1 -0
  89. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  90. package/dist/types/src/testing/generator.d.ts +3 -3
  91. package/dist/types/src/testing/generator.d.ts.map +1 -1
  92. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  93. package/dist/types/src/translations.d.ts +8 -11
  94. package/dist/types/src/translations.d.ts.map +1 -1
  95. package/dist/types/src/types/capabilities.d.ts +1 -2
  96. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  97. package/dist/types/src/types/events.d.ts.map +1 -1
  98. package/dist/types/src/util.d.ts +7 -3
  99. package/dist/types/src/util.d.ts.map +1 -1
  100. package/dist/types/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +62 -43
  102. package/src/NavTreePlugin.node.ts +17 -0
  103. package/src/NavTreePlugin.test.ts +27 -0
  104. package/src/NavTreePlugin.tsx +8 -25
  105. package/src/capabilities/{app-graph-builder/app-graph-builder.ts → app-graph-builder.ts} +7 -7
  106. package/src/capabilities/index.ts +11 -5
  107. package/src/capabilities/{keyboard/keyboard.ts → keyboard.ts} +2 -2
  108. package/src/capabilities/operation-handler.ts +16 -0
  109. package/src/capabilities/{react-surface/react-surface.tsx → react-surface.tsx} +9 -11
  110. package/src/capabilities/{state/state.ts → state.ts} +41 -14
  111. package/src/components/NavTree/NavTree.stories.tsx +26 -24
  112. package/src/components/NavTree/NavTree.tsx +64 -22
  113. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  114. package/src/components/NavTreeContext/index.ts +5 -0
  115. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +7 -9
  116. package/src/components/NavTreeItem/NavTreeItemAction.tsx +56 -40
  117. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +43 -17
  118. package/src/components/Sidebar/L0Menu.tsx +104 -110
  119. package/src/components/Sidebar/L1Panel.tsx +163 -142
  120. package/src/components/Sidebar/L1Tabs.tsx +13 -4
  121. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +1 -2
  122. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
  123. package/src/components/index.ts +0 -3
  124. package/src/components/types.ts +11 -17
  125. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +46 -0
  126. package/src/{components/CommandsDialog/CommandsDialog.tsx → containers/CommandsDialogContent/CommandsDialogContent.tsx} +52 -53
  127. package/src/containers/CommandsDialogContent/index.ts +5 -0
  128. package/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.tsx +8 -6
  129. package/src/containers/CommandsTrigger/index.ts +5 -0
  130. package/src/{components → containers/NavTreeContainer}/NavTreeContainer.tsx +78 -145
  131. package/src/containers/NavTreeContainer/index.ts +6 -0
  132. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +2 -2
  133. package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
  134. package/src/containers/index.ts +12 -0
  135. package/src/experimental/Tree.stories.tsx +64 -61
  136. package/src/experimental/Tree.tsx +3 -3
  137. package/src/hooks/index.ts +8 -0
  138. package/src/hooks/useActions.ts +34 -0
  139. package/src/hooks/useLoadDescendents.ts +23 -0
  140. package/src/hooks/useNavTreeModel.ts +118 -0
  141. package/src/hooks/useNavTreeState.ts +35 -0
  142. package/src/index.ts +8 -2
  143. package/src/meta.ts +3 -2
  144. package/src/operations/expose.ts +42 -0
  145. package/src/operations/index.ts +7 -0
  146. package/src/testing/app-graph-builder.ts +40 -36
  147. package/src/testing/generator.ts +25 -25
  148. package/src/translations.ts +8 -10
  149. package/src/types/capabilities.ts +4 -4
  150. package/src/types/events.ts +2 -2
  151. package/src/util.ts +19 -5
  152. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs +0 -44
  153. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs.map +0 -7
  154. package/dist/lib/browser/chunk-63YLDLLE.mjs +0 -1373
  155. package/dist/lib/browser/chunk-63YLDLLE.mjs.map +0 -7
  156. package/dist/lib/browser/chunk-X2HDG62A.mjs +0 -23
  157. package/dist/lib/browser/chunk-X2HDG62A.mjs.map +0 -7
  158. package/dist/lib/browser/chunk-YKTYPINZ.mjs +0 -19
  159. package/dist/lib/browser/chunk-YKTYPINZ.mjs.map +0 -7
  160. package/dist/lib/browser/keyboard-46XQHY2O.mjs +0 -53
  161. package/dist/lib/browser/keyboard-46XQHY2O.mjs.map +0 -7
  162. package/dist/lib/browser/operation-resolver-NNNDII45.mjs +0 -53
  163. package/dist/lib/browser/operation-resolver-NNNDII45.mjs.map +0 -7
  164. package/dist/lib/browser/react-surface-APSEG23H.mjs +0 -59
  165. package/dist/lib/browser/react-surface-APSEG23H.mjs.map +0 -7
  166. package/dist/lib/browser/state-M5YUCIQU.mjs +0 -121
  167. package/dist/lib/browser/state-M5YUCIQU.mjs.map +0 -7
  168. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +0 -6
  169. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +0 -1
  170. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +0 -3
  171. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +0 -1
  172. package/dist/types/src/capabilities/keyboard/index.d.ts +0 -3
  173. package/dist/types/src/capabilities/keyboard/index.d.ts.map +0 -1
  174. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +0 -5
  175. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +0 -1
  176. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  177. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  178. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  179. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  180. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  181. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  182. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +0 -5
  183. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  184. package/dist/types/src/capabilities/state/index.d.ts +0 -3
  185. package/dist/types/src/capabilities/state/index.d.ts.map +0 -1
  186. package/dist/types/src/capabilities/state/state.d.ts +0 -6
  187. package/dist/types/src/capabilities/state/state.d.ts.map +0 -1
  188. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  189. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  190. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  191. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  192. package/dist/types/src/components/NavTreeContainer.d.ts +0 -16
  193. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  194. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  195. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  196. package/dist/types/src/hooks.d.ts +0 -34
  197. package/dist/types/src/hooks.d.ts.map +0 -1
  198. package/src/capabilities/app-graph-builder/index.ts +0 -7
  199. package/src/capabilities/keyboard/index.ts +0 -7
  200. package/src/capabilities/operation-resolver/index.ts +0 -7
  201. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -42
  202. package/src/capabilities/react-surface/index.ts +0 -7
  203. package/src/capabilities/state/index.ts +0 -7
  204. package/src/components/CommandsDialog/index.ts +0 -6
  205. package/src/hooks.ts +0 -73
  206. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
  207. /package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +0 -0
@@ -2,19 +2,22 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Effect from 'effect/Effect';
6
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Option from 'effect/Option';
6
+ import React, { memo, useCallback, useMemo } from 'react';
7
7
 
8
8
  import { Node } from '@dxos/app-graph';
9
- import { useActionRunner } from '@dxos/plugin-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';
10
12
  import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
13
  import { Tree } from '@dxos/react-ui-list';
12
- import { DropdownMenu, type MenuItem, MenuProvider } from '@dxos/react-ui-menu';
14
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
13
15
  import { Tabs } from '@dxos/react-ui-tabs';
14
16
  import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
15
17
 
16
- import { useIsAlternateTree, useLoadDescendents } from '../../hooks';
17
- import { meta } from '../../meta';
18
+ import { useActions, useLoadDescendents } from '#hooks';
19
+ import { meta } from '#meta';
20
+
18
21
  import { NAV_TREE_ITEM } from '../NavTree';
19
22
  import { useNavTreeContext } from '../NavTreeContext';
20
23
  import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
@@ -23,208 +26,226 @@ export type L1PanelProps = {
23
26
  open?: boolean;
24
27
  path: string[];
25
28
  item: Node.Node;
26
- currentItemId: string;
29
+ isCurrent: boolean;
27
30
  onBack?: () => void;
28
31
  };
29
32
 
30
33
  /**
31
34
  * Space or settings panel.
32
35
  */
33
- export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
36
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
34
37
  const { t } = useTranslation(meta.id);
35
- const navTreeContext = useNavTreeContext();
36
38
  const title = toLocalizedString(item.properties.label, t);
37
-
38
- // TODO(wittjosiah): Support multiple alternate trees.
39
- const { useItems } = navTreeContext;
40
- const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
41
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
42
- const isAlternate = useIsAlternateTree(alternatePath, item);
43
- const useAlternateItems = useCallback(
44
- (node?: Node.Node, { disposition }: { disposition?: string } = {}) => {
45
- // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
46
- return useItems(node, { disposition, sort: node?.id === alternateTree.id });
47
- },
48
- [alternateTree, useItems],
49
- );
39
+ const isActivated = useIsActivatedWorkspace(item);
40
+ const shouldRenderContent = isCurrent || isActivated;
50
41
 
51
42
  return (
52
- <Tabs.Tabpanel
43
+ <Tabs.Panel
53
44
  key={item.id}
54
45
  value={item.id}
55
46
  classNames={[
56
- 'absolute inset-block-0 inline-end-0',
57
- 'is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr]',
58
- 'pbs-[env(safe-area-inset-top)]',
59
- 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',
60
51
  ]}
61
52
  tabIndex={-1}
62
53
  aria-label={title}
54
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
63
55
  {...(!open && { inert: true })}
64
56
  >
65
- {item.id === currentItemId && (
66
- <DensityProvider density='fine'>
67
- <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
68
- <ScrollArea.Root thin orientation='vertical'>
69
- <ScrollArea.Viewport>
70
- {isAlternate ? (
71
- <Tree
72
- {...navTreeContext}
73
- id={alternateTree.id}
74
- root={alternateTree}
75
- path={alternatePath}
76
- levelOffset={5}
77
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
78
- renderColumns={NavTreeItemColumns}
79
- useItems={useAlternateItems}
80
- />
81
- ) : (
82
- <Tree
83
- {...navTreeContext}
84
- id={item.id}
85
- root={item}
86
- path={path}
87
- levelOffset={5}
88
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
89
- renderColumns={NavTreeItemColumns}
90
- draggable
91
- />
92
- )}
93
- </ScrollArea.Viewport>
94
- </ScrollArea.Root>
95
- </DensityProvider>
96
- )}
97
- </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>
98
109
  );
99
110
  };
100
111
 
101
112
  /**
102
113
  * Header row.
103
114
  */
104
- const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
115
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
105
116
  const { t } = useTranslation(meta.id);
106
117
  const { renderItemEnd: ItemEnd } = useNavTreeContext();
107
118
  const title = toLocalizedString(item.properties.label, t);
119
+ const backCapableWorkspace = isPinnedWorkspace(item.id);
108
120
 
109
121
  const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
110
122
  useLoadDescendents(item);
111
123
 
112
124
  return (
113
- <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
114
- <div className='is-6' />
115
- <h2 className='flex-1 truncate min-is-0'>{title}</h2>
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 ? (
130
+ <IconButton
131
+ density='coarse'
132
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
133
+ variant='ghost'
134
+ icon='ph--caret-left--regular'
135
+ iconOnly
136
+ label={t('button-back.button')}
137
+ data-testid='treeView.primaryTreeButton'
138
+ onClick={() => onBack?.()}
139
+ />
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>
116
146
  {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
117
- <div role='none' className='contents app-no-drag'>
147
+ <div role='none' className='contents dx-app-no-drag'>
118
148
  {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
119
149
  <NavTreeItemAction
120
150
  testId={primaryAction.properties?.testId}
121
151
  label={toLocalizedString(primaryAction.properties?.label, t)}
122
152
  icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
123
153
  parent={item}
154
+ path={path}
124
155
  monolithic={Node.isAction(primaryAction)}
125
156
  menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
126
157
  menuType={primaryAction.properties?.menuType}
127
158
  caller={NAV_TREE_ITEM}
128
159
  />
129
160
  )}
130
- {menuActions.length === 1 && (
131
- <IconButton
132
- size={5}
133
- density='coarse'
134
- classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
135
- variant='ghost'
136
- icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
137
- iconOnly
138
- label={toLocalizedString(menuActions[0].properties?.label, t)}
139
- data-testid={menuActions[0].properties?.testId}
140
- onClick={() => onAction(menuActions[0] as Node.Action)}
141
- />
142
- )}
143
- {menuActions.length > 1 && (
144
- <MenuProvider onAction={onAction}>
145
- <DropdownMenu.Root group={item} items={menuActions as MenuItem[]} caller={NAV_TREE_ITEM}>
146
- <DropdownMenu.Trigger asChild>
147
- <IconButton
148
- size={5}
149
- density='coarse'
150
- classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
151
- variant='ghost'
152
- icon='ph--dots-three-vertical--regular'
153
- iconOnly
154
- label={t('tree item actions label')}
155
- data-testid='navtree.treeItem.actionsLevel0'
156
- />
157
- </DropdownMenu.Trigger>
158
- </DropdownMenu.Root>
159
- </MenuProvider>
160
- )}
161
+ <MenuActions item={item} menuActions={menuActions} onAction={onAction} />
161
162
  {ItemEnd && <ItemEnd node={item} open />}
162
163
  </div>
163
164
  </div>
164
165
  );
165
166
  };
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
+
167
175
  /**
168
- * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
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.
169
179
  */
170
- const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
180
+ const MenuActions = ({
181
+ item,
182
+ menuActions,
183
+ onAction,
184
+ }: {
185
+ item: Node.Node;
186
+ } & Pick<L1MenuActions, 'menuActions' | 'onAction'>) => {
171
187
  const { t } = useTranslation(meta.id);
172
- const { setAlternateTree, useActions, ...navTreeContext } = useNavTreeContext();
173
- const runAction = useActionRunner();
174
188
 
175
- // TODO(wittjosiah): Support multiple alternate trees.
176
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
177
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
178
- const isAlternate = useIsAlternateTree(alternatePath, item);
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
+ }
179
207
 
180
- // Graph actions.
181
- const { actions: _actions, groupedActions } = useActions(item);
182
- const [primaryAction, ...secondaryActions] = _actions.toSorted((a, _b) =>
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) =>
183
234
  a.properties?.disposition === 'list-item-primary' ? -1 : 1,
184
235
  );
185
236
 
186
- const graphMenuActions = (
187
- primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : _actions
188
- )
237
+ const menuActions = (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp)
189
238
  .flatMap((action) => (Node.isAction(action) ? [action] : []))
190
239
  .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
191
240
 
192
- // Synthetic settings/back action.
193
- const settingsActionId = `${item.id}~settings`;
194
- const settingsAction = useMemo((): Node.Action | undefined => {
195
- if (!alternateTree) {
196
- return undefined;
197
- }
198
- return {
199
- id: settingsActionId,
200
- type: Node.ActionType,
201
- data: () => Effect.void,
202
- properties: {
203
- label: isAlternate ? ['button back', { ns: meta.id }] : (alternateTree.properties.label ?? alternateTree.id),
204
- icon: isAlternate
205
- ? 'ph--arrow-u-down-left--regular'
206
- : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
207
- disposition: 'list-item',
208
- testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
209
- },
210
- } as Node.Action;
211
- }, [alternateTree, isAlternate, settingsActionId, t]);
212
-
213
- const menuActions = useMemo(
214
- () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
215
- [graphMenuActions, settingsAction],
216
- );
217
-
218
241
  const onAction = useCallback(
219
242
  (action: Node.Action, params?: Node.InvokeProps) => {
220
- if (action.id === settingsActionId) {
221
- setAlternateTree?.(alternatePath, !isAlternate);
222
- } else {
223
- void runAction(action, params);
224
- }
243
+ void runAction(action, { ...params, path });
225
244
  },
226
- [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction],
245
+ [runAction, path],
227
246
  );
228
247
 
229
248
  return { primaryAction, groupedActions, menuActions, onAction };
230
249
  };
250
+
251
+ export const L1Panel = memo(L1Panel$);
@@ -7,10 +7,10 @@ import React from 'react';
7
7
  import { type Node } from '@dxos/app-graph';
8
8
 
9
9
  import { l0ItemType } from '../../util';
10
-
11
10
  import { L1Panel, type L1PanelProps } from './L1Panel';
12
11
 
13
- export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
12
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
13
+ currentItemId: string;
14
14
  path: string[];
15
15
  topLevelItems: Node.Node[];
16
16
  };
@@ -18,13 +18,22 @@ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'
18
18
  /**
19
19
  * Each workspace is an L1 tab.
20
20
  */
21
- export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
21
+ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
22
22
  return (
23
23
  <>
24
24
  {topLevelItems.map((item) => {
25
25
  const type = l0ItemType(item);
26
26
  if (type === 'tab') {
27
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
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
+ );
28
37
  }
29
38
  return null;
30
39
  })}
@@ -9,8 +9,7 @@ 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
-
12
+ title: 'plugins/plugin-navtree/components/UserAccountAvatar',
14
13
  decorators: [withTheme()],
15
14
  component: UserAccountAvatar,
16
15
  } satisfies Meta<typeof UserAccountAvatar>;
@@ -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,10 +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';
10
7
 
11
8
  export type * from './types';
@@ -6,28 +6,22 @@ import type { FC } from 'react';
6
6
 
7
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' | 'useIsCurrent' | 'useIsOpen' | 'blockInstruction' | '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.Node; open: boolean }>;
25
- useActions: (node: Node.Node) => FlattenedActions;
26
- loadDescendents?: (node: Node.Node) => MaybePromise<void>;
27
- useIsAlternateTree?: (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[];
@@ -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 = {};