@dxos/plugin-navtree 0.10.0 → 0.11.1

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 (187) hide show
  1. package/PLUGIN.mdl +8 -6
  2. package/dist/lib/NavTreePlugin.mjs +40 -0
  3. package/dist/lib/NavTreePlugin.mjs.map +1 -0
  4. package/dist/lib/capabilities.mjs +11 -0
  5. package/dist/lib/capabilities.mjs.map +1 -0
  6. package/dist/lib/chunk-CommandsDialogContent.mjs +100 -0
  7. package/dist/lib/chunk-CommandsDialogContent.mjs.map +1 -0
  8. package/dist/lib/chunk-CommandsTrigger.mjs +27 -0
  9. package/dist/lib/chunk-CommandsTrigger.mjs.map +1 -0
  10. package/dist/lib/chunk-NavTreeDocumentTitle.mjs +17 -0
  11. package/dist/lib/chunk-NavTreeDocumentTitle.mjs.map +1 -0
  12. package/dist/lib/chunk-app-graph-builder.mjs +33 -0
  13. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
  14. package/dist/lib/chunk-expose.mjs +31 -0
  15. package/dist/lib/chunk-expose.mjs.map +1 -0
  16. package/dist/lib/chunk-keyboard.mjs +49 -0
  17. package/dist/lib/chunk-keyboard.mjs.map +1 -0
  18. package/dist/lib/{neutral/chunk-IRQMJLRY.mjs → chunk-meta.mjs} +14 -23
  19. package/dist/lib/chunk-meta.mjs.map +1 -0
  20. package/dist/lib/chunk-operation-handler.mjs +11 -0
  21. package/dist/lib/chunk-operation-handler.mjs.map +1 -0
  22. package/dist/lib/chunk-react-surface.mjs +46 -0
  23. package/dist/lib/chunk-react-surface.mjs.map +1 -0
  24. package/dist/lib/chunk-state.mjs +106 -0
  25. package/dist/lib/chunk-state.mjs.map +1 -0
  26. package/dist/lib/chunk-types.mjs +26 -0
  27. package/dist/lib/chunk-types.mjs.map +1 -0
  28. package/dist/lib/chunk-util.mjs +51 -0
  29. package/dist/lib/chunk-util.mjs.map +1 -0
  30. package/dist/lib/components.mjs +723 -0
  31. package/dist/lib/components.mjs.map +1 -0
  32. package/dist/lib/containers.mjs +231 -0
  33. package/dist/lib/containers.mjs.map +1 -0
  34. package/dist/lib/hooks.mjs +162 -0
  35. package/dist/lib/hooks.mjs.map +1 -0
  36. package/dist/lib/index.mjs +4 -0
  37. package/dist/lib/meta.mjs +2 -0
  38. package/dist/lib/operations.mjs +7 -0
  39. package/dist/lib/operations.mjs.map +1 -0
  40. package/dist/lib/plugin.mjs +9 -0
  41. package/dist/lib/plugin.mjs.map +1 -0
  42. package/dist/lib/testing.mjs +397 -0
  43. package/dist/lib/testing.mjs.map +1 -0
  44. package/dist/lib/translations.mjs +17 -0
  45. package/dist/lib/translations.mjs.map +1 -0
  46. package/dist/lib/types.mjs +2 -0
  47. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  48. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
  49. package/dist/types/src/capabilities/keyboard.d.ts +1 -1
  50. package/dist/types/src/capabilities/nav-tree-view-state.d.ts +12 -0
  51. package/dist/types/src/capabilities/nav-tree-view-state.d.ts.map +1 -0
  52. package/dist/types/src/capabilities/nav-tree-view-state.test.d.ts +2 -0
  53. package/dist/types/src/capabilities/nav-tree-view-state.test.d.ts.map +1 -0
  54. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  55. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  56. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  57. package/dist/types/src/capabilities/state.d.ts +1 -1
  58. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  59. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +3 -0
  60. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  61. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -1
  62. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +1 -1
  63. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  64. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/L1Panel.d.ts +5 -2
  66. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  67. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  68. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -1
  69. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +2 -0
  70. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -1
  71. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts +4 -1
  72. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -1
  73. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -1
  74. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts +5 -2
  75. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -1
  76. package/dist/types/src/experimental/Tree.d.ts +0 -1
  77. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  78. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  79. package/dist/types/src/experimental/visit-nodes.d.ts +3 -0
  80. package/dist/types/src/experimental/visit-nodes.d.ts.map +1 -0
  81. package/dist/types/src/hooks/useActions.d.ts +3 -1
  82. package/dist/types/src/hooks/useActions.d.ts.map +1 -1
  83. package/dist/types/src/translations.d.ts +2 -0
  84. package/dist/types/src/translations.d.ts.map +1 -1
  85. package/dist/types/src/types/NavTreeCapabilities.d.ts +1 -1
  86. package/dist/types/src/types/NavTreeCapabilities.d.ts.map +1 -1
  87. package/dist/types/src/types/schema.d.ts +1 -1
  88. package/dist/types/src/types/schema.d.ts.map +1 -1
  89. package/dist/types/src/util.d.ts.map +1 -1
  90. package/dist/types/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +47 -52
  92. package/src/NavTreePlugin.tsx +3 -1
  93. package/src/capabilities/nav-tree-view-state.test.ts +55 -0
  94. package/src/capabilities/nav-tree-view-state.ts +19 -0
  95. package/src/capabilities/{react-surface.tsx → react-surface.ts} +8 -9
  96. package/src/capabilities/state.ts +47 -38
  97. package/src/components/NavTree/NavTree.stories.tsx +26 -5
  98. package/src/components/NavTree/NavTree.tsx +10 -13
  99. package/src/components/NavTreeItem/NavTreeItemAction.tsx +1 -1
  100. package/src/components/Sidebar/L0Menu.tsx +9 -3
  101. package/src/components/Sidebar/L1Panel.tsx +52 -20
  102. package/src/components/Sidebar/L1Tabs.tsx +6 -0
  103. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +49 -44
  104. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +2 -0
  105. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +17 -18
  106. package/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.tsx +2 -0
  107. package/src/experimental/Tree.stories.tsx +2 -1
  108. package/src/experimental/Tree.tsx +2 -18
  109. package/src/experimental/visit-nodes.ts +27 -0
  110. package/src/hooks/useActions.ts +15 -4
  111. package/src/hooks/useNavTreeModel.ts +1 -1
  112. package/src/operations/expose.ts +2 -2
  113. package/src/testing/app-graph-builder.ts +1 -1
  114. package/src/translations.ts +3 -0
  115. package/src/types/NavTreeCapabilities.ts +1 -1
  116. package/src/types/schema.ts +1 -1
  117. package/src/util.ts +7 -9
  118. package/dist/lib/neutral/CommandsDialogContent-TEKBCQ3U.mjs +0 -112
  119. package/dist/lib/neutral/CommandsDialogContent-TEKBCQ3U.mjs.map +0 -7
  120. package/dist/lib/neutral/CommandsTrigger-HGCI22SP.mjs +0 -27
  121. package/dist/lib/neutral/CommandsTrigger-HGCI22SP.mjs.map +0 -7
  122. package/dist/lib/neutral/NavTreeContainer-ICDKC2GI.mjs +0 -12
  123. package/dist/lib/neutral/NavTreeContainer-ICDKC2GI.mjs.map +0 -7
  124. package/dist/lib/neutral/NavTreeDocumentTitle-RMERXZGK.mjs +0 -22
  125. package/dist/lib/neutral/NavTreeDocumentTitle-RMERXZGK.mjs.map +0 -7
  126. package/dist/lib/neutral/NavTreePlugin.mjs +0 -68
  127. package/dist/lib/neutral/NavTreePlugin.mjs.map +0 -7
  128. package/dist/lib/neutral/NavTreePlugin.node.mjs +0 -18
  129. package/dist/lib/neutral/NavTreePlugin.node.mjs.map +0 -7
  130. package/dist/lib/neutral/NavTreePlugin.workerd.mjs +0 -16
  131. package/dist/lib/neutral/NavTreePlugin.workerd.mjs.map +0 -7
  132. package/dist/lib/neutral/app-graph-builder-2DYAIROU.mjs +0 -42
  133. package/dist/lib/neutral/app-graph-builder-2DYAIROU.mjs.map +0 -7
  134. package/dist/lib/neutral/capabilities/index.mjs +0 -17
  135. package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
  136. package/dist/lib/neutral/chunk-4RN6TH36.mjs +0 -292
  137. package/dist/lib/neutral/chunk-4RN6TH36.mjs.map +0 -7
  138. package/dist/lib/neutral/chunk-BAVI2K7A.mjs +0 -27
  139. package/dist/lib/neutral/chunk-BAVI2K7A.mjs.map +0 -7
  140. package/dist/lib/neutral/chunk-EAN4K6OL.mjs +0 -8
  141. package/dist/lib/neutral/chunk-EAN4K6OL.mjs.map +0 -7
  142. package/dist/lib/neutral/chunk-IRQMJLRY.mjs.map +0 -7
  143. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  144. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  145. package/dist/lib/neutral/chunk-JOSNCOV4.mjs +0 -79
  146. package/dist/lib/neutral/chunk-JOSNCOV4.mjs.map +0 -7
  147. package/dist/lib/neutral/components/index.mjs +0 -814
  148. package/dist/lib/neutral/components/index.mjs.map +0 -7
  149. package/dist/lib/neutral/containers/index.mjs +0 -20
  150. package/dist/lib/neutral/containers/index.mjs.map +0 -7
  151. package/dist/lib/neutral/expose-HIBA2NQG.mjs +0 -38
  152. package/dist/lib/neutral/expose-HIBA2NQG.mjs.map +0 -7
  153. package/dist/lib/neutral/hooks/index.mjs +0 -181
  154. package/dist/lib/neutral/hooks/index.mjs.map +0 -7
  155. package/dist/lib/neutral/index.mjs +0 -34
  156. package/dist/lib/neutral/index.mjs.map +0 -7
  157. package/dist/lib/neutral/keyboard-JM2HCF5N.mjs +0 -58
  158. package/dist/lib/neutral/keyboard-JM2HCF5N.mjs.map +0 -7
  159. package/dist/lib/neutral/meta.json +0 -1
  160. package/dist/lib/neutral/meta.mjs +0 -12
  161. package/dist/lib/neutral/meta.mjs.map +0 -7
  162. package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs +0 -13
  163. package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs.map +0 -7
  164. package/dist/lib/neutral/operations/index.mjs +0 -8
  165. package/dist/lib/neutral/operations/index.mjs.map +0 -7
  166. package/dist/lib/neutral/plugin.mjs +0 -16
  167. package/dist/lib/neutral/plugin.mjs.map +0 -7
  168. package/dist/lib/neutral/react-surface-QPMUEDZX.mjs +0 -48
  169. package/dist/lib/neutral/react-surface-QPMUEDZX.mjs.map +0 -7
  170. package/dist/lib/neutral/state-PDZU4PIP.mjs +0 -126
  171. package/dist/lib/neutral/state-PDZU4PIP.mjs.map +0 -7
  172. package/dist/lib/neutral/testing/index.mjs +0 -439
  173. package/dist/lib/neutral/testing/index.mjs.map +0 -7
  174. package/dist/lib/neutral/translations.mjs +0 -23
  175. package/dist/lib/neutral/translations.mjs.map +0 -7
  176. package/dist/lib/neutral/types/index.mjs +0 -10
  177. package/dist/lib/neutral/types/index.mjs.map +0 -7
  178. package/dist/types/src/NavTreePlugin.node.d.ts +0 -4
  179. package/dist/types/src/NavTreePlugin.node.d.ts.map +0 -1
  180. package/dist/types/src/NavTreePlugin.test.d.ts +0 -2
  181. package/dist/types/src/NavTreePlugin.test.d.ts.map +0 -1
  182. package/dist/types/src/NavTreePlugin.workerd.d.ts +0 -4
  183. package/dist/types/src/NavTreePlugin.workerd.d.ts.map +0 -1
  184. package/src/NavTreePlugin.node.ts +0 -17
  185. package/src/NavTreePlugin.test.ts +0 -27
  186. package/src/NavTreePlugin.workerd.ts +0 -16
  187. /package/src/components/NavTreeContext/{NavTreeContext.tsx → NavTreeContext.ts} +0 -0
@@ -6,11 +6,12 @@ import * as Option from 'effect/Option';
6
6
  import React, { memo, useCallback, useMemo } from 'react';
7
7
 
8
8
  import { Node } from '@dxos/app-graph';
9
- import { Paths } from '@dxos/app-toolkit';
9
+ import { GraphPath } from '@dxos/app-toolkit';
10
10
  import { useAppGraph } from '@dxos/app-toolkit/ui';
11
- import { Graph, useActionRunner, useEdges } from '@dxos/plugin-graph';
11
+ import { Graph } from '@dxos/plugin-graph';
12
+ import { useActionRunner, useEdges } from '@dxos/plugin-graph/hooks';
12
13
  import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
13
- import { Tree } from '@dxos/react-ui-list';
14
+ import { Empty, Tree } from '@dxos/react-ui-list';
14
15
  import { Menu, type MenuItem } from '@dxos/react-ui-menu';
15
16
  import { Tabs } from '@dxos/react-ui-tabs';
16
17
  import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
@@ -22,27 +23,38 @@ import { NAV_TREE_ITEM } from '../NavTree';
22
23
  import { useNavTreeContext } from '../NavTreeContext';
23
24
  import { NavTreeItemColumns } from '../NavTreeItem/NavTreeItemColumns';
24
25
 
26
+ /**
27
+ * Delay before the unavailable-workspace message appears: a workspace whose space is still loading has
28
+ * no graph node yet and materializes into a real panel on its own, so it must not flash during that window.
29
+ */
30
+ const RENDER_DELAY = '1s';
31
+
25
32
  export type L1PanelProps = {
26
33
  open?: boolean;
27
34
  path: string[];
28
- item: Node.Node;
35
+ /** The tab's workspace id, which may name a workspace that has no graph node. */
36
+ id: string;
37
+ /** Absent when the workspace is not in the graph; the panel then renders the unavailable message. */
38
+ item?: Node.Node;
29
39
  isCurrent: boolean;
30
40
  onBack?: () => void;
31
41
  };
32
42
 
33
43
  /**
34
- * Space or settings panel.
44
+ * Space or settings panel. Without an `item` — a link to a workspace this identity never had or which no
45
+ * longer exists, or persisted deck state pointing at one after a profile switch — the panel body is the
46
+ * unavailable-workspace message, so the sidebar is never blank.
35
47
  */
36
- const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
48
+ const L1PanelInner = ({ open, path, id, item, isCurrent, onBack }: L1PanelProps) => {
37
49
  const { t } = useTranslation(meta.profile.key);
38
- const title = toLocalizedString(item.properties.label, t);
39
- const isActivated = useIsActivatedWorkspace(item);
50
+ const title = item ? toLocalizedString(item.properties.label, t) : t('workspace-unavailable.heading');
51
+ const isActivated = useIsActivatedWorkspace(id);
40
52
  const shouldRenderContent = isCurrent || isActivated;
41
53
 
42
54
  return (
43
55
  <Tabs.Panel
44
- key={item.id}
45
- value={item.id}
56
+ key={id}
57
+ value={id}
46
58
  classNames={[
47
59
  'absolute inset-y-0 end-0',
48
60
  'w-[calc(100%-var(--dx-l0-size))] lg:w-(--dx-l1-size) grid-cols-1 grid-rows-[var(--dx-rail-size)_1fr]',
@@ -51,18 +63,34 @@ const L1Panel$ = ({ open, path, item, isCurrent, onBack }: L1PanelProps) => {
51
63
  ]}
52
64
  tabIndex={-1}
53
65
  aria-label={title}
54
- {...(isCurrent && { 'data-testid': 'navtree.workspace.visible' })}
66
+ // An unavailable workspace has no tab in the rail, so the generated `aria-labelledby` would
67
+ // reference a missing element.
68
+ {...(!item && { 'aria-labelledby': undefined })}
69
+ {...(isCurrent && {
70
+ 'data-testid': item ? 'navtree.workspace.visible' : 'navtree.workspace.unavailable',
71
+ })}
55
72
  {...(!open && { inert: true })}
56
73
  >
57
- {shouldRenderContent && <L1PanelContent open={open} path={path} item={item} onBack={onBack} />}
74
+ {shouldRenderContent &&
75
+ (item ? (
76
+ <L1PanelContent open={open} path={path} item={item} onBack={onBack} />
77
+ ) : (
78
+ <Empty
79
+ label={t('workspace-unavailable.description')}
80
+ // Second grid row, so the message clears the rail exactly as the tree does, and hugging its
81
+ // top rather than stretching to the row's full height.
82
+ classNames='row-start-2 self-start animate-fade-in'
83
+ style={{ animationDelay: RENDER_DELAY, animationFillMode: 'backwards' }}
84
+ />
85
+ ))}
58
86
  </Tabs.Panel>
59
87
  );
60
88
  };
61
89
 
62
90
  /** Determines whether a workspace tab has been populated with real child content (i.e. expanded at least once). */
63
- const useIsActivatedWorkspace = (item: Node.Node): boolean => {
91
+ const useIsActivatedWorkspace = (id: string): boolean => {
64
92
  const { graph } = useAppGraph();
65
- const edges = useEdges(graph, item.id);
93
+ const edges = useEdges(graph, id);
66
94
 
67
95
  return useMemo(() => {
68
96
  const childIds = edges[Graph.relationKey('child')] ?? [];
@@ -79,13 +107,17 @@ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
79
107
  /**
80
108
  * Mounted panel content for active or previously-visited tabs.
81
109
  */
82
- const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
110
+ const L1PanelContent = ({
111
+ path,
112
+ item,
113
+ onBack,
114
+ }: Pick<L1PanelProps, 'open' | 'path' | 'onBack'> & { item: Node.Node }) => {
83
115
  const navTreeContext = useNavTreeContext();
84
116
 
85
117
  return (
86
118
  <DensityProvider density='md'>
87
119
  <L1PanelHeader path={path} item={item} onBack={onBack} />
88
- <ScrollArea.Root thin orientation='vertical'>
120
+ <ScrollArea.Root centered thin orientation='vertical'>
89
121
  <ScrollArea.Viewport>
90
122
  <Tree
91
123
  classNames='pt-[2px]'
@@ -113,11 +145,11 @@ const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'pat
113
145
  /**
114
146
  * Header row.
115
147
  */
116
- const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path' | 'onBack'>) => {
148
+ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'path' | 'onBack'> & { item: Node.Node }) => {
117
149
  const { t } = useTranslation(meta.profile.key);
118
150
  const { renderItemEnd: ItemEnd } = useNavTreeContext();
119
151
  const title = toLocalizedString(item.properties.label, t);
120
- const backCapableWorkspace = Paths.isPinnedWorkspace(item.id);
152
+ const backCapableWorkspace = GraphPath.isPinnedWorkspace(item.id);
121
153
 
122
154
  const { menuActions, onAction } = useL1MenuActions({ item, path });
123
155
  useLoadDescendents(item);
@@ -211,7 +243,7 @@ const MenuActions = ({
211
243
  /**
212
244
  * Builds the menu actions for the L1 panel header.
213
245
  */
214
- const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>): L1MenuActions => {
246
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'path'> & { item: Node.Node }): L1MenuActions => {
215
247
  const runAction = useActionRunner();
216
248
 
217
249
  const menuActions = getListActions(useActions(item));
@@ -226,4 +258,4 @@ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>):
226
258
  return { menuActions, onAction };
227
259
  };
228
260
 
229
- export const L1Panel = memo(L1Panel$);
261
+ export const L1Panel = memo(L1PanelInner);
@@ -19,6 +19,10 @@ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'onBack'> & {
19
19
  * Each workspace is an L1 tab.
20
20
  */
21
21
  export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1TabsProps) => {
22
+ // The current tab can name a workspace that is not in the graph, in which case it gets an item-less
23
+ // panel carrying the unavailable message rather than no panel at all (a blank sidebar).
24
+ const hasCurrentPanel = topLevelItems.some((item) => item.id === currentItemId && l0ItemType(item) === 'tab');
25
+
22
26
  return (
23
27
  <>
24
28
  {topLevelItems.map((item) => {
@@ -27,6 +31,7 @@ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1T
27
31
  return (
28
32
  <L1Panel
29
33
  key={item.id}
34
+ id={item.id}
30
35
  item={item}
31
36
  path={path}
32
37
  open={open}
@@ -37,6 +42,7 @@ export const L1Tabs = ({ topLevelItems, currentItemId, onBack, open, path }: L1T
37
42
  }
38
43
  return null;
39
44
  })}
45
+ {!hasCurrentPanel && <L1Panel key={currentItemId} id={currentItemId} path={path} open={open} isCurrent />}
40
46
  </>
41
47
  );
42
48
  };
@@ -8,8 +8,9 @@ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
8
  import { LayoutOperation } from '@dxos/app-toolkit';
9
9
  import { useAppGraph } from '@dxos/app-toolkit/ui';
10
10
  import { Keyboard, keySymbols } from '@dxos/keyboard';
11
- import { Graph, Node, useActionRunner } from '@dxos/plugin-graph';
12
- import { useActions } from '@dxos/plugin-graph';
11
+ import { Graph, Node } from '@dxos/plugin-graph';
12
+ import { useActions } from '@dxos/plugin-graph/hooks';
13
+ import { useActionRunner } from '@dxos/plugin-graph/hooks';
13
14
  import { Button, Dialog, toLocalizedString, useTranslation } from '@dxos/react-ui';
14
15
  import { SearchList, useSearchListResults } from '@dxos/react-ui-search';
15
16
  import { osTranslations } from '@dxos/ui-theme';
@@ -71,51 +72,53 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
71
72
  return (
72
73
  <Dialog.Content ref={forwardedRef}>
73
74
  <Dialog.Title srOnly>{t('commands-dialog.title', { ns: meta.profile.key })}</Dialog.Title>
74
- <SearchList.Root onSearch={handleSearch}>
75
- <SearchList.Input placeholder={t('command-list-input.placeholder')} />
76
- <SearchList.Viewport>
77
- {results.map((action) => {
78
- const shortcut =
79
- typeof action.properties.keyBinding === 'string'
80
- ? action.properties.keyBinding
81
- : action.properties.keyBinding?.[getHostPlatform()];
75
+ <Dialog.Body>
76
+ <SearchList.Root onSearch={handleSearch}>
77
+ <SearchList.Input placeholder={t('command-list-input.placeholder')} />
78
+ <SearchList.Viewport>
79
+ {results.map((action) => {
80
+ const shortcut =
81
+ typeof action.properties.keyBinding === 'string'
82
+ ? action.properties.keyBinding
83
+ : action.properties.keyBinding?.[getHostPlatform()];
82
84
 
83
- return (
84
- <SearchList.Item
85
- value={action.id}
86
- key={action.id}
87
- label={toLocalizedString(action.properties.label, t)}
88
- icon={action.properties.icon}
89
- suffix={shortcut ? keySymbols(shortcut).join('') : undefined}
90
- onSelect={() => {
91
- if (action.properties.disabled) {
92
- return;
93
- }
94
-
95
- if (Node.isActionGroup(action)) {
96
- setSelected(action.id);
97
- return;
98
- }
85
+ return (
86
+ <SearchList.Item
87
+ value={action.id}
88
+ key={action.id}
89
+ label={toLocalizedString(action.properties.label, t)}
90
+ icon={action.properties.icon}
91
+ suffix={shortcut ? keySymbols(shortcut).join('') : undefined}
92
+ onSelect={() => {
93
+ if (action.properties.disabled) {
94
+ return;
95
+ }
99
96
 
100
- void invokePromise(LayoutOperation.UpdateDialog, { state: false });
101
- setTimeout(() => {
102
- const lookupId = group?.id ?? action.id;
103
- const node = Graph.getConnections(graph, lookupId, Node.actionRelation('inbound'))[0];
104
- if (node && Node.isAction(action)) {
105
- void runAction(action, { parent: node, caller: KEY_BINDING });
97
+ if (Node.isActionGroup(action)) {
98
+ setSelected(action.id);
99
+ return;
106
100
  }
107
- });
108
- }}
109
- classNames='flex items-center gap-2'
110
- disabled={action.properties.disabled}
111
- {...(action.properties?.testId && {
112
- 'data-testid': action.properties.testId,
113
- })}
114
- />
115
- );
116
- })}
117
- </SearchList.Viewport>
118
- </SearchList.Root>
101
+
102
+ void invokePromise(LayoutOperation.UpdateDialog, { state: false });
103
+ setTimeout(() => {
104
+ const lookupId = group?.id ?? action.id;
105
+ const node = Graph.getConnections(graph, lookupId, Node.actionRelation('inbound'))[0];
106
+ if (node && Node.isAction(action)) {
107
+ void runAction(action, { parent: node, caller: KEY_BINDING });
108
+ }
109
+ });
110
+ }}
111
+ classNames='flex items-center gap-2'
112
+ disabled={action.properties.disabled}
113
+ {...(action.properties?.testId && {
114
+ 'data-testid': action.properties.testId,
115
+ })}
116
+ />
117
+ );
118
+ })}
119
+ </SearchList.Viewport>
120
+ </SearchList.Root>
121
+ </Dialog.Body>
119
122
  <Dialog.ActionBar>
120
123
  <Dialog.Close asChild>
121
124
  <Button classNames='w-full'>{t('close.label', { ns: osTranslations })}</Button>
@@ -125,3 +128,5 @@ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogCo
125
128
  );
126
129
  },
127
130
  );
131
+
132
+ CommandsDialogContent.displayName = 'CommandsDialogContent';
@@ -26,3 +26,5 @@ export const CommandsTrigger = () => {
26
26
  </Button>
27
27
  );
28
28
  };
29
+
30
+ CommandsTrigger.displayName = 'CommandsTrigger';
@@ -10,7 +10,8 @@ import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from
10
10
  import { Surface, useOperationInvoker } from '@dxos/app-framework/ui';
11
11
  import { LayoutOperation } from '@dxos/app-toolkit';
12
12
  import { AppSurface, useAppGraph, useLayout } from '@dxos/app-toolkit/ui';
13
- import { Graph, Node, useActionRunner } from '@dxos/plugin-graph';
13
+ import { Graph, Node } from '@dxos/plugin-graph';
14
+ import { useActionRunner } from '@dxos/plugin-graph/hooks';
14
15
  import { useMediaQuery, useSidebars } from '@dxos/react-ui';
15
16
  import { type TreeData, isTreeData } from '@dxos/react-ui-list';
16
17
  import { arrayMove } from '@dxos/util';
@@ -102,15 +103,11 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
102
103
  if (activeItems.length === 0) {
103
104
  const [item] = getItems(graph, node).filter((node) => !Node.isActionLike(node));
104
105
  if (item && item.data) {
105
- if (layout.mode === 'multi') {
106
- void invokePromise(LayoutOperation.Set, { subject: [item.id] });
107
- } else {
108
- void invokePromise(LayoutOperation.Open, { subject: [item.id] });
109
- }
106
+ void invokePromise(LayoutOperation.Open, { subject: [item.id] });
110
107
  }
111
108
  }
112
109
  },
113
- [invokePromise, graph, layout.mode],
110
+ [invokePromise, graph],
114
111
  );
115
112
 
116
113
  const blockInstruction = useCallback(
@@ -129,7 +126,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
129
126
  }, []);
130
127
 
131
128
  const handleSelect = useCallback(
132
- ({ item: node, path, option }: { item: Node.Node; path: string[]; option: boolean }) => {
129
+ ({ item: node, path, option, shift }: { item: Node.Node; path: string[]; option: boolean; shift: boolean }) => {
133
130
  if (!node.data) {
134
131
  return;
135
132
  }
@@ -144,20 +141,20 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
144
141
 
145
142
  const current = getItem(path).current;
146
143
  if (!current) {
147
- if (layout.mode === 'multi') {
148
- void invokePromise(LayoutOperation.Set, { subject: [node.id] });
149
- } else {
150
- void invokePromise(LayoutOperation.Open, { subject: [node.id], key: node.properties.key });
151
- }
144
+ // Plain click navigates (the deck becomes this item); shift forces a new plank (see the Open
145
+ // handler, which upgrades any disposition to add when shift is held).
146
+ void invokePromise(LayoutOperation.Open, {
147
+ subject: [node.id],
148
+ disposition: 'solo',
149
+ modifiers: { shift },
150
+ });
152
151
  } else if (option) {
153
152
  void invokePromise(LayoutOperation.Close, { subject: [node.id] });
154
153
  } else {
155
154
  void invokePromise(LayoutOperation.ScrollIntoView, { subject: node.id });
156
155
  }
157
156
 
158
- const defaultAction = Graph.getActions(graph, node.id).find(
159
- (action) => action.properties?.disposition === 'default',
160
- );
157
+ const defaultAction = Graph.getActions(graph, node.id).find((action) => Node.hasDisposition(action, 'default'));
161
158
  if (Node.isAction(defaultAction)) {
162
159
  void runAction(defaultAction);
163
160
  }
@@ -166,7 +163,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
166
163
  void invokePromise(LayoutOperation.UpdateSidebar, { state: 'closed' });
167
164
  }
168
165
  },
169
- [graph, invokePromise, getItem, runAction, isLg, layout.mode],
166
+ [graph, invokePromise, getItem, runAction, isLg],
170
167
  );
171
168
 
172
169
  const handleBack = useCallback(() => void invokePromise(LayoutOperation.RevertWorkspace), [invokePromise]);
@@ -237,7 +234,7 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
237
234
  const workspaceChildren = useAtomValue(graph.connections(tab, 'child'));
238
235
  useEffect(() => {
239
236
  for (const child of workspaceChildren) {
240
- if (child.properties.disposition === 'group') {
237
+ if (Node.hasDisposition(child, 'group')) {
241
238
  setItem([Node.RootId, tab, child.id], 'open', true);
242
239
  Graph.expand(graph, child.id, 'child');
243
240
  }
@@ -283,3 +280,5 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
283
280
  );
284
281
 
285
282
  export const NavTreeContainer = memo(NavTreeContainer$);
283
+
284
+ NavTreeContainer.displayName = 'NavTreeContainer';
@@ -16,3 +16,5 @@ export const NavTreeDocumentTitle = ({ node }: { node?: Node.Node }) => {
16
16
  }, [node?.properties?.label]);
17
17
  return null;
18
18
  };
19
+
20
+ NavTreeDocumentTitle.displayName = 'NavTreeDocumentTitle';
@@ -10,7 +10,8 @@ import { Icon, IconButton, Panel, ScrollArea } from '@dxos/react-ui';
10
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
  import { mx } from '@dxos/ui-theme';
12
12
 
13
- import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitNodes, visitor } from './Tree';
13
+ import { type ItemMap, Tree, type TreeNodeData, type TreeProps, visitor } from './Tree';
14
+ import { visitNodes } from './visit-nodes';
14
15
 
15
16
  random.seed(1234);
16
17
 
@@ -7,6 +7,8 @@ import React, { type HTMLAttributes, type PropsWithChildren } from 'react';
7
7
  import { type ClassNameValue, Icon, type Size } from '@dxos/react-ui';
8
8
  import { mx } from '@dxos/ui-theme';
9
9
 
10
+ import { visitNodes } from './visit-nodes';
11
+
10
12
  export const IconButton = ({
11
13
  iconName,
12
14
  classNames,
@@ -56,24 +58,6 @@ export function* visitor(node: TreeNodeData, open: ItemMap): Generator<TreeItera
56
58
  }
57
59
  }
58
60
 
59
- export const visitNodes = <T,>(
60
- node: TreeNodeData,
61
- callback: (node: TreeNodeData, depth: number) => T,
62
- depth = 0,
63
- ): T | undefined => {
64
- const result = callback(node, depth);
65
- if (result) {
66
- return result;
67
- }
68
-
69
- for (const child of node.children ?? []) {
70
- const result = visitNodes(child, callback, depth + 1);
71
- if (result) {
72
- return result;
73
- }
74
- }
75
- };
76
-
77
61
  export type ItemMap<T = boolean> = Record<TreeNodeData['id'], T>;
78
62
 
79
63
  export type TreeNodeProps = {
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type TreeNodeData } from './Tree';
6
+
7
+ // Kept out of `Tree.tsx`: react-refresh only fast-refreshes a module whose
8
+ // exports are all components, so values exported beside them force a full page reload on
9
+ // every edit.
10
+
11
+ export const visitNodes = <T>(
12
+ node: TreeNodeData,
13
+ callback: (node: TreeNodeData, depth: number) => T,
14
+ depth = 0,
15
+ ): T | undefined => {
16
+ const result = callback(node, depth);
17
+ if (result) {
18
+ return result;
19
+ }
20
+
21
+ for (const child of node.children ?? []) {
22
+ const result = visitNodes(child, callback, depth + 1);
23
+ if (result) {
24
+ return result;
25
+ }
26
+ }
27
+ };
@@ -5,19 +5,30 @@
5
5
  import { useMemo } from 'react';
6
6
 
7
7
  import { useAppGraph } from '@dxos/app-toolkit/ui';
8
- import { Graph, Node, useActions as useGraphActions } from '@dxos/plugin-graph';
8
+ import { Graph, Node } from '@dxos/plugin-graph';
9
+ import { useActions as useGraphActions } from '@dxos/plugin-graph/hooks';
10
+ import { applyPresentation } from '@dxos/react-ui-menu';
9
11
 
10
12
  import { type FlattenedActions } from '#types';
11
13
 
14
+ /** Dispositions rendered as navtree item (row/header) actions, most-primary first. */
15
+ const LIST_ITEM_DISPOSITIONS = ['list-item-primary', 'list-item'];
16
+
12
17
  /**
13
18
  * Flattens grouped actions and filters to the list-item dispositions rendered as
14
19
  * navtree item actions. Shared by row (`NavTreeItemColumns`) and header (`L1Panel`)
15
- * so both apply the same normalization rule.
20
+ * so both apply the same normalization rule. Applies each action's `presentation['list-item'
21
+ * | 'list-item-primary']` chrome override, if any, so an action multi-targeting the toolbar
22
+ * and the nav-tree can render appropriately in each.
16
23
  */
17
24
  export const getListActions = ({ actions, groupedActions }: FlattenedActions): Node.Action[] =>
18
25
  actions
19
26
  .flatMap((action) => (Node.isAction(action) ? [action] : (groupedActions[action.id] ?? [])))
20
- .filter((action) => ['list-item', 'list-item-primary'].includes(action.properties?.disposition));
27
+ .filter((action) => Node.hasDisposition(action, LIST_ITEM_DISPOSITIONS))
28
+ .map((action) => {
29
+ const surface = LIST_ITEM_DISPOSITIONS.find((disposition) => Node.hasDisposition(action, disposition));
30
+ return surface ? applyPresentation(action, surface) : action;
31
+ });
21
32
 
22
33
  /** Returns flattened actions and grouped sub-actions for a given graph node. */
23
34
  export const useActions = (node: Node.Node): FlattenedActions => {
@@ -27,7 +38,7 @@ export const useActions = (node: Node.Node): FlattenedActions => {
27
38
  return useMemo(() => {
28
39
  return actions.reduce(
29
40
  (acc: FlattenedActions, arg) => {
30
- if (arg.properties.disposition === 'item') {
41
+ if (Node.hasDisposition(arg, 'item')) {
31
42
  return acc;
32
43
  }
33
44
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-react';
5
+ import { Atom } from '@effect-atom/atom';
6
6
  import * as Option from 'effect/Option';
7
7
  import { useMemo } from 'react';
8
8
 
@@ -10,7 +10,7 @@ import { AppCapabilities, LayoutOperation } from '@dxos/app-toolkit';
10
10
  import { Operation } from '@dxos/compute';
11
11
  import { log } from '@dxos/log';
12
12
  import { Graph } from '@dxos/plugin-graph';
13
- import { expandAttendableId } from '@dxos/react-ui-attention';
13
+ import { Attention } from '@dxos/react-ui-attention';
14
14
 
15
15
  import { NavTreeCapabilities } from '../types';
16
16
 
@@ -20,7 +20,7 @@ const handler: Operation.WithHandler<typeof LayoutOperation.Expose> = LayoutOper
20
20
  const { graph } = yield* Capability.get(AppCapabilities.AppGraph);
21
21
  const { getItem, setItem } = yield* Capability.get(NavTreeCapabilities.State);
22
22
 
23
- const prefixes = expandAttendableId(subject);
23
+ const prefixes = Attention.expandAttendableId(subject);
24
24
 
25
25
  for (const qualifiedId of prefixes) {
26
26
  Graph.expand(graph, qualifiedId, 'child');
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-react';
5
+ import { Atom } from '@effect-atom/atom';
6
6
  import * as Effect from 'effect/Effect';
7
7
 
8
8
  import { type BuilderExtensions } from '@dxos/app-graph';
@@ -17,6 +17,9 @@ export const translations = [
17
17
  'node-actions-menu-invoker.label': 'More options',
18
18
  'tree-item-actions.label': 'More actions',
19
19
  'button-back.button': 'Back to Space',
20
+ 'workspace-unavailable.heading': 'Workspace unavailable',
21
+ 'workspace-unavailable.description':
22
+ 'You don’t have this workspace, or it no longer exists. Select one of your workspaces to continue.',
20
23
  },
21
24
  },
22
25
  },
@@ -4,7 +4,7 @@
4
4
 
5
5
  // @import-as-namespace
6
6
 
7
- import { type Atom } from '@effect-atom/atom-react';
7
+ import { type Atom } from '@effect-atom/atom';
8
8
 
9
9
  import { Capability } from '@dxos/app-framework';
10
10
 
@@ -50,7 +50,7 @@ export type NodeProperties = SharedProperties & {
50
50
  };
51
51
 
52
52
  export type ActionProperties = SharedProperties & {
53
- disposition?: string;
53
+ disposition?: string | string[];
54
54
  hidden?: boolean;
55
55
  caller?: string;
56
56
  menuType?: 'dropdown';
package/src/util.ts CHANGED
@@ -64,15 +64,13 @@ export const resolveMigrationOperation = (
64
64
  // TODO(wittjosiah): Move into node implementation?
65
65
  export const sortActions = (actions: Node.Action[]): Node.Action[] =>
66
66
  actions.sort((a, b) => {
67
- if (a.properties.disposition === b.properties.disposition) {
67
+ const aPrimary = Node.hasDisposition(a, 'list-item-primary');
68
+ const bPrimary = Node.hasDisposition(b, 'list-item-primary');
69
+ if (aPrimary === bPrimary) {
68
70
  return 0;
69
71
  }
70
72
 
71
- if (a.properties.disposition === 'list-item-primary') {
72
- return -1;
73
- }
74
-
75
- return 1;
73
+ return aPrimary ? -1 : 1;
76
74
  });
77
75
 
78
76
  export const getChildren = (
@@ -93,13 +91,13 @@ export const getChildren = (
93
91
  * Determines whether a node should be visible based on its disposition.
94
92
  */
95
93
  export const filterItems = (node: Node.Node, disposition?: string) => {
96
- if (!disposition && node.properties.disposition === 'hidden') {
94
+ if (!disposition && Node.hasDisposition(node, 'hidden')) {
97
95
  return false;
98
96
  } else if (!disposition) {
99
97
  const action = Node.isAction(node);
100
- return !action || node.properties.disposition === 'item';
98
+ return !action || Node.hasDisposition(node, 'item');
101
99
  } else {
102
- return node.properties.disposition === disposition;
100
+ return Node.hasDisposition(node, disposition);
103
101
  }
104
102
  };
105
103