feat(web): the console joins the shared design system
Server Deploy / deploy (push) Successful in 1m6s

web/ was the last app on its own palette — a neutral #0f1117 ground with an
indigo accent, unrelated to the logo navy that site/ and adminsite/ are built
on. It now draws from the same tokens, so all three apps are one system.

It stays locked to dark, taking site/'s dark values. That is what keeps
adminsite/'s light default meaningful: an operator with both open tells them
apart by the ground before clicking anything destructive, and now that both
are the same palette, the ground is the only thing left doing that work.

The colour names stay this app's own — text-primary, border, surface rather
than ink, rule, panel — because every screen already reads that way, and
adminsite/ already establishes that each app names the shared tokens after
its own subject.

Tokens are stored as RGB channels with the hex in a trailing comment. The
console leans on Tailwind opacity modifiers far more than the other two
(bg-danger/10, border-accent/50, ring-accent/30), and <alpha-value> only
compiles against channels; the comments keep the three token blocks
diffable by eye.

Beyond colour:
- radii collapse to site/'s 4px in tailwind.config.ts rather than rewriting
  ~140 rounded-lg classes; rounded-full is untouched for dots and pills
- badges become site/'s chip — mono, uppercase, tracked, currentColor rule,
  no fill — keeping their dot so state is never colour alone
- table column heads take the keyed-label idiom, at text-secondary rather
  than tertiary, which lands under 4.5:1 at that size
- sidebar marks the active item with an accent bar, the device site/ uses
  for the chosen plan, instead of a filled pill that reads as pressable
- filled accent and danger buttons take accent-ink; the dark accent is a
  light blue and danger a coral, and white on either was unreadable
- login's packet pulses shift from green to the accent, so the sign-in
  screen is the same two blues as the marketing hero

The last hex literals and stock-palette classes are gone; the only ones left
are NetworkBackground's canvas fills, which cannot read a CSS variable and
are commented with the token each came from.

Only the web image rebuilds from this.
This commit is contained in:
mrhid6
2026-07-26 18:33:07 +01:00
parent 11ebd324ad
commit 01eda1dbb0
23 changed files with 326 additions and 118 deletions
+2 -2
View File
@@ -170,7 +170,7 @@ function PrivateKeyCard({ keyId }: { keyId: string }) {
</Button>
</div>
) : (
<div className="rounded-lg border border-border bg-[#0a0c14] p-3">
<div className="rounded-lg border border-border bg-well p-3">
<pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-xs text-text-secondary leading-relaxed">
{privateKey}
</pre>
@@ -337,7 +337,7 @@ export default function KeyDetailPage() {
{copiedKey ? <span className="text-success">Copied!</span> : "Copy"}
</button>
</CardHeader>
<div className="rounded-lg border border-border bg-[#0a0c14] p-3">
<div className="rounded-lg border border-border bg-well p-3">
<pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-xs text-text-secondary leading-relaxed">
{key.public_key}
</pre>
+1 -1
View File
@@ -492,7 +492,7 @@ export default function ServerDetailPage() {
>
{updateSuccess ? "Update Sent!" : "Update Agent"}
</Button>
<div className="relative flex-1 min-w-64 rounded-lg border border-border bg-[#0a0c14] px-4 py-2.5 font-mono text-sm">
<div className="relative flex-1 min-w-64 rounded-lg border border-border bg-well px-4 py-2.5 font-mono text-sm">
<span className="text-accent">{server.os_info?.toLowerCase().includes("windows") ? "PS>" : "$"}</span>{" "}
<span className="text-text-primary">{api.getUpdateCommand(server.os_info)}</span>
<button
+1 -1
View File
@@ -92,7 +92,7 @@ export default function NewServerPage() {
)}
</p>
<div className="relative rounded-lg border border-border bg-[#0a0c14] p-4 font-mono text-sm">
<div className="relative rounded-lg border border-border bg-well p-4 font-mono text-sm">
<pre className="overflow-x-auto whitespace-pre-wrap break-all text-text-secondary leading-relaxed">
<span className="text-accent">{os === "windows" ? "PS>" : "$"}</span>{" "}
<span className="text-text-primary">{command}</span>
+5 -2
View File
@@ -16,10 +16,13 @@ function resolveStatus(server: Server, latestVersion: string | undefined): DotSt
return "ok";
}
// The palette has one amber, so the two update states split by meaning rather
// than by shade: a stale agent is a warning, available OS packages are just
// something waiting for you.
const DOT_CLASSES: Record<DotStatus, string> = {
offline: "bg-danger",
"needs-update": "bg-orange-500",
"has-package-updates": "bg-yellow-400",
"needs-update": "bg-warning",
"has-package-updates": "bg-accent",
ok: "bg-success",
};
+2 -2
View File
@@ -80,10 +80,10 @@ export default function LicensePage() {
if (f) setBlob((await f.text()).trim());
}}
/>
{error ? <p className="mt-2 text-sm text-red-400">{error}</p> : null}
{error ? <p className="mt-2 text-sm text-danger">{error}</p> : null}
<button
type="button"
className="mt-3 rounded bg-accent px-3 py-1.5 text-sm"
className="mt-3 rounded bg-accent px-3 py-1.5 text-sm font-semibold text-accent-ink"
disabled={!blob.trim() || save.isPending}
onClick={() => save.mutate()}
>
+3 -3
View File
@@ -332,7 +332,7 @@ export default function WorkflowBuilder() {
<div className="pointer-events-none sticky top-0 z-10 flex justify-center pt-4">
<button
onClick={() => setPickerOpen(true)}
className="pointer-events-auto inline-flex items-center gap-2 rounded-[9px] bg-signal px-4 py-2.5 text-sm font-semibold text-signal-ink shadow-[0_6px_20px_rgba(245,165,36,0.28)] hover:bg-signal/90"
className="pointer-events-auto inline-flex items-center gap-2 rounded bg-signal px-4 py-2.5 text-sm font-semibold text-signal-ink shadow-panel hover:bg-signal/90"
>
<span className="text-base leading-none">+</span> Add step
</button>
@@ -369,7 +369,7 @@ export default function WorkflowBuilder() {
e.dataTransfer.setData("text/plain", JSON.stringify({ kind: "move", from: i }));
}}
onClick={() => setSelected(i)}
className={`w-[340px] cursor-pointer rounded-[10px] border bg-surface p-3 ${isSelected ? "border-signal ring-2 ring-signal/40" : "border-border"}`}
className={`w-[340px] cursor-pointer rounded border bg-surface p-3 ${isSelected ? "border-signal ring-2 ring-signal/40" : "border-border"}`}
>
<div className="mb-2 flex items-center gap-2">
<span className="grid h-5 w-5 place-items-center rounded border border-border font-mono text-[10px] text-text-secondary">{i + 1}</span>
@@ -391,7 +391,7 @@ export default function WorkflowBuilder() {
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => handleDrop(e, 0)}
onClick={() => setPickerOpen(true)}
className="mt-2 w-full rounded-[10px] border border-dashed border-border bg-surface py-6 text-sm text-text-secondary hover:border-signal/50 hover:text-text-primary"
className="mt-2 w-full rounded border border-dashed border-border bg-surface py-6 text-sm text-text-secondary hover:border-signal/50 hover:text-text-primary"
>
+ Add your first step
</button>
@@ -127,7 +127,7 @@ function LogTerminal({ runId, server }: { runId: string; server: ServerRun }) {
const activeStep = server.steps.find((s) => s.status === "running") ?? [...server.steps].reverse().find((s) => s.started_at);
return (
<div className="overflow-hidden rounded-xl border border-border bg-[#0a0b10]">
<div className="overflow-hidden rounded-xl border border-border bg-well">
<div className="flex items-center justify-between gap-2 border-b border-border bg-surface px-4 py-3">
<span className="truncate font-mono text-[13px] font-semibold text-text-primary">
{activeStep ? activeStep.name : "Output"} <span className="font-normal text-text-secondary">{server.hostname}</span>
@@ -167,7 +167,7 @@ function LogLines({ text }: { text: string }) {
const isMarker = body.startsWith("=====");
return (
<span key={i} className="block">
<span className="select-none text-[#565b74]" title={m[1]}>
<span className="select-none text-text-tertiary" title={m[1]}>
{local}{" "}
</span>
<span className={isMarker ? "font-semibold text-accent" : ""}>{body || " "}</span>
+143 -23
View File
@@ -2,56 +2,176 @@
@tailwind components;
@tailwind utilities;
/* ==========================================================================
Vantage control plane design tokens.
These are site/app/globals.css's **dark** token values — same names, same
colours — so the marketing site, the HQ console and this app are one visual
system. Change them in all three apps in the same commit; nothing enforces
the match automatically.
Two things differ from site/ and adminsite/, both deliberate:
1. There is no light block. web/ stays locked to dark. adminsite/ defaults
to light precisely so that a staff member holding both open can tell
them apart before clicking something destructive, and that argument
only holds while this app is the dark one.
2. Every colour is stored as RGB channels, with the hex it came from in a
trailing comment so it still diffs against site/ by eye. The plain
--token is derived from the channels rather than written twice. This app
leans hard on Tailwind's opacity modifiers (bg-danger/10, border-accent/50,
ring-accent/30 and so on), and those only compile when the colour reaches
Tailwind as channels it can drop an alpha into.
========================================================================== */
:root {
color-scheme: dark;
color-scheme: dark;
--ground-rgb: 7 22 40; /* #071628 */
--panel-rgb: 13 33 56; /* #0d2138 */
--panel-2-rgb: 16 40 66; /* #102842 */
--ink-rgb: 228 236 246; /* #e4ecf6 */
--ink-2-rgb: 159 179 202; /* #9fb3ca */
--ink-3-rgb: 113 135 159; /* #71879f */
--rule-rgb: 30 56 85; /* #1e3855 */
--rule-soft-rgb: 23 44 68; /* #172c44 */
--accent-rgb: 91 155 232; /* #5b9be8 */
--accent-ink-rgb: 4 16 31; /* #04101f */
--up-rgb: 79 180 132; /* #4fb484 */
--down-rgb: 226 112 90; /* #e2705a */
--pend-rgb: 214 166 63; /* #d6a63f */
--logo-rgb: 127 178 240; /* #7fb2f0 */
/* Not in site/: hover states and the well.
site/ brightens the accent with a filter, which a Tailwind colour token
cannot do, so the two hovers are named values. --well is the floor the
ground sits on — install one-liners, key blobs and run logs, the places
showing machine output rather than interface. */
--accent-hover-rgb: 127 178 240; /* #7fb2f0, the accent at site/'s hover brightness */
--down-hover-rgb: 200 90 69; /* #c85a45 */
--well-rgb: 4 16 31; /* #04101f */
--ground: rgb(var(--ground-rgb));
--panel: rgb(var(--panel-rgb));
--panel-2: rgb(var(--panel-2-rgb));
--ink: rgb(var(--ink-rgb));
--ink-2: rgb(var(--ink-2-rgb));
--ink-3: rgb(var(--ink-3-rgb));
--rule: rgb(var(--rule-rgb));
--rule-soft: rgb(var(--rule-soft-rgb));
--accent: rgb(var(--accent-rgb));
--accent-ink: rgb(var(--accent-ink-rgb));
--up: rgb(var(--up-rgb));
--down: rgb(var(--down-rgb));
--pend: rgb(var(--pend-rgb));
--logo: rgb(var(--logo-rgb));
--well: rgb(var(--well-rgb));
--shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 20px 44px -26px rgba(0, 0, 0, 0.85);
--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;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #0f1117;
color: #e8eaf0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: var(--ground);
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
/* site/'s heading treatment, which is what stands in for a display face.
Utilities still win where a component sets weight or tracking explicitly. */
h1,
h2,
h3 {
font-weight: 800;
letter-spacing: -0.03em;
text-wrap: balance;
}
code {
font-family: var(--mono);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #1a1d27;
background: var(--panel-2);
}
::-webkit-scrollbar-thumb {
background: #2e3147;
border-radius: 3px;
background: var(--rule);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #3e4160;
background: var(--ink-3);
}
@keyframes led-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.led-pulse {
animation: led-pulse 1.4s ease-in-out infinite;
}
.led-pulse { animation: led-pulse 1.4s ease-in-out infinite; }
@keyframes cell-ring {
0%, 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5); }
50% { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0); }
0%,
100% {
box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.5);
}
50% {
box-shadow: 0 0 0 4px rgb(var(--accent-rgb) / 0);
}
}
.cell-ring {
animation: cell-ring 1.4s ease-in-out infinite;
}
.cell-ring { animation: cell-ring 1.4s ease-in-out infinite; }
@keyframes caret-blink { 50% { opacity: 0; } }
.caret-blink { animation: caret-blink 1s step-end infinite; }
@keyframes caret-blink {
50% {
opacity: 0;
}
}
.caret-blink {
animation: caret-blink 1s step-end infinite;
}
@media (prefers-reduced-motion: reduce) {
.led-pulse, .cell-ring, .caret-blink { animation: none; }
.led-pulse,
.cell-ring,
.caret-blink {
animation: none;
}
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}
+5 -2
View File
@@ -13,8 +13,11 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body className="bg-background text-text-primary">
// data-theme is what site/ and adminsite/ switch on. This app is locked to
// dark, so the attribute is fixed rather than toggleable — it is here so
// the three apps declare their ground the same way.
<html lang="en" className="dark" data-theme="dark">
<body className="bg-background font-sans text-text-primary">
<Providers>{children}</Providers>
</body>
</html>
+4 -3
View File
@@ -50,9 +50,10 @@ export default function LoginPage() {
<NetworkBackground />
<div className="relative w-full max-w-sm">
<div className="mb-8 flex flex-col items-center gap-3">
<Logo className="h-11 w-auto text-text-primary" />
<h1 className="text-2xl font-semibold text-text-primary">Sign in to Vantage</h1>
<h4 className="font-semibold text-text-secondary">Instance: {instanceName}</h4>
<Logo className="h-11 w-auto text-logo" />
<h1 className="text-2xl font-extrabold tracking-[-0.03em] text-text-primary">Sign in to Vantage</h1>
{/* A keyed label, not a heading: site/'s .tag treatment. */}
<p className="font-mono text-[0.7rem] uppercase tracking-[0.15em] text-text-secondary">{instanceName}</p>
</div>
<Card>
+2 -2
View File
@@ -83,7 +83,7 @@ export default function SetupPage() {
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-text-primary">Instance created</h1>
<h1 className="text-xl font-extrabold tracking-[-0.03em] text-text-primary">Instance created</h1>
<p className="mt-1 text-sm text-text-secondary">Your owner account is ready. One more step to finish signing in.</p>
</div>
@@ -123,7 +123,7 @@ export default function SetupPage() {
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-text-primary">Welcome to Vantage</h1>
<h1 className="text-xl font-extrabold tracking-[-0.03em] text-text-primary">Welcome to Vantage</h1>
<p className="mt-1 text-sm text-text-secondary">Create your instance and its owner account to get started.</p>
</div>