// Shared UI primitives + icon library
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;
// --- Icons (lucide-ish, drawn at 16x16 viewBox 24) ---
const Icon = ({ d, size = 16, stroke = 1.6, fill, children, ...rest }) => (
);
const I = {
home: (p) => ,
play: (p) => ,
bot: (p) => ,
key: (p) => ,
wallet: (p) => ,
list: (p) => ,
book: (p) => ,
users: (p) => ,
chart: (p) => ,
cube: (p) => ,
star: (p) => ,
wrench: (p) => ,
cog: (p) => ,
pulse: (p) => ,
plus: (p) => ,
search: (p) => ,
filter: (p) => ,
copy: (p) => ,
trash: (p) => ,
edit: (p) => ,
check: (p) => ,
x: (p) => ,
download: (p) => ,
send: (p) => ,
sun: (p) => ,
moon: (p) => ,
flask: (p) => ,
zap: (p) => ,
arrowUp: (p) => ,
arrowDown: (p) => ,
arrowRight: (p) => ,
eye: (p) => ,
eyeOff: (p) => ,
more: (p) => ,
refresh: (p) => ,
globe: (p) => ,
shield: (p) => ,
spark: (p) => ,
link: (p) => ,
file: (p) => ,
logout: (p) => ,
menu: (p) => ,
warn: (p) => ,
code: (p) => ,
arrowRight: (p) => ,
alert: (p) => ,
};
window.I = I;
window.Icon = Icon;
// --- Sparkline ---
function Spark({ data, color = "var(--accent)", w = 80, h = 30 }) {
if (!data || !data.length) return null;
const min = Math.min(...data), max = Math.max(...data), range = max - min || 1;
const step = w / (data.length - 1);
const pts = data.map((v, i) => `${i*step},${h - ((v-min)/range)*h}`).join(" ");
const area = `M0,${h} L${pts.replaceAll(",", " ").split(" ").map((v, i) => i % 2 ? v : v).join(",")} L${w},${h} Z`;
return (
);
}
window.Spark = Spark;
// --- KPI card ---
function KPI({ label, value, unit, delta, spark, sparkColor, icon }) {
return (
{icon}{label}
{value}{unit ? {unit} : null}
{(delta != null) && (
= 0 ? "pos" : "neg")}>
{delta >= 0 ? "+" : ""}{delta}%
vs last 7d
)}
{spark &&
}
);
}
window.KPI = KPI;
// --- Modal ---
function Modal({ open, onClose, title, sub, children, foot, size }) {
useEffect(() => {
if (!open) return;
const h = (e) => e.key === "Escape" && onClose();
document.addEventListener("keydown", h);
return () => document.removeEventListener("keydown", h);
}, [open, onClose]);
if (!open) return null;
return (
e.stopPropagation()}>
{children}
{foot &&
{foot}
}
);
}
window.Modal = Modal;
// --- Toasts ---
const ToastCtx = createContext(null);
function ToastProvider({ children }) {
const [items, setItems] = useState([]);
const push = useCallback((msg, kind = "info") => {
const id = Math.random();
setItems(s => [...s, { id, msg, kind }]);
setTimeout(() => setItems(s => s.filter(x => x.id !== id)), 3000);
}, []);
return (
{children}
{items.map(t => (
{t.kind === "pos" ? : t.kind === "neg" ? : }
{t.msg}
))}
);
}
function useToast() { return useContext(ToastCtx); }
window.ToastProvider = ToastProvider;
window.useToast = useToast;
// --- Switch ---
function Switch({ on, onChange }) {
return onChange(!on)} role="switch" aria-checked={on}/>;
}
window.Switch = Switch;
// --- Provider chip ---
function ProvChip({ slug }) {
const p = window.HDR_DATA.providers[slug] || { name: slug, color: "#888" };
return
{p.name};
}
window.ProvChip = ProvChip;
// --- Tabs ---
function Tabs({ items, value, onChange }) {
return (
{items.map(it => (
))}
);
}
window.Tabs = Tabs;
// --- Bar chart (simple SVG) ---
function BarChart({ data, height = 200, fmt = v => v, color = "var(--accent)" }) {
const max = Math.max(...data.map(d => d.v), 1);
const w = 100 / data.length;
return (
{data.filter((_, i) => i % Math.ceil(data.length/6) === 0).map((d, i) => {d.label})}
);
}
window.BarChart = BarChart;
// --- Line chart with area ---
function LineChart({ series, height = 220, color = "var(--accent)", fmt = v => v }) {
if (!series || !series.length) return null;
const vals = series.map(s => s.v);
const max = Math.max(...vals), min = Math.min(...vals, 0);
const range = max - min || 1;
const W = 100, H = height;
const step = W / (series.length - 1);
const pts = series.map((s, i) => [i*step, H - 20 - ((s.v - min)/range)*(H - 40)]);
const path = pts.map((p,i) => (i ? "L" : "M") + p[0] + " " + p[1]).join(" ");
const area = path + ` L${W} ${H-20} L0 ${H-20} Z`;
return (
{series.filter((_, i) => i % Math.ceil(series.length/6) === 0).map((s, i) => {s.label})}
);
}
window.LineChart = LineChart;
// --- Donut chart ---
function Donut({ slices, size = 140, thickness = 18 }) {
const total = slices.reduce((a, s) => a + s.v, 0) || 1;
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
let off = 0;
return (
);
}
window.Donut = Donut;
// Copy to clipboard helper
window.copyText = (txt, toast) => {
navigator.clipboard?.writeText(txt).then(() => toast && toast("Copied to clipboard", "pos"));
};