@dxos/plugin-deck 0.8.0 → 0.8.1-main.81238a8

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 (109) hide show
  1. package/dist/lib/browser/{app-graph-builder-IYHAGFA3.mjs → app-graph-builder-K4KVSHNT.mjs} +3 -3
  2. package/dist/lib/browser/{check-app-scheme-S3EYUPMF.mjs → check-app-scheme-6SS6I3RN.mjs} +2 -2
  3. package/dist/lib/browser/{chunk-N7TEPFVR.mjs → chunk-2WTHB3TG.mjs} +1 -1
  4. package/dist/lib/browser/{chunk-N7TEPFVR.mjs.map → chunk-2WTHB3TG.mjs.map} +2 -2
  5. package/dist/lib/browser/chunk-7A5DLPQ3.mjs +24 -0
  6. package/dist/lib/browser/{chunk-FT33W5CI.mjs → chunk-7X43JKZG.mjs} +34 -4
  7. package/dist/lib/browser/chunk-7X43JKZG.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-OEDK54N2.mjs → chunk-7YPIAXSW.mjs} +179 -72
  9. package/dist/lib/browser/chunk-7YPIAXSW.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-KANJBSIX.mjs → chunk-RZLH5F56.mjs} +5 -5
  11. package/dist/lib/browser/{chunk-KANJBSIX.mjs.map → chunk-RZLH5F56.mjs.map} +3 -3
  12. package/dist/lib/browser/{chunk-22AQ5IVX.mjs → chunk-WCNPMAR4.mjs} +2 -2
  13. package/dist/lib/browser/index.mjs +8 -7
  14. package/dist/lib/browser/index.mjs.map +2 -2
  15. package/dist/lib/browser/{intent-resolver-ZD67BRUI.mjs → intent-resolver-MEBOMCYI.mjs} +46 -17
  16. package/dist/lib/browser/intent-resolver-MEBOMCYI.mjs.map +7 -0
  17. package/dist/lib/browser/meta.json +1 -1
  18. package/dist/lib/browser/{react-root-2T6BCRK4.mjs → react-root-PPKFVPO7.mjs} +7 -7
  19. package/dist/lib/browser/{react-surface-GFMOJSIA.mjs → react-surface-7JBPPUHM.mjs} +7 -7
  20. package/dist/lib/browser/{settings-H35U6NHE.mjs → settings-DYS3FFMN.mjs} +4 -4
  21. package/dist/lib/browser/{settings-H35U6NHE.mjs.map → settings-DYS3FFMN.mjs.map} +3 -3
  22. package/dist/lib/browser/{state-U4SHOPJW.mjs → state-DRRCGMU2.mjs} +7 -11
  23. package/dist/lib/browser/state-DRRCGMU2.mjs.map +7 -0
  24. package/dist/lib/browser/tools-NDEUSO4R.mjs +78 -0
  25. package/dist/lib/browser/tools-NDEUSO4R.mjs.map +7 -0
  26. package/dist/lib/browser/types.mjs +10 -4
  27. package/dist/lib/browser/{url-handler-MVHTKUYA.mjs → url-handler-4BCN7AYC.mjs} +7 -9
  28. package/dist/lib/browser/url-handler-4BCN7AYC.mjs.map +7 -0
  29. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  30. package/dist/types/src/capabilities/capabilities.d.ts +26 -2
  31. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  32. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  33. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  34. package/dist/types/src/capabilities/state.d.ts +13 -1
  35. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  36. package/dist/types/src/capabilities/tools.d.ts +1 -0
  37. package/dist/types/src/capabilities/tools.d.ts.map +1 -1
  38. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -1
  39. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts +2 -1
  40. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -1
  41. package/dist/types/src/components/DeckLayout/Banner.d.ts +2 -1
  42. package/dist/types/src/components/DeckLayout/Banner.d.ts.map +1 -1
  43. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +2 -1
  44. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  45. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts +2 -1
  46. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts.map +1 -1
  47. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +2 -1
  48. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  49. package/dist/types/src/components/DeckLayout/Fallback.d.ts +2 -1
  50. package/dist/types/src/components/DeckLayout/Fallback.d.ts.map +1 -1
  51. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts +2 -1
  52. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +1 -1
  53. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +6 -2
  54. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  55. package/dist/types/src/components/DeckLayout/Plank.d.ts +4 -4
  56. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  57. package/dist/types/src/components/DeckLayout/PlankControls.d.ts +6 -1
  58. package/dist/types/src/components/DeckLayout/PlankControls.d.ts.map +1 -1
  59. package/dist/types/src/components/DeckLayout/PlankError.d.ts +3 -2
  60. package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -1
  61. package/dist/types/src/components/DeckLayout/PlankLoading.d.ts +2 -1
  62. package/dist/types/src/components/DeckLayout/PlankLoading.d.ts.map +1 -1
  63. package/dist/types/src/components/DeckLayout/Sidebar.d.ts +2 -1
  64. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  65. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts +4 -3
  66. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts.map +1 -1
  67. package/dist/types/src/components/DeckLayout/StatusBar.d.ts +2 -1
  68. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  69. package/dist/types/src/components/DeckLayout/Toast.d.ts +2 -1
  70. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  71. package/dist/types/src/components/DeckLayout/Topbar.d.ts +2 -1
  72. package/dist/types/src/components/DeckLayout/Topbar.d.ts.map +1 -1
  73. package/dist/types/src/components/LayoutSettings.d.ts +2 -1
  74. package/dist/types/src/components/LayoutSettings.d.ts.map +1 -1
  75. package/dist/types/src/meta.d.ts +2 -5
  76. package/dist/types/src/meta.d.ts.map +1 -1
  77. package/dist/types/src/translations.d.ts +2 -1
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/types.d.ts +42 -11
  80. package/dist/types/src/types.d.ts.map +1 -1
  81. package/package.json +31 -31
  82. package/src/capabilities/intent-resolver.ts +29 -3
  83. package/src/capabilities/settings.ts +1 -1
  84. package/src/capabilities/state.ts +2 -9
  85. package/src/capabilities/tools.ts +34 -22
  86. package/src/capabilities/url-handler.ts +2 -8
  87. package/src/components/DeckLayout/DeckLayout.tsx +36 -11
  88. package/src/components/DeckLayout/NodePlankHeading.tsx +49 -29
  89. package/src/components/DeckLayout/Plank.tsx +181 -111
  90. package/src/components/DeckLayout/PlankControls.tsx +34 -3
  91. package/src/components/LayoutSettings.tsx +3 -3
  92. package/src/meta.ts +2 -2
  93. package/src/translations.ts +4 -3
  94. package/src/types.ts +30 -1
  95. package/src/util/useHoistStatusbar.ts +4 -4
  96. package/dist/lib/browser/chunk-FT33W5CI.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-J65MNI4S.mjs +0 -24
  98. package/dist/lib/browser/chunk-OEDK54N2.mjs.map +0 -7
  99. package/dist/lib/browser/intent-resolver-ZD67BRUI.mjs.map +0 -7
  100. package/dist/lib/browser/state-U4SHOPJW.mjs.map +0 -7
  101. package/dist/lib/browser/tools-64LXGLYR.mjs +0 -59
  102. package/dist/lib/browser/tools-64LXGLYR.mjs.map +0 -7
  103. package/dist/lib/browser/url-handler-MVHTKUYA.mjs.map +0 -7
  104. /package/dist/lib/browser/{app-graph-builder-IYHAGFA3.mjs.map → app-graph-builder-K4KVSHNT.mjs.map} +0 -0
  105. /package/dist/lib/browser/{check-app-scheme-S3EYUPMF.mjs.map → check-app-scheme-6SS6I3RN.mjs.map} +0 -0
  106. /package/dist/lib/browser/{chunk-J65MNI4S.mjs.map → chunk-7A5DLPQ3.mjs.map} +0 -0
  107. /package/dist/lib/browser/{chunk-22AQ5IVX.mjs.map → chunk-WCNPMAR4.mjs.map} +0 -0
  108. /package/dist/lib/browser/{react-root-2T6BCRK4.mjs.map → react-root-PPKFVPO7.mjs.map} +0 -0
  109. /package/dist/lib/browser/{react-surface-GFMOJSIA.mjs.map → react-surface-7JBPPUHM.mjs.map} +0 -0
@@ -19,9 +19,9 @@ import {
19
19
  Dialog as NaturalDialog,
20
20
  Main,
21
21
  Popover,
22
- useOnTransition,
23
22
  type MainProps,
24
23
  useMediaQuery,
24
+ useOnTransition,
25
25
  } from '@dxos/react-ui';
26
26
  import { Stack, StackContext, DEFAULT_HORIZONTAL_SIZE } from '@dxos/react-ui-stack';
27
27
  import { mainPaddingTransitions } from '@dxos/react-ui-theme';
@@ -31,6 +31,7 @@ import { ComplementarySidebar } from './ComplementarySidebar';
31
31
  import { ContentEmpty } from './ContentEmpty';
32
32
  import { Fullscreen } from './Fullscreen';
33
33
  import { Plank } from './Plank';
34
+ import { PlankContentError } from './PlankError';
34
35
  import { Sidebar } from './Sidebar';
35
36
  import { ToggleComplementarySidebarButton, ToggleSidebarButton } from './SidebarButton';
36
37
  import { StatusBar } from './StatusBar';
@@ -47,8 +48,8 @@ export type DeckLayoutProps = {
47
48
  onDismissToast: (id: string) => void;
48
49
  };
49
50
 
50
- const PlankSeparator = ({ index }: { index: number }) =>
51
- index > 0 ? <span role='separator' className='row-span-2 bg-deck is-4' style={{ gridColumn: index * 2 }} /> : null;
51
+ const PlankSeparator = ({ order }: { order: number }) =>
52
+ order > 0 ? <span role='separator' className='row-span-2 bg-deck is-4' style={{ gridColumn: order }} /> : null;
52
53
 
53
54
  export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayoutProps) => {
54
55
  const { dispatchPromise: dispatch } = useIntentDispatcher();
@@ -67,7 +68,7 @@ export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayout
67
68
  deck,
68
69
  toasts,
69
70
  } = context;
70
- const { active, fullscreen, solo, plankSizing } = deck;
71
+ const { active, activeCompanions, fullscreen, solo, plankSizing } = deck;
71
72
  const breakpoint = useBreakpoints();
72
73
  const topbar = layoutAppliesTopbar(breakpoint);
73
74
  const hoistStatusbar = useHoistStatusbar(breakpoint);
@@ -186,6 +187,17 @@ export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayout
186
187
  );
187
188
  const handlePopoverClose = useCallback(() => handlePopoverOpenChange(false), [handlePopoverOpenChange]);
188
189
 
190
+ const { order, itemsCount }: { order: Record<string, number>; itemsCount: number } = useMemo(() => {
191
+ return active.reduce(
192
+ (acc: { order: Record<string, number>; itemsCount: number }, entryId) => {
193
+ acc.order[entryId] = acc.itemsCount + 1;
194
+ acc.itemsCount += activeCompanions?.[entryId] ? 3 : 2;
195
+ return acc;
196
+ },
197
+ { order: {}, itemsCount: 0 },
198
+ );
199
+ }, [active, activeCompanions]);
200
+
189
201
  return (
190
202
  <Popover.Root modal open={!!(popoverAnchorId && delayedPopoverVisibility)} onOpenChange={handlePopoverOpenChange}>
191
203
  <ActiveNode />
@@ -252,14 +264,21 @@ export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayout
252
264
  size='contain'
253
265
  classNames={['absolute inset-block-0 -inset-inline-px', mainPaddingTransitions]}
254
266
  onScroll={handleScroll}
255
- itemsCount={2 * (active.length ?? 0) - 1}
267
+ itemsCount={itemsCount - 1}
256
268
  style={padding}
257
269
  ref={deckRef}
258
270
  >
259
- {active.map((entryId, index) => (
271
+ {active.map((entryId) => (
260
272
  <Fragment key={entryId}>
261
- <PlankSeparator index={index} />
262
- <Plank id={entryId} part='deck' order={index * 2 + 1} active={active} layoutMode={layoutMode} />
273
+ <PlankSeparator order={order[entryId] - 1} />
274
+ <Plank
275
+ id={entryId}
276
+ companionId={activeCompanions?.[entryId]}
277
+ part='deck'
278
+ order={order[entryId]}
279
+ active={active}
280
+ layoutMode={layoutMode}
281
+ />
263
282
  </Fragment>
264
283
  ))}
265
284
  </Stack>
@@ -272,7 +291,12 @@ export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayout
272
291
  {!topbar && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
273
292
  {!topbar && <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />}
274
293
  <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
275
- <Plank id={solo} part='solo' layoutMode={layoutMode} />
294
+ <Plank
295
+ id={solo}
296
+ companionId={solo ? activeCompanions?.[solo] : undefined}
297
+ part='solo'
298
+ layoutMode={layoutMode}
299
+ />
276
300
  </StackContext.Provider>
277
301
  </div>
278
302
  </Main.Content>
@@ -303,10 +327,11 @@ export const DeckLayout = ({ overscroll, showHints, onDismissToast }: DeckLayout
303
327
  onOpenChange={(nextOpen) => (context.dialogOpen = nextOpen)}
304
328
  >
305
329
  {dialogBlockAlign === 'end' ? (
306
- <Surface role='dialog' data={dialogContent} limit={1} />
330
+ // TODO(burdon): Placeholder creates a suspense boundary; replace with defaults.
331
+ <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} placeholder={<div />} />
307
332
  ) : (
308
333
  <Dialog.Overlay blockAlign={dialogBlockAlign}>
309
- <Surface role='dialog' data={dialogContent} limit={1} />
334
+ <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} />
310
335
  </Dialog.Overlay>
311
336
  )}
312
337
  </Dialog.Root>
@@ -10,21 +10,24 @@ import { Icon, Popover, toLocalizedString, useTranslation } from '@dxos/react-ui
10
10
  import { StackItem, type StackItemSigilAction } from '@dxos/react-ui-stack';
11
11
  import { TextTooltip } from '@dxos/react-ui-text-tooltip';
12
12
 
13
- import { PlankControls } from './PlankControls';
13
+ import { PlankCompanionControls, PlankControls } from './PlankControls';
14
14
  import { DECK_PLUGIN } from '../../meta';
15
- import { DeckAction, SLUG_PATH_SEPARATOR } from '../../types';
15
+ import { DeckAction, type ResolvedPart, SLUG_PATH_SEPARATOR } from '../../types';
16
16
  import { useBreakpoints } from '../../util';
17
17
  import { soloInlinePadding } from '../fragments';
18
18
 
19
19
  export type NodePlankHeadingProps = {
20
20
  id: string;
21
- part: 'solo' | 'deck' | 'complementary';
21
+ part: ResolvedPart;
22
22
  node?: Node;
23
23
  canIncrementStart?: boolean;
24
24
  canIncrementEnd?: boolean;
25
25
  popoverAnchorId?: string;
26
26
  pending?: boolean;
27
27
  actions?: StackItemSigilAction[];
28
+ companioned?: 'primary' | 'companion';
29
+ primaryId?: string;
30
+ surfaceVariant?: string;
28
31
  };
29
32
 
30
33
  export const NodePlankHeading = memo(
@@ -37,6 +40,9 @@ export const NodePlankHeading = memo(
37
40
  popoverAnchorId,
38
41
  pending,
39
42
  actions = [],
43
+ companioned,
44
+ primaryId,
45
+ surfaceVariant,
40
46
  }: NodePlankHeadingProps) => {
41
47
  const { t } = useTranslation(DECK_PLUGIN);
42
48
  const { graph } = useAppGraph();
@@ -44,7 +50,14 @@ export const NodePlankHeading = memo(
44
50
  const icon = node?.properties?.icon ?? 'ph--placeholder--regular';
45
51
  const label = pending
46
52
  ? t('pending heading')
47
- : toLocalizedString(node?.properties?.label ?? ['plank heading fallback label', { ns: DECK_PLUGIN }], t);
53
+ : toLocalizedString(
54
+ (surfaceVariant
55
+ ? Array.isArray(node?.properties?.label)
56
+ ? [`${surfaceVariant} plank heading`, node.properties.label[1]]
57
+ : ['companion plank heading fallback label', { ns: DECK_PLUGIN }]
58
+ : node?.properties?.label) ?? ['plank heading fallback label', { ns: DECK_PLUGIN }],
59
+ t,
60
+ );
48
61
  const { dispatchPromise: dispatch } = useIntentDispatcher();
49
62
  const ActionRoot = node && popoverAnchorId === `dxos.org/ui/${DECK_PLUGIN}/${node.id}` ? Popover.Anchor : Fragment;
50
63
 
@@ -105,27 +118,30 @@ export const NodePlankHeading = memo(
105
118
  classNames={[
106
119
  'plb-1 border-be border-separator items-stretch gap-1 sticky inline-start-12 app-drag',
107
120
  part === 'solo' ? soloInlinePadding : 'pli-1',
121
+ surfaceVariant && 'pis-3',
108
122
  ]}
109
123
  >
110
- <ActionRoot>
111
- {node && sigilActions ? (
112
- <StackItem.Sigil
113
- icon={icon}
114
- related={part === 'complementary'}
115
- attendableId={attendableId}
116
- triggerLabel={t('actions menu label')}
117
- actions={sigilActions}
118
- onAction={handleAction}
119
- >
120
- <Surface role='menu-footer' data={{ subject: node.data }} />
121
- </StackItem.Sigil>
122
- ) : (
123
- <StackItem.SigilButton>
124
- <span className='sr-only'>{label}</span>
125
- <Icon icon={icon} size={5} />
126
- </StackItem.SigilButton>
127
- )}
128
- </ActionRoot>
124
+ {!surfaceVariant && (
125
+ <ActionRoot>
126
+ {node && sigilActions ? (
127
+ <StackItem.Sigil
128
+ icon={icon}
129
+ related={part === 'complementary'}
130
+ attendableId={attendableId}
131
+ triggerLabel={t('actions menu label')}
132
+ actions={sigilActions}
133
+ onAction={handleAction}
134
+ >
135
+ <Surface role='menu-footer' data={{ subject: node.data }} />
136
+ </StackItem.Sigil>
137
+ ) : (
138
+ <StackItem.SigilButton>
139
+ <span className='sr-only'>{label}</span>
140
+ <Icon icon={icon} size={5} />
141
+ </StackItem.SigilButton>
142
+ )}
143
+ </ActionRoot>
144
+ )}
129
145
  <TextTooltip text={label} onlyWhenTruncating>
130
146
  <StackItem.HeadingLabel
131
147
  attendableId={attendableId}
@@ -136,12 +152,16 @@ export const NodePlankHeading = memo(
136
152
  </StackItem.HeadingLabel>
137
153
  </TextTooltip>
138
154
  {node && part !== 'complementary' && <Surface role='navbar-end' data={{ subject: node.data }} />}
139
- <PlankControls
140
- capabilities={capabilities}
141
- isSolo={part === 'solo'}
142
- onClick={handlePlankAction}
143
- close={part === 'complementary' ? 'minify-end' : true}
144
- />
155
+ {companioned === 'companion' ? (
156
+ <PlankCompanionControls primary={surfaceVariant ? id : primaryId} />
157
+ ) : (
158
+ <PlankControls
159
+ capabilities={capabilities}
160
+ isSolo={part === 'solo'}
161
+ onClick={handlePlankAction}
162
+ close={part === 'complementary' ? 'minify-end' : true}
163
+ />
164
+ )}
145
165
  </StackItem.Heading>
146
166
  );
147
167
  },
@@ -2,7 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { type KeyboardEvent, memo, useCallback, useLayoutEffect, useMemo, useRef } from 'react';
5
+ import React, {
6
+ Fragment,
7
+ type KeyboardEvent,
8
+ memo,
9
+ type PropsWithChildren,
10
+ useCallback,
11
+ useLayoutEffect,
12
+ useMemo,
13
+ useRef,
14
+ } from 'react';
6
15
 
7
16
  import {
8
17
  createIntent,
@@ -17,133 +26,194 @@ import { useAttendableAttributes } from '@dxos/react-ui-attention';
17
26
  import { StackItem, railGridHorizontal } from '@dxos/react-ui-stack';
18
27
  import { mainIntrinsicSize, mx } from '@dxos/react-ui-theme';
19
28
 
20
- import { NodePlankHeading, type NodePlankHeadingProps } from './NodePlankHeading';
29
+ import { NodePlankHeading } from './NodePlankHeading';
21
30
  import { PlankContentError, PlankError } from './PlankError';
22
31
  import { PlankLoading } from './PlankLoading';
23
32
  import { DeckCapabilities } from '../../capabilities';
24
33
  import { useNode, useMainSize } from '../../hooks';
25
- import { DeckAction, type LayoutMode } from '../../types';
34
+ import { DeckAction, type LayoutMode, type Part, type ResolvedPart, surfaceVariantSeparator } from '../../types';
26
35
 
27
36
  const UNKNOWN_ID = 'unknown_id';
28
37
 
29
38
  export type PlankProps = {
30
39
  id?: string;
31
- part: NodePlankHeadingProps['part'];
40
+ companionId?: string;
41
+ part: Part;
32
42
  path?: string[];
33
43
  order?: number;
34
44
  active?: string[];
35
45
  layoutMode: LayoutMode;
36
46
  };
37
47
 
38
- export const Plank = memo(({ id = UNKNOWN_ID, part, path, order, active, layoutMode }: PlankProps) => {
39
- const { dispatchPromise: dispatch } = useIntentDispatcher();
40
- const { deck, popoverAnchorId, scrollIntoView } = useCapability(DeckCapabilities.DeckState);
41
- const { graph } = useAppGraph();
42
- const node = useNode(graph, id);
43
- const rootElement = useRef<HTMLDivElement | null>(null);
44
- const canResize = layoutMode === 'deck';
45
- const Root = part === 'solo' ? 'article' : StackItem.Root;
46
-
47
- const attendableAttrs = useAttendableAttributes(id);
48
- const index = active ? active.findIndex((entryId) => entryId === id) : 0;
49
- const length = active?.length ?? 1;
50
- const canIncrementStart = active && index !== undefined && index > 0 && length !== undefined && length > 1;
51
- const canIncrementEnd = active && index !== undefined && index < length - 1 && length !== undefined;
52
-
53
- const key = id.split('+')[0];
54
- const size = deck.plankSizing[key] as number | undefined;
55
- const setSize = useCallback(
56
- debounce((nextSize: number) => {
57
- return dispatch(createIntent(DeckAction.UpdatePlankSize, { id: key, size: nextSize }));
58
- }, 200),
59
- [dispatch, key],
60
- );
61
-
62
- // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
63
- const handleKeyDown = useCallback((event: KeyboardEvent) => {
64
- if (event.target === event.currentTarget && event.key === 'Escape') {
65
- rootElement.current?.closest('main')?.focus();
66
- }
67
- }, []);
68
-
69
- useLayoutEffect(() => {
70
- if (scrollIntoView === id) {
71
- // TODO(wittjosiah): When focused on page load, the focus is always visible.
72
- // Forcing focus to something smaller than the plank prevents large focus ring in the interim.
73
- const focusable = rootElement.current?.querySelector('button') || rootElement.current;
74
- focusable?.focus({ preventScroll: true });
75
- layoutMode === 'deck' && focusable?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
76
- // Clear the scroll into view state once it has been actioned.
77
- void dispatch(createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: undefined }));
78
- }
79
- }, [id, scrollIntoView, layoutMode]);
80
-
81
- const isSolo = layoutMode === 'solo' && part === 'solo';
82
- const isAttendable = isSolo || (layoutMode === 'deck' && part === 'deck');
48
+ type PlankImplProps = Omit<PlankProps, 'companionId' | 'part'> & {
49
+ part: ResolvedPart;
50
+ surfaceVariant?: string;
51
+ companioned?: 'primary' | 'companion';
52
+ primaryId?: string;
53
+ };
83
54
 
55
+ const PlankImpl = memo(
56
+ ({
57
+ id = UNKNOWN_ID,
58
+ part,
59
+ path,
60
+ order,
61
+ active,
62
+ layoutMode,
63
+ surfaceVariant,
64
+ companioned,
65
+ primaryId,
66
+ }: PlankImplProps) => {
67
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
68
+ const { deck, popoverAnchorId, scrollIntoView } = useCapability(DeckCapabilities.DeckState);
69
+ const { graph } = useAppGraph();
70
+ const node = useNode(graph, id);
71
+ const rootElement = useRef<HTMLDivElement | null>(null);
72
+ const canResize = layoutMode === 'deck';
73
+ const Root = part.startsWith('solo') ? 'article' : StackItem.Root;
74
+
75
+ const attendableAttrs = useAttendableAttributes(id);
76
+ const index = active ? active.findIndex((entryId) => entryId === id) : 0;
77
+ const length = active?.length ?? 1;
78
+ const canIncrementStart = active && index !== undefined && index > 0 && length !== undefined && length > 1;
79
+ const canIncrementEnd = active && index !== undefined && index < length - 1 && length !== undefined;
80
+
81
+ const sizeKey = `${id.split('+')[0]}${surfaceVariant ? `${surfaceVariantSeparator}${surfaceVariant}` : ''}`;
82
+ const size = deck.plankSizing[sizeKey] as number | undefined;
83
+ const setSize = useCallback(
84
+ debounce((nextSize: number) => {
85
+ return dispatch(createIntent(DeckAction.UpdatePlankSize, { id: sizeKey, size: nextSize }));
86
+ }, 200),
87
+ [dispatch, sizeKey],
88
+ );
89
+
90
+ // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
91
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
92
+ if (event.target === event.currentTarget && event.key === 'Escape') {
93
+ rootElement.current?.closest('main')?.focus();
94
+ }
95
+ }, []);
96
+
97
+ useLayoutEffect(() => {
98
+ if (scrollIntoView === id) {
99
+ // TODO(wittjosiah): When focused on page load, the focus is always visible.
100
+ // Forcing focus to something smaller than the plank prevents large focus ring in the interim.
101
+ const focusable = rootElement.current?.querySelector('button') || rootElement.current;
102
+ focusable?.focus({ preventScroll: true });
103
+ layoutMode === 'deck' && focusable?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
104
+ // Clear the scroll into view state once it has been actioned.
105
+ void dispatch(createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: undefined }));
106
+ }
107
+ }, [id, scrollIntoView, layoutMode]);
108
+
109
+ const isSolo = layoutMode === 'solo' && part === 'solo';
110
+ const isAttendable = isSolo || (layoutMode === 'deck' && part === 'deck');
111
+
112
+ const sizeAttrs = useMainSize();
113
+
114
+ const data = useMemo(
115
+ () =>
116
+ node && {
117
+ subject: node.data,
118
+ variant: surfaceVariant,
119
+ path,
120
+ popoverAnchorId,
121
+ },
122
+ [node, node?.data, path, popoverAnchorId, surfaceVariant],
123
+ );
124
+
125
+ // TODO(wittjosiah): Change prop to accept a component.
126
+ const placeholder = useMemo(() => <PlankLoading />, []);
127
+
128
+ const className = mx(
129
+ 'attention-surface relative',
130
+ isSolo && mainIntrinsicSize,
131
+ isSolo && railGridHorizontal,
132
+ isSolo && 'grid absolute inset-0',
133
+ part === 'deck' && (companioned === 'companion' ? '!border-separator border-ie' : '!border-separator border-li'),
134
+ part.startsWith('solo-') && 'row-span-2 min-is-0',
135
+ part === 'solo-companion' && '!border-separator border-is',
136
+ );
137
+
138
+ return (
139
+ <Root
140
+ ref={rootElement}
141
+ data-testid='deck.plank'
142
+ tabIndex={0}
143
+ {...(part.startsWith('solo')
144
+ ? ({ ...sizeAttrs, className } as any)
145
+ : {
146
+ item: { id },
147
+ size,
148
+ onSizeChange: setSize,
149
+ classNames: className,
150
+ order,
151
+ role: 'article',
152
+ })}
153
+ {...(isAttendable ? attendableAttrs : {})}
154
+ onKeyDown={handleKeyDown}
155
+ >
156
+ {node ? (
157
+ <>
158
+ <NodePlankHeading
159
+ id={id}
160
+ part={part.startsWith('solo-') ? 'solo' : part}
161
+ node={node}
162
+ canIncrementStart={canIncrementStart}
163
+ canIncrementEnd={canIncrementEnd}
164
+ popoverAnchorId={popoverAnchorId}
165
+ companioned={companioned}
166
+ primaryId={primaryId}
167
+ surfaceVariant={surfaceVariant}
168
+ />
169
+ <Surface
170
+ key={node.id}
171
+ role='article'
172
+ data={data}
173
+ limit={1}
174
+ fallback={PlankContentError}
175
+ placeholder={placeholder}
176
+ />
177
+ </>
178
+ ) : (
179
+ <PlankError id={id} part={part} />
180
+ )}
181
+ {canResize && <StackItem.ResizeHandle />}
182
+ </Root>
183
+ );
184
+ },
185
+ );
186
+
187
+ const SplitFrame = ({ children }: PropsWithChildren<{}>) => {
84
188
  const sizeAttrs = useMainSize();
85
-
86
- const data = useMemo(
87
- () =>
88
- node && {
89
- subject: node.data,
90
- path,
91
- popoverAnchorId,
92
- },
93
- [node, node?.data, path, popoverAnchorId],
94
- );
95
-
96
- // TODO(wittjosiah): Change prop to accept a component.
97
- const placeholder = useMemo(() => <PlankLoading />, []);
98
-
99
- const className = mx(
100
- 'attention-surface relative',
101
- isSolo && mainIntrinsicSize,
102
- isSolo && railGridHorizontal,
103
- isSolo ? 'grid absolute inset-0' : '!border-separator border-li',
104
- );
105
-
106
189
  return (
107
- <Root
108
- ref={rootElement}
109
- data-testid='deck.plank'
110
- tabIndex={0}
111
- {...(part === 'solo'
112
- ? ({ ...sizeAttrs, className } as any)
113
- : {
114
- item: { id },
115
- size,
116
- onSizeChange: setSize,
117
- classNames: className,
118
- order,
119
- role: 'article',
120
- })}
121
- {...(isAttendable ? attendableAttrs : {})}
122
- onKeyDown={handleKeyDown}
190
+ <div
191
+ role='none'
192
+ className={mx('grid grid-cols-[1fr_1fr] absolute inset-0', railGridHorizontal, mainIntrinsicSize)}
193
+ {...sizeAttrs}
123
194
  >
124
- {node ? (
125
- <>
126
- <NodePlankHeading
127
- id={id}
128
- part={part}
129
- node={node}
130
- canIncrementStart={canIncrementStart}
131
- canIncrementEnd={canIncrementEnd}
132
- popoverAnchorId={popoverAnchorId}
133
- />
134
- <Surface
135
- key={node.id}
136
- role='article'
137
- data={data}
138
- limit={1}
139
- fallback={PlankContentError}
140
- placeholder={placeholder}
141
- />
142
- </>
143
- ) : (
144
- <PlankError id={id} part={part} />
145
- )}
146
- {canResize && <StackItem.ResizeHandle />}
147
- </Root>
195
+ {children}
196
+ </div>
148
197
  );
149
- });
198
+ };
199
+
200
+ export const Plank = (props: PlankProps) => {
201
+ if (props.companionId) {
202
+ const Root = props.part === 'solo' ? SplitFrame : Fragment;
203
+ return (
204
+ <Root>
205
+ <PlankImpl {...props} {...(props.part === 'solo' ? { part: 'solo-primary' } : {})} companioned='primary' />
206
+ <PlankImpl
207
+ {...props}
208
+ {...(props.companionId.startsWith(surfaceVariantSeparator)
209
+ ? { surfaceVariant: props.companionId.substring(2) }
210
+ : { id: props.companionId, primaryId: props.id })}
211
+ {...(props.part === 'solo' ? { part: 'solo-companion' } : { order: props.order! + 1 })}
212
+ companioned='companion'
213
+ />
214
+ </Root>
215
+ );
216
+ } else {
217
+ return <PlankImpl {...props} />;
218
+ }
219
+ };
@@ -2,8 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { forwardRef } from 'react';
5
+ import React, { forwardRef, useCallback } from 'react';
6
6
 
7
+ import { createIntent, useIntentDispatcher } from '@dxos/app-framework';
8
+ import { invariant } from '@dxos/invariant';
7
9
  import {
8
10
  Button,
9
11
  ButtonGroup,
@@ -15,7 +17,7 @@ import {
15
17
  } from '@dxos/react-ui';
16
18
 
17
19
  import { DECK_PLUGIN } from '../../meta';
18
- import { type DeckAction } from '../../types';
20
+ import { DeckAction } from '../../types';
19
21
 
20
22
  export type PlankControlHandler = (event: DeckAction.PartAdjustment) => void;
21
23
 
@@ -50,6 +52,34 @@ const PlankControl = ({ icon, label, ...props }: Omit<ButtonProps, 'children'> &
50
52
  );
51
53
  };
52
54
 
55
+ const plankControlSpacing = 'pli-2 plb-3';
56
+
57
+ type PlankComplimentControlsProps = {
58
+ primary?: string;
59
+ };
60
+
61
+ export const PlankCompanionControls = forwardRef<HTMLDivElement, PlankComplimentControlsProps>(
62
+ ({ primary }, forwardedRef) => {
63
+ const { t } = useTranslation(DECK_PLUGIN);
64
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
65
+ const handleCloseCompanion = useCallback(() => {
66
+ invariant(primary);
67
+ return dispatch(createIntent(DeckAction.ChangeCompanion, { primary, companion: null }));
68
+ }, []);
69
+ return (
70
+ <div ref={forwardedRef} className='contents app-no-drag'>
71
+ <PlankControl
72
+ label={t('close companion label')}
73
+ variant='ghost'
74
+ icon='ph--minus--regular'
75
+ onClick={handleCloseCompanion}
76
+ classNames={plankControlSpacing}
77
+ />
78
+ </div>
79
+ );
80
+ },
81
+ );
82
+
53
83
  // TODO(wittjosiah): Duplicate of stack LayoutControls?
54
84
  // Translations were to be duplicated between packages.
55
85
  // NOTE(thure): Pinning & unpinning are disabled indefinitely.
@@ -59,7 +89,8 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
59
89
  forwardedRef,
60
90
  ) => {
61
91
  const { t } = useTranslation(DECK_PLUGIN);
62
- const buttonClassNames = variant === 'hide-disabled' ? 'disabled:hidden pli-2 plb-3' : 'pli-2 plb-3';
92
+ const buttonClassNames =
93
+ variant === 'hide-disabled' ? `disabled:hidden ${plankControlSpacing}` : plankControlSpacing;
63
94
 
64
95
  return (
65
96
  <ButtonGroup {...props} classNames={['app-no-drag', classNames]} ref={forwardedRef}>
@@ -72,10 +72,10 @@ export const LayoutSettings = ({ settings }: { settings: DeckSettingsProps }) =>
72
72
  />
73
73
  </DeprecatedFormInput>
74
74
  )}
75
- <DeprecatedFormInput label={t('settings enable ide-style statusbar label')}>
75
+ <DeprecatedFormInput label={t('settings enable statusbar label')}>
76
76
  <Input.Switch
77
- checked={settings.enableIdeStyleStatusbar}
78
- onCheckedChange={(checked) => (settings.enableIdeStyleStatusbar = checked)}
77
+ checked={settings.enableStatusbar}
78
+ onCheckedChange={(checked) => (settings.enableStatusbar = checked)}
79
79
  />
80
80
  </DeprecatedFormInput>
81
81
  </DeprecatedFormContainer>
package/src/meta.ts CHANGED
@@ -6,8 +6,8 @@ import { type PluginMeta } from '@dxos/app-framework';
6
6
 
7
7
  export const DECK_PLUGIN = 'dxos.org/plugin/deck' as const;
8
8
 
9
- export const meta = {
9
+ export const meta: PluginMeta = {
10
10
  id: DECK_PLUGIN,
11
11
  name: 'Deck',
12
12
  icon: 'ph--columns--regular',
13
- } satisfies PluginMeta;
13
+ };
@@ -47,14 +47,15 @@ export default [
47
47
  'show solo plank label': 'Maximize',
48
48
  'close label': 'Close',
49
49
  'minify label': 'Minify',
50
- 'settings overscroll label': 'Plank overscrolling',
51
- 'select overscroll placeholder': 'Select plank overscrolling behavior',
50
+ 'settings overscroll label': 'Plank scrolling',
51
+ 'select overscroll placeholder': 'Select plank scrolling behavior',
52
52
  'settings overscroll centering label': 'Centering',
53
53
  'settings overscroll none label': 'None',
54
- 'settings enable ide-style statusbar label': 'IDE-style statusbar',
54
+ 'settings enable statusbar label': 'Show status bar',
55
55
  'close current label': 'Close current plank',
56
56
  'close others label': 'Close other planks',
57
57
  'close all label': 'Close all planks',
58
+ 'companion plank heading fallback label': 'Related',
58
59
  },
59
60
  },
60
61
  },