@dxos/plugin-deck 0.8.2-main.12df754 → 0.8.2-main.2f9c567

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 (56) hide show
  1. package/dist/lib/browser/{check-app-scheme-SEYECDHI.mjs → check-app-scheme-O7JPE4TM.mjs} +2 -3
  2. package/dist/lib/browser/check-app-scheme-O7JPE4TM.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-VP6FCWFV.mjs → chunk-AHTP72DY.mjs} +80 -55
  4. package/dist/lib/browser/chunk-AHTP72DY.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-E7TOEOGO.mjs +157 -0
  6. package/dist/lib/browser/chunk-E7TOEOGO.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-6HJZL3WT.mjs → chunk-PGSJT5PG.mjs} +2 -2
  8. package/dist/lib/browser/chunk-RKYIMUKW.mjs +24 -0
  9. package/dist/lib/browser/index.mjs +3 -3
  10. package/dist/lib/browser/{intent-resolver-6AK45PT5.mjs → intent-resolver-NO6L67KF.mjs} +33 -26
  11. package/dist/lib/browser/intent-resolver-NO6L67KF.mjs.map +7 -0
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/browser/{react-root-KA2IL5RA.mjs → react-root-5RWCIUXV.mjs} +5 -5
  14. package/dist/lib/browser/{react-surface-LIPGYEYN.mjs → react-surface-DIDOPTH7.mjs} +5 -5
  15. package/dist/lib/browser/{settings-6NU7CF2B.mjs → settings-C7LX2GXF.mjs} +2 -2
  16. package/dist/lib/browser/{state-Z6UY2Z3M.mjs → state-AX74YEJD.mjs} +2 -2
  17. package/dist/lib/browser/{tools-VDVQTJMD.mjs → tools-7W7KZRAX.mjs} +7 -7
  18. package/dist/lib/browser/tools-7W7KZRAX.mjs.map +7 -0
  19. package/dist/lib/browser/types.mjs +1 -1
  20. package/dist/lib/browser/{url-handler-3CARFXQK.mjs → url-handler-AF5SYROZ.mjs} +2 -2
  21. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  22. package/dist/types/src/capabilities/check-app-scheme.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/tools.d.ts.map +1 -1
  26. package/dist/types/src/components/DeckLayout/Popover.d.ts.map +1 -1
  27. package/dist/types/src/components/Plank/Plank.d.ts.map +1 -1
  28. package/dist/types/src/components/Plank/PlankControls.d.ts.map +1 -1
  29. package/dist/types/src/components/Plank/PlankError.d.ts.map +1 -1
  30. package/dist/types/src/components/Plank/PlankHeading.d.ts.map +1 -1
  31. package/dist/types/src/types.d.ts +104 -104
  32. package/dist/types/src/types.d.ts.map +1 -1
  33. package/package.json +31 -29
  34. package/src/capabilities/check-app-scheme.ts +3 -5
  35. package/src/capabilities/intent-resolver.ts +85 -77
  36. package/src/capabilities/tools.ts +4 -3
  37. package/src/components/DeckLayout/Popover.tsx +71 -43
  38. package/src/components/Plank/Plank.tsx +3 -1
  39. package/src/components/Plank/PlankControls.tsx +11 -5
  40. package/src/components/Plank/PlankError.tsx +2 -6
  41. package/src/components/Plank/PlankHeading.tsx +6 -1
  42. package/src/types.ts +72 -71
  43. package/dist/lib/browser/check-app-scheme-SEYECDHI.mjs.map +0 -7
  44. package/dist/lib/browser/chunk-4QSEGMY3.mjs +0 -24
  45. package/dist/lib/browser/chunk-VP6FCWFV.mjs.map +0 -7
  46. package/dist/lib/browser/chunk-ZMJMCN7O.mjs +0 -157
  47. package/dist/lib/browser/chunk-ZMJMCN7O.mjs.map +0 -7
  48. package/dist/lib/browser/intent-resolver-6AK45PT5.mjs.map +0 -7
  49. package/dist/lib/browser/tools-VDVQTJMD.mjs.map +0 -7
  50. /package/dist/lib/browser/{chunk-6HJZL3WT.mjs.map → chunk-PGSJT5PG.mjs.map} +0 -0
  51. /package/dist/lib/browser/{chunk-4QSEGMY3.mjs.map → chunk-RKYIMUKW.mjs.map} +0 -0
  52. /package/dist/lib/browser/{react-root-KA2IL5RA.mjs.map → react-root-5RWCIUXV.mjs.map} +0 -0
  53. /package/dist/lib/browser/{react-surface-LIPGYEYN.mjs.map → react-surface-DIDOPTH7.mjs.map} +0 -0
  54. /package/dist/lib/browser/{settings-6NU7CF2B.mjs.map → settings-C7LX2GXF.mjs.map} +0 -0
  55. /package/dist/lib/browser/{state-Z6UY2Z3M.mjs.map → state-AX74YEJD.mjs.map} +0 -0
  56. /package/dist/lib/browser/{url-handler-3CARFXQK.mjs.map → url-handler-AF5SYROZ.mjs.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { batch } from '@preact/signals-core';
6
- import { pipe } from 'effect';
6
+ import { Schema, Effect, pipe } from 'effect';
7
7
 
8
8
  import {
9
9
  Capabilities,
@@ -15,7 +15,7 @@ import {
15
15
  createIntent,
16
16
  chain,
17
17
  } from '@dxos/app-framework';
18
- import { getTypename, S } from '@dxos/echo-schema';
18
+ import { getTypename } from '@dxos/echo-schema';
19
19
  import { invariant } from '@dxos/invariant';
20
20
  import { isLiveObject } from '@dxos/live-object';
21
21
  import { log } from '@dxos/log';
@@ -67,10 +67,10 @@ export default (context: PluginsContext) =>
67
67
  }),
68
68
  createResolver({
69
69
  intent: LayoutAction.UpdateLayout,
70
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateSidebar.fields.input)`
70
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.UpdateSidebar.fields.input)`
71
71
  // but the filter is not being applied correctly.
72
- filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateSidebar.fields.input> =>
73
- S.is(LayoutAction.UpdateSidebar.fields.input)(data),
72
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.UpdateSidebar.fields.input> =>
73
+ Schema.is(LayoutAction.UpdateSidebar.fields.input)(data),
74
74
  resolve: ({ options }) => {
75
75
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
76
76
  const next = options?.state ?? layout.sidebarState;
@@ -81,10 +81,10 @@ export default (context: PluginsContext) =>
81
81
  }),
82
82
  createResolver({
83
83
  intent: LayoutAction.UpdateLayout,
84
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateComplementary.fields.input)`
84
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.UpdateComplementary.fields.input)`
85
85
  // but the filter is not being applied correctly.
86
- filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateComplementary.fields.input> =>
87
- S.is(LayoutAction.UpdateComplementary.fields.input)(data),
86
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.UpdateComplementary.fields.input> =>
87
+ Schema.is(LayoutAction.UpdateComplementary.fields.input)(data),
88
88
  resolve: ({ subject, options }) => {
89
89
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
90
90
 
@@ -100,10 +100,10 @@ export default (context: PluginsContext) =>
100
100
  }),
101
101
  createResolver({
102
102
  intent: LayoutAction.UpdateLayout,
103
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateDialog.fields.input)`
103
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.UpdateDialog.fields.input)`
104
104
  // but the filter is not being applied correctly.
105
- filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateDialog.fields.input> =>
106
- S.is(LayoutAction.UpdateDialog.fields.input)(data),
105
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.UpdateDialog.fields.input> =>
106
+ Schema.is(LayoutAction.UpdateDialog.fields.input)(data),
107
107
  resolve: ({ subject, options }) => {
108
108
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
109
109
  layout.dialogOpen = options.state ?? Boolean(subject);
@@ -114,10 +114,10 @@ export default (context: PluginsContext) =>
114
114
  }),
115
115
  createResolver({
116
116
  intent: LayoutAction.UpdateLayout,
117
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdatePopover.fields.input)`
117
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.UpdatePopover.fields.input)`
118
118
  // but the filter is not being applied correctly.
119
- filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdatePopover.fields.input> =>
120
- S.is(LayoutAction.UpdatePopover.fields.input)(data),
119
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.UpdatePopover.fields.input> =>
120
+ Schema.is(LayoutAction.UpdatePopover.fields.input)(data),
121
121
  resolve: ({ subject, options }) => {
122
122
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
123
123
  layout.popoverOpen = options.state ?? Boolean(subject);
@@ -133,10 +133,10 @@ export default (context: PluginsContext) =>
133
133
  }),
134
134
  createResolver({
135
135
  intent: LayoutAction.UpdateLayout,
136
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.AddToast.fields.input)`
136
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.AddToast.fields.input)`
137
137
  // but the filter is not being applied correctly.
138
- filter: (data): data is S.Schema.Type<typeof LayoutAction.AddToast.fields.input> =>
139
- S.is(LayoutAction.AddToast.fields.input)(data),
138
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.AddToast.fields.input> =>
139
+ Schema.is(LayoutAction.AddToast.fields.input)(data),
140
140
  resolve: ({ subject }) => {
141
141
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
142
142
  layout.toasts.push(subject);
@@ -144,10 +144,10 @@ export default (context: PluginsContext) =>
144
144
  }),
145
145
  createResolver({
146
146
  intent: LayoutAction.UpdateLayout,
147
- // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.SetLayoutMode.fields.input)`
147
+ // TODO(wittjosiah): This should be able to just be `Schema.is(LayoutAction.SetLayoutMode.fields.input)`
148
148
  // but the filter is not being applied correctly.
149
- filter: (data): data is S.Schema.Type<typeof LayoutAction.SetLayoutMode.fields.input> => {
150
- if (!S.is(LayoutAction.SetLayoutMode.fields.input)(data)) {
149
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SetLayoutMode.fields.input> => {
150
+ if (!Schema.is(LayoutAction.SetLayoutMode.fields.input)(data)) {
151
151
  return false;
152
152
  }
153
153
 
@@ -202,8 +202,8 @@ export default (context: PluginsContext) =>
202
202
  }),
203
203
  createResolver({
204
204
  intent: LayoutAction.UpdateLayout,
205
- filter: (data): data is S.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
206
- S.is(LayoutAction.SwitchWorkspace.fields.input)(data),
205
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
206
+ Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
207
207
  resolve: ({ subject }) => {
208
208
  const state = context.requestCapability(DeckCapabilities.MutableDeckState);
209
209
  batch(() => {
@@ -228,8 +228,8 @@ export default (context: PluginsContext) =>
228
228
  }),
229
229
  createResolver({
230
230
  intent: LayoutAction.UpdateLayout,
231
- filter: (data): data is S.Schema.Type<typeof LayoutAction.RevertWorkspace.fields.input> =>
232
- S.is(LayoutAction.RevertWorkspace.fields.input)(data),
231
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.RevertWorkspace.fields.input> =>
232
+ Schema.is(LayoutAction.RevertWorkspace.fields.input)(data),
233
233
  resolve: () => {
234
234
  const state = context.requestCapability(DeckCapabilities.MutableDeckState);
235
235
  return {
@@ -239,62 +239,70 @@ export default (context: PluginsContext) =>
239
239
  }),
240
240
  createResolver({
241
241
  intent: LayoutAction.UpdateLayout,
242
- filter: (data): data is S.Schema.Type<typeof LayoutAction.Open.fields.input> =>
243
- S.is(LayoutAction.Open.fields.input)(data),
244
- resolve: ({ subject, options }) => {
245
- const { graph } = context.requestCapability(Capabilities.AppGraph);
246
- const state = context.requestCapability(DeckCapabilities.MutableDeckState);
247
- const attention = context.requestCapability(AttentionCapabilities.Attention);
248
- const settings = context
249
- .requestCapabilities(Capabilities.SettingsStore)[0]
250
- ?.getStore<DeckSettingsProps>(DECK_PLUGIN)?.value;
242
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.Open.fields.input> =>
243
+ Schema.is(LayoutAction.Open.fields.input)(data),
244
+ resolve: ({ subject, options }) =>
245
+ Effect.gen(function* () {
246
+ const { graph } = context.requestCapability(Capabilities.AppGraph);
247
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
248
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
249
+ const settings = context
250
+ .requestCapabilities(Capabilities.SettingsStore)[0]
251
+ ?.getStore<DeckSettingsProps>(DECK_PLUGIN)?.value;
251
252
 
252
- const previouslyOpenIds = new Set<string>(state.deck.solo ? [state.deck.solo] : state.deck.active);
253
- batch(() => {
254
- const next = state.deck.solo
255
- ? (subject as string[]).map((id) => createEntryId(id, options?.variant))
256
- : subject.reduce(
257
- (acc, entryId) =>
258
- openEntry(acc, entryId, {
259
- key: options?.key,
260
- positioning: options?.positioning ?? settings?.newPlankPositioning,
261
- pivotId: options?.pivotId,
262
- variant: options?.variant,
263
- }),
264
- state.deck.active,
265
- );
253
+ if (options?.workspace && state.activeDeck !== options?.workspace) {
254
+ const { dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
255
+ yield* dispatch(
256
+ createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: options.workspace }),
257
+ );
258
+ }
266
259
 
267
- return setActive({ next, state, attention });
268
- });
260
+ const previouslyOpenIds = new Set<string>(state.deck.solo ? [state.deck.solo] : state.deck.active);
261
+ batch(() => {
262
+ const next = state.deck.solo
263
+ ? (subject as string[]).map((id) => createEntryId(id, options?.variant))
264
+ : subject.reduce(
265
+ (acc, entryId) =>
266
+ openEntry(acc, entryId, {
267
+ key: options?.key,
268
+ positioning: options?.positioning ?? settings?.newPlankPositioning,
269
+ pivotId: options?.pivotId,
270
+ variant: options?.variant,
271
+ }),
272
+ state.deck.active,
273
+ );
269
274
 
270
- const ids = state.deck.solo ? [state.deck.solo] : state.deck.active;
271
- const newlyOpen = ids.filter((i) => !previouslyOpenIds.has(i));
275
+ return setActive({ next, state, attention });
276
+ });
272
277
 
273
- return {
274
- intents: [
275
- ...(options?.scrollIntoView !== false
276
- ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: newlyOpen[0] ?? subject[0] })]
277
- : []),
278
- createIntent(LayoutAction.Expose, { part: 'navigation', subject: newlyOpen[0] ?? subject[0] }),
279
- ...newlyOpen.map((subjectId) => {
280
- const active = graph?.findNode(subjectId)?.data;
281
- const typename = isLiveObject(active) ? getTypename(active) : undefined;
282
- return createIntent(ObservabilityAction.SendEvent, {
283
- name: 'navigation.activate',
284
- properties: {
285
- subjectId,
286
- typename,
287
- },
288
- });
289
- }),
290
- ],
291
- };
292
- },
278
+ const ids = state.deck.solo ? [state.deck.solo] : state.deck.active;
279
+ const newlyOpen = ids.filter((i) => !previouslyOpenIds.has(i));
280
+
281
+ return {
282
+ intents: [
283
+ ...(options?.scrollIntoView !== false
284
+ ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: newlyOpen[0] ?? subject[0] })]
285
+ : []),
286
+ createIntent(LayoutAction.Expose, { part: 'navigation', subject: newlyOpen[0] ?? subject[0] }),
287
+ ...newlyOpen.map((subjectId) => {
288
+ const active = graph?.findNode(subjectId)?.data;
289
+ const typename = isLiveObject(active) ? getTypename(active) : undefined;
290
+ return createIntent(ObservabilityAction.SendEvent, {
291
+ name: 'navigation.activate',
292
+ properties: {
293
+ subjectId,
294
+ typename,
295
+ },
296
+ });
297
+ }),
298
+ ],
299
+ };
300
+ }),
293
301
  }),
294
302
  createResolver({
295
303
  intent: LayoutAction.UpdateLayout,
296
- filter: (data): data is S.Schema.Type<typeof LayoutAction.Close.fields.input> =>
297
- S.is(LayoutAction.Close.fields.input)(data),
304
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.Close.fields.input> =>
305
+ Schema.is(LayoutAction.Close.fields.input)(data),
298
306
  resolve: ({ subject }) => {
299
307
  const state = context.requestCapability(DeckCapabilities.MutableDeckState);
300
308
  const attention = context.requestCapability(AttentionCapabilities.Attention);
@@ -316,8 +324,8 @@ export default (context: PluginsContext) =>
316
324
  }),
317
325
  createResolver({
318
326
  intent: LayoutAction.UpdateLayout,
319
- filter: (data): data is S.Schema.Type<typeof LayoutAction.Set.fields.input> =>
320
- S.is(LayoutAction.Set.fields.input)(data),
327
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.Set.fields.input> =>
328
+ Schema.is(LayoutAction.Set.fields.input)(data),
321
329
  resolve: ({ subject }) => {
322
330
  const state = context.requestCapability(DeckCapabilities.MutableDeckState);
323
331
  const attention = context.requestCapability(AttentionCapabilities.Attention);
@@ -329,8 +337,8 @@ export default (context: PluginsContext) =>
329
337
  }),
330
338
  createResolver({
331
339
  intent: LayoutAction.UpdateLayout,
332
- filter: (data): data is S.Schema.Type<typeof LayoutAction.ScrollIntoView.fields.input> =>
333
- S.is(LayoutAction.ScrollIntoView.fields.input)(data),
340
+ filter: (data): data is Schema.Schema.Type<typeof LayoutAction.ScrollIntoView.fields.input> =>
341
+ Schema.is(LayoutAction.ScrollIntoView.fields.input)(data),
334
342
  resolve: ({ subject }) => {
335
343
  const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
336
344
  layout.scrollIntoView = subject;
@@ -2,6 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Schema } from 'effect';
6
+
5
7
  import {
6
8
  contributes,
7
9
  createIntent,
@@ -10,7 +12,6 @@ import {
10
12
  type PromiseIntentDispatcher,
11
13
  } from '@dxos/app-framework';
12
14
  import { defineTool, ToolResult } from '@dxos/artifact';
13
- import { S } from '@dxos/echo-schema';
14
15
  import { invariant } from '@dxos/invariant';
15
16
 
16
17
  import { meta } from '../meta';
@@ -35,8 +36,8 @@ export default () =>
35
36
  `,
36
37
  caption: 'Showing item...',
37
38
  // TODO(wittjosiah): Refactor Layout/Navigation/Deck actions so that they can be used directly.
38
- schema: S.Struct({
39
- id: S.String.annotations({
39
+ schema: Schema.Struct({
40
+ id: Schema.String.annotations({
40
41
  description: 'The ID of the item to show.',
41
42
  }),
42
43
  }),
@@ -2,72 +2,100 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
7
 
7
8
  import { Surface, useCapability } from '@dxos/app-framework';
8
- import { Popover } from '@dxos/react-ui';
9
+ import { Popover, type PopoverContentInteractOutsideEvent } from '@dxos/react-ui';
9
10
 
10
11
  import { DeckCapabilities } from '../../capabilities';
11
12
 
12
13
  export type DeckPopoverRootProps = PropsWithChildren<{}>;
13
14
 
15
+ const DEBOUNCE_DELAY = 40;
16
+
17
+ type DeckPopoverContextValue = {
18
+ setOpen: (open: boolean) => void;
19
+ };
20
+
21
+ const [DeckPopoverProvider, useDeckPopoverContext] = createContext<DeckPopoverContextValue>('DeckPopover');
22
+
14
23
  export const PopoverRoot = ({ children }: DeckPopoverRootProps) => {
15
- const context = useCapability(DeckCapabilities.MutableDeckState);
24
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
16
25
  const virtualRef = useRef<HTMLButtonElement | null>(null);
17
26
  const [virtualIter, setVirtualIter] = useState(0);
27
+ const [open, setOpen] = useState(false);
28
+ const debounceRef = useRef<NodeJS.Timeout | null>(null);
18
29
 
19
- // TODO(thure): This is a workaround for the difference in `React`ion time between displaying a Popover and rendering
20
- // the anchor further down the tree. Refactor to use VirtualTrigger or some other approach which does not cause a lag.
21
- const [delayedPopoverVisibility, setDelayedPopoverVisibility] = useState(false);
30
+ // TODO(thure): This is a workaround for the race condition between displaying a Popover and either rendering
31
+ // the anchor further down the tree or measuring the virtual trigger’s client rect.
22
32
  useEffect(() => {
23
- context.popoverOpen ? setTimeout(() => setDelayedPopoverVisibility(true), 40) : setDelayedPopoverVisibility(false);
24
- }, [context.popoverOpen]);
25
-
26
- const handlePopoverOpenChange = useCallback(
27
- (nextOpen: boolean) => {
28
- if (nextOpen && (context.popoverAnchor || context.popoverAnchorId)) {
29
- context.popoverOpen = true;
30
- } else {
31
- context.popoverOpen = false;
32
- context.popoverAnchor = undefined;
33
- context.popoverAnchorId = undefined;
34
- context.popoverSide = undefined;
33
+ setOpen(false);
34
+ if (layout.popoverOpen) {
35
+ if (debounceRef.current) {
36
+ clearTimeout(debounceRef.current);
35
37
  }
36
- },
37
- [context],
38
- );
39
-
40
- useEffect(() => {
41
- virtualRef.current = context.popoverAnchor ?? null;
42
- setVirtualIter((iter) => iter + 1);
43
- }, [context.popoverAnchor]);
38
+ if (layout.popoverAnchor && virtualRef.current !== layout.popoverAnchor) {
39
+ virtualRef.current = layout.popoverAnchor ?? null;
40
+ setVirtualIter((iter) => iter + 1);
41
+ }
42
+ debounceRef.current = setTimeout(() => setOpen(true), DEBOUNCE_DELAY);
43
+ }
44
+ }, [layout.popoverOpen, layout.popoverAnchorId, layout.popoverAnchor, layout.popoverContent]);
44
45
 
45
46
  return (
46
- <Popover.Root
47
- modal
48
- open={!!((context.popoverAnchor || context.popoverAnchorId) && delayedPopoverVisibility)}
49
- onOpenChange={handlePopoverOpenChange}
50
- >
51
- {context.popoverAnchor && <Popover.VirtualTrigger key={virtualIter} virtualRef={virtualRef} />}
52
- {children}
53
- </Popover.Root>
47
+ <DeckPopoverProvider setOpen={setOpen}>
48
+ <Popover.Root modal={false} open={open}>
49
+ {layout.popoverAnchor && <Popover.VirtualTrigger key={virtualIter} virtualRef={virtualRef} />}
50
+ {children}
51
+ </Popover.Root>
52
+ </DeckPopoverProvider>
54
53
  );
55
54
  };
56
55
 
57
56
  export const PopoverContent = () => {
58
- const context = useCapability(DeckCapabilities.MutableDeckState);
59
- const handlePopoverClose = useCallback(() => {
60
- context.popoverOpen = false;
61
- context.popoverAnchor = undefined;
62
- context.popoverAnchorId = undefined;
63
- context.popoverSide = undefined;
64
- }, [context]);
57
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
58
+ const { setOpen } = useDeckPopoverContext('PopoverContent');
59
+
60
+ const handleClose = useCallback(
61
+ (event: KeyboardEvent | PopoverContentInteractOutsideEvent) => {
62
+ if (
63
+ // TODO(thure): CodeMirror should not focus itself when it updates.
64
+ event.type === 'dismissableLayer.focusOutside' &&
65
+ (event.currentTarget as HTMLElement | undefined)?.classList.contains('cm-content')
66
+ ) {
67
+ event.preventDefault();
68
+ } else {
69
+ setOpen(false);
70
+ layout.popoverOpen = false;
71
+ layout.popoverAnchor = undefined;
72
+ layout.popoverAnchorId = undefined;
73
+ layout.popoverSide = undefined;
74
+ }
75
+ },
76
+ [setOpen],
77
+ );
78
+
79
+ const collisionBoundaries: HTMLElement[] = useMemo(() => {
80
+ const closest = layout.popoverAnchor?.closest('[data-popover-collision-boundary]') as
81
+ | HTMLElement
82
+ | null
83
+ | undefined;
84
+ return closest ? [closest] : [];
85
+ }, [layout.popoverAnchor]);
65
86
 
66
87
  return (
67
88
  <Popover.Portal>
68
- <Popover.Content side={context.popoverSide} onEscapeKeyDown={handlePopoverClose}>
89
+ <Popover.Content
90
+ side={layout.popoverSide}
91
+ onInteractOutside={handleClose}
92
+ onEscapeKeyDown={handleClose}
93
+ collisionBoundary={collisionBoundaries}
94
+ sticky='always'
95
+ hideWhenDetached
96
+ >
69
97
  <Popover.Viewport>
70
- <Surface role='popover' data={context.popoverContent} limit={1} />
98
+ <Surface role='popover' data={layout.popoverContent} limit={1} />
71
99
  </Popover.Viewport>
72
100
  <Popover.Arrow />
73
101
  </Popover.Content>
@@ -116,7 +116,7 @@ const PlankImpl = memo(
116
116
  path,
117
117
  popoverAnchorId,
118
118
  },
119
- [node, node?.data, path, popoverAnchorId, primary?.data],
119
+ [node, node?.data, path, popoverAnchorId, primary?.data, variant],
120
120
  );
121
121
 
122
122
  // TODO(wittjosiah): Change prop to accept a component.
@@ -131,6 +131,8 @@ const PlankImpl = memo(
131
131
  part === 'deck' && (companioned === 'companion' ? '!border-separator border-ie' : '!border-separator border-li'),
132
132
  part.startsWith('solo-') && 'row-span-2 grid-rows-subgrid min-is-0',
133
133
  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',
134
136
  );
135
137
 
136
138
  return (
@@ -73,7 +73,7 @@ export const PlankCompanionControls = forwardRef<HTMLDivElement, PlankCompliment
73
73
  <PlankControl
74
74
  label={t('close companion label')}
75
75
  variant='ghost'
76
- icon='ph--caret-left--regular'
76
+ icon='ph--x--regular'
77
77
  onClick={handleCloseCompanion}
78
78
  classNames={plankControlSpacing}
79
79
  />
@@ -114,12 +114,18 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
114
114
  label={t(
115
115
  layoutMode === 'solo--fullscreen'
116
116
  ? 'exit fullscreen label'
117
- : !layoutIsAnySolo
118
- ? 'show solo plank label'
119
- : 'show deck plank label',
117
+ : layoutIsAnySolo
118
+ ? 'show deck plank label'
119
+ : 'show solo plank label',
120
120
  )}
121
121
  classNames={buttonClassNames}
122
- icon={layoutIsAnySolo ? 'ph--corners-in--regular' : 'ph--corners-out--regular'}
122
+ icon={
123
+ layoutMode === 'solo--fullscreen'
124
+ ? 'ph--corners-in--regular'
125
+ : layoutIsAnySolo
126
+ ? 'ph--arrows-in-line-horizontal--regular'
127
+ : 'ph--arrows-out-line-horizontal--regular'
128
+ }
123
129
  onClick={() => onClick?.(layoutMode === 'solo--fullscreen' ? 'solo--fullscreen' : 'solo')}
124
130
  />
125
131
  </>
@@ -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>
@@ -17,6 +17,8 @@ import { PLANK_COMPANION_TYPE, DeckAction, type ResolvedPart, type LayoutMode }
17
17
  import { useBreakpoints } from '../../util';
18
18
  import { soloInlinePadding } from '../fragments';
19
19
 
20
+ const MAX_COMPANIONS = 5;
21
+
20
22
  export type PlankHeadingProps = {
21
23
  id: string;
22
24
  part: ResolvedPart;
@@ -143,7 +145,10 @@ export const PlankHeading = memo(
143
145
  classNames={[
144
146
  'plb-1 border-be border-separator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 contain-layout',
145
147
  part === 'solo' ? soloInlinePadding : 'pli-1',
148
+ layoutMode === 'solo--fullscreen' &&
149
+ 'opacity-0 border-transparent hover:border-separator hover:opacity-100 transition-[border-color,opacity]',
146
150
  ]}
151
+ data-plank-heading
147
152
  >
148
153
  {companions && isCompanionNode ? (
149
154
  <div role='none' className='flex-1 min-is-0 overflow-x-auto scrollbar-thin flex gap-1'>
@@ -152,7 +157,7 @@ export const PlankHeading = memo(
152
157
  key={id}
153
158
  data-id={id}
154
159
  icon={icon}
155
- iconOnly={node?.id !== id}
160
+ iconOnly={companions.length > MAX_COMPANIONS && node?.id !== id}
156
161
  label={toLocalizedString(label, t)}
157
162
  size={5}
158
163
  variant={node?.id === id ? 'primary' : 'default'}