@dxos/plugin-navtree 0.7.5-main.9d2a38b → 0.7.5-main.b19bfc8

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 (110) hide show
  1. package/dist/lib/browser/{app-graph-builder-OF4MMBLH.mjs → app-graph-builder-BL7LRXCM.mjs} +7 -5
  2. package/dist/lib/browser/{app-graph-builder-OF4MMBLH.mjs.map → app-graph-builder-BL7LRXCM.mjs.map} +3 -3
  3. package/dist/lib/browser/chunk-7OKF4GP3.mjs +1156 -0
  4. package/dist/lib/browser/chunk-7OKF4GP3.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-OI7AAA6Q.mjs → chunk-AOAGODQB.mjs} +1 -1
  6. package/dist/lib/browser/chunk-AOAGODQB.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +22 -27
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/intent-resolver-U25VBFH3.mjs +47 -0
  10. package/dist/lib/browser/intent-resolver-U25VBFH3.mjs.map +7 -0
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/react-surface-SIEGT4YS.mjs +57 -0
  13. package/dist/lib/browser/react-surface-SIEGT4YS.mjs.map +7 -0
  14. package/dist/lib/browser/{state-HT65TAYL.mjs → state-IEFQTUPG.mjs} +7 -10
  15. package/dist/lib/browser/state-IEFQTUPG.mjs.map +7 -0
  16. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  17. package/dist/types/src/capabilities/app-graph-builder.d.ts +110 -110
  18. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  19. package/dist/types/src/capabilities/capabilities.d.ts +3 -2
  20. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  21. package/dist/types/src/capabilities/index.d.ts +178 -122
  22. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/state.d.ts +3 -3
  26. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  27. package/dist/types/src/components/CommandsDialog.d.ts +1 -2
  28. package/dist/types/src/components/CommandsDialog.d.ts.map +1 -1
  29. package/dist/types/src/components/CommandsTrigger.d.ts +1 -2
  30. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -1
  31. package/dist/types/src/components/HaloButton.d.ts +1 -2
  32. package/dist/types/src/components/HaloButton.d.ts.map +1 -1
  33. package/dist/types/src/components/HaloButton.stories.d.ts +1 -2
  34. package/dist/types/src/components/HaloButton.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/L0Menu.d.ts +8 -0
  36. package/dist/types/src/components/L0Menu.d.ts.map +1 -0
  37. package/dist/types/src/components/L1Panels.d.ts +13 -0
  38. package/dist/types/src/components/L1Panels.d.ts.map +1 -0
  39. package/dist/types/src/components/NavTree.d.ts +2 -20
  40. package/dist/types/src/components/NavTree.d.ts.map +1 -1
  41. package/dist/types/src/components/NavTree.stories.d.ts +2 -2
  42. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/NavTreeContainer.d.ts +4 -3
  44. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  45. package/dist/types/src/components/NavTreeContext.d.ts +4 -0
  46. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -0
  47. package/dist/types/src/components/NavTreeItemAction.d.ts +5 -5
  48. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -1
  49. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +2 -2
  50. package/dist/types/src/components/NavTreeItemColumns.d.ts +4 -0
  51. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +1 -0
  52. package/dist/types/src/components/NotchStart.d.ts +1 -2
  53. package/dist/types/src/components/NotchStart.d.ts.map +1 -1
  54. package/dist/types/src/components/index.d.ts +2 -0
  55. package/dist/types/src/components/index.d.ts.map +1 -1
  56. package/dist/types/src/components/types.d.ts +26 -0
  57. package/dist/types/src/components/types.d.ts.map +1 -0
  58. package/dist/types/src/experimental/Tree.d.ts +8 -8
  59. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  60. package/dist/types/src/experimental/Tree.stories.d.ts +2 -3
  61. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  62. package/dist/types/src/hooks.d.ts +3 -0
  63. package/dist/types/src/hooks.d.ts.map +1 -0
  64. package/dist/types/src/stories/Graph.stories.d.ts +2 -2
  65. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
  66. package/dist/types/src/stories/StorybookNavTree.d.ts +3 -0
  67. package/dist/types/src/stories/StorybookNavTree.d.ts.map +1 -0
  68. package/dist/types/src/translations.d.ts +0 -6
  69. package/dist/types/src/translations.d.ts.map +1 -1
  70. package/dist/types/src/types.d.ts +2 -1
  71. package/dist/types/src/types.d.ts.map +1 -1
  72. package/dist/types/src/util.d.ts +9 -0
  73. package/dist/types/src/util.d.ts.map +1 -1
  74. package/package.json +33 -31
  75. package/src/NavTreePlugin.tsx +9 -11
  76. package/src/capabilities/app-graph-builder.ts +6 -4
  77. package/src/capabilities/capabilities.ts +3 -2
  78. package/src/capabilities/intent-resolver.ts +22 -16
  79. package/src/capabilities/react-surface.tsx +15 -41
  80. package/src/capabilities/state.ts +6 -9
  81. package/src/components/CommandsDialog.tsx +3 -4
  82. package/src/components/CommandsTrigger.tsx +7 -5
  83. package/src/components/L0Menu.tsx +238 -0
  84. package/src/components/L1Panels.tsx +119 -0
  85. package/src/components/NavTree.stories.tsx +6 -6
  86. package/src/components/NavTree.tsx +26 -116
  87. package/src/components/NavTreeContainer.tsx +114 -33
  88. package/src/components/NavTreeContext.tsx +13 -0
  89. package/src/components/NavTreeItemAction.tsx +12 -4
  90. package/src/components/NavTreeItemColumns.tsx +71 -0
  91. package/src/components/NotchStart.tsx +2 -2
  92. package/src/components/index.ts +2 -0
  93. package/src/components/types.ts +35 -0
  94. package/src/hooks.ts +21 -0
  95. package/src/stories/Graph.stories.tsx +6 -5
  96. package/src/stories/StorybookNavTree.tsx +63 -0
  97. package/src/translations.ts +0 -6
  98. package/src/types.ts +2 -1
  99. package/src/util.ts +56 -2
  100. package/dist/lib/browser/chunk-IMGT2LAL.mjs +0 -658
  101. package/dist/lib/browser/chunk-IMGT2LAL.mjs.map +0 -7
  102. package/dist/lib/browser/chunk-OI7AAA6Q.mjs.map +0 -7
  103. package/dist/lib/browser/intent-resolver-74JPWYBP.mjs +0 -42
  104. package/dist/lib/browser/intent-resolver-74JPWYBP.mjs.map +0 -7
  105. package/dist/lib/browser/react-surface-6QWXI53A.mjs +0 -189
  106. package/dist/lib/browser/react-surface-6QWXI53A.mjs.map +0 -7
  107. package/dist/lib/browser/state-HT65TAYL.mjs.map +0 -7
  108. package/dist/types/src/components/NavTreeFooter.d.ts +0 -6
  109. package/dist/types/src/components/NavTreeFooter.d.ts.map +0 -1
  110. package/src/components/NavTreeFooter.tsx +0 -161
@@ -2,130 +2,40 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { Fragment, useCallback, useEffect, type FC } from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
- import { isAction, type Node } from '@dxos/app-graph';
8
- import { Popover, toLocalizedString, Treegrid, useTranslation } from '@dxos/react-ui';
9
- import { Tree, type TreeProps } from '@dxos/react-ui-list';
10
- import { type MaybePromise } from '@dxos/util';
7
+ import { Tabs } from '@dxos/react-ui-tabs';
8
+ import { byPosition } from '@dxos/util';
11
9
 
12
- import { NavTreeItemAction } from './NavTreeItemAction';
13
- import { NAVTREE_PLUGIN } from '../meta';
14
- import { type FlattenedActions, type NavTreeItemGraphNode } from '../types';
10
+ import { L0Menu } from './L0Menu';
11
+ import { L1Panels } from './L1Panels';
12
+ import { useNavTreeContext } from './NavTreeContext';
13
+ import { type NavTreeProps } from './types';
14
+ import { useLoadDescendents } from '../hooks';
15
15
 
16
16
  export const NAV_TREE_ITEM = 'NavTreeItem';
17
17
 
18
- export type NavTreeProps = Omit<
19
- TreeProps<NavTreeItemGraphNode>,
20
- 'draggable' | 'gridTemplateColumns' | 'renderColumns'
21
- > &
22
- Pick<NavTreeColumnsProps, 'getActions' | 'loadDescendents' | 'renderItemEnd' | 'popoverAnchorId'>;
23
-
24
- export const NavTree = ({ getActions, loadDescendents, renderItemEnd, popoverAnchorId, ...props }: NavTreeProps) => {
25
- const renderColumns = useCallback<NonNullable<TreeProps<NavTreeItemGraphNode>['renderColumns']>>(
26
- ({ item, path, open }) => {
27
- return (
28
- <NavTreeColumns
29
- path={path}
30
- node={item}
31
- getActions={getActions}
32
- open={open}
33
- loadDescendents={loadDescendents}
34
- renderItemEnd={renderItemEnd}
35
- popoverAnchorId={popoverAnchorId}
36
- />
37
- );
38
- },
39
- [renderItemEnd, popoverAnchorId, loadDescendents],
40
- );
41
-
42
- return (
43
- <Tree
44
- {...props}
45
- draggable
46
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
47
- renderColumns={renderColumns}
48
- />
18
+ export const NavTree = ({ id, root }: NavTreeProps) => {
19
+ const { tab, getItems, onBack } = useNavTreeContext();
20
+ const topLevelActions = getItems(root, 'item').toSorted((a, b) => byPosition(a.properties, b.properties));
21
+ const topLevelCollections = getItems(root, 'collection');
22
+ const topLevelWorkspaces = getItems(root, 'workspace');
23
+ const topLevelNavigation = getItems(root, 'navigation');
24
+ const l0Items = useMemo(
25
+ () => [...topLevelActions, ...topLevelCollections, ...topLevelWorkspaces, ...topLevelNavigation],
26
+ [topLevelActions, topLevelCollections, topLevelWorkspaces, topLevelNavigation],
49
27
  );
50
- };
51
-
52
- type NavTreeColumnsProps = {
53
- path: string[];
54
- node: Node;
55
- getActions: (node: Node) => FlattenedActions;
56
- open: boolean;
57
- loadDescendents?: (node: Node) => MaybePromise<void>;
58
- renderItemEnd?: FC<{ node: Node; open: boolean }>;
59
- popoverAnchorId?: string;
60
- };
61
-
62
- const NavTreeColumns = ({
63
- path,
64
- node,
65
- getActions,
66
- open,
67
- loadDescendents,
68
- renderItemEnd: ItemEnd,
69
- popoverAnchorId,
70
- }: NavTreeColumnsProps) => {
71
- const { t } = useTranslation(NAVTREE_PLUGIN);
72
-
73
- const level = path.length - 2;
74
-
75
- const { actions: _actions, groupedActions } = getActions(node);
76
- const [primaryAction, ...secondaryActions] = _actions.toSorted((a, b) =>
77
- a.properties?.disposition === 'toolbar' ? -1 : 1,
78
- );
79
-
80
- const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions : _actions)
81
- .flatMap((action) => (isAction(action) ? [action] : []))
82
- .filter((action) => !action.properties?.hidden);
83
-
84
- const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${node.id}` ? Popover.Anchor : Fragment;
28
+ const pinnedItems = getItems(root, 'pin-end').toSorted((a, b) => byPosition(a.properties, b.properties));
29
+ const topLevelItems = useMemo(() => [...l0Items, ...pinnedItems], [l0Items, pinnedItems]);
85
30
 
86
- // TODO(thure): Ideally this should not be necessary.
87
- useEffect(() => {
88
- const frame = requestAnimationFrame(() => {
89
- void loadDescendents?.(node);
90
- if (primaryAction && !isAction(primaryAction)) {
91
- void loadDescendents?.(primaryAction);
92
- }
93
- });
94
- return () => cancelAnimationFrame(frame);
95
- }, [primaryAction]);
31
+ useLoadDescendents(root);
32
+ const path = useMemo(() => [id], [id]);
96
33
 
97
34
  return (
98
- <>
99
- {primaryAction?.properties?.disposition === 'toolbar' ? (
100
- <NavTreeItemAction
101
- testId={primaryAction.properties?.testId}
102
- label={toLocalizedString(primaryAction.properties?.label, t)}
103
- icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
104
- parent={node}
105
- monolithic={isAction(primaryAction)}
106
- menuActions={isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction.id]}
107
- menuType={primaryAction.properties?.menuType}
108
- caller={NAV_TREE_ITEM}
109
- />
110
- ) : (
111
- <Treegrid.Cell />
112
- )}
113
- <ActionRoot>
114
- {actions.length > 0 ? (
115
- <NavTreeItemAction
116
- testId={`navtree.treeItem.actionsLevel${level}`}
117
- label={t('tree item actions label')}
118
- icon='ph--dots-three-vertical--regular'
119
- parent={node}
120
- menuActions={actions}
121
- menuType='dropdown'
122
- caller={NAV_TREE_ITEM}
123
- />
124
- ) : (
125
- <Treegrid.Cell />
126
- )}
127
- </ActionRoot>
128
- {ItemEnd && <ItemEnd node={node} open={open} />}
129
- </>
35
+ // NOTE(thure): 74px (rather than rem) is intentional in order to match the size of macOS windowing controls
36
+ <Tabs.Root value={tab} orientation='vertical' verticalVariant='stateless' classNames='relative'>
37
+ <L0Menu topLevelItems={l0Items} pinnedItems={pinnedItems} path={path} parent={root} />
38
+ <L1Panels topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} />
39
+ </Tabs.Root>
130
40
  );
131
41
  };
@@ -5,19 +5,28 @@
5
5
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
6
  import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
7
7
  import { untracked } from '@preact/signals-core';
8
- import React, { memo, useCallback, useEffect } from 'react';
8
+ import React, { memo, useCallback, useEffect, useMemo } from 'react';
9
9
 
10
- import { createIntent, LayoutAction, NavigationAction, useIntentDispatcher, Surface } from '@dxos/app-framework';
10
+ import {
11
+ createIntent,
12
+ LayoutAction,
13
+ Surface,
14
+ useAppGraph,
15
+ useCapability,
16
+ useIntentDispatcher,
17
+ useLayout,
18
+ } from '@dxos/app-framework';
11
19
  import { isAction, isActionLike, type Node } from '@dxos/app-graph';
12
- import { useGraph } from '@dxos/plugin-graph';
13
20
  import { isEchoObject, isSpace } from '@dxos/react-client/echo';
14
- import { useMediaQuery, useSidebars } from '@dxos/react-ui';
21
+ import { useMediaQuery } from '@dxos/react-ui';
15
22
  import { isTreeData, type TreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
16
23
  import { mx } from '@dxos/react-ui-theme';
17
24
  import { arrayMove } from '@dxos/util';
18
25
 
19
- import { NAV_TREE_ITEM, NavTree, type NavTreeProps } from './NavTree';
20
- import { NAVTREE_PLUGIN } from '../meta';
26
+ import { NAV_TREE_ITEM, NavTree } from './NavTree';
27
+ import { NavTreeContext } from './NavTreeContext';
28
+ import { type NavTreeContextValue } from './types';
29
+ import { NavTreeCapabilities } from '../capabilities';
21
30
  import { type NavTreeItemGraphNode } from '../types';
22
31
  import {
23
32
  expandActions,
@@ -26,6 +35,7 @@ import {
26
35
  getChildren,
27
36
  getParent,
28
37
  resolveMigrationOperation,
38
+ expandChildrenAndActions,
29
39
  } from '../util';
30
40
 
31
41
  // TODO(thure): Is NavTree truly authoritative in this regard?
@@ -37,24 +47,29 @@ const renderItemEnd = ({ node, open }: { node: Node; open: boolean }) => (
37
47
 
38
48
  export type NavTreeContainerProps = {
39
49
  popoverAnchorId?: string;
40
- } & Pick<NavTreeProps, 'isOpen' | 'isCurrent' | 'onOpenChange'>;
50
+ topbar?: boolean;
51
+ } & Pick<NavTreeContextValue, 'tab'>;
41
52
 
42
- export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }: NavTreeContainerProps) => {
43
- const { closeNavigationSidebar } = useSidebars(NAVTREE_PLUGIN);
53
+ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeContainerProps) => {
44
54
  const [isLg] = useMediaQuery('lg', { ssr: false });
45
55
  const { dispatchPromise: dispatch } = useIntentDispatcher();
46
- const { graph } = useGraph();
56
+ const { graph } = useAppGraph();
57
+ const layout = useLayout();
58
+ const { isOpen, isCurrent, setItem } = useCapability(NavTreeCapabilities.State);
47
59
 
48
60
  const getActions = useCallback((node: Node) => naturalGetActions(graph, node), [graph]);
49
61
 
50
62
  const getItems = useCallback(
51
- (node?: Node) => {
63
+ (node?: Node, disposition?: string) => {
52
64
  return graph.nodes(node ?? graph.root, {
53
65
  filter: (node): node is Node => {
54
66
  return untracked(() => {
55
- const action = isActionLike(node);
56
- const disposition = node.properties.disposition;
57
- return !action || (action && disposition === 'item');
67
+ const action = isAction(node);
68
+ if (!disposition) {
69
+ return !action || node.properties.disposition === 'item';
70
+ }
71
+
72
+ return node.properties.disposition === disposition;
58
73
  });
59
74
  },
60
75
  });
@@ -96,6 +111,33 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
96
111
  [graph],
97
112
  );
98
113
 
114
+ const onOpenChange = useCallback(
115
+ ({ item: { id }, path, open }: { item: Node; path: string[]; open: boolean }) => {
116
+ // TODO(thure): This might become a localstorage leak; openItemIds that no longer exist should be removed from this map.
117
+ setItem(path, 'open', open);
118
+
119
+ if (graph) {
120
+ const node = graph.findNode(id);
121
+ return node && expandChildrenAndActions(graph, node as NavTreeItemGraphNode);
122
+ }
123
+ },
124
+ [graph],
125
+ );
126
+
127
+ const onTabChange = useCallback(
128
+ async (node: NavTreeItemGraphNode) => {
129
+ await dispatch(createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: node.id }));
130
+ // Open the first item if the workspace is empty.
131
+ if (layout.active.length === 0) {
132
+ const [item] = getItems(node).filter((node) => !isActionLike(node));
133
+ if (item && item.data) {
134
+ await dispatch(createIntent(LayoutAction.Open, { part: 'main', subject: [item.id] }));
135
+ }
136
+ }
137
+ },
138
+ [dispatch, layout],
139
+ );
140
+
99
141
  const canDrop = useCallback((source: TreeData, target: TreeData) => {
100
142
  const sourceNode = source.item as Node;
101
143
  const targetNode = target.item as Node;
@@ -123,11 +165,19 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
123
165
 
124
166
  const current = isCurrent(path, node);
125
167
  if (!current) {
126
- void dispatch(createIntent(NavigationAction.Open, { activeParts: { main: [node.id] } }));
168
+ void dispatch(
169
+ createIntent(LayoutAction.Open, {
170
+ part: 'main',
171
+ subject: [node.id],
172
+ options: { key: node.properties.key },
173
+ }),
174
+ );
127
175
  } else if (option) {
128
- void dispatch(createIntent(NavigationAction.Close, { activeParts: { main: [node.id] } }));
176
+ void dispatch(
177
+ createIntent(LayoutAction.Close, { part: 'main', subject: [node.id], options: { state: false } }),
178
+ );
129
179
  } else {
130
- void dispatch(createIntent(LayoutAction.ScrollIntoView, { id: node.id }));
180
+ void dispatch(createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: node.id }));
131
181
  }
132
182
 
133
183
  const defaultAction = graph.actions(node).find((action) => action.properties?.disposition === 'default');
@@ -136,10 +186,15 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
136
186
  }
137
187
 
138
188
  if (!isLg) {
139
- closeNavigationSidebar();
189
+ void dispatch(createIntent(LayoutAction.UpdateSidebar, { part: 'sidebar', options: { state: 'closed' } }));
140
190
  }
141
191
  },
142
- [graph, dispatch, isCurrent, isLg, closeNavigationSidebar],
192
+ [graph, dispatch, isCurrent, isLg],
193
+ );
194
+
195
+ const onBack = useCallback(
196
+ () => dispatch(createIntent(LayoutAction.RevertWorkspace, { part: 'workspace', options: { revert: true } })),
197
+ [dispatch],
143
198
  );
144
199
 
145
200
  // TODO(wittjosiah): Factor out hook.
@@ -201,20 +256,46 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
201
256
  });
202
257
  }, [graph]);
203
258
 
204
- // TODO(thure): What gives this an inline `overflow: initial`?
259
+ const navTreeContextValue = useMemo(
260
+ () => ({
261
+ tab,
262
+ onTabChange,
263
+ onBack,
264
+ getActions,
265
+ loadDescendents,
266
+ renderItemEnd,
267
+ popoverAnchorId,
268
+ topbar,
269
+ getItems,
270
+ getProps,
271
+ isCurrent,
272
+ isOpen,
273
+ onOpenChange,
274
+ canDrop,
275
+ onSelect: handleSelect,
276
+ }),
277
+ [
278
+ tab,
279
+ onTabChange,
280
+ onBack,
281
+ getActions,
282
+ loadDescendents,
283
+ renderItemEnd,
284
+ popoverAnchorId,
285
+ topbar,
286
+ getItems,
287
+ getProps,
288
+ isCurrent,
289
+ isOpen,
290
+ onOpenChange,
291
+ canDrop,
292
+ handleSelect,
293
+ ],
294
+ );
295
+
205
296
  return (
206
- <NavTree
207
- id={graph.root.id}
208
- getActions={getActions}
209
- getItems={getItems}
210
- getProps={getProps}
211
- isCurrent={isCurrent}
212
- loadDescendents={loadDescendents}
213
- renderItemEnd={renderItemEnd}
214
- popoverAnchorId={popoverAnchorId}
215
- canDrop={canDrop}
216
- onSelect={handleSelect}
217
- {...props}
218
- />
297
+ <NavTreeContext.Provider value={navTreeContextValue}>
298
+ <NavTree root={graph.root} id={graph.root.id} />
299
+ </NavTreeContext.Provider>
219
300
  );
220
301
  });
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext, useContext } from 'react';
6
+
7
+ import { raise } from '@dxos/debug';
8
+
9
+ import { type NavTreeContextValue } from './types';
10
+
11
+ export const NavTreeContext = createContext<NavTreeContextValue | null>(null);
12
+
13
+ export const useNavTreeContext = () => useContext(NavTreeContext) ?? raise(new Error('NavTreeContext not found'));
@@ -24,11 +24,8 @@ export type NavTreeItemActionMenuProps = ActionProperties & {
24
24
  const fallbackIcon = 'ph--placeholder--regular';
25
25
 
26
26
  const actionButtonProps = {
27
- iconOnly: true,
28
27
  size: 4 as const,
29
- variant: 'ghost' as const,
30
28
  density: 'fine' as const,
31
- classNames: mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem),
32
29
  };
33
30
 
34
31
  export const NavTreeItemActionDropdownMenu = ({
@@ -46,7 +43,10 @@ export const NavTreeItemActionDropdownMenu = ({
46
43
  <DropdownMenu.Trigger asChild>
47
44
  <IconButton
48
45
  {...actionButtonProps}
46
+ classNames={mx('shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem)}
47
+ variant='ghost'
49
48
  icon={icon ?? fallbackIcon}
49
+ iconOnly
50
50
  label={toLocalizedString(label, t)}
51
51
  data-testid={testId}
52
52
  suppressNextTooltip={suppressNextTooltip}
@@ -59,14 +59,22 @@ export const NavTreeItemActionDropdownMenu = ({
59
59
 
60
60
  export const NavTreeItemMonolithicAction = ({
61
61
  parent,
62
- properties: { disabled, caller, testId, icon } = { label: 'never' },
62
+ properties: { disabled, caller, testId, icon, variant = 'ghost', iconOnly = true } = { label: 'never' },
63
63
  data: invoke,
64
64
  baseLabel,
65
65
  }: Action & { parent: Node; onAction?: (action: Action) => void; baseLabel: string }) => {
66
66
  return (
67
67
  <IconButton
68
68
  {...actionButtonProps}
69
+ variant={variant}
70
+ classNames={mx(
71
+ 'shrink-0',
72
+ hoverableControlItem,
73
+ hoverableOpenControlItem,
74
+ iconOnly ? 'pli-2 pointer-fine:pli-1' : 'p-2 pointer-fine:p-2 mie-1',
75
+ )}
69
76
  icon={icon ?? fallbackIcon}
77
+ iconOnly={iconOnly}
70
78
  label={baseLabel}
71
79
  disabled={disabled}
72
80
  onClick={(event) => {
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { Fragment, memo } from 'react';
6
+
7
+ import { isAction } from '@dxos/app-graph';
8
+ import { Popover, toLocalizedString, Treegrid, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { NAV_TREE_ITEM } from './NavTree';
11
+ import { useNavTreeContext } from './NavTreeContext';
12
+ import { NavTreeItemAction } from './NavTreeItemAction';
13
+ import type { NavTreeItemColumnsProps } from './types';
14
+ import { useLoadDescendents } from '../hooks';
15
+ import { NAVTREE_PLUGIN } from '../meta';
16
+
17
+ export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumnsProps) => {
18
+ const { t } = useTranslation(NAVTREE_PLUGIN);
19
+ const { getActions, renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
20
+
21
+ const level = path.length - 2;
22
+
23
+ const { actions: _actions, groupedActions } = getActions(item);
24
+ const [primaryAction, ...secondaryActions] = _actions.toSorted((a, b) =>
25
+ a.properties?.disposition === 'toolbar' ? -1 : 1,
26
+ );
27
+
28
+ const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions : _actions)
29
+ .flatMap((action) => (isAction(action) ? [action] : []))
30
+ .filter((action) => !action.properties?.hidden);
31
+
32
+ const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${item.id}` ? Popover.Anchor : Fragment;
33
+
34
+ useLoadDescendents(item);
35
+ useLoadDescendents(primaryAction && !isAction(primaryAction) ? primaryAction : undefined);
36
+
37
+ return (
38
+ <div role='none' className='contents app-no-drag'>
39
+ {primaryAction?.properties?.disposition === 'toolbar' && !primaryAction?.properties?.disabled ? (
40
+ <NavTreeItemAction
41
+ testId={primaryAction.properties?.testId}
42
+ label={toLocalizedString(primaryAction.properties?.label, t)}
43
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
44
+ parent={item}
45
+ monolithic={isAction(primaryAction)}
46
+ menuActions={isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction.id]}
47
+ menuType={primaryAction.properties?.menuType}
48
+ caller={NAV_TREE_ITEM}
49
+ />
50
+ ) : (
51
+ <Treegrid.Cell />
52
+ )}
53
+ <ActionRoot>
54
+ {actions.length > 0 ? (
55
+ <NavTreeItemAction
56
+ testId={`navtree.treeItem.actionsLevel${level}`}
57
+ label={t('tree item actions label')}
58
+ icon='ph--dots-three-vertical--regular'
59
+ parent={item}
60
+ menuActions={actions}
61
+ menuType='dropdown'
62
+ caller={NAV_TREE_ITEM}
63
+ />
64
+ ) : (
65
+ <Treegrid.Cell />
66
+ )}
67
+ </ActionRoot>
68
+ {ItemEnd && <ItemEnd node={item} open={open} />}
69
+ </div>
70
+ );
71
+ });
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { createIntent, useIntentDispatcher, useCapabilities } from '@dxos/app-framework';
7
+ import { createIntent, useCapabilities, useIntentDispatcher } from '@dxos/app-framework';
8
8
  import { ClientAction } from '@dxos/plugin-client/types';
9
9
  import { ObservabilityCapabilities } from '@dxos/plugin-observability';
10
10
  import { useIdentity } from '@dxos/react-client/halo';
@@ -19,7 +19,7 @@ export const NotchStart = () => {
19
19
  const { dispatchPromise: dispatch } = useIntentDispatcher();
20
20
  return (
21
21
  <HaloButton
22
- size={8}
22
+ size={12}
23
23
  identityKey={identity?.identityKey.toHex()}
24
24
  hue={identity?.profile?.data?.hue}
25
25
  emoji={identity?.profile?.data?.emoji}
@@ -6,5 +6,7 @@ export * from './CommandsDialog';
6
6
  export * from './CommandsTrigger';
7
7
  export * from './NavTree';
8
8
  export * from './NavTreeContainer';
9
+ export * from './NavTreeContext';
9
10
  export * from './NavTreeDocumentTitle';
10
11
  export * from './NotchStart';
12
+ export * from './types';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type { FC } from 'react';
6
+
7
+ import type { Node } from '@dxos/app-graph';
8
+ import type { TreeProps } from '@dxos/react-ui-list';
9
+ import type { MaybePromise } from '@dxos/util';
10
+
11
+ import { type L1PanelProps } from './L1Panels';
12
+ import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
13
+
14
+ export type NavTreeItemColumnsProps = {
15
+ path: string[];
16
+ item: Node;
17
+ open: boolean;
18
+ };
19
+
20
+ export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'>;
21
+
22
+ export type NavTreeContextValue = Pick<
23
+ TreeProps<NavTreeItemGraphNode>,
24
+ 'getProps' | 'isCurrent' | 'isOpen' | 'onOpenChange' | 'canDrop' | 'onSelect'
25
+ > &
26
+ Pick<L1PanelProps, 'onBack'> & {
27
+ tab: string;
28
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
29
+ getItems: (node?: NavTreeItemGraphNode, disposition?: string) => NavTreeItemGraphNode[];
30
+ getActions: (node: Node) => FlattenedActions;
31
+ loadDescendents?: (node: Node) => MaybePromise<void>;
32
+ renderItemEnd?: FC<{ node: Node; open: boolean }>;
33
+ popoverAnchorId?: string;
34
+ topbar?: boolean;
35
+ };
package/src/hooks.ts ADDED
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect } from 'react';
6
+
7
+ import { type Node } from '@dxos/app-graph';
8
+
9
+ import { useNavTreeContext } from './components';
10
+
11
+ export const useLoadDescendents = (root?: Node) => {
12
+ const { loadDescendents } = useNavTreeContext();
13
+ useEffect(() => {
14
+ const frame = requestAnimationFrame(() => {
15
+ if (root) {
16
+ void loadDescendents?.(root);
17
+ }
18
+ });
19
+ return () => cancelAnimationFrame(frame);
20
+ }, [root]);
21
+ };
@@ -15,10 +15,10 @@ import { registerSignalsRuntime } from '@dxos/echo-signals/react';
15
15
  import { create, type ReactiveObject } from '@dxos/live-object';
16
16
  import { faker } from '@dxos/random';
17
17
  import { isTreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
18
- import { Path } from '@dxos/react-ui-mosaic';
18
+ import { Path } from '@dxos/react-ui-list';
19
19
  import { withLayout, withTheme } from '@dxos/storybook-utils';
20
20
 
21
- import { NavTree } from '../components';
21
+ import { StorybookNavTree } from './StorybookNavTree';
22
22
  import { getActions, getChildren } from '../util';
23
23
 
24
24
  faker.seed(3);
@@ -53,9 +53,9 @@ const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }
53
53
 
54
54
  export const Default = {};
55
55
 
56
- const meta: Meta<typeof NavTree> = {
56
+ const meta: Meta<typeof StorybookNavTree> = {
57
57
  title: 'plugins/plugin-navtree/Graph',
58
- component: NavTree,
58
+ component: StorybookNavTree,
59
59
  decorators: [withTheme, withLayout({ tooltips: true })],
60
60
  render: (args) => {
61
61
  useEffect(() => {
@@ -78,9 +78,10 @@ const meta: Meta<typeof NavTree> = {
78
78
  });
79
79
  }, []);
80
80
 
81
- return <NavTree {...args} />;
81
+ return <StorybookNavTree {...args} />;
82
82
  },
83
83
  args: {
84
+ root: graph.root,
84
85
  id: graph.root.id,
85
86
  getActions: (node: Node) => getActions(graph, node),
86
87
  getItems: (node?: Node) => graph.nodes(node ?? graph.root),