@dxos/plugin-deck 0.6.11 → 0.6.12-main.2d19bf1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/lib/browser/{chunk-YVHGFQQR.mjs → chunk-GVOGPULO.mjs} +1 -1
  2. package/dist/lib/browser/chunk-GVOGPULO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +220 -148
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/meta.mjs +1 -1
  7. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  8. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +2 -2
  9. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  10. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +3 -4
  11. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  12. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +4 -3
  13. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  14. package/dist/types/src/components/DeckLayout/Sidebar.d.ts +2 -3
  15. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  16. package/dist/types/src/components/DeckLayout/StatusBar.d.ts +3 -1
  17. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  18. package/dist/types/src/components/LayoutSettings.d.ts.map +1 -1
  19. package/dist/types/src/hooks/useNode.d.ts.map +1 -1
  20. package/dist/types/src/layout.d.ts.map +1 -1
  21. package/dist/types/src/meta.d.ts.map +1 -1
  22. package/dist/types/src/translations.d.ts +3 -1
  23. package/dist/types/src/translations.d.ts.map +1 -1
  24. package/dist/types/src/types.d.ts +1 -1
  25. package/dist/types/src/types.d.ts.map +1 -1
  26. package/package.json +29 -29
  27. package/src/DeckPlugin.tsx +81 -53
  28. package/src/components/DeckLayout/ComplementarySidebar.tsx +73 -22
  29. package/src/components/DeckLayout/DeckLayout.tsx +42 -60
  30. package/src/components/DeckLayout/NodePlankHeading.tsx +15 -15
  31. package/src/components/DeckLayout/Plank.tsx +2 -2
  32. package/src/components/DeckLayout/Sidebar.tsx +6 -5
  33. package/src/components/DeckLayout/StatusBar.tsx +10 -2
  34. package/src/components/LayoutSettings.tsx +5 -8
  35. package/src/hooks/useNode.ts +5 -1
  36. package/src/layout.ts +1 -0
  37. package/src/meta.ts +3 -1
  38. package/src/translations.ts +3 -1
  39. package/src/types.ts +1 -1
  40. package/dist/lib/browser/chunk-YVHGFQQR.mjs.map +0 -7
@@ -2,7 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { ArrowsOut, type IconProps } from '@phosphor-icons/react';
6
5
  import { batch, effect } from '@preact/signals-core';
7
6
  import { setAutoFreeze } from 'immer';
8
7
  import React, { type PropsWithChildren } from 'react';
@@ -41,7 +40,6 @@ import { createExtension, type Node } from '@dxos/plugin-graph';
41
40
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
42
41
  import { fullyQualifiedId } from '@dxos/react-client/echo';
43
42
  import { translations as deckTranslations } from '@dxos/react-ui-deck';
44
- import { Mosaic } from '@dxos/react-ui-mosaic';
45
43
 
46
44
  import {
47
45
  DeckLayout,
@@ -71,7 +69,7 @@ const isSocket = !!(globalThis as any).__args;
71
69
  // TODO(mjamesderocher): Can we get this directly from Socket?
72
70
  const appScheme = 'composer://';
73
71
 
74
- // TODO(burdon): Evolve into customizable prefs,.
72
+ // TODO(burdon): Evolve into customizable prefs.
75
73
  const customSlots: DeckLayoutProps['slots'] = {
76
74
  wallpaper: {
77
75
  classNames:
@@ -112,7 +110,7 @@ export const DeckPlugin = ({
112
110
  let handleNavigation: () => Promise<void> | undefined;
113
111
 
114
112
  const settings = new LocalStorageStore<DeckSettingsProps>('dxos.org/settings/layout', {
115
- showFooter: false,
113
+ showHints: true,
116
114
  customSlots: false,
117
115
  flatDeck: false,
118
116
  enableNativeRedirect: false,
@@ -190,6 +188,25 @@ export const DeckPlugin = ({
190
188
  }
191
189
  };
192
190
 
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
+
193
210
  return {
194
211
  meta,
195
212
  ready: async (plugins) => {
@@ -198,16 +215,21 @@ export const DeckPlugin = ({
198
215
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
199
216
  clientPlugin = resolvePlugin(plugins, parseClientPlugin);
200
217
 
201
- // prettier-ignore
202
218
  layout
203
219
  .prop({ key: 'layoutMode', storageKey: 'layout-mode', type: LocalStorageStore.enum<LayoutMode>() })
204
220
  .prop({ key: 'sidebarOpen', storageKey: 'sidebar-open', type: LocalStorageStore.bool() })
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>>() });
221
+ .prop({
222
+ key: 'complementarySidebarOpen',
223
+ storageKey: 'complementary-sidebar-open',
224
+ type: LocalStorageStore.bool(),
225
+ });
226
+
227
+ deck.prop({
228
+ key: 'plankSizing',
229
+ storageKey: 'plank-sizing',
230
+ type: LocalStorageStore.json<Record<string, number>>(),
231
+ });
209
232
 
210
- // prettier-ignore
211
233
  location
212
234
  .prop({ key: 'active', storageKey: 'active', type: LocalStorageStore.json<LayoutParts>() })
213
235
  .prop({ key: 'closed', storageKey: 'closed', type: LocalStorageStore.json<string[]>() });
@@ -220,14 +242,17 @@ export const DeckPlugin = ({
220
242
  }),
221
243
  );
222
244
 
223
- // prettier-ignore
224
245
  settings
225
- .prop({ key: 'showFooter', storageKey: 'show-footer', type: LocalStorageStore.bool() })
246
+ .prop({ key: 'showHints', storageKey: 'show-hints', type: LocalStorageStore.bool() })
226
247
  .prop({ key: 'customSlots', storageKey: 'customSlots', type: LocalStorageStore.bool() })
227
248
  .prop({ key: 'flatDeck', storageKey: 'flatDeck', type: LocalStorageStore.bool() })
228
249
  .prop({ key: 'enableNativeRedirect', storageKey: 'enable-native-redirect', type: LocalStorageStore.bool() })
229
250
  .prop({ key: 'disableDeck', storageKey: 'disable-deck', type: LocalStorageStore.bool() }) // Deprecated.
230
- .prop({ key: 'newPlankPositioning', storageKey: 'newPlankPositioning', type: LocalStorageStore.enum<NewPlankPositioning>() })
251
+ .prop({
252
+ key: 'newPlankPositioning',
253
+ storageKey: 'newPlankPositioning',
254
+ type: LocalStorageStore.enum<NewPlankPositioning>(),
255
+ })
231
256
  .prop({ key: 'overscroll', storageKey: 'overscroll', type: LocalStorageStore.enum<Overscroll>() });
232
257
 
233
258
  if (!isSocket && settings.values.enableNativeRedirect) {
@@ -237,7 +262,7 @@ export const DeckPlugin = ({
237
262
  handleNavigation = async () => {
238
263
  const pathname = window.location.pathname;
239
264
  if (pathname === '/reset') {
240
- location.values.active = { sidebar: [{ id: NAV_ID }] };
265
+ handleSetLocation({ sidebar: [{ id: NAV_ID }] });
241
266
  location.values.closed = [];
242
267
  layout.values.layoutMode = 'solo';
243
268
  window.location.pathname = '/';
@@ -250,7 +275,7 @@ export const DeckPlugin = ({
250
275
  }
251
276
 
252
277
  const startingLayout = removePart(location.values.active, 'solo');
253
- location.values.active = mergeLayoutParts(layoutFromUri, startingLayout);
278
+ handleSetLocation(mergeLayoutParts(layoutFromUri, startingLayout));
254
279
  layout.values.layoutMode = 'solo';
255
280
  };
256
281
 
@@ -310,8 +335,7 @@ export const DeckPlugin = ({
310
335
  },
311
336
  properties: {
312
337
  label: ['toggle fullscreen label', { ns: DECK_PLUGIN }],
313
- icon: (props: IconProps) => <ArrowsOut {...props} />,
314
- iconSymbol: 'ph--arrows-out--regular',
338
+ icon: 'ph--arrows-out--regular',
315
339
  keyBinding: {
316
340
  macos: 'ctrl+meta+f',
317
341
  windows: 'shift+ctrl+f',
@@ -329,31 +353,27 @@ export const DeckPlugin = ({
329
353
  ),
330
354
  root: () => {
331
355
  return (
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>
356
+ <DeckLayout
357
+ layoutParts={location.values.active}
358
+ showHints={settings.values.showHints}
359
+ overscroll={settings.values.overscroll}
360
+ flatDeck={settings.values.flatDeck}
361
+ slots={settings.values.customSlots ? customSlots : undefined}
362
+ toasts={layout.values.toasts}
363
+ onDismissToast={(id) => {
364
+ const index = layout.values.toasts.findIndex((toast) => toast.id === id);
365
+ if (index !== -1) {
366
+ // Allow time for the toast to animate out.
367
+ // TODO(burdon): Factor out and unregister timeout.
368
+ setTimeout(() => {
369
+ if (layout.values.toasts[index].id === currentUndoId) {
370
+ currentUndoId = undefined;
371
+ }
372
+ layout.values.toasts.splice(index, 1);
373
+ }, 1_000);
374
+ }
375
+ }}
376
+ />
357
377
  );
358
378
  },
359
379
  surface: {
@@ -472,7 +492,7 @@ export const DeckPlugin = ({
472
492
  }
473
493
  });
474
494
 
475
- location.values.active = newLayout;
495
+ handleSetLocation(newLayout);
476
496
  });
477
497
 
478
498
  const ids = openIds(location.values.active);
@@ -522,10 +542,12 @@ export const DeckPlugin = ({
522
542
  const layoutEntry = { id: data.id };
523
543
  const effectivePart = getEffectivePart(data.part, layout.values.layoutMode);
524
544
 
525
- location.values.active = openEntry(location.values.active, effectivePart, layoutEntry, {
526
- positioning: data.positioning ?? settings.values.newPlankPositioning,
527
- pivotId: data.pivotId,
528
- });
545
+ handleSetLocation(
546
+ openEntry(location.values.active, effectivePart, layoutEntry, {
547
+ positioning: data.positioning ?? settings.values.newPlankPositioning,
548
+ pivotId: data.pivotId,
549
+ }),
550
+ );
529
551
 
530
552
  const intents = [];
531
553
  if (data.scrollIntoView && layout.values.layoutMode === 'deck') {
@@ -561,7 +583,11 @@ export const DeckPlugin = ({
561
583
  }
562
584
  });
563
585
 
564
- location.values.active = newLayout;
586
+ handleSetLocation(newLayout);
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.
565
591
  return { data: true };
566
592
  });
567
593
  }
@@ -570,7 +596,7 @@ export const DeckPlugin = ({
570
596
  case NavigationAction.SET: {
571
597
  return batch(() => {
572
598
  if (isLayoutParts(intent.data?.activeParts)) {
573
- location.values.active = intent.data!.activeParts;
599
+ handleSetLocation(intent.data!.activeParts);
574
600
  }
575
601
  return { data: true };
576
602
  });
@@ -581,10 +607,12 @@ export const DeckPlugin = ({
581
607
  if (isLayoutAdjustment(intent.data)) {
582
608
  const adjustment = intent.data;
583
609
  if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
584
- location.values.active = incrementPlank(location.values.active, {
585
- type: adjustment.type,
586
- layoutCoordinate: adjustment.layoutCoordinate,
587
- });
610
+ handleSetLocation(
611
+ incrementPlank(location.values.active, {
612
+ type: adjustment.type,
613
+ layoutCoordinate: adjustment.layoutCoordinate,
614
+ }),
615
+ );
588
616
  }
589
617
 
590
618
  if (adjustment.type === 'solo') {
@@ -2,12 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
- import { type LayoutParts, SLUG_PATH_SEPARATOR, Surface } from '@dxos/app-framework';
7
+ import {
8
+ type LayoutParts,
9
+ NavigationAction,
10
+ SLUG_PATH_SEPARATOR,
11
+ Surface,
12
+ useIntentDispatcher,
13
+ } from '@dxos/app-framework';
8
14
  import { useGraph } from '@dxos/plugin-graph';
9
15
  import { Main } from '@dxos/react-ui';
10
- import { createAttendableAttributes } from '@dxos/react-ui-attention';
16
+ import { useAttended } from '@dxos/react-ui-attention';
11
17
  import { deckGrid } from '@dxos/react-ui-deck';
12
18
  import { mx } from '@dxos/react-ui-theme';
13
19
 
@@ -15,44 +21,89 @@ import { NodePlankHeading } from './NodePlankHeading';
15
21
  import { PlankContentError } from './PlankError';
16
22
  import { PlankLoading } from './PlankLoading';
17
23
  import { useNode, useNodeActionExpander } from '../../hooks';
24
+ import { DECK_PLUGIN } from '../../meta';
18
25
  import { useLayout } from '../LayoutContext';
19
26
 
20
27
  export type ComplementarySidebarProps = {
21
- id?: string;
28
+ context?: string;
22
29
  layoutParts: LayoutParts;
23
30
  flatDeck?: boolean;
24
31
  };
25
32
 
26
- export const ComplementarySidebar = ({ id, layoutParts, flatDeck }: ComplementarySidebarProps) => {
33
+ const panels = ['comments', 'settings'] as const;
34
+ type Panel = (typeof panels)[number];
35
+ const getPanel = (part?: string): Panel => {
36
+ if (part && panels.findIndex((panel) => panel === part) !== -1) {
37
+ return part as Panel;
38
+ } else {
39
+ return 'settings';
40
+ }
41
+ };
42
+
43
+ export const ComplementarySidebar = ({ layoutParts, flatDeck }: ComplementarySidebarProps) => {
27
44
  const { popoverAnchorId } = useLayout();
45
+ const attended = useAttended();
46
+ const part = getPanel(layoutParts.complementary?.[0].id);
47
+ const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${part}` : undefined;
28
48
  const { graph } = useGraph();
29
49
  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');
50
+ const dispatch = useIntentDispatcher();
32
51
 
33
52
  useNodeActionExpander(node);
34
53
 
54
+ const actions = useMemo(
55
+ () => [
56
+ {
57
+ id: 'complementary-settings',
58
+ data: () => {
59
+ void dispatch({ action: NavigationAction.OPEN, data: { activeParts: { complementary: 'settings' } } });
60
+ },
61
+ properties: {
62
+ label: ['settings label', { ns: DECK_PLUGIN }],
63
+ icon: 'ph--gear--regular',
64
+ menuItemType: 'toggle',
65
+ isChecked: part === 'settings',
66
+ },
67
+ },
68
+ {
69
+ id: 'complementary-comments',
70
+ data: () => {
71
+ void dispatch({ action: NavigationAction.OPEN, data: { activeParts: { complementary: 'comments' } } });
72
+ },
73
+ properties: {
74
+ label: ['comments label', { ns: DECK_PLUGIN }],
75
+ icon: 'ph--chat-text--regular',
76
+ menuItemType: 'toggle',
77
+ isChecked: part === 'comments',
78
+ },
79
+ },
80
+ ],
81
+ [part],
82
+ );
83
+
35
84
  return (
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
- />
85
+ <Main.ComplementarySidebar>
86
+ <div role='none' className={mx(deckGrid, 'grid-cols-1 bs-full')}>
87
+ <NodePlankHeading
88
+ node={node}
89
+ id={id}
90
+ layoutParts={layoutParts}
91
+ layoutPart='complementary'
92
+ popoverAnchorId={popoverAnchorId}
93
+ flatDeck={flatDeck}
94
+ actions={actions}
95
+ />
96
+ {/* TODO(wittjosiah): Render some placeholder when node is undefined. */}
97
+ {node && (
47
98
  <Surface
48
- role='article'
49
- data={{ subject: node.data, part: 'complementary', popoverAnchorId }}
99
+ role={`complementary--${part}`}
100
+ data={{ subject: node.properties.object, popoverAnchorId }}
50
101
  limit={1}
51
102
  fallback={PlankContentError}
52
103
  placeholder={<PlankLoading />}
53
104
  />
54
- </div>
55
- ) : null}
105
+ )}
106
+ </div>
56
107
  </Main.ComplementarySidebar>
57
108
  );
58
109
  };
@@ -3,11 +3,9 @@
3
3
  //
4
4
 
5
5
  import { Sidebar as MenuIcon } from '@phosphor-icons/react';
6
- import React, { useCallback, useEffect, useMemo, useRef, useState, useLayoutEffect, type UIEvent } from 'react';
6
+ import React, { useCallback, useEffect, useMemo, useRef, type UIEvent } from 'react';
7
7
 
8
8
  import {
9
- SLUG_PATH_SEPARATOR,
10
- type Attention,
11
9
  type LayoutEntry,
12
10
  type LayoutParts,
13
11
  Surface,
@@ -15,7 +13,8 @@ import {
15
13
  firstIdInPart,
16
14
  usePlugin,
17
15
  } from '@dxos/app-framework';
18
- import { Button, Dialog, Main, Popover, useTranslation } from '@dxos/react-ui';
16
+ import { Button, Dialog, Main, Popover, useOnTransition, useTranslation } from '@dxos/react-ui';
17
+ import { useAttended } from '@dxos/react-ui-attention';
19
18
  import { Deck } from '@dxos/react-ui-deck';
20
19
  import { getSize } from '@dxos/react-ui-theme';
21
20
 
@@ -35,11 +34,10 @@ import { useLayout } from '../LayoutContext';
35
34
 
36
35
  export type DeckLayoutProps = {
37
36
  layoutParts: LayoutParts;
38
- attention: Attention;
39
37
  toasts: ToastSchema[];
40
38
  flatDeck?: boolean;
41
39
  overscroll: Overscroll;
42
- showHintsFooter: boolean;
40
+ showHints: boolean;
43
41
  slots?: {
44
42
  wallpaper?: { classNames?: string };
45
43
  };
@@ -48,11 +46,10 @@ export type DeckLayoutProps = {
48
46
 
49
47
  export const DeckLayout = ({
50
48
  layoutParts,
51
- attention,
52
49
  toasts,
53
50
  flatDeck,
54
51
  overscroll,
55
- showHintsFooter,
52
+ showHints,
56
53
  slots,
57
54
  onDismissToast,
58
55
  }: DeckLayoutProps) => {
@@ -70,36 +67,42 @@ export const DeckLayout = ({
70
67
  } = context;
71
68
  const { t } = useTranslation(DECK_PLUGIN);
72
69
  const { plankSizing } = useDeckContext();
70
+ const attended = useAttended();
73
71
  const searchPlugin = usePlugin('dxos.org/plugin/search');
74
72
  const fullScreenSlug = useMemo(() => firstIdInPart(layoutParts, 'fullScreen'), [layoutParts]);
75
73
 
76
- const [scrollLeft, setScrollLeft] = useState<number | null>(null);
77
- const deckRef = useRef<HTMLDivElement | null>(null);
78
- const restoreScrollRef = useRef<boolean>(false);
74
+ const scrollLeftRef = useRef<number | null>();
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
+ }, []);
79
86
 
80
87
  /**
81
88
  * Clear scroll restoration state if the window is resized
82
89
  */
83
90
  const handleResize = useCallback(() => {
84
- setScrollLeft(null);
91
+ scrollLeftRef.current = null;
85
92
  }, []);
93
+
86
94
  useEffect(() => {
87
95
  window.addEventListener('resize', handleResize);
88
96
  return () => window.removeEventListener('resize', handleResize);
89
97
  }, [handleResize]);
90
98
 
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;
99
+ const restoreScroll = useCallback(() => {
100
+ if (deckRef.current && scrollLeftRef.current != null) {
101
+ deckRef.current.scrollLeft = scrollLeftRef.current;
101
102
  }
102
- }, [layoutMode, deckRef.current, scrollLeft]);
103
+ }, []);
104
+
105
+ useOnTransition(layoutMode, (mode) => mode !== 'deck', 'deck', restoreScroll);
103
106
 
104
107
  /**
105
108
  * Save scroll position as the user scrolls
@@ -107,22 +110,13 @@ export const DeckLayout = ({
107
110
  const handleScroll = useCallback(
108
111
  (event: UIEvent) => {
109
112
  if (layoutMode === 'deck' && event.currentTarget === event.target) {
110
- // console.log('[save scroll left]', (event.target as HTMLDivElement).scrollLeft);
111
- setScrollLeft((event.target as HTMLDivElement).scrollLeft);
113
+ scrollLeftRef.current = (event.target as HTMLDivElement).scrollLeft;
112
114
  }
113
115
  },
114
116
  [layoutMode],
115
117
  );
116
118
 
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
-
119
+ const firstAttendedId = attended[0];
126
120
  useEffect(() => {
127
121
  // TODO(burdon): Can we prevent the need to re-scroll since the planks are preserved?
128
122
  // E.g., hide the deck and just move the solo article?
@@ -145,10 +139,12 @@ export const DeckLayout = ({
145
139
  return parts;
146
140
  }, [layoutParts.main, layoutParts.solo]);
147
141
 
148
- const padding =
149
- layoutMode === 'deck' && overscroll === 'centering'
150
- ? calculateOverscroll(layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen)
151
- : {};
142
+ const padding = useMemo(() => {
143
+ if (layoutMode === 'deck' && overscroll === 'centering') {
144
+ return calculateOverscroll(layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen);
145
+ }
146
+ return {};
147
+ }, [layoutMode, overscroll, layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen]);
152
148
 
153
149
  if (layoutMode === 'fullscreen') {
154
150
  return <Fullscreen id={fullScreenSlug} />;
@@ -173,25 +169,17 @@ export const DeckLayout = ({
173
169
  <Main.Root
174
170
  navigationSidebarOpen={context.sidebarOpen}
175
171
  onNavigationSidebarOpenChange={(next) => (context.sidebarOpen = next)}
176
- {...(complementarySidebarOpen !== null && {
177
- complementarySidebarOpen: /* complementaryAvailable && */ context.complementarySidebarOpen as boolean,
178
- onComplementarySidebarOpenChange: (next) => (context.complementarySidebarOpen = next),
179
- })}
172
+ complementarySidebarOpen={context.complementarySidebarOpen}
173
+ onComplementarySidebarOpenChange={(next) => (context.complementarySidebarOpen = next)}
180
174
  >
181
175
  {/* Notch */}
182
176
  <Main.Notch classNames='z-[21]'>
183
177
  <Surface role='notch-start' />
184
- <Button
185
- // disabled={!sidebarAvailable}
186
- onClick={() => (context.sidebarOpen = !context.sidebarOpen)}
187
- variant='ghost'
188
- classNames='p-1'
189
- >
178
+ <Button onClick={() => (context.sidebarOpen = !context.sidebarOpen)} variant='ghost' classNames='p-1'>
190
179
  <span className='sr-only'>{t('open navigation sidebar label')}</span>
191
180
  <MenuIcon weight='light' className={getSize(5)} />
192
181
  </Button>
193
182
  <Button
194
- // disabled={!complementaryAvailable}
195
183
  onClick={() => (context.complementarySidebarOpen = !context.complementarySidebarOpen)}
196
184
  variant='ghost'
197
185
  classNames='p-1'
@@ -203,10 +191,11 @@ export const DeckLayout = ({
203
191
  </Main.Notch>
204
192
 
205
193
  {/* Left sidebar. */}
206
- <Sidebar attention={attention} layoutParts={layoutParts} />
194
+ <Sidebar layoutParts={layoutParts} />
207
195
 
208
196
  {/* Right sidebar. */}
209
- <ComplementarySidebar id={complementarySlug} layoutParts={layoutParts} flatDeck={flatDeck} />
197
+ {/* TODO(wittjosiah): Get context from layout parts. */}
198
+ <ComplementarySidebar context='comments' layoutParts={layoutParts} flatDeck={flatDeck} />
210
199
 
211
200
  {/* Dialog overlay to dismiss dialogs. */}
212
201
  <Main.Overlay />
@@ -245,15 +234,8 @@ export const DeckLayout = ({
245
234
  </Main.Content>
246
235
  )}
247
236
 
248
- <StatusBar />
249
-
250
- {/* Help hints. */}
251
- {/* TODO(burdon): Need to make room for this in status bar. */}
252
- {showHintsFooter && (
253
- <div className='fixed bottom-0 left-0 right-0 h-[32px] z-[1] flex justify-center'>
254
- <Surface role='hints' limit={1} />
255
- </div>
256
- )}
237
+ {/* Footer status. */}
238
+ <StatusBar showHints={showHints} />
257
239
 
258
240
  {/* Global popovers. */}
259
241
  <Popover.Portal>