/* ============================================================
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 (
);
}
Object.assign(window, { Icon, Logo, Btn, Sev, StatusBadge, Gauge, PageHeader, ToolCard, Stat, Empty, SevDonut, PlanGate, usePlan });