From 449353150d6f835463232bbf7cd4792e4f097441 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Mon, 13 Jul 2026 18:49:46 +0100 Subject: [PATCH] feat(lint): make contrast check blocking + fix dark subtle-text AA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review on #7009: the contrast check was warn-only and opt-in, so it would never catch regressions. Enforce it here (the follow-up PR): - theme-lint.mjs `contrast` mode now exits non-zero on any sub-floor pair - wired into `task frontend:lint:colors` (runs in the blocking lint gate) - fixed the two failing pairs: --c-text-subtle on dark/portal-dark surfaces (3.36/3.67 → 4.74/5.18) by adding --p-zinc-250 and pointing dark subtle at it All text-on-surface / on-primary pairs now clear WCAG AA per theme. --- .taskfiles/frontend.yml | 3 ++- frontend/editor/scripts/lint/theme-lint.mjs | 17 +++++++++++++---- frontend/editor/src/core/theme/README.md | 4 ++-- frontend/editor/src/core/theme/colors.css | 4 ++-- frontend/editor/src/core/theme/primitives.css | 1 + 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index 1b887f42e5..e6a0d26661 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -212,9 +212,10 @@ tasks: - node editor/scripts/lint/theme-lint.mjs - node editor/scripts/lint/theme-lint.mjs css-colors - node editor/scripts/lint/theme-lint.mjs code-colors + - node editor/scripts/lint/theme-lint.mjs contrast contrast: - desc: "Report low-contrast theme token pairs (warning only, never blocks)" + desc: "Check theme token pairs clear WCAG AA contrast per theme" deps: [install] cmds: - node editor/scripts/lint/theme-lint.mjs contrast diff --git a/frontend/editor/scripts/lint/theme-lint.mjs b/frontend/editor/scripts/lint/theme-lint.mjs index e7adabceec..d9c31ab96e 100644 --- a/frontend/editor/scripts/lint/theme-lint.mjs +++ b/frontend/editor/scripts/lint/theme-lint.mjs @@ -14,7 +14,9 @@ // contexts; `// theme-allow-color` opt-out; // exempt PATHS for rendering/vendor/config). // Scope: all editor/src. (blocking) -// node theme-lint.mjs contrast warn-only WCAG contrast report (never blocks) +// node theme-lint.mjs contrast enforce: text-on-surface / on-primary pairs +// clear WCAG AA per theme (default accent). +// (blocking) // // Structural black / white / transparent (shadows, scrims) are always allowed. @@ -307,9 +309,10 @@ function reportContrast() { } console.log( warnings - ? `⚠ ${warnings} pair(s) below floor — review, not blocking.` + ? `✖ ${warnings} pair(s) below floor.` : "✓ all pairs clear their floor.", ); + return warnings; } // ── css-colors (blocking): no hardcoded colour in ANY source .css ──────────── @@ -453,8 +456,14 @@ function checkCodeColors() { // ── CLI ────────────────────────────────────────────────────────────────────── if (process.argv.includes("contrast")) { - reportContrast(); - process.exit(0); // never blocks + const below = reportContrast(); + if (below) { + console.error( + `\nRaise the failing token's lightness (or its surface's) until every pair clears WCAG AA (4.5 normal / 3.0 on-primary).\n`, + ); + process.exit(1); + } + process.exit(0); } if (process.argv.includes("code-colors")) { diff --git a/frontend/editor/src/core/theme/README.md b/frontend/editor/src/core/theme/README.md index de414c681b..ecc6ade3d7 100644 --- a/frontend/editor/src/core/theme/README.md +++ b/frontend/editor/src/core/theme/README.md @@ -63,9 +63,9 @@ One file: `editor/scripts/lint/theme-lint.mjs` (no baseline). Run via `task fron - **Default (blocking):** enforces "literals only in `primitives.css`; everything else in `core/theme/` references tokens; no duplicate primitives." Scope: `core/theme/`. - **`css-colors` (blocking):** enforces **no hardcoded colour in any source `.css`** across `editor/src` — `primitives.css` (the literal home) and generated `output.css` are the only exemptions. The file list comes from `git ls-files` (not a directory walk), and comments/structural black-white-transparent are ignored. - **`code-colors` (blocking):** enforces **no hardcoded colour in TS/TSX DOM code**. TS/TSX can't be zeroed like CSS (canvas/PDF/pdfium rendering, colour maths, colour pickers, vendor brand and self-contained docs legitimately need numeric colour), so it's **default-deny with layered exemptions**: (1) structural black/white/transparent; (2) detected safe contexts on the line — `var(--x, …)` / `readColor(…)` fallbacks, canvas assignments (`fillStyle`/`ctx.`…), pdf-lib `rgb(0..1)`, and an explicit **`// theme-allow-color `** opt-out; (3) exempt **paths** for whole rendering/vendor/config/illustration areas (viewer/annotation, `*Thumbnail`/`*Overlay`, `*Pdf*`, colour pickers, `onboarding/`, `mantineTheme`/`theme.ts`, tests, stories, type decls, marketing banners). A raw colour anywhere else fails. Adding a new colour to a normal component → blocked; genuine exceptions get the marker. -- **`node theme-lint.mjs contrast`** (task `frontend:contrast`, non-blocking): WCAG contrast report for text-on-surface / on-primary pairs per theme. +- **`contrast` (blocking):** fails if any text-on-surface / on-primary pair misses its WCAG AA floor (4.5 normal, 3.0 on-primary) in any theme, resolved for the default accent. Custom accents are handled at runtime by the `customPrimary.ts` guardrails. -All three blocking modes run under `task frontend:lint:colors`. Source **CSS is at zero** hardcoded colour and **TS/TSX DOM code is clean** (only the exempt rendering/vendor/config areas hold literals). Keep it there — add the literal to `primitives.css` and reference `var(--p-…)`, or for an unavoidable TS/TSX literal add `// theme-allow-color `. +All four blocking modes run under `task frontend:lint:colors`. Source **CSS is at zero** hardcoded colour and **TS/TSX DOM code is clean** (only the exempt rendering/vendor/config areas hold literals). Keep it there — add the literal to `primitives.css` and reference `var(--p-…)`, or for an unavoidable TS/TSX literal add `// theme-allow-color `. ## Gotchas diff --git a/frontend/editor/src/core/theme/colors.css b/frontend/editor/src/core/theme/colors.css index 6bfca0034b..0bbb046e5b 100644 --- a/frontend/editor/src/core/theme/colors.css +++ b/frontend/editor/src/core/theme/colors.css @@ -72,7 +72,7 @@ html[data-app-theme="midnight"] { --c-text: var(--p-zinc-100); --c-text-muted: var(--p-zinc-200); - --c-text-subtle: var(--p-zinc-300); + --c-text-subtle: var(--p-zinc-250); --c-text-on-primary: var(--p-white); --c-border: var(--p-zinc-650); @@ -209,7 +209,7 @@ html[data-app-theme="custom"][data-mantine-color-scheme="dark"] { /* Neutral text / borders / overlay (not accent-tinted). */ --c-text: var(--p-zinc-100); --c-text-muted: var(--p-zinc-200); - --c-text-subtle: var(--p-zinc-300); + --c-text-subtle: var(--p-zinc-250); --c-border-strong: var(--p-zinc-500); --c-overlay: rgba(0, 0, 0, 0.6); diff --git a/frontend/editor/src/core/theme/primitives.css b/frontend/editor/src/core/theme/primitives.css index 459fab88d0..3b0cfab82f 100644 --- a/frontend/editor/src/core/theme/primitives.css +++ b/frontend/editor/src/core/theme/primitives.css @@ -27,6 +27,7 @@ --p-zinc-500: #3f3f46; --p-zinc-400: #52525b; --p-zinc-300: #71717a; + --p-zinc-250: #8a8a93; --p-zinc-200: #a1a1aa; --p-zinc-100: #f4f4f5; --p-blue-400: #60a5fa;