desktop-new: delete the categorical tints, add a proving-ground dashboard

Two changes, both about finding out whether the colour system actually works.

Deleted the six categorical tint roles. Evidence: zero component usage — not one
reference outside the token file and the registry. They were hand-picked literals
of exactly the kind the palette exists to prevent, and the open question 'how
many categorical colours does the product need, and does a person choose one or
does the product assign it' is unanswered, so naming them now was guessing. The
eight palette hues stay; a categorical role gets named when a feature needs to
distinguish things.

That deletion turned a guard check vacuous, which is its own finding: the drift
check named four specific pairs (--tint-purple vs --accent-tint, etc.) and would
have passed silently forever once those tokens were gone. Replaced with the
general rule — no two family steps may resolve to the same palette step — which
catches the same class for any future family without naming pairs. Falsified by
pointing --danger-tint at the purple ramp.

Added /proving-ground: a fictional fleet-monitoring dashboard, deliberately
nothing to do with Buzz. Specimen pages are honest but sympathetic — one
component at a time, on a chosen backdrop, at a chosen size. A dashboard puts
forty decisions next to each other, which is where a missing role shows up. It
is also the surface a generated theme has to be judged on: 'Neon Tokyo' can look
fine as twelve swatches and be unusable as a page.

Built only from role tokens, and it held: no new roles were needed, contrast
passes both modes, and the layout works at 1440 and 620. Page-local parts live
in the feature rather than shared/ui on purpose — this page exists to find out
what repeats, so promoting its pieces first would be guessing.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-09-06 20:41:56 -07:00
parent cef97be05b
commit efb443a2cb
11 changed files with 1120 additions and 75 deletions

View File

@@ -92,8 +92,9 @@ Four layers, and only the role layer is used when building a screen:
The palette was added after the families accumulated 114 hand-picked hex values
with nothing enforcing that two tokens doing the same job agreed — and they
drifted: `accent-2` and `tint-purple` were the same purple in light mode and two
different purples in dark. Referencing a step cannot drift that way, and
drifted: an accent tint and a categorical purple were the same purple in light
mode and two different purples in dark. Referencing a step cannot drift that way,
and
`src/shared/tokens/palette.test.ts` asserts it.
Family steps are **named for their job, not numbered**. `--accent-2` invited

View File

@@ -87,11 +87,11 @@ Buzz is a place where people build together and bring their agents into the room
- **A colour is used one of two ways: solid or tint.** Solid carries an action and takes its paired text; tint carries a meaning and takes coloured text. There is deliberately nothing between them.
- **Accent is signal, never structure.** Reaching for an accent surface where a neutral one belongs is the most common way a functional screen starts to look decorated.
- **Never use a status colour decoratively.** A green that does not mean success teaches people to stop trusting green.
- **Categorical colours are the one place appearance-naming is allowed.** Telling two projects apart genuinely is a choice about appearance.
- **Categorical colours are the one place appearance-naming would be allowed.** Telling two projects apart genuinely is a choice about appearance, so a hue name is honest there. No such roles exist yet — the palette carries eight hues, and a categorical role gets named when a feature actually needs to distinguish things, not before.
- **Opacity is not how you reach a subtler colour.** If a tint looks too strong, take a different palette step — do not dim a stronger one. `purple-950/50` composites to a real, correct colour, which is exactly the trap: it is a colour decision with no name, no light/dark pair, and nothing the contrast guard can measure. A missing shade is a missing palette step, and adding one is an ordinary reviewed edit. `scripts/check-color.mjs` enforces this.
- **Transparency is a different axis from shade, and it has its own tokens.** `glass-*` exists for surfaces something must show through. Alpha baked into a named value at the palette layer is the system working; alpha applied to a token in a component is not.
- **A dark value is authored, never derived.** A hue's dark steps are not its light steps darkened or dimmed — Tailwind's purple gets *more* saturated as it descends, so a dark tint drawn from its bottom end reads as oversaturated. This is why the palette holds two authored ramps per hue rather than one ramp and a transform.
- **Two tokens doing the same job must resolve to the same step, not merely to the same value.** Matching literals drift; a shared reference cannot. The accent tint and the categorical purple were the same colour in light mode and two different colours in dark, and nothing caught it because both were hand-picked.
- **Two tokens doing the same job must resolve to the same step, not merely to the same value.** Matching literals drift; a shared reference cannot. An accent tint and a hand-picked categorical purple were the same colour in light mode and two different colours in dark, and nothing caught it because both held their own value.
## Contrast

View File

@@ -231,12 +231,6 @@ function auditLayers() {
];
const FAMILIES = ["accent", "danger", "success", "warning", "info"];
const JOBS = ["tint", "tint-hover", "border", "fill", "text"];
const CATEGORICAL = [
["--tint-purple", "--accent-tint"],
["--tint-red", "--danger-tint"],
["--tint-green", "--success-tint"],
["--tint-blue", "--info-tint"],
];
// 1. Every palette step exists in both modes and holds a literal.
for (const hue of HUES) {
@@ -280,16 +274,30 @@ function auditLayers() {
}
}
// 3. Two tokens doing one job resolve to one step. Matching values drift.
for (const [a, b] of CATEGORICAL) {
const left = read(modes.light, a);
const right = read(modes.light, b);
if (left && right && left !== right) {
failures.push({
at: rel,
found: `${a}: ${left} vs ${b}: ${right}`,
why: "Same job, different step. Point both at one palette step — this exact pair drifted between modes before the palette existed.",
});
// 3. No two family steps resolve to the same palette step.
//
// The generalised form of the bug that motivated this layer. Two tokens that
// hold the same value are either one job with two names — which drifts the
// moment someone edits one — or two jobs that will become indistinguishable
// on screen. Either way it wants a person to look. This catches it for any
// future family without naming pairs, which the previous version did and
// which went silently vacuous when those tokens were deleted.
const seen = new Map();
for (const family of FAMILIES) {
for (const job of JOBS) {
const name = `--${family}-${job}`;
const value = read(modes.light, name);
if (!value) continue;
const existing = seen.get(value);
if (existing) {
failures.push({
at: rel,
found: `${existing} and ${name} both resolve to ${value}`,
why: "Two family steps on one palette step. Either it is one job with two names, or two jobs that will look identical. Give the second its own step.",
});
} else {
seen.set(value, name);
}
}
}

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { ProvingGroundPage } from "@/features/proving-ground/ui/ProvingGroundPage";
export const Route = createFileRoute("/proving-ground")({
component: ProvingGroundPage,
});

View File

@@ -0,0 +1,187 @@
/**
* Fixture data for the proving ground.
*
* A fictional product — a fleet-monitoring dashboard — deliberately unrelated to
* Buzz. The point is to exercise the design system at the density and variety a
* real page has, without any of it becoming a claim about what Buzz looks like.
* Nothing here is a product decision; it is load for the tokens to carry.
*/
export type Health = "healthy" | "degraded" | "failing" | "paused";
export type Region = {
id: string;
name: string;
code: string;
health: Health;
/** Requests per second, for the sparkline and the headline figure. */
throughput: number;
/** Percentage, one decimal. */
successRate: number;
latencyMs: number;
/** Twelve readings, oldest first. */
history: number[];
};
export type Incident = {
id: string;
title: string;
region: string;
severity: "critical" | "warning" | "info";
openedAt: string;
assignee?: { name: string; initial: string };
acknowledged: boolean;
};
export type Deploy = {
id: string;
service: string;
version: string;
status: "live" | "rolling" | "held" | "rolled-back";
author: string;
at: string;
};
export const REGIONS: Region[] = [
{
id: "use1",
name: "US East",
code: "use1",
health: "healthy",
throughput: 18420,
successRate: 99.98,
latencyMs: 42,
history: [12, 14, 13, 16, 18, 17, 19, 21, 20, 22, 24, 23],
},
{
id: "usw2",
name: "US West",
code: "usw2",
health: "healthy",
throughput: 11260,
successRate: 99.94,
latencyMs: 51,
history: [9, 10, 11, 10, 12, 13, 12, 14, 13, 15, 14, 16],
},
{
id: "euw1",
name: "EU West",
code: "euw1",
health: "degraded",
throughput: 8940,
successRate: 97.21,
latencyMs: 189,
history: [14, 13, 15, 14, 12, 11, 9, 8, 10, 9, 7, 8],
},
{
id: "apse1",
name: "AP Southeast",
code: "apse1",
health: "failing",
throughput: 1180,
successRate: 71.44,
latencyMs: 842,
history: [11, 12, 10, 11, 9, 7, 5, 3, 2, 2, 1, 1],
},
{
id: "sae1",
name: "SA East",
code: "sae1",
health: "paused",
throughput: 0,
successRate: 0,
latencyMs: 0,
history: [6, 6, 5, 6, 4, 3, 0, 0, 0, 0, 0, 0],
},
];
export const INCIDENTS: Incident[] = [
{
id: "INC-4821",
title: "Elevated 5xx from the ingest gateway",
region: "apse1",
severity: "critical",
openedAt: "14 min ago",
assignee: { name: "Priya Raman", initial: "P" },
acknowledged: true,
},
{
id: "INC-4820",
title: "Replica lag above threshold on the primary cluster",
region: "euw1",
severity: "warning",
openedAt: "38 min ago",
assignee: { name: "Tomas Lindqvist", initial: "T" },
acknowledged: true,
},
{
id: "INC-4819",
title: "Certificate expires in 6 days",
region: "usw2",
severity: "info",
openedAt: "2 hr ago",
acknowledged: false,
},
{
id: "INC-4817",
title: "Queue depth climbing without a matching request increase",
region: "euw1",
severity: "warning",
openedAt: "5 hr ago",
assignee: { name: "Dani Okonkwo", initial: "D" },
acknowledged: false,
},
];
export const DEPLOYS: Deploy[] = [
{
id: "d-9931",
service: "ingest-gateway",
version: "2026.9.4",
status: "rolling",
author: "Priya Raman",
at: "6 min ago",
},
{
id: "d-9930",
service: "index-writer",
version: "2026.9.3",
status: "live",
author: "Marco Bellini",
at: "1 hr ago",
},
{
id: "d-9929",
service: "query-planner",
version: "2026.9.3",
status: "held",
author: "Dani Okonkwo",
at: "3 hr ago",
},
{
id: "d-9927",
service: "ingest-gateway",
version: "2026.9.2",
status: "rolled-back",
author: "Tomas Lindqvist",
at: "yesterday",
},
];
/** Maps a health state to the status family that carries its meaning. */
export const HEALTH_TONE: Record<
Health,
"success" | "warning" | "danger" | "neutral"
> = {
healthy: "success",
degraded: "warning",
failing: "danger",
paused: "neutral",
};
export const HEALTH_LABEL: Record<Health, string> = {
healthy: "Healthy",
degraded: "Degraded",
failing: "Failing",
paused: "Paused",
};

View File

@@ -0,0 +1,401 @@
import {
IconActivity,
IconAlertTriangle,
IconChartDots,
IconChevronRight,
IconCloud,
IconMoon,
IconRocket,
IconSun,
} from "@tabler/icons-react";
import { useState } from "react";
import { useColorScheme } from "@/shared/theme/useColorScheme";
import { Avatar } from "@/shared/ui/Avatar";
import { Button } from "@/shared/ui/Button";
import { IconButton } from "@/shared/ui/IconButton";
import { SearchField } from "@/shared/ui/SearchField";
import { SegmentedNavigation } from "@/shared/ui/SegmentedNavigation";
import { DEPLOYS, HEALTH_TONE, INCIDENTS, REGIONS } from "../data";
import {
HealthPill,
KeyValue,
Metric,
Panel,
Row,
Sparkline,
StatusPill,
} from "./parts";
/**
* The proving ground: a fictional fleet-monitoring dashboard.
*
* Deliberately not Buzz. Its job is to load the design system the way a real
* product does — many decisions adjacent to each other, at density, in both
* modes — and to be the surface a generated theme is judged on. A specimen page
* shows one component at a time on a backdrop the designer chose; this shows
* forty at once, which is where a missing role or a wrong neutral step appears.
*
* Built only from role tokens. If something here needs a value the system does
* not have, that is the finding.
*/
const RANGES = [
{ value: "1h", label: "1 hour" },
{ value: "24h", label: "24 hours" },
{ value: "7d", label: "7 days" },
] as const;
const SEVERITY_TONE = {
critical: "danger",
warning: "warning",
info: "info",
} as const;
const DEPLOY_TONE = {
live: "success",
rolling: "accent",
held: "warning",
"rolled-back": "danger",
} as const;
const DEPLOY_LABEL = {
live: "Live",
rolling: "Rolling out",
held: "Held",
"rolled-back": "Rolled back",
} as const;
export function ProvingGroundPage() {
const { scheme, toggle } = useColorScheme();
const [range, setRange] = useState<string>("24h");
const [section, setSection] = useState("overview");
const [query, setQuery] = useState("");
const openIncidents = INCIDENTS.length;
const failing = REGIONS.filter((r) => r.health === "failing").length;
return (
<div className="dash-shell">
<nav className="dash-nav" aria-label="Sections">
<div className="dash-nav-group">
<span className="dash-nav-label text-body-sm">Monitor</span>
<NavItem
icon={<IconActivity size={16} />}
label="Overview"
id="overview"
current={section}
onSelect={setSection}
/>
<NavItem
icon={<IconAlertTriangle size={16} />}
label="Incidents"
id="incidents"
count={openIncidents}
current={section}
onSelect={setSection}
/>
<NavItem
icon={<IconCloud size={16} />}
label="Regions"
id="regions"
count={REGIONS.length}
current={section}
onSelect={setSection}
/>
</div>
<div className="dash-nav-group">
<span className="dash-nav-label text-body-sm">Ship</span>
<NavItem
icon={<IconRocket size={16} />}
label="Deploys"
id="deploys"
current={section}
onSelect={setSection}
/>
<NavItem
icon={<IconChartDots size={16} />}
label="Reports"
id="reports"
current={section}
onSelect={setSection}
/>
</div>
</nav>
<main className="dash-main">
<header className="dash-topbar">
<div className="flex min-w-0 flex-col gap-1">
<h1 className="text-title text-primary">Fleet</h1>
<p className="text-body text-secondary">
Five regions, {openIncidents} open incidents
{failing > 0 ? `, ${failing} failing` : ""}.
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<SearchField
value={query}
onValueChange={setQuery}
placeholder="Search regions and services"
/>
<SegmentedNavigation
label="Time range"
items={RANGES}
value={range}
onValueChange={setRange}
/>
<IconButton
aria-label={
scheme === "dark" ? "Use light mode" : "Use dark mode"
}
onClick={toggle}
icon={
scheme === "dark" ? (
<IconSun size={16} />
) : (
<IconMoon size={16} />
)
}
/>
<Button variant="primary">New check</Button>
</div>
</header>
<div className="dash-metric-grid">
<Metric
label="Requests"
value="39.8"
unit="k/s"
change="+4.2% vs yesterday"
direction="up"
/>
<Metric
label="Success rate"
value="97.4"
unit="%"
change="−1.9 points"
direction="down"
/>
<Metric
label="p95 latency"
value="188"
unit="ms"
change="+61 ms"
direction="down"
/>
<Metric
label="Open incidents"
value="4"
change="2 acknowledged"
direction="flat"
/>
</div>
<div className="dash-columns">
<div className="flex min-w-0 flex-col gap-6">
<Panel
title="Regions"
description="Throughput over the selected window."
>
<div className="dash-region-grid">
{REGIONS.map((region) => (
<article key={region.id} className="dash-region">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-col">
<span className="text-body text-primary">
{region.name}
</span>
<span className="text-mono-sm text-tertiary">
{region.code}
</span>
</div>
<HealthPill health={region.health} />
</div>
<Sparkline
values={region.history}
tone={HEALTH_TONE[region.health]}
/>
<div className="flex flex-col gap-1">
<KeyValue
label="Requests"
value={
region.throughput === 0
? "—"
: `${(region.throughput / 1000).toFixed(1)}k/s`
}
/>
<KeyValue
label="Success"
value={
region.successRate === 0
? "—"
: `${region.successRate.toFixed(2)}%`
}
/>
<KeyValue
label="p95"
value={
region.latencyMs === 0
? "—"
: `${region.latencyMs} ms`
}
/>
</div>
</article>
))}
</div>
</Panel>
<Panel
title="Open incidents"
description="Newest first."
padded={false}
action={
<Button variant="quiet" size="compact">
View all
</Button>
}
>
{INCIDENTS.map((incident) => (
<Row key={incident.id} interactive>
<StatusPill tone={SEVERITY_TONE[incident.severity]}>
{incident.severity}
</StatusPill>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-body text-primary">
{incident.title}
</span>
<span className="text-body-sm text-tertiary">
<span className="text-mono-sm">{incident.id}</span> ·{" "}
{incident.region} · {incident.openedAt}
</span>
</div>
{incident.assignee ? (
<Avatar
size="small"
alt={`Assigned to ${incident.assignee.name}`}
fallback={incident.assignee.initial}
/>
) : (
<span className="text-body-sm text-disabled">
Unassigned
</span>
)}
<IconChevronRight
size={16}
className="text-tertiary"
aria-hidden="true"
/>
</Row>
))}
</Panel>
</div>
<div className="flex min-w-0 flex-col gap-6">
<Panel title="Recent deploys" padded={false}>
{DEPLOYS.map((deploy) => (
<Row key={deploy.id}>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-body text-primary">
{deploy.service}
</span>
<span className="text-body-sm text-tertiary">
<span className="text-mono-sm">{deploy.version}</span> ·{" "}
{deploy.author} · {deploy.at}
</span>
</div>
<StatusPill tone={DEPLOY_TONE[deploy.status]}>
{DEPLOY_LABEL[deploy.status]}
</StatusPill>
</Row>
))}
</Panel>
<Panel
title="Rollout"
description="ingest-gateway 2026.9.4 is going out region by region."
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<span className="text-body-sm text-secondary">
Progress
</span>
<span className="text-body-sm text-primary tabular-nums">
2 of 5 regions
</span>
</div>
{/* A determinate bar: inset track, accent fill. */}
<div
className="h-2 overflow-hidden rounded-pill bg-inset"
role="progressbar"
aria-valuenow={40}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Rollout progress"
>
<div className="h-full w-2/5 rounded-pill bg-accent" />
</div>
</div>
<div className="flex flex-col gap-1">
<KeyValue label="Started" value="6 min ago" />
<KeyValue label="Error budget" value="98.2% remaining" />
<KeyValue label="Next region" value="euw1" />
</div>
<div className="flex flex-wrap gap-2">
<Button variant="primary" size="compact">
Continue rollout
</Button>
<Button variant="quiet" size="compact">
Hold
</Button>
<Button variant="ghost" size="compact" disabled>
Roll back
</Button>
</div>
</div>
</Panel>
</div>
</div>
</main>
</div>
);
}
function NavItem({
icon,
label,
id,
count,
current,
onSelect,
}: {
icon: React.ReactNode;
label: string;
id: string;
count?: number;
current: string;
onSelect: (id: string) => void;
}) {
const isCurrent = current === id;
return (
<button
type="button"
className="dash-nav-item text-body"
aria-current={isCurrent ? "page" : undefined}
onClick={() => onSelect(id)}
>
<span aria-hidden="true" className="flex shrink-0">
{icon}
</span>
{label}
{count !== undefined ? (
<span className="dash-nav-count text-body-sm">{count}</span>
) : null}
</button>
);
}

View File

@@ -0,0 +1,187 @@
import type { ReactNode } from "react";
import { type Health, HEALTH_LABEL, HEALTH_TONE } from "../data";
/**
* Pieces the proving-ground dashboard is assembled from.
*
* These are page-local on purpose. The shared layer earns a component when the
* product repeats one; this page exists to find out *which* things repeat, so
* putting its parts in `shared/ui` first would be guessing. Anything that turns
* out to be load-bearing graduates afterwards.
*/
type Tone = "success" | "warning" | "danger" | "neutral" | "accent" | "info";
/**
* A status pill. Tint fill, coloured text — the tint arrangement from
* DESIGN.md, where a colour means something rather than carrying an action.
*/
export function StatusPill({
tone,
children,
}: {
tone: Tone;
children: ReactNode;
}) {
return (
<span className="status-pill" data-tone={tone}>
{children}
</span>
);
}
export function HealthPill({ health }: { health: Health }) {
return (
<StatusPill tone={HEALTH_TONE[health]}>{HEALTH_LABEL[health]}</StatusPill>
);
}
/** A page region. Panel surface, generous inset, no card-per-row inside. */
export function Panel({
title,
description,
action,
children,
padded = true,
}: {
title: string;
description?: string;
action?: ReactNode;
children: ReactNode;
/** False when the content is edge-to-edge rows that supply their own inset. */
padded?: boolean;
}) {
return (
<section className="dash-panel">
<header className="dash-panel-header">
<div className="flex min-w-0 flex-col gap-1">
<h2 className="text-heading text-primary">{title}</h2>
{description ? (
<p className="text-body-sm text-secondary">{description}</p>
) : null}
</div>
{action}
</header>
<div data-padded={padded || undefined} className="dash-panel-body">
{children}
</div>
</section>
);
}
/**
* A headline figure with its label and change.
*
* Hierarchy comes from size, not colour: the number is `text-display`, the label
* is small and secondary. Only the change direction earns a colour, because a
* direction is a signal.
*/
export function Metric({
label,
value,
unit,
change,
direction,
}: {
label: string;
value: string;
unit?: string;
change?: string;
direction?: "up" | "down" | "flat";
}) {
return (
<div className="dash-metric">
<span className="text-body-sm text-secondary">{label}</span>
<div className="flex items-baseline gap-1.5">
<span className="text-display text-primary tabular-nums">{value}</span>
{unit ? (
<span className="text-body-sm text-tertiary">{unit}</span>
) : null}
</div>
{change ? (
<span
className="text-body-sm tabular-nums"
data-direction={direction}
// A rise is not automatically good, so direction is coloured by what
// it means on this metric rather than by which way it points.
style={{
color:
direction === "up"
? "var(--text-success)"
: direction === "down"
? "var(--text-danger)"
: "var(--text-tertiary)",
}}
>
{change}
</span>
) : null}
</div>
);
}
/**
* A bar sparkline, drawn with divs rather than SVG.
*
* Deliberate: it inherits `currentColor` and the rem-based type scale, so it
* tracks the theme and the font-size preference without a redraw. Decorative —
* the figure beside it carries the information, so this is hidden from
* assistive technology rather than described.
*/
export function Sparkline({
values,
tone = "accent",
}: {
values: number[];
tone?: Tone;
}) {
const peak = Math.max(...values, 1);
// A reading's position in the window is part of its identity — the oldest
// sample is a different thing from the newest — so the key carries both.
const bars = values.map((value, index) => ({
id: `${index}:${value}`,
height: `${Math.max((value / peak) * 100, 4)}%`,
}));
return (
<div className="dash-sparkline" data-tone={tone} aria-hidden="true">
{bars.map((bar) => (
<span
key={bar.id}
className="dash-sparkline-bar"
style={{ height: bar.height }}
/>
))}
</div>
);
}
/** A dense table row. Dividers, edge to edge — never a card per row. */
export function Row({
children,
interactive = false,
}: {
children: ReactNode;
interactive?: boolean;
}) {
return (
<div className="dash-row" data-interactive={interactive || undefined}>
{children}
</div>
);
}
export function KeyValue({
label,
value,
}: {
label: string;
value: ReactNode;
}) {
return (
<div className="flex items-baseline justify-between gap-4">
<span className="text-body-sm text-secondary">{label}</span>
<span className="text-body-sm text-primary tabular-nums">{value}</span>
</div>
);
}

View File

@@ -3,6 +3,7 @@
@import "./typography.css";
@import "./components.css";
@import "./chips.css";
@import "./proving-ground.css";
@import "./design-components.css";
@import "./product.css";
@import "./agents.css";

View File

@@ -0,0 +1,304 @@
@layer components {
/**
* Proving-ground dashboard.
*
* A fictional product built only from role tokens, to find out whether the
* system holds up on a real page rather than on a specimen page. Specimen
* pages are honest but sympathetic — they show one component at a time, on a
* generous backdrop, at a size the designer chose. A dashboard puts forty
* decisions next to each other and is where a missing role shows up.
*/
.dash-shell {
display: grid;
min-height: 100vh;
grid-template-columns: var(--size-navigator) minmax(0, 1fr);
background: var(--bg-app);
}
/* Narrow: the navigator becomes a top strip rather than a drawer. There is no
drawer machinery here yet, and a horizontal strip is honest about that. */
@media (max-width: 60rem) {
.dash-shell {
grid-template-columns: minmax(0, 1fr);
}
}
.dash-nav {
display: flex;
flex-direction: column;
gap: var(--space-6);
padding: var(--space-panel-inset);
border-right: 1px solid var(--border-tertiary);
}
@media (max-width: 60rem) {
.dash-nav {
flex-direction: row;
align-items: center;
gap: var(--space-4);
overflow-x: auto;
border-right: 0;
border-bottom: 1px solid var(--border-tertiary);
padding-block: var(--space-3);
}
}
.dash-nav-group {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-1);
}
@media (max-width: 60rem) {
.dash-nav-group {
flex-direction: row;
align-items: center;
gap: var(--space-1);
}
/* The section labels are useful in a column and noise in a strip. */
.dash-nav-label {
display: none;
}
}
.dash-nav-label {
padding-inline: var(--space-2);
padding-block: var(--space-1);
color: var(--text-tertiary);
}
.dash-nav-item {
display: flex;
align-items: center;
gap: var(--space-row-gap);
border: 0;
border-radius: var(--radius-row);
padding: var(--space-2) var(--space-2);
background: transparent;
color: var(--text-secondary);
font: inherit;
text-align: start;
white-space: nowrap;
cursor: pointer;
transition:
background-color var(--duration-state) var(--easing-state),
color var(--duration-state) var(--easing-state);
}
.dash-nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* Selected is a tint plus coloured text — the tint arrangement. Weight is
never used to say "selected"; that is reserved for structure. */
.dash-nav-item[aria-current="page"] {
background: var(--bg-accent-tint);
color: var(--text-accent);
}
.dash-nav-item:focus-visible {
outline: 2px solid var(--ring-focus);
outline-offset: 2px;
}
.dash-nav-count {
margin-inline-start: auto;
color: var(--text-tertiary);
font-variant-numeric: tabular-nums;
}
@media (max-width: 60rem) {
.dash-nav-count {
margin-inline-start: var(--space-1);
}
}
.dash-main {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-6);
padding: var(--space-panel-inset);
}
.dash-topbar {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
}
.dash-metric-grid {
display: grid;
gap: var(--space-4);
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.dash-metric {
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.dash-columns {
display: grid;
gap: var(--space-6);
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
align-items: start;
}
@media (max-width: 72rem) {
.dash-columns {
grid-template-columns: minmax(0, 1fr);
}
}
.dash-panel {
display: flex;
min-width: 0;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-tertiary);
border-radius: var(--radius-panel);
background: var(--bg-panel);
box-shadow: var(--shadow-xs);
}
.dash-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-5) var(--space-panel-inset);
}
.dash-panel-body[data-padded] {
padding: 0 var(--space-panel-inset) var(--space-panel-inset);
}
/* Dense rows, edge to edge, separated by a line rather than by cards. */
.dash-row {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-3) var(--space-panel-inset);
border-top: 1px solid var(--border-tertiary);
}
.dash-row[data-interactive] {
cursor: pointer;
transition: background-color var(--duration-state) var(--easing-state);
}
.dash-row[data-interactive]:hover {
background: var(--bg-hover);
}
.status-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
border-radius: var(--radius-pill);
padding: 0.125rem var(--space-2);
white-space: nowrap;
}
.status-pill[data-tone="success"] {
background: var(--bg-success-tint);
color: var(--text-success);
}
.status-pill[data-tone="warning"] {
background: var(--bg-warning-tint);
color: var(--text-warning);
}
.status-pill[data-tone="danger"] {
background: var(--bg-danger-tint);
color: var(--text-danger);
}
.status-pill[data-tone="info"] {
background: var(--bg-info-tint);
color: var(--text-info);
}
.status-pill[data-tone="accent"] {
background: var(--bg-accent-tint);
color: var(--text-accent);
}
/* Paused is not a status colour: nothing is wrong, so a colour would teach
people to distrust the coloured ones. */
.status-pill[data-tone="neutral"] {
background: var(--bg-inset);
color: var(--text-secondary);
}
.dash-sparkline {
display: flex;
align-items: flex-end;
gap: 2px;
height: 2rem;
min-width: 4rem;
flex: 1 1 auto;
}
.dash-sparkline-bar {
flex: 1 1 0;
min-width: 2px;
border-radius: 1px;
background: currentColor;
}
.dash-sparkline[data-tone="success"] {
color: var(--bg-success);
}
.dash-sparkline[data-tone="warning"] {
color: var(--bg-warning);
}
.dash-sparkline[data-tone="danger"] {
color: var(--bg-danger);
}
.dash-sparkline[data-tone="accent"] {
color: var(--bg-accent);
}
.dash-sparkline[data-tone="neutral"] {
color: var(--border-secondary);
}
.dash-region-grid {
display: grid;
gap: var(--space-4);
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
/* A region tile is a widget, which is what a card is legitimately for. */
.dash-region {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-3);
border: 1px solid var(--border-tertiary);
border-radius: var(--radius-control);
padding: var(--space-4);
background: var(--bg-inset);
}
@media (prefers-reduced-motion: reduce) {
.dash-nav-item,
.dash-row[data-interactive] {
transition-duration: 0ms;
}
}
}

View File

@@ -10,8 +10,9 @@
*
* The palette exists because the layer above it was 114 hand-picked hex values.
* Nothing enforced that two tokens doing the same job agreed, and they drifted:
* `accent-2` and `tint-purple` were the same purple in light mode and two
* different purples in dark. Picking a step from a ramp cannot drift that way.
* an accent tint and a categorical purple were the same purple in light mode
* and two different purples in dark. Picking a step from a ramp cannot drift
* that way.
*
* It also exists because a hue's dark steps are not its light steps dimmed.
* Tailwind's purple gets *more* saturated as it darkens (950 is 87% saturation),
@@ -357,19 +358,6 @@
--glass-4: #ffffffc7; /* glass hovered */
--glass-5: #ffffffe0; /* nearly solid */
/* --- Categorical. Named by appearance, deliberately: the
choice IS appearance — telling two projects apart.
Every one is the same step as a family tint, from the same
palette. Before the palette existed these were hand-picked
alongside the families and drifted: tint-purple and the
accent tint matched in light mode and disagreed in dark. */
--tint-blue: var(--palette-blue-3);
--tint-purple: var(--palette-purple-3);
--tint-orange: var(--palette-orange-3);
--tint-green: var(--palette-green-3);
--tint-red: var(--palette-red-3);
--tint-cyan: var(--palette-cyan-3);
/* --- Gradients. Numbered background treatments. ---------- */
--gradient-1:
radial-gradient(at 85% 88%, #eef2d4 0%, transparent 62%),
@@ -443,8 +431,8 @@
/* Every coloured family is absent here on purpose. A family points at a
palette step, and `.dark` already redefines the whole palette — so
`--accent-tint` resolves to the dark purple ramp with nothing restated.
This block used to hold 50 hand-picked values, which is where
`tint-purple` and the accent tint drifted apart. */
This block used to hold 50 hand-picked values, which is where the accent
tint and a hand-picked categorical purple drifted apart. */
--glass-1: #1c1c1c5c;
--glass-2: #1c1c1c85;
@@ -686,13 +674,6 @@
--color-panel-glass: var(--bg-panel-glass);
--color-float-glass: var(--bg-float-glass);
--color-tint-blue: var(--tint-blue);
--color-tint-purple: var(--tint-purple);
--color-tint-orange: var(--tint-orange);
--color-tint-green: var(--tint-green);
--color-tint-red: var(--tint-red);
--color-tint-cyan: var(--tint-cyan);
--spacing-workspace-inset: var(--space-workspace-inset);
--spacing-panel-gap: var(--space-panel-gap);
--spacing-panel-inset: var(--space-panel-inset);

View File

@@ -457,22 +457,6 @@ export const ROLE_GROUPS: RoleGroup[] = [
},
],
},
{
id: "categorical",
name: "Categorical tints",
description:
"The one documented exception to naming colours after their job: telling two projects apart genuinely is a choice about appearance, and pretending otherwise would push people back to raw values.",
roles: (["blue", "purple", "orange", "green", "red", "cyan"] as const).map(
(hue) => ({
token: `bg-tint-${hue}`,
variable: `--tint-${hue}`,
pointsAt: "a literal, by design",
use: `A categorical ${hue} tint for distinguishing one thing from another.`,
status: "core" as TokenStatus,
exception: "Named by appearance because the choice is appearance.",
}),
),
},
];
/* ============================================================
@@ -492,17 +476,6 @@ export const VOCABULARY: Array<{ group: string; words: string[] }> = [
{ group: "identity", words: ["accent", "inverse"] },
{ group: "status", words: ["success", "warning", "danger", "info"] },
{ group: "paired", words: ["on-accent", "on-inverse"] },
{
group: "categorical",
words: [
"tint-blue",
"tint-purple",
"tint-orange",
"tint-green",
"tint-red",
"tint-cyan",
],
},
];
export const GRAMMAR = "<property>-<role>[-<modifier>][-<material>][-<state>]";
@@ -870,8 +843,4 @@ export const EXCEPTIONS = [
name: "gradient-1, texture-dots",
why: "Not colours in the ramp sense.",
},
{
name: "the categorical tints",
why: "Named by appearance because the choice is appearance.",
},
];