@volter/editor-game 0.5.65 → 0.5.67
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/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +3 -3
- package/contributions/generations.status.tsx +1 -1
- package/contributions/godot.palette.json +99 -0
- package/contributions/godot.style.ts +82 -0
- package/contributions/godot.view.ts +75 -0
- package/contributions/ingest.service.ts +4 -4
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +11 -5
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/react/react-inspector.service.ts +2 -2
- package/contributions/scene-document.service.ts +5 -5
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +8 -8
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.palette.json +102 -0
- package/contributions/unity.style.ts +72 -0
- package/contributions/unity.view.ts +82 -0
- package/contributions/unreal.palette.json +105 -0
- package/contributions/unreal.style.ts +66 -0
- package/contributions/unreal.view.ts +93 -0
- package/dist-node/serving.mjs +16 -0
- package/package.json +33 -16
- package/serving/index.ts +27 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +4 -4
- package/src/asset-budget/optimize-apply.ts +7 -7
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +16 -16
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +6 -6
- package/src/build/BuildProfilesPanel.tsx +3 -3
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +6 -6
- package/src/coverage/live-project-verbs.ts +2 -2
- package/src/coverage/native-system-coverage.ts +6 -6
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +5 -5
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +13 -53
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +2 -2
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +8 -8
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +6 -8
- package/src/generation/generation-documents.tsx +8 -8
- package/src/generation/generation-jobs.ts +2 -2
- package/src/generation/generation-presentation.ts +1 -1
- package/src/host/adapter-reach.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +97 -6
- package/src/host/api/configurations.ts +2 -2
- package/src/host/authoring/babylon-authoring-adapter.ts +8 -8
- package/src/host/authoring/contract-hierarchy-authoring.ts +1 -1
- package/src/host/authoring/contract-scenes-stories.ts +1 -1
- package/src/host/authoring/creation-site-related.ts +2 -2
- package/src/host/authoring/ephemeral-persistence.ts +1 -1
- package/src/host/authoring/gesture-persist.ts +2 -2
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +8 -8
- package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +42 -17
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +7 -7
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1614 -0
- package/src/host/authoring/pixi-still-presentation.ts +2 -2
- package/src/host/authoring/pixi-structure-history.ts +3 -3
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +6 -6
- package/src/host/authoring/struct-write-pipe.ts +3 -3
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +2 -2
- package/src/host/canvas-entry-runtime.ts +59 -48
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +9 -16
- package/src/host/components/PixiIsolationSceneContent.tsx +17 -16
- package/src/host/components/ThreeIsolationSceneContent.tsx +5 -5
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +29 -61
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +17 -26
- package/src/host/components/world-root-stage.ts +130 -63
- package/src/host/coverage/capability-coverage.ts +13 -1
- package/src/host/coverage/game-contract-seam-evidence.ts +1 -1
- package/src/host/coverage/project-verb-coverage.ts +2 -17
- package/src/host/coverage/system-adapter-coverage.ts +4 -5
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +14 -8
- package/src/host/document-preview-three.ts +2 -2
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-module-access.ts +1 -1
- package/src/host/game-realm-page.ts +16 -8
- package/src/host/game-realm-reclaim.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +6 -6
- package/src/host/history/json-history-resource.ts +3 -3
- package/src/host/instance-extract-actions.ts +1 -1
- package/src/host/instance-fork-actions.ts +1 -1
- package/src/host/projection/dom.ts +2 -2
- package/src/host/projection/pixi.ts +25 -3
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +8 -49
- package/src/host/realm-services.ts +2 -2
- package/src/host/roots/canvas-root.tsx +361 -0
- package/src/host/roots/module-root.ts +1 -1
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/scene-view-drawability.ts +2 -2
- package/src/host/served-bundle-runtime-modules.ts +1 -19
- package/src/host/server-log-bridge.ts +3 -3
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/three-ingest-runtime.ts +1 -1
- package/src/host/use-active-performance-source.ts +3 -3
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +2 -2
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +13 -13
- package/src/ingest/capture-wait-report.ts +1 -1
- package/src/ingest/deferred-ingest-play.ts +11 -10
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +6 -8
- package/src/ingest/ingest-canvas-scene-document.tsx +17 -16
- package/src/ingest/ingest-canvas-scene.ts +5 -8
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +4 -4
- package/src/ingest/ingest-render-debug.ts +11 -11
- package/src/ingest/ingest-siblings.ts +16 -28
- package/src/ingest/module-mode.ts +18 -19
- package/src/ingest/mount-canvas-ingest-root.ts +27 -27
- package/src/ingest/mount-coverage.ts +4 -4
- package/src/ingest/mount-dom-ingest-root.ts +16 -16
- package/src/ingest/mount-ingest-root.ts +14 -14
- package/src/ingest/mount-three-ingest-root.ts +12 -12
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +7 -7
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +449 -14
- package/src/network/network-inspector-model.ts +14 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +99 -142
- package/src/play/play-recording.ts +2 -2
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +4 -4
- package/src/react/design-time-react-mount.ts +37 -85
- package/src/react/dom-authoring-adapter.ts +11 -11
- package/src/react/react-inspector-section.tsx +13 -13
- package/src/react/react-world-authoring-adapter.ts +31 -143
- package/src/react/story-documents/story-documents.tsx +18 -22
- package/src/react/ui-board-document.tsx +10 -11
- package/src/react/ui-component-board.ts +5 -5
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +169 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +657 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +4 -3
- package/src/three/authoring/constraint-inspector-section.tsx +8 -7
- package/src/three/authoring/model-asset-inspector-section.tsx +14 -17
- package/src/three/authoring/oid-source-persistence.ts +13 -13
- package/src/three/authoring/r3f-design-session.ts +79 -61
- package/src/three/authoring/r3f-source-authoring-adapter.ts +112 -108
- package/src/three/authoring/reflection-probe-inspector-section.tsx +5 -4
- package/src/three/authoring/spatial-joint-handles.ts +1 -1
- package/src/three/authoring/spatial-lod-handles.ts +1 -1
- package/src/three/authoring/spatial-particle-handles.ts +1 -1
- package/src/three/authoring/three-authoring-adapter.ts +33 -33
- package/src/three/component-verbs/extract-menu.ts +8 -7
- package/src/three/component-verbs/fork-menu.ts +8 -7
- package/src/three/component-verbs/internals-menu.ts +3 -3
- package/src/three/drei-asset-caches.ts +13 -0
- package/src/three/story-documents/three-story-documents.tsx +24 -34
- package/src/three/three-board/ThreeBoardDocument.tsx +25 -25
- package/src/three/three-board/board-scene.ts +7 -7
- package/src/three/three-board/three-component-board.ts +5 -5
- package/contributions/react/pasteboard.action.ts +0 -41
- package/contributions/xstate-behavior.action.ts +0 -42
- package/contributions/xstate-behavior.document.tsx +0 -73
- package/contributions/xstate-behavior.inspector.tsx +0 -28
- package/contributions/xstate-behavior.menu.ts +0 -23
- package/src/react/pasteboard-materialize.ts +0 -154
- package/src/services/game-audio-unlock.ts +0 -48
- package/src/xstate/XStateBehaviorSection.tsx +0 -130
- package/src/xstate/XStateMachineInspector.tsx +0 -566
- package/src/xstate/character-animation-machine.fixture.ts +0 -74
- package/src/xstate/live-behaviors.ts +0 -106
- package/src/xstate/use-live-actor-state.ts +0 -44
- package/src/xstate/xstate-graph.ts +0 -235
- package/src/xstate/xstate-layout.ts +0 -76
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The compact **Behavior** Inspector section (§4.3): the selected entity's
|
|
3
|
-
* live machine — its id, where the actor IS, the machine's FULL state chart
|
|
4
|
-
* with the active path marked, and the way into the Behavior document.
|
|
5
|
-
*
|
|
6
|
-
* It renders ONLY when `xstate-behavior.inspector.tsx`'s `match` found a real
|
|
7
|
-
* live behavior mark on the selected node, so a template entity (no machine)
|
|
8
|
-
* never sees it.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Button, themeVars } from '@volter/editor-sdk/widgets';
|
|
12
|
-
import {
|
|
13
|
-
subscribeXStateBehaviorInspections,
|
|
14
|
-
xstateBehaviorInspectionsVersion,
|
|
15
|
-
} from '@volter/threejs-runtime/behavior/xstate-inspection';
|
|
16
|
-
import { useSyncExternalStore } from 'react';
|
|
17
|
-
import type { AnyStateMachine, AnyStateNode } from 'xstate';
|
|
18
|
-
import { liveBehaviorFor, openBehaviorDocument } from './live-behaviors';
|
|
19
|
-
import { useLiveActorState } from './use-live-actor-state';
|
|
20
|
-
|
|
21
|
-
/** Every descendant state node of the machine (depth-first, root excluded),
|
|
22
|
-
* for the full-state-list readout below — the same direct `.states` walk
|
|
23
|
-
* `xstate-graph.ts` documents as the sanctioned traversal. */
|
|
24
|
-
function allMachineStates(machine: AnyStateMachine): AnyStateNode[] {
|
|
25
|
-
const out: AnyStateNode[] = [];
|
|
26
|
-
const visit = (node: AnyStateNode): void => {
|
|
27
|
-
for (const child of Object.values(node.states)) {
|
|
28
|
-
out.push(child);
|
|
29
|
-
visit(child);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
visit(machine.root);
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function XStateMachineSection({ nodeId }: { nodeId: string | null }) {
|
|
37
|
-
useSyncExternalStore(
|
|
38
|
-
subscribeXStateBehaviorInspections,
|
|
39
|
-
xstateBehaviorInspectionsVersion,
|
|
40
|
-
xstateBehaviorInspectionsVersion,
|
|
41
|
-
);
|
|
42
|
-
const behavior = nodeId ? liveBehaviorFor(nodeId) : undefined;
|
|
43
|
-
const machine = behavior?.machine;
|
|
44
|
-
// Follow live transitions (re-render per snapshot) — the state list and
|
|
45
|
-
// layer readout below re-derive from each.
|
|
46
|
-
const activeIds = useLiveActorState(machine as AnyStateMachine, behavior?.actor);
|
|
47
|
-
if (!behavior || !machine || !nodeId) return null;
|
|
48
|
-
const activeIdSet = new Set(activeIds ?? []);
|
|
49
|
-
const states = allMachineStates(machine);
|
|
50
|
-
return (
|
|
51
|
-
<div
|
|
52
|
-
data-testid="inspector-xstate-section"
|
|
53
|
-
style={{
|
|
54
|
-
pointerEvents: 'auto',
|
|
55
|
-
padding: 8,
|
|
56
|
-
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
<div
|
|
60
|
-
style={{
|
|
61
|
-
fontSize: 11,
|
|
62
|
-
color: themeVars.content.dim,
|
|
63
|
-
textTransform: 'uppercase',
|
|
64
|
-
marginBottom: 4,
|
|
65
|
-
}}
|
|
66
|
-
>
|
|
67
|
-
Behavior
|
|
68
|
-
</div>
|
|
69
|
-
<div
|
|
70
|
-
style={{ fontSize: 11, color: themeVars.content.primary, fontWeight: 600, marginBottom: 2 }}
|
|
71
|
-
>
|
|
72
|
-
{machine.id}
|
|
73
|
-
</div>
|
|
74
|
-
<div style={{ fontSize: 10, color: themeVars.content.muted, marginBottom: 4 }}>
|
|
75
|
-
<span style={{ color: themeVars.semantic.success, fontWeight: 700 }}>●</span> Live —{' '}
|
|
76
|
-
{activeIds?.map((stateId, index) => (
|
|
77
|
-
<span key={stateId}>
|
|
78
|
-
{index > 0 && ' · '}
|
|
79
|
-
<span
|
|
80
|
-
style={{ fontFamily: themeVars.typography.mono, color: themeVars.accent.default }}
|
|
81
|
-
>
|
|
82
|
-
{stateId.startsWith(`${machine.id}.`)
|
|
83
|
-
? stateId.slice(machine.id.length + 1)
|
|
84
|
-
: stateId}
|
|
85
|
-
</span>
|
|
86
|
-
</span>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
{/* The FULL state chart, active path highlighted — the section must
|
|
90
|
-
show what the machine can do, not only where it is (owner
|
|
91
|
-
feedback, 2026-08-06). */}
|
|
92
|
-
<div data-testid="xstate-section-states" style={{ marginBottom: 6 }}>
|
|
93
|
-
{states.map((state) => {
|
|
94
|
-
const relative = state.id.startsWith(`${machine.root.id}.`)
|
|
95
|
-
? state.id.slice(machine.root.id.length + 1)
|
|
96
|
-
: state.id;
|
|
97
|
-
const depth = relative.split('.').length - 1;
|
|
98
|
-
const active = activeIdSet.has(state.id);
|
|
99
|
-
return (
|
|
100
|
-
<div
|
|
101
|
-
key={state.id}
|
|
102
|
-
data-testid={`xstate-section-state-${relative}`}
|
|
103
|
-
style={{
|
|
104
|
-
paddingLeft: 4 + depth * 12,
|
|
105
|
-
fontSize: 10,
|
|
106
|
-
lineHeight: 1.7,
|
|
107
|
-
fontFamily: themeVars.typography.mono,
|
|
108
|
-
color: active ? themeVars.accent.default : themeVars.content.muted,
|
|
109
|
-
fontWeight: active ? 700 : 400,
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
112
|
-
{active ? '● ' : '○ '}
|
|
113
|
-
{state.key}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
})}
|
|
117
|
-
</div>
|
|
118
|
-
<div style={{ fontSize: 10, color: themeVars.content.dim, marginBottom: 6 }}>
|
|
119
|
-
Source: hand-authored XState TypeScript — inspect-only.
|
|
120
|
-
</div>
|
|
121
|
-
<Button
|
|
122
|
-
size="compact"
|
|
123
|
-
data-testid="xstate-open-graph"
|
|
124
|
-
onClick={() => void openBehaviorDocument(nodeId)}
|
|
125
|
-
>
|
|
126
|
-
Inspect Behavior
|
|
127
|
-
</Button>
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
@@ -1,566 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E3 + E4 (spec §11) — a visual, READ-ONLY inspector for a real XState v5 machine: states
|
|
3
|
-
* render as boxes (nodes), transitions as labeled arrows (edges), and the
|
|
4
|
-
* currently active state(s) are highlighted. XState behavior machines are
|
|
5
|
-
* hand-authored TypeScript; this component INSPECTS them, it never edits/generates/round-trips
|
|
6
|
-
* machine config. That boundary is also stated inline in the panel itself (the "Inspect-only"
|
|
7
|
-
* badge below) — not just in a doc a user might not read.
|
|
8
|
-
*
|
|
9
|
-
* Pure presentation: `buildXStateGraph`/`layoutXStateGraph` (this directory)
|
|
10
|
-
* do all the machine-introspection and layout work; this component only
|
|
11
|
-
* renders their output plus the small amount of interaction state
|
|
12
|
-
* (hover/selection for the per-state metadata detail panel).
|
|
13
|
-
*
|
|
14
|
-
* Visual language (house style — see DataPanel.tsx for the canonical dock
|
|
15
|
-
* tab): chrome is the sans default; monospace is reserved for CODE — state
|
|
16
|
-
* ids and event tokens on edges. Blue
|
|
17
|
-
* (the palette accent) is selection (the clicked node); green (the `success` token) is strictly
|
|
18
|
-
* the LIVE status color (the running state's border + "● ACTIVE" pip and the
|
|
19
|
-
* active transition edge) — two different signals, two different colors.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { faCircleInfo, faLock } from '@fortawesome/free-solid-svg-icons';
|
|
23
|
-
import {
|
|
24
|
-
Button,
|
|
25
|
-
EditorIcon,
|
|
26
|
-
fontSizeVar,
|
|
27
|
-
spaceVar,
|
|
28
|
-
TONE_WARNING,
|
|
29
|
-
themeVars,
|
|
30
|
-
} from '@volter/editor-sdk/widgets';
|
|
31
|
-
import type { InspectableXStateActor } from '@volter/threejs-runtime/behavior/xstate-inspection';
|
|
32
|
-
import { useMemo, useState } from 'react';
|
|
33
|
-
import type { AnyStateMachine } from 'xstate';
|
|
34
|
-
import { useLiveActorState } from './use-live-actor-state';
|
|
35
|
-
import { buildXStateGraph, type XStateGraphNode } from './xstate-graph';
|
|
36
|
-
import { layoutXStateGraph } from './xstate-layout';
|
|
37
|
-
|
|
38
|
-
export interface XStateMachineInspectorProps {
|
|
39
|
-
machine: AnyStateMachine;
|
|
40
|
-
/**
|
|
41
|
-
* Static "current state id" highlight (e.g. `"character-animation.idle"`).
|
|
42
|
-
* Ignored while `actor` is provided AND has emitted at least one snapshot
|
|
43
|
-
* (the live actor becomes the source of truth for the highlight once
|
|
44
|
-
* available). Useful for tests and for callers with no live actor.
|
|
45
|
-
*/
|
|
46
|
-
currentStateId?: string | undefined;
|
|
47
|
-
/**
|
|
48
|
-
* A LIVE xstate actor to highlight and follow in real time (play mode).
|
|
49
|
-
* Optional — when omitted, the panel is a static graph view of the machine
|
|
50
|
-
* shape, optionally highlighted via `currentStateId`. When provided, this
|
|
51
|
-
* component subscribes to it (`actor.subscribe`) and re-highlights on every
|
|
52
|
-
* transition, unsubscribing on unmount/actor change.
|
|
53
|
-
*/
|
|
54
|
-
actor?: InspectableXStateActor | undefined;
|
|
55
|
-
/** Optional heading override (defaults to the machine's root id). */
|
|
56
|
-
title?: string | undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// U6 sweep: this used to be a fully hand-typed local palette re-spelling the
|
|
60
|
-
// token values; the chrome slots now IMPORT the tokens they always mirrored.
|
|
61
|
-
// Graph paint follows the same semantic roles as surrounding editor chrome.
|
|
62
|
-
// State/data identity is expressed through labels and graph structure, not a
|
|
63
|
-
// private syntax palette that becomes illegible under alternate themes.
|
|
64
|
-
const COLORS = {
|
|
65
|
-
bg: themeVars.surface.inset,
|
|
66
|
-
panelBg: themeVars.surface.panel,
|
|
67
|
-
border: themeVars.boundary.default,
|
|
68
|
-
innerBorder: themeVars.boundary.default,
|
|
69
|
-
nodeBg: themeVars.surface.panel,
|
|
70
|
-
nodeHoverBg: themeVars.surface.chrome,
|
|
71
|
-
nodeBorder: themeVars.boundary.strong,
|
|
72
|
-
/** Green = LIVE status only (running state / active edge), never selection.
|
|
73
|
-
* Now the shared `success` token (§3 absorbs the old one-off `#5ec26a`). */
|
|
74
|
-
live: themeVars.semantic.success,
|
|
75
|
-
text: themeVars.content.primary,
|
|
76
|
-
dim: themeVars.content.dim,
|
|
77
|
-
edge: themeVars.content.dim,
|
|
78
|
-
guard: themeVars.semantic.warning,
|
|
79
|
-
/** Blue = selection (the clicked node) / interactive accent. */
|
|
80
|
-
accent: themeVars.accent.default,
|
|
81
|
-
/** Clip names / event tokens remain code, using the theme accent. */
|
|
82
|
-
codeBlue: themeVars.accent.default,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/** Monospace is for CODE only: state ids, event tokens, clip names, JSON.
|
|
86
|
-
* (U6: the one shared editor mono stack, not a fifth local spelling.) */
|
|
87
|
-
const MONO = themeVars.typography.mono;
|
|
88
|
-
|
|
89
|
-
/** DataPanel-style uppercase eyebrow label (detail-pane section headers). */
|
|
90
|
-
const eyebrowStyle: React.CSSProperties = {
|
|
91
|
-
fontSize: fontSizeVar.sm,
|
|
92
|
-
fontWeight: 600,
|
|
93
|
-
textTransform: 'uppercase',
|
|
94
|
-
letterSpacing: 0.4,
|
|
95
|
-
color: COLORS.dim,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
function nodeTestId(id: string): string {
|
|
99
|
-
return `xstate-node-${id.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** One state box. Purely presentational — position comes from the layout. */
|
|
103
|
-
function StateNodeBox({
|
|
104
|
-
n,
|
|
105
|
-
x,
|
|
106
|
-
y,
|
|
107
|
-
width,
|
|
108
|
-
height,
|
|
109
|
-
isActive,
|
|
110
|
-
isSelected,
|
|
111
|
-
onSelect,
|
|
112
|
-
}: {
|
|
113
|
-
n: XStateGraphNode;
|
|
114
|
-
x: number;
|
|
115
|
-
y: number;
|
|
116
|
-
width: number;
|
|
117
|
-
height: number;
|
|
118
|
-
isActive: boolean;
|
|
119
|
-
isSelected: boolean;
|
|
120
|
-
onSelect: () => void;
|
|
121
|
-
}) {
|
|
122
|
-
// Selection (blue) wins the border; live (green) keeps its pip either way.
|
|
123
|
-
return (
|
|
124
|
-
<Button
|
|
125
|
-
className="vgai-xstate-node"
|
|
126
|
-
variant="ghost"
|
|
127
|
-
data-testid={nodeTestId(n.id)}
|
|
128
|
-
data-active={String(isActive)}
|
|
129
|
-
data-selected={isSelected || undefined}
|
|
130
|
-
onClick={onSelect}
|
|
131
|
-
style={{
|
|
132
|
-
position: 'absolute',
|
|
133
|
-
left: x,
|
|
134
|
-
top: y,
|
|
135
|
-
width,
|
|
136
|
-
height,
|
|
137
|
-
}}
|
|
138
|
-
title={n.id}
|
|
139
|
-
>
|
|
140
|
-
<span
|
|
141
|
-
className="vgai-xstate-node-type"
|
|
142
|
-
style={{
|
|
143
|
-
...eyebrowStyle,
|
|
144
|
-
fontSize: fontSizeVar.xs,
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
{n.type}
|
|
148
|
-
{n.isInitial ? ' · initial' : ''}
|
|
149
|
-
</span>
|
|
150
|
-
<span
|
|
151
|
-
style={{
|
|
152
|
-
fontWeight: 600,
|
|
153
|
-
fontSize: fontSizeVar.md,
|
|
154
|
-
display: 'flex',
|
|
155
|
-
alignItems: 'center',
|
|
156
|
-
gap: spaceVar[3],
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
159
|
-
{n.key}
|
|
160
|
-
{isActive && (
|
|
161
|
-
<span
|
|
162
|
-
data-testid={`xstate-node-active-badge-${n.key}`}
|
|
163
|
-
style={{
|
|
164
|
-
color: COLORS.live,
|
|
165
|
-
fontSize: fontSizeVar.xs,
|
|
166
|
-
fontWeight: 700,
|
|
167
|
-
letterSpacing: 0.4,
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
● ACTIVE
|
|
171
|
-
</span>
|
|
172
|
-
)}
|
|
173
|
-
</span>
|
|
174
|
-
</Button>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** SVG arrow connecting two laid-out node boxes, labeled with its event/guard. */
|
|
179
|
-
function EdgeArrow({
|
|
180
|
-
x1,
|
|
181
|
-
y1,
|
|
182
|
-
x2,
|
|
183
|
-
y2,
|
|
184
|
-
label,
|
|
185
|
-
guard,
|
|
186
|
-
isEventless,
|
|
187
|
-
isActive,
|
|
188
|
-
}: {
|
|
189
|
-
x1: number;
|
|
190
|
-
y1: number;
|
|
191
|
-
x2: number;
|
|
192
|
-
y2: number;
|
|
193
|
-
label: string;
|
|
194
|
-
guard: string | undefined;
|
|
195
|
-
isEventless: boolean;
|
|
196
|
-
isActive: boolean;
|
|
197
|
-
}) {
|
|
198
|
-
const midX = (x1 + x2) / 2;
|
|
199
|
-
const midY = (y1 + y2) / 2;
|
|
200
|
-
const stroke = isActive ? COLORS.live : isEventless ? COLORS.guard : COLORS.edge;
|
|
201
|
-
const labelColor = isActive ? COLORS.live : isEventless ? COLORS.guard : COLORS.codeBlue;
|
|
202
|
-
// DISPLAY-only guard tidy-up (the full guardLabel stays in the detail
|
|
203
|
-
// panel's transitions list): an inline arrow-predicate guard reads better
|
|
204
|
-
// on a chip as just its body (`context.speed <= 0.1`), and anything still
|
|
205
|
-
// longer than ~30 chars gets an ellipsis so chips stay near the line's
|
|
206
|
-
// midpoint instead of sprawling across node columns.
|
|
207
|
-
const guardBody = guard?.includes('=>') ? guard.slice(guard.indexOf('=>') + 2).trim() : guard;
|
|
208
|
-
const guardDisplay =
|
|
209
|
-
guardBody && guardBody.length > 30 ? `${guardBody.slice(0, 29)}…` : guardBody;
|
|
210
|
-
// Size the label chip from the FULL displayed text (event + guard suffix) —
|
|
211
|
-
// 9px monospace is ~5.6px/char.
|
|
212
|
-
const fullLabel = guardDisplay ? `${label} [${guardDisplay}]` : label;
|
|
213
|
-
const chipWidth = fullLabel.length * 5.6 + 14;
|
|
214
|
-
return (
|
|
215
|
-
<g data-testid="xstate-edge" opacity={isActive ? 1 : 0.8}>
|
|
216
|
-
<line
|
|
217
|
-
x1={x1}
|
|
218
|
-
y1={y1}
|
|
219
|
-
x2={x2}
|
|
220
|
-
y2={y2}
|
|
221
|
-
stroke={stroke}
|
|
222
|
-
strokeWidth={isActive ? 1.5 : 1}
|
|
223
|
-
markerEnd={isActive ? 'url(#xstate-arrowhead-active)' : 'url(#xstate-arrowhead)'}
|
|
224
|
-
/>
|
|
225
|
-
<rect
|
|
226
|
-
x={midX - chipWidth / 2}
|
|
227
|
-
y={midY - 8}
|
|
228
|
-
width={chipWidth}
|
|
229
|
-
height={16}
|
|
230
|
-
fill={COLORS.panelBg}
|
|
231
|
-
stroke={COLORS.border}
|
|
232
|
-
strokeWidth={1}
|
|
233
|
-
rx={4}
|
|
234
|
-
/>
|
|
235
|
-
<text
|
|
236
|
-
x={midX}
|
|
237
|
-
y={midY + 3}
|
|
238
|
-
fill={labelColor}
|
|
239
|
-
fontSize={9}
|
|
240
|
-
textAnchor="middle"
|
|
241
|
-
fontFamily={MONO}
|
|
242
|
-
>
|
|
243
|
-
{label}
|
|
244
|
-
{guardDisplay && <tspan fill={COLORS.guard}>{` [${guardDisplay}]`}</tspan>}
|
|
245
|
-
</text>
|
|
246
|
-
</g>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function displayedActiveIds(
|
|
251
|
-
liveIds: string[] | null,
|
|
252
|
-
currentStateId: string | undefined,
|
|
253
|
-
): string[] {
|
|
254
|
-
if (liveIds) return liveIds;
|
|
255
|
-
return currentStateId ? [currentStateId] : [];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function XStateMachineInspector({
|
|
259
|
-
machine,
|
|
260
|
-
currentStateId,
|
|
261
|
-
actor,
|
|
262
|
-
title,
|
|
263
|
-
}: XStateMachineInspectorProps) {
|
|
264
|
-
const graph = useMemo(() => buildXStateGraph(machine), [machine]);
|
|
265
|
-
const layout = useMemo(() => layoutXStateGraph(graph), [graph]);
|
|
266
|
-
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
267
|
-
|
|
268
|
-
// Live actor (play mode) takes over the highlight once it has emitted a
|
|
269
|
-
// snapshot; otherwise fall back to the static `currentStateId` prop.
|
|
270
|
-
const liveActiveIds = useLiveActorState(machine, actor);
|
|
271
|
-
const activeIds = displayedActiveIds(liveActiveIds, currentStateId);
|
|
272
|
-
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
273
|
-
|
|
274
|
-
const selectedNode = graph.nodes.find((n) => n.id === selectedId) ?? null;
|
|
275
|
-
const selectedEdges = selectedNode ? graph.edges.filter((e) => e.source === selectedNode.id) : [];
|
|
276
|
-
const context = actor?.getSnapshot().context;
|
|
277
|
-
const contextEntries =
|
|
278
|
-
context && typeof context === 'object' && !Array.isArray(context)
|
|
279
|
-
? Object.entries(context as Record<string, unknown>)
|
|
280
|
-
: [];
|
|
281
|
-
|
|
282
|
-
return (
|
|
283
|
-
// Responsive by construction (no width measuring): the graph and the detail
|
|
284
|
-
// pane sit SIDE BY SIDE when there is room and WRAP — detail STACKS BELOW —
|
|
285
|
-
// when the container is narrow (the inspector rail). `flex-wrap` plus a
|
|
286
|
-
// per-pane min-width floor forces the wrap instead of shrinking either pane
|
|
287
|
-
// to an unusable sliver; `min(…, 100%)` keeps the floor from overflowing a
|
|
288
|
-
// container narrower than the floor itself. The graph is an inherently
|
|
289
|
-
// absolute-positioned 2D canvas, so it is BOUNDED in a fixed-min-height box
|
|
290
|
-
// that scrolls internally (below) — it never forces the section wider.
|
|
291
|
-
<div
|
|
292
|
-
data-testid="xstate-inspector-panel"
|
|
293
|
-
style={{
|
|
294
|
-
display: 'flex',
|
|
295
|
-
flexDirection: 'row',
|
|
296
|
-
flexWrap: 'wrap',
|
|
297
|
-
alignContent: 'flex-start',
|
|
298
|
-
height: '100%',
|
|
299
|
-
minHeight: 0,
|
|
300
|
-
overflowY: 'auto',
|
|
301
|
-
background: COLORS.bg,
|
|
302
|
-
color: COLORS.text,
|
|
303
|
-
fontSize: fontSizeVar.md,
|
|
304
|
-
}}
|
|
305
|
-
>
|
|
306
|
-
<div
|
|
307
|
-
style={{
|
|
308
|
-
flex: '1 1 260px',
|
|
309
|
-
minWidth: 'min(260px, 100%)',
|
|
310
|
-
display: 'flex',
|
|
311
|
-
flexDirection: 'column',
|
|
312
|
-
}}
|
|
313
|
-
>
|
|
314
|
-
{/* Header: machine id + the E4 inspect-only badge/explanation. */}
|
|
315
|
-
<div
|
|
316
|
-
style={{
|
|
317
|
-
display: 'flex',
|
|
318
|
-
alignItems: 'center',
|
|
319
|
-
gap: spaceVar[5],
|
|
320
|
-
padding: `${spaceVar[3]} ${spaceVar[5]}`,
|
|
321
|
-
borderBottom: `1px solid ${COLORS.border}`,
|
|
322
|
-
flexShrink: 0,
|
|
323
|
-
}}
|
|
324
|
-
>
|
|
325
|
-
<span style={{ fontWeight: 600, fontSize: fontSizeVar.md }}>
|
|
326
|
-
{title ?? graph.machineId}
|
|
327
|
-
</span>
|
|
328
|
-
<span
|
|
329
|
-
data-testid="xstate-inspect-only-badge"
|
|
330
|
-
title="XState behavior is hand-authored TypeScript. This view derives the live statechart for debugging and never creates a second machine document."
|
|
331
|
-
style={{
|
|
332
|
-
display: 'inline-flex',
|
|
333
|
-
alignItems: 'center',
|
|
334
|
-
gap: spaceVar[2],
|
|
335
|
-
padding: `${spaceVar[1]} ${spaceVar[4]}`,
|
|
336
|
-
borderRadius: themeVars.shape.full,
|
|
337
|
-
// U6: the shared warning banner tone (banner-tones.ts), not a
|
|
338
|
-
// fourth hand-mixed amber triple.
|
|
339
|
-
background: TONE_WARNING.bg,
|
|
340
|
-
color: TONE_WARNING.fg,
|
|
341
|
-
border: `1px solid ${TONE_WARNING.border}`,
|
|
342
|
-
fontSize: fontSizeVar.xs,
|
|
343
|
-
fontWeight: 700,
|
|
344
|
-
textTransform: 'uppercase',
|
|
345
|
-
letterSpacing: 0.4,
|
|
346
|
-
}}
|
|
347
|
-
>
|
|
348
|
-
<EditorIcon icon={faLock} style={{ fontSize: fontSizeVar.xs }} />
|
|
349
|
-
Inspect-only
|
|
350
|
-
</span>
|
|
351
|
-
<span style={{ color: COLORS.dim, fontSize: fontSizeVar.sm, flex: 1 }}>
|
|
352
|
-
Live behavior statechart · source-owned TypeScript
|
|
353
|
-
</span>
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
{/* Graph canvas */}
|
|
357
|
-
<div
|
|
358
|
-
data-testid="xstate-graph-canvas"
|
|
359
|
-
// `flex: 1` fills a tall panel; `minHeight` is the floor that keeps
|
|
360
|
-
// the graph a usable, internally-scrolled box when the container is
|
|
361
|
-
// auto-height (a section body in the inspector column has no fixed
|
|
362
|
-
// height to fill), so a wide graph pans inside this box rather than
|
|
363
|
-
// pushing the section open.
|
|
364
|
-
style={{ flex: '1 1 auto', minHeight: 300, overflow: 'auto', position: 'relative' }}
|
|
365
|
-
>
|
|
366
|
-
<div style={{ position: 'relative', width: layout.width, height: layout.height }}>
|
|
367
|
-
<svg
|
|
368
|
-
width={layout.width}
|
|
369
|
-
height={layout.height}
|
|
370
|
-
style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
|
|
371
|
-
>
|
|
372
|
-
<defs>
|
|
373
|
-
<marker
|
|
374
|
-
id="xstate-arrowhead"
|
|
375
|
-
markerWidth="8"
|
|
376
|
-
markerHeight="8"
|
|
377
|
-
refX="7"
|
|
378
|
-
refY="4"
|
|
379
|
-
orient="auto"
|
|
380
|
-
>
|
|
381
|
-
<path d="M0,0 L8,4 L0,8 Z" fill={COLORS.edge} />
|
|
382
|
-
</marker>
|
|
383
|
-
<marker
|
|
384
|
-
id="xstate-arrowhead-active"
|
|
385
|
-
markerWidth="8"
|
|
386
|
-
markerHeight="8"
|
|
387
|
-
refX="7"
|
|
388
|
-
refY="4"
|
|
389
|
-
orient="auto"
|
|
390
|
-
>
|
|
391
|
-
<path d="M0,0 L8,4 L0,8 Z" fill={COLORS.live} />
|
|
392
|
-
</marker>
|
|
393
|
-
</defs>
|
|
394
|
-
{graph.edges.map((edge) => {
|
|
395
|
-
const from = layout.positionById.get(edge.source);
|
|
396
|
-
const to = layout.positionById.get(edge.target);
|
|
397
|
-
if (!from || !to) return null;
|
|
398
|
-
const isActive = activeSet.has(edge.source) && activeSet.has(edge.target);
|
|
399
|
-
// Anchor at the right/left edges of the boxes when moving
|
|
400
|
-
// forward in depth, else box-center — keeps arrows readable
|
|
401
|
-
// without a full routing algorithm (deliberately simple, per
|
|
402
|
-
// the "no fancy graph engine required" AC).
|
|
403
|
-
const x1 = from.x + from.width;
|
|
404
|
-
const y1 = from.y + from.height / 2;
|
|
405
|
-
const x2 = to.x;
|
|
406
|
-
const y2 = to.y + to.height / 2;
|
|
407
|
-
return (
|
|
408
|
-
<EdgeArrow
|
|
409
|
-
key={edge.id}
|
|
410
|
-
x1={x1}
|
|
411
|
-
y1={y1}
|
|
412
|
-
x2={x2}
|
|
413
|
-
y2={y2}
|
|
414
|
-
label={edge.eventLabel}
|
|
415
|
-
guard={edge.guardLabel}
|
|
416
|
-
isEventless={edge.isEventless}
|
|
417
|
-
isActive={isActive}
|
|
418
|
-
/>
|
|
419
|
-
);
|
|
420
|
-
})}
|
|
421
|
-
</svg>
|
|
422
|
-
{layout.nodes.map(({ node, x, y, width, height }) => (
|
|
423
|
-
<StateNodeBox
|
|
424
|
-
key={node.id}
|
|
425
|
-
n={node}
|
|
426
|
-
x={x}
|
|
427
|
-
y={y}
|
|
428
|
-
width={width}
|
|
429
|
-
height={height}
|
|
430
|
-
isActive={activeSet.has(node.id)}
|
|
431
|
-
isSelected={selectedId === node.id}
|
|
432
|
-
onSelect={() => setSelectedId(node.id)}
|
|
433
|
-
/>
|
|
434
|
-
))}
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
|
-
</div>
|
|
438
|
-
|
|
439
|
-
{/* Per-state metadata detail panel — beside the graph when wide, wrapped
|
|
440
|
-
BELOW it when narrow (its min-width floor is what forces the wrap). It
|
|
441
|
-
does not grow past `maxWidth`, so a wide panel keeps the graph
|
|
442
|
-
dominant rather than handing half the width to the detail list. */}
|
|
443
|
-
<div
|
|
444
|
-
data-testid="xstate-detail-panel"
|
|
445
|
-
style={{
|
|
446
|
-
flex: '1 1 240px',
|
|
447
|
-
minWidth: 'min(240px, 100%)',
|
|
448
|
-
maxWidth: 320,
|
|
449
|
-
borderLeft: `1px solid ${COLORS.border}`,
|
|
450
|
-
padding: spaceVar[5],
|
|
451
|
-
overflowY: 'auto',
|
|
452
|
-
fontSize: fontSizeVar.md,
|
|
453
|
-
}}
|
|
454
|
-
>
|
|
455
|
-
<div
|
|
456
|
-
style={{
|
|
457
|
-
...eyebrowStyle,
|
|
458
|
-
display: 'flex',
|
|
459
|
-
alignItems: 'center',
|
|
460
|
-
gap: spaceVar[2],
|
|
461
|
-
marginBottom: spaceVar[4],
|
|
462
|
-
}}
|
|
463
|
-
>
|
|
464
|
-
<EditorIcon icon={faCircleInfo} />
|
|
465
|
-
State detail
|
|
466
|
-
</div>
|
|
467
|
-
{!selectedNode ? (
|
|
468
|
-
<div style={{ color: COLORS.dim }}>Click a state node to inspect its metadata.</div>
|
|
469
|
-
) : (
|
|
470
|
-
<div data-testid="xstate-detail-content">
|
|
471
|
-
<div
|
|
472
|
-
style={{
|
|
473
|
-
fontFamily: MONO,
|
|
474
|
-
fontWeight: 600,
|
|
475
|
-
fontSize: fontSizeVar.base,
|
|
476
|
-
marginBottom: spaceVar[2],
|
|
477
|
-
wordBreak: 'break-all',
|
|
478
|
-
}}
|
|
479
|
-
>
|
|
480
|
-
{selectedNode.id}
|
|
481
|
-
</div>
|
|
482
|
-
<div
|
|
483
|
-
style={{ color: COLORS.dim, fontSize: fontSizeVar.base, marginBottom: spaceVar[5] }}
|
|
484
|
-
>
|
|
485
|
-
{selectedNode.type}
|
|
486
|
-
{selectedNode.isInitial ? ' · initial' : ''}
|
|
487
|
-
{activeSet.has(selectedNode.id) ? (
|
|
488
|
-
<span style={{ color: COLORS.live, fontWeight: 700, letterSpacing: 0.4 }}>
|
|
489
|
-
{' · ● '}ACTIVE
|
|
490
|
-
</span>
|
|
491
|
-
) : (
|
|
492
|
-
''
|
|
493
|
-
)}
|
|
494
|
-
</div>
|
|
495
|
-
|
|
496
|
-
<div>
|
|
497
|
-
<div style={{ ...eyebrowStyle, marginBottom: spaceVar[2] }}>Outgoing transitions</div>
|
|
498
|
-
{selectedEdges.length === 0 ? (
|
|
499
|
-
<div style={{ color: COLORS.dim, fontSize: fontSizeVar.base }}>None.</div>
|
|
500
|
-
) : (
|
|
501
|
-
selectedEdges.map((e) => (
|
|
502
|
-
<div key={e.id} style={{ marginBottom: 5, fontSize: fontSizeVar.base }}>
|
|
503
|
-
<span
|
|
504
|
-
style={{
|
|
505
|
-
fontFamily: MONO,
|
|
506
|
-
fontSize: fontSizeVar.sm,
|
|
507
|
-
color: COLORS.accent,
|
|
508
|
-
background: COLORS.panelBg,
|
|
509
|
-
border: `1px solid ${COLORS.innerBorder}`,
|
|
510
|
-
borderRadius: themeVars.shape.small,
|
|
511
|
-
padding: '1px 5px',
|
|
512
|
-
}}
|
|
513
|
-
>
|
|
514
|
-
{e.eventLabel}
|
|
515
|
-
</span>
|
|
516
|
-
<span style={{ color: COLORS.dim }}>{' → '}</span>
|
|
517
|
-
<span style={{ fontFamily: MONO, fontSize: fontSizeVar.sm }}>{e.target}</span>
|
|
518
|
-
{e.guardLabel && (
|
|
519
|
-
<div style={{ color: COLORS.guard, fontSize: fontSizeVar.sm, marginTop: 1 }}>
|
|
520
|
-
guard: <span style={{ fontFamily: MONO }}>{e.guardLabel}</span>
|
|
521
|
-
</div>
|
|
522
|
-
)}
|
|
523
|
-
</div>
|
|
524
|
-
))
|
|
525
|
-
)}
|
|
526
|
-
</div>
|
|
527
|
-
|
|
528
|
-
{contextEntries.length > 0 && (
|
|
529
|
-
<div style={{ marginTop: 14 }} data-testid="xstate-context-table">
|
|
530
|
-
<div style={{ ...eyebrowStyle, marginBottom: 5 }}>Context</div>
|
|
531
|
-
{contextEntries.map(([key, value]) => (
|
|
532
|
-
<div
|
|
533
|
-
key={key}
|
|
534
|
-
style={{
|
|
535
|
-
display: 'grid',
|
|
536
|
-
gridTemplateColumns: 'minmax(70px, 1fr) minmax(60px, 1fr)',
|
|
537
|
-
gap: spaceVar[4],
|
|
538
|
-
padding: '3px 0',
|
|
539
|
-
borderBottom: `1px solid ${COLORS.innerBorder}`,
|
|
540
|
-
fontSize: fontSizeVar.sm,
|
|
541
|
-
}}
|
|
542
|
-
>
|
|
543
|
-
<span style={{ color: COLORS.dim }}>{key}</span>
|
|
544
|
-
<span
|
|
545
|
-
style={{ fontFamily: MONO, overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
546
|
-
>
|
|
547
|
-
{typeof value === 'string' ||
|
|
548
|
-
typeof value === 'number' ||
|
|
549
|
-
typeof value === 'boolean'
|
|
550
|
-
? String(value)
|
|
551
|
-
: value == null
|
|
552
|
-
? 'null'
|
|
553
|
-
: Array.isArray(value)
|
|
554
|
-
? `${value.length} items`
|
|
555
|
-
: 'object'}
|
|
556
|
-
</span>
|
|
557
|
-
</div>
|
|
558
|
-
))}
|
|
559
|
-
</div>
|
|
560
|
-
)}
|
|
561
|
-
</div>
|
|
562
|
-
)}
|
|
563
|
-
</div>
|
|
564
|
-
</div>
|
|
565
|
-
);
|
|
566
|
-
}
|