SPB Git

Repository Public

ui.tsx 4.0 KB 69 lines tsx

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
Files main
Menu

Theme