fix(popover): stronger, centralized active-row highlight (UAT finding)

The roving-focus row in every popover menu was barely distinguishable from the
menu surface — an accent fill at only 24%, and the recipe was copy-pasted across
5 consumers. Centralize it in PopoverItem (ACTIVE_HIGHLIGHT, applied to every
item) and strengthen it: accent/35 fill + a 1px inset accent ring for a defined
edge that reads across all 6 schemes in both themes. Consumers no longer carry
the recipe.

Uses the now-working `bg-accent/NN` / `ring-accent/NN` utilities (v4 does the
opacity modifier via color-mix) instead of the old hand-written color-mix
arbitrary value the v3 no-op forced. Verified in-browser: fill + inset ring
render on the active item, dark and light.
This commit is contained in:
2026-08-08 01:16:21 +02:00
parent e592d800a1
commit 22e0d286c0
6 changed files with 16 additions and 11 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ export default function AddToPlaylist({
// the floating element only). `toggle()` already no-ops while busy, so re-entrancy is
// guarded without touching focus; `aria-busy` on the list conveys the pending state.
label={playlistName(pl, t)}
className="w-full flex items-center gap-2 text-sm px-1.5 py-1.5 rounded-lg text-fg outline-hidden transition hover:bg-card/60 data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)]"
className="w-full flex items-center gap-2 text-sm px-1.5 py-1.5 rounded-lg text-fg outline-hidden transition hover:bg-card/60"
>
<span
className={`grid place-items-center w-4 h-4 rounded border ${
+11 -6
View File
@@ -223,13 +223,18 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
* one exclusive choice, `menuitemcheckbox`+`checked` when several can be on at once,
* `option`+`selected` for a listbox choice, plain `menuitem` for an action.
*
* The active (roving) item carries `data-active` — style the highlight off THAT, not `:focus-visible`:
* The active (roving) item carries `data-active`, and the highlight is applied HERE for every item
* (see ACTIVE_HIGHLIGHT) — consumers don't add it. It's driven off `data-active`, not `:focus-visible`:
* a click-opened menu focuses the first item programmatically, which does NOT reliably trip the
* focus-visible heuristic, so a `:focus-visible` highlight can stay invisible until the first arrow
* key. The recommended recipe is an accent tint — and note `bg-accent/NN` does NOT work here (the
* theme's `--accent` is a hex var, and Tailwind v3 can't inject alpha into it), so use a color-mix
* arbitrary value like the app's own glass idiom:
* `data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)] data-active:text-fg`. */
* 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 =
"data-active:bg-accent/35 data-active:text-fg data-active:ring-1 data-active:ring-inset data-active:ring-accent/60";
export function PopoverItem({
children,
onSelect,
@@ -286,7 +291,7 @@ export function PopoverItem({
tabIndex={active ? 0 : -1}
data-active={active ? "true" : undefined}
disabled={disabled}
className={className}
className={`${ACTIVE_HIGHLIGHT}${className ? ` ${className}` : ""}`}
>
{children}
</button>
+1 -1
View File
@@ -75,7 +75,7 @@ function TagEditorMenu({
checked={on}
onSelect={() => onToggle(tg.id)}
label={tg.name}
className={`w-full flex items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition hover:bg-card data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)] data-active:text-fg ${
className={`w-full flex items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition hover:bg-card ${
on ? "text-fg" : "text-muted"
}`}
>
+1 -1
View File
@@ -78,7 +78,7 @@ export default function LanguageSwitcher({
setOpen(false);
}}
label={l.label}
className="w-full flex items-center justify-between gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg hover:bg-card data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)] data-active:text-fg"
className="w-full flex items-center justify-between gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg hover:bg-card"
>
<span>{l.label}</span>
{l.code === value && <Check className="w-4 h-4 text-accent" />}
+1 -1
View File
@@ -94,7 +94,7 @@ export default function ViewSwitcher<T extends string>({
setOpen(false);
}}
label={o.label}
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-sm text-left outline-hidden transition data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)] ${
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-sm text-left outline-hidden transition ${
on ? "text-accent" : "text-fg hover:bg-card"
}`}
>
+1 -1
View File
@@ -197,7 +197,7 @@ function FilterFunnel<T>({
onApply(col.key, on ? arr.filter((x) => x !== o.value) : [...arr, o.value])
}
label={o.label}
className={`flex w-full items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition data-active:bg-[color-mix(in_srgb,var(--accent)_24%,transparent)] ${
className={`flex w-full items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${
on ? "text-fg" : "text-muted hover:bg-card"
}`}
>