fix(clip): a time's error line sits in the flow, the remove button off the cut, and every player surface rings amber

- useTimecodeField holds a time field's draft for its caller, which places TimecodeError (role
  alert) in its own flow under the fields' row: it no longer lies over the crop row taking its
  clicks, runs out of the editor's card, or joins the field's <label> name.
- The editor's remove-cut buttons sit in a row under the track at their cut (trackLeft), off the
  cut's grab area, where a press drags it; RangeTrack's adornment slot is gone.
- media-focus re-points --focus to the media amber on the Plex player, the YouTube stage and every
  bg-media-chrome surface; a contract test holds each such class string to media-focus or
  media-scope, the field's focus edge to the focus token, and --color-focus to the inline theme.
- An upload's quota headline says the converted file does not fit, not "Storage full".
- DESIGN.md: the tokens paragraph says what is re-pointed where; the decision row measures all
  three colours on the chrome.
This commit is contained in:
2026-10-10 21:29:55 +02:00
parent 89d18b46a6
commit 977ba244fa
22 changed files with 306 additions and 153 deletions
+6 -19
View File
@@ -9,6 +9,7 @@ import {
type ReactNode,
} from "react";
import clsx from "clsx";
import { trackFrac, trackPct } from "./trackPosition";
// A horizontal time track: the shared body of every timeline that marks points in a piece of media
// (the download editor's cut list, the players' clip panel). It draws what it is given — regions
@@ -48,10 +49,6 @@ export interface TrackHandle {
onChange: (time: number, final: boolean) => void;
/** The drawn bar's look (its colour); the hit area around it is the track's. */
barClassName?: string;
/** Drawn at the handle, e.g. a remove button; it should stop its own pointer events. Beside the
* slider, not in it: a slider's content is not a control to assistive tech, and its keys are the
* slider's. */
adornment?: ReactNode;
}
interface TrackRegion {
@@ -108,9 +105,8 @@ export function RangeTrack({
playheadClassName = "bg-white",
}: Props) {
const win: RangeWindow = view ?? { from: 0, to: duration };
const span = Math.max(win.to - win.from, 1e-6);
const frac = (t: number) => (t - win.from) / span;
const pct = (f: number) => `${(clamp(f, 0, 1) * 100).toFixed(3)}%`;
const frac = (t: number) => trackFrac(t, win);
const pct = trackPct;
// The handle a pointer is dragging, and the press on the bare track (a scrub), so a move is only
// acted on between its own down and up.
const [dragging, setDragging] = useState<{
@@ -239,7 +235,7 @@ export function RangeTrack({
e.stopPropagation();
move(next, true);
};
return [
return (
<div
key={h.id}
role="slider"
@@ -279,17 +275,8 @@ export function RangeTrack({
}}
>
<div className={clsx("h-full w-1 rounded-sm", h.barClassName ?? "bg-white/90")} />
</div>,
h.adornment != null && (
<div
key={`${h.id}-adornment`}
className="pointer-events-none absolute inset-y-0 z-base w-6 -translate-x-1/2"
style={{ left: pct(f) }}
>
{h.adornment}
</div>
),
];
</div>
);
})}
</div>
);