"use client"; import { useEffect, useState } from "react"; import type { Compat } from "@/lib/types"; export function Pill({ tone = "neutral", children, title }: { tone?: "neutral" | "good" | "warn" | "bad" | "accent" | "violet"; children: React.ReactNode; title?: string }) { const map: Record = { neutral: "bg-surface-3 text-ink-2 border-border-strong", good: "bg-[#12301f] text-[#5fd39a] border-[#1f4a31]", warn: "bg-[#33260a] text-[#f0b23a] border-[#5a4210]", bad: "bg-[#3a1717] text-[#ff8a8a] border-[#5a2b2b]", accent: "bg-[#12294a] text-[#7db3ff] border-[#1e3f6e]", violet: "bg-[#241f4a] text-[#b9b0ff] border-[#3a3380]", }; return ( {children} ); } export function StatusPill({ status }: { status: string }) { const s = status || "unloaded"; if (s === "ready") return Loaded; if (s === "unloaded") return Unloaded; if (s === "error") return Error; if (s === "unloading" || s === "unloading_previous") return Unloading…; return {s === "queued" ? "Queued" : s === "warming" ? "Warming up" : "Loading…"}; } export function CompatPill({ status, reason }: { status: Compat | string | null; reason?: string | null }) { const t = reason || undefined; switch (status) { case "compatible": return Compatible; case "compatible_with_restrictions": return Restricted; case "experimental": return Experimental; case "not_recommended": return Not recommended; case "incompatible": return Incompatible; default: return Unknown; } } export function SizePill({ cls }: { cls: string | null }) { const tone = cls === "TOO_LARGE" ? "bad" : cls === "XL" ? "warn" : cls === "LARGE" ? "accent" : "neutral"; return {cls || "—"}; } export function Dot({ className = "" }: { className?: string }) { return ; } export function StatTile({ label, value, sub, accent, children }: { label: string; value: React.ReactNode; sub?: React.ReactNode; accent?: string; children?: React.ReactNode }) { return (
{label}
{value}
{sub &&
{sub}
} {children}
); } export function Meter({ value, max, tone = "accent", height = 6 }: { value: number; max: number; tone?: "accent" | "good" | "warn" | "bad"; height?: number }) { const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0; const color = { accent: "var(--color-accent)", good: "var(--color-good)", warn: "var(--color-warn)", bad: "var(--color-bad)" }[tone]; return (
); } export function Progress({ value }: { value: number }) { return ; } /** Tiny SVG sparkline (single series, no axes) with a hover tooltip. */ export function Sparkline({ data, color = "var(--series-1)", height = 44, max, unit = "" }: { data: number[]; color?: string; height?: number; max?: number; unit?: string }) { const [hover, setHover] = useState(null); const w = 240; if (!data.length) return
no data yet
; const mx = max ?? Math.max(...data, 1); const mn = 0; const pts = data.map((v, i) => { const x = (i / Math.max(1, data.length - 1)) * w; const y = height - 3 - ((v - mn) / (mx - mn || 1)) * (height - 6); return [x, y]; }); const path = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" "); const area = `${path} L${w},${height} L0,${height} Z`; return (
{ const r = (e.target as SVGElement).closest("svg")!.getBoundingClientRect(); const i = Math.round(((e.clientX - r.left) / r.width) * (data.length - 1)); setHover(Math.max(0, Math.min(data.length - 1, i))); }} onMouseLeave={() => setHover(null)}> {hover != null && ( <> )} {hover != null && (
{data[hover].toFixed(unit === "%" ? 0 : 1)}{unit}
)}
); } export function Spinner({ size = 14 }: { size?: number }) { return ( ); } export function PageHeader({ title, sub, actions }: { title: string; sub?: React.ReactNode; actions?: React.ReactNode }) { return (

{title}

{sub &&
{sub}
}
{actions &&
{actions}
}
); } export function Empty({ title, sub, action }: { title: string; sub?: string; action?: React.ReactNode }) { return (
{title}
{sub &&
{sub}
} {action &&
{action}
}
); } export function Modal({ open, onClose, title, children, width = 520 }: { open: boolean; onClose: () => void; title: string; children: React.ReactNode; width?: number }) { useEffect(() => { if (!open) return; const h = (e: KeyboardEvent) => e.key === "Escape" && onClose(); window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()}>
{title}
{children}
); } export function Toggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label?: string }) { return ( ); } export function Tabs({ tabs, value, onChange }: { tabs: { id: T; label: string; count?: number }[]; value: T; onChange: (v: T) => void }) { return (
{tabs.map((t) => ( ))}
); } export function useToast() { const [toasts, setToasts] = useState<{ id: number; text: string; tone: "good" | "bad" | "neutral" }[]>([]); const push = (text: string, tone: "good" | "bad" | "neutral" = "neutral") => { const id = Date.now() + Math.random(); setToasts((t) => [...t, { id, text, tone }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 4500); }; const view = (
{toasts.map((t) => (
{t.text}
))}
); return { push, view }; } export function KV({ k, v, mono }: { k: string; v: React.ReactNode; mono?: boolean }) { return (
{k} {v ?? "—"}
); } export function Code({ children }: { children: string }) { const [copied, setCopied] = useState(false); return (
{children}
); }