import { useState } from 'react'; interface Node { id: string; label: string; x: number; y: number; w: number; h: number; type: 'main' | 'side'; desc: string; } interface Edge { from: string; to: string; type: 'main' | 'side'; } const NODES: Node[] = [ { id: 'frontend', label: 'FRONTEND', x: 80, y: 16, w: 160, h: 38, type: 'main', desc: 'Portales, apps y paneles:interfaces donde el usuario opera' }, { id: 'api', label: 'API GATEWAY', x: 80, y: 108, w: 160, h: 38, type: 'main', desc: 'Seguridad, ruteo, rate limiting y versionado unificados' }, { id: 'logic', label: 'LÓGICA DE NEGOCIO', x: 80, y: 200, w: 160, h: 38, type: 'main', desc: 'Reglas de negocio, validaciones, procesamiento y orquestación' }, { id: 'db', label: 'BASE DE DATOS', x: 80, y: 292, w: 160, h: 38, type: 'main', desc: 'Persistencia diseñada para tu modelo:SQL, NoSQL, caché' }, { id: 'infra', label: 'INFRAESTRUCTURA', x: 80, y: 384, w: 160, h: 36, type: 'main', desc: 'Nube, contenedores, CI/CD y observabilidad operacional' }, { id: 'payments', label: 'PAGOS', x: 8, y: 172, w: 62, h: 28, type: 'side', desc: 'Stripe, Conekta:flujo de cobro integrado' }, { id: 'erp', label: 'ERP / CRM', x: 8, y: 222, w: 62, h: 28, type: 'side', desc: 'SAP, Odoo:sincronización bidireccional' }, { id: 'auth', label: 'ACCESO', x: 250, y: 84, w: 62, h: 28, type: 'side', desc: 'SSO, JWT, OAuth 2.0:identidad y permisos por rol' }, { id: 'external', label: 'EXTERNOS', x: 250, y: 134, w: 62, h: 28, type: 'side', desc: 'APIs de terceros:logística, notificaciones, reportes' }, ]; const EDGES: Edge[] = [ { from: 'frontend', to: 'api', type: 'main' }, { from: 'api', to: 'logic', type: 'main' }, { from: 'logic', to: 'db', type: 'main' }, { from: 'db', to: 'infra', type: 'main' }, { from: 'payments', to: 'logic', type: 'side' }, { from: 'erp', to: 'logic', type: 'side' }, { from: 'auth', to: 'api', type: 'side' }, { from: 'external', to: 'api', type: 'side' }, ]; function nodeCenter(n: Node) { return { cx: n.x + n.w / 2, cy: n.y + n.h / 2 }; } function edgeLine(edge: Edge): { x1: number; y1: number; x2: number; y2: number } { const from = NODES.find(n => n.id === edge.from)!; const to = NODES.find(n => n.id === edge.to)!; const fc = nodeCenter(from); const tc = nodeCenter(to); if (edge.type === 'main') { return { x1: fc.cx, y1: from.y + from.h, x2: tc.cx, y2: to.y }; } if (from.x < to.x) { return { x1: from.x + from.w, y1: fc.cy, x2: to.x, y2: tc.cy }; } return { x1: from.x, y1: fc.cy, x2: to.x + to.w, y2: tc.cy }; } function isConnected(nodeId: string, activeId: string | null) { if (!activeId || nodeId === activeId) return false; return EDGES.some(e => (e.from === activeId && e.to === nodeId) || (e.to === activeId && e.from === nodeId), ); } function isEdgeActive(edge: Edge, activeId: string | null) { if (!activeId) return false; return edge.from === activeId || edge.to === activeId; } const TOOLTIP_W = 172; const TAIL_H = 9; const TAIL_W = 14; function tooltipHeight(desc: string): number { const charsPerLine = Math.floor((TOOLTIP_W - 26) / 6.3); // ~aprox chars por línea a 10.5px const lines = Math.ceil(desc.length / charsPerLine); return 14 + 16 + lines * 17 + 14; // paddingTop + label + líneas + paddingBottom } export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) { const [active, setActive] = useState(null); const RED = '#e50914'; const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'; const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)'; const connStroke = 'rgba(229,9,20,0.45)'; const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)'; const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff'; const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)'; void tooltipBorder; const activeDesc = NODES.find(n => n.id === active); return (
setActive(null)} > {/* Edges */} {EDGES.map((edge, i) => { const { x1, y1, x2, y2 } = edgeLine(edge); const edgeActive = isEdgeActive(edge, active); return ( ); })} {/* Nodes */} {NODES.map(node => { const isActive = active === node.id; const isConn = isConnected(node.id, active); const isDim = active !== null && !isActive && !isConn; const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke; const fillOpacity = isActive ? 0.12 : 0; const textColor = isActive ? RED : isDim ? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)') : (isNavyBg ? 'rgba(255,255,255,0.65)' : 'rgba(0,31,91,0.75)'); const { cx } = nodeCenter(node); return ( setActive(node.id)} onFocus={() => setActive(node.id)} onBlur={() => setActive(null)} style={{ cursor: 'default', outline: 'none' }} tabIndex={0} role="button" aria-label={`${node.label}: ${node.desc}`} > {node.label} ); })} {/* Tooltip burbuja estilo chat */} {activeDesc && (() => { const node = NODES.find(n => n.id === active)!; const TOOLTIP_H = tooltipHeight(activeDesc.desc); const cy = node.y + node.h / 2; // nodos izquierda → burbuja a la derecha // nodos derecha → burbuja a la izquierda // nodos centro → burbuja a la derecha const showLeft = node.x > 160; // solo los nodos de la derecha van a la izquierda let tx: number, ty: number, tailPoints: string; if (showLeft) { tx = node.x - TOOLTIP_W - TAIL_H - 4; ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H); const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16); tailPoints = `${tx + TOOLTIP_W},${tailCy - TAIL_W / 2} ${tx + TOOLTIP_W},${tailCy + TAIL_W / 2} ${tx + TOOLTIP_W + TAIL_H},${tailCy}`; } else { tx = node.x + node.w + TAIL_H + 4; ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H); const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16); tailPoints = `${tx},${tailCy - TAIL_W / 2} ${tx},${tailCy + TAIL_W / 2} ${tx - TAIL_H},${tailCy}`; } return (

{activeDesc.label}

{activeDesc.desc}

); })()}
); }