@dxos/plugin-navtree 0.7.5-labs.a279d8c → 0.7.5-labs.ea4b4c2

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 (98) 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-54MPRXMH.mjs +1109 -0
  4. package/dist/lib/browser/chunk-54MPRXMH.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 +21 -26
  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-LNRTNSM6.mjs +57 -0
  13. package/dist/lib/browser/react-surface-LNRTNSM6.mjs.map +7 -0
  14. package/dist/lib/browser/{state-HT65TAYL.mjs → state-65TQKYX7.mjs} +6 -9
  15. package/dist/lib/browser/state-65TQKYX7.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 +22 -22
  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 +88 -32
  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.map +1 -1
  28. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -1
  29. package/dist/types/src/components/L0Menu.d.ts +9 -0
  30. package/dist/types/src/components/L0Menu.d.ts.map +1 -0
  31. package/dist/types/src/components/L1Panels.d.ts +8 -0
  32. package/dist/types/src/components/L1Panels.d.ts.map +1 -0
  33. package/dist/types/src/components/NavTree.d.ts +3 -20
  34. package/dist/types/src/components/NavTree.d.ts.map +1 -1
  35. package/dist/types/src/components/NavTree.stories.d.ts +2 -2
  36. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/NavTreeContainer.d.ts +4 -3
  38. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  39. package/dist/types/src/components/NavTreeContext.d.ts +4 -0
  40. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -0
  41. package/dist/types/src/components/NavTreeItemAction.d.ts +1 -1
  42. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -1
  43. package/dist/types/src/components/NavTreeItemColumns.d.ts +4 -0
  44. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +1 -0
  45. package/dist/types/src/components/index.d.ts +2 -0
  46. package/dist/types/src/components/index.d.ts.map +1 -1
  47. package/dist/types/src/components/types.d.ts +25 -0
  48. package/dist/types/src/components/types.d.ts.map +1 -0
  49. package/dist/types/src/hooks.d.ts +3 -0
  50. package/dist/types/src/hooks.d.ts.map +1 -0
  51. package/dist/types/src/stories/Graph.stories.d.ts +2 -2
  52. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
  53. package/dist/types/src/stories/StorybookNavTree.d.ts +4 -0
  54. package/dist/types/src/stories/StorybookNavTree.d.ts.map +1 -0
  55. package/dist/types/src/translations.d.ts +0 -6
  56. package/dist/types/src/translations.d.ts.map +1 -1
  57. package/dist/types/src/types.d.ts +2 -1
  58. package/dist/types/src/types.d.ts.map +1 -1
  59. package/dist/types/src/util.d.ts +9 -0
  60. package/dist/types/src/util.d.ts.map +1 -1
  61. package/dist/types/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +33 -31
  63. package/src/NavTreePlugin.tsx +8 -10
  64. package/src/capabilities/app-graph-builder.ts +6 -4
  65. package/src/capabilities/capabilities.ts +3 -2
  66. package/src/capabilities/intent-resolver.ts +22 -16
  67. package/src/capabilities/react-surface.tsx +15 -41
  68. package/src/capabilities/state.ts +5 -8
  69. package/src/components/CommandsDialog.tsx +3 -4
  70. package/src/components/CommandsTrigger.tsx +7 -5
  71. package/src/components/L0Menu.tsx +210 -0
  72. package/src/components/L1Panels.tsx +100 -0
  73. package/src/components/NavTree.stories.tsx +5 -5
  74. package/src/components/NavTree.tsx +26 -116
  75. package/src/components/NavTreeContainer.tsx +106 -32
  76. package/src/components/NavTreeContext.tsx +13 -0
  77. package/src/components/NavTreeItemAction.tsx +12 -4
  78. package/src/components/NavTreeItemColumns.tsx +71 -0
  79. package/src/components/NotchStart.tsx +2 -2
  80. package/src/components/index.ts +2 -0
  81. package/src/components/types.ts +33 -0
  82. package/src/hooks.ts +21 -0
  83. package/src/stories/Graph.stories.tsx +5 -4
  84. package/src/stories/StorybookNavTree.tsx +63 -0
  85. package/src/translations.ts +0 -6
  86. package/src/types.ts +2 -1
  87. package/src/util.ts +54 -0
  88. package/dist/lib/browser/chunk-IMGT2LAL.mjs +0 -658
  89. package/dist/lib/browser/chunk-IMGT2LAL.mjs.map +0 -7
  90. package/dist/lib/browser/chunk-OI7AAA6Q.mjs.map +0 -7
  91. package/dist/lib/browser/intent-resolver-74JPWYBP.mjs +0 -42
  92. package/dist/lib/browser/intent-resolver-74JPWYBP.mjs.map +0 -7
  93. package/dist/lib/browser/react-surface-6QWXI53A.mjs +0 -189
  94. package/dist/lib/browser/react-surface-6QWXI53A.mjs.map +0 -7
  95. package/dist/lib/browser/state-HT65TAYL.mjs.map +0 -7
  96. package/dist/types/src/components/NavTreeFooter.d.ts +0 -6
  97. package/dist/types/src/components/NavTreeFooter.d.ts.map +0 -1
  98. package/src/components/NavTreeFooter.tsx +0 -161
@@ -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
67
  const action = isActionLike(node);
56
- const disposition = node.properties.disposition;
57
- return !action || (action && disposition === 'item');
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) {
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,10 @@ 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],
143
193
  );
144
194
 
145
195
  // TODO(wittjosiah): Factor out hook.
@@ -201,20 +251,44 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
201
251
  });
202
252
  }, [graph]);
203
253
 
204
- // TODO(thure): What gives this an inline `overflow: initial`?
254
+ const navTreeContextValue = useMemo(
255
+ () => ({
256
+ tab,
257
+ onTabChange,
258
+ getActions,
259
+ loadDescendents,
260
+ renderItemEnd,
261
+ popoverAnchorId,
262
+ topbar,
263
+ getItems,
264
+ getProps,
265
+ isCurrent,
266
+ isOpen,
267
+ onOpenChange,
268
+ canDrop,
269
+ onSelect: handleSelect,
270
+ }),
271
+ [
272
+ tab,
273
+ onTabChange,
274
+ getActions,
275
+ loadDescendents,
276
+ renderItemEnd,
277
+ popoverAnchorId,
278
+ topbar,
279
+ getItems,
280
+ getProps,
281
+ isCurrent,
282
+ isOpen,
283
+ onOpenChange,
284
+ canDrop,
285
+ handleSelect,
286
+ ],
287
+ );
288
+
205
289
  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
- />
290
+ <NavTreeContext.Provider value={navTreeContextValue}>
291
+ <NavTree root={graph.root} id={graph.root.id} />
292
+ </NavTreeContext.Provider>
219
293
  );
220
294
  });
@@ -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,33 @@
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 { FlattenedActions, NavTreeItemGraphNode } from '../types';
12
+
13
+ export type NavTreeItemColumnsProps = {
14
+ path: string[];
15
+ item: Node;
16
+ open: boolean;
17
+ };
18
+
19
+ export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'>;
20
+
21
+ export type NavTreeContextValue = Pick<
22
+ TreeProps<NavTreeItemGraphNode>,
23
+ 'getProps' | 'isCurrent' | 'isOpen' | 'onOpenChange' | 'canDrop' | 'onSelect'
24
+ > & {
25
+ tab: string;
26
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
27
+ getItems: (node?: NavTreeItemGraphNode, disposition?: string) => NavTreeItemGraphNode[];
28
+ getActions: (node: Node) => FlattenedActions;
29
+ loadDescendents?: (node: Node) => MaybePromise<void>;
30
+ renderItemEnd?: FC<{ node: Node; open: boolean }>;
31
+ popoverAnchorId?: string;
32
+ topbar?: boolean;
33
+ };
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/NavTreeContext';
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
+ };
@@ -18,7 +18,7 @@ import { isTreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
18
18
  import { Path } from '@dxos/react-ui-mosaic';
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),
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo, useState } from 'react';
6
+
7
+ import { NavTree, NavTreeContext, type NavTreeContextValue, type NavTreeProps } from '../components';
8
+ import { type NavTreeItemGraphNode } from '../types';
9
+
10
+ export const StorybookNavTree = ({
11
+ getActions,
12
+ loadDescendents,
13
+ renderItemEnd,
14
+ popoverAnchorId,
15
+ getItems,
16
+ getProps,
17
+ isCurrent,
18
+ canDrop,
19
+ onSelect,
20
+ isOpen,
21
+ onOpenChange,
22
+ ...props
23
+ }: NavTreeProps & NavTreeContextValue) => {
24
+ const [tab, setTab] = useState('never');
25
+
26
+ const contextValue = useMemo(
27
+ () => ({
28
+ tab,
29
+ onTabChange: (node: NavTreeItemGraphNode) => setTab(node.id),
30
+ getActions,
31
+ loadDescendents,
32
+ renderItemEnd,
33
+ popoverAnchorId,
34
+ getItems,
35
+ getProps,
36
+ isCurrent,
37
+ isOpen,
38
+ onOpenChange,
39
+ canDrop,
40
+ onSelect,
41
+ }),
42
+ [
43
+ tab,
44
+ setTab,
45
+ getActions,
46
+ loadDescendents,
47
+ renderItemEnd,
48
+ popoverAnchorId,
49
+ getItems,
50
+ getProps,
51
+ isCurrent,
52
+ isOpen,
53
+ onOpenChange,
54
+ canDrop,
55
+ onSelect,
56
+ ],
57
+ );
58
+ return (
59
+ <NavTreeContext.Provider value={contextValue}>
60
+ <NavTree {...props} />
61
+ </NavTreeContext.Provider>
62
+ );
63
+ };
@@ -13,12 +13,6 @@ export default [
13
13
  'commands dialog title': 'Commands',
14
14
  'command list input placeholder': 'Search for commands…',
15
15
  'node actions menu invoker label': 'More options',
16
- 'warning title': 'WARNING',
17
- 'technology preview message': 'Composer is still in technology preview.',
18
- 'learn more label': 'Learn more',
19
- 'released message': 'This version released {{released}}.',
20
- 'see release label': 'See release on GitHub',
21
- 'powered by dxos message': 'Powered by <dxos>DXOS</dxos>',
22
16
  'tree item actions label': 'More actions',
23
17
  'empty branch message': ' ',
24
18
  },
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type Action, type ActionLike, type Node } from '@dxos/app-graph';
6
6
  import { type Label } from '@dxos/react-ui';
7
- import { type MaybePromise } from '@dxos/util';
7
+ import { type MaybePromise, type Position } from '@dxos/util';
8
8
 
9
9
  export type NavTreeItemGraphNode = Node<
10
10
  any,
@@ -31,6 +31,7 @@ type SharedProperties = {
31
31
  label: Label;
32
32
  icon?: string;
33
33
  disabled?: boolean;
34
+ position?: Position;
34
35
  testId?: string;
35
36
  className?: string;
36
37
  headingClassName?: string;
package/src/util.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  ACTION_TYPE,
11
11
  ACTION_GROUP_TYPE,
12
12
  isAction,
13
+ isActionLike,
13
14
  } from '@dxos/app-graph';
14
15
  import { nonNullable } from '@dxos/util';
15
16
 
@@ -126,3 +127,56 @@ export const expandChildren = (graph: Graph, node: Node) => {
126
127
  export const expandActions = (graph: Graph, node: Node) => {
127
128
  return Promise.all([graph.expand(node, 'outbound', ACTION_TYPE), graph.expand(node, 'outbound', ACTION_GROUP_TYPE)]);
128
129
  };
130
+
131
+ export const l0ItemType = (item: Node<any>) => {
132
+ if (item.properties.disposition === 'collection') {
133
+ return 'collection';
134
+ } else if (isActionLike(item)) {
135
+ return 'action';
136
+ } else if (item.properties.disposition === 'navigation') {
137
+ return 'link';
138
+ } else {
139
+ return 'tab';
140
+ }
141
+ };
142
+
143
+ // Regular expression to match renderable characters
144
+ // Excludes control characters, combining marks, and other non-renderable characters
145
+ const renderableCharRegex =
146
+ /^(?![\p{Control}\p{Mark}\p{Separator}\p{Surrogate}\p{Unassigned}])[\p{L}\p{N}\p{P}\p{S}\p{Emoji}]$/u;
147
+
148
+ /**
149
+ * Returns the first two renderable characters from a string that are separated by non-word characters.
150
+ * Handles Unicode characters correctly.
151
+ *
152
+ * @param {string} label - The input string to process
153
+ * @returns {[string, string]} Array containing the two characters, or empty strings if not found
154
+ */
155
+ export const getFirstTwoRenderableChars = (label: string) => {
156
+ // Convert string to array of Unicode characters
157
+ const characters = Array.from(label);
158
+
159
+ // Keep track of found renderable characters
160
+ const result = ['', ''];
161
+ let foundFirst = false;
162
+
163
+ for (let i = 0; i < characters.length; i++) {
164
+ const char = characters[i];
165
+
166
+ if (renderableCharRegex.test(char)) {
167
+ if (!foundFirst) {
168
+ result[0] = char;
169
+ foundFirst = true;
170
+ } else {
171
+ // Check if there's at least one non-word character between the first and current char
172
+ const textBetween = characters.slice(result[0].length, i).join('');
173
+ if (/[^\p{L}\p{N}_]/u.test(textBetween)) {
174
+ result[1] = char;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ return result;
182
+ };