@dxos/plugin-deck 0.6.12-main.5cc132e → 0.6.12-main.7907542

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.
@@ -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:
@@ -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
246
  .prop({ key: 'showFooter', storageKey: 'show-footer', 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
+ showHintsFooter={settings.values.showFooter}
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') {
@@ -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 { createAttendableAttributes } from '@dxos/react-ui-attention';
10
+ import { useAttended } 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,41 +18,42 @@ import { useNode, useNodeActionExpander } from '../../hooks';
18
18
  import { useLayout } from '../LayoutContext';
19
19
 
20
20
  export type ComplementarySidebarProps = {
21
- id?: string;
21
+ context?: string;
22
22
  layoutParts: LayoutParts;
23
23
  flatDeck?: boolean;
24
24
  };
25
25
 
26
- export const ComplementarySidebar = ({ id, layoutParts, flatDeck }: ComplementarySidebarProps) => {
26
+ export const ComplementarySidebar = ({ context, layoutParts, flatDeck }: ComplementarySidebarProps) => {
27
27
  const { popoverAnchorId } = useLayout();
28
+ const attended = useAttended();
29
+ const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${context}` : undefined;
28
30
  const { graph } = useGraph();
29
31
  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 {...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
- />
36
+ <Main.ComplementarySidebar>
37
+ <div role='none' className={mx(deckGrid, 'grid-cols-1 bs-full')}>
38
+ <NodePlankHeading
39
+ node={node}
40
+ id={id}
41
+ layoutParts={layoutParts}
42
+ layoutPart='complementary'
43
+ popoverAnchorId={popoverAnchorId}
44
+ flatDeck={flatDeck}
45
+ />
46
+ {/* TODO(wittjosiah): Render some placeholder when node is undefined. */}
47
+ {node && (
47
48
  <Surface
48
49
  role='article'
49
- data={{ subject: node.data, part: 'complementary', popoverAnchorId }}
50
+ data={{ subject: node.properties.object, part: 'complementary', popoverAnchorId }}
50
51
  limit={1}
51
52
  fallback={PlankContentError}
52
53
  placeholder={<PlankLoading />}
53
54
  />
54
- </div>
55
- ) : null}
55
+ )}
56
+ </div>
56
57
  </Main.ComplementarySidebar>
57
58
  );
58
59
  };
@@ -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,7 +34,6 @@ 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;
@@ -48,7 +46,6 @@ export type DeckLayoutProps = {
48
46
 
49
47
  export const DeckLayout = ({
50
48
  layoutParts,
51
- attention,
52
49
  toasts,
53
50
  flatDeck,
54
51
  overscroll,
@@ -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 />
@@ -2,13 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Placeholder } from '@phosphor-icons/react';
6
5
  import React, { Fragment, useEffect } from 'react';
7
6
 
8
7
  import {
9
8
  LayoutAction,
10
9
  NavigationAction,
11
- SLUG_COLLECTION_INDICATOR,
12
10
  SLUG_PATH_SEPARATOR,
13
11
  Surface,
14
12
  useIntentDispatcher,
@@ -16,11 +14,10 @@ import {
16
14
  partLength,
17
15
  type LayoutParts,
18
16
  type LayoutPart,
19
- type LayoutEntry,
20
17
  } from '@dxos/app-framework';
21
18
  import { type Node, useGraph } from '@dxos/plugin-graph';
22
- import { Popover, toLocalizedString, useMediaQuery, useTranslation } from '@dxos/react-ui';
23
- import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
19
+ import { Icon, Popover, toLocalizedString, useMediaQuery, useTranslation } from '@dxos/react-ui';
20
+ import { PlankHeading } from '@dxos/react-ui-deck';
24
21
  import { TextTooltip } from '@dxos/react-ui-text-tooltip';
25
22
 
26
23
  import { DECK_PLUGIN } from '../../meta';
@@ -30,8 +27,6 @@ export const NodePlankHeading = ({
30
27
  id,
31
28
  layoutParts,
32
29
  layoutPart,
33
- // TODO(wittjosiah): Unused?
34
- layoutEntry,
35
30
  popoverAnchorId,
36
31
  pending,
37
32
  flatDeck,
@@ -40,14 +35,13 @@ export const NodePlankHeading = ({
40
35
  id?: string;
41
36
  layoutParts?: LayoutParts;
42
37
  layoutPart?: LayoutPart;
43
- layoutEntry?: LayoutEntry;
44
38
  popoverAnchorId?: string;
45
39
  pending?: boolean;
46
40
  flatDeck?: boolean;
47
41
  }) => {
48
42
  const { t } = useTranslation(DECK_PLUGIN);
49
43
  const { graph } = useGraph();
50
- const Icon = node?.properties?.icon ?? Placeholder;
44
+ const icon = node?.properties?.icon ?? 'ph--placeholder--regular';
51
45
  const label = pending
52
46
  ? t('pending heading')
53
47
  : toLocalizedString(node?.properties?.label ?? ['plank heading fallback label', { ns: DECK_PLUGIN }], t);
@@ -80,7 +74,8 @@ export const NodePlankHeading = ({
80
74
  <ActionRoot>
81
75
  {node ? (
82
76
  <PlankHeading.ActionsMenu
83
- Icon={Icon}
77
+ icon={icon}
78
+ related={layoutPart === 'complementary'}
84
79
  attendableId={attendableId}
85
80
  triggerLabel={t('actions menu label')}
86
81
  actions={graph.actions(node)}
@@ -93,12 +88,16 @@ export const NodePlankHeading = ({
93
88
  ) : (
94
89
  <PlankHeading.Button>
95
90
  <span className='sr-only'>{label}</span>
96
- <Icon {...plankHeadingIconProps} />
91
+ <Icon icon={icon} size={5} />
97
92
  </PlankHeading.Button>
98
93
  )}
99
94
  </ActionRoot>
100
95
  <TextTooltip text={label} onlyWhenTruncating>
101
- <PlankHeading.Label attendableId={node?.id} {...(pending && { classNames: 'text-description' })}>
96
+ <PlankHeading.Label
97
+ attendableId={attendableId}
98
+ related={layoutPart === 'complementary'}
99
+ {...(pending && { classNames: 'text-description' })}
100
+ >
102
101
  {label}
103
102
  </PlankHeading.Label>
104
103
  </TextTooltip>
@@ -143,7 +142,6 @@ export const NodePlankHeading = ({
143
142
  action: NavigationAction.CLOSE,
144
143
  data: {
145
144
  activeParts: {
146
- complementary: [`${id}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`],
147
145
  [layoutPart]: [id],
148
146
  },
149
147
  },
@@ -151,7 +149,7 @@ export const NodePlankHeading = ({
151
149
  : { action: NavigationAction.ADJUST, data: { type: eventType, layoutCoordinate } },
152
150
  );
153
151
  }}
154
- close={layoutCoordinate?.part === 'complementary' ? 'minify-end' : true}
152
+ close={layoutPart === 'complementary' ? 'minify-end' : true}
155
153
  />
156
154
  </PlankHeading.Root>
157
155
  );
@@ -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 { createAttendableAttributes } from '@dxos/react-ui-attention';
22
+ import { useAttendableAttributes } 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 = createAttendableAttributes(entry.id);
55
+ const attendableAttrs = useAttendableAttributes(entry.id);
56
56
  const coordinate: LayoutCoordinate = { part, entryId: entry.id };
57
57
 
58
58
  const size = plankSizing?.[entry.id] as number | undefined;
@@ -4,18 +4,19 @@
4
4
 
5
5
  import React, { useMemo } from 'react';
6
6
 
7
- import { type Attention, type LayoutParts, openIds, Surface } from '@dxos/app-framework';
7
+ import { type LayoutParts, openIds, Surface } from '@dxos/app-framework';
8
8
  import { Main } from '@dxos/react-ui';
9
+ import { useAttended } from '@dxos/react-ui-attention';
9
10
 
10
11
  import { useLayout } from '../LayoutContext';
11
12
 
12
13
  export type SidebarProps = {
13
- attention: Attention;
14
14
  layoutParts: LayoutParts;
15
15
  };
16
16
 
17
- export const Sidebar = ({ attention, layoutParts }: SidebarProps) => {
17
+ export const Sidebar = ({ layoutParts }: SidebarProps) => {
18
18
  const { layoutMode, popoverAnchorId } = useLayout();
19
+ const attended = useAttended();
19
20
 
20
21
  const activeIds = useMemo(() => {
21
22
  if (layoutMode === 'solo') {
@@ -31,9 +32,9 @@ export const Sidebar = ({ attention, layoutParts }: SidebarProps) => {
31
32
  () => ({
32
33
  popoverAnchorId,
33
34
  activeIds,
34
- attended: attention.attended,
35
+ attended,
35
36
  }),
36
- [popoverAnchorId, activeIds, attention.attended],
37
+ [popoverAnchorId, activeIds, attended],
37
38
  );
38
39
  return (
39
40
  <Main.NavigationSidebar>
@@ -16,9 +16,13 @@ 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) : undefined);
19
+ const [nodeState, setNodeState] = useState<Node<T> | undefined>(id ? graph.findNode(id, false) : undefined);
20
20
 
21
21
  useEffect(() => {
22
+ if (!id && nodeState) {
23
+ setNodeState(undefined);
24
+ }
25
+
22
26
  if (nodeState?.id === id || !id) {
23
27
  return;
24
28
  }