"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
export function PageHeader({
title,
subtitle,
action,
backHref,
backLabel,
}: {
title: string;
subtitle?: string;
action?: React.ReactNode;
backHref?: string;
backLabel?: string;
}) {
return (
{backHref && (
← {backLabel || "Back"}
)}
{title}
{subtitle &&
{subtitle}
}
{action &&
{action}
}
);
}
export function Spinner({ className = "" }: { className?: string }) {
return (
);
}
export function SkeletonRows({ rows = 3 }: { rows?: number }) {
return (
{Array.from({ length: rows }).map((_, i) => (
))}
);
}
export function EmptyState({
title,
description,
action,
}: {
title: string;
description?: string;
action?: React.ReactNode;
}) {
return (
{title}
{description &&
{description}
}
{action &&
{action}
}
);
}
export function LoadError({
message = "Couldn’t load data.",
onRetry,
}: {
message?: string;
onRetry?: () => void;
}) {
return (
{message}
{onRetry && (
)}
);
}
export function FormError({ message }: { message?: string | null }) {
if (!message) return null;
return {message}
;
}
const STATUS_STYLES: Record = {
queued: "bg-slate-500/20 text-slate-300",
running: "bg-accent/20 text-accent",
waiting_approval: "bg-warn/20 text-warn",
succeeded: "bg-good/20 text-good",
failed: "bg-bad/20 text-bad",
cancelled: "bg-slate-500/20 text-slate-400",
loaded: "bg-good/20 text-good",
error: "bg-bad/20 text-bad",
starting: "bg-warn/20 text-warn",
cloning: "bg-accent/20 text-accent",
stopped: "bg-slate-500/20 text-slate-400",
};
export function StatusBadge({ status }: { status: string }) {
return (
{status.replace(/_/g, " ")}
);
}
export function Modal({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const ref = useRef(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
// Focus the dialog only when it opens (not on every parent re-render).
useEffect(() => {
if (!open) return;
ref.current?.focus();
}, [open]);
// Escape-to-close + body scroll lock while open.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onCloseRef.current();
};
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open]);
if (!open) return null;
return (
e.stopPropagation()}
>
{title}
{children}
);
}
export function ConfirmDialog({
open,
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
destructive = false,
busy = false,
onConfirm,
onCancel,
}: {
open: boolean;
title: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
busy?: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
{message && {message}
}
);
}