"use client"; import { Loader2, CheckCircle2, XCircle } from "lucide-react"; /** * 마법사 전용 공용 입력 컴포넌트 (시안 v2 포팅). * - Field: label + hint + child input wrapper * - TextInput: 텍스트 입력 (mono, prefix/suffix, status 테두리 글로우) * - CheckAvailBadge: subdomain/db_prefix 실시간 검증 인디케이터 */ export type AvailStatus = "idle" | "checking" | "available" | "taken" | "reserved" | "invalid"; export function Field({ label, hint, required, full, error, children, }: { label: string; hint?: React.ReactNode; required?: boolean; full?: boolean; error?: string; children: React.ReactNode; }) { return (
{label} {required && ( )} {hint && ( {hint} )}
{children} {error && (
{error}
)}
); } type TextInputStatus = "ok" | "err" | "warn"; export function TextInput({ value, onChange, placeholder, mono, prefix, suffix, readOnly, type = "text", status, size = "md", }: { value: string; onChange?: (v: string) => void; placeholder?: string; mono?: boolean; prefix?: React.ReactNode; suffix?: React.ReactNode; readOnly?: boolean; type?: string; status?: TextInputStatus; size?: "sm" | "md"; }) { const border = status === "ok" ? "rgb(var(--v5-green-rgb))" : status === "err" ? "var(--v5-red)" : status === "warn" ? "var(--v5-amber)" : "var(--v5-border)"; const glow = status === "ok" ? "0 0 0 3px rgba(var(--v5-green-rgb), 0.12)" : status === "err" ? "0 0 0 3px rgba(var(--v5-red-rgb), 0.15)" : status === "warn" ? "0 0 0 3px rgba(var(--v5-amber-rgb), 0.12)" : "none"; return (
{prefix && (
{prefix}
)} onChange?.(e.target.value)} placeholder={placeholder} readOnly={readOnly} style={{ flex: 1, padding: size === "sm" ? "0.4rem 0.55rem" : "0.55rem 0.7rem", background: "transparent", border: 0, outline: "none", fontSize: size === "sm" ? "0.78rem" : "0.88rem", color: "var(--v5-text)", fontFamily: mono ? "var(--v5-font-mono)" : "inherit", }} /> {suffix && (
{suffix}
)}
); } export function CheckAvailBadge({ status, value }: { status: AvailStatus; value?: string }) { if (!value || status === "idle") return 입력 후 자동 확인; if (status === "checking") return ( 중복 확인 중… ); if (status === "available") return ( 사용 가능 ); if (status === "taken") return ( 이미 사용 중 ); if (status === "reserved") return ( 예약어 (사용 불가) ); return ( 형식 오류 ); } /** TextInput 의 status prop 과 매핑 */ export function availToInputStatus(a: AvailStatus): TextInputStatus | undefined { if (a === "available") return "ok"; if (a === "taken" || a === "reserved" || a === "invalid") return "err"; return undefined; } export function genPassword(length = 12): string { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789@#$"; const arr = new Uint32Array(length); if (typeof window !== "undefined" && window.crypto) { window.crypto.getRandomValues(arr); } let out = ""; for (let i = 0; i < length; i++) { out += chars[arr[i] % chars.length]; } return out; }