← Ledger


title: Design-tokens package — extract Mars palette into @rocky-hq/design-tokens date: 2026-07-01 status: Proposed phase: fe-a-design-tokens (foundation for shared visual identity across pantheon) predecessor: docs/decisions/2026-06-29-phase-7c-c-a-hearth-close.md spec: DESIGN.md (parent) + DESIGN.md (console) plan: (this ADR is the plan) external_repo: rocky-hq/design-tokens (proposed, not yet created) external_pr: (none yet — draft lives at parent design-tokens/ until submodule cutover)

Design-tokens package (proposed)

Problem

The Mars mineral palette (void, oxide, signal, regolith, dust) is currently defined six times, once per app that needs it:

File Palette shape
DESIGN.md (parent) canonical, OKLCH-annotated
DESIGN.json (parent) schema mirror of the above
console/src/app/globals.css:112-134 full Mars palette + --mars-* prefix, 4 void steps, 4 oxide, 4 signal, 3 regolith, 3 dust, 4 status, animations
apps/erid-apex/src/app/globals.css:1-9 flat 7-color subset (--void, --void-deep, --dust, --regolith, --oxide, --signal, --border)
apps/erid-docs/src/app/globals.css:1-32 same 7-color subset
apps/erid-coming-soon/src/app/globals.css:1-87 same 7-color subset

Hatch (devarno-cloud/hatch, external repo) is a seventh copy of the console-shaped superset (dust/dust-muted/dust-faint, void/void-surface/void-elevated, status-* badges).

Two failure modes flow from this:

  1. Drift — Console prefixes its palette --mars-*; erid apps do not. Same colors, incompatible token names. A shared component cannot reference both. A palette edit in DESIGN.md propagates through zero surfaces automatically.
  2. Split identity — Console and hatch (rocky.erid.tech and hatch.devarno.cloud) are two operator citizens under Erid. They currently look like two products because their token vocabularies were assembled independently.

Decision

Extract the Mars palette (plus typography, spacing, radii, animations, utilities) into a package at @rocky-hq/design-tokens, published to GitHub Packages under the same pattern as @rocky-hq/contracts (publishConfig.registry: npm.pkg.github.com, access: restricted, subpath exports).

Package publishes three artifacts, each with a subpath export:

Consumers pick the subpaths they need. Erid brand apps pull /css only (no Tailwind, no animations, no status colors — per the erid "No Status Color Rule"). Console and hatch pull /css + /tailwind and consume the full palette.

Locked decisions

1. Package lives at parent-root design-tokens/ as a draft, promoted to submodule after this ADR merges

Contracts is a git submodule at contracts/. Design-tokens will follow the same pattern (external repo rocky-hq/design-tokens, submodule-mounted at parent-root). Creating the external repo requires a push to GitHub, which is out of scope for this ADR. Draft contents live at parent design-tokens/ until the submodule cutover PR — this keeps the source of truth reviewable in-band without pretending the package is already published.

The contracts/CLAUDE.md push-down policy applies to design-tokens/ from day one: build/test instructions live in the package, not the parent.

2. Token names drop the --mars-* prefix

Console currently prefixes every Mars token with --mars-* (--mars-void, --mars-oxide) and bridges to --color-void via @theme inline. Erid apps use bare names (--void, --oxide).

The package emits bare names (--void-deep, --oxide, --regolith). Rationale:

Console migrates by dropping the prefix in globals.css and updating the @theme inline bridge to read bare names. No .tsx changes required — the Tailwind class names (bg-void, text-oxide) do not change.

3. OKLCH is the primary color space; hex is a fallback

DESIGN.md specifies OKLCH values (oklch(11% 0.04 268) etc.) and the "Tinted-Neutral Rule" (no #000, no #fff). Package emits OKLCH as the primary value. Where hex is retained for a comment or fallback, chroma is verified to be tinted toward the brand hue.

Modern browser support (Chrome 111+, Safari 15.4+, Firefox 113+) covers every operator target. No legacy shim needed.

4. Status colors ship in the package but the erid brand apps do not consume them

Console/hatch operator surfaces render status pills (--status-nominal, --status-warning, --status-critical, --status-offline). Erid brand apps deliberately do not — the "No Status Color Rule" in DESIGN.md (Section 2) reserves status vocabulary for the operator console.

The package includes status colors in /css so operator surfaces can consume them, and documents the "brand surfaces don't import status" convention in the README. The alternative — a separate /css-operator and /css-brand — is a layer boundary that requires enforcement machinery to be worth its weight. A README convention plus a CSS-lint rule is enough for now; split subpaths if the convention starts to leak.

5. Typography tokens name families and scales; font loading stays app-local

Package emits --font-sans = "Inter, system-ui, sans-serif" and --font-mono = "JetBrains Mono, ui-monospace, monospace", plus a type scale (--type-display, --type-headline, --type-body, --type-scene, --type-label).

Font loading — Next.js next/font/google calls, @font-face blocks, preloading — stays in each consumer app. The package does not ship font files or fetch calls. Rationale: font loading is coupled to the framework (Next.js Font, Astro, Remix all differ), and shipping fonts from the tokens package would force a runtime choice on every consumer.

6. DataTable / Badge / Button primitives are NOT extracted in this ADR

Console has 3+ custom badge components (RegistryBadge, EarlyStopBadge, FingerprintBadge, HatchScopeBadge) and 3+ custom table components (AuditLogTable, HatchFeedTable, TablePanel). Hatch has its own badge and table primitives.

Consolidating these is a natural next step, but this ADR scopes to tokens only. Component extraction lands in a follow-up ADR (fe-c-primitive-components) once the token layer is in production and the semantic vocabulary is stable. Premature abstraction of a Badge component before the token names settle would guarantee a rewrite.

7. No animations helper library, no CSS-in-JS, no design-tokens build step for consumers

Package ships static CSS + JSON. No tsc, no PostCSS, no Rollup. Consumers @import the CSS or import the JSON. This mirrors the contracts pattern (contracts ships prebuilt dist/; design-tokens has no build to run).

animations.css ships keyframes (dustDrift, pulseSlow, pulseCritical, telemetryFlicker) as static CSS. Consumers reference them by name; the package does not enforce reduced-motion suppression (that is a consumer concern per DESIGN.md's Accessibility section).

Migration plan

Console (rocky-hq/console)

  1. Add @rocky-hq/design-tokens as a local file:../design-tokens dependency in package.json (draft phase).
  2. Replace globals.css:112-134 (raw --mars-* palette) with @import "@rocky-hq/design-tokens/css";.
  3. Rewrite globals.css:14-84 @theme inline block to read bare names (var(--void) instead of var(--mars-void)), or @import "@rocky-hq/design-tokens/tailwind"; if the bridge maps cleanly.
  4. Verify: npm run build clean, npm run test:run clean, Playwright E2E (npm run e2e) clean. Visual parity via playwright screenshot on all 11 top-level routes before/after.

Zero .tsx changes expected — Tailwind class names (bg-void, text-oxide-bright) preserved by name.

Erid apps (apps/erid-apex, apps/erid-docs, apps/erid-coming-soon)

  1. Add dependency same as Console.
  2. Replace each app's raw palette block in globals.css with @import "@rocky-hq/design-tokens/css";.
  3. Keep app-local styling (.citizen { ... }, .scene { ... }, etc.) — those are surface-specific, not tokens.
  4. Verify: build clean, npm run build produces bit-identical out/ for erid-apex (static export).

Hatch (devarno-cloud/hatch, external)

Cannot land in this rocky-hq PR — hatch lives in a separate repo. This ADR documents the target state:

  1. Hatch adds @rocky-hq/design-tokens as a package dependency (npm workspace or GitHub Packages install, depending on hatch's package.json).
  2. Hatch's globals.css (or equivalent) replaces its raw palette block with the package import.
  3. Hatch keeps its dust-muted/dust-faint/void-surface/void-elevated semantic tokens locally IF they diverge from the package's semantic layer; otherwise consumes the shared ones directly.

Hatch alignment PR opened after rocky-hq/design-tokens is a published submodule.

Contracts alignment

@rocky-hq/contracts is unrelated to design-tokens and stays untouched. The two packages coexist as siblings under the pantheon npm scope.

What this ADR does NOT lock

Acceptance (this ADR)

Sources