mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
## What Fixes five reported false-positive/false-negative patterns in the WCAG contrast audit (`hyperframes validate --contrast`): 1. **SVG fill vs. text color** — foreground read from CSS `color` instead of SVG `fill`. 2. **Cross-component color bleed** — background estimate bleeds into a neighboring panel/layer. 3. **Backdrop-filter glass text** — background estimate misses the blur/tint and reads the raw backdrop. 4. **Partially-overlapping translucent decoration** — a decorative shape inside or partly touching the text's bbox goes undetected. 5. **Solid-fill pill/button** — investigated, did **not** reproduce; already handled correctly by the existing own-background ancestor walk. Not touched. ## Why The audit estimated an element's background two ways: - foreground: always `getComputedStyle(el).color` — wrong for SVG `<text>`/`<tspan>`, which is painted via `fill`, an independent CSS property. - background: a 4px pixel ring sampled just **outside** the text's bounding box, with a fallback to an ancestor's opaque `background-color` for solid pills/buttons. The ring is a proximity heuristic. It's wrong whenever what's immediately outside the text differs from what's actually behind it: - text near the edge of its own panel, with a differently-colored sibling panel/layer just past the bbox — the ring samples the neighbor. - a `backdrop-filter: blur()` glass panel sized only a couple pixels larger than the text — the ring exits the panel into the raw, unblurred, untinted backdrop. - a translucent decoration that only partially overlaps the ring, or sits entirely **inside** the bbox — invisible to the ring regardless of size. ## How **SVG fill (#1):** elements inside an `<svg>` (`el.ownerSVGElement`) now prefer the computed `fill` when it resolves to a solid `rgb()`/`rgba()` color, falling back to `color` for paint values that aren't a plain color (`none`, `context-fill`, gradient/pattern refs). **Cross-comp bleed / glass blur / partial decoration (#2–#4):** replaced the ring-sampling + own-background-ancestor-walk heuristic with a two-phase capture: 1. `__contrastAuditPrepare()` walks the DOM, computes each candidate's foreground (unchanged logic from #1), and **hides that element's own text paint** (`color`/`fill` → `transparent`, layout-neutral — no reflow). 2. The caller takes **one** screenshot with the glyphs invisible (same number of screenshots as before — just moved after the hide instead of before it). 3. `__contrastAuditFinish(imgBase64, time, candidates)` restores the original paint immediately, then samples the **real composited pixels directly inside each element's own bbox** — no proximity heuristic needed, since these are the exact pixels that were behind the glyphs. This is a real architectural change to `contrast-audit.browser.js`'s calling contract (single `__contrastAudit` → `__contrastAuditPrepare`/`__contrastAuditFinish`), with `validate.ts`'s `runContrastAudit` updated to match, including a try/finally restore-safety-net so a mid-loop screenshot/decode failure can't leave a later sample auditing a page with stale hidden text. Mirrored the identical change in `skills/hyperframes-creative/scripts/contrast-report.mjs`, which duplicates the same DOM-walk/sampling logic (not just the WCAG math). There, the **visible** frame for the human-facing overlay image still comes from the producer's normal `captureFrameToBuffer` path (unchanged); only the **background-sampling** capture is a plain `session.page.screenshot()` taken after hiding text — deliberately bypassing `captureFrameToBuffer`, whose static-frame dedup cache knows nothing about the DOM mutation and would hand back a stale pre-mutation buffer. **Solid-fill pill (#5):** reproduced a rounded pill/button with a busy page background outside it. The existing own-background ancestor walk already resolves the pill's declared `background-color` correctly regardless of the rounded corners — confirmed via repro, both before and after this change report the identical (correct) result. No fix needed; left untouched, and this case is covered by the new architecture too (would give the same right answer even without the ancestor-walk fallback). Added `packages/cli/src/commands/contrast-sample.ts` (mirroring the existing `contrast-bg.ts`/`contrast-fg.ts` pattern) hosting the pure sample-rect/grid-point computation, unit tested — the browser-injected scripts can't import it directly, so it's kept in sync by hand, same convention as the rest of this file. ## Test plan - [x] Unit tests: `contrast-fg.test.ts` (SVG fill resolution), `contrast-sample.test.ts` (sample-rect clamping/degenerate cases), plus the full `packages/cli` suite (1424 tests) passes, including an updated `layout-audit.browser.test.ts` case that called the old single-function `__contrastAudit` API directly. - [x] Manual verification — standalone `puppeteer-core` harness against real `chrome-headless-shell`, one minimal HTML fixture per pattern, comparing the audit's reported ratio/verdict against a hand-constructed ground truth: - **SVG fill**: `fill:white` / no `color` on black bg → before: `fg=rgb(0,0,0)` ratio `1:1` (false FAIL); after: `fg=rgb(255,255,255)` ratio `21:1` (correct PASS). - **Cross-comp bleed**: text on a black sibling highlight box 2px larger than the text, white page bg outside it → before: `bg=rgb(255,255,255)` ratio `1.23:1` (false FAIL); after: `bg=rgb(0,0,0)` ratio `17.14:1` (correct PASS). - **Glass blur**: black text on an 18%-white-tinted `backdrop-filter: blur(14px)` panel over a yellow/blue gradient, panel only ~2px larger than the text → before: `bg=rgb(0,64,255)` (raw gradient color, blur/tint completely missed) ratio `3.18:1` (false FAIL); after: `bg=rgb(159,160,165)` (correct blurred/tinted blend) ratio `8.05:1` (correct PASS). - **Partial decoration**: text 92%-covered by a translucent white badge on a dark bg → before: `bg=rgb(16,16,16)` (ring never touches the badge, which sits entirely inside the bbox) ratio `17.45:1` (false PASS); after: `bg=rgb(171,171,171)` (correctly detects the badge) ratio `2.11:1` (correct FAIL). - **Solid pill sanity**: unaffected — `bg=rgb(10,10,10)` ratio `19.8:1` before and after. - [x] End-to-end: ran the actual `hyperframes validate --contrast` CLI command (via `tsx src/cli.ts`) against a real scaffolded project containing all 4 patterns simultaneously — only the genuinely-failing case (the 92%-covered decoration) is reported (`1.09:1`, need `3:1`); the cross-comp-bleed, glass-blur, and solid-pill cases are correctly silent. A second vanilla scaffold with plain white-on-dark text produces zero false positives. - [x] `oxlint`, `oxfmt --check`, and `tsc --noEmit` all pass on the changed files.