fix(player): the Plex player's fills hold on its chrome: an amber head on a white played part

A light theme's accent as a fill on the always-dark chrome was ~2.8:1 (Slate), under the 3:1 a
control needs. A native checkbox and slider tint with `accent-chosen` (the accent elsewhere, the
media amber under `media-marks`), and so does the skip button's countdown. The progress bar plays
white with an amber head: amber there marks a place, as the intro / credits markers do, and an amber
played part made a marker ahead of the head read as already played (owner's choice of three).
This commit is contained in:
2026-10-10 22:56:09 +02:00
parent 93192dadd8
commit 663a7e8012
4 changed files with 27 additions and 8 deletions
+4 -1
View File
@@ -118,7 +118,10 @@ style guide.
player (the Plex player, the YouTube stage) and on every surface painted `--media-chrome` (its menus,
panels and bars), and a form there (`media-scope`) re-points `--danger` too. A chosen item there is
white like the rest, marked by an amber tick (a track) or underline (a tab, `ui/mediaChrome`), never
the accent as text; a choice filled with the accent keeps it, its own text holds on it.
the accent as text; a choice filled with the accent keeps it, its own text holds on it. A fill that
must be seen there — a native checkbox or slider (`accent-chosen`), the skip button's countdown — is
the amber too; the Plex progress bar plays white with an amber head, so an amber marker ahead of
the head never reads as already played.
`theme.contract.test.ts` measures the red on the chrome and holds every class string that paints it
to name one of the two. Black and white are the media scrims and their text. Five schemes × dark
and light (B2): **Midnight** (blue), **Forest** (cyan), **Slate** (fuchsia), **Matrix** (moss) each earn their
+7 -4
View File
@@ -1429,12 +1429,12 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
<>
{/* draining fill across the whole button… */}
<span
className="absolute inset-y-0 left-0 bg-accent/50"
className="absolute inset-y-0 left-0 bg-chosen/50"
style={{ width: `${skipProgress * 100}%` }}
/>
{/* …plus a crisp bottom bar so the countdown is unmistakable */}
<span
className="absolute bottom-0 left-0 h-1 bg-accent"
className="absolute bottom-0 left-0 h-1 bg-chosen"
style={{ width: `${skipProgress * 100}%` }}
/>
</>
@@ -1542,13 +1542,16 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
}}
/>
))}
{/* The played part white, the head the media amber: amber is a marked place on
the chrome (the head, the markers above), and an amber played part made a
marker ahead of the head read as already played. */}
<div
className="absolute inset-y-0 left-0 bg-accent rounded-full"
className="absolute inset-y-0 left-0 bg-white rounded-full"
style={{ width: `${headPct}%` }}
/>
{/* The head: on hover for a mouse, always on touch (C3), where nothing hovers. */}
<div
className={`absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-accent transition-opacity ${
className={`absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-media-mark transition-opacity ${
scrub != null
? "opacity-100"
: "opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100"
@@ -2,7 +2,8 @@ import clsx from "clsx";
import type { InputHTMLAttributes, ReactNode, Ref } from "react";
// A6 (docs/DESIGN.md): checkbox, slider and colour well are the BROWSER's controls, tinted
// with the accent — keyboard, screen reader and forced-colours support come free, and a drawn copy
// with the chosen colour (the accent; the media amber on the player chrome) — keyboard, screen
// reader and forced-colours support come free, and a drawn copy
// would have to rebuild all three. What the browser does not give is the 44 touch target: the labelled
// row (`CheckRow`) or the slider's own box grows to it on a coarse pointer. Like `Input`, a raw
// `<input>` for any of them anywhere else is a lint error.
@@ -22,7 +23,7 @@ export function Checkbox({
type="checkbox"
onChange={(e) => onChange?.(e.target.checked)}
className={clsx(
"size-4 shrink-0 cursor-pointer accent-accent disabled:cursor-not-allowed",
"size-4 shrink-0 cursor-pointer accent-chosen disabled:cursor-not-allowed",
className,
)}
{...rest}
@@ -70,7 +71,7 @@ export function Slider({
type="range"
onChange={(e) => onChange(Number(e.target.value))}
className={clsx(
"min-h-6 w-full cursor-pointer accent-accent pointer-coarse:min-h-(--touch-target)",
"min-h-6 w-full cursor-pointer accent-chosen pointer-coarse:min-h-(--touch-target)",
className,
)}
{...rest}
+12
View File
@@ -100,6 +100,18 @@ describe("the media colours", () => {
);
expect(buttons).toMatch(/aria-pressed:text-chosen/);
expect(buttons).toMatch(/aria-expanded:text-chosen/);
// A native checkbox's and slider's fill is the chosen colour too (the player's settings), and so
// is the skip button's countdown: a fill that must be seen on the chrome.
const native = readFileSync(
new URL("../components/ui/NativeControls.tsx", import.meta.url),
"utf8",
);
expect(native).not.toMatch(/\baccent-accent\b/);
expect(native.match(/\baccent-chosen\b/g)).toHaveLength(2);
const plex = readFileSync(new URL("../components/PlexPlayer.tsx", import.meta.url), "utf8");
// No accent fill at all on the Plex player: its progress bar's played part is white and its head
// the media amber — a marked place, like the intro / credits markers and the clip's playhead.
expect(plex).not.toMatch(/\bbg-accent\b/);
// (A pressed FILLED button keeps the accent fill under its own `text-accent-fg`.)
expect(buttons).not.toMatch(/aria-(pressed|expanded):text-accent(?!-)/);
// The player's own components are chrome throughout: nothing chosen there is the accent's text