diff --git a/.github/workflows/design-system.yml b/.github/workflows/design-system.yml
new file mode 100644
index 00000000000..95c821be543
--- /dev/null
+++ b/.github/workflows/design-system.yml
@@ -0,0 +1,19 @@
+name: Design system
+on:
+ pull_request:
+ paths: ['desktop-next/**', 'packages/**', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.github/workflows/design-system.yml']
+ push:
+ branches: [design-system]
+ paths: ['desktop-next/**', 'packages/**', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.github/workflows/design-system.yml']
+permissions:
+ contents: read
+jobs:
+ design-system:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
+ - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616
+ - run: pnpm install --filter buzz-desktop-next... --frozen-lockfile
+ - run: pnpm --filter buzz-desktop-next check
+ - run: pnpm --filter buzz-desktop-next build
diff --git a/desktop-next/AGENTS.md b/desktop-next/AGENTS.md
index 7866fbaa801..c4848ab53c2 100644
--- a/desktop-next/AGENTS.md
+++ b/desktop-next/AGENTS.md
@@ -196,10 +196,8 @@ time as the product repeats something — see DESIGN.md § Components.
Deliberately, so nobody assumes it was forgotten:
-- **Spacing, radius, and motion tokens.** Their `/design` pages state what is
- still to decide rather than pretending to a system. Typography has landed;
- spacing is next.
-- **Any product component.** No Button, no Dialog, no input layer.
+- **Product integration.** Tokens and shared primitives are built before product surfaces.
+- **Relay-backed product workflows.** The shared component catalog uses local fixtures.
- **Tauri.** This is a web app for now; the native shell comes with the app shell.
- **Relay, auth, event handling.** None of it. When it arrives it comes from the
shared Rust crates, not a reimplementation.
@@ -223,3 +221,7 @@ Two rules that matter from day one:
- **Do not create a capability speculatively.** The bar is a durable product
identity and real composition pressure from two surfaces. Building a clean
codebase is not a licence to relax it.
+
+## Portable foundations
+
+Color and typography source live in packages/design-tokens/src; desktop style files import them. The local registry re-exports the package registry. Geometry and motion derive from foundations.ts through build.mjs. Shared components are intentionally built before product adoption; the original repeated-use threshold does not apply to this foundational catalog.
diff --git a/desktop-next/DESIGN.md b/desktop-next/DESIGN.md
index abdbe2c3bf5..1a46d64a2c3 100644
--- a/desktop-next/DESIGN.md
+++ b/desktop-next/DESIGN.md
@@ -24,7 +24,7 @@ Buzz is a place where people build together and bring their agents into the room
## State
-- **The interface has three states, plus disabled where it matters: default, hover, selected.** There is no pressed state: pressed is too fleeting to read and makes an interface feel jumpy.
+- **The interface has three states, plus disabled where it matters: default, hover, selected.** Buttons may use the shared subtle pressed feedback token. Reduced motion removes its scale change; selected remains distinct from pressed.
- **Hover means one step more contrast, in whichever direction that surface needs.** A light row darkens, a dark chip lightens. Direction lives in the value.
- **Selected is a persistent statement, not a stronger hover.** It should be legible without a cursor present.
- **A selected item in a toggle group is not interactive.** Clicking it does nothing, so it gets no hover.
@@ -179,3 +179,7 @@ Every addition lands in `src/shared/tokens/registry.ts` in the same change that
- **Use an existing component before creating one, and an existing role before adding one.**
- **A new visual treatment that repeats belongs in the system, not in the feature.**
- **If a screen looks right but breaks these rules, the rules are probably wrong — say so.** This document is meant to be argued with, not worked around.
+
+## Open-source component foundation
+
+The shared component library is built proactively for the new client. Public Base UI provides behavior; Buzz owns styling, tokens, and named variants. Use Inter, JetBrains Mono, and Lucide. See OPEN_SOURCE.md for the adaptation contract.
diff --git a/desktop-next/OPEN_SOURCE.md b/desktop-next/OPEN_SOURCE.md
new file mode 100644
index 00000000000..ebfe2ed8c75
--- /dev/null
+++ b/desktop-next/OPEN_SOURCE.md
@@ -0,0 +1,19 @@
+# Open-source design system
+
+This implementation is authored for Buzz under the repository Apache-2.0 license. It uses public Base UI behavior primitives, Inter Variable, JetBrains Mono, and Lucide. Their original licenses remain applicable and must accompany redistributed dependencies.
+
+The visual starting point uses neutral surfaces, pill actions, role-based geometry, and clear content hierarchy. No proprietary font files, icon sets, internal screenshots, Figma snapshots, private source files, or internal service clients are included. References informed visual decisions; this is an independently authored implementation, not an export of an internal repository.
+
+## Intentional adaptations
+
+- Inter replaces proprietary sans typography; JetBrains Mono covers code.
+- Lucide provides interface icons. Illustrations and examples are original or synthetic.
+- Buzz conversation type retains its 13/14/15 preference contract and rem-based zoom.
+- Contrast takes priority over exact reference colors.
+- The app backdrop and glass remain Buzz materials; dense reading sits on panels.
+- Components are now built as a public foundation. Press feedback is tokenized and respects reduced motion.
+- CSS and local registries are authoritative. Neither Figma nor any internal service is required to build or run.
+
+## Asset licenses
+
+Inter and JetBrains Mono: SIL Open Font License 1.1. Lucide: ISC, with MIT notices for Feather-derived icons. Base UI: MIT. Preserve the license files distributed with these dependencies. New Buzz source remains Apache-2.0.
diff --git a/desktop-next/package.json b/desktop-next/package.json
index 6e0bdff5cce..f2e4abbb729 100644
--- a/desktop-next/package.json
+++ b/desktop-next/package.json
@@ -5,14 +5,17 @@
"type": "module",
"scripts": {
"dev": "vite",
- "build": "tsc && vite build",
+ "build": "vite build && tsc --noEmit",
"typecheck": "tsc --noEmit",
"preview": "vite preview",
"lint": "biome lint .",
- "check": "biome check . && node scripts/check-type.mjs && node scripts/check-contrast.mjs",
+ "check": "biome check . && node scripts/check-type.mjs && node scripts/check-contrast.mjs && pnpm check:tokens && pnpm test",
"format": "biome format --write .",
"check:type": "node scripts/check-type.mjs",
- "check:contrast": "node scripts/check-contrast.mjs"
+ "check:contrast": "node scripts/check-contrast.mjs",
+ "check:tokens": "node ../packages/design-tokens/build.mjs --check",
+ "tokens:build": "node ../packages/design-tokens/build.mjs",
+ "test": "pnpm --filter @buzz/design-tokens test"
},
"dependencies": {
"@base-ui-components/react": "1.0.0-rc.0",
@@ -20,7 +23,8 @@
"@fontsource/jetbrains-mono": "^5.3.0",
"@tanstack/react-router": "^1.168.10",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "@buzz/design-tokens": "workspace:*"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
diff --git a/desktop-next/scripts/check-contrast.mjs b/desktop-next/scripts/check-contrast.mjs
index 7cd81f171d9..7390715e471 100644
--- a/desktop-next/scripts/check-contrast.mjs
+++ b/desktop-next/scripts/check-contrast.mjs
@@ -22,7 +22,7 @@ import { fileURLToPath } from "node:url";
import { apcaContrast, wcagRatio } from "./apca.mjs";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
-const TOKENS = join(ROOT, "src/shared/styles/tokens.css");
+const TOKENS = join(ROOT, "../packages/design-tokens/src/tokens.css");
/** Body text. Anything a person must read to use the product. */
const TARGET_BODY = 60;
diff --git a/desktop-next/src/features/design-system/ui/MotionPage.tsx b/desktop-next/src/features/design-system/ui/MotionPage.tsx
index 5fb0ae6e0b3..a60df6a0340 100644
--- a/desktop-next/src/features/design-system/ui/MotionPage.tsx
+++ b/desktop-next/src/features/design-system/ui/MotionPage.tsx
@@ -1,40 +1,20 @@
-import { PageHeader, Section, Stub } from "./primitives";
-
+import { FOUNDATION_GROUPS } from "@buzz/design-tokens/foundations";
+import { PageHeader, Section } from "./primitives";
export function MotionPage() {
+ const group = FOUNDATION_GROUPS[2];
return (
<>
-
-
-
-
-
-
-
-
- {[
- "Direct manipulation follows the pointer exactly, with no easing — smoothing during a drag reads as lag. Spring physics belongs to what happens after release.",
- "Never animate blur. Re-blurring a large surface every frame is expensive enough to feel. Animate opacity instead.",
- "Motion explains a change; it does not decorate one. If removing an animation loses no information, remove it.",
- ].map((rule) => (
-
- {rule}
-
+
+
+
+ {group.roles.map(([name, value, use]) => (
+
+
{name}
+
{value}
+
{use}
+
))}
-
+
>
);
diff --git a/desktop-next/src/features/design-system/ui/RadiusPage.tsx b/desktop-next/src/features/design-system/ui/RadiusPage.tsx
index 6888a94dc61..416761bc448 100644
--- a/desktop-next/src/features/design-system/ui/RadiusPage.tsx
+++ b/desktop-next/src/features/design-system/ui/RadiusPage.tsx
@@ -1,23 +1,20 @@
-import { PageHeader, Section, Stub } from "./primitives";
-
+import { FOUNDATION_GROUPS } from "@buzz/design-tokens/foundations";
+import { PageHeader, Section } from "./primitives";
export function RadiusPage() {
+ const group = FOUNDATION_GROUPS[1];
return (
<>
-
-
-
-
+
+
+
+ {group.roles.map(([name, value, use]) => (
+
+
{name}
+
{value}
+
{use}
+
+ ))}
+
>
);
diff --git a/desktop-next/src/features/design-system/ui/SpacingPage.tsx b/desktop-next/src/features/design-system/ui/SpacingPage.tsx
index 88b60263efd..9c88d77f473 100644
--- a/desktop-next/src/features/design-system/ui/SpacingPage.tsx
+++ b/desktop-next/src/features/design-system/ui/SpacingPage.tsx
@@ -1,24 +1,20 @@
-import { PageHeader, Section, Stub } from "./primitives";
-
+import { FOUNDATION_GROUPS } from "@buzz/design-tokens/foundations";
+import { PageHeader, Section } from "./primitives";
export function SpacingPage() {
+ const group = FOUNDATION_GROUPS[0];
return (
<>
-
-
-
-
+
+
+
+ {group.roles.map(([name, value, use]) => (
+
+
{name}
+
{value}
+
{use}
+
+ ))}
+
>
);
diff --git a/desktop-next/src/main.tsx b/desktop-next/src/main.tsx
index 62d89b66cb1..4d0c11ee4bd 100644
--- a/desktop-next/src/main.tsx
+++ b/desktop-next/src/main.tsx
@@ -1,3 +1,5 @@
+import "@fontsource-variable/inter/index.css";
+import "@fontsource/jetbrains-mono/index.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
diff --git a/desktop-next/src/shared/styles/globals.css b/desktop-next/src/shared/styles/globals.css
index ee3c46406c2..810a05eede5 100644
--- a/desktop-next/src/shared/styles/globals.css
+++ b/desktop-next/src/shared/styles/globals.css
@@ -1,8 +1,7 @@
@import "tailwindcss";
-@import "@fontsource-variable/inter";
-@import "@fontsource/jetbrains-mono";
@import "./tokens.css";
@import "./typography.css";
+@import "@buzz/design-tokens/foundations.css";
@layer base {
body {
diff --git a/desktop-next/src/shared/styles/tokens.css b/desktop-next/src/shared/styles/tokens.css
index 50e4176969d..bfff6b80e65 100644
--- a/desktop-next/src/shared/styles/tokens.css
+++ b/desktop-next/src/shared/styles/tokens.css
@@ -1,385 +1 @@
-/*
- * Buzz colour tokens.
- *
- * Three layers, and only the role layer is ever used when building a screen:
- *
- * LAYER 1 private ramps --neutral-*, --accent-*, --glass-*, gradients
- * LAYER 2 public roles --bg-panel, --text-secondary, ...
- * LAYER 3 components bg-panel, text-secondary (Tailwind utilities)
- *
- * Rules:
- * - Literal values exist only in the ramps. Nothing above a ramp holds one,
- * except the documented exceptions at the bottom of this file.
- * - A component never references a ramp step directly.
- * - Transparency is never applied to a token. It lives inside the value.
- * - Every role holds a light and a dark value under the same name.
- *
- * See DESIGN.md for the judgement this vocabulary cannot express.
- */
-
-/* ============================================================
- LAYER 1 — PRIVATE RAMPS
- ============================================================ */
-
-:root {
- /* --- Neutral. Twelve steps, each step a fixed job. -------- */
- --neutral-1: #ffffff; /* app background */
- --neutral-2: #fafafa; /* subtle background */
- --neutral-3: #f5f5f5; /* component background */
- --neutral-4: #f0f0f0; /* component hover */
- --neutral-5: #e8e8e8; /* component selected */
- --neutral-6: #e5e5e5; /* subtle border */
- --neutral-7: #d4d4d4; /* border */
- --neutral-8: #b8b8b8; /* border hover */
- --neutral-9: #909090; /* solid fill, tertiary text */
- --neutral-10: #737373; /* solid fill hover */
- --neutral-11: #262626; /* dark fill */
- --neutral-12: #0a0a0a; /* high-contrast text */
-
- /* --- Accent. A slot, not a colour. -----------------------
- Nothing above this ramp knows the hue. Replace these five
- values to change the accent; every role below follows.
- Reference hue: the blue from the design exploration. */
- --accent-2: #e7f1ff; /* tinted surface */
- --accent-3: #d0e3ff; /* tint hover / selected */
- --accent-8: #3d87ff; /* border, focus ring */
- --accent-9: #237eff; /* solid fill */
- --accent-11: #1d4ed8; /* accent text */
-
- /* --- Status. Same five jobs as accent, per family. -------- */
- --danger-2: #fee2e2;
- --danger-3: #fecaca;
- --danger-8: #f5b5b5;
- --danger-9: #dc2626;
- --danger-11: #a80008;
-
- --success-2: #e8f7ee;
- --success-3: #d1f0dd;
- --success-8: #a3e0bd;
- --success-9: #16a34a;
- --success-11: #15803d;
-
- --warning-2: #fef3c7;
- --warning-3: #fde68a;
- --warning-8: #f2d98a;
- --warning-9: #eab308;
- --warning-11: #9c5e00;
-
- --info-2: #e7f1ff;
- --info-3: #d0e3ff;
- --info-8: #b8d4ff;
- --info-9: #3d87ff;
- --info-11: #1d4ed8;
-
- /* --- Glass. A translucency ramp of fills. ---------------- */
- --glass-1: #ffffff5c; /* barely there */
- --glass-2: #ffffff85; /* quiet glass */
- --glass-3: #ffffff9e; /* default glass */
- --glass-4: #ffffffc7; /* glass hovered */
- --glass-5: #ffffffe0; /* nearly solid */
-
- /* --- Categorical. Named by appearance, deliberately: the
- choice IS appearance — telling two projects apart. ------- */
- --tint-blue: #e7f1ff;
- --tint-purple: #f3e8ff;
- --tint-orange: #ffedd5;
- --tint-green: #ecfccb;
- --tint-red: #fee2e2;
- --tint-cyan: #cffafe;
-
- /* --- Gradients. Numbered background treatments. ---------- */
- --gradient-1:
- radial-gradient(at 85% 88%, #eef2d4 0%, transparent 62%),
- linear-gradient(158deg, #f5f7f8 0%, #dfeaf3 52%, #e9efe0 100%);
-
- /* --- Texture. Separate from colour and gradient. --------- */
- --texture-dots-color: #0a0a0a0f;
- --texture-dots-size: 28px;
-
- /* --- Blur. A separate axis from translucency. ------------ */
- --blur-sm: 8px;
- --blur-md: 16px;
- --blur-lg: 32px;
-
- /* --- Elevation. From the design exploration, unchanged. -- */
- --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
- --shadow-sm: 0 0 2px rgba(0, 0, 0, 0.1), 0 4px 8px -2px rgba(0, 0, 0, 0.06);
-}
-
-.dark {
- --neutral-1: #0f0f0f;
- --neutral-2: #161616;
- --neutral-3: #1c1c1c;
- --neutral-4: #232323;
- --neutral-5: #2a2a2a;
- --neutral-6: #333333;
- --neutral-7: #424242;
- --neutral-8: #5a5a5a;
- /* Steps 9 and 10 are lighter than a linear ramp would put them, because in
- dark mode they carry text and APCA is polarity-asymmetric: light-on-dark
- needs more separation than the WCAG ratio implies. At #6e6e6e and #8f8f8f
- they measured Lc 23–26 and Lc 38–41 against the Lc 60 body-text target
- while passing WCAG AA — legal, and visibly murky. Sized against the
- lightest dark surface (bg-float #2a2a2a), which is the binding case. */
- --neutral-9: #a6a6a6;
- --neutral-10: #c1c1c1;
- --neutral-11: #e5e5e5;
- --neutral-12: #f5f5f5;
-
- /* Accent step 9 barely moves between modes — a saturated fill
- reads correctly on white and on near-black. Step 11 inverts
- direction: darker than the fill on light, lighter on dark. */
- --accent-2: #152845;
- --accent-3: #1a3358;
- --accent-8: #356dbd;
- --accent-9: #237eff;
- --accent-11: #95c0ff;
-
- --danger-2: #3a1515;
- --danger-3: #4d1c1c;
- --danger-8: #7a2e2e;
- --danger-9: #dc2626;
- --danger-11: #fca5a5;
-
- --success-2: #10261a;
- --success-3: #163524;
- --success-8: #2a5c3f;
- --success-9: #16a34a;
- --success-11: #86e0aa;
-
- --warning-2: #2e2410;
- --warning-3: #423318;
- --warning-8: #6b5320;
- --warning-9: #eab308;
- --warning-11: #f5d878;
-
- --info-2: #152845;
- --info-3: #1a3358;
- --info-8: #356dbd;
- --info-9: #3d87ff;
- --info-11: #95c0ff;
-
- --glass-1: #1c1c1c5c;
- --glass-2: #1c1c1c85;
- --glass-3: #1c1c1ca8;
- --glass-4: #1c1c1cc7;
- --glass-5: #1c1c1ce0;
-
- --tint-blue: #152845;
- --tint-purple: #2b1f45;
- --tint-orange: #3a2412;
- --tint-green: #23300f;
- --tint-red: #3a1515;
- --tint-cyan: #0e2e33;
-
- --gradient-1:
- radial-gradient(at 85% 88%, #23261a 0%, transparent 62%),
- linear-gradient(158deg, #15171b 0%, #172231 52%, #191d19 100%);
-
- --texture-dots-color: #ffffff14;
-}
-
-/* ============================================================
- LAYER 2 — PUBLIC ROLES
- The only layer a screen may use. Every value below points at
- a ramp step, except the documented exceptions at the bottom.
- ============================================================ */
-
-:root {
- /* --- Structural surfaces. Named by region. ---------------
- Ask one question: is it behind, on, above, or in? */
- --bg-app: var(--gradient-1);
- --bg-panel: var(--neutral-1);
- --bg-float: var(--neutral-1);
- --bg-inset: var(--neutral-3);
- --bg-hover: var(--neutral-4);
-
- /* --- Emphasis. One ramp shared by text and borders. ------ */
- --text-primary: var(--neutral-12);
- --text-secondary: var(--neutral-10);
- --text-tertiary: var(--neutral-9);
- /* Deliberately below the contrast targets: low contrast is the signal that a
- control is unavailable, and WCAG exempts inactive controls for the same
- reason. Raising it to Lc 60 would make disabled read as enabled. Never put
- information here that a person needs. */
- --text-disabled: var(--neutral-8);
-
- --border-primary: var(--neutral-7);
- --border-secondary: var(--neutral-6);
- --border-tertiary: var(--neutral-5);
-
- /* --- Identity. Two arrangements only: SOLID fill with
- paired text on it, or TINT fill with coloured text. */
- --bg-accent: var(--accent-9);
- --bg-accent-tint: var(--accent-2);
- --bg-accent-tint-hover: var(--accent-3);
- --text-accent: var(--accent-11);
- --border-accent: var(--accent-8);
-
- --bg-inverse: var(--neutral-11);
- --bg-inverse-hover: var(--neutral-10);
-
- /* --- Status. Identical shape, four times over. ----------- */
- --bg-danger: var(--danger-9);
- --bg-danger-tint: var(--danger-2);
- --bg-danger-tint-hover: var(--danger-3);
- --text-danger: var(--danger-11);
- --border-danger: var(--danger-8);
-
- --bg-success: var(--success-9);
- --bg-success-tint: var(--success-2);
- --bg-success-tint-hover: var(--success-3);
- --text-success: var(--success-11);
- --border-success: var(--success-8);
-
- --bg-warning: var(--warning-9);
- --bg-warning-tint: var(--warning-2);
- --bg-warning-tint-hover: var(--warning-3);
- --text-warning: var(--warning-11);
- --border-warning: var(--warning-8);
-
- --bg-info: var(--info-9);
- --bg-info-tint: var(--info-2);
- --bg-info-tint-hover: var(--info-3);
- --text-info: var(--info-11);
- --border-info: var(--info-8);
-
- /* --- Material. Translucency and blur live inside the value.
- A component never assembles them. */
- --bg-chrome-glass: var(--glass-3);
- --bg-chrome-glass-hover: var(--glass-4);
- --bg-chrome-selected: var(--neutral-1);
- --bg-panel-glass: var(--glass-2);
- --bg-float-glass: var(--glass-3);
-
- /* --- Focus. ----------------------------------------------
- Legible on every neutral surface (WCAG 3.0–3.5:1), but it is an accent
- against accent: on `bg-accent` it measures 1.1:1 and disappears. A focused
- primary button therefore needs the ring separated from the fill — an offset
- in the surface colour, not a different ring hue, so one focus treatment
- works everywhere. Whatever component layer introduces focus owns that
- offset; there is no second ring token. */
- --ring-focus: var(--accent-8);
-
- /* ==========================================================
- DELIBERATE EXCEPTIONS — the entire list.
- ==========================================================
-
- 1. Paired text. Computed from its fill's lightness rather
- than fixed, because white is readable on a blue or
- purple fill and unreadable on yellow or lime. Until the
- computation lands, these hold literals. */
- --text-on-accent: #ffffff;
- --text-on-inverse: var(--neutral-1);
-
- /* The status families need their paired text for the same reason accent does:
- `bg-danger` is a saturated fill, and text on it must be the paired value
- rather than whatever it inherits. The registry documented these four before
- they were declared, so `text-on-danger` resolved to nothing and text on a
- danger button fell back to near-black on red. The token table found it.
- Literals for now, on the same exception as text-on-accent — they become
- computed when the lightness rule lands. */
- --text-on-danger: #ffffff;
- --text-on-success: #ffffff;
- /* A literal, not `var(--neutral-12)`: the warning fill is the same amber in
- both modes, so its paired text must be dark in both. neutral-12 flips to
- near-white in dark mode, which put #f5f5f5 on #eab308 — APCA Lc 34, WCAG
- 1.76. A paired-text token follows its fill, never the mode. */
- --text-on-warning: #0a0a0a;
- --text-on-info: #ffffff;
-
- /* 2. The glass rim. A directional light effect, not a solid
- line: bright along the lit edge, dimmer on the opposite
- one, from one fixed light direction every glass surface
- in the product shares. Not on the glass ramp — that is
- a ramp of fills, and in dark mode the fill is
- translucent near-black while the rim stays translucent
- white. Not one of the numbered gradients either: those
- are background treatments, this is a material detail.
-
- Implemented as two inset shadows because CSS borders
- accept only solid colours. See DESIGN.md. */
- --rim-lit: #ffffff73;
- --rim-shade: #ffffff1f;
-}
-
-.dark {
- --bg-panel: var(--neutral-3);
- --bg-float: var(--neutral-5);
- --bg-inset: var(--neutral-2);
- --bg-chrome-selected: var(--neutral-5);
-
- --rim-lit: #ffffff2e;
- --rim-shade: #ffffff0f;
-}
-
-/* ============================================================
- LAYER 3 — TAILWIND REGISTRATION
- `--color-*: initial` deletes Tailwind's default palette, so
- `text-gray-500` does not exist. Only the roles above do.
- ============================================================ */
-
-@theme inline {
- --color-*: initial;
-
- --color-app: var(--bg-app);
- --color-panel: var(--bg-panel);
- --color-float: var(--bg-float);
- --color-inset: var(--bg-inset);
- --color-hover: var(--bg-hover);
-
- --color-primary: var(--text-primary);
- --color-secondary: var(--text-secondary);
- --color-tertiary: var(--text-tertiary);
- --color-disabled: var(--text-disabled);
-
- /* Borders register in their OWN namespace, not `--color-*`.
- Text and borders share the emphasis names — primary, secondary, tertiary —
- but hold different values: text is the dark end of the neutral ramp
- (12/10/9), borders the light end (7/6/5). `--color-*` is a single namespace,
- so registering borders there would make `border-primary` resolve to the text
- colour and draw every hairline at near-black. It did exactly that until
- these three lines existed. */
- --border-color-primary: var(--border-primary);
- --border-color-secondary: var(--border-secondary);
- --border-color-tertiary: var(--border-tertiary);
-
- --color-accent: var(--bg-accent);
- --color-accent-tint: var(--bg-accent-tint);
- --color-accent-tint-hover: var(--bg-accent-tint-hover);
- --color-on-accent: var(--text-on-accent);
-
- --color-inverse: var(--bg-inverse);
- --color-inverse-hover: var(--bg-inverse-hover);
- --color-on-inverse: var(--text-on-inverse);
-
- --color-danger: var(--bg-danger);
- --color-danger-tint: var(--bg-danger-tint);
- --color-danger-tint-hover: var(--bg-danger-tint-hover);
- --color-on-danger: var(--text-on-danger);
- --color-success: var(--bg-success);
- --color-success-tint: var(--bg-success-tint);
- --color-success-tint-hover: var(--bg-success-tint-hover);
- --color-on-success: var(--text-on-success);
- --color-on-warning: var(--text-on-warning);
- --color-on-info: var(--text-on-info);
- --color-warning: var(--bg-warning);
- --color-warning-tint: var(--bg-warning-tint);
- --color-warning-tint-hover: var(--bg-warning-tint-hover);
- --color-info: var(--bg-info);
- --color-info-tint: var(--bg-info-tint);
- --color-info-tint-hover: var(--bg-info-tint-hover);
-
- --color-chrome-glass: var(--bg-chrome-glass);
- --color-chrome-glass-hover: var(--bg-chrome-glass-hover);
- --color-chrome-selected: var(--bg-chrome-selected);
- --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);
-}
+@import "@buzz/design-tokens/tokens.css";
diff --git a/desktop-next/src/shared/styles/typography.css b/desktop-next/src/shared/styles/typography.css
index 0b7370a73d3..bfbb2a7e003 100644
--- a/desktop-next/src/shared/styles/typography.css
+++ b/desktop-next/src/shared/styles/typography.css
@@ -1,231 +1 @@
-/*
- * Buzz typography tokens.
- *
- * Same three layers as colour, and only the role layer is ever used when
- * building a screen:
- *
- * LAYER 1 private ramps --type-size-*, --type-leading-*, --type-tracking-*
- * LAYER 2 public roles --text-body, --text-title, ...
- * LAYER 3 components text-body, text-title (Tailwind utilities)
- *
- * Rules, matching the colour system:
- * - Literal sizes exist only in the ramp. A role points at a step; a component
- * points at a role. No component holds a size.
- * - A role carries its whole typographic setting — size, line height, letter
- * spacing, weight — because those four are one decision, not four. Tailwind
- * v4 supports this via the `--text---` convention, so
- * `text-body` alone produces correctly set text.
- * - Every size is relative. Nothing here may be expressed in px: fixed pixel
- * text freezes against zoom and breaks the person's font-size preference.
- * This is the one rule the existing client already learned the hard way.
- *
- * Naming note: colour roles are also spelled `--text-*` (`--text-primary`) but
- * are registered in Tailwind's `--color-*` namespace, while sizes register in
- * `--text-*`. So `text-primary` is a colour and `text-body` is a setting — the
- * same split Tailwind itself makes. Size roles are named for editorial jobs
- * (body, title, caption) and colour roles for emphasis levels (primary,
- * secondary), so no one name ever means both.
- *
- * See DESIGN.md for the judgement these values cannot express.
- */
-
-/* ============================================================
- LAYER 1 — PRIVATE RAMPS
- ============================================================ */
-
-:root {
- /* --- The zoom and preference dials. -----------------------
- Two independent controls compose here, as in the existing client:
-
- - Cmd +/- scales the real root font size, so every rem in the app — text,
- gaps, radii — grows together. True zoom.
- - The font-size preference sets --type-scale, nudging only the type ramp
- below, so text changes size without moving the layout.
-
- Every size derives from this virtual rem, which is itself rem-relative and
- therefore rides on top of zoom automatically. At the default preference it
- equals 1rem, making --type-size-4 read as 14px. */
- --type-scale: 1;
- --type-rem: calc(1rem * var(--type-scale));
-
- /* --- Size. Nine steps, each step an editorial job. --------
- A modern ramp for a dense product: tight and frequent through the working
- sizes where real interface text lives, opening up at the display end where
- a step needs to be a visible jump rather than a near-miss. */
- --type-size-1: calc(var(--type-rem) * 0.6875); /* 11px meta */
- --type-size-2: calc(var(--type-rem) * 0.75); /* 12px caption */
- --type-size-3: calc(var(--type-rem) * 0.8125); /* 13px label */
- --type-size-4: calc(var(--type-rem) * 0.875); /* 14px body */
- --type-size-5: calc(var(--type-rem) * 1); /* 16px body large */
- --type-size-6: calc(var(--type-rem) * 1.125); /* 18px subheading */
- --type-size-7: calc(var(--type-rem) * 1.375); /* 22px heading */
- --type-size-8: calc(var(--type-rem) * 1.75); /* 28px title */
- --type-size-9: calc(var(--type-rem) * 2.25); /* 36px display */
-
- /* --- Line height. Unitless, so it scales with its own size.
- Tight for display text, generous for anything read in quantity, and exactly
- 1 for single-line meta so a timestamp cannot enlarge its own row. */
- --type-leading-flat: 1;
- --type-leading-tight: 1.2;
- --type-leading-snug: 1.35;
- --type-leading-normal: 1.5;
- --type-leading-relaxed: 1.6;
-
- /* --- Tracking. An optical correction, not a style. --------
- Inter is drawn for interface text and needs negative tracking as it grows:
- spacing that looks correct at 14px looks loose at 36px. The ramp mirrors
- the size ramp so the correction is applied per step rather than guessed,
- and turns slightly positive at meta sizes where letters need air to stay
- legible. */
- --type-tracking-display: -0.022em;
- --type-tracking-title: -0.019em;
- --type-tracking-heading: -0.017em;
- --type-tracking-subheading: -0.014em;
- --type-tracking-large: -0.011em;
- --type-tracking-body: -0.006em;
- --type-tracking-label: -0.003em;
- --type-tracking-caption: 0em;
- --type-tracking-meta: 0.005em;
-
- /* --- Weight. Four, and the gaps are deliberate. -----------
- Inter Variable can render any weight, which is exactly why the system names
- only these: 500 and 600 sit far enough apart to read as intent rather than
- as a rendering artefact. */
- --type-weight-normal: 400;
- --type-weight-medium: 500;
- --type-weight-semibold: 600;
- --type-weight-bold: 700;
-}
-
-/* Conversation type contract, carried over from the existing client:
- 13 / 14 / 15px before keyboard zoom. */
-:root[data-font-size="smaller"] {
- --type-scale: calc(13 / 14);
-}
-
-:root[data-font-size="larger"] {
- --type-scale: calc(15 / 14);
-}
-
-/* ============================================================
- LAYER 3 — PUBLIC ROLES, REGISTERED WITH TAILWIND
-
- Layers 2 and 3 are one block for typography. A colour role can be a bare
- custom property because a component reads it through `bg-*` or `text-*`; a
- type role has to be a Tailwind theme key or the paired line height, tracking,
- and weight cannot travel with the size. So the role IS the registration.
-
- Named for the job the text does, never for its size — `text-title`, not
- `text-28`. A size name is a value in disguise and goes stale the moment the
- ramp moves.
- ============================================================ */
-
-@theme {
- --font-sans:
- "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
- --font-mono:
- "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
-
- /* --- Display. One per page at most, and many pages need none.
- The only step that is allowed to be decorative. */
- --text-display: var(--type-size-9);
- --text-display--line-height: var(--type-leading-tight);
- --text-display--letter-spacing: var(--type-tracking-display);
- --text-display--font-weight: var(--type-weight-semibold);
-
- /* --- Title. The name of the thing you are looking at. ----- */
- --text-title: var(--type-size-8);
- --text-title--line-height: var(--type-leading-tight);
- --text-title--letter-spacing: var(--type-tracking-title);
- --text-title--font-weight: var(--type-weight-semibold);
-
- /* --- Heading. A section within a page. ------------------- */
- --text-heading: var(--type-size-7);
- --text-heading--line-height: var(--type-leading-snug);
- --text-heading--letter-spacing: var(--type-tracking-heading);
- --text-heading--font-weight: var(--type-weight-semibold);
-
- /* --- Subheading. A group within a section. --------------- */
- --text-subheading: var(--type-size-6);
- --text-subheading--line-height: var(--type-leading-snug);
- --text-subheading--letter-spacing: var(--type-tracking-subheading);
- --text-subheading--font-weight: var(--type-weight-semibold);
-
- /* --- Body large. Introductions and reading columns, where a
- paragraph is the point rather than a description of something else. */
- --text-body-lg: var(--type-size-5);
- --text-body-lg--line-height: var(--type-leading-relaxed);
- --text-body-lg--letter-spacing: var(--type-tracking-large);
- --text-body-lg--font-weight: var(--type-weight-normal);
-
- /* --- Body. The default. If you are unsure, this is it. --- */
- --text-body: var(--type-size-4);
- --text-body--line-height: var(--type-leading-normal);
- --text-body--letter-spacing: var(--type-tracking-body);
- --text-body--font-weight: var(--type-weight-normal);
-
- /* --- Label. Interface text that is acted on rather than
- read: buttons, tabs, menu items, form labels, table headers. Medium weight
- because a label competes with content around it and needs to hold its edge
- without going up a size. */
- --text-label: var(--type-size-3);
- --text-label--line-height: var(--type-leading-snug);
- --text-label--letter-spacing: var(--type-tracking-label);
- --text-label--font-weight: var(--type-weight-medium);
-
- /* --- Caption. A sentence about something else: help text,
- descriptions, empty-state detail. */
- --text-caption: var(--type-size-2);
- --text-caption--line-height: var(--type-leading-normal);
- --text-caption--letter-spacing: var(--type-tracking-caption);
- --text-caption--font-weight: var(--type-weight-normal);
-
- /* --- Meta. Timestamps, counts, tracking numbers. Flat line
- height so a single-line value cannot grow its own row. */
- --text-meta: var(--type-size-1);
- --text-meta--line-height: var(--type-leading-flat);
- --text-meta--letter-spacing: var(--type-tracking-meta);
- --text-meta--font-weight: var(--type-weight-medium);
-
- /* --- Code. Mono, one step down: at equal size a monospace
- face reads larger than Inter and pulls the eye off the sentence. */
- --text-code: var(--type-size-3);
- --text-code--line-height: var(--type-leading-normal);
- --text-code--letter-spacing: var(--type-tracking-caption);
- --text-code--font-weight: var(--type-weight-normal);
-}
-
-/* ============================================================
- BASE
- ============================================================ */
-
-@layer base {
- html {
- font-family: var(--font-sans);
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
-
- body {
- /* Every readable surface starts from the body role and steps away from it
- deliberately. */
- font-size: var(--text-body);
- line-height: var(--text-body--line-height);
- letter-spacing: var(--text-body--letter-spacing);
-
- /* Never synthesise a weight Inter Variable can render properly, and never
- let a font fall back mid-string. */
- font-synthesis-weight: none;
- text-rendering: optimizeLegibility;
- }
-
- code,
- kbd,
- pre,
- samp {
- font-family: var(--font-mono);
- /* Mono digits and letters share one advance width, so a changing value does
- not shift the text around it. */
- font-variant-numeric: tabular-nums;
- }
-}
+@import "@buzz/design-tokens/typography.css";
diff --git a/desktop-next/src/shared/tokens/registry.ts b/desktop-next/src/shared/tokens/registry.ts
index e7a46efc8fa..3126214ee89 100644
--- a/desktop-next/src/shared/tokens/registry.ts
+++ b/desktop-next/src/shared/tokens/registry.ts
@@ -1,688 +1 @@
-/**
- * The token registry.
- *
- * This is the machine-readable description of the colour system: what exists,
- * what each name is for, what it points at, and whether it is core or proposed.
- * The design system pages render from this, so a token added here appears in the
- * documentation automatically and the docs cannot drift from the system.
- *
- * Adding to this file is a normal, unreviewed action — see the growth procedure
- * in DESIGN.md. Every entry needs a `use` sentence and, if proposed, an `owner`.
- */
-
-/** Whether a token is part of the vetted system or someone's addition. */
-export type TokenStatus = "core" | "proposed" | "deprecated";
-
-/** A private ramp step. Components never reference these. */
-export interface RampStep {
- /** Position on the ramp. The number means a job, not a brightness. */
- step: number;
- /** What this step is for. */
- job: string;
- /** The CSS custom property, without `var()`. */
- variable: string;
-}
-
-/** A private ramp. */
-export interface Ramp {
- id: string;
- name: string;
- /** Why this ramp exists and how it behaves. */
- description: string;
- steps: RampStep[];
- /** Rendered over the app backdrop so translucency is visible. */
- translucent?: boolean;
-}
-
-/** A public role. The only layer a screen may use. */
-export interface Role {
- /** The Tailwind class, e.g. `bg-panel`. */
- token: string;
- /** The CSS custom property backing it. */
- variable: string;
- /** What it points at, for display: `neutral 1`, or a note for exceptions. */
- pointsAt: string;
- /** One sentence: when to use this. */
- use: string;
- status: TokenStatus;
- /** Required when status is `proposed`. */
- owner?: string;
- /** Set when the value is a literal rather than a ramp reference. */
- exception?: string;
-}
-
-export interface RoleGroup {
- id: string;
- name: string;
- description: string;
- roles: Role[];
-}
-
-/* ============================================================
- PRIVATE RAMPS
- ============================================================ */
-
-const NEUTRAL_JOBS = [
- "app background",
- "subtle background",
- "component background",
- "component hover",
- "component selected",
- "subtle border",
- "border",
- "border hover",
- "solid fill",
- "solid fill hover",
- "dark fill",
- "high-contrast text",
-];
-
-/** The five accent steps any role points at. The rest exist so the ramp is
- * complete and interpolation stays even. */
-const IDENTITY_STEPS: Array<[number, string]> = [
- [2, "tinted surface"],
- [3, "tint hover / selected"],
- [8, "border, focus ring"],
- [9, "solid fill"],
- [11, "accent text"],
-];
-
-function identityRamp(id: string, name: string, description: string): Ramp {
- return {
- id,
- name,
- description,
- steps: IDENTITY_STEPS.map(([step, job]) => ({
- step,
- job,
- variable: `--${id}-${step}`,
- })),
- };
-}
-
-export const RAMPS: Ramp[] = [
- {
- id: "neutral",
- name: "Neutral",
- description:
- "Twelve steps, each step a fixed job. The number means a job, not a brightness — which is what lets a role encode a position that stays true across every palette.",
- steps: NEUTRAL_JOBS.map((job, i) => ({
- step: i + 1,
- job,
- variable: `--neutral-${i + 1}`,
- })),
- },
- identityRamp(
- "accent",
- "Accent — a slot, not a colour",
- "Nothing above this ramp knows the hue, so the accent can change, become a person's preference, or vary per theme without a single component changing. Step 9 barely moves between modes because a saturated fill reads on white and on near-black; step 11 inverts direction to stay readable.",
- ),
- identityRamp(
- "danger",
- "Danger",
- "The same five jobs as accent. Learning one family teaches all of them.",
- ),
- identityRamp("success", "Success", "The same five jobs as accent."),
- identityRamp("warning", "Warning", "The same five jobs as accent."),
- identityRamp("info", "Info", "The same five jobs as accent."),
- {
- id: "glass",
- name: "Glass",
- description:
- "A translucency ramp of fills. Each step is the mode's surface colour at an increasing opacity, which is what lets a hover move one step up the ramp instead of holding its own literal. Fills only — a glass surface's bright rim is a border, and it is a documented exception.",
- translucent: true,
- steps: [
- { step: 1, job: "barely there", variable: "--glass-1" },
- { step: 2, job: "quiet glass", variable: "--glass-2" },
- { step: 3, job: "default glass", variable: "--glass-3" },
- { step: 4, job: "glass hovered", variable: "--glass-4" },
- { step: 5, job: "nearly solid", variable: "--glass-5" },
- ],
- },
-];
-
-/* ============================================================
- PUBLIC ROLES
- ============================================================ */
-
-function identityGroup(
- id: string,
- label: string,
- description: string,
-): RoleGroup {
- return {
- id,
- name: label,
- description,
- roles: [
- {
- token: `bg-${id}`,
- variable: `--bg-${id}`,
- pointsAt: `${id} 9`,
- use: "The solid fill: primary buttons, active toggles. Takes its paired text.",
- status: "core",
- },
- {
- token: `text-on-${id}`,
- variable: `--text-on-${id}`,
- pointsAt: "computed from the fill's lightness",
- use: `Text sitting on bg-${id}.`,
- status: "core",
- exception:
- "Computed rather than fixed: white is readable on a blue or purple fill and unreadable on yellow or lime.",
- },
- {
- token: `text-${id}`,
- variable: `--text-${id}`,
- pointsAt: `${id} 11`,
- use: "Coloured text on a neutral background: links, labels.",
- status: "core",
- },
- {
- token: `bg-${id}-tint`,
- variable: `--bg-${id}-tint`,
- pointsAt: `${id} 2`,
- use: "A tinted surface carrying meaning: chips, callouts, selected rows. Takes coloured text.",
- status: "core",
- },
- {
- token: `bg-${id}-tint-hover`,
- variable: `--bg-${id}-tint-hover`,
- pointsAt: `${id} 3`,
- use: "That tinted surface hovered or selected.",
- status: "core",
- },
- {
- token: `border-${id}`,
- variable: `--border-${id}`,
- pointsAt: `${id} 8`,
- use: "Focus rings and active borders.",
- status: "core",
- },
- ],
- };
-}
-
-export const ROLE_GROUPS: RoleGroup[] = [
- {
- id: "surfaces",
- name: "Structural surfaces",
- description:
- "Named and closed, because there are only a few right answers and guessing produces a broken interface. Ask one question: is it behind, on, above, or in?",
- roles: [
- {
- token: "bg-app",
- variable: "--bg-app",
- pointsAt: "gradient-1",
- use: "The backdrop everything sits on.",
- status: "core",
- },
- {
- token: "bg-panel",
- variable: "--bg-panel",
- pointsAt: "neutral 1 light / neutral 3 dark",
- use: "Everything sitting on the backdrop: the navigation column, content, timelines, cards, rows.",
- status: "core",
- },
- {
- token: "bg-float",
- variable: "--bg-float",
- pointsAt: "neutral 1 light / neutral 5 dark",
- use: "Anything hovering above the page: menus, dialogs, tooltips, toasts. Shares a light value with bg-panel and diverges in dark, because a shadow cannot carry elevation on a near-black background.",
- status: "core",
- },
- {
- token: "bg-inset",
- variable: "--bg-inset",
- pointsAt: "neutral 3 light / neutral 2 dark",
- use: "Anything pushed in: inputs, code blocks, quotes.",
- status: "core",
- },
- {
- token: "bg-hover",
- variable: "--bg-hover",
- pointsAt: "neutral 4",
- use: "Any neutral row or item under the cursor. Region-less — it works on any neutral surface, which is why there is only one of them.",
- status: "core",
- },
- ],
- },
- {
- id: "emphasis",
- name: "Emphasis",
- description:
- "One three-level ramp shared by text and borders: normal, lesser, really lesser. States that are not levels of emphasis get their own names rather than extending the ramp.",
- roles: [
- {
- token: "text-primary",
- variable: "--text-primary",
- pointsAt: "neutral 12",
- use: "Normal reading text.",
- status: "core",
- },
- {
- token: "text-secondary",
- variable: "--text-secondary",
- pointsAt: "neutral 10",
- use: "Supporting text: author names, labels.",
- status: "core",
- },
- {
- token: "text-tertiary",
- variable: "--text-tertiary",
- pointsAt: "neutral 9",
- use: "Metadata: timestamps, counts, hints.",
- status: "core",
- },
- {
- token: "text-disabled",
- variable: "--text-disabled",
- pointsAt: "neutral 8",
- use: "Unavailable. A state, not a fourth level of the ramp.",
- status: "core",
- },
- {
- token: "border-primary",
- variable: "--border-primary",
- pointsAt: "neutral 7",
- use: "The default visible line.",
- status: "core",
- },
- {
- token: "border-secondary",
- variable: "--border-secondary",
- pointsAt: "neutral 6",
- use: "A lighter separator inside a group.",
- status: "core",
- },
- {
- token: "border-tertiary",
- variable: "--border-tertiary",
- pointsAt: "neutral 5",
- use: "The faintest line.",
- status: "core",
- },
- ],
- },
- identityGroup(
- "accent",
- "Accent",
- "Two arrangements and no third: a solid fill with paired text on it, or a tint fill with coloured text on it. There is deliberately nothing between them.",
- ),
- {
- id: "inverse",
- name: "Inverse",
- description:
- "A high-contrast fill for surfaces that must stand apart from everything: tooltips, toasts.",
- roles: [
- {
- token: "bg-inverse",
- variable: "--bg-inverse",
- pointsAt: "neutral 11",
- use: "High-contrast fill: tooltips, toasts.",
- status: "core",
- },
- {
- token: "bg-inverse-hover",
- variable: "--bg-inverse-hover",
- pointsAt: "neutral 10",
- use: "Its hover. Step 10 sits on the light side of step 11 in the light ramp and the dark side in the dark ramp, so the hover inverts direction with no special-casing.",
- status: "core",
- },
- {
- token: "text-on-inverse",
- variable: "--text-on-inverse",
- pointsAt: "computed from the fill's lightness",
- use: "Text sitting on bg-inverse.",
- status: "core",
- exception: "Computed rather than fixed.",
- },
- ],
- },
- identityGroup("danger", "Danger", "Destructive actions and error states."),
- identityGroup("success", "Success", "Confirmation and healthy states."),
- identityGroup("warning", "Warning", "Caution that is not yet an error."),
- identityGroup("info", "Info", "Neutral information and guidance."),
- {
- id: "material",
- name: "Material",
- description:
- "A translucent blurred surface is the same region in a different material. Translucency and blur live inside the value; a component never assembles them from a fill, a transparency, and a blur amount.",
- roles: [
- {
- token: "bg-chrome-glass",
- variable: "--bg-chrome-glass",
- pointsAt: "glass 3 + blur-md",
- use: "The top bar and floating chrome.",
- status: "core",
- },
- {
- token: "bg-chrome-glass-hover",
- variable: "--bg-chrome-glass-hover",
- pointsAt: "glass 4 + blur-md",
- use: "A chrome control under the cursor. A glass hover moves one step up the ramp, changing opacity and never blur.",
- status: "core",
- },
- {
- token: "bg-chrome-selected",
- variable: "--bg-chrome-selected",
- pointsAt: "neutral 1 light / neutral 5 dark",
- use: "The selected item inside chrome. Carries no material suffix because omitting it means opaque — on glass, elevation reads as less translucency, not a lighter colour.",
- status: "core",
- },
- {
- token: "bg-panel-glass",
- variable: "--bg-panel-glass",
- pointsAt: "glass 2 + blur-lg",
- use: "A panel that should let the backdrop through.",
- status: "core",
- },
- {
- token: "bg-float-glass",
- variable: "--bg-float-glass",
- pointsAt: "glass 3 + blur-md",
- use: "A floating surface that should let content through.",
- status: "core",
- },
- ],
- },
- {
- id: "focus",
- name: "Focus",
- description: "Focus is part of the design, never an artefact to suppress.",
- roles: [
- {
- token: "ring-focus",
- variable: "--ring-focus",
- pointsAt: "accent 8",
- use: "The keyboard focus ring.",
- status: "core",
- },
- ],
- },
- {
- 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.",
- }),
- ),
- },
-];
-
-/* ============================================================
- THE VOCABULARY
- Every token in the system is built from these words. A new name
- combining existing words is routine; a new WORD is what the
- audit reports on its own line.
- ============================================================ */
-
-export const VOCABULARY: Array<{ group: string; words: string[] }> = [
- { group: "property", words: ["bg", "text", "border", "ring"] },
- { group: "region", words: ["app", "panel", "float", "chrome", "inset"] },
- { group: "emphasis", words: ["primary", "secondary", "tertiary"] },
- { group: "state", words: ["hover", "selected", "disabled"] },
- { group: "material", words: ["glass"] },
- { group: "modifier", words: ["tint"] },
- { 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 = "-[-][-][-]";
-
-/** Fixed order, so there is only one correct spelling. */
-export const GRAMMAR_EXAMPLES = {
- legal: ["bg-chrome-glass-hover", "bg-accent-tint-hover", "text-secondary"],
- illegal: ["bg-chrome-hover-glass", "bg-hover-chrome"],
-};
-
-/** Runs per change, by whoever needs the value. Nothing here needs permission. */
-export const GROWTH_PROCEDURE = [
- "Search the role list by intent, not by colour.",
- "A state of an existing role — add the -hover, -selected, or -disabled sibling with both values.",
- "A material variant of an existing role — add the -glass sibling with both values and its blur token.",
- "A new role using existing words — add the name, both values, a one-sentence description, and an owner.",
- "A new hue — generate its ramp, add roles pointing at steps. Never a literal.",
- "A new vocabulary word — allowed, but it is the thing the audit reports on its own line, so use an existing word if one fits.",
- "Never write a raw value. If nothing above applies, say so rather than reaching for a literal.",
-];
-
-/* ============================================================
- TYPOGRAPHY
- ============================================================ */
-
-/** A public type role. Carries its whole setting, because size, line height,
- * tracking, and weight are one decision rather than four. */
-export interface TypeRole {
- /** The Tailwind class, e.g. `text-body`. */
- token: string;
- /** Step on the size ramp this points at, for display. */
- pointsAt: string;
- /** Rendered size at the default preference and zoom, for display only —
- * never a value a component may use. */
- size: string;
- lineHeight: string;
- tracking: string;
- weight: string;
- /** One sentence: when to use this. */
- use: string;
- status: TokenStatus;
- /** Set when the role renders in the mono face. */
- mono?: boolean;
-}
-
-/** Ordered loudest to quietest, which is also the order they are chosen in. */
-export const TYPE_ROLES: TypeRole[] = [
- {
- token: "text-display",
- pointsAt: "size 9",
- size: "36px",
- lineHeight: "1.2",
- tracking: "-0.022em",
- weight: "600",
- use: "One per page at most, and many pages need none. The only step allowed to be decorative.",
- status: "core",
- },
- {
- token: "text-title",
- pointsAt: "size 8",
- size: "28px",
- lineHeight: "1.2",
- tracking: "-0.019em",
- weight: "600",
- use: "The name of the thing you are looking at.",
- status: "core",
- },
- {
- token: "text-heading",
- pointsAt: "size 7",
- size: "22px",
- lineHeight: "1.35",
- tracking: "-0.017em",
- weight: "600",
- use: "A section within a page.",
- status: "core",
- },
- {
- token: "text-subheading",
- pointsAt: "size 6",
- size: "18px",
- lineHeight: "1.35",
- tracking: "-0.014em",
- weight: "600",
- use: "A group within a section.",
- status: "core",
- },
- {
- token: "text-body-lg",
- pointsAt: "size 5",
- size: "16px",
- lineHeight: "1.6",
- tracking: "-0.011em",
- weight: "400",
- use: "Introductions and reading columns, where a paragraph is the point rather than a description of something else.",
- status: "core",
- },
- {
- token: "text-body",
- pointsAt: "size 4",
- size: "14px",
- lineHeight: "1.5",
- tracking: "-0.006em",
- weight: "400",
- use: "The default. If you are unsure, this is it.",
- status: "core",
- },
- {
- token: "text-label",
- pointsAt: "size 3",
- size: "13px",
- lineHeight: "1.35",
- tracking: "-0.003em",
- weight: "500",
- use: "Interface text that is acted on rather than read: buttons, tabs, menu items, form labels, table headers.",
- status: "core",
- },
- {
- token: "text-caption",
- pointsAt: "size 2",
- size: "12px",
- lineHeight: "1.5",
- tracking: "0em",
- weight: "400",
- use: "A sentence about something else: help text, descriptions, empty-state detail.",
- status: "core",
- },
- {
- token: "text-meta",
- pointsAt: "size 1",
- size: "11px",
- lineHeight: "1",
- tracking: "0.005em",
- weight: "500",
- use: "Timestamps, counts, tracking labels. Flat line height so a single-line value cannot grow its own row.",
- status: "core",
- },
- {
- token: "text-code",
- pointsAt: "size 3",
- size: "13px",
- lineHeight: "1.5",
- tracking: "0em",
- weight: "400",
- use: "Inline code and code blocks. One step below body, because at equal size a monospace face reads larger than Inter and pulls the eye off the sentence.",
- status: "core",
- mono: true,
- },
-];
-
-/** The two faces. Both already shipped in every current Buzz client. */
-export const TYPE_FAMILIES = [
- {
- token: "font-sans",
- name: "Inter Variable",
- use: "Everything. Drawn for interface text at small sizes, and already the sans in desktop, web, and mobile.",
- },
- {
- token: "font-mono",
- name: "JetBrains Mono",
- use: "Code, keys, and identifiers. Already the mono in the existing client's terminal.",
- },
-];
-
-/** The private ramps a type role points at. Components never reference these. */
-export const TYPE_RAMPS = [
- {
- id: "size",
- name: "Size",
- description:
- "Nine steps, each step an editorial job. Tight and frequent through the working sizes where real interface text lives, opening up at the display end where a step needs to be a visible jump rather than a near-miss. Every value derives from a virtual rem, so the whole ramp follows the person's font-size preference and keyboard zoom.",
- steps: [
- { step: 1, job: "meta", value: "11px" },
- { step: 2, job: "caption", value: "12px" },
- { step: 3, job: "label, code", value: "13px" },
- { step: 4, job: "body", value: "14px" },
- { step: 5, job: "body large", value: "16px" },
- { step: 6, job: "subheading", value: "18px" },
- { step: 7, job: "heading", value: "22px" },
- { step: 8, job: "title", value: "28px" },
- { step: 9, job: "display", value: "36px" },
- ],
- },
- {
- id: "tracking",
- name: "Tracking",
- description:
- "An optical correction, not a style. Inter needs progressively tighter spacing as it grows — tracking that looks correct at 14px looks loose at 36px — so the ramp mirrors the size ramp and the correction is applied per step rather than guessed. It turns slightly positive at meta sizes, where letters need air to stay legible.",
- steps: [
- { step: 9, job: "display", value: "-0.022em" },
- { step: 8, job: "title", value: "-0.019em" },
- { step: 7, job: "heading", value: "-0.017em" },
- { step: 6, job: "subheading", value: "-0.014em" },
- { step: 5, job: "body large", value: "-0.011em" },
- { step: 4, job: "body", value: "-0.006em" },
- { step: 3, job: "label", value: "-0.003em" },
- { step: 2, job: "caption", value: "0em" },
- { step: 1, job: "meta", value: "0.005em" },
- ],
- },
-];
-
-export const ELEVATION = [
- {
- token: "shadow-xs",
- variable: "--shadow-xs",
- use: "The default lift: a selected pill, a small raised control.",
- },
- {
- token: "shadow-sm",
- variable: "--shadow-sm",
- use: "A floating surface: menus, dialogs, popovers.",
- },
-];
-
-export const BLUR = [
- { token: "blur-sm", variable: "--blur-sm", value: "8px" },
- { token: "blur-md", variable: "--blur-md", value: "16px" },
- { token: "blur-lg", variable: "--blur-lg", value: "32px" },
-];
-
-/** The entire exception list. Everything else points at a ramp step. */
-export const EXCEPTIONS = [
- {
- name: "text-on-accent, text-on-inverse, and the four status pairings",
- why: "Computed from their fill's lightness rather than fixed, because white is readable on a blue or purple fill and unreadable on yellow or lime. This is what keeps a free choice of accent hue from becoming a contrast lottery.",
- },
- {
- name: "--rim-lit, --rim-shade",
- why: "The glass rim is a directional light effect, not a solid line. It is not on the glass ramp — that is a ramp of fills, and in dark mode the fill is translucent near-black while the rim stays translucent white. It is not one of the numbered gradients either: those are background treatments, this is a material detail.",
- },
- {
- 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.",
- },
-];
+export * from "@buzz/design-tokens/registry";
diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md
new file mode 100644
index 00000000000..2bca927e4d3
--- /dev/null
+++ b/packages/design-tokens/README.md
@@ -0,0 +1,7 @@
+# Buzz design tokens
+
+Portable, locally owned design foundations. Color and typography CSS hold the authored values; the role registry describes their purpose. Geometry and motion CSS are generated from foundations.ts. Consumers use public roles, never private ramps.
+
+Import the CSS exports with Tailwind v4. The UI package will also provide compiled CSS for applications without Tailwind. Switch color scheme with a dark class on the root. Fonts are supplied by the consuming application; the reference uses Inter Variable and JetBrains Mono from public packages.
+
+Run the repository-pinned Node against build.mjs to regenerate foundation CSS; --check detects drift.
diff --git a/packages/design-tokens/build.mjs b/packages/design-tokens/build.mjs
new file mode 100644
index 00000000000..d11d089dc37
--- /dev/null
+++ b/packages/design-tokens/build.mjs
@@ -0,0 +1,14 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { FOUNDATION_GROUPS } from "./src/foundations.ts";
+const declarations = FOUNDATION_GROUPS.flatMap((g) =>
+ g.roles.map(([name, value]) => " --" + name + ": " + value + ";"),
+).join("\n");
+const css =
+ "/* Generated by packages/design-tokens/build.mjs. */\n@theme {\n" +
+ declarations +
+ "\n}\n@media (prefers-reduced-motion: reduce) {\n :root { --duration-state: 0ms; --duration-enter: 0ms; --duration-exit: 0ms; --press-scale: 1; }\n}\n";
+const path = new URL("./src/foundations.css", import.meta.url);
+if (process.argv.includes("--check")) {
+ if (readFileSync(path, "utf8") !== css)
+ throw new Error("Foundation CSS is stale: run pnpm tokens:build");
+} else writeFileSync(path, css);
diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json
new file mode 100644
index 00000000000..05c79a295fd
--- /dev/null
+++ b/packages/design-tokens/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@buzz/design-tokens",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "Apache-2.0",
+ "exports": {
+ "./tokens.css": "./src/tokens.css",
+ "./typography.css": "./src/typography.css",
+ "./foundations.css": "./src/foundations.css",
+ "./registry": "./src/registry.ts",
+ "./foundations": "./src/foundations.ts"
+ },
+ "files": [
+ "src",
+ "README.md"
+ ],
+ "devDependencies": {
+ "tailwindcss": "4.3.0"
+ },
+ "scripts": {
+ "test": "node --test test/*.test.mjs"
+ }
+}
diff --git a/packages/design-tokens/src/foundations.css b/packages/design-tokens/src/foundations.css
new file mode 100644
index 00000000000..62936a50877
--- /dev/null
+++ b/packages/design-tokens/src/foundations.css
@@ -0,0 +1,23 @@
+/* Generated by packages/design-tokens/build.mjs. */
+@theme {
+ --space-control-gap: 0.5rem;
+ --space-control-inset: 0.75rem;
+ --space-section-gap: 1.5rem;
+ --space-panel-inset: 1.5rem;
+ --space-page-inset: 2rem;
+ --radius-control: 0.5rem;
+ --radius-notice: 0.75rem;
+ --radius-menu: 1rem;
+ --radius-container: 1.5rem;
+ --radius-overlay: 2rem;
+ --radius-sheet: 2.5rem;
+ --radius-pill: 9999px;
+ --duration-state: 120ms;
+ --duration-enter: 180ms;
+ --duration-exit: 120ms;
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
+ --press-scale: 0.97;
+}
+@media (prefers-reduced-motion: reduce) {
+ :root { --duration-state: 0ms; --duration-enter: 0ms; --duration-exit: 0ms; --press-scale: 1; }
+}
diff --git a/packages/design-tokens/src/foundations.ts b/packages/design-tokens/src/foundations.ts
new file mode 100644
index 00000000000..3ad3a7bf902
--- /dev/null
+++ b/packages/design-tokens/src/foundations.ts
@@ -0,0 +1,45 @@
+/** Role-based geometry and motion shared by components and documentation. */
+export const FOUNDATION_GROUPS = [
+ {
+ name: "Spacing",
+ description:
+ "A four-pixel rhythm, expressed in rem so the entire interface follows zoom.",
+ roles: [
+ ["space-control-gap", "0.5rem", "Space between a label and its icon."],
+ ["space-control-inset", "0.75rem", "Compact control padding."],
+ ["space-section-gap", "1.5rem", "Space between related groups."],
+ ["space-panel-inset", "1.5rem", "Padding inside a panel."],
+ ["space-page-inset", "2rem", "Outer reading gutter."],
+ ],
+ },
+ {
+ name: "Radius",
+ description:
+ "Corners describe the surface: controls, notices, menus, containers, overlays, and sheets.",
+ roles: [
+ ["radius-control", "0.5rem", "Inputs and checkable controls."],
+ ["radius-notice", "0.75rem", "Inline notices and tooltips."],
+ ["radius-menu", "1rem", "Menus and popovers."],
+ ["radius-container", "1.5rem", "Cards and grouped content."],
+ ["radius-overlay", "2rem", "Dialogs."],
+ ["radius-sheet", "2.5rem", "Large sheets."],
+ ["radius-pill", "9999px", "Buttons and compact identity chips."],
+ ],
+ },
+ {
+ name: "Motion",
+ description:
+ "State changes are short. Entering surfaces settle gently. Reduced motion removes movement.",
+ roles: [
+ ["duration-state", "120ms", "Hover and focus changes."],
+ ["duration-enter", "180ms", "Opening a surface."],
+ ["duration-exit", "120ms", "Closing a surface."],
+ [
+ "ease-standard",
+ "cubic-bezier(0.2, 0, 0, 1)",
+ "A quick response with a quiet finish.",
+ ],
+ ["press-scale", "0.97", "Subtle feedback while a button is held."],
+ ],
+ },
+] as const;
diff --git a/packages/design-tokens/src/registry.ts b/packages/design-tokens/src/registry.ts
new file mode 100644
index 00000000000..50c179a7718
--- /dev/null
+++ b/packages/design-tokens/src/registry.ts
@@ -0,0 +1,688 @@
+/**
+ * The token registry.
+ *
+ * This is the machine-readable description of the colour system: what exists,
+ * what each name is for, what it points at, and whether it is core or proposed.
+ * The design system pages render from this, so a token added here appears in the
+ * documentation automatically and the docs cannot drift from the system.
+ *
+ * Adding to this file is a normal, unreviewed action — see the growth procedure
+ * in DESIGN.md. Every entry needs a `use` sentence and, if proposed, an `owner`.
+ */
+
+/** Whether a token is part of the vetted system or someone's addition. */
+export type TokenStatus = "core" | "proposed" | "deprecated";
+
+/** A private ramp step. Components never reference these. */
+export interface RampStep {
+ /** Position on the ramp. The number means a job, not a brightness. */
+ step: number;
+ /** What this step is for. */
+ job: string;
+ /** The CSS custom property, without `var()`. */
+ variable: string;
+}
+
+/** A private ramp. */
+export interface Ramp {
+ id: string;
+ name: string;
+ /** Why this ramp exists and how it behaves. */
+ description: string;
+ steps: RampStep[];
+ /** Rendered over the app backdrop so translucency is visible. */
+ translucent?: boolean;
+}
+
+/** A public role. The only layer a screen may use. */
+export interface Role {
+ /** The Tailwind class, e.g. `bg-panel`. */
+ token: string;
+ /** The CSS custom property backing it. */
+ variable: string;
+ /** What it points at, for display: `neutral 1`, or a note for exceptions. */
+ pointsAt: string;
+ /** One sentence: when to use this. */
+ use: string;
+ status: TokenStatus;
+ /** Required when status is `proposed`. */
+ owner?: string;
+ /** Set when the value is a literal rather than a ramp reference. */
+ exception?: string;
+}
+
+export interface RoleGroup {
+ id: string;
+ name: string;
+ description: string;
+ roles: Role[];
+}
+
+/* ============================================================
+ PRIVATE RAMPS
+ ============================================================ */
+
+const NEUTRAL_JOBS = [
+ "app background",
+ "subtle background",
+ "component background",
+ "component hover",
+ "component selected",
+ "subtle border",
+ "border",
+ "border hover",
+ "solid fill",
+ "solid fill hover",
+ "dark fill",
+ "high-contrast text",
+];
+
+/** The five accent steps any role points at. The rest exist so the ramp is
+ * complete and interpolation stays even. */
+const IDENTITY_STEPS: Array<[number, string]> = [
+ [2, "tinted surface"],
+ [3, "tint hover / selected"],
+ [8, "border, focus ring"],
+ [9, "solid fill"],
+ [11, "accent text"],
+];
+
+function identityRamp(id: string, name: string, description: string): Ramp {
+ return {
+ id,
+ name,
+ description,
+ steps: IDENTITY_STEPS.map(([step, job]) => ({
+ step,
+ job,
+ variable: `--${id}-${step}`,
+ })),
+ };
+}
+
+export const RAMPS: Ramp[] = [
+ {
+ id: "neutral",
+ name: "Neutral",
+ description:
+ "Twelve steps, each step a fixed job. The number means a job, not a brightness — which is what lets a role encode a position that stays true across every palette.",
+ steps: NEUTRAL_JOBS.map((job, i) => ({
+ step: i + 1,
+ job,
+ variable: `--neutral-${i + 1}`,
+ })),
+ },
+ identityRamp(
+ "accent",
+ "Accent — a slot, not a colour",
+ "Nothing above this ramp knows the hue, so the accent can change, become a person's preference, or vary per theme without a single component changing. Step 9 barely moves between modes because a saturated fill reads on white and on near-black; step 11 inverts direction to stay readable.",
+ ),
+ identityRamp(
+ "danger",
+ "Danger",
+ "The same five jobs as accent. Learning one family teaches all of them.",
+ ),
+ identityRamp("success", "Success", "The same five jobs as accent."),
+ identityRamp("warning", "Warning", "The same five jobs as accent."),
+ identityRamp("info", "Info", "The same five jobs as accent."),
+ {
+ id: "glass",
+ name: "Glass",
+ description:
+ "A translucency ramp of fills. Each step is the mode's surface colour at an increasing opacity, which is what lets a hover move one step up the ramp instead of holding its own literal. Fills only — a glass surface's bright rim is a border, and it is a documented exception.",
+ translucent: true,
+ steps: [
+ { step: 1, job: "barely there", variable: "--glass-1" },
+ { step: 2, job: "quiet glass", variable: "--glass-2" },
+ { step: 3, job: "default glass", variable: "--glass-3" },
+ { step: 4, job: "glass hovered", variable: "--glass-4" },
+ { step: 5, job: "nearly solid", variable: "--glass-5" },
+ ],
+ },
+];
+
+/* ============================================================
+ PUBLIC ROLES
+ ============================================================ */
+
+function identityGroup(
+ id: string,
+ label: string,
+ description: string,
+): RoleGroup {
+ return {
+ id,
+ name: label,
+ description,
+ roles: [
+ {
+ token: `bg-${id}`,
+ variable: `--bg-${id}`,
+ pointsAt: `${id} 9`,
+ use: "The solid fill: primary buttons, active toggles. Takes its paired text.",
+ status: "core",
+ },
+ {
+ token: `text-on-${id}`,
+ variable: `--text-on-${id}`,
+ pointsAt: "computed from the fill's lightness",
+ use: `Text sitting on bg-${id}.`,
+ status: "core",
+ exception:
+ "Computed rather than fixed: white is readable on a blue or purple fill and unreadable on yellow or lime.",
+ },
+ {
+ token: `text-${id}`,
+ variable: `--text-${id}`,
+ pointsAt: `${id} 11`,
+ use: "Coloured text on a neutral background: links, labels.",
+ status: "core",
+ },
+ {
+ token: `bg-${id}-tint`,
+ variable: `--bg-${id}-tint`,
+ pointsAt: `${id} 2`,
+ use: "A tinted surface carrying meaning: chips, callouts, selected rows. Takes coloured text.",
+ status: "core",
+ },
+ {
+ token: `bg-${id}-tint-hover`,
+ variable: `--bg-${id}-tint-hover`,
+ pointsAt: `${id} 3`,
+ use: "That tinted surface hovered or selected.",
+ status: "core",
+ },
+ {
+ token: `border-${id}`,
+ variable: `--border-${id}`,
+ pointsAt: `${id} 8`,
+ use: "Focus rings and active borders.",
+ status: "core",
+ },
+ ],
+ };
+}
+
+export const ROLE_GROUPS: RoleGroup[] = [
+ {
+ id: "surfaces",
+ name: "Structural surfaces",
+ description:
+ "Named and closed, because there are only a few right answers and guessing produces a broken interface. Ask one question: is it behind, on, above, or in?",
+ roles: [
+ {
+ token: "bg-app",
+ variable: "--bg-app",
+ pointsAt: "gradient-1",
+ use: "The backdrop everything sits on.",
+ status: "core",
+ },
+ {
+ token: "bg-panel",
+ variable: "--bg-panel",
+ pointsAt: "neutral 1 light / neutral 3 dark",
+ use: "Everything sitting on the backdrop: the navigation column, content, timelines, cards, rows.",
+ status: "core",
+ },
+ {
+ token: "bg-float",
+ variable: "--bg-float",
+ pointsAt: "neutral 1 light / neutral 5 dark",
+ use: "Anything hovering above the page: menus, dialogs, tooltips, toasts. Shares a light value with bg-panel and diverges in dark, because a shadow cannot carry elevation on a near-black background.",
+ status: "core",
+ },
+ {
+ token: "bg-inset",
+ variable: "--bg-inset",
+ pointsAt: "neutral 3 light / neutral 2 dark",
+ use: "Anything pushed in: inputs, code blocks, quotes.",
+ status: "core",
+ },
+ {
+ token: "bg-hover",
+ variable: "--bg-hover",
+ pointsAt: "neutral 4",
+ use: "Any neutral row or item under the cursor. Region-less — it works on any neutral surface, which is why there is only one of them.",
+ status: "core",
+ },
+ ],
+ },
+ {
+ id: "emphasis",
+ name: "Emphasis",
+ description:
+ "One three-level ramp shared by text and borders: normal, lesser, really lesser. States that are not levels of emphasis get their own names rather than extending the ramp.",
+ roles: [
+ {
+ token: "text-primary",
+ variable: "--text-primary",
+ pointsAt: "neutral 12",
+ use: "Normal reading text.",
+ status: "core",
+ },
+ {
+ token: "text-secondary",
+ variable: "--text-secondary",
+ pointsAt: "neutral 10",
+ use: "Supporting text: author names, labels.",
+ status: "core",
+ },
+ {
+ token: "text-tertiary",
+ variable: "--text-tertiary",
+ pointsAt: "neutral 9",
+ use: "Metadata: timestamps, counts, hints.",
+ status: "core",
+ },
+ {
+ token: "text-disabled",
+ variable: "--text-disabled",
+ pointsAt: "neutral 8",
+ use: "Unavailable. A state, not a fourth level of the ramp.",
+ status: "core",
+ },
+ {
+ token: "border-primary",
+ variable: "--border-primary",
+ pointsAt: "neutral 7",
+ use: "The default visible line.",
+ status: "core",
+ },
+ {
+ token: "border-secondary",
+ variable: "--border-secondary",
+ pointsAt: "neutral 6",
+ use: "A lighter separator inside a group.",
+ status: "core",
+ },
+ {
+ token: "border-tertiary",
+ variable: "--border-tertiary",
+ pointsAt: "neutral 5",
+ use: "The faintest line.",
+ status: "core",
+ },
+ ],
+ },
+ identityGroup(
+ "accent",
+ "Accent",
+ "Two arrangements and no third: a solid fill with paired text on it, or a tint fill with coloured text on it. There is deliberately nothing between them.",
+ ),
+ {
+ id: "inverse",
+ name: "Inverse",
+ description:
+ "A high-contrast fill for surfaces that must stand apart from everything: tooltips, toasts.",
+ roles: [
+ {
+ token: "bg-inverse",
+ variable: "--bg-inverse",
+ pointsAt: "neutral 11",
+ use: "High-contrast fill: tooltips, toasts.",
+ status: "core",
+ },
+ {
+ token: "bg-inverse-hover",
+ variable: "--bg-inverse-hover",
+ pointsAt: "neutral 10",
+ use: "Its hover. Step 10 sits on the light side of step 11 in the light ramp and the dark side in the dark ramp, so the hover inverts direction with no special-casing.",
+ status: "core",
+ },
+ {
+ token: "text-on-inverse",
+ variable: "--text-on-inverse",
+ pointsAt: "computed from the fill's lightness",
+ use: "Text sitting on bg-inverse.",
+ status: "core",
+ exception: "Computed rather than fixed.",
+ },
+ ],
+ },
+ identityGroup("danger", "Danger", "Destructive actions and error states."),
+ identityGroup("success", "Success", "Confirmation and healthy states."),
+ identityGroup("warning", "Warning", "Caution that is not yet an error."),
+ identityGroup("info", "Info", "Neutral information and guidance."),
+ {
+ id: "material",
+ name: "Material",
+ description:
+ "A translucent blurred surface is the same region in a different material. Translucency and blur live inside the value; a component never assembles them from a fill, a transparency, and a blur amount.",
+ roles: [
+ {
+ token: "bg-chrome-glass",
+ variable: "--bg-chrome-glass",
+ pointsAt: "glass 3 + blur-md",
+ use: "The top bar and floating chrome.",
+ status: "core",
+ },
+ {
+ token: "bg-chrome-glass-hover",
+ variable: "--bg-chrome-glass-hover",
+ pointsAt: "glass 4 + blur-md",
+ use: "A chrome control under the cursor. A glass hover moves one step up the ramp, changing opacity and never blur.",
+ status: "core",
+ },
+ {
+ token: "bg-chrome-selected",
+ variable: "--bg-chrome-selected",
+ pointsAt: "neutral 1 light / neutral 5 dark",
+ use: "The selected item inside chrome. Carries no material suffix because omitting it means opaque — on glass, elevation reads as less translucency, not a lighter colour.",
+ status: "core",
+ },
+ {
+ token: "bg-panel-glass",
+ variable: "--bg-panel-glass",
+ pointsAt: "glass 2 + blur-lg",
+ use: "A panel that should let the backdrop through.",
+ status: "core",
+ },
+ {
+ token: "bg-float-glass",
+ variable: "--bg-float-glass",
+ pointsAt: "glass 3 + blur-md",
+ use: "A floating surface that should let content through.",
+ status: "core",
+ },
+ ],
+ },
+ {
+ id: "focus",
+ name: "Focus",
+ description: "Focus is part of the design, never an artefact to suppress.",
+ roles: [
+ {
+ token: "ring-focus",
+ variable: "--ring-focus",
+ pointsAt: "accent 8",
+ use: "The keyboard focus ring.",
+ status: "core",
+ },
+ ],
+ },
+ {
+ 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.",
+ }),
+ ),
+ },
+];
+
+/* ============================================================
+ THE VOCABULARY
+ Every token in the system is built from these words. A new name
+ combining existing words is routine; a new WORD is what the
+ audit reports on its own line.
+ ============================================================ */
+
+export const VOCABULARY: Array<{ group: string; words: string[] }> = [
+ { group: "property", words: ["bg", "text", "border", "ring"] },
+ { group: "region", words: ["app", "panel", "float", "chrome", "inset"] },
+ { group: "emphasis", words: ["primary", "secondary", "tertiary"] },
+ { group: "state", words: ["hover", "pressed", "selected", "disabled"] },
+ { group: "material", words: ["glass"] },
+ { group: "modifier", words: ["tint"] },
+ { 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 = "-[-][-][-]";
+
+/** Fixed order, so there is only one correct spelling. */
+export const GRAMMAR_EXAMPLES = {
+ legal: ["bg-chrome-glass-hover", "bg-accent-tint-hover", "text-secondary"],
+ illegal: ["bg-chrome-hover-glass", "bg-hover-chrome"],
+};
+
+/** Runs per change, by whoever needs the value. Nothing here needs permission. */
+export const GROWTH_PROCEDURE = [
+ "Search the role list by intent, not by colour.",
+ "A state of an existing role — add the -hover, -selected, or -disabled sibling with both values.",
+ "A material variant of an existing role — add the -glass sibling with both values and its blur token.",
+ "A new role using existing words — add the name, both values, a one-sentence description, and an owner.",
+ "A new hue — generate its ramp, add roles pointing at steps. Never a literal.",
+ "A new vocabulary word — allowed, but it is the thing the audit reports on its own line, so use an existing word if one fits.",
+ "Never write a raw value. If nothing above applies, say so rather than reaching for a literal.",
+];
+
+/* ============================================================
+ TYPOGRAPHY
+ ============================================================ */
+
+/** A public type role. Carries its whole setting, because size, line height,
+ * tracking, and weight are one decision rather than four. */
+export interface TypeRole {
+ /** The Tailwind class, e.g. `text-body`. */
+ token: string;
+ /** Step on the size ramp this points at, for display. */
+ pointsAt: string;
+ /** Rendered size at the default preference and zoom, for display only —
+ * never a value a component may use. */
+ size: string;
+ lineHeight: string;
+ tracking: string;
+ weight: string;
+ /** One sentence: when to use this. */
+ use: string;
+ status: TokenStatus;
+ /** Set when the role renders in the mono face. */
+ mono?: boolean;
+}
+
+/** Ordered loudest to quietest, which is also the order they are chosen in. */
+export const TYPE_ROLES: TypeRole[] = [
+ {
+ token: "text-display",
+ pointsAt: "size 9",
+ size: "36px",
+ lineHeight: "1.2",
+ tracking: "-0.022em",
+ weight: "600",
+ use: "One per page at most, and many pages need none. The only step allowed to be decorative.",
+ status: "core",
+ },
+ {
+ token: "text-title",
+ pointsAt: "size 8",
+ size: "28px",
+ lineHeight: "1.2",
+ tracking: "-0.019em",
+ weight: "600",
+ use: "The name of the thing you are looking at.",
+ status: "core",
+ },
+ {
+ token: "text-heading",
+ pointsAt: "size 7",
+ size: "22px",
+ lineHeight: "1.35",
+ tracking: "-0.017em",
+ weight: "600",
+ use: "A section within a page.",
+ status: "core",
+ },
+ {
+ token: "text-subheading",
+ pointsAt: "size 6",
+ size: "18px",
+ lineHeight: "1.35",
+ tracking: "-0.014em",
+ weight: "600",
+ use: "A group within a section.",
+ status: "core",
+ },
+ {
+ token: "text-body-lg",
+ pointsAt: "size 5",
+ size: "16px",
+ lineHeight: "1.6",
+ tracking: "-0.011em",
+ weight: "400",
+ use: "Introductions and reading columns, where a paragraph is the point rather than a description of something else.",
+ status: "core",
+ },
+ {
+ token: "text-body",
+ pointsAt: "size 4",
+ size: "14px",
+ lineHeight: "1.5",
+ tracking: "-0.006em",
+ weight: "400",
+ use: "The default. If you are unsure, this is it.",
+ status: "core",
+ },
+ {
+ token: "text-label",
+ pointsAt: "size 3",
+ size: "13px",
+ lineHeight: "1.35",
+ tracking: "-0.003em",
+ weight: "500",
+ use: "Interface text that is acted on rather than read: buttons, tabs, menu items, form labels, table headers.",
+ status: "core",
+ },
+ {
+ token: "text-caption",
+ pointsAt: "size 2",
+ size: "12px",
+ lineHeight: "1.5",
+ tracking: "0em",
+ weight: "400",
+ use: "A sentence about something else: help text, descriptions, empty-state detail.",
+ status: "core",
+ },
+ {
+ token: "text-meta",
+ pointsAt: "size 1",
+ size: "11px",
+ lineHeight: "1",
+ tracking: "0.005em",
+ weight: "500",
+ use: "Timestamps, counts, tracking labels. Flat line height so a single-line value cannot grow its own row.",
+ status: "core",
+ },
+ {
+ token: "text-code",
+ pointsAt: "size 3",
+ size: "13px",
+ lineHeight: "1.5",
+ tracking: "0em",
+ weight: "400",
+ use: "Inline code and code blocks. One step below body, because at equal size a monospace face reads larger than Inter and pulls the eye off the sentence.",
+ status: "core",
+ mono: true,
+ },
+];
+
+/** The two faces. Both already shipped in every current Buzz client. */
+export const TYPE_FAMILIES = [
+ {
+ token: "font-sans",
+ name: "Inter Variable",
+ use: "Everything. Drawn for interface text at small sizes, and already the sans in desktop, web, and mobile.",
+ },
+ {
+ token: "font-mono",
+ name: "JetBrains Mono",
+ use: "Code, keys, and identifiers. Already the mono in the existing client's terminal.",
+ },
+];
+
+/** The private ramps a type role points at. Components never reference these. */
+export const TYPE_RAMPS = [
+ {
+ id: "size",
+ name: "Size",
+ description:
+ "Nine steps, each step an editorial job. Tight and frequent through the working sizes where real interface text lives, opening up at the display end where a step needs to be a visible jump rather than a near-miss. Every value derives from a virtual rem, so the whole ramp follows the person's font-size preference and keyboard zoom.",
+ steps: [
+ { step: 1, job: "meta", value: "11px" },
+ { step: 2, job: "caption", value: "12px" },
+ { step: 3, job: "label, code", value: "13px" },
+ { step: 4, job: "body", value: "14px" },
+ { step: 5, job: "body large", value: "16px" },
+ { step: 6, job: "subheading", value: "18px" },
+ { step: 7, job: "heading", value: "22px" },
+ { step: 8, job: "title", value: "28px" },
+ { step: 9, job: "display", value: "36px" },
+ ],
+ },
+ {
+ id: "tracking",
+ name: "Tracking",
+ description:
+ "An optical correction, not a style. Inter needs progressively tighter spacing as it grows — tracking that looks correct at 14px looks loose at 36px — so the ramp mirrors the size ramp and the correction is applied per step rather than guessed. It turns slightly positive at meta sizes, where letters need air to stay legible.",
+ steps: [
+ { step: 9, job: "display", value: "-0.022em" },
+ { step: 8, job: "title", value: "-0.019em" },
+ { step: 7, job: "heading", value: "-0.017em" },
+ { step: 6, job: "subheading", value: "-0.014em" },
+ { step: 5, job: "body large", value: "-0.011em" },
+ { step: 4, job: "body", value: "-0.006em" },
+ { step: 3, job: "label", value: "-0.003em" },
+ { step: 2, job: "caption", value: "0em" },
+ { step: 1, job: "meta", value: "0.005em" },
+ ],
+ },
+];
+
+export const ELEVATION = [
+ {
+ token: "shadow-xs",
+ variable: "--shadow-xs",
+ use: "The default lift: a selected pill, a small raised control.",
+ },
+ {
+ token: "shadow-sm",
+ variable: "--shadow-sm",
+ use: "A floating surface: menus, dialogs, popovers.",
+ },
+];
+
+export const BLUR = [
+ { token: "blur-sm", variable: "--blur-sm", value: "8px" },
+ { token: "blur-md", variable: "--blur-md", value: "16px" },
+ { token: "blur-lg", variable: "--blur-lg", value: "32px" },
+];
+
+/** The entire exception list. Everything else points at a ramp step. */
+export const EXCEPTIONS = [
+ {
+ name: "text-on-accent, text-on-inverse, and the four status pairings",
+ why: "Computed from their fill's lightness rather than fixed, because white is readable on a blue or purple fill and unreadable on yellow or lime. This is what keeps a free choice of accent hue from becoming a contrast lottery.",
+ },
+ {
+ name: "--rim-lit, --rim-shade",
+ why: "The glass rim is a directional light effect, not a solid line. It is not on the glass ramp — that is a ramp of fills, and in dark mode the fill is translucent near-black while the rim stays translucent white. It is not one of the numbered gradients either: those are background treatments, this is a material detail.",
+ },
+ {
+ 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.",
+ },
+];
diff --git a/packages/design-tokens/src/tokens.css b/packages/design-tokens/src/tokens.css
new file mode 100644
index 00000000000..90f329b4ad7
--- /dev/null
+++ b/packages/design-tokens/src/tokens.css
@@ -0,0 +1,400 @@
+/*
+ * Buzz colour tokens.
+ *
+ * Three layers, and only the role layer is ever used when building a screen:
+ *
+ * LAYER 1 private ramps --neutral-*, --accent-*, --glass-*, gradients
+ * LAYER 2 public roles --bg-panel, --text-secondary, ...
+ * LAYER 3 components bg-panel, text-secondary (Tailwind utilities)
+ *
+ * Rules:
+ * - Literal values exist only in the ramps. Nothing above a ramp holds one,
+ * except the documented exceptions at the bottom of this file.
+ * - A component never references a ramp step directly.
+ * - Transparency is never applied to a token. It lives inside the value.
+ * - Every role holds a light and a dark value under the same name.
+ *
+ * See DESIGN.md for the judgement this vocabulary cannot express.
+ */
+
+/* ============================================================
+ LAYER 1 — PRIVATE RAMPS
+ ============================================================ */
+
+:root {
+ /* --- Neutral. Twelve steps, each step a fixed job. -------- */
+ --neutral-1: #ffffff; /* app background */
+ --neutral-2: #f0f0f0; /* subtle background */
+ --neutral-3: #e8e8e8; /* component background */
+ --neutral-4: #f0f0f0; /* component hover */
+ --neutral-5: #e8e8e8; /* component selected */
+ --neutral-6: #e5e5e5; /* subtle border */
+ --neutral-7: #d4d4d4; /* border */
+ --neutral-8: #b8b8b8; /* border hover */
+ --neutral-9: #666666; /* solid fill, tertiary text */
+ --neutral-10: #525252; /* solid fill hover */
+ --neutral-11: #262626; /* dark fill */
+ --neutral-12: #0a0a0a; /* high-contrast text */
+
+ /* --- Accent. A slot, not a colour. -----------------------
+ Nothing above this ramp knows the hue. Replace these five
+ values to change the accent; every role below follows.
+ Reference hue: the blue from the design exploration. */
+ --accent-2: #e7f1ff; /* tinted surface */
+ --accent-3: #d0e3ff; /* tint hover / selected */
+ --accent-8: #3d87ff; /* border, focus ring */
+ --accent-9: #237eff; /* solid fill */
+ --accent-11: #1d4ed8; /* accent text */
+
+ /* --- Status. Same five jobs as accent, per family. -------- */
+ --danger-2: #fee2e2;
+ --danger-3: #fecaca;
+ --danger-8: #f5b5b5;
+ --danger-9: #dc2626;
+ --danger-11: #a80008;
+
+ --success-2: #e8f7ee;
+ --success-3: #d1f0dd;
+ --success-8: #a3e0bd;
+ --success-9: #16a34a;
+ --success-11: #15803d;
+
+ --warning-2: #fef3c7;
+ --warning-3: #fde68a;
+ --warning-8: #f2d98a;
+ --warning-9: #eab308;
+ --warning-11: #9c5e00;
+
+ --info-2: #e7f1ff;
+ --info-3: #d0e3ff;
+ --info-8: #b8d4ff;
+ --info-9: #3d87ff;
+ --info-11: #1d4ed8;
+
+ /* --- Glass. A translucency ramp of fills. ---------------- */
+ --glass-1: #ffffff5c; /* barely there */
+ --glass-2: #ffffff85; /* quiet glass */
+ --glass-3: #ffffff9e; /* default glass */
+ --glass-4: #ffffffc7; /* glass hovered */
+ --glass-5: #ffffffe0; /* nearly solid */
+
+ /* --- Categorical. Named by appearance, deliberately: the
+ choice IS appearance — telling two projects apart. ------- */
+ --tint-blue: #e7f1ff;
+ --tint-purple: #f3e8ff;
+ --tint-orange: #ffedd5;
+ --tint-green: #ecfccb;
+ --tint-red: #fee2e2;
+ --tint-cyan: #cffafe;
+
+ /* --- Gradients. Numbered background treatments. ---------- */
+ --gradient-1:
+ radial-gradient(at 85% 88%, #eef2d4 0%, transparent 62%),
+ linear-gradient(158deg, #f5f7f8 0%, #dfeaf3 52%, #e9efe0 100%);
+
+ /* --- Texture. Separate from colour and gradient. --------- */
+ --texture-dots-color: #0a0a0a0f;
+ --texture-dots-size: 28px;
+
+ /* --- Blur. A separate axis from translucency. ------------ */
+ --blur-sm: 8px;
+ --blur-md: 16px;
+ --blur-lg: 32px;
+
+ /* --- Elevation. From the design exploration, unchanged. -- */
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-sm: 0 0 2px rgba(0, 0, 0, 0.1), 0 4px 8px -2px rgba(0, 0, 0, 0.06);
+}
+
+.dark {
+ --neutral-1: #0f0f0f;
+ --neutral-2: #161616;
+ --neutral-3: #1c1c1c;
+ --neutral-4: #232323;
+ --neutral-5: #2a2a2a;
+ --neutral-6: #333333;
+ --neutral-7: #424242;
+ --neutral-8: #5a5a5a;
+ /* Steps 9 and 10 are lighter than a linear ramp would put them, because in
+ dark mode they carry text and APCA is polarity-asymmetric: light-on-dark
+ needs more separation than the WCAG ratio implies. At #6e6e6e and #8f8f8f
+ they measured Lc 23–26 and Lc 38–41 against the Lc 60 body-text target
+ while passing WCAG AA — legal, and visibly murky. Sized against the
+ lightest dark surface (bg-float #2a2a2a), which is the binding case. */
+ --neutral-9: #a6a6a6;
+ --neutral-10: #c1c1c1;
+ --neutral-11: #e5e5e5;
+ --neutral-12: #f5f5f5;
+
+ /* Accent step 9 barely moves between modes — a saturated fill
+ reads correctly on white and on near-black. Step 11 inverts
+ direction: darker than the fill on light, lighter on dark. */
+ --accent-2: #152845;
+ --accent-3: #1a3358;
+ --accent-8: #356dbd;
+ --accent-9: #237eff;
+ --accent-11: #95c0ff;
+
+ --danger-2: #3a1515;
+ --danger-3: #4d1c1c;
+ --danger-8: #7a2e2e;
+ --danger-9: #dc2626;
+ --danger-11: #fca5a5;
+
+ --success-2: #10261a;
+ --success-3: #163524;
+ --success-8: #2a5c3f;
+ --success-9: #16a34a;
+ --success-11: #86e0aa;
+
+ --warning-2: #2e2410;
+ --warning-3: #423318;
+ --warning-8: #6b5320;
+ --warning-9: #eab308;
+ --warning-11: #f5d878;
+
+ --info-2: #152845;
+ --info-3: #1a3358;
+ --info-8: #356dbd;
+ --info-9: #3d87ff;
+ --info-11: #95c0ff;
+
+ --glass-1: #1c1c1c5c;
+ --glass-2: #1c1c1c85;
+ --glass-3: #1c1c1ca8;
+ --glass-4: #1c1c1cc7;
+ --glass-5: #1c1c1ce0;
+
+ --tint-blue: #152845;
+ --tint-purple: #2b1f45;
+ --tint-orange: #3a2412;
+ --tint-green: #23300f;
+ --tint-red: #3a1515;
+ --tint-cyan: #0e2e33;
+
+ --gradient-1:
+ radial-gradient(at 85% 88%, #23261a 0%, transparent 62%),
+ linear-gradient(158deg, #15171b 0%, #172231 52%, #191d19 100%);
+
+ --texture-dots-color: #ffffff14;
+}
+
+/* ============================================================
+ LAYER 2 — PUBLIC ROLES
+ The only layer a screen may use. Every value below points at
+ a ramp step, except the documented exceptions at the bottom.
+ ============================================================ */
+
+:root {
+ /* --- Structural surfaces. Named by region. ---------------
+ Ask one question: is it behind, on, above, or in? */
+ --bg-app: var(--gradient-1);
+ --bg-panel: var(--neutral-1);
+ --bg-float: var(--neutral-1);
+ --bg-inset: var(--neutral-3);
+ --bg-hover: var(--neutral-4);
+
+ /* --- Emphasis. One ramp shared by text and borders. ------ */
+ --text-primary: var(--neutral-12);
+ --text-secondary: var(--neutral-10);
+ --text-tertiary: var(--neutral-9);
+ /* Deliberately below the contrast targets: low contrast is the signal that a
+ control is unavailable, and WCAG exempts inactive controls for the same
+ reason. Raising it to Lc 60 would make disabled read as enabled. Never put
+ information here that a person needs. */
+ --text-disabled: var(--neutral-8);
+
+ --border-primary: var(--neutral-7);
+ --border-secondary: var(--neutral-6);
+ --border-tertiary: var(--neutral-5);
+
+ /* --- Identity. Two arrangements only: SOLID fill with
+ paired text on it, or TINT fill with coloured text. */
+ --bg-accent: var(--accent-9);
+ --bg-accent-tint: var(--accent-2);
+ --bg-accent-tint-hover: var(--accent-3);
+ --text-accent: var(--accent-11);
+ --border-accent: var(--accent-8);
+
+ --bg-inverse: var(--neutral-11);
+ --bg-inverse-hover: var(--neutral-10);
+
+ /* --- Status. Identical shape, four times over. ----------- */
+ --bg-danger: var(--danger-9);
+ --bg-danger-tint: var(--danger-2);
+ --bg-danger-tint-hover: var(--danger-3);
+ --text-danger: var(--danger-11);
+ --border-danger: var(--danger-8);
+
+ --bg-success: var(--success-9);
+ --bg-success-tint: var(--success-2);
+ --bg-success-tint-hover: var(--success-3);
+ --text-success: var(--success-11);
+ --border-success: var(--success-8);
+
+ --bg-warning: var(--warning-9);
+ --bg-warning-tint: var(--warning-2);
+ --bg-warning-tint-hover: var(--warning-3);
+ --text-warning: var(--warning-11);
+ --border-warning: var(--warning-8);
+
+ --bg-info: var(--info-9);
+ --bg-info-tint: var(--info-2);
+ --bg-info-tint-hover: var(--info-3);
+ --text-info: var(--info-11);
+ --border-info: var(--info-8);
+
+ /* --- Material. Translucency and blur live inside the value.
+ A component never assembles them. */
+ --bg-chrome-glass: var(--glass-3);
+ --bg-chrome-glass-hover: var(--glass-4);
+ --bg-chrome-selected: var(--neutral-1);
+ --bg-panel-glass: var(--glass-2);
+ --bg-float-glass: var(--glass-3);
+
+ /* --- Focus. ----------------------------------------------
+ Legible on every neutral surface (WCAG 3.0–3.5:1), but it is an accent
+ against accent: on `bg-accent` it measures 1.1:1 and disappears. A focused
+ primary button therefore needs the ring separated from the fill — an offset
+ in the surface colour, not a different ring hue, so one focus treatment
+ works everywhere. Whatever component layer introduces focus owns that
+ offset; there is no second ring token. */
+ --ring-focus: var(--accent-8);
+
+ /* ==========================================================
+ DELIBERATE EXCEPTIONS — the entire list.
+ ==========================================================
+
+ 1. Paired text. Computed from its fill's lightness rather
+ than fixed, because white is readable on a blue or
+ purple fill and unreadable on yellow or lime. Until the
+ computation lands, these hold literals. */
+ --text-on-accent: #ffffff;
+ --text-on-inverse: var(--neutral-1);
+
+ /* The status families need their paired text for the same reason accent does:
+ `bg-danger` is a saturated fill, and text on it must be the paired value
+ rather than whatever it inherits. The registry documented these four before
+ they were declared, so `text-on-danger` resolved to nothing and text on a
+ danger button fell back to near-black on red. The token table found it.
+ Literals for now, on the same exception as text-on-accent — they become
+ computed when the lightness rule lands. */
+ --text-on-danger: #ffffff;
+ --text-on-success: #ffffff;
+ /* A literal, not `var(--neutral-12)`: the warning fill is the same amber in
+ both modes, so its paired text must be dark in both. neutral-12 flips to
+ near-white in dark mode, which put #f5f5f5 on #eab308 — APCA Lc 34, WCAG
+ 1.76. A paired-text token follows its fill, never the mode. */
+ --text-on-warning: #0a0a0a;
+ --text-on-info: #ffffff;
+
+ /* 2. The glass rim. A directional light effect, not a solid
+ line: bright along the lit edge, dimmer on the opposite
+ one, from one fixed light direction every glass surface
+ in the product shares. Not on the glass ramp — that is
+ a ramp of fills, and in dark mode the fill is
+ translucent near-black while the rim stays translucent
+ white. Not one of the numbered gradients either: those
+ are background treatments, this is a material detail.
+
+ Implemented as two inset shadows because CSS borders
+ accept only solid colours. See DESIGN.md. */
+ --rim-lit: #ffffff73;
+ --rim-shade: #ffffff1f;
+}
+
+.dark {
+ --bg-panel: var(--neutral-3);
+ --bg-float: var(--neutral-5);
+ --bg-inset: var(--neutral-2);
+ --bg-chrome-selected: var(--neutral-5);
+
+ --rim-lit: #ffffff2e;
+ --rim-shade: #ffffff0f;
+}
+
+/* ============================================================
+ LAYER 3 — TAILWIND REGISTRATION
+ `--color-*: initial` deletes Tailwind's default palette, so
+ `text-gray-500` does not exist. Only the roles above do.
+ ============================================================ */
+
+@theme inline {
+ --color-*: initial;
+
+ --color-app: var(--bg-app);
+ --color-panel: var(--bg-panel);
+ --color-float: var(--bg-float);
+ --color-inset: var(--bg-inset);
+ --color-hover: var(--bg-hover);
+
+ --color-primary: var(--text-primary);
+ --color-secondary: var(--text-secondary);
+ --color-tertiary: var(--text-tertiary);
+ --color-disabled: var(--text-disabled);
+
+ /* Borders register in their OWN namespace, not `--color-*`.
+ Text and borders share the emphasis names — primary, secondary, tertiary —
+ but hold different values: text is the dark end of the neutral ramp
+ (12/10/9), borders the light end (7/6/5). `--color-*` is a single namespace,
+ so registering borders there would make `border-primary` resolve to the text
+ colour and draw every hairline at near-black. It did exactly that until
+ these three lines existed. */
+ --border-color-primary: var(--border-primary);
+ --border-color-secondary: var(--border-secondary);
+ --border-color-tertiary: var(--border-tertiary);
+
+ --color-accent: var(--bg-accent);
+ --text-color-accent: var(--text-accent);
+ --border-color-accent: var(--border-accent);
+ --text-color-danger: var(--text-danger);
+ --border-color-danger: var(--border-danger);
+ --text-color-success: var(--text-success);
+ --border-color-success: var(--border-success);
+ --text-color-warning: var(--text-warning);
+ --border-color-warning: var(--border-warning);
+ --text-color-info: var(--text-info);
+ --border-color-info: var(--border-info);
+ --ring-color-focus: var(--ring-focus);
+ --color-accent-tint: var(--bg-accent-tint);
+ --color-accent-tint-hover: var(--bg-accent-tint-hover);
+ --color-on-accent: var(--text-on-accent);
+
+ --color-inverse: var(--bg-inverse);
+ --color-inverse-hover: var(--bg-inverse-hover);
+ --color-on-inverse: var(--text-on-inverse);
+
+ --color-danger: var(--bg-danger);
+ --color-danger-tint: var(--bg-danger-tint);
+ --color-danger-tint-hover: var(--bg-danger-tint-hover);
+ --color-on-danger: var(--text-on-danger);
+ --color-success: var(--bg-success);
+ --color-success-tint: var(--bg-success-tint);
+ --color-success-tint-hover: var(--bg-success-tint-hover);
+ --color-on-success: var(--text-on-success);
+ --color-on-warning: var(--text-on-warning);
+ --color-on-info: var(--text-on-info);
+ --color-warning: var(--bg-warning);
+ --color-warning-tint: var(--bg-warning-tint);
+ --color-warning-tint-hover: var(--bg-warning-tint-hover);
+ --color-info: var(--bg-info);
+ --color-info-tint: var(--bg-info-tint);
+ --color-info-tint-hover: var(--bg-info-tint-hover);
+
+ --color-chrome-glass: var(--bg-chrome-glass);
+ --color-chrome-glass-hover: var(--bg-chrome-glass-hover);
+ --color-chrome-selected: var(--bg-chrome-selected);
+ --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);
+}
+
+@utility bg-app {
+ background: var(--bg-app);
+}
diff --git a/packages/design-tokens/src/typography.css b/packages/design-tokens/src/typography.css
new file mode 100644
index 00000000000..0b7370a73d3
--- /dev/null
+++ b/packages/design-tokens/src/typography.css
@@ -0,0 +1,231 @@
+/*
+ * Buzz typography tokens.
+ *
+ * Same three layers as colour, and only the role layer is ever used when
+ * building a screen:
+ *
+ * LAYER 1 private ramps --type-size-*, --type-leading-*, --type-tracking-*
+ * LAYER 2 public roles --text-body, --text-title, ...
+ * LAYER 3 components text-body, text-title (Tailwind utilities)
+ *
+ * Rules, matching the colour system:
+ * - Literal sizes exist only in the ramp. A role points at a step; a component
+ * points at a role. No component holds a size.
+ * - A role carries its whole typographic setting — size, line height, letter
+ * spacing, weight — because those four are one decision, not four. Tailwind
+ * v4 supports this via the `--text---` convention, so
+ * `text-body` alone produces correctly set text.
+ * - Every size is relative. Nothing here may be expressed in px: fixed pixel
+ * text freezes against zoom and breaks the person's font-size preference.
+ * This is the one rule the existing client already learned the hard way.
+ *
+ * Naming note: colour roles are also spelled `--text-*` (`--text-primary`) but
+ * are registered in Tailwind's `--color-*` namespace, while sizes register in
+ * `--text-*`. So `text-primary` is a colour and `text-body` is a setting — the
+ * same split Tailwind itself makes. Size roles are named for editorial jobs
+ * (body, title, caption) and colour roles for emphasis levels (primary,
+ * secondary), so no one name ever means both.
+ *
+ * See DESIGN.md for the judgement these values cannot express.
+ */
+
+/* ============================================================
+ LAYER 1 — PRIVATE RAMPS
+ ============================================================ */
+
+:root {
+ /* --- The zoom and preference dials. -----------------------
+ Two independent controls compose here, as in the existing client:
+
+ - Cmd +/- scales the real root font size, so every rem in the app — text,
+ gaps, radii — grows together. True zoom.
+ - The font-size preference sets --type-scale, nudging only the type ramp
+ below, so text changes size without moving the layout.
+
+ Every size derives from this virtual rem, which is itself rem-relative and
+ therefore rides on top of zoom automatically. At the default preference it
+ equals 1rem, making --type-size-4 read as 14px. */
+ --type-scale: 1;
+ --type-rem: calc(1rem * var(--type-scale));
+
+ /* --- Size. Nine steps, each step an editorial job. --------
+ A modern ramp for a dense product: tight and frequent through the working
+ sizes where real interface text lives, opening up at the display end where
+ a step needs to be a visible jump rather than a near-miss. */
+ --type-size-1: calc(var(--type-rem) * 0.6875); /* 11px meta */
+ --type-size-2: calc(var(--type-rem) * 0.75); /* 12px caption */
+ --type-size-3: calc(var(--type-rem) * 0.8125); /* 13px label */
+ --type-size-4: calc(var(--type-rem) * 0.875); /* 14px body */
+ --type-size-5: calc(var(--type-rem) * 1); /* 16px body large */
+ --type-size-6: calc(var(--type-rem) * 1.125); /* 18px subheading */
+ --type-size-7: calc(var(--type-rem) * 1.375); /* 22px heading */
+ --type-size-8: calc(var(--type-rem) * 1.75); /* 28px title */
+ --type-size-9: calc(var(--type-rem) * 2.25); /* 36px display */
+
+ /* --- Line height. Unitless, so it scales with its own size.
+ Tight for display text, generous for anything read in quantity, and exactly
+ 1 for single-line meta so a timestamp cannot enlarge its own row. */
+ --type-leading-flat: 1;
+ --type-leading-tight: 1.2;
+ --type-leading-snug: 1.35;
+ --type-leading-normal: 1.5;
+ --type-leading-relaxed: 1.6;
+
+ /* --- Tracking. An optical correction, not a style. --------
+ Inter is drawn for interface text and needs negative tracking as it grows:
+ spacing that looks correct at 14px looks loose at 36px. The ramp mirrors
+ the size ramp so the correction is applied per step rather than guessed,
+ and turns slightly positive at meta sizes where letters need air to stay
+ legible. */
+ --type-tracking-display: -0.022em;
+ --type-tracking-title: -0.019em;
+ --type-tracking-heading: -0.017em;
+ --type-tracking-subheading: -0.014em;
+ --type-tracking-large: -0.011em;
+ --type-tracking-body: -0.006em;
+ --type-tracking-label: -0.003em;
+ --type-tracking-caption: 0em;
+ --type-tracking-meta: 0.005em;
+
+ /* --- Weight. Four, and the gaps are deliberate. -----------
+ Inter Variable can render any weight, which is exactly why the system names
+ only these: 500 and 600 sit far enough apart to read as intent rather than
+ as a rendering artefact. */
+ --type-weight-normal: 400;
+ --type-weight-medium: 500;
+ --type-weight-semibold: 600;
+ --type-weight-bold: 700;
+}
+
+/* Conversation type contract, carried over from the existing client:
+ 13 / 14 / 15px before keyboard zoom. */
+:root[data-font-size="smaller"] {
+ --type-scale: calc(13 / 14);
+}
+
+:root[data-font-size="larger"] {
+ --type-scale: calc(15 / 14);
+}
+
+/* ============================================================
+ LAYER 3 — PUBLIC ROLES, REGISTERED WITH TAILWIND
+
+ Layers 2 and 3 are one block for typography. A colour role can be a bare
+ custom property because a component reads it through `bg-*` or `text-*`; a
+ type role has to be a Tailwind theme key or the paired line height, tracking,
+ and weight cannot travel with the size. So the role IS the registration.
+
+ Named for the job the text does, never for its size — `text-title`, not
+ `text-28`. A size name is a value in disguise and goes stale the moment the
+ ramp moves.
+ ============================================================ */
+
+@theme {
+ --font-sans:
+ "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
+ --font-mono:
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+
+ /* --- Display. One per page at most, and many pages need none.
+ The only step that is allowed to be decorative. */
+ --text-display: var(--type-size-9);
+ --text-display--line-height: var(--type-leading-tight);
+ --text-display--letter-spacing: var(--type-tracking-display);
+ --text-display--font-weight: var(--type-weight-semibold);
+
+ /* --- Title. The name of the thing you are looking at. ----- */
+ --text-title: var(--type-size-8);
+ --text-title--line-height: var(--type-leading-tight);
+ --text-title--letter-spacing: var(--type-tracking-title);
+ --text-title--font-weight: var(--type-weight-semibold);
+
+ /* --- Heading. A section within a page. ------------------- */
+ --text-heading: var(--type-size-7);
+ --text-heading--line-height: var(--type-leading-snug);
+ --text-heading--letter-spacing: var(--type-tracking-heading);
+ --text-heading--font-weight: var(--type-weight-semibold);
+
+ /* --- Subheading. A group within a section. --------------- */
+ --text-subheading: var(--type-size-6);
+ --text-subheading--line-height: var(--type-leading-snug);
+ --text-subheading--letter-spacing: var(--type-tracking-subheading);
+ --text-subheading--font-weight: var(--type-weight-semibold);
+
+ /* --- Body large. Introductions and reading columns, where a
+ paragraph is the point rather than a description of something else. */
+ --text-body-lg: var(--type-size-5);
+ --text-body-lg--line-height: var(--type-leading-relaxed);
+ --text-body-lg--letter-spacing: var(--type-tracking-large);
+ --text-body-lg--font-weight: var(--type-weight-normal);
+
+ /* --- Body. The default. If you are unsure, this is it. --- */
+ --text-body: var(--type-size-4);
+ --text-body--line-height: var(--type-leading-normal);
+ --text-body--letter-spacing: var(--type-tracking-body);
+ --text-body--font-weight: var(--type-weight-normal);
+
+ /* --- Label. Interface text that is acted on rather than
+ read: buttons, tabs, menu items, form labels, table headers. Medium weight
+ because a label competes with content around it and needs to hold its edge
+ without going up a size. */
+ --text-label: var(--type-size-3);
+ --text-label--line-height: var(--type-leading-snug);
+ --text-label--letter-spacing: var(--type-tracking-label);
+ --text-label--font-weight: var(--type-weight-medium);
+
+ /* --- Caption. A sentence about something else: help text,
+ descriptions, empty-state detail. */
+ --text-caption: var(--type-size-2);
+ --text-caption--line-height: var(--type-leading-normal);
+ --text-caption--letter-spacing: var(--type-tracking-caption);
+ --text-caption--font-weight: var(--type-weight-normal);
+
+ /* --- Meta. Timestamps, counts, tracking numbers. Flat line
+ height so a single-line value cannot grow its own row. */
+ --text-meta: var(--type-size-1);
+ --text-meta--line-height: var(--type-leading-flat);
+ --text-meta--letter-spacing: var(--type-tracking-meta);
+ --text-meta--font-weight: var(--type-weight-medium);
+
+ /* --- Code. Mono, one step down: at equal size a monospace
+ face reads larger than Inter and pulls the eye off the sentence. */
+ --text-code: var(--type-size-3);
+ --text-code--line-height: var(--type-leading-normal);
+ --text-code--letter-spacing: var(--type-tracking-caption);
+ --text-code--font-weight: var(--type-weight-normal);
+}
+
+/* ============================================================
+ BASE
+ ============================================================ */
+
+@layer base {
+ html {
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ body {
+ /* Every readable surface starts from the body role and steps away from it
+ deliberately. */
+ font-size: var(--text-body);
+ line-height: var(--text-body--line-height);
+ letter-spacing: var(--text-body--letter-spacing);
+
+ /* Never synthesise a weight Inter Variable can render properly, and never
+ let a font fall back mid-string. */
+ font-synthesis-weight: none;
+ text-rendering: optimizeLegibility;
+ }
+
+ code,
+ kbd,
+ pre,
+ samp {
+ font-family: var(--font-mono);
+ /* Mono digits and letters share one advance width, so a changing value does
+ not shift the text around it. */
+ font-variant-numeric: tabular-nums;
+ }
+}
diff --git a/packages/design-tokens/test/utilities.test.mjs b/packages/design-tokens/test/utilities.test.mjs
new file mode 100644
index 00000000000..fa601c83ba1
--- /dev/null
+++ b/packages/design-tokens/test/utilities.test.mjs
@@ -0,0 +1,31 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+import { compile } from "tailwindcss";
+import { ROLE_GROUPS } from "../src/registry.ts";
+const source = readFileSync(
+ new URL("../src/tokens.css", import.meta.url),
+ "utf8",
+);
+test("generated utilities bind the same roles as the registry and contrast audit", async () => {
+ const roles = ROLE_GROUPS.flatMap((g) => g.roles).filter(
+ (r) => !["bg-app"].includes(r.token),
+ );
+ const compiler = await compile(source + "\n@tailwind utilities;");
+ const css = compiler.build(roles.map((r) => r.token));
+ for (const role of roles) {
+ const start = css.indexOf("." + role.token + " {");
+ assert.notEqual(start, -1, "missing utility " + role.token);
+ const rule = css.slice(start, css.indexOf("}", start));
+ assert.ok(
+ rule.includes("var(" + role.variable + ")"),
+ role.token + " must bind " + role.variable + "; got " + rule,
+ );
+ }
+});
+test("off-palette utilities cannot silently bring another palette into the system", async () => {
+ const compiler = await compile(source + "\n@tailwind utilities;");
+ const css = compiler.build(["text-gray-500", "bg-red-500"]);
+ assert.ok(!css.includes(".text-gray-500"));
+ assert.ok(!css.includes(".bg-red-500"));
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be021e6051d..9f86fd47e4b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -313,6 +313,9 @@ importers:
'@base-ui-components/react':
specifier: 1.0.0-rc.0
version: 1.0.0-rc.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@buzz/design-tokens':
+ specifier: workspace:*
+ version: link:../packages/design-tokens
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
@@ -354,6 +357,12 @@ importers:
specifier: ^8.0.0
version: 8.0.16(@types/node@25.6.0)(jiti@2.7.0)(yaml@2.9.0)
+ packages/design-tokens:
+ devDependencies:
+ tailwindcss:
+ specifier: 4.3.0
+ version: 4.3.0
+
web:
dependencies:
'@fontsource-variable/inter':
@@ -604,6 +613,7 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@biomejs/cli-linux-arm64@2.4.16':
resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
@@ -624,6 +634,7 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
@@ -1580,6 +1591,7 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.3':
resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
@@ -1593,7 +1605,6 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.3':
resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
@@ -1730,6 +1741,7 @@ packages:
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
@@ -1867,12 +1879,14 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==}
@@ -1886,12 +1900,14 @@ packages:
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.11.4':
resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==}
@@ -2804,7 +2820,6 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
@@ -2818,6 +2833,7 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index a1bbc5bc7f5..d592a79f201 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,5 @@
packages:
+ - "packages/*"
- "desktop"
- "desktop-next"
- "web"