@dxos/plugin-deck 0.6.13 → 0.6.14-main.2b6a0f3

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 (53) 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 +331 -264
  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/ActiveNode.d.ts +1 -3
  9. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -1
  10. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +2 -4
  11. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  12. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +3 -4
  13. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  14. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +9 -7
  15. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  16. package/dist/types/src/components/DeckLayout/Plank.d.ts +1 -1
  17. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  18. package/dist/types/src/components/DeckLayout/PlankError.d.ts +1 -2
  19. package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -1
  20. package/dist/types/src/components/DeckLayout/Sidebar.d.ts +2 -3
  21. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  22. package/dist/types/src/components/DeckLayout/StatusBar.d.ts +3 -1
  23. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  24. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  25. package/dist/types/src/components/LayoutSettings.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useNode.d.ts.map +1 -1
  27. package/dist/types/src/layout.d.ts.map +1 -1
  28. package/dist/types/src/meta.d.ts.map +1 -1
  29. package/dist/types/src/translations.d.ts +5 -3
  30. package/dist/types/src/translations.d.ts.map +1 -1
  31. package/dist/types/src/types.d.ts +1 -1
  32. package/dist/types/src/types.d.ts.map +1 -1
  33. package/dist/types/src/util/overscroll.d.ts +1 -1
  34. package/dist/types/src/util/overscroll.d.ts.map +1 -1
  35. package/package.json +31 -29
  36. package/src/DeckPlugin.tsx +93 -71
  37. package/src/components/DeckLayout/ActiveNode.tsx +4 -1
  38. package/src/components/DeckLayout/ComplementarySidebar.tsx +70 -28
  39. package/src/components/DeckLayout/DeckLayout.tsx +64 -96
  40. package/src/components/DeckLayout/NodePlankHeading.tsx +130 -127
  41. package/src/components/DeckLayout/Plank.tsx +31 -22
  42. package/src/components/DeckLayout/PlankError.tsx +1 -9
  43. package/src/components/DeckLayout/Sidebar.tsx +6 -5
  44. package/src/components/DeckLayout/StatusBar.tsx +12 -3
  45. package/src/components/DeckLayout/Toast.tsx +3 -3
  46. package/src/components/LayoutSettings.tsx +5 -8
  47. package/src/hooks/useNode.ts +5 -1
  48. package/src/layout.ts +1 -0
  49. package/src/meta.ts +3 -1
  50. package/src/translations.ts +7 -3
  51. package/src/types.ts +1 -1
  52. package/src/util/overscroll.ts +5 -5
  53. package/dist/lib/browser/chunk-YVHGFQQR.mjs.map +0 -7
@@ -2,8 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { ArrowsOut, type IconProps } from '@phosphor-icons/react';
6
- import { batch, effect } from '@preact/signals-core';
5
+ import { batch } from '@preact/signals-core';
7
6
  import { setAutoFreeze } from 'immer';
8
7
  import React, { type PropsWithChildren } from 'react';
9
8
 
@@ -33,6 +32,7 @@ import {
33
32
  } from '@dxos/app-framework';
34
33
  import { type UnsubscribeCallback } from '@dxos/async';
35
34
  import { create, getTypename, isReactiveObject } from '@dxos/echo-schema';
35
+ import { scheduledEffect } from '@dxos/echo-signals/core';
36
36
  import { LocalStorageStore } from '@dxos/local-storage';
37
37
  import { log } from '@dxos/log';
38
38
  import { parseAttentionPlugin, type AttentionPluginProvides } from '@dxos/plugin-attention';
@@ -41,7 +41,6 @@ import { createExtension, type Node } from '@dxos/plugin-graph';
41
41
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
42
42
  import { fullyQualifiedId } from '@dxos/react-client/echo';
43
43
  import { translations as deckTranslations } from '@dxos/react-ui-deck';
44
- import { Mosaic } from '@dxos/react-ui-mosaic';
45
44
 
46
45
  import {
47
46
  DeckLayout,
@@ -71,7 +70,7 @@ const isSocket = !!(globalThis as any).__args;
71
70
  // TODO(mjamesderocher): Can we get this directly from Socket?
72
71
  const appScheme = 'composer://';
73
72
 
74
- // TODO(burdon): Evolve into customizable prefs,.
73
+ // TODO(burdon): Evolve into customizable prefs.
75
74
  const customSlots: DeckLayoutProps['slots'] = {
76
75
  wallpaper: {
77
76
  classNames:
@@ -112,7 +111,7 @@ export const DeckPlugin = ({
112
111
  let handleNavigation: () => Promise<void> | undefined;
113
112
 
114
113
  const settings = new LocalStorageStore<DeckSettingsProps>('dxos.org/settings/layout', {
115
- showFooter: false,
114
+ showHints: false,
116
115
  customSlots: false,
117
116
  flatDeck: false,
118
117
  enableNativeRedirect: false,
@@ -190,6 +189,28 @@ export const DeckPlugin = ({
190
189
  }
191
190
  };
192
191
 
192
+ /**
193
+ * Update the active state and ensure that attention is on an active element.
194
+ */
195
+ const handleSetLocation = (next: LayoutParts) => {
196
+ if (attentionPlugin) {
197
+ const attended = attentionPlugin.provides.attention.attended;
198
+ const [attendedId] = Array.from(attended);
199
+ const ids = (layout.values.layoutMode === 'deck' ? next.main : next.solo)?.map(({ id }) => id) ?? [];
200
+ const isAttendedAvailable = !!attendedId && ids.includes(attendedId);
201
+ if (!isAttendedAvailable) {
202
+ // Allow new plank to render before focusing.
203
+ requestAnimationFrame(() => {
204
+ const nextAttended = layout.values.layoutMode === 'solo' ? next.solo?.[0].id : next.main?.[0]?.id;
205
+ const article = document.querySelector<HTMLElement>(`article[data-attendable-id="${nextAttended}"]`);
206
+ article?.focus();
207
+ });
208
+ }
209
+ }
210
+
211
+ location.values.active = next;
212
+ };
213
+
193
214
  return {
194
215
  meta,
195
216
  ready: async (plugins) => {
@@ -198,16 +219,21 @@ export const DeckPlugin = ({
198
219
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
199
220
  clientPlugin = resolvePlugin(plugins, parseClientPlugin);
200
221
 
201
- // prettier-ignore
202
222
  layout
203
223
  .prop({ key: 'layoutMode', storageKey: 'layout-mode', type: LocalStorageStore.enum<LayoutMode>() })
204
224
  .prop({ key: 'sidebarOpen', storageKey: 'sidebar-open', type: LocalStorageStore.bool() })
205
- .prop({ key: 'complementarySidebarOpen', storageKey: 'complementary-sidebar-open', type: LocalStorageStore.bool() });
225
+ .prop({
226
+ key: 'complementarySidebarOpen',
227
+ storageKey: 'complementary-sidebar-open',
228
+ type: LocalStorageStore.bool(),
229
+ });
230
+
231
+ deck.prop({
232
+ key: 'plankSizing',
233
+ storageKey: 'plank-sizing',
234
+ type: LocalStorageStore.json<Record<string, number>>(),
235
+ });
206
236
 
207
- // prettier-ignore
208
- deck.prop({ key: 'plankSizing', storageKey: 'plank-sizing', type: LocalStorageStore.json<Record<string, number>>() });
209
-
210
- // prettier-ignore
211
237
  location
212
238
  .prop({ key: 'active', storageKey: 'active', type: LocalStorageStore.json<LayoutParts>() })
213
239
  .prop({ key: 'closed', storageKey: 'closed', type: LocalStorageStore.json<string[]>() });
@@ -220,14 +246,17 @@ export const DeckPlugin = ({
220
246
  }),
221
247
  );
222
248
 
223
- // prettier-ignore
224
249
  settings
225
- .prop({ key: 'showFooter', storageKey: 'show-footer', type: LocalStorageStore.bool() })
250
+ .prop({ key: 'showHints', storageKey: 'show-hints', type: LocalStorageStore.bool() })
226
251
  .prop({ key: 'customSlots', storageKey: 'customSlots', type: LocalStorageStore.bool() })
227
252
  .prop({ key: 'flatDeck', storageKey: 'flatDeck', type: LocalStorageStore.bool() })
228
253
  .prop({ key: 'enableNativeRedirect', storageKey: 'enable-native-redirect', type: LocalStorageStore.bool() })
229
254
  .prop({ key: 'disableDeck', storageKey: 'disable-deck', type: LocalStorageStore.bool() }) // Deprecated.
230
- .prop({ key: 'newPlankPositioning', storageKey: 'newPlankPositioning', type: LocalStorageStore.enum<NewPlankPositioning>() })
255
+ .prop({
256
+ key: 'newPlankPositioning',
257
+ storageKey: 'newPlankPositioning',
258
+ type: LocalStorageStore.enum<NewPlankPositioning>(),
259
+ })
231
260
  .prop({ key: 'overscroll', storageKey: 'overscroll', type: LocalStorageStore.enum<Overscroll>() });
232
261
 
233
262
  if (!isSocket && settings.values.enableNativeRedirect) {
@@ -237,7 +266,7 @@ export const DeckPlugin = ({
237
266
  handleNavigation = async () => {
238
267
  const pathname = window.location.pathname;
239
268
  if (pathname === '/reset') {
240
- location.values.active = { sidebar: [{ id: NAV_ID }] };
269
+ handleSetLocation({ sidebar: [{ id: NAV_ID }] });
241
270
  location.values.closed = [];
242
271
  layout.values.layoutMode = 'solo';
243
272
  window.location.pathname = '/';
@@ -250,7 +279,7 @@ export const DeckPlugin = ({
250
279
  }
251
280
 
252
281
  const startingLayout = removePart(location.values.active, 'solo');
253
- location.values.active = mergeLayoutParts(layoutFromUri, startingLayout);
282
+ handleSetLocation(mergeLayoutParts(layoutFromUri, startingLayout));
254
283
  layout.values.layoutMode = 'solo';
255
284
  };
256
285
 
@@ -258,23 +287,13 @@ export const DeckPlugin = ({
258
287
  window.addEventListener('popstate', handleNavigation);
259
288
 
260
289
  unsubscriptionCallbacks.push(
261
- effect(() => {
262
- const selectedPath = soloPartToUri(location.values.active);
263
- // TODO(thure): In some browsers, this only preserves the most recent state change, even though this is not `history.replace`…
264
- history.pushState(null, '', `/${selectedPath}${window.location.search}`);
265
- }),
266
- );
267
-
268
- unsubscriptionCallbacks.push(
269
- effect(() => {
270
- const soloId = location.values.active.solo?.[0].id;
271
- if (layout.values.layoutMode === 'solo' && soloId && layout.values.scrollIntoView !== soloId) {
272
- void intentPlugin?.provides.intent.dispatch({
273
- action: LayoutAction.SCROLL_INTO_VIEW,
274
- data: { id: soloId },
275
- });
276
- }
277
- }),
290
+ scheduledEffect(
291
+ () => ({ selectedPath: soloPartToUri(location.values.active) }),
292
+ ({ selectedPath }) => {
293
+ // TODO(thure): In some browsers, this only preserves the most recent state change, even though this is not `history.replace`…
294
+ history.pushState(null, '', `/${selectedPath}${window.location.search}`);
295
+ },
296
+ ),
278
297
  );
279
298
 
280
299
  layoutModeHistory.values.push(`${layout.values.layoutMode}`);
@@ -310,8 +329,7 @@ export const DeckPlugin = ({
310
329
  },
311
330
  properties: {
312
331
  label: ['toggle fullscreen label', { ns: DECK_PLUGIN }],
313
- icon: (props: IconProps) => <ArrowsOut {...props} />,
314
- iconSymbol: 'ph--arrows-out--regular',
332
+ icon: 'ph--arrows-out--regular',
315
333
  keyBinding: {
316
334
  macos: 'ctrl+meta+f',
317
335
  windows: 'shift+ctrl+f',
@@ -329,31 +347,27 @@ export const DeckPlugin = ({
329
347
  ),
330
348
  root: () => {
331
349
  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>
350
+ <DeckLayout
351
+ layoutParts={location.values.active}
352
+ showHints={settings.values.showHints}
353
+ overscroll={settings.values.overscroll}
354
+ flatDeck={settings.values.flatDeck}
355
+ slots={settings.values.customSlots ? customSlots : undefined}
356
+ toasts={layout.values.toasts}
357
+ onDismissToast={(id) => {
358
+ const index = layout.values.toasts.findIndex((toast) => toast.id === id);
359
+ if (index !== -1) {
360
+ // Allow time for the toast to animate out.
361
+ // TODO(burdon): Factor out and unregister timeout.
362
+ setTimeout(() => {
363
+ if (layout.values.toasts[index].id === currentUndoId) {
364
+ currentUndoId = undefined;
365
+ }
366
+ layout.values.toasts.splice(index, 1);
367
+ }, 1_000);
368
+ }
369
+ }}
370
+ />
357
371
  );
358
372
  },
359
373
  surface: {
@@ -472,7 +486,7 @@ export const DeckPlugin = ({
472
486
  }
473
487
  });
474
488
 
475
- location.values.active = newLayout;
489
+ handleSetLocation(newLayout);
476
490
  });
477
491
 
478
492
  const ids = openIds(location.values.active);
@@ -522,10 +536,12 @@ export const DeckPlugin = ({
522
536
  const layoutEntry = { id: data.id };
523
537
  const effectivePart = getEffectivePart(data.part, layout.values.layoutMode);
524
538
 
525
- location.values.active = openEntry(location.values.active, effectivePart, layoutEntry, {
526
- positioning: data.positioning ?? settings.values.newPlankPositioning,
527
- pivotId: data.pivotId,
528
- });
539
+ handleSetLocation(
540
+ openEntry(location.values.active, effectivePart, layoutEntry, {
541
+ positioning: data.positioning ?? settings.values.newPlankPositioning,
542
+ pivotId: data.pivotId,
543
+ }),
544
+ );
529
545
 
530
546
  const intents = [];
531
547
  if (data.scrollIntoView && layout.values.layoutMode === 'deck') {
@@ -561,7 +577,11 @@ export const DeckPlugin = ({
561
577
  }
562
578
  });
563
579
 
564
- location.values.active = newLayout;
580
+ handleSetLocation(newLayout);
581
+ // TODO(wittjosiah): This needs to also set the closed state.
582
+ // The closed state should be the existing closed state plus the newly closed ids.
583
+ // The closed state should also be updated when opening entries to remove the id from closed.
584
+ // When SET is called the closed ids should also be calculated and set.
565
585
  return { data: true };
566
586
  });
567
587
  }
@@ -570,7 +590,7 @@ export const DeckPlugin = ({
570
590
  case NavigationAction.SET: {
571
591
  return batch(() => {
572
592
  if (isLayoutParts(intent.data?.activeParts)) {
573
- location.values.active = intent.data!.activeParts;
593
+ handleSetLocation(intent.data!.activeParts);
574
594
  }
575
595
  return { data: true };
576
596
  });
@@ -581,10 +601,12 @@ export const DeckPlugin = ({
581
601
  if (isLayoutAdjustment(intent.data)) {
582
602
  const adjustment = intent.data;
583
603
  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
- });
604
+ handleSetLocation(
605
+ incrementPlank(location.values.active, {
606
+ type: adjustment.type,
607
+ layoutCoordinate: adjustment.layoutCoordinate,
608
+ }),
609
+ );
588
610
  }
589
611
 
590
612
  if (adjustment.type === 'solo') {
@@ -6,10 +6,13 @@ import React from 'react';
6
6
 
7
7
  import { Surface } from '@dxos/app-framework';
8
8
  import { useGraph } from '@dxos/plugin-graph';
9
+ import { useAttended } from '@dxos/react-ui-attention';
9
10
 
10
11
  import { useNode, useNodeActionExpander } from '../../hooks';
11
12
 
12
- export const ActiveNode = ({ id }: { id?: string }) => {
13
+ // TODO(burdon): Factor out to effect in plugin set document title.
14
+ export const ActiveNode = () => {
15
+ const [id] = useAttended();
13
16
  const { graph } = useGraph();
14
17
  const activeNode = useNode(graph, id);
15
18
  useNodeActionExpander(activeNode);
@@ -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 LayoutCoordinate,
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,80 @@ 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;
22
- layoutParts: LayoutParts;
28
+ panel?: string;
23
29
  flatDeck?: boolean;
24
30
  };
25
31
 
26
- export const ComplementarySidebar = ({ id, layoutParts, flatDeck }: ComplementarySidebarProps) => {
32
+ type Panel = { id: string; icon: string };
33
+
34
+ // TODO(burdon): Should be provided by plugins.
35
+ const panels: Panel[] = [
36
+ { id: 'settings', icon: 'ph--gear--regular' },
37
+ { id: 'comments', icon: 'ph--chat-text--regular' },
38
+ { id: 'automation', icon: 'ph--atom--regular' },
39
+ { id: 'debug', icon: 'ph--bug--regular' },
40
+ ];
41
+
42
+ const getPanel = (id?: string): Panel['id'] => {
43
+ const panel = panels.find((p) => p.id === id) ?? panels[0];
44
+ return panel.id;
45
+ };
46
+
47
+ export const ComplementarySidebar = ({ panel, flatDeck }: ComplementarySidebarProps) => {
27
48
  const { popoverAnchorId } = useLayout();
49
+ const attended = useAttended();
50
+ const part = getPanel(panel);
51
+ const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${part}` : undefined;
28
52
  const { graph } = useGraph();
29
53
  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
-
54
+ const dispatch = useIntentDispatcher();
33
55
  useNodeActionExpander(node);
34
56
 
57
+ const actions = useMemo(
58
+ () =>
59
+ panels.map(({ id, icon }) => ({
60
+ id: `complementary-${id}`,
61
+ data: () => {
62
+ void dispatch({ action: NavigationAction.OPEN, data: { activeParts: { complementary: id } } });
63
+ },
64
+ properties: {
65
+ label: [`open ${id} label`, { ns: DECK_PLUGIN }],
66
+ icon,
67
+ menuItemType: 'toggle',
68
+ isChecked: part === id,
69
+ },
70
+ })),
71
+ [part],
72
+ );
73
+
74
+ // TODO(wittjosiah): Ensure that id is always defined.
75
+ const coordinate: LayoutCoordinate = useMemo(() => ({ entryId: id ?? 'unknown', part: 'complementary' }), [id]);
76
+
35
77
  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
- />
47
- <Surface
48
- role='article'
49
- data={{ subject: node.data, part: 'complementary', popoverAnchorId }}
50
- limit={1}
51
- fallback={PlankContentError}
52
- placeholder={<PlankLoading />}
53
- />
78
+ <Main.ComplementarySidebar>
79
+ <div role='none' className={mx(deckGrid, 'grid-cols-1 bs-full')}>
80
+ <NodePlankHeading
81
+ coordinate={coordinate}
82
+ node={node}
83
+ popoverAnchorId={popoverAnchorId}
84
+ flatDeck={flatDeck}
85
+ actions={actions}
86
+ />
87
+ <div className='row-span-2 divide-y divide-separator'>
88
+ {node && (
89
+ <Surface
90
+ role={`complementary--${part}`}
91
+ data={{ subject: node.properties.object, popoverAnchorId }}
92
+ fallback={PlankContentError}
93
+ placeholder={<PlankLoading />}
94
+ />
95
+ )}
54
96
  </div>
55
- ) : null}
97
+ </div>
56
98
  </Main.ComplementarySidebar>
57
99
  );
58
100
  };