Skip to content

TECH — id-119: Design-system config-readiness

TECH — id-119: Design-system config-readiness

Section titled “TECH — id-119: Design-system config-readiness”
  • Task: ID-119. Predecessor: RESEARCH.md (the Warm Meridian audit, verbatim).
  • Process: light specs (no PRODUCT) — this TECH is self-contained: goals, invariants, per-finding design, deterministic evidence, sequencing, and the decisions reserved for Liam.
  • Author basis: deterministic extraction of app/globals.css (script-verified counts, not estimates).

Goal: make a reskin / rebrand a configuration operation, not a literal-hunt. Concretely: collapse the hand-maintained colour-literal wall onto a small primitive layer, lift application-domain tokens out of the core design system, adopt one token-naming convention, and route the hardcoded product-name string through the existing branding config.

Non-goals (deferred to /design-sync): choosing brand direction A/B, the typography identity axis (audit finding 4), the logo/wordmark, and any deliberate recolour. ID-119 changes the structure of the token system and must reproduce current colours within tolerance — it is a refactor, not a redesign. The brand-colour injection path (default.json → Zod → buildBrandStyleProps, auto-derived foreground/dark/ring) is exemplary and is left untouched.

  1. B-1 Visual parity. After F1+F2+F3, every rendered colour is identical to main except for an explicitly-approved snap list (see §3.5). Verified by computed-style diff + visual snapshot.
  2. B-2 Zero utility churn (default path). Generated Tailwind utility class names (bg-bid-won, text-governance-approved, …) are unchanged; component files compile without edit unless an opt-in rename is approved (F3 Option B).
  3. B-3 Single source per colour. No raw status/domain OKLCH literal is typed more than once; tuning a colour is a one-primitive edit that ripples to light + dark.
  4. B-4 Core ⊅ domain. The core token file contains no product-taxonomy token (bid/governance/entity/domain/relevance/template/phase/tag/quality/…).
  5. B-5 Product name from config. No user-facing surface, AI prompt, or export hardcodes the product-name string; all read it from BRANDING / CLIENT_CONFIG. The value stays “Knowledge Hub” (no rename in this Task).

3.1 Evidence (script-verified, app/globals.css)

Section titled “3.1 Evidence (script-verified, app/globals.css)”
BucketLiteral defsTargetFidelity
Total --color-* / --domain-* literal defs (light+dark)378
Categorical badge defs (entity/domain/relevance), 16 distinct hues / 21 cats92 (24%)1 formula × hue-var listexact
Neutral defs (C < 0.03)61reference existing --stone-* rampexact
Chromatic status defs225~6 status ramps × role-stepsexact for duplicates; bounded snaps
Bespoke shadcn defaults (--color-star, --color-priority-*, non-integer hue)4keep as literalsn/a

Top duplications eliminated: success-green oklch(0.55 0.15 160) ×11, green-tint oklch(0.93 0.04 160) ×12, amber oklch(0.6 0.14 70) ×10, red-tint oklch(0.95 0.03 20) ×9 — each currently re-typed across freshness / confidence / governance / bid / quality / verified / tag / template, then again in .dark.

Add, above the semantic block in :root:

/* Functional status ramps — role steps reproduce existing literals */
--green-solid: oklch(0.55 0.15 160);
--green-solid-deep: oklch(0.5 0.15 160);
--green-border: oklch(0.8 0.06 160);
--green-tint: oklch(0.93 0.04 160);
--amber-solid: oklch(0.6 0.14 70);
--amber-border: oklch(0.82 0.06 70);
--amber-tint: oklch(0.97 0.02 70);
--amber-tint-strong: oklch(0.93 0.04 70);
--red-solid: oklch(0.55 0.2 20);
--red-solid-deep: oklch(0.5 0.2 20);
--red-border: oklch(0.82 0.06 20);
--red-tint: oklch(0.95 0.03 20);
--blue-solid: oklch(0.55 0.12 240);
--blue-border: oklch(0.8 0.06 240);
--blue-tint: oklch(0.95 0.02 240);
--violet-solid: oklch(0.55 0.15 290);
--violet-border: oklch(0.8 0.06 290);
--violet-tint: oklch(0.95 0.02 290);
--teal-solid: oklch(0.55 0.15 195);
--teal-border: oklch(0.8 0.06 195);
--teal-tint: oklch(0.93 0.04 195);

.dark redefines the same ramp names with the dark values (e.g. --green-solid: oklch(0.7 0.13 160), --green-tint: oklch(0.25 0.06 160)). Because status tokens reference the ramp, the per-token .dark block (currently ~189 lines) collapses to the ramp redefinitions only — dark mode becomes “swap the ramps” (B-3).

3.3 Categorical badge formula (entity / domain / relevance)

Section titled “3.3 Categorical badge formula (entity / domain / relevance)”

These badges already follow a strict formula with only hue varying. Replace the per-category literals with a hue variable consumed by oklch():

--hue-entity-person: 350;
--hue-entity-organisation: 240; /* … 21 per-category vars, 16 distinct values … */
--color-entity-person-bg: oklch(0.93 0.04 var(--hue-entity-person));
--color-entity-person-text: oklch(0.35 0.12 var(--hue-entity-person));
/* .dark: oklch(0.25 0.06 var(--hue-…)) / oklch(0.8 0.1 var(--hue-…)) */

oklch() accepts custom-property components in all current evergreen targets; the executor must add a smoke check (computed style resolves) + confirm Tailwind v4 @theme inline passes the value through. Reskinning a category is then a one-number edit. (Token names preserved → B-2.)

Ramp primitives are raw values, not registered as utilities. Status/domain tokens keep their existing @theme inline registrations unchanged, so bg-*/text-* utilities are byte-identical (B-2).

3.5 Fidelity & the snap decisions (for Liam)

Section titled “3.5 Fidelity & the snap decisions (for Liam)”

The 92 categorical + 61 neutral + all exact-duplicate chromatic defs reproduce exactly. The only fidelity decisions are near-neighbour snaps inside the amber family (it spans hues 45/55/65/70/85 and several close L/C pairs). Rule: the executor produces the complete snap list (old → new with ΔL/ΔC/ΔH); no snap exceeding ΔL 0.03 / ΔC 0.03 / ΔH 5 is applied without an explicit entry on the decision list. Liam approves the snap list at the per-subtask gate. Everything else is exact.

4. F2 — split core vs application-domain tokens

Section titled “4. F2 — split core vs application-domain tokens”
  • Domain (move out): --color-bid-*, --color-governance-*, --domain-*, --color-entity-*, --color-relevance-*, --color-template-*, --color-phase-*, --color-tag-*, --color-quality-*, --color-freshness-*, --color-confidence-*, --color-priority-tier-*, --color-source-si-*, --color-draft-badge-*, --color-verified*, --hue-entity-*.
  • Core (keep): --stone-*, shadcn semantic (--background--ring), --success/--destructive, the F1 ramps, --chart-*, --radius*, --font-*, --spacing-*, the a11y vars, --shadow-*.
  • Ambiguous → reasoned call: --color-status-*, --color-priority-*, --color-star, --color-review-*, --color-progress*, --color-highlight*, --color-action-publish → these are generic-enough UI status; keep in core. (Rationale recorded in the move subtask.)
  • Target: new app/styles/domain-tokens.css holding the domain tokens’ :root + .dark definitions and their @theme inline registrations, @imported from app/globals.css.
  • Critical wiring risk: Tailwind v4 must still see the @theme block. The executor verifies @import placement (relative to @import 'tailwindcss' and the @theme source order) actually generates the domain utilities — a wrong order silently drops every domain utility (gate: grep the built CSS for a sample utility). Depends on F1 landing first (domain file references ramps).

Convention (the deliverable): shadcn semantic tokens stay bare (--background, --primary); the new ramps are scoped --<family>-<role>; application status/domain tokens retain the established --color-* / --domain-* public contract.

Why not mass-rename the --color-* contract: it is consumed two ways —

ConsumerCountRenaming the CSS-var definition…
Tailwind utility classes (bg-bid-won) in components87 files…does not change them if the @theme RHS is updated (utility name is decoupled).
Direct var(--color-*) / var(--domain-*) in inline styles18 files / 54 refsdoes break these regardless — they reference the literal var name.

Options for Liam:

  • A — convention-only (recommended, ~0 churn): publish the convention, apply it to the new ramps + F2 domain file; leave the --color-* status contract intact. Delivers the audit’s intent (one coherent convention going forward) at no component churn.
  • B-lite — drop redundant --color- on definitions, keep utility names: touches the 18 inline-var() files (54 refs). Moderate, cosmetic.
  • B — full rename incl. utilities: 87 files. High churn; better folded into the actual rebrand.

Decision (Liam, this session): B-lite — adopt the convention and drop the redundant --color- prefix on status/domain definitions (keeping utility class names), touching the 18 inline-var() files (54 refs). The full utility-class rename (Option B, ~87 files) is tracked as bl-349, to bundle with the rebrand visual pass. F3 runs after F1+F2.

Evidence: “Knowledge Hub” in 33 source files — app/ 3, components/ 7, lib/ 40 (incl. AI skills 6, procurement exports 3, plus the config source-of-truth), scripts/ 10.

  • Keep (source of truth): lib/branding/clients/default.json, lib/client-config.ts.
  • UI (server): import BRANDING.productName / productShortName directly.
  • UI (client components): verify BRANDING is client-safe (it imports clientEnv + a window-guarded fs require) — if not directly importable client-side, thread via a branding context/provider (check app/layout.tsx for an existing one before adding).
  • AI skill prompts (lib/ai/skills/*.md, 6): introduce a {PRODUCT_NAME} placeholder resolved at prompt-assembly, mirroring the existing {CLIENT_PRODUCT_NAME} / buildDisambiguationBlock + .replaceAll mechanism in lib/ai/classify.ts. Risk: alters prompt text → re-run classification eval to confirm no regression.
  • Exports (lib/procurement/procurement-export-{xlsx,docx}.ts, 3): read from BRANDING. Risk: these may have golden-file / parity tests (__tests__/fixtures/qa-docx-parity/) — update fixtures intentionally, do not let output drift silently.
  • scripts/ (10) + comments: leave (tooling / prose), unless trivially a user-facing CLI string.

Independent of F1–F3 → can run in parallel (subject to §7 isolation note).

  • Order: F1a (additive primitives) → F1b (rewire + collapse .dark + snap list) → F2 (extract domain file) → F3 (convention, Option A) ; Strings parallel ; wrap (full regression + snapshot + token guidance doc) last.
  • Worktree isolation is currently unreliable (native Agent isolation:"worktree" falls back to the shared checkout — observed S383). The F1→F3 chain is inherently serial anyway; serialise the CSS-touching executors; Strings may interleave only if isolation is confirmed, else serialise too.
  • GitNexus / ast-dataflow: CSS custom properties are not graph-indexed — use literal greps for the var sweeps; use GitNexus impact only for the TS-side string consolidation.
  • Per-subtask gate: every CSS subtask ends with bun build + a computed-style/visual diff vs main; the Checker verifies B-1..B-5 and the snap list.
  1. F1 snap list — approve the bounded amber near-neighbour snaps (produced in F1b). (Recommend: approve within ΔL/ΔC 0.03, ΔH 5 — decided at F1b time.)
  2. F3 scope — ✅ DECIDED: B-lite (18 inline-var() files). Full utility rename (Option B, 87 files) → bl-349.
  3. Strings depth — ✅ DECIDED: all three (UI + AI-prompt + exports), with the eval + golden-fixture gates.