From 79afcc2e1632f523d0c4f9409d1d8d1b6e5d95cb Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Sat, 25 Jul 2026 20:31:55 +0100 Subject: [PATCH] docs(plan): match the admin site to site/'s design tokens Replaces the petrol-and-serif direction with site/app/globals.css's token set copied verbatim: brand navy accent, the same neutrals, the same clamp type scale, 1200px rail, 4px radii, and site/'s heading treatment of the sans at weight 800 rather than a serif display face. The semantic three are aliased rather than renamed -- globals.css carries site/'s --up/--pend/--down, and Tailwind exposes them as valid/warn/expired so each app names them for what it actually shows. Same colours either way. Tailwind now holds var() references only, so no component or config can carry a hex value and drift. Buttons match site/'s .btn--solid and .btn--line, including the neutral border on the secondary variant. Records the caveat this creates: site/'s dark accent (#5b9be8) sits nearer web/'s indigo than the light navy does, so the "which app am I in" cue rests on the light ground and dark must not become the default. Drops the guilloche ornament -- site/ carries nothing like it. The ledger stays, because it is information design rather than decoration. Co-Authored-By: Claude Opus 5 --- .../plans/2026-07-25-admin-site.md | 309 ++++++++++++------ 1 file changed, 213 insertions(+), 96 deletions(-) diff --git a/docs/superpowers/plans/2026-07-25-admin-site.md b/docs/superpowers/plans/2026-07-25-admin-site.md index d2401ed..8c1f33b 100644 --- a/docs/superpowers/plans/2026-07-25-admin-site.md +++ b/docs/superpowers/plans/2026-07-25-admin-site.md @@ -10,8 +10,11 @@ ## Global Constraints -- **Design tokens are fixed by the approved direction.** Light ground primary with full dark support, petrol accent `#0d5f6e` / `#5cc6d8`, semantic `--valid #2f8a60` / `--warn #a4761b` / `--expired #c6462f` carried over from `site/`. Semantic hues are **never** reused as an accent, and the accent is never green, amber, red or indigo — indigo belongs to `web/`. -- **Type roles:** serif display (`"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif`) for page titles, counts and the wordmark only; system sans for UI; mono with `tabular-nums` for every UUID, timestamp, blob and limit. +- **The token set is `site/`'s, copied verbatim.** `adminsite/app/globals.css` carries the same custom properties, with the same names and the same hex values, as `site/app/globals.css` — brand navy accent `#0b2a58` light / `#5b9be8` dark, `--up #2f8a60` / `--down #c6462f` / `--pend #b0801f`, the same `--shadow`, the same `--s--1`…`--s-4` clamp scale, `--rail: 1200px`. **When `site/`'s tokens change, change these in the same commit** — they are one visual system in two apps, and there is nothing that enforces the match automatically. +- **Tailwind maps those variables, it does not redefine them.** Colours in `tailwind.config.ts` are `var(--…)` references only. Never write a hex value in a component or in the Tailwind config. +- **Licence state uses `site/`'s semantic tokens.** Tailwind exposes them as `valid`/`warn`/`expired`, aliased onto `--up`/`--pend`/`--down` so this app names them for what it means while staying the same three colours the marketing site uses for up, pending and down. Semantic hues are **never** reused as an accent. +- **Type roles:** `--sans` for everything, `--mono` with `tabular-nums` for every UUID, timestamp, blob and limit. **There is no display face** — matching `site/` means headings are the sans at `font-weight: 800`, `letter-spacing: -0.03em`, `line-height: 1.03`. +- **Distinctness from `web/` rests on ground and hue together.** `web/` is locked to dark with an indigo `#6366f1` accent; this app defaults to light with navy. Note the caveat: in dark mode `site/`'s accent lifts to `#5b9be8`, which is nearer web/'s indigo, so the "which app am I in" cue leans on the light ground. Do not make dark the default. - **State never reads by colour alone.** Every licence state renders as a stripe, a shaped-and-labelled pill, and copy. - **Served at `vantage-hq.hostxtra.co.uk`, published on `3004`.** Spec 4 said 3002, but `deploy/docker-compose.site.yml` now maps `site` to `3002:3000`. Container port stays `3000`. Note the host is deliberately *not* under `*.vantage.hostxtra.co.uk`: that namespace is per-tenant instance subdomains, and the control plane's `APP_ROOT_LABEL` guard resolves an org from the label before `vantage`. A host like `hq.vantage.hostxtra.co.uk` would look like a tenant slug called `hq`. - **`ADMIN_ORIGIN` must contain `https://vantage-hq.hostxtra.co.uk`** exactly — scheme included, no trailing slash. Admin echoes only origins on that list, so a mismatch blocks every browser request while curl from the server keeps working, which is what makes it confusing to diagnose. @@ -75,7 +78,7 @@ Paddle portal deep-links are spec 5's. Until then the billing screen renders the | `adminsite/lib/session.ts` | `useSession`, `useRequireKind` | | `adminsite/lib/query-client.ts` | TanStack config, mirrors `web/` | | `adminsite/lib/format.ts` | dates, days-remaining, licence state derivation | -| `adminsite/components/*` | `EnvBadge`, `StatePill`, `InstanceCard`, `Ledger`, `NotConnected`, `Guilloche`, `Field`, `Button`, `ConfirmPlanChange` | +| `adminsite/components/*` | `EnvBadge`, `StatePill`, `InstanceCard`, `Ledger`, `NotConnected`, `Field`, `Button`, `Queue`, `ConfirmPlanChange` | **Modified:** `admin/internal/api/routes.go`, `admin/internal/api/customer.go`, `admin/internal/api/staff.go`, `admin/internal/auth/customer.go`, `admin/internal/models/models.go`, `deploy/docker-compose.site.yml`, `.gitea/workflows/server-deploy.yml`, `CLAUDE.md`. @@ -469,7 +472,7 @@ git commit -m "feat(admin): staff instance detail, subscriptions and richer sear - Create: `adminsite/package.json`, `adminsite/tsconfig.json`, `adminsite/next.config.ts`, `adminsite/postcss.config.js`, `adminsite/tailwind.config.ts`, `adminsite/app/globals.css`, `adminsite/app/layout.tsx`, `adminsite/.gitignore` **Interfaces:** -- Produces: Tailwind classes `bg-ground bg-panel bg-panel-2 text-ink text-ink-2 text-ink-3 border-rule border-rule-soft text-accent bg-accent text-valid text-warn text-expired`, fonts `font-display font-sans font-mono` +- Produces: Tailwind classes `bg-ground bg-panel bg-panel-2 text-ink text-ink-2 text-ink-3 border-rule border-rule-soft text-accent bg-accent text-valid text-warn text-expired`, fonts `font-sans font-mono` (no display face — headings are the sans at weight 800) - [ ] **Step 1: package.json** @@ -571,11 +574,21 @@ module.exports = { }; ``` -`tailwind.config.ts` — colours point at CSS variables so one token set serves both themes: +`tailwind.config.ts` — every colour is a `var()` reference, so `site/`'s token file is the single source of truth and one set serves both themes: ```ts import type { Config } from "tailwindcss"; +/* + * Tokens are shared with site/ — same names, same values, copied verbatim into + * app/globals.css. Nothing here may hold a hex value: if a colour needs to + * change it changes in globals.css, in both apps, in one commit. + * + * The semantic three are aliased rather than renamed. site/ calls them up, + * down and pend because it shows monitor state; this app calls them valid, + * expired and warn because it shows licence state. Same colours, honest names + * on both sides. + */ const config: Config = { content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], theme: { @@ -592,17 +605,18 @@ const config: Config = { accent: "var(--accent)", "accent-ink": "var(--accent-ink)", "accent-wash": "var(--accent-wash)", - valid: "var(--valid)", - warn: "var(--warn)", - expired: "var(--expired)", - archival: "var(--archival)", + valid: "var(--up)", + warn: "var(--pend)", + expired: "var(--down)", + archival: "var(--ink-3)", }, fontFamily: { - display: ["Iowan Old Style", "Palatino Linotype", "Palatino", "Book Antiqua", "Georgia", "serif"], sans: ["ui-sans-serif", "system-ui", "-apple-system", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"], mono: ["ui-monospace", "Cascadia Mono", "SF Mono", "JetBrains Mono", "Menlo", "Consolas", "monospace"], }, - borderRadius: { DEFAULT: "3px" }, + // site/ uses 4px on panels and buttons, 2px on focus rings. + borderRadius: { DEFAULT: "4px" }, + maxWidth: { rail: "1200px" }, }, }, plugins: [], @@ -613,74 +627,166 @@ export default config; - [ ] **Step 5: app/globals.css** +Copy the token block out of `site/app/globals.css` unchanged — same names, same +values, all three theme selectors. The only addition is `--accent-wash`, which +`site/` has no need for. + ```css @tailwind base; @tailwind components; @tailwind utilities; -/* - * Admin console tokens. Petrol accent, deliberately clear of indigo (web/) and - * of the three semantic hues this app says constantly. Light is home because - * web/ is locked to dark, and telling the two apps apart at a glance is what - * stops a Reissue landing in the wrong tab. - */ +/* ========================================================================== + Vantage admin console design tokens. + + These are site/app/globals.css's tokens, copied verbatim: the marketing site + and this console are one visual system. Change them in both apps in the same + commit — nothing enforces the match automatically. + + Light is the default because web/ is locked to dark, and telling the two + apart at a glance is what stops a Reissue landing in the wrong tab. In dark + mode the accent lifts to #5b9be8, which is nearer web/'s indigo, so the + distinction leans on the ground rather than the hue. + ========================================================================== */ + :root { color-scheme: light dark; - --ground: #eaeff0; + --ground: #eaedf3; --panel: #ffffff; - --panel-2: #f2f6f7; - --ink: #0f2429; - --ink-2: #43606a; - --ink-3: #6d868e; - --rule: #ccd9dc; - --rule-soft: #dfe8ea; - --accent: #0d5f6e; + --panel-2: #f4f6fa; + --ink: #0a1b33; + --ink-2: #41556f; + --ink-3: #6c7f96; + --rule: #cdd6e2; + --rule-soft: #e0e6ef; + --accent: #0b2a58; --accent-ink: #ffffff; - --accent-wash: rgba(13, 95, 110, 0.07); + --up: #2f8a60; + --down: #c6462f; + --pend: #b0801f; + --shadow: 0 1px 0 rgba(10, 27, 51, 0.05), 0 18px 40px -26px rgba(10, 27, 51, 0.45); + --logo: #0b2a58; - /* Semantic, shared with site/ so all three apps agree. Never an accent. */ - --valid: #2f8a60; - --warn: #a4761b; - --expired: #c6462f; - --archival: #7d4a45; + /* Not in site/: the hatched sandbox badge and hover washes need a tinted + fill, and deriving it per-use would drift. */ + --accent-wash: rgba(11, 42, 88, 0.07); + + --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + + --s--1: clamp(0.76rem, 0.74rem + 0.1vw, 0.81rem); + --s-0: clamp(1rem, 0.97rem + 0.14vw, 1.05rem); + --s-1: clamp(1.16rem, 1.09rem + 0.32vw, 1.36rem); + --s-2: clamp(1.5rem, 1.34rem + 0.74vw, 2rem); + --s-3: clamp(2rem, 1.66rem + 1.6vw, 3.1rem); + --s-4: clamp(2.6rem, 1.9rem + 3.3vw, 4.9rem); + + --rail: 1200px; } +/* Dark tokens are defined once and applied through three selectors: the OS + preference, and both explicit values of data-theme so an in-page toggle wins + in either direction. Same pattern as site/. */ @media (prefers-color-scheme: dark) { :root { - --ground: #081a1f; - --panel: #0e262d; - --panel-2: #123039; - --ink: #e2eff1; - --ink-2: #9fbcc3; - --ink-3: #74949c; - --rule: #1e454f; - --rule-soft: #17363f; - --accent: #5cc6d8; - --accent-ink: #04171c; - --accent-wash: rgba(92, 198, 216, 0.1); - --valid: #4fb484; - --warn: #d6a63f; - --expired: #e2705a; - --archival: #c08a84; + --ground: #071628; + --panel: #0d2138; + --panel-2: #102842; + --ink: #e4ecf6; + --ink-2: #9fb3ca; + --ink-3: #71879f; + --rule: #1e3855; + --rule-soft: #172c44; + --accent: #5b9be8; + --accent-ink: #04101f; + --up: #4fb484; + --down: #e2705a; + --pend: #d6a63f; + --shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 20px 44px -26px rgba(0, 0, 0, 0.85); + --logo: #7fb2f0; + --accent-wash: rgba(91, 155, 232, 0.1); } } -* { +:root[data-theme="dark"] { + --ground: #071628; + --panel: #0d2138; + --panel-2: #102842; + --ink: #e4ecf6; + --ink-2: #9fb3ca; + --ink-3: #71879f; + --rule: #1e3855; + --rule-soft: #172c44; + --accent: #5b9be8; + --accent-ink: #04101f; + --up: #4fb484; + --down: #e2705a; + --pend: #d6a63f; + --shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 20px 44px -26px rgba(0, 0, 0, 0.85); + --logo: #7fb2f0; + --accent-wash: rgba(91, 155, 232, 0.1); +} + +:root[data-theme="light"] { + --ground: #eaedf3; + --panel: #ffffff; + --panel-2: #f4f6fa; + --ink: #0a1b33; + --ink-2: #41556f; + --ink-3: #6c7f96; + --rule: #cdd6e2; + --rule-soft: #e0e6ef; + --accent: #0b2a58; + --accent-ink: #ffffff; + --up: #2f8a60; + --down: #c6462f; + --pend: #b0801f; + --shadow: 0 1px 0 rgba(10, 27, 51, 0.05), 0 18px 40px -26px rgba(10, 27, 51, 0.45); + --logo: #0b2a58; + --accent-wash: rgba(11, 42, 88, 0.07); +} + +*, +*::before, +*::after { box-sizing: border-box; - margin: 0; - padding: 0; } body { - background-color: var(--ground); + margin: 0; + background: var(--ground); color: var(--ink); + font-family: var(--sans); + font-size: var(--s-0); + line-height: 1.6; -webkit-font-smoothing: antialiased; } +/* site/'s heading treatment, which is what replaces a display face. */ +h1, +h2, +h3 { + margin: 0; + font-weight: 800; + line-height: 1.03; + letter-spacing: -0.03em; + text-wrap: balance; +} + +p { + margin: 0; +} + +code { + font-family: var(--mono); + font-size: 0.92em; +} + :focus-visible { outline: 2px solid var(--accent); - outline-offset: 1px; + outline-offset: 3px; + border-radius: 2px; } @media (prefers-reduced-motion: reduce) { @@ -693,6 +799,11 @@ body { } ``` +Because `h1`–`h3` carry the weight and tracking from this file, the Tailwind +classes on headings in later tasks (`text-3xl font-extrabold tracking-[-0.03em]`) +are belt-and-braces for elements that are not `h1`–`h3`. Leave them; they cost +nothing and keep a `

` used as a title looking right. + - [ ] **Step 6: app/layout.tsx** ```tsx @@ -709,10 +820,10 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +

-
- +
+ Vantage Licensing @@ -854,7 +965,7 @@ Expected: FAIL — `Failed to resolve import "./NotConnected"`. export function NotConnectedPanel({ url }: { url: string }) { return (
-

+

Not connected to the licensing service

{url ? ( @@ -1375,18 +1486,24 @@ export function EnvBadge() { ```tsx import clsx from "clsx"; -type Props = React.ButtonHTMLAttributes & { variant?: "solid" | "ghost" }; +type Props = React.ButtonHTMLAttributes & { variant?: "solid" | "line" }; +/* + * Matches site/'s .btn--solid and .btn--line exactly, including the neutral + * border on the secondary variant. site/ does not have an accent-outlined + * button and this app should not invent one. + */ export function Button({ variant = "solid", className, ...rest }: Props) { return (
@@ -2232,7 +2349,7 @@ export function RelinkPanel({ return (
-

Moved to a new server?

+

Moved to a new server?

Relinking issues a replacement licence for the new install, covering the rest of your current term. @@ -2249,7 +2366,7 @@ export function RelinkPanel({

@@ -3427,7 +3544,7 @@ export default function StaffInstancePage() { return (
-

{data.instance.name || data.instance.instance_id}

+

{data.instance.name || data.instance.instance_id}

{data.instance.instance_id}

@@ -3444,7 +3561,7 @@ export default function StaffInstancePage() {

-

Licence history

+

Licence history

@@ -3491,7 +3608,7 @@ export default function LicensesPage() { return (
-

Licences

+

Licences