Hiding a library title shipped in 0.26.0 and was, in practice, unusable. Reported from the field as "I'm on the latest canary and there's simply no option to hide a game" — then, a few minutes later, "actually I just found it by clicking the top-right corner."
Both halves of that report are the same defect.
The button fired instead of revealing itself
The card's control row was opacity-0 until group-hover, and an opacity-0 element paints nothing while still hit-testing. So the top-right 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. hidden is the one state that drops the opacity-0, so the control became visible as a consequence of the game being hidden. 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 exact 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.
The fix
Opacity and pointer-events move together, always. Whatever cannot be seen cannot be clicked. This is the part that matters: it removes a whole class of silent destructive click, not just this one instance.
pointer-coarse: shows the row outright wherever the device has no hover to give, rather than leaving 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 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.
Ruled out along the way
@unom/ui's AnimatedCard gloss overlay (material-fx) is z-index: -1 + pointer-events: none, so it was never covering the button, and group does reach an ancestor through AnimatedCard's cn(...). The invisibility was purely the opacity-0.
Verification
Checked against the built CSS rather than by eye, because a variant that fails to compile fails exactly like the bug being fixed:
@media(pointer:coarse) emits both pointer-coarse:opacity-100 and pointer-coarse:pointer-events-auto, and 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.
One file, +19/−3.
Hiding a library title shipped in 0.26.0 and was, in practice, unusable. Reported from the field as "I'm on the latest canary and there's simply no option to hide a game" — then, a few minutes later, "actually I just found it by clicking the top-right corner."
Both halves of that report are the same defect.
## The button fired instead of revealing itself
The card's control row was `opacity-0` until `group-hover`, and **an `opacity-0` element paints nothing while still hit-testing**. So the top-right 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*. `hidden` is the one state that drops the `opacity-0`, so the control became visible as a consequence of the game being hidden. 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 exact 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.
## The fix
- **Opacity and `pointer-events` move together, always.** Whatever cannot be seen cannot be clicked. This is the part that matters: it removes a whole class of silent destructive click, not just this one instance.
- **`pointer-coarse:` shows the row outright** wherever the device has no hover to give, rather than leaving 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 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.
## Ruled out along the way
`@unom/ui`'s `AnimatedCard` gloss overlay (`material-fx`) is `z-index: -1` + `pointer-events: none`, so it was never covering the button, and `group` does reach an ancestor through `AnimatedCard`'s `cn(...)`. The invisibility was purely the `opacity-0`.
## Verification
Checked against the **built CSS** rather than by eye, because a variant that fails to compile fails exactly like the bug being fixed:
- `@media(pointer:coarse)` emits both `pointer-coarse:opacity-100` and `pointer-coarse:pointer-events-auto`, and 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.
One file, +19/−3.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Hiding a library title shipped in 0.26.0 and was, in practice, unusable. Reported from the field as "I'm on the latest canary and there's simply no option to hide a game" — then, a few minutes later, "actually I just found it by clicking the top-right corner."
Both halves of that report are the same defect.
The button fired instead of revealing itself
The card's control row was
opacity-0untilgroup-hover, and anopacity-0element paints nothing while still hit-testing. So the top-right 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.
hiddenis the one state that drops theopacity-0, so the control became visible as a consequence of the game being hidden. The feature announced itself by firing.And the reveal rested on hover alone
:hovernever 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 exact 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.
The fix
pointer-eventsmove together, always. Whatever cannot be seen cannot be clicked. This is the part that matters: it removes a whole class of silent destructive click, not just this one instance.pointer-coarse:shows the row outright wherever the device has no hover to give, rather than leaving touch a second-class path.pointer-events: nonedoes not block focus, so tabbing in still tripsfocus-within, which now restores interactivity along with opacity.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.Ruled out along the way
@unom/ui'sAnimatedCardgloss overlay (material-fx) isz-index: -1+pointer-events: none, so it was never covering the button, andgroupdoes reach an ancestor throughAnimatedCard'scn(...). The invisibility was purely theopacity-0.Verification
Checked against the built CSS rather than by eye, because a variant that fails to compile fails exactly like the bug being fixed:
@media(pointer:coarse)emits bothpointer-coarse:opacity-100andpointer-coarse:pointer-events-auto, and lands at the end of the sheet — media queries add no specificity, so this tie against the base.pointer-events-none/.opacity-0is won on source order, not by accident.group-hover:andfocus-within:forms compile to:is(:where(.group):hover *)and:focus-within, carrying a pseudo-class each, so they win on specificity outright.Console:
tscclean, production build clean, biome clean on the touched file, i18n 633 messages across en+de.One file, +19/−3.