@volter/sdk 0.0.0-stage → 0.5.204

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 (523) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +20 -0
  3. package/README.md +38 -3
  4. package/package.json +510 -4
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +88 -0
  7. package/src/client.ts +1646 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +619 -0
  10. package/src/css-numeric-style.ts +97 -0
  11. package/src/document-probe.ts +282 -0
  12. package/src/editor-view.ts +225 -0
  13. package/src/extension.ts +40 -0
  14. package/src/generations.ts +178 -0
  15. package/src/host.ts +1157 -0
  16. package/src/http-transport.browser.ts +14 -0
  17. package/src/http-transport.node.ts +19 -0
  18. package/src/index.ts +131 -0
  19. package/src/kit/CapabilityCoverageSection.tsx +185 -0
  20. package/src/kit/account-client.ts +333 -0
  21. package/src/kit/action-registry.ts +317 -0
  22. package/src/kit/active-product.ts +76 -0
  23. package/src/kit/active-project.ts +155 -0
  24. package/src/kit/adapter-editor-config.ts +25 -0
  25. package/src/kit/adapter-module.ts +7 -0
  26. package/src/kit/adapter-observation.ts +49 -0
  27. package/src/kit/animation/animation-clock.ts +479 -0
  28. package/src/kit/animation/stage-transport.ts +385 -0
  29. package/src/kit/api/assets.ts +365 -0
  30. package/src/kit/api/project-open.ts +355 -0
  31. package/src/kit/api/project-source.ts +180 -0
  32. package/src/kit/api/project-state.ts +110 -0
  33. package/src/kit/api/relay.ts +270 -0
  34. package/src/kit/api/themes.ts +45 -0
  35. package/src/kit/api-asset-library-wire.ts +45 -0
  36. package/src/kit/api-base.ts +10 -0
  37. package/src/kit/api-build.ts +99 -0
  38. package/src/kit/api-git-wire.ts +56 -0
  39. package/src/kit/api-logs.ts +92 -0
  40. package/src/kit/api-project-identity.ts +74 -0
  41. package/src/kit/api-settings.ts +36 -0
  42. package/src/kit/api-worktrees.ts +205 -0
  43. package/src/kit/asset-capabilities.ts +344 -0
  44. package/src/kit/asset-compare-core.ts +171 -0
  45. package/src/kit/asset-editor-context.tsx +101 -0
  46. package/src/kit/asset-events.ts +96 -0
  47. package/src/kit/asset-inspector-actions.ts +87 -0
  48. package/src/kit/asset-selection-viewer-registry.ts +113 -0
  49. package/src/kit/asset-selection.ts +146 -0
  50. package/src/kit/asset-thumbnails.ts +25 -0
  51. package/src/kit/asset-viewers.ts +115 -0
  52. package/src/kit/asset-workflow/asset-import-jobs.ts +106 -0
  53. package/src/kit/asset-workflow/asset-ledger-backend.ts +126 -0
  54. package/src/kit/asset-workflow/asset-ledger.ts +156 -0
  55. package/src/kit/asset-workflow/asset-materialization-report.ts +140 -0
  56. package/src/kit/asset-workflow/asset-pack-manifest.ts +320 -0
  57. package/src/kit/asset-workflow/asset-types.ts +142 -0
  58. package/src/kit/asset-workflow/audio-preview-player.ts +193 -0
  59. package/src/kit/asset-workflow/audio-waveform.ts +22 -0
  60. package/src/kit/asset-workflow/cloud-asset-client.ts +263 -0
  61. package/src/kit/asset-workflow/hosted-asset-materialization.ts +236 -0
  62. package/src/kit/asset-workflow/image-view-scale.ts +31 -0
  63. package/src/kit/asset-workflow/import-contract.ts +124 -0
  64. package/src/kit/asset-workflow/ledger-write-lock.ts +244 -0
  65. package/src/kit/asset-workflow/pixi-spritesheet.ts +197 -0
  66. package/src/kit/asset-workflow/preview-resource-lifetime.ts +44 -0
  67. package/src/kit/asset-workflow/project-asset-commands.ts +20 -0
  68. package/src/kit/asset-workflow/project-content.ts +288 -0
  69. package/src/kit/asset-workflow/project-source-index.ts +545 -0
  70. package/src/kit/asset-workflow/thumbnail-system.ts +256 -0
  71. package/src/kit/authoring/active-adapter.ts +200 -0
  72. package/src/kit/authoring/active-systems.ts +422 -0
  73. package/src/kit/authoring/adapter-key.ts +18 -0
  74. package/src/kit/authoring/authoring-asset-url.ts +27 -0
  75. package/src/kit/authoring/bootstrap-state.ts +49 -0
  76. package/src/kit/authoring/boundary-authoring-adapter.ts +189 -0
  77. package/src/kit/authoring/canvas-scene-guides.ts +84 -0
  78. package/src/kit/authoring/composite-authoring-adapter.ts +2109 -0
  79. package/src/kit/authoring/consumer-actions.ts +531 -0
  80. package/src/kit/authoring/design-time-layers.ts +852 -0
  81. package/src/kit/authoring/design-time-mount-registry.ts +244 -0
  82. package/src/kit/authoring/edit-mode-authoring.ts +637 -0
  83. package/src/kit/authoring/empty-project-authoring.ts +22 -0
  84. package/src/kit/authoring/instance-source-menu.ts +135 -0
  85. package/src/kit/authoring/layered-pick.ts +185 -0
  86. package/src/kit/authoring/mounted-root-subjects.ts +146 -0
  87. package/src/kit/authoring/no-authoring-adapter.ts +59 -0
  88. package/src/kit/authoring/object3d-document-persistence.ts +122 -0
  89. package/src/kit/authoring/panel-authoring.ts +121 -0
  90. package/src/kit/authoring/project-authoring-session.ts +105 -0
  91. package/src/kit/authoring/provenance.ts +99 -0
  92. package/src/kit/authoring/react-canvas-navigation.ts +259 -0
  93. package/src/kit/authoring/react-design-canvas-style.ts +20 -0
  94. package/src/kit/authoring/react-story-board.ts +917 -0
  95. package/src/kit/authoring/selection-scope.ts +195 -0
  96. package/src/kit/authoring/shell-document-ops.ts +169 -0
  97. package/src/kit/authoring/story-board-chrome-fit.ts +107 -0
  98. package/src/kit/authoring/story-board-presentation.ts +111 -0
  99. package/src/kit/authoring/three-root.ts +67 -0
  100. package/src/kit/authoring/viewport-tool-context.ts +73 -0
  101. package/src/kit/authoring/viewport-tool-owner.ts +38 -0
  102. package/src/kit/authoring/world-session-state.ts +101 -0
  103. package/src/kit/authoring-seam-evidence.ts +300 -0
  104. package/src/kit/availability-tick.ts +66 -0
  105. package/src/kit/bitmap-label.ts +120 -0
  106. package/src/kit/boot-routing.ts +392 -0
  107. package/src/kit/breakpoint-state.ts +43 -0
  108. package/src/kit/build-identity.ts +16 -0
  109. package/src/kit/bytes-codec.ts +62 -0
  110. package/src/kit/cancellation-reason.ts +58 -0
  111. package/src/kit/canvas-frames.ts +88 -0
  112. package/src/kit/capture-camera-pose.ts +77 -0
  113. package/src/kit/capture-size.ts +88 -0
  114. package/src/kit/chrome-registry.ts +159 -0
  115. package/src/kit/chrome-slot-registry.ts +91 -0
  116. package/src/kit/collaboration-client.ts +264 -0
  117. package/src/kit/collaboration-presence.ts +41 -0
  118. package/src/kit/command-dispatch.ts +19 -0
  119. package/src/kit/command-listener.ts +2182 -0
  120. package/src/kit/command-registry.ts +71 -0
  121. package/src/kit/component-board-registry.ts +205 -0
  122. package/src/kit/component-states-registry.ts +199 -0
  123. package/src/kit/components/AlignToolbar.tsx +204 -0
  124. package/src/kit/components/ApplicationMenus.tsx +372 -0
  125. package/src/kit/components/AssetEditorShell.tsx +216 -0
  126. package/src/kit/components/AssetInspectorToolSection.tsx +124 -0
  127. package/src/kit/components/BoardRulers.tsx +354 -0
  128. package/src/kit/components/CanvasAddNodeDialogs.tsx +529 -0
  129. package/src/kit/components/CanvasSceneViewport.tsx +1195 -0
  130. package/src/kit/components/ChromeSlot.tsx +20 -0
  131. package/src/kit/components/CodeView.tsx +470 -0
  132. package/src/kit/components/CompactInspectorShell.tsx +39 -0
  133. package/src/kit/components/ConsolePanel.tsx +273 -0
  134. package/src/kit/components/GameplaySessionTimeline.tsx +295 -0
  135. package/src/kit/components/InspectionProjection.tsx +932 -0
  136. package/src/kit/components/Inspector.tsx +270 -0
  137. package/src/kit/components/InspectorCanvasPreview.tsx +35 -0
  138. package/src/kit/components/InspectorFieldsSection.tsx +290 -0
  139. package/src/kit/components/InspectorStoriesSection.tsx +92 -0
  140. package/src/kit/components/InspectorToolSection.tsx +96 -0
  141. package/src/kit/components/InspectorTransformSection.tsx +245 -0
  142. package/src/kit/components/LightExplorerPanel.tsx +433 -0
  143. package/src/kit/components/MediaProperties.tsx +145 -0
  144. package/src/kit/components/ProjectHeader.tsx +328 -0
  145. package/src/kit/components/ReactCanvasControls.tsx +358 -0
  146. package/src/kit/components/RootSelectionOverlay.tsx +3688 -0
  147. package/src/kit/components/RootTextEditor.tsx +79 -0
  148. package/src/kit/components/SaveStatus.tsx +70 -0
  149. package/src/kit/components/SurfaceCrashBoundary.tsx +105 -0
  150. package/src/kit/components/SurfaceStateOverlay.tsx +24 -0
  151. package/src/kit/components/ToolContributionSurfaces.tsx +49 -0
  152. package/src/kit/components/ToolHost.tsx +380 -0
  153. package/src/kit/components/Toolbar.tsx +811 -0
  154. package/src/kit/components/TransientHint.tsx +44 -0
  155. package/src/kit/components/VersionControlSection.tsx +470 -0
  156. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  157. package/src/kit/components/VolterLogo.tsx +18 -0
  158. package/src/kit/components/WorktreeSwitcher.tsx +712 -0
  159. package/src/kit/components/account-documents.tsx +1162 -0
  160. package/src/kit/components/asset-documents.tsx +794 -0
  161. package/src/kit/components/asset-editor-persistence.ts +216 -0
  162. package/src/kit/components/asset-selection-section.tsx +545 -0
  163. package/src/kit/components/asset-thumbnails.tsx +307 -0
  164. package/src/kit/components/asset-viewers/AudioViewer.tsx +201 -0
  165. package/src/kit/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  166. package/src/kit/components/asset-viewers/ImageViewer.tsx +300 -0
  167. package/src/kit/components/asset-viewers/JsonAssetDocument.tsx +98 -0
  168. package/src/kit/components/asset-viewers/OnlineAssetDetail.tsx +426 -0
  169. package/src/kit/components/asset-viewers/SourceAssetViewer.tsx +356 -0
  170. package/src/kit/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  171. package/src/kit/components/asset-viewers/VideoViewer.tsx +101 -0
  172. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  173. package/src/kit/components/board-guides.ts +150 -0
  174. package/src/kit/components/canvas-scene-hotkeys.ts +37 -0
  175. package/src/kit/components/canvas-temporary-pivot.ts +34 -0
  176. package/src/kit/components/core-utilities.tsx +94 -0
  177. package/src/kit/components/inspector-preview-section.tsx +223 -0
  178. package/src/kit/components/inspector-revert-label.ts +20 -0
  179. package/src/kit/components/inspector-selection.ts +42 -0
  180. package/src/kit/components/inspector-stories-gating.ts +171 -0
  181. package/src/kit/components/inspector-transform-subject.ts +11 -0
  182. package/src/kit/components/inspector-transform.ts +88 -0
  183. package/src/kit/components/kind-documents.tsx +544 -0
  184. package/src/kit/components/primitives/DraftColorInput.tsx +74 -0
  185. package/src/kit/components/project-tool-documents.tsx +402 -0
  186. package/src/kit/components/scene-documents.tsx +221 -0
  187. package/src/kit/components/status-contributions.tsx +407 -0
  188. package/src/kit/components/tool-documents.tsx +302 -0
  189. package/src/kit/components/tool-schema-form.tsx +262 -0
  190. package/src/kit/components/use-after-paint.ts +41 -0
  191. package/src/kit/components/use-project-image-assets.ts +86 -0
  192. package/src/kit/components/workspace-history.ts +32 -0
  193. package/src/kit/components/world-documents.tsx +570 -0
  194. package/src/kit/components/world-overlay-gestures.ts +1939 -0
  195. package/src/kit/composite-screenshot.ts +2238 -0
  196. package/src/kit/content-entry-source-registry.ts +184 -0
  197. package/src/kit/contribution-surfaces.ts +48 -0
  198. package/src/kit/coverage/canvas-reveal.ts +192 -0
  199. package/src/kit/coverage/design-time-surfaces.ts +101 -0
  200. package/src/kit/coverage/ontology-invariants.ts +466 -0
  201. package/src/kit/coverage/session-vitals.ts +503 -0
  202. package/src/kit/crash-null-boundary.ts +36 -0
  203. package/src/kit/creation-site-edit.ts +1491 -0
  204. package/src/kit/creation-site-registry.ts +160 -0
  205. package/src/kit/delegate-harness-registry.ts +134 -0
  206. package/src/kit/document-areas.ts +70 -0
  207. package/src/kit/document-context-registry.ts +193 -0
  208. package/src/kit/document-open-registry.ts +200 -0
  209. package/src/kit/document-play-extension.ts +221 -0
  210. package/src/kit/document-preview-source.ts +20 -0
  211. package/src/kit/document-renderer-session.ts +138 -0
  212. package/src/kit/document-stage-sessions.ts +26 -0
  213. package/src/kit/document-viewports.ts +120 -0
  214. package/src/kit/editor-api.ts +46 -0
  215. package/src/kit/editor-chrome-capture.ts +136 -0
  216. package/src/kit/editor-commands.ts +176 -0
  217. package/src/kit/editor-console.ts +580 -0
  218. package/src/kit/editor-current-view.ts +56 -0
  219. package/src/kit/editor-document-probe.ts +1168 -0
  220. package/src/kit/editor-git-client.ts +115 -0
  221. package/src/kit/editor-hotkeys.ts +728 -0
  222. package/src/kit/editor-lease-view.ts +39 -0
  223. package/src/kit/editor-lease.ts +415 -0
  224. package/src/kit/editor-mode.ts +19 -0
  225. package/src/kit/editor-notifications.ts +140 -0
  226. package/src/kit/editor-presence.ts +563 -0
  227. package/src/kit/editor-presentation-activity.ts +58 -0
  228. package/src/kit/editor-presentation-notice.ts +42 -0
  229. package/src/kit/editor-runtime.tsx +147 -0
  230. package/src/kit/editor-server-response.ts +86 -0
  231. package/src/kit/editor-session-attribution.ts +85 -0
  232. package/src/kit/editor-session-mode.ts +54 -0
  233. package/src/kit/editor-state-facets.ts +74 -0
  234. package/src/kit/editor-view-presentation.ts +777 -0
  235. package/src/kit/environment-images.ts +58 -0
  236. package/src/kit/eyedropper-session.ts +60 -0
  237. package/src/kit/files/file-provider.ts +62 -0
  238. package/src/kit/files/project-files.ts +270 -0
  239. package/src/kit/finders/index.ts +137 -0
  240. package/src/kit/finders/scenes-from-entrypoint-selection.ts +387 -0
  241. package/src/kit/frame/frame-parts.ts +30 -0
  242. package/src/kit/framed-document-capture.ts +34 -0
  243. package/src/kit/game-globals-prelude.ts +143 -0
  244. package/src/kit/game-surface-defaults.ts +33 -0
  245. package/src/kit/gameplay-dom-recording.ts +318 -0
  246. package/src/kit/gameplay-export-state.ts +14 -0
  247. package/src/kit/gameplay-replay.ts +417 -0
  248. package/src/kit/gameplay-session-time.ts +9 -0
  249. package/src/kit/gameplay-sessions.ts +204 -0
  250. package/src/kit/hierarchy-component-marks.ts +298 -0
  251. package/src/kit/hierarchy-internals.ts +197 -0
  252. package/src/kit/hierarchy-kind-icon.ts +217 -0
  253. package/src/kit/hierarchy-menu-registry.ts +67 -0
  254. package/src/kit/hierarchy-node-rows.ts +307 -0
  255. package/src/kit/hierarchy-panel-view.ts +280 -0
  256. package/src/kit/hierarchy-projection.ts +76 -0
  257. package/src/kit/hierarchy-row-media.ts +45 -0
  258. package/src/kit/hierarchy-row-model.ts +308 -0
  259. package/src/kit/hierarchy-rows.ts +11 -0
  260. package/src/kit/hierarchy-walk.ts +86 -0
  261. package/src/kit/history/editor-session.ts +25 -0
  262. package/src/kit/history/history-commands.ts +147 -0
  263. package/src/kit/history/history-delegate.ts +187 -0
  264. package/src/kit/history/history-limit-notices.ts +43 -0
  265. package/src/kit/history/history-service.ts +1189 -0
  266. package/src/kit/history/persistence-coordinator.ts +35 -0
  267. package/src/kit/history/project-file-history.ts +386 -0
  268. package/src/kit/history/project-root-history-backends.ts +139 -0
  269. package/src/kit/history/resource-registry.ts +209 -0
  270. package/src/kit/history/snapshot-store.ts +103 -0
  271. package/src/kit/history/source-history-backend.ts +546 -0
  272. package/src/kit/history-types.ts +124 -0
  273. package/src/kit/hmr-registration-group.ts +67 -0
  274. package/src/kit/hmr-stable-react-context.ts +23 -0
  275. package/src/kit/hotkeys.ts +190 -0
  276. package/src/kit/inference-diagnostics.ts +69 -0
  277. package/src/kit/initial-project.ts +80 -0
  278. package/src/kit/inspection/active-subject.ts +571 -0
  279. package/src/kit/inspection/active-surface.ts +142 -0
  280. package/src/kit/inspection/compose-subject.ts +1055 -0
  281. package/src/kit/inspection/compose.ts +7 -0
  282. package/src/kit/inspection/display.ts +171 -0
  283. package/src/kit/inspection/document-subject.ts +109 -0
  284. package/src/kit/inspection/game-subject.ts +85 -0
  285. package/src/kit/inspection/null-subject.ts +119 -0
  286. package/src/kit/inspection/serialize.ts +357 -0
  287. package/src/kit/inspection/use-active-inspection.ts +180 -0
  288. package/src/kit/inspection-model.ts +542 -0
  289. package/src/kit/inspection-node-media.ts +58 -0
  290. package/src/kit/inspector-presentation.ts +203 -0
  291. package/src/kit/inspector-property-grouping.ts +64 -0
  292. package/src/kit/inspector-section-registry.ts +221 -0
  293. package/src/kit/instance-source-actions.ts +163 -0
  294. package/src/kit/js-heap.ts +71 -0
  295. package/src/kit/key-actions.ts +91 -0
  296. package/src/kit/keymap-presets.ts +428 -0
  297. package/src/kit/layout-policy.ts +31 -0
  298. package/src/kit/light-explorer-model.ts +134 -0
  299. package/src/kit/live-canvas-frame.ts +55 -0
  300. package/src/kit/live-document.ts +296 -0
  301. package/src/kit/live-gesture-lock.ts +50 -0
  302. package/src/kit/live-seam-evidence.ts +11 -0
  303. package/src/kit/live-session-registry.ts +220 -0
  304. package/src/kit/live-transition.ts +391 -0
  305. package/src/kit/manifest-project.ts +107 -0
  306. package/src/kit/module-fetch-diagnosis.ts +192 -0
  307. package/src/kit/mount-failure-report.ts +154 -0
  308. package/src/kit/native-selection-style.ts +497 -0
  309. package/src/kit/object3d-document-write-policy.ts +137 -0
  310. package/src/kit/packaged-runtime.ts +108 -0
  311. package/src/kit/palettes/maya.palette.json +57 -0
  312. package/src/kit/palettes/substance.palette.json +57 -0
  313. package/src/kit/performance-profiler.ts +367 -0
  314. package/src/kit/performance-sources.ts +69 -0
  315. package/src/kit/photograph-notice.ts +141 -0
  316. package/src/kit/play-boot-phase.ts +166 -0
  317. package/src/kit/play-camera-flight.ts +35 -0
  318. package/src/kit/png-encode.worker.ts +26 -0
  319. package/src/kit/presentation-surface.ts +248 -0
  320. package/src/kit/product-command.ts +90 -0
  321. package/src/kit/project-adapter.ts +1140 -0
  322. package/src/kit/project-asset-refresh.ts +23 -0
  323. package/src/kit/project-asset-roots.ts +68 -0
  324. package/src/kit/project-local-state.ts +151 -0
  325. package/src/kit/project-manager.ts +243 -0
  326. package/src/kit/project-module-changes.ts +201 -0
  327. package/src/kit/project-module-split.ts +270 -0
  328. package/src/kit/project-play-layers.ts +25 -0
  329. package/src/kit/project-provenance.ts +115 -0
  330. package/src/kit/project-ready.ts +42 -0
  331. package/src/kit/project-shape.ts +68 -0
  332. package/src/kit/project-tools.ts +107 -0
  333. package/src/kit/projection-types.ts +44 -0
  334. package/src/kit/readiness.ts +113 -0
  335. package/src/kit/renderer-resource-counts.ts +27 -0
  336. package/src/kit/reported-play-state.ts +90 -0
  337. package/src/kit/resolve-contributed-command.ts +14 -0
  338. package/src/kit/resolve-relative-specifier.ts +33 -0
  339. package/src/kit/retained-document-states.ts +91 -0
  340. package/src/kit/scene-document-plan.ts +320 -0
  341. package/src/kit/scene-live-open.ts +210 -0
  342. package/src/kit/scoped-game-css.ts +152 -0
  343. package/src/kit/served-url.ts +5 -0
  344. package/src/kit/session-close.ts +17 -0
  345. package/src/kit/session-tombstone.ts +127 -0
  346. package/src/kit/settings/settings-provider.ts +82 -0
  347. package/src/kit/settings-store.ts +348 -0
  348. package/src/kit/shell-document-state.ts +27 -0
  349. package/src/kit/shell-store-door.ts +45 -0
  350. package/src/kit/shell-store.ts +722 -0
  351. package/src/kit/source-conflict.ts +122 -0
  352. package/src/kit/stage-context.ts +377 -0
  353. package/src/kit/stage-invalidation.ts +25 -0
  354. package/src/kit/stage-store-registry.ts +69 -0
  355. package/src/kit/startup-failure.ts +80 -0
  356. package/src/kit/state-report-deferral.ts +73 -0
  357. package/src/kit/storage/host-files-storage.ts +97 -0
  358. package/src/kit/storage/http-storage.ts +174 -0
  359. package/src/kit/storage/index.ts +75 -0
  360. package/src/kit/storage/mem-storage.ts +158 -0
  361. package/src/kit/storage/path-lock.ts +44 -0
  362. package/src/kit/storage/paths.ts +26 -0
  363. package/src/kit/storage-types.ts +127 -0
  364. package/src/kit/stories/StoryPreviewMount.tsx +306 -0
  365. package/src/kit/stories/compose-project-stories.ts +255 -0
  366. package/src/kit/stories/prefabs-finder.ts +54 -0
  367. package/src/kit/stories/prefabs-from-stories.ts +182 -0
  368. package/src/kit/stories/project-story-regions.ts +24 -0
  369. package/src/kit/stories/story-capture.ts +579 -0
  370. package/src/kit/stories/story-declared-medium.ts +126 -0
  371. package/src/kit/stories/story-discovery.ts +176 -0
  372. package/src/kit/stories/story-dom-runtime.ts +78 -0
  373. package/src/kit/stories/story-grouping.ts +111 -0
  374. package/src/kit/stories/story-mount-turn.ts +27 -0
  375. package/src/kit/stories/story-presentation.ts +215 -0
  376. package/src/kit/stories/story-preview-component.ts +7 -0
  377. package/src/kit/stories/story-registry.ts +530 -0
  378. package/src/kit/stories-scope.ts +35 -0
  379. package/src/kit/story-document-openers.ts +36 -0
  380. package/src/kit/story-thumbnails.ts +47 -0
  381. package/src/kit/surface-keyboard.ts +101 -0
  382. package/src/kit/surface-state.ts +135 -0
  383. package/src/kit/system-seam-evidence.ts +72 -0
  384. package/src/kit/tab-census.ts +202 -0
  385. package/src/kit/tab-lifecycle-client.ts +227 -0
  386. package/src/kit/theme-library.ts +897 -0
  387. package/src/kit/theme-preference.ts +429 -0
  388. package/src/kit/three-viewport-presentation.ts +23 -0
  389. package/src/kit/tool-contribution-play.ts +74 -0
  390. package/src/kit/tool-loader.ts +1918 -0
  391. package/src/kit/transform-mode-request.ts +66 -0
  392. package/src/kit/transient-hint.ts +78 -0
  393. package/src/kit/transport-strip.tsx +174 -0
  394. package/src/kit/ui-source/adapter-region-includes.ts +238 -0
  395. package/src/kit/ui-source/file-region-resolver.ts +302 -0
  396. package/src/kit/ui-source/inspect.ts +775 -0
  397. package/src/kit/ui-source/source-write-backend.ts +605 -0
  398. package/src/kit/ui-source/tier-source-write-backend.ts +279 -0
  399. package/src/kit/user-local-state.ts +105 -0
  400. package/src/kit/viewport-activation-timings.ts +840 -0
  401. package/src/kit/viewport-editor-controls.ts +22 -0
  402. package/src/kit/viewport-presentation.ts +668 -0
  403. package/src/kit/viewport-surface-status.tsx +55 -0
  404. package/src/kit/wait-until.ts +37 -0
  405. package/src/kit/worker-call-metrics.ts +166 -0
  406. package/src/kit/workspace-areas.ts +191 -0
  407. package/src/kit/workspace-aux-commands.ts +11 -0
  408. package/src/kit/workspace-available-documents.ts +142 -0
  409. package/src/kit/workspace-core-utilities.ts +31 -0
  410. package/src/kit/workspace-document-ids.ts +59 -0
  411. package/src/kit/workspace-document-registry.ts +624 -0
  412. package/src/kit/workspace-document-restore.ts +146 -0
  413. package/src/kit/workspace-host-commands.ts +141 -0
  414. package/src/kit/workspace-persistence-gate.ts +40 -0
  415. package/src/kit/workspace-play-utilities.ts +44 -0
  416. package/src/kit/workspace-presets.ts +446 -0
  417. package/src/kit/workspace-regions.ts +276 -0
  418. package/src/kit/workspace-static-panels.ts +73 -0
  419. package/src/kit/workspace-status-registry.ts +121 -0
  420. package/src/kit/workspace-storage.ts +35 -0
  421. package/src/kit/workspace-style.ts +226 -0
  422. package/src/kit/workspace-utility-commands.ts +74 -0
  423. package/src/kit/workspace-utility-registry.ts +263 -0
  424. package/src/kit/world-adoption-event.ts +23 -0
  425. package/src/kit/world-adoption.ts +115 -0
  426. package/src/kit/world-canvas-viewport-state.ts +35 -0
  427. package/src/kit/world-document-routing.ts +104 -0
  428. package/src/kit/world-pan-state.ts +198 -0
  429. package/src/kit/write-pipe.ts +173 -0
  430. package/src/layout-arrangements.ts +5 -0
  431. package/src/layouts.tsx +108 -0
  432. package/src/looks.ts +16 -0
  433. package/src/project/output-roots.ts +73 -0
  434. package/src/project/tab-census.ts +155 -0
  435. package/src/project-tool-catalog.ts +104 -0
  436. package/src/selection.tsx +107 -0
  437. package/src/services.ts +18 -0
  438. package/src/session/build-report.ts +22 -0
  439. package/src/session/collaboration-types.ts +262 -0
  440. package/src/session/command-table.ts +327 -0
  441. package/src/session/discovery.ts +100 -0
  442. package/src/session/editor-brand.ts +48 -0
  443. package/src/session/editor-compatibility.ts +329 -0
  444. package/src/session/editor-control-lifecycle.ts +68 -0
  445. package/src/session/editor-control-protocol.ts +5 -0
  446. package/src/session/entrypoint-selection-readers.ts +66 -0
  447. package/src/session/entrypoint-selection-source.ts +120 -0
  448. package/src/session/game-css-scope.ts +30 -0
  449. package/src/session/hosted-attachment.ts +225 -0
  450. package/src/session/limited-view.ts +82 -0
  451. package/src/session/product-create.ts +24 -0
  452. package/src/session/product-locator.ts +478 -0
  453. package/src/session/project-module-url.ts +242 -0
  454. package/src/session/project-serving.ts +164 -0
  455. package/src/session/project-upgrade.ts +669 -0
  456. package/src/session/registry-format.ts +210 -0
  457. package/src/session/relative-path-guard.ts +56 -0
  458. package/src/session/scoped-game-css.ts +461 -0
  459. package/src/session/source-glob.ts +15 -0
  460. package/src/session/tool-contribution-convention.ts +123 -0
  461. package/src/session/workbench-locator.ts +712 -0
  462. package/src/session.ts +41 -0
  463. package/src/share.ts +160 -0
  464. package/src/source-analysis.ts +28 -0
  465. package/src/source-authoring.ts +439 -0
  466. package/src/tools/errors.ts +91 -0
  467. package/src/tools/provider-execution.ts +70 -0
  468. package/src/tools/registry.ts +341 -0
  469. package/src/tools/types.ts +159 -0
  470. package/src/transport.ts +100 -0
  471. package/src/types.ts +1693 -0
  472. package/src/views.ts +164 -0
  473. package/src/widgets/design-system.ts +93 -0
  474. package/src/widgets/editor-appearance.ts +151 -0
  475. package/src/widgets/editor-material.ts +83 -0
  476. package/src/widgets/icon-set-registry.ts +105 -0
  477. package/src/widgets/index.ts +71 -0
  478. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  479. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  480. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  481. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  482. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  483. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  484. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  485. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  486. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  487. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  488. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  489. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  490. package/src/widgets/inspector-widgets/index.ts +25 -0
  491. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  492. package/src/widgets/interactive-edit-scope.ts +33 -0
  493. package/src/widgets/patterns/Dialog.tsx +140 -0
  494. package/src/widgets/patterns/Fields.tsx +44 -0
  495. package/src/widgets/patterns/List.tsx +25 -0
  496. package/src/widgets/patterns/StateSurface.tsx +40 -0
  497. package/src/widgets/patterns/Surfaces.tsx +122 -0
  498. package/src/widgets/patterns/Tabs.tsx +80 -0
  499. package/src/widgets/patterns/Toolbar.tsx +72 -0
  500. package/src/widgets/patterns/Tree.tsx +72 -0
  501. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  502. package/src/widgets/primitives/Button.tsx +62 -0
  503. package/src/widgets/primitives/ColorInput.tsx +78 -0
  504. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  505. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  506. package/src/widgets/primitives/FormControls.tsx +88 -0
  507. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  508. package/src/widgets/primitives/JsonInput.tsx +113 -0
  509. package/src/widgets/primitives/Layout.tsx +100 -0
  510. package/src/widgets/primitives/Menu.tsx +161 -0
  511. package/src/widgets/primitives/NumberInput.tsx +169 -0
  512. package/src/widgets/primitives/Panel.tsx +80 -0
  513. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  514. package/src/widgets/primitives/Text.tsx +54 -0
  515. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  516. package/src/widgets/primitives/Tooltip.tsx +204 -0
  517. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  518. package/src/widgets/primitives/banner-tones.ts +32 -0
  519. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  520. package/src/widgets/primitives/editor-icons.ts +254 -0
  521. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  522. package/src/widgets/theme.ts +2841 -0
  523. package/src/widgets/z-index.ts +25 -0
@@ -0,0 +1,1168 @@
1
+ /**
2
+ * The product door onto EDITOR CHROME — read and drive what a person sees in
3
+ * the editor's named surfaces, through the session, without play mode.
4
+ *
5
+ * ## Why this exists (WO: "No product door drives or reads EDITOR CHROME")
6
+ *
7
+ * the editor's `eval` command's `page()` step is play-mode-gated and rooted at the GAME
8
+ * container, so an editor surface that is not a running game — a capability's
9
+ * workspace document, the Data sheet, the Project Tools catalog — could be
10
+ * neither driven nor read through the product at all. The Sheets build could
11
+ * not live-verify a TSV paste or a sheet-tab click: it had a screenshot
12
+ * (`editor.captureActiveDocument`) and nothing else. That gap is the defect the
13
+ * doctrine names — when a live surface can only be diagnosed out of band, the
14
+ * product's own doors have failed.
15
+ *
16
+ * ## The API decision, and why it is this small
17
+ *
18
+ * ONE wire command (`document-probe`) carrying a step union, exposed on the
19
+ * live side as `editor.document.{query,click,drag,key,type,paste,select}`.
20
+ * Each verb is a thing an agent cannot otherwise do and cannot fake:
21
+ *
22
+ * - `query` — READ. What is rendered, as data: tag, role, text, attributes,
23
+ * value/checked/disabled, rect. The one door that answers "what does the
24
+ * human see" for a surface that is not a canvas.
25
+ * - `click` — a REAL user gesture (pointerdown/mousedown/mouseup/click plus
26
+ * focus), not `element.click()`, because a component listening on
27
+ * `pointerdown` (react-data-grid's cell selection does) is invisible to the
28
+ * synthetic shortcut. `clicks: 2` is a real DOUBLE click (numbered
29
+ * `detail` plus the `dblclick` React's `onDoubleClick` listens for) — the
30
+ * Outliner's own rename gesture, and an option on the same verb rather
31
+ * than a second one because it is the same gesture on the same target.
32
+ * - `type` — text into a focused field, character by character, committed
33
+ * with Enter. `paste` cannot stand in: an untrusted `ClipboardEvent`
34
+ * performs no default action, so a plain `<input>` keeps its old value.
35
+ * - `key` — keydown/keyup on the explicit target, else on whatever inside the
36
+ * scope has focus. Arrow keys ARE the spreadsheet's cursor. (No `keypress`:
37
+ * it is deprecated and no shipped surface listens for it.)
38
+ * - `paste` — a real `ClipboardEvent` with a populated `DataTransfer`, which
39
+ * is the only way to exercise a paste handler; nothing else in the product
40
+ * can produce one. Verified live against the Data sheet's own `onPaste`,
41
+ * which read the TSV and wrote the file.
42
+ * - `select` — choose a value on a `<select>`. `click` genuinely cannot do
43
+ * this: the option list of a native dropdown is drawn by the OS, so there
44
+ * is nothing inside the document's container for a pointer gesture to
45
+ * resolve against. And a plain `element.value = x` is a no-op against
46
+ * React, which caches the last value it wrote on the node and puts it back
47
+ * on the next render. So this verb goes through the PROTOTYPE's own value
48
+ * setter (which bypasses that cache) and then dispatches `input`/`change` —
49
+ * the established spelling from this repo's own probes, and the one React's
50
+ * synthetic-event layer honours. Without it the animation transport's clip
51
+ * and speed — two `<select>`s — were undrivable through the product.
52
+ *
53
+ * There is deliberately NO `screenshot` verb: `editor.captureActiveDocument()`
54
+ * already captures the document's content (`editor-view-presentation.ts`'s
55
+ * `activeDocumentContent`); this door's scope is the document's whole box —
56
+ * that content plus its header strip and shelf rail — and a synonym would be
57
+ * a second name for one behavior.
58
+ *
59
+ * ## Scope is a VOCABULARY, and the contract is that it is closed
60
+ *
61
+ * This is NOT general page automation, and the refusal is how that stays true.
62
+ * Every step resolves against ONE NAMED SURFACE — `document` (the default),
63
+ * `header`, `shelf`, `rail`, `outliner`, `content` ({@link DocumentProbeScope}) — and a
64
+ * selector that matches nothing inside it, or a resolved element that is not a
65
+ * descendant of it, is REFUSED with a message naming the scope rather than
66
+ * silently reaching into the rest of the page.
67
+ *
68
+ * ## How a scope finds its element: the owner's own stamp, never a class guess
69
+ *
70
+ * The Properties rail and the Outliner are VS CODE VIEWS (`volter.properties`,
71
+ * `volter.outliner`) holding React portals of ours, so their roots are not
72
+ * anywhere near the document's box and cannot be reached by walking down from
73
+ * it. They are found by the id THE WORKBENCH REGISTERED: the contribution
74
+ * hands each view's body element over as a named part, and `offerVolterPart`
75
+ * (`frame/bridge.tsx`) stamps it `data-volter-part="<part>"` at the moment of
76
+ * the handover. A pane that is disposed takes its stamped element with it, so
77
+ * a closed view answers "not open" instead of matching a stale node. The same
78
+ * shape as `activeDocumentContainer`'s `data-workspace-document-id` and as the
79
+ * portal pair below: the surface publishes its own identity, and this module
80
+ * never guesses from a class name.
81
+ *
82
+ * `rail` and `outliner` are EDITOR CHROME rather than the active document, so
83
+ * (unlike `document`/`header`/`shelf`) they are reachable while the Game
84
+ * document is active and while no document is open at all. The `editor.
85
+ * hierarchy()` / `editor.inspect()` doors still report what those panels
86
+ * RESOLVED; this door reports what they DREW and drives their controls —
87
+ * WALK 5's beats 7 and 12 are the gap it closes (`editor.setField('name')`
88
+ * writes the name, but it is not the Outliner's rename, and nothing at all
89
+ * could click a Properties tab).
90
+ *
91
+ * Two deliberate consequences of the containment rule:
92
+ *
93
+ * - A PORTALED overlay THIS DOCUMENT OPENED is in scope; anyone else's is
94
+ * not. Amended 2026-09-21 (B6), and what changed is the DOM rather than the
95
+ * rule: this note used to say a popover portaled to the theme root recorded
96
+ * no ownership, so any heuristic wide enough to reach it would also reach
97
+ * the editor's chrome menus. `ThemeRootPortal` records it now — an anchor
98
+ * span where the portal was declared and a matching stamp on what landed —
99
+ * so {@link scopeRoots} walks that pair from inside the document's box and
100
+ * reaches exactly the overlays the document opened. A chrome menu's anchor
101
+ * is in the chrome, so it is still refused by the same containment check.
102
+ * Without this a document's own header MENU could be opened by its trigger
103
+ * and never chosen from, which is "a control is accepted through its own
104
+ * click" failing one step short (measured on the Model document's Add
105
+ * menu). Widening further — every `.volter-menu` on the root — is still the
106
+ * "general automation framework" this module exists to not become.
107
+ * - The GAME document is refused outright by the three scopes that ARE the
108
+ * active document (see `resolveDocumentScope`): a game is driven through
109
+ * its own registered commands and the play-gated `page` door, never
110
+ * through synthetic DOM gestures. `rail` and `outliner` are the editor's
111
+ * panels around it and stay open.
112
+ */
113
+
114
+ import type {
115
+ DocumentKeyStep,
116
+ DocumentProbeResult,
117
+ DocumentProbeScope,
118
+ DocumentProbeStep,
119
+ ProbedElement,
120
+ } from '@volter/sdk/document-probe';
121
+ import { MAX_DOCUMENT_KEY_HOLD_MS } from '@volter/sdk/document-probe';
122
+ import { surfaceHoldsKeyboard } from '@volter/sdk/kit/surface-keyboard';
123
+ import { GAME_DOCUMENT_ID } from '@volter/sdk/kit/workspace-document-ids';
124
+ import { framedCapture } from '@volter/sdk/kit/framed-document-capture';
125
+ import {
126
+ activeWorkspaceDocument,
127
+ activeWorkspaceDocumentId,
128
+ activeWorkspaceDocumentViewId,
129
+ openWorkspaceDocuments,
130
+ } from '@volter/sdk/kit/workspace-document-registry';
131
+
132
+ /** The active document's mounted content element — what the `document`,
133
+ * `header` and `shelf` scopes resolve from. Shared with
134
+ * `editor-view-presentation.ts`'s capture so a probe and a screenshot can
135
+ * never disagree about what "the active document" is. */
136
+ export function activeDocumentContainer(documentId: string): HTMLElement | null {
137
+ return (
138
+ Array.from(document.querySelectorAll<HTMLElement>('[data-workspace-document-id]')).find(
139
+ (element) =>
140
+ element.dataset['workspaceDocumentId'] === documentId &&
141
+ (activeWorkspaceDocumentViewId() === null ||
142
+ element.dataset['workspaceViewId'] === activeWorkspaceDocumentViewId()),
143
+ ) ?? null
144
+ );
145
+ }
146
+
147
+ interface Scope {
148
+ readonly container: HTMLElement;
149
+ /** Roots the scope also covers beside its container (the inspector's card, for 'rail'). */
150
+ readonly extraRoots?: readonly HTMLElement[];
151
+ readonly name: DocumentProbeScope;
152
+ readonly id: string;
153
+ readonly title: string;
154
+ }
155
+
156
+ /** THE VOCABULARY, and the one place each word is spelled. Every name resolves
157
+ * through its surface's OWN stamp — see this module's header. */
158
+ const SCOPE_NAMES: readonly DocumentProbeScope[] = [
159
+ 'document',
160
+ 'header',
161
+ 'shelf',
162
+ 'rail',
163
+ 'outliner',
164
+ 'content',
165
+ 'utility',
166
+ 'menubar',
167
+ 'area',
168
+ ];
169
+
170
+ /** The two scopes that are VS Code views: the part id the contribution hands
171
+ * over (`frame/bridge.tsx`'s `offerVolterPart` stamps it), and the view id the
172
+ * workbench registered it under (`volter.contribution.ts`). */
173
+ const VIEW_SCOPES = {
174
+ rail: { part: 'properties', view: 'volter.properties', title: 'Properties' },
175
+ outliner: { part: 'outliner', view: 'volter.outliner', title: 'Outliner' },
176
+ content: { part: 'content', view: 'volter.content', title: 'Content' },
177
+ } as const satisfies Record<string, { part: string; view: string; title: string }>;
178
+
179
+ /** The two scopes that are strips of the ACTIVE DOCUMENT's own box, by the
180
+ * `data-testid` their components write (`DocumentHeaderStrip`,
181
+ * `DocumentShelfRail`). */
182
+ const DOCUMENT_STRIPS = {
183
+ header: { prefix: 'document-header', title: 'header strip' },
184
+ shelf: { prefix: 'document-shelf', title: 'tool shelf' },
185
+ } as const satisfies Record<string, { prefix: string; title: string }>;
186
+
187
+ /**
188
+ * EVERY ROOT THIS SCOPE COVERS — the document's own box, and whatever it
189
+ * PORTALED out of itself.
190
+ *
191
+ * `ThemeRootPortal` writes an anchor span where the portal was declared and
192
+ * stamps both ends with one generated id (`data-volter-portal` in place,
193
+ * `data-volter-portal-content` on what landed at the theme root). So a menu the
194
+ * document's own header opened is found by walking that pair from INSIDE the
195
+ * box — not by a heuristic over every `.volter-menu` on the root, which would
196
+ * also reach the editor's chrome menus and is exactly the widening this
197
+ * module's header rules out. A portal the chrome opened has its anchor in the
198
+ * chrome, so it is not a root here and stays refused.
199
+ *
200
+ * Re-read per step rather than cached with the scope: a menu opens and closes
201
+ * between steps, which is the whole point of driving one.
202
+ */
203
+ function scopeRoots(scope: Scope): HTMLElement[] {
204
+ const roots: HTMLElement[] = [scope.container, ...(scope.extraRoots ?? [])];
205
+ // TRANSITIVE, because a portal opens a portal: a menu's PANEL is portaled,
206
+ // and a submenu opened from one of its rows is portaled from inside THAT
207
+ // panel — so the second anchor is not in the document's box at all. One
208
+ // level deep found the Add menu's rows and none of Mesh's (measured
209
+ // 2026-09-21 on the Model document). The walk terminates because each id is
210
+ // taken once.
211
+ for (let index = 0; index < roots.length; index++) {
212
+ for (const anchor of roots[index]!.querySelectorAll<HTMLElement>('[data-volter-portal]')) {
213
+ const id = anchor.dataset['volterPortal'];
214
+ if (id === undefined) continue;
215
+ const content = document.querySelector<HTMLElement>(
216
+ `[data-volter-portal-content="${CSS.escape(id)}"]`,
217
+ );
218
+ if (content && !roots.includes(content)) roots.push(content);
219
+ }
220
+ }
221
+ // A document whose subject runs in a same-origin frame (the Build Player)
222
+ // hands over that frame's own subject, as it does to the capture door.
223
+ const framed = framedCapture(scope.container)?.container;
224
+ if (framed && !roots.includes(framed)) roots.push(framed);
225
+ return roots;
226
+ }
227
+
228
+ /** A registered VIEW's handed-over body, by the part id the workbench stamped
229
+ * on it. `null` when the view is closed — its pane took the element with it. */
230
+ function viewPart(part: string): HTMLElement | null {
231
+ return document.querySelector<HTMLElement>(`[data-volter-part="${part}"]`);
232
+ }
233
+
234
+ /** The utility view showing in the panel: its body is the one of the stamped
235
+ * bodies (`frame/bridge.tsx`'s `setUtilityBody`) that has a box on screen. */
236
+ function resolveUtilityScope(): Scope {
237
+ const showing = [...document.querySelectorAll<HTMLElement>('[data-volter-utility]')].find((body) => {
238
+ const box = body.getBoundingClientRect();
239
+ return box.width > 0 && box.height > 0;
240
+ });
241
+ if (!showing) {
242
+ throw new Error(
243
+ "No utility view is showing, so scope 'utility' has nothing to reach. Show one in the " +
244
+ 'panel (View: Open View…) and retry.',
245
+ );
246
+ }
247
+ const id = showing.dataset['volterUtility'] ?? '';
248
+ return { container: showing, name: 'utility', id, title: id };
249
+ }
250
+
251
+ /** THE WORKSPACE'S AREAS: every open document a workspace placed in an area (`descriptor.area`),
252
+ * as it is on screen. The Model workspace's bottom area is one (the Timeline and its Action and
253
+ * NLA editors, or the Game panel); a person clicks there every session, and no other scope
254
+ * reaches it, because an area is neither the active centre document nor a VS Code view. */
255
+ function resolveAreaScope(): Scope {
256
+ const shown = openWorkspaceDocuments()
257
+ .filter((open) => open.descriptor.area)
258
+ .flatMap((open) =>
259
+ Array.from(document.querySelectorAll<HTMLElement>('[data-workspace-document-id]')).filter(
260
+ (element) => element.dataset['workspaceDocumentId'] === open.descriptor.id,
261
+ ),
262
+ )
263
+ .filter((element) => {
264
+ const box = element.getBoundingClientRect();
265
+ return box.width > 0 && box.height > 0;
266
+ });
267
+ const [first, ...rest] = shown;
268
+ if (!first) {
269
+ throw new Error("No workspace area is showing, so scope 'area' has nothing to reach.");
270
+ }
271
+ // Every area the scope covers is named, so a reader of the answer never takes one area's id
272
+ // for where a match was when several are showing.
273
+ const ids = [...new Set(shown.map((element) => element.dataset['workspaceDocumentId'] ?? ''))];
274
+ return { container: first, extraRoots: rest, name: 'area', id: ids.join(', '), title: ids.length > 1 ? `${ids.length} workspace areas` : 'workspace area' };
275
+ }
276
+
277
+ /** The application menu bar, by the stamp `ApplicationMenus` writes on its own root. The menus
278
+ * its triggers open are portaled, and {@link scopeRoots} reaches them through their anchors. */
279
+ function resolveMenubarScope(): Scope {
280
+ const bar = document.querySelector<HTMLElement>('[data-testid="app-menubar"]');
281
+ if (!bar) {
282
+ throw new Error("The application menus are not on screen, so scope 'menubar' has nothing to reach.");
283
+ }
284
+ return { container: bar, name: 'menubar', id: 'app-menubar', title: 'application menus' };
285
+ }
286
+
287
+ function resolveScope(name: DocumentProbeScope): Scope {
288
+ if (name === 'utility') return resolveUtilityScope();
289
+ if (name === 'menubar') return resolveMenubarScope();
290
+ if (name === 'area') return resolveAreaScope();
291
+ const view =
292
+ name === 'rail' || name === 'outliner' || name === 'content' ? VIEW_SCOPES[name] : null;
293
+ if (view) {
294
+ const container = viewPart(view.part);
295
+ // THE INSPECTOR AS A CARD. The rail's subject is the inspector, which a person may show as a
296
+ // card over the viewport instead of the Properties column (`inspector-presentation.ts`); the
297
+ // workspace's layout host mounts that card outside every document's box, so scope 'rail'
298
+ // covers it too — the same inspector in its other projection, whether or not the Properties
299
+ // view is open beside it.
300
+ // The visible card: every view of the active document mounts one, and a parked view's is
301
+ // in the page with no box.
302
+ const card =
303
+ name === 'rail'
304
+ ? ([
305
+ ...document.querySelectorAll<HTMLElement>('[data-testid="inspector-panel"][data-volter-inspector-presentation="card"]'),
306
+ ].find((candidate) => {
307
+ const box = candidate.getBoundingClientRect();
308
+ return box.width > 0 && box.height > 0;
309
+ }) ?? null)
310
+ : null;
311
+ if (card && !container) return { container: card, name, id: view.view, title: 'Inspector card' };
312
+ if (card && container) return { container, extraRoots: [card], name, id: view.view, title: `${view.title} and the inspector card` };
313
+ if (!container) {
314
+ throw new Error(
315
+ `The ${view.title} view (${view.view}) is not open, so scope '${name}' has nothing to ` +
316
+ 'reach. Open it from the workbench (View: Open View…) and retry — this door reads ' +
317
+ 'and drives what is on screen, and reports its absence rather than an empty match list.',
318
+ );
319
+ }
320
+ return { container, name, id: view.view, title: view.title };
321
+ }
322
+ return resolveDocumentScope(name);
323
+ }
324
+
325
+ /** `document`, `header` and `shelf` — the ACTIVE CENTRE DOCUMENT's box and the
326
+ * two strips inside it. */
327
+ function resolveDocumentScope(name: DocumentProbeScope): Scope {
328
+ const active = activeWorkspaceDocument();
329
+ if (!active)
330
+ throw new Error(
331
+ `No active editor document, so scope '${name}' has nothing to reach. The editor's own ` +
332
+ "views are still readable: scope 'outliner', scope 'rail', and scope 'content'.",
333
+ );
334
+ const id = activeWorkspaceDocumentId() ?? active.descriptor.id;
335
+ // The Game document is the one center document with its OWN doors — and the
336
+ // one where a synthetic DOM gesture is the exact instrument the doctrine
337
+ // bans ("never pilot a game with synthetic key events"). Its input is
338
+ // play-gated (`gated-globals.ts`), so a probe
339
+ // here would either silently do nothing (not playing) or bypass the play
340
+ // path's contract (playing). Refuse toward the honest doors instead of
341
+ // becoming an ungated second one. Its HEADER STRIP is the exception: the
342
+ // editor's own chrome (the Play bar, the resolution, the audio control), not
343
+ // the game's DOM, and otherwise no door reached a control a person clicks
344
+ // there every session.
345
+ if (id === GAME_DOCUMENT_ID && name !== 'header') {
346
+ throw new Error(
347
+ "The Game document is out of this door's scope: drive and read a game through its own " +
348
+ 'doors — `game.commands()`/`game.command(n)`/`game.state(n)`, or `page(step)` during ' +
349
+ "play — never through synthetic DOM gestures. Its header strip (the editor's own Play " +
350
+ "bar and controls) is scope 'header'; the editor's views around it stay readable: scope " +
351
+ "'outliner' and scope 'rail'.",
352
+ );
353
+ }
354
+ const container = activeDocumentContainer(id);
355
+ if (!container) {
356
+ throw new Error(
357
+ `The active document's surface is not mounted: ${id} (${active.title}). ` +
358
+ 'Present it first (`editor.present`) and retry.',
359
+ );
360
+ }
361
+ // The scope is the document's WHOLE box — its header strip and its shelf
362
+ // rail as well as its content (`WorkspaceDocumentSurface`: the strip is a
363
+ // sibling of the content element the id is written on). Three builders
364
+ // measured the same wall: a document's own header controls and menus were
365
+ // unreachable and unreadable through the product, so a header could not be
366
+ // driven by its own click. The Game document's refusal above still holds.
367
+ const box = container.closest<HTMLElement>('.volter-dock-document') ?? container;
368
+ if (name === 'document') return { container: box, name, id, title: active.title };
369
+ // `header` / `shelf` — MEASURED to be inside the box already (the strips are
370
+ // this element's own children), so these names buy AIM rather than reach: a
371
+ // selector that also matches in the content can be pointed at one strip
372
+ // without counting indices. A document that draws no strip (`runtime:
373
+ // false` and no toolbar, or a workspace whose skin hides headers) refuses by
374
+ // name rather than answering zero matches.
375
+ const strip = DOCUMENT_STRIPS[name as keyof typeof DOCUMENT_STRIPS];
376
+ const element = box.querySelector<HTMLElement>(`[data-testid="${strip.prefix}:${id}"]`);
377
+ if (!element) {
378
+ throw new Error(
379
+ `The active document "${active.title}" (${id}) draws no ${strip.title}, so scope ` +
380
+ `'${name}' has nothing to reach. Its whole box is scope 'document'.`,
381
+ );
382
+ }
383
+ return { container: element, name, id, title: `${active.title} — ${strip.title}` };
384
+ }
385
+
386
+ /** The refusal that keeps this door scoped — it names the scope every time,
387
+ * and the vocabulary, because "wrong scope" is the likeliest cause. */
388
+ function outOfScope(scope: Scope, detail: string): Error {
389
+ return new Error(
390
+ `Out of scope: ${detail}. This step ran in scope '${scope.name}' — ` +
391
+ `"${scope.title}" (${scope.id}) — and reaches ONLY inside it. The other surfaces are ` +
392
+ `named, not walked to: ${SCOPE_NAMES.filter((name) => name !== scope.name)
393
+ .map((name) => `'${name}'`)
394
+ .join(', ')} (pass \`{ scope }\`). The rest of the page is deliberately unreachable.`,
395
+ );
396
+ }
397
+
398
+ const MAX_TEXT = 400;
399
+
400
+ /**
401
+ * The sentinel a custom property is resolved against.
402
+ *
403
+ * `getComputedStyle(el).getPropertyValue('--x')` answers with the DECLARED
404
+ * text, so a theme token reads back as its own `color-mix(…)` algebra rather
405
+ * than the colour it paints. To get the paint, the value has to go through a
406
+ * property the browser actually resolves. A throwaway span inside the element
407
+ * inherits the same custom-property cascade, so `color: var(--x)` on it
408
+ * computes to `rgb(…)`.
409
+ *
410
+ * The sentinel is how a NON-colour token stays honest: `color` silently keeps
411
+ * its previous value when the new one does not parse, so a token holding a
412
+ * length would otherwise be reported as whatever colour happened to be there.
413
+ * Seed the sentinel, apply the var, and an unchanged reading means "this did
414
+ * not resolve as a colour" — report the declared text instead of a lie.
415
+ */
416
+ const STYLE_PROBE_SENTINEL = 'rgb(1, 2, 3)';
417
+
418
+ /**
419
+ * `background-color`'s INITIAL value, which is what the probe below computes
420
+ * to whenever the substituted token is not a colour — CSS's own answer for a
421
+ * declaration that is invalid at computed-value time on a NON-INHERITED
422
+ * property.
423
+ */
424
+ const STYLE_PROBE_INITIAL = 'rgba(0, 0, 0, 0)';
425
+
426
+ /**
427
+ * What a custom property PAINTS, its declared text when it is not a colour,
428
+ * or the empty string when THE PROPERTY IS NOT DECLARED AT ALL.
429
+ *
430
+ * That last case is the one this door got wrong first and a builder caught: an
431
+ * undeclared `var(--x)` makes the whole declaration invalid, so the probe fell
432
+ * back and the reading came back as the surrounding text colour — a confident
433
+ * wrong answer, identical for a real token, a misspelled one and a group the
434
+ * palette does not declare. (Measured under Classic:
435
+ * `--volter-viewport-background` and a deliberately nonexistent name both
436
+ * answered `rgb(197, 200, 206)`, and a reader nearly concluded Classic
437
+ * declares a viewport group.) A custom property's computed value is the empty
438
+ * string exactly when it is undeclared, so that is the answer.
439
+ *
440
+ * THE PAINT PROPERTY IS `background-color`, AND THAT IS THE WHOLE TRICK. It
441
+ * used to be `color`, with a sentinel seeded first and the reasoning "an
442
+ * unchanged reading means this did not resolve as a colour". That reasoning is
443
+ * wrong, because `color` is INHERITED: a declaration invalid at computed-value
444
+ * time does not keep its previous value, it takes the INHERITED one — so a
445
+ * token holding a length read back as whatever colour the surrounding text
446
+ * happened to be, the same confident lie one paragraph up. Measured 2026-09-21
447
+ * on the Outliner: `--volter-tree-row-height`, which holds `20px`, answered
448
+ * `rgb(195, 195, 195)` while a reader was measuring row heights with it.
449
+ * `background-color` is NOT inherited, so the same invalid declaration lands
450
+ * on its initial value — `rgba(0, 0, 0, 0)`, a constant this module knows —
451
+ * and "did this resolve as a colour" becomes a fact instead of a guess. The
452
+ * sentinel is still seeded, so a browser that somehow leaves the declaration
453
+ * untouched is also caught. A token whose value IS `transparent` reports its
454
+ * declared text, which is the more useful of the two true answers.
455
+ */
456
+ function resolveCustomProperty(element: HTMLElement, name: string): string {
457
+ const declared = getComputedStyle(element).getPropertyValue(name).trim();
458
+ if (declared === '') return '';
459
+ const owner = element.ownerDocument;
460
+ const probe = owner.createElement('span');
461
+ // Out of flow and invisible: this must not reflow the surface being measured.
462
+ probe.style.position = 'absolute';
463
+ probe.style.pointerEvents = 'none';
464
+ probe.style.visibility = 'hidden';
465
+ probe.style.backgroundColor = STYLE_PROBE_SENTINEL;
466
+ element.appendChild(probe);
467
+ try {
468
+ probe.style.backgroundColor = `var(${name})`;
469
+ const painted = getComputedStyle(probe).backgroundColor.trim();
470
+ return painted === STYLE_PROBE_SENTINEL || painted === STYLE_PROBE_INITIAL ? declared : painted;
471
+ } finally {
472
+ probe.remove();
473
+ }
474
+ }
475
+
476
+ function resolveStyles(element: HTMLElement, names: readonly string[]): Record<string, string> {
477
+ const computed = getComputedStyle(element);
478
+ const styles: Record<string, string> = {};
479
+ for (const name of names) {
480
+ styles[name] = name.startsWith('--')
481
+ ? resolveCustomProperty(element, name)
482
+ : // A standard property is already RESOLVED by the browser here, which is
483
+ // the whole reason to ask for it: the token's expression has become the
484
+ // rgb the person sees.
485
+ computed.getPropertyValue(cssPropertyName(name)).trim();
486
+ }
487
+ return styles;
488
+ }
489
+
490
+ /** `backgroundColor` → `background-color`; a name already dashed passes through. */
491
+ function cssPropertyName(name: string): string {
492
+ return name.includes('-') ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
493
+ }
494
+
495
+ function describeElement(
496
+ element: Element,
497
+ index: number,
498
+ styles?: readonly string[],
499
+ ): ProbedElement {
500
+ const html = element as HTMLElement;
501
+ const attributes: Record<string, string> = {};
502
+ for (const attribute of Array.from(element.attributes)) {
503
+ attributes[attribute.name] = attribute.value;
504
+ }
505
+ const rect = html.getBoundingClientRect();
506
+ const value = (element as HTMLInputElement).value;
507
+ const checked = (element as HTMLInputElement).checked;
508
+ // `||`, not `??`: in a BACKGROUNDED tab — this door's primary caller —
509
+ // Chrome answers `innerText` with the empty string for everything, so the
510
+ // nullish fallback never fired and every probe read a blank document
511
+ // (measured live, 2026-08-14). `textContent` is the honest answer there.
512
+ const text = (html.innerText || element.textContent || '').trim();
513
+ return {
514
+ index,
515
+ tag: element.tagName.toLowerCase(),
516
+ text: text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text,
517
+ attributes,
518
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
519
+ ...(typeof value === 'string' ? { value } : {}),
520
+ ...(typeof checked === 'boolean' ? { checked } : {}),
521
+ ...(html.hasAttribute('disabled') ? { disabled: true } : {}),
522
+ ...(styles && styles.length > 0 ? { styles: resolveStyles(html, styles) } : {}),
523
+ };
524
+ }
525
+
526
+ /** Resolve one target inside the scope, refusing loudly rather than reaching out. */
527
+ function resolveTarget(scope: Scope, selector: string, index: number): HTMLElement {
528
+ const matches = matchesIn(scope, selector);
529
+ const element = matches[index];
530
+ if (!element) {
531
+ throw outOfScope(
532
+ scope,
533
+ `${JSON.stringify(selector)} matched ${matches.length} element(s), so index ${index} does not exist`,
534
+ );
535
+ }
536
+ // querySelectorAll on an element is already descendants-only; this asserts
537
+ // the invariant rather than trusting it. A PORTALED overlay the document
538
+ // opened is one of the roots ({@link scopeRoots}), so it passes here by
539
+ // being inside the root that owns it and not by any relaxation.
540
+ if (!scopeRoots(scope).some((root) => root.contains(element))) {
541
+ throw outOfScope(
542
+ scope,
543
+ `${JSON.stringify(selector)} resolved outside the document's container`,
544
+ );
545
+ }
546
+ return element;
547
+ }
548
+
549
+ function pointerInit(element: HTMLElement, at: readonly [number, number] = [0.5, 0.5]): MouseEventInit {
550
+ const rect = element.getBoundingClientRect();
551
+ return {
552
+ bubbles: true,
553
+ cancelable: true,
554
+ composed: true,
555
+ view: element.ownerDocument.defaultView ?? window,
556
+ clientX: rect.x + rect.width * at[0],
557
+ clientY: rect.y + rect.height * at[1],
558
+ button: 0,
559
+ buttons: 1,
560
+ };
561
+ }
562
+
563
+ /**
564
+ * A REAL click: the same event sequence a mouse produces. `element.click()`
565
+ * dispatches only `click`, so a component that selects on `pointerdown` (the
566
+ * sheet grid's cell selection) never sees the gesture at all.
567
+ *
568
+ * `clicks` > 1 is a REAL double click and not two calls to this function: the
569
+ * browser numbers consecutive presses in `detail`, and a `dblclick` follows
570
+ * the second `click`. React's `onDoubleClick` listens for that `dblclick` and
571
+ * nothing else, so a rename driven by two separate single clicks never starts
572
+ * (measured against the Outliner's own `onDoubleClick` → `onStartEditing`).
573
+ */
574
+ /**
575
+ * What a real press's default action does with focus, which an untrusted `mousedown` never does:
576
+ * focus the nearest element that can hold it, the pressed one or an ancestor (a `tabindex`, a
577
+ * natively focusable control, an editable region). A bare `element.focus()` on a plain `<div>` does
578
+ * nothing, and focus stays wherever a pointer handler along the way put it.
579
+ */
580
+ function focusAsPressed(element: HTMLElement): void {
581
+ let focusable: HTMLElement | null = element;
582
+ while (focusable && !(focusable.hasAttribute('tabindex') || focusable.tabIndex >= 0 || focusable.isContentEditable)) {
583
+ focusable = focusable.parentElement;
584
+ }
585
+ (focusable ?? element).focus({ preventScroll: true });
586
+ }
587
+
588
+ /** A point given as fractions of `element`'s box, in its own document's client pixels. */
589
+ function pointAt(element: HTMLElement, at: readonly [number, number] = [0.5, 0.5]): { clientX: number; clientY: number } {
590
+ const rect = element.getBoundingClientRect();
591
+ return { clientX: rect.x + rect.width * at[0], clientY: rect.y + rect.height * at[1] };
592
+ }
593
+
594
+ /** What a framed page hit-tests at that point of `element`: the element a mouse press there reaches. */
595
+ function pressedInFrame(element: HTMLElement, at?: readonly [number, number]): HTMLElement {
596
+ const { clientX, clientY } = pointAt(element, at);
597
+ const hit = element.ownerDocument.elementFromPoint(clientX, clientY);
598
+ return (hit as HTMLElement | null) ?? element;
599
+ }
600
+
601
+ async function dispatchClick(
602
+ element: HTMLElement,
603
+ clicks = 1,
604
+ at?: readonly [number, number],
605
+ modifiers: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean } = {},
606
+ point?: { clientX: number; clientY: number },
607
+ ): Promise<void> {
608
+ const init = { ...pointerInit(element, at), ...point, ...modifiers };
609
+ const total = Math.max(1, Math.round(clicks));
610
+ const doc = element.ownerDocument;
611
+ for (let n = 1; n <= total; n++) {
612
+ const down = { ...init, detail: n };
613
+ const up = { ...init, buttons: 0, detail: n };
614
+ withoutPointerCapture(element, () => {
615
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...down, pointerType: 'mouse' }));
616
+ element.dispatchEvent(new MouseEvent('mousedown', down));
617
+ focusAsPressed(element);
618
+ });
619
+ // A MOUSE'S PRESS AND RELEASE ARE TWO TASKS, and the page renders between them. Dispatched
620
+ // in one, a press that unmounts its own target (a menu closing on an outside `pointerdown`)
621
+ // still delivered its click, so this door passed Edit › Undo while a person's click did
622
+ // nothing (measured 2026-09-28 with a real click on the browser-substrate page). The release
623
+ // goes where a mouse's would: the pressed element if it is still there, else what is under
624
+ // the point, and a vanished element gets no click.
625
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
626
+ const released = element.isConnected
627
+ ? element
628
+ : ((doc.elementFromPoint(init.clientX ?? 0, init.clientY ?? 0) as HTMLElement | null) ?? doc.body);
629
+ withoutPointerCapture(released, () => {
630
+ released.dispatchEvent(new PointerEvent('pointerup', { ...up, pointerType: 'mouse' }));
631
+ released.dispatchEvent(new MouseEvent('mouseup', up));
632
+ if (released === element) element.dispatchEvent(new MouseEvent('click', up));
633
+ });
634
+ if (released !== element) return;
635
+ }
636
+ if (total >= 2) {
637
+ withoutPointerCapture(element, () =>
638
+ element.dispatchEvent(new MouseEvent('dblclick', { ...init, buttons: 0, detail: total })),
639
+ );
640
+ }
641
+ }
642
+
643
+ /**
644
+ * TYPE `text` into a field the way a person does — one character at a time,
645
+ * through the PROTOTYPE's value setter, between real `keydown`/`keyup`.
646
+ *
647
+ * Three traps this exists for, each measured on a shipped surface:
648
+ * - `paste` cannot stand in. An untrusted `ClipboardEvent` performs NO
649
+ * default action, so a plain `<input>` with no paste handler keeps its old
650
+ * value and the probe reports a click it never made.
651
+ * - `element.value = x` is invisible to React (the value tracker it installs
652
+ * on the node compares the new value against itself and re-renders the old
653
+ * one) — the same trap {@link setSelectValue} documents, and the same cure.
654
+ * - a field may commit on a KEY rather than on `input`. The Outliner's rename
655
+ * reads `event.currentTarget.value` inside `onKeyDown` for Enter; a numeric
656
+ * field in the Properties rail blurs on Enter. So the keys are real and the
657
+ * Enter is the caller's choice, not an implicit one.
658
+ */
659
+ function typeInto(element: HTMLElement, text: string, replace: boolean, enter: boolean): void {
660
+ const editable =
661
+ element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element : null;
662
+ if (!editable) {
663
+ throw new Error(
664
+ `Cannot type into a <${element.tagName.toLowerCase()}>: this verb drives a text field. ` +
665
+ "Click the control that opens one first (the Outliner's rename is a double click — " +
666
+ '`click(selector, { clicks: 2 })`), then `type` into the field it puts up.',
667
+ );
668
+ }
669
+ const proto =
670
+ element instanceof HTMLTextAreaElement
671
+ ? HTMLTextAreaElement.prototype
672
+ : HTMLInputElement.prototype;
673
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
674
+ const write = (next: string) => {
675
+ if (setter) setter.call(editable, next);
676
+ else editable.value = next;
677
+ };
678
+ editable.focus();
679
+ let held = editable.value;
680
+ if (replace && held !== '') {
681
+ // What select-all-and-delete produces, in one edit: a person's first
682
+ // keystroke over a selected field is a replacement, not N backspaces.
683
+ editable.select?.();
684
+ held = '';
685
+ write(held);
686
+ editable.dispatchEvent(
687
+ new InputEvent('input', {
688
+ bubbles: true,
689
+ composed: true,
690
+ inputType: 'deleteContentBackward',
691
+ }),
692
+ );
693
+ }
694
+ for (const character of [...text]) {
695
+ const init: KeyboardEventInit = {
696
+ key: character,
697
+ bubbles: true,
698
+ cancelable: true,
699
+ composed: true,
700
+ };
701
+ editable.dispatchEvent(new KeyboardEvent('keydown', init));
702
+ held += character;
703
+ write(held);
704
+ editable.dispatchEvent(
705
+ new InputEvent('input', {
706
+ bubbles: true,
707
+ composed: true,
708
+ inputType: 'insertText',
709
+ data: character,
710
+ }),
711
+ );
712
+ editable.dispatchEvent(new KeyboardEvent('keyup', init));
713
+ }
714
+ if (enter) {
715
+ const init: KeyboardEventInit = {
716
+ key: 'Enter',
717
+ code: 'Enter',
718
+ bubbles: true,
719
+ cancelable: true,
720
+ composed: true,
721
+ };
722
+ editable.dispatchEvent(new KeyboardEvent('keydown', init));
723
+ editable.dispatchEvent(new KeyboardEvent('keyup', init));
724
+ }
725
+ }
726
+
727
+ /**
728
+ * A synthetic pointer has no id the browser knows, so a listener that calls
729
+ * `setPointerCapture(event.pointerId)` — three's OrbitControls and
730
+ * TransformControls do, on every press — throws `NotFoundError` INTO THE
731
+ * CONSOLE whenever a document yields the press to the viewport (measured
732
+ * 2026-09-02: every missed click on an Asset Lab canvas). Listeners run
733
+ * synchronously inside `dispatchEvent`, so the capture calls are made
734
+ * harmless for exactly the dispatch and restored after; a real pointer is
735
+ * never affected.
736
+ */
737
+ function withoutPointerCapture(element: HTMLElement, dispatch: () => void): void {
738
+ // On the PROTOTYPE, not the target: the viewport's controls listen on the
739
+ // canvas's container and capture THERE, so the event's whole bubble path
740
+ // has to be covered. Synchronous, restored in `finally`, and a real pointer
741
+ // (which never dispatches through here) is untouched. The element's own
742
+ // realm's prototype: a framed page (the Build Player) has its own.
743
+ const realm = element.ownerDocument.defaultView as (Window & typeof globalThis) | null;
744
+ const proto = (realm ?? window).Element.prototype;
745
+ const set = proto.setPointerCapture;
746
+ const release = proto.releasePointerCapture;
747
+ proto.setPointerCapture = () => {};
748
+ proto.releasePointerCapture = () => {};
749
+ try {
750
+ dispatch();
751
+ } finally {
752
+ proto.setPointerCapture = set;
753
+ proto.releasePointerCapture = release;
754
+ }
755
+ }
756
+
757
+ /** A REAL drag: press at `from`, move through `steps` points, release at `to`,
758
+ * all in the element's own box fractions — `dispatchClick`'s sequence with
759
+ * the moves a mouse makes between press and release. Moves carry
760
+ * `buttons: 1` (the primary button is held), the release `buttons: 0`. */
761
+ /** THE DRAG A `hold` LEFT PRESSED, until a `release` step lets go of it where it was left. */
762
+ let heldDrag: { element: HTMLElement; end: MouseEventInit; button: 0 | 1 | 2 } | null = null;
763
+
764
+ function releaseDrag(): HTMLElement {
765
+ const held = heldDrag;
766
+ if (held === null)
767
+ throw new Error('Nothing is held: release lets go of a drag that `hold: true` left pressed, and none is.');
768
+ heldDrag = null;
769
+ withoutPointerCapture(held.element, () => {
770
+ held.element.dispatchEvent(new PointerEvent('pointerup', { ...held.end, pointerType: 'mouse' }));
771
+ held.element.dispatchEvent(new MouseEvent('mouseup', held.end));
772
+ if (held.button === 2) held.element.dispatchEvent(new MouseEvent('contextmenu', held.end));
773
+ });
774
+ return held.element;
775
+ }
776
+
777
+ function dispatchDrag(
778
+ element: HTMLElement,
779
+ from: readonly [number, number],
780
+ to: readonly [number, number],
781
+ steps: number,
782
+ modifiers: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean } = {},
783
+ via: readonly (readonly [number, number])[] = [],
784
+ button: 0 | 1 | 2 = 0,
785
+ hold = false,
786
+ ): void {
787
+ const rect = element.getBoundingClientRect();
788
+ // `buttons` is a bitmask in a different order from `button`: primary 1,
789
+ // secondary 2, middle 4.
790
+ const held = button === 0 ? 1 : button === 2 ? 2 : 4;
791
+ const at = (fx: number, fy: number): MouseEventInit => ({
792
+ bubbles: true,
793
+ cancelable: true,
794
+ composed: true,
795
+ view: window,
796
+ clientX: rect.x + rect.width * fx,
797
+ clientY: rect.y + rect.height * fy,
798
+ button,
799
+ buttons: held,
800
+ altKey: modifiers.altKey ?? false,
801
+ ctrlKey: modifiers.ctrlKey ?? false,
802
+ metaKey: modifiers.metaKey ?? false,
803
+ shiftKey: modifiers.shiftKey ?? false,
804
+ });
805
+ const start = at(from[0], from[1]);
806
+ withoutPointerCapture(element, () => {
807
+ // A REAL POINTER HOVERS BEFORE IT PRESSES, and some targets latch on the
808
+ // hover rather than on the press. three's `TransformControls` is the
809
+ // worked case: `pointerHover` is what sets `this.axis` from the picker
810
+ // under the cursor, and `pointerDown` does nothing at all while `axis` is
811
+ // null — so a synthetic drag that began with `pointerdown` handed the
812
+ // press to the VIEWPORT instead, which read it as a click on empty space
813
+ // and cleared the selection. Measured 2026-09-21 on a Blender Model
814
+ // document: dragging the X arrow of a freshly duplicated cube moved
815
+ // nothing and deselected it.
816
+ //
817
+ // Same family as the `button: -1` note below, and the same rule: mirror
818
+ // what the platform does. `buttons: 0` because nothing is held yet.
819
+ element.dispatchEvent(
820
+ new PointerEvent('pointermove', {
821
+ ...start,
822
+ button: -1,
823
+ buttons: 0,
824
+ pointerType: 'mouse',
825
+ }),
826
+ );
827
+ element.dispatchEvent(new MouseEvent('mousemove', { ...start, buttons: 0 }));
828
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...start, pointerType: 'mouse' }));
829
+ element.dispatchEvent(new MouseEvent('mousedown', start));
830
+ focusAsPressed(element);
831
+ const count = Math.max(1, Math.round(steps));
832
+ const path: (readonly [number, number])[] = [from, ...via, to];
833
+ for (let leg = 0; leg + 1 < path.length; leg++) {
834
+ const a = path[leg] as readonly [number, number];
835
+ const b = path[leg + 1] as readonly [number, number];
836
+ for (let i = 1; i <= count; i++) {
837
+ const t = i / count;
838
+ const move = at(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t);
839
+ // `button: -1` ON A `pointermove`, because that is what a real mouse
840
+ // reports: `PointerEvent.button` names the button whose STATE CHANGED,
841
+ // and on a move none did. Held-ness lives in `buttons: 1` (kept by
842
+ // `at`). This is not a detail — three.js guards on it exactly:
843
+ // `TransformControls.pointerMove` returns at
844
+ // `pointer.button !== -1` (r180, :472), so a drag of a gizmo handle
845
+ // latched the axis, drew the drag helper line, and then moved NOTHING,
846
+ // forever (measured 2026-09-21 on a Blender Model document, where it
847
+ // read as "the transform provider is not writing"). `MouseEvent`'s
848
+ // legacy `mousemove` keeps `button: 0` — a real browser reports that
849
+ // one as 0, and mirroring the platform is the whole job here.
850
+ element.dispatchEvent(
851
+ new PointerEvent('pointermove', { ...move, button: -1, pointerType: 'mouse' }),
852
+ );
853
+ element.dispatchEvent(new MouseEvent('mousemove', move));
854
+ }
855
+ }
856
+ const end = { ...at(to[0], to[1]), buttons: 0 };
857
+ if (hold) {
858
+ heldDrag = { element, end, button };
859
+ return;
860
+ }
861
+ element.dispatchEvent(new PointerEvent('pointerup', { ...end, pointerType: 'mouse' }));
862
+ element.dispatchEvent(new MouseEvent('mouseup', end));
863
+ // A released primary button clicks; a released secondary one asks for the
864
+ // context menu, as the platform does.
865
+ if (button === 2) element.dispatchEvent(new MouseEvent('contextmenu', end));
866
+ else if (button === 0 && from[0] === to[0] && from[1] === to[1]) {
867
+ element.dispatchEvent(new MouseEvent('click', end));
868
+ }
869
+ });
870
+ }
871
+
872
+ /** The element a key goes to: the explicit target, else whatever inside the
873
+ * scope currently has focus, else the container itself. */
874
+ function keyTarget(scope: Scope, explicit: HTMLElement | null): HTMLElement {
875
+ if (explicit) return explicit;
876
+ const focused = document.activeElement;
877
+ if (focused instanceof HTMLElement && scopeRoots(scope).some((root) => root.contains(focused)))
878
+ return focused;
879
+ return scope.container;
880
+ }
881
+
882
+ /** Run a selector inside the scope, turning a malformed one into an honest error. */
883
+ function matchesIn(scope: Scope, selector: string): HTMLElement[] {
884
+ try {
885
+ return scopeRoots(scope).flatMap((root) =>
886
+ Array.from(root.querySelectorAll<HTMLElement>(selector)),
887
+ );
888
+ } catch (error) {
889
+ throw new Error(`Invalid selector ${JSON.stringify(selector)}: ${String(error)}`);
890
+ }
891
+ }
892
+
893
+ /** An optional `selector` resolves to a target; its absence means "the focused
894
+ * element inside the scope" — the shared shape of the `key` and `paste` steps. */
895
+ function gestureTarget(scope: Scope, step: { selector?: string; index?: number }): HTMLElement {
896
+ const explicit =
897
+ step.selector === undefined ? null : resolveTarget(scope, step.selector, step.index ?? 0);
898
+ return keyTarget(scope, explicit);
899
+ }
900
+
901
+ /** The physical key a person presses for `key`, as `KeyboardEvent.code` names it on a US
902
+ * layout: a keystroke that says `code: 'w'` is one no keybinding resolver or game input map
903
+ * recognises. */
904
+ const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
905
+ ' ': 'Space', '`': 'Backquote', '-': 'Minus', '=': 'Equal', '[': 'BracketLeft', ']': 'BracketRight',
906
+ '\\': 'Backslash', ';': 'Semicolon', "'": 'Quote', ',': 'Comma', '.': 'Period', '/': 'Slash',
907
+ };
908
+ function codeOf(key: string): string {
909
+ if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
910
+ if (/^[0-9]$/.test(key)) return `Digit${key}`;
911
+ return PUNCTUATION_CODES[key] ?? key;
912
+ }
913
+
914
+ /** The legacy `keyCode` browsers still give every keystroke and VS Code's keybinding
915
+ * resolution reads; a synthesized event carries 0 unless it is set. */
916
+ const NAMED_KEY_CODES: Readonly<Record<string, number>> = {
917
+ Backspace: 8, Tab: 9, Enter: 13, Escape: 27, ' ': 32, PageUp: 33, PageDown: 34, End: 35, Home: 36,
918
+ ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, Delete: 46,
919
+ };
920
+ function keyCodeOf(key: string): number {
921
+ if (/^[a-z0-9]$/i.test(key)) return key.toUpperCase().charCodeAt(0);
922
+ const fn = /^F([1-9]|1[0-2])$/.exec(key);
923
+ if (fn) return 111 + Number(fn[1]);
924
+ return NAMED_KEY_CODES[key] ?? 0;
925
+ }
926
+
927
+ /** A numpad key's legacy code follows its PHYSICAL key: Numpad0..9 are 96..105 and the
928
+ * decimal 110, where the digit they type would say 48..57. */
929
+ function numpadKeyCode(code: string | undefined): number | null {
930
+ const digit = code === undefined ? null : /^Numpad([0-9])$/.exec(code);
931
+ if (digit) return 96 + Number(digit[1]);
932
+ return code === 'NumpadDecimal' ? 110 : null;
933
+ }
934
+
935
+ function keyEvent(type: 'keydown' | 'keyup', step: DocumentKeyStep): KeyboardEvent {
936
+ const event = new KeyboardEvent(type, keyInit(step));
937
+ const keyCode = numpadKeyCode(step.code) ?? keyCodeOf(step.key);
938
+ Object.defineProperty(event, 'keyCode', { get: () => keyCode });
939
+ Object.defineProperty(event, 'which', { get: () => keyCode });
940
+ return event;
941
+ }
942
+
943
+ function keyInit(step: DocumentKeyStep): KeyboardEventInit {
944
+ return {
945
+ key: step.key,
946
+ code: step.code ?? codeOf(step.key),
947
+ bubbles: true,
948
+ cancelable: true,
949
+ composed: true,
950
+ ...(step.ctrlKey ? { ctrlKey: true } : {}),
951
+ ...(step.metaKey ? { metaKey: true } : {}),
952
+ ...(step.shiftKey ? { shiftKey: true } : {}),
953
+ ...(step.altKey ? { altKey: true } : {}),
954
+ };
955
+ }
956
+
957
+ /**
958
+ * Set a `<select>`'s value the way React can see it.
959
+ *
960
+ * `HTMLSelectElement.prototype`'s own `value` setter is called explicitly
961
+ * because React installs a value tracker on the node: assigning through the
962
+ * instance updates that tracker too, so React compares the new value against
963
+ * itself, concludes nothing changed, and re-renders the OLD value. Going
964
+ * through the prototype descriptor leaves the tracker stale, which is exactly
965
+ * what makes the following `change` read as a real user edit.
966
+ */
967
+ function setSelectValue(element: HTMLSelectElement, value: string): void {
968
+ const options = [...element.options].map((option) => option.value);
969
+ if (!options.includes(value)) {
970
+ throw new Error(
971
+ `No option with value ${JSON.stringify(value)} on this <select> — it offers ` +
972
+ `${options.map((option) => JSON.stringify(option)).join(', ') || '(no options)'}. ` +
973
+ "The value is the option's `value`, not its label; `query` reports both.",
974
+ );
975
+ }
976
+ const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
977
+ if (setter) setter.call(element, value);
978
+ else element.value = value;
979
+ element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
980
+ element.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
981
+ }
982
+
983
+ const PROBE_ACTIONS = ['query', 'click', 'drag', 'release', 'key', 'paste', 'select', 'type'] as const;
984
+
985
+ /**
986
+ * THE WIRE'S TWO FREE-FORM FIELDS, both refused by name.
987
+ *
988
+ * The wire hands the step in untyped (`cmd['step']`), so an unknown verb or an
989
+ * unknown scope must refuse HERE — an exhaustive switch alone would fall
990
+ * through and answer `ok` with nothing, and an unrecognized scope would
991
+ * quietly fall back to the document and answer about a surface the caller did
992
+ * not ask for. Both are the silent pass-through the strict-input rule bans.
993
+ */
994
+ function acceptStep(step: DocumentProbeStep): Scope {
995
+ const action = (step as { action?: unknown } | null | undefined)?.action;
996
+ if (!PROBE_ACTIONS.includes(action as (typeof PROBE_ACTIONS)[number])) {
997
+ throw new Error(
998
+ `Unknown document-probe action ${JSON.stringify(action)}. This door has exactly ` +
999
+ `${PROBE_ACTIONS.length} verbs: ${PROBE_ACTIONS.join(', ')} (\`editor.document.*\`).`,
1000
+ );
1001
+ }
1002
+ const requested = (step as { scope?: unknown }).scope ?? 'document';
1003
+ if (!SCOPE_NAMES.includes(requested as DocumentProbeScope)) {
1004
+ throw new Error(
1005
+ `Unknown scope ${JSON.stringify(requested)}. This door addresses exactly these ` +
1006
+ `surfaces: ${SCOPE_NAMES.map((name) => `'${name}'`).join(', ')}.`,
1007
+ );
1008
+ }
1009
+ return resolveScope(requested as DocumentProbeScope);
1010
+ }
1011
+
1012
+ export async function runDocumentProbe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
1013
+ const scope = acceptStep(step);
1014
+ // WHILE THE GAME HEARS THE KEYBOARD, THIS DOOR READS AND CLICKS ONLY. Every event it dispatches
1015
+ // bubbles to \`window\`, where the game's own listeners pass the same gate this reads
1016
+ // (\`surfaceHoldsKeyboard\`, \`gated-globals.ts\`): while the Game document is active and no view
1017
+ // outside the editor area holds focus, a key, a typed string, a paste or a drag in any scope
1018
+ // would drive the game with synthetic input, which is what refusing the Game document protects.
1019
+ // With focus in a view (a field of the Network inspector, the rail), the game hears none of it.
1020
+ if (
1021
+ activeWorkspaceDocumentId() === GAME_DOCUMENT_ID &&
1022
+ surfaceHoldsKeyboard() &&
1023
+ step.action !== 'query' &&
1024
+ step.action !== 'click' &&
1025
+ step.action !== 'select'
1026
+ ) {
1027
+ throw new Error(
1028
+ `'${step.action}' is refused while the Game document holds the keyboard: its events would ` +
1029
+ "reach the game. Query, click and select still work; click into a view's field first, or " +
1030
+ "drive the game through the game's own doors.",
1031
+ );
1032
+ }
1033
+ const where = { name: scope.name, id: scope.id, title: scope.title };
1034
+ /** Every gesture answers with the element it drove, so a transcript proves
1035
+ * WHAT was driven and not merely that something was. */
1036
+ const drove = (element: HTMLElement): DocumentProbeResult => ({
1037
+ scope: where,
1038
+ matched: 1,
1039
+ elements: [describeElement(element, 0)],
1040
+ });
1041
+ switch (step.action) {
1042
+ case 'query': {
1043
+ const matches = matchesIn(scope, step.selector);
1044
+ return {
1045
+ scope: where,
1046
+ matched: matches.length,
1047
+ elements: matches
1048
+ .slice(0, step.limit ?? 25)
1049
+ .map((element, index) => describeElement(element, index, step.styles)),
1050
+ };
1051
+ }
1052
+ case 'click': {
1053
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1054
+ const at = step.at;
1055
+ if (at !== undefined && !(Array.isArray(at) && at.length === 2 && at.every((v) => typeof v === 'number' && Number.isFinite(v)))) {
1056
+ throw new Error(`click's \`at\` is [x, y], fractions of the element's box; got ${JSON.stringify(at)}.`);
1057
+ }
1058
+ // Inside a framed page the press lands where a mouse would: on whatever the page
1059
+ // hit-tests at that point (its stacking and `pointer-events` decide), which the answer names.
1060
+ const pressed = element.ownerDocument === document ? element : pressedInFrame(element, at);
1061
+ await dispatchClick(pressed, step.clicks ?? 1, at && pressed === element ? at : undefined, {
1062
+ ...(step.altKey ? { altKey: true } : {}),
1063
+ ...(step.ctrlKey ? { ctrlKey: true } : {}),
1064
+ ...(step.metaKey ? { metaKey: true } : {}),
1065
+ ...(step.shiftKey ? { shiftKey: true } : {}),
1066
+ }, pressed === element ? undefined : pointAt(element, at));
1067
+ return drove(pressed);
1068
+ }
1069
+ case 'type': {
1070
+ const target = gestureTarget(scope, step);
1071
+ typeInto(target, step.text, step.replace ?? true, step.enter ?? true);
1072
+ return drove(target);
1073
+ }
1074
+ case 'drag': {
1075
+ if (step.button !== undefined && ![0, 1, 2].includes(step.button))
1076
+ throw new Error(`Unknown button ${JSON.stringify(step.button)}: 0 primary, 1 middle, 2 secondary.`);
1077
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1078
+ // Points are FRACTIONS of the element's box. A pixel value lands far outside it and the
1079
+ // gesture silently goes somewhere else, so a point outside 0..1 is refused by name.
1080
+ const box = element.getBoundingClientRect();
1081
+ const points: [string, readonly [number, number]][] = [
1082
+ ['from', step.from],
1083
+ ...(step.via ?? []).map((point, index): [string, readonly [number, number]] => [`via[${index}]`, point]),
1084
+ ['to', step.to],
1085
+ ];
1086
+ for (const [name, point] of points) {
1087
+ const inside = point.every((value) => Number.isFinite(value) && value >= 0 && value <= 1);
1088
+ if (inside) continue;
1089
+ if (step.leave && name !== 'from' && point.every((value) => Number.isFinite(value))) continue;
1090
+ throw new Error(
1091
+ `drag ${name} ${JSON.stringify(point)} is not a fraction of the element's box: points are ` +
1092
+ `[x, y] from 0 to 1 from its top-left ([0.5, 0.5] is its centre). This element is ` +
1093
+ `${Math.round(box.width)} x ${Math.round(box.height)} px at (${Math.round(box.x)}, ${Math.round(box.y)}); ` +
1094
+ `a page pixel px becomes (px - ${Math.round(box.x)}) / ${Math.round(box.width)}.`,
1095
+ );
1096
+ }
1097
+ dispatchDrag(
1098
+ element,
1099
+ step.from,
1100
+ step.to,
1101
+ step.steps ?? 8,
1102
+ {
1103
+ ...(step.altKey === undefined ? {} : { altKey: step.altKey }),
1104
+ ...(step.ctrlKey === undefined ? {} : { ctrlKey: step.ctrlKey }),
1105
+ ...(step.metaKey === undefined ? {} : { metaKey: step.metaKey }),
1106
+ ...(step.shiftKey === undefined ? {} : { shiftKey: step.shiftKey }),
1107
+ },
1108
+ step.via ?? [],
1109
+ step.button ?? 0,
1110
+ step.hold === true,
1111
+ );
1112
+ return drove(element);
1113
+ }
1114
+ case 'release':
1115
+ return drove(releaseDrag());
1116
+ case 'key': {
1117
+ if (step.holdMs !== undefined && (!Number.isFinite(step.holdMs) ||
1118
+ step.holdMs < 0 || step.holdMs > MAX_DOCUMENT_KEY_HOLD_MS)) {
1119
+ throw new Error(`key holdMs must be finite, nonnegative and at most ${MAX_DOCUMENT_KEY_HOLD_MS}.`);
1120
+ }
1121
+ const target = gestureTarget(scope, step);
1122
+ // A person's keystroke lands where their click put focus; what the workbench decides a
1123
+ // chord means follows that focus (its `volter.stage.focused` context), so the target takes
1124
+ // focus first unless focus is already inside it.
1125
+ if (!(document.activeElement instanceof Node && target.contains(document.activeElement))) {
1126
+ // As a click does: the nearest element that can hold focus, the target or an ancestor.
1127
+ focusAsPressed(target);
1128
+ }
1129
+ // And the keystroke itself goes to the element holding focus, as a person's does: a
1130
+ // target that contains the focused element (the document itself, with no selector) is where
1131
+ // it bubbles through, not where it starts.
1132
+ const active = document.activeElement;
1133
+ const receiver = active instanceof HTMLElement && target.contains(active) ? active : target;
1134
+ receiver.dispatchEvent(keyEvent('keydown', step));
1135
+ try {
1136
+ if (step.holdMs) await new Promise((settle) => setTimeout(settle, step.holdMs));
1137
+ } finally {
1138
+ receiver.dispatchEvent(keyEvent('keyup', step));
1139
+ }
1140
+ return drove(target);
1141
+ }
1142
+ case 'select': {
1143
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1144
+ if (!(element instanceof HTMLSelectElement)) {
1145
+ throw new Error(
1146
+ `${JSON.stringify(step.selector)} resolved a <${element.tagName.toLowerCase()}>, not a ` +
1147
+ "<select>. This verb sets a dropdown's value; a button or a checkbox takes `click`.",
1148
+ );
1149
+ }
1150
+ setSelectValue(element, step.value);
1151
+ return drove(element);
1152
+ }
1153
+ case 'paste': {
1154
+ const target = gestureTarget(scope, step);
1155
+ const data = new DataTransfer();
1156
+ data.setData('text/plain', step.text);
1157
+ target.dispatchEvent(
1158
+ new ClipboardEvent('paste', {
1159
+ bubbles: true,
1160
+ cancelable: true,
1161
+ composed: true,
1162
+ clipboardData: data,
1163
+ }),
1164
+ );
1165
+ return drove(target);
1166
+ }
1167
+ }
1168
+ }