import { type JSX, Show, splitProps, createUniqueId } from 'solid-js'; import { cn } from '../utils/cn'; export interface InputProps extends Omit, 'onInput' | 'onChange' | 'size'> { /** Field label rendered above the control and linked via `for`/`id`. */ label?: string; /** Helper text rendered below the control. */ hint?: string; /** Error text; rendered below the control and flips the field invalid. */ error?: string; /** Control density. Defaults to `md`. */ size?: 'sm' | 'md'; /** Force the invalid (destructive-border) state without an `error` string. */ invalid?: boolean; /** Leading affix (icon, unit). Rendered inside the field row, before the input. */ leading?: JSX.Element; /** Trailing affix (icon, inline button). Rendered inside the field row, after the input. */ trailing?: JSX.Element; /** Fires per keystroke with the current value. */ onValueInput?: (value: string) => void; /** Fires on commit (blur) with the current value. */ onValueChange?: (value: string) => void; } // The single source of the field shell styling — lifted verbatim from the // `inputBase` constant that used to live in `src/components/form-widgets.tsx`. // `Input` now owns it; the form widgets render `Input` rather than re-pasting it. export const FIELD_BASE = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none'; // When a leading/trailing affix is present the border + padding wrap the whole // row and the focus ring is driven off the row (`focus-within`), so the input // itself sits borderless and transparent inside it. const FIELD_ROW = 'flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-ring'; const ROW_INPUT = 'min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed'; const SIZE_SM = 'px-2.5 py-1'; const INVALID = 'border-destructive dark:border-red-400/70'; // Suppress the native search affordances Chrome/WebKit render for `type="search"`. // Without this the browser's `::-webkit-search-cancel-button` (×) stacks on top of // a custom clear control (e.g. kai-search's `part="clear"`) — a double ×. Applied // to the inner `` in BOTH layouts (the field can be `type="search"` either // way; kai-search uses the affix layout for its leading icon). const SEARCH_RESET = '[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-cancel-button]:hidden'; /** * `Input`: the token-themed single-line text field shell. A label/hint/error * stack around a field row that holds an optional `leading` affix, the * ``, and an optional `trailing` affix. The shared border/background/ring * styling lives here (the single field source the form widgets build on). * * Parts: `field` (the bordered control), `input`, `label`, `hint`. * a11y: a generated id links `