@dxos/plugin-deck 0.7.5-main.499c70c → 0.7.5-main.6a330ac

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-FT33W5CI.mjs +128 -0
  8. package/dist/lib/browser/chunk-FT33W5CI.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-G2X3ZDCE.mjs +24 -0
  10. package/dist/lib/browser/chunk-G2X3ZDCE.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-KANJBSIX.mjs +97 -0
  12. package/dist/lib/browser/chunk-KANJBSIX.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-GVOGPULO.mjs → chunk-N7TEPFVR.mjs} +5 -4
  14. package/dist/lib/browser/chunk-N7TEPFVR.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-O4RFYYQ6.mjs +1114 -0
  16. package/dist/lib/browser/chunk-O4RFYYQ6.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-ZD67BRUI.mjs +488 -0
  20. package/dist/lib/browser/intent-resolver-ZD67BRUI.mjs.map +7 -0
  21. package/dist/lib/browser/meta.json +1 -1
  22. package/dist/lib/browser/react-root-6ILKHD5J.mjs +45 -0
  23. package/dist/lib/browser/react-root-6ILKHD5J.mjs.map +7 -0
  24. package/dist/lib/browser/react-surface-O75FKXAI.mjs +39 -0
  25. package/dist/lib/browser/react-surface-O75FKXAI.mjs.map +7 -0
  26. package/dist/lib/browser/settings-H35U6NHE.mjs +28 -0
  27. package/dist/lib/browser/settings-H35U6NHE.mjs.map +7 -0
  28. package/dist/lib/browser/state-U4SHOPJW.mjs +129 -0
  29. package/dist/lib/browser/state-U4SHOPJW.mjs.map +7 -0
  30. package/dist/lib/browser/tools-64LXGLYR.mjs +59 -0
  31. package/dist/lib/browser/tools-64LXGLYR.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 +127 -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 +108 -0
  135. package/src/capabilities/tools.ts +66 -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 +188 -58
  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 +110 -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,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
+ };
@@ -0,0 +1,108 @@
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
+ previousDeck: 'default',
50
+ decks: {
51
+ default: {
52
+ initialized: false,
53
+ active: [],
54
+ inactive: [],
55
+ fullscreen: false,
56
+ solo: undefined,
57
+ plankSizing: {},
58
+ },
59
+ },
60
+ get deck() {
61
+ const deck = this.decks[this.activeDeck];
62
+ invariant(deck, `Deck not found: ${this.activeDeck}`);
63
+ return deck;
64
+ },
65
+ previousMode: {},
66
+ scrollIntoView: undefined,
67
+ });
68
+
69
+ state
70
+ .prop({ key: 'sidebarState', type: LocalStorageStore.enum<SidebarState>() })
71
+ .prop({ key: 'complementarySidebarState', type: LocalStorageStore.enum<SidebarState>() })
72
+ .prop({ key: 'complementarySidebarPanel', type: LocalStorageStore.string({ allowUndefined: true }) })
73
+ .prop({ key: 'decks', type: LocalStorageStore.json<Record<string, Deck>>() })
74
+ .prop({ key: 'activeDeck', type: LocalStorageStore.string() })
75
+ .prop({ key: 'previousDeck', type: LocalStorageStore.string() });
76
+
77
+ const layout = create<Capabilities.Layout>({
78
+ get mode() {
79
+ return getMode(state.values.deck);
80
+ },
81
+ get dialogOpen() {
82
+ return state.values.dialogOpen;
83
+ },
84
+ get sidebarOpen() {
85
+ return state.values.sidebarState === 'expanded';
86
+ },
87
+ get complementarySidebarOpen() {
88
+ return state.values.complementarySidebarState === 'expanded';
89
+ },
90
+ get workspace() {
91
+ return state.values.activeDeck;
92
+ },
93
+ get active() {
94
+ return state.values.deck.solo ? [state.values.deck.solo] : state.values.deck.active;
95
+ },
96
+ get inactive() {
97
+ return state.values.deck.inactive;
98
+ },
99
+ get scrollIntoView() {
100
+ return state.values.scrollIntoView;
101
+ },
102
+ });
103
+
104
+ return [
105
+ contributes(DeckCapabilities.DeckState, state.values, () => state.close()),
106
+ contributes(Capabilities.Layout, layout),
107
+ ];
108
+ };
@@ -0,0 +1,66 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import {
6
+ contributes,
7
+ createIntent,
8
+ Capabilities,
9
+ LayoutAction,
10
+ type PromiseIntentDispatcher,
11
+ } from '@dxos/app-framework';
12
+ import { defineTool, ToolResult } from '@dxos/artifact';
13
+ import { S } from '@dxos/echo-schema';
14
+ import { invariant } from '@dxos/invariant';
15
+
16
+ import { meta } from '../meta';
17
+
18
+ // TODO(burdon): Factor out.
19
+ declare global {
20
+ interface ToolContextExtensions {
21
+ dispatch?: PromiseIntentDispatcher;
22
+ pivotId?: string;
23
+ }
24
+ }
25
+
26
+ export default () =>
27
+ contributes(Capabilities.Tools, [
28
+ defineTool(meta.id, {
29
+ name: 'show',
30
+ // TODO(ZaymonFC): We should update the prompt to teach the LLM the difference between object ids and fully qualified ids.
31
+ description: `
32
+ Show an item in the app. Use this tool to open an artifact.
33
+ When supplying IDs to show, they must be fully qualified like space:object.
34
+ `,
35
+ caption: 'Showing item...',
36
+ // TODO(wittjosiah): Refactor Layout/Navigation/Deck actions so that they can be used directly.
37
+ schema: S.Struct({
38
+ id: S.String.annotations({
39
+ description: 'The ID of the item to show.',
40
+ }),
41
+ pivotId: S.optional(
42
+ S.String.annotations({
43
+ description: 'The ID of the chat. If provided, the item will be added after the pivot item.',
44
+ }),
45
+ ),
46
+ }),
47
+ execute: async ({ id }, { extensions }) => {
48
+ invariant(extensions?.dispatch, 'No intent dispatcher');
49
+ const { data, error } = await extensions.dispatch(
50
+ createIntent(LayoutAction.Open, {
51
+ subject: [id],
52
+ part: 'main',
53
+ options: {
54
+ pivotId: extensions.pivotId,
55
+ positioning: 'end',
56
+ },
57
+ }),
58
+ );
59
+ if (error) {
60
+ return ToolResult.Error(error.message);
61
+ }
62
+
63
+ return ToolResult.Success(data);
64
+ },
65
+ }),
66
+ ]);
@@ -0,0 +1,65 @@
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 && nextDeck !== state.activeDeck) {
35
+ await dispatch(createIntent(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: nextDeck }));
36
+ }
37
+
38
+ if (nextSolo && nextSolo !== state.deck.solo) {
39
+ await dispatch(
40
+ createIntent(LayoutAction.SetLayoutMode, { part: 'mode', subject: nextSolo, options: { mode: 'solo' } }),
41
+ );
42
+ } else if (!nextSolo && state.deck.solo) {
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
+ if (window.location.pathname !== path) {
55
+ // TODO(thure): In some browsers, this only preserves the most recent state change, even though this is not `history.replace`…
56
+ history.pushState(null, '', `${path}${window.location.search}`);
57
+ }
58
+ },
59
+ );
60
+
61
+ return contributes(Capabilities.Null, null, () => {
62
+ window.removeEventListener('popstate', handleNavigation);
63
+ unsubscribe();
64
+ });
65
+ };
@@ -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
 
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Surface } from '@dxos/app-framework';
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/react-ui-theme';
10
+
11
+ import { CloseSidebarButton, ToggleSidebarButton } from './SidebarButton';
12
+
13
+ export const Banner = ({ variant, classNames }: ThemedClassName<{ variant?: 'topbar' | 'sidebar' }>) => {
14
+ return (
15
+ <header
16
+ className={mx(
17
+ 'flex items-stretch relative plb-1 pis-1 pie-2',
18
+ variant === 'topbar' &&
19
+ 'fixed inset-inline-0 block-start-[env(safe-area-inset-top)] bs-[--rail-size] border-be border-separator',
20
+ classNames,
21
+ )}
22
+ >
23
+ {variant === 'sidebar' ? <CloseSidebarButton /> : <ToggleSidebarButton />}
24
+ <span className='self-center grow mis-1'>Composer</span>
25
+ {variant === 'topbar' && (
26
+ <div role='none' className='absolute inset-0 pointer-events-none'>
27
+ <div role='none' className='grid bs-full pointer-fine:p-1 max-is-md mli-auto pointer-events-auto'>
28
+ <Surface role='search-input' limit={1} />
29
+ </div>
30
+ </div>
31
+ )}
32
+ <span role='none' className='grow' />
33
+ <Surface role='header-end' limit={1} />
34
+ <Surface role='notch-start' limit={1} />
35
+ </header>
36
+ );
37
+ };
@@ -2,89 +2,219 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo } from 'react';
5
+ import React, {
6
+ type PropsWithChildren,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useState,
11
+ type MouseEvent,
12
+ Fragment,
13
+ } from 'react';
6
14
 
7
15
  import {
8
16
  createIntent,
9
- type LayoutCoordinate,
10
- NavigationAction,
11
- SLUG_PATH_SEPARATOR,
17
+ LayoutAction,
12
18
  Surface,
19
+ useAppGraph,
20
+ useCapabilities,
21
+ useCapability,
13
22
  useIntentDispatcher,
14
23
  } from '@dxos/app-framework';
15
- import { useGraph } from '@dxos/plugin-graph';
16
- import { Main, ScrollArea } from '@dxos/react-ui';
24
+ import { Main, useTranslation, toLocalizedString, IconButton, ScrollArea as NaturalScrollArea } from '@dxos/react-ui';
17
25
  import { useAttended } from '@dxos/react-ui-attention';
18
- import { railGridHorizontal, StackContext } from '@dxos/react-ui-stack';
19
- import { mx } from '@dxos/react-ui-theme';
26
+ import { Tabs } from '@dxos/react-ui-tabs';
27
+ import { byPosition } from '@dxos/util';
20
28
 
21
- import { NodePlankHeading } from './NodePlankHeading';
22
29
  import { PlankContentError } from './PlankError';
23
30
  import { PlankLoading } from './PlankLoading';
24
- import { useNode, useNodeActionExpander } from '../../hooks';
31
+ import { ToggleComplementarySidebarButton } from './SidebarButton';
32
+ import { DeckCapabilities } from '../../capabilities';
33
+ import { useNode } from '../../hooks';
34
+ import { DECK_PLUGIN } from '../../meta';
25
35
  import { type Panel } from '../../types';
26
- import { useLayout } from '../LayoutContext';
36
+ import { layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
27
37
 
28
38
  export type ComplementarySidebarProps = {
29
- panels: Panel[];
30
39
  current?: string;
31
40
  };
32
41
 
33
- export const ComplementarySidebar = ({ panels, current }: ComplementarySidebarProps) => {
34
- const { popoverAnchorId } = useLayout();
35
- const attended = useAttended();
36
- const panel = (panels.find((p) => p.id === current) ?? panels[0])?.id;
37
- const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${panel}` : undefined;
38
- const { graph } = useGraph();
39
- const node = useNode(graph, id);
42
+ export const ComplementarySidebar = ({ current }: ComplementarySidebarProps) => {
43
+ const { t } = useTranslation(DECK_PLUGIN);
40
44
  const { dispatchPromise: dispatch } = useIntentDispatcher();
41
- useNodeActionExpander(node);
45
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
46
+ const attended = useAttended();
47
+ const { graph } = useAppGraph();
48
+ const node = useNode(graph, attended[0]);
49
+ const breakpoint = useBreakpoints();
50
+ const topbar = layoutAppliesTopbar(breakpoint);
51
+ const hoistStatusbar = useHoistStatusbar(breakpoint);
42
52
 
43
- const actions = useMemo(
44
- () =>
45
- panels.map(({ id, label, icon }) => ({
46
- id: `complementary-${id}`,
47
- data: () => {
48
- void dispatch(createIntent(NavigationAction.Open, { activeParts: { complementary: id } }));
49
- },
50
- properties: {
51
- label,
52
- icon,
53
- menuItemType: 'toggle',
54
- isChecked: panel === id,
55
- },
56
- })),
57
- [panel],
53
+ const panels = useCapabilities(DeckCapabilities.ComplementaryPanel);
54
+ const availablePanels = panels
55
+ .filter((panel) => {
56
+ if (!node || !panel.filter) {
57
+ return true;
58
+ }
59
+
60
+ return panel.filter(node);
61
+ })
62
+ .toSorted(byPosition);
63
+ const activePanelId = availablePanels.find((panel) => panel.id === current)?.id ?? availablePanels[0]?.id;
64
+ const [internalValue, setInternalValue] = useState(activePanelId);
65
+
66
+ useEffect(() => {
67
+ setInternalValue(activePanelId);
68
+ }, [activePanelId]);
69
+
70
+ const handleTabClick = useCallback(
71
+ (event: MouseEvent) => {
72
+ const nextValue = event.currentTarget.getAttribute('data-value') as string;
73
+ if (nextValue === activePanelId) {
74
+ layout.complementarySidebarState = layout.complementarySidebarState === 'expanded' ? 'collapsed' : 'expanded';
75
+ } else {
76
+ setInternalValue(nextValue);
77
+ layout.complementarySidebarState = 'expanded';
78
+ void dispatch(createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: nextValue }));
79
+ }
80
+ },
81
+ [layout, activePanelId, dispatch],
58
82
  );
59
83
 
60
- // TODO(wittjosiah): Ensure that id is always defined.
61
- const coordinate: LayoutCoordinate = useMemo(() => ({ entryId: id ?? 'unknown', part: 'complementary' }), [id]);
84
+ const data = useMemo(
85
+ () =>
86
+ node && {
87
+ id: node.id,
88
+ subject: node.data,
89
+ workspace: layout.activeDeck,
90
+ popoverAnchorId: layout.popoverAnchorId,
91
+ },
92
+ [node, layout.popoverAnchorId],
93
+ );
62
94
 
63
95
  // TODO(burdon): Scroll area should be controlled by surface.
64
96
  return (
65
- <Main.ComplementarySidebar>
66
- <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
67
- <div role='none' className={mx(railGridHorizontal, 'grid grid-cols-[100%] bs-full')}>
68
- <NodePlankHeading coordinate={coordinate} node={node} popoverAnchorId={popoverAnchorId} actions={actions} />
69
- <ScrollArea.Root>
70
- <ScrollArea.Viewport>
71
- {node && (
72
- <Surface
73
- key={id}
74
- role={`complementary--${panel}`}
75
- limit={1}
76
- data={{ id, subject: node.properties.object ?? node.properties.space, popoverAnchorId }}
77
- fallback={PlankContentError}
78
- placeholder={<PlankLoading />}
97
+ <Main.ComplementarySidebar
98
+ classNames={[
99
+ topbar && 'block-start-[calc(env(safe-area-inset-top)+var(--rail-size))]',
100
+ hoistStatusbar && 'block-end-[--statusbar-size]',
101
+ ]}
102
+ >
103
+ <Tabs.Root
104
+ orientation='vertical'
105
+ verticalVariant='stateless'
106
+ value={internalValue}
107
+ attendableId={attended[0]}
108
+ classNames='contents'
109
+ >
110
+ <div
111
+ role='none'
112
+ className='absolute z-[1] inset-block-0 inline-end-0 !is-[--r0-size] pbs-[env(safe-area-inset-top)] pbe-[env(safe-area-inset-bottom)] border-is border-separator grid grid-cols-1 grid-rows-[1fr_min-content] bg-baseSurface contain-layout app-drag'
113
+ >
114
+ <Tabs.Tablist classNames='grid grid-cols-1 auto-rows-[--rail-action] p-1 gap-1 !overflow-y-auto'>
115
+ {availablePanels.map((panel) => (
116
+ <Tabs.Tab key={panel.id} value={panel.id} asChild>
117
+ <IconButton
118
+ label={toLocalizedString(panel.label, t)}
119
+ icon={panel.icon}
120
+ size={5}
121
+ iconOnly
122
+ tooltipSide='left'
123
+ data-value={panel.id}
124
+ variant={
125
+ activePanelId === panel.id
126
+ ? layout.complementarySidebarState === 'expanded'
127
+ ? 'primary'
128
+ : 'default'
129
+ : 'ghost'
130
+ }
131
+ onClick={handleTabClick}
79
132
  />
80
- )}
81
- <ScrollArea.Scrollbar orientation='vertical'>
82
- <ScrollArea.Thumb />
83
- </ScrollArea.Scrollbar>
84
- </ScrollArea.Viewport>
85
- </ScrollArea.Root>
133
+ </Tabs.Tab>
134
+ ))}
135
+ </Tabs.Tablist>
136
+ {!hoistStatusbar && (
137
+ <div role='none' className='grid grid-cols-1 auto-rows-[--rail-item] p-1 overflow-y-auto'>
138
+ <Surface role='status-bar--r0-footer' limit={1} />
139
+ </div>
140
+ )}
141
+ <div role='none' className='hidden lg:grid grid-cols-1 auto-rows-[--rail-action] p-1'>
142
+ <ToggleComplementarySidebarButton />
143
+ </div>
86
144
  </div>
87
- </StackContext.Provider>
145
+ {availablePanels.map((panel) => (
146
+ <Tabs.Tabpanel
147
+ key={panel.id}
148
+ value={panel.id}
149
+ classNames='absolute data-[state="inactive"]:-z-[1] inset-block-0 inline-start-0 is-[calc(100%-var(--r0-size))] lg:is-[--r1-size] grid grid-cols-1 grid-rows-[var(--rail-size)_1fr_min-content] pbs-[env(safe-area-inset-top)]'
150
+ {...(layout.complementarySidebarState !== 'expanded' && { inert: 'true' })}
151
+ >
152
+ <ComplementarySidebarPanel
153
+ panel={panel}
154
+ activePanelId={activePanelId}
155
+ data={data}
156
+ hoistStatusbar={hoistStatusbar}
157
+ />
158
+ </Tabs.Tabpanel>
159
+ ))}
160
+ </Tabs.Root>
88
161
  </Main.ComplementarySidebar>
89
162
  );
90
163
  };
164
+
165
+ type ComplementarySidebarPanelProps = {
166
+ panel: Panel;
167
+ activePanelId: string;
168
+ data?: {
169
+ id: string;
170
+ subject: any;
171
+ workspace: string;
172
+ popoverAnchorId?: string;
173
+ };
174
+ hoistStatusbar: boolean;
175
+ };
176
+
177
+ const ScrollArea = ({ children }: PropsWithChildren) => {
178
+ return (
179
+ <NaturalScrollArea.Root>
180
+ <NaturalScrollArea.Viewport>{children}</NaturalScrollArea.Viewport>
181
+ <NaturalScrollArea.Scrollbar orientation='vertical'>
182
+ <NaturalScrollArea.Thumb />
183
+ </NaturalScrollArea.Scrollbar>
184
+ </NaturalScrollArea.Root>
185
+ );
186
+ };
187
+
188
+ const ComplementarySidebarPanel = ({ panel, activePanelId, data, hoistStatusbar }: ComplementarySidebarPanelProps) => {
189
+ const { t } = useTranslation(DECK_PLUGIN);
190
+
191
+ if (panel.id !== activePanelId || !data) {
192
+ return null;
193
+ }
194
+
195
+ const Wrapper = panel.fixed ? Fragment : ScrollArea;
196
+
197
+ return (
198
+ <>
199
+ <h2 className='flex items-center pli-2 border-separator border-be font-medium'>
200
+ {toLocalizedString(panel.label, t)}
201
+ </h2>
202
+ <Wrapper>
203
+ <Surface
204
+ role={`complementary--${activePanelId}`}
205
+ data={data}
206
+ fallback={PlankContentError}
207
+ placeholder={<PlankLoading />}
208
+ />
209
+ </Wrapper>
210
+ {!hoistStatusbar && (
211
+ <div
212
+ role='contentinfo'
213
+ className='flex flex-wrap justify-center items-center border-bs border-separator pbs-1 pbe-[max(env(safe-area-inset-bottom),0.25rem)]'
214
+ >
215
+ <Surface role='status-bar--r1-footer' limit={1} />
216
+ </div>
217
+ )}
218
+ </>
219
+ );
220
+ };