- 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.
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import { type ComponentProps } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Input } from "./Input";
|
|
import type { TimecodeDraft } from "./useTimecodeField";
|
|
|
|
// A field a cut's time is typed into (lib/timecode): the editor's segment ends, the clip panel's In /
|
|
// Out. A typed draft is the field's until Enter or leaving the field takes it; then the field shows
|
|
// the value as it landed — the caller may clamp it (a time past the end) — and a time it cannot read
|
|
// stays for fixing rather than vanishing or seeming taken: marked invalid, with a line saying how to
|
|
// type one. Nothing is marked while a first time is being typed; once one was offered (Enter,
|
|
// leaving the field) and could not be read, the field re-checks as it is typed, so the mark goes the
|
|
// moment the time reads. One element throughout, so Enter leaves the focus in the field (a field
|
|
// re-created per value dropped it on <body>).
|
|
//
|
|
// The draft lives in `useTimecodeField`, held by the caller, because the line is the caller's to
|
|
// place: in the flow under the fields' row (`TimecodeError`), never laid over the controls below it,
|
|
// and outside a field's <label>, where it would become part of the field's name.
|
|
|
|
type Props = Omit<
|
|
ComponentProps<typeof Input>,
|
|
"value" | "defaultValue" | "onChange" | "invalid"
|
|
> & {
|
|
field: TimecodeDraft;
|
|
/** The id of the `TimecodeError` that speaks for this field while it is invalid. */
|
|
errorId: string;
|
|
};
|
|
|
|
export function TimecodeField({
|
|
field,
|
|
errorId,
|
|
onBlur,
|
|
onKeyDown,
|
|
"aria-describedby": describedBy,
|
|
...rest
|
|
}: Props) {
|
|
const described = [describedBy, field.invalid ? errorId : null].filter(Boolean).join(" ");
|
|
return (
|
|
<Input
|
|
{...rest}
|
|
value={field.text}
|
|
invalid={field.invalid}
|
|
aria-describedby={described || undefined}
|
|
onChange={(e) => field.type(e.target.value)}
|
|
onBlur={(e) => {
|
|
field.offer();
|
|
onBlur?.(e);
|
|
}}
|
|
onKeyDown={(e) => {
|
|
onKeyDown?.(e);
|
|
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
e.preventDefault();
|
|
field.offer();
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** How a time is typed, said once one could not be read: rendered by the caller only while one of
|
|
* its fields is invalid, in the flow under them, and read out as it appears. */
|
|
export function TimecodeError({ id, hours }: { id: string; hours: boolean }) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<p id={id} role="alert" className="text-xs text-danger">
|
|
{t(hours ? "common.timecodeInvalidHours" : "common.timecodeInvalid")}
|
|
</p>
|
|
);
|
|
}
|