Server Deploy / deploy (push) Successful in 53s
The console had components but no shell: a brand bar and a nav strip stacked into 100px carrying eight words, no sign-out, no account identity, and an Overview link hardcoded to text-accent so it read as the current page on every screen. Nine pages each hand-rolled their own header. AppBar replaces both bars and derives its active state from usePathname. Settings moves into AccountMenu — it is your password, not a destination — taking appearance with it, which finally sets the data-theme attribute the token blocks have supported in both directions since they were written. That leaves three customer destinations: Overview, People, Billing. PageFrame adds a support rail so a page has a floor, and InstanceRecord replaces InstanceCard with one component that opens and closes: an account with a single instance used to render a third of a row of summary with its substance a click away. It defaults open when the instance is the only one or needs attention. No plan card in the rail: tier, limits and expiry belong to a licence and a licence belongs to one instance, so an account holding a Free cloud instance and a Professional self-hosted one has no single plan. The rail carries only what is account-wide. Tokens and globals.css are untouched — they stay verbatim shared with site/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 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>
|
|
);
|
|
}
|