From cbbbc526f974a0e72eb03e6e3935ecfc1598b978 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 18 May 2026 08:56:02 +0200 Subject: [PATCH] split contrast gate + ship -hc theme variants (T-114, T-118) The expanded canonicalPairs from T-114 (muted text, status chips, syntax tokens on the code-block surface, panel focus border) made the four named themes fail WCAG-AA. Retuning their palettes to pass would have changed the look users picked them for, so the gate is split instead. `canonicalPairs` shrinks back to the baseline every named theme passes; the new `extendedPairs` carries the stricter set and only runs against themes whose name ends `-hc` or `-cb`. Sibling files (`clide-hc`, `midnight-hc`, `paper-hc`, `terminal-hc`) ship today; the policy lives in D-69 with a back-ref from D-22. Co-Authored-By: Claude --- .pql/changelog/ticket_history/2026-05.sql | 2 + .pql/changelog/tickets/2026-05.sql | 30 ++++++ CHANGELOG.md | 4 + governance/README.md | 1 + governance/decisions/accessibility.md | 9 +- lib/kernel/src/theme/contrast.dart | 106 ++++++++++++++++++- lib/kernel/src/theme/themes/clide-hc.yaml | 38 +++++++ lib/kernel/src/theme/themes/midnight-hc.yaml | 39 +++++++ lib/kernel/src/theme/themes/paper-hc.yaml | 38 +++++++ lib/kernel/src/theme/themes/terminal-hc.yaml | 38 +++++++ lib/main.dart | 4 + lib/test_app.dart | 4 + test/a11y/contrast_test.dart | 18 ++++ 13 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 lib/kernel/src/theme/themes/clide-hc.yaml create mode 100644 lib/kernel/src/theme/themes/midnight-hc.yaml create mode 100644 lib/kernel/src/theme/themes/paper-hc.yaml create mode 100644 lib/kernel/src/theme/themes/terminal-hc.yaml diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index c922f514..f003ba87 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1805,3 +1805,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-108', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:05:38', '2026-05-17 20:05:38', '2026-05-17 20:05:38', NULL, '6815472fbfaa6d7740945b42d4c87f57', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:06:30', '2026-05-17 20:06:30', '2026-05-17 20:06:30', NULL, '52271781bf987a984d9f219956034d1f', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:11:16', '2026-05-17 20:11:16', '2026-05-17 20:11:16', NULL, '6d51964a3ad7f1948f54a7c769336841', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:13:36', '2026-05-17 20:13:36', '2026-05-17 20:13:36', NULL, '0bad40fa7f165f828bb0c0757044491e', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-118', 'status', 'backlog', 'done', NULL, '2026-05-17 20:53:41', '2026-05-17 20:53:41', '2026-05-17 20:53:41', NULL, 'ff0126655805fed0ebe45f313c5ceff7', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 713ed946..c239d0b6 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -1799,3 +1799,33 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, 7. Move `test_app.dart` out of production `main.dart` import graph (gate behind separate entrypoint or `kDebugMode`). Source: consultants.md (Code Quality + Architecture Findings — Minor).', 'done', 'low', NULL, NULL, NULL, '2026-05-17 18:48:20', '2026-05-17 20:11:16', NULL, 'fccc783ff24be83f6d5a498e936c4af9', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'task', 'T-97', 'quick-wins: a11y + contrast gate expansion', 'Batch of small consultant findings that share a domain (a11y / visual): + +1. Expand contrast gate `canonicalPairs` (`test/a11y/contrast_test.dart`) to cover `globalTextMuted` (muted text is everywhere), the `status*` foregrounds, syntax tokens on `panelBackground`, and `panelActiveBorder`. +2. Tokenize the ~43 hardcoded-color sites the UX reviewer flagged — at minimum the modal/palette shadow and window-control colors that don''t adapt to the `paper` light theme. +3. Add a focus-ring rendering at the token level so every focusable widget gets it for free (depends on T-100). +4. Surface text-zoom (Ctrl +/-/0) in the palette so it''s discoverable. + +Source: consultants.md "UX — Findings — [Minor]" + Strengths section.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-17 18:48:28', '2026-05-17 20:13:36', NULL, '1a2776ce1e3dd335966d738f80784a0a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-118', 'task', 'T-97', 'add high-contrast + colour-blind theme variants', 'Per D-69, the four named themes (clide, midnight, paper, terminal) keep +their published palettes and only pass the baseline canonicalPairs +gate. The stricter extendedPairs set in +lib/kernel/src/theme/contrast.dart (muted text, status chips, syntax +tokens on panel surfaces, panel focus border) is gated against any +theme whose name ends in -hc or -cb — but no such themes exist yet. + +Scope: + +1. Ship clide-hc.yaml, midnight-hc.yaml, paper-hc.yaml, + terminal-hc.yaml — sibling theme files whose palettes shift only + enough to clear extendedPairs (4.5:1 for chips / syntax / muted + body, 3:1 for the focus border). +2. Optional follow-up: *-cb.yaml colour-blind variants if/when we + pick a palette policy (deutan vs protan vs tritan, or a single + colour-name-replacement strategy). +3. Register the new themes in lib/main.dart alongside the existing + four. +4. The contrast gate at test/a11y/contrast_test.dart already covers + them — adding the files is enough. + +Sources: D-22, D-69, consultants.md ("UX — Findings — [Minor]").', 'done', 'low', NULL, NULL, 'D-69', '2026-05-17 20:51:47', '2026-05-17 20:53:41', NULL, 'd163731890022856585e9f33432462f6', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dceb4fd..b8a3c6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- Contrast gate split — baseline `canonicalPairs` every theme passes, + strict `extendedPairs` (muted/status/syntax/focus-border) gated to + `-hc` / `-cb` variants. Ships `clide-hc`, `midnight-hc`, `paper-hc`, + `terminal-hc` siblings of the named themes (D-69, T-114, T-118). - Pre-push coverage gate — `make push-check` runs `ci/coverage_gate.sh`, which fails if total line coverage drops below `coverage_floor:` in `pubspec.yaml`. Floor ratchets up only; target 95% (D-66). diff --git a/governance/README.md b/governance/README.md index 44aa29d7..70e19603 100644 --- a/governance/README.md +++ b/governance/README.md @@ -110,6 +110,7 @@ You might also want, project-permitting: - [D-66: Line coverage gate at 95%, ratcheted from current](decisions/testing.md#d-66-line-coverage-gate-at-95-ratcheted-from-current) — _testing_ - [D-67: Pql changelog files are committed alongside code](decisions/process.md#d-67-pql-changelog-files-are-committed-alongside-code) — _process_ - [D-68: Dual integration surface — Bash CLI primary, MCP secondary](decisions/architecture.md#d-68-dual-integration-surface--bash-cli-primary-mcp-secondary) — _architecture_ +- [D-69: published themes are user contracts; ship -hc variants for a11y](decisions/accessibility.md#d-69-published-themes-are-user-contracts-ship--hc-variants-for-a11y) — _accessibility_ ## Open questions diff --git a/governance/decisions/accessibility.md b/governance/decisions/accessibility.md index e02cb1a1..fb1a6418 100644 --- a/governance/decisions/accessibility.md +++ b/governance/decisions/accessibility.md @@ -23,6 +23,13 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish. - **Decision:** Every bundled theme must pass a WCAG-AA contrast check on its canonical token pairs (text/background, link/background, focus-ring/background) at test time. `ci/test_a11y.sh` runs the gate; CI fails on regressions. - **Rationale:** Themes drift under "looks nicer" tweaks; contrast regressions land silently. Running the gate on every PR is the cheapest insurance. Ran the gate on initial themes — caught one summer-night muted token at 2.81:1 (below AA), fixed before landing. - **Cost:** Third-party themes (Tier 6) won't be gated until an extension-time test hook lands. Bundled themes are gated today. -- **Raised by:** 2026-04-21 planning. +- **Raised by:** 2026-04-21 planning. Refined by [D-69](#d-69-published-themes-are-user-contracts-ship-hc-variants-for-a11y) — the gate's *strict* pair set only applies to high-contrast variants; named themes keep their published palettes. + +### D-69: published themes are user contracts; ship -hc variants for a11y +- **Date:** 2026-05-17 +- **Decision:** The four bundled themes that ship under a recognisable name — `clide`, `midnight`, `paper`, `terminal` — are user contracts. Their palette colours (including syntax tokens, status colours, and borderHi) MUST NOT be retuned to satisfy contrast gates. When a stricter contrast check would fail one of them, the fix is one of: (a) ship a sibling theme with `-hc` (high-contrast) or `-cb` (colour-blind) in the name and enforce the strict pair set only there, or (b) split `canonicalPairs` into a *baseline* set every theme must pass and an *extended* set that only the `-hc`/`-cb` variants must pass. +- **Rationale:** Users pick `midnight` because it looks like VS Code, `paper` because it reads as a drafting sheet, `terminal` because of the amber-on-near-black tmux feel. Quietly darkening `paper`'s success/warning/info or boosting `midnight`'s `borderHi` to pass a WCAG-AA check changes what they got and what they signed up for. A11y is a Tier-0 contract ([D-20](#d-20-a11y-is-a-tier-0-contract)), but it's served by *offering* an accessible variant, not by overwriting the aesthetic ones. VS Code itself ships `Default Dark+` and a separate `Default High Contrast` for exactly this reason. +- **Cost:** Two extra theme files per "named" theme when we add a11y variants. The bundled-theme contrast gate ([D-22](#d-22-wcag-aa-contrast-gate-on-bundled-themes)) needs a baseline/extended split so the named themes don't fail the strict pairs. +- **Raised by:** 2026-05-17 — user intervened mid-T-114 when I had retuned `clide`/`midnight`/`paper`/`terminal` palette entries to satisfy the expanded `canonicalPairs`; reverted, decision written, T-114 will follow this rule. --- diff --git a/lib/kernel/src/theme/contrast.dart b/lib/kernel/src/theme/contrast.dart index 058fc462..9a614da5 100644 --- a/lib/kernel/src/theme/contrast.dart +++ b/lib/kernel/src/theme/contrast.dart @@ -39,9 +39,15 @@ double contrastRatio(Color a, Color b, {Color onto = const Color(0xFF808080)}) { /// Minimum ratio required for this pair per WCAG AA. double minimumRatio(ContrastPair pair) => pair.largeText ? 3.0 : 4.5; -/// Canonical set of token pairs each bundled theme must honour. +/// Baseline token pairs every bundled theme must honour. /// -/// The a11y contrast test walks this list per-theme. +/// Per D-22 every named theme passes this set; per D-69 the named +/// themes (`clide`, `midnight`, `paper`, `terminal`) are user +/// contracts whose palettes are not retuned to chase a contrast gate, +/// so only the load-bearing pairs (primary text on its surface, chrome +/// foregrounds, selected list item) sit here. Stricter coverage for +/// muted text, status chips, syntax tokens, and the focus border lives +/// in [extendedPairs], which only `-hc`/`-cb` variants must pass. List canonicalPairs(SurfaceTokens s) => [ ContrastPair( name: 'global.text_on_background', @@ -100,10 +106,100 @@ List canonicalPairs(SurfaceTokens s) => [ ), ]; -/// Convenience for tests: returns the list of pairs that fail WCAG AA. -List failingPairs(SurfaceTokens tokens) { +/// Stricter pair set — only the high-contrast (`-hc`) and colour-blind +/// (`-cb`) theme variants must clear it. See D-69. These are the +/// surfaces a UX consultant flagged in `consultants.md`: muted body +/// text, status chip foregrounds, syntax tokens on the code-block +/// surface, and the focus-indicating panel border. +List extendedPairs(SurfaceTokens s) => [ + ContrastPair( + name: 'global.text_muted_on_background', + foreground: s.globalTextMuted, + background: s.globalBackground, + ), + ContrastPair( + name: 'global.text_muted_on_panel', + foreground: s.globalTextMuted, + background: s.panelBackground, + ), + ContrastPair( + name: 'status.success_on_statusbar', + foreground: s.statusSuccess, + background: s.statusBarBackground, + ), + ContrastPair( + name: 'status.warning_on_statusbar', + foreground: s.statusWarning, + background: s.statusBarBackground, + ), + ContrastPair( + name: 'status.error_on_statusbar', + foreground: s.statusError, + background: s.statusBarBackground, + ), + ContrastPair( + name: 'status.info_on_statusbar', + foreground: s.statusInfo, + background: s.statusBarBackground, + ), + ContrastPair( + name: 'syntax.keyword_on_panel', + foreground: s.syntaxKeyword, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.type_on_panel', + foreground: s.syntaxType, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.string_on_panel', + foreground: s.syntaxString, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.number_on_panel', + foreground: s.syntaxNumber, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.comment_on_panel', + foreground: s.syntaxComment, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.method_on_panel', + foreground: s.syntaxMethod, + background: s.panelBackground, + ), + ContrastPair( + name: 'syntax.punct_on_panel', + foreground: s.syntaxPunct, + background: s.panelBackground, + ), + // WCAG 1.4.11 wants 3:1 for non-text UI components like a focus + // border against the adjacent surface. + ContrastPair( + name: 'panel.active_border_on_background', + foreground: s.panelActiveBorder, + background: s.globalBackground, + largeText: true, + ), + ]; + +/// Convenience for tests: returns the list of [canonicalPairs] that +/// fail WCAG AA. +List failingPairs(SurfaceTokens tokens) => + _failures(canonicalPairs(tokens)); + +/// Strict variant of [failingPairs] — walks [extendedPairs] instead. +/// Intended for the `-hc` / `-cb` theme gate. +List failingExtendedPairs(SurfaceTokens tokens) => + _failures(extendedPairs(tokens)); + +List _failures(List pairs) { final out = []; - for (final p in canonicalPairs(tokens)) { + for (final p in pairs) { final ratio = contrastRatio(p.foreground, p.background); final need = minimumRatio(p); if (ratio < need) { diff --git a/lib/kernel/src/theme/themes/clide-hc.yaml b/lib/kernel/src/theme/themes/clide-hc.yaml new file mode 100644 index 00000000..99353277 --- /dev/null +++ b/lib/kernel/src/theme/themes/clide-hc.yaml @@ -0,0 +1,38 @@ +# clide-hc — high-contrast sibling of `clide` +# +# Same periwinkle silhouette; muted text, status chips, syntax tokens, +# and the focus border bumped to clear the strict contrast gate +# (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart). + +name: clide-hc +display_name: Clide (high contrast) +dark: true + +palette: + background: "#20202C" + panel: "#1A1A24" + surface: "#242838" + muted: "#B1BBE3" + foreground: "#E6E8F2" + secondary: "#B1BBE3" + primary: "#92B5FF" + accent: "#92B5FF" + success: "#A8E8C8" + warning: "#FFD988" + error: "#FF9F9F" + info: "#A6C8FF" + surfaceHi: "#2C3046" + border: "#343850" + borderHi: "#92B5FF" + textDim: "#B1BBE3" + textMute: "#9098C0" + accentSoft: "#2192B5FF" + +syntax: + keyword: "#E0B5F2" + type: "#A6C8FF" + string: "#C2E8B0" + number: "#FFD988" + comment: "#9098C0" + method: "#A6C8FF" + punct: "#B1BBE3" diff --git a/lib/kernel/src/theme/themes/midnight-hc.yaml b/lib/kernel/src/theme/themes/midnight-hc.yaml new file mode 100644 index 00000000..49d2fa8a --- /dev/null +++ b/lib/kernel/src/theme/themes/midnight-hc.yaml @@ -0,0 +1,39 @@ +# midnight-hc — high-contrast sibling of `midnight` +# +# Same VS Code-adjacent silhouette; muted text, status chips, syntax +# tokens, and the focus border bumped to clear the strict contrast +# gate (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart). + +name: midnight-hc +display_name: Midnight (high contrast) +dark: true + +palette: + background: "#1E1E1E" + panel: "#181818" + surface: "#252526" + muted: "#BDBDBD" + foreground: "#F0F0F0" + secondary: "#D4D4D4" + primary: "#82BFFF" + accent: "#82BFFF" + success: "#B5E2B0" + warning: "#E9D89E" + error: "#FFB3A5" + info: "#9BC9F0" + onAccent: "#0B1220" + surfaceHi: "#2D2D2E" + border: "#333333" + borderHi: "#9C9C9C" + textDim: "#BDBDBD" + textMute: "#9A9A9A" + accentSoft: "#2182BFFF" + +syntax: + keyword: "#D9A1D3" + type: "#66D9C2" + string: "#E9B299" + number: "#C8E0B7" + comment: "#99CB80" + method: "#E5E5B5" + punct: "#BDBDBD" diff --git a/lib/kernel/src/theme/themes/paper-hc.yaml b/lib/kernel/src/theme/themes/paper-hc.yaml new file mode 100644 index 00000000..e890f1d8 --- /dev/null +++ b/lib/kernel/src/theme/themes/paper-hc.yaml @@ -0,0 +1,38 @@ +# paper-hc — high-contrast sibling of `paper` +# +# Drafting sheet, red-pencil accent — darker ink so muted text, status +# chips, syntax tokens, and the focus border clear the strict contrast +# gate (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart). + +name: paper-hc +display_name: Paper (high contrast) +dark: false + +palette: + background: "#F4F1EA" + panel: "#ECE7DB" + surface: "#FBF8F1" + muted: "#4A4A48" + foreground: "#0E0E0E" + secondary: "#1F1F1F" + primary: "#8E2D10" + accent: "#6E2410" + success: "#1A4A2B" + warning: "#6A4C00" + error: "#7A1F12" + info: "#154168" + surfaceHi: "#ECE7DB" + border: "#0E0E0E" + borderHi: "#3A3A38" + textDim: "#4A4A48" + textMute: "#7A7A72" + accentSoft: "#218E2D10" + +syntax: + keyword: "#52215C" + type: "#154168" + string: "#1A4A2B" + number: "#6A4C00" + comment: "#4A4A48" + method: "#103E6F" + punct: "#1F1F1F" diff --git a/lib/kernel/src/theme/themes/terminal-hc.yaml b/lib/kernel/src/theme/themes/terminal-hc.yaml new file mode 100644 index 00000000..9abd51fa --- /dev/null +++ b/lib/kernel/src/theme/themes/terminal-hc.yaml @@ -0,0 +1,38 @@ +# terminal-hc — high-contrast sibling of `terminal` +# +# Near-black + amber tmux feel; muted text, status chips, syntax tokens, +# and the focus border bumped to clear the strict contrast gate +# (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart). + +name: terminal-hc +display_name: Terminal (high contrast) +dark: true + +palette: + background: "#0A0A0A" + panel: "#000000" + surface: "#111111" + muted: "#B5B5B5" + foreground: "#F0F0F0" + secondary: "#D6D6D6" + primary: "#FFC868" + accent: "#FFC868" + success: "#B5E2B0" + warning: "#FFD988" + error: "#FFA5A5" + info: "#B0C8FF" + surfaceHi: "#181818" + border: "#242424" + borderHi: "#888888" + textDim: "#B5B5B5" + textMute: "#9A9A9A" + accentSoft: "#21FFC868" + +syntax: + keyword: "#FF8585" + type: "#FFD27A" + string: "#B5E2B0" + number: "#DAA7FF" + comment: "#9A9A9A" + method: "#BCD8FF" + punct: "#B5B5B5" diff --git a/lib/main.dart b/lib/main.dart index df4aabfd..4bbf2102 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -207,6 +207,10 @@ Future> _loadBundledThemes() async { 'lib/kernel/src/theme/themes/midnight.yaml', 'lib/kernel/src/theme/themes/paper.yaml', 'lib/kernel/src/theme/themes/terminal.yaml', + 'lib/kernel/src/theme/themes/clide-hc.yaml', + 'lib/kernel/src/theme/themes/midnight-hc.yaml', + 'lib/kernel/src/theme/themes/paper-hc.yaml', + 'lib/kernel/src/theme/themes/terminal-hc.yaml', ]; final out = []; for (final p in paths) { diff --git a/lib/test_app.dart b/lib/test_app.dart index faf07ede..1438081a 100644 --- a/lib/test_app.dart +++ b/lib/test_app.dart @@ -246,6 +246,10 @@ class _ClideTestAppState extends State { 'lib/kernel/src/theme/themes/midnight.yaml', 'lib/kernel/src/theme/themes/paper.yaml', 'lib/kernel/src/theme/themes/terminal.yaml', + 'lib/kernel/src/theme/themes/clide-hc.yaml', + 'lib/kernel/src/theme/themes/midnight-hc.yaml', + 'lib/kernel/src/theme/themes/paper-hc.yaml', + 'lib/kernel/src/theme/themes/terminal-hc.yaml', ]; for (final p in paths) { final name = p.split('/').last.replaceAll('.yaml', ''); diff --git a/test/a11y/contrast_test.dart b/test/a11y/contrast_test.dart index 8db719e2..d528b800 100644 --- a/test/a11y/contrast_test.dart +++ b/test/a11y/contrast_test.dart @@ -6,6 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; /// [canonicalPairs]) must clear 4.5:1 for normal text / 3:1 for large /// text. Failing pairs are printed with their computed ratio so a /// theme-token regression shows exactly which pair broke. +/// +/// Themes whose name ends in `-hc` (high-contrast) or `-cb` +/// (colour-blind) additionally have to clear the stricter +/// [extendedPairs] set — D-69. void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -15,6 +19,10 @@ void main() { 'lib/kernel/src/theme/themes/midnight.yaml', 'lib/kernel/src/theme/themes/paper.yaml', 'lib/kernel/src/theme/themes/terminal.yaml', + 'lib/kernel/src/theme/themes/clide-hc.yaml', + 'lib/kernel/src/theme/themes/midnight-hc.yaml', + 'lib/kernel/src/theme/themes/paper-hc.yaml', + 'lib/kernel/src/theme/themes/terminal-hc.yaml', ]; for (final path in bundledPaths) { @@ -34,6 +42,16 @@ void main() { '${failures.map((f) => ' - $f').join('\n')}', ); } + final isStrict = def.name.endsWith('-hc') || def.name.endsWith('-cb'); + if (isStrict) { + final extended = failingExtendedPairs(tokens); + if (extended.isNotEmpty) { + fail( + 'Extended contrast failures in ${def.name} (strict gate):\n' + '${extended.map((f) => ' - $f').join('\n')}', + ); + } + } }); } });