@dxos/app-framework 0.10.0 → 0.11.0

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 (294) hide show
  1. package/.storybook/main.mts +1 -1
  2. package/.storybook/preview.mts +2 -2
  3. package/CHANGELOG.md +93 -0
  4. package/TASKS.md +11 -0
  5. package/dist/lib/chunk-SurfaceManager.mjs +960 -0
  6. package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
  7. package/dist/lib/chunk-capability.mjs +15 -0
  8. package/dist/lib/chunk-capability.mjs.map +1 -0
  9. package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
  10. package/dist/lib/chunk-context.mjs.map +1 -0
  11. package/dist/lib/chunk-history-tracker.mjs +160 -0
  12. package/dist/lib/chunk-history-tracker.mjs.map +1 -0
  13. package/dist/lib/chunk-plugin-manager.mjs +1390 -0
  14. package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
  15. package/dist/lib/chunk-plugin-manifest.mjs +125 -0
  16. package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
  17. package/dist/lib/chunk-process-manager-capability.mjs +126 -0
  18. package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
  19. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  20. package/dist/lib/chunk-url-loader.mjs +347 -0
  21. package/dist/lib/chunk-url-loader.mjs.map +1 -0
  22. package/dist/lib/cli.mjs +78 -0
  23. package/dist/lib/cli.mjs.map +1 -0
  24. package/dist/lib/common/activation-events.mjs +35 -0
  25. package/dist/lib/common/activation-events.mjs.map +1 -0
  26. package/dist/lib/common/capabilities.mjs +184 -0
  27. package/dist/lib/common/capabilities.mjs.map +1 -0
  28. package/dist/lib/config.mjs +2 -0
  29. package/dist/lib/core/activation-event.mjs +55 -0
  30. package/dist/lib/core/activation-event.mjs.map +1 -0
  31. package/dist/lib/core/capability.mjs +189 -0
  32. package/dist/lib/core/capability.mjs.map +1 -0
  33. package/dist/lib/core/plugin-manager.mjs +2 -0
  34. package/dist/lib/core/plugin.mjs +340 -0
  35. package/dist/lib/core/plugin.mjs.map +1 -0
  36. package/dist/lib/core/url-loader.mjs +2 -0
  37. package/dist/lib/index.mjs +134 -0
  38. package/dist/lib/index.mjs.map +1 -0
  39. package/dist/lib/testing/react.mjs +70 -0
  40. package/dist/lib/testing/react.mjs.map +1 -0
  41. package/dist/lib/testing.mjs +399 -0
  42. package/dist/lib/testing.mjs.map +1 -0
  43. package/dist/lib/ui.mjs +636 -0
  44. package/dist/lib/ui.mjs.map +1 -0
  45. package/dist/lib/vite-plugin.mjs +986 -0
  46. package/dist/lib/vite-plugin.mjs.map +1 -0
  47. package/dist/plugin/node-esm/index.mjs +4 -3
  48. package/dist/plugin/node-esm/index.mjs.map +3 -3
  49. package/dist/plugin/node-esm/meta.json +1 -1
  50. package/dist/types/src/common/Role.d.ts +31 -0
  51. package/dist/types/src/common/Role.d.ts.map +1 -0
  52. package/dist/types/src/common/Role.test.d.ts +2 -0
  53. package/dist/types/src/common/Role.test.d.ts.map +1 -0
  54. package/dist/types/src/common/capabilities.d.ts +11 -2
  55. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  56. package/dist/types/src/common/index.d.ts +1 -0
  57. package/dist/types/src/common/index.d.ts.map +1 -1
  58. package/dist/types/src/core/capability-manager.d.ts +1 -1
  59. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  60. package/dist/types/src/core/capability.d.ts +38 -5
  61. package/dist/types/src/core/capability.d.ts.map +1 -1
  62. package/dist/types/src/core/plugin-manager.d.ts +1 -1
  63. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  64. package/dist/types/src/core/plugin.d.ts +14 -1
  65. package/dist/types/src/core/plugin.d.ts.map +1 -1
  66. package/dist/types/src/core/registry.d.ts +1 -1
  67. package/dist/types/src/core/registry.d.ts.map +1 -1
  68. package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
  69. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
  70. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  71. package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
  72. package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
  73. package/dist/types/src/testing/harness.d.ts +1 -1
  74. package/dist/types/src/testing/harness.d.ts.map +1 -1
  75. package/dist/types/src/testing/index.d.ts +2 -0
  76. package/dist/types/src/testing/index.d.ts.map +1 -1
  77. package/dist/types/src/testing/react.d.ts +4 -4
  78. package/dist/types/src/testing/react.d.ts.map +1 -1
  79. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.stories.d.ts +6 -0
  81. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  82. package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
  83. package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
  84. package/dist/types/src/ui/components/App/App.d.ts +0 -32
  85. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/App/index.d.ts +1 -0
  87. package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/components/App/loader.d.ts +35 -0
  89. package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
  90. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
  91. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
  92. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
  93. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
  94. package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
  95. package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
  96. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
  97. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
  98. package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
  99. package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
  100. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
  101. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  102. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
  103. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  104. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
  105. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
  106. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
  107. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
  108. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
  109. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
  110. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
  111. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
  112. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
  113. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
  114. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  115. package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
  116. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  117. package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
  118. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  119. package/dist/types/src/ui/components/index.d.ts +2 -0
  120. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  121. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  122. package/dist/types/tsconfig.tsbuildinfo +1 -1
  123. package/moon.yml +25 -18
  124. package/package.json +58 -73
  125. package/src/common/Role.test.ts +41 -0
  126. package/src/common/Role.ts +54 -0
  127. package/src/common/capabilities.ts +17 -2
  128. package/src/common/index.ts +1 -0
  129. package/src/core/capability-manager.test.ts +1 -1
  130. package/src/core/capability-manager.ts +1 -1
  131. package/src/core/capability.ts +45 -6
  132. package/src/core/plugin-manager.test.ts +38 -3
  133. package/src/core/plugin-manager.ts +18 -1
  134. package/src/core/plugin.ts +16 -1
  135. package/src/core/registry.ts +1 -1
  136. package/src/plugin-process-manager/process-manager-capability.ts +25 -2
  137. package/src/testing/StorybookErrorFallback.tsx +40 -0
  138. package/src/testing/harness.ts +1 -1
  139. package/src/testing/index.ts +2 -0
  140. package/src/testing/react.test.tsx +2 -1
  141. package/src/testing/react.tsx +14 -8
  142. package/src/testing/withPluginManager.stories.tsx +16 -1
  143. package/src/testing/withPluginManager.tsx +13 -2
  144. package/src/testing/withSurfaceDebug.tsx +22 -0
  145. package/src/ui/components/App/App.tsx +7 -35
  146. package/src/ui/components/App/index.ts +1 -0
  147. package/src/ui/components/App/loader.ts +39 -0
  148. package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
  149. package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
  150. package/src/ui/components/HomeSection/index.ts +5 -0
  151. package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
  152. package/src/ui/components/NamePopover/index.ts +5 -0
  153. package/src/ui/components/Surface/DESIGN.md +136 -0
  154. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
  155. package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
  156. package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
  157. package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
  158. package/src/ui/components/Surface/SurfaceManager.ts +95 -0
  159. package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
  160. package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
  161. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
  162. package/src/ui/components/Surface/index.ts +24 -22
  163. package/src/ui/components/Surface/types.test.ts +47 -54
  164. package/src/ui/components/Surface/types.ts +66 -91
  165. package/src/ui/components/index.ts +2 -0
  166. package/src/ui/hooks/useApp.tsx +7 -3
  167. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
  168. package/tsconfig.json +3 -0
  169. package/vite.config.ts +26 -0
  170. package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
  171. package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
  172. package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
  173. package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
  174. package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
  175. package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
  176. package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
  177. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  178. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  179. package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
  180. package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
  181. package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
  182. package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
  183. package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
  184. package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
  185. package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
  186. package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
  188. package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
  190. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
  191. package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
  192. package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
  194. package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
  195. package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
  196. package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
  197. package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
  198. package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
  199. package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
  200. package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
  201. package/dist/lib/browser/cli/index.mjs +0 -74
  202. package/dist/lib/browser/cli/index.mjs.map +0 -7
  203. package/dist/lib/browser/common/activation-events.mjs +0 -20
  204. package/dist/lib/browser/common/activation-events.mjs.map +0 -7
  205. package/dist/lib/browser/common/capabilities.mjs +0 -56
  206. package/dist/lib/browser/common/capabilities.mjs.map +0 -7
  207. package/dist/lib/browser/config/index.mjs +0 -8
  208. package/dist/lib/browser/config/index.mjs.map +0 -7
  209. package/dist/lib/browser/core/activation-event.mjs +0 -20
  210. package/dist/lib/browser/core/activation-event.mjs.map +0 -7
  211. package/dist/lib/browser/core/capability.mjs +0 -32
  212. package/dist/lib/browser/core/capability.mjs.map +0 -7
  213. package/dist/lib/browser/core/plugin-manager.mjs +0 -19
  214. package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
  215. package/dist/lib/browser/core/plugin.mjs +0 -51
  216. package/dist/lib/browser/core/plugin.mjs.map +0 -7
  217. package/dist/lib/browser/core/url-loader.mjs +0 -24
  218. package/dist/lib/browser/core/url-loader.mjs.map +0 -7
  219. package/dist/lib/browser/index.mjs +0 -98
  220. package/dist/lib/browser/index.mjs.map +0 -7
  221. package/dist/lib/browser/meta.json +0 -1
  222. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
  223. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
  224. package/dist/lib/browser/testing/index.mjs +0 -359
  225. package/dist/lib/browser/testing/index.mjs.map +0 -7
  226. package/dist/lib/browser/testing/react.mjs +0 -78
  227. package/dist/lib/browser/testing/react.mjs.map +0 -7
  228. package/dist/lib/browser/ui/index.mjs +0 -60
  229. package/dist/lib/browser/ui/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
  231. package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
  232. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
  233. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
  234. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
  235. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
  236. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
  237. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
  238. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
  239. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
  240. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
  241. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
  242. package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
  243. package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
  244. package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
  245. package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
  246. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
  247. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
  248. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
  249. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
  250. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  251. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  252. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
  253. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
  254. package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
  255. package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
  256. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
  257. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
  258. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
  259. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
  260. package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
  261. package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
  262. package/dist/lib/node-esm/cli/index.mjs +0 -75
  263. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  264. package/dist/lib/node-esm/common/activation-events.mjs +0 -21
  265. package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
  266. package/dist/lib/node-esm/common/capabilities.mjs +0 -57
  267. package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
  268. package/dist/lib/node-esm/config/index.mjs +0 -9
  269. package/dist/lib/node-esm/config/index.mjs.map +0 -7
  270. package/dist/lib/node-esm/core/activation-event.mjs +0 -21
  271. package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
  272. package/dist/lib/node-esm/core/capability.mjs +0 -33
  273. package/dist/lib/node-esm/core/capability.mjs.map +0 -7
  274. package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
  275. package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
  276. package/dist/lib/node-esm/core/plugin.mjs +0 -52
  277. package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
  278. package/dist/lib/node-esm/core/url-loader.mjs +0 -25
  279. package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
  280. package/dist/lib/node-esm/index.mjs +0 -99
  281. package/dist/lib/node-esm/index.mjs.map +0 -7
  282. package/dist/lib/node-esm/meta.json +0 -1
  283. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
  284. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
  285. package/dist/lib/node-esm/testing/index.mjs +0 -360
  286. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  287. package/dist/lib/node-esm/testing/react.mjs +0 -79
  288. package/dist/lib/node-esm/testing/react.mjs.map +0 -7
  289. package/dist/lib/node-esm/ui/index.mjs +0 -61
  290. package/dist/lib/node-esm/ui/index.mjs.map +0 -7
  291. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
  292. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
  293. package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
  294. package/vitest.config.ts +0 -14
@@ -2,16 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useAtomValue } from '@effect-atom/atom-react';
5
+ import { RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
6
  import React, {
7
7
  type FC,
8
8
  Fragment,
9
9
  type NamedExoticComponent,
10
10
  Profiler,
11
- type RefAttributes,
12
11
  Suspense,
13
- forwardRef,
14
12
  memo,
13
+ useCallback,
14
+ useContext,
15
15
  useEffect,
16
16
  useMemo,
17
17
  useRef,
@@ -20,22 +20,15 @@ import React, {
20
20
  import { log } from '@dxos/log';
21
21
  import { ErrorBoundary } from '@dxos/react-error-boundary';
22
22
  import { useDefaultValue } from '@dxos/react-hooks';
23
- import { Position } from '@dxos/util';
24
23
 
25
- import { Capabilities } from '../../../common';
26
- import { type CapabilityManager } from '../../../core';
24
+ import { Capabilities, Role } from '../../../common';
27
25
  import { usePluginManager } from '../PluginManager';
28
26
  import { SurfaceContext } from './context';
29
- import { SurfaceInfo } from './SurfaceInfo';
27
+ import { DebugSurface, isSurfaceDebugEnabled, isSurfaceWrapperEnabled } from './SurfaceDebug';
28
+ import { useSurfaceManager } from './SurfaceManagerContext';
29
+ import { nextDataChurn, surfaceMetrics } from './SurfaceMetrics';
30
30
  import { useSurfaceProfilerCallback } from './SurfaceProfilerContext';
31
- import {
32
- type Definition,
33
- type Props,
34
- type RoleToken,
35
- type TokenData,
36
- type TypedProps,
37
- type WebComponentDefinition,
38
- } from './types';
31
+ import { type Definition, type Props, type TypedProps, type WebComponentDefinition } from './types';
39
32
 
40
33
  const DEBUG = import.meta.env?.VITE_DEBUG;
41
34
 
@@ -54,67 +47,58 @@ type WebComponentWrapperProps = {
54
47
  [key: string]: any;
55
48
  };
56
49
 
57
- const WebComponentWrapper = memo(
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 });
50
+ const WebComponentWrapper = memo(({ id, role, data, limit, definition, ...rest }: WebComponentWrapperProps) => {
51
+ const containerRef = useRef<HTMLDivElement>(null);
52
+ const elementRef = useRef<HTMLElement | null>(null);
53
+ const propsRef = useRef({ id, role, data, limit, ...rest });
62
54
 
63
- // Update props ref on every render
64
- propsRef.current = { id, role, data, limit, ...rest };
55
+ // Update props ref on every render.
56
+ propsRef.current = { id, role, data, limit, ...rest };
65
57
 
66
- // Create element only once
67
- useEffect(() => {
68
- if (!containerRef.current || elementRef.current) {
69
- return;
70
- }
58
+ // Create element only once.
59
+ useEffect(() => {
60
+ if (!containerRef.current || elementRef.current) {
61
+ return;
62
+ }
71
63
 
72
- // Create the Web Component
73
- const element = document.createElement(definition.tagName);
74
- elementRef.current = element;
64
+ // Create the Web Component.
65
+ const element = document.createElement(definition.tagName);
66
+ elementRef.current = element;
75
67
 
76
- // Set initial properties on the Web Component
77
- Object.assign(element, propsRef.current);
68
+ // Set initial properties on the Web Component.
69
+ Object.assign(element, propsRef.current);
78
70
 
79
- // Append to container
80
- containerRef.current.appendChild(element);
71
+ // Append to container.
72
+ containerRef.current.appendChild(element);
81
73
 
82
- // Setup ref forwarding if provided
83
- if (typeof forwardedRef === 'function') {
84
- forwardedRef(element);
85
- } else if (forwardedRef) {
86
- forwardedRef.current = element;
74
+ // Cleanup on unmount to prevent memory leaks.
75
+ return () => {
76
+ if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
77
+ containerRef.current.removeChild(elementRef.current);
87
78
  }
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
- }
94
- if (typeof forwardedRef === 'function') {
95
- forwardedRef(null);
96
- } else if (forwardedRef) {
97
- forwardedRef.current = null;
79
+ elementRef.current = null;
80
+ };
81
+ }, [definition.tagName]);
82
+
83
+ const previousPropsRef = useRef<Record<string, any>>({});
84
+
85
+ // Keep all props (including those in `rest`) in sync on the existing element,
86
+ // removing any that disappeared since the last render so no stale config lingers.
87
+ useEffect(() => {
88
+ const element = elementRef.current;
89
+ if (element) {
90
+ for (const key of Object.keys(previousPropsRef.current)) {
91
+ if (!(key in propsRef.current)) {
92
+ Reflect.deleteProperty(element, key);
98
93
  }
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
94
  }
110
-
111
- // Update properties on the existing Web Component
112
95
  Object.assign(element, propsRef.current);
113
- });
96
+ previousPropsRef.current = propsRef.current;
97
+ }
98
+ });
114
99
 
115
- return <div ref={containerRef} />;
116
- }),
117
- );
100
+ return <div ref={containerRef} />;
101
+ });
118
102
 
119
103
  WebComponentWrapper.displayName = 'WebComponentWrapper';
120
104
 
@@ -133,124 +117,142 @@ type SurfaceContextProviderProps = {
133
117
  */
134
118
  // TODO(burdon): Allow DebugPlugin to provide different fallback using react-ui ErrorFallback.
135
119
  const SurfaceContextProvider = memo(
136
- forwardRef<HTMLElement, SurfaceContextProviderProps>(
137
- ({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }, forwardedRef) => {
138
- const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
139
- const onProfilerRender = useSurfaceProfilerCallback();
140
- const profilerId = `surface/${id}/${role}`;
141
-
142
- // Handle Web Component surfaces
143
- if (definition.kind === 'web-component') {
144
- return (
145
- <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback}>
146
- <SurfaceContext.Provider value={contextValue}>
147
- <WebComponentWrapper
148
- id={id}
149
- role={role}
150
- data={data}
151
- limit={limit}
152
- definition={definition}
153
- ref={forwardedRef}
154
- {...rest}
155
- />
156
- </SurfaceContext.Provider>
157
- </ErrorBoundary>
158
- );
159
- }
120
+ ({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }: SurfaceContextProviderProps) => {
121
+ const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
122
+ const onProfilerRender = useSurfaceProfilerCallback();
123
+ const profilerId = `surface/${id}/${role}`;
124
+ // Count error-boundary trips against this surface (dev only).
125
+ const onError = isSurfaceDebugEnabled() ? () => surfaceMetrics.recordError(id, role) : undefined;
126
+
127
+ // Handle Web Component surfaces.
128
+ if (definition.kind === 'web-component') {
129
+ return (
130
+ <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback} onError={onError}>
131
+ <SurfaceContext.Provider value={contextValue}>
132
+ <WebComponentWrapper id={id} role={role} data={data} limit={limit} definition={definition} {...rest} />
133
+ </SurfaceContext.Provider>
134
+ </ErrorBoundary>
135
+ );
136
+ }
160
137
 
161
- // Handle React component surfaces
162
- const Component = definition.component;
163
-
164
- // TODO(burdon): Remove from production build?
165
- const debug = DEBUG || '__DX_DEBUG__' in window;
166
- if (debug) {
167
- return (
168
- <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback}>
169
- <div className='contents' data-id={id} data-role={role}>
170
- <SurfaceContext.Provider value={contextValue}>
171
- <SurfaceInfo ref={forwardedRef}>
172
- {onProfilerRender && !profilerId.includes('org.dxos.plugin.debug') ? (
173
- <Profiler id={profilerId} onRender={onProfilerRender}>
174
- <Component id={id} role={role} data={data} limit={limit} {...rest} />
175
- </Profiler>
176
- ) : (
177
- <Component id={id} role={role} data={data} limit={limit} {...rest} />
178
- )}
179
- </SurfaceInfo>
180
- </SurfaceContext.Provider>
181
- </div>
182
- </ErrorBoundary>
183
- );
184
- }
138
+ // Handle React component surfaces.
139
+ const Component = definition.component;
140
+ // `props` lets a definition register a plain container and map the surface props onto its own,
141
+ // instead of wrapping it in an adapter component (see `TypedReactDefinition.props`).
142
+ const surfaceProps = { id, role, data, limit, ...rest };
143
+ const component = <Component {...(definition.props?.(surfaceProps) ?? surfaceProps)} />;
144
+ const profiled =
145
+ onProfilerRender && !profilerId.includes('org.dxos.plugin.debug') ? (
146
+ <Profiler id={profilerId} onRender={onProfilerRender}>
147
+ {component}
148
+ </Profiler>
149
+ ) : (
150
+ component
151
+ );
185
152
 
153
+ // Dev builds wrap every surface in `<dx-surface>` for DOM inspection / `window.__DX__`; the
154
+ // `__DX_DEBUG__` flag separately gates the visual highlight overlay (see SurfaceDebug).
155
+ if (isSurfaceWrapperEnabled()) {
186
156
  return (
187
- <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback}>
188
- <div className='contents' data-id={id} data-role={role}>
189
- <SurfaceContext.Provider value={contextValue}>
190
- <Component id={id} role={role} data={data} limit={limit} {...rest} ref={forwardedRef} />
191
- </SurfaceContext.Provider>
192
- </div>
157
+ <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback} onError={onError}>
158
+ <SurfaceContext.Provider value={contextValue}>
159
+ <DebugSurface info={contextValue}>{profiled}</DebugSurface>
160
+ </SurfaceContext.Provider>
193
161
  </ErrorBoundary>
194
162
  );
195
- },
196
- ),
163
+ }
164
+
165
+ // Production renders the matched component directly: a surface adds no wrapper element of its own.
166
+ return (
167
+ <ErrorBoundary name='surface' resetKeys={[data]} FallbackComponent={fallback} onError={onError}>
168
+ <SurfaceContext.Provider value={contextValue}>{profiled}</SurfaceContext.Provider>
169
+ </ErrorBoundary>
170
+ );
171
+ },
197
172
  );
198
173
 
199
174
  SurfaceContextProvider.displayName = 'SurfaceContextProvider';
200
175
 
201
176
  /**
202
177
  * 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
178
+ * The `type` prop is a {@link Role.Role} that defines which region and its
204
179
  * associated data contract.
180
+ *
181
+ * A surface is a boundary that may resolve to zero, one, or many components, so
182
+ * it intentionally accepts no `ref`; consumers needing an element should own one
183
+ * inside their contributed component.
205
184
  */
206
- // TODO(burdon): Remove `ref` since relying on this would be error prone.
207
185
  export const SurfaceComponent = memo(
208
- forwardRef<HTMLElement, TypedProps<RoleToken<any>>>(
209
- ({ id: _id, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
210
- const data = useDefaultValue(dataProp, () => ({}));
211
- // TODO(wittjosiah): This will make all surfaces depend on a single signal.
212
- // This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
213
- // This effectively means that plugin modules which contribute surfaces need to all be activated at startup.
214
- // This should be fine for now because it's how it worked prior to capabilities api anyway.
215
- // In the future, it would be nice to be able to bucket the surface contributions by role.
216
- const surfaces = useSurfaces();
217
-
218
- const effectiveRole = type?.role;
219
- if (effectiveRole == null) {
220
- if (DEBUG) {
221
- log.warn('Surface is missing required `type` prop', { id: _id });
222
- }
223
- return null;
186
+ ({
187
+ id: _id,
188
+ type,
189
+ data: dataProp,
190
+ limit,
191
+ placeholder = DEFAULT_PLACEHOLDER,
192
+ ...rest
193
+ }: TypedProps<Role.Role<any>>) => {
194
+ const data = useDefaultValue(dataProp, () => ({}));
195
+ const surfaceManager = useSurfaceManager();
196
+ // Subscribe only to this role's contributions: contributing/removing a surface for a
197
+ // different role keeps this bucket referentially stable, so the atom does not re-render us.
198
+ const effectiveRole = type?.role ?? '';
199
+ const roleCandidates = useAtomValue(surfaceManager.candidatesAtom(effectiveRole));
200
+
201
+ // NOTE: The data guard runs per render so the surface re-dispatches on reactive data changes.
202
+ const definitions = matchCandidates(roleCandidates, effectiveRole, data);
203
+ // `limit != null` (not truthiness) so an explicit `limit={0}` renders nothing.
204
+ const candidates = limit != null ? definitions.slice(0, limit) : definitions;
205
+ const truncated = limit != null && definitions.length > limit;
206
+
207
+ // Dev metrics: track dispatch count, candidate count, and `data` instability (see SurfaceMetrics).
208
+ const churnRef = useRef<{ data: unknown; churn: number }>({ data: undefined, churn: 0 });
209
+ useEffect(() => {
210
+ if (!isSurfaceDebugEnabled() || effectiveRole === '') {
211
+ return;
212
+ }
213
+ const previous = churnRef.current;
214
+ const churn = previous.data === undefined ? 0 : nextDataChurn(previous.data, data, previous.churn);
215
+ churnRef.current = { data, churn };
216
+ for (const definition of candidates) {
217
+ surfaceMetrics.recordDispatch(definition.id, effectiveRole, {
218
+ candidates: candidates.length,
219
+ truncated,
220
+ dataChurn: churn,
221
+ });
224
222
  }
223
+ });
225
224
 
226
- // NOTE: Memoizing the candidates makes the surface not re-render based on reactivity within data.
227
- const definitions = findCandidates(surfaces, { role: effectiveRole, data });
228
- const candidates = limit ? definitions.slice(0, limit) : definitions;
229
- if (DEBUG && candidates.length === 0) {
230
- log.warn('no candidates for surface', { role: effectiveRole, data });
231
- return null;
225
+ if (type?.role == null) {
226
+ if (DEBUG) {
227
+ log.warn('Surface is missing required `type` prop', { id: _id });
232
228
  }
229
+ return null;
230
+ }
233
231
 
234
- return (
235
- <Suspense fallback={placeholder}>
236
- {candidates.map((definition) => (
237
- <SurfaceContextProvider
238
- key={definition.id}
239
- id={definition.id}
240
- role={effectiveRole}
241
- data={data}
242
- limit={limit}
243
- definition={definition}
244
- ref={forwardedRef}
245
- {...rest}
246
- />
247
- ))}
248
- </Suspense>
249
- );
250
- },
251
- ),
252
- ) as (<TToken extends RoleToken<any>>(props: TypedProps<TToken> & RefAttributes<HTMLElement>) => React.ReactNode) &
253
- NamedExoticComponent<TypedProps<RoleToken<any>> & RefAttributes<HTMLElement>>;
232
+ if (DEBUG && candidates.length === 0) {
233
+ log.warn('no candidates for surface', { role: effectiveRole, data });
234
+ return null;
235
+ }
236
+
237
+ return (
238
+ <Suspense fallback={placeholder}>
239
+ {candidates.map((definition) => (
240
+ <SurfaceContextProvider
241
+ key={definition.id}
242
+ id={definition.id}
243
+ role={effectiveRole}
244
+ data={data}
245
+ limit={limit}
246
+ definition={definition}
247
+ {...rest}
248
+ />
249
+ ))}
250
+ </Suspense>
251
+ );
252
+ },
253
+ // The generic call signature is reattached here because `memo` erases it from the inferred type.
254
+ ) as (<TToken extends Role.Role<any>>(props: TypedProps<TToken>) => React.ReactNode) &
255
+ NamedExoticComponent<TypedProps<Role.Role<any>>>;
254
256
 
255
257
  SurfaceComponent.displayName = 'Surface';
256
258
 
@@ -259,18 +261,23 @@ const ErrorFallback = ({ error }: { error: Error }) => {
259
261
  const { message } = error instanceof Error ? error : { message: String(error) };
260
262
  return (
261
263
  <div role='alert' data-testid='error-boundary-fallback'>
262
- <h1 className='flex gap-2 text-sm mt-2 text-info-text'>{message}</h1>
264
+ <h1 className='flex p-2 text-sm text-info-text'>{message}</h1>
263
265
  </div>
264
266
  );
265
267
  };
266
268
 
267
- const findCandidates = (surfaces: Definition[], { role, data }: { role: string; data: Props['data'] }) => {
268
- return Object.values(surfaces)
269
- .filter((definition) =>
270
- Array.isArray(definition.role) ? definition.role.includes(role) : definition.role === role,
271
- )
272
- .filter(({ filter }) => (filter ? filter(data ?? {}, role) : true))
273
- .toSorted(Position.compare);
269
+ /**
270
+ * Filters the pre-indexed candidates for a role through their data guards.
271
+ */
272
+ const matchCandidates = (
273
+ definitions: ReadonlyArray<Definition> | undefined,
274
+ role: string,
275
+ data: Props['data'],
276
+ ): Definition[] => {
277
+ if (!definitions) {
278
+ return [];
279
+ }
280
+ return definitions.filter(({ filter }) => (filter ? filter(data ?? {}, role) : true));
274
281
  };
275
282
 
276
283
  /**
@@ -291,24 +298,37 @@ export const useSurfaces = () => {
291
298
  };
292
299
 
293
300
  /**
294
- * @returns `true` if there is a contributed surface which matches the specified role & data, `false` otherwise.
301
+ * Reports whether a contributed surface matches the given role & data, without mounting it.
295
302
  *
296
303
  * Typed: pass a `type` role token and `data` is constrained to the token's
297
304
  * declared contract (e.g. `AppSurface.Section` requires `attendableId`).
298
305
  */
299
- export function isSurfaceAvailable<TToken extends RoleToken<any>>(
300
- capabilityManager: CapabilityManager.CapabilityManager,
301
- args: { type: TToken; data?: TokenData<TToken> },
302
- ): boolean {
303
- const effectiveRole = args.type?.role;
304
- if (effectiveRole == null) {
305
- return false;
306
- }
306
+ type IsSurfaceAvailable = <TToken extends Role.Role<any>>(args: { type: TToken; data?: Role.Data<TToken> }) => boolean;
307
307
 
308
- const surfaces = capabilityManager.getAll(Capabilities.ReactSurface);
309
- const candidates = findCandidates(surfaces.flat(), {
310
- role: effectiveRole,
311
- data: args.data as Props['data'],
312
- });
313
- return candidates.length > 0;
314
- }
308
+ /**
309
+ * @returns a stable function that checks whether a contributed surface is available for a
310
+ * role & data. The surface manager is captured via context, so the returned function carries
311
+ * no dependency of its own and is safe to store and invoke later — e.g. from inside another
312
+ * callback such as a render-prop or event handler — since it calls no hooks itself.
313
+ */
314
+ export const useIsSurfaceAvailable = (): IsSurfaceAvailable => {
315
+ const surfaceManager = useSurfaceManager();
316
+ const registry = useContext(RegistryContext);
317
+ return useCallback<IsSurfaceAvailable>(
318
+ (args: { type: Role.Role<any>; data?: Props['data'] }) => {
319
+ const effectiveRole = args.type?.role;
320
+ if (effectiveRole == null) {
321
+ return false;
322
+ }
323
+
324
+ const candidates = matchCandidates(
325
+ registry.get(surfaceManager.candidatesAtom(effectiveRole)),
326
+ effectiveRole,
327
+ args.data,
328
+ );
329
+
330
+ return candidates.length > 0;
331
+ },
332
+ [surfaceManager, registry],
333
+ );
334
+ };