TimecodeField drops a draft when the value moves under it (another segment selected, a nudge), so leaving the field never commits it to another cut; it is invalid only once offered, with a line under it saying how to type a time. On the always-dark chrome media-scope points every focus ring at --media-mark and errors at the new --media-danger (a light theme's accent and danger were under 3:1 there). A track whose dragged handle unmounts ends the drag on the pointer's release instead of locking up; a remove refused while clips are queued leaves no focus to take later; each remove-cut button names its cut and is a 24px target. DESIGN.md records the media tokens; stale comments fixed.
59 lines
3.1 KiB
TypeScript
59 lines
3.1 KiB
TypeScript
import clsx from "clsx";
|
|
|
|
// The A4 field frame (docs/DESIGN.md): one bordered box for every text field, whether the frame is the
|
|
// <input> itself (`Input`, `Textarea`) or a wrapper that holds an icon, chips or a clear button beside
|
|
// a bare `InnerInput` (a log search, the Combobox). `focus-within` covers both: an input is within
|
|
// itself. Its own file so `Input.tsx` exports components only (react-refresh).
|
|
|
|
export type FieldSize =
|
|
| "lg" // 40 — the page's one hero field: the header search, the command palette
|
|
| "md" // 32 — a form field, lines up with a `md` Button
|
|
| "sm" // 28 — a toolbar search, a field in a popover or an inline rename, lines up with the toolbar row
|
|
| "xs" // 24 — inside a table row or a pager
|
|
| "heading" // an inline rename of a page title: the field takes the heading's own type (the caller's)
|
|
| "free"; // the caller lays the box out (a multi-line field, a chip field that wraps)
|
|
|
|
// On a coarse pointer the text is 1rem, so a tablet browser does not zoom the page on focus (not on a
|
|
// heading, whose type is already larger).
|
|
const SIZES: Record<FieldSize, string> = {
|
|
lg: "h-10 px-3 text-sm pointer-coarse:text-base",
|
|
md: "h-8 px-3 text-sm pointer-coarse:text-base",
|
|
sm: "h-7 px-2.5 text-sm pointer-coarse:text-base",
|
|
xs: "h-6 px-2 text-xs pointer-coarse:text-base",
|
|
heading: "px-2 py-0.5",
|
|
free: "",
|
|
};
|
|
|
|
// The edge is --edge-strong (the fg mixed 55% into the bg, ≥ 3:1 on every scheme — WCAG 1.4.11); focus
|
|
// turns it into a 2px accent edge (the border plus a 1px ring; `focus`, so a scope over another ground
|
|
// can name its own, see media-scope), the outlined-field convention. Invalid
|
|
// is the danger edge, focused or not. A wrapper frame keys both states to its own INPUT, not to any
|
|
// descendant: the header search's Go button is disabled while the query is empty, and a bare
|
|
// `has-disabled:` faded the whole box. On a coarse pointer the box grows to the 44 touch target.
|
|
const BASE =
|
|
"min-w-0 border edge-strong text-fg placeholder:text-muted transition-colors " +
|
|
"focus-within:border-focus focus-within:ring-1 focus-within:ring-focus focus-within:outline-hidden " +
|
|
"aria-invalid:border-danger aria-invalid:ring-danger " +
|
|
"has-[input[aria-invalid=true]]:border-danger has-[input[aria-invalid=true]]:ring-danger " +
|
|
"disabled:cursor-not-allowed disabled:opacity-50 " +
|
|
"has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 " +
|
|
"pointer-coarse:min-h-(--touch-target)";
|
|
|
|
// `floating`: the field hovers over the scrolling page (the header search), so it keeps the glass-menu
|
|
// surface under its edge instead of a see-through one (A2: what floats over content is glass). The
|
|
// shadow is the `shadow-float` utility (highlight + drop, the menu's own) so the focus ring's box-shadow
|
|
// composes with it instead of dropping it.
|
|
export function fieldClass({
|
|
size = "md",
|
|
pill = false,
|
|
floating = false,
|
|
}: { size?: FieldSize; pill?: boolean; floating?: boolean } = {}) {
|
|
// A4: a search is a pill; every other field r8.
|
|
return clsx(
|
|
BASE,
|
|
SIZES[size],
|
|
pill ? "rounded-full" : "rounded-lg",
|
|
floating ? "glass-menu shadow-float" : "bg-transparent",
|
|
);
|
|
}
|