@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
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './HomeSection';
@@ -0,0 +1,70 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren, useState } from 'react';
6
+
7
+ import { Button, Input, Popover } from '@dxos/react-ui';
8
+
9
+ export type NamePopoverProps = PropsWithChildren<{
10
+ open: boolean;
11
+ placeholder: string;
12
+ /** Translated label for the submit button (e.g. "Create"). */
13
+ submitLabel: string;
14
+ onSubmit: (name: string) => void;
15
+ onCancel: () => void;
16
+ }>;
17
+
18
+ /**
19
+ * Name-entry popover anchored to its trigger (mirrors the object rename popover UX).
20
+ * Enter or the submit button commits — an empty name is allowed; Escape or dismissal cancels.
21
+ */
22
+ export const NamePopover = ({ children, open, placeholder, submitLabel, onSubmit, onCancel }: NamePopoverProps) => {
23
+ const [value, setValue] = useState('');
24
+
25
+ const submit = () => {
26
+ onSubmit(value);
27
+ setValue('');
28
+ };
29
+
30
+ const cancel = () => {
31
+ onCancel();
32
+ setValue('');
33
+ };
34
+
35
+ return (
36
+ <Popover.Root open={open} onOpenChange={(next) => !next && cancel()}>
37
+ <Popover.Trigger asChild>{children}</Popover.Trigger>
38
+ <Popover.Portal>
39
+ <Popover.Content>
40
+ <div className='flex items-center gap-1 p-2'>
41
+ <Input.Root>
42
+ <Input.Label srOnly>{placeholder}</Input.Label>
43
+ <Input.TextInput
44
+ autoFocus
45
+ placeholder={placeholder}
46
+ value={value}
47
+ onChange={(event) => setValue(event.target.value)}
48
+ onKeyDown={(event) => {
49
+ if (event.key === 'Enter') {
50
+ submit();
51
+ } else if (event.key === 'Escape') {
52
+ event.preventDefault();
53
+ event.stopPropagation();
54
+ cancel();
55
+ }
56
+ }}
57
+ />
58
+ </Input.Root>
59
+ <Button variant='primary' onClick={submit}>
60
+ {submitLabel}
61
+ </Button>
62
+ </div>
63
+ <Popover.Arrow />
64
+ </Popover.Content>
65
+ </Popover.Portal>
66
+ </Popover.Root>
67
+ );
68
+ };
69
+
70
+ NamePopover.displayName = 'NamePopover';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './NamePopover';
@@ -0,0 +1,136 @@
1
+ # Surface
2
+
3
+ A Surface is a named region of the UI that is populated dynamically by plugins.
4
+ The consumer declares _what kind_ of region it is (a role) and _what data_ it
5
+ carries; the framework resolves which plugin-contributed component(s) render there.
6
+ This decouples the shell/layout from the plugins that fill it.
7
+
8
+ ## Concepts
9
+
10
+ - **RoleToken** (`makeType<TData>(nsid)`) — a typed dispatch key. Carries a role
11
+ NSID (e.g. `org.dxos.role.article`) plus a phantom `TData` so consumers
12
+ (`type={Token}`) and providers (`makeFilter(Token, guard)`) share one
13
+ type-level contract. Identity is structural: same `role` string ⇒ interchangeable.
14
+ - **SurfaceFilter** (`makeFilter(token, guard?)`) — one or more `(role, guard)`
15
+ bindings. The guard validates the data shape at dispatch time; omitting it
16
+ matches any data at the role.
17
+ - **Definition** — a registered contribution: `{ kind, id, role, position?, filter, … }`.
18
+ `kind: 'react'` carries a `component`; `kind: 'web-component'` carries a `tagName`.
19
+ Authored via `create` / `createWeb`; registered as the `Capabilities.ReactSurface`
20
+ capability.
21
+
22
+ ## Dispatch pipeline
23
+
24
+ ```text
25
+ <Surface type={Token} data={…} limit?={n} />
26
+ └─ useSurfaces() → role-indexed, position-sorted candidate map
27
+ └─ findCandidates(role,data)→ candidates for role, then data guard filter
28
+ └─ Suspense
29
+ └─ SurfaceContextProvider (per candidate) ← ErrorBoundary + SurfaceContext
30
+ └─ React component | WebComponentWrapper
31
+ ```
32
+
33
+ - **Indexing.** A per-manager derived atom builds a `Map<role, Definition[]>`
34
+ once per contribution change with candidates already sorted by
35
+ `Position.compare`. Per-render work is therefore just the data-dependent guard
36
+ filter over one role's bucket — no full scan or re-sort on every render. The
37
+ data guard is intentionally _not_ memoized so a Surface re-dispatches when
38
+ reactive `data` changes. `indexByRole` itself is a pure group-and-sort; the
39
+ `SurfaceManager` instance filters out definitions with an invalid local id
40
+ before indexing (warning once per id via instance state, not a module global —
41
+ so distinct manager instances, e.g. across tests, don't share warning state).
42
+ - **Per-role subscription.** Each Surface subscribes to a derived atom for _its
43
+ role only_ (`getCandidatesAtom`). The atom wraps the role's bucket with
44
+ `Data.array`, so the atom registry compares results structurally (`Equal.equals`)
45
+ and a contribution to another role recomputes to an equal value and is dropped —
46
+ that role's subscribers never re-render. Re-renders therefore scale with the
47
+ _affected role's_ surface count, not the total mounted fleet.
48
+
49
+ Quantified (`SurfaceComponent.test.tsx`, 100 surfaces across 10 roles, one
50
+ contribution per role): the old global subscription re-renders the whole fleet
51
+ on every contribution (100/contribution); per-role re-renders only the affected
52
+ role (20/contribution — one role's surfaces ×2 store commit phases) → **5×
53
+ fewer re-renders**, and the ratio grows with the number of roles. A second case
54
+ confirms 50 unrelated contributions cause 0 re-renders of an unaffected role.
55
+
56
+ - **`limit`.** Caps how many resolved candidates render (e.g. `limit={1}` for
57
+ single-component regions).
58
+ - **`useIsSurfaceAvailable`.** Same matching logic, exposed as `Surface.useIsAvailable`.
59
+ The hook captures the surface manager via context and returns a plain, stable
60
+ function — safe to store and call later (e.g. inside another callback such as
61
+ a render-prop or event handler) since the returned function calls no hooks
62
+ itself — used to probe whether a region would render without mounting it.
63
+
64
+ ## Boundaries
65
+
66
+ Each matched candidate is wrapped in:
67
+
68
+ - **ErrorBoundary** (`name='surface'`, `resetKeys={[data]}`) — a failing surface
69
+ is contained and shown via the `fallback` component; sibling surfaces survive.
70
+ - **SurfaceContext** — exposes `{ id, role, data }` to descendants via
71
+ `useSurface()`. This is the supported way to read surface identity; the shell
72
+ must not reach _into_ a surface.
73
+
74
+ A single `Suspense` (the consumer's `placeholder`) wraps all candidates.
75
+
76
+ ### No `ref`
77
+
78
+ A Surface is a boundary, not a handle. It may resolve to zero, one, or many
79
+ components, so a forwarded `ref` has no well-defined target. The component does
80
+ not accept or forward a `ref`; consumers that need an element should own one
81
+ inside their contributed component.
82
+
83
+ ### Production vs. debug DOM
84
+
85
+ In production a React candidate renders directly inside its context provider —
86
+ no wrapper element — so a Surface adds no layout box of its own. Debug builds
87
+ wrap each candidate in a `<dx-surface>` element (see below) that carries
88
+ `data-id` / `data-role` and provides a measurable boundary for overlays.
89
+
90
+ ## Web Components
91
+
92
+ `createWeb({ tagName, … })` contributes a custom element instead of a React
93
+ component. `WebComponentWrapper` imperatively creates the element once, assigns
94
+ surface props as element properties, keeps them in sync, and removes the element
95
+ on unmount. The same props (`id`, `role`, `data`, `limit`, …) are passed as for
96
+ React surfaces.
97
+
98
+ ## Debugging
99
+
100
+ Enabled when `VITE_DEBUG` is set (build-time) or the runtime flag is toggled
101
+ (no rebuild). When on:
102
+
103
+ - Each React candidate is wrapped in a `<dx-surface data-role data-id>` element —
104
+ named, inspectable in DevTools, and queryable
105
+ (`document.querySelectorAll('dx-surface')`).
106
+ - A single centralized overlay manager draws boundary highlights and an info
107
+ affordance, instead of every surface mounting its own portal and observers.
108
+ - The React `Profiler` is attached per surface; `SurfaceProfiler*` collect
109
+ render counts and durations for an in-app perf panel.
110
+
111
+ ### Dev metrics
112
+
113
+ `SurfaceMetrics` (a module singleton, so the out-of-tree overlay and the in-app
114
+ devtools panel share one source) records, keyed by `surface/<id>/<role>`:
115
+
116
+ - **`dataUnstable` / `dataChurn`** — the consumer's `data` prop identity churns
117
+ across renders without its value changing. This is the most common Surface
118
+ footgun (see Performance notes) and nothing else detects it.
119
+ - **`candidates` / `truncated`** — candidates matched on the last dispatch, and
120
+ whether `limit` dropped some. Diagnoses "nothing renders" (0) and "two things
121
+ render" (ambiguous match).
122
+ - **`dispatches`** — how often the dispatcher re-resolved (the count the per-role
123
+ subscription minimizes).
124
+ - **`errors`** — error-boundary trips for this surface.
125
+ - **`mounts` / `unmounts`** — mount churn (often a thrashing upstream `key`).
126
+
127
+ Recording is gated on the debug flag (zero production cost). Surfaced two ways:
128
+ the `dx-surface` overlay badge (turns ⚠/red on a concern; full metrics on
129
+ expand) and the devtools `SurfaceProfilerPanel` (joined onto render-timing by id).
130
+ `Surface.useMetrics()` / `Surface.clearMetrics()` expose the data.
131
+
132
+ ## Performance notes
133
+
134
+ - Keep `data` referentially stable across renders — it is a memo/`resetKeys`
135
+ dependency. Unstable `data` defeats candidate stability and trips error-boundary
136
+ resets.
@@ -3,21 +3,23 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { useCallback, useEffect, useRef, useState } from 'react';
6
+ import React, { useCallback, useEffect, useState } from 'react';
7
7
 
8
8
  import { random } from '@dxos/random';
9
- import { Panel, Toolbar } from '@dxos/react-ui';
9
+ import { Input, Panel, Toolbar } from '@dxos/react-ui';
10
10
  import { Listbox } from '@dxos/react-ui-list';
11
11
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { type ColorStyles, getHashStyles, mx } from '@dxos/ui-theme';
13
13
 
14
14
  import { Capabilities } from '../../../common';
15
+ import * as Role from '../../../common/Role';
15
16
  import { withPluginManager } from '../../../testing';
16
17
  import { usePluginManager } from '../PluginManager';
17
18
  import { SurfaceComponent, useSurfaces } from './SurfaceComponent';
18
- import { create, makeFilter, makeType } from './types';
19
+ import { isSurfaceDebugEnabled, setSurfaceDebug } from './SurfaceDebug';
20
+ import { create, makeFilter } from './types';
19
21
 
20
- const ItemRole = makeType<{ id: string }>('org.dxos.test.role.item');
22
+ const ItemRole = Role.make<{ id: string }>('org.dxos.test.role.item');
21
23
 
22
24
  type TestComponentProps = {
23
25
  id: string;
@@ -26,19 +28,61 @@ type TestComponentProps = {
26
28
 
27
29
  const TestComponent = ({ styles, id }: TestComponentProps) => {
28
30
  return (
29
- <div className={mx('flex justify-center items-center border rounded-sm', styles.surface, styles.border)}>
30
- <span className={mx('dx-tag font-mono text-lg', styles.text)}>{id}</span>
31
+ <div className={mx('flex justify-center items-center border rounded-sm', styles.bg, styles.border)}>
32
+ <span className={mx('dx-tag font-mono text-lg', styles.fg)}>{id}</span>
31
33
  </div>
32
34
  );
33
35
  };
34
36
 
35
- const DefaultStory = () => {
37
+ const ErrorComponent = () => {
38
+ const [count, setCount] = useState(3);
39
+ useEffect(() => {
40
+ const interval = setInterval(() => {
41
+ setCount((count) => {
42
+ if (count <= 1) {
43
+ clearInterval(interval);
44
+ }
45
+
46
+ return count - 1;
47
+ });
48
+ }, 1_000);
49
+ return () => clearInterval(interval);
50
+ }, []);
51
+
52
+ if (count <= 0) {
53
+ throw new Error('BANG!');
54
+ }
55
+
56
+ return (
57
+ <div className='flex justify-center items-center border border-rose-bg rounded-sm'>
58
+ <span className='font-mono'>Ticking... {count}</span>
59
+ </div>
60
+ );
61
+ };
62
+
63
+ type StoryProps = {
64
+ debug?: boolean;
65
+ };
66
+
67
+ const DefaultStory = ({ debug: debugProp }: StoryProps) => {
36
68
  const manager = usePluginManager();
37
69
  const surfaces = useSurfaces();
38
70
  const [selected, setSelected] = useState<string | undefined>();
71
+ const [debug, setDebug] = useState(debugProp ?? isSurfaceDebugEnabled());
72
+
73
+ // Restore the global debug flag on unmount so other stories don't inherit it.
74
+ useEffect(() => {
75
+ const previous = debugProp ?? isSurfaceDebugEnabled();
76
+ return () => setSurfaceDebug(previous);
77
+ }, []);
78
+
79
+ const handleToggleDebug = useCallback((next: boolean) => {
80
+ setSurfaceDebug(next);
81
+ setDebug(next);
82
+ }, []);
39
83
 
40
84
  const handleAdd = useCallback(() => {
41
- const id = `test-${random.number.int({ min: 0, max: 1_000 })}`;
85
+ const id = `test${random.number.int({ min: 0, max: 1_000 })}`;
42
86
  const styles = getHashStyles(id);
43
87
 
44
88
  manager.capabilities.contribute({
@@ -65,42 +109,13 @@ const DefaultStory = () => {
65
109
  implementation: create({
66
110
  id: 'error',
67
111
  filter: makeFilter(ItemRole, (data) => data.id === 'error'),
68
- component: () => {
69
- const [count, setCount] = useState(3);
70
- useEffect(() => {
71
- const interval = setInterval(() => {
72
- setCount((count) => {
73
- if (count <= 1) {
74
- clearInterval(interval);
75
- }
76
-
77
- return count - 1;
78
- });
79
- }, 1_000);
80
- return () => clearInterval(interval);
81
- }, []);
82
-
83
- if (count <= 0) {
84
- throw new Error('BANG!');
85
- }
86
-
87
- return (
88
- <div className='flex justify-center items-center border border-rose-bg rounded-sm'>
89
- <span className='font-mono'>Ticking... {count}</span>
90
- </div>
91
- );
92
- },
112
+ component: ErrorComponent,
93
113
  }),
94
114
  });
95
115
 
96
116
  setSelected('error');
97
117
  }, [manager]);
98
118
 
99
- const ref = useRef<HTMLElement>(null);
100
- useEffect(() => {
101
- console.log(ref.current);
102
- }, [ref]);
103
-
104
119
  return (
105
120
  <Panel.Root>
106
121
  <Panel.Toolbar asChild>
@@ -108,10 +123,20 @@ const DefaultStory = () => {
108
123
  <Toolbar.Button onClick={handleAdd}>Add</Toolbar.Button>
109
124
  <Toolbar.Button onClick={handleSelect}>Pick</Toolbar.Button>
110
125
  <Toolbar.Button onClick={handleError}>Error</Toolbar.Button>
126
+ <Toolbar.Separator />
127
+ <Input.Root>
128
+ <Input.Label classNames='pr-1'>Debug</Input.Label>
129
+ <Input.Switch checked={debug} onCheckedChange={handleToggleDebug} />
130
+ </Input.Root>
111
131
  </Toolbar.Root>
112
132
  </Panel.Toolbar>
113
- <Panel.Content className='grid grid-cols-2 h-full gap-4 overflow-hidden'>
114
- <SurfaceComponent type={ItemRole} data={selected ? { id: selected } : undefined} limit={1} ref={ref} />
133
+ <Panel.Content classNames='grid grid-cols-2 h-full gap-4 overflow-hidden'>
134
+ <SurfaceComponent
135
+ key={debug ? 'debug' : 'prod'}
136
+ type={ItemRole}
137
+ data={selected ? { id: selected } : undefined}
138
+ limit={1}
139
+ />
115
140
  <div className='overflow-y-auto h-full'>
116
141
  <Listbox.Root>
117
142
  <Listbox.Content aria-label='Surfaces'>
@@ -141,4 +166,8 @@ export default meta;
141
166
 
142
167
  type Story = StoryObj<typeof meta>;
143
168
 
144
- export const Default: Story = {};
169
+ export const Default: Story = {
170
+ args: {
171
+ debug: true,
172
+ },
173
+ };