The AtmosphereConf talks your skyline missed
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: recover covered-card opacity on focus + active (#49)

Addresses code review on #47: with the cubic floor at 0.10 opacity,
heavily covered cards become unreadable for keyboard and touch users
who can't trigger :hover.

The reviewer suggested adding focus-within and tabIndex={0} to the
LumeCard div. Both would be wrong here:
- focus-within only matches when the element or a descendant is focused.
LumeCard's children (interest dot, text, chips, % badge) are not
focusable, so focus-within would never fire from normal Tab navigation.
- Adding tabIndex={0} to LumeCard creates a double-tab problem: Tab once
focuses the wrapping <Link>, Tab again focuses the LumeCard div which
navigates nowhere. Screen readers also announce the same target twice.

Right fix: make the wrapping <Link> drive a named group, then use
group-hover/card, group-focus-visible/card, and group-active/card on the
LumeCard. The Link is naturally focusable (the focus ring goes there),
and visual recovery on the child LumeCard propagates from the group
state. No double-tab, no dead focus-within.

Changes:
- ScoredTalksGrid Link: add `group/card block` and a focus-visible
outline (the previous outline lived on LumeCard, where it was inert
because LumeCard was never the focus target).
- LumeCard: add group-hover/card / group-focus-visible/card / group-
active/card variants alongside the existing :hover modifiers. Direct
:hover stays for callers that don't wrap LumeCard in a focusable
ancestor.
- LumeCard: drop the dead focus-visible:outline-* (Link handles it now).
- Score detail strip: add sm:group-hover/card and sm:group-focus-visible
/card so keyboard tab also reveals the strip on desktop. Mobile
behavior (always visible) unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+25 -3
+5 -1
src/components/scored-talks-grid.tsx
··· 33 33 return ( 34 34 <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> 35 35 {scoredTalks.map(({ talk, score }, index) => ( 36 - <Link key={talk.rkey} href={`/talk/${talk.rkey}`}> 36 + <Link 37 + key={talk.rkey} 38 + href={`/talk/${talk.rkey}`} 39 + className="group/card block rounded-lg focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary-fixed" 40 + > 37 41 <LumeCard 38 42 className="h-full" 39 43 glowIntensity={score?.intensity ?? 0}
+20 -2
src/components/ui/lume-card.tsx
··· 72 72 ? "border-primary-fixed-dim/50" 73 73 : "border-primary-fixed-dim/20", 74 74 "transition-[box-shadow,border-color,opacity] duration-500", 75 + // Direct hover/focus/active recovery for when LumeCard is used 76 + // standalone (no wrapping focusable ancestor). 75 77 "hover:biolume-glow-strong hover:!opacity-100", 76 - "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-fixed", 78 + // Recover via a `group/card` ancestor — typically the wrapping 79 + // <Link> in ScoredTalksGrid. Keyboard focus lands on the Link 80 + // (not on this div), so without the named-group plumbing covered 81 + // cards at the cubic floor (~0.10 opacity) would be unreadable 82 + // for keyboard and touch users. group-hover/card duplicates the 83 + // pointer hover path (since hovering the Link also hovers this 84 + // div in practice, but explicit is clearer); group-focus-visible 85 + // /card handles Tab navigation; group-active/card handles touch 86 + // taps where :hover doesn't apply. 87 + "group-hover/card:biolume-glow-strong group-hover/card:!opacity-100", 88 + "group-focus-visible/card:biolume-glow-strong group-focus-visible/card:!opacity-100", 89 + "group-active/card:!opacity-100", 77 90 isUnderstory ? "animate-breathe" : "", 78 91 className, 79 92 ].join(" ")} ··· 106 119 "px-5 pb-3 pt-0", 107 120 // Mobile: always visible (no hover capability) 108 121 "max-h-12 opacity-100", 109 - // Desktop (sm+): hidden by default, revealed on hover via group-hover 122 + // Desktop (sm+): hidden by default, revealed on hover/focus. 123 + // group-hover targets this LumeCard (the unnamed group); 124 + // group-hover/card and group-focus-visible/card target the 125 + // wrapping Link so keyboard tab also reveals the strip. 110 126 "sm:max-h-0 sm:overflow-hidden sm:opacity-0", 111 127 "sm:transition-all sm:duration-300", 112 128 "sm:group-hover:max-h-12 sm:group-hover:opacity-100", 129 + "sm:group-hover/card:max-h-12 sm:group-hover/card:opacity-100", 130 + "sm:group-focus-visible/card:max-h-12 sm:group-focus-visible/card:opacity-100", 113 131 ].join(" ")} 114 132 > 115 133 <div className="border-t border-primary-fixed-dim/20 pt-2">