@dxos/plugin-deck 0.6.8-main.046e6cf → 0.6.8-staging.63bcb81

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.
@@ -3,11 +3,12 @@
3
3
  //
4
4
 
5
5
  import { Sidebar as MenuIcon } from '@phosphor-icons/react';
6
- import React, { useMemo } from 'react';
6
+ import React, { useEffect, useMemo, useRef } from 'react';
7
7
 
8
8
  import {
9
9
  SLUG_PATH_SEPARATOR,
10
10
  type Attention,
11
+ type LayoutEntry,
11
12
  type LayoutParts,
12
13
  Surface,
13
14
  type Toast as ToastSchema,
@@ -16,7 +17,7 @@ import {
16
17
  } from '@dxos/app-framework';
17
18
  import { Button, Dialog, Main, Popover, useTranslation } from '@dxos/react-ui';
18
19
  import { Deck } from '@dxos/react-ui-deck';
19
- import { getSize, mx } from '@dxos/react-ui-theme';
20
+ import { getSize } from '@dxos/react-ui-theme';
20
21
 
21
22
  import { ActiveNode } from './ActiveNode';
22
23
  import { ComplementarySidebar } from './ComplementarySidebar';
@@ -32,30 +33,27 @@ import { useDeckContext } from '../DeckContext';
32
33
  import { useLayout } from '../LayoutContext';
33
34
 
34
35
  export type DeckLayoutProps = {
35
- showHintsFooter: boolean;
36
- overscroll: Overscroll;
37
- flatDeck?: boolean;
38
- toasts: ToastSchema[];
39
- onDismissToast: (id: string) => void;
40
36
  layoutParts: LayoutParts;
41
37
  attention: Attention;
42
- // TODO(Zan): Deprecate slots.
38
+ toasts: ToastSchema[];
39
+ flatDeck?: boolean;
40
+ overscroll: Overscroll;
41
+ showHintsFooter: boolean;
43
42
  slots?: {
44
43
  wallpaper?: { classNames?: string };
45
- deck?: { classNames?: string };
46
- plank?: { classNames?: string };
47
44
  };
45
+ onDismissToast: (id: string) => void;
48
46
  };
49
47
 
50
48
  export const DeckLayout = ({
51
- showHintsFooter,
49
+ layoutParts,
50
+ attention,
52
51
  toasts,
53
- onDismissToast,
54
52
  flatDeck,
55
- attention,
56
- layoutParts,
57
- slots,
58
53
  overscroll,
54
+ showHintsFooter,
55
+ slots,
56
+ onDismissToast,
59
57
  }: DeckLayoutProps) => {
60
58
  const context = useLayout();
61
59
  const {
@@ -71,7 +69,7 @@ export const DeckLayout = ({
71
69
  } = context;
72
70
  const { t } = useTranslation(DECK_PLUGIN);
73
71
  const { plankSizing } = useDeckContext();
74
-
72
+ const searchPlugin = usePlugin('dxos.org/plugin/search');
75
73
  const fullScreenSlug = useMemo(() => firstIdInPart(layoutParts, 'fullScreen'), [layoutParts]);
76
74
 
77
75
  const complementarySlug = useMemo(() => {
@@ -81,17 +79,39 @@ export const DeckLayout = ({
81
79
  }
82
80
  }, [layoutParts]);
83
81
 
84
- const searchEnabled = !!usePlugin('dxos.org/plugin/search');
85
82
  const activeId = useMemo(() => Array.from(attention.attended ?? [])[0], [attention.attended]);
86
83
 
87
- const overscrollAmount = calculateOverscroll(
88
- layoutMode,
89
- sidebarOpen,
90
- complementarySidebarOpen,
91
- layoutParts,
92
- plankSizing,
93
- overscroll,
94
- );
84
+ const deckRef = useRef<HTMLDivElement | null>(null);
85
+ useEffect(() => {
86
+ // TODO(burdon): Can we prevent the need to re-scroll since the planks are preserved?
87
+ // E.g., hide the deck and just move the solo article?
88
+ if (layoutMode === 'deck' && activeId) {
89
+ // setTimeout(() => {
90
+ // const el = deckRef.current?.querySelector(`article[data-attendable-id="${activeId}"]`);
91
+ // el?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
92
+ // }, 0);
93
+ }
94
+ }, [layoutMode, activeId]);
95
+
96
+ // TODO(burdon): Needs cleaning up.
97
+ const parts: LayoutEntry[] = useMemo(() => {
98
+ const parts = [...(layoutParts.main ?? [])];
99
+ for (const part of layoutParts.solo ?? []) {
100
+ if (!parts.find((entry) => entry.id === part.id)) {
101
+ parts.push(part);
102
+ }
103
+ }
104
+ return parts;
105
+ }, [layoutParts.main, layoutParts.solo]);
106
+
107
+ const showPlank = (part: LayoutEntry) => {
108
+ return layoutMode === 'deck' || layoutParts.solo?.find((entry) => entry.id === part.id);
109
+ };
110
+
111
+ const padding =
112
+ layoutMode === 'deck' && overscroll === 'centering'
113
+ ? calculateOverscroll(layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen)
114
+ : {};
95
115
 
96
116
  if (layoutMode === 'fullscreen') {
97
117
  return <Fullscreen id={fullScreenSlug} />;
@@ -110,6 +130,7 @@ export const DeckLayout = ({
110
130
  }
111
131
  }}
112
132
  >
133
+ {/* TODO(burdon): Factor out hook to set document title. */}
113
134
  <ActiveNode id={activeId} />
114
135
 
115
136
  <Main.Root
@@ -144,83 +165,63 @@ export const DeckLayout = ({
144
165
  <Surface role='notch-end' />
145
166
  </Main.Notch>
146
167
 
147
- {/* Sidebars */}
168
+ {/* Left sidebar. */}
148
169
  <Sidebar attention={attention} layoutParts={layoutParts} />
149
170
 
171
+ {/* Right sidebar. */}
150
172
  <ComplementarySidebar id={complementarySlug} layoutParts={layoutParts} flatDeck={flatDeck} />
151
173
 
152
174
  {/* Dialog overlay to dismiss dialogs. */}
153
175
  <Main.Overlay />
154
176
 
155
- {/* Main content surface. */}
156
- {layoutMode === 'deck' && layoutParts.main && layoutParts.main.length > 0 && (
157
- <Main.Content bounce classNames={['grid', 'block-end-[--statusbar-size]']}>
158
- <div role='none' className='relative'>
159
- <Deck.Root
160
- classNames={mx(
161
- 'absolute inset-0',
162
- !flatDeck && 'surface-deck',
163
- slots?.wallpaper?.classNames,
164
- slots?.deck?.classNames,
165
- 'transition-[padding] duration-200 ease-in-out',
166
- )}
167
- style={{ ...overscrollAmount }}
168
- >
169
- {layoutParts.main.map((layoutEntry) => {
170
- return (
171
- <Plank
172
- key={layoutEntry.id}
173
- entry={layoutEntry}
174
- layoutParts={layoutParts}
175
- part='main'
176
- resizeable
177
- flatDeck={flatDeck}
178
- searchEnabled={searchEnabled}
179
- />
180
- );
181
- })}
182
- </Deck.Root>
183
- </div>
177
+ {/* No content. */}
178
+ {parts.length === 0 && (
179
+ <Main.Content handlesFocus>
180
+ <ContentEmpty />
184
181
  </Main.Content>
185
182
  )}
186
183
 
187
- {/* Solo main content surface. */}
188
- {layoutMode === 'solo' && layoutParts.solo && layoutParts.solo.length > 0 && (
189
- <Main.Content bounce classNames={['grid', 'block-end-[--statusbar-size]']}>
190
- <Deck.Root
191
- classNames={[!flatDeck && 'surface-deck', slots?.wallpaper?.classNames, slots?.deck?.classNames]}
192
- solo={true}
193
- >
194
- {layoutParts.solo.map((layoutEntry) => {
195
- return (
184
+ {/* Solo/deck mode. */}
185
+ {parts.length !== 0 && (
186
+ <Main.Content bounce classNames='grid block-end-[--statusbar-size]' handlesFocus>
187
+ <div role='none' className={layoutMode === 'solo' ? 'contents' : 'relative'}>
188
+ <Deck.Root
189
+ ref={deckRef}
190
+ solo={layoutMode === 'solo'}
191
+ style={padding}
192
+ classNames={[
193
+ !flatDeck && 'surface-deck',
194
+ layoutMode === 'deck' && [
195
+ 'absolute inset-0',
196
+ 'transition-[padding] duration-200 ease-in-out',
197
+ slots?.wallpaper?.classNames,
198
+ ],
199
+ ]}
200
+ >
201
+ {parts.map((layoutEntry) => (
196
202
  <Plank
197
203
  key={layoutEntry.id}
198
204
  entry={layoutEntry}
199
205
  layoutParts={layoutParts}
200
- part='solo'
206
+ part={layoutMode === 'solo' && layoutEntry.id === activeId ? 'solo' : 'main'}
201
207
  flatDeck={flatDeck}
202
- classNames={slots?.plank?.classNames}
208
+ searchEnabled={!!searchPlugin}
209
+ resizeable={layoutMode === 'deck'}
210
+ classNames={showPlank(layoutEntry) ? '' : 'hidden'}
203
211
  />
204
- );
205
- })}
206
- </Deck.Root>
207
- </Main.Content>
208
- )}
209
-
210
- {((layoutMode === 'solo' && (!layoutParts.solo || layoutParts.solo.length === 0)) ||
211
- (layoutMode === 'deck' && (!layoutParts.main || layoutParts.main.length === 0))) && (
212
- <Main.Content>
213
- <ContentEmpty />
212
+ ))}
213
+ </Deck.Root>
214
+ </div>
214
215
  </Main.Content>
215
216
  )}
216
217
 
217
- {/* Note: This is not Main.Content */}
218
- <Main.Content role='none' classNames={['fixed inset-inline-0 block-end-0 z-[2]']}>
218
+ {/* TODO(burdon): Why Main.Content? */}
219
+ <Main.Content role='none' classNames='fixed inset-inline-0 block-end-0 z-[2]'>
219
220
  <Surface role='status-bar' limit={1} />
220
221
  </Main.Content>
221
222
 
222
223
  {/* Help hints. */}
223
- {/* TODO(burdon): Make surface roles/names fully-qualified. */}
224
+ {/* TODO(burdon): Need to make room for this in status bar. */}
224
225
  {showHintsFooter && (
225
226
  <div className='fixed bottom-0 left-0 right-0 h-[32px] z-[1] flex justify-center'>
226
227
  <Surface role='hints' limit={1} />
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Plus } from '@phosphor-icons/react';
6
- import React, { useCallback } from 'react';
6
+ import React, { type KeyboardEvent, useCallback, useLayoutEffect, useRef } from 'react';
7
7
 
8
8
  import {
9
9
  LayoutAction,
@@ -24,7 +24,6 @@ import { Plank as NaturalPlank } from '@dxos/react-ui-deck';
24
24
  import { NodePlankHeading } from './NodePlankHeading';
25
25
  import { PlankContentError, PlankError } from './PlankError';
26
26
  import { PlankLoading } from './PlankLoading';
27
- import { NAV_ID } from './constants';
28
27
  import { DeckAction } from '../../DeckPlugin';
29
28
  import { useNode } from '../../hooks';
30
29
  import { DECK_PLUGIN } from '../../meta';
@@ -49,8 +48,10 @@ export const Plank = ({ entry, layoutParts, part, resizeable, flatDeck, searchEn
49
48
  const { plankSizing } = useDeckContext();
50
49
  const { graph } = useGraph();
51
50
  const node = useNode(graph, entry.id);
51
+ const rootElement = useRef<HTMLDivElement | null>(null);
52
52
 
53
53
  const attendableAttrs = createAttendableAttributes(entry.id);
54
+ const coordinate: LayoutCoordinate = { part, entryId: entry.id };
54
55
 
55
56
  const size = plankSizing?.[entry.id] as number | undefined;
56
57
  const setSize = useCallback(
@@ -60,23 +61,36 @@ export const Plank = ({ entry, layoutParts, part, resizeable, flatDeck, searchEn
60
61
  [dispatch, entry.id],
61
62
  );
62
63
 
63
- const coordinate: LayoutCoordinate = { part, entryId: entry.id };
64
+ // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
65
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
66
+ if (event.target === event.currentTarget && event.key === 'Escape') {
67
+ rootElement.current?.closest('main')?.focus();
68
+ }
69
+ }, []);
70
+
71
+ useLayoutEffect(() => {
72
+ if (scrollIntoView === entry.id) {
73
+ rootElement.current?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
74
+ }
75
+ }, [scrollIntoView]);
64
76
 
65
77
  return (
66
- <NaturalPlank.Root size={size} setSize={setSize}>
67
- <NaturalPlank.Content
68
- {...attendableAttrs}
69
- classNames={[!flatDeck && 'surface-base', classNames]}
70
- scrollIntoViewOnMount={entry.id === scrollIntoView}
71
- suppressAutofocus={entry.id === NAV_ID || !!node?.properties?.managesAutofocus}
72
- >
78
+ <NaturalPlank.Root
79
+ size={size}
80
+ setSize={setSize}
81
+ classNames={classNames}
82
+ {...attendableAttrs}
83
+ onKeyDown={handleKeyDown}
84
+ ref={rootElement}
85
+ >
86
+ <NaturalPlank.Content classNames={[!flatDeck && 'surface-base']}>
73
87
  {node ? (
74
88
  <>
75
89
  <NodePlankHeading
90
+ id={entry.id}
91
+ node={node}
76
92
  layoutPart={coordinate.part}
77
93
  layoutParts={layoutParts}
78
- node={node}
79
- id={entry.id}
80
94
  popoverAnchorId={popoverAnchorId}
81
95
  flatDeck={flatDeck}
82
96
  />
package/src/layout.ts CHANGED
@@ -7,12 +7,12 @@ import {
7
7
  type LayoutAdjustment,
8
8
  type LayoutCoordinate,
9
9
  type LayoutEntry,
10
+ type LayoutPart,
10
11
  type LayoutParts,
11
12
  SLUG_ENTRY_SEPARATOR,
12
13
  SLUG_KEY_VALUE_SEPARATOR,
13
14
  SLUG_LIST_SEPARATOR,
14
15
  SLUG_PATH_SEPARATOR,
15
- type LayoutPart,
16
16
  } from '@dxos/app-framework';
17
17
 
18
18
  import { type NewPlankPositioning } from './types';
@@ -161,7 +161,9 @@ export const mergeLayoutParts = (...layoutParts: LayoutParts[]): LayoutParts =>
161
161
  };
162
162
 
163
163
  //
164
- // --- URI Projection ---------------------------------------------------------
164
+ // URI Projection
165
+ //
166
+
165
167
  const parseLayoutEntry = (itemString: string): LayoutEntry => {
166
168
  // Layout entries are in the form of 'id~path' or just 'id'
167
169
  const [id, path] = itemString.split(SLUG_PATH_SEPARATOR);
@@ -2,70 +2,64 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type LayoutMode, type LayoutParts } from '@dxos/app-framework';
6
- import { PLANK_DEFAULTS } from '@dxos/react-ui-deck';
5
+ import type { CSSProperties } from 'react';
7
6
 
8
- import { type Overscroll } from '../types';
7
+ import { type LayoutEntry } from '@dxos/app-framework';
8
+ import { PLANK_DEFAULTS } from '@dxos/react-ui-deck';
9
9
 
10
+ /**
11
+ * ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
12
+ * | Overscroll Padding Calculation for Centering Planks on Screen. │
13
+ * ├────────────────────────────────────────────────────────────────────────────────────────────────────┤
14
+ * │ NOTE(Zan): I found the way you calculate the overscroll padding to center a plank on the screen │
15
+ * │ at the edges of the scroll context a bit confusing, so I've diagrammed it here. │
16
+ * │ │
17
+ * │ Multiple Planks: │
18
+ * │ ─────────────── │
19
+ * | Use the following overscroll padding calculation centering the boundary planks on the SCREEN. │
20
+ * │ │
21
+ * │ Left Padding: Right Padding: │
22
+ * │ ┌───┬────┬──────────────────┬──────┐ ┌──────┬──────────────────┬────┬───┐ │
23
+ * │ │ │████│ Ideal │ │ │ │ Ideal │████│ │ │
24
+ * │ │ S │█PL█│ first │ │ │ │ last │█PR█│ C │ │
25
+ * │ │ │████│ plank │ │ │ │ plank │████│ │ │
26
+ * │ └───┴────┴──────────────────┴──────┘ └──────┴──────────────────┴────┴───┘ │
27
+ * │ <--------- screen width -----------> <---------- screen width ----------> │
28
+ * │ │
29
+ * │ PL = ((screen width - Plank Width) / 2) - S │
30
+ * │ PR = ((screen width - Plank Width) / 2) - C │
31
+ * │ │
32
+ * │ S = Sidebar width C = Complementary sidebar width │
33
+ * │ PL = Padding Left PR = Padding Right │
34
+ * │ │
35
+ * │ Single Plank: │
36
+ * │ ───────────── │
37
+ * │ For a single plank we use the following overscroll padding calculation to center the plank in │
38
+ * │ the content area: │
39
+ * │ │
40
+ * │ ┌───┬───────────────────────┬───┬───────────────────────┬───┐ │
41
+ * │ │ │███████████████████████│ │███████████████████████│ │ │
42
+ * │ │ S │█████ Left Padding ████│ P │████ Right Padding ████│ C │ │
43
+ * │ │ │███████████████████████│ │███████████████████████│ │ │
44
+ * │ └───┴───────────────────────┴───┴───────────────────────┴───┘ │
45
+ * │ <------------------------ screen width ---------------------> │
46
+ * │ │
47
+ * │ Left/Right Padding Width = (screen width - S - P - C) / 2 │
48
+ * │ │
49
+ * │ S = Sidebar width (may be 0) │
50
+ * │ P = Plank width (centered) │
51
+ * │ C = Complementary sidebar width (may be 0) │
52
+ * └────────────────────────────────────────────────────────────────────────────────────────────────────┘
53
+ */
10
54
  export const calculateOverscroll = (
11
- layoutMode: LayoutMode,
55
+ planks: LayoutEntry[] | undefined,
56
+ plankSizing: Record<string, number>,
12
57
  sidebarOpen: boolean,
13
58
  complementarySidebarOpen: boolean,
14
- layoutParts: LayoutParts,
15
- plankSizing: Record<string, number>,
16
- overscroll: Overscroll,
17
- ) => {
18
- if (!(layoutMode === 'deck' && overscroll === 'centering')) {
59
+ ): Pick<CSSProperties, 'paddingLeft' | 'paddingRight'> | undefined => {
60
+ if (!planks?.length) {
19
61
  return;
20
62
  }
21
- if (!layoutParts.main || layoutParts.main.length === 0) {
22
- return;
23
- }
24
-
25
- /**
26
- * ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
27
- * | Overscroll Padding Calculation for Centering Planks on Screen. │
28
- * ├────────────────────────────────────────────────────────────────────────────────────────────────────┤
29
- * │ NOTE(Zan): I found the way you calculate the overscroll padding to center a plank on the screen │
30
- * │ at the edges of the scroll context a bit confusing, so I've diagrammed it here. │
31
- * │ │
32
- * │ Multiple Planks: │
33
- * │ ─────────────── │
34
- * | Use the following overscroll padding calculation centering the boundary planks on the SCREEN. │
35
- * │ │
36
- * │ Left Padding: Right Padding: │
37
- * │ ┌───┬────┬──────────────────┬──────┐ ┌──────┬──────────────────┬────┬───┐ │
38
- * │ │ │████│ Ideal │ │ │ │ Ideal │████│ │ │
39
- * │ │ S │█PL█│ first │ │ │ │ last │█PR█│ C │ │
40
- * │ │ │████│ plank │ │ │ │ plank │████│ │ │
41
- * │ └───┴────┴──────────────────┴──────┘ └──────┴──────────────────┴────┴───┘ │
42
- * │ <--------- screen width -----------> <---------- screen width ----------> │
43
- * │ │
44
- * │ PL = ((screen width - Plank Width) / 2) - S │
45
- * │ PR = ((screen width - Plank Width) / 2) - C │
46
- * │ │
47
- * │ S = Sidebar width C = Complementary sidebar width │
48
- * │ PL = Padding Left PR = Padding Right │
49
- * │ │
50
- * │ Single Plank: │
51
- * │ ───────────── │
52
- * │ For a single plank we use the following overscroll padding calculation to center the plank in │
53
- * │ the content area: │
54
- * │ │
55
- * │ ┌───┬───────────────────────┬───┬───────────────────────┬───┐ │
56
- * │ │ │███████████████████████│ │███████████████████████│ │ │
57
- * │ │ S │█████ Left Padding ████│ P │████ Right Padding ████│ C │ │
58
- * │ │ │███████████████████████│ │███████████████████████│ │ │
59
- * │ └───┴───────────────────────┴───┴───────────────────────┴───┘ │
60
- * │ <------------------------ screen width ---------------------> │
61
- * │ │
62
- * │ Left/Right Padding Width = (screen width - S - P - C) / 2 │
63
- * │ │
64
- * │ S = Sidebar width (may be 0) │
65
- * │ P = Plank width (centered) │
66
- * │ C = Complementary sidebar width (may be 0) │
67
- * └────────────────────────────────────────────────────────────────────────────────────────────────────┘
68
- */
69
63
 
70
64
  // TODO(Zan): Move complementary sidebar size (360px), sidebar size (270px), plank resize handle size (20px) to CSS variables.
71
65
  const sidebarWidth = sidebarOpen ? '270px' : '0px';
@@ -73,25 +67,24 @@ export const calculateOverscroll = (
73
67
 
74
68
  const getPlankSize = (id: string) => (plankSizing[id] ?? PLANK_DEFAULTS.size).toFixed(2) + 'rem';
75
69
 
76
- if (layoutParts.main.length === 1) {
70
+ if (planks.length === 1) {
77
71
  // Center the plank in the content area.
78
-
79
- const plank = layoutParts.main[0];
72
+ const plank = planks[0];
80
73
  const plankSize = getPlankSize(plank.id);
81
74
  const overscrollPadding = `max(0px, calc(((100dvw - ${sidebarWidth} - ${complementarySidebarWidth} - (${plankSize} + 20px)) / 2)))`;
82
75
 
83
76
  return { paddingLeft: overscrollPadding, paddingRight: overscrollPadding };
84
77
  } else {
85
78
  // Center the plank on the screen.
79
+ const first = planks[0];
80
+ const firstSize = getPlankSize(first.id);
86
81
 
87
- const firstPlank = layoutParts.main[0];
88
- const firstPlankInlineSize = getPlankSize(firstPlank.id);
89
- const paddingLeft = `max(0px, calc(((100dvw - (${firstPlankInlineSize} + 20px)) / 2) - ${sidebarWidth}))`;
90
-
91
- const lastPlank = layoutParts.main[layoutParts.main.length - 1];
92
- const lastPlankInlineSize = getPlankSize(lastPlank.id);
93
- const paddingRight = `max(0px, calc(((100dvw - (${lastPlankInlineSize} + 20px)) / 2) - ${complementarySidebarWidth}))`;
82
+ const last = planks[planks.length - 1];
83
+ const lastSize = getPlankSize(last.id);
94
84
 
95
- return { paddingLeft, paddingRight };
85
+ return {
86
+ paddingLeft: `max(0px, calc(((100dvw - (${firstSize} + 20px)) / 2) - ${sidebarWidth}))`,
87
+ paddingRight: `max(0px, calc(((100dvw - (${lastSize} + 20px)) / 2) - ${complementarySidebarWidth}))`,
88
+ };
96
89
  }
97
90
  };