/** * Input - single-line text field with optional label and hint. * * States: default / focus / disabled / error. Focus is tracked in React state * (the kit styles inline via useTheme, so there is no :focus pseudo to lean on); * on focus the border takes the accent colour plus a soft accent ring built from * C.accentSoft. The error accent reuses the theme's red (PRIO.high) - the only * red the token surface exposes - so nothing is hardcoded. * * All colours come from the theme; sizes/radii from tokens, matching Button/Chip. */ import { useId, useState } from "react"; import type { CSSProperties } from "react"; import { FS, FW, R, SP } from "@rukin/tokens"; import { useTheme } from "./theme"; export type InputProps = { value?: string; onChange?: (value: string) => void; label?: string; /** Helper text under the field. In the error state it turns red. */ hint?: string; placeholder?: string; /** true = red border; string = red border + the string shown as hint. */ error?: boolean | string; disabled?: boolean; type?: string; id?: string; style?: CSSProperties; }; export function Input({ value, onChange, label, hint, placeholder, error, disabled, type = "text", id, style, }: InputProps) { const { C, PRIO } = useTheme(); const [focused, setFocused] = useState(false); const autoId = useId(); const inputId = id ?? autoId; const danger = PRIO.high.dot; const hasError = Boolean(error); const hintText = typeof error === "string" ? error : hint; let borderColor = C.hairline; if (hasError) borderColor = danger; else if (focused) borderColor = C.accent; // Disabled must stay legible on the canvas (grey) fill: a visible neutral // border instead of the hairline, rather than dimming the whole element with // opacity (which also washes the border out). else if (disabled) borderColor = `${C.muted}55`; const ring = focused && !hasError ? `0 0 0 3px ${C.accentSoft}` : "none"; return (