/** * Textarea - multi-line text field, the multi-row sibling of Input. * * Shares Input's visual language exactly: same border/radius/padding, focus * takes the accent + soft ring, error uses the theme red (PRIO.high), disabled * keeps a legible neutral border on the canvas fill. Extras over Input: * - `rows` for the resting height; * - `autoResize` grows the box to fit content (no inner scrollbar); * - `maxLength` + `showCount` render a live "n/max" counter under the field. * * All colours from the theme; sizes/radii from tokens. */ import { useEffect, useId, useRef, useState } from "react"; import type { CSSProperties } from "react"; import { FS, FW, R, SP } from "@rukin/tokens"; import { useTheme } from "./theme"; export type TextareaProps = { value?: string; onChange?: (value: string) => void; label?: string; hint?: string; placeholder?: string; /** true = red border; string = red border + the string shown as hint. */ error?: boolean | string; disabled?: boolean; /** Resting height in text rows. Default 3. */ rows?: number; /** Grow the box to fit content instead of scrolling. */ autoResize?: boolean; maxLength?: number; /** Show a live character counter (n or n/max). */ showCount?: boolean; id?: string; style?: CSSProperties; }; export function Textarea({ value, onChange, label, hint, placeholder, error, disabled, rows = 3, autoResize = false, maxLength, showCount = false, id, style, }: TextareaProps) { const { C, PRIO } = useTheme(); const [focused, setFocused] = useState(false); const autoId = useId(); const inputId = id ?? autoId; const ref = useRef(null); const danger = PRIO.high.dot; const hasError = Boolean(error); const hintText = typeof error === "string" ? error : hint; const count = value?.length ?? 0; let borderColor = C.hairline; if (hasError) borderColor = danger; else if (focused) borderColor = C.accent; else if (disabled) borderColor = `${C.muted}55`; const ring = focused && !hasError ? `0 0 0 3px ${C.accentSoft}` : "none"; // Auto-resize: reset height then match scrollHeight whenever the text changes. useEffect(() => { if (!autoResize) return; const el = ref.current; if (!el) return; el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }, [value, autoResize]); return (
{label && ( )}