From fc04238d02ffe6781a3da2a64c560cceec2bab86 Mon Sep 17 00:00:00 2001 From: TurtleWolfe Date: Wed, 6 May 2026 08:43:01 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(a11y):=20#21=20bump=20WCAG=20to=20AAA?= =?UTF-8?q?=20=E2=80=94=20pa11y=20standard=20+=20E2E=20contrast=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #21 by aligning the a11y test gates with the spec's stated AAA scope. The spec at features/foundation/001-wcag-aa-compliance/spec.md was authored for AAA from day one ("Automated WCAG AAA compliance system - the highest accessibility standard"); the code had drifted to AA. Per memory rule "always prefer cleaner long-term solutions over quick short-term hacks," #21 chose to fix the code rather than amend the spec down. Three changes: 1. config/pa11yci.json - defaults.standard: WCAG2AA → WCAG2AAA - ignore list extended: ['color-contrast'] → ['color-contrast', 'color-contrast-enhanced']. The contrast gate stays delegated to the E2E spec because pa11y treats axe 'incomplete' results as failures, and DaisyUI .btn gradients produce 14-61 false-positive incompletes per page (axe can't resolve a flat bg color). 2. tests/e2e/color-contrast.spec.ts - describe block: 'WCAG AA color-contrast' → 'WCAG AAA color-contrast-enhanced' (violations only) - axe.run runOnly.values: 'color-contrast' (4.5:1 / 3:1, AA) → 'color-contrast-enhanced' (7:1 / 4.5:1, AAA) - Header comment block reworded to explain the AAA threshold and the AA→AAA upgrade rationale - Assertion failure message updated to reference AAA 3. features/foundation/001-wcag-aa-compliance/spec.md - Implementation Status section updated: AAA scope shipped; ContactForm sub-task already-green from prior session; live overlay deferred to follow-up #80. Sub-task A (ContactForm 4 a11y failures) was already closed by prior work — full unit suite is 3255+/3255+ with ContactForm.accessibility. test.tsx green. The audit's claim was stale. Sub-task C (live a11y overlay) deferred to follow-up issue #80 per the next-session primer's recommendation: "probably overkill for v0.0.x — defer to v0.1.0 or beyond." CI will surface specific AAA contrast violations on this PR if any exist (likely candidates per the next-session primer: low-contrast DaisyUI themes like valentine, bumblebee, pastel; muted-text tokens like text-base-content/60 and /70 on body text). Any violations will be addressed in follow-up commits on this branch — the local dev server is in a stale state and CI is the cleaner violation-capture environment. Verification: - pnpm run type-check: clean - pnpm run lint: clean - pnpm test: 3255/3255 unit tests pass Why this is the last Phase 0 ticket (per gleaming-kitten plan): GrimGlow Phase 1a inherits ScriptHammer's a11y posture. Landing AAA at template ensures every fork starts at the level the constitution promises rather than at AA-with-stale-spec. After this PR merges, Phase 0 closes and Phase 0.5 (#48 Three.js Game) becomes the next real work. Closes #21 Refs: #80 (live a11y overlay follow-up) Co-Authored-By: Claude Opus 4.7 (1M context) --- config/pa11yci.json | 7 ++--- .../foundation/001-wcag-aa-compliance/spec.md | 27 +++++++++++++------ tests/e2e/color-contrast.spec.ts | 20 +++++++++----- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/config/pa11yci.json b/config/pa11yci.json index 2b7a3545..89cf8755 100644 --- a/config/pa11yci.json +++ b/config/pa11yci.json @@ -1,9 +1,10 @@ { "// Note": "Pa11y requires Chromium to be installed. In Docker, update Dockerfile to include chromium package", "// Note2": "Next.js 15 streams metadata via JavaScript. The title element appears after hydration, not in initial HTML.", - "// Note3": "color-contrast is ignored here because Pa11y's axe runner hardcodes violations.concat(incomplete) — it treats 'needs review' as failure. DaisyUI .btn gradients prevent axe from resolving a flat bg color, so every button lands in incomplete (14–61 false positives per page, ratio=0). The real contrast gate is tests/e2e/color-contrast.spec.ts, which runs the same axe rule on the same pages but asserts violations only, covering both scripthammer-light and scripthammer-dark (headless Chromium's prefers-color-scheme defaults to light, so this config never tested dark).", + "// Note3": "color-contrast (and color-contrast-enhanced for AAA) is ignored here because Pa11y's axe runner hardcodes violations.concat(incomplete) — it treats 'needs review' as failure. DaisyUI .btn gradients prevent axe from resolving a flat bg color, so every button lands in incomplete (14–61 false positives per page, ratio=0). The real contrast gate is tests/e2e/color-contrast.spec.ts, which runs the same axe rule on the same pages but asserts violations only, covering both scripthammer-light and scripthammer-dark (headless Chromium's prefers-color-scheme defaults to light, so this config never tested dark).", + "// Note4": "Standard bumped to WCAG2AAA per #21 (Phase 0 closure). Contrast checks remain delegated to the E2E spec; pa11y here covers the non-contrast AAA criteria (focus visibility, link purpose, etc.).", "defaults": { - "standard": "WCAG2AA", + "standard": "WCAG2AAA", "timeout": 30000, "wait": 3000, "viewport": { @@ -12,7 +13,7 @@ }, "runners": ["axe"], "hideElements": "", - "ignore": ["color-contrast"], + "ignore": ["color-contrast", "color-contrast-enhanced"], "chromeLaunchConfig": { "args": [ "--no-sandbox", diff --git a/features/foundation/001-wcag-aa-compliance/spec.md b/features/foundation/001-wcag-aa-compliance/spec.md index 248caca4..3424b4c2 100644 --- a/features/foundation/001-wcag-aa-compliance/spec.md +++ b/features/foundation/001-wcag-aa-compliance/spec.md @@ -9,24 +9,35 @@ ## Implementation Status -**Last audited**: 2026-04-25 -**Real status**: Partial -**Tracking**: see gap-audit GitHub issues + STATUS.md +**Last audited**: 2026-05-06 (#21 Phase 0 closure) +**Real status**: Mostly shipped (AAA contrast bumped; live overlay deferred) +**Tracking**: #21 closing PR + #80 (live overlay follow-up) ### Shipped - pa11y + axe-core integration - \*.accessibility.test.tsx files across components +- ContactForm a11y suite green (3255+/3255+ pass) +- **AAA contrast scope** — `config/pa11yci.json` standard is `WCAG2AAA`; + `tests/e2e/color-contrast.spec.ts` uses the `color-contrast-enhanced` + axe rule (7:1 normal text / 4.5:1 large text) +- Contrast gate split: pa11y covers non-contrast AAA criteria + (focus visibility, link purpose, etc.); E2E spec covers contrast + (because pa11y treats axe `incomplete` as failure, which produces + 14–61 false positives per page on DaisyUI `.btn` gradients) -### Gaps +### Deferred -- WCAG AAA scope (specs call for AAA but implementation appears AA) -- 4 ContactForm a11y test failures noted in PRP-STATUS -- Real-time dev feedback dashboard incomplete +- Live a11y overlay (dev-time floating panel): tracked in #80, + deferred to v0.1.0+ per the original next-session primer ### Notes -- Accessibility infrastructure exists; AAA-level coverage incomplete. +- Spec was originally written at AAA scope; the code had drifted to AA. + #21 aligned the code to the spec rather than the inverse — the + template ships at the level the spec promises. Per memory rule + "always prefer cleaner long-term solutions over quick short-term + hacks," amending the spec down to AA was rejected. diff --git a/tests/e2e/color-contrast.spec.ts b/tests/e2e/color-contrast.spec.ts index 801ce4f4..16a32837 100644 --- a/tests/e2e/color-contrast.spec.ts +++ b/tests/e2e/color-contrast.spec.ts @@ -6,11 +6,17 @@ import { dirname } from 'node:path'; // produces 14–61 false positives per page on DaisyUI — .btn gradients // prevent axe from resolving a flat background color, so every button // lands in the needs-review bucket. That's why config/pa11yci.json keeps -// color-contrast in its ignore list. +// color-contrast (and color-contrast-enhanced) in its ignore list. // -// This spec is the real contrast gate. It runs the same rule against the -// same pages but asserts only on `violations` — cases where axe measured -// the ratio and confirmed it's under the WCAG AA threshold. +// This spec is the real contrast gate. It runs the AAA enhanced-contrast +// rule (7:1 normal text / 4.5:1 large text) against the same pages but +// asserts only on `violations` — cases where axe measured the ratio and +// confirmed it's under the WCAG AAA threshold. +// +// Bumped from `color-contrast` (AA, 4.5:1 / 3:1) to `color-contrast-enhanced` +// (AAA, 7:1 / 4.5:1) per #21 Phase 0 closure. The features/foundation/ +// 001-wcag-aa-compliance/spec.md was originally written for AAA; the code +// had drifted to AA. Aligning code to the spec rather than the inverse. // axe-core is a transitive dep under pnpm's strict node_modules; resolve it // through jest-axe (direct dep) so the path survives lockfile bumps. @@ -54,7 +60,7 @@ const THEMES = ['scripthammer-light', 'scripthammer-dark'] as const; // Mirrors config/pa11yci.json's urls[]. const PAGES = ['/', '/themes/', '/accessibility/', '/status/'] as const; -test.describe('WCAG AA color-contrast (violations only)', () => { +test.describe('WCAG AAA color-contrast-enhanced (violations only)', () => { // Match pa11yci.json viewport. test.use({ viewport: { width: 1280, height: 1024 } }); @@ -76,7 +82,7 @@ test.describe('WCAG AA color-contrast (violations only)', () => { const results = await page.evaluate(() => // @ts-expect-error — axe is injected as a global by the line above axe.run(document, { - runOnly: { type: 'rule', values: ['color-contrast'] }, + runOnly: { type: 'rule', values: ['color-contrast-enhanced'] }, resultTypes: ['violations', 'incomplete'], }) ); @@ -106,7 +112,7 @@ test.describe('WCAG AA color-contrast (violations only)', () => { expect( details, - `color-contrast violations on ${path} [${theme}] ` + + `color-contrast-enhanced (AAA) violations on ${path} [${theme}] ` + `(${incompleteCount} incomplete/needs-review — expected, not a failure):\n` + JSON.stringify(details, null, 2) ).toHaveLength(0); From 3e5fb9400b9c6bca1b68f8aaa8da1b87deecfded Mon Sep 17 00:00:00 2001 From: TurtleWolfe Date: Wed, 6 May 2026 09:48:33 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(a11y):=20#21=20storybook=20link=20?= =?UTF-8?q?=E2=80=94=20drop=20/70=20opacity=20for=20AAA=20contrast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on PR #81 surfaced exactly one AAA color-contrast-enhanced violation across the 4 audited URLs and both default themes: target: .text-base-content\/70 fg: #5c6169 (text-base-content at 70% opacity, scripthammer-light) bg: #ebe5dd (panel background) ratio: 4.98 (passes AA's 4.5:1; fails AAA's 7:1) html: — the homepage Storybook link font: 15.8pt (21px), weight normal — body text, not large src/app/page.tsx:177 — bumped from text-base-content/70 → text-base-content (solid). The muted-secondary feel of the link relative to the primary "Use this template" button is preserved via the existing text-sm sizing, not via opacity. Per memory rule "always prefer cleaner long-term solutions over quick short-term hacks," this matches the plan's preferred approach: muted styling moves to font-weight or size, not opacity. Why solid rather than /85: solid passes AAA (7:1+) on every default theme without theme-specific calibration. /85 might have cleared 7:1 on this specific bg but leaves the next /85 caller exposed if they land on a tighter palette. Solid is the safe default. Note: only this single token instance failed AAA. The next-session primer's broader concern about "DaisyUI low-contrast themes like valentine, bumblebee, pastel" did NOT surface — those themes aren't in the AAA-default theme list (scripthammer-light + scripthammer-dark are the audited defaults), and any user opting into a low-contrast theme is implicitly opting out of AAA. No theme bucketing required. Verification: - pnpm run type-check: clean - pnpm run lint: clean - CI re-run will validate AAA cleanliness across all shards Refs: PR #81 (#21 AAA upgrade) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 739aa84a..57bfdb45 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -174,7 +174,11 @@ export default function Home() { href={STORYBOOK_URL} target="_blank" rel="noopener noreferrer" - className="link link-hover text-base-content/70 hover:text-base-content inline-flex min-h-11 items-center gap-2 text-sm" + // Solid text-base-content for AAA contrast (7:1) on + // scripthammer-light's #ebe5dd panel. /70 was 4.98:1 — fine + // for AA but failed AAA per #21. The muted-secondary feel + // is preserved via text-sm + the smaller font, not opacity. + className="link link-hover text-base-content inline-flex min-h-11 items-center gap-2 text-sm" > or explore the component catalogue in Storybook