From de4b0fdcb3f624022874bf9b46d5311062f8abd6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 9 May 2026 08:23:05 -0400 Subject: [PATCH 1/3] Fix dark card visibility, Quarto $secondary, and expand contrast matrix Follow-up to #25 addressing review findings: - **Dark mode card surfaces visible.** `--card`/`--popover`/`--muted` bumped from #131820 (1.08:1 vs --background) to #1A2030 (1.19:1), and `--border`/`--input` bumped from #1E293B (1.32:1 / 1.11:1 on card) to #334155 (1.87:1 / 1.57:1). Cards and their borders are now distinguishable at glance instead of nearly invisible. - **Quarto `$secondary` fixed.** Was resolving to `--secondary-foreground` (#101828, near-black). Bootstrap's `$secondary` is a fill color, so paper renders previously got a near-black .btn-secondary background. Now uses `--secondary` (#F2F4F7), the light gray surface. - **Contrast matrix coverage.** Added pairs for: `--ring` on `--background` light + dark (WCAG SC 1.4.11 non-text 3:1 for focus indicators), dark `--primary-foreground` on `--primary`, dark `--destructive-foreground` on `--destructive`, and the default link color (teal-600) on background. - **`pretest` hook.** `bun run test` now regenerates `tokens.css` / `quarto.scss` before vitest runs, so token edits without an explicit `bun run generate-tokens` no longer surface as a misleading drift failure. Co-Authored-By: Claude Opus 4.7 (1M context) --- changelog.d/contrast-matrix-coverage.added.md | 1 + changelog.d/dark-card-visibility.fixed.md | 1 + changelog.d/quarto-secondary-fill.fixed.md | 1 + package.json | 1 + scripts/generate-css.ts | 5 +- src/theme/quarto.scss | 2 +- src/theme/tokens.css | 14 ++-- src/theme/tokens.ts | 65 +++++++++++++++++-- 8 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 changelog.d/contrast-matrix-coverage.added.md create mode 100644 changelog.d/dark-card-visibility.fixed.md create mode 100644 changelog.d/quarto-secondary-fill.fixed.md diff --git a/changelog.d/contrast-matrix-coverage.added.md b/changelog.d/contrast-matrix-coverage.added.md new file mode 100644 index 0000000..2eb959b --- /dev/null +++ b/changelog.d/contrast-matrix-coverage.added.md @@ -0,0 +1 @@ +Contrast matrix now asserts the focus ring on background (light + dark, WCAG SC 1.4.11 non-text 3:1), dark-mode `primary-foreground` on `primary`, dark-mode `destructive-foreground` on `destructive`, and the default link color (teal-600) on background. Generator now runs before `bun run test` via a `pretest` hook so token edits without `bun run generate-tokens` no longer get a misleading drift failure. diff --git a/changelog.d/dark-card-visibility.fixed.md b/changelog.d/dark-card-visibility.fixed.md new file mode 100644 index 0000000..8d57b92 --- /dev/null +++ b/changelog.d/dark-card-visibility.fixed.md @@ -0,0 +1 @@ +Dark mode card surfaces are now visible. `--card`/`--popover`/`--muted` bumped from `#131820` (1.08:1 vs background — visually invisible) to `#1A2030` (1.19:1), and `--border`/`--input` bumped from `#1E293B` (1.32:1 vs background, 1.11:1 vs card) to `#334155` (1.87:1 / 1.57:1) so card surfaces and their borders are clearly distinguishable. diff --git a/changelog.d/quarto-secondary-fill.fixed.md b/changelog.d/quarto-secondary-fill.fixed.md new file mode 100644 index 0000000..1457a57 --- /dev/null +++ b/changelog.d/quarto-secondary-fill.fixed.md @@ -0,0 +1 @@ +Quarto SCSS `$secondary` now resolves to the secondary surface fill (`#F2F4F7`) instead of the secondary foreground (`#101828`). Bootstrap's `$secondary` is a fill color (e.g. `.btn-secondary` background), so paper renders previously got a near-black secondary button where Bootstrap expects a light gray. diff --git a/package.json b/package.json index 982e7ff..35b4394 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "generate-tokens": "tsx scripts/generate-css.ts", "prebuild": "tsx scripts/generate-css.ts", "build": "vite build && mv dist/ui-kit.css dist/styles.css && tsc -p tsconfig.build.json --emitDeclarationOnly", + "pretest": "tsx scripts/generate-css.ts", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", diff --git a/scripts/generate-css.ts b/scripts/generate-css.ts index 4189915..d6eb297 100644 --- a/scripts/generate-css.ts +++ b/scripts/generate-css.ts @@ -277,7 +277,10 @@ function buildQuartoScss(): string { "/*-- scss:defaults --*/", "", `$primary: ${rootColorsLight["--primary"]};`, - `$secondary: ${rootColorsLight["--secondary-foreground"]};`, + // Bootstrap's $secondary is a *fill* color (e.g. .btn-secondary background), + // not a foreground. Use --secondary, the light gray surface, so paper + // buttons match the dashboard's secondary surface, not its dark text. + `$secondary: ${rootColorsLight["--secondary"]};`, `$success: ${semanticFills.success};`, `$warning: ${semanticFills.warning};`, `$danger: ${semanticFills.error};`, diff --git a/src/theme/quarto.scss b/src/theme/quarto.scss index b6cad9c..e571533 100644 --- a/src/theme/quarto.scss +++ b/src/theme/quarto.scss @@ -15,7 +15,7 @@ /*-- scss:defaults --*/ $primary: #2C7A7B; -$secondary: #101828; +$secondary: #F2F4F7; $success: #22C55E; $warning: #FEC601; $danger: #EF4444; diff --git a/src/theme/tokens.css b/src/theme/tokens.css index 91d42dc..2d353b9 100644 --- a/src/theme/tokens.css +++ b/src/theme/tokens.css @@ -124,7 +124,7 @@ --secondary-foreground: #F5F5F5; /* Muted */ - --muted: #131820; + --muted: #1A2030; --muted-foreground: #9CA3AF; /* Accent */ @@ -135,17 +135,17 @@ --destructive: #F87171; --destructive-foreground: #0B0E14; - /* Chrome */ - --border: #1E293B; - --input: #1E293B; + /* Chrome. --border bumped from #1E293B (1.32:1 on background, 1.11:1 on card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on card). */ + --border: #334155; + --input: #334155; --ring: #38B2AC; - /* Card */ - --card: #131820; + /* Card. Bumped from #131820 (1.08:1 on background — invisible) to #1A2030 (1.19:1 on bg). Together with the bumped --border, the card surface is now clearly distinguishable. */ + --card: #1A2030; --card-foreground: #F5F5F5; /* Popover */ - --popover: #131820; + --popover: #1A2030; --popover-foreground: #F5F5F5; /* Charts (lifted up the brand scale for dark backgrounds) */ diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index d4da38a..d469e5a 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -212,7 +212,7 @@ const darkSections: CssSection[] = [ { name: "Muted", declarations: { - "--muted": "#131820", + "--muted": "#1A2030", "--muted-foreground": "#9CA3AF", }, }, @@ -231,24 +231,29 @@ const darkSections: CssSection[] = [ }, }, { - name: "Chrome", + name: + "Chrome. --border bumped from #1E293B (1.32:1 on background, 1.11:1 on " + + "card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on card).", declarations: { - "--border": "#1E293B", - "--input": "#1E293B", + "--border": "#334155", + "--input": "#334155", "--ring": "#38B2AC", }, }, { - name: "Card", + name: + "Card. Bumped from #131820 (1.08:1 on background — invisible) to " + + "#1A2030 (1.19:1 on bg). Together with the bumped --border, the card " + + "surface is now clearly distinguishable.", declarations: { - "--card": "#131820", + "--card": "#1A2030", "--card-foreground": "#F5F5F5", }, }, { name: "Popover", declarations: { - "--popover": "#131820", + "--popover": "#1A2030", "--popover-foreground": "#F5F5F5", }, }, @@ -672,6 +677,25 @@ export const contrastPairs: readonly ContrastPair[] = [ minRatio: 4.5, mode: "light", }, + { + // SC 1.4.11 (Non-text Contrast, AA): focus indicators must clear 3:1 + // against the adjacent background. The ring sits on --background, so we + // pin its visibility there. + description: "ring on background (light, non-text)", + fg: rootColorsLight["--ring"], + bg: rootColorsLight["--background"], + minRatio: 3, + mode: "light", + }, + { + // Default link styling resolves to teal-600. Asserted as small-text AA so + // links stay legible inside paragraph copy. + description: "link (teal-600) on background (light)", + fg: palette.teal[600], + bg: rootColorsLight["--background"], + minRatio: 4.5, + mode: "light", + }, // ----- Dark mode ----- { description: "foreground on background (dark)", @@ -729,4 +753,31 @@ export const contrastPairs: readonly ContrastPair[] = [ minRatio: 4.5, mode: "dark", }, + { + // Dark-mode primary fill (teal-400) is light enough that we can ink it + // with near-black text. Pin the AA guarantee. + description: "primary-foreground on primary (dark)", + fg: rootColorsDark["--primary-foreground"], + bg: rootColorsDark["--primary"], + minRatio: 4.5, + mode: "dark", + }, + { + // Same pattern for destructive: dark-mode destructive is red-400, so we + // ink with near-black to clear AA. + description: "destructive-foreground on destructive (dark)", + fg: rootColorsDark["--destructive-foreground"], + bg: rootColorsDark["--destructive"], + minRatio: 4.5, + mode: "dark", + }, + { + // Focus indicator must clear SC 1.4.11 (3:1 non-text) against the + // adjacent background in dark mode too. + description: "ring on background (dark, non-text)", + fg: rootColorsDark["--ring"], + bg: rootColorsDark["--background"], + minRatio: 3, + mode: "dark", + }, ]; From ca3c4497a6a12d6eddac91bedadf7d138a7128b8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 9 May 2026 08:29:50 -0400 Subject: [PATCH 2/3] Address review: revert pretest hook, fix old --border ratio comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pretest hook (added in this PR) silently regenerates tokens.css before vitest runs, masking the drift test it was added to make less annoying. The test was specifically designed to catch hand-edits to the generated CSS; under pretest, those edits get clobbered before the assertion runs and CI goes green on drift. The error message ("run \`bun run generate-tokens\`") is already directive, so revert the hook and trust the existing UX. Also fix the inaccurate "1.11:1 on card" claim in the dark-mode --border comment — the actual ratio of #1E293B on #131820 is 1.22:1, and add a note next to the light-mode --ring documenting the SC 1.4.11 dependency (currently 3.51:1 on white, only ~0.5 above the 3:1 floor). Co-Authored-By: Claude Opus 4.7 (1M context) --- changelog.d/contrast-matrix-coverage.added.md | 2 +- package.json | 1 - src/theme/tokens.css | 2 +- src/theme/tokens.ts | 8 ++++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog.d/contrast-matrix-coverage.added.md b/changelog.d/contrast-matrix-coverage.added.md index 2eb959b..008c2c9 100644 --- a/changelog.d/contrast-matrix-coverage.added.md +++ b/changelog.d/contrast-matrix-coverage.added.md @@ -1 +1 @@ -Contrast matrix now asserts the focus ring on background (light + dark, WCAG SC 1.4.11 non-text 3:1), dark-mode `primary-foreground` on `primary`, dark-mode `destructive-foreground` on `destructive`, and the default link color (teal-600) on background. Generator now runs before `bun run test` via a `pretest` hook so token edits without `bun run generate-tokens` no longer get a misleading drift failure. +Contrast matrix now asserts the focus ring on background (light + dark, WCAG SC 1.4.11 non-text 3:1), dark-mode `primary-foreground` on `primary`, dark-mode `destructive-foreground` on `destructive`, and the default link color (teal-600) on background. diff --git a/package.json b/package.json index 35b4394..982e7ff 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "generate-tokens": "tsx scripts/generate-css.ts", "prebuild": "tsx scripts/generate-css.ts", "build": "vite build && mv dist/ui-kit.css dist/styles.css && tsc -p tsconfig.build.json --emitDeclarationOnly", - "pretest": "tsx scripts/generate-css.ts", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", diff --git a/src/theme/tokens.css b/src/theme/tokens.css index 2d353b9..1a43f8f 100644 --- a/src/theme/tokens.css +++ b/src/theme/tokens.css @@ -135,7 +135,7 @@ --destructive: #F87171; --destructive-foreground: #0B0E14; - /* Chrome. --border bumped from #1E293B (1.32:1 on background, 1.11:1 on card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on card). */ + /* Chrome. --border bumped from #1E293B (1.32:1 on background, 1.22:1 on card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on card). */ --border: #334155; --input: #334155; --ring: #38B2AC; diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index d469e5a..92e0789 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -78,6 +78,9 @@ const lightSections: CssSection[] = [ }, }, { + // --ring at teal-500 (#319795) clears WCAG SC 1.4.11 (3:1 non-text) + // against white at 3.51:1 — only ~0.5 above the floor. If you nudge the + // ring lighter (toward teal-400), re-verify against the contrast matrix. name: "Chrome", declarations: { "--border": "#E2E8F0", @@ -232,8 +235,9 @@ const darkSections: CssSection[] = [ }, { name: - "Chrome. --border bumped from #1E293B (1.32:1 on background, 1.11:1 on " + - "card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on card).", + "Chrome. --border bumped from #1E293B (1.32:1 on background, 1.22:1 " + + "on card — visually invisible) to #334155 (1.87:1 on bg, 1.57:1 on " + + "card).", declarations: { "--border": "#334155", "--input": "#334155", From f84f17b2536b185c834ad34da89d5e255265247b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 9 May 2026 08:33:07 -0400 Subject: [PATCH 3/3] =?UTF-8?q?Fix=20changelog:=201.11=20=E2=86=92=201.22?= =?UTF-8?q?=20on=20card=20(matches=20tokens.ts=20comment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review caught that the dark-card-visibility changelog fragment still claimed the old --border was "1.11:1 vs card" — the same number that was wrong in tokens.ts and got fixed there last commit but missed here. The actual #1E293B-on-#131820 ratio is 1.22:1. Tracking issue for the related --input SC 1.4.11 fail filed as #27. Co-Authored-By: Claude Opus 4.7 (1M context) --- changelog.d/dark-card-visibility.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/dark-card-visibility.fixed.md b/changelog.d/dark-card-visibility.fixed.md index 8d57b92..6fd3a51 100644 --- a/changelog.d/dark-card-visibility.fixed.md +++ b/changelog.d/dark-card-visibility.fixed.md @@ -1 +1 @@ -Dark mode card surfaces are now visible. `--card`/`--popover`/`--muted` bumped from `#131820` (1.08:1 vs background — visually invisible) to `#1A2030` (1.19:1), and `--border`/`--input` bumped from `#1E293B` (1.32:1 vs background, 1.11:1 vs card) to `#334155` (1.87:1 / 1.57:1) so card surfaces and their borders are clearly distinguishable. +Dark mode card surfaces are now visible. `--card`/`--popover`/`--muted` bumped from `#131820` (1.08:1 vs background — visually invisible) to `#1A2030` (1.19:1), and `--border`/`--input` bumped from `#1E293B` (1.32:1 vs background, 1.22:1 vs card) to `#334155` (1.87:1 / 1.57:1) so card surfaces and their borders are clearly distinguishable.