@dxos/plugin-simple-layout 0.0.0
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/LICENSE +8 -0
- package/package.json +66 -0
- package/src/SimpleLayoutPlugin.ts +31 -0
- package/src/capabilities/index.ts +7 -0
- package/src/capabilities/operation-resolver/index.ts +10 -0
- package/src/capabilities/operation-resolver/operation-resolver.ts +135 -0
- package/src/capabilities/react-root/index.ts +7 -0
- package/src/capabilities/react-root/react-root.tsx +22 -0
- package/src/capabilities/state/index.ts +9 -0
- package/src/capabilities/state/state.tsx +60 -0
- package/src/components/ContentError.stories.tsx +41 -0
- package/src/components/ContentError.tsx +23 -0
- package/src/components/ContentLoading.stories.tsx +24 -0
- package/src/components/ContentLoading.tsx +10 -0
- package/src/components/Dialog/Dialog.tsx +38 -0
- package/src/components/Dialog/index.ts +5 -0
- package/src/components/Home/Home.tsx +138 -0
- package/src/components/Home/index.ts +5 -0
- package/src/components/Popover/Popover.tsx +102 -0
- package/src/components/Popover/index.ts +5 -0
- package/src/components/SimpleLayout/Banner.tsx +60 -0
- package/src/components/SimpleLayout/Main.tsx +85 -0
- package/src/components/SimpleLayout/NavBar.tsx +97 -0
- package/src/components/SimpleLayout/NavBarstories.tsx +59 -0
- package/src/components/SimpleLayout/SimpleLayout.stories.tsx +107 -0
- package/src/components/SimpleLayout/SimpleLayout.tsx +21 -0
- package/src/components/SimpleLayout/index.ts +5 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useSpotlightDismiss.ts +95 -0
- package/src/index.ts +5 -0
- package/src/meta.ts +16 -0
- package/src/translations.ts +28 -0
- package/src/types/capabilities.ts +37 -0
- package/src/types/index.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2022 DXOS
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/plugin-simple-layout",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Simple layout plugin for minimal UI contexts like popover windows.",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "DXOS.org",
|
|
9
|
+
"sideEffects": true,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/types/src/index.d.ts",
|
|
15
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
16
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"types": "dist/types/src/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@preact-signals/safe-react": "^0.9.0",
|
|
26
|
+
"@radix-ui/react-context": "1.1.1",
|
|
27
|
+
"@dxos/app-framework": "0.8.3",
|
|
28
|
+
"@dxos/log": "0.8.3",
|
|
29
|
+
"@dxos/react-ui-menu": "0.8.3",
|
|
30
|
+
"@dxos/live-object": "0.8.3",
|
|
31
|
+
"@dxos/plugin-graph": "0.8.3",
|
|
32
|
+
"@dxos/operation": "0.0.0",
|
|
33
|
+
"@dxos/react-ui-searchlist": "0.8.3",
|
|
34
|
+
"@dxos/schema": "0.8.3",
|
|
35
|
+
"@dxos/react-ui-attention": "0.8.3",
|
|
36
|
+
"@dxos/react-ui-stack": "0.8.3",
|
|
37
|
+
"@dxos/util": "0.8.3",
|
|
38
|
+
"@dxos/react-ui-mosaic": "0.8.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "~19.2.7",
|
|
42
|
+
"@types/react-dom": "~19.2.3",
|
|
43
|
+
"effect": "3.19.11",
|
|
44
|
+
"react": "~19.2.3",
|
|
45
|
+
"react-dom": "~19.2.3",
|
|
46
|
+
"vite": "7.1.9",
|
|
47
|
+
"@dxos/echo-signals": "0.8.3",
|
|
48
|
+
"@dxos/plugin-search": "0.8.3",
|
|
49
|
+
"@dxos/plugin-client": "0.8.3",
|
|
50
|
+
"@dxos/plugin-space": "0.8.3",
|
|
51
|
+
"@dxos/plugin-testing": "0.0.0",
|
|
52
|
+
"@dxos/ui-theme": "0.0.0",
|
|
53
|
+
"@dxos/react-ui": "0.8.3",
|
|
54
|
+
"@dxos/storybook-utils": "0.8.3"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"effect": "3.19.11",
|
|
58
|
+
"react": "~19.2.3",
|
|
59
|
+
"react-dom": "~19.2.3",
|
|
60
|
+
"@dxos/react-ui": "0.8.3",
|
|
61
|
+
"@dxos/ui-theme": "0.0.0"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Capability, Common, Plugin } from '@dxos/app-framework';
|
|
6
|
+
|
|
7
|
+
import { OperationResolver, ReactRoot, type SimpleLayoutStateOptions, State } from './capabilities';
|
|
8
|
+
import { meta } from './meta';
|
|
9
|
+
import { translations } from './translations';
|
|
10
|
+
|
|
11
|
+
export type SimpleLayoutPluginOptions = {
|
|
12
|
+
/** Whether running in popover window context (hides mobile-specific UI). */
|
|
13
|
+
isPopover?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const SimpleLayoutPlugin = Plugin.define<SimpleLayoutPluginOptions>(meta).pipe(
|
|
17
|
+
Plugin.addModule(({ isPopover = false }) => ({
|
|
18
|
+
id: Capability.getModuleTag(State),
|
|
19
|
+
activatesOn: Common.ActivationEvent.Startup,
|
|
20
|
+
activatesAfter: [Common.ActivationEvent.LayoutReady],
|
|
21
|
+
activate: () => State({ initialState: { isPopover } } satisfies SimpleLayoutStateOptions),
|
|
22
|
+
})),
|
|
23
|
+
Plugin.addModule({
|
|
24
|
+
id: Capability.getModuleTag(ReactRoot),
|
|
25
|
+
activatesOn: Common.ActivationEvent.Startup,
|
|
26
|
+
activate: ReactRoot,
|
|
27
|
+
}),
|
|
28
|
+
Common.Plugin.addOperationResolverModule({ activate: OperationResolver }),
|
|
29
|
+
Common.Plugin.addTranslationsModule({ translations }),
|
|
30
|
+
Plugin.make,
|
|
31
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Capability, Capability as Capability$ } from '@dxos/app-framework';
|
|
6
|
+
|
|
7
|
+
export const OperationResolver: Capability.LazyCapability = Capability$.lazy(
|
|
8
|
+
'OperationResolver',
|
|
9
|
+
() => import('./operation-resolver'),
|
|
10
|
+
);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Capability, Common } from '@dxos/app-framework';
|
|
8
|
+
import { Operation, OperationResolver } from '@dxos/operation';
|
|
9
|
+
|
|
10
|
+
import { SimpleLayoutState } from '../../types';
|
|
11
|
+
|
|
12
|
+
export default Capability.makeModule(
|
|
13
|
+
Effect.fnUntraced(function* () {
|
|
14
|
+
return Capability.contributes(Common.Capability.OperationResolver, [
|
|
15
|
+
//
|
|
16
|
+
// UpdateSidebar - No-op for simple layout.
|
|
17
|
+
//
|
|
18
|
+
OperationResolver.make({
|
|
19
|
+
operation: Common.LayoutOperation.UpdateSidebar,
|
|
20
|
+
handler: () => Effect.void,
|
|
21
|
+
}),
|
|
22
|
+
|
|
23
|
+
//
|
|
24
|
+
// UpdateComplementary - No-op for simple layout.
|
|
25
|
+
//
|
|
26
|
+
OperationResolver.make({
|
|
27
|
+
operation: Common.LayoutOperation.UpdateComplementary,
|
|
28
|
+
handler: () => Effect.void,
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
//
|
|
32
|
+
// UpdateDialog
|
|
33
|
+
//
|
|
34
|
+
OperationResolver.make({
|
|
35
|
+
operation: Common.LayoutOperation.UpdateDialog,
|
|
36
|
+
handler: Effect.fnUntraced(function* (input) {
|
|
37
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
38
|
+
layout.dialogOpen = input.state ?? Boolean(input.subject);
|
|
39
|
+
layout.dialogType = input.type ?? 'default';
|
|
40
|
+
layout.dialogBlockAlign = input.blockAlign ?? 'center';
|
|
41
|
+
layout.dialogOverlayClasses = input.overlayClasses;
|
|
42
|
+
layout.dialogOverlayStyle = input.overlayStyle;
|
|
43
|
+
layout.dialogContent = input.subject ? { component: input.subject, props: input.props } : null;
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
//
|
|
48
|
+
// UpdatePopover
|
|
49
|
+
//
|
|
50
|
+
OperationResolver.make({
|
|
51
|
+
operation: Common.LayoutOperation.UpdatePopover,
|
|
52
|
+
handler: Effect.fnUntraced(function* (input) {
|
|
53
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
54
|
+
layout.popoverOpen = input.state ?? Boolean(input.subject);
|
|
55
|
+
layout.popoverContent =
|
|
56
|
+
typeof input.subject === 'string'
|
|
57
|
+
? { component: input.subject, props: input.props }
|
|
58
|
+
: input.subject
|
|
59
|
+
? { subject: input.subject }
|
|
60
|
+
: undefined;
|
|
61
|
+
layout.popoverSide = input.side;
|
|
62
|
+
layout.popoverVariant = input.variant;
|
|
63
|
+
if (input.variant === 'virtual') {
|
|
64
|
+
layout.popoverAnchor = input.anchor;
|
|
65
|
+
} else {
|
|
66
|
+
layout.popoverAnchorId = input.anchorId;
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
//
|
|
72
|
+
// SwitchWorkspace
|
|
73
|
+
//
|
|
74
|
+
OperationResolver.make({
|
|
75
|
+
operation: Common.LayoutOperation.SwitchWorkspace,
|
|
76
|
+
handler: Effect.fnUntraced(function* (input) {
|
|
77
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
78
|
+
// TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
|
|
79
|
+
// Ideally this should be worked into the data model in a generic way.
|
|
80
|
+
if (!layout.workspace.startsWith('!')) {
|
|
81
|
+
layout.previousWorkspace = layout.workspace;
|
|
82
|
+
}
|
|
83
|
+
layout.workspace = input.subject;
|
|
84
|
+
layout.active = undefined;
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
//
|
|
89
|
+
// RevertWorkspace
|
|
90
|
+
//
|
|
91
|
+
OperationResolver.make({
|
|
92
|
+
operation: Common.LayoutOperation.RevertWorkspace,
|
|
93
|
+
handler: Effect.fnUntraced(function* () {
|
|
94
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
95
|
+
yield* Operation.invoke(Common.LayoutOperation.SwitchWorkspace, {
|
|
96
|
+
subject: layout.previousWorkspace,
|
|
97
|
+
});
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
//
|
|
102
|
+
// Open
|
|
103
|
+
//
|
|
104
|
+
OperationResolver.make({
|
|
105
|
+
operation: Common.LayoutOperation.Open,
|
|
106
|
+
handler: Effect.fnUntraced(function* (input) {
|
|
107
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
108
|
+
layout.active = input.subject[0];
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
//
|
|
113
|
+
// Close
|
|
114
|
+
//
|
|
115
|
+
OperationResolver.make({
|
|
116
|
+
operation: Common.LayoutOperation.Close,
|
|
117
|
+
handler: Effect.fnUntraced(function* () {
|
|
118
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
119
|
+
layout.active = undefined;
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
//
|
|
124
|
+
// Set
|
|
125
|
+
//
|
|
126
|
+
OperationResolver.make({
|
|
127
|
+
operation: Common.LayoutOperation.Set,
|
|
128
|
+
handler: Effect.fnUntraced(function* (input) {
|
|
129
|
+
const layout = yield* Capability.get(SimpleLayoutState);
|
|
130
|
+
layout.active = input.subject[0];
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Capability, Common } from '@dxos/app-framework';
|
|
9
|
+
|
|
10
|
+
import { SimpleLayout } from '../../components';
|
|
11
|
+
import { meta } from '../../meta';
|
|
12
|
+
|
|
13
|
+
export default Capability.makeModule(() =>
|
|
14
|
+
Effect.succeed(
|
|
15
|
+
Capability.contributes(Common.Capability.ReactRoot, {
|
|
16
|
+
id: meta.id,
|
|
17
|
+
root: () => {
|
|
18
|
+
return <SimpleLayout />;
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Capability, Common } from '@dxos/app-framework';
|
|
8
|
+
import { live } from '@dxos/live-object';
|
|
9
|
+
|
|
10
|
+
import { type SimpleLayoutState } from '../../types';
|
|
11
|
+
import { SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
|
|
12
|
+
|
|
13
|
+
const defaultState: SimpleLayoutState = {
|
|
14
|
+
dialogOpen: false,
|
|
15
|
+
workspace: 'default',
|
|
16
|
+
previousWorkspace: 'default',
|
|
17
|
+
isPopover: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SimpleLayoutStateOptions = {
|
|
21
|
+
initialState?: Partial<SimpleLayoutState>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Capability.makeModule(({ initialState }: SimpleLayoutStateOptions = {}) =>
|
|
25
|
+
Effect.sync(() => {
|
|
26
|
+
const state = live<SimpleLayoutState>({ ...defaultState, ...initialState });
|
|
27
|
+
|
|
28
|
+
const layout = live<Common.Capability.Layout>({
|
|
29
|
+
get mode() {
|
|
30
|
+
return 'simple';
|
|
31
|
+
},
|
|
32
|
+
get dialogOpen() {
|
|
33
|
+
return state.dialogOpen;
|
|
34
|
+
},
|
|
35
|
+
get sidebarOpen() {
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
get complementarySidebarOpen() {
|
|
39
|
+
return false;
|
|
40
|
+
},
|
|
41
|
+
get workspace() {
|
|
42
|
+
return state.workspace;
|
|
43
|
+
},
|
|
44
|
+
get active() {
|
|
45
|
+
return state.active ? [state.active] : [];
|
|
46
|
+
},
|
|
47
|
+
get inactive() {
|
|
48
|
+
return [];
|
|
49
|
+
},
|
|
50
|
+
get scrollIntoView() {
|
|
51
|
+
return undefined;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
Capability.contributes(SimpleLayoutStateCapability, state),
|
|
57
|
+
Capability.contributes(Common.Capability.Layout, layout),
|
|
58
|
+
];
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
|
|
7
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
8
|
+
|
|
9
|
+
import { translations } from '../translations';
|
|
10
|
+
|
|
11
|
+
import { ContentError } from './ContentError';
|
|
12
|
+
|
|
13
|
+
const meta = {
|
|
14
|
+
title: 'plugins/plugin-simple-layout/ContentError',
|
|
15
|
+
component: ContentError,
|
|
16
|
+
decorators: [withTheme],
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: 'centered',
|
|
19
|
+
translations,
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof ContentError>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const WithError: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
error: new Error('Something went wrong while loading the content'),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithTypeError: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
error: new TypeError('Cannot read property "foo" of undefined'),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { descriptionMessage, mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { meta } from '../meta';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Factor out.
|
|
13
|
+
export const ContentError = ({ error }: { error?: Error }) => {
|
|
14
|
+
const { t } = useTranslation(meta.id);
|
|
15
|
+
const errorString = error?.toString() ?? '';
|
|
16
|
+
return (
|
|
17
|
+
<div role='none' className='grid place-items-center overflow-y-auto attention-surface'>
|
|
18
|
+
<p role='alert' className={mx(descriptionMessage, 'p-2 break-all rounded-sm')}>
|
|
19
|
+
{error ? errorString : t('error fallback message')}
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
|
|
7
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
8
|
+
|
|
9
|
+
import { ContentLoading } from './ContentLoading';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'plugins/plugin-simple-layout/ContentLoading',
|
|
13
|
+
component: ContentLoading,
|
|
14
|
+
decorators: [withTheme],
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'centered',
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof ContentLoading>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
// TODO(burdon): Show skeleton: https://github.com/dxos/dxos/issues/8259
|
|
8
|
+
export const ContentLoading = () => {
|
|
9
|
+
return <div role='none' className='grid place-items-center attention-surface' />;
|
|
10
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Surface, useCapability } from '@dxos/app-framework/react';
|
|
8
|
+
import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { SimpleLayoutState } from '../../types';
|
|
11
|
+
import { ContentError } from '../ContentError';
|
|
12
|
+
|
|
13
|
+
export const Dialog = () => {
|
|
14
|
+
const layout = useCapability(SimpleLayoutState);
|
|
15
|
+
|
|
16
|
+
const DialogRoot = layout.dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
|
|
17
|
+
const DialogOverlay = layout.dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<DialogRoot
|
|
21
|
+
modal={layout.dialogBlockAlign !== 'end'}
|
|
22
|
+
open={layout.dialogOpen}
|
|
23
|
+
onOpenChange={(nextOpen) => (layout.dialogOpen = nextOpen)}
|
|
24
|
+
>
|
|
25
|
+
{layout.dialogBlockAlign === 'end' ? (
|
|
26
|
+
<Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} placeholder={<div />} />
|
|
27
|
+
) : (
|
|
28
|
+
<DialogOverlay
|
|
29
|
+
blockAlign={layout.dialogBlockAlign}
|
|
30
|
+
classNames={layout.dialogOverlayClasses}
|
|
31
|
+
style={layout.dialogOverlayStyle}
|
|
32
|
+
>
|
|
33
|
+
<Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} />
|
|
34
|
+
</DialogOverlay>
|
|
35
|
+
)}
|
|
36
|
+
</DialogRoot>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Common } from '@dxos/app-framework';
|
|
8
|
+
import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
|
|
9
|
+
import { Graph, Node, useConnections } from '@dxos/plugin-graph';
|
|
10
|
+
import { Avatar, Icon, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
import { Card } from '@dxos/react-ui-mosaic';
|
|
12
|
+
import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
|
|
13
|
+
import { mx } from '@dxos/ui-theme';
|
|
14
|
+
|
|
15
|
+
import { meta } from '../../meta';
|
|
16
|
+
|
|
17
|
+
type HomeProps = ThemedClassName;
|
|
18
|
+
|
|
19
|
+
export const Home = ({ classNames }: HomeProps) => {
|
|
20
|
+
const { t } = useTranslation(meta.id);
|
|
21
|
+
const workspaces = useWorkspaces();
|
|
22
|
+
useLoadDescendents(Node.RootId);
|
|
23
|
+
|
|
24
|
+
const { results, handleSearch } = useSearchListResults({
|
|
25
|
+
items: workspaces,
|
|
26
|
+
extract: (node) => toLocalizedString(node.properties.label, t),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={mx('flex flex-col pli-3', classNames)}>
|
|
31
|
+
{/* <div className='container-max-width'>{t('workspaces heading')}</div> */}
|
|
32
|
+
<SearchList.Root onSearch={handleSearch} classNames='container-max-width'>
|
|
33
|
+
<div className='plb-3'>
|
|
34
|
+
<SearchList.Input placeholder={t('search placeholder')} autoFocus />
|
|
35
|
+
</div>
|
|
36
|
+
<SearchList.Content>
|
|
37
|
+
<SearchList.Viewport classNames='flex flex-col gap-1'>
|
|
38
|
+
{results.map((node) => (
|
|
39
|
+
<Workspace key={node.id} node={node} />
|
|
40
|
+
))}
|
|
41
|
+
</SearchList.Viewport>
|
|
42
|
+
</SearchList.Content>
|
|
43
|
+
</SearchList.Root>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const Workspace = ({ node }: { node: Node.Node }) => {
|
|
49
|
+
const { t } = useTranslation(meta.id);
|
|
50
|
+
const { invokePromise } = useOperationInvoker();
|
|
51
|
+
const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
|
|
52
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
53
|
+
|
|
54
|
+
const handleSelect = useCallback(
|
|
55
|
+
() => invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: node.id }),
|
|
56
|
+
[invokePromise, node.id],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useLoadDescendents(node.id);
|
|
60
|
+
|
|
61
|
+
const name = toLocalizedString(node.properties.label, t);
|
|
62
|
+
const isSelected = selectedValue === node.id;
|
|
63
|
+
|
|
64
|
+
// Register this workspace with the search context.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (ref.current) {
|
|
67
|
+
registerItem(node.id, ref.current, handleSelect);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return () => unregisterItem(node.id);
|
|
71
|
+
}, [node.id, handleSelect, registerItem, unregisterItem]);
|
|
72
|
+
|
|
73
|
+
// Scroll into view when selected.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (isSelected && ref.current) {
|
|
76
|
+
ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
77
|
+
}
|
|
78
|
+
}, [isSelected]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Card.Root
|
|
82
|
+
ref={ref}
|
|
83
|
+
role='button'
|
|
84
|
+
tabIndex={-1}
|
|
85
|
+
data-selected={isSelected}
|
|
86
|
+
classNames={mx('dx-focus-ring', isSelected && 'bg-hoverOverlay')}
|
|
87
|
+
onClick={handleSelect}
|
|
88
|
+
>
|
|
89
|
+
<Card.Chrome classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-cardSpacingInline pie-cardSpacingInline'>
|
|
90
|
+
<Avatar.Root>
|
|
91
|
+
<Avatar.Content
|
|
92
|
+
hue={node.properties.hue}
|
|
93
|
+
icon={node.properties.icon}
|
|
94
|
+
hueVariant='surface'
|
|
95
|
+
variant='square'
|
|
96
|
+
size={12}
|
|
97
|
+
fallback={name}
|
|
98
|
+
/>
|
|
99
|
+
<Avatar.Label>{name}</Avatar.Label>
|
|
100
|
+
<Icon icon='ph--caret-right--regular' />
|
|
101
|
+
</Avatar.Root>
|
|
102
|
+
</Card.Chrome>
|
|
103
|
+
</Card.Root>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const useLoadDescendents = (nodeId?: string) => {
|
|
108
|
+
const { graph } = useAppGraph();
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const frame = requestAnimationFrame(() => {
|
|
112
|
+
if (nodeId) {
|
|
113
|
+
Graph.expand(graph, nodeId, 'outbound');
|
|
114
|
+
Graph.getConnections(graph, nodeId, 'outbound').forEach((child) => {
|
|
115
|
+
Graph.expand(graph, child.id, 'outbound');
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return () => cancelAnimationFrame(frame);
|
|
121
|
+
}, [nodeId, graph]);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const useWorkspaces = () => {
|
|
125
|
+
const { graph } = useAppGraph();
|
|
126
|
+
|
|
127
|
+
// Get root connections to find collections.
|
|
128
|
+
const rootConnections = useConnections(graph, Node.RootId);
|
|
129
|
+
const collections = useMemo(
|
|
130
|
+
() => rootConnections.filter((node) => node.properties.disposition === 'collection'),
|
|
131
|
+
[rootConnections],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Get first collection's children as workspaces.
|
|
135
|
+
// TODO(wittjosiah): Support multiple collections or nested workspaces if needed.
|
|
136
|
+
const firstCollection = collections[0];
|
|
137
|
+
return useConnections(graph, firstCollection?.id);
|
|
138
|
+
};
|