import { useCallback, useRef } from 'react'; import type { DiagramSection } from './types'; import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import type { NodeExecutionState } from '../ExecutionDiagram/types'; import { colorForType } from './node-colors'; import styles from './ProcessDiagram.module.css'; const MINIMAP_WIDTH = 140; const MINIMAP_HEIGHT = 90; const MINIMAP_PADDING = 4; interface MinimapProps { sections: DiagramSection[]; totalWidth: number; totalHeight: number; /** Current zoom/pan state */ scale: number; translateX: number; translateY: number; /** Container pixel dimensions */ containerWidth: number; containerHeight: number; /** Called when user clicks/drags on minimap to pan */ onPan: (translateX: number, translateY: number) => void; /** Execution overlay for coloring nodes */ executionOverlay?: Map; } export function Minimap({ sections, totalWidth, totalHeight, scale, translateX, translateY, containerWidth, containerHeight, onPan, executionOverlay, }: MinimapProps) { const svgRef = useRef(null); const dragging = useRef(false); if (totalWidth <= 0 || totalHeight <= 0) return null; // Scale factor to fit diagram into minimap const innerW = MINIMAP_WIDTH - MINIMAP_PADDING * 2; const innerH = MINIMAP_HEIGHT - MINIMAP_PADDING * 2; const miniScale = Math.min(innerW / totalWidth, innerH / totalHeight); // Viewport rectangle in minimap coordinates // The visible area in diagram-space: from (-translateX/scale) to (-translateX/scale + containerWidth/scale) const vpX = (-translateX / scale) * miniScale + MINIMAP_PADDING; const vpY = (-translateY / scale) * miniScale + MINIMAP_PADDING; const vpW = (containerWidth / scale) * miniScale; const vpH = (containerHeight / scale) * miniScale; const handleClick = useCallback( (e: React.MouseEvent) => { const svg = svgRef.current; if (!svg) return; const rect = svg.getBoundingClientRect(); const mx = e.clientX - rect.left - MINIMAP_PADDING; const my = e.clientY - rect.top - MINIMAP_PADDING; // Convert minimap click to diagram coordinates, then to translate const diagramX = mx / miniScale; const diagramY = my / miniScale; // Center the viewport on the clicked point const newTx = -(diagramX * scale) + containerWidth / 2; const newTy = -(diagramY * scale) + containerHeight / 2; onPan(newTx, newTy); }, [miniScale, scale, containerWidth, containerHeight, onPan], ); const handlePointerDown = useCallback( (e: React.PointerEvent) => { dragging.current = true; (e.currentTarget as SVGSVGElement).setPointerCapture(e.pointerId); }, [], ); const handlePointerMove = useCallback( (e: React.PointerEvent) => { if (!dragging.current) return; const svg = svgRef.current; if (!svg) return; const rect = svg.getBoundingClientRect(); const mx = e.clientX - rect.left - MINIMAP_PADDING; const my = e.clientY - rect.top - MINIMAP_PADDING; const diagramX = mx / miniScale; const diagramY = my / miniScale; const newTx = -(diagramX * scale) + containerWidth / 2; const newTy = -(diagramY * scale) + containerHeight / 2; onPan(newTx, newTy); }, [miniScale, scale, containerWidth, containerHeight, onPan], ); const handlePointerUp = useCallback( (e: React.PointerEvent) => { dragging.current = false; (e.currentTarget as SVGSVGElement).releasePointerCapture(e.pointerId); }, [], ); return (
{/* Background */} {/* Render simplified nodes for each section */} {sections.map((section, si) => ( {renderMinimapNodes(section.nodes, executionOverlay)} ))} {/* Viewport indicator */}
); } function nodeColor( node: DiagramNodeType, overlay?: Map, ): { fill: string; opacity: number } { if (!overlay) return { fill: colorForType(node.type), opacity: 0.7 }; const state = node.id ? overlay.get(node.id) : undefined; if (state?.status === 'COMPLETED') return { fill: 'var(--success)', opacity: 0.85 }; if (state?.status === 'FAILED') return { fill: 'var(--error)', opacity: 0.85 }; // ENDPOINT is always traversed when overlay is active (route entry point) if (node.type === 'ENDPOINT' && overlay.size > 0) return { fill: 'var(--success)', opacity: 0.85 }; // Skipped (overlay active but node not executed) return { fill: 'var(--text-muted)', opacity: 0.35 }; } function renderMinimapNodes( nodes: DiagramNodeType[], overlay?: Map, ): React.ReactNode[] { const elements: React.ReactNode[] = []; for (const node of nodes) { const x = node.x ?? 0; const y = node.y ?? 0; const w = node.width ?? 160; const h = node.height ?? 40; if (node.children && node.children.length > 0) { const { fill, opacity } = nodeColor(node, overlay); elements.push( , ); elements.push(...renderMinimapNodes(node.children, overlay)); } else { const { fill, opacity } = nodeColor(node, overlay); elements.push( , ); } } return elements; }