feat(design): the header search is a 40 glass pill; a wrapper field dims for its input only (E28 S3c)

This commit is contained in:
2026-09-25 00:05:31 +02:00
parent dac9d89f91
commit db10bfbbca
13 changed files with 78 additions and 39 deletions
+22 -7
View File
@@ -6,6 +6,7 @@ import clsx from "clsx";
// 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
@@ -15,6 +16,7 @@ export type FieldSize =
// 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",
@@ -24,18 +26,31 @@ const SIZES: Record<FieldSize, string> = {
// 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), the outlined-field convention. Invalid
// is the danger edge, focused or not. On a coarse pointer the box grows to the 44 touch target.
// 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 bg-transparent text-fg placeholder:text-muted transition-colors " +
"min-w-0 border edge-strong text-fg placeholder:text-muted transition-colors " +
"focus-within:border-accent focus-within:ring-1 focus-within:ring-accent focus-within:outline-hidden " +
"aria-invalid:border-danger aria-invalid:ring-danger has-aria-invalid:border-danger has-aria-invalid:ring-danger " +
"disabled:cursor-not-allowed disabled:opacity-50 has-disabled:cursor-not-allowed has-disabled:opacity-50 " +
"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 a utility so the focus ring's box-shadow composes with it instead of dropping it.
export function fieldClass({
size = "md",
pill = false,
}: { size?: FieldSize; pill?: boolean } = {}) {
// A4: a toolbar search is a pill; every other field r8.
return clsx(BASE, SIZES[size], pill ? "rounded-full" : "rounded-lg");
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-[0_18px_44px_-16px_rgba(0,0,0,0.6)]" : "bg-transparent",
);
}