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
+1 -1
View File
@@ -77,7 +77,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
<h1 className="text-base font-semibold text-text-primary">Can&apos;t load your session</h1>
<p className="mt-2 text-sm text-text-secondary">{error ?? "Unable to load your session."}</p>
<div className="mt-5 flex justify-center gap-2">
<button onClick={() => window.location.reload()} className="rounded-lg bg-accent px-3 py-2 text-sm font-medium text-white">
<button onClick={() => window.location.reload()} className="rounded bg-accent px-3 py-2 text-sm font-semibold text-accent-ink">
Retry
</button>
<a href="/login" className="rounded-lg border border-border px-3 py-2 text-sm font-medium text-text-secondary">
+3 -3
View File
@@ -10,7 +10,7 @@ export function LicenseBanner() {
if (license.state === "expired") {
const when = license.expires_at ? new Date(license.expires_at).toLocaleDateString() : "recently";
return (
<div className="bg-amber-900/40 px-4 py-2 text-sm text-amber-100">
<div className="border-b border-warning/40 bg-warning/10 px-4 py-2 text-sm text-warning">
Your Vantage licence expired on {when}. Your servers and monitors are still running,
but changes are disabled until it is renewed.{" "}
<Link href="/settings/license" className="underline">Add a licence</Link>
@@ -20,7 +20,7 @@ export function LicenseBanner() {
if (license.state === "invalid") {
return (
<div className="bg-red-900/40 px-4 py-2 text-sm text-red-100">
<div className="border-b border-danger/40 bg-danger/10 px-4 py-2 text-sm text-danger">
This instance has no valid licence. Changes are disabled.{" "}
<Link href="/settings/license" className="underline">Add a licence</Link>
</div>
@@ -29,7 +29,7 @@ export function LicenseBanner() {
if (typeof license.days_remaining === "number" && license.days_remaining <= 14) {
return (
<div className="bg-amber-900/25 px-4 py-2 text-sm text-amber-100">
<div className="border-b border-warning/25 bg-warning/[0.07] px-4 py-2 text-sm text-warning">
Your licence expires in {license.days_remaining} day
{license.days_remaining === 1 ? "" : "s"}.
</div>
+12 -6
View File
@@ -28,12 +28,18 @@ interface Pulse {
life: number; // seconds since spawn
}
const BG_TOP = "#0B2A58";
const BG_BOTTOM = "#0B1120";
const NODE_COLOR = "94, 122, 168"; // steel navy, rgb parts for alpha use
const LINE_COLOR = "30, 58, 95"; // #1E3A5F
const PULSE_COLOR = "93, 202, 165"; // #5DCAA5
const PULSE_CORE = "29, 158, 117"; // #1D9E75
/*
* Drawn from the shared tokens rather than read from them: this is canvas, so
* the values have to be literals. They are the light-theme accent falling to
* the app ground, with packets in the dark-theme accent — the same two blues
* the marketing site's hero is built on.
*/
const BG_TOP = "#0b2a58"; // --accent, light
const BG_BOTTOM = "#071628"; // --ground, dark
const NODE_COLOR = "113, 135, 159"; // --ink-3, rgb parts for alpha use
const LINE_COLOR = "30, 56, 85"; // --rule
const PULSE_COLOR = "91, 155, 232"; // --accent, dark
const PULSE_CORE = "228, 236, 246"; // --ink, a white-hot centre
const MAX_DIST = 190; // connect nodes within this px
const NODE_DENSITY = 1 / 22000; // nodes per px^2
+13 -6
View File
@@ -167,10 +167,12 @@ export function Sidebar() {
return (
<aside className="flex h-screen w-60 flex-col border-r border-border bg-surface">
<div className="flex h-16 items-center gap-3 border-b border-border px-5">
<Logo className="h-8 w-8 text-text-primary" />
<Logo className="h-8 w-8 text-logo" />
<div className="min-w-0">
<span className="block text-base font-semibold leading-tight text-text-primary">Vantage</span>
{instance && <span className="block truncate text-xs text-text-secondary">{instance.name}</span>}
<span className="block text-base font-extrabold leading-tight tracking-[-0.035em] text-text-primary">Vantage</span>
{instance && (
<span className="block truncate font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">{instance.name}</span>
)}
</div>
</div>
@@ -182,9 +184,14 @@ export function Sidebar() {
<li key={item.href}>
<Link
href={item.href}
// The active marker is an accent bar, the same device
// site/ uses to mark the chosen plan. A filled pill
// reads as a button you can press again.
className={clsx(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
isActive ? "bg-accent/15 text-accent" : "text-text-secondary hover:bg-surface-2 hover:text-text-primary",
"relative flex items-center gap-3 rounded px-3 py-2.5 text-sm transition-colors",
isActive
? "bg-surface-2 font-semibold text-text-primary before:absolute before:inset-y-1 before:left-0 before:w-[2px] before:rounded-full before:bg-accent before:content-['']"
: "font-medium text-text-secondary hover:bg-surface-2 hover:text-text-primary",
)}
>
{item.icon}
@@ -207,7 +214,7 @@ export function Sidebar() {
</div>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-text-secondary">Vantage v1.0</p>
<p className="font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">Vantage v1.0</p>
{user && (
<button type="button" onClick={handleLogout} className="text-xs text-text-secondary transition-colors hover:text-danger">
Logout
+18 -15
View File
@@ -8,32 +8,35 @@ interface BadgeProps {
className?: string;
}
/*
* site/'s .chip: mono, uppercase, widely tracked, drawn in currentColor with
* no fill. Every badge in this app holds one short state word, which is what
* the treatment is for.
*
* State is never carried by colour alone — the word itself says it, and the
* three state variants add a dot so the distinction survives a monochrome
* screen as well as it survives colour blindness.
*/
const variantClasses: Record<Variant, string> = {
success: "bg-success/15 text-success border-success/30",
warning: "bg-warning/15 text-warning border-warning/30",
danger: "bg-danger/15 text-danger border-danger/30",
neutral: "bg-surface-2 text-text-secondary border-border",
accent: "bg-accent/15 text-accent border-accent/30",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
neutral: "text-text-secondary",
accent: "text-accent",
};
export function Badge({ variant = "neutral", children, className }: BadgeProps) {
const hasState = variant === "success" || variant === "warning" || variant === "danger";
return (
<span
className={clsx(
"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium",
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-sm border border-current px-1.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.08em]",
variantClasses[variant],
className
)}
>
{(variant === "success" || variant === "warning" || variant === "danger") && (
<span
className={clsx("h-1.5 w-1.5 rounded-full", {
"bg-success": variant === "success",
"bg-warning": variant === "warning",
"bg-danger": variant === "danger",
})}
/>
)}
{hasState && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
{children}
</span>
);
+9 -4
View File
@@ -10,13 +10,18 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
}
/*
* site/'s .btn--solid and .btn--line, in Tailwind. Both filled variants take
* accent-ink rather than white: on the dark ground the accent lifts to a light
* blue and danger to a coral, and white on either is unreadable.
*/
const variantClasses: Record<Variant, string> = {
primary:
"bg-accent hover:bg-accent-hover text-white border-transparent",
"bg-accent hover:bg-accent-hover text-accent-ink border-accent hover:border-accent-hover",
secondary:
"bg-surface-2 hover:bg-[#2d3048] text-text-primary border-border",
"bg-surface hover:border-text-tertiary text-text-primary border-border",
danger:
"bg-danger hover:bg-danger-hover text-white border-transparent",
"bg-danger hover:bg-danger-hover text-accent-ink border-danger hover:border-danger-hover",
ghost:
"bg-transparent hover:bg-surface-2 text-text-secondary hover:text-text-primary border-transparent",
};
@@ -37,7 +42,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
ref={ref}
disabled={disabled || loading}
className={clsx(
"inline-flex items-center gap-2 rounded-lg border font-medium transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed",
"inline-flex items-center gap-2 rounded border font-semibold transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed active:translate-y-px",
variantClasses[variant],
sizeClasses[size],
className
+4 -2
View File
@@ -9,7 +9,9 @@ export function Card({ className, padding = true, children, ...props }: CardProp
return (
<div
className={clsx(
"rounded-xl border border-border bg-surface",
// Borders, not shadows. site/ builds its panels out of rules and the
// console has far more panels per screen than the site does.
"rounded border border-border bg-surface",
padding && "p-6",
className
)}
@@ -30,7 +32,7 @@ export function CardHeader({ className, children, ...props }: HTMLAttributes<HTM
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
return (
<h2 className={clsx("text-lg font-semibold text-text-primary", className)} {...props}>
<h2 className={clsx("text-lg font-extrabold tracking-[-0.03em] text-text-primary", className)} {...props}>
{children}
</h2>
);
+1 -1
View File
@@ -28,7 +28,7 @@ export function Modal({
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div
className={`relative z-10 w-full ${wide ? "max-w-2xl" : "max-w-md"} max-h-[90vh] overflow-auto rounded-xl border border-border bg-surface shadow-2xl`}
className={`relative z-10 w-full ${wide ? "max-w-2xl" : "max-w-md"} max-h-[90vh] overflow-auto rounded border border-border bg-surface shadow-panel`}
role="dialog"
aria-modal="true"
>
+5 -1
View File
@@ -45,7 +45,11 @@ export function Th({ className, children, ...props }: ThHTMLAttributes<HTMLTable
return (
<th
className={clsx(
"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-secondary",
// site/'s keyed-label idiom: mono, small, widely tracked, dimmed.
// A column head is a key, not prose.
// text-secondary, not tertiary: a column head is how you navigate the
// table, and tertiary lands under 4.5:1 at this size.
"px-4 py-3 text-left font-mono text-[0.68rem] uppercase tracking-[0.13em] text-text-secondary",
className
)}
{...props}
+3 -3
View File
@@ -32,7 +32,7 @@ function StepCard({ step, onAdd }: { step: WorkflowStep; onAdd: () => void }) {
return (
<button
onClick={onAdd}
className="group relative rounded-[10px] border border-border bg-surface-2 p-3 text-left transition-colors hover:border-signal/55"
className="group relative rounded border border-border bg-surface-2 p-3 text-left transition-colors hover:border-signal/55"
>
<span className="absolute right-3 top-3 text-xs font-semibold text-signal opacity-0 group-hover:opacity-100">
+ Add
@@ -132,13 +132,13 @@ export function StepPickerModal({
<div className="grid grid-cols-2 gap-2.5">
<button
onClick={onAddAdhoc}
className="flex min-h-[74px] items-center justify-center gap-2 rounded-[10px] border border-dashed border-border text-sm text-text-secondary hover:border-signal/55 hover:text-signal"
className="flex min-h-[74px] items-center justify-center gap-2 rounded border border-dashed border-border text-sm text-text-secondary hover:border-signal/55 hover:text-signal"
>
+ New ad-hoc step
</button>
<button
onClick={() => fileRef.current?.click()}
className="flex min-h-[74px] items-center justify-center gap-2 rounded-[10px] border border-dashed border-border text-sm text-text-secondary hover:border-signal/55 hover:text-signal"
className="flex min-h-[74px] items-center justify-center gap-2 rounded border border-dashed border-border text-sm text-text-secondary hover:border-signal/55 hover:text-signal"
>
Import ad-hoc from file
</button>