@dxos/plugin-navtree 0.8.4-main.bc674ce → 0.8.4-main.c351d160a8

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 (165) hide show
  1. package/dist/lib/browser/CommandsDialogContent-2DKIOLJL.mjs +113 -0
  2. package/dist/lib/browser/CommandsDialogContent-2DKIOLJL.mjs.map +7 -0
  3. package/dist/lib/browser/CommandsTrigger-ABODVVMP.mjs +33 -0
  4. package/dist/lib/browser/CommandsTrigger-ABODVVMP.mjs.map +7 -0
  5. package/dist/lib/browser/NavTreeContainer-IQZCK4Y4.mjs +12 -0
  6. package/dist/lib/browser/NavTreeContainer-IQZCK4Y4.mjs.map +7 -0
  7. package/dist/lib/browser/NavTreeDocumentTitle-L6NLLWLD.mjs +26 -0
  8. package/dist/lib/browser/NavTreeDocumentTitle-L6NLLWLD.mjs.map +7 -0
  9. package/dist/lib/browser/{app-graph-builder-RSMA6I22.mjs → app-graph-builder-Z43OTNI2.mjs} +6 -5
  10. package/dist/lib/browser/app-graph-builder-Z43OTNI2.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-YKTYPINZ.mjs → chunk-A4VDCIJC.mjs} +3 -3
  12. package/dist/lib/browser/{chunk-YKTYPINZ.mjs.map → chunk-A4VDCIJC.mjs.map} +1 -1
  13. package/dist/lib/browser/chunk-DINDC6HE.mjs +14 -0
  14. package/dist/lib/browser/chunk-DINDC6HE.mjs.map +7 -0
  15. package/dist/lib/browser/{chunk-AGQDZBPL.mjs → chunk-KF26WA4O.mjs} +5 -5
  16. package/dist/lib/browser/{chunk-AGQDZBPL.mjs.map → chunk-KF26WA4O.mjs.map} +3 -3
  17. package/dist/lib/browser/chunk-OGUYILOW.mjs +1462 -0
  18. package/dist/lib/browser/chunk-OGUYILOW.mjs.map +7 -0
  19. package/dist/lib/browser/index.mjs +35 -31
  20. package/dist/lib/browser/index.mjs.map +3 -3
  21. package/dist/lib/browser/{keyboard-32EH3N4K.mjs → keyboard-HYGFQO7T.mjs} +11 -6
  22. package/dist/lib/browser/keyboard-HYGFQO7T.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/operation-resolver-VDWVUII5.mjs +49 -0
  25. package/dist/lib/browser/operation-resolver-VDWVUII5.mjs.map +7 -0
  26. package/dist/lib/browser/{react-surface-F6CVMS3Y.mjs → react-surface-LFMKIKNY.mjs} +15 -14
  27. package/dist/lib/browser/react-surface-LFMKIKNY.mjs.map +7 -0
  28. package/dist/lib/browser/{state-EVYQOUI3.mjs → state-CWMNNF7K.mjs} +34 -23
  29. package/dist/lib/browser/state-CWMNNF7K.mjs.map +7 -0
  30. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +1 -1
  32. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -1
  33. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +1 -1
  34. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +1 -1
  35. package/dist/types/src/capabilities/operation-resolver/index.d.ts +1 -1
  36. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -1
  37. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +2 -2
  38. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
  39. package/dist/types/src/capabilities/react-surface/index.d.ts +1 -1
  40. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +2 -2
  42. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
  43. package/dist/types/src/capabilities/state/state.d.ts +1 -1
  44. package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
  45. package/dist/types/src/components/NavTree/NavTree.d.ts +10 -32
  46. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  47. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +2 -5
  48. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  50. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  51. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  52. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  53. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +4 -2
  54. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  55. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +1 -1
  56. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  57. package/dist/types/src/components/Sidebar/L0Menu.d.ts +4 -1
  58. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  59. package/dist/types/src/components/Sidebar/L1Panel.d.ts +3 -6
  60. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  61. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +3 -2
  62. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  63. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  64. package/dist/types/src/components/index.d.ts +0 -3
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/components/types.d.ts +5 -13
  67. package/dist/types/src/components/types.d.ts.map +1 -1
  68. package/dist/types/src/{components/CommandsDialog/CommandsDialog.d.ts → containers/CommandsDialogContent/CommandsDialogContent.d.ts} +1 -1
  69. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  70. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +3 -0
  71. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  72. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  73. package/dist/types/src/containers/CommandsTrigger/index.d.ts +3 -0
  74. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  75. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  76. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  77. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  78. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  79. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  80. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +3 -0
  81. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  82. package/dist/types/src/containers/index.d.ts +7 -0
  83. package/dist/types/src/containers/index.d.ts.map +1 -0
  84. package/dist/types/src/experimental/Tree.stories.d.ts +18 -4
  85. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  86. package/dist/types/src/hooks/index.d.ts +5 -0
  87. package/dist/types/src/hooks/index.d.ts.map +1 -0
  88. package/dist/types/src/hooks/useActions.d.ts +5 -0
  89. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  90. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  91. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  93. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  94. package/dist/types/src/{hooks.d.ts → hooks/useNavTreeState.d.ts} +2 -14
  95. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  96. package/dist/types/src/meta.d.ts.map +1 -1
  97. package/dist/types/src/testing/generator.d.ts +3 -3
  98. package/dist/types/src/testing/generator.d.ts.map +1 -1
  99. package/dist/types/src/types/events.d.ts.map +1 -1
  100. package/dist/types/src/util.d.ts +5 -1
  101. package/dist/types/src/util.d.ts.map +1 -1
  102. package/dist/types/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +43 -42
  104. package/src/NavTreePlugin.tsx +22 -24
  105. package/src/capabilities/app-graph-builder/app-graph-builder.ts +4 -3
  106. package/src/capabilities/keyboard/keyboard.ts +6 -5
  107. package/src/capabilities/operation-resolver/operation-resolver.ts +23 -19
  108. package/src/capabilities/react-surface/react-surface.tsx +12 -13
  109. package/src/capabilities/state/state.ts +41 -13
  110. package/src/components/NavTree/NavTree.stories.tsx +20 -18
  111. package/src/components/NavTree/NavTree.tsx +61 -20
  112. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  113. package/src/components/NavTreeContext/index.ts +5 -0
  114. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +2 -2
  115. package/src/components/NavTreeItem/NavTreeItemAction.tsx +40 -23
  116. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +40 -14
  117. package/src/components/Sidebar/L0Menu.tsx +80 -83
  118. package/src/components/Sidebar/L1Panel.tsx +244 -90
  119. package/src/components/Sidebar/L1Tabs.tsx +13 -3
  120. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +2 -3
  121. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
  122. package/src/components/index.ts +0 -3
  123. package/src/components/types.ts +12 -17
  124. package/src/{components/CommandsDialog/CommandsDialog.tsx → containers/CommandsDialogContent/CommandsDialogContent.tsx} +19 -13
  125. package/src/containers/CommandsDialogContent/index.ts +7 -0
  126. package/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.tsx +4 -4
  127. package/src/containers/CommandsTrigger/index.ts +7 -0
  128. package/src/{components → containers/NavTreeContainer}/NavTreeContainer.tsx +75 -141
  129. package/src/containers/NavTreeContainer/index.ts +6 -0
  130. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +1 -1
  131. package/src/containers/NavTreeDocumentTitle/index.ts +7 -0
  132. package/src/containers/index.ts +12 -0
  133. package/src/experimental/Tree.stories.tsx +54 -57
  134. package/src/experimental/Tree.tsx +3 -3
  135. package/src/hooks/index.ts +8 -0
  136. package/src/hooks/useActions.ts +34 -0
  137. package/src/hooks/useLoadDescendents.ts +23 -0
  138. package/src/hooks/useNavTreeModel.ts +118 -0
  139. package/src/{hooks.ts → hooks/useNavTreeState.ts} +3 -34
  140. package/src/meta.ts +2 -2
  141. package/src/testing/app-graph-builder.ts +5 -5
  142. package/src/testing/generator.ts +4 -4
  143. package/src/types/capabilities.ts +1 -1
  144. package/src/types/events.ts +3 -2
  145. package/src/util.ts +18 -4
  146. package/dist/lib/browser/app-graph-builder-RSMA6I22.mjs.map +0 -7
  147. package/dist/lib/browser/chunk-6YPQ625I.mjs +0 -1293
  148. package/dist/lib/browser/chunk-6YPQ625I.mjs.map +0 -7
  149. package/dist/lib/browser/keyboard-32EH3N4K.mjs.map +0 -7
  150. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs +0 -52
  151. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs.map +0 -7
  152. package/dist/lib/browser/react-surface-F6CVMS3Y.mjs.map +0 -7
  153. package/dist/lib/browser/state-EVYQOUI3.mjs.map +0 -7
  154. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  155. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  156. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  157. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  158. package/dist/types/src/components/NavTreeContainer.d.ts +0 -16
  159. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  160. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  161. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  162. package/dist/types/src/hooks.d.ts.map +0 -1
  163. package/src/components/CommandsDialog/index.ts +0 -6
  164. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
  165. /package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +0 -0
@@ -2,150 +2,304 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Option from 'effect/Option';
7
+ import React, { memo, useCallback, useMemo } from 'react';
6
8
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { DensityProvider, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
+ import { Node } from '@dxos/app-graph';
10
+ import { isPinnedWorkspace } from '@dxos/app-toolkit';
11
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
12
+ import { Graph, useActionRunner, useConnections, useEdges } from '@dxos/plugin-graph';
13
+ import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
14
+ import { useAttended } from '@dxos/react-ui-attention';
9
15
  import { Tree } from '@dxos/react-ui-list';
16
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
10
17
  import { Tabs } from '@dxos/react-ui-tabs';
11
18
  import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
19
 
13
- import { useIsAlternateTree } from '../../hooks';
20
+ import { useActions, useIsAlternateTree, useLoadDescendents } from '../../hooks';
14
21
  import { meta } from '../../meta';
22
+ import { NAV_TREE_ITEM } from '../NavTree';
15
23
  import { useNavTreeContext } from '../NavTreeContext';
16
- import { NavTreeItemColumns } from '../NavTreeItem';
24
+ import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
17
25
 
18
26
  export type L1PanelProps = {
19
27
  open?: boolean;
20
28
  path: string[];
21
29
  item: Node.Node;
22
- currentItemId: string;
30
+ isCurrent: boolean;
23
31
  onBack?: () => void;
24
32
  };
25
33
 
26
34
  /**
27
35
  * Space or settings panel.
28
36
  */
29
- export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
37
+ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
30
38
  const { t } = useTranslation(meta.id);
31
- const navTreeContext = useNavTreeContext();
32
39
  const title = toLocalizedString(item.properties.label, t);
33
-
34
- // TODO(wittjosiah): Support multiple alternate trees.
35
- const { useItems } = navTreeContext;
36
- const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
37
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
38
- const isAlternate = useIsAlternateTree(alternatePath, item);
39
- const useAlternateItems = useCallback(
40
- (node?: Node.Node, { disposition }: { disposition?: string } = {}) => {
41
- // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
42
- return useItems(node, { disposition, sort: node?.id === alternateTree.id });
43
- },
44
- [alternateTree, useItems],
45
- );
40
+ const isActivated = useIsActivatedWorkspace(item);
41
+ const shouldRenderContent = isCurrent || isActivated;
46
42
 
47
43
  return (
48
44
  <Tabs.Tabpanel
49
45
  key={item.id}
50
46
  value={item.id}
51
47
  classNames={[
52
- 'absolute inset-block-0 inline-end-0',
53
- 'is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr]',
54
- 'pbs-[env(safe-area-inset-top)]',
55
- item.id === currentItemId && 'grid',
48
+ 'absolute inset-y-0 end-0',
49
+ 'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
50
+ 'py-[env(safe-area-inset-top)]',
51
+ isCurrent && 'grid',
56
52
  ]}
57
53
  tabIndex={-1}
58
54
  aria-label={title}
55
+ {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
59
56
  {...(!open && { inert: true })}
60
57
  >
61
- {item.id === currentItemId && (
62
- <DensityProvider density='fine'>
63
- <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
64
- <div role='none' className='overflow-y-auto'>
65
- {isAlternate ? (
66
- <Tree
67
- {...navTreeContext}
68
- id={alternateTree.id}
69
- root={alternateTree}
70
- path={alternatePath}
71
- levelOffset={5}
72
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
73
- renderColumns={NavTreeItemColumns}
74
- useItems={useAlternateItems}
75
- />
76
- ) : (
77
- <Tree
78
- {...navTreeContext}
79
- id={item.id}
80
- root={item}
81
- path={path}
82
- levelOffset={5}
83
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
84
- renderColumns={NavTreeItemColumns}
85
- draggable
86
- />
87
- )}
88
- </div>
89
- </DensityProvider>
90
- )}
58
+ {shouldRenderContent && <L1PanelContent open={open} path={path} item={item} onBack={onBack} />}
91
59
  </Tabs.Tabpanel>
92
60
  );
93
61
  };
94
62
 
63
+ /** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
64
+ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
65
+ const { graph } = useAppGraph();
66
+ const edges = useEdges(graph, item.id);
67
+
68
+ return useMemo(() => {
69
+ const childIds = edges[Graph.relationKey('child')] ?? [];
70
+ return childIds.some((childId) => {
71
+ const child = Graph.getNode(graph, childId);
72
+ if (Option.isNone(child)) {
73
+ return false;
74
+ }
75
+ return child.value.properties.disposition === undefined;
76
+ });
77
+ }, [edges, graph]);
78
+ };
79
+
95
80
  /**
96
- * Header row.
81
+ * Mounted panel content for active or previously-visited tabs.
97
82
  */
98
- const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
99
- const { t } = useTranslation(meta.id);
100
- const { setAlternateTree, ...navTreeContext } = useNavTreeContext();
101
- const title = toLocalizedString(item.properties.label, t);
83
+ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
84
+ const navTreeContext = useNavTreeContext();
102
85
 
103
- // TODO(wittjosiah): Support multiple alternate trees.
104
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
86
+ const alternateTree = useAlternateTreeItem(item);
105
87
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
106
88
  const isAlternate = useIsAlternateTree(alternatePath, item);
107
- const backCapable = item.id.startsWith('!') || isAlternate;
108
89
 
109
- const handleOpen = useCallback(() => {
110
- setAlternateTree?.(alternatePath, true);
111
- }, [alternatePath, setAlternateTree]);
90
+ return (
91
+ <DensityProvider density='fine'>
92
+ <L1PanelHeader path={path} item={item} onBack={onBack} />
93
+ <ScrollArea.Root thin orientation='vertical'>
94
+ <ScrollArea.Viewport>
95
+ {isAlternate && alternateTree ? (
96
+ <Tree
97
+ model={navTreeContext.model}
98
+ id={alternateTree.id}
99
+ rootId={alternateTree.id}
100
+ path={alternatePath}
101
+ levelOffset={5}
102
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
103
+ renderColumns={NavTreeItemColumns}
104
+ blockInstruction={navTreeContext.blockInstruction}
105
+ canDrop={navTreeContext.canDrop}
106
+ canSelect={navTreeContext.canSelect}
107
+ onOpenChange={navTreeContext.onOpenChange}
108
+ onSelect={navTreeContext.onSelect}
109
+ onItemHover={navTreeContext.onItemHover}
110
+ />
111
+ ) : (
112
+ <Tree
113
+ model={navTreeContext.model}
114
+ id={item.id}
115
+ rootId={item.id}
116
+ path={path}
117
+ levelOffset={5}
118
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
119
+ draggable
120
+ renderColumns={NavTreeItemColumns}
121
+ blockInstruction={navTreeContext.blockInstruction}
122
+ canDrop={navTreeContext.canDrop}
123
+ canSelect={navTreeContext.canSelect}
124
+ onOpenChange={navTreeContext.onOpenChange}
125
+ onSelect={navTreeContext.onSelect}
126
+ onItemHover={navTreeContext.onItemHover}
127
+ />
128
+ )}
129
+ </ScrollArea.Viewport>
130
+ </ScrollArea.Root>
131
+ </DensityProvider>
132
+ );
133
+ };
112
134
 
113
- const handleBack = useCallback(() => {
114
- if (isAlternate) {
115
- setAlternateTree?.(alternatePath, false);
116
- } else {
117
- onBack?.();
135
+ /**
136
+ * Header row.
137
+ */
138
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
139
+ const { t } = useTranslation(meta.id);
140
+ const { renderItemEnd: ItemEnd } = useNavTreeContext();
141
+ const title = toLocalizedString(item.properties.label, t);
142
+ const backCapableWorkspace = isPinnedWorkspace(item.id);
143
+
144
+ const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
145
+ useLoadDescendents(item);
146
+
147
+ // TODO(wittjosiah): Hack to only show containsAttended when attended item is within alternate tree (e.g. settings).
148
+ // Since every object in the space is a descendant of the space node, we'd otherwise always show containsAttended.
149
+ const alternateTree = useAlternateTreeItem(item);
150
+ const attended = useAttended();
151
+ const alternateTreeOpen = useMemo(() => {
152
+ if (!alternateTree) {
153
+ return true;
118
154
  }
119
- }, [isAlternate, alternatePath, setAlternateTree, onBack]);
155
+ const hasAlternateAttended = attended.some(
156
+ (attendedId) => attendedId === alternateTree.id || attendedId.startsWith(alternateTree.id + '/'),
157
+ );
158
+ return !hasAlternateAttended;
159
+ }, [alternateTree, attended]);
120
160
 
121
- // TODO(burdon): Reuse same grid as tree?
122
161
  return (
123
- <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
124
- <div className='is-6' />
125
- <h2 className='flex-1 truncate min-is-0'>{title}</h2>
126
- {(backCapable && (
162
+ <div className='flex w-full items-center border-b border-subdued-separator dx-app-drag dx-density-coarse pe-1'>
163
+ {backCapableWorkspace ? (
127
164
  <IconButton
128
- data-testid='treeView.primaryTreeButton'
165
+ size={5}
166
+ density='coarse'
167
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
129
168
  variant='ghost'
130
- icon='ph--arrow-u-down-left--regular'
169
+ icon='ph--caret-left--regular'
131
170
  iconOnly
132
171
  label={t('button back')}
133
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
134
- onClick={handleBack}
172
+ data-testid='treeView.primaryTreeButton'
173
+ onClick={() => onBack?.()}
135
174
  />
136
- )) ||
137
- (alternateTree && !isAlternate && (
175
+ ) : (
176
+ <div className='w-6' />
177
+ )}
178
+ <h2 className='flex-1 truncate min-w-0'>{title}</h2>
179
+ {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
180
+ <div role='none' className='contents dx-app-no-drag'>
181
+ {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
182
+ <NavTreeItemAction
183
+ testId={primaryAction.properties?.testId}
184
+ label={toLocalizedString(primaryAction.properties?.label, t)}
185
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
186
+ parent={item}
187
+ path={path}
188
+ monolithic={Node.isAction(primaryAction)}
189
+ menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
190
+ menuType={primaryAction.properties?.menuType}
191
+ caller={NAV_TREE_ITEM}
192
+ />
193
+ )}
194
+ {menuActions.length === 1 && (
138
195
  <IconButton
139
- data-testid='treeView.alternateTreeButton'
196
+ size={5}
197
+ density='coarse'
198
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
140
199
  variant='ghost'
141
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
200
+ icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
142
201
  iconOnly
143
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
144
- classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
145
- onClick={handleOpen}
202
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
203
+ data-testid={menuActions[0].properties?.testId}
204
+ onClick={() => onAction(menuActions[0] as Node.Action)}
146
205
  />
147
- ))}
148
- <NavTreeItemColumns open path={path} item={item} density='coarse' />
206
+ )}
207
+ {menuActions.length > 1 && (
208
+ <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
209
+ <Menu.Trigger asChild>
210
+ <IconButton
211
+ size={5}
212
+ density='coarse'
213
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
214
+ variant='ghost'
215
+ icon='ph--dots-three-vertical--regular'
216
+ iconOnly
217
+ label={t('tree item actions label')}
218
+ data-testid='navtree.treeItem.actionsLevel0'
219
+ />
220
+ </Menu.Trigger>
221
+ <Menu.Content group={item} items={menuActions as MenuItem[]} />
222
+ </Menu.Root>
223
+ )}
224
+ {ItemEnd && <ItemEnd node={item} open={alternateTreeOpen} />}
225
+ </div>
149
226
  </div>
150
227
  );
151
228
  };
229
+
230
+ /**
231
+ * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
232
+ */
233
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
234
+ const { t } = useTranslation(meta.id);
235
+ const { setAlternateTree } = useNavTreeContext();
236
+ const { graph } = useAppGraph();
237
+ const runAction = useActionRunner();
238
+
239
+ const alternateTree = useAlternateTreeItem(item);
240
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
241
+ const isAlternate = useIsAlternateTree(alternatePath, item);
242
+
243
+ // Graph actions.
244
+ const { actions: actionsProp, groupedActions } = useActions(item);
245
+ const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
246
+ a.properties?.disposition === 'list-item-primary' ? -1 : 1,
247
+ );
248
+
249
+ const graphMenuActions = (
250
+ primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp
251
+ )
252
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
253
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
254
+
255
+ // Synthetic settings/back action.
256
+ const settingsActionId = `${item.id}~settings`;
257
+ const settingsAction = useMemo((): Node.Action | undefined => {
258
+ if (!alternateTree) {
259
+ return undefined;
260
+ }
261
+ return {
262
+ id: settingsActionId,
263
+ type: Node.ActionType,
264
+ data: () => Effect.void,
265
+ properties: {
266
+ label: isAlternate ? ['button back', { ns: meta.id }] : (alternateTree.properties.label ?? alternateTree.id),
267
+ icon: isAlternate
268
+ ? 'ph--arrow-u-down-left--regular'
269
+ : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
270
+ disposition: 'list-item',
271
+ testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
272
+ },
273
+ } as Node.Action;
274
+ }, [alternateTree, isAlternate, settingsActionId, t]);
275
+
276
+ const menuActions = useMemo(
277
+ () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
278
+ [graphMenuActions, settingsAction],
279
+ );
280
+
281
+ const onAction = useCallback(
282
+ (action: Node.Action, params?: Node.InvokeProps) => {
283
+ if (action.id === settingsActionId) {
284
+ if (alternateTree && !isAlternate) {
285
+ Graph.expand(graph, alternateTree.id, 'child');
286
+ }
287
+ setAlternateTree?.(alternatePath, !isAlternate);
288
+ } else {
289
+ void runAction(action, { ...params, path });
290
+ }
291
+ },
292
+ [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction, graph, alternateTree, path],
293
+ );
294
+
295
+ return { primaryAction, groupedActions, menuActions, onAction };
296
+ };
297
+
298
+ /** Finds the first child with disposition 'alternate-tree' using graph connections directly. */
299
+ const useAlternateTreeItem = (item: Node.Node): Node.Node | undefined => {
300
+ const { graph } = useAppGraph();
301
+ const connections = useConnections(graph, item.id, 'child');
302
+ return connections.find((node) => node.properties.disposition === 'alternate-tree');
303
+ };
304
+
305
+ export const L1Panel = memo(L1Panel$);
@@ -10,7 +10,8 @@ import { l0ItemType } from '../../util';
10
10
 
11
11
  import { L1Panel, type L1PanelProps } from './L1Panel';
12
12
 
13
- export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
13
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
14
+ currentItemId: string;
14
15
  path: string[];
15
16
  topLevelItems: Node.Node[];
16
17
  };
@@ -18,13 +19,22 @@ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'
18
19
  /**
19
20
  * Each workspace is an L1 tab.
20
21
  */
21
- export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
22
+ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
22
23
  return (
23
24
  <>
24
25
  {topLevelItems.map((item) => {
25
26
  const type = l0ItemType(item);
26
27
  if (type === 'tab') {
27
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
28
+ return (
29
+ <L1Panel
30
+ key={item.id}
31
+ item={item}
32
+ path={path}
33
+ open={open}
34
+ onBack={onBack}
35
+ isCurrent={item.id === currentItemId}
36
+ />
37
+ );
28
38
  }
29
39
  return null;
30
40
  })}
@@ -9,9 +9,8 @@ import { withTheme } from '@dxos/react-ui/testing';
9
9
  import { UserAccountAvatar } from './UserAccountAvatar';
10
10
 
11
11
  const meta = {
12
- title: 'plugins/plugin-navtree/UserAccountAvatar',
13
-
14
- decorators: [withTheme],
12
+ title: 'plugins/plugin-navtree/components/UserAccountAvatar',
13
+ decorators: [withTheme()],
15
14
  component: UserAccountAvatar,
16
15
  } satisfies Meta<typeof UserAccountAvatar>;
17
16
 
@@ -22,7 +22,7 @@ export const UserAccountAvatar = ({ size, userId, hue, emoji, status }: UserAcco
22
22
 
23
23
  return (
24
24
  <>
25
- <L0ItemActiveTabIndicator classNames='inset-block-6' />
25
+ <L0ItemActiveTabIndicator classNames='inset-y-6' />
26
26
  <div
27
27
  className='grid place-items-center dx-focus-ring-group-indicator rounded-full'
28
28
  data-joyride='welcome/account'
@@ -2,10 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './CommandsDialog';
6
5
  export * from './NavTree';
7
- export * from './NavTreeContainer';
8
6
  export * from './NavTreeContext';
9
- export * from './NavTreeDocumentTitle';
10
7
 
11
8
  export type * from './types';
@@ -6,28 +6,23 @@ import type { FC } from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/app-graph';
8
8
  import type { Density } from '@dxos/react-ui';
9
- import type { TreeProps } from '@dxos/react-ui-list';
10
- import type { MaybePromise } from '@dxos/util';
9
+ import type { TreeModel, TreeProps } from '@dxos/react-ui-list';
11
10
 
12
- import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
11
+ import type { NavTreeItemGraphNode } from '../types';
13
12
 
14
13
  import type { L1PanelProps } from './Sidebar';
15
14
 
16
- export type NavTreeContextValue = Pick<
17
- TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
18
- 'useItems' | 'getProps' | 'useIsCurrent' | 'useIsOpen' | 'blockInstruction' | 'canDrop' | 'onOpenChange' | 'onSelect'
15
+ export type NavTreeContextValue = {
16
+ model: TreeModel<NavTreeItemGraphNode>;
17
+ popoverAnchorId?: string;
18
+ renderItemEnd?: FC<{ node: Node.Node; open: boolean }>;
19
+ setAlternateTree?: (path: string[], open: boolean) => void;
20
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
21
+ } & Pick<
22
+ TreeProps<NavTreeItemGraphNode>,
23
+ 'blockInstruction' | 'canDrop' | 'canSelect' | 'onOpenChange' | 'onSelect' | 'onItemHover'
19
24
  > &
20
- Pick<L1PanelProps, 'onBack'> & {
21
- tab: string;
22
- topbar?: boolean;
23
- popoverAnchorId?: string;
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
- setAlternateTree?: (path: string[], open: boolean) => void;
29
- onTabChange?: (node: NavTreeItemGraphNode) => void;
30
- };
25
+ Pick<L1PanelProps, 'onBack'>;
31
26
 
32
27
  export type NavTreeItemColumnsProps = {
33
28
  path: string[];
@@ -4,8 +4,9 @@
4
4
 
5
5
  import React, { forwardRef, useMemo, useState } from 'react';
6
6
 
7
- import { Common } from '@dxos/app-framework';
8
- import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
9
10
  import { Keyboard, keySymbols } from '@dxos/keyboard';
10
11
  import { Graph, Node, useActionRunner } from '@dxos/plugin-graph';
11
12
  import { useActions } from '@dxos/plugin-graph';
@@ -37,6 +38,7 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
37
38
  const actionMap = new Set<string>();
38
39
  const actions: Node.ActionLike[] = [];
39
40
  Graph.traverse(graph, {
41
+ relation: ['child', 'action'],
40
42
  visitor: (node, path) => {
41
43
  if (
42
44
  (Node.isAction(node) || Node.isActionGroup(node)) &&
@@ -69,11 +71,11 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
69
71
 
70
72
  return (
71
73
  <Dialog.Content ref={forwardedRef}>
72
- <Dialog.Title>{t('commands dialog title', { ns: meta.id })}</Dialog.Title>
73
- <SearchList.Root onSearch={handleSearch}>
74
- <div aria-label={t('command list input placeholder')} role='combobox' aria-expanded='true'>
75
- <SearchList.Input placeholder={t('command list input placeholder')} />
74
+ <Dialog.Title srOnly>{t('commands dialog title', { ns: meta.id })}</Dialog.Title>
75
+ <Dialog.Body>
76
+ <SearchList.Root onSearch={handleSearch}>
76
77
  <SearchList.Content>
78
+ <SearchList.Input placeholder={t('command list input placeholder')} />
77
79
  <SearchList.Viewport>
78
80
  {results.map((action) => {
79
81
  const shortcut =
@@ -98,9 +100,13 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
98
100
  return;
99
101
  }
100
102
 
101
- invokeSync(Common.LayoutOperation.UpdateDialog, { state: false });
103
+ invokeSync(LayoutOperation.UpdateDialog, { state: false });
102
104
  setTimeout(() => {
103
- const node = Graph.getConnections(graph, group?.id ?? action.id, 'inbound')[0];
105
+ const node = Graph.getConnections(
106
+ graph,
107
+ group?.id ?? action.id,
108
+ Node.childRelation('inbound'),
109
+ )[0];
104
110
  if (node && Node.isAction(action)) {
105
111
  void runAction(action, { parent: node, caller: KEY_BINDING });
106
112
  }
@@ -116,13 +122,13 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
116
122
  })}
117
123
  </SearchList.Viewport>
118
124
  </SearchList.Content>
119
- </div>
120
- </SearchList.Root>
121
- <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
125
+ </SearchList.Root>
126
+ </Dialog.Body>
127
+ <Dialog.ActionBar>
122
128
  <Dialog.Close asChild>
123
- <Button classNames='is-full'>{t('close label', { ns: osTranslations })}</Button>
129
+ <Button classNames='w-full'>{t('close label', { ns: osTranslations })}</Button>
124
130
  </Dialog.Close>
125
- </div>
131
+ </Dialog.ActionBar>
126
132
  </Dialog.Content>
127
133
  );
128
134
  },
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CommandsDialogContent } from './CommandsDialogContent';
6
+
7
+ export default CommandsDialogContent;
@@ -4,8 +4,8 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { Common } from '@dxos/app-framework';
8
- import { useOperationInvoker } from '@dxos/app-framework/react';
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
9
  import { Button, Icon, useTranslation } from '@dxos/react-ui';
10
10
 
11
11
  import { COMMANDS_DIALOG, meta } from '../../meta';
@@ -16,8 +16,8 @@ export const CommandsTrigger = () => {
16
16
  const { t } = useTranslation(meta.id);
17
17
  return (
18
18
  <Button
19
- classNames='m-1 pli-1 lg:pli-2'
20
- onClick={() => invokeSync(Common.LayoutOperation.UpdateDialog, { subject: COMMANDS_DIALOG, blockAlign: 'start' })}
19
+ classNames='m-1 px-1 lg:px-2'
20
+ onClick={() => invokeSync(LayoutOperation.UpdateDialog, { subject: COMMANDS_DIALOG, blockAlign: 'start' })}
21
21
  >
22
22
  <span className='text-description font-normal grow text-start'>{t('command list input placeholder')}</span>
23
23
  <Icon icon='ph--magnifying-glass--regular' size={5} />
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { CommandsTrigger } from './CommandsTrigger';
6
+
7
+ export default CommandsTrigger;