import { type ReactNode, useState } from "react";
import { Link } from "react-router";
import type { ApiError, Job } from "../api";
import { useJob } from "../hooks";
export function Loading({ what = "Loading" }: { what?: string }) { return
{what}…
; }
export function ErrorBox({ error }: { error: ApiError | Error | string | null }) {
if (!error) return null;
const e = error as ApiError;
return {typeof error === "string" ? error : `${e.code ? e.code + ": " : ""}${e.message}`}{e.correlationId ? (ref {e.correlationId}) : null}
;
}
export function Empty({ children }: { children: ReactNode }) { return {children}
; }
const TONE: Record = {
succeeded: "good", done: "good", pass: "good", verified: "good", allow: "good", online: "good", ok: "good", resolved: "good", approved: "good", validated: "good", registered: "good", monitored: "good", tested: "good", independently_reproduced: "good", production: "good",
failed: "critical", dead: "critical", fail: "critical", blocked: "critical", deny: "critical", critical: "critical", revoked: "critical", rejected: "critical", block: "critical",
high: "serious", serious: "serious", quarantined: "serious", stale: "serious", deprecated: "serious", quarantine: "serious", unverified: "serious", research: "accent",
medium: "warn", warn: "warn", warning: "warn", running: "warn", in_progress: "warn", intent: "warn", unobserved: "warn", unknown: "warn", discovery: "warn", open: "warn",
low: "", info: "", queued: "", proposed: "", observed: "", documented: "", implemented: "accent", inspected: "", modeled: "", discovered: "", development: "", accepted: "",
};
export function Badge({ v, label }: { v: string | null | undefined; label?: string }) {
const s = v ?? "—";
return {label ?? s} ;
}
export function Bool({ v, yes = "yes", no = "no" }: { v: boolean | null | undefined; yes?: string; no?: string }) { return {v ? yes : no} ; }
export function Td({ l, children, className, wide }: { l: string; children: ReactNode; className?: string; wide?: boolean }) {
return {children} ;
}
export function KV({ items }: { items: [string, ReactNode][] }) {
return {items.map(([k, v]) => (
{k} {v ?? "—"} ))} ;
}
export function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
return {tabs.map((t) => onChange(t)}>{t} )}
;
}
export function JobStatus({ jobId, label = "job", onDone }: { jobId: string | null; label?: string; onDone?: (j: Job) => void }) {
const job = useJob(jobId, onDone);
if (!jobId) return null;
if (!job) return Submitted {label} {jobId.slice(0, 8)}… waiting for worker
;
const terminal = ["done", "dead", "cancelled", "failed"].includes(job.state);
return (
{label} {job.id.slice(0, 8)} {!terminal && (attempt {job.attempts}, polling…) } details
{job.error &&
{job.error} }
{job.result &&
}
);
}
export function JsonTree({ data, open = false, depth = 0 }: { data: unknown; open?: boolean; depth?: number }) {
if (data === null || data === undefined) return null ;
if (typeof data === "string") return "{data.length > 400 ? data.slice(0, 400) + "…" : data}" ;
if (typeof data === "number" || typeof data === "boolean") return {String(data)} ;
const isArr = Array.isArray(data);
const entries = isArr ? (data as unknown[]).map((v, i) => [String(i), v] as [string, unknown]) : Object.entries(data as Record);
if (entries.length === 0) return {isArr ? "[]" : "{}"} ;
const inner = (
{entries.slice(0, 300).map(([k, v]) => ({isArr ? `[${k}]` : k} : ))}{entries.length > 300 && … {entries.length - 300} more }
);
const wrapper = {isArr ? `array[${entries.length}]` : `object{${entries.length}}`} {inner} ;
return depth === 0 ? {wrapper}
: wrapper;
}
export function CopyBox({ text }: { text: string }) {
const [ok, setOk] = useState(false);
return (
{ navigator.clipboard?.writeText(text).then(() => { setOk(true); setTimeout(() => setOk(false), 1500); }); }}>{ok ? "copied" : "copy"}
{text}
);
}
export function Confirm({ label, onConfirm, className = "btn sm danger", disabled }: { label: string; onConfirm: () => void; className?: string; disabled?: boolean }) {
const [arm, setArm] = useState(false);
if (!arm) return setArm(true)}>{label} ;
return { setArm(false); onConfirm(); }}>confirm {label} setArm(false)}>cancel ;
}
export function Pager({ offset, limit, total, onChange }: { offset: number; limit: number; total: number; onChange: (o: number) => void }) {
return {total === 0 ? "0" : `${offset + 1}–${Math.min(offset + limit, total)}`} of {total} onChange(Math.max(0, offset - limit))}>‹ prev = total} onClick={() => onChange(offset + limit)}>next ›
;
}
export function Spinner() { return working… ; }