@dxos/plugin-deck 0.8.2-main.f11618f → 0.8.2-staging.42af850

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 (142) hide show
  1. package/dist/lib/browser/{app-graph-builder-VYZ4IWI3.mjs → app-graph-builder-M5BT34YG.mjs} +17 -16
  2. package/dist/lib/browser/app-graph-builder-M5BT34YG.mjs.map +7 -0
  3. package/dist/lib/browser/{check-app-scheme-O7JPE4TM.mjs → check-app-scheme-7AXGR6UT.mjs} +2 -2
  4. package/dist/lib/browser/check-app-scheme-7AXGR6UT.mjs.map +7 -0
  5. package/dist/lib/browser/{state-Z6UY2Z3M.mjs → chunk-FX44YX3G.mjs} +7 -5
  6. package/dist/lib/browser/chunk-FX44YX3G.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-JE2ARGEB.mjs +1487 -0
  8. package/dist/lib/browser/chunk-JE2ARGEB.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-XMCG42ID.mjs → chunk-KLN73CM3.mjs} +2 -2
  10. package/dist/lib/browser/{chunk-XMCG42ID.mjs.map → chunk-KLN73CM3.mjs.map} +1 -1
  11. package/dist/lib/browser/chunk-SLQNOATN.mjs +127 -0
  12. package/dist/lib/browser/chunk-SLQNOATN.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-TRFYUEBA.mjs +145 -0
  14. package/dist/lib/browser/chunk-TRFYUEBA.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-YN5OZEGS.mjs +162 -0
  16. package/dist/lib/browser/chunk-YN5OZEGS.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +6 -8
  18. package/dist/lib/browser/index.mjs.map +2 -2
  19. package/dist/lib/browser/{intent-resolver-JKWXWUV6.mjs → intent-resolver-3GAC57UA.mjs} +84 -66
  20. package/dist/lib/browser/intent-resolver-3GAC57UA.mjs.map +7 -0
  21. package/dist/lib/browser/meta.json +1 -1
  22. package/dist/lib/browser/{react-root-S6ZAKNZA.mjs → react-root-ISFFOJZX.mjs} +7 -7
  23. package/dist/lib/browser/{react-surface-I7WZBOGM.mjs → react-surface-A63RQB5N.mjs} +7 -7
  24. package/dist/lib/browser/{settings-6NU7CF2B.mjs → settings-X7GDEXU3.mjs} +4 -4
  25. package/dist/lib/browser/{settings-6NU7CF2B.mjs.map → settings-X7GDEXU3.mjs.map} +3 -3
  26. package/dist/lib/browser/state-VJ6E3ADY.mjs +10 -0
  27. package/dist/lib/browser/state-VJ6E3ADY.mjs.map +7 -0
  28. package/dist/lib/browser/{tools-VDVQTJMD.mjs → tools-N57NQ2LH.mjs} +28 -18
  29. package/dist/lib/browser/tools-N57NQ2LH.mjs.map +7 -0
  30. package/dist/lib/browser/types.mjs +1 -1
  31. package/dist/lib/browser/{url-handler-3CARFXQK.mjs → url-handler-BUGI6XRE.mjs} +5 -5
  32. package/dist/lib/browser/url-handler-BUGI6XRE.mjs.map +7 -0
  33. package/dist/types/src/capabilities/app-graph-builder.d.ts +2 -179
  34. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  35. package/dist/types/src/capabilities/capabilities.d.ts +10 -2
  36. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  37. package/dist/types/src/capabilities/check-app-scheme.d.ts +2 -2
  38. package/dist/types/src/capabilities/check-app-scheme.d.ts.map +1 -1
  39. package/dist/types/src/capabilities/index.d.ts +6 -181
  40. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  42. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  43. package/dist/types/src/capabilities/state.d.ts +7 -3
  44. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  45. package/dist/types/src/capabilities/tools.d.ts +1 -1
  46. package/dist/types/src/capabilities/tools.d.ts.map +1 -1
  47. package/dist/types/src/capabilities/url-handler.d.ts +2 -2
  48. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -1
  49. package/dist/types/src/components/DeckLayout/Banner.d.ts.map +1 -1
  50. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts.map +1 -1
  51. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  52. package/dist/types/src/components/DeckLayout/Popover.d.ts.map +1 -1
  53. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  54. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  55. package/dist/types/src/components/DeckSettings/DeckSettings.d.ts.map +1 -1
  56. package/dist/types/src/components/Plank/Plank.d.ts +18 -5
  57. package/dist/types/src/components/Plank/Plank.d.ts.map +1 -1
  58. package/dist/types/src/components/Plank/Plank.stories.d.ts +3 -3
  59. package/dist/types/src/components/Plank/Plank.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/Plank/PlankControls.d.ts +1 -0
  61. package/dist/types/src/components/Plank/PlankControls.d.ts.map +1 -1
  62. package/dist/types/src/components/Plank/PlankError.d.ts.map +1 -1
  63. package/dist/types/src/components/Plank/PlankHeading.d.ts.map +1 -1
  64. package/dist/types/src/components/Sidebar/ComplementarySidebar.d.ts.map +1 -1
  65. package/dist/types/src/components/Sidebar/Sidebar.d.ts.map +1 -1
  66. package/dist/types/src/components/Sidebar/SidebarButton.d.ts +2 -1
  67. package/dist/types/src/components/Sidebar/SidebarButton.d.ts.map +1 -1
  68. package/dist/types/src/hooks/index.d.ts +5 -1
  69. package/dist/types/src/hooks/index.d.ts.map +1 -1
  70. package/dist/types/src/hooks/useBreakpoints.d.ts.map +1 -0
  71. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
  72. package/dist/types/src/hooks/useDeckCompanions.d.ts +13 -0
  73. package/dist/types/src/hooks/useDeckCompanions.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useHoistStatusbar.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useNodeActionExpander.d.ts.map +1 -1
  76. package/dist/types/src/index.d.ts +1 -1
  77. package/dist/types/src/index.d.ts.map +1 -1
  78. package/dist/types/src/layout.d.ts.map +1 -1
  79. package/dist/types/src/translations.d.ts +0 -1
  80. package/dist/types/src/translations.d.ts.map +1 -1
  81. package/dist/types/src/types.d.ts +106 -104
  82. package/dist/types/src/types.d.ts.map +1 -1
  83. package/dist/types/src/util/index.d.ts +1 -4
  84. package/dist/types/src/util/index.d.ts.map +1 -1
  85. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -1
  86. package/dist/types/src/util/overscroll.d.ts.map +1 -1
  87. package/dist/types/src/util/set-active.d.ts.map +1 -1
  88. package/dist/types/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +38 -30
  90. package/src/capabilities/app-graph-builder.ts +120 -92
  91. package/src/capabilities/check-app-scheme.ts +3 -5
  92. package/src/capabilities/index.ts +1 -0
  93. package/src/capabilities/intent-resolver.ts +100 -78
  94. package/src/capabilities/settings.ts +2 -2
  95. package/src/capabilities/state.ts +4 -2
  96. package/src/capabilities/tools.ts +15 -12
  97. package/src/capabilities/url-handler.ts +4 -4
  98. package/src/components/DeckLayout/ContentEmpty.tsx +3 -2
  99. package/src/components/DeckLayout/DeckLayout.tsx +12 -10
  100. package/src/components/DeckLayout/Dialog.tsx +2 -2
  101. package/src/components/Plank/Plank.stories.tsx +20 -8
  102. package/src/components/Plank/Plank.tsx +101 -68
  103. package/src/components/Plank/PlankControls.tsx +15 -25
  104. package/src/components/Plank/PlankHeading.tsx +22 -10
  105. package/src/components/Sidebar/ComplementarySidebar.tsx +7 -38
  106. package/src/components/Sidebar/Sidebar.tsx +2 -1
  107. package/src/components/Sidebar/SidebarButton.tsx +26 -7
  108. package/src/components/fragments.ts +1 -1
  109. package/src/hooks/index.ts +5 -1
  110. package/src/{util → hooks}/useCompanions.ts +3 -3
  111. package/src/hooks/useDeckCompanions.ts +33 -0
  112. package/src/hooks/useNodeActionExpander.ts +3 -8
  113. package/src/index.ts +1 -1
  114. package/src/translations.ts +0 -1
  115. package/src/types.ts +74 -71
  116. package/src/util/index.ts +1 -4
  117. package/dist/lib/browser/app-graph-builder-VYZ4IWI3.mjs.map +0 -7
  118. package/dist/lib/browser/check-app-scheme-O7JPE4TM.mjs.map +0 -7
  119. package/dist/lib/browser/chunk-6HJZL3WT.mjs +0 -118
  120. package/dist/lib/browser/chunk-6HJZL3WT.mjs.map +0 -7
  121. package/dist/lib/browser/chunk-FLOVGNYB.mjs +0 -81
  122. package/dist/lib/browser/chunk-FLOVGNYB.mjs.map +0 -7
  123. package/dist/lib/browser/chunk-RBJ6DLAC.mjs +0 -24
  124. package/dist/lib/browser/chunk-RBJ6DLAC.mjs.map +0 -7
  125. package/dist/lib/browser/chunk-RDFJGGGX.mjs +0 -1334
  126. package/dist/lib/browser/chunk-RDFJGGGX.mjs.map +0 -7
  127. package/dist/lib/browser/chunk-ZMJMCN7O.mjs +0 -157
  128. package/dist/lib/browser/chunk-ZMJMCN7O.mjs.map +0 -7
  129. package/dist/lib/browser/intent-resolver-JKWXWUV6.mjs.map +0 -7
  130. package/dist/lib/browser/state-Z6UY2Z3M.mjs.map +0 -7
  131. package/dist/lib/browser/tools-VDVQTJMD.mjs.map +0 -7
  132. package/dist/lib/browser/url-handler-3CARFXQK.mjs.map +0 -7
  133. package/dist/types/src/util/useBreakpoints.d.ts.map +0 -1
  134. package/dist/types/src/util/useCompanions.d.ts.map +0 -1
  135. package/dist/types/src/util/useHoistStatusbar.d.ts.map +0 -1
  136. /package/dist/lib/browser/{react-root-S6ZAKNZA.mjs.map → react-root-ISFFOJZX.mjs.map} +0 -0
  137. /package/dist/lib/browser/{react-surface-I7WZBOGM.mjs.map → react-surface-A63RQB5N.mjs.map} +0 -0
  138. /package/dist/types/src/{util → hooks}/useBreakpoints.d.ts +0 -0
  139. /package/dist/types/src/{util → hooks}/useCompanions.d.ts +0 -0
  140. /package/dist/types/src/{util → hooks}/useHoistStatusbar.d.ts +0 -0
  141. /package/src/{util → hooks}/useBreakpoints.ts +0 -0
  142. /package/src/{util → hooks}/useHoistStatusbar.ts +0 -0
@@ -5,39 +5,51 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React from 'react';
8
9
 
9
- import { IntentPlugin } from '@dxos/app-framework';
10
+ import { IntentPlugin, SettingsPlugin } from '@dxos/app-framework';
10
11
  import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { AttentionPlugin } from '@dxos/plugin-attention';
11
13
  import { GraphPlugin } from '@dxos/plugin-graph';
14
+ import { Stack } from '@dxos/react-ui-stack';
12
15
  import { withTheme, withLayout } from '@dxos/storybook-utils';
13
16
 
14
- import { Plank } from './Plank';
17
+ import { Plank, type PlankProps } from './Plank';
18
+ import DeckStateFactory from '../../capabilities/state';
15
19
  import translations from '../../translations';
16
20
 
17
- // TODO(burdon): invariant violation: No capability found for dxos.org/plugin/deck/capability/state
18
- const meta: Meta<typeof Plank> = {
21
+ const meta: Meta<PlankProps> = {
19
22
  title: 'plugins/plugin-deck/Plank',
20
23
  component: Plank,
24
+ render: (args) => {
25
+ return (
26
+ <Stack orientation='horizontal'>
27
+ <Plank {...args} />
28
+ </Stack>
29
+ );
30
+ },
21
31
  decorators: [
22
32
  withPluginManager({
23
- plugins: [IntentPlugin(), GraphPlugin()],
33
+ plugins: [AttentionPlugin(), SettingsPlugin(), IntentPlugin(), GraphPlugin()],
34
+ capabilities: () => DeckStateFactory(),
24
35
  }),
25
36
  withTheme,
26
- withLayout({ fullscreen: true, tooltips: true }),
37
+ withLayout({ fullscreen: true }),
27
38
  ],
28
39
  parameters: {
29
- layout: 'centered',
30
40
  translations,
31
41
  },
32
42
  };
33
43
 
34
44
  export default meta;
35
45
 
36
- type Story = StoryObj<typeof meta>;
46
+ type Story = StoryObj<PlankProps>;
37
47
 
48
+ // TODO(burdon): Need to define surface provider?
38
49
  export const Default: Story = {
39
50
  args: {
40
51
  id: 'plank-1',
41
52
  part: 'solo',
53
+ layoutMode: 'deck',
42
54
  },
43
55
  };
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  import React, {
6
- Fragment,
7
6
  type KeyboardEvent,
8
7
  type PropsWithChildren,
9
8
  memo,
@@ -31,40 +30,117 @@ import { PlankContentError, PlankError } from './PlankError';
31
30
  import { PlankHeading } from './PlankHeading';
32
31
  import { PlankLoading } from './PlankLoading';
33
32
  import { DeckCapabilities } from '../../capabilities';
34
- import { useMainSize } from '../../hooks';
33
+ import { useMainSize, useCompanions } from '../../hooks';
35
34
  import { parseEntryId } from '../../layout';
36
- import { DeckAction, type LayoutMode, type Part, type ResolvedPart, type DeckSettingsProps } from '../../types';
37
- import { useCompanions } from '../../util';
35
+ import { DeckAction, type LayoutMode, type ResolvedPart, type DeckSettingsProps } from '../../types';
38
36
 
39
37
  const UNKNOWN_ID = 'unknown_id';
40
38
 
41
- export type PlankProps = {
39
+ export type PlankProps = Pick<PlankComponentProps, 'layoutMode' | 'part' | 'path' | 'order' | 'active' | 'settings'> & {
42
40
  id?: string;
43
41
  companionId?: string;
44
- part: Part;
45
- path?: string[];
46
- order?: number;
47
- active?: string[];
48
- layoutMode: LayoutMode;
49
- settings?: DeckSettingsProps;
50
42
  };
51
43
 
52
- type PlankImplProps = Omit<PlankProps, 'id' | 'companionId' | 'part'> & {
44
+ // TODO(burdon): Factor out conditional rendering.
45
+ // Remove this wrapper component and render the entire set of planks in the deck with conditional visibility
46
+ // to obviate mounting and unmounting when switching between solo and companion mode?
47
+ // NOTE(thure, in reply): Whether any surface should be rendered and hidden is a performance matter — remember that
48
+ // article surfaces contain full experiences, so being able to unmount them will yield relatively large performance
49
+ // benefits. I think where we anticipate users will definitely want to quickly switch between showing and hiding entire
50
+ // articles, over the (again probably large) performance benefit that unmounting them would confer, we can mount and
51
+ // hide them, but I think that scenario in its most unambiguous form is probably rare. You could extrapolate
52
+ // the scenario to include all “potential” planks such as companions, which we could keep mounted and hidden, but I
53
+ // don’t think the resulting performance would be acceptable. I think the real issue is “perceived performance” which
54
+ // has mitigations that are in between mounting and un-mounting since both of those have tradeoffs; we may need one or more
55
+ // “partially-mounted” experiences, like loading skeletons at the simple end, or screenshots of “sleeping” planks at
56
+ // the advanced end.
57
+
58
+ /**
59
+ * A Plank is the main container for surfaces within a Deck.
60
+ * It may be paired with a companion plank that enables the user to select one of multiple companion surfaces.
61
+ */
62
+ export const Plank = memo(({ id = UNKNOWN_ID, companionId, ...props }: PlankProps) => {
63
+ const { graph } = useAppGraph();
64
+ const node = useNode(graph, id);
65
+ const companions = useCompanions(id);
66
+ const currentCompanion = companions.find(({ id }) => id === companionId);
67
+ const hasCompanion = !!(companionId && currentCompanion);
68
+
69
+ return (
70
+ <PlankContainer solo={props.part === 'solo'} companion={hasCompanion}>
71
+ <PlankComponent
72
+ id={id}
73
+ node={node}
74
+ companioned={hasCompanion ? 'primary' : undefined}
75
+ companions={hasCompanion ? [] : companions}
76
+ {...props}
77
+ {...(props.part === 'solo' ? { part: 'solo-primary' } : {})}
78
+ />
79
+ {hasCompanion && (
80
+ <PlankComponent
81
+ id={companionId}
82
+ node={currentCompanion}
83
+ primary={node}
84
+ companions={companions}
85
+ companioned='companion'
86
+ {...props}
87
+ {...(props.part === 'solo' ? { part: 'solo-companion' } : { order: (props.order ?? 0) + 1 })}
88
+ />
89
+ )}
90
+ </PlankContainer>
91
+ );
92
+ });
93
+
94
+ const PlankContainer = ({ children, solo, companion }: PropsWithChildren<{ solo: boolean; companion: boolean }>) => {
95
+ const sizeAttrs = useMainSize();
96
+ if (!solo) {
97
+ return children;
98
+ }
99
+
100
+ // TODO(burdon): Make resizable.
101
+ return (
102
+ <div
103
+ role='none'
104
+ className={mx('absolute inset-0 grid', companion && 'grid-cols-[1fr_1fr]', railGridHorizontal, mainIntrinsicSize)}
105
+ {...sizeAttrs}
106
+ >
107
+ {children}
108
+ </div>
109
+ );
110
+ };
111
+
112
+ type PlankComponentProps = {
113
+ layoutMode: LayoutMode;
53
114
  id: string;
54
115
  part: ResolvedPart;
55
- node?: Node;
116
+ path?: string[];
117
+ order?: number;
118
+ active?: string[];
119
+ // TODO(burdon): Change to role?
56
120
  companioned?: 'primary' | 'companion';
121
+ node?: Node;
57
122
  primary?: Node;
58
123
  companions?: Node[];
124
+ settings?: DeckSettingsProps;
59
125
  };
60
126
 
61
- const PlankImpl = memo(
62
- ({ id, node, part, path, order, active, layoutMode, companioned, primary, companions, settings }: PlankImplProps) => {
127
+ const PlankComponent = memo(
128
+ ({
129
+ layoutMode,
130
+ id,
131
+ part,
132
+ path,
133
+ order,
134
+ active,
135
+ companioned,
136
+ node,
137
+ primary,
138
+ companions,
139
+ settings,
140
+ }: PlankComponentProps) => {
63
141
  const { dispatchPromise: dispatch } = useIntentDispatcher();
64
142
  const { deck, popoverAnchorId, scrollIntoView } = useCapability(DeckCapabilities.DeckState);
65
- const rootElement = useRef<HTMLDivElement | null>(null);
66
143
  const canResize = layoutMode === 'deck';
67
- const Root = part.startsWith('solo') ? 'article' : StackItem.Root;
68
144
 
69
145
  const attendableAttrs = useAttendableAttributes(primary?.id ?? id);
70
146
  const index = active ? active.findIndex((entryId) => entryId === id) : 0;
@@ -72,10 +148,13 @@ const PlankImpl = memo(
72
148
  const canIncrementStart = active && index !== undefined && index > 0 && length !== undefined && length > 1;
73
149
  const canIncrementEnd = active && index !== undefined && index < length - 1 && length !== undefined;
74
150
 
151
+ const rootElement = useRef<HTMLDivElement | null>(null);
152
+
75
153
  const { variant } = parseEntryId(id);
76
154
  const sizeKey = `${id.split('+')[0]}${variant ? `${ATTENDABLE_PATH_SEPARATOR}${variant}` : ''}`;
77
155
  const size = deck.plankSizing[sizeKey] as number | undefined;
78
- const setSize = useCallback(
156
+
157
+ const handleSizeChange = useCallback(
79
158
  debounce((nextSize: number) => {
80
159
  return dispatch(createIntent(DeckAction.UpdatePlankSize, { id: sizeKey, size: nextSize }));
81
160
  }, 200),
@@ -122,8 +201,9 @@ const PlankImpl = memo(
122
201
  // TODO(wittjosiah): Change prop to accept a component.
123
202
  const placeholder = useMemo(() => <PlankLoading />, []);
124
203
 
204
+ const Root = part.startsWith('solo') ? 'article' : StackItem.Root;
125
205
  const className = mx(
126
- 'attention-surface relative',
206
+ 'attention-surface relative dx-focus-ring-inset-over-all',
127
207
  isSolo && mainIntrinsicSize,
128
208
  isSolo && railGridHorizontal,
129
209
  isSolo && 'absolute inset-0',
@@ -131,8 +211,6 @@ const PlankImpl = memo(
131
211
  part === 'deck' && (companioned === 'companion' ? '!border-separator border-ie' : '!border-separator border-li'),
132
212
  part.startsWith('solo-') && 'row-span-2 grid-rows-subgrid min-is-0',
133
213
  part === 'solo-companion' && '!border-separator border-is',
134
- layoutMode === 'solo--fullscreen' &&
135
- '!transition-[margin-block-start,inline-size] -mbs-[--rail-action] has-[[data-plank-heading]:hover]:mbs-0',
136
214
  );
137
215
 
138
216
  return (
@@ -145,7 +223,7 @@ const PlankImpl = memo(
145
223
  : {
146
224
  item: { id },
147
225
  size,
148
- onSizeChange: setSize,
226
+ onSizeChange: handleSizeChange,
149
227
  classNames: className,
150
228
  order,
151
229
  role: 'article',
@@ -180,54 +258,9 @@ const PlankImpl = memo(
180
258
  ) : (
181
259
  <PlankError id={id} part={part} />
182
260
  )}
261
+
183
262
  {canResize && <StackItem.ResizeHandle />}
184
263
  </Root>
185
264
  );
186
265
  },
187
266
  );
188
-
189
- const SplitFrame = ({ children }: PropsWithChildren<{}>) => {
190
- const sizeAttrs = useMainSize();
191
- return (
192
- <div
193
- role='none'
194
- className={mx('grid grid-cols-[1fr_1fr] absolute inset-0', railGridHorizontal, mainIntrinsicSize)}
195
- {...sizeAttrs}
196
- >
197
- {children}
198
- </div>
199
- );
200
- };
201
-
202
- export const Plank = ({ id = UNKNOWN_ID, ...props }: PlankProps) => {
203
- const { graph } = useAppGraph();
204
- const node = useNode(graph, id);
205
- const companions = useCompanions(id);
206
- const currentCompanion = companions.find(({ id }) => id === props.companionId);
207
-
208
- if (props.companionId) {
209
- const Root = props.part === 'solo' ? SplitFrame : Fragment;
210
- return (
211
- <Root>
212
- <PlankImpl
213
- id={id}
214
- node={node}
215
- companioned='primary'
216
- {...props}
217
- {...(props.part === 'solo' ? { part: 'solo-primary' } : {})}
218
- />
219
- <PlankImpl
220
- id={props.companionId}
221
- node={currentCompanion}
222
- companioned='companion'
223
- primary={node}
224
- companions={companions}
225
- {...props}
226
- {...(props.part === 'solo' ? { part: 'solo-companion' } : { order: props.order! + 1 })}
227
- />
228
- </Root>
229
- );
230
- } else {
231
- return <PlankImpl id={id} node={node} companions={companions} {...props} />;
232
- }
233
- };
@@ -6,15 +6,7 @@ import React, { forwardRef, useCallback } from 'react';
6
6
 
7
7
  import { createIntent, useIntentDispatcher } from '@dxos/app-framework';
8
8
  import { invariant } from '@dxos/invariant';
9
- import {
10
- Button,
11
- ButtonGroup,
12
- type ButtonGroupProps,
13
- type ButtonProps,
14
- Icon,
15
- Tooltip,
16
- useTranslation,
17
- } from '@dxos/react-ui';
9
+ import { ButtonGroup, type ButtonGroupProps, type ButtonProps, IconButton, useTranslation } from '@dxos/react-ui';
18
10
 
19
11
  import { DECK_PLUGIN } from '../../meta';
20
12
  import { DeckAction, type LayoutMode } from '../../types';
@@ -26,6 +18,7 @@ export type PlankCapabilities = {
26
18
  incrementEnd?: boolean;
27
19
  deck?: boolean;
28
20
  solo?: boolean;
21
+ fullscreen?: boolean;
29
22
  companion?: boolean;
30
23
  };
31
24
 
@@ -39,22 +32,10 @@ export type PlankControlsProps = Omit<ButtonGroupProps, 'onClick'> & {
39
32
  };
40
33
 
41
34
  const PlankControl = ({ icon, label, ...props }: Omit<ButtonProps, 'children'> & { label: string; icon: string }) => {
42
- return (
43
- <Tooltip.Root>
44
- <Tooltip.Trigger asChild>
45
- <Button variant='ghost' {...props}>
46
- <span className='sr-only'>{label}</span>
47
- <Icon icon={icon} size={5} />
48
- </Button>
49
- </Tooltip.Trigger>
50
- <Tooltip.Portal>
51
- <Tooltip.Content side='bottom'>{label}</Tooltip.Content>
52
- </Tooltip.Portal>
53
- </Tooltip.Root>
54
- );
35
+ return <IconButton iconOnly label={label} icon={icon} size={5} variant='ghost' tooltipSide='bottom' {...props} />;
55
36
  };
56
37
 
57
- const plankControlSpacing = 'pli-2 plb-3';
38
+ const plankControlSpacing = 'pli-2';
58
39
 
59
40
  type PlankComplimentControlsProps = {
60
41
  primary?: string;
@@ -97,8 +78,8 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
97
78
  const layoutIsAnySolo = !!layoutMode?.startsWith('solo');
98
79
 
99
80
  return (
100
- <ButtonGroup {...props} classNames={['app-no-drag', classNames]} ref={forwardedRef}>
101
- {capabilities.deck && (
81
+ <ButtonGroup {...props} classNames={['app-no-drag !opacity-100', classNames]} ref={forwardedRef}>
82
+ {capabilities.deck ? (
102
83
  <>
103
84
  {capabilities.solo && (
104
85
  <>
@@ -150,6 +131,15 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
150
131
  </>
151
132
  )}
152
133
  </>
134
+ ) : (
135
+ capabilities.fullscreen && (
136
+ <PlankControl
137
+ label={t(layoutMode === 'solo--fullscreen' ? 'exit fullscreen label' : 'show fullscreen plank label')}
138
+ classNames={buttonClassNames}
139
+ icon={layoutMode === 'solo--fullscreen' ? 'ph--corners-in--regular' : 'ph--corners-out--regular'}
140
+ onClick={() => onClick?.('solo--fullscreen')}
141
+ />
142
+ )
153
143
  )}
154
144
 
155
145
  {close && !layoutIsAnySolo && (
@@ -9,12 +9,13 @@ import { type Node } from '@dxos/plugin-graph';
9
9
  import { Icon, IconButton, 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
+ import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/react-ui-theme';
12
13
 
13
14
  import { PlankCompanionControls, PlankControls } from './PlankControls';
15
+ import { useBreakpoints } from '../../hooks';
14
16
  import { parseEntryId } from '../../layout';
15
17
  import { DECK_PLUGIN } from '../../meta';
16
18
  import { PLANK_COMPANION_TYPE, DeckAction, type ResolvedPart, type LayoutMode } from '../../types';
17
- import { useBreakpoints } from '../../util';
18
19
  import { soloInlinePadding } from '../fragments';
19
20
 
20
21
  const MAX_COMPANIONS = 5;
@@ -65,7 +66,9 @@ export const PlankHeading = memo(
65
66
  useEffect(() => {
66
67
  const frame = requestAnimationFrame(() => {
67
68
  // Load actions for the node.
68
- node && graph.actions(node);
69
+ if (node) {
70
+ void graph.expand(node.id);
71
+ }
69
72
  });
70
73
 
71
74
  return () => cancelAnimationFrame(frame);
@@ -78,6 +81,7 @@ export const PlankHeading = memo(
78
81
  solo: breakpoint !== 'mobile' && (part === 'solo' || part === 'deck'),
79
82
  incrementStart: canIncrementStart,
80
83
  incrementEnd: canIncrementEnd,
84
+ fullscreen: !isCompanionNode,
81
85
  companion: !isCompanionNode && companions && companions.length > 0,
82
86
  }),
83
87
  [breakpoint, part, companions, canIncrementStart, canIncrementEnd, isCompanionNode, deckEnabled],
@@ -90,13 +94,16 @@ export const PlankHeading = memo(
90
94
  } else if (variant) {
91
95
  return [];
92
96
  } else {
93
- return [actions, graph.actions(node)].filter((a) => a.length > 0);
97
+ return [actions, graph.getActions(node.id)].filter((a) => a.length > 0);
94
98
  }
95
99
  }, [actions, node, variant, graph]);
96
100
 
97
- const handleAction = useCallback((action: StackItemSigilAction) => {
98
- typeof action.data === 'function' && action.data?.({ node: action as Node, caller: DECK_PLUGIN });
99
- }, []);
101
+ const handleAction = useCallback(
102
+ (action: StackItemSigilAction) => {
103
+ typeof action.data === 'function' && action.data?.({ parent: node, caller: DECK_PLUGIN });
104
+ },
105
+ [node],
106
+ );
100
107
 
101
108
  const handlePlankAction = useCallback(
102
109
  (eventType: DeckAction.PartAdjustment) => {
@@ -143,14 +150,19 @@ export const PlankHeading = memo(
143
150
  return (
144
151
  <StackItem.Heading
145
152
  classNames={[
146
- 'plb-1 border-be border-separator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 contain-layout',
153
+ 'plb-1 border-be border-subduedSeparator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 contain-layout',
147
154
  part === 'solo' ? soloInlinePadding : 'pli-1',
148
- layoutMode === 'solo--fullscreen' &&
149
- 'opacity-0 border-transparent hover:border-separator hover:opacity-100 transition-[border-color,opacity]',
155
+ ...(layoutMode === 'solo--fullscreen'
156
+ ? [
157
+ hoverableControls,
158
+ hoverableFocusedWithinControls,
159
+ '[&>*]:transition-opacity [&>*]:opacity-[--controls-opacity] bg-transparent border-transparent transition-[background-color,border-color] hover-hover:hover:bg-headerSurface focus-within:bg-headerSurface hover-hover:hover:border-subduedSeparator focus-within:border-subduedSeparator',
160
+ ]
161
+ : []),
150
162
  ]}
151
163
  data-plank-heading
152
164
  >
153
- {companions && isCompanionNode ? (
165
+ {companions && isCompanionNode /* TODO(thure): This is a tablist, it should be implemented as such. */ ? (
154
166
  <div role='none' className='flex-1 min-is-0 overflow-x-auto scrollbar-thin flex gap-1'>
155
167
  {companions.map(({ id, properties: { icon, label } }) => (
156
168
  <IconButton
@@ -12,51 +12,20 @@ import React, {
12
12
  useState,
13
13
  } from 'react';
14
14
 
15
- import {
16
- LayoutAction,
17
- Surface,
18
- createIntent,
19
- useAppGraph,
20
- useCapability,
21
- useIntentDispatcher,
22
- } from '@dxos/app-framework';
23
- import { type Node } from '@dxos/plugin-graph';
15
+ import { LayoutAction, Surface, createIntent, useCapability, useIntentDispatcher } from '@dxos/app-framework';
24
16
  import { Main, useTranslation, toLocalizedString, IconButton, type Label } from '@dxos/react-ui';
25
17
  import { Tabs } from '@dxos/react-ui-tabs';
26
- import { byPosition, type Position } from '@dxos/util';
27
18
 
28
19
  import { ToggleComplementarySidebarButton } from './SidebarButton';
29
20
  import { DeckCapabilities } from '../../capabilities';
21
+ import { type DeckCompanion, getCompanionId, useDeckCompanions, useBreakpoints, useHoistStatusbar } from '../../hooks';
30
22
  import { DECK_PLUGIN } from '../../meta';
31
- import { ATTENDABLE_PATH_SEPARATOR, DECK_COMPANION_TYPE, getMode } from '../../types';
32
- import { layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
23
+ import { getMode } from '../../types';
24
+ import { layoutAppliesTopbar } from '../../util';
33
25
  import { PlankContentError, PlankLoading } from '../Plank';
34
26
 
35
27
  const label = ['complementary sidebar title', { ns: DECK_PLUGIN }] satisfies Label;
36
28
 
37
- const getCompanionId = (id: string) => {
38
- const [_, companionId] = id.split(ATTENDABLE_PATH_SEPARATOR);
39
- return companionId ?? 'never';
40
- };
41
-
42
- type DeckCompanion = Node<
43
- any,
44
- {
45
- label: Label;
46
- icon: string;
47
- // TODO(burdon): Scroll area should be controlled by surface.
48
- /** If true, the panel will not be wrapped in a scroll area. */
49
- fixed?: boolean;
50
- position?: Position;
51
- }
52
- >;
53
-
54
- const useDeckCompanions = (): DeckCompanion[] => {
55
- const { graph } = useAppGraph();
56
- const companions = graph.nodes(graph.root, { type: DECK_COMPANION_TYPE }) as DeckCompanion[];
57
- return companions.toSorted((a, b) => byPosition(a.properties, b.properties));
58
- };
59
-
60
29
  export type ComplementarySidebarProps = {
61
30
  current?: string;
62
31
  };
@@ -121,7 +90,7 @@ export const ComplementarySidebar = ({ current }: ComplementarySidebarProps) =>
121
90
  <Tabs.Root orientation='vertical' verticalVariant='stateless' value={internalValue} classNames='contents'>
122
91
  <div
123
92
  role='none'
124
- className='absolute z-[1] inset-block-0 inline-end-0 !is-[--r0-size] pbs-[env(safe-area-inset-top)] pbe-[env(safe-area-inset-bottom)] border-is border-separator grid grid-cols-1 grid-rows-[1fr_min-content] bg-baseSurface contain-layout app-drag'
93
+ className='absolute z-[1] inset-block-0 inline-end-0 !is-[--r0-size] pbs-[env(safe-area-inset-top)] pbe-[env(safe-area-inset-bottom)] border-is border-subduedSeparator grid grid-cols-1 grid-rows-[1fr_min-content] bg-baseSurface contain-layout app-drag'
125
94
  >
126
95
  <Tabs.Tablist classNames='grid grid-cols-1 auto-rows-[--rail-action] p-1 gap-1 !overflow-y-auto'>
127
96
  {companions.map((companion) => (
@@ -200,7 +169,7 @@ const ComplementarySidebarPanel = ({ companion, activeId, data, hoistStatusbar }
200
169
 
201
170
  return (
202
171
  <>
203
- <h2 className='flex items-center pli-2 border-separator border-be font-medium'>
172
+ <h2 className='flex items-center pli-2 border-subduedSeparator border-be font-medium'>
204
173
  {toLocalizedString(companion.properties.label, t)}
205
174
  </h2>
206
175
  <Wrapper>
@@ -214,7 +183,7 @@ const ComplementarySidebarPanel = ({ companion, activeId, data, hoistStatusbar }
214
183
  {!hoistStatusbar && (
215
184
  <div
216
185
  role='contentinfo'
217
- className='flex flex-wrap justify-center items-center border-bs border-separator pbs-1 pbe-[max(env(safe-area-inset-bottom),0.25rem)]'
186
+ className='flex flex-wrap justify-center items-center border-bs border-subduedSeparator pbs-1 pbe-[max(env(safe-area-inset-bottom),0.25rem)]'
218
187
  >
219
188
  <Surface role='status-bar--r1-footer' limit={1} />
220
189
  </div>
@@ -8,9 +8,10 @@ import { Surface, useCapability } from '@dxos/app-framework';
8
8
  import { type Label, Main } from '@dxos/react-ui';
9
9
 
10
10
  import { DeckCapabilities } from '../../capabilities';
11
+ import { useBreakpoints, useHoistStatusbar } from '../../hooks';
11
12
  import { DECK_PLUGIN } from '../../meta';
12
13
  import { getMode } from '../../types';
13
- import { layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
14
+ import { layoutAppliesTopbar } from '../../util';
14
15
 
15
16
  const label = ['sidebar title', { ns: DECK_PLUGIN }] satisfies Label;
16
17
 
@@ -2,12 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React from 'react';
5
+ import React, { useCallback } from 'react';
6
6
 
7
- import { useCapability } from '@dxos/app-framework';
7
+ import { createIntent, LayoutAction, useCapability, useIntentDispatcher } from '@dxos/app-framework';
8
8
  import { IconButton, type IconButtonProps, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
9
 
10
10
  import { DeckCapabilities } from '../../capabilities';
11
+ import { useDeckCompanions, getCompanionId } from '../../hooks';
11
12
  import { DECK_PLUGIN } from '../../meta';
12
13
 
13
14
  export const ToggleSidebarButton = ({
@@ -47,16 +48,34 @@ export const CloseSidebarButton = () => {
47
48
  );
48
49
  };
49
50
 
50
- export const ToggleComplementarySidebarButton = ({ inR0, classNames }: ThemedClassName<{ inR0?: boolean }>) => {
51
+ export const ToggleComplementarySidebarButton = ({
52
+ inR0,
53
+ classNames,
54
+ current,
55
+ }: ThemedClassName<{ inR0?: boolean; current?: string }>) => {
56
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
51
57
  const layoutContext = useCapability(DeckCapabilities.MutableDeckState);
52
58
  const { t } = useTranslation(DECK_PLUGIN);
59
+
60
+ const companions = useDeckCompanions();
61
+ const handleClick = useCallback(async () => {
62
+ layoutContext.complementarySidebarState =
63
+ layoutContext.complementarySidebarState === 'expanded' ? 'collapsed' : 'expanded';
64
+ const firstCompanion = companions[0];
65
+ if (layoutContext.complementarySidebarState === 'expanded' && !current && firstCompanion) {
66
+ await dispatch(
67
+ createIntent(LayoutAction.UpdateComplementary, {
68
+ part: 'complementary',
69
+ subject: getCompanionId(firstCompanion.id),
70
+ }),
71
+ );
72
+ }
73
+ }, [layoutContext, current, companions, dispatch]);
74
+
53
75
  return (
54
76
  <IconButton
55
77
  iconOnly
56
- onClick={() =>
57
- (layoutContext.complementarySidebarState =
58
- layoutContext.complementarySidebarState === 'expanded' ? 'collapsed' : 'expanded')
59
- }
78
+ onClick={handleClick}
60
79
  variant='ghost'
61
80
  label={t('open complementary sidebar label')}
62
81
  classNames={['[&>svg]:-scale-x-100', classNames]}
@@ -7,7 +7,7 @@ import { mx } from '@dxos/react-ui-theme';
7
7
  export const soloInlinePadding =
8
8
  'pis-[calc(env(safe-area-inset-left)+.25rem)] pie-[calc(env(safe-area-inset-left)+.25rem)]';
9
9
 
10
- const sidebarToggleStyles = 'bs-[--rail-item] is-[--rail-item] absolute block-end-2 z-[1] !bg-deck lg:hidden';
10
+ const sidebarToggleStyles = 'bs-[--rail-item] is-[--rail-item] absolute block-end-2 z-[1] !bg-deckSurface lg:hidden';
11
11
 
12
12
  export const fixedSidebarToggleStyles = mx(sidebarToggleStyles, 'inline-start-2');
13
13
 
@@ -2,5 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './useNodeActionExpander';
5
+ export * from './useBreakpoints';
6
+ export * from './useCompanions';
7
+ export * from './useDeckCompanions';
8
+ export * from './useHoistStatusbar';
6
9
  export * from './useMainSize';
10
+ export * from './useNodeActionExpander';
@@ -5,14 +5,14 @@
5
5
  import { useMemo } from 'react';
6
6
 
7
7
  import { useAppGraph } from '@dxos/app-framework';
8
- import { useNode } from '@dxos/plugin-graph';
8
+ import { useConnections } from '@dxos/plugin-graph';
9
9
  import { byPosition } from '@dxos/util';
10
10
 
11
11
  import { PLANK_COMPANION_TYPE } from '../types';
12
12
 
13
13
  export const useCompanions = (id?: string) => {
14
14
  const { graph } = useAppGraph();
15
- const node = useNode(graph, id);
16
- const companions = node ? graph.nodes(node, { type: PLANK_COMPANION_TYPE }) : [];
15
+ const nodes = useConnections(graph, id);
16
+ const companions = nodes.filter((node) => node.type === PLANK_COMPANION_TYPE);
17
17
  return useMemo(() => companions.toSorted((a, b) => byPosition(a.properties, b.properties)), [companions]);
18
18
  };