"use client";
import clsx from "clsx";
import { AlertTriangle, CheckCircle2, CircleDashed, Info, XCircle, type LucideIcon } from "lucide-react";
import Link from "next/link";
import type { ReactNode } from "react";
import { isApiError } from "@/lib/api";
// ---------------------------------------------------------------- page header
export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: ReactNode; actions?: ReactNode }) {
return (
{title}
{subtitle &&
{subtitle}
}
{actions &&
{actions}
}
);
}
export function Card({ title, children, className, actions, id }: { title?: ReactNode; children: ReactNode; className?: string; actions?: ReactNode; id?: string }) {
return (
{(title || actions) && (
{title}
{actions &&
{actions}
}
)}
{children}
);
}
// ---------------------------------------------------------------- status badge
type Tone = "ok" | "warn" | "serious" | "crit" | "neutral" | "accent";
const TONE: Record = {
ok: "bg-ok-soft text-ok-ink",
warn: "bg-warn-soft text-warn-ink",
serious: "bg-serious-soft text-serious-ink",
crit: "bg-crit-soft text-crit-ink",
neutral: "bg-surface-2 text-ink-2",
accent: "bg-accent-soft text-accent-ink",
};
const TONE_ICON: Record = { ok: CheckCircle2, warn: AlertTriangle, serious: AlertTriangle, crit: XCircle, neutral: CircleDashed, accent: Info };
export function toneFor(status: string | null | undefined): Tone {
switch ((status ?? "").toLowerCase()) {
case "ok":
case "active":
case "succeeded":
case "ready":
case "running":
case "verified":
case "allowed":
case "success":
return status?.toLowerCase() === "running" ? "accent" : "ok";
case "warning":
case "planned":
case "queued":
case "paused":
case "pending_configuration":
case "pending":
case "stale":
case "expired":
case "cancelled":
return "warn";
case "degraded":
case "failed":
case "error":
case "dead":
case "denied":
case "unsupported":
case "critical":
return "crit";
case "serious":
case "high":
return "serious";
default:
return "neutral";
}
}
export function StatusBadge({ status, label, icon = true, className }: { status: string | null | undefined; label?: string; icon?: boolean; className?: string }) {
const tone = toneFor(status);
const Icon = TONE_ICON[tone];
const text = label ?? (status ?? "unknown").replace(/_/g, " ");
return (
{icon && }
{text}
);
}
export function SeverityBadge({ severity }: { severity: string | null | undefined }) {
const s = (severity ?? "").toLowerCase();
const tone: Tone = s === "error" || s === "critical" || s === "high" ? "crit" : s === "warning" || s === "medium" ? "warn" : s === "info" || s === "low" ? "accent" : "neutral";
const Icon = TONE_ICON[tone];
return (
{s || "—"}
);
}
// ---------------------------------------------------------------- KPI tile
export function KpiTile({ label, value, sub, tone, href, className }: { label: string; value: ReactNode; sub?: ReactNode; tone?: Tone; href?: string; className?: string }) {
const body = (
<>
{label}
{value}
{sub && {sub}
}
>
);
const cls = clsx("card min-w-0", href && "transition hover:border-accent/60", className);
return href ? (
{body}
) : (
{body}
);
}
// ---------------------------------------------------------------- misc
export function Banner({ tone = "accent", children, className }: { tone?: Tone; children: ReactNode; className?: string }) {
const Icon = TONE_ICON[tone];
const border = { ok: "border-ok/50", warn: "border-warn/60", serious: "border-serious/60", crit: "border-crit/60", neutral: "border-border", accent: "border-accent/40" }[tone];
return (
);
}
export function ErrorState({ error, retry }: { error: unknown; retry?: () => void }) {
const msg = isApiError(error) ? `${error.message} (${error.code}, HTTP ${error.status})` : error instanceof Error ? error.message : "Unknown error";
const rid = isApiError(error) ? error.request_id : null;
const forbidden = isApiError(error) && error.status === 403;
return (
{forbidden ? "Not allowed" : "Could not load"}
{msg}
{rid &&
request_id {rid}
}
{retry && (
)}
);
}
export function Empty({ children = "Nothing here yet." }: { children?: ReactNode }) {
return {children}
;
}
export function Skeleton({ rows = 4, className }: { rows?: number; className?: string }) {
return (
{Array.from({ length: rows }).map((_, i) => (
))}
);
}
export function Field({ label, children, hint, className }: { label: string; children: ReactNode; hint?: ReactNode; className?: string }) {
return (
);
}
export function KV({ items, className }: { items: [ReactNode, ReactNode][]; className?: string }) {
return (
{items.map(([k, v], i) => (
- {k}
- {v ?? "—"}
))}
);
}
export function Mono({ children, className }: { children: ReactNode; className?: string }) {
return {children};
}
export function Tabs({ value, onChange, items }: { value: T; onChange: (v: T) => void; items: { value: T; label: string }[] }) {
return (
{items.map((it) => (
))}
);
}