/* ===========================================================
Shared UI primitives, icons, charts, helpers
=========================================================== */
const { useState, useEffect, useRef, useMemo, useCallback, Fragment } = React;
/* ---------- Icons (inline SVG, currentColor) ---------- */
const I = {
layout: (p) => ,
funnel: (p) => ,
list: (p) => ,
terminal: (p) => ,
trophy: (p) => ,
swords: (p) => ,
retry: (p) => ,
newspaper: (p) => ,
settings: (p) => ,
bell: (p) => ,
moon: (p) => ,
sun: (p) => ,
check: (p) => ,
x: (p) => ,
alert: (p) => ,
arrowUp: (p) => ,
arrowDown: (p) => ,
ext: (p) => ,
chevron: (p) => ,
search: (p) => ,
back: (p) => ,
menu: (p) => ,
spark: (p) => ,
link: (p) => ,
};
/* ---------- Chip / Badge ---------- */
function StatusChip({ status }) {
const map = {
success: { cls: 'ok', label: 'success' },
partial: { cls: 'warn', label: 'partial' },
error: { cls: 'err', label: 'error' },
};
const s = map[status] || { cls: '', label: status };
return {s.label};
}
function Chip({ children, kind, style }) {
return {children};
}
function StatusDot({ status }) {
return ;
}
/* ---------- Sparkline (mini line) ---------- */
function Sparkline({ data, color, height=28 }) {
if (!data || data.length === 0) return null;
const w = 100, h = height;
const min = Math.min(...data), max = Math.max(...data);
const range = max - min || 1;
const pts = data.map((v, i) => {
const x = (i / (data.length - 1)) * w;
const y = h - ((v - min) / range) * (h - 4) - 2;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
return (
);
}
/* ---------- Donut (svg) ---------- */
function Donut({ data, size=170, thickness=24 }) {
// data: [{label, value, color}]
const total = data.reduce((a, d) => a + d.value, 0);
const r = (size - thickness) / 2;
const cx = size / 2, cy = size / 2;
const circ = 2 * Math.PI * r;
let offset = 0;
return (
{data.map((d, i) => (
{d.label}
{d.value.toFixed(3)}
))}
);
}
/* ---------- Funnel ---------- */
function Funnel({ stages }) {
const max = Math.max(...stages.map(s => s.value));
return (
{stages.map((s, i) => {
const w = (s.value / max) * 100;
const prev = i === 0 ? s.value : stages[i-1].value;
const ret = i === 0 ? 100 : (s.value / prev * 100);
const drop = ret < 50;
return (
{s.name}
{s.value.toLocaleString()}
{s.value}
{i === 0 ? '—' : `${ret.toFixed(1)}%`}
);
})}
);
}
/* ---------- Gantt ---------- */
function Gantt({ lanes, totalDuration }) {
return (
{lanes.map((lane, i) => (
{lane.name}
{lane.bars.map((b, j) => {
const left = (b.start / totalDuration) * 100;
const width = (b.duration / totalDuration) * 100;
return (
{b.label}
);
})}
))}
0s
{(totalDuration/4).toFixed(0)}s
{(totalDuration/2).toFixed(0)}s
{(totalDuration*3/4).toFixed(0)}s
{totalDuration.toFixed(0)}s
);
}
/* ---------- Score breakdown stacked bar ---------- */
function ScoreBar({ breakdown, total }) {
const entries = [
{ key: 'source', label: 'Source', color: '#5b8def' },
{ key: 'freshness', label: 'Freshness', color: 'var(--gold)' },
{ key: 'relevance', label: 'Relevance', color: '#4ec9b0' },
{ key: 'trends', label: 'Trends', color: '#d685c3' },
{ key: 'gemini', label: 'Gemini', color: '#c98c4e' },
];
return (
`${e.label}: ${(breakdown[e.key]||0).toFixed(3)}`).join(' · ')}>
{entries.map(e => {
const v = breakdown[e.key] || 0;
const w = (v / total) * 100;
return
;
})}
);
}
/* ---------- helpers ---------- */
function fmtDuration(secs) {
if (secs == null) return '—';
const m = Math.floor(secs / 60);
const s = Math.floor(secs % 60);
return `${m}:${String(s).padStart(2, '0')}`;
}
function fmtAgo(iso) {
const now = Date.now();
const t = new Date(iso).getTime();
const diff = Math.floor((now - t) / 1000);
if (diff < 60) return `${diff}s atrás`;
if (diff < 3600) return `${Math.floor(diff/60)}m atrás`;
if (diff < 86400) return `${Math.floor(diff/3600)}h ${Math.floor((diff%3600)/60)}m atrás`;
return `${Math.floor(diff/86400)}d atrás`;
}
function fmtClock(iso) {
const d = new Date(iso);
return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`;
}
Object.assign(window, {
I, StatusChip, StatusDot, Chip, Sparkline, Donut, Funnel, Gantt, ScoreBar,
fmtDuration, fmtAgo, fmtClock,
});