1"use client";
2
3
7import { forwardRef, type InputHTMLAttributes } from "react";
8import { Loader2 } from "lucide-react";
9import { cn } from "@/lib/utils";
10
11export function Field({
12 label,
13 htmlFor,
14 error,
15 hint,
16 children
17}: {
18 label: string;
19 htmlFor: string;
20 error?: string;
21 hint?: string;
22 children: React.ReactNode;
23}) {
24 return (
25 <div className="space-y-1.5">
26 <label htmlFor={htmlFor} className="block text-sm font-medium">
27 {label}
28 </label>
29 {children}
30 {error ? (
31 <p role="alert" className="text-xs text-red-600">
32 {error}
33 </p>
34 ) : hint ? (
35 <p className="text-xs text-muted-foreground">{hint}</p>
36 ) : null}
37 </div>
38 );
39}
40
41export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
42 function Input({ className, ...props }, ref) {
43 return (
44 <input
45 ref={ref}
46 className={cn(
47 "h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm",
48 "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
49 "disabled:cursor-not-allowed disabled:opacity-50",
50 className
51 )}
52 {...props}
53 />
54 );
55 }
56);
57
58export function SubmitButton({
59 busy,
60 children,
61 ...props
62}: React.ButtonHTMLAttributes<HTMLButtonElement> & { busy?: boolean }) {
63 return (
64 <button
65 type="submit"
66 disabled={props.disabled || busy}
67 className={cn(
68 "inline-flex h-9 w-full items-center justify-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground",
69 "hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
70 )}
71 {...props}
72 >
73 {busy ? <Loader2 className="size-4 animate-spin" aria-hidden /> : null}
74 {children}
75 </button>
76 );
77}
78
79export function FormError({ message, "data-testid": testId }: { message?: string | null; "data-testid"?: string }) {
80 if (!message) return null;
81 return (
82 <p
83 role="alert"
84 data-testid={testId}
85 className="rounded-md border border-red-600/30 bg-red-600/10 px-3 py-2 text-sm text-red-700 dark:text-red-400"
86 >
87 {message}
88 </p>
89 );
90}
91