feat(adminsite): one masthead, a shared page frame, collapsible instance records
Server Deploy / deploy (push) Successful in 53s
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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { AccountMenu } from "@/components/AccountMenu";
|
||||
import { EnvBadge } from "@/components/EnvBadge";
|
||||
|
||||
export type NavLink = { href: string; label: string };
|
||||
|
||||
/*
|
||||
* One masthead in three zones: who you are acting as, where you can go, and
|
||||
* which environment you are in.
|
||||
*
|
||||
* It replaces a brand bar and a separate nav strip. The nav's active state is
|
||||
* derived from the pathname rather than hardcoded — the previous customer nav
|
||||
* marked Overview as current on every page, including the ones that weren't it.
|
||||
*
|
||||
* Staff sit on --panel-2 with a chip where the account name goes. web/ is locked
|
||||
* to dark so this app defaults to light for the same reason CLAUDE.md gives:
|
||||
* telling two consoles apart before you click Reissue. Staff and customer need
|
||||
* that distinction from each other too, and one shade plus one chip buys it
|
||||
* without a second palette.
|
||||
*/
|
||||
export function AppBar({
|
||||
links,
|
||||
context,
|
||||
staff = false,
|
||||
}: {
|
||||
links: NavLink[];
|
||||
context?: React.ReactNode;
|
||||
staff?: boolean;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isCurrent = (href: string) =>
|
||||
// The section root matches only exactly; deeper routes match by prefix,
|
||||
// so /staff/accounts/:id still lights Accounts while /staff/accounts
|
||||
// does not light Operations.
|
||||
href === "/" || href === "/staff"
|
||||
? pathname === href
|
||||
: pathname === href || pathname.startsWith(`${href}/`);
|
||||
|
||||
return (
|
||||
<header className={`border-b border-rule ${staff ? "bg-panel-2" : "bg-panel"}`}>
|
||||
<div className="mx-auto grid max-w-rail grid-cols-[auto_1fr_auto] items-center gap-4 px-5 md:gap-7">
|
||||
<div className="col-start-1 row-start-1 flex min-w-0 items-center gap-3 py-2.5">
|
||||
<span className="flex items-baseline gap-2 text-[1.16rem] font-extrabold tracking-[-0.02em]">
|
||||
Vantage
|
||||
<span className="font-mono text-[0.72rem] font-normal uppercase tracking-[0.14em] text-ink-3">
|
||||
HQ
|
||||
</span>
|
||||
</span>
|
||||
{context && (
|
||||
<>
|
||||
<span aria-hidden className="hidden h-[22px] w-px bg-rule sm:block" />
|
||||
<span className="hidden min-w-0 sm:block">{context}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav
|
||||
aria-label={staff ? "Staff" : "Account"}
|
||||
className="col-span-3 col-start-1 row-start-2 flex items-stretch gap-1 overflow-x-auto border-t border-rule-soft md:col-span-1 md:col-start-2 md:row-start-1 md:border-t-0"
|
||||
>
|
||||
{links.map((l) => {
|
||||
const on = isCurrent(l.href);
|
||||
return (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
aria-current={on ? "page" : undefined}
|
||||
className={`relative inline-flex shrink-0 items-center px-3 py-2.5 font-mono text-[0.72rem] uppercase tracking-[0.08em] md:py-0 ${
|
||||
on
|
||||
? "font-bold text-accent after:absolute after:inset-x-3 after:bottom-0 after:h-0.5 after:bg-accent after:content-['']"
|
||||
: "text-ink-3 hover:text-ink-2"
|
||||
}`}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="col-start-3 row-start-1 flex items-center justify-end gap-2.5 py-2.5">
|
||||
<span className="hidden sm:block">
|
||||
<EnvBadge />
|
||||
</span>
|
||||
<AccountMenu staff={staff} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user