From e93947969f4b3ac3d80fc79905d16252990c81d2 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 10 Aug 2026 19:55:13 +0200 Subject: [PATCH] =?UTF-8?q?fix(console):=20the=20hide=20button=20was=20inv?= =?UTF-8?q?isible=20AND=20clickable=20=E2=80=94=20a=20corner=20nobody=20co?= =?UTF-8?q?uld=20see=20dropped=20games?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding a library title shipped in 0.26.0 and was, in practice, unusable: the operator reported there was simply no button, then found it by CLICKING the empty top-right corner of a poster. Both halves of that are the same defect. The card's control row was `opacity-0` until `group-hover`, and `opacity-0` paints nothing while still HIT-TESTING. So the corner of every tile in the grid was a live hide button with no visual presence at all: a stray click there removed that title from every play surface — the client grid, the native clients, the GameStream app list — with nothing on screen having suggested a control was under the cursor. What read as "the button finally appeared" was the hide taking effect, since `hidden` is the one state that drops the `opacity-0`. The feature announced itself by firing. And the reveal rested on hover ALONE. `:hover` never fires on a touch screen, so on a tablet the hide control was unreachable by construction and discoverable only by the blind click above. The original commit spotted this hazard for UN-hide — it kept those controls always-visible so nobody could be stranded in the hidden state — but left the hide side hover-gated, which is the same trap one step earlier. So opacity and `pointer-events` now move together, always: whatever cannot be seen cannot be clicked. `pointer-coarse:` shows the row outright wherever the device has no hover to give, rather than making touch a second-class path. Keyboard reach is unchanged — `pointer-events: none` does not block focus, so tabbing in still trips `focus-within`, which now restores interactivity along with opacity. The eye icon also gains a `title`. On a scanned entry it is the ONLY control on the card, with no edit/delete beside it to read as a toolbar, and an unlabelled eye-with-slash is not a promise that a game is about to leave the library. Verified in the built CSS rather than by eye, because a variant that does not compile fails exactly like the bug being fixed: `@media(pointer:coarse)` emits both `pointer-coarse:opacity-100` and `pointer-coarse:pointer-events-auto`, and it lands at the END of the sheet — media queries add no specificity, so this tie against the base `.pointer-events-none` / `.opacity-0` is won on source order, not by accident. The `group-hover:` and `focus-within:` forms compile to `:is(:where(.group):hover *)` and `:focus-within`, carrying a pseudo-class each, so they win on specificity outright. Console: tsc clean, production build clean, biome clean on the touched file, i18n 633 messages across en+de. --- web/src/sections/Library/GameCard.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/web/src/sections/Library/GameCard.tsx b/web/src/sections/Library/GameCard.tsx index fdb5d723..ebfcf2e4 100644 --- a/web/src/sections/Library/GameCard.tsx +++ b/web/src/sections/Library/GameCard.tsx @@ -122,10 +122,22 @@ export const GameCard: FC = ({ {/* A hidden card keeps its controls VISIBLE rather than hover-revealed. Hover-to-reveal is fine for an ordinary tile, but the un-hide button is the only way out of the hidden state — requiring a hover to discover it would strand anyone on a touch - screen, which is exactly where the console's pointer work landed. */} + screen, which is exactly where the console's pointer work landed. + Two rules that used to be one, because `opacity-0` alone got BOTH of them wrong: + 1. Invisible must also mean UNCLICKABLE. An `opacity-0` element paints nothing and + still hit-tests, so the top-right corner of every poster was a live hide button + nobody could see — a stray click there dropped that title from every play + surface with no visible cause. Opacity and `pointer-events` move together now. + 2. The reveal cannot rest on hover ALONE. `:hover` never fires on a touch screen, + so the control was unreachable there by construction and discoverable only by + blind-clicking the corner. `pointer-coarse` shows it outright wherever the + device has no hover to give. Keyboard reach is unaffected: `pointer-events: + none` does not block focus, so tabbing in still trips `focus-within`. */}