Skip to content
File

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

typescript56 lines
1import type { InputHTMLAttributes, ReactNode } from "react";
2 
3interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
4 label?: string;
5 error?: string;
6 helperText?: string;
7 icon?: ReactNode;
8}
9 
10export function Input({ label, error, helperText, icon, id, className, ...rest }: InputProps) {
11 const errorId = error && id ? `${id}-error` : undefined;
12 const helperId = helperText && id ? `${id}-helper` : undefined;
13 const describedBy = [errorId, helperId].filter(Boolean).join(" ") || undefined;
14 
15 return (
16 <div>
17 {label && (
18 <label htmlFor={id} className="mb-1.5 block text-sm font-medium text-zinc-300">
19 {label}
20 </label>
21 )}
22 <div className="relative">
23 {icon && (
24 <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-zinc-500">
25 {icon}
26 </div>
27 )}
28 <input
29 id={id}
30 aria-describedby={describedBy}
31 aria-invalid={error ? true : undefined}
32 className={[
33 "w-full rounded-xl border border-zinc-700/60 bg-zinc-800/80 px-4 py-2.5 text-sm text-zinc-100 placeholder:text-zinc-500 read-only:cursor-default read-only:opacity-60 transition-colors focus:border-accent-500/50 focus:outline-none focus:ring-1 focus:ring-accent-500/30",
34 icon ? "pl-10" : "",
35 error ? "border-red-500/40" : "",
36 className,
37 ]
38 .filter(Boolean)
39 .join(" ")}
40 {...rest}
41 />
42 </div>
43 {error && (
44 <p id={errorId} className="mt-1 text-xs text-red-400">
45 {error}
46 </p>
47 )}
48 {helperText && !error && (
49 <p id={helperId} className="mt-1 text-xs text-zinc-500">
50 {helperText}
51 </p>
52 )}
53 </div>
54 );
55}