'use client';
import { useFormStatus } from 'react-dom';
import { useEffect, useMemo, useState, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes } from 'react';
import { Eye, EyeOff, Loader2 } from 'lucide-react';
import { passwordStrength } from '@/lib/auth/password-strength';
import type { ActionState } from '@/lib/auth/state';
import { cn } from '@/lib/format';
export const inputClass = 'block w-full rounded-md border border-border bg-elevated px-3 py-2 text-base text-fg shadow-none placeholder:text-subtle focus:border-border-strong focus:outline-none focus:ring-2 focus:ring-[var(--ri-ring)] md:text-sm disabled:opacity-60';
export function Field({ label, name, error, hint, children, className }: { label: ReactNode; name: string; error?: string | null; hint?: ReactNode; children: ReactNode; className?: string }) {
return (
{children}
{error ? (
{error}
) : hint ? (
{hint}
) : null}
);
}
export function TextInput(props: InputHTMLAttributes & { invalid?: boolean }) {
const { invalid, className, ...rest } = props;
return ;
}
export function Select(props: SelectHTMLAttributes) {
const { className, ...rest } = props;
return ;
}
export function TextArea(props: TextareaHTMLAttributes) {
const { className, ...rest } = props;
return ;
}
export function Checkbox({ name, label, defaultChecked, description }: { name: string; label: ReactNode; defaultChecked?: boolean; description?: ReactNode }) {
return (
);
}
export function SubmitButton({ children, variant = 'primary', className, pendingText, disabled }: { children: ReactNode; variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; className?: string; pendingText?: string; disabled?: boolean }) {
const { pending } = useFormStatus();
const styles = {
primary: 'bg-accent text-accent-fg hover:opacity-90',
secondary: 'border border-border bg-elevated text-fg hover:bg-inset',
danger: 'bg-loss text-white hover:opacity-90',
ghost: 'text-muted hover:bg-inset hover:text-fg',
}[variant];
return (
);
}
export function FormMessage({ state, className }: { state: ActionState; className?: string }) {
if (state.error) {
return (
{state.error}
);
}
if (state.ok && state.message) {
return (
{state.message}
);
}
return null;
}
/** Password input with reveal toggle and live strength meter. */
export function PasswordField({ name = 'password', label = 'Password', autoComplete = 'new-password', error, email, showMeter = true, placeholder }: { name?: string; label?: string; autoComplete?: string; error?: string | null; email?: string; showMeter?: boolean; placeholder?: string }) {
const [value, setValue] = useState('');
const [show, setShow] = useState(false);
const strength = useMemo(() => passwordStrength(value, email), [value, email]);
const colors = ['bg-loss', 'bg-loss', 'bg-alert', 'bg-gain', 'bg-gain'];
return (
setValue(e.target.value)} invalid={Boolean(error)} className="pr-10" placeholder={placeholder} />
{showMeter && value ? (
{[0, 1, 2, 3].map((i) => (
))}
{strength.label}
) : null}
);
}
/** Six-digit code input: numeric keyboard, one-time-code autofill, auto-submit when complete. */
export function CodeInput({ name = 'code', autoSubmit = true, error, label = 'Code' }: { name?: string; autoSubmit?: boolean; error?: string | null; label?: string }) {
const [v, setV] = useState('');
useEffect(() => {
if (autoSubmit && v.length === 6) {
const el = document.getElementById(name) as HTMLInputElement | null;
el?.form?.requestSubmit();
}
}, [v, autoSubmit, name]);
return (
setV(e.target.value.replace(/\D/g, '').slice(0, 6))} className={cn(inputClass, 'mono-num text-center text-2xl tracking-[0.5em] md:text-2xl', error && 'border-loss')} aria-invalid={Boolean(error) || undefined} autoFocus />
);
}
/** Countdown-enabled resend button (client-side cooldown mirrors the server's). */
export function Cooldown({ seconds, children }: { seconds: number; children: (ready: boolean, left: number) => ReactNode }) {
return {children};
}
function CooldownInner({ seconds, children }: { seconds: number; children: (ready: boolean, left: number) => ReactNode }) {
const [left, setLeft] = useState(seconds);
useEffect(() => {
if (seconds <= 0) return;
const t = setInterval(() => setLeft((l) => (l <= 1 ? 0 : l - 1)), 1000);
return () => clearInterval(t);
}, [seconds]);
return <>{children(left <= 0, left)}>;
}