@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
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { useAtomValue } from '@effect-atom/atom-react';
6
6
  import React, {
7
+ type FC,
7
8
  Fragment,
8
9
  type NamedExoticComponent,
9
10
  Profiler,
@@ -19,11 +20,11 @@ import React, {
19
20
  import { log } from '@dxos/log';
20
21
  import { ErrorBoundary } from '@dxos/react-error-boundary';
21
22
  import { useDefaultValue } from '@dxos/react-hooks';
22
- import { byPosition } from '@dxos/util';
23
+ import { Position } from '@dxos/util';
23
24
 
24
25
  import { Capabilities } from '../../../common';
25
26
  import { type CapabilityManager } from '../../../core';
26
- import { usePluginManager } from '../PluginManager/PluginManagerProvider';
27
+ import { usePluginManager } from '../PluginManager';
27
28
  import { SurfaceContext } from './context';
28
29
  import { SurfaceInfo } from './SurfaceInfo';
29
30
  import { useSurfaceProfilerCallback } from './SurfaceProfilerContext';
@@ -36,7 +37,7 @@ import {
36
37
  type WebComponentDefinition,
37
38
  } from './types';
38
39
 
39
- const DEBUG = import.meta.env.VITE_DEBUG;
40
+ const DEBUG = import.meta.env?.VITE_DEBUG;
40
41
 
41
42
  const DEFAULT_PLACEHOLDER = <Fragment />;
42
43
 
@@ -44,78 +45,95 @@ const DEFAULT_PLACEHOLDER = <Fragment />;
44
45
  * Wrapper component for rendering Web Component surfaces.
45
46
  * Handles creation, prop setting, and cleanup of Web Components.
46
47
  */
48
+ type WebComponentWrapperProps = {
49
+ id?: string;
50
+ role: string;
51
+ data?: Record<string, any>;
52
+ limit?: number;
53
+ definition: WebComponentDefinition;
54
+ [key: string]: any;
55
+ };
56
+
47
57
  const WebComponentWrapper = memo(
48
- forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: WebComponentDefinition }>(
49
- ({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
50
- const containerRef = useRef<HTMLDivElement>(null);
51
- const elementRef = useRef<HTMLElement | null>(null);
52
- const propsRef = useRef({ id, role, data, limit, ...rest });
53
-
54
- // Update props ref on every render
55
- propsRef.current = { id, role, data, limit, ...rest };
56
-
57
- // Create element only once
58
- useEffect(() => {
59
- if (!containerRef.current || elementRef.current) {
60
- return;
61
- }
58
+ forwardRef<HTMLElement, WebComponentWrapperProps>(({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
59
+ const containerRef = useRef<HTMLDivElement>(null);
60
+ const elementRef = useRef<HTMLElement | null>(null);
61
+ const propsRef = useRef({ id, role, data, limit, ...rest });
62
+
63
+ // Update props ref on every render
64
+ propsRef.current = { id, role, data, limit, ...rest };
65
+
66
+ // Create element only once
67
+ useEffect(() => {
68
+ if (!containerRef.current || elementRef.current) {
69
+ return;
70
+ }
62
71
 
63
- // Create the Web Component
64
- const element = document.createElement(definition.tagName);
65
- elementRef.current = element;
72
+ // Create the Web Component
73
+ const element = document.createElement(definition.tagName);
74
+ elementRef.current = element;
66
75
 
67
- // Set initial properties on the Web Component
68
- Object.assign(element, propsRef.current);
76
+ // Set initial properties on the Web Component
77
+ Object.assign(element, propsRef.current);
69
78
 
70
- // Append to container
71
- containerRef.current.appendChild(element);
79
+ // Append to container
80
+ containerRef.current.appendChild(element);
72
81
 
73
- // Setup ref forwarding if provided
82
+ // Setup ref forwarding if provided
83
+ if (typeof forwardedRef === 'function') {
84
+ forwardedRef(element);
85
+ } else if (forwardedRef) {
86
+ forwardedRef.current = element;
87
+ }
88
+
89
+ // Cleanup on unmount to prevent memory leaks
90
+ return () => {
91
+ if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
92
+ containerRef.current.removeChild(elementRef.current);
93
+ }
74
94
  if (typeof forwardedRef === 'function') {
75
- forwardedRef(element);
95
+ forwardedRef(null);
76
96
  } else if (forwardedRef) {
77
- forwardedRef.current = element;
78
- }
79
-
80
- // Cleanup on unmount to prevent memory leaks
81
- return () => {
82
- if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
83
- containerRef.current.removeChild(elementRef.current);
84
- }
85
- if (typeof forwardedRef === 'function') {
86
- forwardedRef(null);
87
- } else if (forwardedRef) {
88
- forwardedRef.current = null;
89
- }
90
- elementRef.current = null;
91
- };
92
- }, [definition.tagName, forwardedRef]);
93
-
94
- // Update props on existing element without recreating it
95
- // This runs on every render to ensure all props (including those in `rest`) are kept up to date
96
- useEffect(() => {
97
- const element = elementRef.current;
98
- if (!element) {
99
- return;
97
+ forwardedRef.current = null;
100
98
  }
99
+ elementRef.current = null;
100
+ };
101
+ }, [definition.tagName, forwardedRef]);
102
+
103
+ // Update props on existing element without recreating it
104
+ // This runs on every render to ensure all props (including those in `rest`) are kept up to date
105
+ useEffect(() => {
106
+ const element = elementRef.current;
107
+ if (!element) {
108
+ return;
109
+ }
101
110
 
102
- // Update properties on the existing Web Component
103
- Object.assign(element, propsRef.current);
104
- });
111
+ // Update properties on the existing Web Component
112
+ Object.assign(element, propsRef.current);
113
+ });
105
114
 
106
- return <div ref={containerRef} />;
107
- },
108
- ),
115
+ return <div ref={containerRef} />;
116
+ }),
109
117
  );
110
118
 
111
119
  WebComponentWrapper.displayName = 'WebComponentWrapper';
112
120
 
121
+ type SurfaceContextProviderProps = {
122
+ id: string;
123
+ role: string;
124
+ data?: Record<string, any>;
125
+ limit?: number;
126
+ fallback?: FC<{ error: Error; data?: any }>;
127
+ definition: Definition;
128
+ [key: string]: any;
129
+ };
130
+
113
131
  /**
114
132
  * Wrapper component that provides context for a surface.
115
133
  */
116
134
  // TODO(burdon): Allow DebugPlugin to provide different fallback using react-ui ErrorFallback.
117
135
  const SurfaceContextProvider = memo(
118
- forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: Definition }>(
136
+ forwardRef<HTMLElement, SurfaceContextProviderProps>(
119
137
  ({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }, forwardedRef) => {
120
138
  const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
121
139
  const onProfilerRender = useSurfaceProfilerCallback();
@@ -182,11 +200,13 @@ SurfaceContextProvider.displayName = 'SurfaceContextProvider';
182
200
 
183
201
  /**
184
202
  * A surface is a named region of the screen that can be populated by plugins.
203
+ * The `type` prop is a {@link RoleToken} that defines which region and its
204
+ * associated data contract.
185
205
  */
186
206
  // TODO(burdon): Remove `ref` since relying on this would be error prone.
187
207
  export const SurfaceComponent = memo(
188
- forwardRef<HTMLElement, Props & { type?: RoleToken<any> }>(
189
- ({ id: _id, role, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
208
+ forwardRef<HTMLElement, TypedProps<RoleToken<any>>>(
209
+ ({ id: _id, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
190
210
  const data = useDefaultValue(dataProp, () => ({}));
191
211
  // TODO(wittjosiah): This will make all surfaces depend on a single signal.
192
212
  // This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
@@ -195,10 +215,10 @@ export const SurfaceComponent = memo(
195
215
  // In the future, it would be nice to be able to bucket the surface contributions by role.
196
216
  const surfaces = useSurfaces();
197
217
 
198
- const effectiveRole = role ?? type?.role;
218
+ const effectiveRole = type?.role;
199
219
  if (effectiveRole == null) {
200
220
  if (DEBUG) {
201
- log.warn('Surface has neither `role` nor `type` prop', { id: _id });
221
+ log.warn('Surface is missing required `type` prop', { id: _id });
202
222
  }
203
223
  return null;
204
224
  }
@@ -230,12 +250,12 @@ export const SurfaceComponent = memo(
230
250
  },
231
251
  ),
232
252
  ) as (<TToken extends RoleToken<any>>(props: TypedProps<TToken> & RefAttributes<HTMLElement>) => React.ReactNode) &
233
- NamedExoticComponent<Props & RefAttributes<HTMLElement>>;
253
+ NamedExoticComponent<TypedProps<RoleToken<any>> & RefAttributes<HTMLElement>>;
234
254
 
235
255
  SurfaceComponent.displayName = 'Surface';
236
256
 
237
257
  // TODO(burdon): Make user facing, with telemetry.
238
- const ErrorFallback = ({ error }: Props) => {
258
+ const ErrorFallback = ({ error }: { error: Error }) => {
239
259
  const { message } = error instanceof Error ? error : { message: String(error) };
240
260
  return (
241
261
  <div role='alert' data-testid='error-boundary-fallback'>
@@ -250,7 +270,7 @@ const findCandidates = (surfaces: Definition[], { role, data }: { role: string;
250
270
  Array.isArray(definition.role) ? definition.role.includes(role) : definition.role === role,
251
271
  )
252
272
  .filter(({ filter }) => (filter ? filter(data ?? {}, role) : true))
253
- .toSorted(byPosition);
273
+ .toSorted(Position.compare);
254
274
  };
255
275
 
256
276
  /**
@@ -261,7 +281,7 @@ export const useSurfaces = () => {
261
281
  const surfacesByModule = useAtomValue(manager.capabilities.atomByModule(Capabilities.ReactSurface));
262
282
  return useMemo(() => {
263
283
  const result: Definition[] = [];
264
- for (const [moduleId, surfaces] of Object.entries(surfacesByModule)) {
284
+ for (const [_moduleId, surfaces] of Object.entries(surfacesByModule)) {
265
285
  for (const def of surfaces.flat()) {
266
286
  result.push(def);
267
287
  }
@@ -273,31 +293,22 @@ export const useSurfaces = () => {
273
293
  /**
274
294
  * @returns `true` if there is a contributed surface which matches the specified role & data, `false` otherwise.
275
295
  *
276
- * Two overloads:
277
- * - Typed: pass a `type` role token and `data` is constrained to the token's
278
- * declared contract (e.g. `AppSurface.Section` requires `attendableId`).
279
- * - Legacy: pass a string `role` and `data` is untyped.
296
+ * Typed: pass a `type` role token and `data` is constrained to the token's
297
+ * declared contract (e.g. `AppSurface.Section` requires `attendableId`).
280
298
  */
281
299
  export function isSurfaceAvailable<TToken extends RoleToken<any>>(
282
300
  capabilityManager: CapabilityManager.CapabilityManager,
283
- args: { type: TToken; data?: TokenData<TToken>; role?: never },
284
- ): boolean;
285
- export function isSurfaceAvailable(
286
- capabilityManager: CapabilityManager.CapabilityManager,
287
- args: Pick<Props, 'role' | 'data'> & { type?: undefined },
288
- ): boolean;
289
- export function isSurfaceAvailable(
290
- capabilityManager: CapabilityManager.CapabilityManager,
291
- { role, type, data }: { role?: string; type?: RoleToken<any>; data?: unknown },
301
+ args: { type: TToken; data?: TokenData<TToken> },
292
302
  ): boolean {
293
- const effectiveRole = role ?? type?.role;
303
+ const effectiveRole = args.type?.role;
294
304
  if (effectiveRole == null) {
295
305
  return false;
296
306
  }
307
+
297
308
  const surfaces = capabilityManager.getAll(Capabilities.ReactSurface);
298
309
  const candidates = findCandidates(surfaces.flat(), {
299
310
  role: effectiveRole,
300
- data: data as Props['data'],
311
+ data: args.data as Props['data'],
301
312
  });
302
313
  return candidates.length > 0;
303
314
  }
@@ -4,8 +4,8 @@
4
4
 
5
5
  import React, {
6
6
  type Context,
7
- type PropsWithChildren,
8
7
  type ProfilerOnRenderCallback,
8
+ type PropsWithChildren,
9
9
  createContext,
10
10
  useContext,
11
11
  useMemo,
@@ -4,9 +4,11 @@
4
4
 
5
5
  import { type Context, createContext } from 'react';
6
6
 
7
- import { type Props } from './types';
7
+ export type SurfaceContext = {
8
+ id?: string;
9
+ /** The resolved role NSID (e.g. `org.dxos.role.article`). */
10
+ role: string;
11
+ data?: Record<string, any>;
12
+ };
8
13
 
9
- export type SurfaceContext = Pick<Props, 'id' | 'role' | 'data'>;
10
-
11
- // TODO(burdon): Use @radix-ui/react-context
12
14
  export const SurfaceContext: Context<SurfaceContext | undefined> = createContext<SurfaceContext | undefined>(undefined);
@@ -2,6 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import type { DXN } from '@dxos/keys';
6
+
5
7
  // TODO(wittjosiah): Cleanup to avoid re-naming.
6
8
  import { SurfaceContext } from './context';
7
9
  import { SurfaceComponent, isSurfaceAvailable } from './SurfaceComponent';
@@ -13,15 +15,16 @@ import {
13
15
  useSurfaceProfilerStats,
14
16
  } from './SurfaceProfilerContext';
15
17
  import {
16
- type Definition as SurfaceDefinition,
17
- type RoleToken as SurfaceRoleToken,
18
18
  type SurfaceBinding as SurfaceBindingType,
19
+ type Definition as SurfaceDefinition,
19
20
  type SurfaceFilter as SurfaceFilterType,
21
+ type RoleToken as SurfaceRoleToken,
20
22
  type TokenData as SurfaceTokenData,
21
23
  type TypedProps as SurfaceTypedProps,
22
24
  create as createSurface,
23
25
  createWeb as createWebSurface,
24
26
  isSurfaceFilter as isSurfaceFilterFn,
27
+ makeFilter as makeFilterFn,
25
28
  makeType as makeTypeFn,
26
29
  } from './types';
27
30
 
@@ -41,7 +44,15 @@ export namespace Surface {
41
44
  export type Filter<TData> = SurfaceFilterType<TData>;
42
45
  export type TokenData<T> = SurfaceTokenData<T>;
43
46
  export type TypedProps<TToken extends SurfaceRoleToken<any>> = SurfaceTypedProps<TToken>;
44
- export const makeType = makeTypeFn;
47
+
48
+ /** Mints a typed role token. The NSID is validated at compile time via {@link DXN.Name}. */
49
+ export const makeType: {
50
+ <TData = unknown, T extends string = string>(
51
+ nsid: [DXN.Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
52
+ ): SurfaceRoleToken<TData>;
53
+ } = makeTypeFn;
54
+
55
+ export const makeFilter = makeFilterFn;
45
56
  export const isFilter = isSurfaceFilterFn;
46
57
 
47
58
  export const ProfilerProvider = SurfaceProfilerProvider;
@@ -4,27 +4,39 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
+ import { Position } from '@dxos/util';
8
+
7
9
  import { type CapabilityManager } from '../../../core';
8
10
  import { isSurfaceAvailable } from './SurfaceComponent';
9
- import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeType } from './types';
11
+ import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeFilter, makeType } from './types';
10
12
 
11
13
  describe('Surface.makeType', () => {
12
- test('creates a role token carrying the role string', ({ expect }) => {
13
- const token = makeType<{ subject: string }>('test-role');
14
- expect(token.role).toBe('test-role');
14
+ test('creates a role token with the given NSID', ({ expect }) => {
15
+ const token = makeType<{ subject: string }>('org.dxos.test.role.testRole');
16
+ expect(token.role).toBe('org.dxos.test.role.testRole');
15
17
  });
16
18
 
17
19
  test('tokens with the same role are independent objects (identity-by-role)', ({ expect }) => {
18
- const a = makeType<{ x: number }>('shared');
19
- const b = makeType<{ x: number }>('shared');
20
- expect(a).not.toBe(b);
21
- expect(a.role).toBe(b.role);
20
+ const tokenOne = makeType<{ x: number }>('org.dxos.test.role.shared');
21
+ const tokenTwo = makeType<{ x: number }>('org.dxos.test.role.shared');
22
+ expect(tokenOne).not.toBe(tokenTwo);
23
+ expect(tokenOne.role).toBe(tokenTwo.role);
24
+ });
25
+
26
+ test('throws on invalid NSID at runtime (hyphenated final segment)', ({ expect }) => {
27
+ expect(() => makeType('org.dxos.test.role.bad-name' as any)).toThrow();
28
+ });
29
+
30
+ test('throws on single-segment NSID', ({ expect }) => {
31
+ expect(() => makeType('article' as any)).toThrow();
22
32
  });
23
33
  });
24
34
 
25
35
  describe('isSurfaceFilter', () => {
26
36
  test('distinguishes filter objects from predicate functions', ({ expect }) => {
27
- const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: 'r', guard: () => true }] };
37
+ const filter: SurfaceFilter<Record<string, any>> = {
38
+ bindings: [{ role: 'org.dxos.test.role.r', guard: () => true }],
39
+ };
28
40
  expect(isSurfaceFilter(filter)).toBe(true);
29
41
  expect(isSurfaceFilter(() => true)).toBe(false);
30
42
  expect(isSurfaceFilter({})).toBe(false);
@@ -33,61 +45,49 @@ describe('isSurfaceFilter', () => {
33
45
  });
34
46
 
35
47
  describe('create', () => {
36
- test('accepts the legacy { role, filter } shape', ({ expect }) => {
37
- const def = create({
38
- id: 'legacy',
39
- role: 'article',
40
- filter: (data): data is { x: number } => typeof (data as any).x === 'number',
41
- component: () => null,
42
- });
43
- expect(def.kind).toBe('react');
44
- expect(def.role).toBe('article');
45
- expect(def.filter!({ x: 1 })).toBe(true);
46
- expect(def.filter!({})).toBe(false);
47
- });
48
-
49
48
  test('expands a single-binding SurfaceFilter into a role string', ({ expect }) => {
50
- const filter: SurfaceFilter<Record<string, any>> = {
51
- bindings: [{ role: 'article', guard: (d) => (d as any).subject === 'ok' }],
52
- };
49
+ const token = makeType<Record<string, any>>('org.dxos.test.role.article');
50
+ const filter = makeFilter(token, (data) => data.subject === 'ok');
53
51
  const def = create({ id: 'typedSingle', filter, component: () => null });
54
- expect(def.role).toBe('article');
55
- expect(def.filter!({ subject: 'ok' }, 'article')).toBe(true);
56
- expect(def.filter!({ subject: 'no' }, 'article')).toBe(false);
52
+ expect(def.role).toBe('org.dxos.test.role.article');
53
+ expect(def.filter!({ subject: 'ok' }, 'org.dxos.test.role.article')).toBe(true);
54
+ expect(def.filter!({ subject: 'no' }, 'org.dxos.test.role.article')).toBe(false);
57
55
  });
58
56
 
59
57
  test('expands a multi-binding SurfaceFilter into a role array with role-scoped guards', ({ expect }) => {
58
+ const tokenA = makeType<Record<string, any>>('org.dxos.test.role.article');
59
+ const tokenB = makeType<Record<string, any>>('org.dxos.test.role.section');
60
60
  const filter: SurfaceFilter<Record<string, any>> = {
61
61
  bindings: [
62
- { role: 'article', guard: (d) => (d as any).subject === 'a' },
63
- { role: 'section', guard: (d) => (d as any).subject === 's' },
62
+ { role: tokenA.role, guard: (data) => (data as any).subject === 'a' },
63
+ { role: tokenB.role, guard: (data) => (data as any).subject === 's' },
64
64
  ],
65
65
  };
66
66
  const def = create({ id: 'typedMulti', filter, component: () => null });
67
- expect(def.role).toEqual(['article', 'section']);
67
+ expect(def.role).toEqual([tokenA.role, tokenB.role]);
68
68
  // Role-specific guard.
69
- expect(def.filter!({ subject: 'a' }, 'article')).toBe(true);
70
- expect(def.filter!({ subject: 'a' }, 'section')).toBe(false);
71
- expect(def.filter!({ subject: 's' }, 'section')).toBe(true);
72
- expect(def.filter!({ subject: 's' }, 'article')).toBe(false);
69
+ expect(def.filter!({ subject: 'a' }, tokenA.role)).toBe(true);
70
+ expect(def.filter!({ subject: 'a' }, tokenB.role)).toBe(false);
71
+ expect(def.filter!({ subject: 's' }, tokenB.role)).toBe(true);
72
+ expect(def.filter!({ subject: 's' }, tokenA.role)).toBe(false);
73
73
  // Without role, any binding may match.
74
74
  expect(def.filter!({ subject: 'a' })).toBe(true);
75
75
  expect(def.filter!({ subject: 'unknown' })).toBe(false);
76
76
  });
77
77
 
78
78
  test('passes position through untouched', ({ expect }) => {
79
- const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: 'r', guard: () => true }] };
80
- const def = create({ id: 'pos', filter, component: () => null, position: 'last' });
81
- expect(def.position).toBe('last');
79
+ const token = makeType<Record<string, any>>('org.dxos.test.role.r');
80
+ const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: token.role, guard: () => true }] };
81
+ const def = create({ id: 'pos', filter, component: () => null, position: Position.last });
82
+ expect(def.position).toBe(Position.last);
82
83
  });
83
84
  });
84
85
 
85
86
  describe('role token typing', () => {
86
87
  test('makeType preserves TData through token use-sites', ({ expect }) => {
87
- // Type-level smoke test: if TS compiles, we're fine. The `RoleToken<T>`
88
- // phantom should not impose a runtime constraint.
89
- const token: RoleToken<{ subject: number }> = makeType('numeric');
90
- expect(token.role).toBe('numeric');
88
+ // Type-level smoke test: if TS compiles, we're fine.
89
+ const token: RoleToken<{ subject: number }> = makeType('org.dxos.test.role.numeric');
90
+ expect(token.role).toBe('org.dxos.test.role.numeric');
91
91
  });
92
92
  });
93
93
 
@@ -95,7 +95,7 @@ describe('isSurfaceAvailable typing', () => {
95
95
  // These tests double as static assertions: the `@ts-expect-error` comments
96
96
  // fail to compile if the surrounding expression typechecks, so they verify
97
97
  // the typed overload narrows `data` to the token's declared contract.
98
- const sectionToken = makeType<{ attendableId: string; subject: string }>('section');
98
+ const sectionToken = makeType<{ attendableId: string; subject: string }>('org.dxos.test.role.section');
99
99
  const capabilityManager = { getAll: () => [] } as unknown as CapabilityManager.CapabilityManager;
100
100
 
101
101
  test('typed overload accepts data matching the token contract', () => {
@@ -115,12 +115,4 @@ describe('isSurfaceAvailable typing', () => {
115
115
  // @ts-expect-error — `attendableId` must be a string, not a number.
116
116
  isSurfaceAvailable(capabilityManager, { type: sectionToken, data: { attendableId: 123, subject: 'x' } });
117
117
  });
118
-
119
- test('legacy overload accepts loose data when `role` is a string', () => {
120
- // No error — legacy overload's `data` is untyped (`Record<string, unknown>`).
121
- isSurfaceAvailable(capabilityManager, {
122
- role: 'article',
123
- data: { anything: 'goes' },
124
- });
125
- });
126
118
  });