import clsx from "clsx"; // The A4 field frame (docs/DESIGN.md): one bordered box for every text field, whether the frame is the // 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 = { 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", ); }