/* 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) && (
)}
{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 (
);
}
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 (
);
}
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 (
);
}
/* ---------------- 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) => (
|
{c.label}
|
))}
{rows.map((r) => {
const rid = idKey(r);
return (
onRowClick(r) : undefined}>
{columns.map((c, i) => (
|
{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()}>
{children}
{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
});