@vgai/editor 0.2.0 → 0.4.0-canary.20260715.1
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-CyiTVRi6.js +30 -0
- package/dist/assets/browserAll-fGZ_P1P8.js +1 -0
- package/dist/assets/create-runtime-BzSHBTYw.js +1 -0
- package/dist/assets/game-DQ7YHNS8.js +1 -0
- package/dist/assets/game-DX6Obomf.js +1 -0
- package/dist/assets/game-DoUJj8vY.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-HqybI_X7.js} +3 -3
- package/dist/assets/game-NUAnyPkS.js +1 -0
- package/dist/assets/game-ck_dNIE6.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-hIf5LD4E.js} +1 -1
- package/dist/assets/game-loop-o2CcBMFY.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-C8HM7gbP.js} +1 -1
- package/dist/assets/{game-BI111o5f.js → game-uR_lv3Mp.js} +3 -3
- package/dist/assets/index-B-HA1uu3.js +15412 -0
- package/dist/assets/{index-Bp5cecD6.js → index-CSBAZwMx.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-CoSRCKPS.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-D93bBuUl.js} +1 -1
- package/dist/assets/index-bu01fqch.css +1 -0
- package/dist/assets/ingest-siblings-CCkkE2h7.js +1 -0
- package/dist/assets/mount-manifest-B2ECssU2.js +1 -0
- package/dist/assets/react-18-Bmj05S8J.js +1 -0
- package/dist/assets/webworkerAll-BC6gMOLZ.js +1 -0
- package/dist/assets/world-documents-DXPcAEA0.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 +7718 -3436
- 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 +71 -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 +1236 -296
- 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 +1116 -344
- 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 +620 -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
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import { Checkbox, TextInput, themeVars } from '../design-system';
|
|
2
|
+
/**
|
|
3
|
+
* Story DOCUMENTS (W2, docs/EDITOR-WORKSPACE-SHELL-TODO.md §4.2 / §9 W2 —
|
|
4
|
+
* inventory row B9): a selected CSF story opens as a CENTER workspace
|
|
5
|
+
* document (kind `'story'`, title = the story's export name as authored, §8),
|
|
6
|
+
* mounted through the SAME Storybook portable-story pipeline the Stories
|
|
7
|
+
* dock used (`stories/StoryPreviewMount.tsx`'s isolated `react-dom/client`
|
|
8
|
+
* root — never a child of the editor's own tree), with:
|
|
9
|
+
*
|
|
10
|
+
* - **args → Inspector** (§5 "Storybook args | right Inspector"): a
|
|
11
|
+
* story-scoped section registered through the EXISTING
|
|
12
|
+
* inspector-section registry, live-editable for primitive args (string/
|
|
13
|
+
* number/boolean — the Storybook-controls semantic); edits re-render the
|
|
14
|
+
* SAME composed story with override props (portable stories accept
|
|
15
|
+
* partial props over their composed args — Storybook's own API, not a
|
|
16
|
+
* re-implementation);
|
|
17
|
+
* - **addon results → utilities** (§5 "Actions/interactions … story-scoped
|
|
18
|
+
* utility tabs"): `Actions` (function-valued args are wrapped with
|
|
19
|
+
* logging spies at mount — the actions-addon behavior — and every call
|
|
20
|
+
* from the story's own rendered component is logged) and `Interactions`
|
|
21
|
+
* (the story's `play()` run results) are registered in the W0
|
|
22
|
+
* workspace-utility registry, available only while a story document is
|
|
23
|
+
* active. `WorkspaceDock` renders available registrations as movable
|
|
24
|
+
* utility panels, defaulting to the bottom edge group.
|
|
25
|
+
*
|
|
26
|
+
* §4.2 Play interplay: a story document renders in its OWN isolated preview
|
|
27
|
+
* root — never the live React world root. During Play the live connector
|
|
28
|
+
* owns the React root (design-time layers are suspended, `design-time-
|
|
29
|
+
* layers.ts`); opening/activating a story document must NOT stop Play and
|
|
30
|
+
* does not pretend the story is mounted on the root — it only drops the T6.3
|
|
31
|
+
* input gate (`onActivate` writes the store's `'scene'` tab) exactly like
|
|
32
|
+
* activating any non-Game document. What Stop restores on the root stays
|
|
33
|
+
* owned by `design-time-layers.ts`'s remembered per-root story selection.
|
|
34
|
+
*
|
|
35
|
+
* Per-document runtime state (arg overrides, action log, play results, the
|
|
36
|
+
* live preview container) lives in a module-scope map in the house
|
|
37
|
+
* `useSyncExternalStore` shape; dropped on document dispose.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
41
|
+
import type { EditorAction } from '../action-registry';
|
|
42
|
+
import { Button } from '../design-system';
|
|
43
|
+
import { createHmrRegistrationGroup } from '../hmr-registration-group';
|
|
44
|
+
import { registerInspectorSections } from '../inspector-section-registry';
|
|
45
|
+
import type { ComposedProjectStory } from '../stories/compose-project-stories';
|
|
46
|
+
import { mountIsolatedStory, runStoryPlay } from '../stories/StoryPreviewMount';
|
|
47
|
+
import { getProjectStoryModules, subscribeProjectStoryModules } from '../stories/story-registry';
|
|
48
|
+
import {
|
|
49
|
+
activeWorkspaceDocument,
|
|
50
|
+
openWorkspaceDocument,
|
|
51
|
+
type WorkspaceDocumentContentProps,
|
|
52
|
+
} from '../workspace-document-registry';
|
|
53
|
+
import { registerWorkspaceUtility } from '../workspace-utility-registry';
|
|
54
|
+
import { type Resolution, ResolutionPicker } from './ResolutionPicker';
|
|
55
|
+
|
|
56
|
+
/** One logged action-arg invocation (the actions-addon semantic). */
|
|
57
|
+
export interface StoryActionEntry {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly args: readonly unknown[];
|
|
60
|
+
readonly time: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StoryAccessibilityViolation {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly impact: string | null;
|
|
66
|
+
readonly help: string;
|
|
67
|
+
readonly helpUrl: string;
|
|
68
|
+
readonly nodes: readonly { readonly html: string; readonly target: readonly string[] }[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StoryDocumentState {
|
|
72
|
+
readonly modulePath: string;
|
|
73
|
+
readonly storyName: string;
|
|
74
|
+
readonly mode: 'preview' | 'docs';
|
|
75
|
+
readonly store: StoryDocumentStore;
|
|
76
|
+
argOverrides: Record<string, unknown>;
|
|
77
|
+
actionLog: StoryActionEntry[];
|
|
78
|
+
playResult: { ran: boolean; error?: string } | null;
|
|
79
|
+
accessibility:
|
|
80
|
+
| { status: 'idle' | 'running'; violations: StoryAccessibilityViolation[]; error?: undefined }
|
|
81
|
+
| { status: 'complete'; violations: StoryAccessibilityViolation[]; error?: undefined }
|
|
82
|
+
| { status: 'error'; violations: StoryAccessibilityViolation[]; error: string };
|
|
83
|
+
/** The live isolated preview container (set by the content on mount) —
|
|
84
|
+
* what `play()` receives as its `canvasElement` scope. */
|
|
85
|
+
container: HTMLElement | null;
|
|
86
|
+
/** True only after the current composed story + loaders have mounted. */
|
|
87
|
+
previewReady: boolean;
|
|
88
|
+
/** Invalidates async play/accessibility results across remounts. */
|
|
89
|
+
previewRevision: number;
|
|
90
|
+
resolution: Resolution;
|
|
91
|
+
scale: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const _states = new Map<string, StoryDocumentState>();
|
|
95
|
+
let _version = 0;
|
|
96
|
+
const _listeners = new Set<() => void>();
|
|
97
|
+
|
|
98
|
+
function notifyChanged(): void {
|
|
99
|
+
_version++;
|
|
100
|
+
for (const fn of _listeners) fn();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Subscribe to story-document state changes. Returns unsubscribe. */
|
|
104
|
+
export function subscribeStoryDocuments(fn: () => void): () => void {
|
|
105
|
+
_listeners.add(fn);
|
|
106
|
+
return () => {
|
|
107
|
+
_listeners.delete(fn);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Monotonic change counter — the `getSnapshot` for `useSyncExternalStore`. */
|
|
112
|
+
export function storyDocumentsVersion(): number {
|
|
113
|
+
return _version;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Stable §7.1 identity for one story document. */
|
|
117
|
+
export function storyDocumentId(modulePath: string, storyName: string): string {
|
|
118
|
+
return `story:${modulePath}#${storyName}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Stable identity for the Storybook Docs companion document. */
|
|
122
|
+
export function storyDocsDocumentId(modulePath: string, storyName: string): string {
|
|
123
|
+
return `story-docs:${modulePath}#${storyName}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The state behind an open story document id, or `undefined`. */
|
|
127
|
+
export function storyDocumentState(id: string): StoryDocumentState | undefined {
|
|
128
|
+
return _states.get(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The ACTIVE center document's story state, or `null` when the active
|
|
132
|
+
* document isn't a story (utilities/inspector section gate on this). */
|
|
133
|
+
export function activeStoryDocumentState(): (StoryDocumentState & { id: string }) | null {
|
|
134
|
+
const active = activeWorkspaceDocument();
|
|
135
|
+
if (!active || active.descriptor.kind !== 'story') return null;
|
|
136
|
+
const state = _states.get(active.descriptor.id);
|
|
137
|
+
return state?.mode === 'preview' ? { ...state, id: active.descriptor.id } : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Set one arg override (Inspector Story Args section) — remounts the story
|
|
141
|
+
* against the merged args. `value === undefined` CLEARS the override (the
|
|
142
|
+
* `delete`-the-key convention used elsewhere, e.g. `MaterialSection.tsx`'s
|
|
143
|
+
* `setField`) rather than storing `undefined`/`NaN` — the composed-args
|
|
144
|
+
* spread merge (`composedStoryArgs`/`activeStoryDocumentState` callers) only
|
|
145
|
+
* falls back to the authored value when the key is ABSENT, so a stored
|
|
146
|
+
* `NaN` (e.g. from an emptied number input) would otherwise win over the
|
|
147
|
+
* authored value and also make the input go from controlled to `NaN`. */
|
|
148
|
+
export function setStoryArgOverride(id: string, key: string, value: unknown): void {
|
|
149
|
+
const state = _states.get(id);
|
|
150
|
+
if (!state) return;
|
|
151
|
+
if (value === undefined) {
|
|
152
|
+
const next = { ...state.argOverrides };
|
|
153
|
+
delete next[key];
|
|
154
|
+
state.argOverrides = next;
|
|
155
|
+
} else {
|
|
156
|
+
state.argOverrides = { ...state.argOverrides, [key]: value };
|
|
157
|
+
}
|
|
158
|
+
notifyChanged();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function logStoryAction(id: string, name: string, args: readonly unknown[]): void {
|
|
162
|
+
const state = _states.get(id);
|
|
163
|
+
if (!state) return;
|
|
164
|
+
state.actionLog = [...state.actionLog, { name, args, time: Date.now() }].slice(-200);
|
|
165
|
+
notifyChanged();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Clear the action log (Actions utility affordance). */
|
|
169
|
+
export function clearStoryActionLog(id: string): void {
|
|
170
|
+
const state = _states.get(id);
|
|
171
|
+
if (!state || state.actionLog.length === 0) return;
|
|
172
|
+
state.actionLog = [];
|
|
173
|
+
notifyChanged();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The narrow store surface story documents need (T6.3 gate hand-off —
|
|
177
|
+
* same contract as `asset-documents.tsx`). */
|
|
178
|
+
export interface StoryDocumentStore {
|
|
179
|
+
setActiveViewportTab(tab: 'scene' | 'game'): void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Open (or activate) the center document for one named story. §4.2: this
|
|
184
|
+
* never touches the play session — during Play it only opens a tab; the
|
|
185
|
+
* live connector keeps owning the React root.
|
|
186
|
+
*/
|
|
187
|
+
export function openStoryDocument(
|
|
188
|
+
store: StoryDocumentStore,
|
|
189
|
+
modulePath: string,
|
|
190
|
+
storyName: string,
|
|
191
|
+
): string {
|
|
192
|
+
ensureStoryContributionsRegistered();
|
|
193
|
+
const id = storyDocumentId(modulePath, storyName);
|
|
194
|
+
if (!_states.has(id)) {
|
|
195
|
+
_states.set(id, {
|
|
196
|
+
modulePath,
|
|
197
|
+
storyName,
|
|
198
|
+
mode: 'preview',
|
|
199
|
+
store,
|
|
200
|
+
argOverrides: {},
|
|
201
|
+
actionLog: [],
|
|
202
|
+
playResult: null,
|
|
203
|
+
accessibility: { status: 'idle', violations: [] },
|
|
204
|
+
container: null,
|
|
205
|
+
previewReady: false,
|
|
206
|
+
previewRevision: 0,
|
|
207
|
+
resolution: { label: 'Fill', width: null, height: null },
|
|
208
|
+
scale: 1,
|
|
209
|
+
});
|
|
210
|
+
notifyChanged();
|
|
211
|
+
}
|
|
212
|
+
return openWorkspaceDocument({
|
|
213
|
+
id,
|
|
214
|
+
// §8 — the story's export name exactly as authored in the CSF module
|
|
215
|
+
// (`Default`, `Playing`), never a humanized/uppercased variant.
|
|
216
|
+
title: storyName,
|
|
217
|
+
kind: 'story',
|
|
218
|
+
provenance: { sourcePath: modulePath },
|
|
219
|
+
Content: StoryDocumentContent,
|
|
220
|
+
Toolbar: StoryDocumentToolbar,
|
|
221
|
+
closeable: true,
|
|
222
|
+
onActivate: () => store.setActiveViewportTab('scene'),
|
|
223
|
+
onDispose: (documentId) => {
|
|
224
|
+
if (_states.delete(documentId)) notifyChanged();
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Open the composed story's Storybook Docs companion as a center document. */
|
|
230
|
+
export function openStoryDocsDocument(
|
|
231
|
+
store: StoryDocumentStore,
|
|
232
|
+
modulePath: string,
|
|
233
|
+
storyName: string,
|
|
234
|
+
): string {
|
|
235
|
+
const id = storyDocsDocumentId(modulePath, storyName);
|
|
236
|
+
if (!_states.has(id)) {
|
|
237
|
+
_states.set(id, {
|
|
238
|
+
modulePath,
|
|
239
|
+
storyName,
|
|
240
|
+
mode: 'docs',
|
|
241
|
+
store,
|
|
242
|
+
argOverrides: {},
|
|
243
|
+
actionLog: [],
|
|
244
|
+
playResult: null,
|
|
245
|
+
accessibility: { status: 'idle', violations: [] },
|
|
246
|
+
container: null,
|
|
247
|
+
previewReady: false,
|
|
248
|
+
previewRevision: 0,
|
|
249
|
+
resolution: { label: 'Fill', width: null, height: null },
|
|
250
|
+
scale: 1,
|
|
251
|
+
});
|
|
252
|
+
notifyChanged();
|
|
253
|
+
}
|
|
254
|
+
return openWorkspaceDocument({
|
|
255
|
+
id,
|
|
256
|
+
title: `${storyName} — Docs`,
|
|
257
|
+
kind: 'story',
|
|
258
|
+
provenance: { sourcePath: modulePath },
|
|
259
|
+
Content: StoryDocsDocumentContent,
|
|
260
|
+
closeable: true,
|
|
261
|
+
onActivate: () => store.setActiveViewportTab('scene'),
|
|
262
|
+
onDispose: (documentId) => {
|
|
263
|
+
if (_states.delete(documentId)) notifyChanged();
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Open a story document from a hierarchy story ROW (§4.2 "story rows live
|
|
270
|
+
* beneath their React root"): resolves a composed story id
|
|
271
|
+
* (`ReactWorldAuthoringAdapter`'s portable-story node ids ARE Storybook's
|
|
272
|
+
* composed ids) back to its module + export through the live story registry.
|
|
273
|
+
* Returns `false` when no discovered portable story carries that id — the
|
|
274
|
+
* caller keeps its existing behavior (no fabricated documents).
|
|
275
|
+
*/
|
|
276
|
+
export function openStoryDocumentByStoryId(store: StoryDocumentStore, storyId: string): boolean {
|
|
277
|
+
for (const module_ of getProjectStoryModules()) {
|
|
278
|
+
if (!module_.ok) continue;
|
|
279
|
+
for (const story of module_.stories) {
|
|
280
|
+
if (story.id === storyId) {
|
|
281
|
+
openStoryDocument(store, module_.modulePath, story.name);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Command-palette discovery for every valid CSF story, including modules
|
|
290
|
+
* that are not associated with a mounted React root. React-associated stories
|
|
291
|
+
* also remain available contextually through Hierarchy and Inspector. */
|
|
292
|
+
export function buildStoryActions(store: StoryDocumentStore): EditorAction[] {
|
|
293
|
+
const actions: EditorAction[] = [];
|
|
294
|
+
for (const module_ of getProjectStoryModules()) {
|
|
295
|
+
if (!module_.ok) continue;
|
|
296
|
+
const moduleName = module_.modulePath.split('/').pop() ?? module_.modulePath;
|
|
297
|
+
for (const story of module_.stories) {
|
|
298
|
+
actions.push({
|
|
299
|
+
id: `story:${storyDocumentId(module_.modulePath, story.name)}`,
|
|
300
|
+
label: `Open Story: ${story.name} — ${moduleName}`,
|
|
301
|
+
category: 'action',
|
|
302
|
+
execute: () => void openStoryDocument(store, module_.modulePath, story.name),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return actions;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Test-only reset — drops all story-document state (pair with the document
|
|
310
|
+
* registry's own reset). */
|
|
311
|
+
export function __resetStoryDocumentsForTest(): void {
|
|
312
|
+
_states.clear();
|
|
313
|
+
notifyChanged();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- Story resolution against the live registry ---------------------------
|
|
317
|
+
|
|
318
|
+
function findComposedStory(
|
|
319
|
+
modulePath: string,
|
|
320
|
+
storyName: string,
|
|
321
|
+
): { story: ComposedProjectStory | undefined; moduleError: string | undefined } {
|
|
322
|
+
const module_ = getProjectStoryModules().find((m) => m.modulePath === modulePath);
|
|
323
|
+
if (!module_) return { story: undefined, moduleError: undefined };
|
|
324
|
+
if (!module_.ok) return { story: undefined, moduleError: module_.error };
|
|
325
|
+
return { story: module_.stories.find((s) => s.name === storyName), moduleError: undefined };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A composed story's own args (Storybook attaches them to the portable
|
|
329
|
+
* component). Defensive read — `{}` when absent. */
|
|
330
|
+
export function composedStoryArgs(story: ComposedProjectStory): Record<string, unknown> {
|
|
331
|
+
const args = (story.Component as unknown as Record<string, unknown>)['args'];
|
|
332
|
+
return args && typeof args === 'object' ? (args as Record<string, unknown>) : {};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Merge order: composed args ← wrapped action spies ← user overrides. The
|
|
336
|
+
* spies re-wrap the ORIGINAL function so authored handlers still run. */
|
|
337
|
+
function buildOverrideProps(id: string, story: ComposedProjectStory): Record<string, unknown> {
|
|
338
|
+
const state = _states.get(id);
|
|
339
|
+
const overrides: Record<string, unknown> = {};
|
|
340
|
+
for (const [key, value] of Object.entries(composedStoryArgs(story))) {
|
|
341
|
+
if (typeof value === 'function') {
|
|
342
|
+
overrides[key] = (...callArgs: unknown[]) => {
|
|
343
|
+
logStoryAction(id, key, callArgs);
|
|
344
|
+
return (value as (...a: unknown[]) => unknown)(...callArgs);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { ...overrides, ...(state?.argOverrides ?? {}) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Document content + toolbar -------------------------------------------
|
|
352
|
+
|
|
353
|
+
function StoryDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
354
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
355
|
+
// Re-resolve the composed story on every registry refresh (project change,
|
|
356
|
+
// `story-files-changed` SSE) — same live-rescan physics as the Stories dock.
|
|
357
|
+
useSyncExternalStore(subscribeProjectStoryModules, () => getProjectStoryModules());
|
|
358
|
+
const state = _states.get(documentId);
|
|
359
|
+
const outerRef = useRef<HTMLDivElement | null>(null);
|
|
360
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
361
|
+
const [loading, setLoading] = useState(false);
|
|
362
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
363
|
+
const { story, moduleError } = state
|
|
364
|
+
? findComposedStory(state.modulePath, state.storyName)
|
|
365
|
+
: { story: undefined, moduleError: undefined };
|
|
366
|
+
// Arg-override identity for the remount effect below.
|
|
367
|
+
const overridesKey = state ? JSON.stringify(state.argOverrides) : '';
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const element = outerRef.current;
|
|
371
|
+
if (!element) return;
|
|
372
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
373
|
+
setContainerSize({ width: element.clientWidth, height: element.clientHeight });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
377
|
+
if (!entry) return;
|
|
378
|
+
setContainerSize({ width: entry.contentRect.width, height: entry.contentRect.height });
|
|
379
|
+
});
|
|
380
|
+
observer.observe(element);
|
|
381
|
+
return () => observer.disconnect();
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
const container = containerRef.current;
|
|
386
|
+
if (!container || !state) return;
|
|
387
|
+
if (state.container !== container) {
|
|
388
|
+
state.container = container; // play()'s canvasElement scope
|
|
389
|
+
notifyChanged();
|
|
390
|
+
}
|
|
391
|
+
const previewRevision = state.previewRevision + 1;
|
|
392
|
+
state.previewRevision = previewRevision;
|
|
393
|
+
state.previewReady = false;
|
|
394
|
+
state.playResult = null;
|
|
395
|
+
state.accessibility = { status: 'idle', violations: [] };
|
|
396
|
+
notifyChanged();
|
|
397
|
+
if (!story) {
|
|
398
|
+
setLoading(false);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let cancelled = false;
|
|
402
|
+
let unmountFn: (() => void) | undefined;
|
|
403
|
+
setLoading(true);
|
|
404
|
+
void mountIsolatedStory(
|
|
405
|
+
container,
|
|
406
|
+
state.modulePath,
|
|
407
|
+
state.storyName,
|
|
408
|
+
story.Component,
|
|
409
|
+
buildOverrideProps(documentId, story),
|
|
410
|
+
).then((result) => {
|
|
411
|
+
if (cancelled) {
|
|
412
|
+
result?.unmount();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!result) {
|
|
416
|
+
if (_states.get(documentId)?.previewRevision === previewRevision) setLoading(false);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const current = _states.get(documentId);
|
|
420
|
+
if (!current || current.previewRevision !== previewRevision) {
|
|
421
|
+
result.unmount();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
unmountFn = result.unmount;
|
|
425
|
+
current.previewReady = true;
|
|
426
|
+
setLoading(false);
|
|
427
|
+
notifyChanged();
|
|
428
|
+
});
|
|
429
|
+
return () => {
|
|
430
|
+
cancelled = true;
|
|
431
|
+
unmountFn?.();
|
|
432
|
+
};
|
|
433
|
+
// `story.Component` identity changes on registry refresh; overridesKey on
|
|
434
|
+
// arg edits — both remount, which is exactly the wanted behavior.
|
|
435
|
+
}, [documentId, state, story, overridesKey]);
|
|
436
|
+
|
|
437
|
+
const isFill = state?.resolution.width === null;
|
|
438
|
+
const scale =
|
|
439
|
+
!state || isFill
|
|
440
|
+
? 1
|
|
441
|
+
: Math.min(
|
|
442
|
+
containerSize.width / state.resolution.width!,
|
|
443
|
+
containerSize.height / state.resolution.height!,
|
|
444
|
+
1,
|
|
445
|
+
);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (!state || !Number.isFinite(scale) || scale <= 0 || state.scale === scale) return;
|
|
448
|
+
state.scale = scale;
|
|
449
|
+
notifyChanged();
|
|
450
|
+
}, [scale, state]);
|
|
451
|
+
|
|
452
|
+
if (!state) {
|
|
453
|
+
return (
|
|
454
|
+
<div
|
|
455
|
+
style={{ padding: 12, fontSize: 12, color: themeVars.content.muted, pointerEvents: 'auto' }}
|
|
456
|
+
>
|
|
457
|
+
This story document is no longer available.
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div
|
|
464
|
+
style={{
|
|
465
|
+
flex: 1,
|
|
466
|
+
minHeight: 0,
|
|
467
|
+
display: 'flex',
|
|
468
|
+
flexDirection: 'column',
|
|
469
|
+
pointerEvents: 'auto',
|
|
470
|
+
background: themeVars.surface.panel,
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
{!story && (
|
|
474
|
+
<div
|
|
475
|
+
data-testid="story-document-missing"
|
|
476
|
+
style={{ padding: 12, fontSize: 12, color: themeVars.semantic.danger, maxWidth: 560 }}
|
|
477
|
+
>
|
|
478
|
+
<div style={{ fontWeight: 600, marginBottom: 4 }}>
|
|
479
|
+
{state.modulePath} — no story export named <code>{state.storyName}</code>
|
|
480
|
+
</div>
|
|
481
|
+
<div style={{ color: themeVars.content.muted, whiteSpace: 'pre-wrap' }}>
|
|
482
|
+
{moduleError ?? 'It may have been renamed or removed; close this document.'}
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
{loading && (
|
|
487
|
+
<div
|
|
488
|
+
data-testid="story-loading"
|
|
489
|
+
style={{
|
|
490
|
+
padding: '4px 10px',
|
|
491
|
+
fontSize: 10,
|
|
492
|
+
color: themeVars.content.muted,
|
|
493
|
+
fontStyle: 'italic',
|
|
494
|
+
}}
|
|
495
|
+
>
|
|
496
|
+
loading…
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
<div
|
|
500
|
+
ref={outerRef}
|
|
501
|
+
style={{
|
|
502
|
+
position: 'relative',
|
|
503
|
+
flex: 1,
|
|
504
|
+
minHeight: 0,
|
|
505
|
+
overflow: 'auto',
|
|
506
|
+
background: themeVars.surface.inset,
|
|
507
|
+
display: 'flex',
|
|
508
|
+
alignItems: 'center',
|
|
509
|
+
justifyContent: 'center',
|
|
510
|
+
}}
|
|
511
|
+
>
|
|
512
|
+
<div
|
|
513
|
+
style={
|
|
514
|
+
isFill
|
|
515
|
+
? { position: 'absolute', inset: 0 }
|
|
516
|
+
: {
|
|
517
|
+
position: 'relative',
|
|
518
|
+
width: state.resolution.width! * scale,
|
|
519
|
+
height: state.resolution.height! * scale,
|
|
520
|
+
flex: '0 0 auto',
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
>
|
|
524
|
+
<div
|
|
525
|
+
data-testid="story-preview-root"
|
|
526
|
+
ref={containerRef}
|
|
527
|
+
// A story owns the whole selected device canvas. Containment
|
|
528
|
+
// keeps fixed/full-page UI inside that canvas, never over editor
|
|
529
|
+
// chrome.
|
|
530
|
+
style={{
|
|
531
|
+
position: 'absolute',
|
|
532
|
+
inset: 0,
|
|
533
|
+
isolation: 'isolate',
|
|
534
|
+
contain: 'layout paint',
|
|
535
|
+
overflow: 'auto',
|
|
536
|
+
background: themeVars.surface.inset,
|
|
537
|
+
...(isFill
|
|
538
|
+
? {}
|
|
539
|
+
: {
|
|
540
|
+
width: state.resolution.width!,
|
|
541
|
+
height: state.resolution.height!,
|
|
542
|
+
transform: `scale(${scale})`,
|
|
543
|
+
transformOrigin: 'top left',
|
|
544
|
+
}),
|
|
545
|
+
}}
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function docsDescription(story: ComposedProjectStory): { component?: string; story?: string } {
|
|
554
|
+
const docs = story.parameters['docs'];
|
|
555
|
+
if (!docs || typeof docs !== 'object') return {};
|
|
556
|
+
const description = (docs as Record<string, unknown>)['description'];
|
|
557
|
+
if (typeof description === 'string') return { story: description };
|
|
558
|
+
if (!description || typeof description !== 'object') return {};
|
|
559
|
+
const record = description as Record<string, unknown>;
|
|
560
|
+
return {
|
|
561
|
+
...(typeof record['component'] === 'string' ? { component: record['component'] } : {}),
|
|
562
|
+
...(typeof record['story'] === 'string' ? { story: record['story'] } : {}),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Storybook Docs companion: composed CSF metadata, canvas, and args table. */
|
|
567
|
+
export function StoryDocsDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
568
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
569
|
+
useSyncExternalStore(subscribeProjectStoryModules, () => getProjectStoryModules());
|
|
570
|
+
const state = _states.get(documentId);
|
|
571
|
+
const previewRef = useRef<HTMLDivElement | null>(null);
|
|
572
|
+
const { story, moduleError } = state
|
|
573
|
+
? findComposedStory(state.modulePath, state.storyName)
|
|
574
|
+
: { story: undefined, moduleError: undefined };
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
const container = previewRef.current;
|
|
577
|
+
if (!container || !state || !story) return;
|
|
578
|
+
let disposed = false;
|
|
579
|
+
let unmount: (() => void) | null = null;
|
|
580
|
+
void mountIsolatedStory(container, state.modulePath, state.storyName, story.Component).then(
|
|
581
|
+
(result) => {
|
|
582
|
+
if (!result) return;
|
|
583
|
+
if (disposed) result.unmount();
|
|
584
|
+
else unmount = result.unmount;
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
return () => {
|
|
588
|
+
disposed = true;
|
|
589
|
+
unmount?.();
|
|
590
|
+
};
|
|
591
|
+
}, [state, story]);
|
|
592
|
+
|
|
593
|
+
if (!state || !story) {
|
|
594
|
+
return (
|
|
595
|
+
<div style={{ padding: 16, color: themeVars.semantic.danger, fontSize: 12 }}>
|
|
596
|
+
{moduleError ?? 'This Storybook Docs document is no longer available.'}
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const description = docsDescription(story);
|
|
601
|
+
const args = story.args;
|
|
602
|
+
return (
|
|
603
|
+
<article
|
|
604
|
+
data-testid="story-docs-document"
|
|
605
|
+
style={{
|
|
606
|
+
flex: 1,
|
|
607
|
+
minHeight: 0,
|
|
608
|
+
overflow: 'auto',
|
|
609
|
+
pointerEvents: 'auto',
|
|
610
|
+
background: themeVars.surface.panel,
|
|
611
|
+
padding: '24px clamp(18px, 5vw, 64px)',
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<div
|
|
615
|
+
style={{
|
|
616
|
+
maxWidth: 920,
|
|
617
|
+
margin: '0 auto',
|
|
618
|
+
display: 'flex',
|
|
619
|
+
flexDirection: 'column',
|
|
620
|
+
gap: 22,
|
|
621
|
+
}}
|
|
622
|
+
>
|
|
623
|
+
<header>
|
|
624
|
+
<div style={{ color: themeVars.content.dim, fontSize: 11, marginBottom: 5 }}>
|
|
625
|
+
{story.title ?? state.modulePath}
|
|
626
|
+
</div>
|
|
627
|
+
<h1 style={{ color: themeVars.content.primary, fontSize: 24, margin: 0 }}>
|
|
628
|
+
{story.label}
|
|
629
|
+
</h1>
|
|
630
|
+
{(description.story ?? description.component) && (
|
|
631
|
+
<p
|
|
632
|
+
style={{
|
|
633
|
+
color: themeVars.content.primary,
|
|
634
|
+
lineHeight: 1.6,
|
|
635
|
+
maxWidth: 760,
|
|
636
|
+
whiteSpace: 'pre-wrap',
|
|
637
|
+
}}
|
|
638
|
+
>
|
|
639
|
+
{description.story ?? description.component}
|
|
640
|
+
</p>
|
|
641
|
+
)}
|
|
642
|
+
</header>
|
|
643
|
+
<section>
|
|
644
|
+
<h2 style={{ color: themeVars.content.primary, fontSize: 14 }}>Canvas</h2>
|
|
645
|
+
<div
|
|
646
|
+
ref={previewRef}
|
|
647
|
+
data-testid="story-docs-canvas"
|
|
648
|
+
style={{
|
|
649
|
+
position: 'relative',
|
|
650
|
+
isolation: 'isolate',
|
|
651
|
+
contain: 'layout paint',
|
|
652
|
+
minHeight: 180,
|
|
653
|
+
padding: 18,
|
|
654
|
+
overflow: 'auto',
|
|
655
|
+
background: themeVars.surface.inset,
|
|
656
|
+
border: `1px solid ${themeVars.boundary.default}`,
|
|
657
|
+
borderRadius: themeVars.shape.medium,
|
|
658
|
+
}}
|
|
659
|
+
/>
|
|
660
|
+
</section>
|
|
661
|
+
<section>
|
|
662
|
+
<h2 style={{ color: themeVars.content.primary, fontSize: 14 }}>Args</h2>
|
|
663
|
+
<div
|
|
664
|
+
style={{
|
|
665
|
+
border: `1px solid ${themeVars.boundary.default}`,
|
|
666
|
+
borderRadius: themeVars.shape.medium,
|
|
667
|
+
overflow: 'hidden',
|
|
668
|
+
}}
|
|
669
|
+
>
|
|
670
|
+
{Object.entries(args).map(([name, value]) => (
|
|
671
|
+
<div
|
|
672
|
+
key={name}
|
|
673
|
+
style={{
|
|
674
|
+
display: 'grid',
|
|
675
|
+
gridTemplateColumns: 'minmax(120px, .4fr) minmax(140px, .6fr) 1fr',
|
|
676
|
+
gap: 12,
|
|
677
|
+
padding: '8px 10px',
|
|
678
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
679
|
+
fontSize: 11,
|
|
680
|
+
}}
|
|
681
|
+
>
|
|
682
|
+
<code style={{ color: themeVars.accent.default }}>{name}</code>
|
|
683
|
+
<span style={{ color: themeVars.content.muted }}>
|
|
684
|
+
{Array.isArray(value) ? 'array' : typeof value}
|
|
685
|
+
</span>
|
|
686
|
+
<code
|
|
687
|
+
style={{
|
|
688
|
+
color: themeVars.content.primary,
|
|
689
|
+
overflow: 'hidden',
|
|
690
|
+
textOverflow: 'ellipsis',
|
|
691
|
+
}}
|
|
692
|
+
>
|
|
693
|
+
{typeof value === 'function' ? 'function' : JSON.stringify(value)}
|
|
694
|
+
</code>
|
|
695
|
+
</div>
|
|
696
|
+
))}
|
|
697
|
+
{Object.keys(args).length === 0 && (
|
|
698
|
+
<div style={{ padding: 10, color: themeVars.content.dim, fontSize: 11 }}>
|
|
699
|
+
This story declares no args.
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
702
|
+
</div>
|
|
703
|
+
</section>
|
|
704
|
+
<footer style={{ color: themeVars.content.dim, fontSize: 10 }}>
|
|
705
|
+
{state.modulePath} · {state.storyName}
|
|
706
|
+
</footer>
|
|
707
|
+
</div>
|
|
708
|
+
</article>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Document-local toolbar (§7.1 contribution): source module + Run play(). */
|
|
713
|
+
function StoryDocumentToolbar({ documentId }: WorkspaceDocumentContentProps) {
|
|
714
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
715
|
+
useSyncExternalStore(subscribeProjectStoryModules, () => getProjectStoryModules());
|
|
716
|
+
const state = _states.get(documentId);
|
|
717
|
+
if (!state) return null;
|
|
718
|
+
const { story } = findComposedStory(state.modulePath, state.storyName);
|
|
719
|
+
const hasPlay = typeof story?.Component.play === 'function';
|
|
720
|
+
const result = state.playResult;
|
|
721
|
+
return (
|
|
722
|
+
<div
|
|
723
|
+
data-testid="story-document-toolbar"
|
|
724
|
+
style={{
|
|
725
|
+
display: 'flex',
|
|
726
|
+
alignItems: 'center',
|
|
727
|
+
gap: 8,
|
|
728
|
+
padding: '3px 10px',
|
|
729
|
+
fontSize: 10,
|
|
730
|
+
color: themeVars.content.muted,
|
|
731
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
732
|
+
}}
|
|
733
|
+
>
|
|
734
|
+
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
735
|
+
{state.modulePath} —{' '}
|
|
736
|
+
<code style={{ color: themeVars.accent.default }}>{state.storyName}</code>
|
|
737
|
+
</span>
|
|
738
|
+
<ResolutionPicker
|
|
739
|
+
resolution={state.resolution}
|
|
740
|
+
scale={state.scale}
|
|
741
|
+
onResolutionChange={(resolution) => {
|
|
742
|
+
state.resolution = resolution;
|
|
743
|
+
notifyChanged();
|
|
744
|
+
}}
|
|
745
|
+
/>
|
|
746
|
+
<Button
|
|
747
|
+
size="compact"
|
|
748
|
+
data-testid="story-open-docs"
|
|
749
|
+
onClick={() => openStoryDocsDocument(state.store, state.modulePath, state.storyName)}
|
|
750
|
+
style={{ flexShrink: 0 }}
|
|
751
|
+
>
|
|
752
|
+
Docs
|
|
753
|
+
</Button>
|
|
754
|
+
{hasPlay && story && (
|
|
755
|
+
<Button
|
|
756
|
+
variant="outline"
|
|
757
|
+
size="compact"
|
|
758
|
+
data-testid="story-run-play"
|
|
759
|
+
onClick={async () => {
|
|
760
|
+
if (!state.container || !state.previewReady) return;
|
|
761
|
+
const previewRevision = state.previewRevision;
|
|
762
|
+
const playResult = await runStoryPlay(
|
|
763
|
+
state.modulePath,
|
|
764
|
+
state.storyName,
|
|
765
|
+
story.Component,
|
|
766
|
+
state.container,
|
|
767
|
+
);
|
|
768
|
+
const current = _states.get(documentId);
|
|
769
|
+
if (current?.previewReady && current.previewRevision === previewRevision) {
|
|
770
|
+
current.playResult = playResult;
|
|
771
|
+
notifyChanged();
|
|
772
|
+
}
|
|
773
|
+
}}
|
|
774
|
+
disabled={!state.previewReady}
|
|
775
|
+
style={{ flexShrink: 0 }}
|
|
776
|
+
>
|
|
777
|
+
Run play()
|
|
778
|
+
</Button>
|
|
779
|
+
)}
|
|
780
|
+
{result?.ran && !result.error && (
|
|
781
|
+
<span
|
|
782
|
+
data-testid="story-play-result"
|
|
783
|
+
style={{ color: themeVars.semantic.success, fontWeight: 600 }}
|
|
784
|
+
>
|
|
785
|
+
play() ran
|
|
786
|
+
</span>
|
|
787
|
+
)}
|
|
788
|
+
{result?.error && (
|
|
789
|
+
<span data-testid="story-play-result" style={{ color: themeVars.semantic.danger }}>
|
|
790
|
+
{result.error}
|
|
791
|
+
</span>
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// --- Inspector "Story Args" section (§5: Storybook args → right Inspector) -
|
|
798
|
+
|
|
799
|
+
function ArgRow({ id, name, value }: { id: string; name: string; value: unknown }) {
|
|
800
|
+
return (
|
|
801
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0' }}>
|
|
802
|
+
<span
|
|
803
|
+
style={{
|
|
804
|
+
width: 90,
|
|
805
|
+
flexShrink: 0,
|
|
806
|
+
fontSize: 11,
|
|
807
|
+
color: themeVars.content.muted,
|
|
808
|
+
overflow: 'hidden',
|
|
809
|
+
textOverflow: 'ellipsis',
|
|
810
|
+
}}
|
|
811
|
+
title={name}
|
|
812
|
+
>
|
|
813
|
+
{name}
|
|
814
|
+
</span>
|
|
815
|
+
{typeof value === 'boolean' ? (
|
|
816
|
+
<Checkbox
|
|
817
|
+
className="vgai-checkbox"
|
|
818
|
+
data-testid={`story-arg-${name}`}
|
|
819
|
+
checked={value}
|
|
820
|
+
onChange={(e) => setStoryArgOverride(id, name, e.target.checked)}
|
|
821
|
+
/>
|
|
822
|
+
) : typeof value === 'number' ? (
|
|
823
|
+
<TextInput
|
|
824
|
+
type="number"
|
|
825
|
+
className="vgai-input"
|
|
826
|
+
data-testid={`story-arg-${name}`}
|
|
827
|
+
value={value}
|
|
828
|
+
onChange={(e) => {
|
|
829
|
+
// Empty/unparseable input clears the override (falls back to the
|
|
830
|
+
// authored arg) instead of setting NaN — `Number.parseFloat('')`
|
|
831
|
+
// is NaN, and storing NaN would both win over the authored value
|
|
832
|
+
// in the composed-args merge (see `setStoryArgOverride`'s doc
|
|
833
|
+
// comment) and make this a controlled input whose displayed
|
|
834
|
+
// `value` is `NaN`, which React warns about.
|
|
835
|
+
const raw = e.target.value;
|
|
836
|
+
const parsed = Number.parseFloat(raw);
|
|
837
|
+
setStoryArgOverride(id, name, raw === '' || Number.isNaN(parsed) ? undefined : parsed);
|
|
838
|
+
}}
|
|
839
|
+
style={{ width: 80 }}
|
|
840
|
+
/>
|
|
841
|
+
) : typeof value === 'string' ? (
|
|
842
|
+
<TextInput
|
|
843
|
+
type="text"
|
|
844
|
+
className="vgai-input"
|
|
845
|
+
data-testid={`story-arg-${name}`}
|
|
846
|
+
value={value}
|
|
847
|
+
onChange={(e) => setStoryArgOverride(id, name, e.target.value)}
|
|
848
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
849
|
+
/>
|
|
850
|
+
) : (
|
|
851
|
+
<span
|
|
852
|
+
style={{ fontSize: 11, color: themeVars.content.dim }}
|
|
853
|
+
data-testid={`story-arg-${name}`}
|
|
854
|
+
>
|
|
855
|
+
{typeof value === 'function' ? 'fn (logged to Actions)' : JSON.stringify(value)}
|
|
856
|
+
</span>
|
|
857
|
+
)}
|
|
858
|
+
</div>
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Registered section: renders while the ACTIVE center document is a story —
|
|
863
|
+
* the args of THAT story (composed args merged with live overrides). */
|
|
864
|
+
export function StoryArgsSection() {
|
|
865
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
866
|
+
useSyncExternalStore(subscribeProjectStoryModules, () => getProjectStoryModules());
|
|
867
|
+
const active = activeStoryDocumentState();
|
|
868
|
+
if (!active) return null;
|
|
869
|
+
const { story } = findComposedStory(active.modulePath, active.storyName);
|
|
870
|
+
const args = story ? { ...composedStoryArgs(story), ...active.argOverrides } : {};
|
|
871
|
+
const names = Object.keys(args);
|
|
872
|
+
return (
|
|
873
|
+
<div
|
|
874
|
+
data-testid="inspector-story-args"
|
|
875
|
+
style={{
|
|
876
|
+
pointerEvents: 'auto',
|
|
877
|
+
padding: 8,
|
|
878
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
879
|
+
}}
|
|
880
|
+
>
|
|
881
|
+
<div
|
|
882
|
+
style={{
|
|
883
|
+
fontSize: 11,
|
|
884
|
+
color: themeVars.content.dim,
|
|
885
|
+
textTransform: 'uppercase',
|
|
886
|
+
marginBottom: 4,
|
|
887
|
+
}}
|
|
888
|
+
>
|
|
889
|
+
Story Args
|
|
890
|
+
</div>
|
|
891
|
+
<div
|
|
892
|
+
style={{ fontSize: 11, color: themeVars.content.primary, fontWeight: 600, marginBottom: 4 }}
|
|
893
|
+
>
|
|
894
|
+
{active.storyName}
|
|
895
|
+
</div>
|
|
896
|
+
{names.length === 0 && (
|
|
897
|
+
<div style={{ fontSize: 11, color: themeVars.content.dim }}>
|
|
898
|
+
{story ? 'This story declares no args.' : 'Story source unavailable.'}
|
|
899
|
+
</div>
|
|
900
|
+
)}
|
|
901
|
+
{names.map((name) => (
|
|
902
|
+
<ArgRow key={name} id={active.id} name={name} value={args[name]} />
|
|
903
|
+
))}
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// --- Utilities: Actions + Interactions (§5 story-scoped utility tabs) ------
|
|
909
|
+
|
|
910
|
+
/** Actions utility content: the wrapped-arg call log of the active story. */
|
|
911
|
+
export function StoryActionsUtility() {
|
|
912
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
913
|
+
const active = activeStoryDocumentState();
|
|
914
|
+
if (!active) {
|
|
915
|
+
return (
|
|
916
|
+
<div style={{ padding: 12, fontSize: 12, color: themeVars.content.dim }}>
|
|
917
|
+
No active story.
|
|
918
|
+
</div>
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return (
|
|
922
|
+
<div
|
|
923
|
+
data-testid="story-actions-utility"
|
|
924
|
+
style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 8, fontSize: 11 }}
|
|
925
|
+
>
|
|
926
|
+
<Button
|
|
927
|
+
size="compact"
|
|
928
|
+
style={{ marginBottom: 6 }}
|
|
929
|
+
onClick={() => clearStoryActionLog(active.id)}
|
|
930
|
+
>
|
|
931
|
+
Clear
|
|
932
|
+
</Button>
|
|
933
|
+
{active.actionLog.length === 0 ? (
|
|
934
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
935
|
+
No actions logged — function-valued args are wrapped with logging spies; interact with the
|
|
936
|
+
story preview to see calls here.
|
|
937
|
+
</div>
|
|
938
|
+
) : (
|
|
939
|
+
active.actionLog.map((entry, i) => (
|
|
940
|
+
<div
|
|
941
|
+
key={i}
|
|
942
|
+
style={{ padding: '2px 0', borderBottom: `1px solid ${themeVars.boundary.default}` }}
|
|
943
|
+
>
|
|
944
|
+
<code style={{ color: themeVars.accent.default }}>{entry.name}</code>{' '}
|
|
945
|
+
<span style={{ color: themeVars.content.muted }}>
|
|
946
|
+
(
|
|
947
|
+
{entry.args
|
|
948
|
+
.map((a) => {
|
|
949
|
+
try {
|
|
950
|
+
return typeof a === 'object' ? JSON.stringify(a)?.slice(0, 80) : String(a);
|
|
951
|
+
} catch {
|
|
952
|
+
return String(a);
|
|
953
|
+
}
|
|
954
|
+
})
|
|
955
|
+
.join(', ')}
|
|
956
|
+
)
|
|
957
|
+
</span>
|
|
958
|
+
</div>
|
|
959
|
+
))
|
|
960
|
+
)}
|
|
961
|
+
</div>
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Interactions utility content: the active story's play() run results. */
|
|
966
|
+
export function StoryInteractionsUtility() {
|
|
967
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
968
|
+
const active = activeStoryDocumentState();
|
|
969
|
+
if (!active) {
|
|
970
|
+
return (
|
|
971
|
+
<div style={{ padding: 12, fontSize: 12, color: themeVars.content.dim }}>
|
|
972
|
+
No active story.
|
|
973
|
+
</div>
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const result = active.playResult;
|
|
977
|
+
return (
|
|
978
|
+
<div
|
|
979
|
+
data-testid="story-interactions-utility"
|
|
980
|
+
style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 12, fontSize: 12 }}
|
|
981
|
+
>
|
|
982
|
+
{!result && (
|
|
983
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
984
|
+
No interaction run yet — use the story document's “Run play()”.
|
|
985
|
+
</div>
|
|
986
|
+
)}
|
|
987
|
+
{result && !result.ran && (
|
|
988
|
+
<div style={{ color: themeVars.content.dim }}>This story declares no play() function.</div>
|
|
989
|
+
)}
|
|
990
|
+
{result?.ran && !result.error && (
|
|
991
|
+
<div style={{ color: themeVars.semantic.success, fontWeight: 600 }}>
|
|
992
|
+
play() ran without errors.
|
|
993
|
+
</div>
|
|
994
|
+
)}
|
|
995
|
+
{result?.error && (
|
|
996
|
+
<div style={{ color: themeVars.semantic.danger, whiteSpace: 'pre-wrap' }}>
|
|
997
|
+
{result.error}
|
|
998
|
+
</div>
|
|
999
|
+
)}
|
|
1000
|
+
</div>
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** Run axe-core against the active story's isolated preview root. */
|
|
1005
|
+
export async function runStoryAccessibility(id: string): Promise<void> {
|
|
1006
|
+
const state = _states.get(id);
|
|
1007
|
+
if (!state?.container || !state.previewReady) return;
|
|
1008
|
+
const container = state.container;
|
|
1009
|
+
const previewRevision = state.previewRevision;
|
|
1010
|
+
state.accessibility = { status: 'running', violations: [] };
|
|
1011
|
+
notifyChanged();
|
|
1012
|
+
try {
|
|
1013
|
+
const axe = (await import('axe-core')).default;
|
|
1014
|
+
const result = await axe.run(container);
|
|
1015
|
+
const current = _states.get(id);
|
|
1016
|
+
if (
|
|
1017
|
+
!current?.previewReady ||
|
|
1018
|
+
current.previewRevision !== previewRevision ||
|
|
1019
|
+
current.container !== container
|
|
1020
|
+
)
|
|
1021
|
+
return;
|
|
1022
|
+
current.accessibility = {
|
|
1023
|
+
status: 'complete',
|
|
1024
|
+
violations: result.violations.map((violation) => ({
|
|
1025
|
+
id: violation.id,
|
|
1026
|
+
impact: violation.impact ?? null,
|
|
1027
|
+
help: violation.help,
|
|
1028
|
+
helpUrl: violation.helpUrl,
|
|
1029
|
+
nodes: violation.nodes.map((node) => ({
|
|
1030
|
+
html: node.html,
|
|
1031
|
+
target: node.target.map(String),
|
|
1032
|
+
})),
|
|
1033
|
+
})),
|
|
1034
|
+
};
|
|
1035
|
+
notifyChanged();
|
|
1036
|
+
} catch (reason) {
|
|
1037
|
+
const current = _states.get(id);
|
|
1038
|
+
if (
|
|
1039
|
+
!current?.previewReady ||
|
|
1040
|
+
current.previewRevision !== previewRevision ||
|
|
1041
|
+
current.container !== container
|
|
1042
|
+
)
|
|
1043
|
+
return;
|
|
1044
|
+
current.accessibility = {
|
|
1045
|
+
status: 'error',
|
|
1046
|
+
violations: [],
|
|
1047
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
1048
|
+
};
|
|
1049
|
+
notifyChanged();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Story-scoped accessibility results, powered by the same axe engine Storybook uses. */
|
|
1054
|
+
export function StoryAccessibilityUtility() {
|
|
1055
|
+
useSyncExternalStore(subscribeStoryDocuments, storyDocumentsVersion);
|
|
1056
|
+
const active = activeStoryDocumentState();
|
|
1057
|
+
if (!active)
|
|
1058
|
+
return (
|
|
1059
|
+
<div style={{ padding: 12, fontSize: 12, color: themeVars.content.dim }}>
|
|
1060
|
+
No active story.
|
|
1061
|
+
</div>
|
|
1062
|
+
);
|
|
1063
|
+
const result = active.accessibility;
|
|
1064
|
+
return (
|
|
1065
|
+
<div
|
|
1066
|
+
data-testid="story-accessibility-utility"
|
|
1067
|
+
style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 10, fontSize: 11 }}
|
|
1068
|
+
>
|
|
1069
|
+
<Button
|
|
1070
|
+
variant="outline"
|
|
1071
|
+
size="compact"
|
|
1072
|
+
disabled={!active.container || !active.previewReady || result.status === 'running'}
|
|
1073
|
+
onClick={() => void runStoryAccessibility(active.id)}
|
|
1074
|
+
style={{ marginBottom: 8 }}
|
|
1075
|
+
>
|
|
1076
|
+
{result.status === 'running' ? 'Checking…' : 'Run accessibility check'}
|
|
1077
|
+
</Button>
|
|
1078
|
+
{result.status === 'idle' && (
|
|
1079
|
+
<div style={{ color: themeVars.content.dim }}>
|
|
1080
|
+
Run axe against the mounted story preview.
|
|
1081
|
+
</div>
|
|
1082
|
+
)}
|
|
1083
|
+
{result.status === 'error' && (
|
|
1084
|
+
<div style={{ color: themeVars.semantic.danger }}>{result.error}</div>
|
|
1085
|
+
)}
|
|
1086
|
+
{result.status === 'complete' && result.violations.length === 0 && (
|
|
1087
|
+
<div style={{ color: themeVars.semantic.success, fontWeight: 600 }}>
|
|
1088
|
+
No accessibility violations found.
|
|
1089
|
+
</div>
|
|
1090
|
+
)}
|
|
1091
|
+
{result.violations.map((violation) => (
|
|
1092
|
+
<details
|
|
1093
|
+
key={violation.id}
|
|
1094
|
+
style={{ borderBottom: `1px solid ${themeVars.boundary.default}`, padding: '6px 0' }}
|
|
1095
|
+
>
|
|
1096
|
+
<summary
|
|
1097
|
+
style={{
|
|
1098
|
+
color:
|
|
1099
|
+
violation.impact === 'critical' || violation.impact === 'serious'
|
|
1100
|
+
? themeVars.semantic.danger
|
|
1101
|
+
: themeVars.content.primary,
|
|
1102
|
+
cursor: 'pointer',
|
|
1103
|
+
}}
|
|
1104
|
+
>
|
|
1105
|
+
{violation.help} · {violation.nodes.length} node
|
|
1106
|
+
{violation.nodes.length === 1 ? '' : 's'}
|
|
1107
|
+
</summary>
|
|
1108
|
+
<a
|
|
1109
|
+
href={violation.helpUrl}
|
|
1110
|
+
target="_blank"
|
|
1111
|
+
rel="noreferrer"
|
|
1112
|
+
style={{ color: themeVars.accent.default, display: 'inline-block', margin: '5px 0' }}
|
|
1113
|
+
>
|
|
1114
|
+
How to fix
|
|
1115
|
+
</a>
|
|
1116
|
+
{violation.nodes.map((node, index) => (
|
|
1117
|
+
<pre
|
|
1118
|
+
key={`${violation.id}:${index}`}
|
|
1119
|
+
style={{
|
|
1120
|
+
color: themeVars.content.muted,
|
|
1121
|
+
fontFamily: 'var(--vgai-font-mono)',
|
|
1122
|
+
fontSize: 10,
|
|
1123
|
+
whiteSpace: 'pre-wrap',
|
|
1124
|
+
overflowWrap: 'anywhere',
|
|
1125
|
+
}}
|
|
1126
|
+
>
|
|
1127
|
+
{node.target.join(' ')}
|
|
1128
|
+
{`\n`}
|
|
1129
|
+
{node.html}
|
|
1130
|
+
</pre>
|
|
1131
|
+
))}
|
|
1132
|
+
</details>
|
|
1133
|
+
))}
|
|
1134
|
+
</div>
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const registrationGroup = createHmrRegistrationGroup(import.meta.hot, 'story-contributions');
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Idempotent one-time registration of the story-scoped shell contributions:
|
|
1142
|
+
* the Inspector `Story Args` section and the `Actions`/`Interactions`
|
|
1143
|
+
* utilities. Called from `openStoryDocument` (and importable directly) so
|
|
1144
|
+
* merely importing this module in a test doesn't mutate the registries.
|
|
1145
|
+
*/
|
|
1146
|
+
export function ensureStoryContributionsRegistered(): void {
|
|
1147
|
+
registrationGroup.ensure((track) => {
|
|
1148
|
+
track(
|
|
1149
|
+
registerInspectorSections({
|
|
1150
|
+
match: () => activeWorkspaceDocument()?.descriptor.kind === 'story',
|
|
1151
|
+
Section: StoryArgsSection,
|
|
1152
|
+
}),
|
|
1153
|
+
);
|
|
1154
|
+
const storyActive = () => activeStoryDocumentState() !== null;
|
|
1155
|
+
track(
|
|
1156
|
+
registerWorkspaceUtility({
|
|
1157
|
+
id: 'story-actions',
|
|
1158
|
+
title: 'Actions',
|
|
1159
|
+
Content: StoryActionsUtility,
|
|
1160
|
+
available: storyActive,
|
|
1161
|
+
}),
|
|
1162
|
+
);
|
|
1163
|
+
track(
|
|
1164
|
+
registerWorkspaceUtility({
|
|
1165
|
+
id: 'story-interactions',
|
|
1166
|
+
title: 'Interactions',
|
|
1167
|
+
Content: StoryInteractionsUtility,
|
|
1168
|
+
available: storyActive,
|
|
1169
|
+
}),
|
|
1170
|
+
);
|
|
1171
|
+
track(
|
|
1172
|
+
registerWorkspaceUtility({
|
|
1173
|
+
id: 'story-accessibility',
|
|
1174
|
+
title: 'Accessibility',
|
|
1175
|
+
Content: StoryAccessibilityUtility,
|
|
1176
|
+
available: storyActive,
|
|
1177
|
+
}),
|
|
1178
|
+
);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Test-only: allow re-registration after a registry reset. */
|
|
1183
|
+
export function __resetStoryContributionsForTest(): void {
|
|
1184
|
+
registrationGroup.reset();
|
|
1185
|
+
}
|