// Icons + shared UI atoms for BControl v0.5
const Icon = ({ name, size = 16, color = 'currentColor', strokeWidth = 1.75, style }) => {
const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round', style };
switch (name) {
case 'fire': return ;
case 'warning': return ;
case 'shield': return ;
case 'settings': return ;
case 'refresh': return ;
case 'plus': return ;
case 'close': return ;
case 'check': return ;
case 'trash': return ;
case 'search': return ;
case 'filter': return ;
case 'bell': return ;
case 'logout': return ;
case 'user': return ;
case 'list': return ;
case 'shield-admin': return ;
case 'eye': return ;
case 'power': return ;
case 'send': return ;
case 'dot': return ;
case 'chev-down': return ;
case 'chev-right': return ;
case 'server': return ;
case 'terminal': return ;
case 'clock': return ;
default: return null;
}
};
// ───────── Status pill ─────────
const StatusPill = ({ status }) => {
// status: connected | disconnected | pending
const map = {
connected: { bg: BC.ok.bg, fg: BC.ok.fg, dot: BC.ok.dot, label: 'Conectado' },
pending: { bg: BC.pending.bg, fg: BC.pending.fg, dot: BC.pending.dot, label: 'Pendiente' },
disconnected: { bg: BC.fire.bg, fg: BC.fire.fg, dot: BC.fire.dot, label: 'Desconectado' },
};
const s = map[status] || map.disconnected;
return (
{s.label}
);
};
// ───────── Role badge ─────────
const RoleBadge = ({ role }) => {
const map = {
admin: { bg: '#F3E8FF', fg: '#6B21A8', label: 'ADMIN' },
operator: { bg: BC.brandTint, fg: BC.brandDeep, label: 'OPERADOR' },
viewer: { bg: BC.bgAlt, fg: BC.inkMuted, label: 'LECTOR' },
};
const s = map[role] || map.viewer;
return (
{s.label}
);
};
// ───────── App Header ─────────
function AppHeader({ user, counts, animationGen, shouldAnimate, onLogout, onAdmin, onReconnect, onResetNotifications, liveState, onFilter, activeFilter, section, onSection, density, onDensity, onOpenSettings, onSidebarToggle }) {
const [pillHover, setPillHover] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);
const menuRef = React.useRef(null);
React.useEffect(() => {
if (!menuOpen) return;
const handler = (e) => { if (!menuRef.current?.contains(e.target)) setMenuOpen(false); };
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [menuOpen]);
const NotifIcon = ({ iconName, count, cat, severity }) => {
const sel = activeFilter === cat;
return (
);
};
const tabs = [
{ id: 'monitor', label: 'Monitor', icon: 'server' },
{ id: 'stream', label: 'Eventos', icon: 'list' },
];
if (user?.role === 'admin') tabs.push({ id: 'admin', label: 'Administración', icon: 'shield-admin' });
return (
{/* Logo block — centered absolutely */}
{/* Tabs */}
{tabs.map(t => (
))}
{/* Live pill */}
{/* Notification triad */}
{/* User chip with dropdown menu */}
{menuOpen && (
{/* Header */}
{(user?.username || '?').slice(0,1).toUpperCase()}
{user?.username}
{user?.role}
{/* Density settings */}
Densidad
{[{value:'compact',label:'Compacto'},{value:'comfortable',label:'Cómodo'},{value:'spacious',label:'Espacioso'}].map(opt => (
))}
{/* Reset notifications */}
{/* Logout */}
)}
);
}
// ───────── Status bar ─────────
function StatusBar({ panels, connected, events, filter, onClearFilter, lastUpdate }) {
return (
{panels} paneles
{connected} conectados
{panels - connected} sin conexión
{events} eventos
{filter && (<>
Filtro: {filter}
>)}
{!filter &&
}
Última actualización {formatTs(lastUpdate)}
BControl {APP_VERSION} · America/Santiago
);
}
Object.assign(window, { Icon, StatusPill, RoleBadge, AppHeader, StatusBar });