Files
stack/packages/ui/src/components/Input.tsx
T
2026-02-22 21:12:13 +00:00

87 lines
2.3 KiB
TypeScript

import { useState, forwardRef } from "react";
import type { InputHTMLAttributes, ReactElement } from "react";
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
label?: string;
error?: string;
helperText?: string;
fullWidth?: boolean;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{
label,
error,
helperText,
fullWidth = false,
className = "",
id,
style,
onFocus,
onBlur,
...props
},
ref
): ReactElement {
const [isFocused, setIsFocused] = useState(false);
const inputId = id ?? `input-${Math.random().toString(36).substring(2, 11)}`;
const errorId = error ? `${inputId}-error` : undefined;
const helperId = helperText ? `${inputId}-helper` : undefined;
const inputStyle: React.CSSProperties = {
background: "var(--bg-mid)",
border: error
? `1px solid var(--danger)`
: isFocused
? `1px solid var(--primary)`
: `1px solid var(--border)`,
color: "var(--text)",
outline: "none",
boxShadow: isFocused ? `0 0 0 2px rgba(47,128,255,0.2)` : "none",
...style,
};
const widthClass = fullWidth ? "w-full" : "";
return (
<div className={fullWidth ? "w-full" : ""}>
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium mb-1"
style={{ color: "var(--text-2)" }}
>
{label}
</label>
)}
<input
ref={ref}
id={inputId}
className={`px-3 py-2 rounded-md transition-colors ${widthClass} ${className}`}
style={inputStyle}
aria-invalid={error ? "true" : "false"}
aria-describedby={[errorId, helperId].filter(Boolean).join(" ") || undefined}
onFocus={(e) => {
setIsFocused(true);
onFocus?.(e);
}}
onBlur={(e) => {
setIsFocused(false);
onBlur?.(e);
}}
{...props}
/>
{error && (
<p id={errorId} className="mt-1 text-sm" style={{ color: "var(--danger)" }} role="alert">
{error}
</p>
)}
{helperText && !error && (
<p id={helperId} className="mt-1 text-sm" style={{ color: "var(--muted)" }}>
{helperText}
</p>
)}
</div>
);
});