feat: add traversed/not-traversed visual states to DiagramEdge

Add green solid edges for traversed paths and dashed gray for
not-traversed when execution overlay is active. Includes green
arrowhead marker and overlay threading through CompoundNode and
ErrorSection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 18:47:59 +01:00
parent 2b805ec196
commit 3029704051
4 changed files with 61 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
import type { NodeConfig } from './types';
import type { NodeExecutionState } from '../ExecutionDiagram/types';
import { colorForType, isCompoundType } from './node-colors';
import { DiagramNode } from './DiagramNode';
import { DiagramEdge } from './DiagramEdge';
@@ -17,6 +18,8 @@ interface CompoundNodeProps {
selectedNodeId?: string;
hoveredNodeId: string | null;
nodeConfigs?: Map<string, NodeConfig>;
/** Execution overlay for edge traversal coloring */
executionOverlay?: Map<string, NodeExecutionState>;
onNodeClick: (nodeId: string) => void;
onNodeDoubleClick?: (nodeId: string) => void;
onNodeEnter: (nodeId: string) => void;
@@ -25,7 +28,7 @@ interface CompoundNodeProps {
export function CompoundNode({
node, edges, parentX = 0, parentY = 0,
selectedNodeId, hoveredNodeId, nodeConfigs,
selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
}: CompoundNodeProps) {
const x = (node.x ?? 0) - parentX;
@@ -78,15 +81,21 @@ export function CompoundNode({
{/* Internal edges (rendered after background, before children) */}
<g className="edges">
{internalEdges.map((edge, i) => (
<DiagramEdge
key={`${edge.sourceId}-${edge.targetId}-${i}`}
edge={{
...edge,
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
}}
/>
))}
{internalEdges.map((edge, i) => {
const isTraversed = executionOverlay
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
: undefined;
return (
<DiagramEdge
key={`${edge.sourceId}-${edge.targetId}-${i}`}
edge={{
...edge,
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
}}
traversed={isTraversed}
/>
);
})}
</g>
{/* Children — recurse into compound children, render leaves as DiagramNode */}
@@ -102,6 +111,7 @@ export function CompoundNode({
selectedNodeId={selectedNodeId}
hoveredNodeId={hoveredNodeId}
nodeConfigs={nodeConfigs}
executionOverlay={executionOverlay}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeEnter={onNodeEnter}

View File

@@ -3,9 +3,11 @@ import type { DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'
interface DiagramEdgeProps {
edge: DiagramEdgeType;
offsetY?: number;
/** undefined = no overlay (default gray solid), true = traversed (green solid), false = not traversed (gray dashed) */
traversed?: boolean | undefined;
}
export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
export function DiagramEdge({ edge, offsetY = 0, traversed }: DiagramEdgeProps) {
const pts = edge.points;
if (!pts || pts.length < 2) return null;
@@ -29,9 +31,10 @@ export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
<path
d={d}
fill="none"
stroke="#9CA3AF"
strokeWidth={1.5}
markerEnd="url(#arrowhead)"
stroke={traversed === true ? '#3D7C47' : '#9CA3AF'}
strokeWidth={traversed === true ? 1.5 : traversed === false ? 1 : 1.5}
strokeDasharray={traversed === false ? '4,3' : undefined}
markerEnd={traversed === true ? 'url(#arrowhead-green)' : traversed === false ? undefined : 'url(#arrowhead)'}
/>
{edge.label && pts.length >= 2 && (
<text

View File

@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import type { DiagramSection } from './types';
import type { NodeConfig } from './types';
import type { NodeExecutionState } from '../ExecutionDiagram/types';
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
import { DiagramEdge } from './DiagramEdge';
import { DiagramNode } from './DiagramNode';
@@ -16,6 +17,8 @@ interface ErrorSectionProps {
selectedNodeId?: string;
hoveredNodeId: string | null;
nodeConfigs?: Map<string, NodeConfig>;
/** Execution overlay for edge traversal coloring */
executionOverlay?: Map<string, NodeExecutionState>;
onNodeClick: (nodeId: string) => void;
onNodeDoubleClick?: (nodeId: string) => void;
onNodeEnter: (nodeId: string) => void;
@@ -28,7 +31,7 @@ const VARIANT_COLORS: Record<string, string> = {
};
export function ErrorSection({
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs,
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
}: ErrorSectionProps) {
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
@@ -82,9 +85,14 @@ export function ErrorSection({
<g transform={`translate(${CONTENT_PADDING_LEFT}, ${CONTENT_PADDING_Y})`}>
{/* Edges */}
<g className="edges">
{section.edges.map((edge, i) => (
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
))}
{section.edges.map((edge, i) => {
const isTraversed = executionOverlay
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
: undefined;
return (
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
);
})}
</g>
{/* Nodes */}
@@ -99,6 +107,7 @@ export function ErrorSection({
selectedNodeId={selectedNodeId}
hoveredNodeId={hoveredNodeId}
nodeConfigs={nodeConfigs}
executionOverlay={executionOverlay}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeEnter={onNodeEnter}

View File

@@ -53,6 +53,7 @@ export function ProcessDiagram({
knownRouteIds,
className,
diagramLayout,
executionOverlay,
}: ProcessDiagramProps) {
// Route stack for drill-down navigation
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
@@ -209,14 +210,29 @@ export function ProcessDiagram({
>
<polygon points="0 0, 8 3, 0 6" fill="#9CA3AF" />
</marker>
<marker
id="arrowhead-green"
markerWidth="8"
markerHeight="6"
refX="7"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="#3D7C47" />
</marker>
</defs>
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
{/* Main section top-level edges (not inside compounds) */}
<g className="edges">
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => (
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
))}
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => {
const isTraversed = executionOverlay
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
: undefined;
return (
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
);
})}
</g>
{/* Main section nodes */}
@@ -231,6 +247,7 @@ export function ProcessDiagram({
selectedNodeId={selectedNodeId}
hoveredNodeId={toolbar.hoveredNodeId}
nodeConfigs={nodeConfigs}
executionOverlay={executionOverlay}
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
onNodeEnter={toolbar.onNodeEnter}
@@ -265,6 +282,7 @@ export function ProcessDiagram({
selectedNodeId={selectedNodeId}
hoveredNodeId={toolbar.hoveredNodeId}
nodeConfigs={nodeConfigs}
executionOverlay={executionOverlay}
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
onNodeEnter={toolbar.onNodeEnter}