fix(plex): the player's settings panel is a tab list with named, touch-sized rows

- Its section switch is `ui/Tabs` (new `tone="media"` paint for the dark chrome) with a wired
  tabpanel; the panel opens with the focus on the selected tab.
- On/off rows are `CheckRow` (the whole row, 44 on touch), the "no background" checkbox too.
- Every slider and the four colour wells carry the name their row shows.
- The clock's date-style buttons reach the touch target.
This commit is contained in:
2026-09-27 14:38:27 +02:00
parent 76eead976f
commit a7d41eb5f3
5 changed files with 327 additions and 212 deletions
+16 -5
View File
@@ -15,6 +15,8 @@ import CountBadge from "./CountBadge";
// ONE Tab stop (the selected tab; roving tabindex), the arrows move and select (either axis, since the
// vertical rail is a row below `sm`), Home/End jump to the ends. The panel it controls is wired with
// `useTabPanel`: the tabs point at it (`aria-controls`) and it is labelled by the selected tab.
// `tone="media"` is the same list painted for the player's dark chrome (the Plex player's settings
// panel), which looks the same on every theme because the picture under it does.
export interface TabDef {
id: string;
@@ -32,6 +34,7 @@ export default function Tabs({
className,
idBase,
label,
tone,
}: {
tabs: TabDef[];
active: string;
@@ -45,6 +48,8 @@ export default function Tabs({
idBase?: string;
/** The tab list's accessible name, when the page heading beside it does not say what it switches. */
label?: string;
/** Painted for the media chrome instead of the page (equal-width, white on black). */
tone?: "media";
}) {
const ownBase = useId();
const base = idBase ?? ownBase;
@@ -100,11 +105,17 @@ export default function Tabs({
tabIndex={tb.id === stop ? 0 : -1}
data-testid={tb.testId}
onClick={() => onChange(tb.id)}
className={`pill touch-target items-center rounded-lg px-3 text-sm ${
vertical
? "inline-flex shrink-0 gap-1.5 py-1.5 sm:flex sm:w-full sm:justify-start sm:gap-2 sm:py-2 sm:text-left"
: "inline-flex gap-1.5 py-1.5"
} ${on ? "font-medium shadow-md shadow-accent/20" : "state-layer"}`}
className={
tone === "media"
? `touch-target flex-1 items-center justify-center whitespace-nowrap rounded px-2 py-1 text-2xs ${
on ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10"
}`
: `pill touch-target items-center rounded-lg px-3 text-sm ${
vertical
? "inline-flex shrink-0 gap-1.5 py-1.5 sm:flex sm:w-full sm:justify-start sm:gap-2 sm:py-2 sm:text-left"
: "inline-flex gap-1.5 py-1.5"
} ${on ? "font-medium shadow-md shadow-accent/20" : "state-layer"}`
}
>
{Icon && <Icon className="h-4 w-4 shrink-0" />}
{tb.label}
@@ -27,7 +27,7 @@ const PLAIN: Record<string, [number, string]> = {
"PlaylistsRail.tsx": [1, "the load-error retry"],
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
"PlexInfo.tsx": [1, "the rating filter (a number)"],
"PlexPlayerSettings.tsx": [2, "setting options"],
"PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
"PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
"PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
"PlexSearchBox.tsx": [1, "a listbox option"],