@dxos/plugin-navtree 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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/dist/lib/browser/index.mjs +137 -72
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  5. package/dist/types/src/capabilities/app-graph-builder.d.ts +6 -0
  6. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  7. package/dist/types/src/capabilities/index.d.ts +7 -5
  8. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  9. package/dist/types/src/capabilities/keyboard.d.ts +5 -0
  10. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -0
  11. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  12. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  13. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  14. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  15. package/dist/types/src/capabilities/state.d.ts +6 -0
  16. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  17. package/dist/types/src/components/NavTree/NavTree.d.ts +10 -32
  18. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  19. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +10 -14
  20. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  22. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  23. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  24. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  25. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +7 -3
  26. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  27. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +1 -1
  28. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  30. package/dist/types/src/components/Sidebar/L0Menu.d.ts +4 -1
  31. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  32. package/dist/types/src/components/Sidebar/L1Panel.d.ts +3 -6
  33. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  34. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +3 -2
  35. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  36. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/index.d.ts +0 -3
  38. package/dist/types/src/components/index.d.ts.map +1 -1
  39. package/dist/types/src/components/types.d.ts +5 -13
  40. package/dist/types/src/components/types.d.ts.map +1 -1
  41. package/dist/types/src/{components/CommandsDialog/CommandsDialog.d.ts → containers/CommandsDialogContent/CommandsDialogContent.d.ts} +1 -1
  42. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  43. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +29 -0
  44. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
  45. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
  46. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  47. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  48. package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
  49. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  50. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  51. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  52. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  53. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  54. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  55. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
  56. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  57. package/dist/types/src/containers/index.d.ts +7 -0
  58. package/dist/types/src/containers/index.d.ts.map +1 -0
  59. package/dist/types/src/experimental/Tree.stories.d.ts +18 -4
  60. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  61. package/dist/types/src/hooks/index.d.ts +5 -0
  62. package/dist/types/src/hooks/index.d.ts.map +1 -0
  63. package/dist/types/src/hooks/useActions.d.ts +5 -0
  64. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  65. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  66. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  68. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  69. package/dist/types/src/{hooks.d.ts → hooks/useNavTreeState.d.ts} +2 -14
  70. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  71. package/dist/types/src/meta.d.ts.map +1 -1
  72. package/dist/types/src/operations/expose.d.ts +5 -0
  73. package/dist/types/src/operations/expose.d.ts.map +1 -0
  74. package/dist/types/src/operations/index.d.ts +3 -0
  75. package/dist/types/src/operations/index.d.ts.map +1 -0
  76. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  77. package/dist/types/src/testing/generator.d.ts +3 -3
  78. package/dist/types/src/testing/generator.d.ts.map +1 -1
  79. package/dist/types/src/translations.d.ts +9 -10
  80. package/dist/types/src/translations.d.ts.map +1 -1
  81. package/dist/types/src/types/events.d.ts.map +1 -1
  82. package/dist/types/src/util.d.ts +6 -2
  83. package/dist/types/src/util.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/package.json +54 -43
  86. package/src/NavTreePlugin.tsx +26 -27
  87. package/src/capabilities/{app-graph-builder/app-graph-builder.ts → app-graph-builder.ts} +10 -9
  88. package/src/capabilities/index.ts +11 -5
  89. package/src/capabilities/{keyboard/keyboard.ts → keyboard.ts} +7 -6
  90. package/src/capabilities/operation-handler.ts +16 -0
  91. package/src/capabilities/{react-surface/react-surface.tsx → react-surface.tsx} +15 -15
  92. package/src/capabilities/{state/state.ts → state.ts} +43 -15
  93. package/src/components/NavTree/NavTree.stories.tsx +31 -28
  94. package/src/components/NavTree/NavTree.tsx +64 -22
  95. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  96. package/src/components/NavTreeContext/index.ts +5 -0
  97. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +7 -9
  98. package/src/components/NavTreeItem/NavTreeItemAction.tsx +56 -39
  99. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +43 -17
  100. package/src/components/Sidebar/L0Menu.tsx +104 -113
  101. package/src/components/Sidebar/L1Panel.tsx +253 -94
  102. package/src/components/Sidebar/L1Tabs.tsx +13 -4
  103. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +2 -3
  104. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +1 -1
  105. package/src/components/index.ts +0 -3
  106. package/src/components/types.ts +12 -17
  107. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +45 -0
  108. package/src/{components/CommandsDialog/CommandsDialog.tsx → containers/CommandsDialogContent/CommandsDialogContent.tsx} +23 -21
  109. package/src/containers/CommandsDialogContent/index.ts +5 -0
  110. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +28 -0
  111. package/src/containers/CommandsTrigger/index.ts +5 -0
  112. package/src/{components → containers/NavTreeContainer}/NavTreeContainer.tsx +85 -142
  113. package/src/containers/NavTreeContainer/index.ts +6 -0
  114. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +2 -2
  115. package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
  116. package/src/containers/index.ts +12 -0
  117. package/src/experimental/Tree.stories.tsx +81 -84
  118. package/src/experimental/Tree.tsx +3 -3
  119. package/src/hooks/index.ts +8 -0
  120. package/src/hooks/useActions.ts +34 -0
  121. package/src/hooks/useLoadDescendents.ts +23 -0
  122. package/src/hooks/useNavTreeModel.ts +118 -0
  123. package/src/{hooks.ts → hooks/useNavTreeState.ts} +3 -34
  124. package/src/meta.ts +2 -2
  125. package/src/operations/expose.ts +42 -0
  126. package/src/operations/index.ts +7 -0
  127. package/src/testing/app-graph-builder.ts +40 -36
  128. package/src/testing/generator.ts +25 -25
  129. package/src/translations.ts +9 -10
  130. package/src/types/capabilities.ts +2 -2
  131. package/src/types/events.ts +4 -3
  132. package/src/util.ts +19 -5
  133. package/dist/lib/browser/app-graph-builder-RSMA6I22.mjs +0 -43
  134. package/dist/lib/browser/app-graph-builder-RSMA6I22.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-6YPQ625I.mjs +0 -1293
  136. package/dist/lib/browser/chunk-6YPQ625I.mjs.map +0 -7
  137. package/dist/lib/browser/chunk-AGQDZBPL.mjs +0 -23
  138. package/dist/lib/browser/chunk-AGQDZBPL.mjs.map +0 -7
  139. package/dist/lib/browser/chunk-YKTYPINZ.mjs +0 -19
  140. package/dist/lib/browser/chunk-YKTYPINZ.mjs.map +0 -7
  141. package/dist/lib/browser/keyboard-32EH3N4K.mjs +0 -52
  142. package/dist/lib/browser/keyboard-32EH3N4K.mjs.map +0 -7
  143. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs +0 -52
  144. package/dist/lib/browser/operation-resolver-IB75DC5O.mjs.map +0 -7
  145. package/dist/lib/browser/react-surface-F6CVMS3Y.mjs +0 -58
  146. package/dist/lib/browser/react-surface-F6CVMS3Y.mjs.map +0 -7
  147. package/dist/lib/browser/state-EVYQOUI3.mjs +0 -120
  148. package/dist/lib/browser/state-EVYQOUI3.mjs.map +0 -7
  149. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +0 -6
  150. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +0 -1
  151. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +0 -3
  152. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +0 -1
  153. package/dist/types/src/capabilities/keyboard/index.d.ts +0 -3
  154. package/dist/types/src/capabilities/keyboard/index.d.ts.map +0 -1
  155. package/dist/types/src/capabilities/keyboard/keyboard.d.ts +0 -5
  156. package/dist/types/src/capabilities/keyboard/keyboard.d.ts.map +0 -1
  157. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  158. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  159. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  160. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  161. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  162. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  163. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +0 -5
  164. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  165. package/dist/types/src/capabilities/state/index.d.ts +0 -3
  166. package/dist/types/src/capabilities/state/index.d.ts.map +0 -1
  167. package/dist/types/src/capabilities/state/state.d.ts +0 -6
  168. package/dist/types/src/capabilities/state/state.d.ts.map +0 -1
  169. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  170. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  171. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  172. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  173. package/dist/types/src/components/NavTreeContainer.d.ts +0 -16
  174. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  175. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  176. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  177. package/dist/types/src/hooks.d.ts.map +0 -1
  178. package/src/capabilities/app-graph-builder/index.ts +0 -7
  179. package/src/capabilities/keyboard/index.ts +0 -7
  180. package/src/capabilities/operation-resolver/index.ts +0 -7
  181. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -41
  182. package/src/capabilities/react-surface/index.ts +0 -7
  183. package/src/capabilities/state/index.ts +0 -7
  184. package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -26
  185. package/src/components/CommandsDialog/index.ts +0 -6
  186. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
  187. /package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +0 -0
@@ -8,24 +8,26 @@ import * as Effect from 'effect/Effect';
8
8
  import React, { type KeyboardEvent, useCallback, useRef } from 'react';
9
9
  import { expect, userEvent, within } from 'storybook/test';
10
10
 
11
- import { Capability, Common } from '@dxos/app-framework';
12
- import { useAtomCapability } from '@dxos/app-framework/react';
11
+ import { Capabilities, Capability } from '@dxos/app-framework';
13
12
  import { withPluginManager } from '@dxos/app-framework/testing';
14
- import { OperationResolver } from '@dxos/operation';
13
+ import { useAtomCapability } from '@dxos/app-framework/ui';
14
+ import { AppCapabilities, LayoutOperation } from '@dxos/app-toolkit';
15
+ import { Operation, OperationHandlerSet } from '@dxos/operation';
15
16
  import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
16
- import { faker } from '@dxos/random';
17
+ import { random } from '@dxos/random';
17
18
  import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
18
- import { withTheme } from '@dxos/react-ui/testing';
19
19
  import { useAttention, useAttentionAttributes } from '@dxos/react-ui-attention';
20
20
  import { Stack, StackItem } from '@dxos/react-ui-stack';
21
+ import { withLayout } from '@dxos/react-ui/testing';
21
22
  import { mx } from '@dxos/ui-theme';
22
23
 
24
+ import { NavTreeContainer } from '#containers';
25
+ import { storybookGraphBuilders } from '#testing';
26
+
23
27
  import { NavTreePlugin } from '../../NavTreePlugin';
24
- import { storybookGraphBuilders } from '../../testing';
25
28
  import { translations } from '../../translations';
26
- import { NavTreeContainer } from '../NavTreeContainer';
27
29
 
28
- faker.seed(1234);
30
+ random.seed(1234);
29
31
 
30
32
  const StoryState = Capability.make<Atom.Atom<{ tab: string }>>('story-state');
31
33
 
@@ -39,14 +41,14 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
39
41
  const { hasAttention } = useAttention(attendableId);
40
42
  console.log('hasAttention', hasAttention);
41
43
  return (
42
- <div className='flex p-1 items-center border-be border-separator'>
44
+ <div className='flex p-1 items-center border-b border-separator'>
43
45
  <IconButton
44
46
  density='coarse'
45
- icon='ph--atom--regular'
47
+ icon='ph--circle--regular'
46
48
  label='Test'
47
49
  iconOnly
48
50
  variant={hasAttention ? 'primary' : 'ghost'}
49
- classNames='is-[--rail-action] bs-[--rail-action]'
51
+ classNames='w-(--dx-rail-action) h-(--dx-rail-action)'
50
52
  />
51
53
  <StackItem.ResizeHandle />
52
54
  </div>
@@ -69,22 +71,22 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
69
71
  <StackItem.Root
70
72
  item={{ id: attendableId }}
71
73
  {...attentionAttrs}
72
- classNames='bg-baseSurface border-ie border-separator'
74
+ classNames='bg-base-surface border-e border-separator'
73
75
  size={30}
74
76
  onKeyDown={handleKeyDown}
75
77
  ref={rootElement}
76
78
  >
77
79
  <StoryPlankHeading attendableId={attendableId} />
78
80
  <StackItem.Content toolbar>
79
- <Toolbar.Root classNames='border-be border-subduedSeparator'>
81
+ <Toolbar.Root classNames='border-b border-subdued-separator'>
80
82
  <Toolbar.Button>Test</Toolbar.Button>
81
83
  </Toolbar.Root>
82
84
 
83
- <div className={mx(container, 'm-2 bg-activeSurface')}>
85
+ <div className={mx(container, 'm-2 bg-active-surface')}>
84
86
  <Input.Root>
85
87
  <Input.Label>Level 1 (group)</Input.Label>
86
88
  </Input.Root>
87
- <div className={mx(container, 'bg-baseSurface')}>
89
+ <div className={mx(container, 'bg-base-surface')}>
88
90
  <Input.Root>
89
91
  <Input.Label>Level 2 (base)</Input.Label>
90
92
  <Input.TextArea placeholder='Enter text' />
@@ -100,7 +102,7 @@ const DefaultStory = () => {
100
102
  const state = useAtomCapability(StoryState);
101
103
 
102
104
  return (
103
- <Main.Root complementarySidebarState='closed'>
105
+ <Main.Root navigationSidebarState='expanded'>
104
106
  <Main.NavigationSidebar label='Navigation' classNames='grid'>
105
107
  <NavTreeContainer tab={state.tab} />
106
108
  </Main.NavigationSidebar>
@@ -115,11 +117,11 @@ const DefaultStory = () => {
115
117
  };
116
118
 
117
119
  const meta = {
118
- title: 'plugins/plugin-navtree/NavTree',
120
+ title: 'plugins/plugin-navtree/components/NavTree',
119
121
  component: NavTreeContainer,
120
122
  render: DefaultStory,
121
123
  decorators: [
122
- withTheme,
124
+ withLayout({ layout: 'fullscreen' }),
123
125
  withPluginManager({
124
126
  plugins: [
125
127
  ...corePlugins(),
@@ -130,20 +132,21 @@ const meta = {
130
132
  NavTreePlugin(),
131
133
  ],
132
134
  capabilities: () => {
133
- const storyStateAtom = Atom.make({ tab: 'space-0' }).pipe(Atom.keepAlive);
135
+ const storyStateAtom = Atom.make({ tab: 'root/space-0' }).pipe(Atom.keepAlive);
134
136
  return [
135
137
  Capability.contributes(StoryState, storyStateAtom),
136
- Capability.contributes(Common.Capability.AppGraphBuilder, storybookGraphBuilders()),
137
- Capability.contributes(Common.Capability.OperationResolver, [
138
- OperationResolver.make({
139
- operation: Common.LayoutOperation.SwitchWorkspace,
140
- handler: ({ subject }) =>
138
+ Capability.contributes(AppCapabilities.AppGraphBuilder, storybookGraphBuilders()),
139
+ Capability.contributes(
140
+ Capabilities.OperationHandler,
141
+ OperationHandlerSet.make(
142
+ Operation.withHandler(LayoutOperation.SwitchWorkspace, ({ subject }) =>
141
143
  Effect.gen(function* () {
142
- const registry: Registry.Registry = yield* Capability.get(Common.Capability.AtomRegistry);
144
+ const registry: Registry.Registry = yield* Capability.get(Capabilities.AtomRegistry);
143
145
  registry.set(storyStateAtom, { tab: subject });
144
146
  }),
145
- }),
146
- ]),
147
+ ),
148
+ ),
149
+ ),
147
150
  ];
148
151
  },
149
152
  }),
@@ -159,7 +162,7 @@ export default meta;
159
162
  type Story = StoryObj<typeof NavTreeContainer>;
160
163
 
161
164
  export const Default: Story = {
162
- play: async ({ canvasElement }) => {
165
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
163
166
  const canvas = within(canvasElement);
164
167
 
165
168
  // Find the element with treegrid role and click on its parent
@@ -4,44 +4,34 @@
4
4
 
5
5
  import React, { forwardRef, useMemo } from 'react';
6
6
 
7
- import { type TreeProps } from '@dxos/react-ui-list';
7
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
8
+ import { Node, useConnections, useActions as useGraphActions } from '@dxos/plugin-graph';
8
9
  import { Tabs } from '@dxos/react-ui-tabs';
10
+ import { byPosition } from '@dxos/util';
11
+
12
+ import { useLoadDescendents } from '#hooks';
9
13
 
10
- import { useLoadDescendents } from '../../hooks';
11
- import { type NavTreeItemGraphNode } from '../../types';
12
14
  import { useNavTreeContext } from '../NavTreeContext';
13
15
  import { L0Menu, L1Tabs, type L1TabsProps } from '../Sidebar';
14
16
 
15
17
  export const NAV_TREE_ITEM = 'NavTreeItem';
16
18
 
17
- export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'> & Pick<L1TabsProps, 'open'>;
19
+ export type NavTreeProps = { id: string; root?: Node.Node; tab: string } & Pick<L1TabsProps, 'open'>;
18
20
 
19
- export const NavTree = forwardRef<HTMLDivElement, NavTreeProps>(({ id, root, ...props }, forwardedRef) => {
20
- const { tab, useItems, onBack } = useNavTreeContext();
21
- const topLevelActions = useItems(root, { disposition: 'menu', sort: true });
22
- const topLevelWorkspaces = useItems(root, { disposition: 'workspace' });
23
- const l0Items = topLevelWorkspaces;
24
- const pinnedItems = useItems(root, { disposition: 'pin-end', sort: true });
25
- const userAccountItem = useItems(root, { disposition: 'user-account' })[0];
26
- const topLevelItems = useMemo(
27
- () => [
28
- // prettier-ignore
29
- ...l0Items,
30
- ...pinnedItems,
31
- ...(userAccountItem ? [userAccountItem] : []),
32
- ],
33
- [l0Items, pinnedItems, userAccountItem],
34
- );
21
+ // TODO(wittjosiah): Refactor to Radix-style.
22
+ export const NavTree = forwardRef<HTMLDivElement, NavTreeProps>(({ id, root, tab, ...props }, forwardedRef) => {
23
+ const { onBack, onItemHover } = useNavTreeContext();
24
+ const { topLevelActions, l0Items, pinnedItems, userAccountItem, topLevelItems } = useTopLevelNavItems(root);
35
25
 
36
26
  useLoadDescendents(root);
37
27
  const path = useMemo(() => [id], [id]);
38
28
 
39
29
  return (
40
30
  // TODO(thure): `Tabs.Root` forces all items that should be able to receive focus to use `Tabs.Tab(Primitive)` since
41
- // it uses RovingFocus and doesn’t support moving focus to an item that is not a tab. Assess whether this situation
31
+ // it uses RovingFocus and doesn't support moving focus to an item that is not a tab. Assess whether this situation
42
32
  // should change including whether it should motivate a change in the design/taxonomy, or if this means this should
43
33
  // not use `react-ui-tabs` at all.
44
- <Tabs.Root value={tab} orientation='vertical' verticalVariant='stateless' classNames='relative' ref={forwardedRef}>
34
+ <Tabs.Root value={tab} orientation='vertical' classNames='relative' ref={forwardedRef}>
45
35
  <L0Menu
46
36
  menuActions={topLevelActions}
47
37
  topLevelItems={l0Items}
@@ -49,8 +39,60 @@ export const NavTree = forwardRef<HTMLDivElement, NavTreeProps>(({ id, root, ...
49
39
  userAccountItem={userAccountItem}
50
40
  path={path}
51
41
  parent={root}
42
+ onItemHover={onItemHover}
52
43
  />
53
44
  <L1Tabs topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} {...props} />
54
45
  </Tabs.Root>
55
46
  );
56
47
  });
48
+
49
+ /**
50
+ * Partitions root children into workspaces, pinned items, user-account, and top-level actions.
51
+ */
52
+ const useTopLevelNavItems = (root?: Node.Node) => {
53
+ const { graph } = useAppGraph();
54
+ const rootId = root?.id ?? Node.RootId;
55
+ const rootOutboundItems = useConnections(graph, rootId, 'child');
56
+ const rootActions = useGraphActions(graph, rootId);
57
+
58
+ const { topLevelActions, l0Items, pinnedItems, userAccountItem } = useMemo(() => {
59
+ const topLevelWorkspaces: Node.Node[] = [];
60
+ const outboundPinnedItems: Node.Node[] = [];
61
+ let userAccountItem: Node.Node | undefined;
62
+ for (const node of rootOutboundItems) {
63
+ switch (node.properties.disposition) {
64
+ case 'workspace':
65
+ topLevelWorkspaces.push(node);
66
+ break;
67
+ case 'pin-end':
68
+ outboundPinnedItems.push(node);
69
+ break;
70
+ case 'user-account':
71
+ userAccountItem ??= node;
72
+ break;
73
+ }
74
+ }
75
+
76
+ const topLevelActions = rootActions
77
+ .filter((action) => action.properties.disposition === 'menu')
78
+ .toSorted((a, b) => byPosition(a.properties, b.properties));
79
+ const pinnedItems = [
80
+ ...outboundPinnedItems,
81
+ ...rootActions.filter((action) => action.properties.disposition === 'pin-end'),
82
+ ].toSorted((a, b) => byPosition(a.properties, b.properties));
83
+
84
+ return {
85
+ topLevelActions,
86
+ l0Items: topLevelWorkspaces,
87
+ pinnedItems,
88
+ userAccountItem,
89
+ };
90
+ }, [rootActions, rootOutboundItems]);
91
+
92
+ const topLevelItems = useMemo(
93
+ () => [...l0Items, ...pinnedItems, ...(userAccountItem ? [userAccountItem] : [])],
94
+ [l0Items, pinnedItems, userAccountItem],
95
+ );
96
+
97
+ return { topLevelActions, l0Items, pinnedItems, userAccountItem, topLevelItems };
98
+ };
@@ -6,7 +6,7 @@ import { createContext, useContext } from 'react';
6
6
 
7
7
  import { raise } from '@dxos/debug';
8
8
 
9
- import { type NavTreeContextValue } from './types';
9
+ import { type NavTreeContextValue } from '../types';
10
10
 
11
11
  export const NavTreeContext = createContext<NavTreeContextValue | null>(null);
12
12
 
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './NavTreeContext';
@@ -9,33 +9,32 @@ import { RuntimePlugin } from '@dxos/app-framework';
9
9
  import { withPluginManager } from '@dxos/app-framework/testing';
10
10
  import { type Node } from '@dxos/app-graph';
11
11
  import { corePlugins } from '@dxos/plugin-testing';
12
- import { faker } from '@dxos/random';
13
- import { withTheme } from '@dxos/react-ui/testing';
12
+ import { random } from '@dxos/random';
14
13
 
15
14
  import { NavTreeItemAction, type NavTreeItemActionMenuProps } from './NavTreeItemAction';
16
15
 
17
16
  const parent = {
18
- id: faker.string.uuid(),
17
+ id: random.string.uuid(),
19
18
  type: 'node',
20
19
  data: null,
21
20
  properties: {
22
- label: faker.lorem.words(2),
21
+ label: random.lorem.words(2),
23
22
  icon: 'ph--circle--regular',
24
23
  },
25
24
  } satisfies Node.NodeArg<any>;
26
25
 
27
26
  // TODO(burdon): Factor out across tests.
28
- const menuActions = faker.helpers.multiple(
27
+ const menuActions = random.helpers.multiple(
29
28
  () =>
30
29
  ({
31
- id: faker.string.uuid(),
30
+ id: random.string.uuid(),
32
31
  type: 'action',
33
32
  data: () =>
34
33
  Effect.sync(() => {
35
34
  console.log('invoke');
36
35
  }),
37
36
  properties: {
38
- label: faker.lorem.words(2),
37
+ label: random.lorem.words(2),
39
38
  icon: 'ph--circle--regular',
40
39
  },
41
40
  }) satisfies Node.NodeArg<any>,
@@ -43,7 +42,7 @@ const menuActions = faker.helpers.multiple(
43
42
  );
44
43
 
45
44
  const meta = {
46
- title: 'plugins/plugin-navtree/NavTreeItemAction',
45
+ title: 'plugins/plugin-navtree/components/NavTreeItemAction',
47
46
  component: NavTreeItemAction,
48
47
  args: {
49
48
  icon: 'ph--list--regular',
@@ -52,7 +51,6 @@ const meta = {
52
51
  label: 'Select action',
53
52
  } satisfies Partial<NavTreeItemActionMenuProps>,
54
53
  decorators: [
55
- withTheme,
56
54
  withPluginManager({
57
55
  plugins: [...corePlugins(), RuntimePlugin()],
58
56
  }),
@@ -2,23 +2,16 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React from 'react';
5
+ import React, { useCallback } from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/app-graph';
8
8
  import { useActionRunner } from '@dxos/plugin-graph';
9
9
  import { IconButton, toLocalizedString, useDensityContext, useTranslation } from '@dxos/react-ui';
10
- import { DropdownMenu, type MenuItem, MenuProvider } from '@dxos/react-ui-menu';
11
- import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
10
+ import { Menu, type MenuItem } from '@dxos/react-ui-menu';
11
+ import { composable, composableProps, hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
12
12
 
13
- import { meta } from '../../meta';
14
- import { type ActionProperties } from '../../types';
15
-
16
- export type NavTreeItemActionMenuProps = ActionProperties & {
17
- parent: Node.Node;
18
- caller?: string;
19
- monolithic?: boolean;
20
- menuActions?: Node.Action[];
21
- };
13
+ import { meta } from '#meta';
14
+ import { type ActionProperties } from '#types';
22
15
 
23
16
  const fallbackIcon = 'ph--placeholder--regular';
24
17
 
@@ -26,47 +19,64 @@ const fineActionButtonProps = {
26
19
  size: 4 as const,
27
20
  density: 'fine' as const,
28
21
  };
22
+
29
23
  const coarseActionButtonProps = {
30
24
  size: 5 as const,
31
25
  density: 'coarse' as const,
32
26
  };
33
27
 
34
- export const NavTreeItemActionDropdownMenu = ({
35
- parent,
36
- label,
37
- icon,
38
- testId,
39
- menuActions,
40
- caller,
41
- }: NavTreeItemActionMenuProps) => {
42
- const { t } = useTranslation(meta.id);
43
- const density = useDensityContext();
44
- const runAction = useActionRunner();
28
+ export type NavTreeItemActionMenuProps = ActionProperties & {
29
+ parent: Node.Node;
30
+ path?: string[];
31
+ caller?: string;
32
+ monolithic?: boolean;
33
+ menuActions?: Node.Action[];
34
+ };
45
35
 
46
- return (
47
- <MenuProvider onAction={runAction}>
48
- <DropdownMenu.Root group={parent} items={menuActions as MenuItem[]} caller={caller}>
49
- <DropdownMenu.Trigger asChild>
36
+ export const NavTreeItemActionDropdownMenu = composable<HTMLButtonElement, NavTreeItemActionMenuProps>(
37
+ ({ parent, path, label, icon, testId, menuActions, caller, ...props }, forwardedRef) => {
38
+ const { t } = useTranslation(meta.id);
39
+ const density = useDensityContext();
40
+ const runAction = useActionRunner();
41
+ const handleAction = useCallback(
42
+ (action: Node.Action, params: Node.InvokeProps = {}) => runAction(action, { ...params, path }),
43
+ [runAction, path],
44
+ );
45
+
46
+ return (
47
+ <Menu.Root caller={caller} onAction={handleAction}>
48
+ <Menu.Trigger asChild>
50
49
  <IconButton
51
50
  {...(density === 'coarse' ? coarseActionButtonProps : fineActionButtonProps)}
52
- classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
51
+ {...composableProps(props)}
52
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
53
53
  variant='ghost'
54
54
  icon={icon ?? fallbackIcon}
55
55
  iconOnly
56
56
  label={toLocalizedString(label, t)}
57
57
  data-testid={testId}
58
+ ref={forwardedRef}
58
59
  />
59
- </DropdownMenu.Trigger>
60
- </DropdownMenu.Root>
61
- </MenuProvider>
62
- );
63
- };
60
+ </Menu.Trigger>
61
+ <Menu.Content group={parent} items={menuActions as MenuItem[]} />
62
+ </Menu.Root>
63
+ );
64
+ },
65
+ );
66
+
67
+ NavTreeItemActionDropdownMenu.displayName = 'NavTreeItemActionDropdownMenu';
64
68
 
65
69
  export const NavTreeItemMonolithicAction = (
66
- props: Node.Action & { parent: Node.Node; onAction?: (action: Node.Action) => void; baseLabel: string },
70
+ props: Node.Action & {
71
+ parent: Node.Node;
72
+ path?: string[];
73
+ onAction?: (action: Node.Action) => void;
74
+ baseLabel: string;
75
+ },
67
76
  ) => {
68
77
  const {
69
78
  parent,
79
+ path,
70
80
  properties: { disabled, caller, testId, icon, variant = 'ghost', iconOnly = true } = { label: 'never' },
71
81
  baseLabel,
72
82
  } = props;
@@ -80,7 +90,7 @@ export const NavTreeItemMonolithicAction = (
80
90
  'shrink-0',
81
91
  hoverableControlItem,
82
92
  hoverableOpenControlItem,
83
- iconOnly ? 'pli-2 pointer-fine:pli-1' : 'p-2 pointer-fine:p-2 mie-1',
93
+ iconOnly ? 'px-2 pointer-fine:px-1' : 'p-2 pointer-fine:p-2 me-1',
84
94
  ]}
85
95
  icon={icon ?? fallbackIcon}
86
96
  iconOnly={iconOnly}
@@ -92,22 +102,29 @@ export const NavTreeItemMonolithicAction = (
92
102
  return;
93
103
  }
94
104
 
95
- void runAction(props, caller ? { parent, caller } : { parent });
105
+ void runAction(props, caller ? { parent, caller, path } : { parent, path });
96
106
  }}
97
107
  data-testid={testId}
98
108
  />
99
109
  );
100
110
  };
101
111
 
102
- export const NavTreeItemAction = ({ monolithic, menuActions, parent: node, ...props }: NavTreeItemActionMenuProps) => {
112
+ export const NavTreeItemAction = ({
113
+ monolithic,
114
+ menuActions,
115
+ menuType,
116
+ parent: node,
117
+ path,
118
+ ...props
119
+ }: NavTreeItemActionMenuProps) => {
103
120
  const { t } = useTranslation(meta.id);
104
121
 
105
122
  const monolithicAction = menuActions?.length === 1 && menuActions[0];
106
123
  const baseLabel = toLocalizedString(monolithicAction ? monolithicAction.properties!.label : props.label, t);
107
124
 
108
125
  return monolithic && menuActions?.length === 1 ? (
109
- <NavTreeItemMonolithicAction baseLabel={baseLabel} parent={node} {...menuActions[0]} />
126
+ <NavTreeItemMonolithicAction baseLabel={baseLabel} parent={node} path={path} {...menuActions[0]} />
110
127
  ) : (
111
- <NavTreeItemActionDropdownMenu {...props} label={baseLabel} parent={node} menuActions={menuActions} />
128
+ <NavTreeItemActionDropdownMenu {...props} label={baseLabel} parent={node} path={path} menuActions={menuActions} />
112
129
  );
113
130
  };
@@ -2,40 +2,64 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { Fragment, memo } from 'react';
5
+ import React, { Fragment, memo, useMemo } from 'react';
6
6
 
7
7
  import { Node } from '@dxos/app-graph';
8
8
  import { Popover, Treegrid, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
9
 
10
- import { useLoadDescendents } from '../../hooks';
11
- import { meta } from '../../meta';
10
+ import { useActions } from '#hooks';
11
+ import { meta } from '#meta';
12
+
12
13
  import { NAV_TREE_ITEM } from '../NavTree';
13
14
  import { useNavTreeContext } from '../NavTreeContext';
14
15
  import { type NavTreeItemColumnsProps } from '../types';
15
-
16
16
  import { NavTreeItemAction } from './NavTreeItemAction';
17
17
 
18
18
  export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumnsProps) => {
19
19
  const { t } = useTranslation(meta.id);
20
- const { useActions, renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
20
+ const { renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
21
21
 
22
22
  const level = path.length - 2;
23
- const { actions: _actions, groupedActions } = useActions(item);
24
- const [primaryAction, ...secondaryActions] = _actions.toSorted((a, _b) =>
25
- a.properties?.disposition === 'list-item-primary' ? -1 : 1,
23
+ const { actions: actionsProp, groupedActions } = useActions(item);
24
+ const sortedActions = useMemo(
25
+ () =>
26
+ actionsProp.toSorted((actionA, actionB) => {
27
+ const primaryA = actionA.properties?.disposition === 'list-item-primary';
28
+ const primaryB = actionB.properties?.disposition === 'list-item-primary';
29
+ if (primaryA && !primaryB) {
30
+ return -1;
31
+ }
32
+ if (primaryB && !primaryA) {
33
+ return 1;
34
+ }
35
+ return 0;
36
+ }),
37
+ [actionsProp],
26
38
  );
39
+ const [primaryAction, ...secondaryActions] = sortedActions;
27
40
 
28
- const actions = (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : _actions)
29
- .flatMap((action) => (Node.isAction(action) ? [action] : []))
30
- .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
41
+ const actions = useMemo(
42
+ () =>
43
+ (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : sortedActions)
44
+ .flatMap((action) => (Node.isAction(action) ? [action] : []))
45
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition)),
46
+ [sortedActions, primaryAction],
47
+ );
31
48
 
32
- useLoadDescendents(item);
33
- useLoadDescendents(primaryAction && !Node.isAction(primaryAction) ? (primaryAction as Node.Node) : undefined);
49
+ const primaryMenuActions = useMemo(
50
+ () =>
51
+ primaryAction
52
+ ? Node.isAction(primaryAction)
53
+ ? [primaryAction]
54
+ : groupedActions[primaryAction?.id ?? '']
55
+ : undefined,
56
+ [primaryAction, groupedActions],
57
+ );
34
58
 
35
- const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${item.id}` ? Popover.Anchor : Fragment;
59
+ const ActionRoot = popoverAnchorId === `${NAV_TREE_ITEM}:${item.id}` ? Popover.Anchor : Fragment;
36
60
 
37
61
  return (
38
- <div role='none' className='contents app-no-drag'>
62
+ <div role='none' className='contents dx-app-no-drag'>
39
63
  {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled ? (
40
64
  <Treegrid.Cell classNames='contents'>
41
65
  <NavTreeItemAction
@@ -43,8 +67,9 @@ export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumns
43
67
  label={toLocalizedString(primaryAction.properties?.label, t)}
44
68
  icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
45
69
  parent={item}
70
+ path={path}
46
71
  monolithic={Node.isAction(primaryAction)}
47
- menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
72
+ menuActions={primaryMenuActions}
48
73
  menuType={primaryAction.properties?.menuType}
49
74
  caller={NAV_TREE_ITEM}
50
75
  />
@@ -57,9 +82,10 @@ export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumns
57
82
  <Treegrid.Cell classNames='contents'>
58
83
  <NavTreeItemAction
59
84
  testId={`navtree.treeItem.actionsLevel${level}`}
60
- label={t('tree item actions label')}
85
+ label={t('tree-item-actions.label')}
61
86
  icon='ph--dots-three-vertical--regular'
62
87
  parent={item}
88
+ path={path}
63
89
  menuActions={actions}
64
90
  menuType='dropdown'
65
91
  caller={NAV_TREE_ITEM}