Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/quiet-state-charts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@typeonce/effect-machine-devtools": patch
---

Make statecharts denser by flowing states from top to bottom, replacing full initial-entry lanes with compact top-entry markers, and sizing state cards from their visible names and invocations.

State values now appear as compact JSON-shaped type previews in the state inspector instead of occupying the topology. Transition labels sit directly on clear route segments when space permits, hierarchy-crossing routes avoid compound-state headers, and routes attach to their actual source and target before turning. Horizontally scrolling machine tabs also keep their position when selecting or live-reloading a machine.

Charts remain available when every deterministic layout has only cosmetic label-to-route crossings. Structural failures such as detached edges, node crossings, and overlapping routes still prevent rendering.
2 changes: 1 addition & 1 deletion packages/devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Run the devtools only against code you trust. The server has no authentication a

## Inspection and walkthroughs

The visualizer shows topology as a read-only statechart with native horizontal and vertical scrolling, incremental zoom, and a fit-to-viewport overview. Machine tabs run across the top so the chart uses the rest of the viewport. States remain grouped inside their compound parents, while orthogonal routes connect each enabled transition without requiring a draggable canvas. State cards show projected value fields and invocations at a glance. A single click selects a state or transition, while a double click opens its dismissible inspector. State selection distinguishes incoming from outgoing relationships, and conditional branches with the same source and target share one topology edge while retaining their full details in the inspector.
The visualizer shows topology as a read-only statechart with native horizontal and vertical scrolling, incremental zoom, and a fit-to-viewport overview. Machine tabs run across the top so the chart uses the rest of the viewport. The default flow runs from top to bottom, while parallel regions remain side by side and states stay grouped inside their compound parents. Orthogonal routes connect each enabled transition without requiring a draggable canvas. Compact state cards keep names and invocations visible at a glance; select a state to inspect its projected value fields. A single click selects a state or transition, while a double click opens its dismissible inspector. State selection distinguishes incoming from outgoing relationships, and conditional branches with the same source and target share one topology edge while retaining their full details in the inspector.

The machine document includes projected value and output schemas for every state, plus the public machine and event input contracts. The browser renders those contracts as read-only field metadata: names, projected types, required or optional status, descriptions, ranges, lengths, patterns, and literal or enum values. It never asks for payload values merely to explore a static document.

Expand Down
21 changes: 13 additions & 8 deletions packages/devtools/src/internal/browser/chart-layout-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface ChartNodeLayoutPolicy {
readonly staticPath: boolean
readonly rank: number | null
readonly order: number
readonly layerConstraint: "LAST" | null
readonly layerConstraint: "FIRST" | "LAST" | null
}

export interface ChartEdgeLayoutPolicy {
Expand Down Expand Up @@ -159,17 +159,17 @@ export const makeChartLayoutPolicy = (model: ChartModel): ChartLayoutPolicy => {
staticPath: staticPaths.has(node.path),
rank,
order,
layerConstraint: node.type === "final" ? "LAST" : null
layerConstraint: rank === 0 ? "FIRST" : node.type === "final" ? "LAST" : null
})
})
}

const edgePolicy = (edge: ChartEdge): ChartEdgeLayoutPolicy => {
if (edge.kind === "targetless" || edge.target === edge.source) {
return { direction: "self", sourceSide: "SOUTH", targetSide: "SOUTH" }
return { direction: "self", sourceSide: "EAST", targetSide: "EAST" }
}
if (edge.target === null) {
return { direction: "forward", sourceSide: "EAST", targetSide: "WEST" }
return { direction: "forward", sourceSide: "SOUTH", targetSide: "NORTH" }
}

const sourceLineage = lineage(edge.source, nodes)
Expand All @@ -182,20 +182,25 @@ export const makeChartLayoutPolicy = (model: ChartModel): ChartLayoutPolicy => {
differentAt++
}
if (differentAt === sourceLineage.length) {
return { direction: "forward", sourceSide: "EAST", targetSide: "WEST" }
return { direction: "forward", sourceSide: "EAST", targetSide: "EAST" }
}
if (differentAt === targetLineage.length) {
return { direction: "backward", sourceSide: "WEST", targetSide: "EAST" }
return { direction: "backward", sourceSide: "WEST", targetSide: "WEST" }
}

const source = nodePolicies.get(sourceLineage[differentAt]!) ?? unreachableNode
const target = nodePolicies.get(targetLineage[differentAt]!) ?? unreachableNode
const backward = source.rank !== null && target.rank !== null
? target.rank < source.rank || target.rank === source.rank && target.order < source.order
: target.order < source.order
if (nodes.get(edge.source)?.parent !== nodes.get(edge.target)?.parent) {
return backward
? { direction: "backward", sourceSide: "WEST", targetSide: "WEST" }
: { direction: "forward", sourceSide: "EAST", targetSide: "EAST" }
}
return backward
? { direction: "backward", sourceSide: "WEST", targetSide: "EAST" }
: { direction: "forward", sourceSide: "EAST", targetSide: "WEST" }
? { direction: "backward", sourceSide: "NORTH", targetSide: "SOUTH" }
: { direction: "forward", sourceSide: "SOUTH", targetSide: "NORTH" }
}

return {
Expand Down
Loading