@dxos/plugin-navtree 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5

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