/* ============================================================ FLOWSHIELD — shared UI components (JSX / babel) ============================================================ */ const { useState, useEffect, useRef, useMemo } = React; /* Icon */ function Icon({ name, size = 18, style, className }) { const inner = window.ICONS[name] || ''; return ( ); } /* Brand logo: animated hex shield + flow wave */ function Logo({ size = 34, withText = true, glow = true }) { return (
{withText && (
FlowShield
SEC OPS PLATFORM
)}
); } /* Button */ function Btn({ children, variant = 'default', size, icon, iconR, onClick, style, className = '', glitch, type }) { const cls = ['btn']; if (variant === 'primary') cls.push('btn-primary'); if (variant === 'solid') cls.push('btn-solid'); if (variant === 'ghost') cls.push('btn-ghost'); if (size === 'sm') cls.push('btn-sm'); if (size === 'lg') cls.push('btn-lg'); if (size === 'block') cls.push('btn-block'); return ( ); } /* Severity badge */ function Sev({ level, pulse, count }) { return ( {window.sevLabel(level)}{count != null ? ' · ' + count : ''} ); } /* Status badge */ function StatusBadge({ status }) { const map = { open: { c:'sev-high', t:'Abierto' }, triage: { c:'sev-low', t:'En triaje' }, fixed: { c:'sev-ok', t:'Resuelto' }, running: { c:'sev-low', t:'En curso', pulse:true }, done: { c:'sev-ok', t:'Completado' }, pass: { c:'sev-ok', t:'PASS' }, fail: { c:'sev-crit', t:'FAIL' }, }; const m = map[status] || { c:'sev-info', t:status }; return ( {m.t} ); } /* Radial gauge */ function Gauge({ value, size = 116, label, sub, color = 'var(--acc)', thick = 9 }) { const r = (size - thick) / 2; const c = 2 * Math.PI * r; const [v, setV] = useState(0); useEffect(() => { const t = setTimeout(() => setV(value), 120); return () => clearTimeout(t); }, [value]); return (
{label != null ? label : v + '%'}
{sub &&
{sub}
}
); } /* Page header */ function PageHeader({ title, sub, crumb, right }) { return (
{crumb &&
{crumb}
}

{title}

{sub &&

{sub}

}
{right &&
{right}
}
); } /* Tool card with reticle + cursor-follow glow */ /* Re-render on plan change. */ function usePlan() { const [, bump] = useState(0); useEffect(() => (window.fsPlan ? window.fsPlan.sub(() => bump(n => n + 1)) : undefined), []); return window.fsPlan; } function ToolCard({ tool, onClick }) { const ref = useRef(null); usePlan(); // re-evaluar el bloqueo al cambiar de plan const eng = window.redteamEngine ? window.redteamEngine(tool.id) : null; const planned = !!(eng && eng.status === 'planned'); const tierLocked = !planned && window.fsToolAllowed && !window.fsToolAllowed(tool.id); const locked = planned || tierLocked; const note = planned ? 'PREPARANDO MÓDULO · PRÓXIMAMENTE' : (tierLocked ? ('PLAN ' + (window.fsPlanForTier ? window.fsPlanForTier(window.fsToolTier(tool.id)).toUpperCase() : 'SUPERIOR') + ' · MEJORA TU PLAN') : ''); const onMove = (e) => { if (locked) return; const el = ref.current; if (!el) return; const b = el.getBoundingClientRect(); el.style.setProperty('--mx', ((e.clientX - b.left) / b.width) * 100 + '%'); el.style.setProperty('--my', ((e.clientY - b.top) / b.height) * 100 + '%'); }; return (
{tool.name} {locked && }
{tool.desc}
{locked && (
{note}
)}
); } /* Pantalla mostrada cuando se entra a un módulo no incluido en el plan. */ function PlanGate({ module, onNav }) { const plan = usePlan(); const need = window.fsModuleTier ? window.fsModuleTier(module) : 1; const needName = window.fsPlanForTier ? window.fsPlanForTier(need) : 'superior'; const titles = { blueteam:'Blue Team · Hardening', normativa:'Normativa · Cumplimiento' }; return (
Módulo no incluido en tu plan
Tu plan actual es {plan ? plan.name() : '—'}. Este módulo requiere el plan {needName} o superior.
onNav && onNav('settings')}>Ver planes y cambiar
); } /* Stat tile */ function Stat({ label, value, icon, accent = 'var(--acc)', sub, delta }) { return (
{label}
{value}
{sub &&
{sub}
}
{icon && (
)}
{delta != null && (
= 0 ? 'var(--ok)' : 'var(--red)' }}> {delta >= 0 ? '▲' : '▼'} {Math.abs(delta)} últimos 7d
)}
); } /* Empty state */ function Empty({ icon, title, sub, action }) { return (
{title}
{sub &&
{sub}
} {action}
); } /* Donut for severity breakdown */ function SevDonut({ data, size = 130 }) { const order = ['crit','high','med','low']; const colors = { crit:'var(--sev-crit)', high:'var(--sev-high)', med:'var(--sev-med)', low:'var(--sev-low)' }; const total = order.reduce((s,k)=>s+(data[k]||0),0) || 1; const thick = 14, r = (size-thick)/2, c = 2*Math.PI*r; let off = 0; return (
{order.map(k => { const val = data[k]||0; if (!val) return null; const len = (val/total)*c; const seg = ; off += len; return seg; })}
{total}
HALLAZGOS
); } Object.assign(window, { Icon, Logo, Btn, Sev, StatusBadge, Gauge, PageHeader, ToolCard, Stat, Empty, SevDonut, PlanGate, usePlan });