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

50 lines
2.2 KiB
TypeScript

/*
* Main column plus a fixed support rail.
*
* The rail is what stops a page being empty and the main column is what stops
* it being thin: an account with one instance used to render a third of a row
* of summary and nothing else. The rail carries what is true regardless of how
* many instances exist, so the page has a floor.
*
* It collapses below lg in source order, which puts the main column first on a
* phone. Nothing is hidden at any width if content only fits on a desktop it
* does not belong in the rail.
*/
export function PageFrame({ children, aside }: { children: React.ReactNode; aside?: React.ReactNode }) {
if (!aside) return <div className="grid gap-5">{children}</div>;
return (
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_320px]">
<div className="grid min-w-0 gap-4">{children}</div>
<aside className="grid gap-3.5">{aside}</aside>
</div>
);
}
/** One card in the rail. Title is a label, not a heading you read for pleasure. */
export function RailCard({ title, count, children }: { title: string; count?: number | string; children: React.ReactNode }) {
return (
<section className="grid gap-2.5 rounded border border-rule bg-panel p-3.5">
<header className="flex items-baseline justify-between gap-2.5">
<h2 className="font-mono text-[0.66rem] font-normal uppercase tracking-[0.12em] text-ink-3">{title}</h2>
{count !== undefined && <b className="text-[0.95rem] font-extrabold tabular-nums">{count}</b>}
</header>
{children}
</section>
);
}
/** Key/value rows for the rail. Values are mono so numbers line up. */
export function RailFacts({ rows }: { rows: { label: string; value: React.ReactNode }[] }) {
return (
<dl className="grid gap-1.5">
{rows.map((r) => (
<div key={r.label} className="flex justify-between gap-2.5 text-[0.82rem]">
<dt className="text-ink-3">{r.label}</dt>
<dd className="m-0 truncate font-mono text-[0.78rem] tabular-nums text-ink">{r.value}</dd>
</div>
))}
</dl>
);
}