@vgai/editor 0.2.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/LICENSE +661 -0
- package/README.md +35 -0
- package/dist/a/b/c/test-nested.json +1 -0
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
- package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
- package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
- package/dist/assets/browserAll-CEkWYlFk.js +1 -0
- package/dist/assets/collision-world-DniMB0qq.glb +0 -0
- package/dist/assets/create-runtime-DYIWr3va.js +1 -0
- package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
- package/dist/assets/esbuild-BHljloGq.wasm +0 -0
- package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
- package/dist/assets/game-8SDZPPgH.js +1 -0
- package/dist/assets/game-BI111o5f.js +10 -0
- package/dist/assets/game-CevorxyV.js +1 -0
- package/dist/assets/game-Di1H0gFj.js +1 -0
- package/dist/assets/game-Di7XlywD.js +1 -0
- package/dist/assets/game-DjnCaw8R.js +1 -0
- package/dist/assets/game-Q9A0aXSV.js +1 -0
- package/dist/assets/game-pt_1lQLv.js +1 -0
- package/dist/assets/game-source-CvidN7AB.js +1 -0
- package/dist/assets/game-xsOho_wg.js +83 -0
- package/dist/assets/index-Bp5cecD6.js +12 -0
- package/dist/assets/index-CPVV6pjw.js +1 -0
- package/dist/assets/index-CpOnYyeD.js +213 -0
- package/dist/assets/index-Cs8xwv3e.js +1 -0
- package/dist/assets/index-DL7RcQ0n.js +27 -0
- package/dist/assets/index-Dr8H_6Rl.js +1 -0
- package/dist/assets/index-_bCaipUk.js +13650 -0
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
- package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
- package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
- package/dist/assets/scene-query-BivAHhJu.js +1 -0
- package/dist/assets/stats.module--VATS4Kh.js +1 -0
- package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
- package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
- package/dist/index.html +37 -0
- package/dist/ingest/racing-game/UPSTREAM.md +68 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/index.css +1 -0
- package/dist/ingest/racing-game/images/bronze.png +0 -0
- package/dist/ingest/racing-game/images/gold.png +0 -0
- package/dist/ingest/racing-game/images/silver.png +0 -0
- package/dist/ingest/racing-game/index.html +15 -0
- package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
- package/dist/ingest/racing-game/models/track-draco.glb +0 -0
- package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
- package/dist/ingest/racing-game/rg-entry.js +1 -0
- package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
- package/dist/ingest/racing-game/textures/cursor.svg +3 -0
- package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
- package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
- package/dist/ingest/racing-game/textures/mask.svg +11 -0
- package/dist/ingest/racing-game/textures/no_sound.png +0 -0
- package/dist/ingest/racing-game/three-r139.module.js +50617 -0
- package/dist/ingest/racing-game/vendor.js +179 -0
- package/dist/ingest/racing-game/vgai.game.json +20 -0
- package/dist/ingest/simcity/UPSTREAM.md +65 -0
- package/dist/ingest/simcity/favicon.ico +0 -0
- package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
- package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
- package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
- package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
- package/dist/ingest/simcity/icons/bulldozer.png +0 -0
- package/dist/ingest/simcity/icons/calendar.png +0 -0
- package/dist/ingest/simcity/icons/factory-color.png +0 -0
- package/dist/ingest/simcity/icons/factory.png +0 -0
- package/dist/ingest/simcity/icons/house-color.png +0 -0
- package/dist/ingest/simcity/icons/house.png +0 -0
- package/dist/ingest/simcity/icons/job.png +0 -0
- package/dist/ingest/simcity/icons/office.png +0 -0
- package/dist/ingest/simcity/icons/pause-color.png +0 -0
- package/dist/ingest/simcity/icons/pause.png +0 -0
- package/dist/ingest/simcity/icons/person.png +0 -0
- package/dist/ingest/simcity/icons/play-color.png +0 -0
- package/dist/ingest/simcity/icons/play.png +0 -0
- package/dist/ingest/simcity/icons/power-color.png +0 -0
- package/dist/ingest/simcity/icons/power-line-color.png +0 -0
- package/dist/ingest/simcity/icons/power.png +0 -0
- package/dist/ingest/simcity/icons/road-color.png +0 -0
- package/dist/ingest/simcity/icons/road.png +0 -0
- package/dist/ingest/simcity/icons/select-color.png +0 -0
- package/dist/ingest/simcity/icons/select.png +0 -0
- package/dist/ingest/simcity/icons/store-color.png +0 -0
- package/dist/ingest/simcity/index.html +94 -0
- package/dist/ingest/simcity/index.js +58 -0
- package/dist/ingest/simcity/main.css +233 -0
- package/dist/ingest/simcity/models/armored-truck.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
- package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
- package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
- package/dist/ingest/simcity/models/building-cafe.glb +0 -0
- package/dist/ingest/simcity/models/building-casino.glb +0 -0
- package/dist/ingest/simcity/models/building-cinema.glb +0 -0
- package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
- package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
- package/dist/ingest/simcity/models/building-office-big.glb +0 -0
- package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
- package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
- package/dist/ingest/simcity/models/building-office.glb +0 -0
- package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
- package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
- package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
- package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
- package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger.glb +0 -0
- package/dist/ingest/simcity/models/car-police.glb +0 -0
- package/dist/ingest/simcity/models/car-taxi.glb +0 -0
- package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
- package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
- package/dist/ingest/simcity/models/car-veteran.glb +0 -0
- package/dist/ingest/simcity/models/construction-small.glb +0 -0
- package/dist/ingest/simcity/models/data-center.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory.glb +0 -0
- package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
- package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
- package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
- package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
- package/dist/ingest/simcity/models/truck.glb +0 -0
- package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
- package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
- package/dist/ingest/simcity/textures/base.png +0 -0
- package/dist/ingest/simcity/textures/grid.png +0 -0
- package/dist/ingest/simcity/textures/specular.png +0 -0
- package/dist/ingest/simcity/three-r155.module.js +51466 -0
- package/dist/ingest/simcity/vgai.game.json +20 -0
- package/dist/ingest/tanks/UPSTREAM.md +69 -0
- package/dist/ingest/tanks/index.html +214 -0
- package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
- package/dist/ingest/tanks/tanks-entry.js +92 -0
- package/dist/ingest/tanks/three-r170.module.js +54571 -0
- package/dist/ingest/tanks/vgai.game.json +20 -0
- package/dist/inputmaps/default.inputmap.json +41 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/dist/scenes/untitled.vscn.json +29 -0
- package/dist/textures/waternormals.jpg +0 -0
- package/dist-server/packaged.mjs +18155 -0
- package/package.json +36 -0
- package/server/asset-library-routes.ts +833 -0
- package/server/canonical-path.ts +50 -0
- package/server/cloud-asset-catalog.ts +101 -0
- package/server/dev.ts +350 -0
- package/server/editor-server.ts +1537 -0
- package/server/editor-sse.ts +73 -0
- package/server/game-globals-shadow.ts +58 -0
- package/server/local-asset-catalog.ts +163 -0
- package/server/packaged.ts +571 -0
- package/server/prod.ts +75 -0
- package/server/project-optimize-deps-entries.ts +149 -0
- package/server/project-view.ts +65 -0
- package/server/server-utils.ts +430 -0
- package/server/session-registry.ts +93 -0
- package/src/EditorContext.tsx +87 -0
- package/src/achieved-tier.ts +126 -0
- package/src/action-registry.ts +268 -0
- package/src/adapter-resolver.ts +1858 -0
- package/src/asset-events.ts +100 -0
- package/src/authoring/active-adapter.ts +98 -0
- package/src/authoring/active-systems.ts +50 -0
- package/src/authoring/boundary-authoring-adapter.ts +178 -0
- package/src/authoring/composite-authoring-adapter.ts +891 -0
- package/src/authoring/design-time-layers.ts +776 -0
- package/src/authoring/edit-mode-authoring.ts +643 -0
- package/src/authoring/embed-only-adapter.ts +57 -0
- package/src/authoring/ephemeral-persistence.ts +26 -0
- package/src/authoring/eyedropper-session.ts +69 -0
- package/src/authoring/first-party-hierarchy-menu.ts +74 -0
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
- package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
- package/src/authoring/first-party-inspector/register.tsx +108 -0
- package/src/authoring/ingest-authoring-adapter.ts +475 -0
- package/src/authoring/ingest-capture.ts +94 -0
- package/src/authoring/ingest-game-adapter.ts +291 -0
- package/src/authoring/ingest-iframe-adapter.ts +38 -0
- package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
- package/src/authoring/ingest-overlay.ts +193 -0
- package/src/authoring/layered-pick.ts +87 -0
- package/src/authoring/mount-failure-report.ts +112 -0
- package/src/authoring/overlay-report.ts +52 -0
- package/src/authoring/project-scenes.ts +111 -0
- package/src/authoring/react-dom-authoring-adapter.ts +795 -0
- package/src/authoring/react-inspector-section.tsx +1041 -0
- package/src/authoring/react-world-authoring-adapter.ts +2400 -0
- package/src/authoring/root-activation.ts +61 -0
- package/src/authoring/scene-ui-layer.ts +485 -0
- package/src/authoring/story-overlay.ts +48 -0
- package/src/authoring/ui-authoring-adapter.ts +325 -0
- package/src/authoring/ui-hierarchy-menu.ts +40 -0
- package/src/authoring/ui-inspector-sections.tsx +156 -0
- package/src/authoring/undo-timeline.ts +145 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
- package/src/authoring/viewport-pick-context.ts +31 -0
- package/src/authoring/viewport-raycast.ts +80 -0
- package/src/authoring/world-overlay.ts +76 -0
- package/src/authoring/world-pan-state.ts +115 -0
- package/src/authoring/world-session-state.ts +102 -0
- package/src/authoring/world2d-authoring-adapter.ts +313 -0
- package/src/browser-external-watch.ts +98 -0
- package/src/browser-play-entry.ts +112 -0
- package/src/browser-project-scripts.ts +112 -0
- package/src/browser-transpile.ts +246 -0
- package/src/browser-watch-reload.ts +100 -0
- package/src/command-listener.ts +326 -0
- package/src/components/AlignToolbar.tsx +189 -0
- package/src/components/AppRoot.tsx +73 -0
- package/src/components/AssetBrowser.tsx +502 -0
- package/src/components/BottomPanel.tsx +326 -0
- package/src/components/BuildPanel.tsx +455 -0
- package/src/components/CameraInfo.tsx +56 -0
- package/src/components/CommandPalette.tsx +265 -0
- package/src/components/ConsolePanel.tsx +272 -0
- package/src/components/DataPanel.tsx +1268 -0
- package/src/components/DefaultEditorLayout.tsx +509 -0
- package/src/components/FolderTree.tsx +179 -0
- package/src/components/GameHierarchy.tsx +1349 -0
- package/src/components/Inspector.tsx +671 -0
- package/src/components/InspectorToolSection.tsx +74 -0
- package/src/components/MountFailureBanner.tsx +110 -0
- package/src/components/NetworkSection.tsx +109 -0
- package/src/components/OnlineAssetBrowser.tsx +448 -0
- package/src/components/OverlayReportBanner.tsx +122 -0
- package/src/components/PlayBar.tsx +356 -0
- package/src/components/PrefabSaveDialog.tsx +201 -0
- package/src/components/ProjectHeader.tsx +174 -0
- package/src/components/ProjectScreen.tsx +1004 -0
- package/src/components/RightPanel.tsx +166 -0
- package/src/components/RootActivateDialog.tsx +105 -0
- package/src/components/SaveDialog.tsx +197 -0
- package/src/components/SaveStatus.tsx +69 -0
- package/src/components/StatsOverlay.tsx +35 -0
- package/src/components/ToolHost.tsx +80 -0
- package/src/components/Toolbar.tsx +307 -0
- package/src/components/ViewportOverlay.tsx +234 -0
- package/src/components/ViewportPanel.tsx +369 -0
- package/src/components/WorldContextMenu.tsx +257 -0
- package/src/components/WorldSelectionOverlay.tsx +2003 -0
- package/src/components/WorldTextEditor.tsx +86 -0
- package/src/components/asset-viewers/AssetViewer.tsx +38 -0
- package/src/components/asset-viewers/AudioViewer.tsx +149 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
- package/src/components/asset-viewers/ImageViewer.tsx +65 -0
- package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
- package/src/components/asset-viewers/ModelViewer.tsx +180 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
- package/src/components/data-csv.ts +301 -0
- package/src/components/entity-name-commit.ts +18 -0
- package/src/components/inspector-property-grouping.ts +65 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
- package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
- package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
- package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/components/inspector-widgets/FontPicker.tsx +196 -0
- package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
- package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
- package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
- package/src/components/inspector-widgets/color-utils.ts +202 -0
- package/src/components/inspector-widgets/gallery-main.tsx +167 -0
- package/src/components/inspector-widgets/index.ts +22 -0
- package/src/components/inspector-widgets/shared.tsx +135 -0
- package/src/components/inspectors/AnimationSection.tsx +143 -0
- package/src/components/inspectors/AudioSection.tsx +215 -0
- package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
- package/src/components/inspectors/CameraSection.tsx +105 -0
- package/src/components/inspectors/ComponentsSection.tsx +358 -0
- package/src/components/inspectors/JointsSection.tsx +255 -0
- package/src/components/inspectors/LightSection.tsx +116 -0
- package/src/components/inspectors/MaterialSection.tsx +1189 -0
- package/src/components/inspectors/MeshSection.tsx +99 -0
- package/src/components/inspectors/NavigationSection.tsx +58 -0
- package/src/components/inspectors/ParticleSection.tsx +681 -0
- package/src/components/inspectors/PhysicsSection.tsx +326 -0
- package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
- package/src/components/inspectors/RenderingSection.tsx +109 -0
- package/src/components/inspectors/ShadowSection.tsx +84 -0
- package/src/components/inspectors/SplineSection.tsx +191 -0
- package/src/components/inspectors/TransformSection.tsx +191 -0
- package/src/components/inspectors/json-schema-fields.ts +281 -0
- package/src/components/inspectors/multi-edit-utils.ts +49 -0
- package/src/components/inspectors/zod-fields.ts +184 -0
- package/src/components/primitives/ColorInput.tsx +109 -0
- package/src/components/primitives/JsonInput.tsx +98 -0
- package/src/components/primitives/NumberInput.tsx +164 -0
- package/src/components/primitives/Panel.tsx +68 -0
- package/src/components/primitives/SectionHeader.tsx +45 -0
- package/src/components/primitives/Tooltip.tsx +82 -0
- package/src/components/primitives/Vec3Input.tsx +62 -0
- package/src/components/world-overlay-gestures.ts +1438 -0
- package/src/doctor/data-checks.ts +285 -0
- package/src/doctor/report.ts +386 -0
- package/src/editor-api.ts +700 -0
- package/src/editor-console.ts +194 -0
- package/src/editor-hotkeys.ts +441 -0
- package/src/editor-layers.ts +2 -0
- package/src/editor-mode.ts +28 -0
- package/src/editor-store.ts +2225 -0
- package/src/editor-viewport.ts +1924 -0
- package/src/entity-factory.ts +221 -0
- package/src/game-globals-prelude.ts +27 -0
- package/src/gated-globals.ts +131 -0
- package/src/generated/setup-by-example.generated.ts +53 -0
- package/src/gizmo-registry.ts +608 -0
- package/src/handle-system.ts +221 -0
- package/src/hierarchy-menu-registry.ts +70 -0
- package/src/hierarchy-rows.ts +73 -0
- package/src/hosted-example.ts +160 -0
- package/src/hotkeys.ts +142 -0
- package/src/ingest/discovery-react.ts +132 -0
- package/src/ingest/discovery.ts +205 -0
- package/src/ingest/discovery2d.ts +204 -0
- package/src/ingest/esm-entry-resolution.ts +102 -0
- package/src/ingest/games/bloom-composer/game.js +99 -0
- package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
- package/src/ingest/games/embed-bundle/embed.html +10 -0
- package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
- package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
- package/src/ingest/games/games-fps/game.js +479 -0
- package/src/ingest/games/games-fps/vgai.game.json +24 -0
- package/src/ingest/games/iframe-reachable/game-source.js +58 -0
- package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
- package/src/ingest/games/interactive-cubes/game.js +139 -0
- package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
- package/src/ingest/games/mismatched-three/fake-three.js +38 -0
- package/src/ingest/games/mismatched-three/game.js +10 -0
- package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
- package/src/ingest/games/own-three-bundled/game.js +43 -0
- package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
- package/src/ingest/games/pointerlock-maze/game.js +288 -0
- package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
- package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
- package/src/ingest/games-2d/bunnymark/game.ts +88 -0
- package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy/game.ts +148 -0
- package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
- package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
- package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
- package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
- package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
- package/src/ingest/host-namespace-registry.ts +196 -0
- package/src/ingest/types.ts +127 -0
- package/src/ingest-hierarchy-rows.ts +207 -0
- package/src/ingest-mode.ts +2092 -0
- package/src/ingest-siblings.ts +466 -0
- package/src/initial-scene.ts +194 -0
- package/src/inspector-section-registry.ts +117 -0
- package/src/interactive-edit-scope.ts +34 -0
- package/src/main.tsx +23 -0
- package/src/manifest-project.ts +43 -0
- package/src/model-thumbnail.ts +156 -0
- package/src/module-mode.ts +513 -0
- package/src/navmesh-handler.ts +94 -0
- package/src/packaged-runtime.ts +49 -0
- package/src/play-mode.ts +1148 -0
- package/src/playground/main.ts +206 -0
- package/src/project-manager.ts +173 -0
- package/src/scene-diff.ts +112 -0
- package/src/scene-serializer.ts +144 -0
- package/src/scene-sync.ts +587 -0
- package/src/scene-ui-mode/active-scene-ui.ts +58 -0
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
- package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
- package/src/script-loader.ts +143 -0
- package/src/storage/fsa-storage.ts +55 -0
- package/src/storage/handle-storage.ts +150 -0
- package/src/storage/handle-store.ts +116 -0
- package/src/storage/http-storage.ts +137 -0
- package/src/storage/index.ts +147 -0
- package/src/storage/mem-storage.ts +135 -0
- package/src/storage/opfs-storage.ts +25 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/seed.ts +106 -0
- package/src/storage/types.ts +110 -0
- package/src/tool-loader.ts +291 -0
- package/src/ui-editor/inspector.tsx +563 -0
- package/src/ui-editor/node-ops.ts +266 -0
- package/src/ui-editor/overlay.tsx +380 -0
- package/src/ui-editor/palette.tsx +114 -0
- package/src/ui-editor/react-store.ts +78 -0
- package/src/ui-editor/timeline-panel.tsx +443 -0
- package/src/ui-editor/ui-edit-store.ts +552 -0
- package/src/ui-editor/ui-prefab.ts +95 -0
- package/src/ui-source/inspect.ts +544 -0
- package/src/ui-source/oid-transform.ts +133 -0
- package/src/ui-source/source-write-backend.ts +203 -0
- package/src/ui-source/tailwind-map.ts +174 -0
- package/src/ui-source/writer.ts +1175 -0
- package/src/widgets/index.ts +73 -0
- package/template/.claude/skills/editor/SKILL.md +252 -0
- package/template/CLAUDE.md +512 -0
- package/template/LICENSE +30 -0
- package/template/emit-schemas.ts +42 -0
- package/template/index.html +17 -0
- package/template/package.json +53 -0
- package/template/project.json +9 -0
- package/template/public/audio/ambient-arena.wav +0 -0
- package/template/public/audio/ambient-music.wav +0 -0
- package/template/public/audio/ambient-nature.wav +0 -0
- package/template/public/audio/ambient-wind.wav +0 -0
- package/template/public/inputmaps/default.inputmap.json +41 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/template/public/scenes/default.vscn.json +43 -0
- package/template/public/scenes/untitled.vscn.json +29 -0
- package/template/public/textures/waternormals.jpg +0 -0
- package/template/server/colyseus-setup.ts +85 -0
- package/template/server/main.ts +34 -0
- package/template/server/rooms/arena-room.ts +420 -0
- package/template/server/rooms/game-room.ts +162 -0
- package/template/server/rooms.ts +15 -0
- package/template/server/tsconfig.json +19 -0
- package/template/src/data/README.md +40 -0
- package/template/src/data/assets.ts +31 -0
- package/template/src/data/tuning.data.json +5 -0
- package/template/src/data/tuning.schema.json +21 -0
- package/template/src/data/tuning.schema.ts +34 -0
- package/template/src/data/tuning.ts +24 -0
- package/template/src/main.ts +157 -0
- package/template/src/scripts/components/data-spinner.ts +45 -0
- package/template/src/scripts/components/scene-camera.ts +40 -0
- package/template/src/scripts/main.ts +94 -0
- package/template/src/scripts/registry.ts +46 -0
- package/template/src/tools/README.md +14 -0
- package/template/src/tools/example.tool.tsx +91 -0
- package/template/src/tools/spin.tool.tsx +101 -0
- package/template/src/ui/Button.tsx +27 -0
- package/template/src/ui/DefaultHud.tsx +40 -0
- package/template/src/ui/KeyHint.tsx +25 -0
- package/template/src/ui/Panel.tsx +52 -0
- package/template/src/ui/RebindPanel.tsx +129 -0
- package/template/src/ui/Slider.tsx +28 -0
- package/template/src/ui/StatusDot.tsx +22 -0
- package/template/src/ui/Toggle.tsx +19 -0
- package/template/src/ui/game-state.tsx +37 -0
- package/template/src/ui/index.ts +7 -0
- package/template/src/ui/mount-default-hud.tsx +19 -0
- package/template/src/ui/mount-ui.ts +15 -0
- package/template/tsconfig.json +36 -0
- package/template/validate-asset-content.ts +78 -0
- package/template/validate-manifest.ts +55 -0
- package/template/validate-scenes.ts +211 -0
- package/template/vgai.game.json +21 -0
- package/template/vite.config.ts +128 -0
- package/vite-plugin-example-assets.ts +60 -0
- package/vite-plugin-game-globals.ts +46 -0
- package/vite-plugin-game-static.ts +71 -0
- package/vite-plugin-project-game-static.ts +84 -0
- package/vite-plugin-react-game-static.ts +98 -0
- package/vite-plugin-react-world-provider.ts +81 -0
- package/vite-plugin-react-world-runtime.ts +76 -0
- package/vite-plugin-ui-oid.ts +561 -0
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-world inspector section (C2, spec 27 §5 C2,
|
|
3
|
+
* docs/unified-world-editor/27-visual-react-editing.md) — the "wire the
|
|
4
|
+
* widgets" half of Phase C. C1 (`components/inspector-widgets/`) built a
|
|
5
|
+
* PURE widget kit with no wiring; this module is the adapter-module
|
|
6
|
+
* contribution that plugs it into the ONE inspector shell
|
|
7
|
+
* (`components/Inspector.tsx`) via `inspector-section-registry.ts`, the SAME
|
|
8
|
+
* seam `authoring/first-party-inspector/register.tsx` (threejs) and
|
|
9
|
+
* `authoring/ui-inspector-sections.tsx` (scene-UI) already use — see those
|
|
10
|
+
* two files for the sanctioned pattern this one follows.
|
|
11
|
+
*
|
|
12
|
+
* Matches any node owned by a react/DOM adapter — `ReactWorldAuthoringAdapter`
|
|
13
|
+
* (Lane B, `data-oid` component-source writes, T6.2) or
|
|
14
|
+
* `ReactDomAuthoringAdapter` (the ingested/overlay DOM adapter, wave 10) —
|
|
15
|
+
* whether the active adapter IS one of those directly (a bare single-world
|
|
16
|
+
* session) or is a `CompositeAuthoringAdapter` routing to one as `id`'s
|
|
17
|
+
* owner (multi-world edit mode). Renders every `PropertyDescriptor` the
|
|
18
|
+
* adapter's `InspectorProvider.properties(id)` reports, GROUPED by
|
|
19
|
+
* `PropertyDescriptor.group` (falling back to one flat "Style" group for an
|
|
20
|
+
* adapter, like `ReactDomAuthoringAdapter`, whose descriptors declare none),
|
|
21
|
+
* mapping each descriptor to the right C1 widget instead of a generic
|
|
22
|
+
* `<input>` — see `widgetKindFor`'s doc comment for the exact mapping table.
|
|
23
|
+
* `suppresses: ['properties']` replaces the shell's generic property grid
|
|
24
|
+
* for a matched node (Rule zero: this section still talks to the node ONLY
|
|
25
|
+
* through `adapter.inspector`/`adapter.hierarchy`, never the DOM/fiber the
|
|
26
|
+
* adapter itself holds).
|
|
27
|
+
*
|
|
28
|
+
* `onChange` (every interaction tick) and `onChangeEnd` (the commit) both
|
|
29
|
+
* route through the SAME `adapter.inspector.set` call — unlike
|
|
30
|
+
* `BoxEditProvider`'s begin/apply/end split (a spatial-gesture concern,
|
|
31
|
+
* Phase B), `ReactWorldAuthoringAdapter.inspector.set` has exactly ONE write
|
|
32
|
+
* path (T0 §2/A4): it echoes the value into its optimistic cache
|
|
33
|
+
* synchronously (so the field never snaps back) and fires the async
|
|
34
|
+
* source-write itself. There is no separate "preview without writing" mode
|
|
35
|
+
* to route `onChange` through, so both handlers call `commit`, mirroring the
|
|
36
|
+
* shell's own pre-existing `setProp` helper (`components/Inspector.tsx`)
|
|
37
|
+
* which already does the same on every keystroke of the generic grid this
|
|
38
|
+
* section replaces.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type { AuthoringAdapter, EditorNode, PropertyDescriptor } from '@engine/adapter';
|
|
42
|
+
import { useMemo } from 'react';
|
|
43
|
+
import { groupProperties } from '../components/inspector-property-grouping';
|
|
44
|
+
import {
|
|
45
|
+
AlignmentGrid,
|
|
46
|
+
type AlignmentValue,
|
|
47
|
+
BorderEditor,
|
|
48
|
+
BorderRadiusEditor,
|
|
49
|
+
type BorderRadiusValue,
|
|
50
|
+
type BorderSide,
|
|
51
|
+
type BorderValue,
|
|
52
|
+
ColorSwatch,
|
|
53
|
+
composeFilter,
|
|
54
|
+
composeGradient,
|
|
55
|
+
composeShadows,
|
|
56
|
+
FilterEditor,
|
|
57
|
+
type FilterFunc,
|
|
58
|
+
FONT_MONO,
|
|
59
|
+
FontPicker,
|
|
60
|
+
GradientEditor,
|
|
61
|
+
type GradientValue,
|
|
62
|
+
inputStyle,
|
|
63
|
+
isUniformBorder,
|
|
64
|
+
isUniformRadius,
|
|
65
|
+
parseFilterFunctions,
|
|
66
|
+
parseGradient,
|
|
67
|
+
parseShadows,
|
|
68
|
+
rowStyle,
|
|
69
|
+
ScrubbableInput,
|
|
70
|
+
ShadowEditor,
|
|
71
|
+
TextShadowEditor,
|
|
72
|
+
THEME,
|
|
73
|
+
} from '../components/inspector-widgets';
|
|
74
|
+
import { useEditorStore } from '../EditorContext';
|
|
75
|
+
import {
|
|
76
|
+
type InspectorSectionProps,
|
|
77
|
+
registerInspectorSections,
|
|
78
|
+
} from '../inspector-section-registry';
|
|
79
|
+
import { effectiveColorFromChain, getAvailableFonts } from '../ui-source/inspect';
|
|
80
|
+
import { CompositeAuthoringAdapter } from './composite-authoring-adapter';
|
|
81
|
+
import { beginEyedropperSession } from './eyedropper-session';
|
|
82
|
+
import { ReactDomAuthoringAdapter } from './react-dom-authoring-adapter';
|
|
83
|
+
import {
|
|
84
|
+
cssColorToHex,
|
|
85
|
+
numericStyleValue,
|
|
86
|
+
ReactWorldAuthoringAdapter,
|
|
87
|
+
} from './react-world-authoring-adapter';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The child adapter that actually owns `node.id` — a bare react/DOM adapter
|
|
91
|
+
* itself, or (via a `CompositeAuthoringAdapter`) whichever child's
|
|
92
|
+
* `ownerOf`/`childAdapters()` claims the id (the SAME public T0 query
|
|
93
|
+
* `components/Inspector.tsx`'s `worldGroupChildAdapter`/`catalogNodeOwnerAdapter`
|
|
94
|
+
* use — never decode a reserved id here). `null` for anything not owned by a
|
|
95
|
+
* react/DOM adapter (a threejs/scene-UI node, or an unresolved id).
|
|
96
|
+
*/
|
|
97
|
+
export function resolveReactOwner(
|
|
98
|
+
node: EditorNode | null,
|
|
99
|
+
adapter: AuthoringAdapter,
|
|
100
|
+
): ReactWorldAuthoringAdapter | ReactDomAuthoringAdapter | null {
|
|
101
|
+
if (!node) return null;
|
|
102
|
+
const target: AuthoringAdapter | null = (() => {
|
|
103
|
+
if (!(adapter instanceof CompositeAuthoringAdapter)) return adapter;
|
|
104
|
+
const owner = adapter.ownerOf(node.id);
|
|
105
|
+
if (owner === null) return null;
|
|
106
|
+
return adapter.childAdapters().find((c) => c.worldId === owner)?.adapter ?? null;
|
|
107
|
+
})();
|
|
108
|
+
if (target instanceof ReactWorldAuthoringAdapter) return target;
|
|
109
|
+
if (target instanceof ReactDomAuthoringAdapter) return target;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The registry `match`: any real selection owned by a react/DOM adapter. */
|
|
114
|
+
export const isReactDomSelection = (node: EditorNode | null, adapter: AuthoringAdapter): boolean =>
|
|
115
|
+
resolveReactOwner(node, adapter) !== null;
|
|
116
|
+
|
|
117
|
+
// --- widget mapping (the C2 "map PropertyDescriptor.type/group -> widget" table) ---
|
|
118
|
+
|
|
119
|
+
export type ReactPropertyWidgetKind =
|
|
120
|
+
| 'color'
|
|
121
|
+
| 'font'
|
|
122
|
+
| 'gradient'
|
|
123
|
+
| 'shadow'
|
|
124
|
+
| 'text-shadow'
|
|
125
|
+
| 'filter'
|
|
126
|
+
| 'backdrop-filter'
|
|
127
|
+
| 'radius'
|
|
128
|
+
| 'transform'
|
|
129
|
+
| 'scrub'
|
|
130
|
+
| 'select'
|
|
131
|
+
| 'checkbox'
|
|
132
|
+
| 'json'
|
|
133
|
+
| 'text';
|
|
134
|
+
|
|
135
|
+
function propNameFromPath(path: string): string {
|
|
136
|
+
const idx = path.indexOf('.');
|
|
137
|
+
return idx === -1 ? path : path.slice(idx + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A single `PropertyDescriptor` -> the C1 widget that renders it. Structured
|
|
142
|
+
* CSS values (gradient/shadow/filter/radius) are matched by their well-known
|
|
143
|
+
* style-prop NAME (the react adapters' `STYLE_PROPERTIES` tables are the
|
|
144
|
+
* only source of these paths today — `PropertyDescriptor.type` alone can't
|
|
145
|
+
* distinguish a plain string from a composable CSS function list); every
|
|
146
|
+
* other descriptor maps off its plain `type`.
|
|
147
|
+
*/
|
|
148
|
+
export function widgetKindFor(prop: PropertyDescriptor): ReactPropertyWidgetKind {
|
|
149
|
+
const name = propNameFromPath(prop.path);
|
|
150
|
+
if (prop.type === 'color') return 'color';
|
|
151
|
+
if (/^fontFamily$/i.test(name)) return 'font';
|
|
152
|
+
if (name === 'background' || name === 'backgroundImage') return 'gradient';
|
|
153
|
+
if (name === 'boxShadow') return 'shadow';
|
|
154
|
+
if (name === 'textShadow') return 'text-shadow';
|
|
155
|
+
if (name === 'filter') return 'filter';
|
|
156
|
+
if (name === 'backdropFilter') return 'backdrop-filter';
|
|
157
|
+
if (name === 'borderRadius') return 'radius';
|
|
158
|
+
// Scoped to `type: 'string'` (the react-world `style.transform` CSS-string
|
|
159
|
+
// descriptor) so an unrelated `transform` name of a DIFFERENT type — e.g. a
|
|
160
|
+
// threejs `vec3` position/transform prop — still falls through to its own
|
|
161
|
+
// generic widget instead of being misread as a CSS transform string.
|
|
162
|
+
if (prop.type === 'string' && name === 'transform') return 'transform';
|
|
163
|
+
if (prop.type === 'boolean') return 'checkbox';
|
|
164
|
+
if (prop.type === 'enum') return 'select';
|
|
165
|
+
if (prop.type === 'json') return 'json';
|
|
166
|
+
if (prop.type === 'number') return 'scrub';
|
|
167
|
+
// 'string' / 'vec3' / 'asset' — no dedicated widget in this task's mapping;
|
|
168
|
+
// falls through to a plain text input (see the report's "no widget" list).
|
|
169
|
+
return 'text';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A composed row: either one property, or a "combo" of several sibling
|
|
173
|
+
* properties this section renders as ONE richer widget (per-side border,
|
|
174
|
+
* 3x3 flex alignment) instead of N separate fields. */
|
|
175
|
+
export type PropertyRow =
|
|
176
|
+
| {
|
|
177
|
+
kind: 'border';
|
|
178
|
+
width: PropertyDescriptor;
|
|
179
|
+
style: PropertyDescriptor;
|
|
180
|
+
color: PropertyDescriptor;
|
|
181
|
+
}
|
|
182
|
+
| { kind: 'align'; justify: PropertyDescriptor; align: PropertyDescriptor }
|
|
183
|
+
| { kind: 'single'; prop: PropertyDescriptor };
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Groups the react-world adapter's separate `borderWidth`/`borderStyle`/
|
|
187
|
+
* `borderColor` scalars into one `BorderEditor` row, and `justifyContent`/
|
|
188
|
+
* `alignItems` into one `AlignmentGrid` row — order-preserving single pass
|
|
189
|
+
* (a combo is emitted at the position of its FIRST member; already-consumed
|
|
190
|
+
* members are skipped later in the pass). Any property list missing one or
|
|
191
|
+
* more combo members (e.g. `ReactDomAuthoringAdapter`'s flatter list) simply
|
|
192
|
+
* never forms that combo — every member renders as its own `single` row.
|
|
193
|
+
*/
|
|
194
|
+
/** Detects the `borderWidth`/`borderStyle`/`borderColor` trio starting at
|
|
195
|
+
* `p` and, if all three are present, pushes ONE `border` combo row. Split
|
|
196
|
+
* out of {@link buildPropertyRows} to keep its own cognitive complexity
|
|
197
|
+
* under biome's ceiling (same discipline as `GradientEditor.tsx`'s helper
|
|
198
|
+
* extraction). Returns `true` only when a combo row was actually formed (so
|
|
199
|
+
* the caller can `continue` past all three consumed members); `false` when
|
|
200
|
+
* `p` isn't a border-trio member, or is one but its siblings aren't present
|
|
201
|
+
* (falls through to an ordinary `single` row instead). */
|
|
202
|
+
function tryBorderCombo(
|
|
203
|
+
p: PropertyDescriptor,
|
|
204
|
+
byPath: ReadonlyMap<string, PropertyDescriptor>,
|
|
205
|
+
consumed: Set<string>,
|
|
206
|
+
rows: PropertyRow[],
|
|
207
|
+
): boolean {
|
|
208
|
+
const isBorderProp =
|
|
209
|
+
p.path === 'style.borderWidth' ||
|
|
210
|
+
p.path === 'style.borderStyle' ||
|
|
211
|
+
p.path === 'style.borderColor';
|
|
212
|
+
if (!isBorderProp) return false;
|
|
213
|
+
const width = byPath.get('style.borderWidth');
|
|
214
|
+
const style = byPath.get('style.borderStyle');
|
|
215
|
+
const color = byPath.get('style.borderColor');
|
|
216
|
+
if (!width || !style || !color) return false;
|
|
217
|
+
rows.push({ kind: 'border', width, style, color });
|
|
218
|
+
consumed.add(width.path);
|
|
219
|
+
consumed.add(style.path);
|
|
220
|
+
consumed.add(color.path);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Detects the `justifyContent`/`alignItems` pair starting at `p` and, if
|
|
225
|
+
* both are present, pushes ONE `align` combo row. See {@link tryBorderCombo}'s
|
|
226
|
+
* doc comment for why this is its own function. */
|
|
227
|
+
function tryAlignCombo(
|
|
228
|
+
p: PropertyDescriptor,
|
|
229
|
+
byPath: ReadonlyMap<string, PropertyDescriptor>,
|
|
230
|
+
consumed: Set<string>,
|
|
231
|
+
rows: PropertyRow[],
|
|
232
|
+
): boolean {
|
|
233
|
+
const isAlignProp = p.path === 'style.justifyContent' || p.path === 'style.alignItems';
|
|
234
|
+
if (!isAlignProp) return false;
|
|
235
|
+
const justify = byPath.get('style.justifyContent');
|
|
236
|
+
const align = byPath.get('style.alignItems');
|
|
237
|
+
if (!justify || !align) return false;
|
|
238
|
+
rows.push({ kind: 'align', justify, align });
|
|
239
|
+
consumed.add(justify.path);
|
|
240
|
+
consumed.add(align.path);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function buildPropertyRows(props: readonly PropertyDescriptor[]): PropertyRow[] {
|
|
245
|
+
const byPath = new Map(props.map((p) => [p.path, p] as const));
|
|
246
|
+
const consumed = new Set<string>();
|
|
247
|
+
const rows: PropertyRow[] = [];
|
|
248
|
+
for (const p of props) {
|
|
249
|
+
if (consumed.has(p.path)) continue;
|
|
250
|
+
if (tryBorderCombo(p, byPath, consumed, rows)) continue;
|
|
251
|
+
if (tryAlignCombo(p, byPath, consumed, rows)) continue;
|
|
252
|
+
rows.push({ kind: 'single', prop: p });
|
|
253
|
+
}
|
|
254
|
+
return rows;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const UNITLESS_NUMBER_PROPS = new Set(['opacity', 'zIndex', 'flexGrow', 'flexShrink']);
|
|
258
|
+
|
|
259
|
+
function numericValue(v: unknown): number {
|
|
260
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function testIdFor(path: string): string {
|
|
264
|
+
return `react-prop-${path.replace(/\./g, '-')}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const DEFAULT_GRADIENT: GradientValue = {
|
|
268
|
+
type: 'linear',
|
|
269
|
+
angle: 180,
|
|
270
|
+
stops: [
|
|
271
|
+
{ color: '#ffffff', position: 0 },
|
|
272
|
+
{ color: '#000000', position: 1 },
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/** A labeled row shell, matching `components/Inspector.tsx`'s own generic
|
|
277
|
+
* property row layout (`NUM_STYLE`'s spacing/label conventions, ported to
|
|
278
|
+
* this kit's `THEME`/`rowStyle`) so a react section slots in visually next
|
|
279
|
+
* to the sections it replaces. The wrapper's own test id is the row's
|
|
280
|
+
* `${testId}-row` — DISTINCT from the interactive control inside it (which
|
|
281
|
+
* carries the bare `testId`), so a select/text/json control and its row
|
|
282
|
+
* wrapper never collide (spec 27 §5 C2, S1). */
|
|
283
|
+
function Field({
|
|
284
|
+
label,
|
|
285
|
+
testId,
|
|
286
|
+
children,
|
|
287
|
+
}: {
|
|
288
|
+
label: string;
|
|
289
|
+
testId?: string | undefined;
|
|
290
|
+
children: React.ReactNode;
|
|
291
|
+
}): React.ReactElement {
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
data-testid={testId ? `${testId}-row` : undefined}
|
|
295
|
+
style={{ ...rowStyle, padding: '4px 8px' }}
|
|
296
|
+
>
|
|
297
|
+
<span style={{ width: 76, flexShrink: 0, fontSize: 11, color: THEME.textMuted }}>
|
|
298
|
+
{label}
|
|
299
|
+
</span>
|
|
300
|
+
<div style={{ flex: 1, minWidth: 0 }}>{children}</div>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** {@link PropertyRow}'s two combo variants — excludes `single` so
|
|
306
|
+
* {@link ComboRow} (only ever called for a combo row, see `GroupBlock`)
|
|
307
|
+
* narrows on `row.kind` without TS widening back to the full union. */
|
|
308
|
+
type ComboPropertyRow = Extract<PropertyRow, { kind: 'border' } | { kind: 'align' }>;
|
|
309
|
+
|
|
310
|
+
interface ComboRowProps {
|
|
311
|
+
adapter: AuthoringAdapter;
|
|
312
|
+
id: string;
|
|
313
|
+
row: ComboPropertyRow;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Every per-kind row renderer's shared inputs — `raw` is the CURRENT value
|
|
317
|
+
* read off `adapter.inspector.get`, `commit` writes a (possibly different)
|
|
318
|
+
* path through the SAME `adapter.inspector.set` + `notifyIngestEdit` pair
|
|
319
|
+
* (see the file doc comment) so a combo write-back (border/align, above)
|
|
320
|
+
* and a plain single-prop write share one code path. */
|
|
321
|
+
interface KindRowProps {
|
|
322
|
+
prop: PropertyDescriptor;
|
|
323
|
+
raw: unknown;
|
|
324
|
+
disabled: boolean | undefined;
|
|
325
|
+
testId: string;
|
|
326
|
+
commit: (path: string, value: unknown) => void;
|
|
327
|
+
fonts: readonly string[];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ColorRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
331
|
+
return (
|
|
332
|
+
<Field label={prop.label} testId={testId}>
|
|
333
|
+
<ColorSwatch
|
|
334
|
+
testId={testId}
|
|
335
|
+
value={String(raw ?? '#ffffff')}
|
|
336
|
+
disabled={disabled}
|
|
337
|
+
onChange={(c) => commit(prop.path, c)}
|
|
338
|
+
onChangeEnd={(c) => commit(prop.path, c)}
|
|
339
|
+
eyedropperFallback={() => effectiveColorFromChain([String(raw ?? '')])}
|
|
340
|
+
// D3.d (spec 27 §6) — no native EyeDropper: begin an interactive
|
|
341
|
+
// canvas-pick session (`WorldSelectionOverlay` renders the
|
|
342
|
+
// cursor-following swatch and resolves it) instead of the static
|
|
343
|
+
// synchronous fallback above, which this session-based path replaces
|
|
344
|
+
// whenever the host (this section, mounted inside the real editor)
|
|
345
|
+
// is present to wire it.
|
|
346
|
+
onEyedropperPick={(apply) => beginEyedropperSession(apply)}
|
|
347
|
+
/>
|
|
348
|
+
</Field>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function FontRow({ prop, raw, disabled, fonts, testId, commit }: KindRowProps): React.ReactElement {
|
|
353
|
+
return (
|
|
354
|
+
<Field label={prop.label} testId={testId}>
|
|
355
|
+
<FontPicker
|
|
356
|
+
testId={testId}
|
|
357
|
+
value={String(raw ?? '')}
|
|
358
|
+
fonts={fonts}
|
|
359
|
+
disabled={disabled}
|
|
360
|
+
onChange={(f) => commit(prop.path, f)}
|
|
361
|
+
/>
|
|
362
|
+
</Field>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function GradientRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
367
|
+
const parsed = parseGradient(String(raw ?? 'none'));
|
|
368
|
+
// S2 (anti-shim, CLAUDE.md "adapters never fabricate first-party data"): when
|
|
369
|
+
// the element has NO real gradient (`parseGradient` -> null), render an honest
|
|
370
|
+
// empty state — NOT a fabricated white→black `DEFAULT_GRADIENT` preview that
|
|
371
|
+
// misrepresents the source as already having a gradient. `DEFAULT_GRADIENT` is
|
|
372
|
+
// still the SEED for an explicit user "add" (a real, source-writing action),
|
|
373
|
+
// never a passive display value.
|
|
374
|
+
if (!parsed) {
|
|
375
|
+
return (
|
|
376
|
+
<Field label={prop.label} testId={testId}>
|
|
377
|
+
<button
|
|
378
|
+
type="button"
|
|
379
|
+
data-testid={testId}
|
|
380
|
+
disabled={disabled}
|
|
381
|
+
onClick={() => commit(prop.path, composeGradient(DEFAULT_GRADIENT))}
|
|
382
|
+
style={{
|
|
383
|
+
...inputStyle(disabled),
|
|
384
|
+
width: '100%',
|
|
385
|
+
textAlign: 'left',
|
|
386
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
387
|
+
color: THEME.textMuted,
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
No gradient — click to add
|
|
391
|
+
</button>
|
|
392
|
+
</Field>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return (
|
|
396
|
+
<Field label={prop.label} testId={testId}>
|
|
397
|
+
<GradientEditor
|
|
398
|
+
testId={testId}
|
|
399
|
+
value={parsed}
|
|
400
|
+
disabled={disabled}
|
|
401
|
+
onChange={(g) => commit(prop.path, composeGradient(g))}
|
|
402
|
+
onChangeEnd={(g) => commit(prop.path, composeGradient(g))}
|
|
403
|
+
/>
|
|
404
|
+
</Field>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function ShadowRow({
|
|
409
|
+
prop,
|
|
410
|
+
raw,
|
|
411
|
+
disabled,
|
|
412
|
+
testId,
|
|
413
|
+
commit,
|
|
414
|
+
withSpreadInset,
|
|
415
|
+
}: KindRowProps & { withSpreadInset: boolean }): React.ReactElement {
|
|
416
|
+
const parsed = parseShadows(String(raw ?? 'none'), withSpreadInset);
|
|
417
|
+
const Widget = withSpreadInset ? ShadowEditor : TextShadowEditor;
|
|
418
|
+
return (
|
|
419
|
+
<Field label={prop.label} testId={testId}>
|
|
420
|
+
<Widget
|
|
421
|
+
value={parsed}
|
|
422
|
+
disabled={disabled}
|
|
423
|
+
onChange={(list) => commit(prop.path, composeShadows(list, withSpreadInset))}
|
|
424
|
+
onChangeEnd={(list) => commit(prop.path, composeShadows(list, withSpreadInset))}
|
|
425
|
+
/>
|
|
426
|
+
</Field>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function FilterRow({
|
|
431
|
+
prop,
|
|
432
|
+
raw,
|
|
433
|
+
disabled,
|
|
434
|
+
testId,
|
|
435
|
+
commit,
|
|
436
|
+
label,
|
|
437
|
+
}: KindRowProps & { label: string }): React.ReactElement {
|
|
438
|
+
const parsed = parseFilterFunctions(String(raw ?? 'none'));
|
|
439
|
+
return (
|
|
440
|
+
<Field label={prop.label} testId={testId}>
|
|
441
|
+
<FilterEditor
|
|
442
|
+
testId={testId}
|
|
443
|
+
label={label}
|
|
444
|
+
value={parsed}
|
|
445
|
+
disabled={disabled}
|
|
446
|
+
onChange={(f: FilterFunc[]) => commit(prop.path, composeFilter(f))}
|
|
447
|
+
onChangeEnd={(f: FilterFunc[]) => commit(prop.path, composeFilter(f))}
|
|
448
|
+
/>
|
|
449
|
+
</Field>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Corner suffixes (`borderTopLeftRadius`, …) shared by {@link RadiusRow}'s
|
|
454
|
+
* read + write-back — the per-corner longhand CSS the shorthand `borderRadius`
|
|
455
|
+
* descriptor (`prop`/`raw`/`commit` above) never had a slot for. */
|
|
456
|
+
const RADIUS_CORNERS: ReadonlyArray<{ key: keyof BorderRadiusValue; suffix: string }> = [
|
|
457
|
+
{ key: 'topLeft', suffix: 'TopLeft' },
|
|
458
|
+
{ key: 'topRight', suffix: 'TopRight' },
|
|
459
|
+
{ key: 'bottomLeft', suffix: 'BottomLeft' },
|
|
460
|
+
{ key: 'bottomRight', suffix: 'BottomRight' },
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
/** U2 (spec 27 §5 "Effects: radius per-corner") — true per-corner write-back.
|
|
464
|
+
* Reads each corner's own `style.border<Corner>Radius` longhand through
|
|
465
|
+
* `adapter.inspector.get` (falling back to the shorthand `raw` when a
|
|
466
|
+
* corner's own computed value is unset/unparseable — the vitest `node` env
|
|
467
|
+
* resolver falls back to inline `.style`, which has no implicit per-corner
|
|
468
|
+
* values the way a REAL computed style always does) so the widget reflects
|
|
469
|
+
* a PRIOR non-uniform edit instead of collapsing back to uniform on every
|
|
470
|
+
* re-render. Write-back keeps the uniform shorthand fast-path (`commit`
|
|
471
|
+
* the same `borderRadius` path this row already used) but writes the four
|
|
472
|
+
* longhand corners when the widget reports a non-uniform value — removing
|
|
473
|
+
* the documented `:397-409` limitation (per-corner UI, uniform-only write). */
|
|
474
|
+
function RadiusRow({
|
|
475
|
+
adapter,
|
|
476
|
+
id,
|
|
477
|
+
prop,
|
|
478
|
+
raw,
|
|
479
|
+
disabled,
|
|
480
|
+
testId,
|
|
481
|
+
commit,
|
|
482
|
+
removeStyle,
|
|
483
|
+
}: KindRowProps & {
|
|
484
|
+
adapter: AuthoringAdapter;
|
|
485
|
+
id: string;
|
|
486
|
+
removeStyle: (path: string) => void;
|
|
487
|
+
}): React.ReactElement {
|
|
488
|
+
const fallback = numericValue(raw);
|
|
489
|
+
const value: BorderRadiusValue = RADIUS_CORNERS.reduce((acc, { key, suffix }) => {
|
|
490
|
+
const cornerRaw = adapter.inspector?.get(id, `style.border${suffix}Radius`);
|
|
491
|
+
acc[key] = numericStyleValue(cornerRaw) ?? fallback;
|
|
492
|
+
return acc;
|
|
493
|
+
}, {} as BorderRadiusValue);
|
|
494
|
+
const writeBack = (v: BorderRadiusValue): void => {
|
|
495
|
+
if (isUniformRadius(v)) {
|
|
496
|
+
commit(prop.path, v.topLeft);
|
|
497
|
+
// D1 — a uniform edit must also CLEAR any per-corner longhands a prior
|
|
498
|
+
// non-uniform edit wrote, or the stale longhand silently overrides the
|
|
499
|
+
// shorthand on reload. `removeStyle` no-ops when the longhand isn't
|
|
500
|
+
// authored, so this is safe on a clean element (never pollutes source).
|
|
501
|
+
for (const { suffix } of RADIUS_CORNERS) {
|
|
502
|
+
removeStyle(`style.border${suffix}Radius`);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
for (const { key, suffix } of RADIUS_CORNERS) {
|
|
507
|
+
commit(`style.border${suffix}Radius`, v[key]);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
return (
|
|
511
|
+
<Field label={prop.label} testId={testId}>
|
|
512
|
+
<BorderRadiusEditor
|
|
513
|
+
value={value}
|
|
514
|
+
disabled={disabled}
|
|
515
|
+
onChange={writeBack}
|
|
516
|
+
onChangeEnd={writeBack}
|
|
517
|
+
/>
|
|
518
|
+
</Field>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function ScrubRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
523
|
+
const unit = UNITLESS_NUMBER_PROPS.has(propNameFromPath(prop.path)) ? '' : 'px';
|
|
524
|
+
return (
|
|
525
|
+
<ScrubbableInput
|
|
526
|
+
testId={testId}
|
|
527
|
+
label={prop.label}
|
|
528
|
+
value={numericValue(raw)}
|
|
529
|
+
unit={unit}
|
|
530
|
+
disabled={disabled}
|
|
531
|
+
onChange={(v) => commit(prop.path, v)}
|
|
532
|
+
onChangeEnd={(v) => commit(prop.path, v)}
|
|
533
|
+
style={{ padding: '4px 8px' }}
|
|
534
|
+
/>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function SelectRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
539
|
+
return (
|
|
540
|
+
<Field label={prop.label} testId={testId}>
|
|
541
|
+
<select
|
|
542
|
+
data-testid={testId}
|
|
543
|
+
value={String(raw ?? '')}
|
|
544
|
+
disabled={disabled}
|
|
545
|
+
onChange={(e) => commit(prop.path, e.target.value)}
|
|
546
|
+
style={{
|
|
547
|
+
...inputStyle(disabled),
|
|
548
|
+
width: '100%',
|
|
549
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
{(prop.options ?? []).map((o) => (
|
|
553
|
+
<option key={String(o)} value={String(o)}>
|
|
554
|
+
{String(o)}
|
|
555
|
+
</option>
|
|
556
|
+
))}
|
|
557
|
+
</select>
|
|
558
|
+
</Field>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function CheckboxRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
563
|
+
return (
|
|
564
|
+
<label
|
|
565
|
+
style={{ display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', fontSize: 12 }}
|
|
566
|
+
>
|
|
567
|
+
<input
|
|
568
|
+
type="checkbox"
|
|
569
|
+
data-testid={testId}
|
|
570
|
+
checked={raw !== false}
|
|
571
|
+
disabled={disabled}
|
|
572
|
+
onChange={(e) => commit(prop.path, e.target.checked)}
|
|
573
|
+
/>
|
|
574
|
+
{prop.label}
|
|
575
|
+
</label>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function JsonRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
580
|
+
return (
|
|
581
|
+
<Field label={prop.label} testId={testId}>
|
|
582
|
+
<textarea
|
|
583
|
+
data-testid={testId}
|
|
584
|
+
defaultValue={JSON.stringify(raw ?? null)}
|
|
585
|
+
disabled={disabled}
|
|
586
|
+
style={{ ...inputStyle(disabled), width: '100%', minHeight: 40, fontFamily: FONT_MONO }}
|
|
587
|
+
onBlur={(e) => {
|
|
588
|
+
try {
|
|
589
|
+
commit(prop.path, JSON.parse(e.target.value));
|
|
590
|
+
} catch {
|
|
591
|
+
// Invalid JSON — leave the field as typed, don't write garbage.
|
|
592
|
+
}
|
|
593
|
+
}}
|
|
594
|
+
/>
|
|
595
|
+
</Field>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function TextRow({ prop, raw, disabled, testId, commit }: KindRowProps): React.ReactElement {
|
|
600
|
+
return (
|
|
601
|
+
<Field label={prop.label} testId={testId}>
|
|
602
|
+
<input
|
|
603
|
+
type="text"
|
|
604
|
+
data-testid={testId}
|
|
605
|
+
readOnly={disabled}
|
|
606
|
+
value={String(raw ?? '')}
|
|
607
|
+
onChange={(e) => commit(prop.path, e.target.value)}
|
|
608
|
+
style={{ ...inputStyle(disabled), width: '100%' }}
|
|
609
|
+
/>
|
|
610
|
+
</Field>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** U3 (spec 27 §5 "Transform: rotate/scale") — a structured `{rotateDeg, scale}`
|
|
615
|
+
* pair, when `style.transform` is losslessly representable that way. */
|
|
616
|
+
export interface TransformValue {
|
|
617
|
+
rotateDeg: number;
|
|
618
|
+
scale: number;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const TRANSFORM_ROTATE_SCALE_RE = /^rotate\(\s*(-?[\d.]+)deg\s*\)\s*scale\(\s*(-?[\d.]+)\s*\)$/i;
|
|
622
|
+
const TRANSFORM_SCALE_ROTATE_RE = /^scale\(\s*(-?[\d.]+)\s*\)\s*rotate\(\s*(-?[\d.]+)deg\s*\)$/i;
|
|
623
|
+
// S3 — single-function literals (a JSX-authored `rotate(45deg)` or `scale(1.5)`
|
|
624
|
+
// alone, missing axis defaulted): computed style is always a matrix in the real
|
|
625
|
+
// browser, but these are exactly what an author hand-writes, so give them the
|
|
626
|
+
// structured row too rather than the raw TextRow.
|
|
627
|
+
const TRANSFORM_ROTATE_ONLY_RE = /^rotate\(\s*(-?[\d.]+)deg\s*\)$/i;
|
|
628
|
+
const TRANSFORM_SCALE_ONLY_RE = /^scale\(\s*(-?[\d.]+)\s*\)$/i;
|
|
629
|
+
const TRANSFORM_MATRIX_RE =
|
|
630
|
+
/^matrix\(\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*\)$/;
|
|
631
|
+
const TRANSFORM_EPS = 1e-4;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Parse a `style.transform` value (either the author's literal
|
|
635
|
+
* `rotate(<deg>deg) scale(<n>)` — the exact form this row writes — or a
|
|
636
|
+
* REAL browser's computed 2x2 `matrix(a, b, c, d, e, f)`, since Chromium
|
|
637
|
+
* always normalizes an authored transform to matrix form, unlike this
|
|
638
|
+
* repo's headless/vitest fallback which echoes the literal string straight
|
|
639
|
+
* from inline style) into a structured `{rotateDeg, scale}` pair, or `null`
|
|
640
|
+
* when the value can't be represented that way LOSSLESSLY — a nonzero
|
|
641
|
+
* translate (`e`/`f`), a skew/non-uniform scale (`c !== -b` or `d !== a`),
|
|
642
|
+
* `matrix3d(...)`, or any other transform function list. `null` is the
|
|
643
|
+
* signal {@link TransformRow} uses to fall back to the plain `TextRow` —
|
|
644
|
+
* every value this doesn't understand stays editable as raw text, so no
|
|
645
|
+
* expressiveness is ever silently dropped.
|
|
646
|
+
*/
|
|
647
|
+
export function parseTransform(raw: string): TransformValue | null {
|
|
648
|
+
const v = raw.trim();
|
|
649
|
+
if (v === '' || v === 'none') return { rotateDeg: 0, scale: 1 };
|
|
650
|
+
const lit = TRANSFORM_ROTATE_SCALE_RE.exec(v);
|
|
651
|
+
if (lit) return { rotateDeg: Number(lit[1]), scale: Number(lit[2]) };
|
|
652
|
+
const swapped = TRANSFORM_SCALE_ROTATE_RE.exec(v);
|
|
653
|
+
if (swapped) return { rotateDeg: Number(swapped[2]), scale: Number(swapped[1]) };
|
|
654
|
+
const rotOnly = TRANSFORM_ROTATE_ONLY_RE.exec(v);
|
|
655
|
+
if (rotOnly) return { rotateDeg: Number(rotOnly[1]), scale: 1 }; // S3 — missing scale defaults 1
|
|
656
|
+
const scaleOnly = TRANSFORM_SCALE_ONLY_RE.exec(v);
|
|
657
|
+
if (scaleOnly) return { rotateDeg: 0, scale: Number(scaleOnly[1]) }; // S3 — missing rotate defaults 0
|
|
658
|
+
const m = TRANSFORM_MATRIX_RE.exec(v);
|
|
659
|
+
if (!m) return null; // matrix3d / translate / skew / any other function list
|
|
660
|
+
const [a, b, c, d, e, f] = m.slice(1, 7).map(Number) as [
|
|
661
|
+
number,
|
|
662
|
+
number,
|
|
663
|
+
number,
|
|
664
|
+
number,
|
|
665
|
+
number,
|
|
666
|
+
number,
|
|
667
|
+
];
|
|
668
|
+
if (Math.abs(e) > TRANSFORM_EPS || Math.abs(f) > TRANSFORM_EPS) return null; // has a translate
|
|
669
|
+
if (Math.abs(c + b) > TRANSFORM_EPS || Math.abs(d - a) > TRANSFORM_EPS) return null; // skew / non-uniform scale
|
|
670
|
+
const scale = Math.hypot(a, b);
|
|
671
|
+
if (scale < TRANSFORM_EPS) return null; // degenerate (zero scale)
|
|
672
|
+
const rotateDeg = (Math.atan2(b, a) * 180) / Math.PI;
|
|
673
|
+
return {
|
|
674
|
+
rotateDeg: Math.round(rotateDeg * 100) / 100,
|
|
675
|
+
scale: Math.round(scale * 1000) / 1000,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** The inverse of {@link parseTransform}'s literal fast-path — always
|
|
680
|
+
* composes the readable `rotate()/scale()` form, never a matrix, so the
|
|
681
|
+
* written source stays human-editable. */
|
|
682
|
+
export function composeTransform(v: TransformValue): string {
|
|
683
|
+
return `rotate(${v.rotateDeg}deg) scale(${v.scale})`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function TransformRow(props: KindRowProps): React.ReactElement {
|
|
687
|
+
const { prop, raw, disabled, testId, commit } = props;
|
|
688
|
+
const parsed = parseTransform(String(raw ?? 'none'));
|
|
689
|
+
if (!parsed) return <TextRow {...props} />; // safety net — no expressiveness lost
|
|
690
|
+
const writeBack = (next: TransformValue): void => commit(prop.path, composeTransform(next));
|
|
691
|
+
return (
|
|
692
|
+
<Field label={prop.label} testId={testId}>
|
|
693
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
694
|
+
<ScrubbableInput
|
|
695
|
+
testId={`${testId}-rotate`}
|
|
696
|
+
label="Rotate"
|
|
697
|
+
value={parsed.rotateDeg}
|
|
698
|
+
unit="deg"
|
|
699
|
+
disabled={disabled}
|
|
700
|
+
onChange={(v) => writeBack({ ...parsed, rotateDeg: v })}
|
|
701
|
+
onChangeEnd={(v) => writeBack({ ...parsed, rotateDeg: v })}
|
|
702
|
+
style={{ flex: 1 }}
|
|
703
|
+
/>
|
|
704
|
+
<ScrubbableInput
|
|
705
|
+
testId={`${testId}-scale`}
|
|
706
|
+
label="Scale"
|
|
707
|
+
value={parsed.scale}
|
|
708
|
+
unit="x"
|
|
709
|
+
step={0.1}
|
|
710
|
+
disabled={disabled}
|
|
711
|
+
onChange={(v) => writeBack({ ...parsed, scale: v })}
|
|
712
|
+
onChangeEnd={(v) => writeBack({ ...parsed, scale: v })}
|
|
713
|
+
style={{ flex: 1 }}
|
|
714
|
+
/>
|
|
715
|
+
</div>
|
|
716
|
+
</Field>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** One `single`-kind row: reads the current value, picks the widget via
|
|
721
|
+
* {@link widgetKindFor}, and delegates to the matching `*Row` component
|
|
722
|
+
* above — each extracted so this dispatcher's own branching stays well
|
|
723
|
+
* under biome's complexity ceiling (same discipline `GradientEditor.tsx`/
|
|
724
|
+
* `ShadowEditor.tsx` document for their own helper splits). Both
|
|
725
|
+
* `onChange`/`onChangeEnd` route through the SAME `commit` (file doc
|
|
726
|
+
* comment: this adapter has one write path, not a separate preview mode). */
|
|
727
|
+
function SingleRow({
|
|
728
|
+
adapter,
|
|
729
|
+
id,
|
|
730
|
+
prop,
|
|
731
|
+
fonts,
|
|
732
|
+
}: {
|
|
733
|
+
adapter: AuthoringAdapter;
|
|
734
|
+
id: string;
|
|
735
|
+
prop: PropertyDescriptor;
|
|
736
|
+
fonts: readonly string[];
|
|
737
|
+
}): React.ReactElement {
|
|
738
|
+
const store = useEditorStore();
|
|
739
|
+
const commit = (path: string, value: unknown): void => {
|
|
740
|
+
adapter.inspector?.set(id, path, value);
|
|
741
|
+
store.notifyIngestEdit();
|
|
742
|
+
};
|
|
743
|
+
// D1 — remove a stale CSS longhand override (no-op when unauthored); optional
|
|
744
|
+
// on the inspector, so a non-react adapter simply skips the cleanup.
|
|
745
|
+
const removeStyle = (path: string): void => {
|
|
746
|
+
adapter.inspector?.remove?.(id, path);
|
|
747
|
+
store.notifyIngestEdit();
|
|
748
|
+
};
|
|
749
|
+
const rowProps: KindRowProps = {
|
|
750
|
+
prop,
|
|
751
|
+
raw: adapter.inspector?.get(id, prop.path),
|
|
752
|
+
disabled: prop.readonly,
|
|
753
|
+
testId: testIdFor(prop.path),
|
|
754
|
+
commit,
|
|
755
|
+
fonts,
|
|
756
|
+
};
|
|
757
|
+
switch (widgetKindFor(prop)) {
|
|
758
|
+
case 'color':
|
|
759
|
+
return <ColorRow {...rowProps} />;
|
|
760
|
+
case 'font':
|
|
761
|
+
return <FontRow {...rowProps} />;
|
|
762
|
+
case 'gradient':
|
|
763
|
+
return <GradientRow {...rowProps} />;
|
|
764
|
+
case 'shadow':
|
|
765
|
+
return <ShadowRow {...rowProps} withSpreadInset />;
|
|
766
|
+
case 'text-shadow':
|
|
767
|
+
return <ShadowRow {...rowProps} withSpreadInset={false} />;
|
|
768
|
+
case 'filter':
|
|
769
|
+
return <FilterRow {...rowProps} label="Filter" />;
|
|
770
|
+
case 'backdrop-filter':
|
|
771
|
+
return <FilterRow {...rowProps} label="Backdrop Filter" />;
|
|
772
|
+
case 'radius':
|
|
773
|
+
return <RadiusRow {...rowProps} adapter={adapter} id={id} removeStyle={removeStyle} />;
|
|
774
|
+
case 'transform':
|
|
775
|
+
return <TransformRow {...rowProps} />;
|
|
776
|
+
case 'scrub':
|
|
777
|
+
return <ScrubRow {...rowProps} />;
|
|
778
|
+
case 'select':
|
|
779
|
+
return <SelectRow {...rowProps} />;
|
|
780
|
+
case 'checkbox':
|
|
781
|
+
return <CheckboxRow {...rowProps} />;
|
|
782
|
+
case 'json':
|
|
783
|
+
return <JsonRow {...rowProps} />;
|
|
784
|
+
default:
|
|
785
|
+
return <TextRow {...rowProps} />;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** A `border`/`align` combo row — reads all its member props, composes the
|
|
790
|
+
* widget's structured value, and writes every member back on change. */
|
|
791
|
+
function ComboRow({ adapter, id, row }: ComboRowProps): React.ReactElement | null {
|
|
792
|
+
const store = useEditorStore();
|
|
793
|
+
const commit = (path: string, value: unknown): void => {
|
|
794
|
+
adapter.inspector?.set(id, path, value);
|
|
795
|
+
store.notifyIngestEdit();
|
|
796
|
+
};
|
|
797
|
+
const removeStyle = (path: string): void => {
|
|
798
|
+
adapter.inspector?.remove?.(id, path);
|
|
799
|
+
store.notifyIngestEdit();
|
|
800
|
+
};
|
|
801
|
+
if (row.kind === 'border') {
|
|
802
|
+
// U2 (spec 27 §5 "Stroke: per-side border") — the shorthand descriptors
|
|
803
|
+
// (`row.width`/`row.style`/`row.color`) are the uniform fallback; each
|
|
804
|
+
// side's OWN longhand (`borderTopWidth`, …) is read through the same
|
|
805
|
+
// `adapter.inspector.get` and wins when present, so a prior per-side
|
|
806
|
+
// edit survives the next re-render instead of collapsing back to
|
|
807
|
+
// uniform (mirrors `RadiusRow`'s corner read, same fallback rationale:
|
|
808
|
+
// the vitest `node` env resolver has no implicit per-side computed
|
|
809
|
+
// values the way a real `CSSStyleDeclaration` always does).
|
|
810
|
+
const fallback: BorderSide = {
|
|
811
|
+
width: numericValue(adapter.inspector?.get(id, row.width.path)),
|
|
812
|
+
style: String(adapter.inspector?.get(id, row.style.path) ?? 'solid'),
|
|
813
|
+
color: String(adapter.inspector?.get(id, row.color.path) ?? '#000000'),
|
|
814
|
+
};
|
|
815
|
+
const readSide = (suffix: 'Top' | 'Right' | 'Bottom' | 'Left'): BorderSide => {
|
|
816
|
+
const w = numericStyleValue(adapter.inspector?.get(id, `style.border${suffix}Width`));
|
|
817
|
+
const st = adapter.inspector?.get(id, `style.border${suffix}Style`);
|
|
818
|
+
const c = adapter.inspector?.get(id, `style.border${suffix}Color`);
|
|
819
|
+
// D1 — normalize the per-side color longhand through `cssColorToHex`: it's
|
|
820
|
+
// not in `STYLE_PROPERTY_TYPE`, so `inspector.get` hands back the RAW
|
|
821
|
+
// computed value (`rgb(…)`), and letting that flow into a later uniform
|
|
822
|
+
// commit would rewrite the author's hex literal as an `rgb()` string.
|
|
823
|
+
const cHex = typeof c === 'string' ? cssColorToHex(c) : undefined;
|
|
824
|
+
return {
|
|
825
|
+
width: w ?? fallback.width,
|
|
826
|
+
style: typeof st === 'string' && st ? st : fallback.style,
|
|
827
|
+
color: cHex ?? fallback.color,
|
|
828
|
+
};
|
|
829
|
+
};
|
|
830
|
+
const value: BorderValue = {
|
|
831
|
+
top: readSide('Top'),
|
|
832
|
+
right: readSide('Right'),
|
|
833
|
+
bottom: readSide('Bottom'),
|
|
834
|
+
left: readSide('Left'),
|
|
835
|
+
};
|
|
836
|
+
// Uniform edits keep the pre-existing shorthand fast-path (3 commits to
|
|
837
|
+
// the `borderWidth`/`borderStyle`/`borderColor` descriptors this row
|
|
838
|
+
// always had); a non-uniform widget value now writes the 4 per-side
|
|
839
|
+
// CSS longhands instead of collapsing to `.top` — removing the
|
|
840
|
+
// documented limitation this file used to note here.
|
|
841
|
+
const sideSuffixes = ['Top', 'Right', 'Bottom', 'Left'] as const;
|
|
842
|
+
const writeBack = (v: BorderValue): void => {
|
|
843
|
+
if (isUniformBorder(v)) {
|
|
844
|
+
commit(row.width.path, v.top.width);
|
|
845
|
+
commit(row.style.path, v.top.style);
|
|
846
|
+
commit(row.color.path, v.top.color);
|
|
847
|
+
// D1 — clear any per-side longhands a prior non-uniform edit wrote, so
|
|
848
|
+
// the uniform shorthand actually wins on reload (removeStyle no-ops
|
|
849
|
+
// when a longhand isn't authored — safe on a clean element).
|
|
850
|
+
for (const suffix of sideSuffixes) {
|
|
851
|
+
removeStyle(`style.border${suffix}Width`);
|
|
852
|
+
removeStyle(`style.border${suffix}Style`);
|
|
853
|
+
removeStyle(`style.border${suffix}Color`);
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const sides: ReadonlyArray<{
|
|
858
|
+
suffix: 'Top' | 'Right' | 'Bottom' | 'Left';
|
|
859
|
+
side: BorderSide;
|
|
860
|
+
}> = sideSuffixes.map((suffix) => ({
|
|
861
|
+
suffix,
|
|
862
|
+
side: v[suffix.toLowerCase() as 'top' | 'right' | 'bottom' | 'left'],
|
|
863
|
+
}));
|
|
864
|
+
for (const { suffix, side } of sides) {
|
|
865
|
+
commit(`style.border${suffix}Width`, side.width);
|
|
866
|
+
commit(`style.border${suffix}Style`, side.style);
|
|
867
|
+
commit(`style.border${suffix}Color`, side.color);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
return (
|
|
871
|
+
<Field label="Border" testId="react-prop-border">
|
|
872
|
+
<BorderEditor value={value} onChange={writeBack} onChangeEnd={writeBack} />
|
|
873
|
+
</Field>
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const value: AlignmentValue = {
|
|
877
|
+
justify:
|
|
878
|
+
(adapter.inspector?.get(id, row.justify.path) as AlignmentValue['justify']) ?? 'flex-start',
|
|
879
|
+
align: (adapter.inspector?.get(id, row.align.path) as AlignmentValue['align']) ?? 'flex-start',
|
|
880
|
+
};
|
|
881
|
+
return (
|
|
882
|
+
<Field label="Align" testId="react-prop-align">
|
|
883
|
+
<AlignmentGrid
|
|
884
|
+
value={value}
|
|
885
|
+
onChange={(v) => {
|
|
886
|
+
commit(row.justify.path, v.justify);
|
|
887
|
+
commit(row.align.path, v.align);
|
|
888
|
+
}}
|
|
889
|
+
/>
|
|
890
|
+
</Field>
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function rowKey(row: PropertyRow): string {
|
|
895
|
+
if (row.kind === 'single') return row.prop.path;
|
|
896
|
+
if (row.kind === 'border') return `border:${row.width.path}`;
|
|
897
|
+
return `align:${row.justify.path}`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function GroupBlock({
|
|
901
|
+
name,
|
|
902
|
+
groupId,
|
|
903
|
+
props,
|
|
904
|
+
adapter,
|
|
905
|
+
id,
|
|
906
|
+
fonts,
|
|
907
|
+
}: {
|
|
908
|
+
name: string;
|
|
909
|
+
groupId: string;
|
|
910
|
+
props: readonly PropertyDescriptor[];
|
|
911
|
+
adapter: AuthoringAdapter;
|
|
912
|
+
id: string;
|
|
913
|
+
fonts: readonly string[];
|
|
914
|
+
}): React.ReactElement {
|
|
915
|
+
const rows = buildPropertyRows(props);
|
|
916
|
+
return (
|
|
917
|
+
<div
|
|
918
|
+
data-testid={`react-inspector-group-${groupId}`}
|
|
919
|
+
style={{ borderTop: `1px solid ${THEME.border}`, marginTop: 4 }}
|
|
920
|
+
>
|
|
921
|
+
<div
|
|
922
|
+
style={{
|
|
923
|
+
fontSize: 11,
|
|
924
|
+
fontWeight: 600,
|
|
925
|
+
color: THEME.textMuted,
|
|
926
|
+
padding: '6px 8px 2px',
|
|
927
|
+
textTransform: 'uppercase',
|
|
928
|
+
letterSpacing: 0.4,
|
|
929
|
+
}}
|
|
930
|
+
>
|
|
931
|
+
{name}
|
|
932
|
+
</div>
|
|
933
|
+
{rows.map((row) =>
|
|
934
|
+
row.kind === 'single' ? (
|
|
935
|
+
<SingleRow key={rowKey(row)} adapter={adapter} id={id} prop={row.prop} fonts={fonts} />
|
|
936
|
+
) : (
|
|
937
|
+
<ComboRow key={rowKey(row)} adapter={adapter} id={id} row={row} />
|
|
938
|
+
),
|
|
939
|
+
)}
|
|
940
|
+
</div>
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** The Lane-B ("edits component source -> every instance") disclosure —
|
|
945
|
+
* ports `ui-editor/source-edit-panel.tsx`'s header COPY, not the component
|
|
946
|
+
* itself (that one is bound to `UIEditStore`/an ad hoc `/__ui-source/index`
|
|
947
|
+
* fetch — Rule zero bans importing `UIEditStore` into this shell-registered
|
|
948
|
+
* section). Shown only for a `ReactWorldAuthoringAdapter` (real Lane B,
|
|
949
|
+
* `data-oid` component-source writes); a `ReactDomAuthoringAdapter`
|
|
950
|
+
* (ingested, overlay-only — never touches source) gets its own honest note
|
|
951
|
+
* instead, so neither lane's write model is ever misrepresented. */
|
|
952
|
+
function LaneDisclosure({
|
|
953
|
+
owner,
|
|
954
|
+
node,
|
|
955
|
+
}: {
|
|
956
|
+
owner: ReactWorldAuthoringAdapter | ReactDomAuthoringAdapter;
|
|
957
|
+
node: EditorNode | null;
|
|
958
|
+
}): React.ReactElement {
|
|
959
|
+
const isLaneB = owner instanceof ReactWorldAuthoringAdapter;
|
|
960
|
+
return (
|
|
961
|
+
<div
|
|
962
|
+
data-testid={isLaneB ? 'react-lane-b-disclosure' : 'react-overlay-disclosure'}
|
|
963
|
+
style={{
|
|
964
|
+
padding: 8,
|
|
965
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
966
|
+
background: isLaneB ? 'rgba(59,130,246,0.08)' : 'rgba(255,255,255,0.04)',
|
|
967
|
+
color: THEME.text,
|
|
968
|
+
fontSize: 11,
|
|
969
|
+
lineHeight: 1.5,
|
|
970
|
+
}}
|
|
971
|
+
>
|
|
972
|
+
{isLaneB ? (
|
|
973
|
+
<>
|
|
974
|
+
<strong style={{ color: THEME.accent }}>Component source.</strong> Edits change{' '}
|
|
975
|
+
<code style={{ fontFamily: FONT_MONO }}>{node?.label ?? 'this component'}</code>'s source
|
|
976
|
+
— every rendered instance updates.
|
|
977
|
+
</>
|
|
978
|
+
) : (
|
|
979
|
+
<>
|
|
980
|
+
<strong style={{ color: THEME.textMuted }}>Session overlay (ingested game).</strong> Edits
|
|
981
|
+
are NOT written to source — they persist as a per-project overlay only.
|
|
982
|
+
</>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** The registered section itself (C2). Renders every `PropertyDescriptor`
|
|
989
|
+
* from `adapter.inspector.properties(nodeId)`, grouped and widget-mapped —
|
|
990
|
+
* see the file doc comment for the write-routing contract. */
|
|
991
|
+
function ReactPropertiesSection({
|
|
992
|
+
adapter,
|
|
993
|
+
nodeId,
|
|
994
|
+
}: InspectorSectionProps): React.ReactElement | null {
|
|
995
|
+
// Re-render is driven by the shell's own `useSyncExternalStore` subscription
|
|
996
|
+
// (`components/Inspector.tsx`) — every `SingleRow`/`ComboRow` below calls
|
|
997
|
+
// `store.notifyIngestEdit()` itself on commit (its own `useEditorStore()`),
|
|
998
|
+
// which re-renders the whole Inspector tree including this section; no
|
|
999
|
+
// separate subscription is needed here.
|
|
1000
|
+
const fonts = useMemo(() => getAvailableFonts(), []);
|
|
1001
|
+
if (!nodeId) return null;
|
|
1002
|
+
const node = adapter.hierarchy.node(nodeId);
|
|
1003
|
+
const owner = resolveReactOwner(node, adapter);
|
|
1004
|
+
if (!owner) return null;
|
|
1005
|
+
|
|
1006
|
+
const props = (adapter.inspector?.properties(nodeId) ?? []).filter((p) => p.path !== 'name');
|
|
1007
|
+
const { ungrouped, groups } = groupProperties(props);
|
|
1008
|
+
|
|
1009
|
+
return (
|
|
1010
|
+
<div data-testid="react-inspector-section">
|
|
1011
|
+
<LaneDisclosure owner={owner} node={node} />
|
|
1012
|
+
{ungrouped.length > 0 && (
|
|
1013
|
+
<GroupBlock
|
|
1014
|
+
name="Style"
|
|
1015
|
+
groupId="style"
|
|
1016
|
+
props={ungrouped}
|
|
1017
|
+
adapter={adapter}
|
|
1018
|
+
id={nodeId}
|
|
1019
|
+
fonts={fonts}
|
|
1020
|
+
/>
|
|
1021
|
+
)}
|
|
1022
|
+
{groups.map((g) => (
|
|
1023
|
+
<GroupBlock
|
|
1024
|
+
key={g.groupId}
|
|
1025
|
+
name={g.name}
|
|
1026
|
+
groupId={g.groupId}
|
|
1027
|
+
props={g.properties}
|
|
1028
|
+
adapter={adapter}
|
|
1029
|
+
id={nodeId}
|
|
1030
|
+
fonts={fonts}
|
|
1031
|
+
/>
|
|
1032
|
+
))}
|
|
1033
|
+
</div>
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
registerInspectorSections({
|
|
1038
|
+
match: isReactDomSelection,
|
|
1039
|
+
Section: ReactPropertiesSection,
|
|
1040
|
+
suppresses: ['properties'],
|
|
1041
|
+
});
|