fix(design): the command palette's active row takes the menus' roving highlight, not an invisible bg-card; the Plex typeahead's hand copy of it goes (E28)

The palette row was ~1.07:1 on its glass (B3 names bg-card hovers a violation). ROVING_HIGHLIGHT is
exported from AnchoredPopover for the listboxes that are not PopoverItems; a contract holds that no file
paints the fill itself and every hand-built option row takes the recipe.
This commit is contained in:
2026-09-25 23:51:39 +02:00
parent ce171fa954
commit 6088e476a7
5 changed files with 65 additions and 9 deletions
+4 -4
View File
@@ -11,6 +11,7 @@ import { useDebounced } from "../lib/useDebounced";
import { availablePlexFields, healPlexField, plexFieldLabelKey } from "../lib/plexSearch";
import { Pressable } from "./ui/Pressable";
import { SURFACE } from "./ui/surfaces";
import { ROVING_HIGHLIGHT } from "./ui/AnchoredPopover";
// The Plex header search box: the shared SearchBar enriched with the S2 search scope-pill (a
// GitHub-style field selector inside the box) and an instant typeahead of the top matches attached
@@ -156,10 +157,9 @@ export default function PlexSearchBox() {
onMouseEnter={() => setHi(i)}
onClick={() => pick(i)}
// The pointer moves `hi` too, so the highlight IS the hover: the menus' roving
// recipe (AnchoredPopover's HIGHLIGHT_UNSELECTED), one look for the mouse and the keys.
className={`flex w-full items-center gap-3 rounded-lg p-1.5 transition ${
hi === i ? "bg-accent/35 text-fg ring-1 ring-inset ring-accent/60" : ""
}`}
// recipe (AnchoredPopover's ROVING_HIGHLIGHT), one look for the mouse and the keys.
data-active={hi === i ? "true" : undefined}
className={`flex w-full items-center gap-3 rounded-lg p-1.5 transition ${ROVING_HIGHLIGHT}`}
>
<img
src={c.thumb}
@@ -96,7 +96,9 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
// EXCEPTION: a menu whose SELECTED item paints its OWN row background (a solid bg-accent — the
// table-filter options) passes `keepSelectedPaint`, so the highlight yields to ring-only there; a fill
// would downgrade that solid paint to a 35% tint (the R9 concern, now scoped to just that idiom).
const HIGHLIGHT_UNSELECTED =
// Exported as ROVING_HIGHLIGHT for a listbox that is not a PopoverItem (the command palette, the Plex
// typeahead): the row carries `data-active` and takes this, so the keyboard row looks the same everywhere.
export const ROVING_HIGHLIGHT =
"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:bg-accent/35 data-active:text-fg data-active:ring-2 data-active:ring-inset data-active:ring-accent/80";
@@ -214,7 +216,7 @@ export function PopoverItem({
? keepSelectedPaint
? HIGHLIGHT_SELECTED_RING_ONLY
: HIGHLIGHT_SELECTED
: HIGHLIGHT_UNSELECTED
: ROVING_HIGHLIGHT
}${className ? ` ${className}` : ""}`}
>
{children}
@@ -124,6 +124,19 @@ describe("CommandPalette", () => {
).toContain("page:feed");
});
// E28 F35: the active row wore `bg-card`, ~1.07:1 on the palette's glass. It is the menus' row now.
it("marks the active row with the menus' roving highlight, and only that row", () => {
open();
const rows = screen.getAllByRole("option");
const active = rows.filter((r) => r.hasAttribute("data-active"));
expect(active).toHaveLength(1);
expect(active[0]!.getAttribute("aria-selected")).toBe("true");
for (const r of rows) {
expect(r.className).toContain("data-active:bg-accent/35");
expect(r.className).not.toMatch(/(^|\s)bg-card(\s|$)/);
}
});
// The arrows walk the flat result list, not the sections — crossing a group heading is invisible.
it("walks results with the arrows, across group boundaries", () => {
open();
@@ -7,6 +7,7 @@ import { closeTopOverlayThenRun } from "../../lib/history";
import { pageStep, pageTarget } from "../../lib/listPaging";
import { Pressable } from "./Pressable";
import { InnerInput } from "./Input";
import { ROVING_HIGHLIGHT } from "./AnchoredPopover";
import { fieldClass } from "./fieldStyles";
// The app's jump-to bar: one text field over every place you can go and every app-wide thing you can
@@ -270,9 +271,9 @@ function CommandPaletteBody({ onClose, groups }: { onClose: () => void; groups:
if (e.movementX === 0 && e.movementY === 0) return;
setPicked(item.id);
}}
className={`flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-sm ${
item.id === activeId ? "bg-card text-fg" : "text-muted hover:text-fg"
}`}
// The menus' roving highlight: a `bg-card` row was ~1.07:1 on the palette's glass (E28).
data-active={item.id === activeId ? "true" : undefined}
className={`flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-sm text-muted hover:text-fg ${ROVING_HIGHLIGHT}`}
>
{item.icon && <span className="shrink-0">{item.icon}</span>}
<span className="min-w-0 flex-1 truncate">{item.label}</span>