Repository Public
1import Link from "next/link";23export const usd = (n: number | null | undefined, digits = 2) => (n === null || n === undefined ? "unknown" : `$${Number(n).toFixed(digits)}`);4export const when = (d: Date | string | null | undefined) => (d ? new Intl.DateTimeFormat("en-CA", { dateStyle: "medium", timeStyle: "short", timeZone: "America/Toronto" }).format(new Date(d)) : "—");5export const ago = (d: Date | string | null | undefined) => {6 if (!d) return "—";7 const s = Math.round((Date.now() - new Date(d).getTime()) / 1000);8 return s < 60 ? `${s} s ago` : s < 3600 ? `${Math.round(s / 60)} min ago` : s < 86400 ? `${Math.round(s / 3600)} h ago` : `${Math.round(s / 86400)} d ago`;9};10export const bytes = (n: number) => (n > 1e9 ? `${(n / 1e9).toFixed(2)} GB` : n > 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.round(n / 1e3)} kB`);1112const TONE: Record<string, string> = {13 done: "ok", published: "ok", qa_passed: "ok", approved: "ok", filmed: "ok", resolved: "ok", online: "ok",14 running: "run", queued: "run", submitted: "run", scheduled: "run",15 failed: "bad", qa_failed: "bad", blocked: "warn", needs_approval: "warn", hold: "warn", unpublished: "warn", open: "warn",16};17export function Status({ value, label }: { value: string; label?: string }) {18 const tone = TONE[value];19 return <span className={`status ${tone ? `status-${tone}` : ""}`}>{label ?? value.replace(/_/g, " ")}</span>;20}2122export function Notice({ ok, err }: { ok?: string; err?: string }) {23 if (!ok && !err) return null;24 return (25 <div role={err ? "alert" : "status"} className={`mb-5 rounded-2xl px-4 py-3 text-sm ${err ? "notice-err" : "notice-ok"}`}>26 <span className="break-words">{err ?? ok}</span>27 </div>28 );29}3031export function PageHead({ title, sub, children }: { title: string; sub?: string; children?: React.ReactNode }) {32 return (33 <div className="mb-6 flex flex-wrap items-end justify-between gap-4">34 <div><h1 className="display text-3xl md:text-4xl">{title}</h1>{sub && <p className="mt-1 max-w-2xl text-sm text-[var(--color-faint)]">{sub}</p>}</div>35 {children && <div className="flex flex-wrap gap-2">{children}</div>}36 </div>37 );38}39export function Panel({ title, children, action, id }: { title?: string; children: React.ReactNode; action?: React.ReactNode; id?: string }) {40 return (41 <section id={id} className="card p-5">42 {(title || action) && <div className="mb-4 flex items-center justify-between gap-3">{title && <h2 className="label">{title}</h2>}{action}</div>}43 {children}44 </section>45 );46}47export function Stat({ label, value, sub, tone }: { label: string; value: React.ReactNode; sub?: string; tone?: "bad" | "warn" | "ok" }) {48 return (49 <div className="card p-4">50 <p className="label">{label}</p>51 <p className={`display mt-2 text-4xl tabular-nums ${tone === "bad" ? "text-[var(--color-bad)]" : tone === "warn" ? "text-[var(--color-warn)]" : ""}`}>{value}</p>52 {sub && <p className="mt-1 text-xs text-[var(--color-faint)]">{sub}</p>}53 </div>54 );55}56/** Spend against a ceiling. */57export function Meter({ label, spent, cap }: { label: string; spent: number; cap: number }) {58 const pct = cap > 0 ? Math.min(100, (spent / cap) * 100) : 0;59 return (60 <div>61 <div className="flex justify-between text-xs"><span className="text-[var(--color-muted)]">{label}</span><span className="tabular-nums">{usd(spent)} <span className="text-[var(--color-faint)]">/ {usd(cap)}</span></span></div>62 <div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-[var(--color-line)]" role="progressbar" aria-label={label} aria-valuenow={Math.round(pct)} aria-valuemin={0} aria-valuemax={100}>63 <div className="h-full rounded-full" style={{ width: `${pct}%`, background: pct > 90 ? "var(--color-bad)" : pct > 70 ? "var(--color-warn)" : "var(--color-lamp)" }} />64 </div>65 </div>66 );67}68export const A = ({ href, children }: { href: string; children: React.ReactNode }) => <Link href={href} className="text-[var(--color-text)] underline decoration-[var(--color-line)] underline-offset-4 hover:decoration-[var(--color-lamp)]">{children}</Link>;69