@dxos/app-framework 0.9.0 → 0.9.1-staging.ee54ba693a

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 (224) hide show
  1. package/dist/lib/browser/{capability-BOPAKKWG.mjs → capability-S44TE2H7.mjs} +9 -9
  2. package/dist/lib/browser/{chunk-66IXTIVK.mjs → chunk-3NQLTQCP.mjs} +6 -7
  3. package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +7 -0
  4. package/dist/lib/browser/chunk-53HJHMS5.mjs +7 -0
  5. package/dist/lib/browser/chunk-53HJHMS5.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-Y66ELD4U.mjs → chunk-KRJ4KK2W.mjs} +36 -7
  7. package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-KZUDO43J.mjs → chunk-LUH4Y5F2.mjs} +31 -23
  9. package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-CO3XLNUX.mjs → chunk-NKVRSMFN.mjs} +18 -18
  11. package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +7 -0
  12. package/dist/lib/browser/{chunk-DC3WRPBV.mjs → chunk-OO53M5UK.mjs} +45 -12
  13. package/dist/lib/browser/chunk-OO53M5UK.mjs.map +7 -0
  14. package/dist/lib/browser/{chunk-IW44C7UL.mjs → chunk-OPTS3ZSN.mjs} +1 -1
  15. package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +7 -0
  16. package/dist/lib/browser/{chunk-HFLKXMT7.mjs → chunk-U5MESPM5.mjs} +61 -45
  17. package/dist/lib/browser/chunk-U5MESPM5.mjs.map +7 -0
  18. package/dist/lib/browser/{chunk-XHS5KDNX.mjs → chunk-UGYCLOXE.mjs} +25 -25
  19. package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +7 -0
  20. package/dist/lib/browser/{chunk-5GY3YOEL.mjs → chunk-VF37YTXJ.mjs} +5 -5
  21. package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +7 -0
  22. package/dist/lib/browser/{chunk-FO3IYSLV.mjs → chunk-YUVQIOTB.mjs} +29 -37
  23. package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +7 -0
  24. package/dist/lib/browser/cli/index.mjs +9 -9
  25. package/dist/lib/browser/cli/index.mjs.map +2 -2
  26. package/dist/lib/browser/common/activation-events.mjs +7 -7
  27. package/dist/lib/browser/common/capabilities.mjs +7 -7
  28. package/dist/lib/browser/config/index.mjs +8 -0
  29. package/dist/lib/browser/config/index.mjs.map +7 -0
  30. package/dist/lib/browser/core/activation-event.mjs +1 -1
  31. package/dist/lib/browser/core/capability.mjs +1 -1
  32. package/dist/lib/browser/core/plugin-manager.mjs +4 -4
  33. package/dist/lib/browser/core/plugin.mjs +12 -2
  34. package/dist/lib/browser/core/url-loader.mjs +3 -3
  35. package/dist/lib/browser/index.mjs +14 -10
  36. package/dist/lib/browser/index.mjs.map +1 -1
  37. package/dist/lib/browser/meta.json +1 -1
  38. package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs → process-manager-capability-QK4BMKMN.mjs} +9 -9
  39. package/dist/lib/browser/testing/index.mjs +15 -15
  40. package/dist/lib/browser/testing/index.mjs.map +3 -3
  41. package/dist/lib/browser/testing/react.mjs +11 -11
  42. package/dist/lib/browser/testing/react.mjs.map +3 -3
  43. package/dist/lib/browser/ui/index.mjs +18 -10
  44. package/dist/lib/node-esm/{capability-KZWRTX7R.mjs → capability-3YRF77LV.mjs} +9 -9
  45. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +9 -0
  46. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +7 -0
  47. package/dist/lib/node-esm/{chunk-XIGPWXCQ.mjs → chunk-4SE7SX7N.mjs} +45 -12
  48. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +7 -0
  49. package/dist/lib/node-esm/{chunk-XOCUANHO.mjs → chunk-BMW6P6QA.mjs} +6 -7
  50. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +7 -0
  51. package/dist/lib/node-esm/{chunk-A5CHGAZE.mjs → chunk-DRT75WGD.mjs} +61 -45
  52. package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +7 -0
  53. package/dist/lib/node-esm/{chunk-XGBUKRCK.mjs → chunk-FKMVORZN.mjs} +31 -23
  54. package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +7 -0
  55. package/dist/lib/node-esm/{chunk-CTKEZHKF.mjs → chunk-GLH4LGHZ.mjs} +1 -1
  56. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-SX5NVETP.mjs → chunk-IL7O7IRX.mjs} +25 -25
  58. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +7 -0
  59. package/dist/lib/node-esm/{chunk-FSVGKTF6.mjs → chunk-ND2HETHD.mjs} +36 -7
  60. package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +7 -0
  61. package/dist/lib/node-esm/{chunk-KFZEB6BV.mjs → chunk-TLKYZKPW.mjs} +5 -5
  62. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +7 -0
  63. package/dist/lib/node-esm/{chunk-WK7OIQKI.mjs → chunk-VCPGX7WN.mjs} +29 -37
  64. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +7 -0
  65. package/dist/lib/node-esm/{chunk-NULBHQYS.mjs → chunk-XSZMX53L.mjs} +18 -18
  66. package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +7 -0
  67. package/dist/lib/node-esm/cli/index.mjs +9 -9
  68. package/dist/lib/node-esm/cli/index.mjs.map +2 -2
  69. package/dist/lib/node-esm/common/activation-events.mjs +7 -7
  70. package/dist/lib/node-esm/common/capabilities.mjs +7 -7
  71. package/dist/lib/node-esm/config/index.mjs +9 -0
  72. package/dist/lib/node-esm/config/index.mjs.map +7 -0
  73. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  74. package/dist/lib/node-esm/core/capability.mjs +1 -1
  75. package/dist/lib/node-esm/core/plugin-manager.mjs +4 -4
  76. package/dist/lib/node-esm/core/plugin.mjs +12 -2
  77. package/dist/lib/node-esm/core/url-loader.mjs +3 -3
  78. package/dist/lib/node-esm/index.mjs +14 -10
  79. package/dist/lib/node-esm/index.mjs.map +1 -1
  80. package/dist/lib/node-esm/meta.json +1 -1
  81. package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs → process-manager-capability-5S7WZ2JR.mjs} +9 -9
  82. package/dist/lib/node-esm/testing/index.mjs +15 -15
  83. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  84. package/dist/lib/node-esm/testing/react.mjs +11 -11
  85. package/dist/lib/node-esm/testing/react.mjs.map +3 -3
  86. package/dist/lib/node-esm/ui/index.mjs +18 -10
  87. package/dist/plugin/node-esm/index.mjs +414 -81
  88. package/dist/plugin/node-esm/index.mjs.map +4 -4
  89. package/dist/plugin/node-esm/meta.json +1 -1
  90. package/dist/types/src/cli/cli.d.ts +1 -1
  91. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  92. package/dist/types/src/common/capabilities.d.ts +2 -2
  93. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  94. package/dist/types/src/config/index.d.ts +2 -0
  95. package/dist/types/src/config/index.d.ts.map +1 -0
  96. package/dist/types/src/core/activation-event.d.ts +9 -6
  97. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  98. package/dist/types/src/core/capability.d.ts +9 -3
  99. package/dist/types/src/core/capability.d.ts.map +1 -1
  100. package/dist/types/src/core/edge-registry-plugin-provider.d.ts +6 -11
  101. package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -1
  102. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  103. package/dist/types/src/core/plugin-manifest.d.ts +25 -7
  104. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -1
  105. package/dist/types/src/core/plugin.d.ts +89 -91
  106. package/dist/types/src/core/plugin.d.ts.map +1 -1
  107. package/dist/types/src/core/registry.d.ts +17 -57
  108. package/dist/types/src/core/registry.d.ts.map +1 -1
  109. package/dist/types/src/core/url-loader.d.ts +2 -2
  110. package/dist/types/src/core/url-loader.d.ts.map +1 -1
  111. package/dist/types/src/index.d.ts +1 -0
  112. package/dist/types/src/index.d.ts.map +1 -1
  113. package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -3
  114. package/dist/types/src/plugin-process-manager/history/capability.d.ts.map +1 -1
  115. package/dist/types/src/plugin-process-manager/meta.d.ts +32 -2
  116. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
  117. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -4
  118. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  119. package/dist/types/src/testing/harness.d.ts +1 -1
  120. package/dist/types/src/testing/harness.d.ts.map +1 -1
  121. package/dist/types/src/testing/react.d.ts +6 -5
  122. package/dist/types/src/testing/react.d.ts.map +1 -1
  123. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  124. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  125. package/dist/types/src/ui/components/App/App.stories.d.ts +2 -2
  126. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  127. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts +6 -0
  128. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts.map +1 -1
  129. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +6 -10
  130. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  131. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  132. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts +1 -1
  133. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  134. package/dist/types/src/ui/components/Surface/context.d.ts +6 -2
  135. package/dist/types/src/ui/components/Surface/context.d.ts.map +1 -1
  136. package/dist/types/src/ui/components/Surface/index.d.ts +11 -4
  137. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  138. package/dist/types/src/ui/components/Surface/types.d.ts +52 -72
  139. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  140. package/dist/types/src/ui/hooks/useApp.d.ts +1 -1
  141. package/dist/types/src/ui/hooks/useCapabilities.d.ts +17 -1
  142. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  143. package/dist/types/src/vite-plugin/composer/index.d.ts +11 -4
  144. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -1
  145. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -1
  146. package/dist/types/src/vite-plugin/index.d.ts +1 -0
  147. package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
  148. package/dist/types/src/vite-plugin/load.d.ts +16 -0
  149. package/dist/types/src/vite-plugin/load.d.ts.map +1 -0
  150. package/dist/types/src/vite-plugin/manifest.d.ts +16 -4
  151. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
  152. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  153. package/dist/types/tsconfig.tsbuildinfo +1 -1
  154. package/docs/registry-spec.md +331 -0
  155. package/moon.yml +1 -0
  156. package/package.json +50 -39
  157. package/src/cli/cli.ts +2 -2
  158. package/src/common/activation-events.ts +3 -3
  159. package/src/common/capabilities.ts +18 -18
  160. package/src/config/index.ts +5 -0
  161. package/src/core/activation-event.ts +15 -8
  162. package/src/core/capability.ts +18 -6
  163. package/src/core/edge-registry-plugin-provider.ts +41 -42
  164. package/src/core/plugin-manager.test.ts +133 -137
  165. package/src/core/plugin-manager.ts +25 -19
  166. package/src/core/plugin-manifest.test.ts +5 -5
  167. package/src/core/plugin-manifest.ts +13 -10
  168. package/src/core/plugin.ts +58 -114
  169. package/src/core/registry.ts +20 -60
  170. package/src/core/url-loader.test.ts +2 -2
  171. package/src/core/url-loader.ts +23 -19
  172. package/src/index.ts +1 -0
  173. package/src/testing/harness.ts +5 -3
  174. package/src/testing/react.test.tsx +4 -2
  175. package/src/testing/react.tsx +10 -9
  176. package/src/testing/service.ts +2 -2
  177. package/src/testing/withPluginManager.stories.tsx +4 -2
  178. package/src/testing/withPluginManager.tsx +3 -1
  179. package/src/ui/components/App/App.stories.tsx +2 -2
  180. package/src/ui/components/PluginManager/PluginManagerProvider.ts +7 -0
  181. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +22 -22
  182. package/src/ui/components/Surface/SurfaceComponent.tsx +90 -79
  183. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +1 -1
  184. package/src/ui/components/Surface/context.ts +6 -4
  185. package/src/ui/components/Surface/index.ts +14 -3
  186. package/src/ui/components/Surface/types.test.ts +43 -51
  187. package/src/ui/components/Surface/types.ts +99 -64
  188. package/src/ui/hooks/useApp.test.tsx +1 -1
  189. package/src/ui/hooks/useApp.tsx +2 -2
  190. package/src/ui/hooks/useCapabilities.ts +49 -2
  191. package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +11 -11
  192. package/src/vite-plugin/boot-loader/loader.ts +3 -3
  193. package/src/vite-plugin/composer/index.ts +134 -61
  194. package/src/vite-plugin/import-map/index.ts +23 -7
  195. package/src/vite-plugin/index.ts +1 -0
  196. package/src/vite-plugin/load.ts +57 -0
  197. package/src/vite-plugin/manifest.test.ts +3 -3
  198. package/src/vite-plugin/manifest.ts +21 -10
  199. package/src/vite-plugin/packages.ts +0 -2
  200. package/tsconfig.json +5 -2
  201. package/dist/lib/browser/chunk-5GY3YOEL.mjs.map +0 -7
  202. package/dist/lib/browser/chunk-66IXTIVK.mjs.map +0 -7
  203. package/dist/lib/browser/chunk-CO3XLNUX.mjs.map +0 -7
  204. package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +0 -7
  205. package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +0 -7
  206. package/dist/lib/browser/chunk-HFLKXMT7.mjs.map +0 -7
  207. package/dist/lib/browser/chunk-IW44C7UL.mjs.map +0 -7
  208. package/dist/lib/browser/chunk-KZUDO43J.mjs.map +0 -7
  209. package/dist/lib/browser/chunk-XHS5KDNX.mjs.map +0 -7
  210. package/dist/lib/browser/chunk-Y66ELD4U.mjs.map +0 -7
  211. package/dist/lib/node-esm/chunk-A5CHGAZE.mjs.map +0 -7
  212. package/dist/lib/node-esm/chunk-CTKEZHKF.mjs.map +0 -7
  213. package/dist/lib/node-esm/chunk-FSVGKTF6.mjs.map +0 -7
  214. package/dist/lib/node-esm/chunk-KFZEB6BV.mjs.map +0 -7
  215. package/dist/lib/node-esm/chunk-NULBHQYS.mjs.map +0 -7
  216. package/dist/lib/node-esm/chunk-SX5NVETP.mjs.map +0 -7
  217. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +0 -7
  218. package/dist/lib/node-esm/chunk-XGBUKRCK.mjs.map +0 -7
  219. package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +0 -7
  220. package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +0 -7
  221. /package/dist/lib/browser/{capability-BOPAKKWG.mjs.map → capability-S44TE2H7.mjs.map} +0 -0
  222. /package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs.map → process-manager-capability-QK4BMKMN.mjs.map} +0 -0
  223. /package/dist/lib/node-esm/{capability-KZWRTX7R.mjs.map → capability-3YRF77LV.mjs.map} +0 -0
  224. /package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs.map → process-manager-capability-5S7WZ2JR.mjs.map} +0 -0
@@ -2,16 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import type { FC, PropsWithChildren, ReactNode, RefCallback } from 'react';
5
+ import type { FC, PropsWithChildren, ReactNode, Ref } from 'react';
6
6
 
7
+ import type { DXN } from '@dxos/keys';
8
+ import { log } from '@dxos/log';
7
9
  import type { MakeOptional, Position } from '@dxos/util';
8
10
 
9
11
  /**
10
- * A typed role token. Carries a role string for runtime dispatch plus a phantom
12
+ * A typed role token. Carries a role NSID for runtime dispatch plus a phantom
11
13
  * data type so consumers using `type={SomeToken}` and providers using
12
14
  * `AppSurface.object(SomeToken, ...)` share a single type-level contract.
13
15
  *
14
- * Mint via {@link makeType}.
16
+ * Mint via {@link makeType}. The `role` field holds the bare NSID (e.g.
17
+ * `org.dxos.role.article`) — the dispatch key, `data-role` attribute value,
18
+ * and what `.role` comparisons inside matched components see.
15
19
  */
16
20
  export type RoleToken<TData> = {
17
21
  readonly role: string;
@@ -20,8 +24,8 @@ export type RoleToken<TData> = {
20
24
  };
21
25
 
22
26
  /**
23
- * One entry in a {@link SurfaceFilter} — a role plus the guard that validates
24
- * the data shape when the Surface dispatcher is matching that role.
27
+ * One entry in a {@link SurfaceFilter} — a role NSID plus the guard that
28
+ * validates the data shape when the Surface dispatcher is matching that role.
25
29
  */
26
30
  export type SurfaceBinding = {
27
31
  readonly role: string;
@@ -52,17 +56,61 @@ export const isSurfaceFilter = (value: unknown): value is SurfaceFilter<any> =>
52
56
  typeof value === 'object' && value !== null && Array.isArray((value as { bindings?: unknown }).bindings);
53
57
 
54
58
  /**
55
- * Mints a typed role token. Identity is structural; all tokens with the same
56
- * `role` string are interchangeable at runtime.
59
+ * Mints a typed role token. The NSID is validated at compile time via
60
+ * {@link DXN.Name}: the final segment must be camelCase (no hyphens).
61
+ * Dynamic NSIDs (template-literal / runtime strings) are validated at runtime.
62
+ *
63
+ * Identity is structural; all tokens with the same `role` string are
64
+ * interchangeable at runtime.
57
65
  */
58
- export const makeType = <TData>(role: string): RoleToken<TData> => ({ role });
66
+ export const makeType: {
67
+ <TData = unknown, T extends string = string>(
68
+ nsid: [DXN.Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
69
+ ): RoleToken<TData>;
70
+ } = <TData>(nsid: string): RoleToken<TData> => {
71
+ // Runtime guard for dynamic NSIDs that bypass the compile-time DXN.Name<T> check.
72
+ if (
73
+ typeof nsid !== 'string' ||
74
+ !/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$/.test(
75
+ nsid,
76
+ )
77
+ ) {
78
+ throw new Error(
79
+ `Invalid role NSID: "${nsid}". Must be a dotted name with a camelCase final segment (no hyphens in the last segment).`,
80
+ );
81
+ }
82
+ return { role: nsid };
83
+ };
59
84
 
60
85
  /**
61
- * Props that are passed to the Surface component.
86
+ * Creates a {@link SurfaceFilter} from a role token and an optional guard.
87
+ *
88
+ * When `guard` is omitted the filter matches any data at the token's role
89
+ * (role-only dispatch). Pass a guard to add runtime data-shape validation on
90
+ * top of the role match.
62
91
  *
63
- * The role can be provided either as a string via `role` or as a typed
64
- * {@link RoleToken} via `type`. The typed form narrows `data` to the token's
65
- * declared shape.
92
+ * This is the framework-level primitive; `@dxos/app-toolkit` builds richer
93
+ * domain-aware helpers (ECHO schema checks, literal matching, etc.) on top of it.
94
+ */
95
+ export const makeFilter = <TData>(token: RoleToken<TData>, guard?: (data: TData) => boolean): SurfaceFilter<TData> => {
96
+ const boundGuard =
97
+ guard == null
98
+ ? () => true
99
+ : (data: unknown): boolean => {
100
+ try {
101
+ return guard(data as TData);
102
+ } catch (err) {
103
+ log.catch(err);
104
+ return false;
105
+ }
106
+ };
107
+ return { bindings: [{ role: token.role, guard: boundGuard }] };
108
+ };
109
+
110
+ /**
111
+ * Typed Surface consumer props — carries the role/data-shape contract via a
112
+ * {@link RoleToken}. The `type` prop is required; the string `role` prop has
113
+ * been removed from the consumer-facing API.
66
114
  */
67
115
  export type Props<T extends Record<string, any> = Record<string, unknown>> = {
68
116
  /**
@@ -76,22 +124,7 @@ export type Props<T extends Record<string, any> = Record<string, unknown>> = {
76
124
  * The placeholder component will be rendered while the surface component is loading.
77
125
  */
78
126
  placeholder?: ReactNode;
79
- } & MakeOptional<CoreProps<T>, 'id' | 'data' | 'role'> & {
80
- /**
81
- * Explicitly disallow `type` on the untyped Props overload — if the caller
82
- * provides `type={AppSurface.X}`, TypeScript must route to the typed
83
- * overload (where `data` is narrowed by the token). Without this, the
84
- * catch-all index signature below would accept any `type` value and mask
85
- * the typed overload entirely.
86
- */
87
- type?: undefined;
88
- } /**
89
- * Additional props to pass to the component.
90
- * These props are not used by Surface itself but may be used by components which resolve the surface.
91
- * Exclude known prop names to prevent overriding well-defined props.
92
- */ & {
93
- [K in keyof Record<string, any>]: K extends keyof CoreProps<T> | 'fallback' | 'placeholder' | 'type' ? never : any;
94
- };
127
+ } & MakeOptional<CoreProps<T>, 'id' | 'data'>;
95
128
 
96
129
  /**
97
130
  * Typed Surface consumer props — carries the role/data-shape contract via a
@@ -118,6 +151,8 @@ export type TypedProps<TToken extends RoleToken<any>> = {
118
151
 
119
152
  /**
120
153
  * NOTE: If `[key: string]: unknown` is included in shared types, when re-used other fields become unknown as well.
154
+ * The `type` prop is the only way to set the role; it is required here and
155
+ * optional in {@link Props} via MakeOptional.
121
156
  */
122
157
  export type CoreProps<T extends Record<string, any> = Record<string, unknown>> = PropsWithChildren<{
123
158
  /**
@@ -126,12 +161,9 @@ export type CoreProps<T extends Record<string, any> = Record<string, unknown>> =
126
161
  id: string;
127
162
 
128
163
  /**
129
- * Role defines how the data should be rendered. For new code, prefer the
130
- * typed overload of the Surface component which takes a {@link RoleToken}
131
- * via the `type` prop instead — it enforces a role/data contract at compile
132
- * time.
164
+ * The role token defining how the data should be rendered.
133
165
  */
134
- role: string;
166
+ type: RoleToken<T>;
135
167
 
136
168
  /**
137
169
  * The data to be rendered by the surface.
@@ -146,9 +178,20 @@ export type CoreProps<T extends Record<string, any> = Record<string, unknown>> =
146
178
  }>;
147
179
 
148
180
  // TODO(burdon): Remove ref since relying on this would be error prone.
149
- export type ComponentProps<T extends Record<string, any> = Record<string, any>> = CoreProps<T> & {
150
- ref?: RefCallback<HTMLElement>;
151
- } & Record<string, any>;
181
+ export type ComponentProps<T extends Record<string, any> = Record<string, any>> = {
182
+ /**
183
+ * The resolved role NSID string (e.g. `org.dxos.role.article`). Present on
184
+ * matched components only — multi-role containers can branch on this value
185
+ * by comparing against a token's `.role` (e.g. `role === AppSurface.Section.role`).
186
+ */
187
+ role: string;
188
+ ref?: Ref<HTMLElement>;
189
+ } & PropsWithChildren<{
190
+ id: string;
191
+ data: T;
192
+ limit?: number | undefined;
193
+ }> &
194
+ Record<string, any>;
152
195
 
153
196
  /**
154
197
  * React component used to render a surface once is has matched.
@@ -163,24 +206,30 @@ export type ComponentFunction<T extends Record<string, any> = Record<string, any
163
206
  * The optional `filter` receives the consumer-supplied `role` as its second
164
207
  * argument so a single definition can encode role-specific guards (used by the
165
208
  * {@link SurfaceFilter}-based `create` overload).
209
+ *
210
+ * @internal The `role` field is populated by the framework from the filter
211
+ * bindings and is not authored directly.
166
212
  */
167
213
  export type ReactDefinition<T extends Record<string, any> = any> = Readonly<{
168
214
  kind: 'react';
169
215
  id: string;
170
216
  role: string | string[];
171
- position?: Position;
217
+ position?: Position.Position;
172
218
  component: ComponentFunction<T>;
173
219
  filter?: (data: Record<string, unknown>, role?: string) => data is T;
174
220
  }>;
175
221
 
176
222
  /**
177
223
  * Definition of when a Web Component surface should be rendered.
224
+ *
225
+ * @internal The `role` field is populated by the framework from the filter
226
+ * bindings and is not authored directly.
178
227
  */
179
228
  export type WebComponentDefinition<T extends Record<string, any> = any> = Readonly<{
180
229
  kind: 'web-component';
181
230
  id: string;
182
231
  role: string | string[];
183
- position?: Position;
232
+ position?: Position.Position;
184
233
  /**
185
234
  * The tag name of the Web Component to render.
186
235
  * The Web Component will receive the same props as React surfaces via properties/attributes.
@@ -201,7 +250,7 @@ export type TypedReactDefinition<T extends Record<string, any> = any> = Readonly
201
250
  id: string;
202
251
  filter: SurfaceFilter<T>;
203
252
  component: ComponentFunction<T>;
204
- position?: Position;
253
+ position?: Position.Position;
205
254
  }>;
206
255
 
207
256
  /**
@@ -211,7 +260,7 @@ export type TypedWebComponentDefinition<T extends Record<string, any> = any> = R
211
260
  id: string;
212
261
  filter: SurfaceFilter<T>;
213
262
  tagName: string;
214
- position?: Position;
263
+ position?: Position.Position;
215
264
  }>;
216
265
 
217
266
  const expandBindings = <T extends Record<string, any>>(
@@ -250,41 +299,27 @@ const validateLocalId = (id: string): void => {
250
299
  };
251
300
 
252
301
  /**
253
- * Creates a React surface definition.
302
+ * Creates a React surface definition from a typed filter.
254
303
  */
255
304
  export function create<T extends Record<string, any> = any>(definition: TypedReactDefinition<T>): ReactDefinition<T>;
256
- export function create<T extends Record<string, any> = any>(
257
- definition: Omit<ReactDefinition<T>, 'kind'>,
258
- ): ReactDefinition<T>;
259
- export function create<T extends Record<string, any> = any>(
260
- definition: TypedReactDefinition<T> | Omit<ReactDefinition<T>, 'kind'>,
261
- ): ReactDefinition<T> {
305
+ export function create<T extends Record<string, any> = any>(definition: TypedReactDefinition<T>): ReactDefinition<T> {
262
306
  validateLocalId(definition.id);
263
- if (isSurfaceFilter(definition.filter)) {
264
- const { id, filter, component, position } = definition as TypedReactDefinition<T>;
265
- const { role, guard } = expandBindings(filter);
266
- return { kind: 'react', id, role, position, component, filter: guard };
267
- }
268
- return { ...(definition as Omit<ReactDefinition<T>, 'kind'>), kind: 'react' };
307
+ const { id, filter, component, position } = definition;
308
+ const { role, guard } = expandBindings(filter);
309
+ return { kind: 'react', id, role, position, component, filter: guard };
269
310
  }
270
311
 
271
312
  /**
272
- * Creates a Web Component surface definition.
313
+ * Creates a Web Component surface definition from a typed filter.
273
314
  */
274
315
  export function createWeb<T extends Record<string, any> = any>(
275
316
  definition: TypedWebComponentDefinition<T>,
276
317
  ): WebComponentDefinition<T>;
277
318
  export function createWeb<T extends Record<string, any> = any>(
278
- definition: Omit<WebComponentDefinition<T>, 'kind'>,
279
- ): WebComponentDefinition<T>;
280
- export function createWeb<T extends Record<string, any> = any>(
281
- definition: TypedWebComponentDefinition<T> | Omit<WebComponentDefinition<T>, 'kind'>,
319
+ definition: TypedWebComponentDefinition<T>,
282
320
  ): WebComponentDefinition<T> {
283
321
  validateLocalId(definition.id);
284
- if (isSurfaceFilter(definition.filter)) {
285
- const { id, filter, tagName, position } = definition as TypedWebComponentDefinition<T>;
286
- const { role, guard } = expandBindings(filter);
287
- return { kind: 'web-component', id, role, position, tagName, filter: guard };
288
- }
289
- return { ...(definition as Omit<WebComponentDefinition<T>, 'kind'>), kind: 'web-component' };
322
+ const { id, filter, tagName, position } = definition;
323
+ const { role, guard } = expandBindings(filter);
324
+ return { kind: 'web-component', id, role, position, tagName, filter: guard };
290
325
  }
@@ -18,7 +18,7 @@ const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test'), name:
18
18
 
19
19
  const pluginLoader = (plugins: Plugin.Plugin[]) =>
20
20
  Effect.fn(function* (id: string) {
21
- const plugin = plugins.find((plugin) => plugin.meta.id === id);
21
+ const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
22
22
  if (!plugin) {
23
23
  return yield* Effect.fail(new Error(`Plugin not found: ${id}`));
24
24
  }
@@ -94,7 +94,7 @@ export type UseAppOptions = {
94
94
  * );
95
95
  *
96
96
  * @param params.pluginLoader A function which loads new plugins.
97
- * @param params.plugins All plugins available to the application. Plugins whose `meta.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).
97
+ * @param params.plugins All plugins available to the application. Plugins whose `meta.profile.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).
98
98
  * @param params.defaults Default plugins are enabled by default but can be disabled by the user.
99
99
  * @param params.cacheEnabled Whether to cache enabled plugins in localStorage.
100
100
  * @param params.safeMode Whether to enable safe mode, which disables optional plugins.
@@ -123,7 +123,7 @@ export const useApp = ({
123
123
  pluginLoaderProp ??
124
124
  ((id: string) =>
125
125
  Effect.sync(() => {
126
- const plugin = plugins.find((plugin) => plugin.meta.id === id);
126
+ const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
127
127
  invariant(plugin, `Plugin not found: ${id}`);
128
128
  return { plugin };
129
129
  })),
@@ -2,14 +2,21 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Atom, useAtomValue } from '@effect-atom/atom-react';
5
+ import { Atom } from '@effect-atom/atom';
6
+ import { useAtomValue } from '@effect-atom/atom-react';
6
7
  import { useCallback } from 'react';
7
8
 
8
9
  import { invariant } from '@dxos/invariant';
9
10
 
10
11
  import { Capabilities } from '../../common';
11
12
  import { type Capability } from '../../core';
12
- import { usePluginManager } from '../components';
13
+ import { useOptionalPluginManager, usePluginManager } from '../components';
14
+
15
+ /** Stable empty result for capability lookups made outside a plugin manager. */
16
+ const emptyCapabilities = Atom.make(() => [] as const);
17
+
18
+ /** Stable atom yielding `undefined`, used as the fallback for optional atom-capability lookups. */
19
+ const emptyAtomValue = Atom.make(() => undefined);
13
20
 
14
21
  /**
15
22
  * Hook to request capabilities from the plugin context.
@@ -32,6 +39,24 @@ export const useCapability = <T>(interfaceDef: Capability.InterfaceDef<T>) => {
32
39
  return capabilities[0];
33
40
  };
34
41
 
42
+ /**
43
+ * Hook to request capabilities without requiring a plugin manager.
44
+ * @returns An array of capabilities, or an empty array when rendered outside a {@link PluginManagerProvider}.
45
+ */
46
+ export const useOptionalCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>): readonly T[] => {
47
+ const manager = useOptionalPluginManager();
48
+ return useAtomValue(
49
+ manager ? manager.capabilities.atom(interfaceDef) : (emptyCapabilities as Atom.Atom<readonly T[]>),
50
+ );
51
+ };
52
+
53
+ /**
54
+ * Hook to request a single capability without requiring a plugin manager.
55
+ * @returns The first matching capability, or `undefined` when none is registered (or there is no plugin manager).
56
+ */
57
+ export const useOptionalCapability = <T>(interfaceDef: Capability.InterfaceDef<T>): T | undefined =>
58
+ useOptionalCapabilities(interfaceDef)[0];
59
+
35
60
  /**
36
61
  * Hook to get the current value of an atom capability.
37
62
  * Automatically subscribes to changes.
@@ -62,6 +87,28 @@ export const useAtomCapabilityState = <T>(
62
87
  return [value, update];
63
88
  };
64
89
 
90
+ /**
91
+ * Tolerant variant of {@link useAtomCapabilityState}: returns `[undefined, noop]` when the atom
92
+ * capability is not registered (e.g. the contributing plugin is not installed) rather than
93
+ * throwing. The updater is a no-op while the capability is absent.
94
+ */
95
+ export const useOptionalAtomCapabilityState = <T>(
96
+ atomCapability: Capability.InterfaceDef<Atom.Writable<T>>,
97
+ ): [T | undefined, (fn: (current: T) => T) => void] => {
98
+ const registry = useOptionalCapability(Capabilities.AtomRegistry);
99
+ const atom = useOptionalCapability(atomCapability);
100
+ const value = useAtomValue(atom ?? emptyAtomValue);
101
+ const update = useCallback(
102
+ (fn: (current: T) => T) => {
103
+ if (registry && atom) {
104
+ registry.set(atom, fn(registry.get(atom)));
105
+ }
106
+ },
107
+ [registry, atom],
108
+ );
109
+ return [value, update];
110
+ };
111
+
65
112
  /**
66
113
  * Hook to get the operation invoker capability.
67
114
  */
@@ -148,12 +148,12 @@ const LoaderHarness = () => {
148
148
  {/* Float the controls above the loader's `z-index: 10` fixed backdrop. */}
149
149
  <div
150
150
  style={{
151
- position: 'fixed',
152
- top: '12px',
153
- left: '12px',
151
+ 'position': 'fixed',
152
+ 'top': '12px',
153
+ 'left': '12px',
154
154
  'z-index': 9999,
155
- display: 'flex',
156
- gap: '12px',
155
+ 'display': 'flex',
156
+ 'gap': '12px',
157
157
  'align-items': 'center',
158
158
  }}
159
159
  >
@@ -161,20 +161,20 @@ const LoaderHarness = () => {
161
161
  type='button'
162
162
  onClick={advance}
163
163
  style={{
164
- padding: '6px 14px',
164
+ 'padding': '6px 14px',
165
165
  'font-size': '13px',
166
166
  'font-weight': '600',
167
- color: '#fff',
168
- background: '#017ab7',
169
- border: 'none',
167
+ 'color': '#fff',
168
+ 'background': '#017ab7',
169
+ 'border': 'none',
170
170
  'border-radius': '6px',
171
- cursor: 'pointer',
171
+ 'cursor': 'pointer',
172
172
  'box-shadow': '0 1px 3px rgba(0,0,0,0.25)',
173
173
  }}
174
174
  >
175
175
  {sim() === 'creep' ? 'Start progress' : 'Finish + reset'}
176
176
  </button>
177
- <span style={{ 'font-family': 'ui-sans-serif, system-ui, sans-serif', 'font-size': '12px', color: '#888' }}>
177
+ <span style={{ 'font-family': 'ui-sans-serif, system-ui, sans-serif', 'font-size': '12px', 'color': '#888' }}>
178
178
  {sim()}
179
179
  </span>
180
180
  </div>
@@ -8,7 +8,7 @@ import * as babel from '@babel/core';
8
8
  import babelPresetTypeScript from '@babel/preset-typescript';
9
9
  // @ts-ignore — babel-preset-solid ships no .d.ts.
10
10
  import babelPresetSolid from 'babel-preset-solid';
11
- import { build, type Plugin as EsbuildPlugin } from 'esbuild';
11
+ import { type Plugin as EsbuildPlugin, build } from 'esbuild';
12
12
  import { basename, dirname } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { type Plugin } from 'vite';
@@ -181,8 +181,8 @@ export const bootLoaderPlugin = ({ status, markSvg, include = ['index.html'] }:
181
181
  tag: 'div',
182
182
  injectTo: 'body-prepend',
183
183
  attrs: {
184
- id: rootId,
185
- role: 'status',
184
+ 'id': rootId,
185
+ 'role': 'status',
186
186
  'aria-live': 'polite',
187
187
  'aria-label': 'Initializing',
188
188
  },