@dxos/plugin-simple-layout 0.8.4-main.bc674ce → 0.8.4-main.d05673bc65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/lib/browser/chunk-MDPEKLKR.mjs +1163 -0
  2. package/dist/lib/browser/chunk-MDPEKLKR.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-P77G4YTR.mjs → chunk-MRR7PXSM.mjs} +5 -5
  4. package/dist/lib/browser/chunk-MRR7PXSM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +20 -19
  6. package/dist/lib/browser/index.mjs.map +3 -3
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/{operation-resolver-775UYAC2.mjs → operation-resolver-VTZ6HZ4B.mjs} +37 -46
  9. package/dist/lib/browser/operation-resolver-VTZ6HZ4B.mjs.map +7 -0
  10. package/dist/lib/browser/{react-root-KM55OMGJ.mjs → react-root-WVQYY2JA.mjs} +5 -5
  11. package/dist/lib/browser/react-root-WVQYY2JA.mjs.map +7 -0
  12. package/dist/lib/browser/{react-surface-BABGAWGY.mjs → react-surface-VLBR37ED.mjs} +18 -13
  13. package/dist/lib/browser/react-surface-VLBR37ED.mjs.map +7 -0
  14. package/dist/lib/browser/{spotlight-dismiss-VSNOPETH.mjs → spotlight-dismiss-67PHYS5B.mjs} +3 -3
  15. package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +7 -0
  16. package/dist/lib/browser/{state-OUFTC2KV.mjs → state-TXSMUWYI.mjs} +5 -4
  17. package/dist/lib/browser/state-TXSMUWYI.mjs.map +7 -0
  18. package/dist/lib/browser/url-handler-RBRONH7S.mjs +151 -0
  19. package/dist/lib/browser/url-handler-RBRONH7S.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-DCKASLMP.mjs +1164 -0
  21. package/dist/lib/node-esm/chunk-DCKASLMP.mjs.map +7 -0
  22. package/dist/lib/node-esm/{chunk-F5TEKVJG.mjs → chunk-WMNTJ2MK.mjs} +5 -5
  23. package/dist/lib/node-esm/chunk-WMNTJ2MK.mjs.map +7 -0
  24. package/dist/lib/node-esm/index.mjs +20 -19
  25. package/dist/lib/node-esm/index.mjs.map +3 -3
  26. package/dist/lib/node-esm/meta.json +1 -1
  27. package/dist/lib/node-esm/{operation-resolver-LDNYS3DI.mjs → operation-resolver-R7CQ6ERU.mjs} +37 -46
  28. package/dist/lib/node-esm/operation-resolver-R7CQ6ERU.mjs.map +7 -0
  29. package/dist/lib/node-esm/{react-root-36UYFEEB.mjs → react-root-XBNDM7BE.mjs} +5 -5
  30. package/dist/lib/node-esm/react-root-XBNDM7BE.mjs.map +7 -0
  31. package/dist/lib/node-esm/{react-surface-CGHFVWU3.mjs → react-surface-U5NHA367.mjs} +18 -13
  32. package/dist/lib/node-esm/react-surface-U5NHA367.mjs.map +7 -0
  33. package/dist/lib/node-esm/{spotlight-dismiss-L5PCWIJG.mjs → spotlight-dismiss-RMLRZUVY.mjs} +3 -3
  34. package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +7 -0
  35. package/dist/lib/node-esm/{state-Q2ZA26W5.mjs → state-JMX6FAG4.mjs} +5 -4
  36. package/dist/lib/node-esm/state-JMX6FAG4.mjs.map +7 -0
  37. package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs +152 -0
  38. package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs.map +7 -0
  39. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -1
  40. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +2 -2
  41. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
  42. package/dist/types/src/capabilities/react-root/react-root.d.ts +1 -1
  43. package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -1
  44. package/dist/types/src/capabilities/react-surface/index.d.ts +1 -1
  45. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -1
  46. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +2 -2
  47. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
  48. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +1 -1
  49. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -1
  50. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +1 -1
  51. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -1
  52. package/dist/types/src/capabilities/state/index.d.ts +1 -1
  53. package/dist/types/src/capabilities/state/state.d.ts +1 -1
  54. package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
  55. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +5 -3
  56. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -1
  57. package/dist/types/src/components/ContentError.stories.d.ts +1 -3
  58. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/ContentLoading/ContentLoading.d.ts.map +1 -0
  60. package/dist/types/src/components/ContentLoading/ContentLoading.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/ContentLoading/index.d.ts +2 -0
  62. package/dist/types/src/components/ContentLoading/index.d.ts.map +1 -0
  63. package/dist/types/src/components/Home/Home.d.ts.map +1 -1
  64. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts +35 -0
  65. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -0
  66. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts +7 -0
  67. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -0
  68. package/dist/types/src/components/MobileLayout/index.d.ts +2 -0
  69. package/dist/types/src/components/MobileLayout/index.d.ts.map +1 -0
  70. package/dist/types/src/components/NavBranch/NavBranch.d.ts +11 -0
  71. package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
  72. package/dist/types/src/components/NavBranch/index.d.ts +2 -0
  73. package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
  74. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  75. package/dist/types/src/components/SimpleLayout/AppBar.d.ts +26 -0
  76. package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -0
  77. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +47 -0
  78. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -0
  79. package/dist/types/src/components/SimpleLayout/Drawer.d.ts +1 -1
  80. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -1
  81. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
  82. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +10 -3
  83. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
  84. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +4 -4
  85. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
  86. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
  87. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
  88. package/dist/types/src/components/SimpleLayout/index.d.ts +3 -0
  89. package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -1
  90. package/dist/types/src/components/hooks.d.ts +4 -2
  91. package/dist/types/src/components/hooks.d.ts.map +1 -1
  92. package/dist/types/src/components/index.d.ts +2 -1
  93. package/dist/types/src/components/index.d.ts.map +1 -1
  94. package/dist/types/src/hooks/actions.d.ts +19 -0
  95. package/dist/types/src/hooks/actions.d.ts.map +1 -0
  96. package/dist/types/src/hooks/index.d.ts +4 -0
  97. package/dist/types/src/hooks/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useAppBarProps.d.ts +7 -0
  99. package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useCompanions.d.ts +5 -1
  101. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -1
  102. package/dist/types/src/hooks/useDrawerActions.d.ts +13 -0
  103. package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -0
  104. package/dist/types/src/hooks/useNavbarActions.d.ts +14 -0
  105. package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -0
  106. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +3 -3
  107. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -1
  108. package/dist/types/src/types/capabilities.d.ts +7 -6
  109. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  110. package/dist/types/src/types/events.d.ts.map +1 -1
  111. package/dist/types/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +36 -30
  113. package/src/SimpleLayoutPlugin.ts +10 -9
  114. package/src/capabilities/operation-resolver/operation-resolver.ts +33 -46
  115. package/src/capabilities/react-root/react-root.tsx +2 -2
  116. package/src/capabilities/react-surface/react-surface.tsx +14 -11
  117. package/src/capabilities/spotlight-dismiss/spotlight-dismiss.ts +2 -2
  118. package/src/capabilities/state/state.tsx +4 -3
  119. package/src/capabilities/url-handler/url-handler.ts +98 -45
  120. package/src/components/ContentError.stories.tsx +8 -7
  121. package/src/components/{ContentLoading.stories.tsx → ContentLoading/ContentLoading.stories.tsx} +2 -2
  122. package/src/components/{ContentLoading.tsx → ContentLoading/ContentLoading.tsx} +1 -1
  123. package/src/components/ContentLoading/index.ts +5 -0
  124. package/src/components/Dialog/Dialog.tsx +5 -5
  125. package/src/components/Home/Home.tsx +43 -35
  126. package/src/components/MobileLayout/MobileLayout.stories.tsx +129 -0
  127. package/src/components/MobileLayout/MobileLayout.tsx +305 -0
  128. package/src/components/MobileLayout/index.ts +5 -0
  129. package/src/components/NavBranch/NavBranch.tsx +130 -0
  130. package/src/components/{Workspace → NavBranch}/index.ts +1 -1
  131. package/src/components/Popover/Popover.tsx +17 -7
  132. package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
  133. package/src/components/SimpleLayout/AppBar.tsx +94 -0
  134. package/src/components/SimpleLayout/Drawer.tsx +25 -80
  135. package/src/components/SimpleLayout/Main.tsx +40 -30
  136. package/src/components/SimpleLayout/NavBar.stories.tsx +131 -23
  137. package/src/components/SimpleLayout/NavBar.tsx +15 -47
  138. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +20 -11
  139. package/src/components/SimpleLayout/SimpleLayout.tsx +38 -19
  140. package/src/components/SimpleLayout/index.ts +3 -0
  141. package/src/components/hooks.ts +9 -9
  142. package/src/components/index.ts +2 -1
  143. package/src/hooks/actions.ts +83 -0
  144. package/src/hooks/index.ts +4 -0
  145. package/src/hooks/useAppBarProps.ts +115 -0
  146. package/src/hooks/useCompanions.ts +8 -5
  147. package/src/hooks/useDrawerActions.ts +100 -0
  148. package/src/hooks/useNavbarActions.ts +87 -0
  149. package/src/hooks/useSimpleLayoutState.ts +5 -5
  150. package/src/meta.ts +1 -1
  151. package/src/types/capabilities.ts +11 -7
  152. package/src/types/events.ts +3 -2
  153. package/dist/lib/browser/chunk-LR3EE3VB.mjs +0 -789
  154. package/dist/lib/browser/chunk-LR3EE3VB.mjs.map +0 -7
  155. package/dist/lib/browser/chunk-P77G4YTR.mjs.map +0 -7
  156. package/dist/lib/browser/operation-resolver-775UYAC2.mjs.map +0 -7
  157. package/dist/lib/browser/react-root-KM55OMGJ.mjs.map +0 -7
  158. package/dist/lib/browser/react-surface-BABGAWGY.mjs.map +0 -7
  159. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +0 -7
  160. package/dist/lib/browser/state-OUFTC2KV.mjs.map +0 -7
  161. package/dist/lib/browser/url-handler-DOUFQIAC.mjs +0 -54
  162. package/dist/lib/browser/url-handler-DOUFQIAC.mjs.map +0 -7
  163. package/dist/lib/node-esm/chunk-F5TEKVJG.mjs.map +0 -7
  164. package/dist/lib/node-esm/chunk-HB2B3LLG.mjs +0 -790
  165. package/dist/lib/node-esm/chunk-HB2B3LLG.mjs.map +0 -7
  166. package/dist/lib/node-esm/operation-resolver-LDNYS3DI.mjs.map +0 -7
  167. package/dist/lib/node-esm/react-root-36UYFEEB.mjs.map +0 -7
  168. package/dist/lib/node-esm/react-surface-CGHFVWU3.mjs.map +0 -7
  169. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +0 -7
  170. package/dist/lib/node-esm/state-Q2ZA26W5.mjs.map +0 -7
  171. package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs +0 -55
  172. package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs.map +0 -7
  173. package/dist/types/src/components/ContentError.d.ts +0 -5
  174. package/dist/types/src/components/ContentError.d.ts.map +0 -1
  175. package/dist/types/src/components/ContentLoading.d.ts.map +0 -1
  176. package/dist/types/src/components/ContentLoading.stories.d.ts.map +0 -1
  177. package/dist/types/src/components/SimpleLayout/Banner.d.ts +0 -8
  178. package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +0 -1
  179. package/dist/types/src/components/Workspace/Workspace.d.ts +0 -9
  180. package/dist/types/src/components/Workspace/Workspace.d.ts.map +0 -1
  181. package/dist/types/src/components/Workspace/index.d.ts +0 -2
  182. package/dist/types/src/components/Workspace/index.d.ts.map +0 -1
  183. package/src/components/ContentError.tsx +0 -23
  184. package/src/components/SimpleLayout/Banner.tsx +0 -113
  185. package/src/components/Workspace/Workspace.tsx +0 -115
  186. /package/dist/types/src/components/{ContentLoading.d.ts → ContentLoading/ContentLoading.d.ts} +0 -0
  187. /package/dist/types/src/components/{ContentLoading.stories.d.ts → ContentLoading/ContentLoading.stories.d.ts} +0 -0
@@ -4,17 +4,19 @@
4
4
 
5
5
  import React, { useMemo } from 'react';
6
6
 
7
- import { Surface, useAppGraph } from '@dxos/app-framework/react';
7
+ import { Surface } from '@dxos/app-framework/ui';
8
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
8
9
  import { useNode } from '@dxos/plugin-graph';
9
- import { Main as NaturalMain, useSidebars } from '@dxos/react-ui';
10
+ import { ErrorFallback } from '@dxos/react-ui';
11
+ import { useAttentionAttributes } from '@dxos/react-ui-attention';
10
12
  import { mx } from '@dxos/ui-theme';
11
13
 
12
- import { useSimpleLayoutState } from '../../hooks';
13
- import { ContentError } from '../ContentError';
14
+ import { useAppBarProps, useNavbarActions, useSimpleLayoutState } from '../../hooks';
14
15
  import { ContentLoading } from '../ContentLoading';
15
- import { useLoadDescendents } from '../hooks';
16
+ import { useExpandPath } from '../hooks';
17
+ import { useMobileLayout } from '../MobileLayout';
16
18
 
17
- import { Banner } from './Banner';
19
+ import { AppBar } from './AppBar';
18
20
  import { NavBar } from './NavBar';
19
21
 
20
22
  const MAIN_NAME = 'SimpleLayout.Main';
@@ -24,15 +26,16 @@ const MAIN_NAME = 'SimpleLayout.Main';
24
26
  */
25
27
  export const Main = () => {
26
28
  const { state } = useSimpleLayoutState();
27
- const { graph } = useAppGraph();
28
29
  const id = state.active ?? state.workspace;
29
- const node = useNode(graph, id);
30
-
31
- // Ensures that children are loaded so that they are available to navigate to.
32
- useLoadDescendents(id);
30
+ const attentionAttrs = useAttentionAttributes(id);
31
+ const { keyboardOpen } = useMobileLayout(MAIN_NAME);
32
+ const { actions, onAction } = useNavbarActions();
33
+ const appBarProps = useAppBarProps();
33
34
 
34
35
  const placeholder = useMemo(() => <ContentLoading />, []);
35
36
 
37
+ const { graph } = useAppGraph();
38
+ const node = useNode(graph, id);
36
39
  const data = useMemo(() => {
37
40
  return (
38
41
  node && {
@@ -44,28 +47,35 @@ export const Main = () => {
44
47
  );
45
48
  }, [id, node, node?.data, node?.properties, state.popoverAnchorId]);
46
49
 
47
- const { drawerState } = useSidebars(MAIN_NAME);
48
- const showNavBar = !state.isPopover && drawerState === 'closed';
50
+ useExpandPath(id);
51
+
52
+ // TODO(burdon): BUG: When showing ANY statusbar the size progressively shrinks when the keyboard opens/closes.
53
+ const showNavBar = !keyboardOpen && !state.isPopover && state.drawerState === 'closed';
49
54
 
50
55
  return (
51
- <NaturalMain.Content
52
- bounce
53
- classNames={mx('bs-full', 'pbs-[env(safe-area-inset-top)] pbe-[env(safe-area-inset-bottom)]')}
56
+ <div
57
+ role='none'
58
+ className={mx(
59
+ 'h-full grid overflow-hidden bg-toolbar-surface',
60
+ showNavBar
61
+ ? 'grid-rows-[var(--dx-rail-action)_1fr_var(--dx-toolbar-size)]'
62
+ : 'grid-rows-[var(--dx-rail-action)_1fr]',
63
+ )}
64
+ {...attentionAttrs}
54
65
  >
55
- <div
56
- role='none'
57
- className={mx(
58
- 'grid bs-full overflow-hidden',
59
- showNavBar ? 'grid-rows-[min-content_1fr_min-content]' : 'grid-rows-[min-content_1fr]',
60
- )}
61
- >
62
- <Banner classNames='border-be border-separator' node={node} />
63
- <article className='bs-full overflow-hidden'>
64
- <Surface key={id} role='article' data={data} limit={1} fallback={ContentError} placeholder={placeholder} />
65
- </article>
66
- {showNavBar && <NavBar classNames='border-bs border-separator' activeId={id} />}
67
- </div>
68
- </NaturalMain.Content>
66
+ <AppBar {...appBarProps} />
67
+ <article className='h-full overflow-hidden bg-base-surface'>
68
+ <Surface.Surface
69
+ key={id}
70
+ role='article'
71
+ data={data}
72
+ limit={1}
73
+ fallback={ErrorFallback}
74
+ placeholder={placeholder}
75
+ />
76
+ </article>
77
+ {showNavBar && <NavBar classNames='border-y border-subdued-separator' actions={actions} onAction={onAction} />}
78
+ </div>
69
79
  );
70
80
  };
71
81
 
@@ -2,25 +2,97 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Atom } from '@effect-atom/atom-react';
5
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useMemo } from 'react';
8
+ import { type Mock, expect, fn, screen, userEvent, within } from 'storybook/test';
6
9
 
7
- import { withPluginManager } from '@dxos/app-framework/testing';
8
- import { corePlugins } from '@dxos/plugin-testing';
9
- import { withTheme } from '@dxos/react-ui/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { type ActionGraphProps, createGapSeparator, createMenuAction, createMenuItemGroup } from '@dxos/react-ui-menu';
12
+ import { withRegistry } from '@dxos/storybook-utils';
10
13
 
11
14
  import { translations } from '../../translations';
12
15
 
13
16
  import { NavBar } from './NavBar';
14
17
 
18
+ const MAIN_MENU_GROUP_ID = 'navbar-main-menu';
19
+
20
+ const buildEmptyActions = (): ActionGraphProps => ({ nodes: [], edges: [] });
21
+
22
+ const buildCompanionOnlyActions = (): ActionGraphProps => {
23
+ const result: ActionGraphProps = { nodes: [], edges: [] };
24
+ const companions = [
25
+ createMenuAction('companion-browse', () => console.log('Browse'), {
26
+ icon: 'ph--house--regular',
27
+ label: 'Browse',
28
+ iconOnly: true,
29
+ }),
30
+ createMenuAction('companion-notifications', () => console.log('Notifications'), {
31
+ icon: 'ph--bell--regular',
32
+ label: 'Notifications',
33
+ iconOnly: true,
34
+ }),
35
+ createMenuAction('companion-profile', () => console.log('Profile'), {
36
+ icon: 'ph--user--regular',
37
+ label: 'Profile',
38
+ iconOnly: true,
39
+ }),
40
+ ];
41
+ result.nodes.push(...companions);
42
+ result.edges.push(...companions.map((c) => ({ source: 'root', target: c.id, relation: 'child' })));
43
+ return result;
44
+ };
45
+
46
+ const buildDefaultActions = (): ActionGraphProps => {
47
+ const result: ActionGraphProps = { nodes: [], edges: [] };
48
+ const gapSeparator = createGapSeparator('navbar-gap');
49
+ const mainMenuGroup = createMenuItemGroup(MAIN_MENU_GROUP_ID, {
50
+ variant: 'dropdownMenu',
51
+ icon: 'ph--plus--regular',
52
+ iconOnly: true,
53
+ label: 'Main menu',
54
+ testId: 'simpleLayoutPlugin.addSpace',
55
+ });
56
+ const companions = [
57
+ createMenuAction('companion-browse', () => console.log('Browse'), {
58
+ icon: 'ph--house--regular',
59
+ label: 'Browse',
60
+ iconOnly: true,
61
+ }),
62
+ createMenuAction('companion-notifications', () => console.log('Notifications'), {
63
+ icon: 'ph--bell--regular',
64
+ label: 'Notifications',
65
+ iconOnly: true,
66
+ }),
67
+ ];
68
+ const menuActions = [
69
+ createMenuAction('action-create-space', () => console.log('Create space'), {
70
+ icon: 'ph--planet--regular',
71
+ label: 'Create space',
72
+ }),
73
+ createMenuAction('action-join-space', () => console.log('Join space'), {
74
+ icon: 'ph--sign-in--regular',
75
+ label: 'Join space',
76
+ }),
77
+ createMenuAction('action-settings', () => console.log('Settings'), {
78
+ icon: 'ph--gear--regular',
79
+ label: 'Settings',
80
+ }),
81
+ ];
82
+ result.nodes.push(...companions, ...gapSeparator.nodes, mainMenuGroup, ...menuActions);
83
+ result.edges.push(
84
+ ...companions.map((c) => ({ source: 'root', target: c.id, relation: 'child' })),
85
+ ...gapSeparator.edges,
86
+ { source: 'root', target: mainMenuGroup.id, relation: 'child' },
87
+ ...menuActions.map((action) => ({ source: MAIN_MENU_GROUP_ID, target: action.id, relation: 'child' })),
88
+ );
89
+ return result;
90
+ };
91
+
15
92
  const meta = {
16
- title: 'plugins/plugin-simple-layout/NavBar',
93
+ title: 'plugins/plugin-simple-layout/components/NavBar',
17
94
  component: NavBar,
18
- decorators: [
19
- withTheme,
20
- withPluginManager({
21
- plugins: [...corePlugins()],
22
- }),
23
- ],
95
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
24
96
  parameters: {
25
97
  layout: 'fullscreen',
26
98
  translations,
@@ -31,26 +103,62 @@ export default meta;
31
103
 
32
104
  type Story = StoryObj<typeof meta>;
33
105
 
106
+ const DefaultStory = ({ onAction }: { onAction: (action: { id: string }) => void }) => {
107
+ const actions = useMemo(() => Atom.make(buildDefaultActions()).pipe(Atom.keepAlive), []);
108
+
109
+ return <NavBar classNames='border-y border-separator' actions={actions} onAction={onAction} />;
110
+ };
111
+
34
112
  export const Default: Story = {
113
+ tags: ['test'],
35
114
  args: {
36
- activeId: undefined,
115
+ onAction: fn(),
116
+ } as any,
117
+ render: (args: any) => <DefaultStory onAction={args.onAction} />,
118
+ play: async ({ args, canvasElement }) => {
119
+ const canvas = within(canvasElement);
120
+
121
+ // Verify the navbar renders with the toolbar.
122
+ await expect(canvas.getByRole('toolbar')).toBeInTheDocument();
123
+
124
+ // Test companion action click (Browse button).
125
+ const browseButton = canvas.getByRole('button', { name: /browse/i });
126
+ await expect(browseButton).toBeInTheDocument();
127
+ await userEvent.click(browseButton);
128
+ await expect(args.onAction).toHaveBeenCalledTimes(1);
129
+ await expect((args.onAction as Mock).mock.calls[0][0]).toHaveProperty('id', 'companion-browse');
130
+
131
+ // Test dropdown menu opens and action fires.
132
+ const menuTrigger = canvas.getByRole('button', { name: /main menu/i });
133
+ await expect(menuTrigger).toBeInTheDocument();
134
+ await userEvent.click(menuTrigger);
135
+
136
+ // Wait for menu to open and click an action (menu items render in a portal).
137
+ const createSpaceAction = await screen.findByRole('menuitem', { name: /create space/i });
138
+ await userEvent.click(createSpaceAction);
139
+ await expect(args.onAction).toHaveBeenCalledTimes(2);
140
+ await expect((args.onAction as Mock).mock.calls[1][0]).toHaveProperty('id', 'action-create-space');
37
141
  },
38
142
  };
39
143
 
40
- export const BrowseActive: Story = {
41
- args: {
42
- activeId: 'some-document',
43
- },
144
+ const CompanionsOnlyStory = () => {
145
+ const actions = useMemo(() => Atom.make(buildCompanionOnlyActions()).pipe(Atom.keepAlive), []);
146
+
147
+ return <NavBar actions={actions} onAction={(action) => console.log('Action:', action.id)} />;
44
148
  };
45
149
 
46
- export const NotificationsActive: Story = {
47
- args: {
48
- activeId: 'notifications',
49
- },
150
+ export const CompanionsOnly: Story = {
151
+ args: {} as any,
152
+ render: () => <CompanionsOnlyStory />,
50
153
  };
51
154
 
52
- export const ProfileActive: Story = {
53
- args: {
54
- activeId: 'profile',
55
- },
155
+ const EmptyStory = () => {
156
+ const actions = useMemo(() => Atom.make(buildEmptyActions()).pipe(Atom.keepAlive), []);
157
+
158
+ return <NavBar actions={actions} onAction={(action) => console.log('Action:', action.id)} />;
159
+ };
160
+
161
+ export const Empty: Story = {
162
+ args: {} as any,
163
+ render: () => <EmptyStory />,
56
164
  };
@@ -2,64 +2,32 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { type Atom } from '@effect-atom/atom-react';
5
6
  import React from 'react';
6
7
 
7
- import { Common } from '@dxos/app-framework';
8
- import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
9
- import { Node, useActionRunner, useConnections } from '@dxos/plugin-graph';
10
- import { IconButton, type ThemedClassName, Toolbar, Tooltip, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
- import { DropdownMenu, MenuProvider } from '@dxos/react-ui-menu';
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import { type ActionExecutor, type ActionGraphProps, Menu, useMenuActions } from '@dxos/react-ui-menu';
12
10
  import { mx } from '@dxos/ui-theme';
13
11
 
14
- import { useCompanions } from '../../hooks';
15
- import { meta } from '../../meta';
16
-
17
12
  const NAVBAR_NAME = 'SimpleLayout.NavBar';
18
13
 
19
14
  export type NavBarProps = ThemedClassName<{
20
- /** Active AppGraph node ID. */
21
- activeId?: string;
15
+ /** Action graph atom for the toolbar. */
16
+ actions: Atom.Atom<ActionGraphProps>;
17
+ /** Action executor callback. */
18
+ onAction?: ActionExecutor;
22
19
  }>;
23
20
 
24
- export const NavBar = ({ classNames, activeId }: NavBarProps) => {
25
- const { t } = useTranslation(meta.id);
26
- const { graph } = useAppGraph();
27
- const runAction = useActionRunner();
28
- const { invokePromise } = useOperationInvoker();
29
-
30
- const connections = useConnections(graph, Node.RootId);
31
- const menuActions = connections.filter((node) => node.properties.disposition === 'menu');
32
-
33
- const companions = useCompanions(activeId);
21
+ /**
22
+ * Presentational navbar component that renders a toolbar from an action graph.
23
+ */
24
+ export const NavBar = ({ classNames, actions, onAction }: NavBarProps) => {
25
+ const menu = useMenuActions(actions);
34
26
 
35
27
  return (
36
- <Toolbar.Root classNames={mx('justify-center', classNames)}>
37
- {companions.map((companion) => (
38
- <Toolbar.IconButton
39
- key={companion.id}
40
- icon={companion.properties.icon ?? 'ph--placeholder--regular'}
41
- iconOnly
42
- label={toLocalizedString(companion.properties.label, t)}
43
- onClick={() => {
44
- void invokePromise(Common.LayoutOperation.Open, {
45
- subject: [companion.id],
46
- });
47
- }}
48
- />
49
- ))}
50
-
51
- <Toolbar.Separator variant='gap' />
52
-
53
- <MenuProvider onAction={runAction}>
54
- <DropdownMenu.Root items={menuActions}>
55
- <Tooltip.Trigger asChild content={t('app menu label')}>
56
- <DropdownMenu.Trigger asChild data-testid='simpleLayoutPlugin.addSpace'>
57
- <IconButton icon='ph--plus--regular' iconOnly label={t('main menu label')} />
58
- </DropdownMenu.Trigger>
59
- </Tooltip.Trigger>
60
- </DropdownMenu.Root>
61
- </MenuProvider>
62
- </Toolbar.Root>
28
+ <Menu.Root {...menu} alwaysActive onAction={onAction}>
29
+ <Menu.Toolbar density='coarse' classNames={mx(classNames)} />
30
+ </Menu.Root>
63
31
  );
64
32
  };
65
33
 
@@ -5,16 +5,17 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Effect from 'effect/Effect';
7
7
 
8
- import { Capability, Common, Plugin } from '@dxos/app-framework';
8
+ import { ActivationEvents, Capabilities, Capability, Plugin } from '@dxos/app-framework';
9
9
  import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { AppActivationEvents, AppPlugin } from '@dxos/app-toolkit';
11
+ import { Collection } from '@dxos/echo';
10
12
  import { ClientOperation, ClientPlugin } from '@dxos/plugin-client';
11
13
  import { SearchPlugin } from '@dxos/plugin-search';
12
14
  import { SpacePlugin } from '@dxos/plugin-space';
13
15
  import { SpaceOperation } from '@dxos/plugin-space/types';
14
16
  import { corePlugins } from '@dxos/plugin-testing';
15
- import { withTheme } from '@dxos/react-ui/testing';
17
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
18
  import { translations as searchTranslation } from '@dxos/react-ui-searchlist';
17
- import { Collection } from '@dxos/schema';
18
19
 
19
20
  import { OperationResolver, type SimpleLayoutStateOptions, State } from '../../capabilities';
20
21
  import { meta as pluginMeta } from '../../meta';
@@ -24,18 +25,18 @@ import { translations } from '../../translations';
24
25
  import { SimpleLayout } from './SimpleLayout';
25
26
 
26
27
  const TestPlugin = Plugin.define<SimpleLayoutPluginOptions>(pluginMeta).pipe(
28
+ AppPlugin.addOperationResolverModule({ activate: OperationResolver }),
27
29
  Plugin.addModule(({ isPopover = false }) => ({
28
30
  id: Capability.getModuleTag(State),
29
- activatesOn: Common.ActivationEvent.Startup,
30
- activatesAfter: [Common.ActivationEvent.LayoutReady],
31
+ activatesOn: ActivationEvents.Startup,
32
+ activatesAfter: [AppActivationEvents.LayoutReady],
31
33
  activate: () => State({ initialState: { isPopover } } satisfies SimpleLayoutStateOptions),
32
34
  })),
33
- Common.Plugin.addOperationResolverModule({ activate: OperationResolver }),
34
35
  Plugin.addModule({
35
36
  id: 'setup',
36
- activatesOn: Common.ActivationEvent.OperationInvokerReady,
37
+ activatesOn: ActivationEvents.OperationInvokerReady,
37
38
  activate: Effect.fnUntraced(function* () {
38
- const { invoke } = yield* Capability.get(Common.Capability.OperationInvoker);
39
+ const { invoke } = yield* Capability.get(Capabilities.OperationInvoker);
39
40
  yield* invoke(ClientOperation.CreateIdentity, {});
40
41
  const { space: work } = yield* invoke(SpaceOperation.Create, { name: 'Work Space' });
41
42
  const { space: sharedProject } = yield* invoke(SpaceOperation.Create, { name: 'Shared Project' });
@@ -79,7 +80,7 @@ const createPluginManager = ({ isPopover }: { isPopover: boolean }) => {
79
80
  };
80
81
 
81
82
  const meta = {
82
- title: 'plugins/plugin-simple-layout/SimpleLayout',
83
+ title: 'plugins/plugin-simple-layout/components/SimpleLayout',
83
84
  component: SimpleLayout,
84
85
  parameters: {
85
86
  layout: 'fullscreen',
@@ -96,9 +97,17 @@ type Story = StoryObj<typeof meta>;
96
97
  * `moon run storybook-react:serve dev -H 0.0.0.0`
97
98
  */
98
99
  export const Default: Story = {
99
- decorators: [withTheme, createPluginManager({ isPopover: false })],
100
+ decorators: [
101
+ withTheme(),
102
+ withLayout({ layout: 'column', classNames: 'relative' }),
103
+ createPluginManager({ isPopover: false }),
104
+ ],
100
105
  };
101
106
 
102
107
  export const Popover: Story = {
103
- decorators: [withTheme, createPluginManager({ isPopover: true })],
108
+ decorators: [
109
+ withTheme(),
110
+ withLayout({ layout: 'column', classNames: 'relative' }),
111
+ createPluginManager({ isPopover: true }),
112
+ ],
104
113
  };
@@ -2,39 +2,58 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback } from 'react';
5
+ import React, { useEffect, useRef, useState } from 'react';
6
6
 
7
- import { type DrawerState, Main as NaturalMain } from '@dxos/react-ui';
7
+ import { Splitter, type SplitterMode } from '@dxos/react-ui';
8
8
  import { Mosaic } from '@dxos/react-ui-mosaic';
9
9
 
10
10
  import { useSimpleLayoutState } from '../../hooks';
11
11
  import { Dialog } from '../Dialog';
12
+ import { MobileLayout } from '../MobileLayout';
12
13
  import { PopoverContent, PopoverRoot } from '../Popover';
13
14
 
14
15
  import { Drawer } from './Drawer';
15
16
  import { Main } from './Main';
16
17
 
18
+ // TODO(burdon): Mobile/Desktop variance?
17
19
  export const SimpleLayout = () => {
18
- const { state, updateState } = useSimpleLayoutState();
20
+ const { state } = useSimpleLayoutState();
21
+ const [keyboardOpen, setKeyboardOpen] = useState(false);
22
+ const [splitterMode, setSplitterMode] = useState<SplitterMode>('upper');
19
23
 
20
- const handleDrawerStateChange = useCallback(
21
- (nextState: DrawerState) => {
22
- // Sync all drawer state changes to state.
23
- updateState((s) => ({ ...s, drawerState: nextState }));
24
- },
25
- [updateState],
26
- );
24
+ const drawerRef = useRef<HTMLDivElement>(null);
25
+ useEffect(() => {
26
+ if (keyboardOpen) {
27
+ // Determine which panel has focus and expand that one.
28
+ const activeElement = document.activeElement;
29
+ const drawerHasFocus = drawerRef.current?.contains(activeElement);
30
+ setSplitterMode(drawerHasFocus ? 'lower' : 'upper');
31
+ } else {
32
+ setSplitterMode(state.drawerState === 'closed' ? 'upper' : state.drawerState === 'open' ? 'both' : 'lower');
33
+ }
34
+ }, [state.drawerState, keyboardOpen]);
27
35
 
28
36
  return (
29
- <Mosaic.Root>
30
- <NaturalMain.Root drawerState={state.drawerState ?? 'closed'} onDrawerStateChange={handleDrawerStateChange}>
31
- <PopoverRoot>
32
- <Main />
33
- <Drawer />
34
- <Dialog />
35
- <PopoverContent />
36
- </PopoverRoot>
37
- </NaturalMain.Root>
37
+ <Mosaic.Root classNames='contents'>
38
+ <MobileLayout.Root
39
+ classNames='bg-toolbar-surface'
40
+ onKeyboardOpenChange={(keyboardOpen: boolean) => setKeyboardOpen(keyboardOpen)}
41
+ >
42
+ <MobileLayout.Panel safe={{ top: true, bottom: splitterMode === 'upper' }}>
43
+ <PopoverRoot>
44
+ <Splitter.Root mode={splitterMode} ratio={0.55}>
45
+ <Splitter.Panel position='upper'>
46
+ <Main />
47
+ </Splitter.Panel>
48
+ <Splitter.Panel position='lower' ref={drawerRef}>
49
+ <Drawer />
50
+ </Splitter.Panel>
51
+ </Splitter.Root>
52
+ <Dialog />
53
+ <PopoverContent />
54
+ </PopoverRoot>
55
+ </MobileLayout.Panel>
56
+ </MobileLayout.Root>
38
57
  </Mosaic.Root>
39
58
  );
40
59
  };
@@ -2,4 +2,7 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ export * from './AppBar';
6
+ export * from './Main';
7
+ export * from './NavBar';
5
8
  export * from './SimpleLayout';
@@ -4,23 +4,23 @@
4
4
 
5
5
  import { useEffect } from 'react';
6
6
 
7
- import { useAppGraph } from '@dxos/app-framework/react';
7
+ import { expandAttendableId } from '@dxos/react-ui-attention';
8
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
8
9
  import { Graph } from '@dxos/plugin-graph';
9
10
 
10
11
  /**
11
- * Hook to expand graph nodes two levels deep when directly linked to.
12
+ * Expand graph nodes along the full path from root to the given node ID.
13
+ * Walks each progressive prefix, ensuring ancestor nodes are materialized
14
+ * before attempting to access their children.
12
15
  */
13
- export const useLoadDescendents = (nodeId?: string) => {
16
+ export const useExpandPath = (nodeId?: string) => {
14
17
  const { graph } = useAppGraph();
15
18
 
16
19
  useEffect(() => {
17
20
  if (nodeId) {
18
- // First level: expand the node itself.
19
- Graph.expand(graph, nodeId, 'outbound');
20
- // Second level: expand each child.
21
- Graph.getConnections(graph, nodeId, 'outbound').forEach((child) => {
22
- Graph.expand(graph, child.id, 'outbound');
23
- });
21
+ for (const prefix of expandAttendableId(nodeId)) {
22
+ Graph.expand(graph, prefix, 'child');
23
+ }
24
24
  }
25
25
  }, [nodeId, graph]);
26
26
  };
@@ -4,6 +4,7 @@
4
4
 
5
5
  export * from './ContentLoading';
6
6
  export * from './Home';
7
+ export * from './MobileLayout';
7
8
  export * from './Popover';
8
9
  export * from './SimpleLayout';
9
- export * from './Workspace';
10
+ export * from './NavBranch';
@@ -0,0 +1,83 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Atom } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { type AppCapabilities, getCompanionVariant } from '@dxos/app-toolkit';
9
+ import { Node } from '@dxos/plugin-graph';
10
+ import { type ActionGraphProps } from '@dxos/react-ui-menu';
11
+ import { byPosition } from '@dxos/util';
12
+
13
+ import { type SimpleLayoutState } from '../types';
14
+
15
+ // TODO(wittjosiah): Factor out to shared location with plugin-deck.
16
+ export const PLANK_COMPANION_TYPE = 'org.dxos.plugin.deck.plank-companion';
17
+
18
+ export type CompanionActionsConfig = {
19
+ /** Prefix for companion action IDs (e.g. 'navbar' or 'drawer') */
20
+ idPrefix: string;
21
+ /** Optional: highlight companion with this variant */
22
+ selectedVariant?: string;
23
+ /** State updater for toggling the drawer. */
24
+ updateState: (fn: (state: SimpleLayoutState) => SimpleLayoutState) => void;
25
+ };
26
+
27
+ /**
28
+ * Creates action graph nodes and edges for companion actions.
29
+ * Shared logic between useNavbarActions and useDrawerActions.
30
+ */
31
+ // TODO(burdon): Use builder pattern.
32
+ export const createCompanionActions = (
33
+ graph: AppCapabilities.AppGraph['graph'],
34
+ stateAtom: Atom.Atom<SimpleLayoutState>,
35
+ get: (atom: Atom.Atom<any>) => any,
36
+ config: CompanionActionsConfig,
37
+ ): Pick<ActionGraphProps, 'nodes' | 'edges'> => {
38
+ const { idPrefix, selectedVariant, updateState } = config;
39
+
40
+ // Derive activeId from state atom.
41
+ const state = get(stateAtom);
42
+ const activeId = state.active ?? state.workspace;
43
+
44
+ // Get companions from graph connections for activeId.
45
+ const activeConnections = activeId ? get(graph.connections(activeId, 'child')) : [];
46
+ const companions = activeConnections
47
+ .filter((node: Node.Node) => node.type === PLANK_COMPANION_TYPE)
48
+ .toSorted((a: Node.Node, b: Node.Node) => byPosition(a.properties, b.properties));
49
+
50
+ const nodes: ActionGraphProps['nodes'] = [];
51
+ const edges: ActionGraphProps['edges'] = [];
52
+
53
+ companions.forEach((companion: Node.Node) => {
54
+ const companionVariant = getCompanionVariant(companion.id);
55
+ const companionAction = {
56
+ id: `${idPrefix}-companion-${companion.id}`,
57
+ type: Node.ActionType,
58
+ properties: {
59
+ icon: companion.properties.icon ?? 'ph--placeholder--regular',
60
+ label: companion.properties.label,
61
+ iconOnly: true,
62
+ ...(selectedVariant !== undefined && {
63
+ variant: selectedVariant === companionVariant ? 'primary' : 'ghost',
64
+ }),
65
+ },
66
+ data: () =>
67
+ Effect.sync(() =>
68
+ updateState((current) => {
69
+ const closing = current.companionVariant === companionVariant && current.drawerState !== 'closed';
70
+ return {
71
+ ...current,
72
+ companionVariant: closing ? undefined : companionVariant,
73
+ drawerState: closing ? 'closed' : 'open',
74
+ };
75
+ }),
76
+ ),
77
+ };
78
+ nodes.push(companionAction);
79
+ edges.push({ source: 'root', target: companionAction.id, relation: 'child' });
80
+ });
81
+
82
+ return { nodes, edges };
83
+ };
@@ -2,5 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ export * from './useAppBarProps';
5
6
  export * from './useCompanions';
7
+ export * from './actions';
8
+ export * from './useDrawerActions';
9
+ export * from './useNavbarActions';
6
10
  export * from './useSimpleLayoutState';