@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
@@ -2,16 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Capabilities, contributes, createIntent, LayoutAction, type PluginsContext } from '@dxos/app-framework';
5
+ import { Capabilities, contributes, createIntent, LayoutAction, type PluginContext } from '@dxos/app-framework';
6
6
  import { scheduledEffect } from '@dxos/echo-signals/core';
7
7
 
8
8
  import { DeckCapabilities } from './capabilities';
9
9
  import { defaultDeck } from '../types';
10
10
 
11
11
  // TODO(wittjosiah): Cleanup the url handling. May justify introducing routing capabilities.
12
- export default async (context: PluginsContext) => {
13
- const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher) ?? {};
14
- const state = context.requestCapability(DeckCapabilities.MutableDeckState);
12
+ export default async (context: PluginContext) => {
13
+ const { dispatchPromise: dispatch } = context.getCapability(Capabilities.IntentDispatcher);
14
+ const state = context.getCapability(DeckCapabilities.MutableDeckState);
15
15
 
16
16
  const handleNavigation = async () => {
17
17
  const pathname = window.location.pathname;
@@ -4,19 +4,24 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { Surface } from '@dxos/app-framework';
7
+ import { Surface, useCapability } from '@dxos/app-framework';
8
8
 
9
- import { layoutAppliesTopbar, useBreakpoints } from '../../util';
9
+ import { DeckCapabilities } from '../../capabilities';
10
+ import { useBreakpoints } from '../../hooks';
11
+ import { getMode } from '../../types';
12
+ import { layoutAppliesTopbar } from '../../util';
10
13
  import { ToggleSidebarButton } from '../Sidebar';
11
14
  import { fixedSidebarToggleStyles } from '../fragments';
12
15
 
13
16
  export const ContentEmpty = () => {
14
17
  const breakpoint = useBreakpoints();
15
- const topbar = layoutAppliesTopbar(breakpoint);
18
+ const { deck } = useCapability(DeckCapabilities.MutableDeckState);
19
+ const layoutMode = getMode(deck);
20
+ const topbar = layoutAppliesTopbar(breakpoint, layoutMode);
16
21
  return (
17
22
  <div
18
23
  role='none'
19
- className='grid place-items-center p-8 relative bg-deck'
24
+ className='grid place-items-center p-8 relative bg-deckSurface'
20
25
  data-testid='layoutPlugin.firstRunMessage'
21
26
  >
22
27
  <Surface role='keyshortcuts' />
@@ -3,41 +3,34 @@
3
3
  //
4
4
 
5
5
  import { untracked } from '@preact/signals-core';
6
- import React, { useCallback, useEffect, useMemo, useRef, type UIEvent, Fragment, useState } from 'react';
6
+ import React, { useCallback, useEffect, useMemo, useRef, type UIEvent, Fragment } from 'react';
7
7
 
8
8
  import {
9
9
  Capabilities,
10
10
  LayoutAction,
11
- Surface,
12
11
  createIntent,
13
12
  useCapability,
14
13
  useIntentDispatcher,
15
14
  usePluginManager,
16
15
  } from '@dxos/app-framework';
17
16
  import { AttentionCapabilities } from '@dxos/plugin-attention';
18
- import {
19
- AlertDialog,
20
- Dialog as NaturalDialog,
21
- Main,
22
- Popover,
23
- type MainProps,
24
- useMediaQuery,
25
- useOnTransition,
26
- } from '@dxos/react-ui';
17
+ import { Main, type MainProps, useMediaQuery, useOnTransition } from '@dxos/react-ui';
27
18
  import { Stack, StackContext, DEFAULT_HORIZONTAL_SIZE } from '@dxos/react-ui-stack';
28
19
  import { mainPaddingTransitions } from '@dxos/react-ui-theme';
29
20
 
30
21
  import { ActiveNode } from './ActiveNode';
31
22
  import { ContentEmpty } from './ContentEmpty';
32
- import { Fullscreen } from './Fullscreen';
23
+ import { Dialog } from './Dialog';
24
+ import { PopoverContent, PopoverRoot } from './Popover';
33
25
  import { StatusBar } from './StatusBar';
34
26
  import { Toast } from './Toast';
35
27
  import { Topbar } from './Topbar';
36
28
  import { DeckCapabilities } from '../../capabilities';
29
+ import { useBreakpoints, useHoistStatusbar } from '../../hooks';
37
30
  import { DECK_PLUGIN } from '../../meta';
38
31
  import { type DeckSettingsProps, getMode } from '../../types';
39
- import { calculateOverscroll, layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
40
- import { Plank, PlankContentError } from '../Plank';
32
+ import { calculateOverscroll, layoutAppliesTopbar } from '../../util';
33
+ import { Plank } from '../Plank';
41
34
  import { ComplementarySidebar, Sidebar, ToggleComplementarySidebarButton, ToggleSidebarButton } from '../Sidebar';
42
35
  import { fixedComplementarySidebarToggleStyles, fixedSidebarToggleStyles } from '../fragments';
43
36
 
@@ -46,47 +39,28 @@ export type DeckLayoutProps = {
46
39
  };
47
40
 
48
41
  const PlankSeparator = ({ order }: { order: number }) =>
49
- order > 0 ? <span role='separator' className='row-span-2 bg-deck is-4' style={{ gridColumn: order }} /> : null;
42
+ order > 0 ? <span role='separator' className='row-span-2 bg-deckSurface is-4' style={{ gridColumn: order }} /> : null;
50
43
 
51
44
  export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
52
45
  const { dispatchPromise: dispatch } = useIntentDispatcher();
53
46
  const settings = useCapability(Capabilities.SettingsStore).getStore<DeckSettingsProps>(DECK_PLUGIN)!.value;
54
47
  const context = useCapability(DeckCapabilities.MutableDeckState);
55
- const {
56
- sidebarState,
57
- complementarySidebarState,
58
- complementarySidebarPanel,
59
- dialogOpen,
60
- dialogContent,
61
- dialogBlockAlign,
62
- dialogType,
63
- popoverOpen,
64
- popoverContent,
65
- popoverAnchorId,
66
- deck,
67
- toasts,
68
- } = context;
48
+ const { sidebarState, complementarySidebarState, complementarySidebarPanel, deck, toasts } = context;
69
49
  const { active, activeCompanions, fullscreen, solo, plankSizing } = deck;
70
50
  const breakpoint = useBreakpoints();
71
- const topbar = layoutAppliesTopbar(breakpoint);
72
- const hoistStatusbar = useHoistStatusbar(breakpoint);
51
+ const layoutMode = getMode(deck);
52
+ const topbar = layoutAppliesTopbar(breakpoint, layoutMode);
53
+ const hoistStatusbar = useHoistStatusbar(breakpoint, layoutMode);
73
54
  const pluginManager = usePluginManager();
74
55
 
75
56
  const scrollLeftRef = useRef<number | null>();
76
57
  const deckRef = useRef<HTMLDivElement>(null);
77
58
 
78
- // TODO(thure): This is a workaround for the difference in `React`ion time between displaying a Popover and rendering
79
- // the anchor further down the tree. Refactor to use VirtualTrigger or some other approach which does not cause a lag.
80
- const [delayedPopoverVisibility, setDelayedPopoverVisibility] = useState(false);
81
- useEffect(() => {
82
- popoverOpen ? setTimeout(() => setDelayedPopoverVisibility(true), 40) : setDelayedPopoverVisibility(false);
83
- }, [popoverOpen]);
84
-
85
59
  // Ensure the first plank is attended when the deck is first rendered.
86
60
  useEffect(() => {
87
61
  // NOTE: Not `useAttended` so that the layout component is not re-rendered when the attended list changes.
88
62
  const attended = untracked(() => {
89
- const attention = pluginManager.context.requestCapability(AttentionCapabilities.Attention);
63
+ const attention = pluginManager.context.getCapability(AttentionCapabilities.Attention);
90
64
  return attention.current;
91
65
  });
92
66
  const firstId = solo ?? active[0];
@@ -105,7 +79,7 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
105
79
  if (!isNotMobile && getMode(deck) === 'deck') {
106
80
  // NOTE: Not `useAttended` so that the layout component is not re-rendered when the attended list changes.
107
81
  const attended = untracked(() => {
108
- const attention = pluginManager.context.requestCapability(AttentionCapabilities.Attention);
82
+ const attention = pluginManager.context.getCapability(AttentionCapabilities.Attention);
109
83
  return attention.current;
110
84
  });
111
85
 
@@ -118,14 +92,15 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
118
92
  }
119
93
  }, [isNotMobile, deck, dispatch]);
120
94
 
121
- // If deck is disabled in settings, ensure that the layout is in solo mode.
95
+ // When deck is disabled in settings, set to solo mode if the current layout mode is deck.
96
+ // TODO(thure): Applying this as an effect should be avoided over emitting the intent only when the setting changes.
122
97
  useEffect(() => {
123
- if (!settings.enableDeck) {
98
+ if (!settings.enableDeck && layoutMode === 'deck') {
124
99
  void dispatch(
125
100
  createIntent(LayoutAction.SetLayoutMode, { part: 'mode', subject: active[0], options: { mode: 'solo' } }),
126
101
  );
127
102
  }
128
- }, [settings.enableDeck, dispatch, active]);
103
+ }, [settings.enableDeck, dispatch, active, layoutMode]);
129
104
 
130
105
  /**
131
106
  * Clear scroll restoration state if the window is resized
@@ -144,8 +119,6 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
144
119
  deckRef.current.scrollLeft = scrollLeftRef.current;
145
120
  }
146
121
  }, []);
147
-
148
- const layoutMode = getMode(deck);
149
122
  useOnTransition(layoutMode, (mode) => mode !== 'deck', 'deck', restoreScroll);
150
123
 
151
124
  /**
@@ -178,22 +151,6 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
178
151
  [topbar, hoistStatusbar],
179
152
  );
180
153
 
181
- const Dialog = dialogType === 'alert' ? AlertDialog : NaturalDialog;
182
-
183
- const handlePopoverOpenChange = useCallback(
184
- (nextOpen: boolean) => {
185
- if (nextOpen && popoverAnchorId) {
186
- context.popoverOpen = true;
187
- } else {
188
- context.popoverOpen = false;
189
- context.popoverAnchorId = undefined;
190
- context.popoverSide = undefined;
191
- }
192
- },
193
- [context],
194
- );
195
- const handlePopoverClose = useCallback(() => handlePopoverOpenChange(false), [handlePopoverOpenChange]);
196
-
197
154
  const { order, itemsCount }: { order: Record<string, number>; itemsCount: number } = useMemo(() => {
198
155
  return active.reduce(
199
156
  (acc: { order: Record<string, number>; itemsCount: number }, entryId) => {
@@ -206,146 +163,124 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
206
163
  }, [active, activeCompanions]);
207
164
 
208
165
  return (
209
- <Popover.Root modal open={!!(popoverAnchorId && delayedPopoverVisibility)} onOpenChange={handlePopoverOpenChange}>
166
+ <PopoverRoot>
210
167
  <ActiveNode />
211
168
 
212
- {fullscreen && <Fullscreen id={solo} />}
213
-
214
- {!fullscreen && (
215
- <Main.Root
216
- navigationSidebarState={context.sidebarState}
217
- onNavigationSidebarStateChange={(next) => (context.sidebarState = next)}
218
- complementarySidebarState={context.complementarySidebarState}
219
- onComplementarySidebarStateChange={(next) => (context.complementarySidebarState = next)}
220
- >
221
- {/* Left sidebar. */}
222
- <Sidebar />
169
+ <Main.Root
170
+ navigationSidebarState={fullscreen ? 'closed' : context.sidebarState}
171
+ onNavigationSidebarStateChange={(next) => (context.sidebarState = next)}
172
+ complementarySidebarState={fullscreen ? 'closed' : context.complementarySidebarState}
173
+ onComplementarySidebarStateChange={(next) => (context.complementarySidebarState = next)}
174
+ >
175
+ {/* Left sidebar. */}
176
+ <Sidebar />
223
177
 
224
- {/* Right sidebar. */}
225
- <ComplementarySidebar current={complementarySidebarPanel} />
178
+ {/* Right sidebar. */}
179
+ <ComplementarySidebar current={complementarySidebarPanel} />
226
180
 
227
- {/* Dialog overlay to dismiss dialogs. */}
228
- <Main.Overlay />
181
+ {/* Dialog overlay to dismiss dialogs. */}
182
+ <Main.Overlay />
229
183
 
230
- {/* No content. */}
231
- {isEmpty && (
232
- <Main.Content bounce handlesFocus classNames={mainPosition}>
233
- <ContentEmpty />
234
- </Main.Content>
235
- )}
184
+ {/* No content. */}
185
+ {isEmpty && (
186
+ <Main.Content bounce handlesFocus classNames={mainPosition}>
187
+ <ContentEmpty />
188
+ </Main.Content>
189
+ )}
236
190
 
237
- {/* Solo/deck mode. */}
238
- {!isEmpty && (
239
- <Main.Content
240
- bounce
241
- classNames={mainPosition}
242
- handlesFocus
243
- style={
244
- {
245
- '--dx-main-sidebarWidth':
246
- sidebarState === 'expanded'
247
- ? 'var(--nav-sidebar-size)'
248
- : sidebarState === 'collapsed'
249
- ? 'var(--l0-size)'
250
- : '0',
251
- '--dx-main-complementaryWidth':
252
- complementarySidebarState === 'expanded'
253
- ? 'var(--complementary-sidebar-size)'
254
- : complementarySidebarState === 'collapsed'
255
- ? 'var(--rail-size)'
256
- : '0',
257
- '--dx-main-contentFirstWidth': `${plankSizing[active[0] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
258
- '--dx-main-contentLastWidth': `${plankSizing[active[(active.length ?? 1) - 1] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
259
- } as MainProps['style']
260
- }
191
+ {/* Solo/deck mode. */}
192
+ {!isEmpty && (
193
+ <Main.Content
194
+ bounce
195
+ handlesFocus
196
+ classNames={mainPosition}
197
+ style={
198
+ {
199
+ '--dx-main-sidebarWidth':
200
+ sidebarState === 'expanded'
201
+ ? 'var(--nav-sidebar-size)'
202
+ : sidebarState === 'collapsed'
203
+ ? 'var(--l0-size)'
204
+ : '0',
205
+ '--dx-main-complementaryWidth':
206
+ complementarySidebarState === 'expanded'
207
+ ? 'var(--complementary-sidebar-size)'
208
+ : complementarySidebarState === 'collapsed'
209
+ ? 'var(--rail-size)'
210
+ : '0',
211
+ '--dx-main-contentFirstWidth': `${plankSizing[active[0] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
212
+ '--dx-main-contentLastWidth': `${plankSizing[active[(active.length ?? 1) - 1] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
213
+ } as MainProps['style']
214
+ }
215
+ >
216
+ <div
217
+ role='none'
218
+ className={!solo ? 'relative bg-deckSurface overflow-hidden' : 'sr-only'}
219
+ {...(solo && { inert: '' })}
261
220
  >
262
- <div
263
- role='none'
264
- className={!solo ? 'relative bg-deck overflow-hidden' : 'sr-only'}
265
- {...(solo && { inert: '' })}
221
+ {!topbar && !fullscreen && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
222
+ {!topbar && !fullscreen && (
223
+ <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />
224
+ )}
225
+ <Stack
226
+ ref={deckRef}
227
+ orientation='horizontal'
228
+ size='contain'
229
+ classNames={['absolute inset-block-0 -inset-inline-px', mainPaddingTransitions]}
230
+ itemsCount={itemsCount - 1}
231
+ style={padding}
232
+ onScroll={handleScroll}
266
233
  >
267
- {!topbar && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
268
- {!topbar && <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />}
269
- <Stack
270
- ref={deckRef}
271
- orientation='horizontal'
272
- size='contain'
273
- classNames={['absolute inset-block-0 -inset-inline-px', mainPaddingTransitions]}
274
- itemsCount={itemsCount - 1}
275
- style={padding}
276
- onScroll={handleScroll}
277
- >
278
- {active.map((entryId) => (
279
- <Fragment key={entryId}>
280
- <PlankSeparator order={order[entryId] - 1} />
281
- <Plank
282
- id={entryId}
283
- companionId={activeCompanions?.[entryId]}
284
- part='deck'
285
- order={order[entryId]}
286
- active={active}
287
- layoutMode={layoutMode}
288
- settings={settings}
289
- />
290
- </Fragment>
291
- ))}
292
- </Stack>
293
- </div>
294
- <div
295
- role='none'
296
- className={solo ? 'relative bg-deck overflow-hidden' : 'sr-only'}
297
- {...(!solo && { inert: '' })}
298
- >
299
- {!topbar && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
300
- {!topbar && <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />}
301
- <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
302
- <Plank
303
- id={solo}
304
- companionId={solo ? activeCompanions?.[solo] : undefined}
305
- part='solo'
306
- layoutMode={layoutMode}
307
- settings={settings}
308
- />
309
- </StackContext.Provider>
310
- </div>
311
- </Main.Content>
312
- )}
234
+ {active.map((entryId) => (
235
+ <Fragment key={entryId}>
236
+ <PlankSeparator order={order[entryId] - 1} />
237
+ <Plank
238
+ id={entryId}
239
+ companionId={activeCompanions?.[entryId]}
240
+ part='deck'
241
+ order={order[entryId]}
242
+ active={active}
243
+ layoutMode={layoutMode}
244
+ settings={settings}
245
+ />
246
+ </Fragment>
247
+ ))}
248
+ </Stack>
249
+ </div>
250
+ <div
251
+ role='none'
252
+ className={solo ? 'relative bg-deckSurface overflow-hidden' : 'sr-only'}
253
+ {...(!solo && { inert: '' })}
254
+ >
255
+ {!topbar && !fullscreen && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
256
+ {!topbar && !fullscreen && (
257
+ <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />
258
+ )}
259
+ <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
260
+ <Plank
261
+ id={solo}
262
+ companionId={solo ? activeCompanions?.[solo] : undefined}
263
+ part='solo'
264
+ layoutMode={layoutMode}
265
+ settings={settings}
266
+ />
267
+ </StackContext.Provider>
268
+ </div>
269
+ </Main.Content>
270
+ )}
313
271
 
314
- {/* Topbar. */}
315
- {topbar && <Topbar />}
272
+ {/* Topbar. */}
273
+ {topbar && <Topbar />}
316
274
 
317
- {/* Status bar. */}
318
- {hoistStatusbar && <StatusBar showHints={settings.showHints} />}
319
- </Main.Root>
320
- )}
275
+ {/* Status bar. */}
276
+ {hoistStatusbar && <StatusBar showHints={settings.showHints} />}
277
+ </Main.Root>
321
278
 
322
279
  {/* Global popovers. */}
323
- <Popover.Portal>
324
- <Popover.Content side={context.popoverSide} onEscapeKeyDown={handlePopoverClose}>
325
- <Popover.Viewport>
326
- <Surface role='popover' data={popoverContent} limit={1} />
327
- </Popover.Viewport>
328
- <Popover.Arrow />
329
- </Popover.Content>
330
- </Popover.Portal>
280
+ <PopoverContent />
331
281
 
332
282
  {/* Global dialog. */}
333
- {/* TODO(thure): End block alignment affecting `modal` and whether the surface renders in an overlay is tailored
334
- to the needs of the ambient chat dialog. As the feature matures, consider separating concerns. */}
335
- <Dialog.Root
336
- modal={dialogBlockAlign !== 'end'}
337
- open={dialogOpen}
338
- onOpenChange={(nextOpen) => (context.dialogOpen = nextOpen)}
339
- >
340
- {dialogBlockAlign === 'end' ? (
341
- // TODO(burdon): Placeholder creates a suspense boundary; replace with defaults.
342
- <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} placeholder={<div />} />
343
- ) : (
344
- <Dialog.Overlay blockAlign={dialogBlockAlign}>
345
- <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} />
346
- </Dialog.Overlay>
347
- )}
348
- </Dialog.Root>
283
+ <Dialog />
349
284
 
350
285
  {/* Global toasts. */}
351
286
  {toasts?.map((toast) => (
@@ -361,6 +296,6 @@ export const DeckLayout = ({ onDismissToast }: DeckLayoutProps) => {
361
296
  }}
362
297
  />
363
298
  ))}
364
- </Popover.Root>
299
+ </PopoverRoot>
365
300
  );
366
301
  };
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Surface, useCapability } from '@dxos/app-framework';
8
+ import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
9
+
10
+ import { DeckCapabilities } from '../../capabilities';
11
+ import { PlankContentError } from '../Plank';
12
+
13
+ export const Dialog = () => {
14
+ const context = useCapability(DeckCapabilities.MutableDeckState);
15
+ const { dialogOpen, dialogType, dialogBlockAlign, dialogOverlayClasses, dialogOverlayStyle, dialogContent } = context;
16
+ const Root = dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
17
+ const Overlay = dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
18
+
19
+ // TODO(thure): End block alignment affecting `modal` and whether the surface renders in an overlay is tailored to the needs of the ambient chat dialog. As the feature matures, consider separating concerns.
20
+ return (
21
+ <Root
22
+ modal={dialogBlockAlign !== 'end'}
23
+ open={dialogOpen}
24
+ onOpenChange={(nextOpen) => (context.dialogOpen = nextOpen)}
25
+ >
26
+ {dialogBlockAlign === 'end' ? (
27
+ // TODO(burdon): Placeholder creates a suspense boundary; replace with defaults.
28
+ <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} placeholder={<div />} />
29
+ ) : (
30
+ <Overlay blockAlign={dialogBlockAlign} classNames={dialogOverlayClasses} style={dialogOverlayStyle}>
31
+ <Surface role='dialog' data={dialogContent} limit={1} fallback={PlankContentError} />
32
+ </Overlay>
33
+ )}
34
+ </Root>
35
+ );
36
+ };
@@ -0,0 +1,104 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { Surface, useCapability } from '@dxos/app-framework';
9
+ import { Popover, type PopoverContentInteractOutsideEvent } from '@dxos/react-ui';
10
+
11
+ import { DeckCapabilities } from '../../capabilities';
12
+
13
+ export type DeckPopoverRootProps = PropsWithChildren<{}>;
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
+
23
+ export const PopoverRoot = ({ children }: DeckPopoverRootProps) => {
24
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
25
+ const virtualRef = useRef<HTMLButtonElement | null>(null);
26
+ const [virtualIter, setVirtualIter] = useState(0);
27
+ const [open, setOpen] = useState(false);
28
+ const debounceRef = useRef<NodeJS.Timeout | null>(null);
29
+
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.
32
+ useEffect(() => {
33
+ setOpen(false);
34
+ if (layout.popoverOpen) {
35
+ if (debounceRef.current) {
36
+ clearTimeout(debounceRef.current);
37
+ }
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]);
45
+
46
+ return (
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>
53
+ );
54
+ };
55
+
56
+ export const PopoverContent = () => {
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]);
86
+
87
+ return (
88
+ <Popover.Portal>
89
+ <Popover.Content
90
+ side={layout.popoverSide}
91
+ onInteractOutside={handleClose}
92
+ onEscapeKeyDown={handleClose}
93
+ collisionBoundary={collisionBoundaries}
94
+ sticky='always'
95
+ hideWhenDetached
96
+ >
97
+ <Popover.Viewport>
98
+ <Surface role='popover' data={layout.popoverContent} limit={1} />
99
+ </Popover.Viewport>
100
+ <Popover.Arrow />
101
+ </Popover.Content>
102
+ </Popover.Portal>
103
+ );
104
+ };