import { cn } from "~/utils/cn";
import type { StatusState } from "~/lib/types";
const AVATAR_COLORS = [
"bg-orange-700",
"bg-sky-700",
"bg-emerald-700",
"bg-rose-700",
"bg-violet-700",
"bg-amber-700",
"bg-teal-700",
"bg-fuchsia-700",
];
const SECONDS = 1000;
const MINUTE = 60 * SECONDS;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;
const KB = 1024;
function hash(value: string): number {
let out = 0;
for (const char of value) out = (out * 31 + char.charCodeAt(0)) | 0;
return Math.abs(out);
}
export function Avatar({ name, size = "md" }: { name: string; size?: "sm" | "md" | "lg" }) {
const sizeClass = size === "sm" ? "size-5 text-[10px]" : size === "lg" ? "size-20 text-3xl" : "size-8 text-sm";
return (
{name.slice(0, 1)}
);
}
export function formatRelative(timestamp: number, now = Date.now()): string {
const delta = now - timestamp;
const units: Array<[number, string]> = [
[YEAR, "year"],
[MONTH, "month"],
[DAY, "day"],
[HOUR, "hour"],
[MINUTE, "minute"],
];
for (const [size, label] of units) {
if (delta >= size) {
const count = Math.floor(delta / size);
return `${count} ${label}${count === 1 ? "" : "s"} ago`;
}
}
return "just now";
}
export function RelativeTime({ timestamp }: { timestamp: number }) {
return (
);
}
export function formatDuration(start: number | null, end: number | null): string {
if (!start) return "";
const total = Math.max(0, Math.round(((end ?? Date.now()) - start) / SECONDS));
const minutes = Math.floor(total / 60);
return minutes > 0 ? `${minutes}m ${total % 60}s` : `${total}s`;
}
export function formatBytes(bytes: number): string {
if (bytes < KB) return `${bytes} B`;
if (bytes < KB * KB) return `${(bytes / KB).toFixed(1)} KB`;
if (bytes < KB * KB * KB) return `${(bytes / KB / KB).toFixed(1)} MB`;
return `${(bytes / KB / KB / KB).toFixed(1)} GB`;
}
const BADGE_TONES = {
neutral: "border-gray-700 text-gray-400",
accent: "border-accent-600/60 bg-accent-950 text-accent-300",
green: "border-emerald-800 bg-emerald-950/50 text-emerald-300",
red: "border-red-900 bg-red-950/50 text-red-300",
purple: "border-violet-800 bg-violet-950/50 text-violet-300",
amber: "border-amber-800 bg-amber-950/40 text-amber-300",
} as const;
export function Badge({
tone = "neutral",
children,
className,
}: {
tone?: keyof typeof BADGE_TONES;
children: React.ReactNode;
className?: string;
}) {
return (
{children}
);
}
export function LabelChip({ name, color }: { name: string; color: string }) {
return (
{name}
);
}
const STATUS_STYLES: Record = {
success: { className: "bg-emerald-500", label: "Passing" },
failure: { className: "bg-red-500", label: "Failing" },
error: { className: "bg-red-500", label: "Errored" },
pending: { className: "bg-amber-400 animate-pulse-dot", label: "Pending" },
in_progress: { className: "bg-amber-400 animate-pulse-dot", label: "Running" },
queued: { className: "bg-gray-500 animate-pulse-dot", label: "Queued" },
cancelled: { className: "bg-gray-500", label: "Cancelled" },
skipped: { className: "bg-gray-600", label: "Skipped" },
};
export function StatusDot({ state, className }: { state: StatusState | string | null; className?: string }) {
if (!state) return null;
const style = STATUS_STYLES[state] ?? STATUS_STYLES.queued;
return (
{style.label}
);
}
export function runState(status: string, conclusion: string | null): string {
return status === "completed" ? (conclusion ?? "skipped") : status;
}
export function Panel({
title,
actions,
children,
className,
}: {
title?: React.ReactNode;
actions?: React.ReactNode;
children: React.ReactNode;
className?: string;
}) {
return (
{(title || actions) && (
{title}
{actions && {actions}
}
)}
{children}
);
}
export function EmptyState({ title, children }: { title: string; children?: React.ReactNode }) {
return (
∅
{title}
{children &&
{children}
}
);
}
export function Markdown({ html, className }: { html: string; className?: string }) {
return (
);
}
export function PageContainer({ children, className }: { children: React.ReactNode; className?: string }) {
return {children}
;
}