@dxos/plugin-navtree 0.8.4-main.67995b8 → 0.8.4-main.6fa680abb7

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 (239) hide show
  1. package/dist/lib/browser/CommandsDialogContent-2DKIOLJL.mjs +113 -0
  2. package/dist/lib/browser/CommandsDialogContent-2DKIOLJL.mjs.map +7 -0
  3. package/dist/lib/browser/CommandsTrigger-ABODVVMP.mjs +33 -0
  4. package/dist/lib/browser/CommandsTrigger-ABODVVMP.mjs.map +7 -0
  5. package/dist/lib/browser/NavTreeContainer-TLSYS4KK.mjs +12 -0
  6. package/dist/lib/browser/NavTreeContainer-TLSYS4KK.mjs.map +7 -0
  7. package/dist/lib/browser/NavTreeDocumentTitle-L6NLLWLD.mjs +26 -0
  8. package/dist/lib/browser/NavTreeDocumentTitle-L6NLLWLD.mjs.map +7 -0
  9. package/dist/lib/browser/app-graph-builder-Z43OTNI2.mjs +44 -0
  10. package/dist/lib/browser/app-graph-builder-Z43OTNI2.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-A4VDCIJC.mjs +19 -0
  12. package/dist/lib/browser/chunk-A4VDCIJC.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-AYF2TX65.mjs +14 -0
  14. package/dist/lib/browser/chunk-AYF2TX65.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-JT37R5HS.mjs +1468 -0
  16. package/dist/lib/browser/chunk-JT37R5HS.mjs.map +7 -0
  17. package/dist/lib/browser/chunk-KF26WA4O.mjs +23 -0
  18. package/dist/lib/browser/chunk-KF26WA4O.mjs.map +7 -0
  19. package/dist/lib/browser/index.mjs +86 -95
  20. package/dist/lib/browser/index.mjs.map +4 -4
  21. package/dist/lib/browser/{keyboard-QRS6WOQH.mjs → keyboard-HYGFQO7T.mjs} +25 -19
  22. package/dist/lib/browser/keyboard-HYGFQO7T.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/operation-resolver-VDWVUII5.mjs +49 -0
  25. package/dist/lib/browser/operation-resolver-VDWVUII5.mjs.map +7 -0
  26. package/dist/lib/browser/react-surface-D2RLMV4Z.mjs +59 -0
  27. package/dist/lib/browser/react-surface-D2RLMV4Z.mjs.map +7 -0
  28. package/dist/lib/browser/state-CWMNNF7K.mjs +131 -0
  29. package/dist/lib/browser/state-CWMNNF7K.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 -105
  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 +27 -5
  57. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  59. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  60. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  61. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  62. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +10 -8
  63. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  64. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +33 -4
  65. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  66. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  67. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  68. package/dist/types/src/components/Sidebar/L0Menu.d.ts +10 -5
  69. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  70. package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -7
  71. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  72. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +5 -4
  73. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  74. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  75. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  76. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  77. package/dist/types/src/components/index.d.ts +0 -3
  78. package/dist/types/src/components/index.d.ts.map +1 -1
  79. package/dist/types/src/components/types.d.ts +8 -16
  80. package/dist/types/src/components/types.d.ts.map +1 -1
  81. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
  82. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  83. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +3 -0
  84. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  85. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  86. package/dist/types/src/containers/CommandsTrigger/index.d.ts +3 -0
  87. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  88. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  89. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  90. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  91. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  92. package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
  93. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  94. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +3 -0
  95. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  96. package/dist/types/src/containers/index.d.ts +7 -0
  97. package/dist/types/src/containers/index.d.ts.map +1 -0
  98. package/dist/types/src/experimental/Tree.d.ts +1 -2
  99. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  100. package/dist/types/src/experimental/Tree.stories.d.ts +23 -6
  101. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  102. package/dist/types/src/hooks/index.d.ts +5 -0
  103. package/dist/types/src/hooks/index.d.ts.map +1 -0
  104. package/dist/types/src/hooks/useActions.d.ts +5 -0
  105. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  106. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  107. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  108. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  109. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  110. package/dist/types/src/hooks/useNavTreeState.d.ts +22 -0
  111. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  112. package/dist/types/src/index.d.ts +1 -2
  113. package/dist/types/src/index.d.ts.map +1 -1
  114. package/dist/types/src/meta.d.ts +3 -4
  115. package/dist/types/src/meta.d.ts.map +1 -1
  116. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  117. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/generator.d.ts +5 -6
  119. package/dist/types/src/testing/generator.d.ts.map +1 -1
  120. package/dist/types/src/testing/update-state.d.ts +2 -2
  121. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  122. package/dist/types/src/types/capabilities.d.ts +16 -0
  123. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  124. package/dist/types/src/types/events.d.ts +5 -0
  125. package/dist/types/src/types/events.d.ts.map +1 -0
  126. package/dist/types/src/types/index.d.ts +4 -0
  127. package/dist/types/src/types/index.d.ts.map +1 -0
  128. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  129. package/dist/types/src/types/schema.d.ts.map +1 -0
  130. package/dist/types/src/util.d.ts +12 -9
  131. package/dist/types/src/util.d.ts.map +1 -1
  132. package/dist/types/tsconfig.tsbuildinfo +1 -1
  133. package/package.json +57 -55
  134. package/src/NavTreePlugin.tsx +64 -84
  135. package/src/capabilities/app-graph-builder/app-graph-builder.ts +42 -0
  136. package/src/capabilities/app-graph-builder/index.ts +7 -0
  137. package/src/capabilities/index.ts +5 -9
  138. package/src/capabilities/keyboard/index.ts +7 -0
  139. package/src/capabilities/keyboard/keyboard.ts +66 -0
  140. package/src/capabilities/operation-resolver/index.ts +7 -0
  141. package/src/capabilities/operation-resolver/operation-resolver.ts +45 -0
  142. package/src/capabilities/react-surface/index.ts +7 -0
  143. package/src/capabilities/react-surface/react-surface.tsx +53 -0
  144. package/src/capabilities/state/index.ts +7 -0
  145. package/src/capabilities/state/state.ts +149 -0
  146. package/src/components/NavTree/NavTree.stories.tsx +105 -98
  147. package/src/components/NavTree/NavTree.tsx +67 -32
  148. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  149. package/src/components/NavTreeContext/index.ts +5 -0
  150. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +21 -13
  151. package/src/components/NavTreeItem/NavTreeItemAction.tsx +61 -37
  152. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +68 -35
  153. package/src/components/Sidebar/L0Menu.tsx +170 -179
  154. package/src/components/Sidebar/L1Panel.tsx +252 -101
  155. package/src/components/Sidebar/L1Tabs.tsx +19 -33
  156. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -9
  157. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  158. package/src/components/index.ts +1 -3
  159. package/src/components/types.ts +15 -19
  160. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +135 -0
  161. package/src/containers/CommandsDialogContent/index.ts +7 -0
  162. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +26 -0
  163. package/src/containers/CommandsTrigger/index.ts +7 -0
  164. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +271 -0
  165. package/src/containers/NavTreeContainer/index.ts +6 -0
  166. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +5 -4
  167. package/src/containers/NavTreeDocumentTitle/index.ts +7 -0
  168. package/src/containers/index.ts +12 -0
  169. package/src/experimental/Tree.stories.tsx +78 -83
  170. package/src/experimental/Tree.tsx +6 -23
  171. package/src/hooks/index.ts +8 -0
  172. package/src/hooks/useActions.ts +34 -0
  173. package/src/hooks/useLoadDescendents.ts +23 -0
  174. package/src/hooks/useNavTreeModel.ts +118 -0
  175. package/src/hooks/useNavTreeState.ts +42 -0
  176. package/src/index.ts +1 -2
  177. package/src/meta.ts +11 -8
  178. package/src/testing/app-graph-builder.ts +159 -234
  179. package/src/testing/generator.ts +20 -18
  180. package/src/testing/update-state.ts +6 -6
  181. package/src/types/capabilities.ts +21 -0
  182. package/src/types/events.ts +12 -0
  183. package/src/types/index.ts +7 -0
  184. package/src/{types.ts → types/schema.ts} +9 -8
  185. package/src/util.ts +26 -33
  186. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs +0 -45
  187. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs.map +0 -7
  188. package/dist/lib/browser/chunk-ERDGQTH5.mjs +0 -16
  189. package/dist/lib/browser/chunk-ERDGQTH5.mjs.map +0 -7
  190. package/dist/lib/browser/chunk-WCARAZ7J.mjs +0 -15
  191. package/dist/lib/browser/chunk-WCARAZ7J.mjs.map +0 -7
  192. package/dist/lib/browser/chunk-WEMCSQI7.mjs +0 -1469
  193. package/dist/lib/browser/chunk-WEMCSQI7.mjs.map +0 -7
  194. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs +0 -47
  195. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs.map +0 -7
  196. package/dist/lib/browser/keyboard-QRS6WOQH.mjs.map +0 -7
  197. package/dist/lib/browser/react-surface-JSJ5SQX5.mjs +0 -51
  198. package/dist/lib/browser/react-surface-JSJ5SQX5.mjs.map +0 -7
  199. package/dist/lib/browser/state-54DY3DOY.mjs +0 -117
  200. package/dist/lib/browser/state-54DY3DOY.mjs.map +0 -7
  201. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  202. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  203. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  204. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  205. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  206. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  207. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  208. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  209. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  210. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  211. package/dist/types/src/capabilities/state.d.ts +0 -20
  212. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  213. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
  214. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  215. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  216. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  217. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  218. package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
  219. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  220. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  221. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  222. package/dist/types/src/events.d.ts +0 -4
  223. package/dist/types/src/events.d.ts.map +0 -1
  224. package/dist/types/src/hooks.d.ts +0 -3
  225. package/dist/types/src/hooks.d.ts.map +0 -1
  226. package/dist/types/src/types.d.ts.map +0 -1
  227. package/src/capabilities/app-graph-builder.ts +0 -52
  228. package/src/capabilities/capabilities.ts +0 -20
  229. package/src/capabilities/intent-resolver.ts +0 -37
  230. package/src/capabilities/keyboard.ts +0 -59
  231. package/src/capabilities/react-surface.tsx +0 -45
  232. package/src/capabilities/state.ts +0 -104
  233. package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
  234. package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -35
  235. package/src/components/CommandsDialog/index.ts +0 -6
  236. package/src/components/NavTreeContainer.tsx +0 -313
  237. package/src/events.ts +0 -11
  238. package/src/hooks.ts +0 -21
  239. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
@@ -2,153 +2,304 @@
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 { IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
+ import { Node } from '@dxos/app-graph';
10
+ import { isPinnedWorkspace } from '@dxos/app-toolkit';
11
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
12
+ import { Graph, useActionRunner, useConnections, useEdges } from '@dxos/plugin-graph';
13
+ import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
14
+ import { useAttended } from '@dxos/react-ui-attention';
9
15
  import { Tree } from '@dxos/react-ui-list';
16
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
10
17
  import { Tabs } from '@dxos/react-ui-tabs';
11
- import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/react-ui-theme';
18
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
19
 
20
+ import { useActions, useIsAlternateTree, useLoadDescendents } from '../../hooks';
13
21
  import { meta } from '../../meta';
22
+ import { NAV_TREE_ITEM } from '../NavTree';
14
23
  import { useNavTreeContext } from '../NavTreeContext';
15
- import { NavTreeItemColumns } from '../NavTreeItem';
24
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
16
25
 
17
26
  export type L1PanelProps = {
18
27
  open?: boolean;
19
28
  path: string[];
20
- item: Node<any>;
21
- currentItemId: string;
29
+ item: Node.Node;
30
+ isCurrent: boolean;
22
31
  onBack?: () => void;
23
32
  };
24
33
 
25
34
  /**
26
35
  * Space or settings panel.
27
36
  */
28
- export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
37
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
29
38
  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
39
  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
- );
40
+ const isActivated = useIsActivatedWorkspace(item);
41
+ const shouldRenderContent = isCurrent || isActivated;
46
42
 
47
43
  return (
48
44
  <Tabs.Tabpanel
49
45
  key={item.id}
50
46
  value={item.id}
51
47
  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',
48
+ 'absolute inset-y-0 end-0',
49
+ 'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
50
+ 'py-[env(safe-area-inset-top)]',
51
+ isCurrent && 'grid',
56
52
  ]}
57
53
  tabIndex={-1}
58
54
  aria-label={title}
59
- {...(!open && { inert: 'true' })}
55
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
56
+ {...(!open && { inert: true })}
60
57
  >
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
- )}
58
+ {shouldRenderContent && <L1PanelContent open={open} path={path} item={item} onBack={onBack} />}
91
59
  </Tabs.Tabpanel>
92
60
  );
93
61
  };
94
62
 
63
+ /** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
64
+ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
65
+ const { graph } = useAppGraph();
66
+ const edges = useEdges(graph, item.id);
67
+
68
+ return useMemo(() => {
69
+ const childIds = edges[Graph.relationKey('child')] ?? [];
70
+ return childIds.some((childId) => {
71
+ const child = Graph.getNode(graph, childId);
72
+ if (Option.isNone(child)) {
73
+ return false;
74
+ }
75
+ return child.value.properties.disposition === undefined;
76
+ });
77
+ }, [edges, graph]);
78
+ };
79
+
80
+ /**
81
+ * Mounted panel content for active or previously-visited tabs.
82
+ */
83
+ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
84
+ const navTreeContext = useNavTreeContext();
85
+
86
+ const alternateTree = useAlternateTreeItem(item);
87
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
88
+ const isAlternate = useIsAlternateTree(alternatePath, item);
89
+
90
+ return (
91
+ <DensityProvider density='fine'>
92
+ <L1PanelHeader path={path} item={item} onBack={onBack} />
93
+ <ScrollArea.Root thin orientation='vertical'>
94
+ <ScrollArea.Viewport>
95
+ {isAlternate && alternateTree ? (
96
+ <Tree
97
+ model={navTreeContext.model}
98
+ id={alternateTree.id}
99
+ rootId={alternateTree.id}
100
+ path={alternatePath}
101
+ levelOffset={5}
102
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
103
+ renderColumns={NavTreeItemColumns}
104
+ blockInstruction={navTreeContext.blockInstruction}
105
+ canDrop={navTreeContext.canDrop}
106
+ canSelect={navTreeContext.canSelect}
107
+ onOpenChange={navTreeContext.onOpenChange}
108
+ onSelect={navTreeContext.onSelect}
109
+ onItemHover={navTreeContext.onItemHover}
110
+ />
111
+ ) : (
112
+ <Tree
113
+ model={navTreeContext.model}
114
+ id={item.id}
115
+ rootId={item.id}
116
+ path={path}
117
+ levelOffset={5}
118
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
119
+ draggable
120
+ renderColumns={NavTreeItemColumns}
121
+ blockInstruction={navTreeContext.blockInstruction}
122
+ canDrop={navTreeContext.canDrop}
123
+ canSelect={navTreeContext.canSelect}
124
+ onOpenChange={navTreeContext.onOpenChange}
125
+ onSelect={navTreeContext.onSelect}
126
+ onItemHover={navTreeContext.onItemHover}
127
+ />
128
+ )}
129
+ </ScrollArea.Viewport>
130
+ </ScrollArea.Root>
131
+ </DensityProvider>
132
+ );
133
+ };
134
+
95
135
  /**
96
136
  * Header row.
97
137
  */
98
- const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
138
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
99
139
  const { t } = useTranslation(meta.id);
100
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
140
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
101
141
  const title = toLocalizedString(item.properties.label, t);
142
+ const backCapableWorkspace = isPinnedWorkspace(item.id);
102
143
 
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?.();
144
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
145
+ useLoadDescendents(item);
146
+
147
+ // TODO(wittjosiah): Hack to only show containsAttended when attended item is within alternate tree (e.g. settings).
148
+ // Since every object in the space is a descendant of the space node, we'd otherwise always show containsAttended.
149
+ const alternateTree = useAlternateTreeItem(item);
150
+ const attended = useAttended();
151
+ const alternateTreeOpen = useMemo(() => {
152
+ if (!alternateTree) {
153
+ return true;
118
154
  }
119
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
155
+ const hasAlternateAttended = attended.some(
156
+ (attendedId) => attendedId === alternateTree.id || attendedId.startsWith(alternateTree.id + '/'),
157
+ );
158
+ return !hasAlternateAttended;
159
+ }, [alternateTree, attended]);
120
160
 
121
- // TODO(burdon): Reuse same grid as tree?
122
161
  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
- <div>
127
- {(backCapable && (
162
+ <div className='flex w-full items-center border-b border-subdued-separator dx-app-drag dx-density-coarse pe-1'>
163
+ {backCapableWorkspace ? (
164
+ <IconButton
165
+ size={5}
166
+ density='coarse'
167
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
168
+ variant='ghost'
169
+ icon='ph--caret-left--regular'
170
+ iconOnly
171
+ label={t('button back')}
172
+ data-testid='treeView.primaryTreeButton'
173
+ onClick={() => onBack?.()}
174
+ />
175
+ ) : (
176
+ <div className='w-6' />
177
+ )}
178
+ <h2 className='flex-1 truncate min-w-0'>{title}</h2>
179
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
180
+ <div role='none' className='contents dx-app-no-drag'>
181
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
182
+ <NavTreeItemAction
183
+ testId={primaryAction.properties?.testId}
184
+ label={toLocalizedString(primaryAction.properties?.label, t)}
185
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
186
+ parent={item}
187
+ path={path}
188
+ monolithic={Node.isAction(primaryAction)}
189
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
190
+ menuType={primaryAction.properties?.menuType}
191
+ caller={NAV_TREE_ITEM}
192
+ />
193
+ )}
194
+ {menuActions.length === 1 && (
128
195
  <IconButton
196
+ size={5}
197
+ density='coarse'
198
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
129
199
  variant='ghost'
130
- icon='ph--arrow-u-down-left--regular'
200
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
131
201
  iconOnly
132
- size={5}
133
- label={t('button back')}
134
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
135
- onClick={handleBack}
202
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
203
+ data-testid={menuActions[0].properties?.testId}
204
+ onClick={() => onAction(menuActions[0] as Node.Action)}
136
205
  />
137
- )) ||
138
- (alternateTree && !isAlternate && (
139
- <IconButton
140
- data-testid='treeView.alternateTreeButton'
141
- variant='ghost'
142
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
143
- iconOnly
144
- size={5}
145
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
146
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
147
- onClick={handleOpen}
148
- />
149
- )) || <div />}
206
+ )}
207
+ {menuActions.length > 1 && (
208
+ <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
209
+ <Menu.Trigger asChild>
210
+ <IconButton
211
+ size={5}
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
+ {ItemEnd && <ItemEnd node={item} open={alternateTreeOpen} />}
150
225
  </div>
151
- <NavTreeItemColumns open path={path} item={item} density='coarse' />
152
226
  </div>
153
227
  );
154
228
  };
229
+
230
+ /**
231
+ * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
232
+ */
233
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
234
+ const { t } = useTranslation(meta.id);
235
+ const { setAlternateTree } = useNavTreeContext();
236
+ const { graph } = useAppGraph();
237
+ const runAction = useActionRunner();
238
+
239
+ const alternateTree = useAlternateTreeItem(item);
240
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
241
+ const isAlternate = useIsAlternateTree(alternatePath, item);
242
+
243
+ // Graph actions.
244
+ const { actions: actionsProp, groupedActions } = useActions(item);
245
+ const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
246
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
247
+ );
248
+
249
+ const graphMenuActions = (
250
+ primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp
251
+ )
252
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
253
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
254
+
255
+ // Synthetic settings/back action.
256
+ const settingsActionId = `${item.id}~settings`;
257
+ const settingsAction = useMemo((): Node.Action | undefined => {
258
+ if (!alternateTree) {
259
+ return undefined;
260
+ }
261
+ return {
262
+ id: settingsActionId,
263
+ type: Node.ActionType,
264
+ data: () => Effect.void,
265
+ properties: {
266
+ label: isAlternate ? ['button back', { ns: meta.id }] : (alternateTree.properties.label ?? alternateTree.id),
267
+ icon: isAlternate
268
+ ? 'ph--arrow-u-down-left--regular'
269
+ : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
270
+ disposition: 'list-item',
271
+ testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
272
+ },
273
+ } as Node.Action;
274
+ }, [alternateTree, isAlternate, settingsActionId, t]);
275
+
276
+ const menuActions = useMemo(
277
+ () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
278
+ [graphMenuActions, settingsAction],
279
+ );
280
+
281
+ const onAction = useCallback(
282
+ (action: Node.Action, params?: Node.InvokeProps) => {
283
+ if (action.id === settingsActionId) {
284
+ if (alternateTree && !isAlternate) {
285
+ Graph.expand(graph, alternateTree.id, 'child');
286
+ }
287
+ setAlternateTree?.(alternatePath, !isAlternate);
288
+ } else {
289
+ void runAction(action, { ...params, path });
290
+ }
291
+ },
292
+ [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction, graph, alternateTree, path],
293
+ );
294
+
295
+ return { primaryAction, groupedActions, menuActions, onAction };
296
+ };
297
+
298
+ /** Finds the first child with disposition 'alternate-tree' using graph connections directly. */
299
+ const useAlternateTreeItem = (item: Node.Node): Node.Node | undefined => {
300
+ const { graph } = useAppGraph();
301
+ const connections = useConnections(graph, item.id, 'child');
302
+ return connections.find((node) => node.properties.disposition === 'alternate-tree');
303
+ };
304
+
305
+ export const L1Panel = memo(L1Panel$);
@@ -2,56 +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';
10
+
12
11
  import { L1Panel, type L1PanelProps } from './L1Panel';
13
12
 
14
- export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
13
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
14
+ currentItemId: string;
15
15
  path: string[];
16
- topLevelItems: Node<any>[];
16
+ topLevelItems: Node.Node[];
17
17
  };
18
18
 
19
19
  /**
20
- * Each space is an L1 tab.
20
+ * Each workspace is an L1 tab.
21
21
  */
22
- export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
22
+ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
23
23
  return (
24
24
  <>
25
25
  {topLevelItems.map((item) => {
26
26
  const type = l0ItemType(item);
27
- switch (type) {
28
- case 'tab':
29
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
30
- case 'collection':
31
- return <L1PanelCollection key={item.id} item={item} {...props} />;
32
- default:
33
- 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
+ );
34
38
  }
39
+ return null;
35
40
  })}
36
41
  </>
37
42
  );
38
43
  };
39
-
40
- /**
41
- *
42
- */
43
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
44
- const { useItems } = useNavTreeContext();
45
- useLoadDescendents(item);
46
- const collectionItems = useItems(item);
47
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
48
- return (
49
- <>
50
- {collectionItems
51
- .filter((item) => l0ItemType(item) === 'tab')
52
- .map((item) => (
53
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
54
- ))}
55
- </>
56
- );
57
- };
@@ -2,20 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
- import { type Meta } from '@storybook/react-vite';
8
-
9
- import { withTheme } from '@dxos/storybook-utils';
7
+ import { withTheme } from '@dxos/react-ui/testing';
10
8
 
11
9
  import { UserAccountAvatar } from './UserAccountAvatar';
12
10
 
13
- const meta: Meta<typeof UserAccountAvatar> = {
14
- title: 'plugins/plugin-navtree/UserAccountAvatar',
11
+ const meta = {
12
+ title: 'plugins/plugin-navtree/components/UserAccountAvatar',
13
+ decorators: [withTheme()],
15
14
  component: UserAccountAvatar,
16
- decorators: [withTheme],
17
- };
15
+ } satisfies Meta<typeof UserAccountAvatar>;
18
16
 
19
17
  export default meta;
20
18
 
21
- export const Default = {};
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = {};
@@ -7,6 +7,8 @@ import React from 'react';
7
7
  import { Avatar, type AvatarStatus, type Size } from '@dxos/react-ui';
8
8
  import { hexToFallback } from '@dxos/util';
9
9
 
10
+ import { L0ItemActiveTabIndicator } from '../Sidebar';
11
+
10
12
  export type UserAccountAvatarProps = {
11
13
  size?: Size;
12
14
  userId?: string;
@@ -19,17 +21,23 @@ export const UserAccountAvatar = ({ size, userId, hue, emoji, status }: UserAcco
19
21
  const fallbackValue = hexToFallback(userId ?? '0');
20
22
 
21
23
  return (
22
- <div className='grid place-items-center' data-joyride='welcome/account'>
23
- <Avatar.Root>
24
- <Avatar.Content
25
- variant='circle'
26
- size={size ?? 12}
27
- status={status ?? 'active'}
28
- hue={hue || fallbackValue.hue}
29
- fallback={emoji || fallbackValue.emoji}
30
- data-testid='treeView.userAccount'
31
- />
32
- </Avatar.Root>
33
- </div>
24
+ <>
25
+ <L0ItemActiveTabIndicator classNames='inset-y-6' />
26
+ <div
27
+ className='grid place-items-center dx-focus-ring-group-indicator rounded-full'
28
+ data-joyride='welcome/account'
29
+ >
30
+ <Avatar.Root>
31
+ <Avatar.Content
32
+ variant='circle'
33
+ size={size ?? 12}
34
+ status={status ?? 'active'}
35
+ hue={hue || fallbackValue.hue}
36
+ fallback={emoji || fallbackValue.emoji}
37
+ data-testid='treeView.userAccount'
38
+ />
39
+ </Avatar.Root>
40
+ </div>
41
+ </>
34
42
  );
35
43
  };
@@ -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,33 +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';
10
+
11
+ import type { NavTreeItemGraphNode } from '../types';
11
12
 
12
13
  import type { L1PanelProps } from './Sidebar';
13
- import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
14
14
 
15
- export type NavTreeContextValue = Pick<
16
- TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
17
- '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'
18
24
  > &
19
- Pick<L1PanelProps, 'onBack'> & {
20
- tab: string;
21
- topbar?: boolean;
22
- popoverAnchorId?: string;
23
- renderItemEnd?: FC<{ node: Node; open: boolean }>;
24
- getActions: (node: Node) => FlattenedActions;
25
- loadDescendents?: (node: Node) => MaybePromise<void>;
26
- isAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;
27
- setAlternateTree?: (path: string[], open: boolean) => void;
28
- onTabChange?: (node: NavTreeItemGraphNode) => void;
29
- };
25
+ Pick<L1PanelProps, 'onBack'>;
30
26
 
31
27
  export type NavTreeItemColumnsProps = {
32
28
  path: string[];
33
- item: Node;
29
+ item: Node.Node;
34
30
  open: boolean;
35
31
  density?: Density;
36
32
  };