@volter/editor-core 0.5.57

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 (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,937 @@
1
+ import { themeVars, zIndex } from '@volter/editor-sdk/widgets';
2
+ import {
3
+ REACT_STORY_TRANSPARENCY_BASE,
4
+ reactStoryTransparencyBackground,
5
+ } from './react-design-canvas-style';
6
+ import {
7
+ applyStoryChrome,
8
+ FRAME_LABEL_GAP as FLOATING_LABEL_GAP,
9
+ FRAME_LABEL_HEIGHT as FLOATING_LABEL_HEIGHT,
10
+ GROUP_LABEL_GAP,
11
+ GROUP_LABEL_HEIGHT,
12
+ placeStoryChrome,
13
+ } from './story-board-chrome-fit';
14
+ import type {
15
+ ResolvedStoryPresentation,
16
+ StoryBoardPresentation,
17
+ StoryPresentationIndex,
18
+ } from './story-board-presentation';
19
+ import { getRootCanvasViewport, subscribeRootCanvasViewport } from './world-canvas-viewport-state';
20
+ import { getRootPan, setRootView, subscribeRootPan } from './world-pan-state';
21
+
22
+ const DEFAULT_FRAME_WIDTH = 640;
23
+ const DEFAULT_FRAME_HEIGHT = 400;
24
+ const BOARD_PADDING = 64;
25
+ const FRAME_GAP = 64;
26
+ const VIEW_PADDING = 72;
27
+ /** Below this client width a truncated frame title is visual noise. */
28
+ const MIN_FRAME_LABEL_WIDTH = 28;
29
+
30
+ /** Vertical space between two group blocks — the ordinary frame gap plus the
31
+ * chrome that sits above the next block (its group label and its frames'
32
+ * own floating labels). Never applied to a single-group board, which is why
33
+ * an ungrouped project lays out byte-for-byte as it did before grouping. */
34
+ const GROUP_BLOCK_GAP =
35
+ FRAME_GAP + GROUP_LABEL_HEIGHT + GROUP_LABEL_GAP + FLOATING_LABEL_HEIGHT + FLOATING_LABEL_GAP;
36
+
37
+ export interface ReactStoryBoardStory {
38
+ readonly id: string;
39
+ readonly label: string;
40
+ readonly modulePath: string;
41
+ /** Composed CSF meta title when the module authored one. Organization input
42
+ * only (`authoring/story-board-presentation.ts`). */
43
+ readonly title?: string | undefined;
44
+ readonly parameters?: Readonly<Record<string, unknown>> | undefined;
45
+ readonly globals?: Readonly<Record<string, unknown>> | undefined;
46
+ readonly viewportLocked?: boolean | undefined;
47
+ /**
48
+ * The frame's own size in authored px, for a story that BAKES its cell.
49
+ *
50
+ * A dom story omits it: its frame is a Storybook viewport (or the shared
51
+ * responsive preset), which is what makes a UI frame a phone or a desktop.
52
+ * A canvas story supplies the content-bounds frame resolved by its native
53
+ * board — so the shared substrate uses that exact authored-space rectangle
54
+ * rather than scaling it into a default UI viewport. See
55
+ * `@vgai/canvas`'s `canvas-board/CanvasBoardDocument.tsx`.
56
+ */
57
+ readonly frameSize?: { readonly width: number; readonly height: number } | undefined;
58
+ }
59
+
60
+ /** One component section: its named story frames, in Storybook order. */
61
+ interface BoardGroup {
62
+ readonly key: string;
63
+ readonly label: string;
64
+ readonly stories: ReactStoryBoardStory[];
65
+ }
66
+
67
+ /** Project the shared Storybook index into component sections. */
68
+ function groupStories(
69
+ stories: readonly ReactStoryBoardStory[],
70
+ index: StoryPresentationIndex,
71
+ ): BoardGroup[] {
72
+ const byId = new Map(stories.map((story) => [story.id, story]));
73
+ return index.sections.map((section) => ({
74
+ key: section.id,
75
+ label: section.label,
76
+ stories: section.storyIds.flatMap((storyId) => {
77
+ const story = byId.get(storyId);
78
+ return story ? [story] : [];
79
+ }),
80
+ }));
81
+ }
82
+
83
+ const pathSegments = (path: string): string[] =>
84
+ path
85
+ .split('/')
86
+ .map((part) => part.trim())
87
+ .filter(Boolean);
88
+ interface BoardBlock {
89
+ key: string;
90
+ label: string | null;
91
+ stories: ReactStoryBoardStory[];
92
+ }
93
+ function boardBlocks(groups: readonly BoardGroup[]): BoardBlock[] {
94
+ const soloSections = new Set(
95
+ groups.filter((group) => group.stories.length === 1).map((group) => group.key),
96
+ );
97
+ const blocks: BoardBlock[] = [];
98
+ {
99
+ const byCategory = new Map<string, BoardBlock>();
100
+ for (const group of groups) {
101
+ if (!soloSections.has(group.key)) {
102
+ blocks.push({ key: group.key, label: group.label, stories: [...group.stories] });
103
+ continue;
104
+ }
105
+ const category = pathSegments(group.label).slice(0, -1).join('/');
106
+ let block = byCategory.get(category);
107
+ if (!block) {
108
+ block = { key: `category:${category}`, label: category || null, stories: [] };
109
+ byCategory.set(category, block);
110
+ blocks.push(block);
111
+ }
112
+ block.stories.push(...group.stories);
113
+ }
114
+ }
115
+
116
+ return blocks;
117
+ }
118
+
119
+ /** Shared authored-space placement for the board and its chrome-free preview. */
120
+ export function reactStoryBoardLayout(
121
+ stories: readonly ReactStoryBoardStory[],
122
+ index: StoryPresentationIndex,
123
+ presentationFor: (story: ReactStoryBoardStory) => { width: number; height: number },
124
+ ) {
125
+ const blocks = boardBlocks(groupStories(stories, index));
126
+ const layouts = new Map<string, FrameLayout>();
127
+ const groupOrigins = new Map<string, { x: number; y: number; roomAbove: number }>();
128
+ let blockTop = BOARD_PADDING;
129
+ let widestBlock = 0;
130
+
131
+ // One block per BoardBlock (see the blocks derivation above): variant
132
+ // sections as themselves, solo cards in shared category grids.
133
+ blocks.forEach((group, blockIndex) => {
134
+ const columns = Math.max(1, Math.ceil(Math.sqrt(group.stories.length)));
135
+ const rows = Math.ceil(group.stories.length / columns);
136
+ const presentations = group.stories.map((story) => presentationFor(story));
137
+ const columnWidths = Array.from({ length: columns }, () => 0);
138
+ const rowHeights = Array.from({ length: rows }, () => 0);
139
+ presentations.forEach((presentation, index) => {
140
+ const column = index % columns;
141
+ const row = Math.floor(index / columns);
142
+ columnWidths[column] = Math.max(columnWidths[column]!, presentation.width);
143
+ rowHeights[row] = Math.max(rowHeights[row]!, presentation.height);
144
+ });
145
+ const columnX = columnWidths.map((_, column) =>
146
+ columnWidths.slice(0, column).reduce((sum, width) => sum + width + FRAME_GAP, 0),
147
+ );
148
+ const rowY = rowHeights.map((_, row) =>
149
+ rowHeights.slice(0, row).reduce((sum, height) => sum + height + FRAME_GAP, 0),
150
+ );
151
+ // Nothing sits above the board's very first row, so its chrome is never
152
+ // constrained; every later block sits one block gap below its predecessor.
153
+ // An unlabeled block carries no group title, so it borrows only the room
154
+ // its own frame labels need.
155
+ const blockRoom =
156
+ blockIndex === 0
157
+ ? Number.POSITIVE_INFINITY
158
+ : group.label !== null
159
+ ? GROUP_BLOCK_GAP
160
+ : FRAME_GAP + FLOATING_LABEL_HEIGHT + FLOATING_LABEL_GAP;
161
+ if (group.label !== null) {
162
+ groupOrigins.set(group.key, { x: BOARD_PADDING, y: blockTop, roomAbove: blockRoom });
163
+ }
164
+
165
+ group.stories.forEach((story, index) => {
166
+ const row = Math.floor(index / columns);
167
+ const column = index % columns;
168
+ const presentation = presentations[index]!;
169
+ const { width, height } = presentation;
170
+ const x = BOARD_PADDING + columnX[column]!;
171
+ const y = blockTop + rowY[row]!;
172
+ layouts.set(story.id, {
173
+ x,
174
+ y,
175
+ width,
176
+ height,
177
+ roomAbove: row === 0 ? blockRoom : FRAME_GAP,
178
+ withGroupLabel: blocks.length > 1 && row === 0 && group.label !== null,
179
+ });
180
+ });
181
+
182
+ const blockWidth =
183
+ columnWidths.reduce((sum, width) => sum + width, 0) + Math.max(0, columns - 1) * FRAME_GAP;
184
+ const blockHeight =
185
+ rowHeights.reduce((sum, height) => sum + height, 0) + Math.max(0, rows - 1) * FRAME_GAP;
186
+ widestBlock = Math.max(widestBlock, blockWidth);
187
+ blockTop += blockHeight + GROUP_BLOCK_GAP;
188
+ });
189
+
190
+ const boardWidth = BOARD_PADDING * 2 + widestBlock;
191
+ const boardHeight = blockTop - GROUP_BLOCK_GAP + BOARD_PADDING;
192
+
193
+ return { layouts, groupOrigins, width: boardWidth, height: stories.length ? boardHeight : 0 };
194
+ }
195
+
196
+ export interface ReactStoryBoardFrame {
197
+ readonly element: HTMLElement;
198
+ readonly content: HTMLElement;
199
+ }
200
+
201
+ export type ReactStoryBoardSelectionIntent = 'highlight' | 'zoom';
202
+
203
+ interface FrameLayout {
204
+ readonly x: number;
205
+ readonly y: number;
206
+ readonly width: number;
207
+ readonly height: number;
208
+ /** Authored px between this frame's top edge and the bottom edge of the
209
+ * frames above it — `Infinity` on the board's own first row. Multiplied by
210
+ * zoom it is the client-space room the floating chrome must fit into. */
211
+ readonly roomAbove: number;
212
+ /** True on the first row of a block on a grouped board: that row's chrome
213
+ * stack also carries the group name, which is served first. */
214
+ readonly withGroupLabel: boolean;
215
+ }
216
+
217
+ export interface ReactStoryBoard {
218
+ readonly element: HTMLElement;
219
+ readonly frames: ReadonlyMap<string, ReactStoryBoardFrame>;
220
+ activeContent(): HTMLElement;
221
+ activate(storyId: string, focus?: boolean): void;
222
+ clearError(storyId: string): void;
223
+ fitAll(): void;
224
+ selectAtClientPoint(clientX: number, clientY: number): boolean;
225
+ zoomAtClientPoint(clientX: number, clientY: number): boolean;
226
+ setError(storyId: string, message: string): void;
227
+ /**
228
+ * Present the story's MEASURED content rectangle as its cell — the dom
229
+ * lane's version of the canvas exhibit's content-bounds frame + crop. The
230
+ * mount keeps its declared-viewport harness; the frame becomes a window
231
+ * onto `offset → offset + frame`. `null` restores the declared
232
+ * presentation. The caller (design-time-layers) measures after each mount;
233
+ * a `fullscreen` story never gets one.
234
+ */
235
+ setMeasuredFrame(
236
+ storyId: string,
237
+ measured: {
238
+ frame: { width: number; height: number };
239
+ mount: { width: number; height: number };
240
+ offset: { x: number; y: number };
241
+ } | null,
242
+ ): void;
243
+ /** Show (non-empty) or clear (`null`) the self-explanation on EVERY card —
244
+ * the condition it reports is a project-level fact, never a per-story one. */
245
+ setNote(message: string | null): void;
246
+ viewport(): ReactStoryBoardViewportState;
247
+ setViewport(viewportId: string | null): void;
248
+ setRotation(rotated: boolean): void;
249
+ dispose(): void;
250
+ }
251
+
252
+ export interface ReactStoryBoardViewportState {
253
+ readonly storyId: string;
254
+ readonly selectedViewportId: string | null;
255
+ readonly locked: boolean;
256
+ readonly rotated: boolean;
257
+ readonly choices: ResolvedStoryPresentation['choices'];
258
+ }
259
+
260
+ const boardsByContainer = new WeakMap<HTMLElement, ReactStoryBoard>();
261
+ const boardViewportListeners = new Set<() => void>();
262
+ const EMPTY_BOARD_VIEWPORT: ReactStoryBoardViewportState | null = null;
263
+
264
+ function notifyBoardViewport(): void {
265
+ for (const listener of boardViewportListeners) listener();
266
+ }
267
+
268
+ export function subscribeReactStoryBoardViewport(listener: () => void): () => void {
269
+ boardViewportListeners.add(listener);
270
+ return () => boardViewportListeners.delete(listener);
271
+ }
272
+
273
+ /**
274
+ * The active board's frame placements in BOARD coordinates (the unzoomed
275
+ * layer pixels `layout()` writes to each frame's `style.left/top`) — what
276
+ * "Materialize pasteboard" reads to turn the DERIVED auto-grid into AUTHORED
277
+ * `<At x y>` literals. Null when the container holds no board.
278
+ */
279
+ export function reactStoryBoardFramePlacements(
280
+ element: HTMLElement | null,
281
+ ): Array<{ storyId: string; x: number; y: number }> | null {
282
+ // Boards register under the layer's PARENT (`createReactStoryBoard`'s
283
+ // `container`), while the queryable marker sits on the layer itself —
284
+ // accept either, so a caller holding `[data-vgai-react-story-board]` works.
285
+ const board = element
286
+ ? (boardsByContainer.get(element) ??
287
+ (element.parentElement ? boardsByContainer.get(element.parentElement) : undefined))
288
+ : undefined;
289
+ if (!board) return null;
290
+ const placements: Array<{ storyId: string; x: number; y: number }> = [];
291
+ for (const [storyId, frame] of board.frames) {
292
+ placements.push({
293
+ storyId,
294
+ x: Math.round(Number.parseFloat(frame.element.style.left) || 0),
295
+ y: Math.round(Number.parseFloat(frame.element.style.top) || 0),
296
+ });
297
+ }
298
+ return placements;
299
+ }
300
+
301
+ export function getReactStoryBoardViewport(
302
+ container: HTMLElement | null,
303
+ ): ReactStoryBoardViewportState | null {
304
+ return container ? (boardsByContainer.get(container)?.viewport() ?? EMPTY_BOARD_VIEWPORT) : null;
305
+ }
306
+
307
+ export function setActiveReactStoryBoardViewport(
308
+ container: HTMLElement | null,
309
+ viewportId: string | null,
310
+ ): boolean {
311
+ const board = container ? boardsByContainer.get(container) : undefined;
312
+ if (!board) return false;
313
+ board.setViewport(viewportId);
314
+ return true;
315
+ }
316
+
317
+ export function toggleActiveReactStoryBoardViewportRotation(
318
+ container: HTMLElement | null,
319
+ ): boolean {
320
+ const board = container ? boardsByContainer.get(container) : undefined;
321
+ if (!board || board.viewport().locked) return false;
322
+ board.setRotation(!board.viewport().rotated);
323
+ return true;
324
+ }
325
+
326
+ function fallbackFrameSize(): { width: number; height: number } {
327
+ const viewport = getRootCanvasViewport();
328
+ return {
329
+ width: viewport.width ?? DEFAULT_FRAME_WIDTH,
330
+ height: viewport.height ?? DEFAULT_FRAME_HEIGHT,
331
+ };
332
+ }
333
+
334
+ function fitBounds(
335
+ container: HTMLElement,
336
+ bounds: { x: number; y: number; width: number; height: number },
337
+ maximumZoom: number,
338
+ ): void {
339
+ if (bounds.width <= 0 || bounds.height <= 0) return;
340
+ const availableWidth = Math.max(1, container.clientWidth - VIEW_PADDING * 2);
341
+ const availableHeight = Math.max(1, container.clientHeight - VIEW_PADDING * 2);
342
+ const zoom = Math.min(
343
+ maximumZoom,
344
+ Math.max(0.1, Math.min(availableWidth / bounds.width, availableHeight / bounds.height)),
345
+ );
346
+ setRootView(
347
+ container.clientWidth / 2 - (bounds.x + bounds.width / 2) * zoom,
348
+ container.clientHeight / 2 - (bounds.y + bounds.height / 2) * zoom,
349
+ zoom,
350
+ );
351
+ }
352
+
353
+ /** Fit the board hosted by `container`, if it has one. Used by viewport chrome. */
354
+ export function fitReactStoryBoard(container: HTMLElement): boolean {
355
+ const board = boardsByContainer.get(container);
356
+ if (!board) return false;
357
+ board.fitAll();
358
+ return true;
359
+ }
360
+
361
+ /** Focus an inactive story frame from a canvas pointer press. Returns true
362
+ * only when the point was inside an inactive frame and its story-selection
363
+ * callback was dispatched. The selection overlay uses this to give the whole
364
+ * frame—not only its small label—the expected Figma-style click target. */
365
+ export function selectReactStoryFrameAtPoint(
366
+ container: HTMLElement | null,
367
+ clientX: number,
368
+ clientY: number,
369
+ ): boolean {
370
+ if (!container) return false;
371
+ return boardsByContainer.get(container)?.selectAtClientPoint(clientX, clientY) ?? false;
372
+ }
373
+
374
+ /** Zoom to the story frame under a canvas double-click. Unlike single-click
375
+ * selection, this also handles the already-active frame so recentering never
376
+ * requires a hierarchy round trip. */
377
+ export function zoomReactStoryFrameAtPoint(
378
+ container: HTMLElement | null,
379
+ clientX: number,
380
+ clientY: number,
381
+ ): boolean {
382
+ if (!container) return false;
383
+ return boardsByContainer.get(container)?.zoomAtClientPoint(clientX, clientY) ?? false;
384
+ }
385
+
386
+ /**
387
+ * Editor-only Figma-style canvas over a set of CSF stories — THE board
388
+ * substrate, not a dom-only one. Every story owns an isolated frame; the active
389
+ * frame is only the authoring focus, never a runtime state.
390
+ *
391
+ * Two boards mount it, and the difference between them is membership and what
392
+ * goes IN a frame, never layout: the `UI`/`Dev` boards
393
+ * (`design-time-layers.ts`) mount each dom story's React render into
394
+ * `frames.get(id).content`, and Play tears that whole board down before
395
+ * mounting the root's real manifest entry; the `2D` board
396
+ * (`@vgai/canvas`'s `canvas-board/CanvasBoardDocument.tsx`) puts each canvas story's own mounted
397
+ * Pixi host in the same place, cropped to the mounted stage's content bounds.
398
+ * A frame is DOM either way — a canvas story renders a `<canvas>`, which is
399
+ * why it is an ordinary frame here and not a second board engine.
400
+ */
401
+ export function createReactStoryBoard(
402
+ layer: HTMLElement,
403
+ stories: readonly ReactStoryBoardStory[],
404
+ initialStoryId: string,
405
+ onStorySelected: (storyId: string, intent: ReactStoryBoardSelectionIntent) => void,
406
+ // INJECTED, not defaulted (`authoring/story-board-presentation.ts`): the
407
+ // board is host chrome's substrate and must not import the CSF
408
+ // implementation that organizes and sizes the frames. `resolve` is called
409
+ // INSIDE the layout pass below, per story and per viewport override, so a
410
+ // caller cannot pre-compute the answers and hand over a table.
411
+ presentation: StoryBoardPresentation,
412
+ ): ReactStoryBoard {
413
+ if (stories.length === 0) throw new Error('A React story board requires at least one story.');
414
+
415
+ const container = layer.parentElement;
416
+ if (!container) throw new Error('A React story board layer must be attached before mounting.');
417
+
418
+ layer.dataset['vgaiReactStoryBoard'] = 'true';
419
+ // The board may be larger than the visible document. Individual story
420
+ // frames provide their own layout/paint containment for fixed-position UI.
421
+ layer.style.contain = 'none';
422
+
423
+ const boardElement = document.createElement('div');
424
+ boardElement.dataset['testid'] = 'react-story-board';
425
+ boardElement.dataset['vgaiReactStoryBoardCanvas'] = 'true';
426
+ boardElement.style.position = 'absolute';
427
+ boardElement.style.left = '0';
428
+ boardElement.style.top = '0';
429
+ boardElement.style.transformOrigin = '0 0';
430
+ layer.appendChild(boardElement);
431
+
432
+ // Labels are editor chrome, not game DOM. Keep them in a synchronized
433
+ // sibling above RootSelectionOverlay (z=overlayLow), otherwise that
434
+ // full-cover interaction layer correctly used for DOM picking intercepts
435
+ // every label click before it reaches the board.
436
+ const chromeElement = document.createElement('div');
437
+ chromeElement.dataset['testid'] = 'react-story-board-chrome';
438
+ chromeElement.dataset['vgaiStoryBoardChromeFor'] = layer.dataset['worldId'] ?? '';
439
+ chromeElement.style.position = 'absolute';
440
+ chromeElement.style.inset = '0';
441
+ chromeElement.style.zIndex = String(zIndex.sticky);
442
+ chromeElement.style.display = layer.style.display || 'block';
443
+ chromeElement.style.pointerEvents = 'none';
444
+ chromeElement.style.overflow = 'visible';
445
+ container.appendChild(chromeElement);
446
+
447
+ const frames = new Map<string, ReactStoryBoardFrame>();
448
+ const errors = new Map<string, HTMLElement>();
449
+ const notes = new Map<string, HTMLElement>();
450
+ const labels = new Map<string, HTMLButtonElement>();
451
+ const layouts = new Map<string, FrameLayout>();
452
+ // The dom lane's transcription of the canvas board's exhibit model
453
+ // (`canvas-board-model.ts`): the MOUNT is a harness at the story's declared
454
+ // viewport — the box its anchors resolve against — while the FRAME presents
455
+ // the measured content rectangle inside it. Filled per story by the owner
456
+ // (design-time-layers) after each mount; a story whose presentation is
457
+ // `fullscreen` never gets an entry and presents its whole declared box.
458
+ const measuredFrames = new Map<
459
+ string,
460
+ {
461
+ frame: { width: number; height: number };
462
+ mount: { width: number; height: number };
463
+ offset: { x: number; y: number };
464
+ }
465
+ >();
466
+ // Organization is presentation-only and fixed for the board's lifetime.
467
+ // A board with one component needs no redundant section label.
468
+ const groups = groupStories(stories, presentation.index);
469
+ const groupLabels = new Map<string, HTMLElement>();
470
+ const groupOrigins = new Map<string, { x: number; y: number; roomAbove: number }>();
471
+ const viewportOverrides = new Map<string, string | null>();
472
+ const rotationOverrides = new Map<string, boolean>();
473
+ let viewportSnapshot: ReactStoryBoardViewportState;
474
+ let activeStoryId = stories.some((story) => story.id === initialStoryId)
475
+ ? initialStoryId
476
+ : stories[0]!.id;
477
+ let disposed = false;
478
+ let pendingFit = false;
479
+
480
+ const presentationFor = (story: ReactStoryBoardStory): ResolvedStoryPresentation => {
481
+ const resolved = presentation.resolve(
482
+ story,
483
+ story.frameSize ?? fallbackFrameSize(),
484
+ viewportOverrides.has(story.id) ? viewportOverrides.get(story.id) : undefined,
485
+ rotationOverrides.get(story.id),
486
+ );
487
+ // A measured content frame wins the CELL: the board cell is the subject's
488
+ // rectangle, not the whole mount harness. Same `fullscreen` collapse as an
489
+ // exact `frameSize` below, and for the same reason — the measured
490
+ // rectangle already owns its inset.
491
+ const measured = measuredFrames.get(story.id);
492
+ if (measured) {
493
+ return {
494
+ ...resolved,
495
+ width: measured.frame.width,
496
+ height: measured.frame.height,
497
+ layout: 'fullscreen',
498
+ };
499
+ }
500
+ // A story that supplies an exact frame already owns its inset/crop.
501
+ // A `padded` default layout would add a second inset inside that measured
502
+ // rectangle, so the content would sit clipped and off-centre. Nothing else
503
+ // about the presentation changes — a viewport the story really authored
504
+ // still wins the size above.
505
+ return story.frameSize ? { ...resolved, layout: 'fullscreen' } : resolved;
506
+ };
507
+
508
+ const updateViewportSnapshot = (): void => {
509
+ const presentation = presentationFor(stories.find((story) => story.id === activeStoryId)!);
510
+ viewportSnapshot = {
511
+ storyId: activeStoryId,
512
+ selectedViewportId: presentation.selectedViewportId,
513
+ locked: presentation.locked,
514
+ rotated: presentation.rotated,
515
+ choices: presentation.choices,
516
+ };
517
+ };
518
+ updateViewportSnapshot();
519
+
520
+ // The board's presented hierarchy is Category → Component → Variant, and a
521
+ // SINGLE-variant component folds only its variant level away. Generated
522
+ // projects emit one `Default` export per component module, so componentized
523
+ // sections would otherwise render a section title above every card AND a
524
+ // "Default" label on it — a section per frame, twice-labeled, one row each.
525
+ // The solo card takes its component's name (the title path's last segment)
526
+ // and joins its CATEGORY's block — the path minus that segment ("Prefabs",
527
+ // "UI/Screens") — so the categories the titles author remain the board's
528
+ // grouping. A section with real variants keeps its own titled block and its
529
+ // variant labels, which is the shape the two-level names exist for.
530
+ const soloSections = new Set(
531
+ groups.filter((group) => group.stories.length === 1).map((group) => group.key),
532
+ );
533
+ const pathSegments = (path: string): string[] =>
534
+ path
535
+ .split('/')
536
+ .map((segment) => segment.trim())
537
+ .filter(Boolean);
538
+ const presentedLabels = new Map<string, string>();
539
+ for (const group of groups) {
540
+ for (const story of group.stories) {
541
+ const componentName =
542
+ (story.title ? pathSegments(story.title).at(-1) : undefined) ??
543
+ pathSegments(group.label).at(-1);
544
+ presentedLabels.set(
545
+ story.id,
546
+ soloSections.has(group.key) ? (componentName ?? story.label) : story.label,
547
+ );
548
+ }
549
+ }
550
+
551
+ /** Layout/label blocks: variant sections as themselves, solo sections merged
552
+ * per category (order of first appearance; a pathless category is an
553
+ * unlabeled block). Fixed for the board's lifetime, like the groups. */
554
+ const blocks = boardBlocks(groups);
555
+
556
+ for (const story of groups.flatMap((group) => group.stories)) {
557
+ const frame = document.createElement('section');
558
+ frame.dataset['testid'] = 'react-story-frame';
559
+ frame.dataset['storyId'] = story.id;
560
+ frame.dataset['storyLabel'] = story.label;
561
+ frame.dataset['storyGroup'] =
562
+ presentation.index.sections.find((section) => section.storyIds.includes(story.id))?.label ??
563
+ '';
564
+ frame.style.position = 'absolute';
565
+ frame.style.border = `1px solid ${themeVars.boundary.strong}`;
566
+ frame.style.borderRadius = '0';
567
+ // The transparency checkerboard lives on the FRAME, behind the content box
568
+ // it exactly covers — never as an inline background on `content` itself.
569
+ // `content` is a game-CSS scope root (`scoped-game-css.ts`), and an inline
570
+ // background there beats every rule in the game's own stylesheet, so a
571
+ // game whose `body` paints an opaque backdrop would have shown the
572
+ // editor's checkerboard through its own HUD. Behind the content box the
573
+ // checkerboard still shows through wherever the story authors nothing,
574
+ // which is the whole thing it is for.
575
+ frame.style.backgroundColor = REACT_STORY_TRANSPARENCY_BASE;
576
+ frame.style.backgroundImage = reactStoryTransparencyBackground();
577
+ frame.style.backgroundPosition = '0 0, 8px 8px';
578
+ frame.style.backgroundSize = '16px 16px';
579
+ frame.style.boxShadow = 'none';
580
+
581
+ const label = document.createElement('button');
582
+ label.type = 'button';
583
+ label.dataset['testid'] = 'react-story-frame-label';
584
+ label.dataset['storyId'] = story.id;
585
+ label.title = `${presentedLabels.get(story.id) ?? story.label} — click to select, double-click to zoom — ${story.modulePath}`;
586
+ label.textContent = presentedLabels.get(story.id) ?? story.label;
587
+ label.style.position = 'absolute';
588
+ label.style.height = `${FLOATING_LABEL_HEIGHT}px`;
589
+ label.style.padding = '0 2px';
590
+ label.style.border = '0';
591
+ label.style.appearance = 'none';
592
+ label.style.background = 'transparent';
593
+ label.style.color = themeVars.content.primary;
594
+ label.style.font = `600 12px ${themeVars.typography.sans}`;
595
+ label.style.lineHeight = `${FLOATING_LABEL_HEIGHT}px`;
596
+ label.style.textAlign = 'left';
597
+ label.style.cursor = 'default';
598
+ label.style.pointerEvents = 'auto';
599
+ label.style.whiteSpace = 'nowrap';
600
+ label.style.overflow = 'hidden';
601
+ label.style.textOverflow = 'ellipsis';
602
+ label.style.width = 'max-content';
603
+ label.addEventListener('click', () => onStorySelected(story.id, 'highlight'));
604
+ label.addEventListener('dblclick', () => onStorySelected(story.id, 'zoom'));
605
+
606
+ const content = document.createElement('div');
607
+ content.dataset['testid'] = 'react-story-frame-content';
608
+ content.dataset['storyId'] = story.id;
609
+ content.style.position = 'absolute';
610
+ content.style.left = '0';
611
+ content.style.top = '0';
612
+ content.style.overflow = 'hidden';
613
+ content.style.contain = 'layout paint';
614
+ content.style.boxSizing = 'border-box';
615
+
616
+ // WHY A CARD EXPLAINS ITSELF. A component whose layout lives in its
617
+ // game's page-level stylesheet renders, without that sheet, as real DOM
618
+ // with zero layout — and a person looking at the board reads that as the
619
+ // editor being broken, not as a missing declaration (it was read that way
620
+ // three times before this note existed). So the card says what is missing,
621
+ // in the place the missing thing is SEEN. It is board CHROME, a sibling of
622
+ // `content` and deliberately never inside it: the authoring adapter walks
623
+ // `activeContent().children` as the story's own source tree, and an
624
+ // editor-owned div in there would surface in the hierarchy as an element
625
+ // the story does not have.
626
+ const note = document.createElement('div');
627
+ note.dataset['testid'] = 'react-story-frame-note';
628
+ note.dataset['storyId'] = story.id;
629
+ note.style.position = 'absolute';
630
+ note.style.left = '0';
631
+ note.style.right = '0';
632
+ note.style.bottom = '0';
633
+ note.style.display = 'none';
634
+ note.style.padding = '6px 8px';
635
+ note.style.boxSizing = 'border-box';
636
+ note.style.background = themeVars.surface.overlay;
637
+ note.style.color = themeVars.content.muted;
638
+ note.style.font = `11px ${themeVars.typography.sans}`;
639
+ note.style.pointerEvents = 'none';
640
+
641
+ const error = document.createElement('div');
642
+ error.dataset['testid'] = 'react-story-frame-error';
643
+ error.dataset['storyId'] = story.id;
644
+ error.style.position = 'absolute';
645
+ error.style.inset = '0';
646
+ error.style.display = 'none';
647
+ error.style.padding = '16px';
648
+ error.style.boxSizing = 'border-box';
649
+ error.style.background = themeVars.surface.overlay;
650
+ error.style.color = themeVars.semantic.danger;
651
+ error.style.font = `12px ${themeVars.typography.mono}`;
652
+ error.style.pointerEvents = 'none';
653
+
654
+ frame.append(content, note, error);
655
+ boardElement.appendChild(frame);
656
+ chromeElement.appendChild(label);
657
+ frames.set(story.id, { element: frame, content });
658
+ errors.set(story.id, error);
659
+ notes.set(story.id, note);
660
+ labels.set(story.id, label);
661
+ }
662
+
663
+ // Group names are chrome of the SAME floating kind as the frame labels —
664
+ // constant size above a zooming board, in the same synchronized sibling
665
+ // layer. They name a cluster; they are not selectable, so unlike a frame
666
+ // label they take no pointer events.
667
+ // One block needs no name over it — a single title above the whole board
668
+ // distinguishes nothing, which is the same rule the single-group board
669
+ // always held.
670
+ const showBlockLabels = blocks.length > 1;
671
+ if (showBlockLabels) {
672
+ for (const block of blocks) {
673
+ // An unlabeled block (pathless solo cards) shows nothing above itself —
674
+ // its cards already wear their component names.
675
+ if (block.label === null) continue;
676
+ const groupLabel = document.createElement('div');
677
+ groupLabel.dataset['testid'] = 'react-story-group-label';
678
+ groupLabel.dataset['storyGroup'] = block.key;
679
+ groupLabel.textContent = block.label;
680
+ groupLabel.style.position = 'absolute';
681
+ groupLabel.style.height = `${GROUP_LABEL_HEIGHT}px`;
682
+ groupLabel.style.lineHeight = `${GROUP_LABEL_HEIGHT}px`;
683
+ groupLabel.style.color = themeVars.content.primary;
684
+ groupLabel.style.font = `700 14px ${themeVars.typography.sans}`;
685
+ groupLabel.style.whiteSpace = 'nowrap';
686
+ groupLabel.style.pointerEvents = 'none';
687
+ groupLabel.style.width = 'max-content';
688
+ chromeElement.appendChild(groupLabel);
689
+ groupLabels.set(block.key, groupLabel);
690
+ }
691
+ }
692
+
693
+ const updateActiveAppearance = (): void => {
694
+ for (const [storyId, frame] of frames) {
695
+ const active = storyId === activeStoryId;
696
+ frame.element.dataset['active'] = active ? 'true' : 'false';
697
+ frame.element.style.outline = active ? `2px solid ${themeVars.accent.default}` : 'none';
698
+ frame.element.style.outlineOffset = active ? '3px' : '0';
699
+ const label = labels.get(storyId)!;
700
+ label.setAttribute('aria-pressed', String(active));
701
+ label.style.color = active ? themeVars.accent.default : themeVars.content.muted;
702
+ }
703
+ };
704
+
705
+ /** The authored room above a row, in client px at the current zoom. */
706
+ const clientRoom = (authored: number, zoom: number): number =>
707
+ Number.isFinite(authored) ? authored * zoom : Number.POSITIVE_INFINITY;
708
+
709
+ // Story names are editor chrome, not board content. Convert each authored-
710
+ // space frame origin into client-space placement instead of transforming
711
+ // the label subtree; font size, height, and hit target therefore remain
712
+ // constant while the frames zoom underneath them, matching Figma labels.
713
+ // At far zoom that constant chrome no longer fits the gap it lives in, so
714
+ // `placeStoryChrome` degrades it (scale, then fade, then drop) rather than
715
+ // letting it paint over the frames above — see story-board-chrome-fit.ts.
716
+ const positionLabels = (): void => {
717
+ const view = getRootPan();
718
+ for (const [storyId, frameLayout] of layouts) {
719
+ const label = labels.get(storyId)!;
720
+ const { frame } = placeStoryChrome(
721
+ clientRoom(frameLayout.roomAbove, view.zoom),
722
+ frameLayout.withGroupLabel,
723
+ );
724
+ label.style.left = `${view.x + frameLayout.x * view.zoom}px`;
725
+ label.style.top = `${view.y + frameLayout.y * view.zoom - frame.offset}px`;
726
+ // A floating name may borrow half the inter-frame gap, but never the
727
+ // next frame's column. The old 80px floor made labels for small 2D
728
+ // sprites collide at overview zoom even while their frames were apart;
729
+ // at extreme zoom a title truncates, then disappears below the readable
730
+ // width above instead of claiming space from its neighbour.
731
+ const maxWidth = Math.max(1, (frameLayout.width + FRAME_GAP / 2) * view.zoom);
732
+ label.style.maxWidth = `${maxWidth}px`;
733
+ applyStoryChrome(
734
+ label,
735
+ maxWidth >= MIN_FRAME_LABEL_WIDTH ? frame : { ...frame, opacity: 0, visible: false },
736
+ );
737
+ }
738
+ for (const [key, origin] of groupOrigins) {
739
+ const groupLabel = groupLabels.get(key);
740
+ if (!groupLabel) continue;
741
+ const { group } = placeStoryChrome(clientRoom(origin.roomAbove, view.zoom), true);
742
+ if (!group) continue;
743
+ groupLabel.style.left = `${view.x + origin.x * view.zoom}px`;
744
+ groupLabel.style.top = `${view.y + origin.y * view.zoom - group.offset}px`;
745
+ applyStoryChrome(groupLabel, group);
746
+ }
747
+ };
748
+
749
+ // One block per Storybook component, stacked top to bottom. Frames inside a
750
+ // block may differ in size because each story owns its viewport globals.
751
+ const layout = (): void => {
752
+ layouts.clear();
753
+ groupOrigins.clear();
754
+ const placement = reactStoryBoardLayout(stories, presentation.index, presentationFor);
755
+ for (const [key, origin] of placement.groupOrigins) groupOrigins.set(key, origin);
756
+ for (const story of stories) {
757
+ const frameLayout = placement.layouts.get(story.id);
758
+ if (!frameLayout) continue;
759
+ layouts.set(story.id, frameLayout);
760
+ const { x, y, width, height } = frameLayout;
761
+ const presentation = presentationFor(story);
762
+ const frame = frames.get(story.id)!;
763
+ frame.element.style.left = `${x}px`;
764
+ frame.element.style.top = `${y}px`;
765
+ frame.element.style.width = `${width}px`;
766
+ frame.element.style.height = `${height}px`;
767
+ // A measured story's CONTENT stays at its mount-harness size and is
768
+ // translated so the measured rectangle fills the frame — the frame is
769
+ // a window onto the declared viewport, exactly the canvas exhibit's
770
+ // crop. Everything else keeps content == frame.
771
+ const measured = measuredFrames.get(story.id);
772
+ frame.element.style.overflow = measured ? 'hidden' : '';
773
+ frame.content.style.width = `${measured ? measured.mount.width : width}px`;
774
+ frame.content.style.height = `${measured ? measured.mount.height : height}px`;
775
+ frame.content.style.left = `${measured ? -measured.offset.x : 0}px`;
776
+ frame.content.style.top = `${measured ? -measured.offset.y : 0}px`;
777
+ frame.content.dataset['storyLayout'] = presentation.layout;
778
+ frame.content.style.padding = presentation.layout === 'padded' ? '16px' : '0';
779
+ frame.content.style.display = presentation.layout === 'centered' ? 'flex' : 'block';
780
+ frame.content.style.alignItems = presentation.layout === 'centered' ? 'center' : '';
781
+ frame.content.style.justifyContent = presentation.layout === 'centered' ? 'center' : '';
782
+ }
783
+ const boardWidth = placement.width;
784
+ const boardHeight = placement.height;
785
+ boardElement.style.width = `${boardWidth}px`;
786
+ boardElement.style.height = `${boardHeight}px`;
787
+ layer.style.width = `${boardWidth}px`;
788
+ layer.style.height = `${boardHeight}px`;
789
+ positionLabels();
790
+ };
791
+
792
+ const fitAll = (): void => {
793
+ const width = Number.parseFloat(boardElement.style.width);
794
+ const height = Number.parseFloat(boardElement.style.height);
795
+ fitBounds(container, { x: 0, y: 0, width, height }, 1);
796
+ };
797
+
798
+ const scheduleFitActive = (): void => {
799
+ if (pendingFit) return;
800
+ pendingFit = true;
801
+ queueMicrotask(() => {
802
+ pendingFit = false;
803
+ if (!disposed) activate(activeStoryId, true);
804
+ });
805
+ };
806
+
807
+ const activate = (storyId: string, focus = false): void => {
808
+ const frame = frames.get(storyId);
809
+ const frameLayout = layouts.get(storyId);
810
+ if (!frame || !frameLayout) return;
811
+ activeStoryId = storyId;
812
+ updateActiveAppearance();
813
+ updateViewportSnapshot();
814
+ notifyBoardViewport();
815
+ if (focus) fitBounds(container, frameLayout, 2);
816
+ };
817
+
818
+ const storyAtClientPoint = (clientX: number, clientY: number): string | null => {
819
+ for (const [storyId, frame] of frames) {
820
+ const rect = frame.content.getBoundingClientRect();
821
+ if (
822
+ clientX >= rect.left &&
823
+ clientX <= rect.right &&
824
+ clientY >= rect.top &&
825
+ clientY <= rect.bottom
826
+ ) {
827
+ return storyId;
828
+ }
829
+ }
830
+ return null;
831
+ };
832
+
833
+ const unsubscribeViewport = subscribeRootCanvasViewport(() => {
834
+ layout();
835
+ scheduleFitActive();
836
+ });
837
+ const unsubscribePan = subscribeRootPan(positionLabels);
838
+
839
+ layout();
840
+ updateActiveAppearance();
841
+ positionLabels();
842
+
843
+ const board: ReactStoryBoard = {
844
+ element: boardElement,
845
+ frames,
846
+ activeContent: () => frames.get(activeStoryId)!.content,
847
+ activate,
848
+ clearError: (storyId) => {
849
+ const error = errors.get(storyId);
850
+ if (!error) return;
851
+ error.style.display = 'none';
852
+ error.textContent = '';
853
+ },
854
+ fitAll,
855
+ selectAtClientPoint: (clientX, clientY) => {
856
+ const storyId = storyAtClientPoint(clientX, clientY);
857
+ if (!storyId || storyId === activeStoryId) return false;
858
+ onStorySelected(storyId, 'highlight');
859
+ return true;
860
+ },
861
+ zoomAtClientPoint: (clientX, clientY) => {
862
+ const storyId = storyAtClientPoint(clientX, clientY);
863
+ if (!storyId) return false;
864
+ onStorySelected(storyId, 'zoom');
865
+ return true;
866
+ },
867
+ setError: (storyId, message) => {
868
+ const error = errors.get(storyId);
869
+ if (!error) return;
870
+ error.textContent = message;
871
+ error.style.display = 'block';
872
+ },
873
+ setMeasuredFrame: (storyId, measured) => {
874
+ if (disposed || !frames.has(storyId)) return;
875
+ const previous = measuredFrames.get(storyId);
876
+ if (measured === null) {
877
+ if (!previous) return;
878
+ measuredFrames.delete(storyId);
879
+ } else {
880
+ if (
881
+ previous &&
882
+ previous.frame.width === measured.frame.width &&
883
+ previous.frame.height === measured.frame.height &&
884
+ previous.mount.width === measured.mount.width &&
885
+ previous.mount.height === measured.mount.height &&
886
+ previous.offset.x === measured.offset.x &&
887
+ previous.offset.y === measured.offset.y
888
+ ) {
889
+ return;
890
+ }
891
+ measuredFrames.set(storyId, measured);
892
+ }
893
+ layout();
894
+ positionLabels();
895
+ },
896
+ setNote: (message) => {
897
+ for (const note of notes.values()) {
898
+ note.textContent = message ?? '';
899
+ note.style.display = message === null ? 'none' : 'block';
900
+ }
901
+ },
902
+ viewport: () => viewportSnapshot,
903
+ setViewport: (viewportId) => {
904
+ if (viewportSnapshot.locked) return;
905
+ viewportOverrides.set(activeStoryId, viewportId);
906
+ layout();
907
+ updateViewportSnapshot();
908
+ notifyBoardViewport();
909
+ activate(activeStoryId, true);
910
+ },
911
+ setRotation: (rotated) => {
912
+ if (viewportSnapshot.locked) return;
913
+ rotationOverrides.set(activeStoryId, rotated);
914
+ layout();
915
+ updateViewportSnapshot();
916
+ notifyBoardViewport();
917
+ activate(activeStoryId, true);
918
+ },
919
+ dispose: () => {
920
+ if (disposed) return;
921
+ disposed = true;
922
+ unsubscribeViewport();
923
+ unsubscribePan();
924
+ if (boardsByContainer.get(container) === board) boardsByContainer.delete(container);
925
+ notifyBoardViewport();
926
+ boardElement.remove();
927
+ chromeElement.remove();
928
+ },
929
+ };
930
+ boardsByContainer.set(container, board);
931
+ notifyBoardViewport();
932
+ // Open at a useful editing scale on the active story. The complete-board
933
+ // overview remains one explicit "Fit board" click away; auto-fitting every
934
+ // story made desktop-resolution screens postage stamps on first open.
935
+ scheduleFitActive();
936
+ return board;
937
+ }