@dxos/plugin-navtree 0.8.2-staging.7ac8446 → 0.8.2

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 (149) hide show
  1. package/dist/lib/browser/{app-graph-builder-26D3MUCA.mjs → app-graph-builder-P7AY3K72.mjs} +7 -6
  2. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-4RQJXI4D.mjs → chunk-33VLL4IJ.mjs} +2 -2
  4. package/dist/lib/browser/chunk-33VLL4IJ.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-MFAWMFJC.mjs +1418 -0
  6. package/dist/lib/browser/chunk-MFAWMFJC.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +6 -11
  8. package/dist/lib/browser/index.mjs.map +3 -3
  9. package/dist/lib/browser/{intent-resolver-BZMUS24C.mjs → intent-resolver-NAYKKAOP.mjs} +7 -7
  10. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs.map +7 -0
  11. package/dist/lib/browser/keyboard-DDFATRUG.mjs +51 -0
  12. package/dist/lib/browser/keyboard-DDFATRUG.mjs.map +7 -0
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/{react-surface-3HCM3SHJ.mjs → react-surface-HGA3TAQU.mjs} +3 -3
  15. package/dist/lib/browser/{state-M5UJ3MHL.mjs → state-R2UMYAHN.mjs} +18 -12
  16. package/dist/lib/browser/state-R2UMYAHN.mjs.map +7 -0
  17. package/dist/types/src/capabilities/app-graph-builder.d.ts +2 -179
  18. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  19. package/dist/types/src/capabilities/capabilities.d.ts +7 -4
  20. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  21. package/dist/types/src/capabilities/index.d.ts +38 -183
  22. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  24. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/keyboard.d.ts +2 -2
  26. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  27. package/dist/types/src/capabilities/state.d.ts +9 -6
  28. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  29. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -0
  30. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -0
  31. package/dist/types/src/components/CommandsDialog/index.d.ts +3 -0
  32. package/dist/types/src/components/CommandsDialog/index.d.ts.map +1 -0
  33. package/dist/types/src/components/NavTree/NavTree.d.ts +8 -0
  34. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -0
  35. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +8 -0
  36. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/NavTree/index.d.ts +2 -0
  38. package/dist/types/src/components/NavTree/index.d.ts.map +1 -0
  39. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  40. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -1
  41. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -1
  42. package/dist/types/src/components/{NavTreeItemAction.d.ts → NavTreeItem/NavTreeItemAction.d.ts} +3 -5
  43. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -0
  44. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +8 -0
  45. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -0
  46. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +4 -0
  47. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -0
  48. package/dist/types/src/components/NavTreeItem/index.d.ts +3 -0
  49. package/dist/types/src/components/NavTreeItem/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Sidebar/L0Menu.d.ts +13 -0
  51. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -0
  52. package/dist/types/src/components/Sidebar/L1Panel.d.ts +15 -0
  53. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -0
  54. package/dist/types/src/components/Sidebar/index.d.ts +3 -0
  55. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -0
  56. package/dist/types/src/components/{UserAccountAvatar.d.ts → UserAccountAvatar/UserAccountAvatar.d.ts} +1 -1
  57. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -0
  58. package/dist/types/src/components/{UserAccountAvatar.stories.d.ts → UserAccountAvatar/UserAccountAvatar.stories.d.ts} +1 -2
  59. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/UserAccountAvatar/index.d.ts +2 -0
  61. package/dist/types/src/components/UserAccountAvatar/index.d.ts.map +1 -0
  62. package/dist/types/src/components/index.d.ts +0 -1
  63. package/dist/types/src/components/index.d.ts.map +1 -1
  64. package/dist/types/src/components/types.d.ts +20 -14
  65. package/dist/types/src/components/types.d.ts.map +1 -1
  66. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  67. package/dist/types/src/hooks.d.ts.map +1 -1
  68. package/dist/types/src/testing/app-graph-builder.d.ts +3 -0
  69. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -0
  70. package/dist/types/src/testing/generator.d.ts +7 -6
  71. package/dist/types/src/testing/generator.d.ts.map +1 -1
  72. package/dist/types/src/testing/index.d.ts +1 -0
  73. package/dist/types/src/testing/index.d.ts.map +1 -1
  74. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  75. package/dist/types/src/translations.d.ts +1 -0
  76. package/dist/types/src/translations.d.ts.map +1 -1
  77. package/dist/types/src/util.d.ts +7 -10
  78. package/dist/types/src/util.d.ts.map +1 -1
  79. package/dist/types/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +42 -32
  81. package/src/NavTreePlugin.tsx +2 -2
  82. package/src/capabilities/app-graph-builder.ts +38 -28
  83. package/src/capabilities/capabilities.ts +5 -4
  84. package/src/capabilities/intent-resolver.ts +8 -7
  85. package/src/capabilities/keyboard.ts +37 -32
  86. package/src/capabilities/state.ts +23 -17
  87. package/src/components/{CommandsDialog.tsx → CommandsDialog/CommandsDialog.tsx} +8 -7
  88. package/src/components/{CommandsTrigger.tsx → CommandsDialog/CommandsTrigger.tsx} +1 -1
  89. package/src/components/CommandsDialog/index.ts +6 -0
  90. package/src/components/NavTree/NavTree.stories.tsx +159 -0
  91. package/src/components/NavTree/NavTree.tsx +62 -0
  92. package/src/components/NavTree/index.ts +5 -0
  93. package/src/components/NavTreeContainer.tsx +85 -67
  94. package/src/components/NavTreeContext.tsx +1 -1
  95. package/src/components/{NavTreeItemAction.stories.tsx → NavTreeItem/NavTreeItemAction.stories.tsx} +12 -6
  96. package/src/components/{NavTreeItemAction.tsx → NavTreeItem/NavTreeItemAction.tsx} +19 -22
  97. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +73 -0
  98. package/src/components/NavTreeItem/index.ts +6 -0
  99. package/src/components/{L0Menu.tsx → Sidebar/L0Menu.tsx} +186 -131
  100. package/src/components/Sidebar/L1Panel.tsx +175 -0
  101. package/src/components/Sidebar/index.ts +6 -0
  102. package/src/components/{UserAccountAvatar.stories.tsx → UserAccountAvatar/UserAccountAvatar.stories.tsx} +5 -5
  103. package/src/components/{UserAccountAvatar.tsx → UserAccountAvatar/UserAccountAvatar.tsx} +7 -7
  104. package/src/components/UserAccountAvatar/index.ts +5 -0
  105. package/src/components/index.ts +0 -1
  106. package/src/components/types.ts +20 -15
  107. package/src/testing/app-graph-builder.ts +224 -0
  108. package/src/testing/generator.ts +15 -12
  109. package/src/testing/index.ts +1 -0
  110. package/src/translations.ts +1 -0
  111. package/src/util.ts +10 -32
  112. package/dist/lib/browser/app-graph-builder-26D3MUCA.mjs.map +0 -7
  113. package/dist/lib/browser/chunk-4RQJXI4D.mjs.map +0 -7
  114. package/dist/lib/browser/chunk-DN43QXSX.mjs +0 -1222
  115. package/dist/lib/browser/chunk-DN43QXSX.mjs.map +0 -7
  116. package/dist/lib/browser/intent-resolver-BZMUS24C.mjs.map +0 -7
  117. package/dist/lib/browser/keyboard-IQXHKUWM.mjs +0 -46
  118. package/dist/lib/browser/keyboard-IQXHKUWM.mjs.map +0 -7
  119. package/dist/lib/browser/state-M5UJ3MHL.mjs.map +0 -7
  120. package/dist/types/src/components/CommandsDialog.d.ts.map +0 -1
  121. package/dist/types/src/components/CommandsTrigger.d.ts.map +0 -1
  122. package/dist/types/src/components/L0Menu.d.ts +0 -10
  123. package/dist/types/src/components/L0Menu.d.ts.map +0 -1
  124. package/dist/types/src/components/L1Panels.d.ts +0 -14
  125. package/dist/types/src/components/L1Panels.d.ts.map +0 -1
  126. package/dist/types/src/components/NavTree.d.ts +0 -5
  127. package/dist/types/src/components/NavTree.d.ts.map +0 -1
  128. package/dist/types/src/components/NavTree.stories.d.ts +0 -7
  129. package/dist/types/src/components/NavTree.stories.d.ts.map +0 -1
  130. package/dist/types/src/components/NavTreeItemAction.d.ts.map +0 -1
  131. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +0 -41
  132. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +0 -1
  133. package/dist/types/src/components/NavTreeItemColumns.d.ts +0 -4
  134. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +0 -1
  135. package/dist/types/src/components/UserAccountAvatar.d.ts.map +0 -1
  136. package/dist/types/src/components/UserAccountAvatar.stories.d.ts.map +0 -1
  137. package/dist/types/src/stories/Graph.stories.d.ts +0 -7
  138. package/dist/types/src/stories/Graph.stories.d.ts.map +0 -1
  139. package/dist/types/src/stories/StorybookNavTree.d.ts +0 -4
  140. package/dist/types/src/stories/StorybookNavTree.d.ts.map +0 -1
  141. package/src/components/L1Panels.tsx +0 -115
  142. package/src/components/NavTree.stories.tsx +0 -107
  143. package/src/components/NavTree.tsx +0 -51
  144. package/src/components/NavTreeItemColumns.tsx +0 -71
  145. package/src/stories/Graph.stories.tsx +0 -130
  146. package/src/stories/StorybookNavTree.tsx +0 -63
  147. /package/dist/lib/browser/{react-surface-3HCM3SHJ.mjs.map → react-surface-HGA3TAQU.mjs.map} +0 -0
  148. /package/dist/types/src/components/{CommandsDialog.d.ts → CommandsDialog/CommandsDialog.d.ts} +0 -0
  149. /package/dist/types/src/components/{CommandsTrigger.d.ts → CommandsDialog/CommandsTrigger.d.ts} +0 -0
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './CommandsDialog';
6
+ export * from './CommandsTrigger';
@@ -0,0 +1,159 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import { Schema } from 'effect';
9
+ import React from 'react';
10
+
11
+ import {
12
+ Capabilities,
13
+ IntentPlugin,
14
+ LayoutAction,
15
+ SettingsPlugin,
16
+ contributes,
17
+ createResolver,
18
+ defineCapability,
19
+ useCapability,
20
+ } from '@dxos/app-framework';
21
+ import { withPluginManager } from '@dxos/app-framework/testing';
22
+ import { live } from '@dxos/live-object';
23
+ import { AttentionPlugin } from '@dxos/plugin-attention';
24
+ import { GraphPlugin } from '@dxos/plugin-graph';
25
+ import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
26
+ import { ThemePlugin } from '@dxos/plugin-theme';
27
+ import { faker } from '@dxos/random';
28
+ import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
29
+ import { useAttendableAttributes, useAttention } from '@dxos/react-ui-attention';
30
+ import { Stack, StackItem } from '@dxos/react-ui-stack';
31
+ import { defaultTx, mx } from '@dxos/react-ui-theme';
32
+ import { withLayout } from '@dxos/storybook-utils';
33
+
34
+ import { NavTreePlugin } from '../../NavTreePlugin';
35
+ import { storybookGraphBuilders } from '../../testing';
36
+ import translations from '../../translations';
37
+ import { NavTreeContainer } from '../NavTreeContainer';
38
+
39
+ faker.seed(1234);
40
+
41
+ const StoryState = defineCapability<{ tab: string }>('story-state');
42
+
43
+ // TODO(burdon): Fix outline (e.g., button in sidebar nav is clipped when focused).
44
+ // TODO(burdon): Consider similar containment of: Table, Sheet, Kanban Column, Form, etc.
45
+
46
+ const container = 'flex flex-col grow gap-2 p-4 rounded-md';
47
+
48
+ // TODO(burdon): Factor out PlankHeader.
49
+ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
50
+ const { hasAttention } = useAttention(attendableId);
51
+ console.log('hasAttention', hasAttention);
52
+ return (
53
+ <div className='flex p-1 items-center border-b border-separator'>
54
+ <IconButton
55
+ density='coarse'
56
+ icon='ph--atom--regular'
57
+ label='Test'
58
+ iconOnly
59
+ variant={hasAttention ? 'primary' : 'ghost'}
60
+ classNames='is-[--rail-action] bs-[--rail-action]'
61
+ />
62
+ <StackItem.ResizeHandle />
63
+ </div>
64
+ );
65
+ };
66
+
67
+ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
68
+ const attentionAttrs = useAttendableAttributes(attendableId);
69
+
70
+ return (
71
+ <StackItem.Root
72
+ item={{ id: attendableId }}
73
+ {...attentionAttrs}
74
+ classNames='bg-baseSurface border-ie border-separator'
75
+ size={30}
76
+ >
77
+ <StoryPlankHeading attendableId={attendableId} />
78
+ <StackItem.Content toolbar>
79
+ <Toolbar.Root classNames='border-b border-separator'>
80
+ <Toolbar.Button>Test</Toolbar.Button>
81
+ </Toolbar.Root>
82
+
83
+ <div className={mx(container, 'm-2 bg-activeSurface')}>
84
+ <Input.Root>
85
+ <Input.Label>Level 1 (group)</Input.Label>
86
+ </Input.Root>
87
+ <div className={mx(container, 'bg-baseSurface')}>
88
+ <Input.Root>
89
+ <Input.Label>Level 2 (base)</Input.Label>
90
+ <Input.TextArea placeholder='Enter text' />
91
+ </Input.Root>
92
+ </div>
93
+ </div>
94
+ </StackItem.Content>
95
+ </StackItem.Root>
96
+ );
97
+ };
98
+
99
+ const DefaultStory = () => {
100
+ const state = useCapability(StoryState);
101
+
102
+ return (
103
+ <Main.Root complementarySidebarState='closed'>
104
+ <Main.NavigationSidebar label='Navigation' classNames='grid'>
105
+ <NavTreeContainer tab={state.tab} />
106
+ </Main.NavigationSidebar>
107
+ <Main.Content bounce handlesFocus>
108
+ <Stack orientation='horizontal' size='contain'>
109
+ <StoryPlank attendableId='space-0:object-0' />
110
+ <StoryPlank attendableId='space-0:object-1' />
111
+ </Stack>
112
+ </Main.Content>
113
+ </Main.Root>
114
+ );
115
+ };
116
+
117
+ const meta: Meta<typeof NavTreeContainer> = {
118
+ title: 'plugins/plugin-navtree/NavTree',
119
+ component: NavTreeContainer,
120
+ render: DefaultStory,
121
+ decorators: [
122
+ withPluginManager({
123
+ plugins: [
124
+ ThemePlugin({ tx: defaultTx }),
125
+ StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
126
+ GraphPlugin(),
127
+ IntentPlugin(),
128
+ SettingsPlugin(),
129
+ AttentionPlugin(),
130
+ NavTreePlugin(),
131
+ ],
132
+ capabilities: (context) => [
133
+ contributes(StoryState, live({ tab: 'space-0' })),
134
+ contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders),
135
+ contributes(Capabilities.IntentResolver, [
136
+ createResolver({
137
+ intent: LayoutAction.UpdateLayout,
138
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
139
+ Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
140
+ resolve: ({ subject }) => {
141
+ const state = context.getCapability(StoryState);
142
+ state.tab = subject;
143
+ },
144
+ }),
145
+ ]),
146
+ ],
147
+ }),
148
+ withLayout({ fullscreen: true }),
149
+ ],
150
+ parameters: {
151
+ translations,
152
+ },
153
+ };
154
+
155
+ export default meta;
156
+
157
+ type Story = StoryObj<typeof NavTreeContainer>;
158
+
159
+ export const Default: Story = {};
@@ -0,0 +1,62 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { type TreeProps } from '@dxos/react-ui-list';
8
+ import { Tabs } from '@dxos/react-ui-tabs';
9
+
10
+ import { useLoadDescendents } from '../../hooks';
11
+ import { type NavTreeItemGraphNode } from '../../types';
12
+ import { useNavTreeContext } from '../NavTreeContext';
13
+ import { L0Menu, L1Panels, type L1PanelsProps } from '../Sidebar';
14
+
15
+ export const NAV_TREE_ITEM = 'NavTreeItem';
16
+
17
+ export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'> & Pick<L1PanelsProps, 'open'>;
18
+
19
+ export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
20
+ const { tab, useItems, onBack } = useNavTreeContext();
21
+ const topLevelActions = useItems(root, { disposition: 'menu', sort: true });
22
+ const topLevelCollections = useItems(root, { disposition: 'collection' });
23
+ const topLevelWorkspaces = useItems(root, { disposition: 'workspace' });
24
+ const topLevelNavigation = useItems(root, { disposition: 'navigation' });
25
+ const l0Items = useMemo(
26
+ () => [
27
+ // prettier-ignore
28
+ ...topLevelCollections,
29
+ ...topLevelWorkspaces,
30
+ ...topLevelNavigation,
31
+ ],
32
+ [topLevelCollections, topLevelWorkspaces, topLevelNavigation],
33
+ );
34
+ const pinnedItems = useItems(root, { disposition: 'pin-end', sort: true });
35
+ const userAccountItem = useItems(root, { disposition: 'user-account' })[0];
36
+ const topLevelItems = useMemo(
37
+ () => [
38
+ // prettier-ignore
39
+ ...l0Items,
40
+ ...pinnedItems,
41
+ ...(userAccountItem ? [userAccountItem] : []),
42
+ ],
43
+ [l0Items, pinnedItems, userAccountItem],
44
+ );
45
+
46
+ useLoadDescendents(root);
47
+ const path = useMemo(() => [id], [id]);
48
+
49
+ return (
50
+ <Tabs.Root value={tab} orientation='vertical' verticalVariant='stateless' classNames='relative'>
51
+ <L0Menu
52
+ menuActions={topLevelActions}
53
+ topLevelItems={l0Items}
54
+ pinnedItems={pinnedItems}
55
+ userAccountItem={userAccountItem}
56
+ path={path}
57
+ parent={root}
58
+ />
59
+ <L1Panels topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} {...props} />
60
+ </Tabs.Root>
61
+ );
62
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './NavTree';
@@ -16,28 +16,22 @@ import {
16
16
  useIntentDispatcher,
17
17
  useLayout,
18
18
  } from '@dxos/app-framework';
19
- import { isAction, isActionLike, type Node } from '@dxos/app-graph';
19
+ import { isAction, isActionLike, type ReadableGraph, ROOT_ID, type Node } from '@dxos/app-graph';
20
20
  import { PLANK_COMPANION_TYPE } from '@dxos/plugin-deck/types';
21
+ import { useConnections } from '@dxos/plugin-graph';
21
22
  import { isEchoObject, isSpace } from '@dxos/react-client/echo';
22
- import { useMediaQuery } from '@dxos/react-ui';
23
+ import { useMediaQuery, useSidebars } from '@dxos/react-ui';
23
24
  import { isTreeData, type TreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
24
25
  import { mx } from '@dxos/react-ui-theme';
25
- import { arrayMove } from '@dxos/util';
26
+ import { arrayMove, byPosition } from '@dxos/util';
26
27
 
27
28
  import { NAV_TREE_ITEM, NavTree } from './NavTree';
28
29
  import { NavTreeContext } from './NavTreeContext';
29
30
  import { type NavTreeContextValue } from './types';
30
31
  import { NavTreeCapabilities } from '../capabilities';
32
+ import { NAVTREE_PLUGIN } from '../meta';
31
33
  import { type NavTreeItemGraphNode } from '../types';
32
- import {
33
- expandActions,
34
- expandChildren,
35
- getActions as naturalGetActions,
36
- getChildren,
37
- getParent,
38
- resolveMigrationOperation,
39
- expandChildrenAndActions,
40
- } from '../util';
34
+ import { getActions as naturalGetActions, getChildren, getParent, resolveMigrationOperation } from '../util';
41
35
 
42
36
  // TODO(thure): Is NavTree truly authoritative in this regard?
43
37
  export const NODE_TYPE = 'dxos/app-graph/node';
@@ -49,6 +43,29 @@ const renderItemEnd = ({ node, open }: { node: Node; open: boolean }) => (
49
43
  const getChildrenFilter = (node: Node): node is Node =>
50
44
  untracked(() => !isActionLike(node) && node.type !== PLANK_COMPANION_TYPE);
51
45
 
46
+ const filterItems = (node: Node, disposition?: string) => {
47
+ if (!disposition && (node.properties.disposition === 'hidden' || node.properties.disposition === 'alternate-tree')) {
48
+ return false;
49
+ } else if (!disposition) {
50
+ const action = isAction(node);
51
+ return !action || node.properties.disposition === 'item';
52
+ } else {
53
+ return node.properties.disposition === disposition;
54
+ }
55
+ };
56
+
57
+ const getItems = (graph: ReadableGraph, node?: Node, disposition?: string) => {
58
+ return graph.getConnections(node?.id ?? ROOT_ID, 'outbound').filter((node) => filterItems(node, disposition));
59
+ };
60
+
61
+ const useItems = (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
62
+ const { graph } = useAppGraph();
63
+ const connections = useConnections(graph, node?.id ?? ROOT_ID).filter((node) =>
64
+ filterItems(node, options?.disposition),
65
+ );
66
+ return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
67
+ };
68
+
52
69
  export type NavTreeContainerProps = {
53
70
  popoverAnchorId?: string;
54
71
  topbar?: boolean;
@@ -58,34 +75,15 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
58
75
  const [isLg] = useMediaQuery('lg', { ssr: false });
59
76
  const { dispatchPromise: dispatch } = useIntentDispatcher();
60
77
  const { graph } = useAppGraph();
78
+ const { isOpen, isCurrent, isAlternateTree, setItem } = useCapability(NavTreeCapabilities.State);
61
79
  const layout = useLayout();
62
- const { isOpen, isCurrent, setItem } = useCapability(NavTreeCapabilities.State);
80
+ const { navigationSidebarState } = useSidebars(NAVTREE_PLUGIN);
63
81
 
64
82
  const getActions = useCallback((node: Node) => naturalGetActions(graph, node), [graph]);
65
83
 
66
- const getItems = useCallback(
67
- (node?: Node, disposition?: string) => {
68
- return graph.nodes(node ?? graph.root, {
69
- filter: (node): node is Node => {
70
- return untracked(() => {
71
- if (node.properties.disposition === 'hidden') {
72
- return false;
73
- } else if (!disposition) {
74
- const action = isAction(node);
75
- return !action || node.properties.disposition === 'item';
76
- } else {
77
- return node.properties.disposition === disposition;
78
- }
79
- });
80
- },
81
- });
82
- },
83
- [graph],
84
- );
85
-
86
84
  const getProps = useCallback(
87
85
  (node: Node, path: string[]): PropsFromTreeItem => {
88
- const children = getChildren(graph, node, getChildrenFilter, path);
86
+ const children = getChildren(graph, node, path).filter(getChildrenFilter);
89
87
  const parentOf =
90
88
  children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
91
89
  return {
@@ -104,49 +102,59 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
104
102
 
105
103
  const loadDescendents = useCallback(
106
104
  (node: Node) => {
107
- void expandActions(graph, node);
108
- if (!isActionLike(node)) {
109
- void expandChildren(graph, node).then(() =>
110
- // Load one level deeper, which resolves some juddering observed on open/close.
111
- Promise.all(
112
- getChildren(graph, node).flatMap((child) => [expandActions(graph, child), expandChildren(graph, child)]),
113
- ),
114
- );
115
- }
105
+ graph.expand(node.id, 'outbound');
106
+ // Load one level deeper, which resolves some juddering observed on open/close.
107
+ graph.getConnections(node.id, 'outbound').forEach((child) => {
108
+ graph.expand(child.id, 'outbound');
109
+ });
116
110
  },
117
111
  [graph],
118
112
  );
119
113
 
120
- const onOpenChange = useCallback(
114
+ const handleOpenChange = useCallback(
121
115
  ({ item: { id }, path, open }: { item: Node; path: string[]; open: boolean }) => {
122
116
  // TODO(thure): This might become a localstorage leak; openItemIds that no longer exist should be removed from this map.
123
117
  setItem(path, 'open', open);
124
-
125
- if (graph) {
126
- const node = graph.findNode(id);
127
- return node && expandChildrenAndActions(graph, node as NavTreeItemGraphNode);
128
- }
118
+ graph.expand(id, 'outbound');
129
119
  },
130
120
  [graph],
131
121
  );
132
122
 
133
- const onTabChange = useCallback(
123
+ const handleTabChange = useCallback(
134
124
  async (node: NavTreeItemGraphNode) => {
125
+ await dispatch(
126
+ createIntent(LayoutAction.UpdateSidebar, {
127
+ part: 'sidebar',
128
+ options: {
129
+ state:
130
+ node.id === tab
131
+ ? navigationSidebarState === 'expanded'
132
+ ? isLg
133
+ ? 'collapsed'
134
+ : 'closed'
135
+ : 'expanded'
136
+ : 'expanded',
137
+ },
138
+ }),
139
+ );
140
+
135
141
  await dispatch(createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: node.id }));
142
+
136
143
  // Open the first item if the workspace is empty.
137
144
  if (layout.active.length === 0) {
138
- const [item] = getItems(node).filter((node) => !isActionLike(node));
145
+ const [item] = getItems(graph, node).filter((node) => !isActionLike(node));
139
146
  if (item && item.data) {
140
147
  await dispatch(createIntent(LayoutAction.Open, { part: 'main', subject: [item.id] }));
141
148
  }
142
149
  }
143
150
  },
144
- [dispatch, layout],
151
+ [dispatch, layout.active, tab, navigationSidebarState, isLg],
145
152
  );
146
153
 
147
154
  const canDrop = useCallback((source: TreeData, target: TreeData) => {
148
155
  const sourceNode = source.item as Node;
149
156
  const targetNode = target.item as Node;
157
+ // TODO(wittjosiah): This shouldn't depend directly on client, there should be a graph-level abstraction.
150
158
  if (isSpace(targetNode.data)) {
151
159
  // TODO(wittjosiah): Find a way to only allow space as source for rearranging.
152
160
  return isEchoObject(sourceNode.data) || isSpace(sourceNode.data);
@@ -164,8 +172,8 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
164
172
  }
165
173
 
166
174
  if (isAction(node)) {
167
- const [parent] = graph.nodes(node, { relation: 'inbound' });
168
- void (parent && node.data({ node: parent, caller: NAV_TREE_ITEM }));
175
+ const [parent] = graph.getConnections(node.id, 'inbound');
176
+ void (parent && node.data({ parent, caller: NAV_TREE_ITEM }));
169
177
  return;
170
178
  }
171
179
 
@@ -186,7 +194,7 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
186
194
  void dispatch(createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: node.id }));
187
195
  }
188
196
 
189
- const defaultAction = graph.actions(node).find((action) => action.properties?.disposition === 'default');
197
+ const defaultAction = graph.getActions(node.id).find((action) => action.properties?.disposition === 'default');
190
198
  if (isAction(defaultAction)) {
191
199
  void (defaultAction.data as () => void)();
192
200
  }
@@ -198,7 +206,7 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
198
206
  [graph, dispatch, isCurrent, isLg],
199
207
  );
200
208
 
201
- const onBack = useCallback(
209
+ const handleBack = useCallback(
202
210
  () => dispatch(createIntent(LayoutAction.RevertWorkspace, { part: 'workspace', options: { revert: true } })),
203
211
  [dispatch],
204
212
  );
@@ -225,8 +233,8 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
225
233
  : resolveMigrationOperation(graph, sourceNode, targetPath, targetNode);
226
234
  const sourceParent = getParent(graph, sourceNode, sourcePath);
227
235
  const targetParent = getParent(graph, targetNode, targetPath);
228
- const sourceItems = getItems(sourceParent);
229
- const targetItems = getItems(targetParent);
236
+ const sourceItems = getItems(graph, sourceParent);
237
+ const targetItems = getItems(graph, targetParent);
230
238
  const sourceIndex = sourceItems.findIndex(({ id }) => id === sourceNode.id);
231
239
  const targetIndex = targetItems.findIndex(({ id }) => id === targetNode.id);
232
240
  const migrationIndex =
@@ -262,46 +270,56 @@ export const NavTreeContainer = memo(({ tab, popoverAnchorId, topbar }: NavTreeC
262
270
  });
263
271
  }, [graph]);
264
272
 
273
+ const setAlternateTree = useCallback(
274
+ (path: string[], open: boolean) => {
275
+ setItem(path, 'alternateTree', open);
276
+ },
277
+ [setItem],
278
+ );
279
+
265
280
  const navTreeContextValue = useMemo(
266
281
  () => ({
282
+ useItems,
267
283
  tab,
268
- onTabChange,
269
- onBack,
270
284
  getActions,
271
285
  loadDescendents,
272
286
  renderItemEnd,
273
287
  popoverAnchorId,
274
288
  topbar,
275
- getItems,
276
289
  getProps,
277
290
  isCurrent,
278
291
  isOpen,
279
- onOpenChange,
280
292
  canDrop,
293
+ isAlternateTree,
294
+ setAlternateTree,
295
+ onTabChange: handleTabChange,
296
+ onOpenChange: handleOpenChange,
281
297
  onSelect: handleSelect,
298
+ onBack: handleBack,
282
299
  }),
283
300
  [
284
301
  tab,
285
- onTabChange,
286
- onBack,
287
302
  getActions,
288
303
  loadDescendents,
289
304
  renderItemEnd,
290
305
  popoverAnchorId,
291
306
  topbar,
292
- getItems,
293
307
  getProps,
294
308
  isCurrent,
295
309
  isOpen,
296
- onOpenChange,
297
310
  canDrop,
311
+ isAlternateTree,
312
+ setAlternateTree,
313
+ handleTabChange,
314
+ handleOpenChange,
298
315
  handleSelect,
316
+ handleBack,
299
317
  ],
300
318
  );
301
319
 
302
320
  return (
303
321
  <NavTreeContext.Provider value={navTreeContextValue}>
304
- <NavTree root={graph.root} id={graph.root.id} />
322
+ <NavTree id={ROOT_ID} root={graph.root} open={layout.sidebarOpen} />
305
323
  </NavTreeContext.Provider>
306
324
  );
307
325
  });
@@ -10,4 +10,4 @@ import { type NavTreeContextValue } from './types';
10
10
 
11
11
  export const NavTreeContext = createContext<NavTreeContextValue | null>(null);
12
12
 
13
- export const useNavTreeContext = () => useContext(NavTreeContext) ?? raise(new Error('NavTreeContext not found'));
13
+ export const useNavTreeContext = () => useContext(NavTreeContext) ?? raise(new Error('Missing NavTreeContext'));
@@ -4,6 +4,8 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+
7
9
  import { type NodeArg } from '@dxos/app-graph';
8
10
  import { faker } from '@dxos/random';
9
11
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -37,19 +39,23 @@ const menuActions = faker.helpers.multiple(
37
39
  { count: 20 },
38
40
  );
39
41
 
40
- export default {
42
+ const meta: Meta<typeof NavTreeItemAction> = {
41
43
  title: 'plugins/plugin-navtree/NavTreeItemAction',
42
44
  component: NavTreeItemAction,
43
- decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
45
+ decorators: [withTheme, withLayout()],
44
46
  args: {
45
47
  icon: 'ph--list--regular',
46
48
  parent,
47
49
  menuActions,
48
50
  label: 'Select action',
49
- onAction: (action) => {
50
- console.log(action);
51
- },
52
51
  } satisfies Partial<NavTreeItemActionMenuProps>,
52
+ parameters: {
53
+ layout: 'centered',
54
+ },
53
55
  };
54
56
 
55
- export const Default = {};
57
+ export default meta;
58
+
59
+ type Story = StoryObj<typeof meta>;
60
+
61
+ export const Default: Story = {};
@@ -2,54 +2,56 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { type MutableRefObject, useRef } from 'react';
5
+ import React from 'react';
6
6
 
7
7
  import { type Action, type Node } from '@dxos/app-graph';
8
- import { useTranslation, toLocalizedString, IconButton } from '@dxos/react-ui';
8
+ import { useTranslation, toLocalizedString, IconButton, useDensityContext } from '@dxos/react-ui';
9
9
  import { DropdownMenu, MenuProvider, type MenuItem } from '@dxos/react-ui-menu';
10
10
  import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
11
11
 
12
- import { NAVTREE_PLUGIN } from '../meta';
13
- import { type ActionProperties } from '../types';
12
+ import { NAVTREE_PLUGIN } from '../../meta';
13
+ import { type ActionProperties } from '../../types';
14
14
 
15
15
  export type NavTreeItemActionMenuProps = ActionProperties & {
16
16
  parent: Node;
17
17
  caller?: string;
18
18
  monolithic?: boolean;
19
19
  menuActions?: Action[];
20
- suppressNextTooltip?: MutableRefObject<boolean>;
21
- onAction?: (action: Action) => void;
22
20
  };
23
21
 
24
22
  const fallbackIcon = 'ph--placeholder--regular';
25
23
 
26
- const actionButtonProps = {
24
+ const fineActionButtonProps = {
27
25
  size: 4 as const,
28
26
  density: 'fine' as const,
29
27
  };
28
+ const coarseActionButtonProps = {
29
+ size: 5 as const,
30
+ density: 'coarse' as const,
31
+ };
30
32
 
31
33
  export const NavTreeItemActionDropdownMenu = ({
34
+ parent,
32
35
  label,
33
36
  icon,
34
37
  testId,
35
38
  menuActions,
36
- onAction,
39
+ caller,
37
40
  }: NavTreeItemActionMenuProps) => {
38
41
  const { t } = useTranslation(NAVTREE_PLUGIN);
39
- const suppressNextTooltip = useRef<boolean>(false);
42
+ const density = useDensityContext();
40
43
  return (
41
- <MenuProvider onAction={onAction}>
42
- <DropdownMenu.Root items={menuActions as MenuItem[]} suppressNextTooltip={suppressNextTooltip}>
44
+ <MenuProvider>
45
+ <DropdownMenu.Root group={parent} items={menuActions as MenuItem[]} caller={caller}>
43
46
  <DropdownMenu.Trigger asChild>
44
47
  <IconButton
45
- {...actionButtonProps}
48
+ {...(density === 'coarse' ? coarseActionButtonProps : fineActionButtonProps)}
46
49
  classNames={mx('shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem)}
47
50
  variant='ghost'
48
51
  icon={icon ?? fallbackIcon}
49
52
  iconOnly
50
53
  label={toLocalizedString(label, t)}
51
54
  data-testid={testId}
52
- suppressNextTooltip={suppressNextTooltip}
53
55
  />
54
56
  </DropdownMenu.Trigger>
55
57
  </DropdownMenu.Root>
@@ -63,9 +65,10 @@ export const NavTreeItemMonolithicAction = ({
63
65
  data: invoke,
64
66
  baseLabel,
65
67
  }: Action & { parent: Node; onAction?: (action: Action) => void; baseLabel: string }) => {
68
+ const density = useDensityContext();
66
69
  return (
67
70
  <IconButton
68
- {...actionButtonProps}
71
+ {...(density === 'coarse' ? coarseActionButtonProps : fineActionButtonProps)}
69
72
  variant={variant}
70
73
  classNames={mx(
71
74
  'shrink-0',
@@ -82,7 +85,7 @@ export const NavTreeItemMonolithicAction = ({
82
85
  return;
83
86
  }
84
87
  event.stopPropagation();
85
- void invoke?.(caller ? { node: parent, caller } : { node: parent });
88
+ void invoke?.(caller ? { parent, caller } : { parent });
86
89
  }}
87
90
  data-testid={testId}
88
91
  />
@@ -98,12 +101,6 @@ export const NavTreeItemAction = ({ monolithic, menuActions, parent: node, ...pr
98
101
  return monolithic && menuActions?.length === 1 ? (
99
102
  <NavTreeItemMonolithicAction baseLabel={baseLabel} parent={node} {...menuActions[0]} />
100
103
  ) : (
101
- <NavTreeItemActionDropdownMenu
102
- {...props}
103
- label={baseLabel}
104
- parent={node}
105
- menuActions={menuActions}
106
- onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
107
- />
104
+ <NavTreeItemActionDropdownMenu {...props} label={baseLabel} parent={node} menuActions={menuActions} />
108
105
  );
109
106
  };