import { useCallback, useRef } from 'react'; import type { DiagramSection } from './types'; import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; 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; } export function Minimap({ sections, totalWidth, totalHeight, scale, translateX, translateY, containerWidth, containerHeight, onPan, }: 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)} ))} {/* Viewport indicator */}
); } function renderMinimapNodes(nodes: DiagramNodeType[]): 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; const color = colorForType(node.type); if (node.children && node.children.length > 0) { // Compound: border only elements.push( , ); elements.push(...renderMinimapNodes(node.children)); } else { // Leaf: filled rectangle elements.push( , ); } } return elements; }