@dxos/plugin-navtree 0.8.3-main.7f5a14c → 0.8.3

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 (36) hide show
  1. package/dist/lib/browser/{chunk-T3ABOVHL.mjs → chunk-ZJPL4XJD.mjs} +83 -73
  2. package/dist/lib/browser/chunk-ZJPL4XJD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +10 -4
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/{react-surface-IMVZZ6MM.mjs → react-surface-H3JRTVNU.mjs} +2 -2
  7. package/dist/lib/browser/{state-D7KDRDBV.mjs → state-J6AKZRB4.mjs} +13 -10
  8. package/dist/lib/browser/state-J6AKZRB4.mjs.map +7 -0
  9. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  10. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  11. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -1
  12. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +1 -0
  13. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  15. package/dist/types/src/components/Sidebar/L0Menu.d.ts +1 -1
  16. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  17. package/dist/types/src/components/types.d.ts +5 -6
  18. package/dist/types/src/components/types.d.ts.map +1 -1
  19. package/dist/types/src/testing/app-graph-builder.d.ts +2 -1
  20. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  21. package/dist/types/src/testing/generator.d.ts.map +1 -1
  22. package/dist/types/src/util.d.ts +1 -1
  23. package/package.json +36 -34
  24. package/src/NavTreePlugin.tsx +8 -2
  25. package/src/capabilities/state.ts +17 -11
  26. package/src/components/CommandsDialog/CommandsDialog.tsx +16 -10
  27. package/src/components/NavTree/NavTree.stories.tsx +44 -3
  28. package/src/components/NavTreeContainer.tsx +4 -16
  29. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +1 -1
  30. package/src/components/Sidebar/L0Menu.tsx +58 -49
  31. package/src/components/types.ts +3 -7
  32. package/src/testing/app-graph-builder.ts +249 -203
  33. package/src/testing/generator.ts +2 -2
  34. package/dist/lib/browser/chunk-T3ABOVHL.mjs.map +0 -7
  35. package/dist/lib/browser/state-D7KDRDBV.mjs.map +0 -7
  36. /package/dist/lib/browser/{react-surface-IMVZZ6MM.mjs.map → react-surface-H3JRTVNU.mjs.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-navtree",
3
- "version": "0.8.3-main.7f5a14c",
3
+ "version": "0.8.3",
4
4
  "description": "DXOS Surface plugin for a tree view visualization of the graph.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -31,29 +31,29 @@
31
31
  "@radix-ui/react-use-controllable-state": "1.1.0",
32
32
  "date-fns": "^3.3.1",
33
33
  "effect": "^3.13.3",
34
- "@dxos/app-graph": "0.8.3-main.7f5a14c",
35
- "@dxos/async": "0.8.3-main.7f5a14c",
36
- "@dxos/app-framework": "0.8.3-main.7f5a14c",
37
- "@dxos/debug": "0.8.3-main.7f5a14c",
38
- "@dxos/echo": "0.8.3-main.7f5a14c",
39
- "@dxos/echo-schema": "0.8.3-main.7f5a14c",
40
- "@dxos/invariant": "0.8.3-main.7f5a14c",
41
- "@dxos/keyboard": "0.8.3-main.7f5a14c",
42
- "@dxos/local-storage": "0.8.3-main.7f5a14c",
43
- "@dxos/live-object": "0.8.3-main.7f5a14c",
44
- "@dxos/log": "0.8.3-main.7f5a14c",
45
- "@dxos/plugin-deck": "0.8.3-main.7f5a14c",
46
- "@dxos/plugin-graph": "0.8.3-main.7f5a14c",
47
- "@dxos/plugin-observability": "0.8.3-main.7f5a14c",
48
- "@dxos/protocols": "0.8.3-main.7f5a14c",
49
- "@dxos/react-ui-attention": "0.8.3-main.7f5a14c",
50
- "@dxos/react-client": "0.8.3-main.7f5a14c",
51
- "@dxos/react-ui-menu": "0.8.3-main.7f5a14c",
52
- "@dxos/react-ui-list": "0.8.3-main.7f5a14c",
53
- "@dxos/react-ui-searchlist": "0.8.3-main.7f5a14c",
54
- "@dxos/react-ui-stack": "0.8.3-main.7f5a14c",
55
- "@dxos/react-ui-tabs": "0.8.3-main.7f5a14c",
56
- "@dxos/util": "0.8.3-main.7f5a14c"
34
+ "@dxos/app-framework": "0.8.3",
35
+ "@dxos/echo": "0.8.3",
36
+ "@dxos/echo-schema": "0.8.3",
37
+ "@dxos/invariant": "0.8.3",
38
+ "@dxos/keyboard": "0.8.3",
39
+ "@dxos/live-object": "0.8.3",
40
+ "@dxos/local-storage": "0.8.3",
41
+ "@dxos/log": "0.8.3",
42
+ "@dxos/app-graph": "0.8.3",
43
+ "@dxos/plugin-deck": "0.8.3",
44
+ "@dxos/plugin-graph": "0.8.3",
45
+ "@dxos/plugin-observability": "0.8.3",
46
+ "@dxos/react-client": "0.8.3",
47
+ "@dxos/protocols": "0.8.3",
48
+ "@dxos/react-ui-attention": "0.8.3",
49
+ "@dxos/react-ui-list": "0.8.3",
50
+ "@dxos/react-ui-menu": "0.8.3",
51
+ "@dxos/react-ui-stack": "0.8.3",
52
+ "@dxos/react-ui-searchlist": "0.8.3",
53
+ "@dxos/react-ui-tabs": "0.8.3",
54
+ "@dxos/async": "0.8.3",
55
+ "@dxos/util": "0.8.3",
56
+ "@dxos/debug": "0.8.3"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@effect-rx/rx-react": "^0.34.1",
@@ -64,14 +64,16 @@
64
64
  "react": "~18.2.0",
65
65
  "react-dom": "~18.2.0",
66
66
  "vite": "5.4.7",
67
- "@dxos/echo-signals": "0.8.3-main.7f5a14c",
68
- "@dxos/plugin-storybook-layout": "0.8.3-main.7f5a14c",
69
- "@dxos/plugin-theme": "0.8.3-main.7f5a14c",
70
- "@dxos/random": "0.8.3-main.7f5a14c",
71
- "@dxos/plugin-attention": "0.8.3-main.7f5a14c",
72
- "@dxos/react-ui": "0.8.3-main.7f5a14c",
73
- "@dxos/react-ui-theme": "0.8.3-main.7f5a14c",
74
- "@dxos/storybook-utils": "0.8.3-main.7f5a14c"
67
+ "@dxos/plugin-attention": "0.8.3",
68
+ "@dxos/plugin-client": "0.8.3",
69
+ "@dxos/plugin-space": "0.8.3",
70
+ "@dxos/echo-signals": "0.8.3",
71
+ "@dxos/plugin-storybook-layout": "0.8.3",
72
+ "@dxos/plugin-theme": "0.8.3",
73
+ "@dxos/random": "0.8.3",
74
+ "@dxos/react-ui": "0.8.3",
75
+ "@dxos/react-ui-theme": "0.8.3",
76
+ "@dxos/storybook-utils": "0.8.3"
75
77
  },
76
78
  "peerDependencies": {
77
79
  "@effect-rx/rx-react": "^0.34.1",
@@ -80,8 +82,8 @@
80
82
  "effect": "^3.13.3",
81
83
  "react": "~18.2.0",
82
84
  "react-dom": "~18.2.0",
83
- "@dxos/react-ui": "0.8.3-main.7f5a14c",
84
- "@dxos/react-ui-theme": "0.8.3-main.7f5a14c"
85
+ "@dxos/react-ui": "0.8.3",
86
+ "@dxos/react-ui-theme": "0.8.3"
85
87
  },
86
88
  "publishConfig": {
87
89
  "access": "public"
@@ -55,12 +55,18 @@ export const NavTreePlugin = () =>
55
55
  }),
56
56
  defineModule({
57
57
  id: `${meta.id}/module/expose`,
58
- activatesOn: allOf(Events.DispatcherReady, Events.LayoutReady, NavTreeEvents.StateReady),
58
+ activatesOn: allOf(Events.DispatcherReady, Events.AppGraphReady, Events.LayoutReady, NavTreeEvents.StateReady),
59
59
  activate: async (context) => {
60
60
  const layout = context.getCapability(Capabilities.Layout);
61
61
  const { dispatchPromise: dispatch } = context.getCapability(Capabilities.IntentDispatcher);
62
+ const { graph } = context.getCapability(Capabilities.AppGraph);
62
63
  if (dispatch && layout.active.length === 1) {
63
- await dispatch(createIntent(LayoutAction.Expose, { part: 'navigation', subject: layout.active[0] }));
64
+ // TODO(wittjosiah): This should really be fired once the navtree renders for the first time.
65
+ // That is the point at which the graph is expanded and the path should be available.
66
+ void graph
67
+ .waitForPath({ target: layout.active[0] }, { timeout: 30_000 })
68
+ .then(() => dispatch(createIntent(LayoutAction.Expose, { part: 'navigation', subject: layout.active[0] })))
69
+ .catch(() => {});
64
70
  }
65
71
 
66
72
  return [];
@@ -2,10 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-core';
5
+ import { effect, untracked } from '@preact/signals-core';
6
6
 
7
7
  import { Capabilities, contributes, type PluginContext } from '@dxos/app-framework';
8
- import { type Live, Obj } from '@dxos/echo';
8
+ import { type Live } from '@dxos/echo';
9
+ import { live } from '@dxos/live-object';
9
10
  import { Path } from '@dxos/react-ui-list';
10
11
 
11
12
  import { NavTreeCapabilities } from './capabilities';
@@ -23,7 +24,7 @@ const getInitialState = () => {
23
24
  const cached: [string, { open: boolean; current: boolean }][] = JSON.parse(stringified);
24
25
  return cached.map(([key, value]): [string, Live<{ open: boolean; current: boolean }>] => [
25
26
  key,
26
- Obj.make({ open: value.open, current: false }),
27
+ live({ open: value.open, current: false }),
27
28
  ]);
28
29
  } catch {}
29
30
  };
@@ -38,15 +39,15 @@ export default (context: PluginContext) => {
38
39
  const state = new Map<string, Live<{ open: boolean; current: boolean; alternateTree?: boolean }>>(
39
40
  getInitialState() ?? [
40
41
  // TODO(thure): Initialize these dynamically.
41
- ['root', Obj.make({ open: true, current: false })],
42
- ['root~dxos.org/plugin/space-spaces', Obj.make({ open: true, current: false })],
43
- ['root~dxos.org/plugin/files', Obj.make({ open: true, current: false })],
42
+ ['root', live({ open: true, current: false })],
43
+ ['root~dxos.org/plugin/space-spaces', live({ open: true, current: false })],
44
+ ['root~dxos.org/plugin/files', live({ open: true, current: false })],
44
45
  ],
45
46
  );
46
47
 
47
48
  const getItem = (_path: string[]) => {
48
49
  const path = Path.create(..._path);
49
- const value = state.get(path) ?? Obj.make({ open: false, current: false, alternateTree: false });
50
+ const value = state.get(path) ?? live({ open: false, current: false, alternateTree: false });
50
51
  if (!state.has(path)) {
51
52
  state.set(path, value);
52
53
  }
@@ -73,9 +74,7 @@ export default (context: PluginContext) => {
73
74
  const removed = previous.filter((id) => !layout.active.includes(id));
74
75
  previous = layout.active;
75
76
 
76
- // TODO(wittjosiah): This is setTimeout because there's a race between the keys be initialized.
77
- // This could be avoided if the location was a path as well and not just an id.
78
- setTimeout(() => {
77
+ const handleUpdate = () => {
79
78
  removed.forEach((id) => {
80
79
  const keys = Array.from(state.keys()).filter((key) => Path.last(key) === id);
81
80
  keys.forEach((key) => {
@@ -89,7 +88,14 @@ export default (context: PluginContext) => {
89
88
  setItem(Path.parts(key), 'current', true);
90
89
  });
91
90
  });
92
- });
91
+ };
92
+
93
+ // TODO(wittjosiah): This is setTimeout because there's a race between the keys be initialized.
94
+ // Keys are initialized on the first render of an item in the navtree.
95
+ // This could be avoided if the location was a path as well and not just an id.
96
+ const timeout = setTimeout(handleUpdate, 500);
97
+ untracked(() => handleUpdate());
98
+ return () => clearTimeout(timeout);
93
99
  });
94
100
 
95
101
  return contributes(NavTreeCapabilities.State, { state, getItem, setItem, isOpen, isCurrent, isAlternateTree }, () =>
@@ -10,6 +10,12 @@ import { Keyboard, keySymbols } from '@dxos/keyboard';
10
10
  import { useActions } from '@dxos/plugin-graph';
11
11
  import { Button, Dialog, Icon, useTranslation, toLocalizedString } from '@dxos/react-ui';
12
12
  import { SearchList } from '@dxos/react-ui-searchlist';
13
+ import {
14
+ cardDialogContent,
15
+ cardDialogHeader,
16
+ cardDialogPaddedOverflow,
17
+ cardDialogSearchListRoot,
18
+ } from '@dxos/react-ui-stack';
13
19
  import { descriptionText, mx } from '@dxos/react-ui-theme';
14
20
  import { getHostPlatform } from '@dxos/util';
15
21
 
@@ -56,13 +62,13 @@ export const CommandsDialogContent = ({ selected: initial }: { selected?: string
56
62
  const actions = isActionGroup(group) ? groupActions : allActions;
57
63
 
58
64
  return (
59
- <Dialog.Content classNames={['md:max-is-[30rem] overflow-hidden mbs-12']}>
60
- <Dialog.Title>{t('commands dialog title', { ns: NAVTREE_PLUGIN })}</Dialog.Title>
65
+ <Dialog.Content classNames={cardDialogContent}>
66
+ <Dialog.Title classNames={cardDialogHeader}>{t('commands dialog title', { ns: NAVTREE_PLUGIN })}</Dialog.Title>
61
67
 
62
68
  {/* TODO(burdon): BUG: Overscrolls container. */}
63
- <SearchList.Root label={t('command list input placeholder')} classNames='flex flex-col grow overflow-hidden my-2'>
64
- <SearchList.Input placeholder={t('command list input placeholder')} classNames='px-1 my-2' />
65
- <SearchList.Content classNames='max-bs-[24rem] overflow-auto'>
69
+ <SearchList.Root label={t('command list input placeholder')} classNames={cardDialogSearchListRoot}>
70
+ <SearchList.Input placeholder={t('command list input placeholder')} />
71
+ <SearchList.Content classNames={cardDialogPaddedOverflow}>
66
72
  {actions?.map((action) => {
67
73
  const label = toLocalizedString(action.properties.label, t);
68
74
  const shortcut =
@@ -102,11 +108,11 @@ export const CommandsDialogContent = ({ selected: initial }: { selected?: string
102
108
  </SearchList.Content>
103
109
  </SearchList.Root>
104
110
 
105
- <Dialog.Close asChild>
106
- <Button variant='primary' classNames='mbs-2'>
107
- {t('close label', { ns: 'os' })}
108
- </Button>
109
- </Dialog.Close>
111
+ <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
112
+ <Dialog.Close asChild>
113
+ <Button classNames='is-full'>{t('close label', { ns: 'os' })}</Button>
114
+ </Dialog.Close>
115
+ </div>
110
116
  </Dialog.Content>
111
117
  );
112
118
  };
@@ -19,9 +19,11 @@ import {
19
19
  useCapability,
20
20
  } from '@dxos/app-framework';
21
21
  import { withPluginManager } from '@dxos/app-framework/testing';
22
- import { Obj } from '@dxos/echo';
22
+ import { live } from '@dxos/live-object';
23
23
  import { AttentionPlugin } from '@dxos/plugin-attention';
24
+ import { ClientPlugin } from '@dxos/plugin-client';
24
25
  import { GraphPlugin } from '@dxos/plugin-graph';
26
+ import { SpacePlugin } from '@dxos/plugin-space';
25
27
  import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
26
28
  import { ThemePlugin } from '@dxos/plugin-theme';
27
29
  import { faker } from '@dxos/random';
@@ -130,8 +132,8 @@ const meta: Meta<typeof NavTreeContainer> = {
130
132
  NavTreePlugin(),
131
133
  ],
132
134
  capabilities: (context) => [
133
- contributes(StoryState, Obj.make({ tab: 'space-0' })),
134
- contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders),
135
+ contributes(StoryState, live({ tab: 'space-0' })),
136
+ contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders(context)),
135
137
  contributes(Capabilities.IntentResolver, [
136
138
  createResolver({
137
139
  intent: LayoutAction.UpdateLayout,
@@ -157,3 +159,42 @@ export default meta;
157
159
  type Story = StoryObj<typeof NavTreeContainer>;
158
160
 
159
161
  export const Default: Story = {};
162
+
163
+ // TODO(wittjosiah): Deduplicate plugins/capabilities with default story.
164
+ export const WithClient: Story = {
165
+ decorators: [
166
+ withPluginManager({
167
+ plugins: [
168
+ ThemePlugin({ tx: defaultTx }),
169
+ GraphPlugin(),
170
+ IntentPlugin(),
171
+ SettingsPlugin(),
172
+ AttentionPlugin(),
173
+ NavTreePlugin(),
174
+ ClientPlugin({
175
+ onClientInitialized: async (_, client) => {
176
+ await client.halo.createIdentity();
177
+ },
178
+ }),
179
+ SpacePlugin(),
180
+ // Needs to be last so that the dialog and popovers have access to all contexts.
181
+ StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
182
+ ],
183
+ capabilities: (context) => [
184
+ contributes(StoryState, live({ tab: 'space-0' })),
185
+ contributes(Capabilities.IntentResolver, [
186
+ createResolver({
187
+ intent: LayoutAction.UpdateLayout,
188
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
189
+ Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
190
+ resolve: ({ subject }) => {
191
+ const state = context.getCapability(StoryState);
192
+ state.tab = subject;
193
+ },
194
+ }),
195
+ ]),
196
+ ],
197
+ }),
198
+ withLayout({ fullscreen: true }),
199
+ ],
200
+ };
@@ -17,12 +17,10 @@ import {
17
17
  useLayout,
18
18
  } from '@dxos/app-framework';
19
19
  import { isAction, isActionLike, ROOT_ID, type Node, type ReadableGraph } from '@dxos/app-graph';
20
- import { Obj } from '@dxos/echo';
21
20
  import { PLANK_COMPANION_TYPE } from '@dxos/plugin-deck/types';
22
21
  import { useConnections } from '@dxos/plugin-graph';
23
- import { isSpace } from '@dxos/react-client/echo';
24
22
  import { useMediaQuery, useSidebars } from '@dxos/react-ui';
25
- import { isTreeData, type PropsFromTreeItem, type TreeData } from '@dxos/react-ui-list';
23
+ import { isTreeData, type TreeItemDataProps, type TreeData } from '@dxos/react-ui-list';
26
24
  import { mx } from '@dxos/react-ui-theme';
27
25
  import { arrayMove, byPosition } from '@dxos/util';
28
26
 
@@ -83,7 +81,7 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
83
81
  const getActions = useCallback((node: Node) => naturalGetActions(graph, node), [graph]);
84
82
 
85
83
  const getProps = useCallback(
86
- (node: Node, path: string[]): PropsFromTreeItem => {
84
+ (node: Node, path: string[]): TreeItemDataProps => {
87
85
  const children = getChildren(graph, node, path).filter(getChildrenFilter);
88
86
  const parentOf =
89
87
  children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
@@ -152,18 +150,8 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
152
150
  [dispatch, layout.active, tab, navigationSidebarState, isLg],
153
151
  );
154
152
 
155
- const canDrop = useCallback((source: TreeData, target: TreeData) => {
156
- const sourceNode = source.item as Node;
157
- const targetNode = target.item as Node;
158
- // TODO(wittjosiah): This shouldn't depend directly on client, there should be a graph-level abstraction.
159
- if (isSpace(targetNode.data)) {
160
- // TODO(wittjosiah): Find a way to only allow space as source for rearranging.
161
- return Obj.isObject(sourceNode.data) || isSpace(sourceNode.data);
162
- } else if (Obj.isObject(targetNode.data)) {
163
- return Obj.isObject(sourceNode.data);
164
- } else {
165
- return false;
166
- }
153
+ const canDrop = useCallback(({ source, target }: { source: TreeData; target: TreeData }) => {
154
+ return target.item.properties.canDrop?.(source) ?? false;
167
155
  }, []);
168
156
 
169
157
  const handleSelect = useCallback(
@@ -27,7 +27,7 @@ export const NavTreeItemColumns = memo(({ path, item, open, density = 'fine' }:
27
27
 
28
28
  const actions = (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : _actions)
29
29
  .flatMap((action) => (isAction(action) ? [action] : []))
30
- .filter((action) => !action.properties?.hidden);
30
+ .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
31
31
 
32
32
  const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${item.id}` ? Popover.Anchor : Fragment;
33
33
 
@@ -24,16 +24,7 @@ import React, {
24
24
 
25
25
  import { type Node } from '@dxos/app-graph';
26
26
  import { invariant } from '@dxos/invariant';
27
- import {
28
- Icon,
29
- IconButton,
30
- ListItem,
31
- ScrollArea,
32
- Tooltip,
33
- toLocalizedString,
34
- useMediaQuery,
35
- useTranslation,
36
- } from '@dxos/react-ui';
27
+ import { Icon, ListItem, ScrollArea, Tooltip, toLocalizedString, useMediaQuery, useTranslation } from '@dxos/react-ui';
37
28
  import { MenuProvider, DropdownMenu, type MenuItem } from '@dxos/react-ui-menu';
38
29
  import type { StackItemRearrangeHandler } from '@dxos/react-ui-stack';
39
30
  import { Tabs } from '@dxos/react-ui-tabs';
@@ -100,6 +91,11 @@ const l0Breakpoints: Record<string, string> = {
100
91
  lg: 'hidden lg:grid',
101
92
  };
102
93
 
94
+ const l0ItemRoot =
95
+ 'group/l0item flex w-full justify-center items-center relative data[type!="collection"]:cursor-pointer app-no-drag dx-focus-ring-group';
96
+
97
+ const l0ItemContent = 'flex justify-center items-center dx-focus-ring-group-indicator transition-colors rounded-sm';
98
+
103
99
  const L0ItemRoot = forwardRef<HTMLElement, PropsWithChildren<L0ItemRootProps>>(
104
100
  ({ item, parent, path, children }, forwardedRef) => {
105
101
  const { getProps } = useNavTreeContext();
@@ -125,10 +121,7 @@ const L0ItemRoot = forwardRef<HTMLElement, PropsWithChildren<L0ItemRootProps>>(
125
121
  <Root
126
122
  {...(rootProps as any)}
127
123
  data-type={type}
128
- className={mx(
129
- 'group/l0item flex w-full justify-center items-center relative data[type!="collection"]:cursor-pointer app-no-drag dx-focus-ring-group',
130
- l0Breakpoints[item.properties.l0Breakpoint],
131
- )}
124
+ className={mx(l0ItemRoot, l0Breakpoints[item.properties.l0Breakpoint])}
132
125
  ref={forwardedRef}
133
126
  >
134
127
  {children}
@@ -138,7 +131,7 @@ const L0ItemRoot = forwardRef<HTMLElement, PropsWithChildren<L0ItemRootProps>>(
138
131
  },
139
132
  );
140
133
 
141
- const L0Avator = ({ item }: Pick<L0ItemProps, 'item'>) => {
134
+ const L0Avatar = ({ item }: Pick<L0ItemProps, 'item'>) => {
142
135
  const { t } = useTranslation(NAVTREE_PLUGIN);
143
136
  const type = l0ItemType(item);
144
137
  const hue = item.properties.hue ?? null;
@@ -161,10 +154,8 @@ const L0Item = ({ item, parent, path, pinned, onRearrange }: L0ItemProps) => {
161
154
  const { t } = useTranslation(NAVTREE_PLUGIN);
162
155
  const itemElement = useRef<HTMLElement | null>(null);
163
156
  const [closestEdge, setEdge] = useState<Edge | null>(null);
164
- const type = l0ItemType(item);
165
157
  const localizedString = toLocalizedString(item.properties.label, t);
166
158
  const hue = item.properties.hue ?? null;
167
- const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
168
159
 
169
160
  useLayoutEffect(() => {
170
161
  // NOTE(thure): This is copied from StackItem, perhaps this should be DRYed out.
@@ -227,30 +218,43 @@ const L0Item = ({ item, parent, path, pinned, onRearrange }: L0ItemProps) => {
227
218
  <div
228
219
  role='none'
229
220
  data-frame={true}
221
+ {...(hue && { style: { background: `var(--dx-${hue}Surface)` } })}
230
222
  className={mx(
231
- 'flex justify-center items-center dx-focus-ring-group-indicator transition-colors rounded',
232
- // TODO(burdon): Create reusable button/component and/or create var for size.
223
+ l0ItemContent,
233
224
  pinned
234
- ? 'bg-transparent w-[50px] p-2 group-hover/l0item:bg-activeSurface'
235
- : 'bg-activeSurface w-[50px] h-[50px]',
225
+ ? 'p-2 group-hover/l0item:bg-activeSurface'
226
+ : 'is-[--l0-avatar-size] bs-[--l0-avatar-size] bg-activeSurface',
236
227
  )}
237
- {...(hue && { style: { background: `var(--dx-${hue}Surface)` } })}
238
228
  >
239
- {(item.properties.icon && <Icon icon={item.properties.icon} size={pinned ? 5 : 6} {...hueFgStyle} />) ||
240
- (type === 'tab' && item.properties.disposition !== 'pin-end' && <L0Avator item={item} />)}
229
+ <ItemAvatar item={item} />
241
230
  </div>
242
231
  <div
243
232
  role='none'
244
- className='hidden group-aria-selected/l0item:block absolute inline-start-0 inset-block-4 is-1 bg-accentSurface rounded-ie'
233
+ className='hidden group-aria-selected/l0item:block absolute inline-start-0 inset-block-2 is-1 bg-accentSurface rounded-ie'
245
234
  />
246
235
  <span id={`${item.id}__label`} className='sr-only'>
247
236
  {localizedString}
248
237
  </span>
249
- {onRearrange && closestEdge && <ListItem.DropIndicator edge={closestEdge} />}
238
+ {closestEdge && <ListItem.DropIndicator edge={closestEdge} />}
250
239
  </L0ItemRoot>
251
240
  );
252
241
  };
253
242
 
243
+ const ItemAvatar = ({ item }: Pick<L0ItemProps, 'item'>) => {
244
+ const type = l0ItemType(item);
245
+ if (item.properties.icon) {
246
+ const hue = item.properties.hue ?? null;
247
+ const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
248
+ return <Icon icon={item.properties.icon} size={5} {...hueFgStyle} />;
249
+ }
250
+
251
+ if (type === 'tab' && item.properties.disposition !== 'pin-end') {
252
+ return <L0Avatar item={item} />;
253
+ }
254
+
255
+ return null;
256
+ };
257
+
254
258
  //
255
259
  // L0Collection
256
260
  //
@@ -261,6 +265,7 @@ const L0Collection = ({ item, path }: L0ItemProps) => {
261
265
  const collectionItems = navTreeContext.useItems(item);
262
266
  const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
263
267
  const { id, testId } = navTreeContext.getProps?.(item, path) ?? {};
268
+
264
269
  const handleRearrange = useCallback<StackItemRearrangeHandler<L0ItemData>>(
265
270
  (source, target, closestEdge) => {
266
271
  invariant(item.properties.onRearrangeChildren);
@@ -306,7 +311,7 @@ export type L0MenuProps = {
306
311
  menuActions: MenuItem[];
307
312
  topLevelItems: Node<any>[];
308
313
  pinnedItems: Node<any>[];
309
- userAccountItem: Node<any>;
314
+ userAccountItem?: Node<any>;
310
315
  parent?: Node<any>;
311
316
  path: string[];
312
317
  };
@@ -317,37 +322,40 @@ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountIte
317
322
  return (
318
323
  <Tabs.Tablist
319
324
  classNames={[
320
- 'group/l0 absolute z-[1] inset-block-0 inline-start-0 rounded-is-lg',
321
- 'grid grid-cols-[var(--l0-size)] grid-rows-[var(--rail-size)_1fr_min-content_var(--l0-size)] gap-1 contain-layout',
325
+ 'group/l0 absolute z-[1] inset-block-0 inline-start-0 rounded-is',
326
+ 'grid grid-cols-[var(--l0-size)] grid-rows-[var(--rail-size)_1fr_min-content_var(--l0-size)] contain-layout',
322
327
  '!is-[--l0-size] bg-baseSurface border-ie border-subduedSeparator app-drag pbe-[env(safe-area-inset-bottom)]',
323
328
  ]}
324
329
  >
325
- <div role='none' className='flex justify-center p-1'>
326
- <MenuProvider>
327
- <DropdownMenu.Root group={parent} items={menuActions}>
328
- <DropdownMenu.Trigger asChild>
329
- {/* TODO(wittjosiah): Use L0Item trigger. */}
330
- {/* TODO(burdon): Replace with Sigil. */}
331
- <IconButton
332
- iconOnly
333
- icon='ph--dots-three--regular'
334
- size={5}
335
- label={t('app menu label')}
336
- tooltipSide='right'
337
- classNames='w-[50px] _bg-primary-500'
338
- data-testid='spacePlugin.addSpace'
339
- />
330
+ {/* TODO(wittjosiah): Use L0Item trigger. */}
331
+ <MenuProvider>
332
+ <DropdownMenu.Root group={parent} items={menuActions}>
333
+ <Tooltip.Trigger content={t('app menu label')} side='right' asChild>
334
+ <DropdownMenu.Trigger
335
+ data-testid='spacePlugin.addSpace'
336
+ className={mx(l0ItemRoot, 'grid place-items-center')}
337
+ >
338
+ <div
339
+ role='none'
340
+ className={mx(
341
+ l0ItemContent,
342
+ 'is-[--rail-action] bs-[--rail-action] group-hover/l0item:bg-hoverSurface',
343
+ )}
344
+ >
345
+ <Icon icon='ph--list--regular' size={5} />
346
+ </div>
340
347
  </DropdownMenu.Trigger>
341
- </DropdownMenu.Root>
342
- </MenuProvider>
343
- </div>
348
+ </Tooltip.Trigger>
349
+ </DropdownMenu.Root>
350
+ </MenuProvider>
344
351
 
352
+ {/* Space list. */}
345
353
  <ScrollArea.Root>
346
354
  <ScrollArea.Viewport>
347
355
  <div
348
356
  role='none'
349
357
  className={mx([
350
- 'flex flex-col gap-2 pbs-1',
358
+ 'flex flex-col gap-1 pbs-1',
351
359
  '[body[data-platform="darwin"]_&]:pbs-[calc(30px+0.25rem)]',
352
360
  '[body[data-platform="ios"]_&]:pbs-[max(env(safe-area-inset-top),0.25rem)]',
353
361
  ])}
@@ -366,6 +374,7 @@ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountIte
366
374
  </ScrollArea.Viewport>
367
375
  </ScrollArea.Root>
368
376
 
377
+ {/* Actions. */}
369
378
  <div role='none' className='grid grid-cols-1 auto-rows-[--rail-action] pbs-2'>
370
379
  {pinnedItems
371
380
  .filter((item) => l0ItemType(item) !== 'collection')
@@ -382,7 +391,7 @@ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountIte
382
391
  hue={userAccountItem.properties.hue}
383
392
  emoji={userAccountItem.properties.emoji}
384
393
  status={userAccountItem.properties.status}
385
- size={11}
394
+ size={10}
386
395
  />
387
396
  </L0ItemRoot>
388
397
  </div>
@@ -9,22 +9,18 @@ import type { Density } from '@dxos/react-ui';
9
9
  import type { TreeProps } from '@dxos/react-ui-list';
10
10
  import type { MaybePromise } from '@dxos/util';
11
11
 
12
- import { type L1PanelProps } from './Sidebar';
12
+ import type { L1PanelProps } from './Sidebar';
13
13
  import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
14
14
 
15
15
  export type NavTreeContextValue = Pick<
16
- TreeProps<NavTreeItemGraphNode>,
17
- 'getProps' | 'isCurrent' | 'isOpen' | 'canDrop' | 'onOpenChange' | 'onSelect'
16
+ TreeProps<NavTreeItemGraphNode, { disposition?: string; sort?: boolean }>,
17
+ 'useItems' | 'getProps' | 'isCurrent' | 'isOpen' | 'canDrop' | 'onOpenChange' | 'onSelect'
18
18
  > &
19
19
  Pick<L1PanelProps, 'onBack'> & {
20
20
  tab: string;
21
21
  topbar?: boolean;
22
22
  popoverAnchorId?: string;
23
23
  renderItemEnd?: FC<{ node: Node; open: boolean }>;
24
- useItems: (
25
- node?: NavTreeItemGraphNode,
26
- options?: { disposition?: string; sort?: boolean },
27
- ) => NavTreeItemGraphNode[];
28
24
  getActions: (node: Node) => FlattenedActions;
29
25
  loadDescendents?: (node: Node) => MaybePromise<void>;
30
26
  isAlternateTree?: (path: string[], item: NavTreeItemGraphNode) => boolean;