@dxos/plugin-deck 0.8.2-staging.7ac8446 → 0.8.2

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 (155) 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-SEYECDHI.mjs → check-app-scheme-7AXGR6UT.mjs} +2 -3
  4. package/dist/lib/browser/check-app-scheme-7AXGR6UT.mjs.map +7 -0
  5. package/dist/lib/browser/{state-7TN26M42.mjs → chunk-FX44YX3G.mjs} +11 -8
  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 +8 -8
  18. package/dist/lib/browser/index.mjs.map +2 -2
  19. package/dist/lib/browser/{intent-resolver-UDYKO2QW.mjs → intent-resolver-3GAC57UA.mjs} +135 -92
  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-XLXN2VEW.mjs → react-root-ISFFOJZX.mjs} +7 -7
  23. package/dist/lib/browser/{react-surface-WNGMZL7I.mjs → react-surface-A63RQB5N.mjs} +7 -7
  24. package/dist/lib/browser/{settings-HMDGSBGO.mjs → settings-X7GDEXU3.mjs} +6 -6
  25. package/dist/lib/browser/settings-X7GDEXU3.mjs.map +7 -0
  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-SC6QEN7R.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-ODG4B6NX.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 +18 -8
  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 +8 -183
  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 +12 -7
  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/Dialog.d.ts +3 -0
  53. package/dist/types/src/components/DeckLayout/Dialog.d.ts.map +1 -0
  54. package/dist/types/src/components/DeckLayout/Popover.d.ts +5 -0
  55. package/dist/types/src/components/DeckLayout/Popover.d.ts.map +1 -0
  56. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  57. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  58. package/dist/types/src/components/DeckSettings/DeckSettings.d.ts.map +1 -1
  59. package/dist/types/src/components/Plank/Plank.d.ts +18 -5
  60. package/dist/types/src/components/Plank/Plank.d.ts.map +1 -1
  61. package/dist/types/src/components/Plank/Plank.stories.d.ts +3 -3
  62. package/dist/types/src/components/Plank/Plank.stories.d.ts.map +1 -1
  63. package/dist/types/src/components/Plank/PlankControls.d.ts +3 -2
  64. package/dist/types/src/components/Plank/PlankControls.d.ts.map +1 -1
  65. package/dist/types/src/components/Plank/PlankError.d.ts.map +1 -1
  66. package/dist/types/src/components/Plank/PlankHeading.d.ts +3 -2
  67. package/dist/types/src/components/Plank/PlankHeading.d.ts.map +1 -1
  68. package/dist/types/src/components/Sidebar/ComplementarySidebar.d.ts.map +1 -1
  69. package/dist/types/src/components/Sidebar/Sidebar.d.ts.map +1 -1
  70. package/dist/types/src/components/Sidebar/SidebarButton.d.ts +2 -1
  71. package/dist/types/src/components/Sidebar/SidebarButton.d.ts.map +1 -1
  72. package/dist/types/src/hooks/index.d.ts +5 -1
  73. package/dist/types/src/hooks/index.d.ts.map +1 -1
  74. package/dist/types/src/hooks/useBreakpoints.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
  76. package/dist/types/src/hooks/useDeckCompanions.d.ts +13 -0
  77. package/dist/types/src/hooks/useDeckCompanions.d.ts.map +1 -0
  78. package/dist/types/src/hooks/useHoistStatusbar.d.ts +3 -0
  79. package/dist/types/src/hooks/useHoistStatusbar.d.ts.map +1 -0
  80. package/dist/types/src/hooks/useNodeActionExpander.d.ts.map +1 -1
  81. package/dist/types/src/index.d.ts +1 -1
  82. package/dist/types/src/index.d.ts.map +1 -1
  83. package/dist/types/src/layout.d.ts.map +1 -1
  84. package/dist/types/src/translations.d.ts +2 -1
  85. package/dist/types/src/translations.d.ts.map +1 -1
  86. package/dist/types/src/types.d.ts +108 -104
  87. package/dist/types/src/types.d.ts.map +1 -1
  88. package/dist/types/src/util/index.d.ts +1 -4
  89. package/dist/types/src/util/index.d.ts.map +1 -1
  90. package/dist/types/src/util/layoutAppliesTopbar.d.ts +2 -1
  91. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -1
  92. package/dist/types/src/util/overscroll.d.ts.map +1 -1
  93. package/dist/types/src/util/set-active.d.ts.map +1 -1
  94. package/dist/types/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +39 -30
  96. package/src/capabilities/app-graph-builder.ts +120 -92
  97. package/src/capabilities/check-app-scheme.ts +3 -7
  98. package/src/capabilities/index.ts +3 -2
  99. package/src/capabilities/intent-resolver.ts +181 -135
  100. package/src/capabilities/settings.ts +4 -4
  101. package/src/capabilities/state.ts +7 -4
  102. package/src/capabilities/tools.ts +15 -12
  103. package/src/capabilities/url-handler.ts +4 -4
  104. package/src/components/DeckLayout/ContentEmpty.tsx +9 -4
  105. package/src/components/DeckLayout/DeckLayout.tsx +123 -188
  106. package/src/components/DeckLayout/Dialog.tsx +36 -0
  107. package/src/components/DeckLayout/Popover.tsx +104 -0
  108. package/src/components/Plank/Plank.stories.tsx +20 -8
  109. package/src/components/Plank/Plank.tsx +105 -69
  110. package/src/components/Plank/PlankControls.tsx +53 -57
  111. package/src/components/Plank/PlankError.tsx +2 -6
  112. package/src/components/Plank/PlankHeading.tsx +31 -12
  113. package/src/components/Sidebar/ComplementarySidebar.tsx +36 -57
  114. package/src/components/Sidebar/Sidebar.tsx +7 -4
  115. package/src/components/Sidebar/SidebarButton.tsx +26 -7
  116. package/src/components/fragments.ts +1 -1
  117. package/src/hooks/index.ts +5 -1
  118. package/src/{util → hooks}/useCompanions.ts +3 -3
  119. package/src/hooks/useDeckCompanions.ts +33 -0
  120. package/src/{util → hooks}/useHoistStatusbar.ts +9 -4
  121. package/src/hooks/useNodeActionExpander.ts +3 -8
  122. package/src/index.ts +1 -1
  123. package/src/translations.ts +2 -1
  124. package/src/types.ts +77 -71
  125. package/src/util/index.ts +1 -4
  126. package/src/util/layoutAppliesTopbar.ts +8 -2
  127. package/dist/lib/browser/app-graph-builder-VYZ4IWI3.mjs.map +0 -7
  128. package/dist/lib/browser/check-app-scheme-SEYECDHI.mjs.map +0 -7
  129. package/dist/lib/browser/chunk-6ZSOFCPP.mjs +0 -117
  130. package/dist/lib/browser/chunk-6ZSOFCPP.mjs.map +0 -7
  131. package/dist/lib/browser/chunk-B4LOJUWW.mjs +0 -24
  132. package/dist/lib/browser/chunk-B4LOJUWW.mjs.map +0 -7
  133. package/dist/lib/browser/chunk-FJBMNSUC.mjs +0 -1289
  134. package/dist/lib/browser/chunk-FJBMNSUC.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-FLOVGNYB.mjs +0 -81
  136. package/dist/lib/browser/chunk-FLOVGNYB.mjs.map +0 -7
  137. package/dist/lib/browser/chunk-RJNCG4ND.mjs +0 -154
  138. package/dist/lib/browser/chunk-RJNCG4ND.mjs.map +0 -7
  139. package/dist/lib/browser/intent-resolver-UDYKO2QW.mjs.map +0 -7
  140. package/dist/lib/browser/settings-HMDGSBGO.mjs.map +0 -7
  141. package/dist/lib/browser/state-7TN26M42.mjs.map +0 -7
  142. package/dist/lib/browser/tools-SC6QEN7R.mjs.map +0 -7
  143. package/dist/lib/browser/url-handler-ODG4B6NX.mjs.map +0 -7
  144. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts +0 -5
  145. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +0 -1
  146. package/dist/types/src/util/useBreakpoints.d.ts.map +0 -1
  147. package/dist/types/src/util/useCompanions.d.ts.map +0 -1
  148. package/dist/types/src/util/useHoistStatusbar.d.ts +0 -2
  149. package/dist/types/src/util/useHoistStatusbar.d.ts.map +0 -1
  150. package/src/components/DeckLayout/Fullscreen.tsx +0 -31
  151. /package/dist/lib/browser/{react-root-XLXN2VEW.mjs.map → react-root-ISFFOJZX.mjs.map} +0 -0
  152. /package/dist/lib/browser/{react-surface-WNGMZL7I.mjs.map → react-surface-A63RQB5N.mjs.map} +0 -0
  153. /package/dist/types/src/{util → hooks}/useBreakpoints.d.ts +0 -0
  154. /package/dist/types/src/{util → hooks}/useCompanions.d.ts +0 -0
  155. /package/src/{util → hooks}/useBreakpoints.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),
@@ -101,9 +180,9 @@ const PlankImpl = memo(
101
180
  }
102
181
  }, [id, scrollIntoView, layoutMode]);
103
182
 
104
- const isSolo = layoutMode === 'solo' && part === 'solo';
183
+ const isSolo = layoutMode.startsWith('solo') && part === 'solo';
105
184
  const isAttendable =
106
- (layoutMode === 'solo' && part.startsWith('solo')) || (layoutMode === 'deck' && part === 'deck');
185
+ (layoutMode.startsWith('solo') && part.startsWith('solo')) || (layoutMode === 'deck' && part === 'deck');
107
186
 
108
187
  const sizeAttrs = useMainSize();
109
188
 
@@ -116,14 +195,15 @@ const PlankImpl = memo(
116
195
  path,
117
196
  popoverAnchorId,
118
197
  },
119
- [node, node?.data, path, popoverAnchorId, primary?.data],
198
+ [node, node?.data, path, popoverAnchorId, primary?.data, variant],
120
199
  );
121
200
 
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',
@@ -143,7 +223,7 @@ const PlankImpl = memo(
143
223
  : {
144
224
  item: { id },
145
225
  size,
146
- onSizeChange: setSize,
226
+ onSizeChange: handleSizeChange,
147
227
  classNames: className,
148
228
  order,
149
229
  role: 'article',
@@ -157,6 +237,7 @@ const PlankImpl = memo(
157
237
  id={id}
158
238
  part={part.startsWith('solo-') ? 'solo' : part}
159
239
  node={node}
240
+ layoutMode={layoutMode}
160
241
  deckEnabled={settings?.enableDeck}
161
242
  canIncrementStart={canIncrementStart}
162
243
  canIncrementEnd={canIncrementEnd}
@@ -177,54 +258,9 @@ const PlankImpl = memo(
177
258
  ) : (
178
259
  <PlankError id={id} part={part} />
179
260
  )}
261
+
180
262
  {canResize && <StackItem.ResizeHandle />}
181
263
  </Root>
182
264
  );
183
265
  },
184
266
  );
185
-
186
- const SplitFrame = ({ children }: PropsWithChildren<{}>) => {
187
- const sizeAttrs = useMainSize();
188
- return (
189
- <div
190
- role='none'
191
- className={mx('grid grid-cols-[1fr_1fr] absolute inset-0', railGridHorizontal, mainIntrinsicSize)}
192
- {...sizeAttrs}
193
- >
194
- {children}
195
- </div>
196
- );
197
- };
198
-
199
- export const Plank = ({ id = UNKNOWN_ID, ...props }: PlankProps) => {
200
- const { graph } = useAppGraph();
201
- const node = useNode(graph, id);
202
- const companions = useCompanions(id);
203
- const currentCompanion = companions.find(({ id }) => id === props.companionId);
204
-
205
- if (props.companionId) {
206
- const Root = props.part === 'solo' ? SplitFrame : Fragment;
207
- return (
208
- <Root>
209
- <PlankImpl
210
- id={id}
211
- node={node}
212
- companioned='primary'
213
- {...props}
214
- {...(props.part === 'solo' ? { part: 'solo-primary' } : {})}
215
- />
216
- <PlankImpl
217
- id={props.companionId}
218
- node={currentCompanion}
219
- companioned='companion'
220
- primary={node}
221
- companions={companions}
222
- {...props}
223
- {...(props.part === 'solo' ? { part: 'solo-companion' } : { order: props.order! + 1 })}
224
- />
225
- </Root>
226
- );
227
- } else {
228
- return <PlankImpl id={id} node={node} companions={companions} {...props} />;
229
- }
230
- };
@@ -6,18 +6,10 @@ 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
- import { DeckAction } from '../../types';
12
+ import { DeckAction, type LayoutMode } from '../../types';
21
13
 
22
14
  export type PlankControlHandler = (event: DeckAction.PartAdjustment) => void;
23
15
 
@@ -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
 
@@ -34,27 +27,15 @@ export type PlankControlsProps = Omit<ButtonGroupProps, 'onClick'> & {
34
27
  variant?: 'hide-disabled' | 'default';
35
28
  close?: boolean | 'minify-start' | 'minify-end';
36
29
  capabilities: PlankCapabilities;
37
- isSolo?: boolean;
30
+ layoutMode?: LayoutMode;
38
31
  pin?: 'start' | 'end' | 'both';
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;
@@ -73,8 +54,7 @@ export const PlankCompanionControls = forwardRef<HTMLDivElement, PlankCompliment
73
54
  <PlankControl
74
55
  label={t('close companion label')}
75
56
  variant='ghost'
76
- // icon='ph--minus--regular'
77
- icon='ph--caret-left--regular'
57
+ icon='ph--x--regular'
78
58
  onClick={handleCloseCompanion}
79
59
  classNames={plankControlSpacing}
80
60
  />
@@ -88,35 +68,51 @@ export const PlankCompanionControls = forwardRef<HTMLDivElement, PlankCompliment
88
68
  // NOTE(thure): Pinning & unpinning are disabled indefinitely.
89
69
  export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
90
70
  (
91
- { children, classNames, variant = 'default', capabilities, isSolo, pin, close = false, onClick, ...props },
71
+ { children, classNames, variant = 'default', capabilities, layoutMode, pin, close = false, onClick, ...props },
92
72
  forwardedRef,
93
73
  ) => {
94
74
  const { t } = useTranslation(DECK_PLUGIN);
95
75
  const buttonClassNames =
96
76
  variant === 'hide-disabled' ? `disabled:hidden ${plankControlSpacing}` : plankControlSpacing;
97
77
 
98
- return (
99
- <ButtonGroup {...props} classNames={['app-no-drag', classNames]} ref={forwardedRef}>
100
- {/* {pin && !isSolo && ['both', 'start'].includes(pin) && (
101
- <PlankControl
102
- label={t('pin start label')}
103
- variant='ghost'
104
- classNames={buttonClassNames}
105
- onClick={() => onClick?.('pin-start')}
106
- icon='ph--caret-line-left--regular'
107
- />
108
- )} */}
78
+ const layoutIsAnySolo = !!layoutMode?.startsWith('solo');
109
79
 
110
- {capabilities.deck && capabilities.solo && (
80
+ return (
81
+ <ButtonGroup {...props} classNames={['app-no-drag !opacity-100', classNames]} ref={forwardedRef}>
82
+ {capabilities.deck ? (
111
83
  <>
112
- <PlankControl
113
- label={isSolo ? t('show deck plank label') : t('show solo plank label')}
114
- classNames={buttonClassNames}
115
- icon={isSolo ? 'ph--corners-in--regular' : 'ph--corners-out--regular'}
116
- onClick={() => onClick?.('solo')}
117
- />
84
+ {capabilities.solo && (
85
+ <>
86
+ {layoutMode === 'solo' && (
87
+ <PlankControl
88
+ label={t('show fullscreen plank label')}
89
+ classNames={buttonClassNames}
90
+ icon='ph--corners-out--regular'
91
+ onClick={() => onClick?.('solo--fullscreen')}
92
+ />
93
+ )}
94
+ <PlankControl
95
+ label={t(
96
+ layoutMode === 'solo--fullscreen'
97
+ ? 'exit fullscreen label'
98
+ : layoutIsAnySolo
99
+ ? 'show deck plank label'
100
+ : 'show solo plank label',
101
+ )}
102
+ classNames={buttonClassNames}
103
+ icon={
104
+ layoutMode === 'solo--fullscreen'
105
+ ? 'ph--corners-in--regular'
106
+ : layoutIsAnySolo
107
+ ? 'ph--arrows-in-line-horizontal--regular'
108
+ : 'ph--arrows-out-line-horizontal--regular'
109
+ }
110
+ onClick={() => onClick?.(layoutMode === 'solo--fullscreen' ? 'solo--fullscreen' : 'solo')}
111
+ />
112
+ </>
113
+ )}
118
114
 
119
- {!isSolo && (
115
+ {!layoutIsAnySolo && (
120
116
  <>
121
117
  <PlankControl
122
118
  label={t('increment start label')}
@@ -135,18 +131,18 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
135
131
  </>
136
132
  )}
137
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
+ )
138
143
  )}
139
144
 
140
- {/* {pin && !isSolo && ['both', 'end'].includes(pin) && (
141
- <PlankControl
142
- label={t('pin end label')}
143
- classNames={buttonClassNames}
144
- icon='ph--caret-line-right--regular'
145
- onClick={() => onClick?.('pin-end')}
146
- />
147
- )} */}
148
-
149
- {close && !isSolo && (
145
+ {close && !layoutIsAnySolo && (
150
146
  <PlankControl
151
147
  label={t(`${typeof close === 'string' ? 'minify' : 'close'} label`)}
152
148
  classNames={buttonClassNames}
@@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react';
6
6
 
7
7
  import { type Node } from '@dxos/plugin-graph';
8
8
  import { useTranslation } from '@dxos/react-ui';
9
- import { descriptionText, mx } from '@dxos/react-ui-theme';
9
+ import { descriptionMessage, mx } from '@dxos/react-ui-theme';
10
10
 
11
11
  import { PlankHeading, type PlankHeadingProps } from './PlankHeading';
12
12
  import { PlankLoading } from './PlankLoading';
@@ -19,11 +19,7 @@ export const PlankContentError = ({ error }: { error?: Error }) => {
19
19
  <div role='none' className='overflow-auto p-8 attention-surface grid place-items-center'>
20
20
  <p
21
21
  role='alert'
22
- className={mx(
23
- descriptionText,
24
- 'break-words border border-dashed border-separator rounded-lg p-8',
25
- errorString.length < 256 && 'text-lg',
26
- )}
22
+ className={mx(descriptionMessage, 'break-words rounded-lg p-8', errorString.length < 256 && 'text-lg')}
27
23
  >
28
24
  {error ? errorString : t('error fallback message')}
29
25
  </p>
@@ -9,17 +9,21 @@ 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
- import { PLANK_COMPANION_TYPE, DeckAction, type ResolvedPart } from '../../types';
17
- import { useBreakpoints } from '../../util';
18
+ import { PLANK_COMPANION_TYPE, DeckAction, type ResolvedPart, type LayoutMode } from '../../types';
18
19
  import { soloInlinePadding } from '../fragments';
19
20
 
21
+ const MAX_COMPANIONS = 5;
22
+
20
23
  export type PlankHeadingProps = {
21
24
  id: string;
22
25
  part: ResolvedPart;
26
+ layoutMode?: LayoutMode;
23
27
  node?: Node;
24
28
  deckEnabled?: boolean;
25
29
  canIncrementStart?: boolean;
@@ -45,6 +49,7 @@ export const PlankHeading = memo(
45
49
  pending,
46
50
  companioned,
47
51
  companions,
52
+ layoutMode,
48
53
  actions = [],
49
54
  }: PlankHeadingProps) => {
50
55
  const { t } = useTranslation(DECK_PLUGIN);
@@ -61,7 +66,9 @@ export const PlankHeading = memo(
61
66
  useEffect(() => {
62
67
  const frame = requestAnimationFrame(() => {
63
68
  // Load actions for the node.
64
- node && graph.actions(node);
69
+ if (node) {
70
+ void graph.expand(node.id);
71
+ }
65
72
  });
66
73
 
67
74
  return () => cancelAnimationFrame(frame);
@@ -74,6 +81,7 @@ export const PlankHeading = memo(
74
81
  solo: breakpoint !== 'mobile' && (part === 'solo' || part === 'deck'),
75
82
  incrementStart: canIncrementStart,
76
83
  incrementEnd: canIncrementEnd,
84
+ fullscreen: !isCompanionNode,
77
85
  companion: !isCompanionNode && companions && companions.length > 0,
78
86
  }),
79
87
  [breakpoint, part, companions, canIncrementStart, canIncrementEnd, isCompanionNode, deckEnabled],
@@ -86,17 +94,20 @@ export const PlankHeading = memo(
86
94
  } else if (variant) {
87
95
  return [];
88
96
  } else {
89
- return [actions, graph.actions(node)].filter((a) => a.length > 0);
97
+ return [actions, graph.getActions(node.id)].filter((a) => a.length > 0);
90
98
  }
91
99
  }, [actions, node, variant, graph]);
92
100
 
93
- const handleAction = useCallback((action: StackItemSigilAction) => {
94
- typeof action.data === 'function' && action.data?.({ node: action as Node, caller: DECK_PLUGIN });
95
- }, []);
101
+ const handleAction = useCallback(
102
+ (action: StackItemSigilAction) => {
103
+ typeof action.data === 'function' && action.data?.({ parent: node, caller: DECK_PLUGIN });
104
+ },
105
+ [node],
106
+ );
96
107
 
97
108
  const handlePlankAction = useCallback(
98
109
  (eventType: DeckAction.PartAdjustment) => {
99
- if (eventType === 'solo') {
110
+ if (eventType.startsWith('solo')) {
100
111
  return dispatch(createIntent(DeckAction.Adjust, { type: eventType, id }));
101
112
  } else if (eventType === 'close') {
102
113
  if (part === 'complementary') {
@@ -139,18 +150,26 @@ export const PlankHeading = memo(
139
150
  return (
140
151
  <StackItem.Heading
141
152
  classNames={[
142
- 'plb-1 border-be border-separator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 layout-contain',
153
+ 'plb-1 border-be border-subduedSeparator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 contain-layout',
143
154
  part === 'solo' ? soloInlinePadding : 'pli-1',
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
+ : []),
144
162
  ]}
163
+ data-plank-heading
145
164
  >
146
- {companions && isCompanionNode ? (
165
+ {companions && isCompanionNode /* TODO(thure): This is a tablist, it should be implemented as such. */ ? (
147
166
  <div role='none' className='flex-1 min-is-0 overflow-x-auto scrollbar-thin flex gap-1'>
148
167
  {companions.map(({ id, properties: { icon, label } }) => (
149
168
  <IconButton
150
169
  key={id}
151
170
  data-id={id}
152
171
  icon={icon}
153
- iconOnly={node?.id !== id}
172
+ iconOnly={companions.length > MAX_COMPANIONS && node?.id !== id}
154
173
  label={toLocalizedString(label, t)}
155
174
  size={5}
156
175
  variant={node?.id === id ? 'primary' : 'default'}
@@ -196,7 +215,7 @@ export const PlankHeading = memo(
196
215
  ) : (
197
216
  <PlankControls
198
217
  capabilities={capabilities}
199
- isSolo={part === 'solo'}
218
+ layoutMode={layoutMode}
200
219
  close={part === 'complementary' ? 'minify-end' : true}
201
220
  onClick={handlePlankAction}
202
221
  />