/* global React, lucide */ // ======================================================================= // BlazeConnector Admin v3 — UI primitives // StatusBadge, KpiCard, DataTable, FilterBar, JsonViewer, EmptyState, // Modal, Drawer, ConfirmDialog, Channel/Tenant avatars // ======================================================================= const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React; /* ---------------- Lucide icon helper -------------------------------- */ function Icon({ name, size = 14, className = '', style }) { const ref = useRef(null); useLayoutEffect(() => { if (ref.current && window.lucide) { ref.current.setAttribute('data-lucide', name); ref.current.innerHTML = ''; try { window.lucide.createIcons({ icons: window.lucide.icons, attrs: {}, nameAttr: 'data-lucide' }); } catch (e) {} } }, [name]); return ; } /* ---------------- Channel / tenant avatars ------------------------- */ function ChannelChip({ name, size = 'md' }) { const cls = 'ch ' + (size === 'sm' ? 'sm' : size === 'lg' ? 'lg' : '') + ' ch-' + name; return {window.CHANNEL_LETTER[name] || '·'}; } function TenantAvatar({ tenant, size = 20 }) { if (!tenant) return null; const palette = ['av-1', 'av-2', 'av-3', 'av-4', 'av-5', 'av-6', 'av-7']; const cls = palette[(tenant.id || '').charCodeAt(4) % palette.length]; const initials = tenant.name.split(' ').slice(0, 2).map(s => s[0]).join('').toUpperCase(); return {initials}; } function UserAvatar({ name, size = 24 }) { const palette = ['av-1', 'av-2', 'av-3', 'av-4', 'av-5', 'av-6']; const cls = palette[(name || '').charCodeAt(0) % palette.length]; const initials = (name || '?').split(' ').slice(0, 2).map(s => s[0]).join('').toUpperCase(); return {initials}; } /* ---------------- StatusBadge --------------------------------------- */ const STATUS_MAP = { active: 'success', healthy: 'success', delivered: 'success', read: 'success', sent: 'info', queued: 'warn', pending: 'warn', failed: 'danger', error: 'danger', down: 'danger', critical: 'danger', expired: 'neutral', inactive: 'neutral', suspended: 'neutral', revoked: 'neutral', disabled: 'neutral', warning: 'warn', degraded: 'warn', info: 'info', processing: 'info', running: 'info', approved: 'success', rejected: 'danger', synced: 'info' }; function StatusBadge({ status, tone, label, square = false }) { const t = tone || STATUS_MAP[(status || '').toLowerCase()] || 'neutral'; return ( {!square && } {label || status} ); } /* ---------------- Card / SectionCard ------------------------------- */ function SectionCard({ title, sub, icon, actions, children, flush = false, className = '' }) { return (
{(title || actions) && (
{icon && } {title} {sub && {sub}}
{actions &&
{actions}
}
)}
{children}
); } /* ---------------- KpiCard ------------------------------------------ */ function fmtNum(n) { if (n == null) return '—'; if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (Math.abs(n) >= 1e4) return (n / 1e3).toFixed(1) + 'k'; return n.toLocaleString('en-US'); } function KpiCard({ label, value, unit, delta, deltaTone, series, foot, color = 'orange', icon }) { return (
{icon && }{label} {delta != null && ( 0 ? 'up' : delta < 0 ? 'down' : 'neutral'))}> {delta > 0 ? '↑' : delta < 0 ? '↓' : '·'}{Math.abs(delta)}% )}
{typeof value === 'number' ? fmtNum(value) : value} {unit && {unit}}
{series && } {foot &&
{foot}
}
); } /* ---------------- Sparkline + Area + Bar (SVG, no deps) ------------ */ const COLOR_MAP = { orange: '#FF5A1F', magenta: '#F000D8', violet: '#7A3CFF', blue: '#1E7BFF', green: '#2ECC5A', warn: '#F2A900', danger: '#E03E3E', teal: '#16C6B9' }; function Sparkline({ data, color = 'orange', strokeWidth = 1.5, className = '' }) { if (!data || data.length === 0) return null; const w = 100, h = 30, pad = 2; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const step = (w - pad * 2) / (data.length - 1 || 1); const pts = data.map((v, i) => [pad + i * step, h - pad - ((v - min) / range) * (h - pad * 2)]); const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' '); const area = path + ' L' + (w - pad).toFixed(2) + ',' + (h - pad) + ' L' + pad + ',' + (h - pad) + ' Z'; const c = COLOR_MAP[color] || color; const gradId = 'spark-' + color + '-' + Math.random().toString(36).slice(2, 7); return ( ); } function AreaChart({ data, height = 180, color = 'orange', yAxis = true, xLabels = null, gridLines = 4, formatY }) { if (!data || data.length === 0) return null; const w = 600, h = height; const padL = yAxis ? 32 : 6, padR = 8, padT = 8, padB = xLabels ? 22 : 8; const min = 0; const max = Math.max(...data) * 1.15 || 1; const range = max - min || 1; const innerW = w - padL - padR; const innerH = h - padT - padB; const step = innerW / (data.length - 1 || 1); const pts = data.map((v, i) => [padL + i * step, padT + innerH - ((v - min) / range) * innerH]); const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' '); const area = path + ' L' + (padL + innerW).toFixed(2) + ',' + (padT + innerH) + ' L' + padL + ',' + (padT + innerH) + ' Z'; const c = COLOR_MAP[color] || color; const gradId = 'area-' + color + '-' + Math.random().toString(36).slice(2, 7); const yTicks = []; for (let i = 0; i <= gridLines; i++) { const v = (max / gridLines) * i; const y = padT + innerH - (i / gridLines) * innerH; yTicks.push({ v, y }); } return ( {yTicks.map((t, i) => ( {yAxis && {formatY ? formatY(t.v) : fmtNum(Math.round(t.v))}} ))} {xLabels && xLabels.map((lbl, i) => { const x = padL + (i / (xLabels.length - 1)) * innerW; return {lbl}; })} ); } function BarChart({ data, height = 160, color = 'blue', xLabels = null, formatY }) { if (!data || data.length === 0) return null; const w = 600, h = height; const padL = 32, padR = 6, padT = 8, padB = xLabels ? 22 : 6; const max = Math.max(...data) * 1.15 || 1; const innerW = w - padL - padR; const innerH = h - padT - padB; const barW = (innerW / data.length) * 0.62; const gap = innerW / data.length; const c = COLOR_MAP[color] || color; const yTicks = [0, 0.25, 0.5, 0.75, 1].map(p => ({ v: max * p, y: padT + innerH - p * innerH })); return ( {yTicks.map((t, i) => ( {formatY ? formatY(t.v) : fmtNum(Math.round(t.v))} ))} {data.map((v, i) => { const bh = (v / max) * innerH; const x = padL + i * gap + (gap - barW) / 2; const y = padT + innerH - bh; return ; })} {xLabels && xLabels.map((lbl, i) => { const x = padL + (i + 0.5) * gap; return {lbl}; })} ); } function StackedBar({ segments, height = 8 }) { // segments: [{ value, color, label }] const total = segments.reduce((a, s) => a + s.value, 0) || 1; return (
{segments.map((s, i) => (
))}
); } function Donut({ segments, size = 120, thickness = 14, centerLabel, centerSub }) { const r = (size - thickness) / 2; const cx = size / 2, cy = size / 2; const total = segments.reduce((a, s) => a + s.value, 0) || 1; const circ = 2 * Math.PI * r; let acc = 0; return ( {segments.map((s, i) => { const len = (s.value / total) * circ; const off = -acc; acc += len; return ; })} {centerLabel && {centerLabel}} {centerSub && {centerSub}} ); } /* ---------------- DataTable (controlled but light) ------------------ */ function DataTable({ columns, rows, density = 'cozy', empty, onRowClick, selectedId, getRowId }) { if (!rows || rows.length === 0) { return empty || ; } const idKey = getRowId || ((r) => r.id); return (
{columns.map((c, i) => ( ))} {rows.map((r) => { const rid = idKey(r); return ( onRowClick(r) : undefined}> {columns.map((c, i) => ( ))} ); })}
{c.label}
{c.render ? c.render(r) : r[c.key]}
); } /* ---------------- FilterBar / chips -------------------------------- */ function FilterBar({ children }) { return
{children}
; } function FilterSearch({ value, onChange, placeholder = 'Buscar' }) { return (
onChange(e.target.value)} placeholder={placeholder} /> {value && }
); } function FilterChip({ label, value, active, onClick, icon = 'chevrons-up-down' }) { return ( ); } /* ---------------- JSON viewer (syntax-highlighted) ------------------ */ function JsonViewer({ data, maxHeight }) { const text = useMemo(() => { try { return JSON.stringify(data, null, 2); } catch (e) { return String(data); } }, [data]); // syntax highlight const highlighted = text .replace(/&/g, '&').replace(//g, '>') .replace(/("|")([^"]+)("|")(\s*:)/g, '"$2"$4') .replace(/:\s*("|")([^"]*?)("|")/g, ': "$2"') .replace(/:\s*(-?\d+\.?\d*)/g, ': $1') .replace(/:\s*(true|false)/g, ': $1') .replace(/:\s*null/g, ': null'); return
; } /* ---------------- Empty state -------------------------------------- */ function EmptyState({ icon = 'inbox', title, sub, action }) { return (
{title}
{sub &&
{sub}
} {action &&
{action}
}
); } /* ---------------- Modal + Drawer + Confirm -------------------------- */ function Modal({ title, sub, onClose, children, actions }) { useEffect(() => { const fn = (e) => { if (e.key === 'Escape') onClose && onClose(); }; window.addEventListener('keydown', fn); return () => window.removeEventListener('keydown', fn); }, [onClose]); return (
e.stopPropagation()}>

{title}

{sub &&

{sub}

}
{children}
{actions &&
{actions}
}
); } function Drawer({ title, sub, onClose, children, actions, width }) { useEffect(() => { const fn = (e) => { if (e.key === 'Escape') onClose && onClose(); }; window.addEventListener('keydown', fn); return () => window.removeEventListener('keydown', fn); }, [onClose]); return (
); } function ConfirmDialog({ title, message, danger = false, confirmLabel = 'Confirmar', onConfirm, onClose }) { return ( }> ); } /* ---------------- Misc helpers ------------------------------------- */ function timeAgo(iso) { const d = (Date.now() - new Date(iso).getTime()) / 1000; if (d < 5) return 'ahora'; if (d < 60) return Math.floor(d) + 's'; if (d < 3600) return Math.floor(d / 60) + 'm'; if (d < 86400) return Math.floor(d / 3600) + 'h'; if (d < 86400 * 7) return Math.floor(d / 86400) + 'd'; return new Date(iso).toLocaleDateString('es-DO'); } function fmtTime(iso) { const d = new Date(iso); return d.toLocaleTimeString('es-DO', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } function fmtDateTime(iso) { const d = new Date(iso); return d.toLocaleDateString('es-DO', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false }); } function fmtFullTime(iso) { const d = new Date(iso); return d.toISOString().replace('T', ' ').slice(0, 23) + 'Z'; } Object.assign(window, { Icon, ChannelChip, TenantAvatar, UserAvatar, StatusBadge, SectionCard, KpiCard, Sparkline, AreaChart, BarChart, StackedBar, Donut, DataTable, FilterBar, FilterSearch, FilterChip, JsonViewer, EmptyState, Modal, Drawer, ConfirmDialog, fmtNum, timeAgo, fmtTime, fmtDateTime, fmtFullTime, COLOR_MAP });