@dxos/plugin-deck 0.7.5-main.9d2a38b → 0.7.5-main.e9bb01b

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 (204) hide show
  1. package/dist/lib/browser/{app-graph-builder-67VRUD5K.mjs → app-graph-builder-CI6ZFMNL.mjs} +57 -31
  2. package/dist/lib/browser/app-graph-builder-CI6ZFMNL.mjs.map +7 -0
  3. package/dist/lib/browser/{check-app-scheme-GEX6W2R5.mjs → check-app-scheme-S3EYUPMF.mjs} +3 -3
  4. package/dist/lib/browser/{check-app-scheme-GEX6W2R5.mjs.map → check-app-scheme-S3EYUPMF.mjs.map} +2 -2
  5. package/dist/lib/browser/chunk-M2L53AIH.mjs +126 -0
  6. package/dist/lib/browser/chunk-M2L53AIH.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-JQJ5UWVB.mjs → chunk-N7TEPFVR.mjs} +3 -2
  8. package/dist/lib/browser/{chunk-JQJ5UWVB.mjs.map → chunk-N7TEPFVR.mjs.map} +3 -3
  9. package/dist/lib/browser/chunk-NYZJCVAU.mjs +22 -0
  10. package/dist/lib/browser/chunk-NYZJCVAU.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-WXNLVMK2.mjs +1119 -0
  12. package/dist/lib/browser/chunk-WXNLVMK2.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-5VFDMW5M.mjs → chunk-YQ2GWTDU.mjs} +2 -2
  14. package/dist/lib/browser/chunk-YQ2GWTDU.mjs.map +7 -0
  15. package/dist/lib/browser/index.mjs +32 -78
  16. package/dist/lib/browser/index.mjs.map +4 -4
  17. package/dist/lib/browser/intent-resolver-CSXFDKTC.mjs +494 -0
  18. package/dist/lib/browser/intent-resolver-CSXFDKTC.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -1
  20. package/dist/lib/browser/{react-root-UL7ZDRVZ.mjs → react-root-ECDQZYQT.mjs} +10 -14
  21. package/dist/lib/browser/react-root-ECDQZYQT.mjs.map +7 -0
  22. package/dist/lib/browser/react-surface-4WIQZW2S.mjs +38 -0
  23. package/dist/lib/browser/react-surface-4WIQZW2S.mjs.map +7 -0
  24. package/dist/lib/browser/{settings-FNWW6WIJ.mjs → settings-WACNLCPB.mjs} +6 -7
  25. package/dist/lib/browser/settings-WACNLCPB.mjs.map +7 -0
  26. package/dist/lib/browser/state-VPOYUKK6.mjs +117 -0
  27. package/dist/lib/browser/state-VPOYUKK6.mjs.map +7 -0
  28. package/dist/lib/browser/types.mjs +16 -4
  29. package/dist/lib/browser/url-handler-HLF42IHP.mjs +70 -0
  30. package/dist/lib/browser/url-handler-HLF42IHP.mjs.map +7 -0
  31. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  32. package/dist/types/src/capabilities/{layout/app-graph-builder.d.ts → app-graph-builder.d.ts} +22 -22
  33. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/capabilities.d.ts +132 -3
  35. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  36. package/dist/types/src/capabilities/check-app-scheme.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/index.d.ts +187 -3
  38. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  39. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/set-active.d.ts +9 -0
  43. package/dist/types/src/capabilities/set-active.d.ts.map +1 -0
  44. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/state.d.ts +76 -0
  46. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -0
  48. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -1
  49. package/dist/types/src/components/DeckLayout/ComplementarySidebar.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 +1 -4
  52. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  53. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +1 -1
  54. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +3 -3
  55. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  56. package/dist/types/src/components/DeckLayout/Plank.d.ts +8 -6
  57. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  58. package/dist/types/src/components/DeckLayout/PlankControls.d.ts +2 -2
  59. package/dist/types/src/components/DeckLayout/PlankControls.d.ts.map +1 -1
  60. package/dist/types/src/components/DeckLayout/PlankError.d.ts +4 -3
  61. package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -1
  62. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  63. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts +5 -3
  64. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts.map +1 -1
  65. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  66. package/dist/types/src/components/DeckLayout/Toast.d.ts +2 -2
  67. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  68. package/dist/types/src/components/fragments.d.ts +2 -0
  69. package/dist/types/src/components/fragments.d.ts.map +1 -1
  70. package/dist/types/src/components/index.d.ts +0 -2
  71. package/dist/types/src/components/index.d.ts.map +1 -1
  72. package/dist/types/src/hooks/useMainSize.d.ts +2 -2
  73. package/dist/types/src/layout.d.ts +5 -19
  74. package/dist/types/src/layout.d.ts.map +1 -1
  75. package/dist/types/src/meta.d.ts +1 -0
  76. package/dist/types/src/meta.d.ts.map +1 -1
  77. package/dist/types/src/translations.d.ts +3 -3
  78. package/dist/types/src/types.d.ts +107 -2
  79. package/dist/types/src/types.d.ts.map +1 -1
  80. package/dist/types/src/util/index.d.ts +2 -1
  81. package/dist/types/src/util/index.d.ts.map +1 -1
  82. package/dist/types/src/util/layoutAppliesTopbar.d.ts +2 -0
  83. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -0
  84. package/dist/types/src/util/useHoistStatusbar.d.ts.map +1 -1
  85. package/package.json +30 -30
  86. package/src/DeckPlugin.ts +12 -58
  87. package/src/capabilities/{layout/app-graph-builder.ts → app-graph-builder.ts} +36 -28
  88. package/src/capabilities/capabilities.ts +4 -3
  89. package/src/capabilities/{navigation/check-app-scheme.ts → check-app-scheme.ts} +2 -2
  90. package/src/capabilities/index.ts +11 -3
  91. package/src/capabilities/intent-resolver.ts +350 -0
  92. package/src/capabilities/{layout/react-root.tsx → react-root.tsx} +7 -11
  93. package/src/capabilities/react-surface.tsx +31 -0
  94. package/src/capabilities/set-active.ts +43 -0
  95. package/src/capabilities/{settings/settings.ts → settings.ts} +4 -5
  96. package/src/capabilities/state.ts +102 -0
  97. package/src/capabilities/url-handler.ts +63 -0
  98. package/src/components/DeckLayout/ActiveNode.tsx +2 -3
  99. package/src/components/DeckLayout/ComplementarySidebar.tsx +118 -67
  100. package/src/components/DeckLayout/ContentEmpty.tsx +7 -10
  101. package/src/components/DeckLayout/DeckLayout.tsx +103 -61
  102. package/src/components/DeckLayout/Fullscreen.tsx +2 -3
  103. package/src/components/DeckLayout/NodePlankHeading.tsx +57 -65
  104. package/src/components/DeckLayout/Plank.tsx +32 -41
  105. package/src/components/DeckLayout/PlankControls.tsx +11 -10
  106. package/src/components/DeckLayout/PlankError.tsx +6 -5
  107. package/src/components/DeckLayout/Sidebar.tsx +17 -20
  108. package/src/components/DeckLayout/SidebarButton.tsx +25 -31
  109. package/src/components/DeckLayout/StatusBar.tsx +5 -11
  110. package/src/components/DeckLayout/Toast.tsx +2 -2
  111. package/src/components/LayoutSettings.tsx +8 -8
  112. package/src/components/fragments.ts +8 -0
  113. package/src/components/index.ts +0 -2
  114. package/src/hooks/useMainSize.ts +3 -3
  115. package/src/layout.ts +43 -212
  116. package/src/meta.ts +1 -0
  117. package/src/translations.ts +8 -8
  118. package/src/types.ts +88 -2
  119. package/src/util/index.ts +2 -1
  120. package/src/util/layoutAppliesTopbar.ts +7 -0
  121. package/src/util/useHoistStatusbar.ts +17 -8
  122. package/dist/lib/browser/app-graph-builder-67VRUD5K.mjs.map +0 -7
  123. package/dist/lib/browser/chunk-2M4PXYNB.mjs +0 -1052
  124. package/dist/lib/browser/chunk-2M4PXYNB.mjs.map +0 -7
  125. package/dist/lib/browser/chunk-2PJNBVCY.mjs +0 -39
  126. package/dist/lib/browser/chunk-2PJNBVCY.mjs.map +0 -7
  127. package/dist/lib/browser/chunk-4C2AFTET.mjs +0 -186
  128. package/dist/lib/browser/chunk-4C2AFTET.mjs.map +0 -7
  129. package/dist/lib/browser/chunk-5VFDMW5M.mjs.map +0 -7
  130. package/dist/lib/browser/chunk-KY5WXIXY.mjs +0 -44
  131. package/dist/lib/browser/chunk-KY5WXIXY.mjs.map +0 -7
  132. package/dist/lib/browser/deck-PLCSKPGL.mjs +0 -26
  133. package/dist/lib/browser/deck-PLCSKPGL.mjs.map +0 -7
  134. package/dist/lib/browser/intent-resolver-FVOQSTBX.mjs +0 -152
  135. package/dist/lib/browser/intent-resolver-FVOQSTBX.mjs.map +0 -7
  136. package/dist/lib/browser/intent-resolver-K7GW4A2I.mjs +0 -249
  137. package/dist/lib/browser/intent-resolver-K7GW4A2I.mjs.map +0 -7
  138. package/dist/lib/browser/location-QHRBQBQN.mjs +0 -35
  139. package/dist/lib/browser/location-QHRBQBQN.mjs.map +0 -7
  140. package/dist/lib/browser/react-context-3BDW7W2N.mjs +0 -32
  141. package/dist/lib/browser/react-context-3BDW7W2N.mjs.map +0 -7
  142. package/dist/lib/browser/react-root-UL7ZDRVZ.mjs.map +0 -7
  143. package/dist/lib/browser/react-surface-VPNOGGNN.mjs +0 -28
  144. package/dist/lib/browser/react-surface-VPNOGGNN.mjs.map +0 -7
  145. package/dist/lib/browser/settings-FNWW6WIJ.mjs.map +0 -7
  146. package/dist/lib/browser/state-7I5BD7SE.mjs +0 -34
  147. package/dist/lib/browser/state-7I5BD7SE.mjs.map +0 -7
  148. package/dist/lib/browser/url-handler-Z5B7LD3N.mjs +0 -76
  149. package/dist/lib/browser/url-handler-Z5B7LD3N.mjs.map +0 -7
  150. package/dist/types/src/capabilities/layout/app-graph-builder.d.ts.map +0 -1
  151. package/dist/types/src/capabilities/layout/deck.d.ts +0 -4
  152. package/dist/types/src/capabilities/layout/deck.d.ts.map +0 -1
  153. package/dist/types/src/capabilities/layout/index.d.ts +0 -229
  154. package/dist/types/src/capabilities/layout/index.d.ts.map +0 -1
  155. package/dist/types/src/capabilities/layout/intent-resolver.d.ts.map +0 -1
  156. package/dist/types/src/capabilities/layout/react-context.d.ts +0 -8
  157. package/dist/types/src/capabilities/layout/react-context.d.ts.map +0 -1
  158. package/dist/types/src/capabilities/layout/react-root.d.ts.map +0 -1
  159. package/dist/types/src/capabilities/layout/state.d.ts +0 -42
  160. package/dist/types/src/capabilities/layout/state.d.ts.map +0 -1
  161. package/dist/types/src/capabilities/navigation/check-app-scheme.d.ts.map +0 -1
  162. package/dist/types/src/capabilities/navigation/index.d.ts +0 -5
  163. package/dist/types/src/capabilities/navigation/index.d.ts.map +0 -1
  164. package/dist/types/src/capabilities/navigation/intent-resolver.d.ts +0 -4
  165. package/dist/types/src/capabilities/navigation/intent-resolver.d.ts.map +0 -1
  166. package/dist/types/src/capabilities/navigation/location.d.ts +0 -4
  167. package/dist/types/src/capabilities/navigation/location.d.ts.map +0 -1
  168. package/dist/types/src/capabilities/navigation/set-location.d.ts +0 -10
  169. package/dist/types/src/capabilities/navigation/set-location.d.ts.map +0 -1
  170. package/dist/types/src/capabilities/navigation/url-handler.d.ts.map +0 -1
  171. package/dist/types/src/capabilities/settings/index.d.ts +0 -3
  172. package/dist/types/src/capabilities/settings/index.d.ts.map +0 -1
  173. package/dist/types/src/capabilities/settings/react-surface.d.ts.map +0 -1
  174. package/dist/types/src/capabilities/settings/settings.d.ts.map +0 -1
  175. package/dist/types/src/components/DeckContext.d.ts +0 -11
  176. package/dist/types/src/components/DeckContext.d.ts.map +0 -1
  177. package/dist/types/src/components/LayoutContext.d.ts +0 -5
  178. package/dist/types/src/components/LayoutContext.d.ts.map +0 -1
  179. package/dist/types/src/layout.test.d.ts +0 -2
  180. package/dist/types/src/layout.test.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/capabilities/layout/deck.ts +0 -25
  184. package/src/capabilities/layout/index.ts +0 -12
  185. package/src/capabilities/layout/intent-resolver.ts +0 -128
  186. package/src/capabilities/layout/react-context.tsx +0 -26
  187. package/src/capabilities/layout/state.ts +0 -32
  188. package/src/capabilities/navigation/index.ts +0 -10
  189. package/src/capabilities/navigation/intent-resolver.ts +0 -216
  190. package/src/capabilities/navigation/location.ts +0 -28
  191. package/src/capabilities/navigation/set-location.ts +0 -38
  192. package/src/capabilities/navigation/url-handler.ts +0 -67
  193. package/src/capabilities/settings/index.ts +0 -8
  194. package/src/capabilities/settings/react-surface.tsx +0 -23
  195. package/src/components/DeckContext.ts +0 -19
  196. package/src/components/LayoutContext.ts +0 -12
  197. package/src/layout.test.ts +0 -380
  198. package/src/util/layout-parts.ts +0 -12
  199. /package/dist/types/src/capabilities/{navigation/check-app-scheme.d.ts → check-app-scheme.d.ts} +0 -0
  200. /package/dist/types/src/capabilities/{layout/intent-resolver.d.ts → intent-resolver.d.ts} +0 -0
  201. /package/dist/types/src/capabilities/{layout/react-root.d.ts → react-root.d.ts} +0 -0
  202. /package/dist/types/src/capabilities/{settings/react-surface.d.ts → react-surface.d.ts} +0 -0
  203. /package/dist/types/src/capabilities/{settings/settings.d.ts → settings.d.ts} +0 -0
  204. /package/dist/types/src/capabilities/{navigation/url-handler.d.ts → url-handler.d.ts} +0 -0
@@ -0,0 +1,350 @@
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 { nonNullable } from '@dxos/util';
24
+
25
+ import { DeckCapabilities } from './capabilities';
26
+ import { setActive } from './set-active';
27
+ import { closeEntry, incrementPlank, openEntry } from '../layout';
28
+ import { DECK_PLUGIN } from '../meta';
29
+ import { DeckAction, type LayoutMode, type DeckSettingsProps, isLayoutMode, getMode } from '../types';
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
+ },
117
+ }),
118
+ createResolver({
119
+ intent: LayoutAction.UpdateLayout,
120
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.AddToast.fields.input)`
121
+ // but the filter is not being applied correctly.
122
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.AddToast.fields.input> =>
123
+ S.is(LayoutAction.AddToast.fields.input)(data),
124
+ resolve: ({ subject }) => {
125
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
126
+ layout.toasts.push(subject);
127
+ },
128
+ }),
129
+ createResolver({
130
+ intent: LayoutAction.UpdateLayout,
131
+ // TODO(wittjosiah): This should be able to just be `S.is(LayoutAction.SetLayoutMode.fields.input)`
132
+ // but the filter is not being applied correctly.
133
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.SetLayoutMode.fields.input> => {
134
+ if (!S.is(LayoutAction.SetLayoutMode.fields.input)(data)) {
135
+ return false;
136
+ }
137
+
138
+ if ('mode' in data.options) {
139
+ return isLayoutMode(data.options.mode);
140
+ }
141
+
142
+ return true;
143
+ },
144
+ resolve: ({ subject, options }) => {
145
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
146
+
147
+ const setMode = (mode: LayoutMode) => {
148
+ const deck = state.deck;
149
+ const current = deck.solo ? [deck.solo] : deck.active;
150
+ // When un-soloing, the solo entry is added to the deck.
151
+ const next = (
152
+ mode === 'solo' ? [subject ?? deck.solo ?? deck.active[0]] : [...deck.active, deck.solo]
153
+ ).filter(nonNullable);
154
+
155
+ const removed = current.filter((id) => !next.includes(id));
156
+ const closed = Array.from(new Set([...deck.inactive.filter((id) => !next.includes(id)), ...removed]));
157
+ deck.inactive = closed;
158
+
159
+ if (mode === 'solo' && next[0]) {
160
+ deck.solo = next[0];
161
+ } else if (mode !== 'solo' && deck.solo) {
162
+ deck.solo = undefined;
163
+ deck.initialized = true;
164
+ }
165
+
166
+ if (mode === 'fullscreen' && !deck.fullscreen) {
167
+ deck.fullscreen = true;
168
+ } else if (mode !== 'fullscreen' && deck.fullscreen) {
169
+ deck.fullscreen = false;
170
+ }
171
+ };
172
+
173
+ return batch(() => {
174
+ if ('mode' in options) {
175
+ const current = getMode(state.deck);
176
+ if (current !== options.mode) {
177
+ state.previousMode[state.activeDeck] = current;
178
+ }
179
+ setMode(options.mode as LayoutMode);
180
+ } else if ('revert' in options) {
181
+ const last = state.previousMode[state.activeDeck];
182
+ setMode(last ?? 'solo');
183
+ } else {
184
+ log.warn('Invalid layout mode', options);
185
+ }
186
+ });
187
+ },
188
+ }),
189
+ createResolver({
190
+ intent: LayoutAction.UpdateLayout,
191
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
192
+ S.is(LayoutAction.SwitchWorkspace.fields.input)(data),
193
+ resolve: ({ subject }) => {
194
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
195
+ batch(() => {
196
+ state.activeDeck = subject;
197
+ if (!state.decks[subject]) {
198
+ state.decks[subject] = { initialized: false, active: [], inactive: [], fullscreen: false, plankSizing: {} };
199
+ }
200
+ });
201
+
202
+ const first = state.decks[state.activeDeck].active[0];
203
+ if (first) {
204
+ return {
205
+ intents: [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: first })],
206
+ };
207
+ }
208
+ },
209
+ }),
210
+ createResolver({
211
+ intent: LayoutAction.UpdateLayout,
212
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Open.fields.input> =>
213
+ S.is(LayoutAction.Open.fields.input)(data),
214
+ resolve: ({ subject, options }) => {
215
+ const { graph } = context.requestCapability(Capabilities.AppGraph);
216
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
217
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
218
+ const settings = context
219
+ .requestCapabilities(Capabilities.SettingsStore)[0]
220
+ ?.getStore<DeckSettingsProps>(DECK_PLUGIN)?.value;
221
+
222
+ const previouslyOpenIds = new Set<string>(state.deck.solo ? [state.deck.solo] : state.deck.active);
223
+ batch(() => {
224
+ const next = state.deck.solo
225
+ ? (subject as string[])
226
+ : subject.reduce(
227
+ (acc, entryId) =>
228
+ openEntry(acc, entryId, {
229
+ key: options?.key,
230
+ positioning: options?.positioning ?? settings?.newPlankPositioning,
231
+ pivotId: options?.pivotId,
232
+ }),
233
+ state.deck.active,
234
+ );
235
+
236
+ return setActive({ next, state, attention });
237
+ });
238
+
239
+ const ids = state.deck.solo ? [state.deck.solo] : state.deck.active;
240
+ const newlyOpen = ids.filter((i) => !previouslyOpenIds.has(i));
241
+
242
+ return {
243
+ intents: [
244
+ ...(options?.scrollIntoView !== false
245
+ ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: newlyOpen[0] ?? subject[0] })]
246
+ : []),
247
+ createIntent(LayoutAction.Expose, { part: 'navigation', subject: newlyOpen[0] ?? subject[0] }),
248
+ ...newlyOpen.map((id) => {
249
+ const active = graph?.findNode(id)?.data;
250
+ const typename = isReactiveObject(active) ? getTypename(active) : undefined;
251
+ return createIntent(ObservabilityAction.SendEvent, {
252
+ name: 'navigation.activate',
253
+ properties: {
254
+ id,
255
+ typename,
256
+ },
257
+ });
258
+ }),
259
+ ],
260
+ };
261
+ },
262
+ }),
263
+ createResolver({
264
+ intent: LayoutAction.UpdateLayout,
265
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Close.fields.input> =>
266
+ S.is(LayoutAction.Close.fields.input)(data),
267
+ resolve: ({ subject }) => {
268
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
269
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
270
+ const next = subject.reduce((acc, id) => closeEntry(acc, id), state.deck.active);
271
+ const toAttend = setActive({ next, state, attention });
272
+ return {
273
+ intents: toAttend ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: toAttend })] : [],
274
+ };
275
+ },
276
+ }),
277
+ createResolver({
278
+ intent: LayoutAction.UpdateLayout,
279
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.Set.fields.input> =>
280
+ S.is(LayoutAction.Set.fields.input)(data),
281
+ resolve: ({ subject }) => {
282
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
283
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
284
+ const toAttend = setActive({ next: subject as string[], state, attention });
285
+ return {
286
+ intents: toAttend ? [createIntent(LayoutAction.ScrollIntoView, { part: 'current', subject: toAttend })] : [],
287
+ };
288
+ },
289
+ }),
290
+ createResolver({
291
+ intent: LayoutAction.UpdateLayout,
292
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.ScrollIntoView.fields.input> =>
293
+ S.is(LayoutAction.ScrollIntoView.fields.input)(data),
294
+ resolve: ({ subject }) => {
295
+ const layout = context.requestCapability(DeckCapabilities.MutableDeckState);
296
+ layout.scrollIntoView = subject;
297
+ },
298
+ }),
299
+ createResolver({
300
+ intent: DeckAction.UpdatePlankSize,
301
+ resolve: (data) => {
302
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
303
+ state.deck.plankSizing[data.id] = data.size;
304
+ },
305
+ }),
306
+ createResolver({
307
+ intent: DeckAction.Adjust,
308
+ resolve: (adjustment) => {
309
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
310
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
311
+
312
+ return batch(() => {
313
+ if (adjustment.type === 'increment-end' || adjustment.type === 'increment-start') {
314
+ setActive({
315
+ next: incrementPlank(state.deck.active, adjustment),
316
+ state,
317
+ attention,
318
+ });
319
+ }
320
+
321
+ if (adjustment.type === 'solo') {
322
+ const entryId = adjustment.id;
323
+ if (!state.deck.solo) {
324
+ // Solo the entry.
325
+ return {
326
+ intents: [
327
+ createIntent(LayoutAction.SetLayoutMode, {
328
+ part: 'mode',
329
+ subject: entryId,
330
+ options: { mode: 'solo' },
331
+ }),
332
+ ],
333
+ };
334
+ } else {
335
+ // Un-solo the current entry.
336
+ return {
337
+ intents: [
338
+ // NOTE: The order of these is important.
339
+ pipe(
340
+ createIntent(LayoutAction.SetLayoutMode, { part: 'mode', options: { mode: 'deck' } }),
341
+ chain(LayoutAction.Open, { part: 'main', subject: [entryId] }),
342
+ ),
343
+ ],
344
+ };
345
+ }
346
+ }
347
+ });
348
+ },
349
+ }),
350
+ ]);
@@ -6,18 +6,16 @@ import React, { useCallback } from 'react';
6
6
 
7
7
  import { Capabilities, contributes, useCapabilities, useCapability } from '@dxos/app-framework';
8
8
 
9
- import { DeckLayout } from '../../components';
10
- import { DECK_PLUGIN } from '../../meta';
11
- import { type DeckSettingsProps } from '../../types';
12
- import { DeckCapabilities } from '../capabilities';
9
+ import { DeckCapabilities } from './capabilities';
10
+ import { DeckLayout } from '../components';
11
+ import { DECK_PLUGIN } from '../meta';
12
+ import { type DeckSettingsProps } from '../types';
13
13
 
14
14
  export default () =>
15
15
  contributes(Capabilities.ReactRoot, {
16
16
  id: DECK_PLUGIN,
17
17
  root: () => {
18
- const layout = useCapability(Capabilities.Layout);
19
- const location = useCapability(Capabilities.Location);
20
- const deck = useCapability(DeckCapabilities.MutableDeckState);
18
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
21
19
  const settings = useCapability(Capabilities.SettingsStore).getStore<DeckSettingsProps>(DECK_PLUGIN)!.value;
22
20
  const panels = useCapabilities(DeckCapabilities.ComplementaryPanel);
23
21
 
@@ -28,8 +26,8 @@ export default () =>
28
26
  // Allow time for the toast to animate out.
29
27
  // TODO(burdon): Factor out and unregister timeout.
30
28
  setTimeout(() => {
31
- if (layout.toasts[index].id === deck.currentUndoId) {
32
- deck.currentUndoId = undefined;
29
+ if (layout.toasts[index].id === layout.currentUndoId) {
30
+ layout.currentUndoId = undefined;
33
31
  }
34
32
  layout.toasts.splice(index, 1);
35
33
  }, 1_000);
@@ -40,10 +38,8 @@ export default () =>
40
38
 
41
39
  return (
42
40
  <DeckLayout
43
- layoutParts={location.active}
44
41
  showHints={settings.showHints}
45
42
  overscroll={settings.overscroll}
46
- toasts={layout.toasts}
47
43
  panels={panels}
48
44
  onDismissToast={handleDismissToast}
49
45
  />
@@ -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,43 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { batch } from '@preact/signals-core';
6
+
7
+ import { type AttentionManager } from '@dxos/plugin-attention';
8
+
9
+ import { type DeckState } from '../types';
10
+
11
+ export type SetActiveOptions = {
12
+ next: string[];
13
+ state: DeckState;
14
+ attention?: AttentionManager;
15
+ };
16
+
17
+ export const setActive = ({ next, state, attention }: SetActiveOptions) => {
18
+ return batch(() => {
19
+ const active = state.deck.solo ? [state.deck.solo] : state.deck.active;
20
+ const removed = active.filter((id) => !next.includes(id));
21
+ const closed = Array.from(new Set([...state.deck.inactive.filter((id) => !next.includes(id)), ...removed]));
22
+
23
+ state.deck.inactive = closed;
24
+
25
+ if (state.deck.solo || !state.deck.initialized) {
26
+ state.deck.solo = next[0];
27
+ } else {
28
+ state.deck.active = next;
29
+ }
30
+
31
+ if (attention) {
32
+ const attended = attention.current;
33
+ const [attendedId] = Array.from(attended);
34
+ const isAttendedAvailable = !!attendedId && next.includes(attendedId);
35
+ if (!isAttendedAvailable) {
36
+ const attendedIndex = active.indexOf(attendedId);
37
+ // If outside of bounds, focus on the first/last plank, otherwise focus on the new plank in the same position.
38
+ const index = attendedIndex === -1 ? 0 : attendedIndex >= next.length ? next.length - 1 : attendedIndex;
39
+ return next[index];
40
+ }
41
+ }
42
+ });
43
+ };
@@ -5,17 +5,16 @@
5
5
  import { Capabilities, contributes } from '@dxos/app-framework';
6
6
  import { create } from '@dxos/live-object';
7
7
 
8
- import { DECK_PLUGIN } from '../../meta';
9
- import { DeckSettingsSchema, type DeckSettingsProps } from '../../types';
8
+ import { DECK_PLUGIN } from '../meta';
9
+ import { DeckSettingsSchema, type DeckSettingsProps } from '../types';
10
10
 
11
11
  export default () => {
12
12
  const settings = create<DeckSettingsProps>({
13
13
  showHints: false,
14
- customSlots: false,
15
- flatDeck: false,
16
14
  enableNativeRedirect: false,
15
+ enableIdeStyleStatusbar: true,
17
16
  newPlankPositioning: 'start',
18
- overscroll: 'centering',
17
+ overscroll: 'none',
19
18
  });
20
19
 
21
20
  return contributes(Capabilities.Settings, { schema: DeckSettingsSchema, prefix: DECK_PLUGIN, value: settings });
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes } from '@dxos/app-framework';
6
+ import { invariant } from '@dxos/invariant';
7
+ import { create } from '@dxos/live-object';
8
+ import { LocalStorageStore } from '@dxos/local-storage';
9
+ import { type SidebarState } from '@dxos/react-ui';
10
+
11
+ import { DeckCapabilities } from './capabilities';
12
+ import { DECK_PLUGIN } from '../meta';
13
+ import { getMode, type Deck, type DeckState } from '../types';
14
+
15
+ const boolean = /true|false/;
16
+
17
+ // TODO(thure, 18 Feb 2025): Remove after the next release.
18
+
19
+ const migrateSidebarStateDefaults = {
20
+ [`${DECK_PLUGIN}/complementary-sidebar-state`]: 'expanded',
21
+ [`${DECK_PLUGIN}/sidebar-state`]: 'collapsed',
22
+ };
23
+
24
+ const migrateSidebarState = () => {
25
+ Object.entries(migrateSidebarStateDefaults).forEach(([key, defaultValue]) => {
26
+ if (boolean.test(localStorage.getItem(key) ?? 'never')) {
27
+ localStorage.setItem(key, defaultValue);
28
+ }
29
+ });
30
+ };
31
+
32
+ export default () => {
33
+ migrateSidebarState();
34
+
35
+ const state = new LocalStorageStore<DeckState>(DECK_PLUGIN, {
36
+ sidebarState: 'expanded',
37
+ complementarySidebarState: 'collapsed',
38
+ complementarySidebarPanel: undefined,
39
+ dialogContent: null,
40
+ dialogOpen: false,
41
+ dialogBlockAlign: undefined,
42
+ dialogType: undefined,
43
+ popoverContent: null,
44
+ popoverAnchorId: undefined,
45
+ popoverOpen: false,
46
+ toasts: [],
47
+ currentUndoId: undefined,
48
+ activeDeck: 'default',
49
+ decks: {
50
+ default: {
51
+ initialized: false,
52
+ active: [],
53
+ inactive: [],
54
+ fullscreen: false,
55
+ solo: undefined,
56
+ plankSizing: {},
57
+ },
58
+ },
59
+ get deck() {
60
+ const deck = this.decks[this.activeDeck];
61
+ invariant(deck, `Deck not found: ${this.activeDeck}`);
62
+ return deck;
63
+ },
64
+ previousMode: {},
65
+ scrollIntoView: undefined,
66
+ });
67
+
68
+ state
69
+ .prop({ key: 'sidebarState', type: LocalStorageStore.enum<SidebarState>() })
70
+ .prop({ key: 'complementarySidebarState', type: LocalStorageStore.enum<SidebarState>() })
71
+ .prop({ key: 'decks', type: LocalStorageStore.json<Record<string, Deck>>() })
72
+ .prop({ key: 'activeDeck', type: LocalStorageStore.string() });
73
+
74
+ const layout = create<Capabilities.Layout>({
75
+ get mode() {
76
+ return getMode(state.values.deck);
77
+ },
78
+ get dialogOpen() {
79
+ return state.values.dialogOpen;
80
+ },
81
+ get sidebarOpen() {
82
+ return state.values.sidebarState === 'expanded';
83
+ },
84
+ get complementarySidebarOpen() {
85
+ return state.values.complementarySidebarState === 'expanded';
86
+ },
87
+ get active() {
88
+ return state.values.deck.solo ? [state.values.deck.solo] : state.values.deck.active;
89
+ },
90
+ get inactive() {
91
+ return state.values.deck.inactive;
92
+ },
93
+ get scrollIntoView() {
94
+ return state.values.scrollIntoView;
95
+ },
96
+ });
97
+
98
+ return [
99
+ contributes(DeckCapabilities.DeckState, state.values, () => state.close()),
100
+ contributes(Capabilities.Layout, layout),
101
+ ];
102
+ };
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes, createIntent, LayoutAction, type PluginsContext } from '@dxos/app-framework';
6
+ import { scheduledEffect } from '@dxos/echo-signals/core';
7
+
8
+ import { DeckCapabilities } from './capabilities';
9
+
10
+ // TODO(wittjosiah): Cleanup the url handling. May justify introducing routing capabilities.
11
+ export default async (context: PluginsContext) => {
12
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher) ?? {};
13
+ const state = context.requestCapability(DeckCapabilities.MutableDeckState);
14
+
15
+ const handleNavigation = async () => {
16
+ const pathname = window.location.pathname;
17
+ if (pathname === '/reset') {
18
+ state.activeDeck = 'default';
19
+ state.decks = {
20
+ default: {
21
+ initialized: false,
22
+ active: [],
23
+ inactive: [],
24
+ fullscreen: false,
25
+ solo: undefined,
26
+ plankSizing: {},
27
+ },
28
+ };
29
+ window.location.pathname = '/';
30
+ return;
31
+ }
32
+
33
+ const [_, nextDeck, nextSolo] = pathname.split('/');
34
+ if (nextDeck) {
35
+ await dispatch(createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: nextDeck }));
36
+ }
37
+
38
+ if (nextSolo) {
39
+ await dispatch(
40
+ createIntent(LayoutAction.SetLayoutMode, { part: 'mode', subject: nextSolo, options: { mode: 'solo' } }),
41
+ );
42
+ } else {
43
+ await dispatch(createIntent(LayoutAction.SetLayoutMode, { part: 'mode', options: { mode: 'deck' } }));
44
+ }
45
+ };
46
+
47
+ await handleNavigation();
48
+ window.addEventListener('popstate', handleNavigation);
49
+
50
+ const unsubscribe = scheduledEffect(
51
+ () => ({ solo: state.deck.solo, activeDeck: state.activeDeck }),
52
+ ({ solo, activeDeck }) => {
53
+ const path = solo ? `/${activeDeck}/${solo}` : `/${activeDeck}`;
54
+ // TODO(thure): In some browsers, this only preserves the most recent state change, even though this is not `history.replace`…
55
+ history.pushState(null, '', `${path}${window.location.search}`);
56
+ },
57
+ );
58
+
59
+ return contributes(Capabilities.Null, null, () => {
60
+ window.removeEventListener('popstate', handleNavigation);
61
+ unsubscribe();
62
+ });
63
+ };
@@ -4,8 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { Surface } from '@dxos/app-framework';
8
- import { useGraph } from '@dxos/plugin-graph';
7
+ import { Surface, useAppGraph } from '@dxos/app-framework';
9
8
  import { useAttended } from '@dxos/react-ui-attention';
10
9
 
11
10
  import { useNode, useNodeActionExpander } from '../../hooks';
@@ -13,7 +12,7 @@ import { useNode, useNodeActionExpander } from '../../hooks';
13
12
  // TODO(burdon): Factor out to effect in plugin set document title.
14
13
  export const ActiveNode = () => {
15
14
  const [id] = useAttended();
16
- const { graph } = useGraph();
15
+ const { graph } = useAppGraph();
17
16
  const activeNode = useNode(graph, id);
18
17
  useNodeActionExpander(activeNode);
19
18