@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.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- 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.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 {
|
|
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: [
|
|
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: [
|
|
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
|
-
*
|
|
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
|
|
527
|
-
*
|
|
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 `
|
|
595
|
-
*
|
|
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:
|
|
691
|
-
*
|
|
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
|
-
|
|
1056
|
+
disposeReactRootAdapter(): void {
|
|
893
1057
|
this.disposeReloadSignal?.();
|
|
1058
|
+
this.storyHistory?.dispose();
|
|
894
1059
|
}
|
|
895
1060
|
|
|
896
|
-
private
|
|
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
|
-
|
|
902
|
-
|
|
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
|
|
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: '
|
|
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
|
|
958
|
-
|
|
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
|
|
973
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1705
|
-
//
|
|
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
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
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
|
-
|
|
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
|
|
2347
|
+
return false;
|
|
2011
2348
|
}
|
|
2012
|
-
if (!
|
|
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
|
|
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 (
|
|
2361
|
+
if (backend.writeCss) {
|
|
2025
2362
|
const cssTarget = pickCssRuleTarget(this.matchedCssRules(n.el), prop);
|
|
2026
2363
|
if (cssTarget) {
|
|
2027
|
-
const { rule
|
|
2028
|
-
const res = await
|
|
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
|
|
2377
|
+
return false;
|
|
2049
2378
|
}
|
|
2050
2379
|
// res.generated ⇒ selector is generated CSS — fall through to inline/class routing.
|
|
2051
2380
|
}
|
|
2052
2381
|
}
|
|
2053
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
2297
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|