@dxos/plugin-deck 0.6.12-staging.e11e696 → 0.6.12
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/index.mjs +103 -103
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/DeckPlugin.d.ts.map +1 -1
- package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +2 -2
- package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
- package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +3 -2
- package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
- package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +3 -2
- package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
- package/dist/types/src/components/DeckLayout/Sidebar.d.ts +3 -2
- package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
- package/dist/types/src/hooks/useNode.d.ts.map +1 -1
- package/dist/types/src/layout.d.ts.map +1 -1
- package/package.json +29 -29
- package/src/DeckPlugin.tsx +51 -79
- package/src/components/DeckLayout/ComplementarySidebar.tsx +19 -20
- package/src/components/DeckLayout/DeckLayout.tsx +49 -38
- package/src/components/DeckLayout/NodePlankHeading.tsx +14 -12
- package/src/components/DeckLayout/Plank.tsx +2 -2
- package/src/components/DeckLayout/Sidebar.tsx +5 -6
- package/src/hooks/useNode.ts +1 -5
- package/src/layout.ts +0 -1
package/src/DeckPlugin.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { ArrowsOut, type IconProps } from '@phosphor-icons/react';
|
|
5
6
|
import { batch, effect } from '@preact/signals-core';
|
|
6
7
|
import { setAutoFreeze } from 'immer';
|
|
7
8
|
import React, { type PropsWithChildren } from 'react';
|
|
@@ -40,6 +41,7 @@ import { createExtension, type Node } from '@dxos/plugin-graph';
|
|
|
40
41
|
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
41
42
|
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
42
43
|
import { translations as deckTranslations } from '@dxos/react-ui-deck';
|
|
44
|
+
import { Mosaic } from '@dxos/react-ui-mosaic';
|
|
43
45
|
|
|
44
46
|
import {
|
|
45
47
|
DeckLayout,
|
|
@@ -69,7 +71,7 @@ const isSocket = !!(globalThis as any).__args;
|
|
|
69
71
|
// TODO(mjamesderocher): Can we get this directly from Socket?
|
|
70
72
|
const appScheme = 'composer://';
|
|
71
73
|
|
|
72
|
-
// TODO(burdon): Evolve into customizable prefs
|
|
74
|
+
// TODO(burdon): Evolve into customizable prefs,.
|
|
73
75
|
const customSlots: DeckLayoutProps['slots'] = {
|
|
74
76
|
wallpaper: {
|
|
75
77
|
classNames:
|
|
@@ -188,25 +190,6 @@ export const DeckPlugin = ({
|
|
|
188
190
|
}
|
|
189
191
|
};
|
|
190
192
|
|
|
191
|
-
/**
|
|
192
|
-
* Update the active state and ensure that attention is on an active element.
|
|
193
|
-
*/
|
|
194
|
-
const handleSetLocation = (next: LayoutParts) => {
|
|
195
|
-
if (attentionPlugin) {
|
|
196
|
-
const attended = attentionPlugin.provides.attention.attended;
|
|
197
|
-
const [attendedId] = Array.from(attended);
|
|
198
|
-
const ids = (layout.values.layoutMode === 'deck' ? next.main : next.solo)?.map(({ id }) => id) ?? [];
|
|
199
|
-
const isAttendedAvailable = !!attendedId && ids.includes(attendedId);
|
|
200
|
-
if (!isAttendedAvailable) {
|
|
201
|
-
const nextAttended = next.main?.[0]?.id;
|
|
202
|
-
const article = document.querySelector<HTMLElement>(`article[data-attendable-id="${nextAttended}"]`);
|
|
203
|
-
article?.focus();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
location.values.active = next;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
193
|
return {
|
|
211
194
|
meta,
|
|
212
195
|
ready: async (plugins) => {
|
|
@@ -215,21 +198,16 @@ export const DeckPlugin = ({
|
|
|
215
198
|
attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
|
|
216
199
|
clientPlugin = resolvePlugin(plugins, parseClientPlugin);
|
|
217
200
|
|
|
201
|
+
// prettier-ignore
|
|
218
202
|
layout
|
|
219
203
|
.prop({ key: 'layoutMode', storageKey: 'layout-mode', type: LocalStorageStore.enum<LayoutMode>() })
|
|
220
204
|
.prop({ key: 'sidebarOpen', storageKey: 'sidebar-open', type: LocalStorageStore.bool() })
|
|
221
|
-
.prop({
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
deck.prop({
|
|
228
|
-
key: 'plankSizing',
|
|
229
|
-
storageKey: 'plank-sizing',
|
|
230
|
-
type: LocalStorageStore.json<Record<string, number>>(),
|
|
231
|
-
});
|
|
205
|
+
.prop({ key: 'complementarySidebarOpen', storageKey: 'complementary-sidebar-open', type: LocalStorageStore.bool() });
|
|
206
|
+
|
|
207
|
+
// prettier-ignore
|
|
208
|
+
deck.prop({ key: 'plankSizing', storageKey: 'plank-sizing', type: LocalStorageStore.json<Record<string, number>>() });
|
|
232
209
|
|
|
210
|
+
// prettier-ignore
|
|
233
211
|
location
|
|
234
212
|
.prop({ key: 'active', storageKey: 'active', type: LocalStorageStore.json<LayoutParts>() })
|
|
235
213
|
.prop({ key: 'closed', storageKey: 'closed', type: LocalStorageStore.json<string[]>() });
|
|
@@ -242,17 +220,14 @@ export const DeckPlugin = ({
|
|
|
242
220
|
}),
|
|
243
221
|
);
|
|
244
222
|
|
|
223
|
+
// prettier-ignore
|
|
245
224
|
settings
|
|
246
225
|
.prop({ key: 'showFooter', storageKey: 'show-footer', type: LocalStorageStore.bool() })
|
|
247
226
|
.prop({ key: 'customSlots', storageKey: 'customSlots', type: LocalStorageStore.bool() })
|
|
248
227
|
.prop({ key: 'flatDeck', storageKey: 'flatDeck', type: LocalStorageStore.bool() })
|
|
249
228
|
.prop({ key: 'enableNativeRedirect', storageKey: 'enable-native-redirect', type: LocalStorageStore.bool() })
|
|
250
229
|
.prop({ key: 'disableDeck', storageKey: 'disable-deck', type: LocalStorageStore.bool() }) // Deprecated.
|
|
251
|
-
.prop({
|
|
252
|
-
key: 'newPlankPositioning',
|
|
253
|
-
storageKey: 'newPlankPositioning',
|
|
254
|
-
type: LocalStorageStore.enum<NewPlankPositioning>(),
|
|
255
|
-
})
|
|
230
|
+
.prop({ key: 'newPlankPositioning', storageKey: 'newPlankPositioning', type: LocalStorageStore.enum<NewPlankPositioning>() })
|
|
256
231
|
.prop({ key: 'overscroll', storageKey: 'overscroll', type: LocalStorageStore.enum<Overscroll>() });
|
|
257
232
|
|
|
258
233
|
if (!isSocket && settings.values.enableNativeRedirect) {
|
|
@@ -262,7 +237,7 @@ export const DeckPlugin = ({
|
|
|
262
237
|
handleNavigation = async () => {
|
|
263
238
|
const pathname = window.location.pathname;
|
|
264
239
|
if (pathname === '/reset') {
|
|
265
|
-
|
|
240
|
+
location.values.active = { sidebar: [{ id: NAV_ID }] };
|
|
266
241
|
location.values.closed = [];
|
|
267
242
|
layout.values.layoutMode = 'solo';
|
|
268
243
|
window.location.pathname = '/';
|
|
@@ -275,7 +250,7 @@ export const DeckPlugin = ({
|
|
|
275
250
|
}
|
|
276
251
|
|
|
277
252
|
const startingLayout = removePart(location.values.active, 'solo');
|
|
278
|
-
|
|
253
|
+
location.values.active = mergeLayoutParts(layoutFromUri, startingLayout);
|
|
279
254
|
layout.values.layoutMode = 'solo';
|
|
280
255
|
};
|
|
281
256
|
|
|
@@ -335,7 +310,8 @@ export const DeckPlugin = ({
|
|
|
335
310
|
},
|
|
336
311
|
properties: {
|
|
337
312
|
label: ['toggle fullscreen label', { ns: DECK_PLUGIN }],
|
|
338
|
-
icon:
|
|
313
|
+
icon: (props: IconProps) => <ArrowsOut {...props} />,
|
|
314
|
+
iconSymbol: 'ph--arrows-out--regular',
|
|
339
315
|
keyBinding: {
|
|
340
316
|
macos: 'ctrl+meta+f',
|
|
341
317
|
windows: 'shift+ctrl+f',
|
|
@@ -353,27 +329,31 @@ export const DeckPlugin = ({
|
|
|
353
329
|
),
|
|
354
330
|
root: () => {
|
|
355
331
|
return (
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
332
|
+
<Mosaic.Root>
|
|
333
|
+
<DeckLayout
|
|
334
|
+
attention={attentionPlugin?.provides.attention ?? { attended: new Set() }}
|
|
335
|
+
layoutParts={location.values.active}
|
|
336
|
+
showHintsFooter={settings.values.showFooter}
|
|
337
|
+
overscroll={settings.values.overscroll}
|
|
338
|
+
flatDeck={settings.values.flatDeck}
|
|
339
|
+
slots={settings.values.customSlots ? customSlots : undefined}
|
|
340
|
+
toasts={layout.values.toasts}
|
|
341
|
+
onDismissToast={(id) => {
|
|
342
|
+
const index = layout.values.toasts.findIndex((toast) => toast.id === id);
|
|
343
|
+
if (index !== -1) {
|
|
344
|
+
// Allow time for the toast to animate out.
|
|
345
|
+
// TODO(burdon): Factor out and unregister timeout.
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
if (layout.values.toasts[index].id === currentUndoId) {
|
|
348
|
+
currentUndoId = undefined;
|
|
349
|
+
}
|
|
350
|
+
layout.values.toasts.splice(index, 1);
|
|
351
|
+
}, 1_000);
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
<Mosaic.DragOverlay />
|
|
356
|
+
</Mosaic.Root>
|
|
377
357
|
);
|
|
378
358
|
},
|
|
379
359
|
surface: {
|
|
@@ -492,7 +472,7 @@ export const DeckPlugin = ({
|
|
|
492
472
|
}
|
|
493
473
|
});
|
|
494
474
|
|
|
495
|
-
|
|
475
|
+
location.values.active = newLayout;
|
|
496
476
|
});
|
|
497
477
|
|
|
498
478
|
const ids = openIds(location.values.active);
|
|
@@ -542,12 +522,10 @@ export const DeckPlugin = ({
|
|
|
542
522
|
const layoutEntry = { id: data.id };
|
|
543
523
|
const effectivePart = getEffectivePart(data.part, layout.values.layoutMode);
|
|
544
524
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}),
|
|
550
|
-
);
|
|
525
|
+
location.values.active = openEntry(location.values.active, effectivePart, layoutEntry, {
|
|
526
|
+
positioning: data.positioning ?? settings.values.newPlankPositioning,
|
|
527
|
+
pivotId: data.pivotId,
|
|
528
|
+
});
|
|
551
529
|
|
|
552
530
|
const intents = [];
|
|
553
531
|
if (data.scrollIntoView && layout.values.layoutMode === 'deck') {
|
|
@@ -583,11 +561,7 @@ export const DeckPlugin = ({
|
|
|
583
561
|
}
|
|
584
562
|
});
|
|
585
563
|
|
|
586
|
-
|
|
587
|
-
// TODO(wittjosiah): This needs to also set the closed state.
|
|
588
|
-
// The closed state should be the existing closed state plus the newly closed ids.
|
|
589
|
-
// The closed state should also be updated when opening entries to remove the id from closed.
|
|
590
|
-
// When SET is called the closed ids should also be calculated and set.
|
|
564
|
+
location.values.active = newLayout;
|
|
591
565
|
return { data: true };
|
|
592
566
|
});
|
|
593
567
|
}
|
|
@@ -596,7 +570,7 @@ export const DeckPlugin = ({
|
|
|
596
570
|
case NavigationAction.SET: {
|
|
597
571
|
return batch(() => {
|
|
598
572
|
if (isLayoutParts(intent.data?.activeParts)) {
|
|
599
|
-
|
|
573
|
+
location.values.active = intent.data!.activeParts;
|
|
600
574
|
}
|
|
601
575
|
return { data: true };
|
|
602
576
|
});
|
|
@@ -607,12 +581,10 @@ export const DeckPlugin = ({
|
|
|
607
581
|
if (isLayoutAdjustment(intent.data)) {
|
|
608
582
|
const adjustment = intent.data;
|
|
609
583
|
if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}),
|
|
615
|
-
);
|
|
584
|
+
location.values.active = incrementPlank(location.values.active, {
|
|
585
|
+
type: adjustment.type,
|
|
586
|
+
layoutCoordinate: adjustment.layoutCoordinate,
|
|
587
|
+
});
|
|
616
588
|
}
|
|
617
589
|
|
|
618
590
|
if (adjustment.type === 'solo') {
|
|
@@ -7,7 +7,7 @@ import React from 'react';
|
|
|
7
7
|
import { type LayoutParts, SLUG_PATH_SEPARATOR, Surface } from '@dxos/app-framework';
|
|
8
8
|
import { useGraph } from '@dxos/plugin-graph';
|
|
9
9
|
import { Main } from '@dxos/react-ui';
|
|
10
|
-
import {
|
|
10
|
+
import { createAttendableAttributes } from '@dxos/react-ui-attention';
|
|
11
11
|
import { deckGrid } from '@dxos/react-ui-deck';
|
|
12
12
|
import { mx } from '@dxos/react-ui-theme';
|
|
13
13
|
|
|
@@ -18,42 +18,41 @@ import { useNode, useNodeActionExpander } from '../../hooks';
|
|
|
18
18
|
import { useLayout } from '../LayoutContext';
|
|
19
19
|
|
|
20
20
|
export type ComplementarySidebarProps = {
|
|
21
|
-
|
|
21
|
+
id?: string;
|
|
22
22
|
layoutParts: LayoutParts;
|
|
23
23
|
flatDeck?: boolean;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export const ComplementarySidebar = ({
|
|
26
|
+
export const ComplementarySidebar = ({ id, layoutParts, flatDeck }: ComplementarySidebarProps) => {
|
|
27
27
|
const { popoverAnchorId } = useLayout();
|
|
28
|
-
const attended = useAttended();
|
|
29
|
-
const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${context}` : undefined;
|
|
30
28
|
const { graph } = useGraph();
|
|
31
29
|
const node = useNode(graph, id);
|
|
30
|
+
// const complementaryAvailable = useMemo(() => id === NAV_ID || !!node, [id, node]);
|
|
31
|
+
const complementaryAttrs = createAttendableAttributes(id?.split(SLUG_PATH_SEPARATOR)[0] ?? 'never');
|
|
32
32
|
|
|
33
33
|
useNodeActionExpander(node);
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
<Main.ComplementarySidebar>
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{node && (
|
|
36
|
+
<Main.ComplementarySidebar {...complementaryAttrs}>
|
|
37
|
+
{node ? (
|
|
38
|
+
<div role='none' className={mx(deckGrid, 'grid-cols-1 bs-full')}>
|
|
39
|
+
<NodePlankHeading
|
|
40
|
+
node={node}
|
|
41
|
+
id={id}
|
|
42
|
+
layoutParts={layoutParts}
|
|
43
|
+
layoutPart='complementary'
|
|
44
|
+
popoverAnchorId={popoverAnchorId}
|
|
45
|
+
flatDeck={flatDeck}
|
|
46
|
+
/>
|
|
48
47
|
<Surface
|
|
49
48
|
role='article'
|
|
50
|
-
data={{ subject: node.
|
|
49
|
+
data={{ subject: node.data, part: 'complementary', popoverAnchorId }}
|
|
51
50
|
limit={1}
|
|
52
51
|
fallback={PlankContentError}
|
|
53
52
|
placeholder={<PlankLoading />}
|
|
54
53
|
/>
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
</div>
|
|
55
|
+
) : null}
|
|
57
56
|
</Main.ComplementarySidebar>
|
|
58
57
|
);
|
|
59
58
|
};
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Sidebar as MenuIcon } from '@phosphor-icons/react';
|
|
6
|
-
import React, { useCallback, useEffect, useMemo, useRef, type UIEvent } from 'react';
|
|
6
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState, useLayoutEffect, type UIEvent } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
SLUG_PATH_SEPARATOR,
|
|
10
|
+
type Attention,
|
|
9
11
|
type LayoutEntry,
|
|
10
12
|
type LayoutParts,
|
|
11
13
|
Surface,
|
|
@@ -13,8 +15,7 @@ import {
|
|
|
13
15
|
firstIdInPart,
|
|
14
16
|
usePlugin,
|
|
15
17
|
} from '@dxos/app-framework';
|
|
16
|
-
import { Button, Dialog, Main, Popover,
|
|
17
|
-
import { useAttended } from '@dxos/react-ui-attention';
|
|
18
|
+
import { Button, Dialog, Main, Popover, useTranslation } from '@dxos/react-ui';
|
|
18
19
|
import { Deck } from '@dxos/react-ui-deck';
|
|
19
20
|
import { getSize } from '@dxos/react-ui-theme';
|
|
20
21
|
|
|
@@ -34,6 +35,7 @@ import { useLayout } from '../LayoutContext';
|
|
|
34
35
|
|
|
35
36
|
export type DeckLayoutProps = {
|
|
36
37
|
layoutParts: LayoutParts;
|
|
38
|
+
attention: Attention;
|
|
37
39
|
toasts: ToastSchema[];
|
|
38
40
|
flatDeck?: boolean;
|
|
39
41
|
overscroll: Overscroll;
|
|
@@ -46,6 +48,7 @@ export type DeckLayoutProps = {
|
|
|
46
48
|
|
|
47
49
|
export const DeckLayout = ({
|
|
48
50
|
layoutParts,
|
|
51
|
+
attention,
|
|
49
52
|
toasts,
|
|
50
53
|
flatDeck,
|
|
51
54
|
overscroll,
|
|
@@ -67,42 +70,36 @@ export const DeckLayout = ({
|
|
|
67
70
|
} = context;
|
|
68
71
|
const { t } = useTranslation(DECK_PLUGIN);
|
|
69
72
|
const { plankSizing } = useDeckContext();
|
|
70
|
-
const attended = useAttended();
|
|
71
73
|
const searchPlugin = usePlugin('dxos.org/plugin/search');
|
|
72
74
|
const fullScreenSlug = useMemo(() => firstIdInPart(layoutParts, 'fullScreen'), [layoutParts]);
|
|
73
75
|
|
|
74
|
-
const
|
|
75
|
-
const deckRef = useRef<HTMLDivElement>(null);
|
|
76
|
-
|
|
77
|
-
// Ensure the first plank is attended when the deck is first rendered.
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
const firstId = layoutMode === 'solo' ? firstIdInPart(layoutParts, 'solo') : firstIdInPart(layoutParts, 'main');
|
|
80
|
-
if (attended.length === 0 && firstId) {
|
|
81
|
-
// TODO(wittjosiah): Focusing the type button is a workaround.
|
|
82
|
-
// If the plank is directly focused on first load the focus ring appears.
|
|
83
|
-
document.querySelector<HTMLElement>(`article[data-attendable-id="${firstId}"] button`)?.focus();
|
|
84
|
-
}
|
|
85
|
-
}, []);
|
|
76
|
+
const [scrollLeft, setScrollLeft] = useState<number | null>(null);
|
|
77
|
+
const deckRef = useRef<HTMLDivElement | null>(null);
|
|
78
|
+
const restoreScrollRef = useRef<boolean>(false);
|
|
86
79
|
|
|
87
80
|
/**
|
|
88
81
|
* Clear scroll restoration state if the window is resized
|
|
89
82
|
*/
|
|
90
83
|
const handleResize = useCallback(() => {
|
|
91
|
-
|
|
84
|
+
setScrollLeft(null);
|
|
92
85
|
}, []);
|
|
93
|
-
|
|
94
86
|
useEffect(() => {
|
|
95
87
|
window.addEventListener('resize', handleResize);
|
|
96
88
|
return () => window.removeEventListener('resize', handleResize);
|
|
97
89
|
}, [handleResize]);
|
|
98
90
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Restore scroll when returning to deck mode
|
|
93
|
+
*/
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
if (layoutMode !== 'deck') {
|
|
96
|
+
restoreScrollRef.current = true;
|
|
97
|
+
} else if (restoreScrollRef.current && deckRef.current && scrollLeft) {
|
|
98
|
+
// console.log('[restoring scrollLeft]', scrollLeft);
|
|
99
|
+
deckRef.current.scrollLeft = scrollLeft;
|
|
100
|
+
restoreScrollRef.current = false;
|
|
102
101
|
}
|
|
103
|
-
}, []);
|
|
104
|
-
|
|
105
|
-
useOnTransition(layoutMode, (mode) => mode !== 'deck', 'deck', restoreScroll);
|
|
102
|
+
}, [layoutMode, deckRef.current, scrollLeft]);
|
|
106
103
|
|
|
107
104
|
/**
|
|
108
105
|
* Save scroll position as the user scrolls
|
|
@@ -110,13 +107,22 @@ export const DeckLayout = ({
|
|
|
110
107
|
const handleScroll = useCallback(
|
|
111
108
|
(event: UIEvent) => {
|
|
112
109
|
if (layoutMode === 'deck' && event.currentTarget === event.target) {
|
|
113
|
-
|
|
110
|
+
// console.log('[save scroll left]', (event.target as HTMLDivElement).scrollLeft);
|
|
111
|
+
setScrollLeft((event.target as HTMLDivElement).scrollLeft);
|
|
114
112
|
}
|
|
115
113
|
},
|
|
116
114
|
[layoutMode],
|
|
117
115
|
);
|
|
118
116
|
|
|
119
|
-
const
|
|
117
|
+
const complementarySlug = useMemo(() => {
|
|
118
|
+
const entry = layoutParts.complementary?.at(0);
|
|
119
|
+
if (entry) {
|
|
120
|
+
return entry.path ? `${entry.id}${SLUG_PATH_SEPARATOR}${entry.path}` : entry.id;
|
|
121
|
+
}
|
|
122
|
+
}, [layoutParts]);
|
|
123
|
+
|
|
124
|
+
const firstAttendedId = useMemo(() => Array.from(attention.attended ?? [])[0], [attention.attended]);
|
|
125
|
+
|
|
120
126
|
useEffect(() => {
|
|
121
127
|
// TODO(burdon): Can we prevent the need to re-scroll since the planks are preserved?
|
|
122
128
|
// E.g., hide the deck and just move the solo article?
|
|
@@ -139,12 +145,10 @@ export const DeckLayout = ({
|
|
|
139
145
|
return parts;
|
|
140
146
|
}, [layoutParts.main, layoutParts.solo]);
|
|
141
147
|
|
|
142
|
-
const padding =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return {};
|
|
147
|
-
}, [layoutMode, overscroll, layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen]);
|
|
148
|
+
const padding =
|
|
149
|
+
layoutMode === 'deck' && overscroll === 'centering'
|
|
150
|
+
? calculateOverscroll(layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen)
|
|
151
|
+
: {};
|
|
148
152
|
|
|
149
153
|
if (layoutMode === 'fullscreen') {
|
|
150
154
|
return <Fullscreen id={fullScreenSlug} />;
|
|
@@ -169,17 +173,25 @@ export const DeckLayout = ({
|
|
|
169
173
|
<Main.Root
|
|
170
174
|
navigationSidebarOpen={context.sidebarOpen}
|
|
171
175
|
onNavigationSidebarOpenChange={(next) => (context.sidebarOpen = next)}
|
|
172
|
-
complementarySidebarOpen
|
|
173
|
-
|
|
176
|
+
{...(complementarySidebarOpen !== null && {
|
|
177
|
+
complementarySidebarOpen: /* complementaryAvailable && */ context.complementarySidebarOpen as boolean,
|
|
178
|
+
onComplementarySidebarOpenChange: (next) => (context.complementarySidebarOpen = next),
|
|
179
|
+
})}
|
|
174
180
|
>
|
|
175
181
|
{/* Notch */}
|
|
176
182
|
<Main.Notch classNames='z-[21]'>
|
|
177
183
|
<Surface role='notch-start' />
|
|
178
|
-
<Button
|
|
184
|
+
<Button
|
|
185
|
+
// disabled={!sidebarAvailable}
|
|
186
|
+
onClick={() => (context.sidebarOpen = !context.sidebarOpen)}
|
|
187
|
+
variant='ghost'
|
|
188
|
+
classNames='p-1'
|
|
189
|
+
>
|
|
179
190
|
<span className='sr-only'>{t('open navigation sidebar label')}</span>
|
|
180
191
|
<MenuIcon weight='light' className={getSize(5)} />
|
|
181
192
|
</Button>
|
|
182
193
|
<Button
|
|
194
|
+
// disabled={!complementaryAvailable}
|
|
183
195
|
onClick={() => (context.complementarySidebarOpen = !context.complementarySidebarOpen)}
|
|
184
196
|
variant='ghost'
|
|
185
197
|
classNames='p-1'
|
|
@@ -191,11 +203,10 @@ export const DeckLayout = ({
|
|
|
191
203
|
</Main.Notch>
|
|
192
204
|
|
|
193
205
|
{/* Left sidebar. */}
|
|
194
|
-
<Sidebar layoutParts={layoutParts} />
|
|
206
|
+
<Sidebar attention={attention} layoutParts={layoutParts} />
|
|
195
207
|
|
|
196
208
|
{/* Right sidebar. */}
|
|
197
|
-
{
|
|
198
|
-
<ComplementarySidebar context='comments' layoutParts={layoutParts} flatDeck={flatDeck} />
|
|
209
|
+
<ComplementarySidebar id={complementarySlug} layoutParts={layoutParts} flatDeck={flatDeck} />
|
|
199
210
|
|
|
200
211
|
{/* Dialog overlay to dismiss dialogs. */}
|
|
201
212
|
<Main.Overlay />
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Placeholder } from '@phosphor-icons/react';
|
|
5
6
|
import React, { Fragment, useEffect } from 'react';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
LayoutAction,
|
|
9
10
|
NavigationAction,
|
|
11
|
+
SLUG_COLLECTION_INDICATOR,
|
|
10
12
|
SLUG_PATH_SEPARATOR,
|
|
11
13
|
Surface,
|
|
12
14
|
useIntentDispatcher,
|
|
@@ -14,10 +16,11 @@ import {
|
|
|
14
16
|
partLength,
|
|
15
17
|
type LayoutParts,
|
|
16
18
|
type LayoutPart,
|
|
19
|
+
type LayoutEntry,
|
|
17
20
|
} from '@dxos/app-framework';
|
|
18
21
|
import { type Node, useGraph } from '@dxos/plugin-graph';
|
|
19
|
-
import {
|
|
20
|
-
import { PlankHeading } from '@dxos/react-ui-deck';
|
|
22
|
+
import { Popover, toLocalizedString, useMediaQuery, useTranslation } from '@dxos/react-ui';
|
|
23
|
+
import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
|
|
21
24
|
import { TextTooltip } from '@dxos/react-ui-text-tooltip';
|
|
22
25
|
|
|
23
26
|
import { DECK_PLUGIN } from '../../meta';
|
|
@@ -27,6 +30,8 @@ export const NodePlankHeading = ({
|
|
|
27
30
|
id,
|
|
28
31
|
layoutParts,
|
|
29
32
|
layoutPart,
|
|
33
|
+
// TODO(wittjosiah): Unused?
|
|
34
|
+
layoutEntry,
|
|
30
35
|
popoverAnchorId,
|
|
31
36
|
pending,
|
|
32
37
|
flatDeck,
|
|
@@ -35,13 +40,14 @@ export const NodePlankHeading = ({
|
|
|
35
40
|
id?: string;
|
|
36
41
|
layoutParts?: LayoutParts;
|
|
37
42
|
layoutPart?: LayoutPart;
|
|
43
|
+
layoutEntry?: LayoutEntry;
|
|
38
44
|
popoverAnchorId?: string;
|
|
39
45
|
pending?: boolean;
|
|
40
46
|
flatDeck?: boolean;
|
|
41
47
|
}) => {
|
|
42
48
|
const { t } = useTranslation(DECK_PLUGIN);
|
|
43
49
|
const { graph } = useGraph();
|
|
44
|
-
const
|
|
50
|
+
const Icon = node?.properties?.icon ?? Placeholder;
|
|
45
51
|
const label = pending
|
|
46
52
|
? t('pending heading')
|
|
47
53
|
: toLocalizedString(node?.properties?.label ?? ['plank heading fallback label', { ns: DECK_PLUGIN }], t);
|
|
@@ -74,8 +80,7 @@ export const NodePlankHeading = ({
|
|
|
74
80
|
<ActionRoot>
|
|
75
81
|
{node ? (
|
|
76
82
|
<PlankHeading.ActionsMenu
|
|
77
|
-
|
|
78
|
-
related={layoutPart === 'complementary'}
|
|
83
|
+
Icon={Icon}
|
|
79
84
|
attendableId={attendableId}
|
|
80
85
|
triggerLabel={t('actions menu label')}
|
|
81
86
|
actions={graph.actions(node)}
|
|
@@ -88,16 +93,12 @@ export const NodePlankHeading = ({
|
|
|
88
93
|
) : (
|
|
89
94
|
<PlankHeading.Button>
|
|
90
95
|
<span className='sr-only'>{label}</span>
|
|
91
|
-
<Icon
|
|
96
|
+
<Icon {...plankHeadingIconProps} />
|
|
92
97
|
</PlankHeading.Button>
|
|
93
98
|
)}
|
|
94
99
|
</ActionRoot>
|
|
95
100
|
<TextTooltip text={label} onlyWhenTruncating>
|
|
96
|
-
<PlankHeading.Label
|
|
97
|
-
attendableId={attendableId}
|
|
98
|
-
related={layoutPart === 'complementary'}
|
|
99
|
-
{...(pending && { classNames: 'text-description' })}
|
|
100
|
-
>
|
|
101
|
+
<PlankHeading.Label attendableId={node?.id} {...(pending && { classNames: 'text-description' })}>
|
|
101
102
|
{label}
|
|
102
103
|
</PlankHeading.Label>
|
|
103
104
|
</TextTooltip>
|
|
@@ -142,6 +143,7 @@ export const NodePlankHeading = ({
|
|
|
142
143
|
action: NavigationAction.CLOSE,
|
|
143
144
|
data: {
|
|
144
145
|
activeParts: {
|
|
146
|
+
complementary: [`${id}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`],
|
|
145
147
|
[layoutPart]: [id],
|
|
146
148
|
},
|
|
147
149
|
},
|
|
@@ -149,7 +151,7 @@ export const NodePlankHeading = ({
|
|
|
149
151
|
: { action: NavigationAction.ADJUST, data: { type: eventType, layoutCoordinate } },
|
|
150
152
|
);
|
|
151
153
|
}}
|
|
152
|
-
close={
|
|
154
|
+
close={layoutCoordinate?.part === 'complementary' ? 'minify-end' : true}
|
|
153
155
|
/>
|
|
154
156
|
</PlankHeading.Root>
|
|
155
157
|
);
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { debounce } from '@dxos/async';
|
|
20
20
|
import { useGraph } from '@dxos/plugin-graph';
|
|
21
21
|
import { Button, Tooltip, useTranslation } from '@dxos/react-ui';
|
|
22
|
-
import {
|
|
22
|
+
import { createAttendableAttributes } from '@dxos/react-ui-attention';
|
|
23
23
|
import { Plank as NaturalPlank } from '@dxos/react-ui-deck';
|
|
24
24
|
import { mainIntrinsicSize } from '@dxos/react-ui-theme';
|
|
25
25
|
|
|
@@ -52,7 +52,7 @@ export const Plank = ({ entry, layoutParts, part, flatDeck, searchEnabled, layou
|
|
|
52
52
|
const rootElement = useRef<HTMLDivElement | null>(null);
|
|
53
53
|
const resizeable = layoutMode === 'deck';
|
|
54
54
|
|
|
55
|
-
const attendableAttrs =
|
|
55
|
+
const attendableAttrs = createAttendableAttributes(entry.id);
|
|
56
56
|
const coordinate: LayoutCoordinate = { part, entryId: entry.id };
|
|
57
57
|
|
|
58
58
|
const size = plankSizing?.[entry.id] as number | undefined;
|
|
@@ -4,19 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type LayoutParts, openIds, Surface } from '@dxos/app-framework';
|
|
7
|
+
import { type Attention, type LayoutParts, openIds, Surface } from '@dxos/app-framework';
|
|
8
8
|
import { Main } from '@dxos/react-ui';
|
|
9
|
-
import { useAttended } from '@dxos/react-ui-attention';
|
|
10
9
|
|
|
11
10
|
import { useLayout } from '../LayoutContext';
|
|
12
11
|
|
|
13
12
|
export type SidebarProps = {
|
|
13
|
+
attention: Attention;
|
|
14
14
|
layoutParts: LayoutParts;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export const Sidebar = ({ layoutParts }: SidebarProps) => {
|
|
17
|
+
export const Sidebar = ({ attention, layoutParts }: SidebarProps) => {
|
|
18
18
|
const { layoutMode, popoverAnchorId } = useLayout();
|
|
19
|
-
const attended = useAttended();
|
|
20
19
|
|
|
21
20
|
const activeIds = useMemo(() => {
|
|
22
21
|
if (layoutMode === 'solo') {
|
|
@@ -32,9 +31,9 @@ export const Sidebar = ({ layoutParts }: SidebarProps) => {
|
|
|
32
31
|
() => ({
|
|
33
32
|
popoverAnchorId,
|
|
34
33
|
activeIds,
|
|
35
|
-
attended,
|
|
34
|
+
attended: attention.attended,
|
|
36
35
|
}),
|
|
37
|
-
[popoverAnchorId, activeIds, attended],
|
|
36
|
+
[popoverAnchorId, activeIds, attention.attended],
|
|
38
37
|
);
|
|
39
38
|
return (
|
|
40
39
|
<Main.NavigationSidebar>
|
package/src/hooks/useNode.ts
CHANGED
|
@@ -16,13 +16,9 @@ import { type Graph, type Node } from '@dxos/plugin-graph';
|
|
|
16
16
|
*/
|
|
17
17
|
// TODO(wittjosiah): Factor out.
|
|
18
18
|
export const useNode = <T = any>(graph: Graph, id?: string, timeout?: number): Node<T> | undefined => {
|
|
19
|
-
const [nodeState, setNodeState] = useState<Node<T> | undefined>(id ? graph.findNode(id
|
|
19
|
+
const [nodeState, setNodeState] = useState<Node<T> | undefined>(id ? graph.findNode(id) : undefined);
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
if (!id && nodeState) {
|
|
23
|
-
setNodeState(undefined);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
if (nodeState?.id === id || !id) {
|
|
27
23
|
return;
|
|
28
24
|
}
|