feat(a11y): R9 focus foundation — glass shadow fix, focus-visible ring, highlight/selected split

S1 — recipe foundation:
- Move .glass/.glass-card/.glass-menu into @layer components so a JSX shadow-* utility overrides the
  glass box-shadow again; the ~9 dead shadow-* on glass surfaces (modals, menus, save-bar, chat) now
  paint their intended drop-shadow. Perf-mode and :hover overrides stay unlayered so they still win.
- Add a .selectable recipe: the hover tint menu/list rows already use, PLUS the app-wide
  focus-visible ring that was missing on non-input controls (the biggest a11y gap).
- Split the popover roving highlight from the selected state (AnchoredPopover): a selected/checked
  PopoverItem now gets a ring ONLY, so its resting paint (bg-accent fill / text-accent / checkbox)
  survives being highlighted instead of downgrading to a 35% tint or being recoloured. Fixes the 4
  review findings deferred from R10-S0 generically; drops the leftover per-consumer data-active ring
  in tableHeader.

S2 — worst offenders:
- Account-menu trigger + switch/add/sign-out rows adopt .selectable (visible keyboard focus).
- Saved-views rows: group-focus-within reveals the hover-only action buttons for keyboard users;
  aria-current on the active row; focus-visible ring on every button.
- The 3 invisible-focus inputs get an accent focus ring: SearchBar + ShareDialog via focus-within on
  the glass pill/row, AboutCell via focus-visible on the trigger.

Deferred to a later R9 sprint: the full unification of all ~14 selection idioms, a shared <NavItem>,
and .is-current/.chip recipes (no consumer yet in this pass).

Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style checks confirm the glass
shadow now wins, the focus outlines render (2px accent), and the split renders (selected ring-only,
unselected fill).
This commit is contained in:
2026-08-08 04:27:51 +02:00
parent 280ba87f25
commit 51f9c8d0ed
8 changed files with 108 additions and 78 deletions
+9 -5
View File
@@ -250,11 +250,15 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
* focus-visible heuristic, so a `:focus-visible` highlight can stay invisible until the first arrow
* key. (Since the Tailwind v4 migration `bg-accent/NN` works — v4 does the opacity modifier via
* color-mix — so the recipe no longer needs a hand-written color-mix arbitrary value.) */
// Applied to EVERY PopoverItem so the roving-focus row is clearly visible across all schemes: an
// accent fill plus a 1px inset accent ring for a defined edge. (The previous per-consumer 24% fill
// alone read as barely-there — a real UAT finding, 2026-08-08.)
const ACTIVE_HIGHLIGHT =
// The roving-focus highlight, SPLIT by selection state (R9). A NON-selected row gets the familiar
// accent fill + inset ring. A SELECTED/checked row gets ring-ONLY, so the consumer's own resting
// selected paint (a bg-accent fill, text-accent, a checkbox) SURVIVES being highlighted instead of
// being downgraded to a 35% tint or having its text recoloured (the R10-S0 review findings). The two
// can't be merged onto one recipe: the fill and the selected paint both drive background/color on the
// same element, so the highlight must yield to the selected state rather than overwrite it.
const HIGHLIGHT_UNSELECTED =
"data-active:bg-accent/35 data-active:text-fg data-active:ring-1 data-active:ring-inset data-active:ring-accent/60";
const HIGHLIGHT_SELECTED = "data-active:ring-2 data-active:ring-inset data-active:ring-accent/70";
export function PopoverItem({
children,
@@ -323,7 +327,7 @@ export function PopoverItem({
tabIndex={focusActive ? 0 : -1}
data-active={visualActive ? "true" : undefined}
disabled={disabled}
className={`${ACTIVE_HIGHLIGHT}${className ? ` ${className}` : ""}`}
className={`${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`}
>
{children}
</button>