From 9348e337963ec60a33dd9be691fab451d4776cac Mon Sep 17 00:00:00 2001 From: Zaldaryon <273555259+Zaldaryon@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:07:04 -0300 Subject: [PATCH] a11y(styles): make the Grid selected-card border opaque Fixes #258. The selected-card border in Grid.tsx (bg-vsd/50 border-vsl/25) sat at 25% alpha next to its own dark fill, reading about 1.53:1 worst case, below the 3:1 WCAG 1.4.11 floor for a UI boundary that is the sole indicator of a state (ModListCard.tsx's selected={installed}, the only consumer). Raised during review of PR #251, filed separately since the gap predates it. Unlike ConfigPage's selected background-tile border, whose inner edge paints over an arbitrary user-chosen thumbnail so no single ratio can be asserted for it (its outer edge holds 1.4.11 against the fixed section panel at 4.85:1, and the border-2 there answers 1.4.1 instead), the Grid card's backdrop is fully known on both sides: the card's own bg-vsd/50 fill composited over the grid panel's fixed scrim stack. That makes the ratio actually computable, so the fix is opacity, not width: the border drops its /25 and reads at full strength, which the same compositing math the rest of text-contrast.test.ts already uses puts at about 4.82:1, comfortably clear of the floor. tests/text-contrast.test.ts gets a real assertion where there was previously only a comment explaining why the border went unmeasured. Verified it actually pins the fix: reverting the border to border-vsl/25 makes the test's anchor regex fail to match, since it now requires the bare border-vsl string with no alpha suffix. --- src/renderer/src/components/ui/Grid.tsx | 6 +++++- tests/text-contrast.test.ts | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/ui/Grid.tsx b/src/renderer/src/components/ui/Grid.tsx index e4afddaf..e2863f48 100644 --- a/src/renderer/src/components/ui/Grid.tsx +++ b/src/renderer/src/components/ui/Grid.tsx @@ -85,7 +85,11 @@ export function GridItem({ exit="exit" className={clsx( "w-full h-full rounded-sm backdrop-blur-xs border cursor-pointer shadow-sm shadow-zinc-950/50 hover:shadow-none duration-200", - selected ? "bg-vsd/50 border-vsl/25" : "bg-zinc-950/50 border-zinc-400/5", + // Full opacity, not the /25 this used to carry: the fill and panel behind it are dark + // enough that a translucent border barely separated from them (issue #258, 1.53:1 worst + // case against the 3:1 WCAG 1.4.11 floor for a boundary that is the sole selected-state + // cue). Opaque clears it with room to spare; see text-contrast.test.ts. + selected ? "bg-vsd/50 border-vsl" : "bg-zinc-950/50 border-zinc-400/5", onClick && "cursor-pointer", className )} diff --git a/tests/text-contrast.test.ts b/tests/text-contrast.test.ts index 4decee2a..96d013fb 100644 --- a/tests/text-contrast.test.ts +++ b/tests/text-contrast.test.ts @@ -288,17 +288,25 @@ describe("the brand accent where it carries text", () => { assertReadable("pending task icon", accent, TASKS_ROW, NON_TEXT_FLOOR) }) + it("keeps the Grid selected-card border above the non-text bar", () => { + // #258: the border used to sit at partial alpha, which barely separated from the panel behind + // it (1.53:1 worst case, below the 3:1 floor for a boundary that is the sole selected-state + // cue: see ModListCard.tsx's `selected={installed}`). It is opaque now, but the backdrop it + // reads against is still the card's own bg-vsd/NN fill composited over the grid panel, not the + // panel alone: a border painted at the default border-box clip shows through the fill wherever + // the fill itself has any transparency, which bg-vsd/NN always does here. + const fillAlpha = Number(match("components/ui/Grid.tsx", /selected \? "bg-vsd\/(\d+) border-vsl"/)[1]) / 100 + const fill: Layer = [themeColor("vsd"), fillAlpha] + const border: Layer = [themeColor("vsl"), 1] + assertReadable("Grid selected-card border", border, [shell, gridPanel, fill], NON_TEXT_FLOOR) + }) + it("keeps the accent ramp and its selected borders coherent", () => { const dark = luminance(themeColor("vsd")) const base = luminance(themeColor("vs")) const light = luminance(themeColor("vsl")) assert.ok(dark < base && base < light, "the vs/vsl/vsd ramp should stay dark-to-light in that order") - // The Grid border is decorative, not the sole indicator of the selected state (it sits at 25% - // alpha alongside a bg-vsd/50 fill), so it gets no contrast assertion here, only a pin that it - // still tracks --color-vsl; its own non-text-contrast gap predates this change and is tracked - // separately (issue filed alongside this PR). - match("components/ui/Grid.tsx", /selected \? "bg-vsd\/50 border-vsl\/(\d+)"/) // The ConfigPage tile border has a different backdrop on each of its two edges, so there is no // single ratio to assert here. Inside is the player's own thumbnail. Outside is the section panel // over the shell, which is FORM_SECTION above, so the accent's ratio on that edge is already