diff --git a/desktop-new/AGENTS.md b/desktop-new/AGENTS.md index 98ee74dceb..b47cb7975e 100644 --- a/desktop-new/AGENTS.md +++ b/desktop-new/AGENTS.md @@ -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 diff --git a/desktop-new/DESIGN.md b/desktop-new/DESIGN.md index f1d8241589..ee94f9c9d8 100644 --- a/desktop-new/DESIGN.md +++ b/desktop-new/DESIGN.md @@ -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 diff --git a/desktop-new/scripts/check-color.mjs b/desktop-new/scripts/check-color.mjs index 1c33035b80..9338762f1f 100644 --- a/desktop-new/scripts/check-color.mjs +++ b/desktop-new/scripts/check-color.mjs @@ -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); + } } } diff --git a/desktop-new/src/app/routes/proving-ground.tsx b/desktop-new/src/app/routes/proving-ground.tsx new file mode 100644 index 0000000000..ff103a937a --- /dev/null +++ b/desktop-new/src/app/routes/proving-ground.tsx @@ -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, +}); diff --git a/desktop-new/src/features/proving-ground/data.ts b/desktop-new/src/features/proving-ground/data.ts new file mode 100644 index 0000000000..1780500221 --- /dev/null +++ b/desktop-new/src/features/proving-ground/data.ts @@ -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 = { + healthy: "Healthy", + degraded: "Degraded", + failing: "Failing", + paused: "Paused", +}; diff --git a/desktop-new/src/features/proving-ground/ui/ProvingGroundPage.tsx b/desktop-new/src/features/proving-ground/ui/ProvingGroundPage.tsx new file mode 100644 index 0000000000..e963473588 --- /dev/null +++ b/desktop-new/src/features/proving-ground/ui/ProvingGroundPage.tsx @@ -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("24h"); + const [section, setSection] = useState("overview"); + const [query, setQuery] = useState(""); + + const openIncidents = INCIDENTS.length; + const failing = REGIONS.filter((r) => r.health === "failing").length; + + return ( +
+ + +
+
+
+

Fleet

+

+ Five regions, {openIncidents} open incidents + {failing > 0 ? `, ${failing} failing` : ""}. +

+
+
+ + + + ) : ( + + ) + } + /> + +
+
+ +
+ + + + +
+ +
+
+ +
+ {REGIONS.map((region) => ( +
+
+
+ + {region.name} + + + {region.code} + +
+ +
+ +
+ + + +
+
+ ))} +
+
+ + + View all + + } + > + {INCIDENTS.map((incident) => ( + + + {incident.severity} + +
+ + {incident.title} + + + {incident.id} ·{" "} + {incident.region} · {incident.openedAt} + +
+ {incident.assignee ? ( + + ) : ( + + Unassigned + + )} +
+ ))} +
+
+ +
+ + {DEPLOYS.map((deploy) => ( + +
+ + {deploy.service} + + + {deploy.version} ·{" "} + {deploy.author} · {deploy.at} + +
+ + {DEPLOY_LABEL[deploy.status]} + +
+ ))} +
+ + +
+
+
+ + Progress + + + 2 of 5 regions + +
+ {/* A determinate bar: inset track, accent fill. */} +
+
+
+
+ +
+ + + +
+ +
+ + + +
+
+ +
+
+
+
+ ); +} + +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 ( + + ); +} diff --git a/desktop-new/src/features/proving-ground/ui/parts.tsx b/desktop-new/src/features/proving-ground/ui/parts.tsx new file mode 100644 index 0000000000..66833718e1 --- /dev/null +++ b/desktop-new/src/features/proving-ground/ui/parts.tsx @@ -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 ( + + {children} + + ); +} + +export function HealthPill({ health }: { health: Health }) { + return ( + {HEALTH_LABEL[health]} + ); +} + +/** 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 ( +
+
+
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+ {action} +
+
+ {children} +
+
+ ); +} + +/** + * 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 ( +
+ {label} +
+ {value} + {unit ? ( + {unit} + ) : null} +
+ {change ? ( + + {change} + + ) : null} +
+ ); +} + +/** + * 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 ( + + ); +} + +/** A dense table row. Dividers, edge to edge — never a card per row. */ +export function Row({ + children, + interactive = false, +}: { + children: ReactNode; + interactive?: boolean; +}) { + return ( +
+ {children} +
+ ); +} + +export function KeyValue({ + label, + value, +}: { + label: string; + value: ReactNode; +}) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/desktop-new/src/shared/styles/globals.css b/desktop-new/src/shared/styles/globals.css index 0f91ac28df..fed24b7798 100644 --- a/desktop-new/src/shared/styles/globals.css +++ b/desktop-new/src/shared/styles/globals.css @@ -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"; diff --git a/desktop-new/src/shared/styles/proving-ground.css b/desktop-new/src/shared/styles/proving-ground.css new file mode 100644 index 0000000000..56a25937a8 --- /dev/null +++ b/desktop-new/src/shared/styles/proving-ground.css @@ -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; + } + } +} diff --git a/desktop-new/src/shared/styles/tokens.css b/desktop-new/src/shared/styles/tokens.css index 13194796db..ed83889bcc 100644 --- a/desktop-new/src/shared/styles/tokens.css +++ b/desktop-new/src/shared/styles/tokens.css @@ -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); diff --git a/desktop-new/src/shared/tokens/registry.ts b/desktop-new/src/shared/tokens/registry.ts index 4c726640e8..f58f341e10 100644 --- a/desktop-new/src/shared/tokens/registry.ts +++ b/desktop-new/src/shared/tokens/registry.ts @@ -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 = "-[-][-][-]"; @@ -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.", - }, ];