@dxos/plugin-navtree 0.8.3-staging.0fa589b → 0.8.4-main.1068cf700f

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 (185) 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 +89 -100
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/{keyboard-DDFATRUG.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 +32 -3
  48. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  49. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +29 -4
  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 +33 -4
  58. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +2 -2
  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 +6 -7
  64. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +12 -0
  66. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -0
  67. package/dist/types/src/components/Sidebar/index.d.ts +1 -0
  68. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -1
  69. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  70. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  71. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  72. package/dist/types/src/components/index.d.ts +1 -1
  73. package/dist/types/src/components/index.d.ts.map +1 -1
  74. package/dist/types/src/components/types.d.ts +8 -8
  75. package/dist/types/src/components/types.d.ts.map +1 -1
  76. package/dist/types/src/experimental/Tree.d.ts +4 -5
  77. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  78. package/dist/types/src/experimental/Tree.stories.d.ts +23 -6
  79. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  80. package/dist/types/src/hooks.d.ts +32 -1
  81. package/dist/types/src/hooks.d.ts.map +1 -1
  82. package/dist/types/src/index.d.ts +1 -2
  83. package/dist/types/src/index.d.ts.map +1 -1
  84. package/dist/types/src/meta.d.ts +3 -4
  85. package/dist/types/src/meta.d.ts.map +1 -1
  86. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  87. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  88. package/dist/types/src/testing/generator.d.ts +5 -6
  89. package/dist/types/src/testing/generator.d.ts.map +1 -1
  90. package/dist/types/src/testing/update-state.d.ts +2 -2
  91. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  92. package/dist/types/src/translations.d.ts +14 -13
  93. package/dist/types/src/translations.d.ts.map +1 -1
  94. package/dist/types/src/types/capabilities.d.ts +16 -0
  95. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  96. package/dist/types/src/types/events.d.ts +5 -0
  97. package/dist/types/src/types/events.d.ts.map +1 -0
  98. package/dist/types/src/types/index.d.ts +4 -0
  99. package/dist/types/src/types/index.d.ts.map +1 -0
  100. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  101. package/dist/types/src/types/schema.d.ts.map +1 -0
  102. package/dist/types/src/util.d.ts +8 -9
  103. package/dist/types/src/util.d.ts.map +1 -1
  104. package/dist/types/tsconfig.tsbuildinfo +1 -1
  105. package/package.json +56 -55
  106. package/src/NavTreePlugin.tsx +64 -84
  107. package/src/capabilities/app-graph-builder/app-graph-builder.ts +42 -0
  108. package/src/capabilities/app-graph-builder/index.ts +7 -0
  109. package/src/capabilities/index.ts +5 -9
  110. package/src/capabilities/keyboard/index.ts +7 -0
  111. package/src/capabilities/keyboard/keyboard.ts +66 -0
  112. package/src/capabilities/operation-resolver/index.ts +7 -0
  113. package/src/capabilities/operation-resolver/operation-resolver.ts +42 -0
  114. package/src/capabilities/react-surface/index.ts +7 -0
  115. package/src/capabilities/react-surface/react-surface.tsx +55 -0
  116. package/src/capabilities/state/index.ts +7 -0
  117. package/src/capabilities/state/state.ts +122 -0
  118. package/src/components/CommandsDialog/CommandsDialog.tsx +108 -98
  119. package/src/components/CommandsDialog/CommandsTrigger.tsx +8 -19
  120. package/src/components/NavTree/NavTree.stories.tsx +100 -94
  121. package/src/components/NavTree/NavTree.tsx +12 -18
  122. package/src/components/NavTreeContainer.tsx +283 -259
  123. package/src/components/NavTreeDocumentTitle.tsx +5 -4
  124. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +21 -13
  125. package/src/components/NavTreeItem/NavTreeItemAction.tsx +28 -21
  126. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +38 -32
  127. package/src/components/Sidebar/L0Menu.tsx +115 -143
  128. package/src/components/Sidebar/L1Panel.tsx +177 -122
  129. package/src/components/Sidebar/L1Tabs.tsx +33 -0
  130. package/src/components/Sidebar/index.ts +1 -0
  131. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -8
  132. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  133. package/src/components/index.ts +2 -1
  134. package/src/components/types.ts +9 -8
  135. package/src/experimental/Tree.stories.tsx +77 -85
  136. package/src/experimental/Tree.tsx +17 -35
  137. package/src/hooks.ts +54 -2
  138. package/src/index.ts +1 -2
  139. package/src/meta.ts +11 -8
  140. package/src/testing/app-graph-builder.ts +159 -234
  141. package/src/testing/generator.ts +20 -18
  142. package/src/testing/update-state.ts +6 -6
  143. package/src/translations.ts +7 -4
  144. package/src/types/capabilities.ts +21 -0
  145. package/src/types/events.ts +12 -0
  146. package/src/types/index.ts +7 -0
  147. package/src/{types.ts → types/schema.ts} +9 -8
  148. package/src/util.ts +14 -35
  149. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs +0 -45
  150. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs.map +0 -7
  151. package/dist/lib/browser/chunk-33VLL4IJ.mjs +0 -15
  152. package/dist/lib/browser/chunk-33VLL4IJ.mjs.map +0 -7
  153. package/dist/lib/browser/chunk-IEW3JD5C.mjs +0 -16
  154. package/dist/lib/browser/chunk-IEW3JD5C.mjs.map +0 -7
  155. package/dist/lib/browser/chunk-ZJPL4XJD.mjs +0 -1429
  156. package/dist/lib/browser/chunk-ZJPL4XJD.mjs.map +0 -7
  157. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs +0 -47
  158. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs.map +0 -7
  159. package/dist/lib/browser/keyboard-DDFATRUG.mjs.map +0 -7
  160. package/dist/lib/browser/react-surface-H3JRTVNU.mjs +0 -51
  161. package/dist/lib/browser/react-surface-H3JRTVNU.mjs.map +0 -7
  162. package/dist/lib/browser/state-J6AKZRB4.mjs +0 -117
  163. package/dist/lib/browser/state-J6AKZRB4.mjs.map +0 -7
  164. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  165. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  166. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  167. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  168. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  169. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  170. package/dist/types/src/capabilities/keyboard.d.ts +0 -4
  171. package/dist/types/src/capabilities/keyboard.d.ts.map +0 -1
  172. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  173. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  174. package/dist/types/src/capabilities/state.d.ts +0 -20
  175. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  176. package/dist/types/src/events.d.ts +0 -4
  177. package/dist/types/src/events.d.ts.map +0 -1
  178. package/dist/types/src/types.d.ts.map +0 -1
  179. package/src/capabilities/app-graph-builder.ts +0 -52
  180. package/src/capabilities/capabilities.ts +0 -20
  181. package/src/capabilities/intent-resolver.ts +0 -37
  182. package/src/capabilities/keyboard.ts +0 -59
  183. package/src/capabilities/react-surface.tsx +0 -45
  184. package/src/capabilities/state.ts +0 -104
  185. package/src/events.ts +0 -11
@@ -2,71 +2,52 @@
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 { Button, type ButtonProps, Icon, 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, mx } from '@dxos/react-ui-theme';
14
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
15
 
13
- import { useLoadDescendents } from '../../hooks';
14
- import { NAVTREE_PLUGIN } from '../../meta';
15
- import { l0ItemType } from '../../util';
16
+ import { useIsAlternateTree, useLoadDescendents } from '../../hooks';
17
+ import { meta } from '../../meta';
18
+ import { NAV_TREE_ITEM } from '../NavTree';
16
19
  import { useNavTreeContext } from '../NavTreeContext';
17
- import { NavTreeItemColumns } from '../NavTreeItem';
18
-
19
- const headingBackButtonLabel = 'inset-0 min-is-0 truncate flex items-center pis-2';
20
-
21
- const TitleButton = ({ title, onClick }: { title: string } & Pick<ButtonProps, 'onClick'>) => {
22
- return (
23
- <Button
24
- variant='ghost'
25
- classNames='pli-1 flex-1 group text-start justify-start bs-[--rail-action] font-normal'
26
- onClick={onClick}
27
- >
28
- <Icon icon='ph--caret-left--bold' size={3} />
29
- <span className={mx(headingBackButtonLabel)}>{title}</span>
30
- </Button>
31
- );
32
- };
20
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
33
21
 
34
22
  export type L1PanelProps = {
35
23
  open?: boolean;
36
- item: Node<any>;
37
24
  path: string[];
25
+ item: Node.Node;
38
26
  currentItemId: string;
39
27
  onBack?: () => void;
40
28
  };
41
29
 
42
- const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
43
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
44
- const { t } = useTranslation(NAVTREE_PLUGIN);
30
+ /**
31
+ * Space or settings panel.
32
+ */
33
+ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
34
+ const { t } = useTranslation(meta.id);
35
+ const navTreeContext = useNavTreeContext();
36
+ const title = toLocalizedString(item.properties.label, t);
45
37
 
46
38
  // TODO(wittjosiah): Support multiple alternate trees.
47
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
39
+ const { useItems } = navTreeContext;
40
+ const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
48
41
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
49
- const handleOpen = useCallback(() => setAlternateTree?.(alternatePath, true), [alternatePath, setAlternateTree]);
50
- const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
42
+ const isAlternate = useIsAlternateTree(alternatePath, item);
51
43
  const useAlternateItems = useCallback(
52
- (node?: Node, { disposition }: { disposition?: string } = {}) => {
44
+ (node?: Node.Node, { disposition }: { disposition?: string } = {}) => {
53
45
  // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
54
- return navTreeContext.useItems(node, { disposition, sort: node?.id === alternateTree.id });
46
+ return useItems(node, { disposition, sort: node?.id === alternateTree.id });
55
47
  },
56
- [navTreeContext, alternateTree],
48
+ [alternateTree, useItems],
57
49
  );
58
50
 
59
- const handleBack = useCallback(() => {
60
- if (isAlternate) {
61
- setAlternateTree?.(alternatePath, false);
62
- } else {
63
- onBack?.();
64
- }
65
- }, [isAlternate, onBack, alternatePath, setAlternateTree]);
66
-
67
- const title = toLocalizedString(item.properties.label, t);
68
- const backCapable = item.id.startsWith('!') || isAlternate;
69
-
70
51
  return (
71
52
  <Tabs.Tabpanel
72
53
  key={item.id}
@@ -79,97 +60,171 @@ const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
79
60
  ]}
80
61
  tabIndex={-1}
81
62
  aria-label={title}
82
- {...(!open && { inert: 'true' })}
63
+ {...(!open && { inert: true })}
83
64
  >
84
65
  {item.id === currentItemId && (
85
- <>
86
- <h2 className='flex items-center border-be border-subduedSeparator app-drag pli-1'>
87
- {backCapable ? (
88
- <TitleButton title={title} onClick={handleBack} />
89
- ) : (
90
- <span className='flex-1 truncate min-is-0 pis-6'>{title}</span>
91
- )}
92
- {alternateTree && !isAlternate && (
93
- <IconButton
94
- variant='ghost'
95
- classNames={mx('shrink-0', hoverableControlItem, hoverableOpenControlItem, 'pli-2 pointer-fine:pli-1')}
96
- iconOnly
97
- size={5}
98
- density='coarse'
99
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
100
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
101
- data-testid='treeView.alternateTreeButton'
102
- onClick={handleOpen}
103
- />
104
- )}
105
- <NavTreeItemColumns path={path} item={item} open density='coarse' />
106
- </h2>
107
- <div role='none' className='overflow-y-auto'>
108
- {isAlternate ? (
109
- <Tree
110
- {...navTreeContext}
111
- useItems={useAlternateItems}
112
- id={alternateTree.id}
113
- root={alternateTree}
114
- path={alternatePath}
115
- levelOffset={5}
116
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
117
- renderColumns={NavTreeItemColumns}
118
- />
119
- ) : (
120
- <Tree
121
- {...navTreeContext}
122
- id={item.id}
123
- root={item}
124
- path={path}
125
- levelOffset={5}
126
- draggable
127
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
128
- renderColumns={NavTreeItemColumns}
129
- />
130
- )}
131
- </div>
132
- </>
66
+ <DensityProvider density='fine'>
67
+ <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
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>
133
96
  )}
134
97
  </Tabs.Tabpanel>
135
98
  );
136
99
  };
137
100
 
138
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
139
- const { useItems } = useNavTreeContext();
101
+ /**
102
+ * Header row.
103
+ */
104
+ const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
105
+ const { t } = useTranslation(meta.id);
106
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
107
+ const title = toLocalizedString(item.properties.label, t);
108
+
109
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
140
110
  useLoadDescendents(item);
141
- const collectionItems = useItems(item);
142
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
111
+
143
112
  return (
144
- <>
145
- {collectionItems
146
- .filter((item) => l0ItemType(item) === 'tab')
147
- .map((item) => (
148
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
149
- ))}
150
- </>
113
+ <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
114
+ <div className='is-6' />
115
+ <h2 className='flex-1 truncate min-is-0'>{title}</h2>
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 && (
131
+ <IconButton
132
+ size={5}
133
+ density='coarse'
134
+ classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
135
+ variant='ghost'
136
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
137
+ iconOnly
138
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
139
+ data-testid={menuActions[0].properties?.testId}
140
+ onClick={() => onAction(menuActions[0] as Node.Action)}
141
+ />
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>
163
+ </div>
151
164
  );
152
165
  };
153
166
 
154
- export type L1PanelsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
155
- topLevelItems: Node<any>[];
156
- path: string[];
157
- };
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();
158
174
 
159
- export const L1Panels = ({ topLevelItems, onBack, ...props }: L1PanelsProps) => {
160
- return (
161
- <>
162
- {topLevelItems.map((item) => {
163
- const type = l0ItemType(item);
164
- switch (type) {
165
- case 'collection':
166
- return <L1PanelCollection key={item.id} item={item} {...props} />;
167
- case 'tab':
168
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
169
- default:
170
- return null;
171
- }
172
- })}
173
- </>
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,
174
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 };
175
230
  };
@@ -0,0 +1,33 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type Node } from '@dxos/app-graph';
8
+
9
+ import { l0ItemType } from '../../util';
10
+
11
+ import { L1Panel, type L1PanelProps } from './L1Panel';
12
+
13
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
14
+ path: string[];
15
+ topLevelItems: Node.Node[];
16
+ };
17
+
18
+ /**
19
+ * Each workspace is an L1 tab.
20
+ */
21
+ export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
22
+ return (
23
+ <>
24
+ {topLevelItems.map((item) => {
25
+ const type = l0ItemType(item);
26
+ if (type === 'tab') {
27
+ return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
28
+ }
29
+ return null;
30
+ })}
31
+ </>
32
+ );
33
+ };
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './L0Menu';
6
6
  export * from './L1Panel';
7
+ export * from './L1Tabs';
@@ -2,20 +2,21 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
- import { type Meta } from '@storybook/react';
8
-
9
- import { withTheme } from '@dxos/storybook-utils';
7
+ import { withTheme } from '@dxos/react-ui/testing';
10
8
 
11
9
  import { UserAccountAvatar } from './UserAccountAvatar';
12
10
 
13
- const meta: Meta<typeof UserAccountAvatar> = {
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;
20
19
 
21
- export const Default = {};
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {};
@@ -7,6 +7,8 @@ import React from 'react';
7
7
  import { Avatar, type AvatarStatus, type Size } from '@dxos/react-ui';
8
8
  import { hexToFallback } from '@dxos/util';
9
9
 
10
+ import { L0ItemActiveTabIndicator } from '../Sidebar';
11
+
10
12
  export type UserAccountAvatarProps = {
11
13
  size?: Size;
12
14
  userId?: string;
@@ -19,17 +21,23 @@ export const UserAccountAvatar = ({ size, userId, hue, emoji, status }: UserAcco
19
21
  const fallbackValue = hexToFallback(userId ?? '0');
20
22
 
21
23
  return (
22
- <div className='grid place-items-center' data-joyride='welcome/account'>
23
- <Avatar.Root>
24
- <Avatar.Content
25
- variant='circle'
26
- size={size ?? 12}
27
- status={status ?? 'active'}
28
- hue={hue || fallbackValue.hue}
29
- fallback={emoji || fallbackValue.emoji}
30
- data-testid='treeView.userAccount'
31
- />
32
- </Avatar.Root>
33
- </div>
24
+ <>
25
+ <L0ItemActiveTabIndicator classNames='inset-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
- export * from './types';
10
+
11
+ export type * from './types';
@@ -4,33 +4,34 @@
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';
11
11
 
12
- import type { L1PanelProps } from './Sidebar';
13
12
  import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
14
13
 
14
+ import type { L1PanelProps } from './Sidebar';
15
+
15
16
  export type NavTreeContextValue = Pick<
16
17
  TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
17
- 'useItems' | 'getProps' | 'isCurrent' | 'isOpen' | 'canDrop' | 'onOpenChange' | 'onSelect'
18
+ 'useItems' | 'getProps' | 'useIsCurrent' | 'useIsOpen' | 'blockInstruction' | 'canDrop' | 'onOpenChange' | 'onSelect'
18
19
  > &
19
20
  Pick<L1PanelProps, 'onBack'> & {
20
21
  tab: string;
21
22
  topbar?: boolean;
22
23
  popoverAnchorId?: string;
23
- renderItemEnd?: FC<{ node: Node; open: boolean }>;
24
- getActions: (node: Node) => FlattenedActions;
25
- loadDescendents?: (node: Node) => MaybePromise<void>;
26
- isAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;
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;
27
28
  setAlternateTree?: (path: string[], open: boolean) => void;
28
29
  onTabChange?: (node: NavTreeItemGraphNode) => void;
29
30
  };
30
31
 
31
32
  export type NavTreeItemColumnsProps = {
32
33
  path: string[];
33
- item: Node;
34
+ item: Node.Node;
34
35
  open: boolean;
35
36
  density?: Density;
36
37
  };