@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -15,15 +15,9 @@
15
15
  * verbatim (`../ui-source/source-write-backend.ts`'s `SourceWriteBackend`, the same
16
16
  * `/__ui-source/write` + `/__ui-source/struct` dev-server endpoints
17
17
  * `vite-plugin-ui-oid.ts` serves for `UIAuthoringAdapter`/`SourceEditPanel`) — this
18
- * file does NOT invent a second source-writer. What IS new here (§1.E): edits enter
19
- * the T3.2 global undo timeline as INVERSE-WRITE entries — the prior literal (read
20
- * off the live DOM before the write) is captured and written back on undo. Structural
21
- * ops (D-1, Phase D, spec27 §2) use a DIFFERENT inverse — a per-OID one is unsound for
22
- * them (OIDs are content-signature keyed; `reorder`/`delete` reassign occurrence
23
- * indices; `delete` has no inverse payload at all) — a checksum-guarded WHOLE-FILE
24
- * snapshot restore instead (`pushStructUndo`/`restoreStructSnapshot`,
25
- * `SourceWriteBackend.restoreSource`), so every structural op (including `delete`) now
26
- * pushes exactly one undo entry.
18
+ * file does NOT invent a second source-writer. Every edit prepares complete next-file
19
+ * text and commits it through one checksum-guarded source history resource. Style,
20
+ * text, prop, and structural changes therefore share exact-byte undo/redo semantics.
27
21
  *
28
22
  * Persistence mirrors `UIAuthoringAdapter`'s de-stubbed pattern exactly: writes are
29
23
  * immediate (server-side, on commit), so `save()` is an honest no-op; `destination`
@@ -34,6 +28,7 @@
34
28
  import type {
35
29
  AuthoringAdapter,
36
30
  AuthoringCapabilities,
31
+ AuthoringProvenance,
37
32
  BoxEditProvider,
38
33
  ColorSampleProvider,
39
34
  DOMRectLike,
@@ -50,9 +45,9 @@ import type {
50
45
  StructureProvider,
51
46
  TextProvider,
52
47
  } from '@engine/adapter';
53
- import type { UIStory } from '@vgai/scene-ui/registry';
54
- import type { UIComponentRegistry } from '@vgai/scene-ui/SceneUI';
55
48
  import type { EditorStore } from '../editor-store';
49
+ import { JsonHistoryResource } from '../history/json-history-resource';
50
+ import { withProjectSourceHistory } from '../history/source-history-backend';
56
51
  import {
57
52
  browserOrInlineResolver,
58
53
  type ComputedStyleResolver,
@@ -70,7 +65,8 @@ import type { OidEntry } from '../ui-source/oid-transform';
70
65
  import type { SourceWriteBackend } from '../ui-source/source-write-backend';
71
66
  import { type CssRuleTarget, pickCssRuleTarget } from '../ui-source/writer';
72
67
  import { getStoryOverlay, setStoryOverlay } from './story-overlay';
73
- import { notifyUndoableOp, registerUndoAdapter } from './undo-timeline';
68
+ import type { UIComponentRegistry, UIStory } from './story-types';
69
+ import { getWorldPan } from './world-pan-state';
74
70
 
75
71
  /**
76
72
  * The minimal structural shape this adapter needs from a live DOM element —
@@ -118,6 +114,16 @@ export interface OidElementLike {
118
114
  readonly textContent?: string | null;
119
115
  }
120
116
 
117
+ const MAX_ELEMENT_LABEL_LENGTH = 64;
118
+
119
+ /** Compact the browser's semantic text into a stable hierarchy-row label. */
120
+ function normalizeElementText(value: string | null | undefined): string | null {
121
+ const text = value?.replace(/\s+/g, ' ').trim();
122
+ if (!text) return null;
123
+ if (text.length <= MAX_ELEMENT_LABEL_LENGTH) return text;
124
+ return `${text.slice(0, MAX_ELEMENT_LABEL_LENGTH - 1).trimEnd()}…`;
125
+ }
126
+
121
127
  /** Read one style property off an `OidElementLike.style` of either shape (a real
122
128
  * `CSSStyleDeclaration` or a plain-object test fixture). */
123
129
  export function styleProp(style: unknown, prop: string): unknown {
@@ -377,6 +383,14 @@ const STYLE_PROPERTIES: ReadonlyArray<{
377
383
  { prop: 'zIndex', label: 'Z Index', type: 'number', group: 'Position' },
378
384
  { prop: 'flexGrow', label: 'Grow', type: 'number', group: 'Position' },
379
385
  { prop: 'flexShrink', label: 'Shrink', type: 'number', group: 'Position' },
386
+ { prop: 'flexBasis', label: 'Basis', type: 'string', group: 'Position' },
387
+ {
388
+ prop: 'alignSelf',
389
+ label: 'Self Align',
390
+ type: 'enum',
391
+ group: 'Position',
392
+ options: ['auto', 'stretch', 'flex-start', 'center', 'flex-end', 'baseline'],
393
+ },
380
394
  // -- Spacing (per-side) --
381
395
  { prop: 'marginTop', label: 'Margin T', type: 'number', group: 'Spacing' },
382
396
  { prop: 'marginRight', label: 'Margin R', type: 'number', group: 'Spacing' },
@@ -441,7 +455,24 @@ const STYLE_PROPERTIES: ReadonlyArray<{
441
455
  label: 'Blend',
442
456
  type: 'enum',
443
457
  group: 'Effects',
444
- options: ['normal', 'multiply', 'screen', 'overlay'],
458
+ options: [
459
+ 'normal',
460
+ 'multiply',
461
+ 'screen',
462
+ 'overlay',
463
+ 'darken',
464
+ 'lighten',
465
+ 'color-dodge',
466
+ 'color-burn',
467
+ 'hard-light',
468
+ 'soft-light',
469
+ 'difference',
470
+ 'exclusion',
471
+ 'hue',
472
+ 'saturation',
473
+ 'color',
474
+ 'luminosity',
475
+ ],
445
476
  },
446
477
  { prop: 'filter', label: 'Filter', type: 'string', group: 'Effects' },
447
478
  { prop: 'backdropFilter', label: 'Backdrop', type: 'string', group: 'Effects' },
@@ -451,7 +482,24 @@ const STYLE_PROPERTIES: ReadonlyArray<{
451
482
  label: 'Cursor',
452
483
  type: 'enum',
453
484
  group: 'Effects',
454
- options: ['auto', 'default', 'pointer', 'text', 'move', 'grab', 'not-allowed'],
485
+ options: [
486
+ 'auto',
487
+ 'default',
488
+ 'pointer',
489
+ 'text',
490
+ 'move',
491
+ 'wait',
492
+ 'help',
493
+ 'crosshair',
494
+ 'not-allowed',
495
+ 'grab',
496
+ 'grabbing',
497
+ 'zoom-in',
498
+ 'zoom-out',
499
+ 'col-resize',
500
+ 'row-resize',
501
+ 'none',
502
+ ],
455
503
  },
456
504
  // -- Transform --
457
505
  { prop: 'transform', label: 'Transform', type: 'string', group: 'Transform' },
@@ -461,6 +509,7 @@ const STYLE_PATH_PREFIX = 'style.';
461
509
  const PROP_PATH_PREFIX = 'prop.';
462
510
  /** Cap 7: inspector path prefix for the document's design tokens (`token.<name>`). */
463
511
  const TOKEN_PATH_PREFIX = 'token.';
512
+ const PORTABLE_STORY_ARG_PATH_PREFIX = 'story.arg.';
464
513
  /** D3.R1 (spec 27 §2/§5 U4 precedent, reopen fix) — the `dynamicPaths` key for
465
514
  * Cap 3's text edit (`editText`), so a dynamic-body refusal is recorded and
466
515
  * surfaced through the SAME session-scoped after-touch mechanism the
@@ -468,11 +517,14 @@ const TOKEN_PATH_PREFIX = 'token.';
468
517
  * `properties()` descriptor), just a stable key for `` `${id}|${TEXT_PATH}` ``. */
469
518
  const TEXT_PATH = 'text';
470
519
  /** B3 — catalog node id prefix (`catalog:<registryKey>`) and the one
471
- * `entry (live preview)` node id, collision-proof against OID-derived ids
520
+ * Live Preview node id, collision-proof against OID-derived ids
472
521
  * (`o<counter36><hash36>`, never containing `:`) and composite ids
473
522
  * (`world:<id>`, only ever seen by the COMPOSITE, never by this adapter). */
474
523
  const CATALOG_ID_PREFIX = 'catalog:';
475
524
  const CATALOG_ENTRY_ID = 'catalog-entry';
525
+ /** D20 — fallback id for callers that supply portable stories without the
526
+ * richer per-CSF-document projection. Story ids remain Storybook-owned. */
527
+ const PORTABLE_DOCUMENT_ID = 'portable-document';
476
528
  /** B3 — inspector path prefix for a catalog node's active-story props
477
529
  * (`storyProp.<name>`) — deliberately distinct from {@link PROP_PATH_PREFIX}
478
530
  * (a DOM node's live fiber props): a catalog node has no call site to read a
@@ -523,10 +575,8 @@ const LONGHAND_TO_SHORTHAND: Readonly<Record<string, string>> = {
523
575
  * string it is given — it has no allow-list of its own, and no void/non-void
524
576
  * element distinction (every insert is self-closing, syntactically valid
525
577
  * JSX for every one of these real HTML element names). So this is a CURATED
526
- * subset, not a writer-enforced ceiling — the same "small sensible palette,
527
- * not the writer's full permissiveness" stance `ui-authoring-adapter.ts`'s
528
- * own `CREATABLE_KINDS` doc comment takes (there for UINode kinds; here for
529
- * literal HTML tag names, since a react-world node's "kind" IS its tag).
578
+ * subset, not a writer-enforced ceiling. A React root node's native kind is
579
+ * its literal HTML tag name.
530
580
  */
531
581
  const CREATABLE_KINDS: ReadonlyArray<{ kind: string; label: string }> = [
532
582
  { kind: 'div', label: 'Container' },
@@ -539,11 +589,6 @@ const CREATABLE_KINDS: ReadonlyArray<{ kind: string; label: string }> = [
539
589
  { kind: 'li', label: 'List Item' },
540
590
  ];
541
591
 
542
- interface PendingUndo {
543
- undo: () => void;
544
- redo: () => void;
545
- }
546
-
547
592
  export interface ReactWorldAuthoringOptions {
548
593
  /** T3.2 slice-3 write seam. Absent ⇒ no dev-server backend in this session (a
549
594
  * hosted/browser build) — selection/inspection still work; writes report
@@ -583,6 +628,33 @@ export interface ReactWorldAuthoringOptions {
583
628
  * tracks WHICH story is active and reports the change.
584
629
  */
585
630
  onStoryApplied?: ((story: (UIStory & { id: string }) | null) => void) | undefined;
631
+ /** D20 — portable Storybook CSF stories associated with this native React
632
+ * root. When present they are the canonical provider exposed to the shell;
633
+ * the legacy registry story sources remain a compatibility fallback only. */
634
+ portableStories?: ReadonlyArray<PortableStoryRef> | undefined;
635
+ /** Compatibility identity when all portable stories belong to one module. */
636
+ portableDocument?: { label: string; path?: string | undefined } | undefined;
637
+ /** Source documents projected above their own portable CSF stories. */
638
+ portableDocuments?:
639
+ | ReadonlyArray<{
640
+ id: string;
641
+ label: string;
642
+ path?: string | undefined;
643
+ storyIds: ReadonlyArray<string>;
644
+ }>
645
+ | undefined;
646
+ /** Initially mounted portable story id (normally the explicit/default CSF
647
+ * story selected by the design-time layer). */
648
+ activePortableStoryId?: string | undefined;
649
+ /** Re-render hook for a portable CSF selection. The design-time layer owns
650
+ * Storybook mounting; this adapter owns only selection/provider routing. */
651
+ onPortableStoryApplied?: ((storyId: string | null) => void) | undefined;
652
+ /** Session-scoped Storybook Controls write. The design-time layer remounts
653
+ * the same composed story with these args; CSF remains the canonical
654
+ * definition and Reset restores its composed defaults. */
655
+ onPortableStoryArgsChanged?:
656
+ | ((storyId: string, args: Record<string, unknown>) => void)
657
+ | undefined;
586
658
  /**
587
659
  * B3 — the react world's registered components (a project's
588
660
  * `src/ui/registry.ts` `components`, `SceneUI.tsx`'s flat
@@ -591,8 +663,8 @@ export interface ReactWorldAuthoringOptions {
591
663
  * (§0's gate — `play-mode.ts`'s `installMultiWorldAuthoring` never passes
592
664
  * this option, so PLAY mode's react-world hierarchy is untouched by B3's
593
665
  * construction). Non-empty ⇒ `hierarchy.roots()` becomes the CATALOG: one
594
- * `kind:'component'` node per registered key, plus one `'entry (live
595
- * preview)'` node nesting the ordinary DOM-walk roots one level down (see
666
+ * `kind:'component'` node per registered key, plus one `Live Preview` node
667
+ * nesting the ordinary DOM-walk roots one level down (see
596
668
  * the class doc comment's storybook-model rationale and
597
669
  * `docs/unified-world-editor/26-unified-editor-spec.md` §3 B3).
598
670
  */
@@ -619,8 +691,18 @@ export interface ReactWorldAuthoringOptions {
619
691
  worldId?: string | undefined;
620
692
  }
621
693
 
694
+ export interface PortableStoryRef extends StoryRef {
695
+ args?: Readonly<Record<string, unknown>>;
696
+ }
697
+
622
698
  export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
623
699
  readonly capabilities: AuthoringCapabilities;
700
+
701
+ readonly provenance: AuthoringProvenance = {
702
+ source: 'source-code',
703
+ label: 'jsx',
704
+ detail: 'The JSX source is the document — edits write back to the component source file.',
705
+ };
624
706
  private readonly writeBackend: SourceWriteBackend | undefined;
625
707
  private readonly computedStyle: ComputedStyleResolver;
626
708
  private readonly matchedCssRules: (el: unknown) => CssRuleTarget[];
@@ -687,14 +769,8 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
687
769
  * D3.R4 (reopen fix), widened by D3.R5 — a COUNTER (not a boolean) of source writes
688
770
  * whose own pre-HMR `notifyIngestEdit()` carries no fresh DOM shape, still awaiting
689
771
  * their post-HMR "reload landed" reconcile. Incremented by every SUCCESSFUL write on
690
- * a path with no `valueEcho` of its own: `structOp`, `removeElement`,
691
- * `restoreStructSnapshot` (the struct-undo/redo inverse — success only, never on a
692
- * refused/sha-mismatched restore, which writes nothing and has no reload to await),
693
- * `editText`'s success path, and `applyTextWrite` (the text-undo/redo inverse —
694
- * again success only). D3.R4 originally covered only `structOp`/`removeElement`; the
695
- * D3.R5 reopen found the SAME staleness, unflagged, at the struct-undo/redo closure
696
- * (every `structOp`/`removeElement` undo routes through `restoreStructSnapshot`) and
697
- * at a successful text write (`editText`/`applyTextWrite` flip `hasText`, itself a
772
+ * a path with no `valueEcho` of its own: structural writes and `editText`.
773
+ * A successful text write flips `hasText`, itself a
698
774
  * `findEmptyContainers` hint-eligibility criterion — text populates no `valueEcho`,
699
775
  * so neither existing reconcile branch fired for it).
700
776
  *
@@ -720,10 +796,21 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
720
796
  /** A4 — unsubscribes this adapter's `vite:afterUpdate` reconcile hook (see the constructor);
721
797
  * `undefined` when there is no HMR context (hosted build / vitest `node` env). */
722
798
  private readonly disposeReloadSignal: (() => void) | undefined;
723
- private readonly undoStack: PendingUndo[] = [];
724
- private readonly redoStack: PendingUndo[] = [];
725
799
  private readonly storySources: ReadonlyArray<Record<string, UIStory>>;
726
800
  private readonly onStoryApplied: ((story: (UIStory & { id: string }) | null) => void) | undefined;
801
+ private readonly portableStories: ReadonlyArray<PortableStoryRef>;
802
+ private readonly portableStoryArgs = new Map<string, Record<string, unknown>>();
803
+ private readonly portableDocuments: ReadonlyArray<{
804
+ id: string;
805
+ label: string;
806
+ path?: string | undefined;
807
+ storyIds: ReadonlyArray<string>;
808
+ }>;
809
+ private readonly portableDefaultStoryId: string | null;
810
+ private readonly onPortableStoryApplied: ((storyId: string | null) => void) | undefined;
811
+ private readonly onPortableStoryArgsChanged:
812
+ | ((storyId: string, args: Record<string, unknown>) => void)
813
+ | undefined;
727
814
  /** D4 — the currently-applied story id for this world (world-level in B2,
728
815
  * not per-node — see {@link stories}'s doc comment); `null` = no story
729
816
  * applied (the layer mounts against empty data, today's B1 behavior). */
@@ -736,6 +823,7 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
736
823
  | ((v: { componentKey: string; story: (UIStory & { id: string }) | null } | null) => void)
737
824
  | undefined;
738
825
  private readonly worldId: string | undefined;
826
+ private readonly storyHistory: JsonHistoryResource<Record<string, UIStory>> | null;
739
827
  /** B3 — the currently-isolated catalog node, or `null` (the entry preview
740
828
  * is rendered). See {@link stories}'s `isolate`. */
741
829
  private isolated: { nodeId: string; componentKey: string; storyId: string | undefined } | null =
@@ -759,12 +847,25 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
759
847
  priorInline: Map<string, string>;
760
848
  } | null = null;
761
849
 
850
+ /**
851
+ * Serializes {@link commitBoxEdit} runs so two gestures never overlap.
852
+ *
853
+ * `boxEdit.end()` fire-and-forgets an ASYNC commit (it cannot await — the
854
+ * gesture API is synchronous, driven straight from pointer/key handlers), and
855
+ * `editor-hotkeys.ts` drives `apply()`+`end()` on EVERY keypress. Hold an
856
+ * arrow key, or align a multi-selection, and the second commit calls
857
+ * a second source-history gesture while the first is still awaiting its
858
+ * writes. Chaining here keeps adapter preview/echo updates ordered too; the
859
+ * scoped history backend independently serializes each complete callback.
860
+ */
861
+ private boxEditTail: Promise<void> = Promise.resolve();
862
+
762
863
  constructor(
763
864
  private readonly root: OidElementLike,
764
865
  private readonly store: EditorStore,
765
866
  opts: ReactWorldAuthoringOptions = {},
766
867
  ) {
767
- this.writeBackend = opts.writeBackend;
868
+ this.writeBackend = withProjectSourceHistory(opts.writeBackend, store.projectHistory);
768
869
  this.computedStyle = opts.computedStyle ?? browserOrInlineResolver;
769
870
  this.matchedCssRules =
770
871
  opts.matchedCssRules ??
@@ -772,9 +873,77 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
772
873
  this.designTokens = opts.designTokens ?? (() => getDesignTokens());
773
874
  this.storySources = opts.storySources ?? [];
774
875
  this.onStoryApplied = opts.onStoryApplied;
876
+ this.portableStories = opts.portableStories ?? [];
877
+ for (const story of this.portableStories) {
878
+ this.portableStoryArgs.set(story.id, { ...(story.args ?? {}) });
879
+ }
880
+ const declaredPortableDocuments =
881
+ opts.portableDocuments ??
882
+ (this.portableStories.length > 0
883
+ ? [
884
+ {
885
+ id: PORTABLE_DOCUMENT_ID,
886
+ label: opts.portableDocument?.label ?? 'React Preview',
887
+ ...(opts.portableDocument?.path ? { path: opts.portableDocument.path } : {}),
888
+ storyIds: this.portableStories.map((story) => story.id),
889
+ },
890
+ ]
891
+ : []);
892
+ const knownStoryIds = new Set(this.portableStories.map((story) => story.id));
893
+ const claimedStoryIds = new Set<string>();
894
+ const documentIds = new Set<string>();
895
+ const portableDocuments = declaredPortableDocuments.flatMap((document) => {
896
+ if (documentIds.has(document.id)) return [];
897
+ documentIds.add(document.id);
898
+ const storyIds = document.storyIds.filter((storyId) => {
899
+ if (!knownStoryIds.has(storyId) || claimedStoryIds.has(storyId)) return false;
900
+ claimedStoryIds.add(storyId);
901
+ return true;
902
+ });
903
+ return storyIds.length > 0 ? [{ ...document, storyIds }] : [];
904
+ });
905
+ const unassignedStoryIds = this.portableStories
906
+ .map((story) => story.id)
907
+ .filter((storyId) => !claimedStoryIds.has(storyId));
908
+ if (unassignedStoryIds.length > 0) {
909
+ portableDocuments.push({
910
+ id: documentIds.has(PORTABLE_DOCUMENT_ID)
911
+ ? `${PORTABLE_DOCUMENT_ID}:unassigned`
912
+ : PORTABLE_DOCUMENT_ID,
913
+ label: opts.portableDocument?.label ?? 'React Preview',
914
+ ...(opts.portableDocument?.path ? { path: opts.portableDocument.path } : {}),
915
+ storyIds: unassignedStoryIds,
916
+ });
917
+ }
918
+ this.portableDocuments = portableDocuments;
919
+ this.portableDefaultStoryId = this.portableStories.some(
920
+ (story) => story.id === opts.activePortableStoryId,
921
+ )
922
+ ? (opts.activePortableStoryId ?? null)
923
+ : (this.portableStories[0]?.id ?? null);
924
+ this.onPortableStoryApplied = opts.onPortableStoryApplied;
925
+ this.onPortableStoryArgsChanged = opts.onPortableStoryArgsChanged;
926
+ this.activeStoryId = this.portableDefaultStoryId;
775
927
  this.catalogComponents = opts.catalogComponents ?? {};
776
928
  this.onIsolate = opts.onIsolate;
777
929
  this.worldId = opts.worldId;
930
+ this.storyHistory =
931
+ store.projectHistory && this.worldId
932
+ ? new JsonHistoryResource({
933
+ history: store.projectHistory,
934
+ kind: 'overlay',
935
+ scope: 'session',
936
+ sessionId: `react-story-${this.worldId}-${crypto.randomUUID()}`,
937
+ displayName: `${this.worldId} Story Overlay`,
938
+ capture: () => structuredClone(getStoryOverlay(this.worldId!) ?? {}),
939
+ restore: (stories) => {
940
+ setStoryOverlay(this.worldId!, structuredClone(stories));
941
+ if (this.isolated?.storyId)
942
+ this.doIsolate(this.isolated.nodeId, this.isolated.storyId);
943
+ this.store.notifyIngestEdit();
944
+ },
945
+ })
946
+ : null;
778
947
  this.capabilities = {
779
948
  transform: false, // no 3D gizmo — DOM has no Object3D pose (§1.F)
780
949
  material: false,
@@ -784,11 +953,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
784
953
  reparent: true, // Cap 5: real source reparent (drag-to-reparent)
785
954
  persist: true,
786
955
  };
787
- // T3.2 slice 2: this adapter IS the undo/redo target the editor-global
788
- // timeline dispatches to for entries it records (`pushUndo`). The caller
789
- // (`play-mode.ts`'s `exitMultiWorldAuthoring`) unregisters it on teardown,
790
- // mirroring `IngestAuthoringAdapter`.
791
- registerUndoAdapter(this);
792
956
  // A4 (spec 27 §3) — re-sync the optimistic echo once HMR has actually re-rendered the
793
957
  // react tree. Vite fires `vite:afterUpdate` after it applies an HMR update — here, the
794
958
  // dev-server source-write this adapter triggered (→ file watcher → react-refresh
@@ -889,25 +1053,93 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
889
1053
 
890
1054
  /** A4 — release the `vite:afterUpdate` reconcile subscription. Idempotent; safe when no
891
1055
  * HMR context was present (the disposer is `undefined`). */
892
- disposeReactWorldAdapter(): void {
1056
+ disposeReactRootAdapter(): void {
893
1057
  this.disposeReloadSignal?.();
1058
+ this.storyHistory?.dispose();
894
1059
  }
895
1060
 
896
- private toEditorNode(n: OidNode): EditorNode {
897
- // Component label priority (Cap 1): the OID source index (`component:tag`), then the
898
- // LIVE React fiber's component name (`getReactComponentName` — resolves for a
899
- // component's root element even before/without the index fetch), then the DOM tag.
1061
+ private componentName(n: OidNode): string | null {
900
1062
  const entry = this.oidIndex.get(n.oid);
901
- const component = entry?.component ?? getReactComponentName(n.el);
902
- const label = component ? `${component}:${n.tag}` : n.tag;
1063
+ return entry?.component ?? getReactComponentName(n.el) ?? null;
1064
+ }
1065
+
1066
+ private elementLabel(n: OidNode, tree: OidTree): string {
1067
+ // Preserve the explicit escape hatch, but make ordinary DOM semantics
1068
+ // sufficient by default. Authored React should not need component splits
1069
+ // or `data-vgai-name` merely to produce a legible hierarchy.
1070
+ const editorLabel = normalizeElementText(n.el.getAttribute('data-vgai-name'));
1071
+ if (editorLabel) return editorLabel;
1072
+
1073
+ const labelledBy = n.el.getAttribute('aria-labelledby');
1074
+ if (labelledBy) {
1075
+ const labels = labelledBy
1076
+ .split(/\s+/)
1077
+ .map((id) =>
1078
+ Array.from(tree.nodes.values()).find(
1079
+ (candidate) => candidate.el.getAttribute('id') === id,
1080
+ ),
1081
+ )
1082
+ .map((candidate) => normalizeElementText(candidate?.el.textContent))
1083
+ .filter((label): label is string => label !== null);
1084
+ const combinedLabel = normalizeElementText(labels.join(' '));
1085
+ if (combinedLabel) return combinedLabel;
1086
+ }
1087
+
1088
+ const accessibleLabel = normalizeElementText(n.el.getAttribute('aria-label'));
1089
+ if (accessibleLabel) return accessibleLabel;
1090
+
1091
+ // A semantic section without explicit ARIA normally takes its identity
1092
+ // from its heading. Restrict this to direct children so a large nested
1093
+ // subtree cannot accidentally lend an unrelated heading to its parent.
1094
+ if (['section', 'article', 'aside', 'nav', 'main', 'header', 'footer'].includes(n.tag)) {
1095
+ const heading = n.childIds
1096
+ .map((id) => tree.nodes.get(id))
1097
+ .find((child) => child !== undefined && /^h[1-6]$/.test(child.tag));
1098
+ const headingText = normalizeElementText(heading?.el.textContent);
1099
+ if (headingText) return headingText;
1100
+ }
1101
+
1102
+ // Text-bearing HTML elements are already named by their content in the
1103
+ // browser/accessibility model. Use the same identity, with a short cap so
1104
+ // a paragraph cannot turn into an unbounded hierarchy row.
1105
+ if (
1106
+ /^(h[1-6]|p|span|label|button|a|time|output|dt|dd|li|legend|caption|summary)$/.test(n.tag)
1107
+ ) {
1108
+ const text = normalizeElementText(n.el.textContent);
1109
+ if (text) return text;
1110
+ }
1111
+
1112
+ for (const attribute of ['alt', 'title', 'placeholder', 'name']) {
1113
+ const value = normalizeElementText(n.el.getAttribute(attribute));
1114
+ if (value) return value;
1115
+ }
1116
+
1117
+ const id = n.el.getAttribute('id');
1118
+ if (id) return `#${id}`;
1119
+ return n.tag;
1120
+ }
1121
+
1122
+ private toEditorNode(n: OidNode, tree: OidTree = this.snapshot()): EditorNode {
1123
+ // A component name marks only the boundary where that component enters the
1124
+ // native tree. Descendants owned by the same component use their own DOM
1125
+ // identity, avoiding the old `Game2048Page:div` repeated on every row.
1126
+ const component = this.componentName(n);
1127
+ const parent = n.parentId ? tree.nodes.get(n.parentId) : null;
1128
+ const parentComponent = parent ? this.componentName(parent) : null;
1129
+ const isComponentBoundary = component !== null && component !== parentComponent;
903
1130
  const node: EditorNode = {
904
1131
  id: n.id,
905
- label,
1132
+ label: isComponentBoundary ? component : this.elementLabel(n, tree),
1133
+ ...(isComponentBoundary ? { secondaryLabel: `<${n.tag}>` } : {}),
1134
+ role: isComponentBoundary ? 'component' : 'element',
906
1135
  kind: n.tag,
907
1136
  parentId: n.parentId,
908
1137
  childIds: n.childIds,
909
1138
  flags: { runtimeOnly: true },
910
1139
  };
1140
+ if (this.portableStories.length > 0 && node.parentId === null) {
1141
+ return { ...node, parentId: this.activeStoryId };
1142
+ }
911
1143
  // B3 — a former DOM ROOT (`parentId === null`) nests one level under the
912
1144
  // catalog's entry-preview node when the catalog is on (§0's reconciliation:
913
1145
  // GameHierarchy's sibling/drag math keys off `parentId`, so a root that
@@ -931,6 +1163,7 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
931
1163
  return {
932
1164
  id: `${CATALOG_ID_PREFIX}${key}`,
933
1165
  label: key,
1166
+ role: 'component',
934
1167
  kind: 'component',
935
1168
  parentId: null,
936
1169
  childIds: [],
@@ -938,13 +1171,14 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
938
1171
  };
939
1172
  }
940
1173
 
941
- /** B3 — the one `'entry (live preview)'` node; its children are the
1174
+ /** B3 — the one Live Preview node; its children are the
942
1175
  * DOM-walk's OWN root ids (unchanged, still resolved by `hierarchy.node()`
943
1176
  * through the ordinary DOM path — see {@link toEditorNode}'s remap). */
944
1177
  private entryNode(rootIds: string[]): EditorNode {
945
1178
  return {
946
1179
  id: CATALOG_ENTRY_ID,
947
- label: 'entry (live preview)',
1180
+ label: 'Live Preview',
1181
+ role: 'document',
948
1182
  kind: 'group',
949
1183
  parentId: null,
950
1184
  childIds: rootIds,
@@ -952,16 +1186,56 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
952
1186
  };
953
1187
  }
954
1188
 
1189
+ private portableDocumentNode(document: (typeof this.portableDocuments)[number]): EditorNode {
1190
+ return {
1191
+ id: document.id,
1192
+ label: document.label,
1193
+ ...(document.path ? { secondaryLabel: document.path } : {}),
1194
+ role: 'document',
1195
+ kind: 'tsx',
1196
+ parentId: null,
1197
+ childIds: [...document.storyIds],
1198
+ flags: { runtimeOnly: true },
1199
+ };
1200
+ }
1201
+
1202
+ private portableDocumentForStory(storyId: string) {
1203
+ return this.portableDocuments.find((document) => document.storyIds.includes(storyId)) ?? null;
1204
+ }
1205
+
1206
+ private portableStoryNode(story: StoryRef, rootIds: string[]): EditorNode {
1207
+ const active = story.id === this.activeStoryId;
1208
+ return {
1209
+ id: story.id,
1210
+ label: story.label,
1211
+ role: 'story',
1212
+ kind: 'story',
1213
+ parentId: this.portableDocumentForStory(story.id)?.id ?? PORTABLE_DOCUMENT_ID,
1214
+ childIds: active ? rootIds : [],
1215
+ flags: { runtimeOnly: true, active },
1216
+ };
1217
+ }
1218
+
955
1219
  readonly hierarchy: HierarchyProvider = {
956
1220
  roots: () => {
957
- const { nodes, rootIds } = this.snapshot();
958
- if (!this.catalogOn) return rootIds.map((id) => this.toEditorNode(nodes.get(id)!));
1221
+ const tree = this.snapshot();
1222
+ const { nodes, rootIds } = tree;
1223
+ if (this.portableStories.length > 0) {
1224
+ return this.portableDocuments.map((document) => this.portableDocumentNode(document));
1225
+ }
1226
+ if (!this.catalogOn) return rootIds.map((id) => this.toEditorNode(nodes.get(id)!, tree));
959
1227
  return [
960
1228
  ...Object.keys(this.catalogComponents).map((key) => this.catalogNode(key)),
961
1229
  this.entryNode(rootIds),
962
1230
  ];
963
1231
  },
964
1232
  node: (id) => {
1233
+ if (this.portableStories.length > 0) {
1234
+ const document = this.portableDocuments.find((candidate) => candidate.id === id);
1235
+ if (document) return this.portableDocumentNode(document);
1236
+ const story = this.portableStories.find((candidate) => candidate.id === id);
1237
+ if (story) return this.portableStoryNode(story, this.snapshot().rootIds);
1238
+ }
965
1239
  if (this.catalogOn) {
966
1240
  if (id === CATALOG_ENTRY_ID) return this.entryNode(this.snapshot().rootIds);
967
1241
  if (id.startsWith(CATALOG_ID_PREFIX)) {
@@ -969,8 +1243,9 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
969
1243
  return key in this.catalogComponents ? this.catalogNode(key) : null;
970
1244
  }
971
1245
  }
972
- const n = this.snapshot().nodes.get(id);
973
- return n ? this.toEditorNode(n) : null;
1246
+ const tree = this.snapshot();
1247
+ const n = tree.nodes.get(id);
1248
+ return n ? this.toEditorNode(n, tree) : null;
974
1249
  },
975
1250
  object3D: () => null, // react entities are DOM, not Object3D — no 3D gizmo binding
976
1251
  idForObject3D: () => null,
@@ -993,6 +1268,13 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
993
1268
  * `OidNode`s already resolved by `walkOidTree`, not a live `querySelectorAll`
994
1269
  * over a DOM attribute string.
995
1270
  *
1271
+ * An element that EXPLICITLY opts out with inline `pointer-events:none` is
1272
+ * skipped. The design-time host itself is `pointer-events:none`, so checking
1273
+ * computed style would incorrectly inherit that host policy into every
1274
+ * authorable descendant. Reading the node's own declaration instead preserves
1275
+ * click-to-author descendants while making an authored full-viewport
1276
+ * click-through wrapper behave as click-through here too.
1277
+ *
996
1278
  * The winning id is the SAME disambiguated id `hierarchy`/`selection`
997
1279
  * already use (this IS `walkOidTree`'s own id space) — a hit routes
998
1280
  * straight into `composite.selection.set([id])` with no translation. A
@@ -1015,6 +1297,7 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1015
1297
  // unlocked node is behind/around it). See `lockedIds`'s doc
1016
1298
  // comment for why this Set, not a real inspector-backed field.
1017
1299
  if (this.lockedIds.has(node.id)) continue;
1300
+ if (styleProp(node.el.style, 'pointerEvents') === 'none') continue;
1018
1301
  const rect = node.el.getBoundingClientRect?.();
1019
1302
  if (!rect) continue; // no live rect (test fixture, or unmounted) — never a candidate
1020
1303
  if (
@@ -1056,7 +1339,13 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1056
1339
  height: number;
1057
1340
  }): DOMRectLike {
1058
1341
  const host = this.hostRect();
1059
- return { x: r.left - host.left, y: r.top - host.top, width: r.width, height: r.height };
1342
+ const zoom = getWorldPan().zoom;
1343
+ return {
1344
+ x: (r.left - host.left) / zoom,
1345
+ y: (r.top - host.top) / zoom,
1346
+ width: r.width / zoom,
1347
+ height: r.height / zoom,
1348
+ };
1060
1349
  }
1061
1350
 
1062
1351
  /**
@@ -1227,7 +1516,16 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1227
1516
  const session = this.boxEditSession;
1228
1517
  this.boxEditSession = null;
1229
1518
  if (!session || session.id !== id || session.touched.size === 0) return;
1230
- void this.commitBoxEdit(id, session.touched, session.priorInline);
1519
+ // Queue behind any still-running commit — see `boxEditTail`. A failed
1520
+ // commit must not poison the chain for every later gesture, so the tail
1521
+ // absorbs the rejection (commitBoxEdit reports its own failures).
1522
+ this.boxEditTail = this.boxEditTail.then(
1523
+ () => this.commitBoxEdit(id, session.touched, session.priorInline),
1524
+ () => this.commitBoxEdit(id, session.touched, session.priorInline),
1525
+ );
1526
+ void this.boxEditTail.catch((error) => {
1527
+ console.error('[ReactWorldAuthoringAdapter] box edit failed and was not committed.', error);
1528
+ });
1231
1529
  },
1232
1530
  };
1233
1531
 
@@ -1244,6 +1542,7 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1244
1542
  touched: Map<string, string | number>,
1245
1543
  priorInline: Map<string, string>,
1246
1544
  ): Promise<void> {
1545
+ const wasDirty = this.dirty;
1247
1546
  // A4 — echo every touched prop BEFORE any write, so the inspector field shows
1248
1547
  // the dragged value at once (same discipline as `inspector.set`). The echo holds
1249
1548
  // the BARE numeric (a `type:'number'` descriptor's `Inspector` field needs an
@@ -1252,34 +1551,44 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1252
1551
  for (const [prop, value] of touched) {
1253
1552
  this.setEcho(id, `${STYLE_PATH_PREFIX}${prop}`, value);
1254
1553
  }
1255
- const entries: PendingUndo[] = [];
1256
- for (const [prop, value] of touched) {
1257
- // D1 (spec 27 §4 B2/B3 reload-safety) — a numeric length-prop value must
1258
- // persist to JSX SOURCE in a form React honors on REMOUNT. React 19 DROPS a
1259
- // bare UNITLESS numeric STRING (the writer quotes `String(152)` → `width:
1260
- // '152'`) on reload — the element collapses to content size (verified in real
1261
- // Chromium + React 19) — but HONORS a quoted CSS length (`width: '152px'`).
1262
- // `boxEdit.apply` stores length props as bare NUMBERS and CSS-string props
1263
- // (`transform` → `'rotate(90deg)'`) as strings, so a numeric value here is
1264
- // EXACTLY the set of length props (width/height/left/top/margin*/padding*)
1265
- // needing a `px` unit — suffix only those, leaving `transform` untouched.
1266
- // (The live-preview `el.style` path already applied a px-suffixed string via
1267
- // `mapBoxEditPatchKey.cssValue`; only the persisted-source form was unitless.)
1268
- // Localized to this box-edit commit — the color/`inspector.set` write path is
1269
- // deliberately NOT changed.
1270
- const writeValue = typeof value === 'number' ? `${value}px` : value;
1271
- const entry = await this.writeStyleEntry(id, prop, writeValue, priorInline.get(prop));
1272
- if (entry) entries.push(entry);
1554
+ const commit = async (backend: SourceWriteBackend | undefined): Promise<void> => {
1555
+ for (const [prop, value] of touched) {
1556
+ // D1 (spec 27 §4 B2/B3 reload-safety) — a numeric length-prop value must
1557
+ // persist to JSX SOURCE in a form React honors on REMOUNT. React 19 DROPS a
1558
+ // bare UNITLESS numeric STRING (the writer quotes `String(152)` → `width:
1559
+ // '152'`) on reload — the element collapses to content size (verified in real
1560
+ // Chromium + React 19) — but HONORS a quoted CSS length (`width: '152px'`).
1561
+ // `boxEdit.apply` stores length props as bare NUMBERS and CSS-string props
1562
+ // (`transform` → `'rotate(90deg)'`) as strings, so a numeric value here is
1563
+ // EXACTLY the set of length props (width/height/left/top/margin*/padding*)
1564
+ // needing a `px` unit — suffix only those, leaving `transform` untouched.
1565
+ // (The live-preview `el.style` path already applied a px-suffixed string via
1566
+ // `mapBoxEditPatchKey.cssValue`; only the persisted-source form was unitless.)
1567
+ // Localized to this box-edit commit — the color/`inspector.set` write path is
1568
+ // deliberately NOT changed.
1569
+ const writeValue = typeof value === 'number' ? `${value}px` : value;
1570
+ await this.writeStyleEntry(id, prop, writeValue, priorInline.get(prop), backend);
1571
+ }
1572
+ };
1573
+ // The history-managed backend supplies an explicit scoped writer. Holding
1574
+ // the callback as one queue entry makes the gesture failure-atomic and
1575
+ // prevents unrelated inspector writes from being absorbed/interleaved.
1576
+ try {
1577
+ if (this.writeBackend?.runGesture) {
1578
+ await this.writeBackend.runGesture(`Edit ${touched.size} Styles`, commit);
1579
+ } else {
1580
+ await commit(this.writeBackend);
1581
+ }
1582
+ } catch (error) {
1583
+ const node = this.snapshot().nodes.get(id);
1584
+ for (const [prop, prior] of priorInline) {
1585
+ this.clearEcho(id, `${STYLE_PATH_PREFIX}${prop}`);
1586
+ if (node?.el.style) (node.el.style as Record<string, unknown>)[prop] = prior;
1587
+ }
1588
+ this.dirty = wasDirty;
1589
+ this.store.notifyIngestEdit();
1590
+ throw error;
1273
1591
  }
1274
- if (entries.length === 0) return;
1275
- this.pushUndo({
1276
- undo: () => {
1277
- for (let i = entries.length - 1; i >= 0; i--) entries[i]!.undo();
1278
- },
1279
- redo: () => {
1280
- for (const entry of entries) entry.redo();
1281
- },
1282
- });
1283
1592
  }
1284
1593
 
1285
1594
  /**
@@ -1313,6 +1622,22 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1313
1622
 
1314
1623
  readonly inspector: InspectorProvider = {
1315
1624
  properties: (id: string): PropertyDescriptor[] => {
1625
+ const portableDocument = this.portableDocuments.find((document) => document.id === id);
1626
+ if (portableDocument) {
1627
+ return portableDocument.path
1628
+ ? [
1629
+ {
1630
+ path: 'document.path',
1631
+ label: 'Source',
1632
+ type: 'string',
1633
+ readonly: true,
1634
+ group: 'Document',
1635
+ },
1636
+ ]
1637
+ : [];
1638
+ }
1639
+ if (this.portableStories.some((story) => story.id === id))
1640
+ return this.portableStoryArgDescriptors(id);
1316
1641
  // B3 — a catalog node (`catalog:<key>`) has no DOM element/fiber behind
1317
1642
  // it at all (`object3D`/`idForObject3D` are null for these ids too) —
1318
1643
  // style/Cap-4-props/tokens would be garbage for it. Its ONE inspector
@@ -1345,6 +1670,19 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1345
1670
  );
1346
1671
  },
1347
1672
  get: (id, path) => {
1673
+ const portableDocument = this.portableDocuments.find((document) => document.id === id);
1674
+ if (portableDocument) {
1675
+ return path === 'document.path' ? portableDocument.path : undefined;
1676
+ }
1677
+ if (this.portableStories.some((story) => story.id === id)) {
1678
+ if (path === 'story.active') return id === this.activeStoryId;
1679
+ if (path.startsWith(PORTABLE_STORY_ARG_PATH_PREFIX)) {
1680
+ return this.portableStoryArgs.get(id)?.[
1681
+ path.slice(PORTABLE_STORY_ARG_PATH_PREFIX.length)
1682
+ ];
1683
+ }
1684
+ return undefined;
1685
+ }
1348
1686
  // A4 — read THROUGH the optimistic echo: a value just set (style/prop) wins over the
1349
1687
  // live-DOM walk until the source-write + HMR land, so the field never snaps back.
1350
1688
  const echoed = this.readEcho(id, path);
@@ -1406,6 +1744,18 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1406
1744
  return raw || undefined; // an unset computed value reads '' — surface as blank
1407
1745
  },
1408
1746
  set: (id, path, value) => {
1747
+ if (this.portableDocuments.some((document) => document.id === id)) return;
1748
+ if (this.portableStories.some((story) => story.id === id)) {
1749
+ if (!path.startsWith(PORTABLE_STORY_ARG_PATH_PREFIX)) return;
1750
+ const name = path.slice(PORTABLE_STORY_ARG_PATH_PREFIX.length);
1751
+ const current = this.portableStoryArgs.get(id);
1752
+ if (!current || !(name in current)) return;
1753
+ const next = { ...current, [name]: value };
1754
+ this.portableStoryArgs.set(id, next);
1755
+ this.onPortableStoryArgsChanged?.(id, { ...next });
1756
+ this.store.notifyIngestEdit();
1757
+ return;
1758
+ }
1409
1759
  if (this.isCatalogId(id)) {
1410
1760
  if (path.startsWith(STORY_PROP_PATH_PREFIX)) {
1411
1761
  this.writeStoryProp(id, path.slice(STORY_PROP_PATH_PREFIX.length), value);
@@ -1464,6 +1814,52 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1464
1814
  },
1465
1815
  };
1466
1816
 
1817
+ /** Storybook Controls descriptors for an ordinary CSF story node. Functions
1818
+ * and other non-serializable implementation values stay out of the form;
1819
+ * they remain present in the complete args object passed to the composed
1820
+ * story. */
1821
+ private portableStoryArgDescriptors(storyId: string): PropertyDescriptor[] {
1822
+ const args = this.portableStoryArgs.get(storyId) ?? {};
1823
+ return Object.entries(args).flatMap(([name, value]) => {
1824
+ if (typeof value === 'function' || typeof value === 'symbol' || value === undefined)
1825
+ return [];
1826
+ const type: PropertyDescriptor['type'] =
1827
+ typeof value === 'boolean'
1828
+ ? 'boolean'
1829
+ : typeof value === 'number'
1830
+ ? 'number'
1831
+ : typeof value === 'string'
1832
+ ? 'string'
1833
+ : 'json';
1834
+ return [
1835
+ {
1836
+ path: `${PORTABLE_STORY_ARG_PATH_PREFIX}${name}`,
1837
+ label: name,
1838
+ type,
1839
+ group: 'Args',
1840
+ },
1841
+ ];
1842
+ });
1843
+ }
1844
+
1845
+ /** Current complete args for a portable story, including callbacks omitted
1846
+ * from the visual Controls list. Returned as a copy so inspector UI cannot
1847
+ * mutate adapter state behind the provider contract. */
1848
+ portableArgs(storyId: string): Record<string, unknown> | null {
1849
+ const args = this.portableStoryArgs.get(storyId);
1850
+ return args ? { ...args } : null;
1851
+ }
1852
+
1853
+ /** Restore the selected story's composed CSF defaults. */
1854
+ resetPortableArgs(storyId: string): void {
1855
+ const story = this.portableStories.find((candidate) => candidate.id === storyId);
1856
+ if (!story) return;
1857
+ const next = { ...(story.args ?? {}) };
1858
+ this.portableStoryArgs.set(storyId, next);
1859
+ this.onPortableStoryArgsChanged?.(storyId, { ...next });
1860
+ this.store.notifyIngestEdit();
1861
+ }
1862
+
1467
1863
  /** B3 — true for a catalog node id (`catalog:<key>`), never for the entry
1468
1864
  * node or an ordinary DOM node id. */
1469
1865
  private isCatalogId(id: string): boolean {
@@ -1558,7 +1954,11 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1558
1954
  data: base?.data ?? {},
1559
1955
  props: { ...(base?.props ?? {}), [propName]: value },
1560
1956
  };
1561
- setStoryOverlay(this.worldId, { ...(getStoryOverlay(this.worldId) ?? {}), [storyId]: patched });
1957
+ this.storyHistory?.assertCanMutate();
1958
+ const before = structuredClone(getStoryOverlay(this.worldId) ?? {});
1959
+ const after = { ...before, [storyId]: patched };
1960
+ setStoryOverlay(this.worldId, after);
1961
+ void this.storyHistory?.record(`Edit ${propName}`, before, structuredClone(after));
1562
1962
  this.doIsolate(nodeId, storyId); // re-render against the patched story
1563
1963
  }
1564
1964
 
@@ -1624,9 +2024,26 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1624
2024
  */
1625
2025
  readonly stories: StoriesProvider = {
1626
2026
  storiesFor: (_nodeId): StoryRef[] =>
1627
- [...this.mergedStories().entries()].map(([id, story]) => ({ id, label: story.label })),
2027
+ this.portableStories.length > 0
2028
+ ? [...this.portableStories]
2029
+ : [...this.mergedStories().entries()].map(([id, story]) => ({ id, label: story.label })),
1628
2030
  active: (_nodeId) => this.activeStoryId,
1629
2031
  apply: (_nodeId, storyId) => {
2032
+ if (this.portableStories.length > 0) {
2033
+ if (storyId !== null && !this.portableStories.some((story) => story.id === storyId)) {
2034
+ console.warn(
2035
+ `[ReactWorldAuthoringAdapter] stories.apply: unknown portable CSF story id "${storyId}" — ignoring.`,
2036
+ );
2037
+ return;
2038
+ }
2039
+ // Portable CSF always renders a design state. Clearing the picker
2040
+ // means "restore the document's default", not "orphan the live DOM
2041
+ // roots outside every story row".
2042
+ this.activeStoryId = storyId ?? this.portableDefaultStoryId;
2043
+ this.onPortableStoryApplied?.(storyId);
2044
+ this.store.notifyIngestEdit();
2045
+ return;
2046
+ }
1630
2047
  if (storyId === null) {
1631
2048
  this.activeStoryId = null;
1632
2049
  this.onStoryApplied?.(null);
@@ -1701,14 +2118,8 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1701
2118
  }
1702
2119
 
1703
2120
  readonly structure: StructureProvider = {
1704
- // Cap 5 (React visual-edit parity): real source structural ops. D-1 (Phase D,
1705
- // spec27 §2): EVERY structural op — including `create`/`duplicate`, whose new
1706
- // element has no OID until the next transform/HMR — IS pushed to the undo
1707
- // timeline, via a checksum-guarded WHOLE-FILE snapshot inverse (`pushStructUndo`)
1708
- // rather than a per-OID one. That sidesteps the OID problem entirely: undo/redo
1709
- // never needs to address the new/moved element by OID, it just restores the
1710
- // file's prior/next full text. (Previously irreversible in this slice — see
1711
- // `pushStructUndo`'s doc comment for the full rationale and the delete case.)
2121
+ // Every structural op journals complete next-file text. Undo never needs to
2122
+ // address a moved or deleted element by an OID that may no longer exist.
1712
2123
  create: (kind, parentId) => {
1713
2124
  void this.structOp(parentId ?? '', 'create', { wrapperTag: kind });
1714
2125
  return '';
@@ -1862,6 +2273,21 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1862
2273
  return this.snapshot().nodes.get(id)?.oid;
1863
2274
  }
1864
2275
 
2276
+ /**
2277
+ * C4 (spec §9): map an entity id to its component/source location, when available —
2278
+ * the read side of `oidIndex` (populated from `SourceWriteBackend.index()`, same as
2279
+ * `toEditorNode`'s label lookup). Returns `undefined` when the id has no OID (e.g. a
2280
+ * catalog node) or the index hasn't resolved (no dev-server backend, or the index
2281
+ * fetch hasn't landed yet) — an honest "unavailable", never a guess. The editor UI
2282
+ * (`react-inspector-section.tsx`'s `LaneDisclosure`) uses this to offer copy-path /
2283
+ * go-to-line without ever exposing a write surface.
2284
+ */
2285
+ sourceLocation(id: string): OidEntry | undefined {
2286
+ const oid = this.oidOf(id);
2287
+ if (!oid) return undefined;
2288
+ return this.oidIndex.get(oid);
2289
+ }
2290
+
1865
2291
  /** Run a structural op through the write backend (loud degradation without one). */
1866
2292
  private async structOp(
1867
2293
  id: string,
@@ -1885,9 +2311,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1885
2311
  return;
1886
2312
  }
1887
2313
  this.dirty = true;
1888
- // D-1 (Phase D, spec27 §2): push the whole-file-snapshot undo/redo entry —
1889
- // see `pushStructUndo`'s doc comment.
1890
- this.pushStructUndo(op, res);
1891
2314
  // D3.R4 (reopen fix) — this notify's `storeVersion` still reflects the PRE-HMR DOM (see
1892
2315
  // `pendingSourceReconcile`'s doc comment); queue a pending reconcile so the upcoming
1893
2316
  // `vite:afterUpdate` (or a test's direct `reconcileEchoAfterReload()` call) notifies AGAIN
@@ -1897,96 +2320,9 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1897
2320
  }
1898
2321
 
1899
2322
  /**
1900
- * D-1 (Phase D, spec27 §2 "known limitation to schedule, not hide"): push the
1901
- * checksum-guarded whole-file-snapshot undo/redo entry for a successful structural
1902
- * write — shared by {@link structOp} (create/duplicate/wrap/unwrap/reorder/reparent)
1903
- * and {@link removeElement} (delete). A naive per-OID inverse is UNSOUND for a
1904
- * structural op: OIDs are `file:component:tag:nthOccurrence` content signatures,
1905
- * `reorder`/`delete` reassign occurrence indices out from under any OID captured
1906
- * before the op, and `delete` has no inverse payload of its own (there is nothing
1907
- * left to re-target). Instead the inverse is the WHOLE touched file's prior/next
1908
- * text (`res.prevSource`/`res.newSource`, captured server-side by `handleStruct` —
1909
- * `vite-plugin-ui-oid.ts`) replayed through a checksum-guarded restore
1910
- * (`SourceWriteBackend.restoreSource`) — a plain overwrite the server refuses,
1911
- * WITHOUT writing, if the file's current sha256 doesn't match the hash the caller
1912
- * expects (an out-of-band change since, e.g. a hand-edit). Degrades to "not
1913
- * undoable" (console.warn, no entry pushed) when the backend lacks `restoreSource`
1914
- * or the write response is missing a snapshot field (an old/mocked backend) — an
1915
- * honest limitation, never a crash.
1916
- */
1917
- private pushStructUndo(
1918
- op: string,
1919
- res: {
1920
- file?: string;
1921
- prevSource?: string;
1922
- newSource?: string;
1923
- prevSha?: string;
1924
- newSha?: string;
1925
- },
1926
- ): void {
1927
- const { file, prevSource, newSource, prevSha, newSha } = res;
1928
- if (
1929
- !this.writeBackend?.restoreSource ||
1930
- file === undefined ||
1931
- prevSource === undefined ||
1932
- newSource === undefined ||
1933
- prevSha === undefined ||
1934
- newSha === undefined
1935
- ) {
1936
- console.warn(
1937
- `[ReactWorldAuthoringAdapter] struct "${op}" is not undoable: the write backend ` +
1938
- 'does not support whole-file restore (missing restoreSource or a snapshot field).',
1939
- );
1940
- return;
1941
- }
1942
- this.pushUndo({
1943
- undo: () => void this.restoreStructSnapshot(file, prevSource, newSha),
1944
- redo: () => void this.restoreStructSnapshot(file, newSource, prevSha),
1945
- });
1946
- }
1947
-
1948
- /**
1949
- * D-1 — the structural undo/redo inverse: POST the checksum-guarded whole-file
1950
- * restore. A refusal (`res.restored === false` — the file's CURRENT sha256 didn't
1951
- * match `ifMatchSha`, e.g. it changed out-of-band since this entry was recorded) is
1952
- * a LOUD no-op: `restoreSource` never writes on refusal, and this still notifies the
1953
- * store (same pattern as the D2 dynamic-write-refusal notify, `writeStyleEntry`/
1954
- * `writePropEdit`) so the UI ticks and can render the refusal instead of silently
1955
- * doing nothing.
1956
- *
1957
- * D3.R5 (reopen fix) — every `structOp`/`removeElement` undo/redo closure routes
1958
- * here (see `pushStructUndo`), so a SUCCESSFUL restore is itself a structural DOM
1959
- * change with the exact same pre-HMR-notify timing gap `structOp`/`removeElement`
1960
- * have (see `pendingSourceReconcile`'s doc comment) — queue a pending reconcile so
1961
- * hint-cache consumers reconcile once the restore's own remount actually lands. NOT
1962
- * queued on a refused restore: `restoreSource` never writes when refused, so there is
1963
- * no reload to await.
1964
- */
1965
- private async restoreStructSnapshot(
1966
- file: string,
1967
- source: string,
1968
- ifMatchSha: string,
1969
- ): Promise<void> {
1970
- if (!this.writeBackend?.restoreSource) return;
1971
- const res = await this.writeBackend.restoreSource(file, source, ifMatchSha);
1972
- if (!res.restored) {
1973
- console.warn(
1974
- `[ReactWorldAuthoringAdapter] structural undo/redo refused for "${file}": ` +
1975
- `${res.error ?? 'the file changed since this entry was recorded'}`,
1976
- );
1977
- } else {
1978
- this.pendingSourceReconcile++;
1979
- }
1980
- this.store.notifyIngestEdit();
1981
- }
1982
-
1983
- /**
1984
- * T0 (spec 27 §4, B1) — the write body extracted from {@link writeStyle}, RETURNING
1985
- * the resulting {@link PendingUndo} entry instead of pushing it, so a caller that
1986
- * writes MULTIPLE props for one logical gesture (`boxEdit`'s {@link commitBoxEdit})
1987
- * can compose them into a single undo entry itself. `writeStyle` (below) is now a
1988
- * one-line wrapper: identical behavior for every existing single-prop caller
1989
- * (`inspector.set`). `priorInlineOverride`, when given, is used as the captured
2323
+ * T0 (spec 27 §4, B1) — the write body extracted from {@link writeStyle}, returning
2324
+ * whether source changed so a multi-property box gesture can reconcile once.
2325
+ * `priorInlineOverride`, when given, is used as the captured
1990
2326
  * PRE-gesture inline value instead of re-reading `n.el.style` — needed because
1991
2327
  * `boxEdit.apply` already mutated the live inline style for live preview before
1992
2328
  * `end` gets here, so a fresh DOM read would see the LAST previewed value, not the
@@ -1999,7 +2335,8 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
1999
2335
  prop: string,
2000
2336
  value: string | number,
2001
2337
  priorInlineOverride?: string,
2002
- ): Promise<PendingUndo | null> {
2338
+ backend: SourceWriteBackend | undefined = this.writeBackend,
2339
+ ): Promise<boolean> {
2003
2340
  // A4 — the echo key `inspector.set` populated for this write; cleared on any failure
2004
2341
  // return below so a refused/unbacked write can't leave the field stuck on a value the
2005
2342
  // source never took.
@@ -2007,37 +2344,29 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2007
2344
  const n = this.snapshot().nodes.get(id);
2008
2345
  if (!n) {
2009
2346
  this.clearEcho(id, echoPath);
2010
- return null;
2347
+ return false;
2011
2348
  }
2012
- if (!this.writeBackend) {
2349
+ if (!backend) {
2013
2350
  this.clearEcho(id, echoPath);
2014
2351
  console.warn(
2015
2352
  `[ReactWorldAuthoringAdapter] cannot write "${prop}" on "${id}": no source-write ` +
2016
2353
  'backend in this session (hosted/no dev server) — selection/inspection still work.',
2017
2354
  );
2018
- return null;
2355
+ return false;
2019
2356
  }
2020
2357
  // Cap 2 (React visual-edit parity): if a first-party CSS RULE declares this property,
2021
2358
  // edit that CSS FILE (cascade-correct: the last matched rule wins) instead of writing
2022
2359
  // inline/class. A `generated: true` response means the selector isn't in source
2023
2360
  // (Tailwind/styled-components) — fall through to the inline/class path below.
2024
- if (this.writeBackend.writeCss) {
2361
+ if (backend.writeCss) {
2025
2362
  const cssTarget = pickCssRuleTarget(this.matchedCssRules(n.el), prop);
2026
2363
  if (cssTarget) {
2027
- const { rule, prevValue } = cssTarget;
2028
- const res = await this.writeBackend.writeCss(
2029
- rule.sourceFile,
2030
- rule.selectorText,
2031
- prop,
2032
- String(value),
2033
- );
2364
+ const { rule } = cssTarget;
2365
+ const res = await backend.writeCss(rule.sourceFile, rule.selectorText, prop, String(value));
2034
2366
  if (res.changed) {
2035
2367
  this.dirty = true;
2036
2368
  this.store.notifyIngestEdit();
2037
- return {
2038
- undo: () => void this.applyCssWrite(rule, prop, prevValue),
2039
- redo: () => void this.applyCssWrite(rule, prop, String(value)),
2040
- };
2369
+ return true;
2041
2370
  }
2042
2371
  if (!res.generated) {
2043
2372
  this.clearEcho(id, echoPath);
@@ -2045,18 +2374,12 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2045
2374
  `[ReactWorldAuthoringAdapter] CSS write refused/no-op for selector ` +
2046
2375
  `"${rule.selectorText}" prop "${prop}": ${res.error ?? 'no change'}`,
2047
2376
  );
2048
- return null;
2377
+ return false;
2049
2378
  }
2050
2379
  // res.generated ⇒ selector is generated CSS — fall through to inline/class routing.
2051
2380
  }
2052
2381
  }
2053
- // Capture the prior literal from the LIVE DOM before writing — the inverse
2054
- // write for undo (§1.E: "store the prior literal, write it back on undo") —
2055
- // unless the caller already captured the TRUE pre-gesture value itself.
2056
- const prevRaw =
2057
- priorInlineOverride !== undefined ? priorInlineOverride : styleProp(n.el.style, prop);
2058
- const prev: string | number = typeof prevRaw === 'number' ? prevRaw : String(prevRaw ?? '');
2059
- const res = await this.writeBackend.writeStyle(n.oid, prop, String(value));
2382
+ const res = await backend.writeStyle(n.oid, prop, String(value));
2060
2383
  if (!res.changed) {
2061
2384
  this.clearEcho(id, echoPath);
2062
2385
  if (res.dynamic) this.markDynamic(id, echoPath);
@@ -2070,7 +2393,7 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2070
2393
  `[ReactWorldAuthoringAdapter] style write refused/no-op for oid "${n.oid}" ` +
2071
2394
  `prop "${prop}": ${res.dynamic ? 'value is a dynamic expression (guarded)' : (res.error ?? 'no change')}`,
2072
2395
  );
2073
- return null;
2396
+ return false;
2074
2397
  }
2075
2398
  this.dirty = true;
2076
2399
  // D-A4 (wave 13, docs/WAVE13-JSX-SPREAD-UNDO-DESIGN.md): if this write took the
@@ -2080,7 +2403,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2080
2403
  // literal and REPLACE it with a hardcoded runtime value, baking a literal
2081
2404
  // into a spot the source never had one. Redo is a plain write either way (a
2082
2405
  // re-append is just a write).
2083
- const appended = res.appended === true;
2084
2406
  // B1-parity live preview for single-prop inspector writes. The `boxEdit`
2085
2407
  // gesture patches `n.el.style` live during the drag (see `boxEdit.apply`),
2086
2408
  // but a plain single-prop write (a color / any inspector field) only wrote
@@ -2099,17 +2421,11 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2099
2421
  (n.el.style as Record<string, unknown>)[prop] = String(value);
2100
2422
  }
2101
2423
  this.store.notifyIngestEdit();
2102
- return {
2103
- undo: appended
2104
- ? () => void this.applyStyleRemove(n.oid, prop)
2105
- : () => void this.applyStyleWrite(n.oid, prop, prev),
2106
- redo: () => void this.applyStyleWrite(n.oid, prop, value),
2107
- };
2424
+ return true;
2108
2425
  }
2109
2426
 
2110
2427
  private async writeStyle(id: string, prop: string, value: string | number): Promise<void> {
2111
- const entry = await this.writeStyleEntry(id, prop, value);
2112
- if (entry) this.pushUndo(entry);
2428
+ await this.writeStyleEntry(id, prop, value);
2113
2429
  }
2114
2430
 
2115
2431
  /**
@@ -2125,38 +2441,11 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2125
2441
  private async removeStyleProp(id: string, prop: string): Promise<void> {
2126
2442
  const n = this.snapshot().nodes.get(id);
2127
2443
  if (!n || !this.writeBackend) return;
2128
- const prevRaw = styleProp(n.el.style, prop);
2129
- const prev: string | number = typeof prevRaw === 'number' ? prevRaw : String(prevRaw ?? '');
2130
2444
  const res = await this.writeBackend.removeStyle(n.oid, prop);
2131
2445
  if (!res.changed) return; // longhand wasn't authored — nothing removed, no undo
2132
2446
  this.dirty = true;
2133
2447
  if (n.el.style) delete (n.el.style as Record<string, unknown>)[prop];
2134
2448
  this.store.notifyIngestEdit();
2135
- this.pushUndo({
2136
- undo: () => void this.applyStyleWrite(n.oid, prop, prev),
2137
- redo: () => void this.applyStyleRemove(n.oid, prop),
2138
- });
2139
- }
2140
-
2141
- private async applyStyleWrite(oid: string, prop: string, value: string | number): Promise<void> {
2142
- if (!this.writeBackend) return;
2143
- await this.writeBackend.writeStyle(oid, prop, String(value));
2144
- this.store.notifyIngestEdit();
2145
- }
2146
-
2147
- /** D-A4: the append-aware undo target — removes a prop that had no prior source
2148
- * literal, instead of writing a captured runtime value back into it. */
2149
- private async applyStyleRemove(oid: string, prop: string): Promise<void> {
2150
- if (!this.writeBackend) return;
2151
- await this.writeBackend.removeStyle(oid, prop);
2152
- this.store.notifyIngestEdit();
2153
- }
2154
-
2155
- /** Re-apply a CSS-file write (used by the undo/redo inverse for a Cap 2 CSS edit). */
2156
- private async applyCssWrite(rule: CssRuleTarget, prop: string, value: string): Promise<void> {
2157
- if (!this.writeBackend?.writeCss) return;
2158
- await this.writeBackend.writeCss(rule.sourceFile, rule.selectorText, prop, value);
2159
- this.store.notifyIngestEdit();
2160
2449
  }
2161
2450
 
2162
2451
  /**
@@ -2191,11 +2480,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2191
2480
  return;
2192
2481
  }
2193
2482
  this.dirty = true;
2194
- const prev = res.prevText ?? '';
2195
- this.pushUndo({
2196
- undo: () => void this.applyTextWrite(n.oid, prev),
2197
- redo: () => void this.applyTextWrite(n.oid, newText),
2198
- });
2199
2483
  // D3.R5 (reopen fix) — a successful text write flips `hasText`, itself a
2200
2484
  // `findEmptyContainers` hint-eligibility criterion (`ui-source/inspect.ts`), yet
2201
2485
  // populates no `valueEcho` (echo is only ever set by the inspector style/prop
@@ -2205,16 +2489,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2205
2489
  this.store.notifyIngestEdit();
2206
2490
  }
2207
2491
 
2208
- private async applyTextWrite(oid: string, text: string): Promise<void> {
2209
- if (!this.writeBackend?.writeText) return;
2210
- const res = await this.writeBackend.writeText(oid, text);
2211
- // D3.R5 (reopen fix) — the text-undo/redo inverse is itself a text write with the
2212
- // same `hasText` reconcile need as `editText`'s success path above; queued on
2213
- // success only — a refused/no-op write never touches the counter.
2214
- if (res.changed) this.pendingSourceReconcile++;
2215
- this.store.notifyIngestEdit();
2216
- }
2217
-
2218
2492
  /**
2219
2493
  * Cap 4 (React visual-edit parity): write a component prop at its CALL SITE (the
2220
2494
  * `<Component …>` tag), resolved from the live fiber (`getComponentProps` → callSiteOid).
@@ -2247,7 +2521,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2247
2521
  );
2248
2522
  return;
2249
2523
  }
2250
- const prev = cp.props[prop] ?? '';
2251
2524
  const res = await this.writeBackend.writeProp(cp.callSiteOid, prop, value);
2252
2525
  if (!res.changed) {
2253
2526
  this.clearEcho(id, echoPath);
@@ -2262,16 +2535,6 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2262
2535
  return;
2263
2536
  }
2264
2537
  this.dirty = true;
2265
- this.pushUndo({
2266
- undo: () => void this.applyPropWrite(cp.callSiteOid, prop, prev),
2267
- redo: () => void this.applyPropWrite(cp.callSiteOid, prop, value),
2268
- });
2269
- this.store.notifyIngestEdit();
2270
- }
2271
-
2272
- private async applyPropWrite(callSiteOid: string, prop: string, value: string): Promise<void> {
2273
- if (!this.writeBackend?.writeProp) return;
2274
- await this.writeBackend.writeProp(callSiteOid, prop, value);
2275
2538
  this.store.notifyIngestEdit();
2276
2539
  }
2277
2540
 
@@ -2293,11 +2556,8 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2293
2556
  return;
2294
2557
  }
2295
2558
  this.dirty = true;
2296
- // D-1 (Phase D, spec27 §2): delete IS undoable now, via the SAME checksum-guarded
2297
- // whole-file-snapshot inverse `structOp` uses (`pushStructUndo`) — it sidesteps
2298
- // "no element-insertion primitive" entirely by never re-inserting the deleted
2299
- // element from an OID; it replays the whole file's PRIOR text instead.
2300
- this.pushStructUndo('delete', res);
2559
+ // Whole-file source history restores deletion without fabricating an inverse
2560
+ // element insertion from an OID that no longer exists.
2301
2561
  // D3.R4 (reopen fix) — same pre-HMR-notify timing gap `structOp` has (see
2302
2562
  // `pendingSourceReconcile`'s doc comment); `delete` is one of the two triggering
2303
2563
  // scenarios the reviewer's contrast probe reproduced.
@@ -2343,36 +2603,11 @@ export class ReactWorldAuthoringAdapter implements AuthoringAdapter {
2343
2603
  return;
2344
2604
  }
2345
2605
  this.dirty = true;
2346
- // D-1 (Phase D, spec27 §2): same checksum-guarded whole-file-snapshot inverse
2347
- // `removeElement`/`structOp` use (`pushStructUndo`) — ONE entry for the whole
2348
- // batch (a bonus of batching, not the point of this fix).
2349
- this.pushStructUndo('delete-many', res);
2606
+ // One prepared batch source write becomes one history transaction.
2350
2607
  this.pendingSourceReconcile++;
2351
2608
  this.store.notifyIngestEdit();
2352
2609
  }
2353
2610
 
2354
- private pushUndo(entry: PendingUndo): void {
2355
- this.undoStack.push(entry);
2356
- this.redoStack.length = 0;
2357
- notifyUndoableOp(this); // T3.2 slice 2 — tag this op on the global timeline
2358
- }
2359
-
2360
- undo(): boolean {
2361
- const e = this.undoStack.pop();
2362
- if (!e) return false;
2363
- e.undo();
2364
- this.redoStack.push(e);
2365
- return true;
2366
- }
2367
-
2368
- redo(): boolean {
2369
- const e = this.redoStack.pop();
2370
- if (!e) return false;
2371
- e.redo();
2372
- this.undoStack.push(e);
2373
- return true;
2374
- }
2375
-
2376
2611
  subscribe(listener: () => void): () => void {
2377
2612
  return this.store.subscribe(listener);
2378
2613
  }