@dxos/plugin-navtree 0.8.3 → 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 (212) 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 -125
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +21 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/types/src/NavTreePlugin.d.ts +3 -1
  9. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  10. package/dist/types/src/NavTreePlugin.node.d.ts +4 -0
  11. package/dist/types/src/NavTreePlugin.node.d.ts.map +1 -0
  12. package/dist/types/src/NavTreePlugin.test.d.ts +2 -0
  13. package/dist/types/src/NavTreePlugin.test.d.ts.map +1 -0
  14. package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
  15. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  16. package/dist/types/src/capabilities/index.d.ts +7 -105
  17. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  18. package/dist/types/src/capabilities/keyboard.d.ts +3 -2
  19. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  20. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  21. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  22. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  23. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/state.d.ts +4 -18
  25. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  26. package/dist/types/src/components/NavTree/NavTree.d.ts +12 -5
  27. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  28. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +25 -5
  29. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  31. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  32. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  33. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  34. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +13 -9
  35. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  36. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +33 -4
  37. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +2 -2
  39. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  40. package/dist/types/src/components/Sidebar/L0Menu.d.ts +10 -5
  41. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  42. package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -8
  43. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  44. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +13 -0
  45. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -0
  46. package/dist/types/src/components/Sidebar/index.d.ts +1 -0
  47. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -1
  48. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  49. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  50. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/index.d.ts +1 -4
  52. package/dist/types/src/components/index.d.ts.map +1 -1
  53. package/dist/types/src/components/types.d.ts +8 -17
  54. package/dist/types/src/components/types.d.ts.map +1 -1
  55. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
  56. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  57. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +28 -0
  58. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
  59. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
  60. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  61. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  62. package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
  63. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  64. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  65. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  66. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  67. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  68. package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
  69. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  70. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
  71. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  72. package/dist/types/src/containers/index.d.ts +7 -0
  73. package/dist/types/src/containers/index.d.ts.map +1 -0
  74. package/dist/types/src/experimental/Tree.d.ts +5 -6
  75. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  76. package/dist/types/src/experimental/Tree.stories.d.ts +23 -6
  77. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  78. package/dist/types/src/hooks/index.d.ts +5 -0
  79. package/dist/types/src/hooks/index.d.ts.map +1 -0
  80. package/dist/types/src/hooks/useActions.d.ts +5 -0
  81. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  82. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  83. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  84. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  85. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  86. package/dist/types/src/hooks/useNavTreeState.d.ts +18 -0
  87. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  88. package/dist/types/src/index.d.ts +3 -3
  89. package/dist/types/src/index.d.ts.map +1 -1
  90. package/dist/types/src/meta.d.ts +3 -4
  91. package/dist/types/src/meta.d.ts.map +1 -1
  92. package/dist/types/src/operations/expose.d.ts +5 -0
  93. package/dist/types/src/operations/expose.d.ts.map +1 -0
  94. package/dist/types/src/operations/index.d.ts +3 -0
  95. package/dist/types/src/operations/index.d.ts.map +1 -0
  96. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  97. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  98. package/dist/types/src/testing/generator.d.ts +5 -6
  99. package/dist/types/src/testing/generator.d.ts.map +1 -1
  100. package/dist/types/src/testing/update-state.d.ts +2 -2
  101. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  102. package/dist/types/src/translations.d.ts +11 -13
  103. package/dist/types/src/translations.d.ts.map +1 -1
  104. package/dist/types/src/types/capabilities.d.ts +15 -0
  105. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  106. package/dist/types/src/types/events.d.ts +5 -0
  107. package/dist/types/src/types/events.d.ts.map +1 -0
  108. package/dist/types/src/types/index.d.ts +4 -0
  109. package/dist/types/src/types/index.d.ts.map +1 -0
  110. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  111. package/dist/types/src/types/schema.d.ts.map +1 -0
  112. package/dist/types/src/util.d.ts +12 -9
  113. package/dist/types/src/util.d.ts.map +1 -1
  114. package/dist/types/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +79 -59
  116. package/src/NavTreePlugin.node.ts +17 -0
  117. package/src/NavTreePlugin.test.ts +27 -0
  118. package/src/NavTreePlugin.tsx +51 -88
  119. package/src/capabilities/app-graph-builder.ts +34 -44
  120. package/src/capabilities/index.ts +10 -8
  121. package/src/capabilities/keyboard.ts +58 -51
  122. package/src/capabilities/operation-handler.ts +16 -0
  123. package/src/capabilities/react-surface.tsx +46 -38
  124. package/src/capabilities/state.ts +129 -84
  125. package/src/components/NavTree/NavTree.stories.tsx +111 -103
  126. package/src/components/NavTree/NavTree.tsx +71 -35
  127. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  128. package/src/components/NavTreeContext/index.ts +5 -0
  129. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +26 -20
  130. package/src/components/NavTreeItem/NavTreeItemAction.tsx +73 -50
  131. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +70 -38
  132. package/src/components/Sidebar/L0Menu.tsx +176 -210
  133. package/src/components/Sidebar/L1Panel.tsx +212 -136
  134. package/src/components/Sidebar/L1Tabs.tsx +42 -0
  135. package/src/components/Sidebar/index.ts +1 -0
  136. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -9
  137. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  138. package/src/components/index.ts +2 -4
  139. package/src/components/types.ts +14 -19
  140. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +46 -0
  141. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +127 -0
  142. package/src/containers/CommandsDialogContent/index.ts +5 -0
  143. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +28 -0
  144. package/src/containers/CommandsTrigger/index.ts +5 -0
  145. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +271 -0
  146. package/src/containers/NavTreeContainer/index.ts +6 -0
  147. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +5 -4
  148. package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
  149. package/src/containers/index.ts +12 -0
  150. package/src/experimental/Tree.stories.tsx +111 -116
  151. package/src/experimental/Tree.tsx +16 -34
  152. package/src/hooks/index.ts +8 -0
  153. package/src/hooks/useActions.ts +34 -0
  154. package/src/hooks/useLoadDescendents.ts +23 -0
  155. package/src/hooks/useNavTreeModel.ts +118 -0
  156. package/src/hooks/useNavTreeState.ts +35 -0
  157. package/src/index.ts +8 -3
  158. package/src/meta.ts +12 -8
  159. package/src/operations/expose.ts +42 -0
  160. package/src/operations/index.ts +7 -0
  161. package/src/testing/app-graph-builder.ts +167 -238
  162. package/src/testing/generator.ts +41 -39
  163. package/src/testing/update-state.ts +6 -6
  164. package/src/translations.ts +13 -12
  165. package/src/types/capabilities.ts +21 -0
  166. package/src/types/events.ts +12 -0
  167. package/src/types/index.ts +7 -0
  168. package/src/{types.ts → types/schema.ts} +9 -8
  169. package/src/util.ts +26 -33
  170. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs +0 -45
  171. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs.map +0 -7
  172. package/dist/lib/browser/chunk-33VLL4IJ.mjs +0 -15
  173. package/dist/lib/browser/chunk-33VLL4IJ.mjs.map +0 -7
  174. package/dist/lib/browser/chunk-IEW3JD5C.mjs +0 -16
  175. package/dist/lib/browser/chunk-IEW3JD5C.mjs.map +0 -7
  176. package/dist/lib/browser/chunk-ZJPL4XJD.mjs +0 -1429
  177. package/dist/lib/browser/chunk-ZJPL4XJD.mjs.map +0 -7
  178. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs +0 -47
  179. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs.map +0 -7
  180. package/dist/lib/browser/keyboard-DDFATRUG.mjs +0 -51
  181. package/dist/lib/browser/keyboard-DDFATRUG.mjs.map +0 -7
  182. package/dist/lib/browser/react-surface-H3JRTVNU.mjs +0 -51
  183. package/dist/lib/browser/react-surface-H3JRTVNU.mjs.map +0 -7
  184. package/dist/lib/browser/state-J6AKZRB4.mjs +0 -117
  185. package/dist/lib/browser/state-J6AKZRB4.mjs.map +0 -7
  186. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  187. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  188. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  189. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  190. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
  191. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  192. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  193. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  194. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  195. package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
  196. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  197. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  198. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  199. package/dist/types/src/events.d.ts +0 -4
  200. package/dist/types/src/events.d.ts.map +0 -1
  201. package/dist/types/src/hooks.d.ts +0 -3
  202. package/dist/types/src/hooks.d.ts.map +0 -1
  203. package/dist/types/src/types.d.ts.map +0 -1
  204. package/src/capabilities/capabilities.ts +0 -20
  205. package/src/capabilities/intent-resolver.ts +0 -37
  206. package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
  207. package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -37
  208. package/src/components/CommandsDialog/index.ts +0 -6
  209. package/src/components/NavTreeContainer.tsx +0 -314
  210. package/src/events.ts +0 -11
  211. package/src/hooks.ts +0 -21
  212. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
@@ -2,174 +2,250 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Option from 'effect/Option';
6
+ import React, { memo, useCallback, useMemo } from 'react';
6
7
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { Button, type ButtonProps, Icon, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { Node } from '@dxos/app-graph';
9
+ import { isPinnedWorkspace } from '@dxos/app-toolkit';
10
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
11
+ import { Graph, useActionRunner, useEdges } from '@dxos/plugin-graph';
12
+ import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
13
  import { Tree } from '@dxos/react-ui-list';
14
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
10
15
  import { Tabs } from '@dxos/react-ui-tabs';
11
- import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
16
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
17
 
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';
18
+ import { useActions, useLoadDescendents } from '#hooks';
19
+ import { meta } from '#meta';
20
20
 
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
- };
21
+ import { NAV_TREE_ITEM } from '../NavTree';
22
+ import { useNavTreeContext } from '../NavTreeContext';
23
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
33
24
 
34
25
  export type L1PanelProps = {
35
26
  open?: boolean;
36
- item: Node<any>;
37
27
  path: string[];
38
- currentItemId: string;
28
+ item: Node.Node;
29
+ isCurrent: boolean;
39
30
  onBack?: () => void;
40
31
  };
41
32
 
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
-
33
+ /**
34
+ * Space or settings panel.
35
+ */
36
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
37
+ const { t } = useTranslation(meta.id);
67
38
  const title = toLocalizedString(item.properties.label, t);
68
- const backCapable = item.id.startsWith('!') || isAlternate;
39
+ const isActivated = useIsActivatedWorkspace(item);
40
+ const shouldRenderContent = isCurrent || isActivated;
69
41
 
70
42
  return (
71
- <Tabs.Tabpanel
43
+ <Tabs.Panel
72
44
  key={item.id}
73
45
  value={item.id}
74
46
  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',
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',
79
51
  ]}
80
52
  tabIndex={-1}
81
53
  aria-label={title}
82
- {...(!open && { inert: 'true' })}
54
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
55
+ {...(!open && { inert: true })}
83
56
  >
84
- {item.id === currentItemId && (
85
- <>
86
- <h2 className='flex items-center border-be border-subduedSeparator app-drag pli-1'>
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 && (
93
- <IconButton
94
- variant='ghost'
95
- classNames={mx('shrink-0', hoverableControlItem, hoverableOpenControlItem, 'pli-2 pointer-fine:pli-1')}
96
- iconOnly
97
- size={5}
98
- density='coarse'
99
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
100
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
101
- data-testid='treeView.alternateTreeButton'
102
- onClick={handleOpen}
103
- />
104
- )}
105
- <NavTreeItemColumns path={path} item={item} open density='coarse' />
106
- </h2>
107
- <div role='none' className='overflow-y-auto'>
108
- {isAlternate ? (
109
- <Tree
110
- {...navTreeContext}
111
- useItems={useAlternateItems}
112
- id={alternateTree.id}
113
- root={alternateTree}
114
- path={alternatePath}
115
- levelOffset={5}
116
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
117
- renderColumns={NavTreeItemColumns}
118
- />
119
- ) : (
120
- <Tree
121
- {...navTreeContext}
122
- id={item.id}
123
- root={item}
124
- path={path}
125
- levelOffset={5}
126
- draggable
127
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
128
- renderColumns={NavTreeItemColumns}
129
- />
130
- )}
131
- </div>
132
- </>
133
- )}
134
- </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>
135
109
  );
136
110
  };
137
111
 
138
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
139
- const { useItems } = useNavTreeContext();
112
+ /**
113
+ * Header row.
114
+ */
115
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
116
+ const { t } = useTranslation(meta.id);
117
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
118
+ const title = toLocalizedString(item.properties.label, t);
119
+ const backCapableWorkspace = isPinnedWorkspace(item.id);
120
+
121
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
140
122
  useLoadDescendents(item);
141
- const collectionItems = useItems(item);
142
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
123
+
143
124
  return (
144
- <>
145
- {collectionItems
146
- .filter((item) => l0ItemType(item) === 'tab')
147
- .map((item) => (
148
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
149
- ))}
150
- </>
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>
146
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
147
+ <div role='none' className='contents dx-app-no-drag'>
148
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
149
+ <NavTreeItemAction
150
+ testId={primaryAction.properties?.testId}
151
+ label={toLocalizedString(primaryAction.properties?.label, t)}
152
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
153
+ parent={item}
154
+ path={path}
155
+ monolithic={Node.isAction(primaryAction)}
156
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
157
+ menuType={primaryAction.properties?.menuType}
158
+ caller={NAV_TREE_ITEM}
159
+ />
160
+ )}
161
+ <MenuActions item={item} menuActions={menuActions} onAction={onAction} />
162
+ {ItemEnd && <ItemEnd node={item} open />}
163
+ </div>
164
+ </div>
151
165
  );
152
166
  };
153
167
 
154
- export type L1PanelsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
155
- topLevelItems: Node<any>[];
156
- path: string[];
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;
157
173
  };
158
174
 
159
- export const L1Panels = ({ topLevelItems, onBack, ...props }: L1PanelsProps) => {
175
+ /**
176
+ * Header menu actions for an L1 workspace tab. Renders nothing for an empty
177
+ * `menuActions`, a single inline icon button for one action, and a
178
+ * `…`-menu trigger for multiple.
179
+ */
180
+ const MenuActions = ({
181
+ item,
182
+ menuActions,
183
+ onAction,
184
+ }: {
185
+ item: Node.Node;
186
+ } & Pick<L1MenuActions, 'menuActions' | 'onAction'>) => {
187
+ const { t } = useTranslation(meta.id);
188
+
189
+ if (menuActions.length === 0) {
190
+ return null;
191
+ }
192
+
193
+ if (menuActions.length === 1) {
194
+ return (
195
+ <IconButton
196
+ density='coarse'
197
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
198
+ variant='ghost'
199
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
200
+ iconOnly
201
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
202
+ data-testid={menuActions[0].properties?.testId}
203
+ onClick={() => onAction(menuActions[0] as Node.Action)}
204
+ />
205
+ );
206
+ }
207
+
160
208
  return (
161
- <>
162
- {topLevelItems.map((item) => {
163
- const type = l0ItemType(item);
164
- switch (type) {
165
- case 'collection':
166
- return <L1PanelCollection key={item.id} item={item} {...props} />;
167
- case 'tab':
168
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
169
- default:
170
- return null;
171
- }
172
- })}
173
- </>
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>
174
223
  );
175
224
  };
225
+
226
+ /**
227
+ * Builds the menu actions for the L1 panel header.
228
+ */
229
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>): L1MenuActions => {
230
+ const runAction = useActionRunner();
231
+
232
+ const { actions: actionsProp, groupedActions } = useActions(item);
233
+ const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
234
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
235
+ );
236
+
237
+ const menuActions = (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp)
238
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
239
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
240
+
241
+ const onAction = useCallback(
242
+ (action: Node.Action, params?: Node.InvokeProps) => {
243
+ void runAction(action, { ...params, path });
244
+ },
245
+ [runAction, path],
246
+ );
247
+
248
+ return { primaryAction, groupedActions, menuActions, onAction };
249
+ };
250
+
251
+ export const L1Panel = memo(L1Panel$);
@@ -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';
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,28 @@
4
4
 
5
5
  import type { FC } from 'react';
6
6
 
7
- import type { Node } from '@dxos/app-graph';
7
+ import { type Node } from '@dxos/app-graph';
8
8
  import type { Density } from '@dxos/react-ui';
9
- import type { TreeProps } from '@dxos/react-ui-list';
10
- import type { MaybePromise } from '@dxos/util';
9
+ import type { TreeModel, TreeProps } from '@dxos/react-ui-list';
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
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
20
+ } & Pick<
21
+ TreeProps<NavTreeItemGraphNode>,
22
+ 'blockInstruction' | 'canDrop' | 'canSelect' | 'onOpenChange' | 'onSelect' | 'onItemHover'
18
23
  > &
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
- };
24
+ Pick<L1PanelProps, 'onBack'>;
30
25
 
31
26
  export type NavTreeItemColumnsProps = {
32
27
  path: string[];
33
- item: Node;
28
+ item: Node.Node;
34
29
  open: boolean;
35
30
  density?: Density;
36
31
  };
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { withPluginManager } from '@dxos/app-framework/testing';
9
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
10
+ import { Dialog } from '@dxos/react-ui';
11
+ import { withLayout } from '@dxos/react-ui/testing';
12
+
13
+ import { translations } from '#translations';
14
+
15
+ import { NavTreePlugin } from '../../NavTreePlugin';
16
+ import { CommandsDialogContent } from './CommandsDialogContent';
17
+
18
+ const DefaultStory = () => (
19
+ <Dialog.Root defaultOpen>
20
+ <Dialog.Overlay>
21
+ <CommandsDialogContent />
22
+ </Dialog.Overlay>
23
+ </Dialog.Root>
24
+ );
25
+
26
+ const meta = {
27
+ title: 'plugins/plugin-navtree/containers/CommandsDialogContent',
28
+ component: CommandsDialogContent,
29
+ render: DefaultStory,
30
+ decorators: [
31
+ withLayout({ layout: 'fullscreen' }),
32
+ withPluginManager({
33
+ plugins: [...corePlugins(), StorybookPlugin({}), NavTreePlugin()],
34
+ }),
35
+ ],
36
+ parameters: {
37
+ layout: 'fullscreen',
38
+ translations,
39
+ },
40
+ } satisfies Meta<typeof CommandsDialogContent>;
41
+
42
+ export default meta;
43
+
44
+ type Story = StoryObj<typeof meta>;
45
+
46
+ export const Default: Story = {};