@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.
@@ -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
- 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
- });
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
- handleSetLocation({ sidebar: [{ id: NAV_ID }] });
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
- handleSetLocation(mergeLayoutParts(layoutFromUri, startingLayout));
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: 'ph--arrows-out--regular',
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
- <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
- />
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
- handleSetLocation(newLayout);
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
- handleSetLocation(
546
- openEntry(location.values.active, effectivePart, layoutEntry, {
547
- positioning: data.positioning ?? settings.values.newPlankPositioning,
548
- pivotId: data.pivotId,
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
- 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.
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
- handleSetLocation(intent.data!.activeParts);
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
- handleSetLocation(
611
- incrementPlank(location.values.active, {
612
- type: adjustment.type,
613
- layoutCoordinate: adjustment.layoutCoordinate,
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 { useAttended } from '@dxos/react-ui-attention';
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
- context?: string;
21
+ id?: string;
22
22
  layoutParts: LayoutParts;
23
23
  flatDeck?: boolean;
24
24
  };
25
25
 
26
- export const ComplementarySidebar = ({ context, layoutParts, flatDeck }: ComplementarySidebarProps) => {
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
- <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 && (
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.properties.object, part: 'complementary', popoverAnchorId }}
49
+ data={{ subject: node.data, part: 'complementary', popoverAnchorId }}
51
50
  limit={1}
52
51
  fallback={PlankContentError}
53
52
  placeholder={<PlankLoading />}
54
53
  />
55
- )}
56
- </div>
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, useOnTransition, useTranslation } from '@dxos/react-ui';
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 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
- }, []);
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
- scrollLeftRef.current = null;
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
- const restoreScroll = useCallback(() => {
100
- if (deckRef.current && scrollLeftRef.current != null) {
101
- deckRef.current.scrollLeft = scrollLeftRef.current;
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
- scrollLeftRef.current = (event.target as HTMLDivElement).scrollLeft;
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 firstAttendedId = attended[0];
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 = 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]);
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={context.complementarySidebarOpen}
173
- onComplementarySidebarOpenChange={(next) => (context.complementarySidebarOpen = next)}
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 onClick={() => (context.sidebarOpen = !context.sidebarOpen)} variant='ghost' classNames='p-1'>
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
- {/* TODO(wittjosiah): Get context from layout parts. */}
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 { Icon, Popover, toLocalizedString, useMediaQuery, useTranslation } from '@dxos/react-ui';
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 icon = node?.properties?.icon ?? 'ph--placeholder--regular';
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
- icon={icon}
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 icon={icon} size={5} />
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={layoutPart === 'complementary' ? 'minify-end' : true}
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 { useAttendableAttributes } from '@dxos/react-ui-attention';
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 = useAttendableAttributes(entry.id);
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>
@@ -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, false) : undefined);
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
  }
package/src/layout.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  //
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
-
5
4
  import { produce } from 'immer';
6
5
 
7
6
  import {