Files
2026-07-28 12:46:28 +01:00

91 lines
3.4 KiB
TypeScript

"use client";
import Link from "next/link";
import { useState } from "react";
/*
* One record-line entry. `copy` marks the value as worth lifting to the
* clipboard an instance UUID or a licence ID, the strings people paste into
* support tickets.
*/
export type RecordField = { key: string; value: string; copy?: boolean };
function CopyButton({ value }: { value: string }) {
const [done, setDone] = useState(false);
return (
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
setDone(true);
setTimeout(() => setDone(false), 1200);
} catch {
// Clipboard is refused without a secure context or a user
// gesture the browser trusts. The value is on screen and
// selectable either way, so this needs no error state.
}
}}
className="rounded-sm border border-rule px-1.5 py-px font-mono text-[0.62rem] uppercase tracking-[0.1em] text-ink-3 hover:border-accent hover:text-accent"
>
{done ? "Copied" : "Copy"}
</button>
);
}
/*
* The page frame every screen starts with, replacing nine hand-rolled header
* blocks that each picked their own gaps and their own place for actions.
*
* The record line is the one new idea: Vantage HQ is a registry, so every screen
* is a record and records have reference numbers. Giving the reference a fixed
* slot, in mono, above the fold, means "where is the ID" stops being a per-page
* question. It costs one hairline rule.
*/
export function PageHeader({
back,
title,
subtitle,
actions,
record,
status,
}: {
back?: { href: string; label: string };
title: string;
subtitle?: React.ReactNode;
actions?: React.ReactNode;
record?: RecordField[];
status?: React.ReactNode;
}) {
return (
<header className="grid gap-3">
{back && (
<Link href={back.href} className="justify-self-start font-mono text-[0.7rem] uppercase tracking-[0.1em] text-ink-3 hover:text-accent">
&larr; {back.label}
</Link>
)}
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<h1 className="text-[1.9rem]">{title}</h1>
{subtitle && <p className="mt-1 text-[0.92rem] text-ink-2">{subtitle}</p>}
</div>
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
</div>
{(record?.length || status) && (
<div className="flex flex-wrap items-center gap-x-5 gap-y-2.5 border-t border-rule pt-2.5">
{record?.map((f) => (
<span key={f.key} className="flex items-center gap-2">
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">{f.key}</span>
<span className="font-mono text-[0.78rem] tabular-nums text-ink-2">{f.value}</span>
{f.copy && <CopyButton value={f.value} />}
</span>
))}
{status && <span className="ml-auto">{status}</span>}
</div>
)}
</header>
);
}