@dxos/plugin-deck 0.8.2-main.12df754 → 0.8.2-main.30e4dbb

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 (74) 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-E7TOEOGO.mjs +157 -0
  4. package/dist/lib/browser/chunk-E7TOEOGO.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-HWEH5OJ7.mjs +24 -0
  6. package/dist/lib/browser/{chunk-6HJZL3WT.mjs → chunk-PGSJT5PG.mjs} +2 -2
  7. package/dist/lib/browser/{chunk-VP6FCWFV.mjs → chunk-ZAL26IIZ.mjs} +104 -80
  8. package/dist/lib/browser/chunk-ZAL26IIZ.mjs.map +7 -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-SK3KDPHZ.mjs} +5 -5
  14. package/dist/lib/browser/{react-surface-LIPGYEYN.mjs → react-surface-LSSPY2BU.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/capabilities/url-handler.d.ts.map +1 -1
  27. package/dist/types/src/components/DeckLayout/Banner.d.ts.map +1 -1
  28. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  29. package/dist/types/src/components/DeckLayout/Popover.d.ts.map +1 -1
  30. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  31. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  32. package/dist/types/src/components/DeckSettings/DeckSettings.d.ts.map +1 -1
  33. package/dist/types/src/components/Plank/Plank.d.ts.map +1 -1
  34. package/dist/types/src/components/Plank/PlankControls.d.ts.map +1 -1
  35. package/dist/types/src/components/Plank/PlankError.d.ts.map +1 -1
  36. package/dist/types/src/components/Plank/PlankHeading.d.ts.map +1 -1
  37. package/dist/types/src/components/Sidebar/ComplementarySidebar.d.ts.map +1 -1
  38. package/dist/types/src/components/Sidebar/SidebarButton.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useNodeActionExpander.d.ts.map +1 -1
  40. package/dist/types/src/layout.d.ts.map +1 -1
  41. package/dist/types/src/types.d.ts +104 -104
  42. package/dist/types/src/types.d.ts.map +1 -1
  43. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -1
  44. package/dist/types/src/util/overscroll.d.ts.map +1 -1
  45. package/dist/types/src/util/set-active.d.ts.map +1 -1
  46. package/dist/types/src/util/useCompanions.d.ts.map +1 -1
  47. package/dist/types/src/util/useHoistStatusbar.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +31 -29
  50. package/src/capabilities/check-app-scheme.ts +3 -5
  51. package/src/capabilities/intent-resolver.ts +85 -77
  52. package/src/capabilities/tools.ts +4 -3
  53. package/src/components/DeckLayout/Popover.tsx +71 -43
  54. package/src/components/Plank/Plank.stories.tsx +1 -1
  55. package/src/components/Plank/Plank.tsx +3 -1
  56. package/src/components/Plank/PlankControls.tsx +14 -28
  57. package/src/components/Plank/PlankError.tsx +2 -6
  58. package/src/components/Plank/PlankHeading.tsx +12 -4
  59. package/src/components/Sidebar/SidebarButton.tsx +3 -0
  60. package/src/types.ts +72 -71
  61. package/dist/lib/browser/check-app-scheme-SEYECDHI.mjs.map +0 -7
  62. package/dist/lib/browser/chunk-4QSEGMY3.mjs +0 -24
  63. package/dist/lib/browser/chunk-VP6FCWFV.mjs.map +0 -7
  64. package/dist/lib/browser/chunk-ZMJMCN7O.mjs +0 -157
  65. package/dist/lib/browser/chunk-ZMJMCN7O.mjs.map +0 -7
  66. package/dist/lib/browser/intent-resolver-6AK45PT5.mjs.map +0 -7
  67. package/dist/lib/browser/tools-VDVQTJMD.mjs.map +0 -7
  68. /package/dist/lib/browser/{chunk-4QSEGMY3.mjs.map → chunk-HWEH5OJ7.mjs.map} +0 -0
  69. /package/dist/lib/browser/{chunk-6HJZL3WT.mjs.map → chunk-PGSJT5PG.mjs.map} +0 -0
  70. /package/dist/lib/browser/{react-root-KA2IL5RA.mjs.map → react-root-SK3KDPHZ.mjs.map} +0 -0
  71. /package/dist/lib/browser/{react-surface-LIPGYEYN.mjs.map → react-surface-LSSPY2BU.mjs.map} +0 -0
  72. /package/dist/lib/browser/{settings-6NU7CF2B.mjs.map → settings-C7LX2GXF.mjs.map} +0 -0
  73. /package/dist/lib/browser/{state-Z6UY2Z3M.mjs.map → state-AX74YEJD.mjs.map} +0 -0
  74. /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>
@@ -23,7 +23,7 @@ const meta: Meta<typeof Plank> = {
23
23
  plugins: [IntentPlugin(), GraphPlugin()],
24
24
  }),
25
25
  withTheme,
26
- withLayout({ fullscreen: true, tooltips: true }),
26
+ withLayout({ fullscreen: true }),
27
27
  ],
28
28
  parameters: {
29
29
  layout: 'centered',
@@ -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 (
@@ -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';
@@ -39,22 +31,10 @@ export type PlankControlsProps = Omit<ButtonGroupProps, 'onClick'> & {
39
31
  };
40
32
 
41
33
  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
- );
34
+ return <IconButton iconOnly label={label} icon={icon} size={5} variant='ghost' tooltipSide='bottom' {...props} />;
55
35
  };
56
36
 
57
- const plankControlSpacing = 'pli-2 plb-3';
37
+ const plankControlSpacing = 'pli-2';
58
38
 
59
39
  type PlankComplimentControlsProps = {
60
40
  primary?: string;
@@ -73,7 +53,7 @@ export const PlankCompanionControls = forwardRef<HTMLDivElement, PlankCompliment
73
53
  <PlankControl
74
54
  label={t('close companion label')}
75
55
  variant='ghost'
76
- icon='ph--caret-left--regular'
56
+ icon='ph--x--regular'
77
57
  onClick={handleCloseCompanion}
78
58
  classNames={plankControlSpacing}
79
59
  />
@@ -114,12 +94,18 @@ export const PlankControls = forwardRef<HTMLDivElement, PlankControlsProps>(
114
94
  label={t(
115
95
  layoutMode === 'solo--fullscreen'
116
96
  ? 'exit fullscreen label'
117
- : !layoutIsAnySolo
118
- ? 'show solo plank label'
119
- : 'show deck plank label',
97
+ : layoutIsAnySolo
98
+ ? 'show deck plank label'
99
+ : 'show solo plank label',
120
100
  )}
121
101
  classNames={buttonClassNames}
122
- icon={layoutIsAnySolo ? 'ph--corners-in--regular' : 'ph--corners-out--regular'}
102
+ icon={
103
+ layoutMode === 'solo--fullscreen'
104
+ ? 'ph--corners-in--regular'
105
+ : layoutIsAnySolo
106
+ ? 'ph--arrows-in-line-horizontal--regular'
107
+ : 'ph--arrows-out-line-horizontal--regular'
108
+ }
123
109
  onClick={() => onClick?.(layoutMode === 'solo--fullscreen' ? 'solo--fullscreen' : 'solo')}
124
110
  />
125
111
  </>
@@ -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;
@@ -92,9 +94,12 @@ export const PlankHeading = memo(
92
94
  }
93
95
  }, [actions, node, variant, graph]);
94
96
 
95
- const handleAction = useCallback((action: StackItemSigilAction) => {
96
- typeof action.data === 'function' && action.data?.({ node: action as Node, caller: DECK_PLUGIN });
97
- }, []);
97
+ const handleAction = useCallback(
98
+ (action: StackItemSigilAction) => {
99
+ typeof action.data === 'function' && action.data?.({ parent: node, caller: DECK_PLUGIN });
100
+ },
101
+ [node],
102
+ );
98
103
 
99
104
  const handlePlankAction = useCallback(
100
105
  (eventType: DeckAction.PartAdjustment) => {
@@ -143,7 +148,10 @@ export const PlankHeading = memo(
143
148
  classNames={[
144
149
  'plb-1 border-be border-separator items-stretch gap-1 sticky inline-start-12 app-drag min-is-0 contain-layout',
145
150
  part === 'solo' ? soloInlinePadding : 'pli-1',
151
+ layoutMode === 'solo--fullscreen' &&
152
+ 'opacity-0 border-transparent hover:border-separator hover:opacity-100 transition-[border-color,opacity]',
146
153
  ]}
154
+ data-plank-heading
147
155
  >
148
156
  {companions && isCompanionNode ? (
149
157
  <div role='none' className='flex-1 min-is-0 overflow-x-auto scrollbar-thin flex gap-1'>
@@ -152,7 +160,7 @@ export const PlankHeading = memo(
152
160
  key={id}
153
161
  data-id={id}
154
162
  icon={icon}
155
- iconOnly={node?.id !== id}
163
+ iconOnly={companions.length > MAX_COMPANIONS && node?.id !== id}
156
164
  label={toLocalizedString(label, t)}
157
165
  size={5}
158
166
  variant={node?.id === id ? 'primary' : 'default'}
@@ -50,8 +50,11 @@ export const CloseSidebarButton = () => {
50
50
  export const ToggleComplementarySidebarButton = ({ inR0, classNames }: ThemedClassName<{ inR0?: boolean }>) => {
51
51
  const layoutContext = useCapability(DeckCapabilities.MutableDeckState);
52
52
  const { t } = useTranslation(DECK_PLUGIN);
53
+ // TODO(thure): This should have a tooltip but is suppressed because focus is getting set on this twice when the app
54
+ // first mounts, causing even `suppressNextTooltip` not to have the intended effect.
53
55
  return (
54
56
  <IconButton
57
+ noTooltip
55
58
  iconOnly
56
59
  onClick={() =>
57
60
  (layoutContext.complementarySidebarState =