@dxos/app-framework 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (278) hide show
  1. package/dist/lib/browser/{capability-BBBBAPDI.mjs → capability-Q5XRXRD2.mjs} +10 -10
  2. package/dist/lib/browser/{capability-OP63CD5N.mjs → capability-V7LR4LQN.mjs} +11 -11
  3. package/dist/lib/browser/capability-V7LR4LQN.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs → chunk-23D4SJUE.mjs} +3 -3
  5. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs.map → chunk-23D4SJUE.mjs.map} +1 -1
  6. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs → chunk-3JWJXGLK.mjs} +1 -1
  7. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs.map → chunk-3JWJXGLK.mjs.map} +1 -1
  8. package/dist/lib/browser/{chunk-GX4TUNM6.mjs → chunk-3ZS2A3DN.mjs} +170 -226
  9. package/dist/lib/browser/chunk-3ZS2A3DN.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-I34GF4NG.mjs → chunk-45CHLTBV.mjs} +2 -2
  11. package/dist/lib/browser/chunk-5LAIGWLU.mjs +467 -0
  12. package/dist/lib/browser/chunk-5LAIGWLU.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-QSXYHXCE.mjs → chunk-66IXTIVK.mjs} +1 -1
  14. package/dist/lib/browser/{chunk-QSXYHXCE.mjs.map → chunk-66IXTIVK.mjs.map} +2 -2
  15. package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
  16. package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
  17. package/dist/lib/browser/chunk-G7SDBRKH.mjs +1 -0
  18. package/dist/lib/browser/chunk-JXCBZSBJ.mjs +372 -0
  19. package/dist/lib/browser/chunk-JXCBZSBJ.mjs.map +7 -0
  20. package/dist/lib/browser/chunk-MX5DKEJH.mjs +584 -0
  21. package/dist/lib/browser/chunk-MX5DKEJH.mjs.map +7 -0
  22. package/dist/lib/browser/{chunk-JKWMHZP6.mjs → chunk-WBHCSOBW.mjs} +2 -2
  23. package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
  24. package/dist/lib/browser/{chunk-FU4GAFUQ.mjs → chunk-Z55LVAGN.mjs} +80 -15
  25. package/dist/lib/browser/chunk-Z55LVAGN.mjs.map +7 -0
  26. package/dist/lib/browser/{chunk-F7FW2RK2.mjs → chunk-ZGJAZSNE.mjs} +7 -32
  27. package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
  28. package/dist/lib/browser/cli/index.mjs +11 -27
  29. package/dist/lib/browser/cli/index.mjs.map +2 -2
  30. package/dist/lib/browser/common/activation-events.mjs +7 -7
  31. package/dist/lib/browser/common/capabilities.mjs +7 -7
  32. package/dist/lib/browser/core/activation-event.mjs +1 -1
  33. package/dist/lib/browser/core/capability.mjs +1 -1
  34. package/dist/lib/browser/core/plugin-manager.mjs +6 -4
  35. package/dist/lib/browser/core/plugin.mjs +10 -2
  36. package/dist/lib/browser/core/url-loader.mjs +13 -5
  37. package/dist/lib/browser/index.mjs +22 -18
  38. package/dist/lib/browser/index.mjs.map +3 -3
  39. package/dist/lib/browser/{invoker-capability-H5PPENOC.mjs → invoker-capability-LNX4CGIV.mjs} +12 -11
  40. package/dist/lib/browser/invoker-capability-LNX4CGIV.mjs.map +7 -0
  41. package/dist/lib/browser/meta.json +1 -1
  42. package/dist/lib/browser/testing/index.mjs +144 -27
  43. package/dist/lib/browser/testing/index.mjs.map +4 -4
  44. package/dist/lib/browser/testing/react.mjs +78 -0
  45. package/dist/lib/browser/testing/react.mjs.map +7 -0
  46. package/dist/lib/browser/ui/index.mjs +18 -14
  47. package/dist/lib/node-esm/{capability-AWBEMRYR.mjs → capability-EW5GJCI6.mjs} +10 -10
  48. package/dist/lib/node-esm/{capability-WFEG6CIZ.mjs → capability-YKBMMD53.mjs} +11 -11
  49. package/dist/lib/node-esm/capability-YKBMMD53.mjs.map +7 -0
  50. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
  51. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
  52. package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs → chunk-6XW6LET6.mjs} +2 -2
  53. package/dist/lib/node-esm/{chunk-URWHJQT2.mjs → chunk-D347W3KO.mjs} +7 -32
  54. package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
  55. package/dist/lib/node-esm/chunk-D5PO2WXX.mjs +373 -0
  56. package/dist/lib/node-esm/chunk-D5PO2WXX.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-ULUEXB7Q.mjs → chunk-HTBJU5FX.mjs} +80 -15
  58. package/dist/lib/node-esm/chunk-HTBJU5FX.mjs.map +7 -0
  59. package/dist/lib/node-esm/chunk-KM2F6GH6.mjs +468 -0
  60. package/dist/lib/node-esm/chunk-KM2F6GH6.mjs.map +7 -0
  61. package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs → chunk-OZ7DZA5Z.mjs} +1 -1
  62. package/dist/lib/node-esm/{chunk-BCEOLX47.mjs → chunk-Q7XBFII4.mjs} +170 -226
  63. package/dist/lib/node-esm/chunk-Q7XBFII4.mjs.map +7 -0
  64. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs → chunk-SBS2YMPT.mjs} +3 -3
  65. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs.map → chunk-SBS2YMPT.mjs.map} +1 -1
  66. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs → chunk-SDJ4B2LU.mjs} +1 -1
  67. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs.map → chunk-SDJ4B2LU.mjs.map} +1 -1
  68. package/dist/lib/node-esm/{chunk-G3RTFSNG.mjs → chunk-WFSRZKBP.mjs} +2 -2
  69. package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
  70. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs +585 -0
  71. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs.map +7 -0
  72. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs → chunk-XOCUANHO.mjs} +1 -1
  73. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs.map → chunk-XOCUANHO.mjs.map} +2 -2
  74. package/dist/lib/node-esm/cli/index.mjs +11 -27
  75. package/dist/lib/node-esm/cli/index.mjs.map +2 -2
  76. package/dist/lib/node-esm/common/activation-events.mjs +7 -7
  77. package/dist/lib/node-esm/common/capabilities.mjs +7 -7
  78. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  79. package/dist/lib/node-esm/core/capability.mjs +1 -1
  80. package/dist/lib/node-esm/core/plugin-manager.mjs +6 -4
  81. package/dist/lib/node-esm/core/plugin.mjs +10 -2
  82. package/dist/lib/node-esm/core/url-loader.mjs +13 -5
  83. package/dist/lib/node-esm/index.mjs +22 -18
  84. package/dist/lib/node-esm/index.mjs.map +3 -3
  85. package/dist/lib/node-esm/{invoker-capability-S3ZA527J.mjs → invoker-capability-O4T5PHLA.mjs} +12 -11
  86. package/dist/lib/node-esm/invoker-capability-O4T5PHLA.mjs.map +7 -0
  87. package/dist/lib/node-esm/meta.json +1 -1
  88. package/dist/lib/node-esm/testing/index.mjs +144 -27
  89. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  90. package/dist/lib/node-esm/testing/react.mjs +79 -0
  91. package/dist/lib/node-esm/testing/react.mjs.map +7 -0
  92. package/dist/lib/node-esm/ui/index.mjs +18 -14
  93. package/dist/plugin/node-esm/index.mjs +480 -32
  94. package/dist/plugin/node-esm/index.mjs.map +4 -4
  95. package/dist/plugin/node-esm/meta.json +1 -1
  96. package/dist/types/src/common/capabilities.d.ts +2 -1
  97. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  98. package/dist/types/src/common/operations.d.ts +1 -1
  99. package/dist/types/src/common/operations.d.ts.map +1 -1
  100. package/dist/types/src/core/activation-event.d.ts +4 -4
  101. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  102. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  103. package/dist/types/src/core/capability.d.ts +2 -2
  104. package/dist/types/src/core/capability.d.ts.map +1 -1
  105. package/dist/types/src/core/index.d.ts +2 -0
  106. package/dist/types/src/core/index.d.ts.map +1 -1
  107. package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
  108. package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
  109. package/dist/types/src/core/plugin-manager.d.ts +51 -2
  110. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  111. package/dist/types/src/core/plugin-manifest.d.ts +76 -0
  112. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
  113. package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
  114. package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
  115. package/dist/types/src/core/plugin.d.ts +107 -6
  116. package/dist/types/src/core/plugin.d.ts.map +1 -1
  117. package/dist/types/src/core/url-loader.d.ts +90 -3
  118. package/dist/types/src/core/url-loader.d.ts.map +1 -1
  119. package/dist/types/src/helpers.d.ts.map +1 -1
  120. package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
  121. package/dist/types/src/plugin-operation/history/errors.d.ts +6 -6
  122. package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
  123. package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
  124. package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
  125. package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
  126. package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
  127. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
  128. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
  129. package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
  130. package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
  131. package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
  132. package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
  133. package/dist/types/src/plugin-operation/testing.d.ts +2 -1
  134. package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
  135. package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
  136. package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
  137. package/dist/types/src/testing/harness.d.ts +67 -0
  138. package/dist/types/src/testing/harness.d.ts.map +1 -0
  139. package/dist/types/src/testing/index.d.ts +1 -0
  140. package/dist/types/src/testing/index.d.ts.map +1 -1
  141. package/dist/types/src/testing/react.d.ts +27 -0
  142. package/dist/types/src/testing/react.d.ts.map +1 -0
  143. package/dist/types/src/testing/react.test.d.ts +2 -0
  144. package/dist/types/src/testing/react.test.d.ts.map +1 -0
  145. package/dist/types/src/testing/service.d.ts.map +1 -1
  146. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  147. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  148. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  149. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  150. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
  151. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
  152. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
  153. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
  154. package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
  155. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
  156. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  157. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
  158. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  159. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  160. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  161. package/dist/types/src/ui/components/Surface/index.d.ts +16 -6
  162. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  163. package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
  164. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  165. package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
  166. package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
  167. package/dist/types/src/ui/components/index.d.ts +1 -0
  168. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  169. package/dist/types/src/ui/hooks/useApp.d.ts +29 -3
  170. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  171. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  172. package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
  173. package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
  174. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
  175. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
  176. package/dist/types/src/vite-plugin/boot-loader/index.d.ts +52 -0
  177. package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
  178. package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
  179. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
  180. package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
  181. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
  182. package/dist/types/src/vite-plugin/index.d.ts +4 -2
  183. package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
  184. package/dist/types/src/vite-plugin/manifest.d.ts +37 -0
  185. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
  186. package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
  187. package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
  188. package/dist/types/src/vite-plugin/packages.d.ts +10 -4
  189. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  190. package/dist/types/tsconfig.tsbuildinfo +1 -1
  191. package/moon.yml +1 -0
  192. package/package.json +33 -59
  193. package/src/common/capabilities.ts +2 -1
  194. package/src/common/operations.ts +1 -1
  195. package/src/core/capability.ts +1 -1
  196. package/src/core/index.ts +2 -0
  197. package/src/core/plugin-asset-cache.ts +60 -0
  198. package/src/core/plugin-manager.test.ts +246 -5
  199. package/src/core/plugin-manager.ts +167 -25
  200. package/src/core/plugin-manifest.test.ts +48 -0
  201. package/src/core/plugin-manifest.ts +102 -0
  202. package/src/core/plugin.ts +135 -10
  203. package/src/core/url-loader.test.ts +104 -5
  204. package/src/core/url-loader.ts +226 -37
  205. package/src/plugin-operation/OperationPlugin.ts +2 -2
  206. package/src/plugin-operation/history/capability.ts +1 -1
  207. package/src/plugin-operation/history/history-tracker.test.ts +2 -1
  208. package/src/plugin-operation/history/history-tracker.ts +1 -1
  209. package/src/plugin-operation/history/types.ts +1 -1
  210. package/src/plugin-operation/history/undo-mapping.ts +1 -1
  211. package/src/plugin-operation/history/undo-registry.ts +1 -1
  212. package/src/plugin-operation/invoker-capability.ts +2 -1
  213. package/src/plugin-operation/testing.ts +2 -1
  214. package/src/plugin-runtime/RuntimePlugin.ts +2 -2
  215. package/src/testing/harness.ts +229 -0
  216. package/src/testing/index.ts +1 -0
  217. package/src/testing/react.test.tsx +48 -0
  218. package/src/testing/react.tsx +113 -0
  219. package/src/testing/withPluginManager.stories.tsx +1 -1
  220. package/src/ui/components/App/App.stories.tsx +1 -1
  221. package/src/ui/components/App/App.tsx +25 -2
  222. package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
  223. package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
  224. package/src/ui/components/Placeholder/index.ts +5 -0
  225. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +4 -2
  226. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  227. package/src/ui/components/Surface/SurfaceComponent.tsx +83 -46
  228. package/src/ui/components/Surface/index.ts +20 -1
  229. package/src/ui/components/Surface/types.test.ts +126 -0
  230. package/src/ui/components/Surface/types.ts +164 -12
  231. package/src/ui/components/index.ts +1 -0
  232. package/src/ui/hooks/useApp.tsx +165 -41
  233. package/src/ui/hooks/useLoading.tsx +14 -6
  234. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +263 -0
  235. package/src/vite-plugin/boot-loader/boot-loader.css +294 -0
  236. package/src/vite-plugin/boot-loader/boot-loader.js +274 -0
  237. package/src/vite-plugin/boot-loader/index.ts +112 -0
  238. package/src/vite-plugin/composer/index.ts +277 -0
  239. package/src/vite-plugin/import-map/index.ts +524 -0
  240. package/src/vite-plugin/index.ts +6 -2
  241. package/src/vite-plugin/manifest.test.ts +24 -0
  242. package/src/vite-plugin/manifest.ts +50 -0
  243. package/src/vite-plugin/packages.ts +169 -10
  244. package/tsconfig.json +9 -0
  245. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  246. package/dist/lib/browser/capability-OP63CD5N.mjs.map +0 -7
  247. package/dist/lib/browser/chunk-F7FW2RK2.mjs.map +0 -7
  248. package/dist/lib/browser/chunk-FU4GAFUQ.mjs.map +0 -7
  249. package/dist/lib/browser/chunk-GX4TUNM6.mjs.map +0 -7
  250. package/dist/lib/browser/chunk-JKWMHZP6.mjs.map +0 -7
  251. package/dist/lib/browser/chunk-LVJW5EFU.mjs +0 -157
  252. package/dist/lib/browser/chunk-LVJW5EFU.mjs.map +0 -7
  253. package/dist/lib/browser/chunk-RFSO3JRG.mjs +0 -1
  254. package/dist/lib/browser/chunk-WPE6AL7I.mjs +0 -905
  255. package/dist/lib/browser/chunk-WPE6AL7I.mjs.map +0 -7
  256. package/dist/lib/browser/invoker-capability-H5PPENOC.mjs.map +0 -7
  257. package/dist/lib/node-esm/capability-WFEG6CIZ.mjs.map +0 -7
  258. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs +0 -158
  259. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs.map +0 -7
  260. package/dist/lib/node-esm/chunk-BCEOLX47.mjs.map +0 -7
  261. package/dist/lib/node-esm/chunk-G3RTFSNG.mjs.map +0 -7
  262. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs +0 -906
  263. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs.map +0 -7
  264. package/dist/lib/node-esm/chunk-ULUEXB7Q.mjs.map +0 -7
  265. package/dist/lib/node-esm/chunk-URWHJQT2.mjs.map +0 -7
  266. package/dist/lib/node-esm/invoker-capability-S3ZA527J.mjs.map +0 -7
  267. package/dist/types/src/vite-plugin/composer-plugin.d.ts +0 -18
  268. package/dist/types/src/vite-plugin/composer-plugin.d.ts.map +0 -1
  269. package/dist/types/src/vite-plugin/import-map-plugin.d.ts +0 -16
  270. package/dist/types/src/vite-plugin/import-map-plugin.d.ts.map +0 -1
  271. package/src/vite-plugin/composer-plugin.ts +0 -128
  272. package/src/vite-plugin/import-map-plugin.ts +0 -314
  273. /package/dist/lib/browser/{capability-BBBBAPDI.mjs.map → capability-Q5XRXRD2.mjs.map} +0 -0
  274. /package/dist/lib/browser/{chunk-I34GF4NG.mjs.map → chunk-45CHLTBV.mjs.map} +0 -0
  275. /package/dist/lib/browser/{chunk-RFSO3JRG.mjs.map → chunk-G7SDBRKH.mjs.map} +0 -0
  276. /package/dist/lib/node-esm/{capability-AWBEMRYR.mjs.map → capability-EW5GJCI6.mjs.map} +0 -0
  277. /package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs.map → chunk-6XW6LET6.mjs.map} +0 -0
  278. /package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs.map → chunk-OZ7DZA5Z.mjs.map} +0 -0
@@ -6,8 +6,63 @@ import type { FC, PropsWithChildren, ReactNode, RefCallback } from 'react';
6
6
 
7
7
  import type { MakeOptional, Position } from '@dxos/util';
8
8
 
9
+ /**
10
+ * A typed role token. Carries a role string for runtime dispatch plus a phantom
11
+ * data type so consumers using `type={SomeToken}` and providers using
12
+ * `AppSurface.object(SomeToken, ...)` share a single type-level contract.
13
+ *
14
+ * Mint via {@link makeType}.
15
+ */
16
+ export type RoleToken<TData> = {
17
+ readonly role: string;
18
+ /** Covariant phantom; never accessed at runtime. */
19
+ readonly _phantom?: TData;
20
+ };
21
+
22
+ /**
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.
25
+ */
26
+ export type SurfaceBinding = {
27
+ readonly role: string;
28
+ readonly guard: (data: unknown) => boolean;
29
+ };
30
+
31
+ /**
32
+ * Typed filter binding for {@link create}. Combines one or more `(role, guard)`
33
+ * pairs so a provider can register against multiple roles while keeping the
34
+ * role and its data shape in a single expression.
35
+ */
36
+ export type SurfaceFilter<TData> = {
37
+ readonly bindings: ReadonlyArray<SurfaceBinding>;
38
+ /** Covariant phantom; never accessed at runtime. */
39
+ readonly _phantom?: TData;
40
+ };
41
+
42
+ /**
43
+ * Narrow data type carried by a role token.
44
+ */
45
+ export type TokenData<T> = T extends RoleToken<infer D> ? D : never;
46
+
47
+ /**
48
+ * Runtime guard for {@link SurfaceFilter}. Distinguishes new-style filter
49
+ * bindings from legacy predicate filters.
50
+ */
51
+ export const isSurfaceFilter = (value: unknown): value is SurfaceFilter<any> =>
52
+ typeof value === 'object' && value !== null && Array.isArray((value as { bindings?: unknown }).bindings);
53
+
54
+ /**
55
+ * Mints a typed role token. Identity is structural; all tokens with the same
56
+ * `role` string are interchangeable at runtime.
57
+ */
58
+ export const makeType = <TData>(role: string): RoleToken<TData> => ({ role });
59
+
9
60
  /**
10
61
  * Props that are passed to the Surface component.
62
+ *
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.
11
66
  */
12
67
  export type Props<T extends Record<string, any> = Record<string, unknown>> = {
13
68
  /**
@@ -21,16 +76,46 @@ export type Props<T extends Record<string, any> = Record<string, unknown>> = {
21
76
  * The placeholder component will be rendered while the surface component is loading.
22
77
  */
23
78
  placeholder?: ReactNode;
24
- } & MakeOptional<CoreProps<T>, 'id' | 'data'> &
25
- /**
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
+ } /**
26
89
  * Additional props to pass to the component.
27
90
  * These props are not used by Surface itself but may be used by components which resolve the surface.
28
91
  * Exclude known prop names to prevent overriding well-defined props.
29
- */
30
- {
31
- [K in keyof Record<string, any>]: K extends keyof CoreProps<T> | 'fallback' | 'placeholder' ? never : any;
92
+ */ & {
93
+ [K in keyof Record<string, any>]: K extends keyof CoreProps<T> | 'fallback' | 'placeholder' | 'type' ? never : any;
32
94
  };
33
95
 
96
+ /**
97
+ * Typed Surface consumer props — carries the role/data-shape contract via a
98
+ * {@link RoleToken}. Available as a typed overload of the Surface component so
99
+ * ad-hoc `type` fields on arbitrary spread props don't conflict with the
100
+ * untyped consumer form.
101
+ */
102
+ export type TypedProps<TToken extends RoleToken<any>> = {
103
+ fallback?: FC<{ error: Error; data?: any }>;
104
+ placeholder?: ReactNode;
105
+ id?: string;
106
+ type: TToken;
107
+ data?: TokenData<TToken>;
108
+ limit?: number | undefined;
109
+ } & {
110
+ /**
111
+ * Additional pass-through props. Known prop names are excluded so the
112
+ * catch-all doesn't widen `data` / `type` / etc. to `any` at the intersection.
113
+ */
114
+ [K in keyof Record<string, any>]: K extends 'fallback' | 'placeholder' | 'id' | 'type' | 'data' | 'limit'
115
+ ? never
116
+ : any;
117
+ };
118
+
34
119
  /**
35
120
  * NOTE: If `[key: string]: unknown` is included in shared types, when re-used other fields become unknown as well.
36
121
  */
@@ -41,7 +126,10 @@ export type CoreProps<T extends Record<string, any> = Record<string, unknown>> =
41
126
  id: string;
42
127
 
43
128
  /**
44
- * Role defines how the data should be rendered.
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.
45
133
  */
46
134
  role: string;
47
135
 
@@ -71,6 +159,10 @@ export type ComponentFunction<T extends Record<string, any> = Record<string, any
71
159
 
72
160
  /**
73
161
  * Definition of when a React component surface should be rendered.
162
+ *
163
+ * The optional `filter` receives the consumer-supplied `role` as its second
164
+ * argument so a single definition can encode role-specific guards (used by the
165
+ * {@link SurfaceFilter}-based `create` overload).
74
166
  */
75
167
  export type ReactDefinition<T extends Record<string, any> = any> = Readonly<{
76
168
  kind: 'react';
@@ -78,7 +170,7 @@ export type ReactDefinition<T extends Record<string, any> = any> = Readonly<{
78
170
  role: string | string[];
79
171
  position?: Position;
80
172
  component: ComponentFunction<T>;
81
- filter?: (data: Record<string, unknown>) => data is T;
173
+ filter?: (data: Record<string, unknown>, role?: string) => data is T;
82
174
  }>;
83
175
 
84
176
  /**
@@ -94,7 +186,7 @@ export type WebComponentDefinition<T extends Record<string, any> = any> = Readon
94
186
  * The Web Component will receive the same props as React surfaces via properties/attributes.
95
187
  */
96
188
  tagName: string;
97
- filter?: (data: Record<string, unknown>) => data is T;
189
+ filter?: (data: Record<string, unknown>, role?: string) => data is T;
98
190
  }>;
99
191
 
100
192
  /**
@@ -102,16 +194,76 @@ export type WebComponentDefinition<T extends Record<string, any> = any> = Readon
102
194
  */
103
195
  export type Definition<T extends Record<string, any> = any> = ReactDefinition<T> | WebComponentDefinition<T>;
104
196
 
197
+ /**
198
+ * Typed React surface definition — role is derived from the filter's bindings.
199
+ */
200
+ export type TypedReactDefinition<T extends Record<string, any> = any> = Readonly<{
201
+ id: string;
202
+ filter: SurfaceFilter<T>;
203
+ component: ComponentFunction<T>;
204
+ position?: Position;
205
+ }>;
206
+
207
+ /**
208
+ * Typed Web Component surface definition.
209
+ */
210
+ export type TypedWebComponentDefinition<T extends Record<string, any> = any> = Readonly<{
211
+ id: string;
212
+ filter: SurfaceFilter<T>;
213
+ tagName: string;
214
+ position?: Position;
215
+ }>;
216
+
217
+ const expandBindings = <T extends Record<string, any>>(
218
+ filter: SurfaceFilter<T>,
219
+ ): { role: string | string[]; guard: (data: Record<string, unknown>, role?: string) => data is T } => {
220
+ const bindings = filter.bindings;
221
+ const roles = Array.from(new Set(bindings.map((binding) => binding.role)));
222
+ const guard = (data: Record<string, unknown>, role?: string): data is T => {
223
+ if (role != null) {
224
+ // Multiple bindings may share a role (e.g. via `oneOf` of same-role filters);
225
+ // the role matches if ANY of them passes.
226
+ return bindings.some((entry) => entry.role === role && entry.guard(data));
227
+ }
228
+ return bindings.some((entry) => entry.guard(data));
229
+ };
230
+ return { role: roles.length === 1 ? roles[0] : roles, guard };
231
+ };
232
+
105
233
  /**
106
234
  * Creates a React surface definition.
107
235
  */
108
- export const create = <T extends Record<string, any> = any>(
236
+ export function create<T extends Record<string, any> = any>(definition: TypedReactDefinition<T>): ReactDefinition<T>;
237
+ export function create<T extends Record<string, any> = any>(
109
238
  definition: Omit<ReactDefinition<T>, 'kind'>,
110
- ): ReactDefinition<T> => ({ ...definition, kind: 'react' });
239
+ ): ReactDefinition<T>;
240
+ export function create<T extends Record<string, any> = any>(
241
+ definition: TypedReactDefinition<T> | Omit<ReactDefinition<T>, 'kind'>,
242
+ ): ReactDefinition<T> {
243
+ if (isSurfaceFilter(definition.filter)) {
244
+ const { id, filter, component, position } = definition as TypedReactDefinition<T>;
245
+ const { role, guard } = expandBindings(filter);
246
+ return { kind: 'react', id, role, position, component, filter: guard };
247
+ }
248
+ return { ...(definition as Omit<ReactDefinition<T>, 'kind'>), kind: 'react' };
249
+ }
111
250
 
112
251
  /**
113
252
  * Creates a Web Component surface definition.
114
253
  */
115
- export const createWeb = <T extends Record<string, any> = any>(
254
+ export function createWeb<T extends Record<string, any> = any>(
255
+ definition: TypedWebComponentDefinition<T>,
256
+ ): WebComponentDefinition<T>;
257
+ export function createWeb<T extends Record<string, any> = any>(
116
258
  definition: Omit<WebComponentDefinition<T>, 'kind'>,
117
- ): WebComponentDefinition<T> => ({ ...definition, kind: 'web-component' });
259
+ ): WebComponentDefinition<T>;
260
+ export function createWeb<T extends Record<string, any> = any>(
261
+ definition: TypedWebComponentDefinition<T> | Omit<WebComponentDefinition<T>, 'kind'>,
262
+ ): WebComponentDefinition<T> {
263
+ if (isSurfaceFilter(definition.filter)) {
264
+ const { id, filter, tagName, position } = definition as TypedWebComponentDefinition<T>;
265
+ const { role, guard } = expandBindings(filter);
266
+ return { kind: 'web-component', id, role, position, tagName, filter: guard };
267
+ }
268
+ return { ...(definition as Omit<WebComponentDefinition<T>, 'kind'>), kind: 'web-component' };
269
+ }
@@ -3,5 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './App';
6
+ export * from './Placeholder';
6
7
  export * from './PluginManager';
7
8
  export * from './Surface';
@@ -30,8 +30,33 @@ export type StartupProgress = {
30
30
  total: number;
31
31
  /** Fractional progress (0-1). */
32
32
  progress: number;
33
- /** Human-readable label for the currently activating module. */
34
- status?: string;
33
+ /**
34
+ * Raw activation event key (e.g. `org.dxos.app-framework.event.startup`).
35
+ * Set on event-level transitions, *and* on module-level transitions where
36
+ * it carries the parent activation event that first triggered the
37
+ * module's load (plumbed through `_loadCapabilitiesForModules` →
38
+ * `_loadModule`). Consumers can use this either as the primary id (when
39
+ * {@link module} is absent) or as an extra "context" field alongside
40
+ * {@link module}.
41
+ */
42
+ event?: string;
43
+ /**
44
+ * Raw module id (e.g. `org.dxos.plugin.observability.module.ReactSurface`)
45
+ * when the in-flight activation is module-level. When present,
46
+ * {@link event} may also be set, identifying the parent activation that
47
+ * triggered this module's load.
48
+ */
49
+ module?: string;
50
+ /**
51
+ * Pre-humanized label for the currently surfaced transition (module
52
+ * label if {@link module} is set, otherwise the event label), supplied
53
+ * for consumers that want a sensible default. Hosts that prefer to
54
+ * render their own label can read the raw {@link event}/{@link module}
55
+ * fields and ignore this — the framework leaves the policy choice
56
+ * (which transitions to surface, how to format them, whether to drop
57
+ * sub-modules entirely) to the host's `Placeholder`.
58
+ */
59
+ humanizedName?: string;
35
60
  };
36
61
 
37
62
  export type PlaceholderProps = {
@@ -42,6 +67,7 @@ export type PlaceholderProps = {
42
67
  export type UseAppOptions = {
43
68
  pluginManager?: PluginManager.PluginManager;
44
69
  pluginLoader?: PluginManager.ManagerOptions['pluginLoader'];
70
+ onPluginRemove?: PluginManager.ManagerOptions['onRemove'];
45
71
  plugins?: Plugin.Plugin[];
46
72
  core?: string[];
47
73
  defaults?: string[];
@@ -86,6 +112,7 @@ export type UseAppOptions = {
86
112
  export const useApp = ({
87
113
  pluginManager,
88
114
  pluginLoader: pluginLoaderProp,
115
+ onPluginRemove,
89
116
  plugins: pluginsProp,
90
117
  core: coreProp,
91
118
  defaults: defaultsProp,
@@ -131,10 +158,10 @@ export const useApp = ({
131
158
  );
132
159
  const isExternalManager = !!pluginManager;
133
160
  const manager = useMemo(() => {
134
- const mgr = pluginManager ?? PluginManager.make({ pluginLoader, plugins, core, enabled });
161
+ const mgr = pluginManager ?? PluginManager.make({ pluginLoader, plugins, core, enabled, onRemove: onPluginRemove });
135
162
  log('useApp: useMemo created/reused manager', { provided: !!pluginManager });
136
163
  return mgr;
137
- }, [pluginManager, pluginLoader, plugins, core, enabled]);
164
+ }, [pluginManager, pluginLoader, plugins, core, enabled, onPluginRemove]);
138
165
 
139
166
  useEffect(() => {
140
167
  if (!cacheEnabled) {
@@ -164,38 +191,89 @@ export const useApp = ({
164
191
  module: 'org.dxos.app-framework.atom-registry',
165
192
  });
166
193
 
167
- // Poll manager atoms for progress (avoids PubSub subscription race).
168
- const progressInterval = setInterval(() => {
169
- if (readyRef.current) {
170
- clearInterval(progressInterval);
171
- return;
172
- }
173
- const active = manager.getActive();
174
- const modules = manager.getModules();
175
- const total = modules.length;
176
- const activated = active.length;
177
- const lastModule = active.length > 0 ? active[active.length - 1] : undefined;
178
- setStartupProgress({
179
- activated,
180
- total,
181
- progress: total > 0 ? activated / total : 0,
182
- status: lastModule ? humanizeModuleId(lastModule) : undefined,
183
- });
184
- }, 100);
185
-
186
194
  const fiber = Effect.gen(function* () {
187
195
  const queue = yield* PubSub.subscribe(manager.activation);
188
196
  const listener = yield* Effect.forkDaemon(
189
197
  Queue.take(queue).pipe(
190
- Effect.tap(({ event, state, error: error$ }) =>
198
+ Effect.tap(({ event, state, module, error: error$ }) =>
191
199
  Effect.sync(() => {
192
- if (event === ActivationEvents.Startup.id && state === 'activated') {
200
+ // Event-level Startup activated (no `module` field) fires once,
201
+ // after every module triggered by Startup has finished. Module
202
+ // activations now also carry their parent event id (so the trace
203
+ // can attribute a module to its triggering event), which means
204
+ // each module activated under Startup publishes
205
+ // `{ event: 'startup', state: 'activated', module: <id> }`. Without
206
+ // the `!module` guard the listener would mark the app ready on
207
+ // the *first* such module rather than waiting for the event-level
208
+ // completion — leaving downstream capabilities (operation-invoker,
209
+ // app-graph, …) un-registered when the placeholder dismisses.
210
+ if (event === ActivationEvents.Startup.id && state === 'activated' && !module) {
193
211
  clearTimeout(timeoutId);
194
- clearInterval(progressInterval);
195
212
  setReady(true);
196
213
  readyRef.current = true;
197
214
  // Trigger startup profiler dump if available.
198
215
  (globalThis as any).composer?.profiler?.dump();
216
+ // Notify any host observability layer that startup completed.
217
+ // A `CustomEvent` keeps this generic — app-framework doesn't
218
+ // import a provider, and consumers can capture the startup
219
+ // summary without us picking one.
220
+ if (typeof window !== 'undefined') {
221
+ window.dispatchEvent(new CustomEvent('app-framework:startup-activated'));
222
+ }
223
+ return;
224
+ }
225
+ // `activating` is the start-of-load signal. Surface the raw
226
+ // `module` (or `event`) plus a pre-humanized label so the
227
+ // host placeholder can decide what to render — show
228
+ // everything, suppress noisy sub-modules, group by plugin,
229
+ // or apply its own formatting. We intentionally do NOT touch
230
+ // these fields on `activated`: pairing the two would cause
231
+ // back-to-back identical updates to the host's effect, which
232
+ // the boot loader treats as a re-trigger because
233
+ // `progress.progress` moved. Leaving the label alone on
234
+ // completion keeps it accurate ("now activating X") until
235
+ // the next module starts.
236
+ if (module && state === 'activating' && !readyRef.current) {
237
+ setStartupProgress((current) => ({
238
+ ...current,
239
+ // `event` here is the activation event that first
240
+ // triggered this module load (plumbed through
241
+ // `_loadCapabilitiesForModules` → `_loadModule`).
242
+ // Falsy/empty falls back to undefined so consumers can
243
+ // tell "no parent context" from "parent context: <X>".
244
+ event: event || undefined,
245
+ module,
246
+ humanizedName: humanizeModuleId(module),
247
+ }));
248
+ }
249
+ // Update the activation count when a module commits. The
250
+ // ring's fraction comes from this; `event`/`module`/
251
+ // `humanizedName` were set by the matching `activating`
252
+ // message above and are left alone so the count can advance
253
+ // without re-firing the host's status callback.
254
+ if (module && state === 'activated' && !readyRef.current) {
255
+ const active = manager.getActive();
256
+ const total = manager.getModules().length;
257
+ setStartupProgress((current) => ({
258
+ ...current,
259
+ activated: active.length,
260
+ total,
261
+ progress: total > 0 ? active.length / total : 0,
262
+ }));
263
+ }
264
+ // Event-level `activating` (no `module`) — fired at the start
265
+ // of `_activateModulesForEvent` and recursively for each
266
+ // before/after event. Surfaces a label during the gap before
267
+ // the first module-level message lands; subsequent module
268
+ // updates immediately overwrite this with a more specific
269
+ // label.
270
+ if (event && !module && state === 'activating' && !readyRef.current) {
271
+ setStartupProgress((current) => ({
272
+ ...current,
273
+ event,
274
+ module: undefined,
275
+ humanizedName: humanizeEventKey(event),
276
+ }));
199
277
  }
200
278
  if (error$ && !readyRef.current) {
201
279
  setError(error$);
@@ -232,7 +310,6 @@ export const useApp = ({
232
310
  return () => {
233
311
  log('useApp: effect cleanup');
234
312
  clearTimeout(timeoutId);
235
- clearInterval(progressInterval);
236
313
  void runAndForwardErrors(Fiber.interrupt(fiber));
237
314
  if (!isExternalManager) {
238
315
  void runAndForwardErrors(manager.shutdown());
@@ -240,6 +317,9 @@ export const useApp = ({
240
317
  };
241
318
  }, [manager]);
242
319
 
320
+ const progressRef = useRef(startupProgress);
321
+ progressRef.current = startupProgress;
322
+
243
323
  return useCallback(
244
324
  () => (
245
325
  <ErrorBoundary name='app' FallbackComponent={fallback}>
@@ -251,7 +331,7 @@ export const useApp = ({
251
331
  ready={ready}
252
332
  error={error}
253
333
  debounce={debounce}
254
- progress={startupProgress}
334
+ progress={progressRef.current}
255
335
  />
256
336
  </RegistryContext.Provider>
257
337
  </ContextProtocolProvider>
@@ -269,21 +349,65 @@ const setupDevtools = (manager: PluginManager.PluginManager) => {
269
349
 
270
350
  /**
271
351
  * Extracts a human-readable label from a module ID.
272
- * E.g., "org.dxos.plugin.markdown.module.ReactSurface" → "Markdown".
352
+ *
353
+ * Module IDs follow `org.dxos.plugin.<plugin-slug>.module.<module-name>`,
354
+ * where `<plugin-slug>` is kebab-case and `<module-name>` is either an
355
+ * explicit string (often kebab-case, e.g. `'observability'`, `'namespace'`)
356
+ * or a capability tag in PascalCase from `Capability.getModuleTag(...)`
357
+ * (e.g. `'ReactSurface'`, `'AppGraphBuilder'`). The output is
358
+ * `"Title Case Plugin: kebab-module"` so the visible status names both the
359
+ * plugin and the aspect being activated, helping disambiguate the multiple
360
+ * modules a plugin contributes.
361
+ *
362
+ * Examples:
363
+ * - "org.dxos.plugin.markdown.module.ReactSurface" → "Markdown: react-surface"
364
+ * - "org.dxos.plugin.observability.module.AppGraphBuilder" → "Observability: app-graph-builder"
365
+ * - "org.dxos.plugin.observability.module.observability" → "Observability"
366
+ * (the module name matches the plugin slug — collapsed to avoid
367
+ * "Observability: observability" noise.)
273
368
  */
274
369
  const humanizeModuleId = (moduleId: string): string => {
275
- // Extract plugin name from pattern: org.dxos.plugin.<name>.module.<capability>
276
- const pluginMatch = moduleId.match(/\.plugin\.([^.]+)\./);
277
- if (pluginMatch) {
278
- const name = pluginMatch[1];
279
- // Convert kebab-case to title case.
280
- return name
281
- .split('-')
282
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
283
- .join(' ');
370
+ const match = moduleId.match(/\.plugin\.([^.]+)\.module\.(.+)$/);
371
+ if (!match) {
372
+ // Fallback: use the last segment.
373
+ const parts = moduleId.split('.');
374
+ return parts[parts.length - 1];
284
375
  }
376
+ const [, pluginSlug, moduleName] = match;
377
+ const pluginLabel = pluginSlug
378
+ .split('-')
379
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
380
+ .join(' ');
381
+ // Normalise the module name to kebab-case so PascalCase capability tags
382
+ // ("ReactSurface") read consistently with explicit kebab IDs
383
+ // ("operation-handler"). The two-step substitution handles consecutive
384
+ // uppercase runs (`URLLoader` → `url-loader`) without splitting them
385
+ // mid-acronym.
386
+ const moduleLabel = moduleName
387
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
388
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
389
+ .toLowerCase();
390
+ // Capability modules whose name matches the plugin slug (e.g. the
391
+ // observability plugin's `observability` capability module) would render
392
+ // as "Observability: observability" — drop the redundant suffix.
393
+ if (moduleLabel === pluginSlug) {
394
+ return pluginLabel;
395
+ }
396
+ return `${pluginLabel}: ${moduleLabel}`;
397
+ };
285
398
 
286
- // Fallback: use the last segment.
287
- const parts = moduleId.split('.');
288
- return parts[parts.length - 1];
399
+ /**
400
+ * Extracts a human-readable label from an activation event key.
401
+ * E.g., "org.dxos.app-framework.event.setup-react-surface" → "Setup React Surface".
402
+ */
403
+ const humanizeEventKey = (eventKey: string): string => {
404
+ // Strip a leading specifier (composite key form: "<id>:<specifier>").
405
+ const id = eventKey.split(':')[0];
406
+ // Match the trailing segment after `.event.`.
407
+ const match = id.match(/\.event\.(.+)$/);
408
+ const slug = match ? match[1] : (id.split('.').pop() ?? id);
409
+ return slug
410
+ .split('-')
411
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
412
+ .join(' ');
289
413
  };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useEffect, useState } from 'react';
5
+ import { useEffect, useRef, useState } from 'react';
6
6
 
7
7
  export enum LoadingState {
8
8
  Loading = 0,
@@ -23,6 +23,14 @@ export enum LoadingState {
23
23
  */
24
24
  export const useLoading = (ready: boolean, debounce = 0) => {
25
25
  const [stage, setStage] = useState<LoadingState>(LoadingState.Loading);
26
+ // Mirror `ready` into a ref so the interval's `setStage` callback can read
27
+ // the latest value without depending on the effect re-running. The pure
28
+ // closure-capture pattern (with `ready` in deps) sticks the FSM at
29
+ // `FadeIn` whenever HMR doesn't propagate the dep change end-to-end —
30
+ // a ref sidesteps that entirely and fires the read on every tick.
31
+ const readyRef = useRef(ready);
32
+ readyRef.current = ready;
33
+
26
34
  useEffect(() => {
27
35
  if (!debounce) {
28
36
  return;
@@ -30,18 +38,18 @@ export const useLoading = (ready: boolean, debounce = 0) => {
30
38
 
31
39
  const i = setInterval(() => {
32
40
  setStage((stage) => {
41
+ const isReady = readyRef.current;
33
42
  switch (stage) {
34
43
  case LoadingState.Loading: {
35
- if (!ready) {
44
+ if (!isReady) {
36
45
  return LoadingState.FadeIn;
37
- } else {
38
- clearInterval(i);
39
- return LoadingState.Done;
40
46
  }
47
+ clearInterval(i);
48
+ return LoadingState.Done;
41
49
  }
42
50
 
43
51
  case LoadingState.FadeIn: {
44
- if (ready) {
52
+ if (isReady) {
45
53
  return LoadingState.FadeOut;
46
54
  }
47
55
  break;