diff --git a/src/components/OtpField.astro b/src/components/OtpField.astro new file mode 100644 index 0000000..15955e8 --- /dev/null +++ b/src/components/OtpField.astro @@ -0,0 +1,279 @@ +--- +import type { HTMLAttributes } from "astro/types"; +import { cn } from "../utils/cn"; + +interface Props extends HTMLAttributes<"div"> { + id?: string; + name: string; + label?: string; + description?: string; + error?: string; // SSR error (optional) + required?: boolean; + disabled?: boolean; + value?: string; // initial value (e.g., from server) + length?: number; // number of boxes + placeholder?: string | string[]; + onlyDigits?: boolean; // restrict to 0-9 + size?: "sm" | "md" | "lg"; // box sizes + autoFocus?: boolean; + class?: string; +} + +const p = Astro.props as Props; + +const fieldId = p.id ?? p.name; +const descId = p.description ? `${fieldId}-desc` : undefined; +const errId = `${fieldId}-err`; + +const len = Math.max(1, p.length ?? 6); +const initial = (p.value ?? "").slice(0, len); +const boxesSSR = Array.from({ length: len }, (_, i) => initial[i] ?? ""); + +const sizeCls = + p.size === "sm" + ? "wr:h-10 wr:w-10 wr:text-base" + : p.size === "lg" + ? "wr:h-14 wr:w-14 wr:text-xl" + : "wr:h-12 wr:w-12 wr:text-lg"; + +const boxBase = + "wr:rounded-lg wr:border wr:border-gray-200 wr:bg-background wr:text-center wr:tracking-widest " + + "wr:focus-visible:outline-none wr:focus-visible:ring-2 wr:focus-visible:ring-primary/40 " + + "wr:disabled:opacity-50 wr:disabled:pointer-events-none " + + "wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-100"; + +const groupCls = "wr:flex wr:items-center wr:gap-2 wr:select-none"; +--- + +
+ {p.description} +
+ ) + } ++ {p.error ?? ""} +
+