@dxos/plugin-simple-layout 0.0.0 → 0.8.4-main.69d29f4

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 (154) hide show
  1. package/dist/lib/browser/chunk-CLPGTNWJ.mjs +29 -0
  2. package/dist/lib/browser/chunk-CLPGTNWJ.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-FK4M7GJV.mjs +613 -0
  4. package/dist/lib/browser/chunk-FK4M7GJV.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +94 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs +168 -0
  9. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs.map +7 -0
  10. package/dist/lib/browser/react-root-6ARAPH3O.mjs +21 -0
  11. package/dist/lib/browser/react-root-6ARAPH3O.mjs.map +7 -0
  12. package/dist/lib/browser/react-surface-SO7B23GS.mjs +39 -0
  13. package/dist/lib/browser/react-surface-SO7B23GS.mjs.map +7 -0
  14. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs +66 -0
  15. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +7 -0
  16. package/dist/lib/browser/state-H4IGICBB.mjs +45 -0
  17. package/dist/lib/browser/state-H4IGICBB.mjs.map +7 -0
  18. package/dist/lib/browser/url-handler-7CFGTLNG.mjs +54 -0
  19. package/dist/lib/browser/url-handler-7CFGTLNG.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs +614 -0
  21. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs.map +7 -0
  22. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs +31 -0
  23. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs.map +7 -0
  24. package/dist/lib/node-esm/index.mjs +95 -0
  25. package/dist/lib/node-esm/index.mjs.map +7 -0
  26. package/dist/lib/node-esm/meta.json +1 -0
  27. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs +169 -0
  28. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs.map +7 -0
  29. package/dist/lib/node-esm/react-root-2CPA2ZUS.mjs +22 -0
  30. package/dist/lib/node-esm/react-root-2CPA2ZUS.mjs.map +7 -0
  31. package/dist/lib/node-esm/react-surface-FKAV56MO.mjs +40 -0
  32. package/dist/lib/node-esm/react-surface-FKAV56MO.mjs.map +7 -0
  33. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs +68 -0
  34. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +7 -0
  35. package/dist/lib/node-esm/state-QIU2LMLT.mjs +46 -0
  36. package/dist/lib/node-esm/state-QIU2LMLT.mjs.map +7 -0
  37. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs +55 -0
  38. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs.map +7 -0
  39. package/dist/types/src/SimpleLayoutPlugin.d.ts +7 -0
  40. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/index.d.ts +7 -0
  42. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  44. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  46. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/react-root/index.d.ts +6 -0
  48. package/dist/types/src/capabilities/react-root/index.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/react-root/react-root.d.ts +9 -0
  50. package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  52. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  54. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  55. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +3 -0
  56. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -0
  57. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +14 -0
  58. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -0
  59. package/dist/types/src/capabilities/state/index.d.ts +13 -0
  60. package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
  61. package/dist/types/src/capabilities/state/state.d.ts +19 -0
  62. package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
  63. package/dist/types/src/capabilities/url-handler/index.d.ts +3 -0
  64. package/dist/types/src/capabilities/url-handler/index.d.ts.map +1 -0
  65. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +10 -0
  66. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -0
  67. package/dist/types/src/components/ContentError.d.ts +5 -0
  68. package/dist/types/src/components/ContentError.d.ts.map +1 -0
  69. package/dist/types/src/components/ContentError.stories.d.ts +35 -0
  70. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -0
  71. package/dist/types/src/components/ContentLoading.d.ts +3 -0
  72. package/dist/types/src/components/ContentLoading.d.ts.map +1 -0
  73. package/dist/types/src/components/ContentLoading.stories.d.ts +13 -0
  74. package/dist/types/src/components/ContentLoading.stories.d.ts.map +1 -0
  75. package/dist/types/src/components/Dialog/Dialog.d.ts +3 -0
  76. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -0
  77. package/dist/types/src/components/Dialog/index.d.ts +2 -0
  78. package/dist/types/src/components/Dialog/index.d.ts.map +1 -0
  79. package/dist/types/src/components/Home/Home.d.ts +7 -0
  80. package/dist/types/src/components/Home/Home.d.ts.map +1 -0
  81. package/dist/types/src/components/Home/index.d.ts +2 -0
  82. package/dist/types/src/components/Home/index.d.ts.map +1 -0
  83. package/dist/types/src/components/Popover/Popover.d.ts +4 -0
  84. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -0
  85. package/dist/types/src/components/Popover/index.d.ts +2 -0
  86. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  87. package/dist/types/src/components/SimpleLayout/Banner.d.ts +8 -0
  88. package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +1 -0
  89. package/dist/types/src/components/SimpleLayout/Main.d.ts +9 -0
  90. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -0
  91. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +8 -0
  92. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -0
  93. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +39 -0
  94. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts +3 -0
  96. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -0
  97. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +37 -0
  98. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -0
  99. package/dist/types/src/components/SimpleLayout/index.d.ts +2 -0
  100. package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -0
  101. package/dist/types/src/components/Workspace/Workspace.d.ts +9 -0
  102. package/dist/types/src/components/Workspace/Workspace.d.ts.map +1 -0
  103. package/dist/types/src/components/Workspace/index.d.ts +2 -0
  104. package/dist/types/src/components/Workspace/index.d.ts.map +1 -0
  105. package/dist/types/src/components/hooks.d.ts +5 -0
  106. package/dist/types/src/components/hooks.d.ts.map +1 -0
  107. package/dist/types/src/components/index.d.ts +6 -0
  108. package/dist/types/src/components/index.d.ts.map +1 -0
  109. package/dist/types/src/hooks/index.d.ts +2 -0
  110. package/dist/types/src/hooks/index.d.ts.map +1 -0
  111. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +7 -0
  112. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -0
  113. package/dist/types/src/index.d.ts +2 -0
  114. package/dist/types/src/index.d.ts.map +1 -0
  115. package/dist/types/src/meta.d.ts +3 -0
  116. package/dist/types/src/meta.d.ts.map +1 -0
  117. package/dist/types/src/translations.d.ts +20 -0
  118. package/dist/types/src/translations.d.ts.map +1 -0
  119. package/dist/types/src/types/capabilities.d.ts +31 -0
  120. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  121. package/dist/types/src/types/events.d.ts +6 -0
  122. package/dist/types/src/types/events.d.ts.map +1 -0
  123. package/dist/types/src/types/index.d.ts +3 -0
  124. package/dist/types/src/types/index.d.ts.map +1 -0
  125. package/dist/types/tsconfig.tsbuildinfo +1 -0
  126. package/package.json +29 -24
  127. package/src/SimpleLayoutPlugin.ts +20 -4
  128. package/src/capabilities/index.ts +3 -0
  129. package/src/capabilities/operation-resolver/operation-resolver.ts +82 -39
  130. package/src/capabilities/react-surface/index.ts +7 -0
  131. package/src/capabilities/react-surface/react-surface.tsx +40 -0
  132. package/src/capabilities/spotlight-dismiss/index.ts +7 -0
  133. package/src/{hooks/useSpotlightDismiss.ts → capabilities/spotlight-dismiss/spotlight-dismiss.ts} +31 -40
  134. package/src/capabilities/state/state.tsx +21 -32
  135. package/src/capabilities/url-handler/index.ts +7 -0
  136. package/src/capabilities/url-handler/url-handler.ts +80 -0
  137. package/src/components/Dialog/Dialog.tsx +14 -14
  138. package/src/components/Home/Home.tsx +53 -61
  139. package/src/components/Popover/Popover.tsx +45 -27
  140. package/src/components/SimpleLayout/Banner.tsx +50 -28
  141. package/src/components/SimpleLayout/Main.tsx +40 -44
  142. package/src/components/SimpleLayout/NavBar.tsx +18 -41
  143. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +2 -9
  144. package/src/components/SimpleLayout/SimpleLayout.tsx +0 -1
  145. package/src/components/Workspace/Workspace.tsx +115 -0
  146. package/src/components/Workspace/index.ts +5 -0
  147. package/src/components/hooks.ts +30 -0
  148. package/src/components/index.ts +1 -0
  149. package/src/hooks/index.ts +1 -1
  150. package/src/hooks/useSimpleLayoutState.ts +30 -0
  151. package/src/types/capabilities.ts +8 -1
  152. package/src/types/events.ts +14 -0
  153. package/src/types/index.ts +1 -0
  154. /package/src/components/SimpleLayout/{NavBarstories.tsx → NavBar.stories.tsx} +0 -0
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/plugin-simple-layout",
3
- "version": "0.0.0",
3
+ "version": "0.8.4-main.69d29f4",
4
4
  "description": "Simple layout plugin for minimal UI contexts like popover windows.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "sideEffects": true,
@@ -22,20 +26,20 @@
22
26
  "src"
23
27
  ],
24
28
  "dependencies": {
25
- "@preact-signals/safe-react": "^0.9.0",
29
+ "@effect-atom/atom": "^0.4.13",
30
+ "@effect-atom/atom-react": "^0.4.6",
26
31
  "@radix-ui/react-context": "1.1.1",
27
- "@dxos/app-framework": "0.8.3",
28
- "@dxos/log": "0.8.3",
29
- "@dxos/react-ui-menu": "0.8.3",
30
- "@dxos/live-object": "0.8.3",
31
- "@dxos/plugin-graph": "0.8.3",
32
- "@dxos/operation": "0.0.0",
33
- "@dxos/react-ui-searchlist": "0.8.3",
34
- "@dxos/schema": "0.8.3",
35
- "@dxos/react-ui-attention": "0.8.3",
36
- "@dxos/react-ui-stack": "0.8.3",
37
- "@dxos/util": "0.8.3",
38
- "@dxos/react-ui-mosaic": "0.8.3"
32
+ "@dxos/log": "0.8.4-main.69d29f4",
33
+ "@dxos/app-framework": "0.8.4-main.69d29f4",
34
+ "@dxos/operation": "0.8.4-main.69d29f4",
35
+ "@dxos/plugin-graph": "0.8.4-main.69d29f4",
36
+ "@dxos/react-ui-menu": "0.8.4-main.69d29f4",
37
+ "@dxos/react-ui-mosaic": "0.8.4-main.69d29f4",
38
+ "@dxos/react-ui-attention": "0.8.4-main.69d29f4",
39
+ "@dxos/react-ui-searchlist": "0.8.4-main.69d29f4",
40
+ "@dxos/react-ui-stack": "0.8.4-main.69d29f4",
41
+ "@dxos/schema": "0.8.4-main.69d29f4",
42
+ "@dxos/util": "0.8.4-main.69d29f4"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/react": "~19.2.7",
@@ -44,21 +48,22 @@
44
48
  "react": "~19.2.3",
45
49
  "react-dom": "~19.2.3",
46
50
  "vite": "7.1.9",
47
- "@dxos/echo-signals": "0.8.3",
48
- "@dxos/plugin-search": "0.8.3",
49
- "@dxos/plugin-client": "0.8.3",
50
- "@dxos/plugin-space": "0.8.3",
51
- "@dxos/plugin-testing": "0.0.0",
52
- "@dxos/ui-theme": "0.0.0",
53
- "@dxos/react-ui": "0.8.3",
54
- "@dxos/storybook-utils": "0.8.3"
51
+ "@dxos/plugin-client": "0.8.4-main.69d29f4",
52
+ "@dxos/plugin-preview": "0.8.4-main.69d29f4",
53
+ "@dxos/plugin-search": "0.8.4-main.69d29f4",
54
+ "@dxos/plugin-space": "0.8.4-main.69d29f4",
55
+ "@dxos/react-ui": "0.8.4-main.69d29f4",
56
+ "@dxos/plugin-testing": "0.8.4-main.69d29f4",
57
+ "@dxos/schema": "0.8.4-main.69d29f4",
58
+ "@dxos/storybook-utils": "0.8.4-main.69d29f4",
59
+ "@dxos/ui-theme": "0.8.4-main.69d29f4"
55
60
  },
56
61
  "peerDependencies": {
57
62
  "effect": "3.19.11",
58
63
  "react": "~19.2.3",
59
64
  "react-dom": "~19.2.3",
60
- "@dxos/react-ui": "0.8.3",
61
- "@dxos/ui-theme": "0.0.0"
65
+ "@dxos/react-ui": "0.8.4-main.69d29f4",
66
+ "@dxos/ui-theme": "0.8.4-main.69d29f4"
62
67
  },
63
68
  "publishConfig": {
64
69
  "access": "public"
@@ -2,11 +2,12 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Capability, Common, Plugin } from '@dxos/app-framework';
5
+ import { ActivationEvent, Capability, Common, Plugin } from '@dxos/app-framework';
6
6
 
7
- import { OperationResolver, ReactRoot, type SimpleLayoutStateOptions, State } from './capabilities';
7
+ import { OperationResolver, ReactRoot, ReactSurface, SpotlightDismiss, State, UrlHandler } from './capabilities';
8
8
  import { meta } from './meta';
9
9
  import { translations } from './translations';
10
+ import { SimpleLayoutEvents } from './types';
10
11
 
11
12
  export type SimpleLayoutPluginOptions = {
12
13
  /** Whether running in popover window context (hides mobile-specific UI). */
@@ -17,14 +18,29 @@ export const SimpleLayoutPlugin = Plugin.define<SimpleLayoutPluginOptions>(meta)
17
18
  Plugin.addModule(({ isPopover = false }) => ({
18
19
  id: Capability.getModuleTag(State),
19
20
  activatesOn: Common.ActivationEvent.Startup,
20
- activatesAfter: [Common.ActivationEvent.LayoutReady],
21
- activate: () => State({ initialState: { isPopover } } satisfies SimpleLayoutStateOptions),
21
+ activatesAfter: [SimpleLayoutEvents.StateReady, Common.ActivationEvent.LayoutReady],
22
+ activate: () => State({ initialState: { isPopover } }),
23
+ })),
24
+ Plugin.addModule(({ isPopover = false }) => ({
25
+ id: Capability.getModuleTag(SpotlightDismiss),
26
+ activatesOn: Common.ActivationEvent.Startup,
27
+ activate: () => SpotlightDismiss({ isPopover }),
22
28
  })),
23
29
  Plugin.addModule({
24
30
  id: Capability.getModuleTag(ReactRoot),
25
31
  activatesOn: Common.ActivationEvent.Startup,
26
32
  activate: ReactRoot,
27
33
  }),
34
+ Plugin.addModule({
35
+ id: Capability.getModuleTag(ReactSurface),
36
+ activatesOn: Common.ActivationEvent.Startup,
37
+ activate: ReactSurface,
38
+ }),
39
+ Plugin.addModule({
40
+ id: Capability.getModuleTag(UrlHandler),
41
+ activatesOn: ActivationEvent.allOf(Common.ActivationEvent.OperationInvokerReady, SimpleLayoutEvents.StateReady),
42
+ activate: UrlHandler,
43
+ }),
28
44
  Common.Plugin.addOperationResolverModule({ activate: OperationResolver }),
29
45
  Common.Plugin.addTranslationsModule({ translations }),
30
46
  Plugin.make,
@@ -4,4 +4,7 @@
4
4
 
5
5
  export * from './operation-resolver';
6
6
  export * from './react-root';
7
+ export * from './react-surface';
8
+ export * from './spotlight-dismiss';
7
9
  export * from './state';
10
+ export * from './url-handler';
@@ -7,10 +7,21 @@ import * as Effect from 'effect/Effect';
7
7
  import { Capability, Common } from '@dxos/app-framework';
8
8
  import { Operation, OperationResolver } from '@dxos/operation';
9
9
 
10
- import { SimpleLayoutState } from '../../types';
10
+ import { type SimpleLayoutState, SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
11
+
12
+ /** Maximum number of items to keep in navigation history. */
13
+ const MAX_HISTORY_LENGTH = 50;
11
14
 
12
15
  export default Capability.makeModule(
13
16
  Effect.fnUntraced(function* () {
17
+ const registry = yield* Capability.get(Common.Capability.AtomRegistry);
18
+ const stateAtom = yield* Capability.get(SimpleLayoutStateCapability);
19
+
20
+ const getState = () => registry.get(stateAtom);
21
+ const updateState = (fn: (current: SimpleLayoutState) => SimpleLayoutState) => {
22
+ registry.set(stateAtom, fn(getState()));
23
+ };
24
+
14
25
  return Capability.contributes(Common.Capability.OperationResolver, [
15
26
  //
16
27
  // UpdateSidebar - No-op for simple layout.
@@ -34,13 +45,15 @@ export default Capability.makeModule(
34
45
  OperationResolver.make({
35
46
  operation: Common.LayoutOperation.UpdateDialog,
36
47
  handler: Effect.fnUntraced(function* (input) {
37
- const layout = yield* Capability.get(SimpleLayoutState);
38
- layout.dialogOpen = input.state ?? Boolean(input.subject);
39
- layout.dialogType = input.type ?? 'default';
40
- layout.dialogBlockAlign = input.blockAlign ?? 'center';
41
- layout.dialogOverlayClasses = input.overlayClasses;
42
- layout.dialogOverlayStyle = input.overlayStyle;
43
- layout.dialogContent = input.subject ? { component: input.subject, props: input.props } : null;
48
+ updateState((state) => ({
49
+ ...state,
50
+ dialogOpen: input.state ?? Boolean(input.subject),
51
+ dialogType: input.type ?? 'default',
52
+ dialogBlockAlign: input.blockAlign ?? 'center',
53
+ dialogOverlayClasses: input.overlayClasses,
54
+ dialogOverlayStyle: input.overlayStyle,
55
+ dialogContent: input.subject ? { component: input.subject, props: input.props } : undefined,
56
+ }));
44
57
  }),
45
58
  }),
46
59
 
@@ -50,21 +63,22 @@ export default Capability.makeModule(
50
63
  OperationResolver.make({
51
64
  operation: Common.LayoutOperation.UpdatePopover,
52
65
  handler: Effect.fnUntraced(function* (input) {
53
- const layout = yield* Capability.get(SimpleLayoutState);
54
- layout.popoverOpen = input.state ?? Boolean(input.subject);
55
- layout.popoverContent =
56
- typeof input.subject === 'string'
57
- ? { component: input.subject, props: input.props }
58
- : input.subject
59
- ? { subject: input.subject }
60
- : undefined;
61
- layout.popoverSide = input.side;
62
- layout.popoverVariant = input.variant;
63
- if (input.variant === 'virtual') {
64
- layout.popoverAnchor = input.anchor;
65
- } else {
66
- layout.popoverAnchorId = input.anchorId;
67
- }
66
+ updateState((state) => ({
67
+ ...state,
68
+ popoverOpen: input.state ?? Boolean(input.subject),
69
+ popoverKind: input.kind ?? 'base',
70
+ popoverTitle: input.kind === 'card' ? input.title : undefined,
71
+ popoverContent:
72
+ typeof input.subject === 'string'
73
+ ? { component: input.subject, props: input.props }
74
+ : input.subject
75
+ ? { subject: input.subject }
76
+ : undefined,
77
+ popoverSide: input.side,
78
+ popoverVariant: input.variant,
79
+ popoverAnchor: input.variant === 'virtual' ? input.anchor : state.popoverAnchor,
80
+ popoverAnchorId: input.variant !== 'virtual' ? input.anchorId : state.popoverAnchorId,
81
+ }));
68
82
  }),
69
83
  }),
70
84
 
@@ -74,14 +88,16 @@ export default Capability.makeModule(
74
88
  OperationResolver.make({
75
89
  operation: Common.LayoutOperation.SwitchWorkspace,
76
90
  handler: Effect.fnUntraced(function* (input) {
77
- const layout = yield* Capability.get(SimpleLayoutState);
78
- // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
79
- // Ideally this should be worked into the data model in a generic way.
80
- if (!layout.workspace.startsWith('!')) {
81
- layout.previousWorkspace = layout.workspace;
82
- }
83
- layout.workspace = input.subject;
84
- layout.active = undefined;
91
+ updateState((state) => ({
92
+ ...state,
93
+ // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
94
+ // Ideally this should be worked into the data model in a generic way.
95
+ previousWorkspace: !state.workspace.startsWith('!') ? state.workspace : state.previousWorkspace,
96
+ workspace: input.subject,
97
+ active: undefined,
98
+ // Clear history when switching workspaces.
99
+ history: [],
100
+ }));
85
101
  }),
86
102
  }),
87
103
 
@@ -91,9 +107,9 @@ export default Capability.makeModule(
91
107
  OperationResolver.make({
92
108
  operation: Common.LayoutOperation.RevertWorkspace,
93
109
  handler: Effect.fnUntraced(function* () {
94
- const layout = yield* Capability.get(SimpleLayoutState);
110
+ const state = getState();
95
111
  yield* Operation.invoke(Common.LayoutOperation.SwitchWorkspace, {
96
- subject: layout.previousWorkspace,
112
+ subject: state.previousWorkspace,
97
113
  });
98
114
  }),
99
115
  }),
@@ -104,8 +120,18 @@ export default Capability.makeModule(
104
120
  OperationResolver.make({
105
121
  operation: Common.LayoutOperation.Open,
106
122
  handler: Effect.fnUntraced(function* (input) {
107
- const layout = yield* Capability.get(SimpleLayoutState);
108
- layout.active = input.subject[0];
123
+ updateState((state) => {
124
+ // Push current active to history if it exists.
125
+ const newHistory = state.active ? [...state.history, state.active] : state.history;
126
+ // Limit history length to prevent memory issues.
127
+ const trimmedHistory =
128
+ newHistory.length > MAX_HISTORY_LENGTH ? newHistory.slice(-MAX_HISTORY_LENGTH) : newHistory;
129
+ return {
130
+ ...state,
131
+ active: input.subject[0],
132
+ history: trimmedHistory,
133
+ };
134
+ });
109
135
  }),
110
136
  }),
111
137
 
@@ -115,8 +141,23 @@ export default Capability.makeModule(
115
141
  OperationResolver.make({
116
142
  operation: Common.LayoutOperation.Close,
117
143
  handler: Effect.fnUntraced(function* () {
118
- const layout = yield* Capability.get(SimpleLayoutState);
119
- layout.active = undefined;
144
+ updateState((state) => {
145
+ // Pop from history if available.
146
+ if (state.history.length > 0) {
147
+ const newHistory = [...state.history];
148
+ const previousActive = newHistory.pop();
149
+ return {
150
+ ...state,
151
+ active: previousActive,
152
+ history: newHistory,
153
+ };
154
+ }
155
+ // No history, just clear active.
156
+ return {
157
+ ...state,
158
+ active: undefined,
159
+ };
160
+ });
120
161
  }),
121
162
  }),
122
163
 
@@ -126,8 +167,10 @@ export default Capability.makeModule(
126
167
  OperationResolver.make({
127
168
  operation: Common.LayoutOperation.Set,
128
169
  handler: Effect.fnUntraced(function* (input) {
129
- const layout = yield* Capability.get(SimpleLayoutState);
130
- layout.active = input.subject[0];
170
+ updateState((state) => ({
171
+ ...state,
172
+ active: input.subject[0],
173
+ }));
131
174
  }),
132
175
  }),
133
176
  ]);
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capability } from '@dxos/app-framework';
6
+
7
+ export const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));
@@ -0,0 +1,40 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import React from 'react';
7
+
8
+ import { Capability, Common } from '@dxos/app-framework';
9
+ import { Node } from '@dxos/plugin-graph';
10
+
11
+ import { Home, Workspace } from '../../components';
12
+ import { meta } from '../../meta';
13
+
14
+ type SurfaceData = {
15
+ attendableId: string;
16
+ properties: Record<string, any>;
17
+ };
18
+
19
+ const ALLOWED_DISPOSITIONS = ['workspace', 'user-account', 'pin-end'];
20
+
21
+ export default Capability.makeModule(() =>
22
+ Effect.succeed(
23
+ Capability.contributes(Common.Capability.ReactSurface, [
24
+ Common.createSurface({
25
+ id: `${meta.id}/home`,
26
+ role: 'article',
27
+ filter: (data): data is SurfaceData => data.attendableId === Node.RootId,
28
+ component: () => <Home />,
29
+ }),
30
+ Common.createSurface({
31
+ id: `${meta.id}/workspace-article`,
32
+ role: 'article',
33
+ position: 'fallback',
34
+ filter: (data): data is SurfaceData =>
35
+ ALLOWED_DISPOSITIONS.includes((data.properties as Record<string, any>)?.disposition),
36
+ component: ({ data }) => <Workspace id={data.attendableId} />,
37
+ }),
38
+ ]),
39
+ ),
40
+ );
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capability } from '@dxos/app-framework';
6
+
7
+ export const SpotlightDismiss = Capability.lazy('SpotlightDismiss', () => import('./spotlight-dismiss'));
@@ -5,14 +5,14 @@
5
5
  // Based on the frontend-driven dismiss pattern from:
6
6
  // https://github.com/Jedliu/tauri-template-demo
7
7
 
8
- import { useEffect } from 'react';
8
+ import * as Effect from 'effect/Effect';
9
9
 
10
+ import { Capability, Common } from '@dxos/app-framework';
10
11
  import { log } from '@dxos/log';
11
12
  import { isTauri } from '@dxos/util';
12
13
 
13
14
  /**
14
15
  * Get the Tauri window API from the global object.
15
- * Returns undefined if not running in Tauri.
16
16
  */
17
17
  const getTauriWindow = (): any => {
18
18
  const tauri = (globalThis as any).__TAURI__;
@@ -21,60 +21,46 @@ const getTauriWindow = (): any => {
21
21
 
22
22
  /**
23
23
  * Get the Tauri core API (invoke) from the global object.
24
- * Returns undefined if not running in Tauri.
25
24
  */
26
25
  const getTauriCore = (): any => {
27
26
  const tauri = (globalThis as any).__TAURI__;
28
27
  return tauri?.core;
29
28
  };
30
29
 
30
+ export type SpotlightDismissOptions = {
31
+ /** Whether running in popover window context. */
32
+ isPopover?: boolean;
33
+ };
34
+
31
35
  /**
32
- * Hook to set up spotlight panel dismiss behavior.
36
+ * Capability that sets up spotlight panel dismiss behavior.
33
37
  * When running in Tauri popover mode, listens for focus loss and Escape key
34
- * to dismiss the spotlight panel.
38
+ * to dismiss the spotlight panel. Runs at startup before React renders.
35
39
  */
36
- export const useSpotlightDismiss = (isPopover: boolean | undefined) => {
37
- // Handle blur (click outside) to dismiss spotlight.
38
- useEffect(() => {
40
+ export default Capability.makeModule(({ isPopover = false }: SpotlightDismissOptions = {}) =>
41
+ Effect.promise(async () => {
39
42
  if (!isPopover || !isTauri()) {
40
- return;
43
+ return [];
41
44
  }
42
45
 
43
- let cleanup: (() => void) | undefined;
44
-
45
- const setup = async () => {
46
- try {
47
- const tauriWindow = getTauriWindow();
48
- const tauriCore = getTauriCore();
49
- if (!tauriWindow || !tauriCore) {
50
- return;
51
- }
52
-
46
+ // Set up focus listener.
47
+ let focusCleanup: (() => void) | undefined;
48
+ try {
49
+ const tauriWindow = getTauriWindow();
50
+ const tauriCore = getTauriCore();
51
+ if (tauriWindow && tauriCore) {
53
52
  const win = tauriWindow.getCurrentWindow();
54
- const unlisten = await win.onFocusChanged(async ({ payload }: { payload: boolean }) => {
53
+ focusCleanup = await win.onFocusChanged(async ({ payload }: { payload: boolean }) => {
55
54
  if (!payload) {
56
55
  await tauriCore.invoke('hide_spotlight');
57
56
  }
58
57
  });
59
- cleanup = unlisten;
60
- } catch (err) {
61
- log.catch(err);
62
58
  }
63
- };
64
-
65
- void setup();
66
-
67
- return () => {
68
- cleanup?.();
69
- };
70
- }, [isPopover]);
71
-
72
- // Handle Escape key to dismiss spotlight.
73
- useEffect(() => {
74
- if (!isPopover || !isTauri()) {
75
- return;
59
+ } catch (err) {
60
+ log.catch(err);
76
61
  }
77
62
 
63
+ // Set up Escape key listener.
78
64
  const handleKeyDown = async (event: KeyboardEvent) => {
79
65
  if (event.key === 'Escape') {
80
66
  event.preventDefault();
@@ -88,8 +74,13 @@ export const useSpotlightDismiss = (isPopover: boolean | undefined) => {
88
74
  }
89
75
  }
90
76
  };
91
-
92
77
  window.addEventListener('keydown', handleKeyDown);
93
- return () => window.removeEventListener('keydown', handleKeyDown);
94
- }, [isPopover]);
95
- };
78
+
79
+ return Capability.contributes(Common.Capability.Null, null, () =>
80
+ Effect.sync(() => {
81
+ focusCleanup?.();
82
+ window.removeEventListener('keydown', handleKeyDown);
83
+ }),
84
+ );
85
+ }),
86
+ );
@@ -2,18 +2,20 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Atom } from '@effect-atom/atom-react';
5
6
  import * as Effect from 'effect/Effect';
6
7
 
7
8
  import { Capability, Common } from '@dxos/app-framework';
8
- import { live } from '@dxos/live-object';
9
+ import { Node } from '@dxos/plugin-graph';
9
10
 
10
11
  import { type SimpleLayoutState } from '../../types';
11
12
  import { SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
12
13
 
13
14
  const defaultState: SimpleLayoutState = {
14
15
  dialogOpen: false,
15
- workspace: 'default',
16
- previousWorkspace: 'default',
16
+ workspace: Node.RootId,
17
+ previousWorkspace: Node.RootId,
18
+ history: [],
17
19
  isPopover: false,
18
20
  };
19
21
 
@@ -23,38 +25,25 @@ export type SimpleLayoutStateOptions = {
23
25
 
24
26
  export default Capability.makeModule(({ initialState }: SimpleLayoutStateOptions = {}) =>
25
27
  Effect.sync(() => {
26
- const state = live<SimpleLayoutState>({ ...defaultState, ...initialState });
27
-
28
- const layout = live<Common.Capability.Layout>({
29
- get mode() {
30
- return 'simple';
31
- },
32
- get dialogOpen() {
33
- return state.dialogOpen;
34
- },
35
- get sidebarOpen() {
36
- return false;
37
- },
38
- get complementarySidebarOpen() {
39
- return false;
40
- },
41
- get workspace() {
42
- return state.workspace;
43
- },
44
- get active() {
45
- return state.active ? [state.active] : [];
46
- },
47
- get inactive() {
48
- return [];
49
- },
50
- get scrollIntoView() {
51
- return undefined;
52
- },
28
+ const stateAtom = Atom.make<SimpleLayoutState>({ ...defaultState, ...initialState });
29
+
30
+ const layoutAtom = Atom.make((get): Common.Capability.Layout => {
31
+ const state = get(stateAtom);
32
+ return {
33
+ mode: 'simple',
34
+ dialogOpen: state.dialogOpen,
35
+ sidebarOpen: false,
36
+ complementarySidebarOpen: false,
37
+ workspace: state.workspace,
38
+ active: state.active ? [state.active] : [],
39
+ inactive: [],
40
+ scrollIntoView: undefined,
41
+ };
53
42
  });
54
43
 
55
44
  return [
56
- Capability.contributes(SimpleLayoutStateCapability, state),
57
- Capability.contributes(Common.Capability.Layout, layout),
45
+ Capability.contributes(SimpleLayoutStateCapability, stateAtom),
46
+ Capability.contributes(Common.Capability.Layout, layoutAtom),
58
47
  ];
59
48
  }),
60
49
  );
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capability } from '@dxos/app-framework';
6
+
7
+ export const UrlHandler = Capability.lazy('UrlHandler', () => import('./url-handler'));
@@ -0,0 +1,80 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capability, Common } from '@dxos/app-framework';
8
+ import { Node } from '@dxos/plugin-graph';
9
+
10
+ import { type SimpleLayoutState, SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
11
+
12
+ /**
13
+ * URL handler for simple layout that syncs browser URL with layout state.
14
+ * URL format: /{workspace} or /{workspace}/{active}
15
+ * Root is represented as / or /root.
16
+ */
17
+ export default Capability.makeModule(
18
+ Effect.fnUntraced(function* () {
19
+ const { invokeSync } = yield* Capability.get(Common.Capability.OperationInvoker);
20
+
21
+ /**
22
+ * Handle navigation events (initial load and popstate).
23
+ * Parses URL and updates state accordingly.
24
+ */
25
+ const handleNavigation = () => {
26
+ const pathname = window.location.pathname;
27
+
28
+ // Parse URL segments: /{workspace}/{active}
29
+ const [_, nextWorkspace, nextActive] = pathname.split('/');
30
+
31
+ // Determine target workspace (empty or 'root' means Node.RootId).
32
+ const targetWorkspace = !nextWorkspace || nextWorkspace === 'root' ? Node.RootId : nextWorkspace;
33
+
34
+ // Navigate via operations (they will update state accordingly).
35
+ invokeSync(Common.LayoutOperation.SwitchWorkspace, { subject: targetWorkspace });
36
+ if (nextActive) {
37
+ invokeSync(Common.LayoutOperation.Open, { subject: [nextActive] });
38
+ }
39
+ };
40
+
41
+ // Handle initial URL and listen for browser navigation.
42
+ yield* Effect.sync(() => handleNavigation());
43
+ window.addEventListener('popstate', handleNavigation);
44
+
45
+ // Subscribe to state changes to update the URL.
46
+ let lastWorkspace: string | undefined;
47
+ let lastActive: string | undefined;
48
+ const unsubscribe = yield* Common.Capability.subscribeAtom(
49
+ SimpleLayoutStateCapability,
50
+ (state: SimpleLayoutState) => {
51
+ const { workspace, active } = state;
52
+
53
+ // Only update URL if relevant state changed.
54
+ if (workspace !== lastWorkspace || active !== lastActive) {
55
+ lastWorkspace = workspace;
56
+ lastActive = active;
57
+
58
+ // Build path: root is represented as /, other workspaces as /{workspace}.
59
+ let path: string;
60
+ if (workspace === Node.RootId) {
61
+ path = active ? `/${Node.RootId}/${active}` : '/';
62
+ } else {
63
+ path = active ? `/${workspace}/${active}` : `/${workspace}`;
64
+ }
65
+
66
+ if (window.location.pathname !== path) {
67
+ history.pushState(null, '', `${path}${window.location.search}`);
68
+ }
69
+ }
70
+ },
71
+ );
72
+
73
+ return Capability.contributes(Common.Capability.Null, null, () =>
74
+ Effect.sync(() => {
75
+ window.removeEventListener('popstate', handleNavigation);
76
+ unsubscribe();
77
+ }),
78
+ );
79
+ }),
80
+ );