@dxos/plugin-deck 0.7.5-main.9d26e3a → 0.7.5-main.b19bfc8

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 (188) hide show
  1. package/dist/lib/browser/app-graph-builder-IYHAGFA3.mjs +151 -0
  2. package/dist/lib/browser/app-graph-builder-IYHAGFA3.mjs.map +7 -0
  3. package/dist/lib/browser/check-app-scheme-S3EYUPMF.mjs +33 -0
  4. package/dist/lib/browser/check-app-scheme-S3EYUPMF.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-22AQ5IVX.mjs +17 -0
  6. package/dist/lib/browser/chunk-22AQ5IVX.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-CYE6QZBQ.mjs +128 -0
  8. package/dist/lib/browser/chunk-CYE6QZBQ.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-DIM5INBX.mjs +24 -0
  10. package/dist/lib/browser/chunk-DIM5INBX.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-DIXE74SK.mjs +1097 -0
  12. package/dist/lib/browser/chunk-DIXE74SK.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-KANJBSIX.mjs +97 -0
  14. package/dist/lib/browser/chunk-KANJBSIX.mjs.map +7 -0
  15. package/dist/lib/browser/{chunk-GVOGPULO.mjs → chunk-N7TEPFVR.mjs} +5 -4
  16. package/dist/lib/browser/chunk-N7TEPFVR.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +104 -1776
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/intent-resolver-MWUADUNI.mjs +488 -0
  20. package/dist/lib/browser/intent-resolver-MWUADUNI.mjs.map +7 -0
  21. package/dist/lib/browser/meta.json +1 -1
  22. package/dist/lib/browser/react-root-IELFERPV.mjs +45 -0
  23. package/dist/lib/browser/react-root-IELFERPV.mjs.map +7 -0
  24. package/dist/lib/browser/react-surface-WL45R43W.mjs +39 -0
  25. package/dist/lib/browser/react-surface-WL45R43W.mjs.map +7 -0
  26. package/dist/lib/browser/settings-YONG3QB7.mjs +28 -0
  27. package/dist/lib/browser/settings-YONG3QB7.mjs.map +7 -0
  28. package/dist/lib/browser/state-MZZL5S2D.mjs +124 -0
  29. package/dist/lib/browser/state-MZZL5S2D.mjs.map +7 -0
  30. package/dist/lib/browser/tools-5LDJNU56.mjs +51 -0
  31. package/dist/lib/browser/tools-5LDJNU56.mjs.map +7 -0
  32. package/dist/lib/browser/types.mjs +16 -4
  33. package/dist/lib/browser/url-handler-MVHTKUYA.mjs +72 -0
  34. package/dist/lib/browser/url-handler-MVHTKUYA.mjs.map +7 -0
  35. package/dist/types/src/DeckPlugin.d.ts +1 -5
  36. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  37. package/dist/types/src/capabilities/app-graph-builder.d.ts +181 -0
  38. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/capabilities.d.ts +142 -0
  40. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/check-app-scheme.d.ts +4 -0
  42. package/dist/types/src/capabilities/check-app-scheme.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/index.d.ts +190 -0
  44. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  46. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/react-root.d.ts +7 -0
  48. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  50. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/settings.d.ts +4 -0
  52. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/state.d.ts +79 -0
  54. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  55. package/dist/types/src/capabilities/tools.d.ts +10 -0
  56. package/dist/types/src/capabilities/tools.d.ts.map +1 -0
  57. package/dist/types/src/capabilities/url-handler.d.ts +4 -0
  58. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -0
  59. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts +1 -2
  60. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -1
  61. package/dist/types/src/components/DeckLayout/Banner.d.ts +5 -0
  62. package/dist/types/src/components/DeckLayout/Banner.d.ts.map +1 -0
  63. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts +1 -4
  64. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  65. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts +1 -2
  66. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts.map +1 -1
  67. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +2 -7
  68. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  69. package/dist/types/src/components/DeckLayout/Fallback.d.ts +1 -2
  70. package/dist/types/src/components/DeckLayout/Fallback.d.ts.map +1 -1
  71. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts +1 -2
  72. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +1 -1
  73. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +3 -3
  74. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  75. package/dist/types/src/components/DeckLayout/Plank.d.ts +8 -6
  76. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  77. package/dist/types/src/components/DeckLayout/PlankControls.d.ts +2 -2
  78. package/dist/types/src/components/DeckLayout/PlankControls.d.ts.map +1 -1
  79. package/dist/types/src/components/DeckLayout/PlankError.d.ts +6 -6
  80. package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -1
  81. package/dist/types/src/components/DeckLayout/PlankLoading.d.ts +1 -2
  82. package/dist/types/src/components/DeckLayout/PlankLoading.d.ts.map +1 -1
  83. package/dist/types/src/components/DeckLayout/Sidebar.d.ts +1 -6
  84. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  85. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts +7 -0
  86. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts.map +1 -0
  87. package/dist/types/src/components/DeckLayout/StatusBar.d.ts +1 -2
  88. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  89. package/dist/types/src/components/DeckLayout/Toast.d.ts +2 -3
  90. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  91. package/dist/types/src/components/DeckLayout/Topbar.d.ts +2 -0
  92. package/dist/types/src/components/DeckLayout/Topbar.d.ts.map +1 -0
  93. package/dist/types/src/components/LayoutSettings.d.ts +1 -2
  94. package/dist/types/src/components/LayoutSettings.d.ts.map +1 -1
  95. package/dist/types/src/components/fragments.d.ts +4 -0
  96. package/dist/types/src/components/fragments.d.ts.map +1 -0
  97. package/dist/types/src/components/index.d.ts +0 -2
  98. package/dist/types/src/components/index.d.ts.map +1 -1
  99. package/dist/types/src/events.d.ts +5 -0
  100. package/dist/types/src/events.d.ts.map +1 -0
  101. package/dist/types/src/hooks/useMainSize.d.ts +2 -2
  102. package/dist/types/src/hooks/useNode.d.ts.map +1 -1
  103. package/dist/types/src/index.d.ts +3 -2
  104. package/dist/types/src/index.d.ts.map +1 -1
  105. package/dist/types/src/layout.d.ts +5 -19
  106. package/dist/types/src/layout.d.ts.map +1 -1
  107. package/dist/types/src/meta.d.ts +4 -4
  108. package/dist/types/src/meta.d.ts.map +1 -1
  109. package/dist/types/src/translations.d.ts +4 -2
  110. package/dist/types/src/translations.d.ts.map +1 -1
  111. package/dist/types/src/types.d.ts +121 -20
  112. package/dist/types/src/types.d.ts.map +1 -1
  113. package/dist/types/src/util/index.d.ts +4 -2
  114. package/dist/types/src/util/index.d.ts.map +1 -1
  115. package/dist/types/src/util/layoutAppliesTopbar.d.ts +2 -0
  116. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -0
  117. package/dist/types/src/util/set-active.d.ts +9 -0
  118. package/dist/types/src/util/set-active.d.ts.map +1 -0
  119. package/dist/types/src/util/useBreakpoints.d.ts +2 -0
  120. package/dist/types/src/util/useBreakpoints.d.ts.map +1 -0
  121. package/dist/types/src/util/useHoistStatusbar.d.ts +2 -0
  122. package/dist/types/src/util/useHoistStatusbar.d.ts.map +1 -0
  123. package/dist/types/tsconfig.tsbuildinfo +1 -1
  124. package/package.json +32 -37
  125. package/src/DeckPlugin.ts +87 -0
  126. package/src/capabilities/app-graph-builder.ts +113 -0
  127. package/src/capabilities/capabilities.ts +15 -0
  128. package/src/capabilities/check-app-scheme.ts +44 -0
  129. package/src/capabilities/index.ts +17 -0
  130. package/src/capabilities/intent-resolver.ts +368 -0
  131. package/src/capabilities/react-root.tsx +46 -0
  132. package/src/capabilities/react-surface.tsx +31 -0
  133. package/src/capabilities/settings.ts +21 -0
  134. package/src/capabilities/state.ts +107 -0
  135. package/src/capabilities/tools.ts +61 -0
  136. package/src/capabilities/url-handler.ts +65 -0
  137. package/src/components/DeckLayout/ActiveNode.tsx +2 -3
  138. package/src/components/DeckLayout/Banner.tsx +37 -0
  139. package/src/components/DeckLayout/ComplementarySidebar.tsx +142 -59
  140. package/src/components/DeckLayout/ContentEmpty.tsx +9 -4
  141. package/src/components/DeckLayout/DeckLayout.tsx +151 -99
  142. package/src/components/DeckLayout/Fullscreen.tsx +2 -3
  143. package/src/components/DeckLayout/NodePlankHeading.tsx +64 -77
  144. package/src/components/DeckLayout/Plank.tsx +35 -43
  145. package/src/components/DeckLayout/PlankControls.tsx +12 -11
  146. package/src/components/DeckLayout/PlankError.tsx +6 -5
  147. package/src/components/DeckLayout/Sidebar.tsx +19 -9
  148. package/src/components/DeckLayout/SidebarButton.tsx +68 -0
  149. package/src/components/DeckLayout/StatusBar.tsx +6 -12
  150. package/src/components/DeckLayout/Toast.tsx +2 -2
  151. package/src/components/DeckLayout/Topbar.tsx +11 -0
  152. package/src/components/LayoutSettings.tsx +8 -8
  153. package/src/components/fragments.ts +14 -0
  154. package/src/components/index.ts +0 -2
  155. package/src/events.ts +12 -0
  156. package/src/hooks/useMainSize.ts +3 -3
  157. package/src/hooks/useNode.ts +3 -1
  158. package/src/index.ts +3 -4
  159. package/src/layout.ts +43 -212
  160. package/src/meta.ts +3 -2
  161. package/src/translations.ts +8 -6
  162. package/src/types.ts +104 -36
  163. package/src/util/index.ts +4 -2
  164. package/src/util/layoutAppliesTopbar.ts +7 -0
  165. package/src/util/set-active.ts +47 -0
  166. package/src/util/useBreakpoints.ts +11 -0
  167. package/src/util/useHoistStatusbar.ts +20 -0
  168. package/dist/lib/browser/chunk-GVOGPULO.mjs.map +0 -7
  169. package/dist/lib/browser/chunk-ZC3K6C2W.mjs +0 -37
  170. package/dist/lib/browser/chunk-ZC3K6C2W.mjs.map +0 -7
  171. package/dist/lib/browser/meta.mjs +0 -9
  172. package/dist/lib/browser/meta.mjs.map +0 -7
  173. package/dist/types/src/components/DeckContext.d.ts +0 -8
  174. package/dist/types/src/components/DeckContext.d.ts.map +0 -1
  175. package/dist/types/src/components/LayoutContext.d.ts +0 -5
  176. package/dist/types/src/components/LayoutContext.d.ts.map +0 -1
  177. package/dist/types/src/layout.test.d.ts +0 -2
  178. package/dist/types/src/layout.test.d.ts.map +0 -1
  179. package/dist/types/src/util/check-app-scheme.d.ts +0 -2
  180. package/dist/types/src/util/check-app-scheme.d.ts.map +0 -1
  181. package/dist/types/src/util/layout-parts.d.ts +0 -7
  182. package/dist/types/src/util/layout-parts.d.ts.map +0 -1
  183. package/src/DeckPlugin.tsx +0 -623
  184. package/src/components/DeckContext.ts +0 -14
  185. package/src/components/LayoutContext.ts +0 -12
  186. package/src/layout.test.ts +0 -380
  187. package/src/util/check-app-scheme.ts +0 -21
  188. package/src/util/layout-parts.ts +0 -12
@@ -0,0 +1,113 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes, createIntent, LayoutAction, type PluginsContext } from '@dxos/app-framework';
6
+ import { AttentionCapabilities } from '@dxos/plugin-attention';
7
+ import { createExtension, type Node, ROOT_ID } from '@dxos/plugin-graph';
8
+
9
+ import { DeckCapabilities } from './capabilities';
10
+ import { DECK_PLUGIN } from '../meta';
11
+
12
+ export default (context: PluginsContext) =>
13
+ contributes(
14
+ Capabilities.AppGraphBuilder,
15
+ createExtension({
16
+ id: DECK_PLUGIN,
17
+ filter: (node): node is Node<null> => node.id === ROOT_ID,
18
+ actions: () => {
19
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
20
+
21
+ // NOTE(Zan): This is currently disabled.
22
+ // TODO(Zan): Fullscreen needs to know the active node and provide that to the layout part.
23
+ const _fullscreen = {
24
+ id: `${LayoutAction.UpdateLayout._tag}/fullscreen`,
25
+ data: async () => {
26
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
27
+ await dispatch(createIntent(LayoutAction.SetLayoutMode, { part: 'mode', options: { mode: 'fullscreen' } }));
28
+ },
29
+ properties: {
30
+ label: ['toggle fullscreen label', { ns: DECK_PLUGIN }],
31
+ icon: 'ph--arrows-out--regular',
32
+ keyBinding: {
33
+ macos: 'ctrl+meta+f',
34
+ windows: 'shift+ctrl+f',
35
+ },
36
+ },
37
+ };
38
+
39
+ const closeCurrent = {
40
+ id: `${LayoutAction.Close._tag}/current`,
41
+ data: async () => {
42
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
43
+ const attended = attention.current.at(-1);
44
+ if (attended) {
45
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
46
+ await dispatch(
47
+ createIntent(LayoutAction.Close, { part: 'main', subject: [attended], options: { state: false } }),
48
+ );
49
+ }
50
+ },
51
+ properties: {
52
+ label: ['close current label', { ns: DECK_PLUGIN }],
53
+ icon: 'ph--x--regular',
54
+ },
55
+ };
56
+
57
+ const closeOthers = {
58
+ id: `${LayoutAction.Close._tag}/others`,
59
+ data: async () => {
60
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
61
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
62
+ const attended = attention.current.at(-1);
63
+ const ids = state.deck.active.filter((id) => id !== attended) ?? [];
64
+ await dispatch(createIntent(LayoutAction.Close, { part: 'main', subject: ids, options: { state: false } }));
65
+ },
66
+ properties: {
67
+ label: ['close others label', { ns: DECK_PLUGIN }],
68
+ icon: 'ph--x-square--regular',
69
+ },
70
+ };
71
+
72
+ const closeAll = {
73
+ id: `${LayoutAction.Close._tag}/all`,
74
+ data: async () => {
75
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
76
+ await dispatch(
77
+ createIntent(LayoutAction.Close, {
78
+ part: 'main',
79
+ subject: state.deck.active,
80
+ options: { state: false },
81
+ }),
82
+ );
83
+ },
84
+ properties: {
85
+ label: ['close all label', { ns: DECK_PLUGIN }],
86
+ icon: 'ph--x-circle--regular',
87
+ },
88
+ };
89
+
90
+ const toggleSidebar = {
91
+ id: `${LayoutAction.UpdateSidebar._tag}/nav`,
92
+ data: async () => {
93
+ state.sidebarState = state.sidebarState === 'expanded' ? 'collapsed' : 'expanded';
94
+ },
95
+ properties: {
96
+ label: [
97
+ state.sidebarState === 'expanded' ? 'collapse navigation sidebar label' : 'open navigation sidebar label',
98
+ { ns: DECK_PLUGIN },
99
+ ],
100
+ icon: 'ph--sidebar--regular',
101
+ keyBinding: {
102
+ macos: 'meta+b',
103
+ },
104
+ disposition: 'pin-end',
105
+ position: 'hoist',
106
+ l0Breakpoint: 'lg',
107
+ },
108
+ };
109
+
110
+ return !state.deck.solo ? [closeCurrent, closeOthers, closeAll, toggleSidebar] : [toggleSidebar];
111
+ },
112
+ }),
113
+ );
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { defineCapability } from '@dxos/app-framework';
6
+ import { type DeepReadonly } from '@dxos/util';
7
+
8
+ import { DECK_PLUGIN } from '../meta';
9
+ import { type DeckState, type Panel } from '../types';
10
+
11
+ export namespace DeckCapabilities {
12
+ export const DeckState = defineCapability<DeepReadonly<DeckState>>(`${DECK_PLUGIN}/capability/state`);
13
+ export const MutableDeckState = defineCapability<DeckState>(`${DECK_PLUGIN}/capability/state`);
14
+ export const ComplementaryPanel = defineCapability<Panel>(`${DECK_PLUGIN}/capability/complementary-panel`);
15
+ }
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+ //
5
+ // Copyright 2025 DXOS.org
6
+ //
7
+
8
+ import { Capabilities, contributes, type PluginsContext } from '@dxos/app-framework';
9
+
10
+ import { DECK_PLUGIN } from '../meta';
11
+ import { type DeckSettingsProps } from '../types';
12
+
13
+ const isSocket = !!(globalThis as any).__args;
14
+
15
+ // TODO(mjamesderocher): Can we get this directly from Socket?
16
+ const appScheme = 'composer://';
17
+
18
+ // TODO(mjamesderocher): Factor out as part of NavigationPlugin.
19
+ const checkAppScheme = (url: string) => {
20
+ const iframe = document.createElement('iframe');
21
+ iframe.style.display = 'none';
22
+ document.body.appendChild(iframe);
23
+
24
+ iframe.src = url + window.location.pathname.replace(/^\/+/, '') + window.location.search;
25
+
26
+ const timer = setTimeout(() => {
27
+ document.body.removeChild(iframe);
28
+ }, 3000);
29
+
30
+ window.addEventListener('pagehide', (event) => {
31
+ clearTimeout(timer);
32
+ document.body.removeChild(iframe);
33
+ });
34
+ };
35
+
36
+ export default async (context: PluginsContext) => {
37
+ const settingsStore = context.requestCapability(Capabilities.SettingsStore);
38
+ const settings = settingsStore.getStore<DeckSettingsProps>(DECK_PLUGIN)?.value;
39
+ if (!isSocket && settings?.enableNativeRedirect) {
40
+ checkAppScheme(appScheme);
41
+ }
42
+
43
+ return contributes(Capabilities.Null, null);
44
+ };
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { lazy } from '@dxos/app-framework';
6
+
7
+ export const AppGraphBuilder = lazy(() => import('./app-graph-builder'));
8
+ export const CheckAppScheme = lazy(() => import('./check-app-scheme'));
9
+ export const LayoutIntentResolver = lazy(() => import('./intent-resolver'));
10
+ export const ReactRoot = lazy(() => import('./react-root'));
11
+ export const ReactSurface = lazy(() => import('./react-surface'));
12
+ export const DeckSettings = lazy(() => import('./settings'));
13
+ export const DeckState = lazy(() => import('./state'));
14
+ export const Tools = lazy(() => import('./tools'));
15
+ export const UrlHandler = lazy(() => import('./url-handler'));
16
+
17
+ export * from './capabilities';
@@ -0,0 +1,368 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { batch } from '@preact/signals-core';
6
+ import { pipe } from 'effect';
7
+
8
+ import {
9
+ Capabilities,
10
+ createResolver,
11
+ contributes,
12
+ IntentAction,
13
+ LayoutAction,
14
+ type PluginsContext,
15
+ createIntent,
16
+ chain,
17
+ } from '@dxos/app-framework';
18
+ import { getTypename, S } from '@dxos/echo-schema';
19
+ import { isReactiveObject } from '@dxos/live-object';
20
+ import { log } from '@dxos/log';
21
+ import { AttentionCapabilities } from '@dxos/plugin-attention';
22
+ import { ObservabilityAction } from '@dxos/plugin-observability/types';
23
+ import { isNonNullable } from '@dxos/util';
24
+
25
+ import { DeckCapabilities } from './capabilities';
26
+ import { closeEntry, incrementPlank, openEntry } from '../layout';
27
+ import { DECK_PLUGIN } from '../meta';
28
+ import { DeckAction, type LayoutMode, type DeckSettingsProps, isLayoutMode, getMode } from '../types';
29
+ import { setActive } from '../util';
30
+
31
+ export default (context: PluginsContext) =>
32
+ contributes(Capabilities.IntentResolver, [
33
+ createResolver({
34
+ intent: IntentAction.ShowUndo,
35
+ resolve: (data) => {
36
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
37
+ const { undoPromise: undo } = context.requestCapability(Capabilities.IntentDispatcher);
38
+
39
+ // TODO(wittjosiah): Support undoing further back than the last action.
40
+ if (layout.currentUndoId) {
41
+ layout.toasts = layout.toasts.filter((toast) => toast.id !== layout.currentUndoId);
42
+ }
43
+ layout.currentUndoId = `${IntentAction.ShowUndo._tag}-${Date.now()}`;
44
+ layout.toasts = [
45
+ ...layout.toasts,
46
+ {
47
+ id: layout.currentUndoId,
48
+ title: data.message ?? ['undo available label', { ns: DECK_PLUGIN }],
49
+ duration: 10_000,
50
+ actionLabel: ['undo action label', { ns: DECK_PLUGIN }],
51
+ actionAlt: ['undo action alt', { ns: DECK_PLUGIN }],
52
+ closeLabel: ['undo close label', { ns: DECK_PLUGIN }],
53
+ onAction: () => undo(),
54
+ },
55
+ ];
56
+ },
57
+ }),
58
+ createResolver({
59
+ intent: LayoutAction.UpdateLayout,
60
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateSidebar.fields.input)`
61
+ // but the filter is not being applied correctly.
62
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateSidebar.fields.input> =>
63
+ S.is(LayoutAction.UpdateSidebar.fields.input)(data),
64
+ resolve: ({ options }) => {
65
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
66
+ const next = options?.state ?? layout.sidebarState;
67
+ if (next !== layout.sidebarState) {
68
+ layout.sidebarState = next;
69
+ }
70
+ },
71
+ }),
72
+ createResolver({
73
+ intent: LayoutAction.UpdateLayout,
74
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateComplementary.fields.input)`
75
+ // but the filter is not being applied correctly.
76
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateComplementary.fields.input> =>
77
+ S.is(LayoutAction.UpdateComplementary.fields.input)(data),
78
+ resolve: ({ subject, options }) => {
79
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
80
+
81
+ if (layout.complementarySidebarPanel !== subject) {
82
+ layout.complementarySidebarPanel = subject;
83
+ }
84
+
85
+ const next = subject ? 'expanded' : options?.state ?? layout.complementarySidebarState;
86
+ if (next !== layout.complementarySidebarState) {
87
+ layout.complementarySidebarState = next;
88
+ }
89
+ },
90
+ }),
91
+ createResolver({
92
+ intent: LayoutAction.UpdateLayout,
93
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdateDialog.fields.input)`
94
+ // but the filter is not being applied correctly.
95
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateDialog.fields.input> =>
96
+ S.is(LayoutAction.UpdateDialog.fields.input)(data),
97
+ resolve: ({ subject, options }) => {
98
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
99
+ layout.dialogOpen = options.state ?? Boolean(subject);
100
+ layout.dialogContent = subject ? { component: subject, props: options.props } : null;
101
+ layout.dialogBlockAlign = options.blockAlign ?? 'center';
102
+ layout.dialogType = options.type ?? 'default';
103
+ },
104
+ }),
105
+ createResolver({
106
+ intent: LayoutAction.UpdateLayout,
107
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.UpdatePopover.fields.input)`
108
+ // but the filter is not being applied correctly.
109
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdatePopover.fields.input> =>
110
+ S.is(LayoutAction.UpdatePopover.fields.input)(data),
111
+ resolve: ({ subject, options }) => {
112
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
113
+ layout.popoverOpen = options.state ?? Boolean(subject);
114
+ layout.popoverContent = subject ? { component: subject, props: options.props } : null;
115
+ layout.popoverAnchorId = options.anchorId;
116
+ layout.popoverSide = options.side;
117
+ },
118
+ }),
119
+ createResolver({
120
+ intent: LayoutAction.UpdateLayout,
121
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.AddToast.fields.input)`
122
+ // but the filter is not being applied correctly.
123
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.AddToast.fields.input> =>
124
+ S.is(LayoutAction.AddToast.fields.input)(data),
125
+ resolve: ({ subject }) => {
126
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
127
+ layout.toasts.push(subject);
128
+ },
129
+ }),
130
+ createResolver({
131
+ intent: LayoutAction.UpdateLayout,
132
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.SetLayoutMode.fields.input)`
133
+ // but the filter is not being applied correctly.
134
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.SetLayoutMode.fields.input> => {
135
+ if (!S.is(LayoutAction.SetLayoutMode.fields.input)(data)) {
136
+ return false;
137
+ }
138
+
139
+ if ('mode' in data.options) {
140
+ return isLayoutMode(data.options.mode);
141
+ }
142
+
143
+ return true;
144
+ },
145
+ resolve: ({ subject, options }) => {
146
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
147
+
148
+ const setMode = (mode: LayoutMode) => {
149
+ const deck = state.deck;
150
+ const current = deck.solo ? [deck.solo] : deck.active;
151
+ // When un-soloing, the solo entry is added to the deck.
152
+ const next = (
153
+ mode !== 'deck' ? [subject ?? deck.solo ?? deck.active[0]] : [...deck.active, deck.solo]
154
+ ).filter(isNonNullable);
155
+
156
+ const removed = current.filter((id) => !next.includes(id));
157
+ const closed = Array.from(new Set([...deck.inactive.filter((id) => !next.includes(id)), ...removed]));
158
+ deck.inactive = closed;
159
+
160
+ if (mode !== 'deck' && next[0]) {
161
+ deck.solo = next[0];
162
+ } else if (mode === 'deck' && deck.solo) {
163
+ deck.solo = undefined;
164
+ deck.initialized = true;
165
+ }
166
+
167
+ if (mode === 'fullscreen' && !deck.fullscreen) {
168
+ deck.fullscreen = true;
169
+ } else if (mode !== 'fullscreen' && deck.fullscreen) {
170
+ deck.fullscreen = false;
171
+ }
172
+ };
173
+
174
+ return batch(() => {
175
+ if ('mode' in options) {
176
+ const current = getMode(state.deck);
177
+ if (current !== options.mode) {
178
+ state.previousMode[state.activeDeck] = current;
179
+ }
180
+ setMode(options.mode as LayoutMode);
181
+ } else if ('revert' in options) {
182
+ const last = state.previousMode[state.activeDeck];
183
+ setMode(last ?? 'solo');
184
+ } else {
185
+ log.warn('Invalid layout mode', options);
186
+ }
187
+ });
188
+ },
189
+ }),
190
+ createResolver({
191
+ intent: LayoutAction.UpdateLayout,
192
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
193
+ S.is(LayoutAction.SwitchWorkspace.fields.input)(data),
194
+ resolve: ({ subject }) => {
195
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
196
+ batch(() => {
197
+ // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
198
+ // Ideally this should be worked into the data model in a generic way.
199
+ if (!state.activeDeck.startsWith('!')) {
200
+ state.previousDeck = state.activeDeck;
201
+ }
202
+ state.activeDeck = subject;
203
+ if (!state.decks[subject]) {
204
+ state.decks[subject] = { initialized: false, active: [], inactive: [], fullscreen: false, plankSizing: {} };
205
+ }
206
+ });
207
+
208
+ const first = state.deck.solo ? state.deck.solo : state.deck.active[0];
209
+ if (first) {
210
+ return {
211
+ intents: [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: first })],
212
+ };
213
+ }
214
+ },
215
+ }),
216
+ createResolver({
217
+ intent: LayoutAction.UpdateLayout,
218
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.RevertWorkspace.fields.input> =>
219
+ S.is(LayoutAction.RevertWorkspace.fields.input)(data),
220
+ resolve: () => {
221
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
222
+ return {
223
+ intents: [createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: state.previousDeck })],
224
+ };
225
+ },
226
+ }),
227
+ createResolver({
228
+ intent: LayoutAction.UpdateLayout,
229
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Open.fields.input> =>
230
+ S.is(LayoutAction.Open.fields.input)(data),
231
+ resolve: ({ subject, options }) => {
232
+ const { graph } = context.requestCapability(Capabilities.AppGraph);
233
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
234
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
235
+ const settings = context
236
+ .requestCapabilities(Capabilities.SettingsStore)[0]
237
+ ?.getStore<DeckSettingsProps>(DECK_PLUGIN)?.value;
238
+
239
+ const previouslyOpenIds = new Set<string>(state.deck.solo ? [state.deck.solo] : state.deck.active);
240
+ batch(() => {
241
+ const next = state.deck.solo
242
+ ? (subject as string[])
243
+ : subject.reduce(
244
+ (acc, entryId) =>
245
+ openEntry(acc, entryId, {
246
+ key: options?.key,
247
+ positioning: options?.positioning ?? settings?.newPlankPositioning,
248
+ pivotId: options?.pivotId,
249
+ }),
250
+ state.deck.active,
251
+ );
252
+
253
+ return setActive({ next, state, attention });
254
+ });
255
+
256
+ const ids = state.deck.solo ? [state.deck.solo] : state.deck.active;
257
+ const newlyOpen = ids.filter((i) => !previouslyOpenIds.has(i));
258
+
259
+ return {
260
+ intents: [
261
+ ...(options?.scrollIntoView !== false
262
+ ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: newlyOpen[0] ?? subject[0] })]
263
+ : []),
264
+ createIntent(LayoutAction.Expose, { part: 'navigation', subject: newlyOpen[0] ?? subject[0] }),
265
+ ...newlyOpen.map((id) => {
266
+ const active = graph?.findNode(id)?.data;
267
+ const typename = isReactiveObject(active) ? getTypename(active) : undefined;
268
+ return createIntent(ObservabilityAction.SendEvent, {
269
+ name: 'navigation.activate',
270
+ properties: {
271
+ id,
272
+ typename,
273
+ },
274
+ });
275
+ }),
276
+ ],
277
+ };
278
+ },
279
+ }),
280
+ createResolver({
281
+ intent: LayoutAction.UpdateLayout,
282
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Close.fields.input> =>
283
+ S.is(LayoutAction.Close.fields.input)(data),
284
+ resolve: ({ subject }) => {
285
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
286
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
287
+ const active = state.deck.solo ? [state.deck.solo] : state.deck.active;
288
+ const next = subject.reduce((acc, id) => closeEntry(acc, id), active);
289
+ const toAttend = setActive({ next, state, attention });
290
+ return {
291
+ intents: toAttend ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: toAttend })] : [],
292
+ };
293
+ },
294
+ }),
295
+ createResolver({
296
+ intent: LayoutAction.UpdateLayout,
297
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Set.fields.input> =>
298
+ S.is(LayoutAction.Set.fields.input)(data),
299
+ resolve: ({ subject }) => {
300
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
301
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
302
+ const toAttend = setActive({ next: subject as string[], state, attention });
303
+ return {
304
+ intents: toAttend ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: toAttend })] : [],
305
+ };
306
+ },
307
+ }),
308
+ createResolver({
309
+ intent: LayoutAction.UpdateLayout,
310
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.ScrollIntoView.fields.input> =>
311
+ S.is(LayoutAction.ScrollIntoView.fields.input)(data),
312
+ resolve: ({ subject }) => {
313
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
314
+ layout.scrollIntoView = subject;
315
+ },
316
+ }),
317
+ createResolver({
318
+ intent: DeckAction.UpdatePlankSize,
319
+ resolve: (data) => {
320
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
321
+ state.deck.plankSizing[data.id] = data.size;
322
+ },
323
+ }),
324
+ createResolver({
325
+ intent: DeckAction.Adjust,
326
+ resolve: (adjustment) => {
327
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
328
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
329
+
330
+ return batch(() => {
331
+ if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
332
+ setActive({
333
+ next: incrementPlank(state.deck.active, adjustment),
334
+ state,
335
+ attention,
336
+ });
337
+ }
338
+
339
+ if (adjustment.type === 'solo') {
340
+ const entryId = adjustment.id;
341
+ if (!state.deck.solo) {
342
+ // Solo the entry.
343
+ return {
344
+ intents: [
345
+ createIntent(LayoutAction.SetLayoutMode, {
346
+ part: 'mode',
347
+ subject: entryId,
348
+ options: { mode: 'solo' },
349
+ }),
350
+ ],
351
+ };
352
+ } else {
353
+ // Un-solo the current entry.
354
+ return {
355
+ intents: [
356
+ // NOTE: The order of these is important.
357
+ pipe(
358
+ createIntent(LayoutAction.SetLayoutMode, { part: 'mode', options: { mode: 'deck' } }),
359
+ chain(LayoutAction.Open, { part: 'main', subject: [entryId] }),
360
+ ),
361
+ ],
362
+ };
363
+ }
364
+ }
365
+ });
366
+ },
367
+ }),
368
+ ]);
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Capabilities, contributes, useCapability } from '@dxos/app-framework';
8
+
9
+ import { DeckCapabilities } from './capabilities';
10
+ import { DeckLayout } from '../components';
11
+ import { DECK_PLUGIN } from '../meta';
12
+ import { type DeckSettingsProps } from '../types';
13
+
14
+ export default () =>
15
+ contributes(Capabilities.ReactRoot, {
16
+ id: DECK_PLUGIN,
17
+ root: () => {
18
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
19
+ const settings = useCapability(Capabilities.SettingsStore).getStore<DeckSettingsProps>(DECK_PLUGIN)!.value;
20
+
21
+ const handleDismissToast = useCallback(
22
+ (id: string) => {
23
+ const index = layout.toasts.findIndex((toast) => toast.id === id);
24
+ if (index !== -1) {
25
+ // Allow time for the toast to animate out.
26
+ // TODO(burdon): Factor out and unregister timeout.
27
+ setTimeout(() => {
28
+ if (layout.toasts[index].id === layout.currentUndoId) {
29
+ layout.currentUndoId = undefined;
30
+ }
31
+ layout.toasts.splice(index, 1);
32
+ }, 1_000);
33
+ }
34
+ },
35
+ [layout.toasts],
36
+ );
37
+
38
+ return (
39
+ <DeckLayout
40
+ showHints={settings.showHints}
41
+ overscroll={settings.overscroll}
42
+ onDismissToast={handleDismissToast}
43
+ />
44
+ );
45
+ },
46
+ });
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Capabilities, contributes, createSurface } from '@dxos/app-framework';
8
+ import { SettingsStore } from '@dxos/local-storage';
9
+
10
+ import { LayoutSettings } from '../components';
11
+ import { Banner } from '../components/DeckLayout/Banner';
12
+ import { DECK_PLUGIN } from '../meta';
13
+ import { type DeckSettingsProps } from '../types';
14
+
15
+ export default () =>
16
+ contributes(Capabilities.ReactSurface, [
17
+ createSurface({
18
+ id: `${DECK_PLUGIN}/settings`,
19
+ role: 'article',
20
+ filter: (data): data is { subject: SettingsStore<DeckSettingsProps> } =>
21
+ data.subject instanceof SettingsStore && data.subject.prefix === DECK_PLUGIN,
22
+ component: ({ data: { subject } }) => <LayoutSettings settings={subject.value} />,
23
+ }),
24
+ createSurface({
25
+ id: `${DECK_PLUGIN}/banner`,
26
+ role: 'banner',
27
+ component: ({ data }) => {
28
+ return <Banner variant={data.variant} />;
29
+ },
30
+ }),
31
+ ]);
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes } from '@dxos/app-framework';
6
+ import { create } from '@dxos/live-object';
7
+
8
+ import { DECK_PLUGIN } from '../meta';
9
+ import { DeckSettingsSchema, type DeckSettingsProps } from '../types';
10
+
11
+ export default () => {
12
+ const settings = create<DeckSettingsProps>({
13
+ showHints: false,
14
+ enableNativeRedirect: false,
15
+ enableIdeStyleStatusbar: true,
16
+ newPlankPositioning: 'start',
17
+ overscroll: 'none',
18
+ });
19
+
20
+ return contributes(Capabilities.Settings, { schema: DeckSettingsSchema, prefix: DECK_PLUGIN, value: settings });
21
+ };