@dxos/plugin-navtree 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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 (223) hide show
  1. package/dist/lib/browser/index.mjs +157 -124
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +21 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/types/src/NavTreePlugin.d.ts +2 -1
  7. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  8. package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
  9. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  10. package/dist/types/src/capabilities/index.d.ts +7 -105
  11. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  12. package/dist/types/src/capabilities/keyboard.d.ts +3 -2
  13. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  14. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  15. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  16. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  17. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  18. package/dist/types/src/capabilities/state.d.ts +4 -18
  19. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  20. package/dist/types/src/components/NavTree/NavTree.d.ts +12 -5
  21. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  22. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +25 -5
  23. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/{NavTreeContext.d.ts → NavTreeContext/NavTreeContext.d.ts} +1 -1
  25. package/dist/types/src/components/NavTreeContext/NavTreeContext.d.ts.map +1 -0
  26. package/dist/types/src/components/NavTreeContext/index.d.ts +2 -0
  27. package/dist/types/src/components/NavTreeContext/index.d.ts.map +1 -0
  28. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +13 -9
  29. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  30. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +33 -4
  31. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +2 -2
  33. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  34. package/dist/types/src/components/Sidebar/L0Menu.d.ts +10 -5
  35. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  36. package/dist/types/src/components/Sidebar/L1Panel.d.ts +4 -8
  37. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  38. package/dist/types/src/components/Sidebar/L1Tabs.d.ts +13 -0
  39. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -0
  40. package/dist/types/src/components/Sidebar/index.d.ts +1 -0
  41. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -1
  42. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  43. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  44. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/index.d.ts +1 -4
  46. package/dist/types/src/components/index.d.ts.map +1 -1
  47. package/dist/types/src/components/types.d.ts +8 -17
  48. package/dist/types/src/components/types.d.ts.map +1 -1
  49. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts +6 -0
  50. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.d.ts.map +1 -0
  51. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +28 -0
  52. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -0
  53. package/dist/types/src/containers/CommandsDialogContent/index.d.ts +2 -0
  54. package/dist/types/src/containers/CommandsDialogContent/index.d.ts.map +1 -0
  55. package/dist/types/src/containers/CommandsTrigger/CommandsTrigger.d.ts.map +1 -0
  56. package/dist/types/src/containers/CommandsTrigger/index.d.ts +2 -0
  57. package/dist/types/src/containers/CommandsTrigger/index.d.ts.map +1 -0
  58. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts +9 -0
  59. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -0
  60. package/dist/types/src/containers/NavTreeContainer/index.d.ts +3 -0
  61. package/dist/types/src/containers/NavTreeContainer/index.d.ts.map +1 -0
  62. package/dist/types/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.d.ts +1 -1
  63. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -0
  64. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts +2 -0
  65. package/dist/types/src/containers/NavTreeDocumentTitle/index.d.ts.map +1 -0
  66. package/dist/types/src/containers/index.d.ts +7 -0
  67. package/dist/types/src/containers/index.d.ts.map +1 -0
  68. package/dist/types/src/experimental/Tree.d.ts +5 -6
  69. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  70. package/dist/types/src/experimental/Tree.stories.d.ts +23 -6
  71. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  72. package/dist/types/src/hooks/index.d.ts +5 -0
  73. package/dist/types/src/hooks/index.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useActions.d.ts +5 -0
  75. package/dist/types/src/hooks/useActions.d.ts.map +1 -0
  76. package/dist/types/src/hooks/useLoadDescendents.d.ts +6 -0
  77. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -0
  78. package/dist/types/src/hooks/useNavTreeModel.d.ts +7 -0
  79. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -0
  80. package/dist/types/src/hooks/useNavTreeState.d.ts +18 -0
  81. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -0
  82. package/dist/types/src/index.d.ts +1 -2
  83. package/dist/types/src/index.d.ts.map +1 -1
  84. package/dist/types/src/meta.d.ts +3 -4
  85. package/dist/types/src/meta.d.ts.map +1 -1
  86. package/dist/types/src/operations/expose.d.ts +5 -0
  87. package/dist/types/src/operations/expose.d.ts.map +1 -0
  88. package/dist/types/src/operations/index.d.ts +3 -0
  89. package/dist/types/src/operations/index.d.ts.map +1 -0
  90. package/dist/types/src/testing/app-graph-builder.d.ts +2 -3
  91. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  92. package/dist/types/src/testing/generator.d.ts +5 -6
  93. package/dist/types/src/testing/generator.d.ts.map +1 -1
  94. package/dist/types/src/testing/update-state.d.ts +2 -2
  95. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  96. package/dist/types/src/translations.d.ts +8 -10
  97. package/dist/types/src/translations.d.ts.map +1 -1
  98. package/dist/types/src/types/capabilities.d.ts +15 -0
  99. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  100. package/dist/types/src/types/events.d.ts +5 -0
  101. package/dist/types/src/types/events.d.ts.map +1 -0
  102. package/dist/types/src/types/index.d.ts +4 -0
  103. package/dist/types/src/types/index.d.ts.map +1 -0
  104. package/dist/types/src/{types.d.ts → types/schema.d.ts} +10 -9
  105. package/dist/types/src/types/schema.d.ts.map +1 -0
  106. package/dist/types/src/util.d.ts +12 -9
  107. package/dist/types/src/util.d.ts.map +1 -1
  108. package/dist/types/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +74 -59
  110. package/src/NavTreePlugin.tsx +66 -86
  111. package/src/capabilities/app-graph-builder.ts +34 -44
  112. package/src/capabilities/index.ts +10 -8
  113. package/src/capabilities/keyboard.ts +58 -51
  114. package/src/capabilities/operation-handler.ts +16 -0
  115. package/src/capabilities/react-surface.tsx +46 -38
  116. package/src/capabilities/state.ts +129 -84
  117. package/src/components/NavTree/NavTree.stories.tsx +111 -103
  118. package/src/components/NavTree/NavTree.tsx +71 -35
  119. package/src/components/{NavTreeContext.tsx → NavTreeContext/NavTreeContext.tsx} +1 -1
  120. package/src/components/NavTreeContext/index.ts +5 -0
  121. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +25 -19
  122. package/src/components/NavTreeItem/NavTreeItemAction.tsx +73 -50
  123. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +70 -38
  124. package/src/components/Sidebar/L0Menu.tsx +176 -210
  125. package/src/components/Sidebar/L1Panel.tsx +212 -135
  126. package/src/components/Sidebar/L1Tabs.tsx +42 -0
  127. package/src/components/Sidebar/index.ts +1 -0
  128. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -9
  129. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  130. package/src/components/index.ts +2 -4
  131. package/src/components/types.ts +14 -19
  132. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +46 -0
  133. package/src/containers/CommandsDialogContent/CommandsDialogContent.tsx +127 -0
  134. package/src/containers/CommandsDialogContent/index.ts +5 -0
  135. package/src/containers/CommandsTrigger/CommandsTrigger.tsx +28 -0
  136. package/src/containers/CommandsTrigger/index.ts +5 -0
  137. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +271 -0
  138. package/src/containers/NavTreeContainer/index.ts +6 -0
  139. package/src/{components → containers/NavTreeDocumentTitle}/NavTreeDocumentTitle.tsx +5 -4
  140. package/src/containers/NavTreeDocumentTitle/index.ts +5 -0
  141. package/src/containers/index.ts +12 -0
  142. package/src/experimental/Tree.stories.tsx +110 -115
  143. package/src/experimental/Tree.tsx +16 -34
  144. package/src/hooks/index.ts +8 -0
  145. package/src/hooks/useActions.ts +34 -0
  146. package/src/hooks/useLoadDescendents.ts +23 -0
  147. package/src/hooks/useNavTreeModel.ts +118 -0
  148. package/src/hooks/useNavTreeState.ts +35 -0
  149. package/src/index.ts +1 -2
  150. package/src/meta.ts +11 -8
  151. package/src/operations/expose.ts +42 -0
  152. package/src/operations/index.ts +7 -0
  153. package/src/testing/app-graph-builder.ts +167 -238
  154. package/src/testing/generator.ts +41 -39
  155. package/src/testing/update-state.ts +6 -6
  156. package/src/translations.ts +8 -9
  157. package/src/types/capabilities.ts +21 -0
  158. package/src/types/events.ts +12 -0
  159. package/src/types/index.ts +7 -0
  160. package/src/{types.ts → types/schema.ts} +9 -8
  161. package/src/util.ts +26 -33
  162. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs +0 -45
  163. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs.map +0 -7
  164. package/dist/lib/browser/chunk-ERDGQTH5.mjs +0 -16
  165. package/dist/lib/browser/chunk-ERDGQTH5.mjs.map +0 -7
  166. package/dist/lib/browser/chunk-TZA65AKW.mjs +0 -1428
  167. package/dist/lib/browser/chunk-TZA65AKW.mjs.map +0 -7
  168. package/dist/lib/browser/chunk-WCARAZ7J.mjs +0 -15
  169. package/dist/lib/browser/chunk-WCARAZ7J.mjs.map +0 -7
  170. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs +0 -47
  171. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs.map +0 -7
  172. package/dist/lib/browser/keyboard-QRS6WOQH.mjs +0 -51
  173. package/dist/lib/browser/keyboard-QRS6WOQH.mjs.map +0 -7
  174. package/dist/lib/browser/react-surface-N2HPMYCU.mjs +0 -51
  175. package/dist/lib/browser/react-surface-N2HPMYCU.mjs.map +0 -7
  176. package/dist/lib/browser/state-54DY3DOY.mjs +0 -117
  177. package/dist/lib/browser/state-54DY3DOY.mjs.map +0 -7
  178. package/dist/lib/node-esm/app-graph-builder-GY7GCTGU.mjs +0 -46
  179. package/dist/lib/node-esm/app-graph-builder-GY7GCTGU.mjs.map +0 -7
  180. package/dist/lib/node-esm/chunk-CY7QWR4R.mjs +0 -1429
  181. package/dist/lib/node-esm/chunk-CY7QWR4R.mjs.map +0 -7
  182. package/dist/lib/node-esm/chunk-P2JGW3WR.mjs +0 -18
  183. package/dist/lib/node-esm/chunk-P2JGW3WR.mjs.map +0 -7
  184. package/dist/lib/node-esm/chunk-V3QXFDDP.mjs +0 -16
  185. package/dist/lib/node-esm/chunk-V3QXFDDP.mjs.map +0 -7
  186. package/dist/lib/node-esm/index.mjs +0 -150
  187. package/dist/lib/node-esm/index.mjs.map +0 -7
  188. package/dist/lib/node-esm/intent-resolver-R7UNPSZY.mjs +0 -48
  189. package/dist/lib/node-esm/intent-resolver-R7UNPSZY.mjs.map +0 -7
  190. package/dist/lib/node-esm/keyboard-MOZWHFUM.mjs +0 -52
  191. package/dist/lib/node-esm/keyboard-MOZWHFUM.mjs.map +0 -7
  192. package/dist/lib/node-esm/meta.json +0 -1
  193. package/dist/lib/node-esm/react-surface-EW5FDMW4.mjs +0 -52
  194. package/dist/lib/node-esm/react-surface-EW5FDMW4.mjs.map +0 -7
  195. package/dist/lib/node-esm/state-Z3OTRC2I.mjs +0 -118
  196. package/dist/lib/node-esm/state-Z3OTRC2I.mjs.map +0 -7
  197. package/dist/types/src/capabilities/capabilities.d.ts +0 -21
  198. package/dist/types/src/capabilities/capabilities.d.ts.map +0 -1
  199. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  200. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  201. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +0 -5
  202. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  203. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  204. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  205. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  206. package/dist/types/src/components/NavTreeContainer.d.ts +0 -9
  207. package/dist/types/src/components/NavTreeContainer.d.ts.map +0 -1
  208. package/dist/types/src/components/NavTreeContext.d.ts.map +0 -1
  209. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +0 -1
  210. package/dist/types/src/events.d.ts +0 -4
  211. package/dist/types/src/events.d.ts.map +0 -1
  212. package/dist/types/src/hooks.d.ts +0 -3
  213. package/dist/types/src/hooks.d.ts.map +0 -1
  214. package/dist/types/src/types.d.ts.map +0 -1
  215. package/src/capabilities/capabilities.ts +0 -20
  216. package/src/capabilities/intent-resolver.ts +0 -37
  217. package/src/components/CommandsDialog/CommandsDialog.tsx +0 -118
  218. package/src/components/CommandsDialog/CommandsTrigger.tsx +0 -37
  219. package/src/components/CommandsDialog/index.ts +0 -6
  220. package/src/components/NavTreeContainer.tsx +0 -314
  221. package/src/events.ts +0 -11
  222. package/src/hooks.ts +0 -21
  223. /package/dist/types/src/{components/CommandsDialog → containers/CommandsTrigger}/CommandsTrigger.d.ts +0 -0
@@ -2,44 +2,52 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React from 'react';
5
+ import * as Effect from 'effect/Effect';
6
+ import React, { type ComponentProps } from 'react';
6
7
 
7
- import { Capabilities, contributes, createSurface } from '@dxos/app-framework';
8
- import { isGraphNode } from '@dxos/plugin-graph';
8
+ import { Capabilities, Capability } from '@dxos/app-framework';
9
+ import { Surface } from '@dxos/app-framework/ui';
10
+ import { AppSurface } from '@dxos/app-toolkit/ui';
11
+ import { Node } from '@dxos/plugin-graph';
9
12
 
10
- import { CommandsDialogContent, CommandsTrigger, NavTreeDocumentTitle, NavTreeContainer } from '../components';
11
- import { COMMANDS_DIALOG, NAVTREE_PLUGIN } from '../meta';
13
+ import { CommandsDialogContent, CommandsTrigger, NavTreeContainer, NavTreeDocumentTitle } from '#containers';
14
+ import { COMMANDS_DIALOG } from '#meta';
12
15
 
13
- export default () =>
14
- contributes(Capabilities.ReactSurface, [
15
- createSurface({
16
- id: COMMANDS_DIALOG,
17
- role: 'dialog',
18
- filter: (data): data is { props: { selected?: string } } => data.component === COMMANDS_DIALOG,
19
- component: ({ data }) => <CommandsDialogContent {...data.props} />,
20
- }),
21
- createSurface({
22
- id: `${NAVTREE_PLUGIN}/navigation`,
23
- role: 'navigation',
24
- filter: (data): data is { popoverAnchorId?: string; topbar: boolean; current: string } =>
25
- typeof data.current === 'string',
26
- component: ({ data }) => (
27
- <NavTreeContainer
28
- tab={data.current}
29
- popoverAnchorId={data.popoverAnchorId as string | undefined}
30
- topbar={data.topbar as boolean}
31
- />
32
- ),
33
- }),
34
- createSurface({
35
- id: `${NAVTREE_PLUGIN}/document-title`,
36
- role: 'document-title',
37
- component: ({ data }) => <NavTreeDocumentTitle node={isGraphNode(data.subject) ? data.subject : undefined} />,
38
- }),
39
- createSurface({
40
- id: `${NAVTREE_PLUGIN}/search-input`,
41
- role: 'search-input',
42
- position: 'fallback',
43
- component: () => <CommandsTrigger />,
44
- }),
45
- ]);
16
+ export default Capability.makeModule(() =>
17
+ Effect.succeed(
18
+ Capability.contributes(Capabilities.ReactSurface, [
19
+ Surface.create({
20
+ id: COMMANDS_DIALOG,
21
+ filter: AppSurface.component<ComponentProps<typeof CommandsDialogContent>>(AppSurface.Dialog, COMMANDS_DIALOG),
22
+ component: ({ data, ref }) => <CommandsDialogContent {...data.props} ref={ref} />,
23
+ }),
24
+ Surface.create({
25
+ id: 'navigation',
26
+ role: 'navigation',
27
+ filter: (data): data is { popoverAnchorId?: string; current: string } => typeof data.current === 'string',
28
+ component: ({ data, ref }) => {
29
+ return (
30
+ <NavTreeContainer
31
+ tab={data.current}
32
+ popoverAnchorId={data.popoverAnchorId as string | undefined}
33
+ ref={ref}
34
+ />
35
+ );
36
+ },
37
+ }),
38
+ Surface.create({
39
+ id: 'document-title',
40
+ role: 'document-title',
41
+ component: ({ data }) => (
42
+ <NavTreeDocumentTitle node={Node.isGraphNode(data.subject) ? data.subject : undefined} />
43
+ ),
44
+ }),
45
+ Surface.create({
46
+ id: 'search-input',
47
+ role: 'search-input',
48
+ position: 'fallback',
49
+ component: () => <CommandsTrigger />,
50
+ }),
51
+ ]),
52
+ ),
53
+ );
@@ -2,103 +2,148 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { effect, untracked } from '@preact/signals-core';
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
6
7
 
7
- import { Capabilities, contributes, type PluginContext } from '@dxos/app-framework';
8
- import { type Live } from '@dxos/echo';
9
- import { live } from '@dxos/live-object';
8
+ import { Capabilities, Capability } from '@dxos/app-framework';
9
+ import { AppCapabilities } from '@dxos/app-toolkit';
10
+ import { Graph, Node } from '@dxos/plugin-graph';
10
11
  import { Path } from '@dxos/react-ui-list';
11
12
 
12
- import { NavTreeCapabilities } from './capabilities';
13
- import { NAVTREE_PLUGIN } from '../meta';
13
+ import { meta } from '#meta';
14
+ import { type NavTreeCapabilities as NC, NavTreeCapabilities } from '#types';
14
15
 
15
- const KEY = `${NAVTREE_PLUGIN}/state/v1`;
16
+ const KEY = `${meta.id}.state.v1`;
16
17
 
17
- const getInitialState = () => {
18
+ /** Default item state for new entries. */
19
+ const defaultItemState: NC.NavTreeItemState = { open: false, current: false };
20
+
21
+ /** L0 (top-level workspace) paths are direct children of root — not part of the expandable tree model. */
22
+ const isTopLevelPath = (path: string[]): boolean => path.length === 2 && path[0] === Node.RootId;
23
+
24
+ /** Default state entries for initial tree structure. */
25
+ // TODO(thure): Initialize these dynamically.
26
+ const defaultStateEntries: [string, NC.NavTreeItemState][] = [['root', { open: true, current: false }]];
27
+
28
+ const getInitialState = (): Map<string, NC.NavTreeItemState> => {
18
29
  const stringified = localStorage.getItem(KEY);
19
30
  if (!stringified) {
20
- return;
31
+ return new Map(defaultStateEntries);
21
32
  }
22
33
 
23
34
  try {
24
35
  const cached: [string, { open: boolean; current: boolean }][] = JSON.parse(stringified);
25
- return cached.map(([key, value]): [string, Live<{ open: boolean; current: boolean }>] => [
26
- key,
27
- live({ open: value.open, current: false }),
28
- ]);
29
- } catch {}
36
+ return new Map(cached.map(([key, value]) => [key, { open: value.open, current: false }]));
37
+ } catch {
38
+ return new Map(defaultStateEntries);
39
+ }
30
40
  };
31
41
 
32
- export default (context: PluginContext) => {
33
- const layout = context.getCapability(Capabilities.Layout);
34
-
35
- // TODO(wittjosiah): This currently needs to be not a Live at the root.
36
- // If it is a Live then React errors when initializing new paths because of state change during render.
37
- // Ideally this could be a Live but be able to access and update the root level without breaking render.
38
- // Wrapping accesses and updates in `untracked` didn't seem to work in all cases.
39
- const state = new Map<string, Live<{ open: boolean; current: boolean; alternateTree?: boolean }>>(
40
- getInitialState() ?? [
41
- // TODO(thure): Initialize these dynamically.
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 })],
45
- ],
46
- );
47
-
48
- const getItem = (_path: string[]) => {
49
- const path = Path.create(..._path);
50
- const value = state.get(path) ?? live({ open: false, current: false, alternateTree: false });
51
- if (!state.has(path)) {
52
- state.set(path, value);
53
- }
54
-
55
- return value;
56
- };
57
-
58
- const setItem = (path: string[], key: 'open' | 'current' | 'alternateTree', next: boolean) => {
59
- const value = getItem(path);
60
- value[key] = next;
61
-
62
- localStorage.setItem(KEY, JSON.stringify(Array.from(state.entries())));
63
- };
64
-
65
- const isOpen = (path: string[]) => getItem(path).open;
66
- const isCurrent = (path: string[]) => getItem(path).current;
67
- const isAlternateTree = (path: string[]) => {
68
- const item = getItem(path);
69
- return item.alternateTree ?? false;
70
- };
71
-
72
- let previous: string[] = [];
73
- const unsubscribe = effect(() => {
74
- const removed = previous.filter((id) => !layout.active.includes(id));
75
- previous = layout.active;
76
-
77
- const handleUpdate = () => {
78
- removed.forEach((id) => {
79
- const keys = Array.from(state.keys()).filter((key) => Path.last(key) === id);
80
- keys.forEach((key) => {
81
- setItem(Path.parts(key), 'current', false);
82
- });
83
- });
42
+ export default Capability.makeModule(
43
+ Effect.fnUntraced(function* () {
44
+ const registry = yield* Capability.get(Capabilities.AtomRegistry);
45
+ const layoutAtom = yield* Capability.get(AppCapabilities.Layout);
46
+
47
+ // Backing state for persistence (not reactive).
48
+ const backingState = getInitialState();
49
+
50
+ // Per-path atom family for fine-grained reactivity.
51
+ // keepAlive prevents atoms from being garbage collected when components unmount,
52
+ // ensuring state is preserved across deletion/restoration cycles.
53
+ const itemAtomFamily = Atom.family((pathString: string) =>
54
+ Atom.make<NC.NavTreeItemState>(backingState.get(pathString) ?? { ...defaultItemState }).pipe(Atom.keepAlive),
55
+ );
56
+
57
+ const getItemAtom = (path: string[]): Atom.Atom<NC.NavTreeItemState> => {
58
+ const pathString = Path.create(...path);
59
+ if (!backingState.has(pathString)) {
60
+ backingState.set(pathString, { ...defaultItemState });
61
+ }
62
+ return itemAtomFamily(pathString);
63
+ };
84
64
 
85
- layout.active.forEach((id) => {
86
- const keys = Array.from(new Set([...state.keys(), id])).filter((key) => Path.last(key) === id);
87
- keys.forEach((key) => {
88
- setItem(Path.parts(key), 'current', true);
89
- });
90
- });
65
+ const getItem = (path: string[]): NC.NavTreeItemState => {
66
+ return registry.get(getItemAtom(path));
91
67
  };
92
68
 
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);
99
- });
100
-
101
- return contributes(NavTreeCapabilities.State, { state, getItem, setItem, isOpen, isCurrent, isAlternateTree }, () =>
102
- unsubscribe(),
103
- );
104
- };
69
+ const setItem = (path: string[], key: 'open' | 'current', next: boolean) => {
70
+ const pathString = Path.create(...path);
71
+ const atom = itemAtomFamily(pathString);
72
+ const currentValue = registry.get(atom);
73
+ const newValue = { ...currentValue, [key]: next };
74
+
75
+ registry.set(atom, newValue);
76
+
77
+ if (!(key === 'open' && isTopLevelPath(path))) {
78
+ backingState.set(pathString, newValue);
79
+ localStorage.setItem(KEY, JSON.stringify(Array.from(backingState.entries())));
80
+ }
81
+ };
82
+
83
+ // Subscribe to layout changes to update current state.
84
+ let previous: string[] = [];
85
+ const unsubscribe = registry.subscribe(layoutAtom, (layout) => {
86
+ const removed = previous.filter((id) => !layout.active.includes(id));
87
+ previous = layout.active;
88
+
89
+ const handleUpdate = () => {
90
+ // Mark removed items as not current.
91
+ removed.forEach((id) => {
92
+ const keys = Array.from(backingState.keys()).filter((key) => Path.last(key) === id);
93
+ keys.forEach((key) => {
94
+ setItem(Path.parts(key), 'current', false);
95
+ });
96
+ });
97
+
98
+ // Mark active items as current.
99
+ layout.active.forEach((id: string) => {
100
+ const keys = Array.from(new Set([...backingState.keys(), id])).filter((key) => Path.last(key) === id);
101
+ keys.forEach((key) => {
102
+ setItem(Path.parts(key), 'current', true);
103
+ });
104
+ });
105
+ };
106
+
107
+ // TODO(wittjosiah): This is setTimeout because there's a race between the keys being initialized.
108
+ // Keys are initialized on the first render of an item in the navtree.
109
+ // This could be avoided if the location was a path as well and not just an id.
110
+ const timeout = setTimeout(handleUpdate, 500);
111
+ handleUpdate();
112
+ return () => clearTimeout(timeout);
113
+ });
114
+
115
+ // Once graph is ready, expand every node marked open in state so the graph has children loaded for rendering.
116
+ yield* Effect.gen(function* () {
117
+ const { graph } = yield* Capability.waitFor(AppCapabilities.AppGraph);
118
+
119
+ // Always expand the active workspace so its subtree is initialized.
120
+ const layout = registry.get(layoutAtom);
121
+ if (layout.workspace) {
122
+ Graph.expand(graph, layout.workspace, 'child');
123
+ }
124
+
125
+ // Expand persisted open nodes, skipping inactive workspace tabs.
126
+ const openPaths = Array.from(backingState.entries())
127
+ .filter(([, state]) => state.open)
128
+ .map(([pathString]) => Path.parts(pathString))
129
+ .filter((path) => !isTopLevelPath(path));
130
+ for (const path of openPaths) {
131
+ const nodeId = path[path.length - 1];
132
+ if (!nodeId) {
133
+ continue;
134
+ }
135
+ Graph.expand(graph, nodeId, 'child');
136
+ }
137
+ }).pipe(Effect.forkDaemon);
138
+
139
+ return Capability.contributes(
140
+ NavTreeCapabilities.State,
141
+ {
142
+ getItem,
143
+ getItemAtom,
144
+ setItem,
145
+ },
146
+ () => Effect.sync(() => unsubscribe()),
147
+ );
148
+ }),
149
+ );
@@ -2,45 +2,34 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
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';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import * as Effect from 'effect/Effect';
8
+ import React, { type KeyboardEvent, useCallback, useRef } from 'react';
9
+ import { expect, userEvent, within } from 'storybook/test';
10
+
11
+ import { Capabilities, Capability } from '@dxos/app-framework';
21
12
  import { withPluginManager } from '@dxos/app-framework/testing';
22
- import { live } from '@dxos/live-object';
23
- import { AttentionPlugin } from '@dxos/plugin-attention';
24
- import { ClientPlugin } from '@dxos/plugin-client';
25
- import { GraphPlugin } from '@dxos/plugin-graph';
26
- import { SpacePlugin } from '@dxos/plugin-space';
27
- import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
28
- import { ThemePlugin } from '@dxos/plugin-theme';
29
- import { faker } from '@dxos/random';
13
+ import { useAtomCapability } from '@dxos/app-framework/ui';
14
+ import { AppCapabilities, LayoutOperation } from '@dxos/app-toolkit';
15
+ import { Operation, OperationHandlerSet } from '@dxos/compute';
16
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
17
+ import { random } from '@dxos/random';
30
18
  import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
31
- import { useAttentionAttributes, useAttention } from '@dxos/react-ui-attention';
19
+ import { useAttention, useAttentionAttributes } from '@dxos/react-ui-attention';
32
20
  import { Stack, StackItem } from '@dxos/react-ui-stack';
33
- import { defaultTx, mx } from '@dxos/react-ui-theme';
34
- import { withLayout } from '@dxos/storybook-utils';
21
+ import { withLayout } from '@dxos/react-ui/testing';
22
+ import { mx } from '@dxos/ui-theme';
23
+
24
+ import { NavTreeContainer } from '#containers';
25
+ import { storybookGraphBuilders } from '#testing';
26
+ import { translations } from '#translations';
35
27
 
36
28
  import { NavTreePlugin } from '../../NavTreePlugin';
37
- import { storybookGraphBuilders } from '../../testing';
38
- import { translations } from '../../translations';
39
- import { NavTreeContainer } from '../NavTreeContainer';
40
29
 
41
- faker.seed(1234);
30
+ random.seed(1234);
42
31
 
43
- const StoryState = defineCapability<{ tab: string }>('story-state');
32
+ const StoryState = Capability.make<Atom.Atom<{ tab: string }>>('story-state');
44
33
 
45
34
  // TODO(burdon): Fix outline (e.g., button in sidebar nav is clipped when focused).
46
35
  // TODO(burdon): Consider similar containment of: Table, Sheet, Kanban Column, Form, etc.
@@ -55,11 +44,11 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
55
44
  <div className='flex p-1 items-center border-b border-separator'>
56
45
  <IconButton
57
46
  density='coarse'
58
- icon='ph--atom--regular'
47
+ icon='ph--circle--regular'
59
48
  label='Test'
60
49
  iconOnly
61
50
  variant={hasAttention ? 'primary' : 'ghost'}
62
- classNames='is-[--rail-action] bs-[--rail-action]'
51
+ classNames='w-(--dx-rail-action) h-(--dx-rail-action)'
63
52
  />
64
53
  <StackItem.ResizeHandle />
65
54
  </div>
@@ -68,25 +57,36 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
68
57
 
69
58
  const StoryPlank = ({ attendableId }: { attendableId: string }) => {
70
59
  const attentionAttrs = useAttentionAttributes(attendableId);
60
+ const rootElement = useRef<HTMLDivElement | null>(null);
61
+
62
+ // NOTE(thure): This is the same workaround as in Plank, but that component is out of scope for this story.
63
+ // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
64
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
65
+ if (event.target === event.currentTarget && event.key === 'Escape') {
66
+ rootElement.current?.closest('main')?.focus();
67
+ }
68
+ }, []);
71
69
 
72
70
  return (
73
71
  <StackItem.Root
74
72
  item={{ id: attendableId }}
75
73
  {...attentionAttrs}
76
- classNames='bg-baseSurface border-ie border-separator'
74
+ classNames='bg-base-surface border-e border-separator'
77
75
  size={30}
76
+ onKeyDown={handleKeyDown}
77
+ ref={rootElement}
78
78
  >
79
79
  <StoryPlankHeading attendableId={attendableId} />
80
80
  <StackItem.Content toolbar>
81
- <Toolbar.Root classNames='border-b border-subduedSeparator'>
81
+ <Toolbar.Root classNames='border-b border-subdued-separator'>
82
82
  <Toolbar.Button>Test</Toolbar.Button>
83
83
  </Toolbar.Root>
84
84
 
85
- <div className={mx(container, 'm-2 bg-activeSurface')}>
85
+ <div className={mx(container, 'm-2 bg-active-surface')}>
86
86
  <Input.Root>
87
87
  <Input.Label>Level 1 (group)</Input.Label>
88
88
  </Input.Root>
89
- <div className={mx(container, 'bg-baseSurface')}>
89
+ <div className={mx(container, 'bg-base-surface')}>
90
90
  <Input.Root>
91
91
  <Input.Label>Level 2 (base)</Input.Label>
92
92
  <Input.TextArea placeholder='Enter text' />
@@ -99,10 +99,10 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
99
99
  };
100
100
 
101
101
  const DefaultStory = () => {
102
- const state = useCapability(StoryState);
102
+ const state = useAtomCapability(StoryState);
103
103
 
104
104
  return (
105
- <Main.Root complementarySidebarState='closed'>
105
+ <Main.Root navigationSidebarState='expanded'>
106
106
  <Main.NavigationSidebar label='Navigation' classNames='grid'>
107
107
  <NavTreeContainer tab={state.tab} />
108
108
  </Main.NavigationSidebar>
@@ -116,85 +116,93 @@ const DefaultStory = () => {
116
116
  );
117
117
  };
118
118
 
119
- const meta: Meta<typeof NavTreeContainer> = {
120
- title: 'plugins/plugin-navtree/NavTree',
119
+ const meta = {
120
+ title: 'plugins/plugin-navtree/components/NavTree',
121
121
  component: NavTreeContainer,
122
122
  render: DefaultStory,
123
123
  decorators: [
124
+ withLayout({ layout: 'fullscreen' }),
124
125
  withPluginManager({
125
126
  plugins: [
126
- ThemePlugin({ tx: defaultTx }),
127
- StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
128
- GraphPlugin(),
129
- IntentPlugin(),
130
- SettingsPlugin(),
131
- AttentionPlugin(),
127
+ ...corePlugins(),
128
+ StorybookPlugin({
129
+ initialState: { sidebarState: 'expanded' },
130
+ }),
131
+
132
132
  NavTreePlugin(),
133
133
  ],
134
- capabilities: (context) => [
135
- contributes(StoryState, live({ tab: 'space-0' })),
136
- contributes(Capabilities.AppGraphBuilder, storybookGraphBuilders(context)),
137
- contributes(Capabilities.IntentResolver, [
138
- createResolver({
139
- intent: LayoutAction.UpdateLayout,
140
- filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
141
- Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
142
- resolve: ({ subject }) => {
143
- const state = context.getCapability(StoryState);
144
- state.tab = subject;
145
- },
146
- }),
147
- ]),
148
- ],
134
+ capabilities: () => {
135
+ const storyStateAtom = Atom.make({ tab: 'root/space-0' }).pipe(Atom.keepAlive);
136
+ return [
137
+ Capability.contributes(StoryState, storyStateAtom),
138
+ Capability.contributes(AppCapabilities.AppGraphBuilder, storybookGraphBuilders()),
139
+ Capability.contributes(
140
+ Capabilities.OperationHandler,
141
+ OperationHandlerSet.make(
142
+ Operation.withHandler(LayoutOperation.SwitchWorkspace, ({ subject }) =>
143
+ Effect.gen(function* () {
144
+ const registry: Registry.Registry = yield* Capability.get(Capabilities.AtomRegistry);
145
+ registry.set(storyStateAtom, { tab: subject });
146
+ }),
147
+ ),
148
+ ),
149
+ ),
150
+ ];
151
+ },
149
152
  }),
150
- withLayout({ fullscreen: true }),
151
153
  ],
152
154
  parameters: {
155
+ layout: 'fullscreen',
153
156
  translations,
154
157
  },
155
- };
158
+ } satisfies Meta<typeof NavTreeContainer>;
156
159
 
157
160
  export default meta;
158
161
 
159
162
  type Story = StoryObj<typeof NavTreeContainer>;
160
163
 
161
- export const Default: Story = {};
164
+ export const Default: Story = {
165
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
166
+ const canvas = within(canvasElement);
162
167
 
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
- ],
168
+ // Find the element with treegrid role and click on its parent
169
+ const treegridElement = await canvas.findByRole('treegrid');
170
+ const treegridParent = treegridElement.parentElement;
171
+ if (treegridParent) {
172
+ await userEvent.click(treegridParent);
173
+ }
174
+
175
+ // Press Escape
176
+ await userEvent.keyboard('{Escape}');
177
+
178
+ // Confirm that focus is on an element with attribute data-main-landmark="0"
179
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
180
+
181
+ // Press Tab
182
+ await userEvent.keyboard('{Tab}');
183
+
184
+ // Confirm that focus is now on an element with data-main-landmark="1"
185
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
186
+
187
+ // Press Tab
188
+ await userEvent.keyboard('{Tab}');
189
+
190
+ // Confirm that focus is now on an element with data-main-landmark="0"
191
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
192
+
193
+ // Press Shift-Tab
194
+ await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
195
+
196
+ // Press Enter
197
+ await userEvent.keyboard('{Enter}');
198
+
199
+ // Confirm that focus is now on an element with data-attendable-id="space-0:object-0"
200
+ await expect(document.activeElement).toHaveAttribute('data-attendable-id', 'space-0:object-0');
201
+
202
+ // Press Escape
203
+ await userEvent.keyboard('{Escape}');
204
+
205
+ // Confirm that focus is now on an element with data-main-landmark="1"
206
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
207
+ },
200
208
  };