@dxos/app-framework 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731

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 (177) hide show
  1. package/dist/lib/browser/{capability-K5XIVCQU.mjs → capability-JCUNWZ66.mjs} +7 -7
  2. package/dist/lib/browser/{chunk-5AHASNDW.mjs → chunk-2ZIYRDAF.mjs} +1 -1
  3. package/dist/lib/browser/{chunk-5AHASNDW.mjs.map → chunk-2ZIYRDAF.mjs.map} +1 -1
  4. package/dist/lib/browser/{chunk-QLML5QFJ.mjs → chunk-7MBI66M7.mjs} +12 -6
  5. package/dist/lib/browser/chunk-7MBI66M7.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-VJ5PFAWC.mjs → chunk-BMLCIFZW.mjs} +37 -16
  7. package/dist/lib/browser/chunk-BMLCIFZW.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-SLX73WRZ.mjs → chunk-DC3WRPBV.mjs} +14 -5
  9. package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-BRK6GYNB.mjs → chunk-E5DTDNDY.mjs} +20 -19
  11. package/dist/lib/browser/chunk-E5DTDNDY.mjs.map +7 -0
  12. package/dist/lib/browser/{chunk-KLHQNYJ2.mjs → chunk-KZUDO43J.mjs} +2 -2
  13. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +12 -0
  14. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +7 -0
  15. package/dist/lib/browser/{chunk-UVTGHZQF.mjs → chunk-VBG5F5UF.mjs} +51 -88
  16. package/dist/lib/browser/chunk-VBG5F5UF.mjs.map +7 -0
  17. package/dist/lib/browser/cli/index.mjs +5 -5
  18. package/dist/lib/browser/common/activation-events.mjs +3 -3
  19. package/dist/lib/browser/common/capabilities.mjs +4 -4
  20. package/dist/lib/browser/core/plugin-manager.mjs +2 -2
  21. package/dist/lib/browser/core/plugin.mjs +3 -1
  22. package/dist/lib/browser/core/url-loader.mjs +2 -2
  23. package/dist/lib/browser/index.mjs +13 -14
  24. package/dist/lib/browser/index.mjs.map +3 -3
  25. package/dist/lib/browser/meta.json +1 -1
  26. package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs → process-manager-capability-4FUA4TRP.mjs} +6 -6
  27. package/dist/lib/browser/testing/index.mjs +11 -10
  28. package/dist/lib/browser/testing/index.mjs.map +3 -3
  29. package/dist/lib/browser/testing/react.mjs +6 -6
  30. package/dist/lib/browser/ui/index.mjs +7 -9
  31. package/dist/lib/node-esm/{capability-RLKFFLTB.mjs → capability-7TCQK7ZP.mjs} +7 -7
  32. package/dist/lib/node-esm/{chunk-SFYCO3PT.mjs → chunk-4BX2AE47.mjs} +37 -16
  33. package/dist/lib/node-esm/chunk-4BX2AE47.mjs.map +7 -0
  34. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +14 -0
  35. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +7 -0
  36. package/dist/lib/node-esm/{chunk-LJNUFNDO.mjs → chunk-GEWHMHM7.mjs} +12 -6
  37. package/dist/lib/node-esm/chunk-GEWHMHM7.mjs.map +7 -0
  38. package/dist/lib/node-esm/{chunk-BYHYYJZH.mjs → chunk-KSH4OVMY.mjs} +20 -19
  39. package/dist/lib/node-esm/chunk-KSH4OVMY.mjs.map +7 -0
  40. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs → chunk-QBUGLR4H.mjs} +1 -1
  41. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs.map → chunk-QBUGLR4H.mjs.map} +1 -1
  42. package/dist/lib/node-esm/{chunk-42UNAKYO.mjs → chunk-XGBUKRCK.mjs} +2 -2
  43. package/dist/lib/node-esm/{chunk-6S45OMUP.mjs → chunk-XIGPWXCQ.mjs} +14 -5
  44. package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +7 -0
  45. package/dist/lib/node-esm/{chunk-JNT72ZCN.mjs → chunk-Y7JKXNDU.mjs} +51 -88
  46. package/dist/lib/node-esm/chunk-Y7JKXNDU.mjs.map +7 -0
  47. package/dist/lib/node-esm/cli/index.mjs +5 -5
  48. package/dist/lib/node-esm/common/activation-events.mjs +3 -3
  49. package/dist/lib/node-esm/common/capabilities.mjs +4 -4
  50. package/dist/lib/node-esm/core/plugin-manager.mjs +2 -2
  51. package/dist/lib/node-esm/core/plugin.mjs +3 -1
  52. package/dist/lib/node-esm/core/url-loader.mjs +2 -2
  53. package/dist/lib/node-esm/index.mjs +13 -14
  54. package/dist/lib/node-esm/index.mjs.map +3 -3
  55. package/dist/lib/node-esm/meta.json +1 -1
  56. package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs → process-manager-capability-F23D62RN.mjs} +6 -6
  57. package/dist/lib/node-esm/testing/index.mjs +11 -10
  58. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  59. package/dist/lib/node-esm/testing/react.mjs +6 -6
  60. package/dist/lib/node-esm/ui/index.mjs +7 -9
  61. package/dist/plugin/node-esm/index.mjs +155 -77
  62. package/dist/plugin/node-esm/index.mjs.map +4 -4
  63. package/dist/plugin/node-esm/meta.json +1 -1
  64. package/dist/types/src/common/capabilities.d.ts +1 -1
  65. package/dist/types/src/common/index.d.ts +0 -1
  66. package/dist/types/src/common/index.d.ts.map +1 -1
  67. package/dist/types/src/common/translations.d.ts +1 -1
  68. package/dist/types/src/core/capability-manager.d.ts +4 -0
  69. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  70. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  71. package/dist/types/src/core/plugin.d.ts +33 -6
  72. package/dist/types/src/core/plugin.d.ts.map +1 -1
  73. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts +15 -1
  74. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts.map +1 -1
  75. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts +1 -1
  76. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -1
  77. package/dist/types/src/plugin-process-manager/meta.d.ts +1 -1
  78. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
  79. package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  81. package/dist/types/src/ui/components/App/App.d.ts +34 -2
  82. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  83. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  84. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  85. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/index.d.ts +0 -1
  87. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/hooks/useApp.d.ts +1 -7
  89. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  90. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  91. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts +6 -0
  92. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -1
  93. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +26 -23
  94. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -1
  95. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
  96. package/dist/types/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +40 -35
  98. package/src/common/capabilities.ts +1 -1
  99. package/src/common/index.ts +0 -1
  100. package/src/common/translations.ts +1 -1
  101. package/src/core/capability-manager.test.ts +3 -0
  102. package/src/core/capability-manager.ts +22 -1
  103. package/src/core/plugin-manager.test.ts +51 -21
  104. package/src/core/plugin-manager.ts +10 -5
  105. package/src/core/plugin.ts +44 -8
  106. package/src/core/url-loader.test.ts +2 -1
  107. package/src/plugin-process-manager/history/history-tracker.test.ts +39 -60
  108. package/src/plugin-process-manager/history/history-tracker.ts +32 -16
  109. package/src/plugin-process-manager/history/undo-mapping.ts +1 -1
  110. package/src/plugin-process-manager/history/undo-registry.test.ts +3 -1
  111. package/src/plugin-process-manager/meta.ts +6 -4
  112. package/src/plugin-process-manager/testing.ts +6 -5
  113. package/src/testing/react.test.tsx +3 -1
  114. package/src/testing/withPluginManager.tsx +4 -3
  115. package/src/ui/components/App/App.stories.tsx +9 -11
  116. package/src/ui/components/App/App.tsx +80 -17
  117. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +9 -14
  118. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  119. package/src/ui/components/Surface/SurfaceComponent.tsx +1 -1
  120. package/src/ui/components/Surface/types.test.ts +2 -2
  121. package/src/ui/components/Surface/types.ts +21 -0
  122. package/src/ui/components/index.ts +0 -1
  123. package/src/ui/hooks/useApp.test.tsx +3 -1
  124. package/src/ui/hooks/useApp.tsx +4 -18
  125. package/src/ui/hooks/useCapabilities.ts +1 -0
  126. package/src/ui/hooks/useProcessManagerRuntime.ts +12 -2
  127. package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +198 -0
  128. package/src/vite-plugin/boot-loader/loader-app/Loader.tsx +166 -0
  129. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +311 -0
  130. package/src/vite-plugin/boot-loader/loader-app/bridge.ts +25 -0
  131. package/src/vite-plugin/boot-loader/loader-app/entry.tsx +21 -0
  132. package/src/vite-plugin/boot-loader/loader-app/mount.tsx +70 -0
  133. package/src/vite-plugin/boot-loader/loader-app/store.test.ts +137 -0
  134. package/src/vite-plugin/boot-loader/loader-app/store.ts +149 -0
  135. package/src/vite-plugin/boot-loader/loader-app/tsconfig.json +11 -0
  136. package/src/vite-plugin/boot-loader/loader-app/types.ts +78 -0
  137. package/src/vite-plugin/boot-loader/loader.ts +133 -63
  138. package/src/vite-plugin/manifest.test.ts +19 -10
  139. package/src/vite-plugin/manifest.ts +9 -3
  140. package/tsconfig.json +6 -7
  141. package/dist/lib/browser/chunk-BRK6GYNB.mjs.map +0 -7
  142. package/dist/lib/browser/chunk-KFDF7KR3.mjs +0 -42
  143. package/dist/lib/browser/chunk-KFDF7KR3.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-QLML5QFJ.mjs.map +0 -7
  145. package/dist/lib/browser/chunk-SLX73WRZ.mjs.map +0 -7
  146. package/dist/lib/browser/chunk-UVTGHZQF.mjs.map +0 -7
  147. package/dist/lib/browser/chunk-VJ5PFAWC.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-6S45OMUP.mjs.map +0 -7
  149. package/dist/lib/node-esm/chunk-BYHYYJZH.mjs.map +0 -7
  150. package/dist/lib/node-esm/chunk-JNT72ZCN.mjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-LJNUFNDO.mjs.map +0 -7
  152. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs +0 -43
  153. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-SFYCO3PT.mjs.map +0 -7
  155. package/dist/types/src/common/operations.d.ts +0 -19
  156. package/dist/types/src/common/operations.d.ts.map +0 -1
  157. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +0 -64
  158. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +0 -1
  159. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +0 -19
  160. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +0 -1
  161. package/dist/types/src/ui/components/Placeholder/index.d.ts +0 -2
  162. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +0 -1
  163. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +0 -34
  164. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +0 -1
  165. package/src/common/operations.ts +0 -35
  166. package/src/ui/components/Placeholder/Placeholder.stories.tsx +0 -77
  167. package/src/ui/components/Placeholder/Placeholder.tsx +0 -155
  168. package/src/ui/components/Placeholder/index.ts +0 -5
  169. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +0 -270
  170. package/src/vite-plugin/boot-loader/boot-loader.css +0 -320
  171. package/src/vite-plugin/boot-loader/boot-loader.js +0 -325
  172. /package/dist/lib/browser/{capability-K5XIVCQU.mjs.map → capability-JCUNWZ66.mjs.map} +0 -0
  173. /package/dist/lib/browser/{chunk-KLHQNYJ2.mjs.map → chunk-KZUDO43J.mjs.map} +0 -0
  174. /package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs.map → process-manager-capability-4FUA4TRP.mjs.map} +0 -0
  175. /package/dist/lib/node-esm/{capability-RLKFFLTB.mjs.map → capability-7TCQK7ZP.mjs.map} +0 -0
  176. /package/dist/lib/node-esm/{chunk-42UNAKYO.mjs.map → chunk-XGBUKRCK.mjs.map} +0 -0
  177. /package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs.map → process-manager-capability-F23D62RN.mjs.map} +0 -0
@@ -87,7 +87,7 @@ const DefaultStory = () => {
87
87
  }
88
88
 
89
89
  return (
90
- <div className='flex justify-center items-center border border-rose-fill rounded-sm'>
90
+ <div className='flex justify-center items-center border border-rose-bg rounded-sm'>
91
91
  <span className='font-mono'>Ticking... {count}</span>
92
92
  </div>
93
93
  );
@@ -263,7 +263,7 @@ export const useSurfaces = () => {
263
263
  const result: Definition[] = [];
264
264
  for (const [moduleId, surfaces] of Object.entries(surfacesByModule)) {
265
265
  for (const def of surfaces.flat()) {
266
- result.push({ ...def, id: `${moduleId}.${def.id}` });
266
+ result.push(def);
267
267
  }
268
268
  }
269
269
  return result;
@@ -50,7 +50,7 @@ describe('create', () => {
50
50
  const filter: SurfaceFilter<Record<string, any>> = {
51
51
  bindings: [{ role: 'article', guard: (d) => (d as any).subject === 'ok' }],
52
52
  };
53
- const def = create({ id: 'typed-single', filter, component: () => null });
53
+ const def = create({ id: 'typedSingle', filter, component: () => null });
54
54
  expect(def.role).toBe('article');
55
55
  expect(def.filter!({ subject: 'ok' }, 'article')).toBe(true);
56
56
  expect(def.filter!({ subject: 'no' }, 'article')).toBe(false);
@@ -63,7 +63,7 @@ describe('create', () => {
63
63
  { role: 'section', guard: (d) => (d as any).subject === 's' },
64
64
  ],
65
65
  };
66
- const def = create({ id: 'typed-multi', filter, component: () => null });
66
+ const def = create({ id: 'typedMulti', filter, component: () => null });
67
67
  expect(def.role).toEqual(['article', 'section']);
68
68
  // Role-specific guard.
69
69
  expect(def.filter!({ subject: 'a' }, 'article')).toBe(true);
@@ -230,6 +230,25 @@ const expandBindings = <T extends Record<string, any>>(
230
230
  return { role: roles.length === 1 ? roles[0] : roles, guard };
231
231
  };
232
232
 
233
+ /**
234
+ * Validates that a surface or extension local ID follows NSID conventions:
235
+ * each dot-separated segment must be alphanumeric, and the final segment must
236
+ * be camelCase (no hyphens). This mirrors the rule enforced when the id is
237
+ * appended to a plugin's NSID to form a full DXN path.
238
+ *
239
+ * @example Valid: 'about', 'integrationArticle', 'article.journal'
240
+ * @example Invalid: 'integration-article', 'plugin-spec'
241
+ */
242
+ const validateLocalId = (id: string): void => {
243
+ const segments = id.split('.');
244
+ const finalSegment = segments[segments.length - 1];
245
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(finalSegment)) {
246
+ throw new Error(
247
+ `Invalid surface id: "${id}". The final segment "${finalSegment}" must be camelCase (letters and digits only, starting with a letter — no hyphens or underscores).`,
248
+ );
249
+ }
250
+ };
251
+
233
252
  /**
234
253
  * Creates a React surface definition.
235
254
  */
@@ -240,6 +259,7 @@ export function create<T extends Record<string, any> = any>(
240
259
  export function create<T extends Record<string, any> = any>(
241
260
  definition: TypedReactDefinition<T> | Omit<ReactDefinition<T>, 'kind'>,
242
261
  ): ReactDefinition<T> {
262
+ validateLocalId(definition.id);
243
263
  if (isSurfaceFilter(definition.filter)) {
244
264
  const { id, filter, component, position } = definition as TypedReactDefinition<T>;
245
265
  const { role, guard } = expandBindings(filter);
@@ -260,6 +280,7 @@ export function createWeb<T extends Record<string, any> = any>(
260
280
  export function createWeb<T extends Record<string, any> = any>(
261
281
  definition: TypedWebComponentDefinition<T> | Omit<WebComponentDefinition<T>, 'kind'>,
262
282
  ): WebComponentDefinition<T> {
283
+ validateLocalId(definition.id);
263
284
  if (isSurfaceFilter(definition.filter)) {
264
285
  const { id, filter, tagName, position } = definition as TypedWebComponentDefinition<T>;
265
286
  const { role, guard } = expandBindings(filter);
@@ -3,6 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './App';
6
- export * from './Placeholder';
7
6
  export * from './PluginManager';
8
7
  export * from './Surface';
@@ -7,12 +7,14 @@ import { render, waitFor } from '@testing-library/react';
7
7
  import * as Effect from 'effect/Effect';
8
8
  import React from 'react';
9
9
 
10
+ import { DXN } from '@dxos/keys';
11
+
10
12
  import { ActivationEvents, Capabilities } from '../../common';
11
13
  import { Capability, Plugin, PluginManager } from '../../core';
12
14
  import { useApp } from './useApp';
13
15
 
14
16
  const String = Capability.make<{ string: string }>('org.dxos.test.string');
15
- const testMeta = { id: 'org.dxos.plugin.test', name: 'Test', tags: ['system'] };
17
+ const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test'), name: 'Test', tags: ['system'] });
16
18
 
17
19
  const pluginLoader = (plugins: Plugin.Plugin[]) =>
18
20
  Effect.fn(function* (id: string) {
@@ -59,11 +59,6 @@ export type StartupProgress = {
59
59
  humanizedName?: string;
60
60
  };
61
61
 
62
- export type PlaceholderProps = {
63
- stage?: number;
64
- progress?: StartupProgress;
65
- };
66
-
67
62
  export type UseAppOptions = {
68
63
  pluginManager?: PluginManager.PluginManager;
69
64
  pluginLoader?: PluginManager.ManagerOptions['pluginLoader'];
@@ -81,7 +76,6 @@ export type UseAppOptions = {
81
76
  debounce?: number;
82
77
  timeout?: number;
83
78
  fallback?: FC<FallbackProps>;
84
- placeholder?: FC<PlaceholderProps>;
85
79
  };
86
80
 
87
81
  /**
@@ -105,7 +99,6 @@ export type UseAppOptions = {
105
99
  * @param params.cacheEnabled Whether to cache enabled plugins in localStorage.
106
100
  * @param params.safeMode Whether to enable safe mode, which disables optional plugins.
107
101
  * @param params.fallback Fallback component to render if an error occurs during startup.
108
- * @param params.placeholder Placeholder component to render during startup.
109
102
  */
110
103
  export const useApp = ({
111
104
  pluginManager,
@@ -115,7 +108,6 @@ export const useApp = ({
115
108
  plugins: pluginsProp,
116
109
  defaults: defaultsProp,
117
110
  setupEvents: setupEventsProp,
118
- placeholder,
119
111
  fallback = ErrorFallback,
120
112
  cacheEnabled = false,
121
113
  safeMode = false,
@@ -211,7 +203,7 @@ export const useApp = ({
211
203
  // the `!module` guard the listener would mark the app ready on
212
204
  // the *first* such module rather than waiting for the event-level
213
205
  // completion — leaving downstream capabilities (operation-invoker,
214
- // app-graph, …) un-registered when the placeholder dismisses.
206
+ // app-graph, …) un-registered when the boot loader dismisses.
215
207
  if (event === ActivationEvents.Startup.id && state === 'activated' && !module) {
216
208
  clearTimeout(timeoutId);
217
209
  setReady(true);
@@ -229,7 +221,7 @@ export const useApp = ({
229
221
  }
230
222
  // `activating` is the start-of-load signal. Surface the raw
231
223
  // `module` (or `event`) plus a pre-humanized label so the
232
- // host placeholder can decide what to render — show
224
+ // boot loader can decide how to render each transition — show
233
225
  // everything, suppress noisy sub-modules, group by plugin,
234
226
  // or apply its own formatting. We intentionally do NOT touch
235
227
  // these fields on `activated`: pairing the two would cause
@@ -331,19 +323,13 @@ export const useApp = ({
331
323
  <PluginManagerProvider value={manager}>
332
324
  <ContextProtocolProvider value={manager} context={PluginManagerContext}>
333
325
  <RegistryContext.Provider value={manager.registry}>
334
- <App
335
- placeholder={placeholder}
336
- ready={ready}
337
- error={error}
338
- debounce={debounce}
339
- progress={progressRef.current}
340
- />
326
+ <App ready={ready} error={error} debounce={debounce} progress={progressRef.current} />
341
327
  </RegistryContext.Provider>
342
328
  </ContextProtocolProvider>
343
329
  </PluginManagerProvider>
344
330
  </ErrorBoundary>
345
331
  ),
346
- [fallback, manager, placeholder, ready, error],
332
+ [fallback, manager, ready, error],
347
333
  );
348
334
  };
349
335
 
@@ -25,6 +25,7 @@ export const useCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>) =>
25
25
  * @returns The capability.
26
26
  * @throws If no capability is found.
27
27
  */
28
+ // TODO(burdon): Option not to throw?
28
29
  export const useCapability = <T>(interfaceDef: Capability.InterfaceDef<T>) => {
29
30
  const capabilities = useCapabilities(interfaceDef);
30
31
  invariant(capabilities.length > 0, `No capability found for ${interfaceDef.identifier}`);
@@ -4,9 +4,10 @@
4
4
 
5
5
  import * as Context from 'effect/Context';
6
6
  import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
7
8
  import { type DependencyList, use, useCallback, useMemo } from 'react';
8
9
 
9
- import { ServiceResolver } from '@dxos/compute';
10
+ import { Operation, ServiceResolver } from '@dxos/compute';
10
11
  import { unwrapExit } from '@dxos/effect';
11
12
  import type { SpaceId } from '@dxos/keys';
12
13
 
@@ -26,6 +27,12 @@ export const useProcessManagerRuntime = (): Capabilities.ProcessManagerRuntime =
26
27
  * The `tags` tuple must list every service the effect requires (beyond the
27
28
  * fixed {@link Capabilities.ProcessManagerRuntimeServices}); these services are
28
29
  * resolved for the given `spaceId` through the runtime's service resolver.
30
+ *
31
+ * Nested `Operation.invoke` / `Operation.schedule` calls within the effect
32
+ * inherit `spaceId` as a default {@link Operation.InvokeOptions} so that
33
+ * spawned operation processes have a space context (and therefore can resolve
34
+ * space-scoped services like `Database.Service`) without each call site having
35
+ * to thread the id through manually.
29
36
  */
30
37
  export const useSpaceCallback = <const Tags extends readonly Context.Tag<any, any>[], T>(
31
38
  spaceId: SpaceId | undefined,
@@ -38,7 +45,10 @@ export const useSpaceCallback = <const Tags extends readonly Context.Tag<any, an
38
45
  if (spaceId === undefined) {
39
46
  throw new TypeError('Space not provided to useSpaceCallback');
40
47
  }
41
- const layer = ServiceResolver.provide({ space: spaceId }, ...tags);
48
+ const layer = Layer.merge(
49
+ ServiceResolver.provide({ space: spaceId }, ...tags),
50
+ Operation.withInvocationOptions({ spaceId }),
51
+ );
42
52
  return runtime.runPromise(fn().pipe(Effect.provide(layer)) as Effect.Effect<T, any, any>);
43
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
44
54
  }, [runtime, spaceId, ...(deps ?? [])]);
@@ -0,0 +1,198 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createSignal, onCleanup, onMount } from 'solid-js';
6
+ import { type Meta, type StoryObj } from 'storybook-solidjs-vite';
7
+
8
+ import bootLoaderCss from './loader-app/boot-loader.css?raw';
9
+ import { mountLoader } from './loader-app/mount';
10
+
11
+ //
12
+ // Development surface for the boot loader. Mounts the *real* Solid `Loader`
13
+ // (via `mountLoader` — the same entry the inlined production bundle runs) into a
14
+ // `#boot-loader` backdrop, then drives it through `window.__bootLoader` exactly
15
+ // as the host app does. There is no reconstruction of the loader DOM, so the
16
+ // story and production cannot drift. Iterate the loader's look here with HMR.
17
+ //
18
+
19
+ /**
20
+ * Inlined snapshot of `packages/ui/brand/assets/icons/composer-icon.svg` — keeps
21
+ * the story self-contained (production pipes the same file in via the plugin's
22
+ * `markSvg` option) while previewing the real brand palette.
23
+ */
24
+ const COMPOSER_MARK = `
25
+ <svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
26
+ <g transform="matrix(0.969697,0,0,1,-570.182,0)">
27
+ <rect x="588" y="0" width="264" height="256" style="fill:none;"/>
28
+ <g transform="matrix(0.917198,0,0,1,-223.93,-876)">
29
+ <path d="M1065.83,1064L1029.14,1064C991.913,1064 961.684,1037.12 961.684,1004C961.684,971.197 991.282,944.542 1028.02,944.008L1028.02,944L1047.85,944L1039.21,968L1029.14,968C1006.79,968 988.669,984.118 988.669,1004C988.669,1023.87 1006.81,1040 1029.14,1040C1029.38,1040 1029.62,1040 1029.85,1040L1029.85,1040L1074.47,1040L1065.83,1064ZM1083.11,1040L1083.11,1040L1083.11,1064L1083.11,1064L1083.11,1040Z" style="fill:rgb(1,122,183);"/>
30
+ </g>
31
+ <path d="M761.579,164L720,164C699.51,164 682.875,147.869 682.875,128C682.875,108.452 698.942,92.543 718.969,92.014L718.969,92L729.238,92L721.317,116L720,116C713.165,116 707.625,121.373 707.625,128C707.625,134.623 713.17,140 720,140C720.072,140 720.144,139.999 720.216,139.998L720.216,140L769.5,140L761.579,164Z" style="fill:rgb(6,197,253);"/>
32
+ <path d="M745.738,212L720.025,212L720,212C672.202,212 633.389,174.377 633.375,128.024C633.361,81.958 671.591,44.542 718.969,44.006L718.969,44L719.975,44L745.079,44L737.159,68L719.982,68C685.809,68.01 658.115,94.88 658.125,128.017C658.135,161.126 685.858,188 720,188L720.018,188C720.378,188 720.738,187.997 721.098,187.991L721.098,188L753.659,188L745.738,212Z" style="fill:rgb(10,75,105);"/>
33
+ <g transform="matrix(1.03125,0,0,1,588,0)">
34
+ <path d="M128,236C68.393,236 20,187.607 20,128C20,68.353 68.353,20 128,20L160,20L152.319,44L128,44C81.608,44 44,81.608 44,128C44,174.361 81.639,212 128,212C127.756,212.004 127.878,212.004 128,212.003C128.122,212.002 128.244,212 128,212L152.958,212L145.277,236L128,236ZM128,236C128.628,236 127.372,236.011 128,236Z" style="fill:rgb(5,40,61);"/>
35
+ </g>
36
+ </g>
37
+ </svg>
38
+ `;
39
+
40
+ /** Approximate plugin count for the simulation (production count is dynamic). */
41
+ const STORY_PLUGIN_COUNT = 80;
42
+ /** Tick interval for the determinate-progress simulation, in ms. */
43
+ const STORY_TICK_MS = 50;
44
+
45
+ type SimState = 'creep' | 'progress' | 'done';
46
+
47
+ /**
48
+ * Harness component: owns the `#boot-loader` backdrop, mounts the real loader,
49
+ * and emits `__bootLoader` calls to simulate the host. The driver is the source
50
+ * of truth — there is no React/Solid shadow of its progress.
51
+ */
52
+ const LoaderHarness = () => {
53
+ let container!: HTMLDivElement;
54
+ let styleEl: HTMLStyleElement | undefined;
55
+ let dispose: (() => void) | undefined;
56
+ let timer: ReturnType<typeof setInterval> | undefined;
57
+ const [sim, setSim] = createSignal<SimState>('creep');
58
+
59
+ const teardown = () => {
60
+ if (timer != null) {
61
+ clearInterval(timer);
62
+ timer = undefined;
63
+ }
64
+ dispose?.();
65
+ dispose = undefined;
66
+ };
67
+
68
+ const mount = () => {
69
+ teardown();
70
+ // The loader treats its host element as the `#boot-loader` backdrop (styled
71
+ // by the inline CSS) and renders the disc + status into it — mirroring the
72
+ // static markup the plugin injects in production.
73
+ const backdrop = document.createElement('div');
74
+ backdrop.id = 'boot-loader';
75
+ backdrop.setAttribute('role', 'status');
76
+ backdrop.setAttribute('aria-live', 'polite');
77
+ backdrop.setAttribute('aria-label', 'Initializing');
78
+ container.appendChild(backdrop);
79
+ dispose = mountLoader(backdrop, { markSvg: COMPOSER_MARK, status: 'Loading…' });
80
+ };
81
+
82
+ const runCreepMessages = () => {
83
+ const phases = ['Loading framework…', 'Reading configuration…', 'Starting services…'];
84
+ let index = 0;
85
+ timer = setInterval(() => {
86
+ window.__bootLoader?.status({ humanized: phases[index] });
87
+ index += 1;
88
+ if (index >= phases.length && timer != null) {
89
+ clearInterval(timer);
90
+ timer = undefined;
91
+ }
92
+ }, 600);
93
+ };
94
+
95
+ const runProgress = () => {
96
+ let loaded = 0;
97
+ timer = setInterval(() => {
98
+ loaded += Math.min(1.5, 0.4 + (loaded % 3) * 0.3);
99
+ const fraction = Math.min(1, loaded / STORY_PLUGIN_COUNT);
100
+ window.__bootLoader?.progress(fraction);
101
+ window.__bootLoader?.status({
102
+ humanized: 'Loading plugins',
103
+ range: { index: Math.round(loaded), total: STORY_PLUGIN_COUNT },
104
+ });
105
+ if (loaded >= STORY_PLUGIN_COUNT && timer != null) {
106
+ clearInterval(timer);
107
+ timer = undefined;
108
+ window.__bootLoader?.status({ humanized: 'Starting Composer…' });
109
+ setSim('done');
110
+ }
111
+ }, STORY_TICK_MS);
112
+ };
113
+
114
+ const advance = () => {
115
+ if (sim() === 'creep') {
116
+ setSim('progress');
117
+ if (timer != null) {
118
+ clearInterval(timer);
119
+ timer = undefined;
120
+ }
121
+ runProgress();
122
+ } else if (sim() === 'progress' || sim() === 'done') {
123
+ // Play the dismissal outro, then re-mount for another run.
124
+ window.__bootLoader?.ready();
125
+ setSim('creep');
126
+ setTimeout(() => {
127
+ mount();
128
+ runCreepMessages();
129
+ }, 700);
130
+ }
131
+ };
132
+
133
+ onMount(() => {
134
+ styleEl = document.createElement('style');
135
+ styleEl.textContent = bootLoaderCss;
136
+ document.head.appendChild(styleEl);
137
+ mount();
138
+ runCreepMessages();
139
+ });
140
+
141
+ onCleanup(() => {
142
+ teardown();
143
+ styleEl?.remove();
144
+ });
145
+
146
+ return (
147
+ <>
148
+ {/* Float the controls above the loader's `z-index: 10` fixed backdrop. */}
149
+ <div
150
+ style={{
151
+ position: 'fixed',
152
+ top: '12px',
153
+ left: '12px',
154
+ 'z-index': 9999,
155
+ display: 'flex',
156
+ gap: '12px',
157
+ 'align-items': 'center',
158
+ }}
159
+ >
160
+ <button
161
+ type='button'
162
+ onClick={advance}
163
+ style={{
164
+ padding: '6px 14px',
165
+ 'font-size': '13px',
166
+ 'font-weight': '600',
167
+ color: '#fff',
168
+ background: '#017ab7',
169
+ border: 'none',
170
+ 'border-radius': '6px',
171
+ cursor: 'pointer',
172
+ 'box-shadow': '0 1px 3px rgba(0,0,0,0.25)',
173
+ }}
174
+ >
175
+ {sim() === 'creep' ? 'Start progress' : 'Finish + reset'}
176
+ </button>
177
+ <span style={{ 'font-family': 'ui-sans-serif, system-ui, sans-serif', 'font-size': '12px', color: '#888' }}>
178
+ {sim()}
179
+ </span>
180
+ </div>
181
+ <div ref={container as any} />
182
+ </>
183
+ );
184
+ };
185
+
186
+ const meta: Meta = {
187
+ title: 'sdk/app-framework/vite-plugin/BootLoader',
188
+ render: () => <LoaderHarness />,
189
+ parameters: {
190
+ layout: 'fullscreen',
191
+ },
192
+ };
193
+
194
+ export default meta;
195
+
196
+ type Story = StoryObj;
197
+
198
+ export const Default: Story = {};
@@ -0,0 +1,166 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Component, For, createEffect, createSignal, onCleanup, onMount } from 'solid-js';
6
+
7
+ import { type LoaderStore } from './store';
8
+
9
+ // Ring geometry in the SVG's `0 0 100 100` viewBox. The radius leaves a couple
10
+ // of units of padding so the stroke, its round cap, and the end marker aren't
11
+ // clipped at the viewBox edge.
12
+ const RING_RADIUS = 48;
13
+ const RING_CENTER = 50;
14
+ // Leading-edge marker: a small dot drawn at the arc's head in an unmasked layer.
15
+ const MARKER_RADIUS = 1; // viewBox units → ~3.8px on the 384px disc
16
+
17
+ /**
18
+ * Read an element's *current animated* translateY (px) from its live transform
19
+ * matrix — the interpolated value mid-transition, not the last-written property.
20
+ * Used by the status FLIP so successive appends chain off the in-flight position
21
+ * instead of yanking the track back to a fixed invert target.
22
+ */
23
+ const readTranslateY = (element: HTMLElement): number => {
24
+ const computed = getComputedStyle(element).transform;
25
+ if (!computed || computed === 'none') {
26
+ return 0;
27
+ }
28
+ const match = computed.match(/matrix.*\(([^)]+)\)/);
29
+ if (!match) {
30
+ return 0;
31
+ }
32
+ const values = match[1].split(',');
33
+ // 2D `matrix(a, b, c, d, tx, ty)` → ty at index 5; 3D `matrix3d(...)` → ty at 13.
34
+ if (values.length === 6) {
35
+ return Number.parseFloat(values[5]) || 0;
36
+ }
37
+ if (values.length === 16) {
38
+ return Number.parseFloat(values[13]) || 0;
39
+ }
40
+ return 0;
41
+ };
42
+
43
+ export type LoaderProps = {
44
+ /** Reactive source of truth for progress, status lines, and lifecycle phase. */
45
+ store: LoaderStore;
46
+ /** Inline SVG markup for the brand mark rendered inside the ring. */
47
+ markSvg?: string;
48
+ };
49
+
50
+ /**
51
+ * The boot loader, authored as a single Solid component — the one source of
52
+ * truth for the loader DOM. `bootLoaderPlugin` bundles this (Solid runtime
53
+ * inlined) into `index.html`; the storybook mounts the very same component, so
54
+ * the two can no longer drift. The DOM structure, ids, and classes mirror
55
+ * `boot-loader.css`.
56
+ *
57
+ * The full-screen backdrop (`#boot-loader`) is injected as static markup so it
58
+ * paints from CSS before this bundle executes; this component renders the disc
59
+ * and status log *into* that backdrop. The dismissal outro (fading `#boot-loader`)
60
+ * and its teardown are owned by {@link mountLoader}, which has the host element.
61
+ */
62
+ export const Loader: Component<LoaderProps> = (props) => {
63
+ let trackRef: HTMLDivElement | undefined;
64
+ let previousCount = props.store.lines().length;
65
+
66
+ // FLIP-style slide on each appended line: snap the track down by one
67
+ // line-height (chained off any in-flight translate) with no transition, force
68
+ // a reflow, then animate back to translateY(0) so the new entry rises from
69
+ // below the bottom-anchored viewport. Range ticks (length unchanged) skip it.
70
+ createEffect(() => {
71
+ const count = props.store.lines().length;
72
+ const track = trackRef;
73
+ if (track && count > previousCount) {
74
+ const currentY = readTranslateY(track);
75
+ const lineHeight = track.lastElementChild?.getBoundingClientRect().height ?? 0;
76
+ track.style.transition = 'none';
77
+ track.style.transform = `translateY(${currentY + lineHeight}px)`;
78
+ void track.offsetHeight;
79
+ track.style.transition = '';
80
+ track.style.transform = 'translateY(0)';
81
+ }
82
+ previousCount = count;
83
+ });
84
+
85
+ // Eased display progress. A `<path>`'s `d` can't be CSS-transitioned, so the
86
+ // arc would snap on every store tick; instead we ease a displayed value toward
87
+ // the store's progress with a requestAnimationFrame loop (~per-frame lerp) and
88
+ // recompute the path each frame. This restores the compositor-smooth growth
89
+ // the old `transition: stroke-dashoffset` gave, while keeping the path +
90
+ // `marker-end` (the marker rides the arc's recomputed end automatically).
91
+ const [shown, setShown] = createSignal(props.store.progress());
92
+ let raf: number | undefined;
93
+ const animate = () => {
94
+ const target = props.store.progress();
95
+ const current = shown();
96
+ // Snap when within a hair to settle; otherwise lerp ~18% of the gap/frame.
97
+ const next = Math.abs(target - current) < 0.05 ? target : current + (target - current) * 0.18;
98
+ if (next !== current) {
99
+ setShown(next);
100
+ }
101
+ raf = requestAnimationFrame(animate);
102
+ };
103
+
104
+ onMount(() => {
105
+ raf = requestAnimationFrame(animate);
106
+ });
107
+
108
+ onCleanup(() => {
109
+ if (raf != null) {
110
+ cancelAnimationFrame(raf);
111
+ }
112
+ });
113
+
114
+ // Determinate ring as an SVG `<path>` arc: an anticlockwise sweep starting at
115
+ // 12 o'clock whose angle grows with the eased `shown()` progress. The sweep is
116
+ // capped just shy of a full turn so the single arc command never degenerates at
117
+ // 100%. The leading-edge dot is its `head` point, rendered separately (below) in
118
+ // an *unmasked* layer — drawing it via `marker-end` on this path would put it
119
+ // under the ring's conic fade mask, whose hard edge bisects the dot into a
120
+ // half-circle.
121
+ const arc = () => {
122
+ const fraction = Math.min(shown() / 100, 0.9999);
123
+ const sweep = fraction * 2 * Math.PI;
124
+ const start = -Math.PI / 2; // 12 o'clock
125
+ const end = start - sweep; // anticlockwise (decreasing angle)
126
+ const x0 = RING_CENTER + RING_RADIUS * Math.cos(start);
127
+ const y0 = RING_CENTER + RING_RADIUS * Math.sin(start);
128
+ const headX = RING_CENTER + RING_RADIUS * Math.cos(end);
129
+ const headY = RING_CENTER + RING_RADIUS * Math.sin(end);
130
+ const largeArc = sweep > Math.PI ? 1 : 0;
131
+ // sweep-flag 0 = anticlockwise (negative-angle) direction.
132
+ return { d: `M ${x0} ${y0} A ${RING_RADIUS} ${RING_RADIUS} 0 ${largeArc} 0 ${headX} ${headY}`, headX, headY };
133
+ };
134
+
135
+ // Once host-driven, the brand mark eases grayscale → colour and stays there.
136
+ const isHostDriven = () => props.store.phase() !== 'creep';
137
+
138
+ return (
139
+ <>
140
+ <div id='boot-loader-disc' data-host-driven={isHostDriven() ? '' : undefined}>
141
+ <svg
142
+ id='boot-loader-ring'
143
+ viewBox='0 0 100 100'
144
+ aria-hidden='true'
145
+ style={{ '--boot-loader-arc': String(shown()) }}
146
+ >
147
+ {shown() > 0 ? <path class='boot-loader-ring-progress' d={arc().d} /> : null}
148
+ </svg>
149
+ {/* Leading-edge dot in its own unmasked layer (see `arc()`), so the ring's fade mask
150
+ never clips it. Stacked over `#boot-loader-ring` via the disc grid. */}
151
+ {shown() > 0 ? (
152
+ <svg id='boot-loader-ring-head' viewBox='0 0 100 100' aria-hidden='true'>
153
+ <circle class='boot-loader-ring-marker' cx={arc().headX} cy={arc().headY} r={MARKER_RADIUS} />
154
+ </svg>
155
+ ) : null}
156
+ {props.markSvg ? <div id='boot-loader-mark' innerHTML={props.markSvg} /> : null}
157
+ </div>
158
+ <div id='boot-loader-status'>
159
+ <div id='boot-loader-status-fade' />
160
+ <div id='boot-loader-status-track' ref={trackRef}>
161
+ <For each={props.store.lines()}>{(line) => <div class='boot-loader-status-line'>{line.text}</div>}</For>
162
+ </div>
163
+ </div>
164
+ </>
165
+ );
166
+ };