@dxos/plugin-navtree 0.8.4-main.406dc2a → 0.8.4-main.52d7546f51

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 (170) hide show
  1. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs +44 -0
  2. package/dist/lib/browser/app-graph-builder-XGA4GPGL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-63YLDLLE.mjs +1373 -0
  4. package/dist/lib/browser/chunk-63YLDLLE.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-X2HDG62A.mjs +23 -0
  6. package/dist/lib/browser/chunk-X2HDG62A.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-YKTYPINZ.mjs +19 -0
  8. package/dist/lib/browser/chunk-YKTYPINZ.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +83 -91
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/{keyboard-AHSAWKXA.mjs → keyboard-46XQHY2O.mjs} +21 -19
  12. package/dist/lib/browser/keyboard-46XQHY2O.mjs.map +7 -0
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/operation-resolver-NNNDII45.mjs +53 -0
  15. package/dist/lib/browser/operation-resolver-NNNDII45.mjs.map +7 -0
  16. package/dist/lib/browser/react-surface-APSEG23H.mjs +59 -0
  17. package/dist/lib/browser/react-surface-APSEG23H.mjs.map +7 -0
  18. package/dist/lib/browser/state-M5YUCIQU.mjs +121 -0
  19. package/dist/lib/browser/state-M5YUCIQU.mjs.map +7 -0
  20. package/dist/types/src/NavTreePlugin.d.ts +2 -1
  21. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  22. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  23. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  25. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  26. package/dist/types/src/capabilities/index.d.ts +5 -108
  27. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  28. package/dist/types/src/capabilities/keyboard/index.d.ts +3 -0
  29. package/dist/types/src/capabilities/keyboard/index.d.ts.map +1 -0
  30. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +5 -0
  31. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  33. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  35. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  36. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  37. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  38. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  39. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/state/index.d.ts +3 -0
  41. package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/state/state.d.ts +6 -0
  43. package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
  44. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +3 -2
  45. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -1
  46. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -1
  47. package/dist/types/src/components/NavTree/NavTree.d.ts +30 -1
  48. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  49. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +4 -1
  50. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/NavTreeContainer.d.ts +8 -1
  52. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  53. package/dist/types/src/components/NavTreeDocumentTitle.d.ts +1 -1
  54. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -1
  55. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +6 -6
  56. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  57. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +2 -1
  58. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Sidebar/L0Menu.d.ts +4 -4
  60. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  61. package/dist/types/src/components/Sidebar/L1Panel.d.ts +2 -2
  62. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  63. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +2 -2
  64. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/components/types.d.ts +7 -7
  67. package/dist/types/src/components/types.d.ts.map +1 -1
  68. package/dist/types/src/experimental/Tree.stories.d.ts +15 -4
  69. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  70. package/dist/types/src/hooks.d.ts +32 -1
  71. package/dist/types/src/hooks.d.ts.map +1 -1
  72. package/dist/types/src/index.d.ts +1 -2
  73. package/dist/types/src/index.d.ts.map +1 -1
  74. package/dist/types/src/meta.d.ts +2 -2
  75. package/dist/types/src/meta.d.ts.map +1 -1
  76. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  77. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  78. package/dist/types/src/testing/generator.d.ts +5 -6
  79. package/dist/types/src/testing/generator.d.ts.map +1 -1
  80. package/dist/types/src/testing/update-state.d.ts +2 -2
  81. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  82. package/dist/types/src/types/capabilities.d.ts +16 -0
  83. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  84. package/dist/types/src/types/events.d.ts +5 -0
  85. package/dist/types/src/types/events.d.ts.map +1 -0
  86. package/dist/types/src/types/index.d.ts +4 -0
  87. package/dist/types/src/types/index.d.ts.map +1 -0
  88. package/dist/types/src/{types.d.ts → types/schema.d.ts} +7 -7
  89. package/dist/types/src/types/schema.d.ts.map +1 -0
  90. package/dist/types/src/util.d.ts +7 -7
  91. package/dist/types/src/util.d.ts.map +1 -1
  92. package/dist/types/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +54 -52
  94. package/src/NavTreePlugin.tsx +53 -72
  95. package/src/capabilities/app-graph-builder/app-graph-builder.ts +42 -0
  96. package/src/capabilities/app-graph-builder/index.ts +7 -0
  97. package/src/capabilities/index.ts +5 -9
  98. package/src/capabilities/keyboard/index.ts +7 -0
  99. package/src/capabilities/keyboard/keyboard.ts +66 -0
  100. package/src/capabilities/operation-resolver/index.ts +7 -0
  101. package/src/capabilities/operation-resolver/operation-resolver.ts +42 -0
  102. package/src/capabilities/react-surface/index.ts +7 -0
  103. package/src/capabilities/react-surface/react-surface.tsx +55 -0
  104. package/src/capabilities/state/index.ts +7 -0
  105. package/src/capabilities/state/state.ts +122 -0
  106. package/src/components/CommandsDialog/CommandsDialog.tsx +107 -97
  107. package/src/components/CommandsDialog/CommandsTrigger.tsx +4 -13
  108. package/src/components/NavTree/NavTree.stories.tsx +36 -44
  109. package/src/components/NavTree/NavTree.tsx +5 -15
  110. package/src/components/NavTreeContainer.tsx +255 -263
  111. package/src/components/NavTreeDocumentTitle.tsx +3 -2
  112. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +17 -7
  113. package/src/components/NavTreeItem/NavTreeItemAction.tsx +18 -12
  114. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +5 -5
  115. package/src/components/Sidebar/L0Menu.tsx +69 -98
  116. package/src/components/Sidebar/L1Panel.tsx +151 -72
  117. package/src/components/Sidebar/L1Tabs.tsx +6 -31
  118. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +1 -1
  119. package/src/components/index.ts +1 -0
  120. package/src/components/types.ts +7 -7
  121. package/src/experimental/Tree.stories.tsx +45 -54
  122. package/src/experimental/Tree.tsx +3 -3
  123. package/src/hooks.ts +54 -2
  124. package/src/index.ts +1 -2
  125. package/src/meta.ts +7 -2
  126. package/src/testing/app-graph-builder.ts +159 -235
  127. package/src/testing/generator.ts +19 -17
  128. package/src/testing/update-state.ts +6 -6
  129. package/src/types/capabilities.ts +21 -0
  130. package/src/types/events.ts +12 -0
  131. package/src/types/index.ts +7 -0
  132. package/src/{types.ts → types/schema.ts} +6 -6
  133. package/src/util.ts +13 -16
  134. package/dist/lib/browser/app-graph-builder-YAPMENYT.mjs +0 -46
  135. package/dist/lib/browser/app-graph-builder-YAPMENYT.mjs.map +0 -7
  136. package/dist/lib/browser/chunk-IMFXKPSB.mjs +0 -1482
  137. package/dist/lib/browser/chunk-IMFXKPSB.mjs.map +0 -7
  138. package/dist/lib/browser/chunk-LPAREX6J.mjs +0 -15
  139. package/dist/lib/browser/chunk-LPAREX6J.mjs.map +0 -7
  140. package/dist/lib/browser/chunk-ZENLR72O.mjs +0 -14
  141. package/dist/lib/browser/chunk-ZENLR72O.mjs.map +0 -7
  142. package/dist/lib/browser/intent-resolver-TCQXGF6U.mjs +0 -47
  143. package/dist/lib/browser/intent-resolver-TCQXGF6U.mjs.map +0 -7
  144. package/dist/lib/browser/keyboard-AHSAWKXA.mjs.map +0 -7
  145. package/dist/lib/browser/react-surface-AH2TP73M.mjs +0 -51
  146. package/dist/lib/browser/react-surface-AH2TP73M.mjs.map +0 -7
  147. package/dist/lib/browser/state-2Q623V7U.mjs +0 -117
  148. package/dist/lib/browser/state-2Q623V7U.mjs.map +0 -7
  149. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  150. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  151. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  152. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  153. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  154. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  155. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  156. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  157. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  158. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  159. package/dist/types/src/capabilities/state.d.ts +0 -20
  160. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  161. package/dist/types/src/events.d.ts +0 -4
  162. package/dist/types/src/events.d.ts.map +0 -1
  163. package/dist/types/src/types.d.ts.map +0 -1
  164. package/src/capabilities/app-graph-builder.ts +0 -53
  165. package/src/capabilities/capabilities.ts +0 -20
  166. package/src/capabilities/intent-resolver.ts +0 -37
  167. package/src/capabilities/keyboard.ts +0 -59
  168. package/src/capabilities/react-surface.tsx +0 -45
  169. package/src/capabilities/state.ts +0 -105
  170. package/src/events.ts +0 -11
@@ -2,22 +2,27 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import * as Effect from 'effect/Effect';
5
6
  import React, { useCallback, useMemo } from 'react';
6
7
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { DensityProvider, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { Node } from '@dxos/app-graph';
9
+ import { useActionRunner } from '@dxos/plugin-graph';
10
+ import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
11
  import { Tree } from '@dxos/react-ui-list';
12
+ import { DropdownMenu, type MenuItem, MenuProvider } from '@dxos/react-ui-menu';
10
13
  import { Tabs } from '@dxos/react-ui-tabs';
11
- import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/react-ui-theme';
14
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
15
 
16
+ import { useIsAlternateTree, useLoadDescendents } from '../../hooks';
13
17
  import { meta } from '../../meta';
18
+ import { NAV_TREE_ITEM } from '../NavTree';
14
19
  import { useNavTreeContext } from '../NavTreeContext';
15
- import { NavTreeItemColumns } from '../NavTreeItem';
20
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
16
21
 
17
22
  export type L1PanelProps = {
18
23
  open?: boolean;
19
24
  path: string[];
20
- item: Node<any>;
25
+ item: Node.Node;
21
26
  currentItemId: string;
22
27
  onBack?: () => void;
23
28
  };
@@ -27,16 +32,16 @@ export type L1PanelProps = {
27
32
  */
28
33
  export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
29
34
  const { t } = useTranslation(meta.id);
30
- const { isAlternateTree, ...navTreeContext } = useNavTreeContext();
35
+ const navTreeContext = useNavTreeContext();
31
36
  const title = toLocalizedString(item.properties.label, t);
32
37
 
33
38
  // TODO(wittjosiah): Support multiple alternate trees.
34
39
  const { useItems } = navTreeContext;
35
40
  const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
36
41
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
37
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
42
+ const isAlternate = useIsAlternateTree(alternatePath, item);
38
43
  const useAlternateItems = useCallback(
39
- (node?: Node, { disposition }: { disposition?: string } = {}) => {
44
+ (node?: Node.Node, { disposition }: { disposition?: string } = {}) => {
40
45
  // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
41
46
  return useItems(node, { disposition, sort: node?.id === alternateTree.id });
42
47
  },
@@ -60,31 +65,33 @@ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProp
60
65
  {item.id === currentItemId && (
61
66
  <DensityProvider density='fine'>
62
67
  <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
63
- <div role='none' className='overflow-y-auto'>
64
- {isAlternate ? (
65
- <Tree
66
- {...navTreeContext}
67
- id={alternateTree.id}
68
- root={alternateTree}
69
- path={alternatePath}
70
- levelOffset={5}
71
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
72
- renderColumns={NavTreeItemColumns}
73
- useItems={useAlternateItems}
74
- />
75
- ) : (
76
- <Tree
77
- {...navTreeContext}
78
- id={item.id}
79
- root={item}
80
- path={path}
81
- levelOffset={5}
82
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
83
- renderColumns={NavTreeItemColumns}
84
- draggable
85
- />
86
- )}
87
- </div>
68
+ <ScrollArea.Root thin orientation='vertical'>
69
+ <ScrollArea.Viewport>
70
+ {isAlternate ? (
71
+ <Tree
72
+ {...navTreeContext}
73
+ id={alternateTree.id}
74
+ root={alternateTree}
75
+ path={alternatePath}
76
+ levelOffset={5}
77
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
78
+ renderColumns={NavTreeItemColumns}
79
+ useItems={useAlternateItems}
80
+ />
81
+ ) : (
82
+ <Tree
83
+ {...navTreeContext}
84
+ id={item.id}
85
+ root={item}
86
+ path={path}
87
+ levelOffset={5}
88
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
89
+ renderColumns={NavTreeItemColumns}
90
+ draggable
91
+ />
92
+ )}
93
+ </ScrollArea.Viewport>
94
+ </ScrollArea.Root>
88
95
  </DensityProvider>
89
96
  )}
90
97
  </Tabs.Tabpanel>
@@ -96,56 +103,128 @@ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProp
96
103
  */
97
104
  const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
98
105
  const { t } = useTranslation(meta.id);
99
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
106
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
100
107
  const title = toLocalizedString(item.properties.label, t);
101
108
 
102
- // TODO(wittjosiah): Support multiple alternate trees.
103
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
104
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
105
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
106
- const backCapable = item.id.startsWith('!') || isAlternate;
107
-
108
- const handleOpen = useCallback(() => {
109
- setAlternateTree?.(alternatePath, true);
110
- }, [alternatePath, setAlternateTree]);
111
-
112
- const handleBack = useCallback(() => {
113
- if (isAlternate) {
114
- setAlternateTree?.(alternatePath, false);
115
- } else {
116
- onBack?.();
117
- }
118
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
109
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
110
+ useLoadDescendents(item);
119
111
 
120
- // TODO(burdon): Reuse same grid as tree?
121
112
  return (
122
113
  <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
123
114
  <div className='is-6' />
124
115
  <h2 className='flex-1 truncate min-is-0'>{title}</h2>
125
- {(backCapable && (
126
- <IconButton
127
- variant='ghost'
128
- icon='ph--arrow-u-down-left--regular'
129
- iconOnly
130
- size={5}
131
- label={t('button back')}
132
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
133
- onClick={handleBack}
134
- />
135
- )) ||
136
- (alternateTree && !isAlternate && (
116
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
117
+ <div role='none' className='contents app-no-drag'>
118
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
119
+ <NavTreeItemAction
120
+ testId={primaryAction.properties?.testId}
121
+ label={toLocalizedString(primaryAction.properties?.label, t)}
122
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
123
+ parent={item}
124
+ monolithic={Node.isAction(primaryAction)}
125
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
126
+ menuType={primaryAction.properties?.menuType}
127
+ caller={NAV_TREE_ITEM}
128
+ />
129
+ )}
130
+ {menuActions.length === 1 && (
137
131
  <IconButton
138
- data-testid='treeView.alternateTreeButton'
132
+ size={5}
133
+ density='coarse'
134
+ classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
139
135
  variant='ghost'
140
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
136
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
141
137
  iconOnly
142
- size={5}
143
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
144
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
145
- onClick={handleOpen}
138
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
139
+ data-testid={menuActions[0].properties?.testId}
140
+ onClick={() => onAction(menuActions[0] as Node.Action)}
146
141
  />
147
- ))}
148
- <NavTreeItemColumns open path={path} item={item} density='coarse' />
142
+ )}
143
+ {menuActions.length > 1 && (
144
+ <MenuProvider onAction={onAction}>
145
+ <DropdownMenu.Root group={item} items={menuActions as MenuItem[]} caller={NAV_TREE_ITEM}>
146
+ <DropdownMenu.Trigger asChild>
147
+ <IconButton
148
+ size={5}
149
+ density='coarse'
150
+ classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
151
+ variant='ghost'
152
+ icon='ph--dots-three-vertical--regular'
153
+ iconOnly
154
+ label={t('tree item actions label')}
155
+ data-testid='navtree.treeItem.actionsLevel0'
156
+ />
157
+ </DropdownMenu.Trigger>
158
+ </DropdownMenu.Root>
159
+ </MenuProvider>
160
+ )}
161
+ {ItemEnd && <ItemEnd node={item} open />}
162
+ </div>
149
163
  </div>
150
164
  );
151
165
  };
166
+
167
+ /**
168
+ * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
169
+ */
170
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
171
+ const { t } = useTranslation(meta.id);
172
+ const { setAlternateTree, useActions, ...navTreeContext } = useNavTreeContext();
173
+ const runAction = useActionRunner();
174
+
175
+ // TODO(wittjosiah): Support multiple alternate trees.
176
+ const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
177
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
178
+ const isAlternate = useIsAlternateTree(alternatePath, item);
179
+
180
+ // Graph actions.
181
+ const { actions: _actions, groupedActions } = useActions(item);
182
+ const [primaryAction, ...secondaryActions] = _actions.toSorted((a, _b) =>
183
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
184
+ );
185
+
186
+ const graphMenuActions = (
187
+ primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : _actions
188
+ )
189
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
190
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
191
+
192
+ // Synthetic settings/back action.
193
+ const settingsActionId = `${item.id}~settings`;
194
+ const settingsAction = useMemo((): Node.Action | undefined => {
195
+ if (!alternateTree) {
196
+ return undefined;
197
+ }
198
+ return {
199
+ id: settingsActionId,
200
+ type: Node.ActionType,
201
+ data: () => Effect.void,
202
+ properties: {
203
+ label: isAlternate ? ['button back', { ns: meta.id }] : (alternateTree.properties.label ?? alternateTree.id),
204
+ icon: isAlternate
205
+ ? 'ph--arrow-u-down-left--regular'
206
+ : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
207
+ disposition: 'list-item',
208
+ testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
209
+ },
210
+ } as Node.Action;
211
+ }, [alternateTree, isAlternate, settingsActionId, t]);
212
+
213
+ const menuActions = useMemo(
214
+ () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
215
+ [graphMenuActions, settingsAction],
216
+ );
217
+
218
+ const onAction = useCallback(
219
+ (action: Node.Action, params?: Node.InvokeProps) => {
220
+ if (action.id === settingsActionId) {
221
+ setAlternateTree?.(alternatePath, !isAlternate);
222
+ } else {
223
+ void runAction(action, params);
224
+ }
225
+ },
226
+ [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction],
227
+ );
228
+
229
+ return { primaryAction, groupedActions, menuActions, onAction };
230
+ };
@@ -2,57 +2,32 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo } from 'react';
5
+ import React from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/app-graph';
8
8
 
9
- import { useLoadDescendents } from '../../hooks';
10
9
  import { l0ItemType } from '../../util';
11
- import { useNavTreeContext } from '../NavTreeContext';
12
10
 
13
11
  import { L1Panel, type L1PanelProps } from './L1Panel';
14
12
 
15
13
  export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
16
14
  path: string[];
17
- topLevelItems: Node<any>[];
15
+ topLevelItems: Node.Node[];
18
16
  };
19
17
 
20
18
  /**
21
- * Each space is an L1 tab.
19
+ * Each workspace is an L1 tab.
22
20
  */
23
21
  export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
24
22
  return (
25
23
  <>
26
24
  {topLevelItems.map((item) => {
27
25
  const type = l0ItemType(item);
28
- switch (type) {
29
- case 'tab':
30
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
31
- case 'collection':
32
- return <L1PanelCollection key={item.id} item={item} {...props} />;
33
- default:
34
- return null;
26
+ if (type === 'tab') {
27
+ return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
35
28
  }
29
+ return null;
36
30
  })}
37
31
  </>
38
32
  );
39
33
  };
40
-
41
- /**
42
- *
43
- */
44
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
45
- const { useItems } = useNavTreeContext();
46
- useLoadDescendents(item);
47
- const collectionItems = useItems(item);
48
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
49
- return (
50
- <>
51
- {collectionItems
52
- .filter((item) => l0ItemType(item) === 'tab')
53
- .map((item) => (
54
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
55
- ))}
56
- </>
57
- );
58
- };
@@ -11,7 +11,7 @@ import { UserAccountAvatar } from './UserAccountAvatar';
11
11
  const meta = {
12
12
  title: 'plugins/plugin-navtree/UserAccountAvatar',
13
13
 
14
- decorators: [withTheme],
14
+ decorators: [withTheme()],
15
15
  component: UserAccountAvatar,
16
16
  } satisfies Meta<typeof UserAccountAvatar>;
17
17
 
@@ -7,4 +7,5 @@ export * from './NavTree';
7
7
  export * from './NavTreeContainer';
8
8
  export * from './NavTreeContext';
9
9
  export * from './NavTreeDocumentTitle';
10
+
10
11
  export type * from './types';
@@ -4,7 +4,7 @@
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
9
  import type { TreeProps } from '@dxos/react-ui-list';
10
10
  import type { MaybePromise } from '@dxos/util';
@@ -15,23 +15,23 @@ import type { L1PanelProps } from './Sidebar';
15
15
 
16
16
  export type NavTreeContextValue = Pick<
17
17
  TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
18
- 'useItems' | 'getProps' | 'isCurrent' | 'isOpen' | 'canDrop' | 'onOpenChange' | 'onSelect'
18
+ 'useItems' | 'getProps' | 'useIsCurrent' | 'useIsOpen' | 'blockInstruction' | 'canDrop' | 'onOpenChange' | 'onSelect'
19
19
  > &
20
20
  Pick<L1PanelProps, 'onBack'> & {
21
21
  tab: string;
22
22
  topbar?: boolean;
23
23
  popoverAnchorId?: string;
24
- renderItemEnd?: FC<{ node: Node; open: boolean }>;
25
- useActions: (node: Node) => FlattenedActions;
26
- loadDescendents?: (node: Node) => MaybePromise<void>;
27
- isAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;
24
+ renderItemEnd?: FC<{ node: Node.Node; open: boolean }>;
25
+ useActions: (node: Node.Node) => FlattenedActions;
26
+ loadDescendents?: (node: Node.Node) => MaybePromise<void>;
27
+ useIsAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;
28
28
  setAlternateTree?: (path: string[], open: boolean) => void;
29
29
  onTabChange?: (node: NavTreeItemGraphNode) => void;
30
30
  };
31
31
 
32
32
  export type NavTreeItemColumnsProps = {
33
33
  path: string[];
34
- item: Node;
34
+ item: Node.Node;
35
35
  open: boolean;
36
36
  density?: Density;
37
37
  };
@@ -3,39 +3,17 @@
3
3
  //
4
4
 
5
5
  import { type Meta } from '@storybook/react-vite';
6
- import React, { type JSX, type PropsWithChildren, useEffect, useState } from 'react';
6
+ import React, { useEffect, useState } from 'react';
7
7
 
8
8
  import { faker } from '@dxos/random';
9
- import { Icon, IconButton } from '@dxos/react-ui';
10
- import { withTheme } from '@dxos/react-ui/testing';
11
- import { mx } from '@dxos/react-ui-theme';
9
+ import { Icon, IconButton, Layout, ScrollArea } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { mx } from '@dxos/ui-theme';
12
12
 
13
13
  import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitNodes, visitor } from './Tree';
14
14
 
15
15
  faker.seed(1234);
16
16
 
17
- /**
18
- * Space:
19
- * - Space panels are separated from each other to give them more weight than folders.
20
- * - Private space isn't different (just first, different icon).
21
- * - Edit titles in place.
22
- *
23
- * Navigation:
24
- * - up/down across spaces/folder peers
25
- * - right to enter space/folder children
26
- * - left to parent
27
- */
28
- const Container = ({ children, sidebar }: PropsWithChildren<{ sidebar: JSX.Element }>) => {
29
- return (
30
- <div className='flex'>
31
- {/* TODO(burdon): Custom thin scrollbar. */}
32
- {/* TODO(burdon): Horizontal scrolling within navtree? */}
33
- <div className='flex flex-col overflow-y-auto w-[300px] bg-neutral-100 dark:bg-neutral-950'>{sidebar}</div>
34
- <div className='flex flex-col grow overflow-hidden'>{children}</div>
35
- </div>
36
- );
37
- };
38
-
39
17
  // TODO(burdon): Generate.
40
18
  const data: TreeNodeData[] = [
41
19
  {
@@ -134,7 +112,7 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
134
112
  }
135
113
  }, []);
136
114
 
137
- const handleCreateItem: TreeProps['onMenuAction'] = (id, action) => {
115
+ const handleCreateItem: TreeProps['onMenuAction'] = (id) => {
138
116
  setItems((items) => {
139
117
  let parent: TreeNodeData | undefined;
140
118
  visitNodes(root, (node) => {
@@ -167,52 +145,65 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
167
145
  };
168
146
 
169
147
  return (
170
- <div className='flex flex-col overflow-hidden'>
171
- <div className='flex flex-col overflow-y-auto'>
172
- <Tree
173
- className='p-0.5 gap-1'
174
- node={root}
175
- open={openItems}
176
- selected={selectedItems}
177
- active={activeItems}
178
- onChangeOpen={(id, open) => setOpenItems((items) => ({ ...items, [id]: open }))}
179
- onChangeSelected={(id, open) => setSelectedItems((items) => ({ ...items, [id]: open }))}
180
- onMenuAction={handleCreateItem}
181
- getSlots={(node, open, depth, ancestors) => {
182
- if (depth === 1) {
183
- return {
184
- root: 'rounded bg-white dark:bg-neutral-850',
185
- header: mx('rounded-t bg-neutral-50 dark:bg-neutral-900', !open && 'rounded-b'),
186
- };
187
- }
188
- }}
189
- />
190
- </div>
191
-
192
- <div className='flex items-center my-2 px-2 gap-2'>
148
+ <Layout.Main>
149
+ <ScrollArea.Root orientation='vertical' thin>
150
+ <ScrollArea.Viewport>
151
+ <Tree
152
+ className='p-0.5 gap-1'
153
+ node={root}
154
+ open={openItems}
155
+ selected={selectedItems}
156
+ active={activeItems}
157
+ onChangeOpen={(id, open) => setOpenItems((items) => ({ ...items, [id]: open }))}
158
+ onChangeSelected={(id, open) => setSelectedItems((items) => ({ ...items, [id]: open }))}
159
+ onMenuAction={handleCreateItem}
160
+ getSlots={(node, open, depth) => {
161
+ if (depth === 1) {
162
+ return {
163
+ root: 'rounded bg-white dark:bg-neutral-850',
164
+ header: mx('rounded-t bg-neutral-50 dark:bg-neutral-900', !open && 'rounded-b'),
165
+ };
166
+ }
167
+ }}
168
+ />
169
+ </ScrollArea.Viewport>
170
+ </ScrollArea.Root>
171
+
172
+ <div className='flex items-center my-2 pli-2 gap-2'>
193
173
  <IconButton icon='ph--plus-circle--regular' iconOnly label='Create space' onClick={handleCreateSpace} />
194
174
  <span className='grow text-sm' onClick={handleCreateSpace}>
195
175
  New space
196
176
  </span>
197
177
  <Icon icon='ph--list--regular' />
198
178
  </div>
199
- </div>
179
+ </Layout.Main>
200
180
  );
201
181
  };
202
182
 
203
183
  const meta = {
204
184
  title: 'plugins/plugin-navtree/experimental/Tree',
205
185
  component: Tree,
206
- decorators: [withTheme],
186
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'is-[20rem]' })],
207
187
  parameters: {
208
- layout: 'column',
188
+ layout: 'fullscreen',
209
189
  },
210
190
  } satisfies Meta<typeof Tree>;
211
191
 
212
192
  export default meta;
213
193
 
194
+ /**
195
+ * Space:
196
+ * - Space panels are separated from each other to give them more weight than folders.
197
+ * - Private space isn't different (just first, different icon).
198
+ * - Edit titles in place.
199
+ *
200
+ * Navigation:
201
+ * - up/down across spaces/folder peers
202
+ * - right to enter space/folder children
203
+ * - left to parent
204
+ */
214
205
  export const Default = () => {
215
- return <Container sidebar={<Sidebar mutate />} />;
206
+ return <Sidebar mutate />;
216
207
  };
217
208
 
218
209
  export const Visitor = () => {
@@ -5,7 +5,7 @@
5
5
  import React, { type HTMLAttributes, type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ClassNameValue, Icon, type Size } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
 
10
10
  export const IconButton = ({
11
11
  iconName,
@@ -19,7 +19,7 @@ export const IconButton = ({
19
19
  } & Pick<HTMLAttributes<HTMLDivElement>, 'onClick'>) => {
20
20
  // TODO(burdon): Density aware.
21
21
  return (
22
- <div className={mx('flex w-6 h-6 items-center justify-center select-none', classNames)} onClick={onClick}>
22
+ <div className={mx('flex is-6 bs-6 items-center justify-center select-none', classNames)} onClick={onClick}>
23
23
  <Icon icon={iconName} classNames='cursor-pointer' size={size} />
24
24
  </div>
25
25
  );
@@ -193,7 +193,7 @@ export const TreeNodeRow = (props: TreeNodeProps & { className?: string }) => {
193
193
  <Grid
194
194
  style={{ paddingLeft: `${(depth - 1) * 24}px` }}
195
195
  className={mx(
196
- 'group w-full items-center cursor-pointer bg-hoverSurface',
196
+ 'group is-full items-center cursor-pointer bg-hoverSurface',
197
197
  // TODO(burdon): Use data-active.
198
198
  selected?.[id] && 'bg-activeSurface',
199
199
  className,
package/src/hooks.ts CHANGED
@@ -2,13 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useEffect } from 'react';
5
+ import { type Atom, useAtomValue } from '@effect-atom/atom-react';
6
+ import { useEffect, useMemo } from 'react';
6
7
 
8
+ import { useCapability } from '@dxos/app-framework/ui';
7
9
  import { type Node } from '@dxos/app-graph';
8
10
 
9
11
  import { useNavTreeContext } from './components';
12
+ import { NavTreeCapabilities } from './types';
10
13
 
11
- export const useLoadDescendents = (root?: Node) => {
14
+ export const useLoadDescendents = (root?: Node.Node) => {
12
15
  const { loadDescendents } = useNavTreeContext();
13
16
  useEffect(() => {
14
17
  const frame = requestAnimationFrame(() => {
@@ -19,3 +22,52 @@ export const useLoadDescendents = (root?: Node) => {
19
22
  return () => cancelAnimationFrame(frame);
20
23
  }, [root]);
21
24
  };
25
+
26
+ export type UseNavTreeStateResult = {
27
+ getItem: (path: string[]) => NavTreeCapabilities.NavTreeItemState;
28
+ getItemAtom: (path: string[]) => Atom.Atom<NavTreeCapabilities.NavTreeItemState>;
29
+ setItem: (path: string[], key: 'open' | 'current' | 'alternateTree', next: boolean) => void;
30
+ };
31
+
32
+ /**
33
+ * Hook that provides access to navtree state functions.
34
+ * For reactive subscriptions, use `useNavTreeItemState` or `useAtomValue(getItemAtom(path))`.
35
+ */
36
+ export const useNavTreeState = (): UseNavTreeStateResult => {
37
+ return useCapability(NavTreeCapabilities.State);
38
+ };
39
+
40
+ /**
41
+ * Hook that subscribes to a single navtree item's state.
42
+ * Use this for fine-grained reactivity.
43
+ */
44
+ export const useNavTreeItemState = (path: string[]): NavTreeCapabilities.NavTreeItemState => {
45
+ const { getItemAtom } = useCapability(NavTreeCapabilities.State);
46
+ // Memoize the path to avoid creating new atoms on every render.
47
+ const pathKey = useMemo(() => path.join('~'), [path]);
48
+ const atom = useMemo(() => getItemAtom(path), [getItemAtom, pathKey]);
49
+ return useAtomValue(atom);
50
+ };
51
+
52
+ /**
53
+ * Hook that subscribes to the open state for a tree item.
54
+ * Designed to be passed to Tree component as useIsOpen prop.
55
+ */
56
+ export const useIsOpen = (path: string[], _item?: any): boolean => {
57
+ return useNavTreeItemState(path).open;
58
+ };
59
+
60
+ /**
61
+ * Hook that subscribes to the current state for a tree item.
62
+ * Designed to be passed to Tree component as useIsCurrent prop.
63
+ */
64
+ export const useIsCurrent = (path: string[], _item?: any): boolean => {
65
+ return useNavTreeItemState(path).current;
66
+ };
67
+
68
+ /**
69
+ * Hook that subscribes to the alternateTree state for a tree item.
70
+ */
71
+ export const useIsAlternateTree = (path: string[], _item?: any): boolean => {
72
+ return useNavTreeItemState(path).alternateTree ?? false;
73
+ };
package/src/index.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { NavTreeCapabilities } from './capabilities';
6
- export { NavTreeEvents } from './events';
5
+ export { NavTreeCapabilities, NavTreeEvents } from './types';
7
6
  export * from './NavTreePlugin';
8
7
  export * from './meta';
9
8
  export * from './util';