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

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 (83) hide show
  1. package/dist/lib/browser/{chunk-FK4M7GJV.mjs → chunk-LR3EE3VB.mjs} +298 -122
  2. package/dist/lib/browser/chunk-LR3EE3VB.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-CLPGTNWJ.mjs → chunk-P77G4YTR.mjs} +1 -1
  4. package/dist/lib/browser/chunk-P77G4YTR.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +13 -7
  6. package/dist/lib/browser/index.mjs.map +2 -2
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/{operation-resolver-LTB63NKP.mjs → operation-resolver-775UYAC2.mjs} +50 -15
  9. package/dist/lib/browser/operation-resolver-775UYAC2.mjs.map +7 -0
  10. package/dist/lib/browser/{react-root-6ARAPH3O.mjs → react-root-KM55OMGJ.mjs} +3 -3
  11. package/dist/lib/browser/{react-surface-SO7B23GS.mjs → react-surface-BABGAWGY.mjs} +3 -3
  12. package/dist/lib/browser/{state-H4IGICBB.mjs → state-OUFTC2KV.mjs} +5 -3
  13. package/dist/lib/browser/state-OUFTC2KV.mjs.map +7 -0
  14. package/dist/lib/browser/{url-handler-7CFGTLNG.mjs → url-handler-DOUFQIAC.mjs} +2 -2
  15. package/dist/lib/node-esm/{chunk-MUVVYBUE.mjs → chunk-F5TEKVJG.mjs} +1 -1
  16. package/dist/lib/node-esm/chunk-F5TEKVJG.mjs.map +7 -0
  17. package/dist/lib/node-esm/{chunk-EGFZAVBD.mjs → chunk-HB2B3LLG.mjs} +298 -122
  18. package/dist/lib/node-esm/chunk-HB2B3LLG.mjs.map +7 -0
  19. package/dist/lib/node-esm/index.mjs +13 -7
  20. package/dist/lib/node-esm/index.mjs.map +2 -2
  21. package/dist/lib/node-esm/meta.json +1 -1
  22. package/dist/lib/node-esm/{operation-resolver-7O6O7T4Q.mjs → operation-resolver-LDNYS3DI.mjs} +50 -15
  23. package/dist/lib/node-esm/operation-resolver-LDNYS3DI.mjs.map +7 -0
  24. package/dist/lib/node-esm/{react-root-2CPA2ZUS.mjs → react-root-36UYFEEB.mjs} +3 -3
  25. package/dist/lib/node-esm/{react-surface-FKAV56MO.mjs → react-surface-CGHFVWU3.mjs} +3 -3
  26. package/dist/lib/node-esm/{state-QIU2LMLT.mjs → state-Q2ZA26W5.mjs} +5 -3
  27. package/dist/lib/node-esm/state-Q2ZA26W5.mjs.map +7 -0
  28. package/dist/lib/node-esm/{url-handler-4LYP3JM7.mjs → url-handler-DVAZZEUO.mjs} +2 -2
  29. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
  30. package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
  31. package/dist/types/src/components/ContentError.stories.d.ts +6 -0
  32. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +1 -1
  34. package/dist/types/src/components/SimpleLayout/Drawer.d.ts +9 -0
  35. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -0
  36. package/dist/types/src/components/SimpleLayout/Main.d.ts +1 -1
  37. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
  38. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +5 -2
  39. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
  40. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +10 -6
  41. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
  43. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +10 -0
  44. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/hooks.d.ts.map +1 -1
  46. package/dist/types/src/hooks/index.d.ts +1 -0
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useCompanions.d.ts +8 -0
  49. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
  50. package/dist/types/src/translations.d.ts +6 -0
  51. package/dist/types/src/translations.d.ts.map +1 -1
  52. package/dist/types/src/types/capabilities.d.ts +5 -1
  53. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  54. package/dist/types/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +23 -23
  56. package/src/capabilities/operation-resolver/operation-resolver.ts +50 -13
  57. package/src/capabilities/state/state.tsx +2 -0
  58. package/src/components/SimpleLayout/Banner.tsx +35 -4
  59. package/src/components/SimpleLayout/Drawer.tsx +151 -0
  60. package/src/components/SimpleLayout/Main.tsx +31 -40
  61. package/src/components/SimpleLayout/NavBar.stories.tsx +0 -3
  62. package/src/components/SimpleLayout/NavBar.tsx +29 -37
  63. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +4 -0
  64. package/src/components/SimpleLayout/SimpleLayout.tsx +26 -6
  65. package/src/components/hooks.ts +8 -12
  66. package/src/hooks/index.ts +1 -0
  67. package/src/hooks/useCompanions.ts +19 -0
  68. package/src/translations.ts +6 -0
  69. package/src/types/capabilities.ts +6 -1
  70. package/dist/lib/browser/chunk-CLPGTNWJ.mjs.map +0 -7
  71. package/dist/lib/browser/chunk-FK4M7GJV.mjs.map +0 -7
  72. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs.map +0 -7
  73. package/dist/lib/browser/state-H4IGICBB.mjs.map +0 -7
  74. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs.map +0 -7
  75. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs.map +0 -7
  76. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs.map +0 -7
  77. package/dist/lib/node-esm/state-QIU2LMLT.mjs.map +0 -7
  78. /package/dist/lib/browser/{react-root-6ARAPH3O.mjs.map → react-root-KM55OMGJ.mjs.map} +0 -0
  79. /package/dist/lib/browser/{react-surface-SO7B23GS.mjs.map → react-surface-BABGAWGY.mjs.map} +0 -0
  80. /package/dist/lib/browser/{url-handler-7CFGTLNG.mjs.map → url-handler-DOUFQIAC.mjs.map} +0 -0
  81. /package/dist/lib/node-esm/{react-root-2CPA2ZUS.mjs.map → react-root-36UYFEEB.mjs.map} +0 -0
  82. /package/dist/lib/node-esm/{react-surface-FKAV56MO.mjs.map → react-surface-CGHFVWU3.mjs.map} +0 -0
  83. /package/dist/lib/node-esm/{url-handler-4LYP3JM7.mjs.map → url-handler-DVAZZEUO.mjs.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-simple-layout",
3
- "version": "0.8.4-main.937b3ca",
3
+ "version": "0.8.4-main.bc674ce",
4
4
  "description": "Simple layout plugin for minimal UI contexts like popover windows.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -29,17 +29,17 @@
29
29
  "@effect-atom/atom": "^0.4.13",
30
30
  "@effect-atom/atom-react": "^0.4.6",
31
31
  "@radix-ui/react-context": "1.1.1",
32
- "@dxos/app-framework": "0.8.4-main.937b3ca",
33
- "@dxos/operation": "0.8.4-main.937b3ca",
34
- "@dxos/log": "0.8.4-main.937b3ca",
35
- "@dxos/react-ui-attention": "0.8.4-main.937b3ca",
36
- "@dxos/react-ui-menu": "0.8.4-main.937b3ca",
37
- "@dxos/react-ui-mosaic": "0.8.4-main.937b3ca",
38
- "@dxos/plugin-graph": "0.8.4-main.937b3ca",
39
- "@dxos/react-ui-searchlist": "0.8.4-main.937b3ca",
40
- "@dxos/schema": "0.8.4-main.937b3ca",
41
- "@dxos/react-ui-stack": "0.8.4-main.937b3ca",
42
- "@dxos/util": "0.8.4-main.937b3ca"
32
+ "@dxos/app-framework": "0.8.4-main.bc674ce",
33
+ "@dxos/log": "0.8.4-main.bc674ce",
34
+ "@dxos/operation": "0.8.4-main.bc674ce",
35
+ "@dxos/react-ui-attention": "0.8.4-main.bc674ce",
36
+ "@dxos/plugin-graph": "0.8.4-main.bc674ce",
37
+ "@dxos/react-ui-menu": "0.8.4-main.bc674ce",
38
+ "@dxos/react-ui-searchlist": "0.8.4-main.bc674ce",
39
+ "@dxos/react-ui-mosaic": "0.8.4-main.bc674ce",
40
+ "@dxos/react-ui-stack": "0.8.4-main.bc674ce",
41
+ "@dxos/schema": "0.8.4-main.bc674ce",
42
+ "@dxos/util": "0.8.4-main.bc674ce"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/react": "~19.2.7",
@@ -48,22 +48,22 @@
48
48
  "react": "~19.2.3",
49
49
  "react-dom": "~19.2.3",
50
50
  "vite": "7.1.9",
51
- "@dxos/plugin-preview": "0.8.4-main.937b3ca",
52
- "@dxos/plugin-client": "0.8.4-main.937b3ca",
53
- "@dxos/plugin-search": "0.8.4-main.937b3ca",
54
- "@dxos/plugin-space": "0.8.4-main.937b3ca",
55
- "@dxos/schema": "0.8.4-main.937b3ca",
56
- "@dxos/react-ui": "0.8.4-main.937b3ca",
57
- "@dxos/storybook-utils": "0.8.4-main.937b3ca",
58
- "@dxos/plugin-testing": "0.8.4-main.937b3ca",
59
- "@dxos/ui-theme": "0.8.4-main.937b3ca"
51
+ "@dxos/plugin-client": "0.8.4-main.bc674ce",
52
+ "@dxos/plugin-preview": "0.8.4-main.bc674ce",
53
+ "@dxos/plugin-search": "0.8.4-main.bc674ce",
54
+ "@dxos/plugin-space": "0.8.4-main.bc674ce",
55
+ "@dxos/plugin-testing": "0.8.4-main.bc674ce",
56
+ "@dxos/react-ui": "0.8.4-main.bc674ce",
57
+ "@dxos/ui-theme": "0.8.4-main.bc674ce",
58
+ "@dxos/schema": "0.8.4-main.bc674ce",
59
+ "@dxos/storybook-utils": "0.8.4-main.bc674ce"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "effect": "3.19.11",
63
63
  "react": "~19.2.3",
64
64
  "react-dom": "~19.2.3",
65
- "@dxos/react-ui": "0.8.4-main.937b3ca",
66
- "@dxos/ui-theme": "0.8.4-main.937b3ca"
65
+ "@dxos/react-ui": "0.8.4-main.bc674ce",
66
+ "@dxos/ui-theme": "0.8.4-main.bc674ce"
67
67
  },
68
68
  "publishConfig": {
69
69
  "access": "public"
@@ -6,12 +6,19 @@ import * as Effect from 'effect/Effect';
6
6
 
7
7
  import { Capability, Common } from '@dxos/app-framework';
8
8
  import { Operation, OperationResolver } from '@dxos/operation';
9
+ import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
9
10
 
10
11
  import { type SimpleLayoutState, SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
11
12
 
12
13
  /** Maximum number of items to keep in navigation history. */
13
14
  const MAX_HISTORY_LENGTH = 50;
14
15
 
16
+ /** Parse entry ID to extract primary ID and variant. */
17
+ const parseEntryId = (entryId: string) => {
18
+ const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
19
+ return { id, variant };
20
+ };
21
+
15
22
  export default Capability.makeModule(
16
23
  Effect.fnUntraced(function* () {
17
24
  const registry = yield* Capability.get(Common.Capability.AtomRegistry);
@@ -23,6 +30,15 @@ export default Capability.makeModule(
23
30
  };
24
31
 
25
32
  return Capability.contributes(Common.Capability.OperationResolver, [
33
+ //
34
+ // SetLayoutMode
35
+ //
36
+ // TODO(burdon): No-op for to fix startup bug?
37
+ OperationResolver.make({
38
+ operation: Common.LayoutOperation.SetLayoutMode,
39
+ handler: Effect.fnUntraced(function* () {}),
40
+ }),
41
+
26
42
  //
27
43
  // UpdateSidebar - No-op for simple layout.
28
44
  //
@@ -32,11 +48,18 @@ export default Capability.makeModule(
32
48
  }),
33
49
 
34
50
  //
35
- // UpdateComplementary - No-op for simple layout.
51
+ // UpdateComplementary - Controls companion drawer.
36
52
  //
37
53
  OperationResolver.make({
38
54
  operation: Common.LayoutOperation.UpdateComplementary,
39
- handler: () => Effect.void,
55
+ handler: Effect.fnUntraced(function* (input) {
56
+ if (input.state === 'closed') {
57
+ updateState((state) => ({
58
+ ...state,
59
+ drawerState: 'closed',
60
+ }));
61
+ }
62
+ }),
40
63
  }),
41
64
 
42
65
  //
@@ -120,18 +143,32 @@ export default Capability.makeModule(
120
143
  OperationResolver.make({
121
144
  operation: Common.LayoutOperation.Open,
122
145
  handler: Effect.fnUntraced(function* (input) {
123
- updateState((state) => {
124
- // Push current active to history if it exists.
125
- const newHistory = state.active ? [...state.history, state.active] : state.history;
126
- // Limit history length to prevent memory issues.
127
- const trimmedHistory =
128
- newHistory.length > MAX_HISTORY_LENGTH ? newHistory.slice(-MAX_HISTORY_LENGTH) : newHistory;
129
- return {
146
+ const id = input.subject[0];
147
+ const { variant } = parseEntryId(id);
148
+
149
+ if (variant) {
150
+ // It's a companion - store the variant preference and open drawer.
151
+ updateState((state) => ({
130
152
  ...state,
131
- active: input.subject[0],
132
- history: trimmedHistory,
133
- };
134
- });
153
+ companionVariant: variant,
154
+ // Open drawer if closed, otherwise preserve current state (expanded/full).
155
+ drawerState: state.drawerState === 'closed' || !state.drawerState ? 'expanded' : state.drawerState,
156
+ }));
157
+ } else {
158
+ // Regular navigation - update active and history.
159
+ updateState((state) => {
160
+ // Push current active to history if it exists.
161
+ const newHistory = state.active ? [...state.history, state.active] : state.history;
162
+ // Limit history length to prevent memory issues.
163
+ const trimmedHistory =
164
+ newHistory.length > MAX_HISTORY_LENGTH ? newHistory.slice(-MAX_HISTORY_LENGTH) : newHistory;
165
+ return {
166
+ ...state,
167
+ active: id,
168
+ history: trimmedHistory,
169
+ };
170
+ });
171
+ }
135
172
  }),
136
173
  }),
137
174
 
@@ -17,6 +17,8 @@ const defaultState: SimpleLayoutState = {
17
17
  previousWorkspace: Node.RootId,
18
18
  history: [],
19
19
  isPopover: false,
20
+ companionVariant: undefined,
21
+ drawerState: 'closed',
20
22
  };
21
23
 
22
24
  export type SimpleLayoutStateOptions = {
@@ -2,12 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import React, { Fragment, useCallback, useMemo } from 'react';
6
6
 
7
7
  import { Common } from '@dxos/app-framework';
8
8
  import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
9
- import { Graph, Node } from '@dxos/plugin-graph';
10
- import { IconButton, type ThemedClassName, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
+ import { Graph, Node, useActionRunner, useActions } from '@dxos/plugin-graph';
10
+ import { IconButton, Popover, type ThemedClassName, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
+ import { DropdownMenu, MenuProvider } from '@dxos/react-ui-menu';
11
12
  import { mx, osTranslations } from '@dxos/ui-theme';
12
13
 
13
14
  import { useSimpleLayoutState } from '../../hooks';
@@ -33,9 +34,19 @@ export const Banner = ({ node, classNames }: BannerProps) => {
33
34
  const { state } = useSimpleLayoutState();
34
35
  const { invokePromise } = useOperationInvoker();
35
36
  const { graph } = useAppGraph();
37
+ const runAction = useActionRunner();
36
38
 
37
39
  const label = (node && toLocalizedString(node.properties.label, t)) ?? t('current app name', { ns: osTranslations });
38
40
 
41
+ // Get actions for the current node, filtered by disposition.
42
+ // NOTE: Graph expansion is handled by useLoadDescendents in Main.tsx.
43
+ const allActions = useActions(graph, node?.id);
44
+ const actions = useMemo(() => {
45
+ return allActions.filter((a) =>
46
+ ['list-item', 'list-item-primary', 'heading-list-item'].includes(a.properties.disposition),
47
+ );
48
+ }, [allActions]);
49
+
39
50
  // Check if current active item is a top-level workspace/collection child.
40
51
  const isTopLevelItem = useMemo(() => {
41
52
  if (!state.active) {
@@ -58,6 +69,9 @@ export const Banner = ({ node, classNames }: BannerProps) => {
58
69
  }
59
70
  }, [invokePromise, state.active, state.history.length, isTopLevelItem]);
60
71
 
72
+ // Wrap the menu trigger with Popover.Anchor when the popoverAnchorId matches.
73
+ const AnchorRoot = node && state.popoverAnchorId === `dxos.org/ui/${meta.id}/${node.id}` ? Popover.Anchor : Fragment;
74
+
61
75
  if (!node) {
62
76
  return null;
63
77
  }
@@ -76,7 +90,24 @@ export const Banner = ({ node, classNames }: BannerProps) => {
76
90
  <div />
77
91
  )}
78
92
  <h1 className={'grow text-center truncate font-medium'}>{label}</h1>
79
- <IconButton iconOnly variant='ghost' icon='ph--dots-three-vertical--regular' label={t('menu label')} />
93
+ {actions.length > 0 ? (
94
+ <AnchorRoot>
95
+ <MenuProvider onAction={runAction}>
96
+ <DropdownMenu.Root items={actions} caller={meta.id}>
97
+ <DropdownMenu.Trigger asChild>
98
+ <IconButton
99
+ iconOnly
100
+ variant='ghost'
101
+ icon='ph--dots-three-vertical--regular'
102
+ label={t('actions menu label')}
103
+ />
104
+ </DropdownMenu.Trigger>
105
+ </DropdownMenu.Root>
106
+ </MenuProvider>
107
+ </AnchorRoot>
108
+ ) : (
109
+ <span />
110
+ )}
80
111
  </Toolbar.Root>
81
112
  );
82
113
  };
@@ -0,0 +1,151 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo } from 'react';
6
+
7
+ import { Surface, useAppGraph } from '@dxos/app-framework/react';
8
+ import { type Node, useNode } from '@dxos/plugin-graph';
9
+ import { IconButton, Main as NaturalMain, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
+ import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
11
+
12
+ import { useCompanions, useSimpleLayoutState } from '../../hooks';
13
+ import { meta } from '../../meta';
14
+ import { ContentError } from '../ContentError';
15
+ import { ContentLoading } from '../ContentLoading';
16
+
17
+ const DRAWER_NAME = 'SimpleLayout.Drawer';
18
+
19
+ /**
20
+ * Companion drawer component.
21
+ */
22
+ export const Drawer = () => {
23
+ const { t } = useTranslation(meta.id);
24
+ const { state, updateState } = useSimpleLayoutState();
25
+ const { graph } = useAppGraph();
26
+
27
+ const placeholder = useMemo(() => <ContentLoading />, []);
28
+
29
+ // Get all companions for the current active (primary) item.
30
+ const activeId = state.active ?? state.workspace;
31
+ const companions = useCompanions(activeId);
32
+ const { companionId, variant } = useSelectedCompanion(companions, state.companionVariant);
33
+
34
+ // Get node for the selected companion.
35
+ const node = useNode(graph, companionId);
36
+ const parentNode = useNode(graph, activeId);
37
+
38
+ // Build Surface data for the companion content.
39
+ const data = useMemo(() => {
40
+ return (
41
+ node && {
42
+ attendableId: companionId,
43
+ subject: node.data,
44
+ companionTo: parentNode?.data,
45
+ properties: node.properties,
46
+ variant,
47
+ }
48
+ );
49
+ }, [companionId, node, parentNode, variant]);
50
+
51
+ // Handle tab click to switch companions.
52
+ const handleTabClick = useCallback(
53
+ (companion: Node.Node) => {
54
+ const [, companionVariant] = companion.id.split(ATTENDABLE_PATH_SEPARATOR);
55
+ updateState((s) => ({ ...s, companionVariant }));
56
+ },
57
+ [updateState],
58
+ );
59
+
60
+ // Handle expand/collapse toggle.
61
+ const handleToggleExpand = useCallback(() => {
62
+ updateState((s) => ({
63
+ ...s,
64
+ drawerState: s.drawerState === 'full' ? 'expanded' : 'full',
65
+ }));
66
+ }, [updateState]);
67
+
68
+ // Handle close.
69
+ const handleClose = useCallback(() => {
70
+ updateState((s) => ({ ...s, drawerState: 'closed' }));
71
+ }, [updateState]);
72
+
73
+ const drawerState = state.drawerState ?? 'closed';
74
+ if (drawerState === 'closed') {
75
+ return null;
76
+ }
77
+
78
+ const isFullyExpanded = drawerState === 'full';
79
+
80
+ return (
81
+ <NaturalMain.Drawer label={t('drawer label')}>
82
+ <Toolbar.Root>
83
+ {/* TODO(thure): IMPORTANT: This is a tablist; it should be implemented as such. */}
84
+ <div role='tablist' className='flex-1 min-is-0 overflow-x-auto scrollbar-none flex gap-1'>
85
+ {/* TODO(burdon): Factor out in common with NavBar. */}
86
+ {companions.map((companion) => (
87
+ <IconButton
88
+ key={companion.id}
89
+ role='tab'
90
+ aria-selected={companionId === companion.id}
91
+ icon={companion.properties.icon}
92
+ iconOnly
93
+ label={toLocalizedString(companion.properties.label, t)}
94
+ variant={companionId === companion.id ? 'primary' : 'ghost'}
95
+ onClick={() => handleTabClick(companion)}
96
+ />
97
+ ))}
98
+ </div>
99
+ <Toolbar.Separator variant='gap' />
100
+ <Toolbar.IconButton
101
+ icon={isFullyExpanded ? 'ph--arrow-down--regular' : 'ph--arrow-up--regular'}
102
+ iconOnly
103
+ label={isFullyExpanded ? t('collapse drawer label') : t('expand drawer label')}
104
+ onClick={handleToggleExpand}
105
+ />
106
+ <Toolbar.IconButton icon='ph--x--regular' iconOnly label={t('close drawer label')} onClick={handleClose} />
107
+ </Toolbar.Root>
108
+ {/* TODO(burdon): Fix containment. */}
109
+ <Surface role='article' data={data} limit={1} fallback={ContentError} placeholder={placeholder} />
110
+ </NaturalMain.Drawer>
111
+ );
112
+ };
113
+
114
+ Drawer.displayName = DRAWER_NAME;
115
+
116
+ /** Parse entry ID to extract primary ID and variant. */
117
+ const parseEntryId = (entryId: string) => {
118
+ const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
119
+ return { id, variant };
120
+ };
121
+
122
+ /**
123
+ * Resolves which companion to show based on variant preference.
124
+ * Falls back to first available if preferred variant not available.
125
+ */
126
+ const useSelectedCompanion = (companions: Node.Node[], preferredVariant?: string) => {
127
+ const selectedCompanion = useMemo(() => {
128
+ if (companions.length === 0) {
129
+ return undefined;
130
+ }
131
+
132
+ // Try to find companion matching the preferred variant.
133
+ if (preferredVariant) {
134
+ const preferred = companions.find((c) => {
135
+ const { variant } = parseEntryId(c.id);
136
+ return variant === preferredVariant;
137
+ });
138
+ if (preferred) {
139
+ return preferred;
140
+ }
141
+ }
142
+
143
+ // Fallback to first companion.
144
+ return companions[0];
145
+ }, [companions, preferredVariant]);
146
+
147
+ const companionId = selectedCompanion?.id;
148
+ const { variant } = parseEntryId(companionId ?? '');
149
+
150
+ return { selectedCompanion, companionId, variant };
151
+ };
@@ -2,80 +2,71 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
7
  import { Surface, useAppGraph } from '@dxos/app-framework/react';
8
- import { log } from '@dxos/log';
9
8
  import { useNode } from '@dxos/plugin-graph';
10
- import { Main as NaturalMain } from '@dxos/react-ui';
11
- import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
12
- import { Mosaic } from '@dxos/react-ui-mosaic';
9
+ import { Main as NaturalMain, useSidebars } from '@dxos/react-ui';
13
10
  import { mx } from '@dxos/ui-theme';
14
11
 
15
12
  import { useSimpleLayoutState } from '../../hooks';
16
13
  import { ContentError } from '../ContentError';
17
14
  import { ContentLoading } from '../ContentLoading';
15
+ import { useLoadDescendents } from '../hooks';
18
16
 
19
17
  import { Banner } from './Banner';
20
18
  import { NavBar } from './NavBar';
21
19
 
20
+ const MAIN_NAME = 'SimpleLayout.Main';
21
+
22
22
  /**
23
- * Main root component.
23
+ * Main content component.
24
24
  */
25
25
  export const Main = () => {
26
26
  const { state } = useSimpleLayoutState();
27
- const id = state.active ?? state.workspace;
28
- const showNavBar = !state.isPopover;
29
27
  const { graph } = useAppGraph();
28
+ const id = state.active ?? state.workspace;
30
29
  const node = useNode(graph, id);
31
30
 
31
+ // Ensures that children are loaded so that they are available to navigate to.
32
+ useLoadDescendents(id);
33
+
32
34
  const placeholder = useMemo(() => <ContentLoading />, []);
33
35
 
34
36
  const data = useMemo(() => {
35
- const { variant } = parseEntryId(id);
36
37
  return (
37
38
  node && {
38
39
  attendableId: id,
39
40
  subject: node.data,
40
41
  properties: node.properties,
41
42
  popoverAnchorId: state.popoverAnchorId,
42
- variant,
43
43
  }
44
44
  );
45
45
  }, [id, node, node?.data, node?.properties, state.popoverAnchorId]);
46
46
 
47
- const handleActiveIdChange = useCallback((nextActiveId: string | null) => {
48
- log.info('navigate', { nextActiveId });
49
- }, []);
47
+ const { drawerState } = useSidebars(MAIN_NAME);
48
+ const showNavBar = !state.isPopover && drawerState === 'closed';
50
49
 
51
50
  return (
52
- <Mosaic.Root>
53
- <NaturalMain.Root complementarySidebarState='closed' navigationSidebarState='closed'>
54
- <NaturalMain.Content
55
- bounce
56
- classNames={mx(
57
- 'dx-mobile-main dx-mobile-main-scroll-area--flush',
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='contents'>
64
- <Surface key={id} role='article' data={data} limit={1} fallback={ContentError} placeholder={placeholder} />
65
- </article>
66
- {showNavBar && (
67
- <NavBar classNames='border-bs border-separator' activeId={id} onActiveIdChange={handleActiveIdChange} />
68
- )}
69
- </NaturalMain.Content>
70
- </NaturalMain.Root>
71
- </Mosaic.Root>
51
+ <NaturalMain.Content
52
+ bounce
53
+ classNames={mx('bs-full', 'pbs-[env(safe-area-inset-top)] pbe-[env(safe-area-inset-bottom)]')}
54
+ >
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>
72
69
  );
73
70
  };
74
71
 
75
- // TODO(wittjosiah): Factor out. Copied from deck plugin.
76
- const parseEntryId = (entryId: string) => {
77
- const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
78
- return { id, variant };
79
- };
80
-
81
- Main.displayName = 'SimpleLayout.Main';
72
+ Main.displayName = MAIN_NAME;
@@ -25,9 +25,6 @@ const meta = {
25
25
  layout: 'fullscreen',
26
26
  translations,
27
27
  },
28
- argTypes: {
29
- onActiveIdChange: { action: 'activeIdChanged' },
30
- },
31
28
  } satisfies Meta<typeof NavBar>;
32
29
 
33
30
  export default meta;
@@ -4,71 +4,63 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useAppGraph } from '@dxos/app-framework/react';
7
+ import { Common } from '@dxos/app-framework';
8
+ import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
8
9
  import { Node, useActionRunner, useConnections } from '@dxos/plugin-graph';
9
- import { IconButton, type ThemedClassName, Toolbar, Tooltip, useTranslation } from '@dxos/react-ui';
10
+ import { IconButton, type ThemedClassName, Toolbar, Tooltip, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
11
  import { DropdownMenu, MenuProvider } from '@dxos/react-ui-menu';
11
12
  import { mx } from '@dxos/ui-theme';
12
13
 
14
+ import { useCompanions } from '../../hooks';
13
15
  import { meta } from '../../meta';
14
16
 
17
+ const NAVBAR_NAME = 'SimpleLayout.NavBar';
18
+
15
19
  export type NavBarProps = ThemedClassName<{
20
+ /** Active AppGraph node ID. */
16
21
  activeId?: string;
17
- onActiveIdChange?: (nextActiveId: string | null) => void;
18
22
  }>;
19
23
 
20
- export const NavBar = ({ classNames, activeId, onActiveIdChange }: NavBarProps) => {
24
+ export const NavBar = ({ classNames, activeId }: NavBarProps) => {
21
25
  const { t } = useTranslation(meta.id);
22
26
  const { graph } = useAppGraph();
23
27
  const runAction = useActionRunner();
28
+ const { invokePromise } = useOperationInvoker();
24
29
 
25
30
  const connections = useConnections(graph, Node.RootId);
26
31
  const menuActions = connections.filter((node) => node.properties.disposition === 'menu');
27
32
 
28
- const isBrowseActive = activeId !== 'notifications' && activeId !== 'profile';
33
+ const companions = useCompanions(activeId);
29
34
 
30
35
  return (
31
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
+
32
53
  <MenuProvider onAction={runAction}>
33
54
  <DropdownMenu.Root items={menuActions}>
34
55
  <Tooltip.Trigger asChild content={t('app menu label')}>
35
- <DropdownMenu.Trigger asChild data-testid='spacePlugin.addSpace'>
56
+ <DropdownMenu.Trigger asChild data-testid='simpleLayoutPlugin.addSpace'>
36
57
  <IconButton icon='ph--plus--regular' iconOnly label={t('main menu label')} />
37
58
  </DropdownMenu.Trigger>
38
59
  </Tooltip.Trigger>
39
60
  </DropdownMenu.Root>
40
61
  </MenuProvider>
41
- {/*
42
- <ButtonGroup>
43
- <IconButton
44
- {...buttonProps}
45
- label={t('browse label')}
46
- icon='ph--squares-four--regular'
47
- onClick={() => onActiveIdChange?.(null)}
48
- variant={isBrowseActive ? 'primary' : 'default'}
49
- {...(isBrowseActive && { 'aria-current': 'location' })}
50
- />
51
- <IconButton
52
- {...buttonProps}
53
- label={t('notifications label')}
54
- icon='ph--bell-simple--regular'
55
- onClick={() => onActiveIdChange?.('notifications')}
56
- variant={activeId === 'notifications' ? 'primary' : 'default'}
57
- {...(activeId === 'notifications' && { 'aria-current': 'location' })}
58
- />
59
- <Button
60
- variant={activeId === 'profile' ? 'primary' : 'default'}
61
- onClick={() => onActiveIdChange?.('profile')}
62
- classNames={buttonProps.classNames}
63
- >
64
- <span className='sr-only'>{t('profile label')}</span>
65
- <Avatar.Root>
66
- <Avatar.Label classNames='sr-only'>Profile display name</Avatar.Label>
67
- <Avatar.Content size={8} status='active' hue='cyan' fallback='🗿' />
68
- </Avatar.Root>
69
- </Button>
70
- </ButtonGroup>
71
- */}
72
62
  </Toolbar.Root>
73
63
  );
74
64
  };
65
+
66
+ NavBar.displayName = NAVBAR_NAME;
@@ -91,6 +91,10 @@ export default meta;
91
91
 
92
92
  type Story = StoryObj<typeof meta>;
93
93
 
94
+ /**
95
+ * NOTE: To expose to iphone on network:
96
+ * `moon run storybook-react:serve dev -H 0.0.0.0`
97
+ */
94
98
  export const Default: Story = {
95
99
  decorators: [withTheme, createPluginManager({ isPopover: false })],
96
100
  };