Skip to content
File

Blob: src/client/components/ui/input.tsx

typescript54 lines
1import { forwardRef, useId, type InputHTMLAttributes, type ReactNode } from "react";
2 
3interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
4 label?: ReactNode;
5 labelIcon?: ReactNode;
6 helperText?: string;
7 error?: string;
8}
9 
10export const Input = forwardRef<HTMLInputElement, InputProps>(
11 ({ label, labelIcon, helperText, error, className, id: idProp, ...rest }, ref) => {
12 const generatedId = useId();
13 const id = idProp ?? generatedId;
14 const helperId = helperText ? `${id}-helper` : undefined;
15 const errorId = error ? `${id}-error` : undefined;
16 
17 return (
18 <div className="space-y-1.5">
19 {label ? (
20 <label htmlFor={id} className="flex items-center gap-1.5 text-sm font-medium text-zinc-400">
21 {labelIcon}
22 {label}
23 </label>
24 ) : null}
25 <input
26 ref={ref}
27 id={id}
28 aria-describedby={errorId ?? helperId}
29 aria-invalid={error ? true : undefined}
30 className={[
31 "w-full rounded-lg border border-zinc-700/60 bg-zinc-800 px-4 py-2.5 text-sm text-zinc-100 placeholder:text-zinc-500 focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas disabled:opacity-50",
32 error && "border-red-500/50 focus:border-red-500/50 focus:ring-red-500/30",
33 className,
34 ]
35 .filter(Boolean)
36 .join(" ")}
37 {...rest}
38 />
39 {error ? (
40 <p id={errorId} className="text-xs text-red-400">
41 {error}
42 </p>
43 ) : helperText ? (
44 <p id={helperId} className="text-xs text-zinc-400">
45 {helperText}
46 </p>
47 ) : null}
48 </div>
49 );
50 },
51);
52 
53Input.displayName = "Input";