Files
siftlode/frontend/src/components/ui/fieldStyles.ts
T
peter c27cfdcb9c fix(clip): typed times belong to their value, and every mark on the player chrome can be seen
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.
2026-10-10 20:28:52 +02:00

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",
);
}