@dxos/plugin-simple-layout 0.8.4-main.9735255 → 0.8.4-main.c85a9c8dae
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-P77G4YTR.mjs → chunk-7VLT3S46.mjs} +3 -3
- package/dist/lib/browser/chunk-7VLT3S46.mjs.map +7 -0
- package/dist/lib/browser/chunk-TMZNLVT2.mjs +1170 -0
- package/dist/lib/browser/chunk-TMZNLVT2.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +20 -19
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{operation-resolver-775UYAC2.mjs → operation-resolver-BYRIQOQT.mjs} +30 -28
- package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs.map +7 -0
- package/dist/lib/browser/{react-root-KM55OMGJ.mjs → react-root-MMB575WY.mjs} +5 -5
- package/dist/lib/browser/react-root-MMB575WY.mjs.map +7 -0
- package/dist/lib/browser/{react-surface-BABGAWGY.mjs → react-surface-M6CURANW.mjs} +10 -8
- package/dist/lib/browser/react-surface-M6CURANW.mjs.map +7 -0
- package/dist/lib/browser/{spotlight-dismiss-VSNOPETH.mjs → spotlight-dismiss-67PHYS5B.mjs} +3 -3
- package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +7 -0
- package/dist/lib/browser/{state-OUFTC2KV.mjs → state-A3PGDWWZ.mjs} +5 -4
- package/dist/lib/browser/state-A3PGDWWZ.mjs.map +7 -0
- package/dist/lib/browser/url-handler-HTIUY6WL.mjs +152 -0
- package/dist/lib/browser/url-handler-HTIUY6WL.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-FLOYBAHE.mjs +1171 -0
- package/dist/lib/node-esm/chunk-FLOYBAHE.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-F5TEKVJG.mjs → chunk-VIDE5UMB.mjs} +3 -3
- package/dist/lib/node-esm/chunk-VIDE5UMB.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +20 -19
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{operation-resolver-LDNYS3DI.mjs → operation-resolver-BDTFNCS2.mjs} +30 -28
- package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs.map +7 -0
- package/dist/lib/node-esm/{react-root-36UYFEEB.mjs → react-root-ENZKVSY4.mjs} +5 -5
- package/dist/lib/node-esm/react-root-ENZKVSY4.mjs.map +7 -0
- package/dist/lib/node-esm/{react-surface-CGHFVWU3.mjs → react-surface-ITVNQYLG.mjs} +10 -8
- package/dist/lib/node-esm/react-surface-ITVNQYLG.mjs.map +7 -0
- package/dist/lib/node-esm/{spotlight-dismiss-L5PCWIJG.mjs → spotlight-dismiss-RMLRZUVY.mjs} +3 -3
- package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +7 -0
- package/dist/lib/node-esm/{state-Q2ZA26W5.mjs → state-ZCFZTTPL.mjs} +5 -4
- package/dist/lib/node-esm/state-ZCFZTTPL.mjs.map +7 -0
- package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs +153 -0
- package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs.map +7 -0
- package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +2 -2
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-root/react-root.d.ts +1 -1
- package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface/index.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts +2 -2
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -1
- package/dist/types/src/capabilities/state/index.d.ts +1 -1
- package/dist/types/src/capabilities/state/state.d.ts +1 -1
- package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts +3 -1
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -1
- package/dist/types/src/components/ContentError.stories.d.ts +1 -3
- package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
- package/dist/types/src/components/Home/Home.d.ts.map +1 -1
- package/dist/types/src/components/MobileLayout/MobileLayout.d.ts +35 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts +7 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/MobileLayout/index.d.ts +2 -0
- package/dist/types/src/components/MobileLayout/index.d.ts.map +1 -0
- package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/AppBar.d.ts +26 -0
- package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +47 -0
- package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts +1 -1
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts +10 -3
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +4 -4
- 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.map +1 -1
- package/dist/types/src/components/SimpleLayout/index.d.ts +3 -0
- package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Workspace/Workspace.d.ts +3 -1
- package/dist/types/src/components/Workspace/Workspace.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/actions.d.ts +20 -0
- package/dist/types/src/hooks/actions.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAppBarProps.d.ts +7 -0
- package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -0
- package/dist/types/src/hooks/useCompanions.d.ts +5 -1
- package/dist/types/src/hooks/useCompanions.d.ts.map +1 -1
- package/dist/types/src/hooks/useDrawerActions.d.ts +13 -0
- package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useNavbarActions.d.ts +14 -0
- package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useSimpleLayoutState.d.ts +3 -3
- package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -1
- package/dist/types/src/types/capabilities.d.ts +7 -6
- package/dist/types/src/types/capabilities.d.ts.map +1 -1
- package/dist/types/src/types/events.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -30
- package/src/SimpleLayoutPlugin.ts +10 -9
- package/src/capabilities/operation-resolver/operation-resolver.ts +24 -22
- package/src/capabilities/react-root/react-root.tsx +2 -2
- package/src/capabilities/react-surface/react-surface.tsx +6 -5
- package/src/capabilities/spotlight-dismiss/spotlight-dismiss.ts +2 -2
- package/src/capabilities/state/state.tsx +4 -3
- package/src/capabilities/url-handler/url-handler.ts +111 -34
- package/src/components/ContentError.stories.tsx +8 -7
- package/src/components/ContentLoading.stories.tsx +2 -2
- package/src/components/ContentLoading.tsx +1 -1
- package/src/components/Dialog/Dialog.tsx +5 -5
- package/src/components/Home/Home.tsx +41 -33
- package/src/components/MobileLayout/MobileLayout.stories.tsx +129 -0
- package/src/components/MobileLayout/MobileLayout.tsx +305 -0
- package/src/components/MobileLayout/index.ts +5 -0
- package/src/components/Popover/Popover.tsx +17 -7
- package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
- package/src/components/SimpleLayout/AppBar.tsx +94 -0
- package/src/components/SimpleLayout/Drawer.tsx +22 -68
- package/src/components/SimpleLayout/Main.tsx +40 -29
- package/src/components/SimpleLayout/NavBar.stories.tsx +131 -23
- package/src/components/SimpleLayout/NavBar.tsx +15 -47
- package/src/components/SimpleLayout/SimpleLayout.stories.tsx +20 -11
- package/src/components/SimpleLayout/SimpleLayout.tsx +38 -19
- package/src/components/SimpleLayout/index.ts +3 -0
- package/src/components/Workspace/Workspace.tsx +34 -24
- package/src/components/hooks.ts +4 -4
- package/src/components/index.ts +1 -0
- package/src/hooks/actions.ts +85 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAppBarProps.ts +116 -0
- package/src/hooks/useCompanions.ts +8 -5
- package/src/hooks/useDrawerActions.ts +98 -0
- package/src/hooks/useNavbarActions.ts +86 -0
- package/src/hooks/useSimpleLayoutState.ts +5 -5
- package/src/types/capabilities.ts +10 -6
- package/src/types/events.ts +3 -2
- package/dist/lib/browser/chunk-LR3EE3VB.mjs +0 -789
- package/dist/lib/browser/chunk-LR3EE3VB.mjs.map +0 -7
- package/dist/lib/browser/chunk-P77G4YTR.mjs.map +0 -7
- package/dist/lib/browser/operation-resolver-775UYAC2.mjs.map +0 -7
- package/dist/lib/browser/react-root-KM55OMGJ.mjs.map +0 -7
- package/dist/lib/browser/react-surface-BABGAWGY.mjs.map +0 -7
- package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +0 -7
- package/dist/lib/browser/state-OUFTC2KV.mjs.map +0 -7
- package/dist/lib/browser/url-handler-DOUFQIAC.mjs +0 -54
- package/dist/lib/browser/url-handler-DOUFQIAC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-F5TEKVJG.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HB2B3LLG.mjs +0 -790
- package/dist/lib/node-esm/chunk-HB2B3LLG.mjs.map +0 -7
- package/dist/lib/node-esm/operation-resolver-LDNYS3DI.mjs.map +0 -7
- package/dist/lib/node-esm/react-root-36UYFEEB.mjs.map +0 -7
- package/dist/lib/node-esm/react-surface-CGHFVWU3.mjs.map +0 -7
- package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +0 -7
- package/dist/lib/node-esm/state-Q2ZA26W5.mjs.map +0 -7
- package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs +0 -55
- package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs.map +0 -7
- package/dist/types/src/components/ContentError.d.ts +0 -5
- package/dist/types/src/components/ContentError.d.ts.map +0 -1
- package/dist/types/src/components/SimpleLayout/Banner.d.ts +0 -8
- package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +0 -1
- package/src/components/ContentError.tsx +0 -23
- package/src/components/SimpleLayout/Banner.tsx +0 -113
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
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';
|
|
9
|
+
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { type ActionGraphProps, createMenuAction } from '@dxos/react-ui-menu';
|
|
12
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
13
|
+
|
|
14
|
+
import { translations } from '../../translations';
|
|
15
|
+
import { MobileLayout } from '../MobileLayout';
|
|
16
|
+
|
|
17
|
+
import { AppBar, type AppBarProps } from './AppBar';
|
|
18
|
+
|
|
19
|
+
const buildEmptyActions = (): ActionGraphProps => ({ nodes: [], edges: [] });
|
|
20
|
+
|
|
21
|
+
const buildDefaultActions = (): ActionGraphProps => {
|
|
22
|
+
const result: ActionGraphProps = { nodes: [], edges: [] };
|
|
23
|
+
const actions = [
|
|
24
|
+
createMenuAction('action-edit', () => console.log('Edit'), {
|
|
25
|
+
icon: 'ph--pencil--regular',
|
|
26
|
+
label: 'Edit',
|
|
27
|
+
}),
|
|
28
|
+
createMenuAction('action-share', () => console.log('Share'), {
|
|
29
|
+
icon: 'ph--share--regular',
|
|
30
|
+
label: 'Share',
|
|
31
|
+
}),
|
|
32
|
+
createMenuAction('action-delete', () => console.log('Delete'), {
|
|
33
|
+
icon: 'ph--trash--regular',
|
|
34
|
+
label: 'Delete',
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
result.nodes.push(...actions);
|
|
38
|
+
result.edges.push(...actions.map((a) => ({ source: 'root', target: a.id, relation: 'child' })));
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type StoryProps = Omit<AppBarProps, 'actions'> & {
|
|
43
|
+
actions: ActionGraphProps;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DefaultStory = ({ actions: actionsProp, ...props }: StoryProps) => {
|
|
47
|
+
const actions = useMemo(() => Atom.make(actionsProp).pipe(Atom.keepAlive), [actionsProp]);
|
|
48
|
+
return (
|
|
49
|
+
<MobileLayout.Root>
|
|
50
|
+
<AppBar {...props} actions={actions} />
|
|
51
|
+
</MobileLayout.Root>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const meta = {
|
|
56
|
+
title: 'plugins/plugin-simple-layout/components/AppBar',
|
|
57
|
+
render: DefaultStory,
|
|
58
|
+
decorators: [
|
|
59
|
+
withTheme(),
|
|
60
|
+
withLayout({
|
|
61
|
+
layout: 'column',
|
|
62
|
+
classNames: 'relative',
|
|
63
|
+
}),
|
|
64
|
+
withRegistry,
|
|
65
|
+
],
|
|
66
|
+
parameters: {
|
|
67
|
+
layout: 'fullscreen',
|
|
68
|
+
translations,
|
|
69
|
+
},
|
|
70
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
71
|
+
|
|
72
|
+
export default meta;
|
|
73
|
+
|
|
74
|
+
type Story = StoryObj<StoryProps>;
|
|
75
|
+
|
|
76
|
+
export const Default: Story = {
|
|
77
|
+
tags: ['test'],
|
|
78
|
+
args: {
|
|
79
|
+
actions: buildDefaultActions(),
|
|
80
|
+
title: 'Document Title',
|
|
81
|
+
showBackButton: true,
|
|
82
|
+
onAction: fn(),
|
|
83
|
+
onBack: fn(),
|
|
84
|
+
},
|
|
85
|
+
play: async ({ args, canvasElement }) => {
|
|
86
|
+
const canvas = within(canvasElement);
|
|
87
|
+
|
|
88
|
+
// Verify the banner renders with the correct title.
|
|
89
|
+
await expect(canvas.getByRole('banner')).toBeInTheDocument();
|
|
90
|
+
await expect(canvas.getByText('Document Title')).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
// Test back button click.
|
|
93
|
+
const backButton = canvas.getByRole('button', { name: /back/i });
|
|
94
|
+
await expect(backButton).toBeInTheDocument();
|
|
95
|
+
await userEvent.click(backButton);
|
|
96
|
+
await expect(args.onBack).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
// Test actions menu opens and action fires.
|
|
99
|
+
const menuTrigger = canvas.getByRole('button', { name: /actions/i });
|
|
100
|
+
await expect(menuTrigger).toBeInTheDocument();
|
|
101
|
+
await userEvent.click(menuTrigger);
|
|
102
|
+
|
|
103
|
+
// Wait for menu to open and click an action (menu items render in a portal).
|
|
104
|
+
const editAction = await screen.findByRole('menuitem', { name: /edit/i });
|
|
105
|
+
await userEvent.click(editAction);
|
|
106
|
+
await expect(args.onAction).toHaveBeenCalledTimes(1);
|
|
107
|
+
await expect((args.onAction as Mock).mock.calls[0][0]).toHaveProperty('id', 'action-edit');
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const NoBackButton: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
actions: buildDefaultActions(),
|
|
114
|
+
title: 'Home',
|
|
115
|
+
showBackButton: false,
|
|
116
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const LongTitle: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
actions: buildDefaultActions(),
|
|
123
|
+
title: 'This is a very long document title that should be truncated when it exceeds the available space',
|
|
124
|
+
showBackButton: true,
|
|
125
|
+
onBack: () => console.log('Back clicked'),
|
|
126
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const NoActions: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
actions: buildEmptyActions(),
|
|
133
|
+
title: 'Empty Document',
|
|
134
|
+
showBackButton: true,
|
|
135
|
+
onBack: () => console.log('Back clicked'),
|
|
136
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const Empty: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
actions: buildEmptyActions(),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Atom, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { Fragment } from 'react';
|
|
7
|
+
|
|
8
|
+
import { IconButton, Popover, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { type ActionExecutor, type ActionGraphProps, Menu, useMenuActions } from '@dxos/react-ui-menu';
|
|
10
|
+
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
11
|
+
|
|
12
|
+
import { meta } from '../../meta';
|
|
13
|
+
import { useMobileLayout } from '../MobileLayout';
|
|
14
|
+
|
|
15
|
+
const APP_BAR_NAME = 'SimpleLayout.AppBar';
|
|
16
|
+
|
|
17
|
+
export type AppBarProps = ThemedClassName<{
|
|
18
|
+
/** Title/label to display in the banner. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Action graph atom for the dropdown menu. */
|
|
21
|
+
actions: Atom.Atom<ActionGraphProps>;
|
|
22
|
+
/** Whether to show the back button. */
|
|
23
|
+
showBackButton?: boolean;
|
|
24
|
+
/** Popover anchor ID for the dropdown trigger. */
|
|
25
|
+
popoverAnchorId?: string;
|
|
26
|
+
/** Action executor callback. */
|
|
27
|
+
onAction?: ActionExecutor;
|
|
28
|
+
/** Callback when back button is clicked. */
|
|
29
|
+
onBack?: () => void;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* AppBar component that renders a title, optional back button, and actions dropdown.
|
|
34
|
+
*/
|
|
35
|
+
export const AppBar = ({
|
|
36
|
+
classNames,
|
|
37
|
+
title,
|
|
38
|
+
actions,
|
|
39
|
+
showBackButton,
|
|
40
|
+
popoverAnchorId,
|
|
41
|
+
onAction,
|
|
42
|
+
onBack,
|
|
43
|
+
}: AppBarProps) => {
|
|
44
|
+
const { t } = useTranslation(meta.id);
|
|
45
|
+
const menu = useMenuActions(actions);
|
|
46
|
+
const actionsValue = useAtomValue(actions);
|
|
47
|
+
const hasActions = actionsValue.nodes.length > 0;
|
|
48
|
+
const { keyboardOpen } = useMobileLayout(APP_BAR_NAME);
|
|
49
|
+
|
|
50
|
+
// Fall back to app name if no title provided.
|
|
51
|
+
const displayTitle = title ?? t('current app name', { ns: osTranslations });
|
|
52
|
+
|
|
53
|
+
// Wrap the menu trigger with Popover.Anchor when the popoverAnchorId is set.
|
|
54
|
+
const AnchorRoot = popoverAnchorId ? Popover.Anchor : Fragment;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Toolbar.Root
|
|
58
|
+
role='banner'
|
|
59
|
+
classNames={mx(
|
|
60
|
+
'grid grid-cols-[var(--dx-rail-size)_1fr_var(--dx-rail-size)] items-center',
|
|
61
|
+
'dx-density-fine',
|
|
62
|
+
classNames,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{keyboardOpen ? (
|
|
66
|
+
<IconButton variant='ghost' icon='ph--x--regular' iconOnly label={t('done label')} />
|
|
67
|
+
) : showBackButton ? (
|
|
68
|
+
<IconButton variant='ghost' icon='ph--caret-left--regular' iconOnly label={t('back label')} onClick={onBack} />
|
|
69
|
+
) : (
|
|
70
|
+
<div />
|
|
71
|
+
)}
|
|
72
|
+
<h1 className='text-center truncate font-thin uppercase'>{displayTitle}</h1>
|
|
73
|
+
{hasActions ? (
|
|
74
|
+
<AnchorRoot>
|
|
75
|
+
<Menu.Root {...menu} caller={meta.id} onAction={onAction}>
|
|
76
|
+
<Menu.Trigger asChild>
|
|
77
|
+
<IconButton
|
|
78
|
+
variant='ghost'
|
|
79
|
+
icon='ph--dots-three-vertical--regular'
|
|
80
|
+
iconOnly
|
|
81
|
+
label={t('actions menu label')}
|
|
82
|
+
/>
|
|
83
|
+
</Menu.Trigger>
|
|
84
|
+
<Menu.Content />
|
|
85
|
+
</Menu.Root>
|
|
86
|
+
</AnchorRoot>
|
|
87
|
+
) : (
|
|
88
|
+
<span />
|
|
89
|
+
)}
|
|
90
|
+
</Toolbar.Root>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
AppBar.displayName = APP_BAR_NAME;
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Surface
|
|
7
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
8
|
+
import { useAppGraph } from '@dxos/app-toolkit/ui';
|
|
8
9
|
import { type Node, useNode } from '@dxos/plugin-graph';
|
|
9
|
-
import {
|
|
10
|
+
import { ErrorFallback, Panel } from '@dxos/react-ui';
|
|
10
11
|
import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
|
|
12
|
+
import { Menu, useMenuActions } from '@dxos/react-ui-menu';
|
|
11
13
|
|
|
12
|
-
import { useCompanions, useSimpleLayoutState } from '../../hooks';
|
|
13
|
-
import { meta } from '../../meta';
|
|
14
|
-
import { ContentError } from '../ContentError';
|
|
14
|
+
import { useCompanions, useDrawerActions, useSimpleLayoutState } from '../../hooks';
|
|
15
15
|
import { ContentLoading } from '../ContentLoading';
|
|
16
16
|
|
|
17
17
|
const DRAWER_NAME = 'SimpleLayout.Drawer';
|
|
@@ -20,16 +20,15 @@ const DRAWER_NAME = 'SimpleLayout.Drawer';
|
|
|
20
20
|
* Companion drawer component.
|
|
21
21
|
*/
|
|
22
22
|
export const Drawer = () => {
|
|
23
|
-
const { t } = useTranslation(meta.id);
|
|
24
|
-
const { state, updateState } = useSimpleLayoutState();
|
|
25
23
|
const { graph } = useAppGraph();
|
|
24
|
+
const { state: layoutState } = useSimpleLayoutState();
|
|
26
25
|
|
|
27
26
|
const placeholder = useMemo(() => <ContentLoading />, []);
|
|
28
27
|
|
|
29
28
|
// Get all companions for the current active (primary) item.
|
|
30
|
-
const activeId =
|
|
29
|
+
const activeId = layoutState.active ?? layoutState.workspace;
|
|
31
30
|
const companions = useCompanions(activeId);
|
|
32
|
-
const { companionId, variant } = useSelectedCompanion(companions,
|
|
31
|
+
const { companionId, variant } = useSelectedCompanion(companions, layoutState.companionVariant);
|
|
33
32
|
|
|
34
33
|
// Get node for the selected companion.
|
|
35
34
|
const node = useNode(graph, companionId);
|
|
@@ -48,66 +47,21 @@ export const Drawer = () => {
|
|
|
48
47
|
);
|
|
49
48
|
}, [companionId, node, parentNode, variant]);
|
|
50
49
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
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';
|
|
50
|
+
// Get drawer actions (tabs + toolbar buttons).
|
|
51
|
+
const { actions, onAction } = useDrawerActions(DRAWER_NAME);
|
|
52
|
+
const menu = useMenuActions(actions);
|
|
79
53
|
|
|
80
54
|
return (
|
|
81
|
-
<
|
|
82
|
-
<Toolbar
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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>
|
|
55
|
+
<Panel.Root>
|
|
56
|
+
<Panel.Toolbar>
|
|
57
|
+
<Menu.Root {...menu} alwaysActive onAction={onAction}>
|
|
58
|
+
<Menu.Toolbar density='coarse' />
|
|
59
|
+
</Menu.Root>
|
|
60
|
+
</Panel.Toolbar>
|
|
61
|
+
<Panel.Content asChild>
|
|
62
|
+
<Surface.Surface role='article' data={data} limit={1} fallback={ErrorFallback} placeholder={placeholder} />
|
|
63
|
+
</Panel.Content>
|
|
64
|
+
</Panel.Root>
|
|
111
65
|
);
|
|
112
66
|
};
|
|
113
67
|
|
|
@@ -4,17 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Surface
|
|
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 {
|
|
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
16
|
import { useLoadDescendents } from '../hooks';
|
|
17
|
+
import { useMobileLayout } from '../MobileLayout/MobileLayout';
|
|
16
18
|
|
|
17
|
-
import {
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,36 @@ export const Main = () => {
|
|
|
44
47
|
);
|
|
45
48
|
}, [id, node, node?.data, node?.properties, state.popoverAnchorId]);
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
// Ensures that children are loaded so that they are available to navigate to.
|
|
51
|
+
useLoadDescendents(id);
|
|
52
|
+
|
|
53
|
+
// TODO(burdon): BUG: When showing ANY statusbar the size progressively shrinks when the keyboard opens/closes.
|
|
54
|
+
const showNavBar = !keyboardOpen && !state.isPopover && state.drawerState === 'closed';
|
|
49
55
|
|
|
50
56
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
<div
|
|
58
|
+
role='none'
|
|
59
|
+
className={mx(
|
|
60
|
+
'h-full grid overflow-hidden bg-toolbar-surface',
|
|
61
|
+
showNavBar
|
|
62
|
+
? 'grid-rows-[var(--dx-rail-action)_1fr_var(--dx-toolbar-size)]'
|
|
63
|
+
: 'grid-rows-[var(--dx-rail-action)_1fr]',
|
|
64
|
+
)}
|
|
65
|
+
{...attentionAttrs}
|
|
54
66
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
</NaturalMain.Content>
|
|
67
|
+
<AppBar {...appBarProps} />
|
|
68
|
+
<article className='h-full overflow-hidden bg-base-surface'>
|
|
69
|
+
<Surface.Surface
|
|
70
|
+
key={id}
|
|
71
|
+
role='article'
|
|
72
|
+
data={data}
|
|
73
|
+
limit={1}
|
|
74
|
+
fallback={ErrorFallback}
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
/>
|
|
77
|
+
</article>
|
|
78
|
+
{showNavBar && <NavBar classNames='border-y border-subdued-separator' actions={actions} onAction={onAction} />}
|
|
79
|
+
</div>
|
|
69
80
|
);
|
|
70
81
|
};
|
|
71
82
|
|
|
@@ -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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
47
|
-
args: {
|
|
48
|
-
|
|
49
|
-
},
|
|
150
|
+
export const CompanionsOnly: Story = {
|
|
151
|
+
args: {} as any,
|
|
152
|
+
render: () => <CompanionsOnlyStory />,
|
|
50
153
|
};
|
|
51
154
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
};
|