@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.
- package/dist/lib/browser/{chunk-FK4M7GJV.mjs → chunk-LR3EE3VB.mjs} +298 -122
- package/dist/lib/browser/chunk-LR3EE3VB.mjs.map +7 -0
- package/dist/lib/browser/{chunk-CLPGTNWJ.mjs → chunk-P77G4YTR.mjs} +1 -1
- package/dist/lib/browser/chunk-P77G4YTR.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +13 -7
- package/dist/lib/browser/index.mjs.map +2 -2
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{operation-resolver-LTB63NKP.mjs → operation-resolver-775UYAC2.mjs} +50 -15
- package/dist/lib/browser/operation-resolver-775UYAC2.mjs.map +7 -0
- package/dist/lib/browser/{react-root-6ARAPH3O.mjs → react-root-KM55OMGJ.mjs} +3 -3
- package/dist/lib/browser/{react-surface-SO7B23GS.mjs → react-surface-BABGAWGY.mjs} +3 -3
- package/dist/lib/browser/{state-H4IGICBB.mjs → state-OUFTC2KV.mjs} +5 -3
- package/dist/lib/browser/state-OUFTC2KV.mjs.map +7 -0
- package/dist/lib/browser/{url-handler-7CFGTLNG.mjs → url-handler-DOUFQIAC.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-MUVVYBUE.mjs → chunk-F5TEKVJG.mjs} +1 -1
- package/dist/lib/node-esm/chunk-F5TEKVJG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-EGFZAVBD.mjs → chunk-HB2B3LLG.mjs} +298 -122
- package/dist/lib/node-esm/chunk-HB2B3LLG.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +13 -7
- package/dist/lib/node-esm/index.mjs.map +2 -2
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{operation-resolver-7O6O7T4Q.mjs → operation-resolver-LDNYS3DI.mjs} +50 -15
- package/dist/lib/node-esm/operation-resolver-LDNYS3DI.mjs.map +7 -0
- package/dist/lib/node-esm/{react-root-2CPA2ZUS.mjs → react-root-36UYFEEB.mjs} +3 -3
- package/dist/lib/node-esm/{react-surface-FKAV56MO.mjs → react-surface-CGHFVWU3.mjs} +3 -3
- package/dist/lib/node-esm/{state-QIU2LMLT.mjs → state-Q2ZA26W5.mjs} +5 -3
- package/dist/lib/node-esm/state-Q2ZA26W5.mjs.map +7 -0
- package/dist/lib/node-esm/{url-handler-4LYP3JM7.mjs → url-handler-DVAZZEUO.mjs} +2 -2
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
- package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
- package/dist/types/src/components/ContentError.stories.d.ts +6 -0
- package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts +9 -0
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/Main.d.ts +1 -1
- package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts +5 -2
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +10 -6
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +10 -0
- package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/hooks.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useCompanions.d.ts +8 -0
- package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +6 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/capabilities.d.ts +5 -1
- package/dist/types/src/types/capabilities.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -23
- package/src/capabilities/operation-resolver/operation-resolver.ts +50 -13
- package/src/capabilities/state/state.tsx +2 -0
- package/src/components/SimpleLayout/Banner.tsx +35 -4
- package/src/components/SimpleLayout/Drawer.tsx +151 -0
- package/src/components/SimpleLayout/Main.tsx +31 -40
- package/src/components/SimpleLayout/NavBar.stories.tsx +0 -3
- package/src/components/SimpleLayout/NavBar.tsx +29 -37
- package/src/components/SimpleLayout/SimpleLayout.stories.tsx +4 -0
- package/src/components/SimpleLayout/SimpleLayout.tsx +26 -6
- package/src/components/hooks.ts +8 -12
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCompanions.ts +19 -0
- package/src/translations.ts +6 -0
- package/src/types/capabilities.ts +6 -1
- package/dist/lib/browser/chunk-CLPGTNWJ.mjs.map +0 -7
- package/dist/lib/browser/chunk-FK4M7GJV.mjs.map +0 -7
- package/dist/lib/browser/operation-resolver-LTB63NKP.mjs.map +0 -7
- package/dist/lib/browser/state-H4IGICBB.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-EGFZAVBD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MUVVYBUE.mjs.map +0 -7
- package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs.map +0 -7
- package/dist/lib/node-esm/state-QIU2LMLT.mjs.map +0 -7
- /package/dist/lib/browser/{react-root-6ARAPH3O.mjs.map → react-root-KM55OMGJ.mjs.map} +0 -0
- /package/dist/lib/browser/{react-surface-SO7B23GS.mjs.map → react-surface-BABGAWGY.mjs.map} +0 -0
- /package/dist/lib/browser/{url-handler-7CFGTLNG.mjs.map → url-handler-DOUFQIAC.mjs.map} +0 -0
- /package/dist/lib/node-esm/{react-root-2CPA2ZUS.mjs.map → react-root-36UYFEEB.mjs.map} +0 -0
- /package/dist/lib/node-esm/{react-surface-FKAV56MO.mjs.map → react-surface-CGHFVWU3.mjs.map} +0 -0
- /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.
|
|
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.
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/react-ui-attention": "0.8.4-main.
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/react-ui-
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/react-ui-
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/util": "0.8.4-main.
|
|
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-
|
|
52
|
-
"@dxos/plugin-
|
|
53
|
-
"@dxos/plugin-search": "0.8.4-main.
|
|
54
|
-
"@dxos/plugin-space": "0.8.4-main.
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
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.
|
|
66
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
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 -
|
|
51
|
+
// UpdateComplementary - Controls companion drawer.
|
|
36
52
|
//
|
|
37
53
|
OperationResolver.make({
|
|
38
54
|
operation: Common.LayoutOperation.UpdateComplementary,
|
|
39
|
-
handler: ()
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
48
|
-
|
|
49
|
-
}, []);
|
|
47
|
+
const { drawerState } = useSidebars(MAIN_NAME);
|
|
48
|
+
const showNavBar = !state.isPopover && drawerState === 'closed';
|
|
50
49
|
|
|
51
50
|
return (
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -4,71 +4,63 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
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
|
|
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
|
|
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='
|
|
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
|
};
|