@dxos/plugin-deck 0.6.8-staging.77f93a3 → 0.6.8-staging.dec6b33

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,
@@ -32,28 +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
- // TODO(burdon): Rename planks or just items?
41
36
  layoutParts: LayoutParts;
42
37
  attention: Attention;
38
+ toasts: ToastSchema[];
39
+ flatDeck?: boolean;
40
+ overscroll: Overscroll;
41
+ showHintsFooter: boolean;
43
42
  slots?: {
44
43
  wallpaper?: { classNames?: string };
45
44
  };
45
+ onDismissToast: (id: string) => void;
46
46
  };
47
47
 
48
48
  export const DeckLayout = ({
49
- showHintsFooter,
49
+ layoutParts,
50
+ attention,
50
51
  toasts,
51
- onDismissToast,
52
52
  flatDeck,
53
- attention,
54
- layoutParts,
55
- slots,
56
53
  overscroll,
54
+ showHintsFooter,
55
+ slots,
56
+ onDismissToast,
57
57
  }: DeckLayoutProps) => {
58
58
  const context = useLayout();
59
59
  const {
@@ -69,9 +69,7 @@ export const DeckLayout = ({
69
69
  } = context;
70
70
  const { t } = useTranslation(DECK_PLUGIN);
71
71
  const { plankSizing } = useDeckContext();
72
-
73
- const searchEnabled = !!usePlugin('dxos.org/plugin/search');
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(() => {
@@ -83,19 +81,37 @@ export const DeckLayout = ({
83
81
 
84
82
  const activeId = useMemo(() => Array.from(attention.attended ?? [])[0], [attention.attended]);
85
83
 
86
- // TODO(burdon): Very specific args (move local to file or create struct?)
87
- const overscrollAmount = calculateOverscroll(
88
- layoutMode,
89
- layoutParts,
90
- plankSizing,
91
- sidebarOpen,
92
- complementarySidebarOpen,
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
+ };
95
110
 
96
- const isEmpty =
97
- (layoutMode === 'solo' && (!layoutParts.solo || layoutParts.solo.length === 0)) ||
98
- (layoutMode === 'deck' && (!layoutParts.main || layoutParts.main.length === 0));
111
+ const padding =
112
+ layoutMode === 'deck' && overscroll === 'centering'
113
+ ? calculateOverscroll(layoutParts.main, plankSizing, sidebarOpen, complementarySidebarOpen)
114
+ : {};
99
115
 
100
116
  if (layoutMode === 'fullscreen') {
101
117
  return <Fullscreen id={fullScreenSlug} />;
@@ -114,7 +130,7 @@ export const DeckLayout = ({
114
130
  }
115
131
  }}
116
132
  >
117
- {/* TODO(burdon): Document this. */}
133
+ {/* TODO(burdon): Factor out hook to set document title. */}
118
134
  <ActiveNode id={activeId} />
119
135
 
120
136
  <Main.Root
@@ -149,73 +165,63 @@ export const DeckLayout = ({
149
165
  <Surface role='notch-end' />
150
166
  </Main.Notch>
151
167
 
152
- {/* Sidebars */}
168
+ {/* Left sidebar. */}
153
169
  <Sidebar attention={attention} layoutParts={layoutParts} />
154
170
 
171
+ {/* Right sidebar. */}
155
172
  <ComplementarySidebar id={complementarySlug} layoutParts={layoutParts} flatDeck={flatDeck} />
156
173
 
157
174
  {/* Dialog overlay to dismiss dialogs. */}
158
175
  <Main.Overlay />
159
176
 
160
177
  {/* No content. */}
161
- {isEmpty && (
162
- <Main.Content>
178
+ {parts.length === 0 && (
179
+ <Main.Content handlesFocus>
163
180
  <ContentEmpty />
164
181
  </Main.Content>
165
182
  )}
166
183
 
167
184
  {/* Solo/deck mode. */}
168
- {!isEmpty && (
169
- <Main.Content bounce classNames={['grid', 'block-end-[--statusbar-size]']}>
170
- <Deck.Root
171
- classNames={[
172
- !flatDeck && 'surface-deck',
173
- layoutMode === 'deck' && [
174
- 'absolute inset-0',
175
- 'transition-[padding] duration-200 ease-in-out',
176
- slots?.wallpaper?.classNames,
177
- ],
178
- ]}
179
- solo={layoutMode === 'solo'}
180
- style={{ ...overscrollAmount }}
181
- >
182
- {layoutMode === 'solo' &&
183
- layoutParts.solo?.map((layoutEntry) => {
184
- return (
185
- <Plank
186
- key={layoutEntry.id}
187
- entry={layoutEntry}
188
- layoutParts={layoutParts}
189
- part='solo'
190
- flatDeck={flatDeck}
191
- />
192
- );
193
- })}
194
- {layoutMode === 'deck' &&
195
- layoutParts.main?.map((layoutEntry) => {
196
- return (
197
- <Plank
198
- key={layoutEntry.id}
199
- entry={layoutEntry}
200
- layoutParts={layoutParts}
201
- part='main'
202
- resizeable
203
- flatDeck={flatDeck}
204
- searchEnabled={searchEnabled}
205
- />
206
- );
207
- })}
208
- </Deck.Root>
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) => (
202
+ <Plank
203
+ key={layoutEntry.id}
204
+ entry={layoutEntry}
205
+ layoutParts={layoutParts}
206
+ part={layoutMode === 'solo' && layoutEntry.id === activeId ? 'solo' : 'main'}
207
+ flatDeck={flatDeck}
208
+ searchEnabled={!!searchPlugin}
209
+ resizeable={layoutMode === 'deck'}
210
+ classNames={showPlank(layoutEntry) ? '' : 'hidden'}
211
+ />
212
+ ))}
213
+ </Deck.Root>
214
+ </div>
209
215
  </Main.Content>
210
216
  )}
211
217
 
212
- {/* Note: This is not Main.Content */}
213
- <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]'>
214
220
  <Surface role='status-bar' limit={1} />
215
221
  </Main.Content>
216
222
 
217
223
  {/* Help hints. */}
218
- {/* TODO(burdon): Make surface roles/names fully-qualified? */}
224
+ {/* TODO(burdon): Need to make room for this in status bar. */}
219
225
  {showHintsFooter && (
220
226
  <div className='fixed bottom-0 left-0 right-0 h-[32px] z-[1] flex justify-center'>
221
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, useLayoutEffect, useRef } from 'react';
6
+ import React, { type KeyboardEvent, useCallback, useLayoutEffect, useRef } from 'react';
7
7
 
8
8
  import {
9
9
  LayoutAction,
@@ -48,8 +48,10 @@ export const Plank = ({ entry, layoutParts, part, resizeable, flatDeck, searchEn
48
48
  const { plankSizing } = useDeckContext();
49
49
  const { graph } = useGraph();
50
50
  const node = useNode(graph, entry.id);
51
+ const rootElement = useRef<HTMLDivElement | null>(null);
51
52
 
52
53
  const attendableAttrs = createAttendableAttributes(entry.id);
54
+ const coordinate: LayoutCoordinate = { part, entryId: entry.id };
53
55
 
54
56
  const size = plankSizing?.[entry.id] as number | undefined;
55
57
  const setSize = useCallback(
@@ -59,18 +61,29 @@ export const Plank = ({ entry, layoutParts, part, resizeable, flatDeck, searchEn
59
61
  [dispatch, entry.id],
60
62
  );
61
63
 
62
- 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
+ }, []);
63
70
 
64
- const ref = useRef<HTMLDivElement | null>(null);
65
71
  useLayoutEffect(() => {
66
72
  if (scrollIntoView === entry.id) {
67
- ref.current?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
73
+ rootElement.current?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
68
74
  }
69
75
  }, [scrollIntoView]);
70
76
 
71
77
  return (
72
- <NaturalPlank.Root size={size} setSize={setSize}>
73
- <NaturalPlank.Content {...attendableAttrs} ref={ref} classNames={[!flatDeck && 'surface-base', classNames]}>
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']}>
74
87
  {node ? (
75
88
  <>
76
89
  <NodePlankHeading
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,96 +2,89 @@
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,
12
- layoutParts: LayoutParts,
55
+ planks: LayoutEntry[] | undefined,
13
56
  plankSizing: Record<string, number>,
14
57
  sidebarOpen: boolean,
15
58
  complementarySidebarOpen: boolean,
16
- overscroll: Overscroll,
17
- ) => {
18
- if (!(layoutMode === 'deck' && overscroll === 'centering')) {
19
- return;
20
- }
21
- if (!layoutParts.main || layoutParts.main.length === 0) {
59
+ ): Pick<CSSProperties, 'paddingLeft' | 'paddingRight'> | undefined => {
60
+ if (!planks?.length) {
22
61
  return;
23
62
  }
24
63
 
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
-
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';
72
66
  const complementarySidebarWidth = complementarySidebarOpen ? '360px' : '0px';
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
  };