@dxos/plugin-navtree 0.8.4-main.1f223c7 → 0.8.4-main.2244d791bb

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 (178) 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 -93
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/{keyboard-HMTMA33N.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 -105
  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 +5 -3
  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 +3 -3
  58. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  60. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  61. package/dist/types/src/components/Sidebar/L0Menu.d.ts +6 -4
  62. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  63. package/dist/types/src/components/Sidebar/L1Panel.d.ts +2 -2
  64. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +2 -2
  66. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  67. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  68. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +1 -2
  69. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  70. package/dist/types/src/components/index.d.ts.map +1 -1
  71. package/dist/types/src/components/types.d.ts +7 -7
  72. package/dist/types/src/components/types.d.ts.map +1 -1
  73. package/dist/types/src/experimental/Tree.d.ts +1 -2
  74. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  75. package/dist/types/src/experimental/Tree.stories.d.ts +18 -5
  76. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  77. package/dist/types/src/hooks.d.ts +32 -1
  78. package/dist/types/src/hooks.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +1 -2
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/meta.d.ts +3 -4
  82. package/dist/types/src/meta.d.ts.map +1 -1
  83. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  84. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  85. package/dist/types/src/testing/generator.d.ts +5 -6
  86. package/dist/types/src/testing/generator.d.ts.map +1 -1
  87. package/dist/types/src/testing/update-state.d.ts +2 -2
  88. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  89. package/dist/types/src/types/capabilities.d.ts +16 -0
  90. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  91. package/dist/types/src/types/events.d.ts +5 -0
  92. package/dist/types/src/types/events.d.ts.map +1 -0
  93. package/dist/types/src/types/index.d.ts +4 -0
  94. package/dist/types/src/types/index.d.ts.map +1 -0
  95. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  96. package/dist/types/src/types/schema.d.ts.map +1 -0
  97. package/dist/types/src/util.d.ts +7 -7
  98. package/dist/types/src/util.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +55 -53
  101. package/src/NavTreePlugin.tsx +63 -83
  102. package/src/capabilities/app-graph-builder/app-graph-builder.ts +42 -0
  103. package/src/capabilities/app-graph-builder/index.ts +7 -0
  104. package/src/capabilities/index.ts +5 -9
  105. package/src/capabilities/keyboard/index.ts +7 -0
  106. package/src/capabilities/keyboard/keyboard.ts +66 -0
  107. package/src/capabilities/operation-resolver/index.ts +7 -0
  108. package/src/capabilities/operation-resolver/operation-resolver.ts +42 -0
  109. package/src/capabilities/react-surface/index.ts +7 -0
  110. package/src/capabilities/react-surface/react-surface.tsx +55 -0
  111. package/src/capabilities/state/index.ts +7 -0
  112. package/src/capabilities/state/state.ts +122 -0
  113. package/src/components/CommandsDialog/CommandsDialog.tsx +108 -98
  114. package/src/components/CommandsDialog/CommandsTrigger.tsx +6 -15
  115. package/src/components/NavTree/NavTree.stories.tsx +95 -89
  116. package/src/components/NavTree/NavTree.tsx +9 -15
  117. package/src/components/NavTreeContainer.tsx +256 -257
  118. package/src/components/NavTreeDocumentTitle.tsx +5 -4
  119. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +18 -10
  120. package/src/components/NavTreeItem/NavTreeItemAction.tsx +23 -16
  121. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +32 -26
  122. package/src/components/Sidebar/L0Menu.tsx +113 -141
  123. package/src/components/Sidebar/L1Panel.tsx +155 -77
  124. package/src/components/Sidebar/L1Tabs.tsx +6 -31
  125. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +3 -4
  126. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  127. package/src/components/index.ts +1 -0
  128. package/src/components/types.ts +7 -7
  129. package/src/experimental/Tree.stories.tsx +67 -77
  130. package/src/experimental/Tree.tsx +7 -24
  131. package/src/hooks.ts +54 -2
  132. package/src/index.ts +1 -2
  133. package/src/meta.ts +11 -8
  134. package/src/testing/app-graph-builder.ts +159 -234
  135. package/src/testing/generator.ts +20 -18
  136. package/src/testing/update-state.ts +6 -6
  137. package/src/types/capabilities.ts +21 -0
  138. package/src/types/events.ts +12 -0
  139. package/src/types/index.ts +7 -0
  140. package/src/{types.ts → types/schema.ts} +9 -8
  141. package/src/util.ts +13 -16
  142. package/dist/lib/browser/app-graph-builder-6JDBQ25U.mjs +0 -45
  143. package/dist/lib/browser/app-graph-builder-6JDBQ25U.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-ERDGQTH5.mjs +0 -16
  145. package/dist/lib/browser/chunk-ERDGQTH5.mjs.map +0 -7
  146. package/dist/lib/browser/chunk-WCARAZ7J.mjs +0 -15
  147. package/dist/lib/browser/chunk-WCARAZ7J.mjs.map +0 -7
  148. package/dist/lib/browser/chunk-ZKVY35IG.mjs +0 -1469
  149. package/dist/lib/browser/chunk-ZKVY35IG.mjs.map +0 -7
  150. package/dist/lib/browser/intent-resolver-N35ZIHFD.mjs +0 -47
  151. package/dist/lib/browser/intent-resolver-N35ZIHFD.mjs.map +0 -7
  152. package/dist/lib/browser/keyboard-HMTMA33N.mjs.map +0 -7
  153. package/dist/lib/browser/react-surface-5PZ4XJT7.mjs +0 -51
  154. package/dist/lib/browser/react-surface-5PZ4XJT7.mjs.map +0 -7
  155. package/dist/lib/browser/state-P5DGL7KS.mjs +0 -117
  156. package/dist/lib/browser/state-P5DGL7KS.mjs.map +0 -7
  157. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  158. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  159. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  160. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  161. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  162. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  163. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  164. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  165. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  166. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  167. package/dist/types/src/capabilities/state.d.ts +0 -20
  168. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  169. package/dist/types/src/events.d.ts +0 -4
  170. package/dist/types/src/events.d.ts.map +0 -1
  171. package/dist/types/src/types.d.ts.map +0 -1
  172. package/src/capabilities/app-graph-builder.ts +0 -52
  173. package/src/capabilities/capabilities.ts +0 -20
  174. package/src/capabilities/intent-resolver.ts +0 -37
  175. package/src/capabilities/keyboard.ts +0 -59
  176. package/src/capabilities/react-surface.tsx +0 -45
  177. package/src/capabilities/state.ts +0 -105
  178. 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 { 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,17 +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();
31
- // NOTE(burdon): I'm very uneasy about this pattern of declaring hooks as context props.
32
- const { useItems } = navTreeContext;
35
+ const navTreeContext = useNavTreeContext();
33
36
  const title = toLocalizedString(item.properties.label, t);
34
37
 
35
38
  // TODO(wittjosiah): Support multiple alternate trees.
39
+ const { useItems } = navTreeContext;
36
40
  const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
37
41
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
38
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
42
+ const isAlternate = useIsAlternateTree(alternatePath, item);
39
43
  const useAlternateItems = useCallback(
40
- (node?: Node, { disposition }: { disposition?: string } = {}) => {
44
+ (node?: Node.Node, { disposition }: { disposition?: string } = {}) => {
41
45
  // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
42
46
  return useItems(node, { disposition, sort: node?.id === alternateTree.id });
43
47
  },
@@ -56,37 +60,39 @@ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProp
56
60
  ]}
57
61
  tabIndex={-1}
58
62
  aria-label={title}
59
- {...(!open && { inert: 'true' })}
63
+ {...(!open && { inert: true })}
60
64
  >
61
65
  {item.id === currentItemId && (
62
- <>
66
+ <DensityProvider density='fine'>
63
67
  <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
64
- <div role='none' className='overflow-y-auto'>
65
- {isAlternate ? (
66
- <Tree
67
- {...navTreeContext}
68
- useItems={useAlternateItems}
69
- id={alternateTree.id}
70
- root={alternateTree}
71
- path={alternatePath}
72
- levelOffset={5}
73
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
74
- renderColumns={NavTreeItemColumns}
75
- />
76
- ) : (
77
- <Tree
78
- {...navTreeContext}
79
- id={item.id}
80
- root={item}
81
- path={path}
82
- levelOffset={5}
83
- draggable
84
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
85
- renderColumns={NavTreeItemColumns}
86
- />
87
- )}
88
- </div>
89
- </>
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>
95
+ </DensityProvider>
90
96
  )}
91
97
  </Tabs.Tabpanel>
92
98
  );
@@ -97,56 +103,128 @@ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProp
97
103
  */
98
104
  const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
99
105
  const { t } = useTranslation(meta.id);
100
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
106
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
101
107
  const title = toLocalizedString(item.properties.label, t);
102
108
 
103
- // TODO(wittjosiah): Support multiple alternate trees.
104
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
105
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
106
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
107
- const backCapable = item.id.startsWith('!') || isAlternate;
108
-
109
- const handleOpen = useCallback(() => {
110
- setAlternateTree?.(alternatePath, true);
111
- }, [alternatePath, setAlternateTree]);
112
-
113
- const handleBack = useCallback(() => {
114
- if (isAlternate) {
115
- setAlternateTree?.(alternatePath, false);
116
- } else {
117
- onBack?.();
118
- }
119
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
109
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
110
+ useLoadDescendents(item);
120
111
 
121
- // TODO(burdon): Reuse same grid as tree?
122
112
  return (
123
113
  <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
124
114
  <div className='is-6' />
125
115
  <h2 className='flex-1 truncate min-is-0'>{title}</h2>
126
- {(backCapable && (
127
- <IconButton
128
- variant='ghost'
129
- icon='ph--arrow-u-down-left--regular'
130
- iconOnly
131
- size={5}
132
- label={t('button back')}
133
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
134
- onClick={handleBack}
135
- />
136
- )) ||
137
- (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 && (
138
131
  <IconButton
139
- data-testid='treeView.alternateTreeButton'
132
+ size={5}
133
+ density='coarse'
134
+ classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
140
135
  variant='ghost'
141
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
136
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
142
137
  iconOnly
143
- size={5}
144
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
145
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
146
- 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)}
147
141
  />
148
- ))}
149
- <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>
150
163
  </div>
151
164
  );
152
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
- };
@@ -2,18 +2,17 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
 
9
- import { withTheme } from '@dxos/storybook-utils';
7
+ import { withTheme } from '@dxos/react-ui/testing';
10
8
 
11
9
  import { UserAccountAvatar } from './UserAccountAvatar';
12
10
 
13
11
  const meta = {
14
12
  title: 'plugins/plugin-navtree/UserAccountAvatar',
13
+
14
+ decorators: [withTheme()],
15
15
  component: UserAccountAvatar,
16
- decorators: [withTheme],
17
16
  } satisfies Meta<typeof UserAccountAvatar>;
18
17
 
19
18
  export default meta;
@@ -7,6 +7,8 @@ import React from 'react';
7
7
  import { Avatar, type AvatarStatus, type Size } from '@dxos/react-ui';
8
8
  import { hexToFallback } from '@dxos/util';
9
9
 
10
+ import { L0ItemActiveTabIndicator } from '../Sidebar';
11
+
10
12
  export type UserAccountAvatarProps = {
11
13
  size?: Size;
12
14
  userId?: string;
@@ -19,17 +21,23 @@ export const UserAccountAvatar = ({ size, userId, hue, emoji, status }: UserAcco
19
21
  const fallbackValue = hexToFallback(userId ?? '0');
20
22
 
21
23
  return (
22
- <div className='grid place-items-center' data-joyride='welcome/account'>
23
- <Avatar.Root>
24
- <Avatar.Content
25
- variant='circle'
26
- size={size ?? 12}
27
- status={status ?? 'active'}
28
- hue={hue || fallbackValue.hue}
29
- fallback={emoji || fallbackValue.emoji}
30
- data-testid='treeView.userAccount'
31
- />
32
- </Avatar.Root>
33
- </div>
24
+ <>
25
+ <L0ItemActiveTabIndicator classNames='inset-block-6' />
26
+ <div
27
+ className='grid place-items-center dx-focus-ring-group-indicator rounded-full'
28
+ data-joyride='welcome/account'
29
+ >
30
+ <Avatar.Root>
31
+ <Avatar.Content
32
+ variant='circle'
33
+ size={size ?? 12}
34
+ status={status ?? 'active'}
35
+ hue={hue || fallbackValue.hue}
36
+ fallback={emoji || fallbackValue.emoji}
37
+ data-testid='treeView.userAccount'
38
+ />
39
+ </Avatar.Root>
40
+ </div>
41
+ </>
34
42
  );
35
43
  };
@@ -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
  };
@@ -2,42 +2,18 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { type JSX, type PropsWithChildren, useEffect, useState } from 'react';
5
+ import { type Meta } from '@storybook/react-vite';
6
+ import React, { useEffect, useState } from 'react';
9
7
 
10
8
  import { faker } from '@dxos/random';
11
- import { Icon, IconButton } from '@dxos/react-ui';
12
- import { modalSurface, mx } from '@dxos/react-ui-theme';
13
- import { withLayout, withTheme } from '@dxos/storybook-utils';
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';
14
12
 
15
13
  import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitNodes, visitor } from './Tree';
16
14
 
17
15
  faker.seed(1234);
18
16
 
19
- /**
20
- * Space:
21
- * - Space panels are separated from each other to give them more weight than folders.
22
- * - Private space isn't different (just first, different icon).
23
- * - Edit titles in place.
24
- *
25
- * Navigation:
26
- * - up/down across spaces/folder peers
27
- * - right to enter space/folder children
28
- * - left to parent
29
- */
30
- const Container = ({ children, sidebar }: PropsWithChildren<{ sidebar: JSX.Element }>) => {
31
- return (
32
- <div className='flex'>
33
- {/* TODO(burdon): Custom thin scrollbar. */}
34
- {/* TODO(burdon): Horizontal scrolling within navtree? */}
35
- <div className='flex flex-col overflow-y-auto w-[300px] bg-neutral-100 dark:bg-neutral-950'>{sidebar}</div>
36
- <div className='flex flex-col grow overflow-hidden'>{children}</div>
37
- </div>
38
- );
39
- };
40
-
41
17
  // TODO(burdon): Generate.
42
18
  const data: TreeNodeData[] = [
43
19
  {
@@ -105,27 +81,6 @@ const data: TreeNodeData[] = [
105
81
  },
106
82
  ];
107
83
 
108
- export const Default = () => {
109
- return <Container sidebar={<Sidebar mutate />} />;
110
- };
111
-
112
- export const Visitor = () => {
113
- const [items] = useState(data);
114
- const root = items[0];
115
- const [openItems] = useState<ItemMap>({ [root.children![0].id]: true });
116
- return (
117
- <div className='flex flex-col'>
118
- {Array.from(visitor(root, openItems)).map(({ node: { id, title }, depth }) => (
119
- <div key={id} className='grid grid-cols-[40px_400px_400px] font-mono'>
120
- <div>{depth}</div>
121
- <div>{id}</div>
122
- <div>{title}</div>
123
- </div>
124
- ))}
125
- </div>
126
- );
127
- };
128
-
129
84
  const Sidebar = ({ mutate }: { mutate?: boolean }) => {
130
85
  const [items, setItems] = useState(data);
131
86
  const root = items[0];
@@ -157,7 +112,7 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
157
112
  }
158
113
  }, []);
159
114
 
160
- const handleCreateItem: TreeProps['onMenuAction'] = (id, action) => {
115
+ const handleCreateItem: TreeProps['onMenuAction'] = (id) => {
161
116
  setItems((items) => {
162
117
  let parent: TreeNodeData | undefined;
163
118
  visitNodes(root, (node) => {
@@ -190,45 +145,80 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
190
145
  };
191
146
 
192
147
  return (
193
- <div className='flex flex-col overflow-hidden'>
194
- <div className='flex flex-col overflow-y-auto'>
195
- <Tree
196
- className='p-0.5 gap-1'
197
- node={root}
198
- open={openItems}
199
- selected={selectedItems}
200
- active={activeItems}
201
- onChangeOpen={(id, open) => setOpenItems((items) => ({ ...items, [id]: open }))}
202
- onChangeSelected={(id, open) => setSelectedItems((items) => ({ ...items, [id]: open }))}
203
- onMenuAction={handleCreateItem}
204
- getSlots={(node, open, depth, ancestors) => {
205
- if (depth === 1) {
206
- return {
207
- root: 'rounded bg-white dark:bg-neutral-850',
208
- header: mx('rounded-t bg-neutral-50 dark:bg-neutral-900', !open && 'rounded-b'),
209
- };
210
- }
211
- }}
212
- />
213
- </div>
214
-
215
- <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'>
216
173
  <IconButton icon='ph--plus-circle--regular' iconOnly label='Create space' onClick={handleCreateSpace} />
217
174
  <span className='grow text-sm' onClick={handleCreateSpace}>
218
175
  New space
219
176
  </span>
220
177
  <Icon icon='ph--list--regular' />
221
178
  </div>
222
- </div>
179
+ </Layout.Main>
223
180
  );
224
181
  };
225
182
 
226
183
  const meta = {
227
184
  title: 'plugins/plugin-navtree/experimental/Tree',
228
185
  component: Tree,
229
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: modalSurface })],
186
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'is-[20rem]' })],
187
+ parameters: {
188
+ layout: 'fullscreen',
189
+ },
230
190
  } satisfies Meta<typeof Tree>;
231
191
 
232
192
  export default meta;
233
193
 
234
- type Story = StoryObj<typeof meta>;
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
+ */
205
+ export const Default = () => {
206
+ return <Sidebar mutate />;
207
+ };
208
+
209
+ export const Visitor = () => {
210
+ const [items] = useState(data);
211
+ const root = items[0];
212
+ const [openItems] = useState<ItemMap>({ [root.children![0].id]: true });
213
+ return (
214
+ <div className='flex flex-col'>
215
+ {Array.from(visitor(root, openItems)).map(({ node: { id, title }, depth }) => (
216
+ <div key={id} className='grid grid-cols-[40px_400px_400px] font-mono'>
217
+ <div>{depth}</div>
218
+ <div>{id}</div>
219
+ <div>{title}</div>
220
+ </div>
221
+ ))}
222
+ </div>
223
+ );
224
+ };