import { useEffect, type ButtonHTMLAttributes, type ReactNode } from "react"; import { Loader2, X } from "lucide-react"; export function cx(...c: (string | false | null | undefined)[]) { return c.filter(Boolean).join(" "); } type Variant = "primary" | "secondary" | "ghost" | "danger" | "warn"; const variants: Record = { primary: "bg-accent text-white hover:bg-violet-500", secondary: "bg-panel-2 border border-line hover:bg-[#222633] text-gray-100", ghost: "hover:bg-panel-2 text-gray-300", danger: "bg-red-600/90 text-white hover:bg-red-500", warn: "bg-amber-500/90 text-black hover:bg-amber-400", }; export function Button({ variant = "secondary", loading, className, children, size = "md", ...props }: ButtonHTMLAttributes & { variant?: Variant; loading?: boolean; size?: "sm" | "md" }) { return ( ); } export function Card({ children, className }: { children: ReactNode; className?: string }) { return
{children}
; } const tones = { gray: "bg-gray-500/15 text-gray-300 border-gray-500/25", green: "bg-emerald-500/15 text-emerald-300 border-emerald-500/25", blue: "bg-sky-500/15 text-sky-300 border-sky-500/25", amber: "bg-amber-500/15 text-amber-300 border-amber-500/25", red: "bg-red-500/15 text-red-300 border-red-500/25", violet: "bg-violet-500/15 text-violet-300 border-violet-500/25", }; export function Badge({ tone = "gray", children, className }: { tone?: keyof typeof tones; children: ReactNode; className?: string }) { return ( {children} ); } export const statusTone: Record = { linked: "green", identical: "blue", different: "amber", similar: "violet", unmatched: "gray", error: "red", }; export function Spinner({ className }: { className?: string }) { return ; } export function Modal({ open, onClose, title, children, wide, }: { open: boolean; onClose: () => void; title: ReactNode; children: ReactNode; wide?: boolean; }) { 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 Field({ label, hint, children }: { label: string; hint?: ReactNode; children: ReactNode }) { return ( ); } export const inputCls = "w-full h-9 rounded-md border border-line bg-bg px-3 text-sm outline-none focus:border-accent focus:ring-1 focus:ring-accent placeholder:text-gray-600"; export function Toggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label?: ReactNode }) { return ( ); } export function Alert({ tone, children, icon }: { tone: "amber" | "red" | "blue" | "green"; children: ReactNode; icon?: ReactNode }) { const c = { amber: "border-amber-500/30 bg-amber-500/10 text-amber-200", red: "border-red-500/30 bg-red-500/10 text-red-200", blue: "border-sky-500/30 bg-sky-500/10 text-sky-200", green: "border-emerald-500/30 bg-emerald-500/10 text-emerald-200", }[tone]; return
{icon}
{children}
; } export function Empty({ icon, title, children }: { icon: ReactNode; title: string; children?: ReactNode }) { return (
{icon}
{title}
{children &&
{children}
}
); } export function formatBytes(n: number) { if (!n) return "0 B"; const u = ["B", "KB", "MB", "GB", "TB"]; const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), u.length - 1); return `${(n / 1024 ** i).toFixed(i ? 1 : 0)} ${u[i]}`; } export function formatAgo(unix: number | null | undefined) { if (!unix) return "never"; const s = Math.floor(Date.now() / 1000 - unix); if (s < 60) return "just now"; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400) return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; } export function formatDuration(sec: number) { if (!sec) return "–"; const m = Math.floor(sec / 60); return `${m}:${String(Math.round(sec % 60)).padStart(2, "0")}`; }