@dxos/plugin-navtree 0.8.3 → 0.8.4-main.2e9d522

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 (64) hide show
  1. package/dist/lib/browser/{app-graph-builder-P7AY3K72.mjs → app-graph-builder-5J4LRWDX.mjs} +3 -3
  2. package/dist/lib/browser/{chunk-IEW3JD5C.mjs → chunk-ERDGQTH5.mjs} +2 -2
  3. package/dist/lib/browser/{chunk-33VLL4IJ.mjs → chunk-WCARAZ7J.mjs} +3 -3
  4. package/dist/lib/browser/{chunk-ZJPL4XJD.mjs → chunk-WEMCSQI7.mjs} +188 -148
  5. package/dist/lib/browser/chunk-WEMCSQI7.mjs.map +7 -0
  6. package/dist/lib/browser/index.mjs +12 -11
  7. package/dist/lib/browser/index.mjs.map +3 -3
  8. package/dist/lib/browser/{intent-resolver-NAYKKAOP.mjs → intent-resolver-3XYN2CBU.mjs} +5 -5
  9. package/dist/lib/browser/{keyboard-DDFATRUG.mjs → keyboard-QRS6WOQH.mjs} +3 -3
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/{react-surface-H3JRTVNU.mjs → react-surface-JSJ5SQX5.mjs} +5 -5
  12. package/dist/lib/browser/{state-J6AKZRB4.mjs → state-54DY3DOY.mjs} +4 -4
  13. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -1
  14. package/dist/types/src/components/NavTree/NavTree.d.ts +2 -2
  15. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  16. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +1 -1
  17. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  19. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +1 -1
  20. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  22. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  23. package/dist/types/src/components/Sidebar/L1Panel.d.ts +5 -6
  24. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  25. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +12 -0
  26. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -0
  27. package/dist/types/src/components/Sidebar/index.d.ts +1 -0
  28. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -1
  29. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +1 -1
  30. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/index.d.ts +1 -1
  32. package/dist/types/src/components/index.d.ts.map +1 -1
  33. package/dist/types/src/experimental/Tree.d.ts +5 -5
  34. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  35. package/dist/types/src/experimental/Tree.stories.d.ts +1 -1
  36. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  37. package/dist/types/src/translations.d.ts +14 -13
  38. package/dist/types/src/translations.d.ts.map +1 -1
  39. package/dist/types/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +40 -42
  41. package/src/NavTreePlugin.tsx +1 -1
  42. package/src/components/CommandsDialog/CommandsTrigger.tsx +2 -4
  43. package/src/components/NavTree/NavTree.stories.tsx +2 -2
  44. package/src/components/NavTree/NavTree.tsx +3 -3
  45. package/src/components/NavTreeContainer.tsx +3 -4
  46. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +1 -1
  47. package/src/components/NavTreeItem/NavTreeItemAction.tsx +4 -4
  48. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +2 -3
  49. package/src/components/Sidebar/L1Panel.tsx +72 -93
  50. package/src/components/Sidebar/L1Tabs.tsx +57 -0
  51. package/src/components/Sidebar/index.ts +1 -0
  52. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +1 -1
  53. package/src/components/index.ts +1 -1
  54. package/src/experimental/Tree.stories.tsx +9 -9
  55. package/src/experimental/Tree.tsx +15 -16
  56. package/src/translations.ts +7 -4
  57. package/dist/lib/browser/chunk-ZJPL4XJD.mjs.map +0 -7
  58. /package/dist/lib/browser/{app-graph-builder-P7AY3K72.mjs.map → app-graph-builder-5J4LRWDX.mjs.map} +0 -0
  59. /package/dist/lib/browser/{chunk-IEW3JD5C.mjs.map → chunk-ERDGQTH5.mjs.map} +0 -0
  60. /package/dist/lib/browser/{chunk-33VLL4IJ.mjs.map → chunk-WCARAZ7J.mjs.map} +0 -0
  61. /package/dist/lib/browser/{intent-resolver-NAYKKAOP.mjs.map → intent-resolver-3XYN2CBU.mjs.map} +0 -0
  62. /package/dist/lib/browser/{keyboard-DDFATRUG.mjs.map → keyboard-QRS6WOQH.mjs.map} +0 -0
  63. /package/dist/lib/browser/{react-surface-H3JRTVNU.mjs.map → react-surface-JSJ5SQX5.mjs.map} +0 -0
  64. /package/dist/lib/browser/{state-J6AKZRB4.mjs.map → state-54DY3DOY.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",
3
+ "version": "0.8.4-main.2e9d522",
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",
@@ -30,60 +30,58 @@
30
30
  "@radix-ui/react-dismissable-layer": "1.1.5",
31
31
  "@radix-ui/react-use-controllable-state": "1.1.0",
32
32
  "date-fns": "^3.3.1",
33
- "effect": "^3.13.3",
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"
33
+ "effect": "3.17.0",
34
+ "@dxos/app-framework": "0.8.4-main.2e9d522",
35
+ "@dxos/app-graph": "0.8.4-main.2e9d522",
36
+ "@dxos/async": "0.8.4-main.2e9d522",
37
+ "@dxos/debug": "0.8.4-main.2e9d522",
38
+ "@dxos/echo": "0.8.4-main.2e9d522",
39
+ "@dxos/echo-schema": "0.8.4-main.2e9d522",
40
+ "@dxos/invariant": "0.8.4-main.2e9d522",
41
+ "@dxos/live-object": "0.8.4-main.2e9d522",
42
+ "@dxos/keyboard": "0.8.4-main.2e9d522",
43
+ "@dxos/local-storage": "0.8.4-main.2e9d522",
44
+ "@dxos/log": "0.8.4-main.2e9d522",
45
+ "@dxos/plugin-deck": "0.8.4-main.2e9d522",
46
+ "@dxos/plugin-graph": "0.8.4-main.2e9d522",
47
+ "@dxos/plugin-observability": "0.8.4-main.2e9d522",
48
+ "@dxos/react-client": "0.8.4-main.2e9d522",
49
+ "@dxos/protocols": "0.8.4-main.2e9d522",
50
+ "@dxos/react-ui-attention": "0.8.4-main.2e9d522",
51
+ "@dxos/react-ui-list": "0.8.4-main.2e9d522",
52
+ "@dxos/react-ui-menu": "0.8.4-main.2e9d522",
53
+ "@dxos/react-ui-searchlist": "0.8.4-main.2e9d522",
54
+ "@dxos/react-ui-stack": "0.8.4-main.2e9d522",
55
+ "@dxos/react-ui-tabs": "0.8.4-main.2e9d522",
56
+ "@dxos/util": "0.8.4-main.2e9d522"
57
57
  },
58
58
  "devDependencies": {
59
- "@effect-rx/rx-react": "^0.34.1",
60
- "@effect/platform": "0.80.12",
61
- "@phosphor-icons/react": "^2.1.5",
59
+ "@effect-rx/rx-react": "0.38.0",
60
+ "@effect/platform": "0.89.0",
62
61
  "@types/react": "~18.2.0",
63
62
  "@types/react-dom": "~18.2.0",
64
63
  "react": "~18.2.0",
65
64
  "react-dom": "~18.2.0",
66
65
  "vite": "5.4.7",
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"
66
+ "@dxos/plugin-attention": "0.8.4-main.2e9d522",
67
+ "@dxos/echo-signals": "0.8.4-main.2e9d522",
68
+ "@dxos/plugin-client": "0.8.4-main.2e9d522",
69
+ "@dxos/plugin-space": "0.8.4-main.2e9d522",
70
+ "@dxos/plugin-storybook-layout": "0.8.4-main.2e9d522",
71
+ "@dxos/plugin-theme": "0.8.4-main.2e9d522",
72
+ "@dxos/random": "0.8.4-main.2e9d522",
73
+ "@dxos/react-ui-theme": "0.8.4-main.2e9d522",
74
+ "@dxos/react-ui": "0.8.4-main.2e9d522",
75
+ "@dxos/storybook-utils": "0.8.4-main.2e9d522"
77
76
  },
78
77
  "peerDependencies": {
79
78
  "@effect-rx/rx-react": "^0.34.1",
80
- "@effect/platform": "0.80.12",
81
- "@phosphor-icons/react": "^2.1.5",
79
+ "@effect/platform": "^0.80.12",
82
80
  "effect": "^3.13.3",
83
81
  "react": "~18.2.0",
84
82
  "react-dom": "~18.2.0",
85
- "@dxos/react-ui": "0.8.3",
86
- "@dxos/react-ui-theme": "0.8.3"
83
+ "@dxos/react-ui": "0.8.4-main.2e9d522",
84
+ "@dxos/react-ui-theme": "0.8.4-main.2e9d522"
87
85
  },
88
86
  "publishConfig": {
89
87
  "access": "public"
@@ -18,7 +18,7 @@ import { AppGraphBuilder, IntentResolver, Keyboard, ReactSurface, State } from '
18
18
  import { NODE_TYPE } from './components';
19
19
  import { NavTreeEvents } from './events';
20
20
  import { meta } from './meta';
21
- import translations from './translations';
21
+ import { translations } from './translations';
22
22
 
23
23
  export const NavTreePlugin = () =>
24
24
  definePlugin(meta, [
@@ -2,12 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { MagnifyingGlass } from '@phosphor-icons/react';
6
5
  import React from 'react';
7
6
 
8
7
  import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
9
- import { Button, useTranslation } from '@dxos/react-ui';
10
- import { getSize } from '@dxos/react-ui-theme';
8
+ import { Button, Icon, useTranslation } from '@dxos/react-ui';
11
9
 
12
10
  import { COMMANDS_DIALOG, NAVTREE_PLUGIN } from '../../meta';
13
11
 
@@ -31,7 +29,7 @@ export const CommandsTrigger = () => {
31
29
  }
32
30
  >
33
31
  <span className='text-description font-normal grow text-start'>{t('command list input placeholder')}</span>
34
- <MagnifyingGlass className={getSize(5)} />
32
+ <Icon icon='ph--magnifying-glass--regular' size={5} />
35
33
  </Button>
36
34
  );
37
35
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type StoryObj, type Meta } from '@storybook/react';
7
+ import { type StoryObj, type Meta } from '@storybook/react-vite';
8
8
  import { Schema } from 'effect';
9
9
  import React from 'react';
10
10
 
@@ -35,7 +35,7 @@ import { withLayout } from '@dxos/storybook-utils';
35
35
 
36
36
  import { NavTreePlugin } from '../../NavTreePlugin';
37
37
  import { storybookGraphBuilders } from '../../testing';
38
- import translations from '../../translations';
38
+ import { translations } from '../../translations';
39
39
  import { NavTreeContainer } from '../NavTreeContainer';
40
40
 
41
41
  faker.seed(1234);
@@ -10,11 +10,11 @@ import { Tabs } from '@dxos/react-ui-tabs';
10
10
  import { useLoadDescendents } from '../../hooks';
11
11
  import { type NavTreeItemGraphNode } from '../../types';
12
12
  import { useNavTreeContext } from '../NavTreeContext';
13
- import { L0Menu, L1Panels, type L1PanelsProps } from '../Sidebar';
13
+ import { L0Menu, L1Tabs, type L1TabsProps } from '../Sidebar';
14
14
 
15
15
  export const NAV_TREE_ITEM = 'NavTreeItem';
16
16
 
17
- export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'> & Pick<L1PanelsProps, 'open'>;
17
+ export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'> & Pick<L1TabsProps, 'open'>;
18
18
 
19
19
  export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
20
20
  const { tab, useItems, onBack } = useNavTreeContext();
@@ -56,7 +56,7 @@ export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
56
56
  path={path}
57
57
  parent={root}
58
58
  />
59
- <L1Panels topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} {...props} />
59
+ <L1Tabs topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} {...props} />
60
60
  </Tabs.Root>
61
61
  );
62
62
  };
@@ -59,10 +59,9 @@ const getItems = (graph: ReadableGraph, node?: Node, disposition?: string) => {
59
59
 
60
60
  const useItems = (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
61
61
  const { graph } = useAppGraph();
62
- const connections = useConnections(graph, node?.id ?? ROOT_ID).filter((node) =>
63
- filterItems(node, options?.disposition),
64
- );
65
- return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
62
+ const connections = useConnections(graph, node?.id ?? ROOT_ID);
63
+ const filtered = connections.filter((node) => filterItems(node, options?.disposition));
64
+ return options?.sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
66
65
  };
67
66
 
68
67
  export type NavTreeContainerProps = {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type Meta, type StoryObj } from '@storybook/react';
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
8
 
9
9
  import { type NodeArg } from '@dxos/app-graph';
10
10
  import { faker } from '@dxos/random';
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  import { type Action, type Node } from '@dxos/app-graph';
8
8
  import { useTranslation, toLocalizedString, IconButton, useDensityContext } from '@dxos/react-ui';
9
9
  import { DropdownMenu, MenuProvider, type MenuItem } from '@dxos/react-ui-menu';
10
- import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
10
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/react-ui-theme';
11
11
 
12
12
  import { NAVTREE_PLUGIN } from '../../meta';
13
13
  import { type ActionProperties } from '../../types';
@@ -46,7 +46,7 @@ export const NavTreeItemActionDropdownMenu = ({
46
46
  <DropdownMenu.Trigger asChild>
47
47
  <IconButton
48
48
  {...(density === 'coarse' ? coarseActionButtonProps : fineActionButtonProps)}
49
- classNames={mx('shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem)}
49
+ classNames={['shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem]}
50
50
  variant='ghost'
51
51
  icon={icon ?? fallbackIcon}
52
52
  iconOnly
@@ -70,12 +70,12 @@ export const NavTreeItemMonolithicAction = ({
70
70
  <IconButton
71
71
  {...(density === 'coarse' ? coarseActionButtonProps : fineActionButtonProps)}
72
72
  variant={variant}
73
- classNames={mx(
73
+ classNames={[
74
74
  'shrink-0',
75
75
  hoverableControlItem,
76
76
  hoverableOpenControlItem,
77
77
  iconOnly ? 'pli-2 pointer-fine:pli-1' : 'p-2 pointer-fine:p-2 mie-1',
78
- )}
78
+ ]}
79
79
  icon={icon ?? fallbackIcon}
80
80
  iconOnly={iconOnly}
81
81
  label={baseLabel}
@@ -12,16 +12,15 @@ import { useLoadDescendents } from '../../hooks';
12
12
  import { NAVTREE_PLUGIN } from '../../meta';
13
13
  import { NAV_TREE_ITEM } from '../NavTree';
14
14
  import { useNavTreeContext } from '../NavTreeContext';
15
- import type { NavTreeItemColumnsProps } from '../types';
15
+ import { type NavTreeItemColumnsProps } from '../types';
16
16
 
17
17
  export const NavTreeItemColumns = memo(({ path, item, open, density = 'fine' }: NavTreeItemColumnsProps) => {
18
18
  const { t } = useTranslation(NAVTREE_PLUGIN);
19
19
  const { getActions, renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
20
20
 
21
21
  const level = path.length - 2;
22
-
23
22
  const { actions: _actions, groupedActions } = getActions(item);
24
- const [primaryAction, ...secondaryActions] = _actions.toSorted((a, b) =>
23
+ const [primaryAction, ...secondaryActions] = _actions.toSorted((a, _b) =>
25
24
  a.properties?.disposition === 'list-item-primary' ? -1 : 1,
26
25
  );
27
26
 
@@ -5,68 +5,45 @@
5
5
  import React, { useCallback, useMemo } from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/app-graph';
8
- import { Button, type ButtonProps, Icon, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
9
  import { Tree } from '@dxos/react-ui-list';
10
10
  import { Tabs } from '@dxos/react-ui-tabs';
11
- import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
11
+ import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/react-ui-theme';
12
12
 
13
- import { useLoadDescendents } from '../../hooks';
14
- import { NAVTREE_PLUGIN } from '../../meta';
15
- import { l0ItemType } from '../../util';
13
+ import { meta } from '../../meta';
16
14
  import { useNavTreeContext } from '../NavTreeContext';
17
15
  import { NavTreeItemColumns } from '../NavTreeItem';
18
16
 
19
- const headingBackButtonLabel = 'inset-0 min-is-0 truncate flex items-center pis-2';
20
-
21
- const TitleButton = ({ title, onClick }: { title: string } & Pick<ButtonProps, 'onClick'>) => {
22
- return (
23
- <Button
24
- variant='ghost'
25
- classNames='pli-1 flex-1 group text-start justify-start bs-[--rail-action] font-normal'
26
- onClick={onClick}
27
- >
28
- <Icon icon='ph--caret-left--bold' size={3} />
29
- <span className={mx(headingBackButtonLabel)}>{title}</span>
30
- </Button>
31
- );
32
- };
33
-
34
17
  export type L1PanelProps = {
35
18
  open?: boolean;
36
- item: Node<any>;
37
19
  path: string[];
20
+ item: Node<any>;
38
21
  currentItemId: string;
39
22
  onBack?: () => void;
40
23
  };
41
24
 
42
- const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
43
- const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
44
- const { t } = useTranslation(NAVTREE_PLUGIN);
25
+ /**
26
+ * Space or settings panel.
27
+ */
28
+ export const L1Panel = ({ open, path, item, currentItemId, onBack }: L1PanelProps) => {
29
+ const { t } = useTranslation(meta.id);
30
+ const { isAlternateTree, ...navTreeContext } = useNavTreeContext();
31
+ // NOTE(burdon): I'm very uneasy about this pattern of declaring hooks as context props.
32
+ const { useItems } = navTreeContext;
33
+ const title = toLocalizedString(item.properties.label, t);
45
34
 
46
35
  // TODO(wittjosiah): Support multiple alternate trees.
47
- const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
36
+ const alternateTree = useItems(item, { disposition: 'alternate-tree' })[0];
48
37
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
49
- const handleOpen = useCallback(() => setAlternateTree?.(alternatePath, true), [alternatePath, setAlternateTree]);
50
38
  const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
51
39
  const useAlternateItems = useCallback(
52
40
  (node?: Node, { disposition }: { disposition?: string } = {}) => {
53
41
  // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
54
- return navTreeContext.useItems(node, { disposition, sort: node?.id === alternateTree.id });
42
+ return useItems(node, { disposition, sort: node?.id === alternateTree.id });
55
43
  },
56
- [navTreeContext, alternateTree],
44
+ [alternateTree, useItems],
57
45
  );
58
46
 
59
- const handleBack = useCallback(() => {
60
- if (isAlternate) {
61
- setAlternateTree?.(alternatePath, false);
62
- } else {
63
- onBack?.();
64
- }
65
- }, [isAlternate, onBack, alternatePath, setAlternateTree]);
66
-
67
- const title = toLocalizedString(item.properties.label, t);
68
- const backCapable = item.id.startsWith('!') || isAlternate;
69
-
70
47
  return (
71
48
  <Tabs.Tabpanel
72
49
  key={item.id}
@@ -83,27 +60,7 @@ const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
83
60
  >
84
61
  {item.id === currentItemId && (
85
62
  <>
86
- <h2 className='flex items-center border-be border-subduedSeparator app-drag pli-1'>
87
- {backCapable ? (
88
- <TitleButton title={title} onClick={handleBack} />
89
- ) : (
90
- <span className='flex-1 truncate min-is-0 pis-6'>{title}</span>
91
- )}
92
- {alternateTree && !isAlternate && (
93
- <IconButton
94
- variant='ghost'
95
- classNames={mx('shrink-0', hoverableControlItem, hoverableOpenControlItem, 'pli-2 pointer-fine:pli-1')}
96
- iconOnly
97
- size={5}
98
- density='coarse'
99
- icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
100
- label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
101
- data-testid='treeView.alternateTreeButton'
102
- onClick={handleOpen}
103
- />
104
- )}
105
- <NavTreeItemColumns path={path} item={item} open density='coarse' />
106
- </h2>
63
+ <L1PanelHeader path={path} item={item} currentItemId={currentItemId} onBack={onBack} />
107
64
  <div role='none' className='overflow-y-auto'>
108
65
  {isAlternate ? (
109
66
  <Tree
@@ -135,41 +92,63 @@ const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
135
92
  );
136
93
  };
137
94
 
138
- const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
139
- const { useItems } = useNavTreeContext();
140
- useLoadDescendents(item);
141
- const collectionItems = useItems(item);
142
- const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
143
- return (
144
- <>
145
- {collectionItems
146
- .filter((item) => l0ItemType(item) === 'tab')
147
- .map((item) => (
148
- <L1Panel key={item.id} item={item} path={groupPath} {...props} />
149
- ))}
150
- </>
151
- );
152
- };
95
+ /**
96
+ * Header row.
97
+ */
98
+ const L1PanelHeader = ({ item, path, onBack }: L1PanelProps) => {
99
+ const { t } = useTranslation(meta.id);
100
+ const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
101
+ const title = toLocalizedString(item.properties.label, t);
153
102
 
154
- export type L1PanelsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
155
- topLevelItems: Node<any>[];
156
- path: string[];
157
- };
103
+ // TODO(wittjosiah): Support multiple alternate trees.
104
+ const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
105
+ const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
106
+ const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
107
+ const backCapable = item.id.startsWith('!') || isAlternate;
108
+
109
+ const handleOpen = useCallback(() => {
110
+ setAlternateTree?.(alternatePath, true);
111
+ }, [alternatePath, setAlternateTree]);
112
+
113
+ const handleBack = useCallback(() => {
114
+ if (isAlternate) {
115
+ setAlternateTree?.(alternatePath, false);
116
+ } else {
117
+ onBack?.();
118
+ }
119
+ }, [isAlternate, alternatePath, setAlternateTree, onBack]);
158
120
 
159
- export const L1Panels = ({ topLevelItems, onBack, ...props }: L1PanelsProps) => {
121
+ // TODO(burdon): Reuse same grid as tree?
160
122
  return (
161
- <>
162
- {topLevelItems.map((item) => {
163
- const type = l0ItemType(item);
164
- switch (type) {
165
- case 'collection':
166
- return <L1PanelCollection key={item.id} item={item} {...props} />;
167
- case 'tab':
168
- return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
169
- default:
170
- return null;
171
- }
172
- })}
173
- </>
123
+ <div className='flex is-full items-center border-be border-subduedSeparator app-drag density-coarse'>
124
+ <div className='is-6' />
125
+ <h2 className='flex-1 truncate min-is-0'>{title}</h2>
126
+ <div>
127
+ {(backCapable && (
128
+ <IconButton
129
+ variant='ghost'
130
+ icon='ph--arrow-u-down-left--regular'
131
+ iconOnly
132
+ size={5}
133
+ label={t('button back')}
134
+ classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
135
+ onClick={handleBack}
136
+ />
137
+ )) ||
138
+ (alternateTree && !isAlternate && (
139
+ <IconButton
140
+ data-testid='treeView.alternateTreeButton'
141
+ variant='ghost'
142
+ icon={alternateTree.properties.icon ?? 'ph--placeholder--regular'}
143
+ iconOnly
144
+ size={5}
145
+ label={toLocalizedString(alternateTree.properties.label ?? alternateTree.id, t)}
146
+ classNames={[hoverableControlItem, hoverableOpenControlItem, 'pointer-fine:pli-1']}
147
+ onClick={handleOpen}
148
+ />
149
+ )) || <div />}
150
+ </div>
151
+ <NavTreeItemColumns open path={path} item={item} density='coarse' />
152
+ </div>
174
153
  );
175
154
  };
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { type Node } from '@dxos/app-graph';
8
+
9
+ import { useLoadDescendents } from '../../hooks';
10
+ import { l0ItemType } from '../../util';
11
+ import { useNavTreeContext } from '../NavTreeContext';
12
+ import { L1Panel, type L1PanelProps } from './L1Panel';
13
+
14
+ export type L1TabsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
15
+ path: string[];
16
+ topLevelItems: Node<any>[];
17
+ };
18
+
19
+ /**
20
+ * Each space is an L1 tab.
21
+ */
22
+ export const L1Tabs = ({ topLevelItems, onBack, ...props }: L1TabsProps) => {
23
+ return (
24
+ <>
25
+ {topLevelItems.map((item) => {
26
+ const type = l0ItemType(item);
27
+ switch (type) {
28
+ case 'tab':
29
+ return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
30
+ case 'collection':
31
+ return <L1PanelCollection key={item.id} item={item} {...props} />;
32
+ default:
33
+ return null;
34
+ }
35
+ })}
36
+ </>
37
+ );
38
+ };
39
+
40
+ /**
41
+ *
42
+ */
43
+ const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
44
+ const { useItems } = useNavTreeContext();
45
+ useLoadDescendents(item);
46
+ const collectionItems = useItems(item);
47
+ const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
48
+ return (
49
+ <>
50
+ {collectionItems
51
+ .filter((item) => l0ItemType(item) === 'tab')
52
+ .map((item) => (
53
+ <L1Panel key={item.id} item={item} path={groupPath} {...props} />
54
+ ))}
55
+ </>
56
+ );
57
+ };
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './L0Menu';
6
6
  export * from './L1Panel';
7
+ export * from './L1Tabs';
@@ -4,7 +4,7 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type Meta } from '@storybook/react';
7
+ import { type Meta } from '@storybook/react-vite';
8
8
 
9
9
  import { withTheme } from '@dxos/storybook-utils';
10
10
 
@@ -7,4 +7,4 @@ export * from './NavTree';
7
7
  export * from './NavTreeContainer';
8
8
  export * from './NavTreeContext';
9
9
  export * from './NavTreeDocumentTitle';
10
- export * from './types';
10
+ export type * from './types';
@@ -4,11 +4,11 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { House, List, Planet, PlusCircle, Sailboat } from '@phosphor-icons/react';
8
- import { type Meta } from '@storybook/react';
7
+ import { type Meta } from '@storybook/react-vite';
9
8
  import React, { type JSX, type PropsWithChildren, useEffect, useState } from 'react';
10
9
 
11
10
  import { faker } from '@dxos/random';
11
+ import { Icon, IconButton } from '@dxos/react-ui';
12
12
  import { modalSurface, mx } from '@dxos/react-ui-theme';
13
13
  import { withLayout, withTheme } from '@dxos/storybook-utils';
14
14
 
@@ -48,7 +48,7 @@ const data: TreeNodeData[] = [
48
48
  id: faker.string.uuid(),
49
49
  title: 'plugins/Personal Space',
50
50
  color: 'text-green-400',
51
- Icon: House,
51
+ iconName: 'ph--house--regular',
52
52
  children: [
53
53
  {
54
54
  id: faker.string.uuid(),
@@ -83,7 +83,7 @@ const data: TreeNodeData[] = [
83
83
  {
84
84
  id: faker.string.uuid(),
85
85
  title: faker.commerce.productName(),
86
- Icon: Planet,
86
+ iconName: 'ph--planet--regular',
87
87
  children: [
88
88
  {
89
89
  id: faker.string.uuid(),
@@ -94,12 +94,12 @@ const data: TreeNodeData[] = [
94
94
  {
95
95
  id: faker.string.uuid(),
96
96
  title: faker.commerce.productName(),
97
- Icon: Sailboat,
97
+ iconName: 'ph--sailboat--regular',
98
98
  },
99
99
  {
100
100
  id: faker.string.uuid(),
101
101
  title: faker.commerce.productName(),
102
- Icon: Planet,
102
+ iconName: 'ph--planet--regular',
103
103
  },
104
104
  ],
105
105
  },
@@ -181,7 +181,7 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
181
181
  setItems((items) => {
182
182
  items[0].children?.push({
183
183
  id: faker.string.uuid(),
184
- Icon: Planet,
184
+ iconName: 'ph--planet--regular',
185
185
  title: faker.commerce.productName(),
186
186
  });
187
187
 
@@ -213,11 +213,11 @@ const Sidebar = ({ mutate }: { mutate?: boolean }) => {
213
213
  </div>
214
214
 
215
215
  <div className='flex items-center my-2 px-2 gap-2'>
216
- <PlusCircle onClick={handleCreateSpace} />
216
+ <IconButton icon='ph--plus-circle--regular' iconOnly label='Create space' onClick={handleCreateSpace} />
217
217
  <span className='grow text-sm' onClick={handleCreateSpace}>
218
218
  New space
219
219
  </span>
220
- <List />
220
+ <Icon icon='ph--list--regular' />
221
221
  </div>
222
222
  </div>
223
223
  );