first commit

This commit is contained in:
2025-08-16 09:52:47 +05:30
commit 4319823c76
249 changed files with 1192986 additions and 0 deletions
+430
View File
@@ -0,0 +1,430 @@
---
import type { HTMLAttributes } from "astro/types";
import { cva } from "class-variance-authority";
import { cn } from "../utils/cn";
import Icon from "./Icon.astro";
type Kind =
| "text"
| "email"
| "password"
| "number"
| "textarea"
| "select"
| "date"
| "time"
| "file"
| "checkbox"
| "radio"
| "switch";
interface Option {
value: string;
label: string;
disabled?: boolean;
}
interface Props extends HTMLAttributes<"div"> {
id?: string;
name: string;
label?: string;
kind?: Kind;
placeholder?: string;
description?: string;
error?: string;
required?: boolean;
disabled?: boolean;
value?: string | number | boolean;
options?: Option[];
multiple?: boolean;
rows?: number;
prefixText?: string;
suffixText?: string;
iconLeft?: string;
iconRight?: string;
size?: "sm" | "md" | "lg";
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 inputBase = cva(
"py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-lg sm:text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600",
{
variants: {
size: {
sm: "h-10 px-3 text-sm",
md: "h-11 px-3 text-sm",
lg: "h-12 px-4 text-base",
},
withIconLeft: { true: "pl-10" },
withIconRight: { true: "pr-10" },
},
},
);
const selectBase = cva(
"py-3 px-4 pe-9 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600",
{
variants: {
size: {
sm: "h-10 px-3 text-sm",
md: "h-11 px-3 text-sm",
lg: "h-12 px-4 text-base",
},
withIconLeft: { true: "pl-10" },
withIconRight: { true: "pr-10" },
},
},
);
const iconSize =
p.size === "sm" ? "size-4" : p.size === "lg" ? "size-5" : "size-5";
const labelCls = "block text-sm font-medium mb-2 dark:text-white";
const helpCls = "mt-2 text-sm text-gray-500 dark:text-neutral-500";
const errCls = "text-sm text-red-600 mt-2";
const groupCls = "relative flex items-stretch";
const addonBase =
"px-4 inline-flex items-center min-w-fit rounded-s-md border border-e-0 border-gray-200 bg-gray-50 text-sm text-gray-500 dark:bg-neutral-700 dark:border-neutral-700 dark:text-neutral-400";
const controlWrap = "relative";
---
<div class={cn("space-y-1.5", p.class)} data-field={p.name}>
{
p.label &&
[
"text",
"textarea",
"email",
"password",
"number",
"date",
"time",
"file",
"select",
].includes(p.kind ?? "text") && (
<label for={fieldId} class={labelCls}>
{p.label} {p.required && <span class="text-danger">*</span>}
</label>
)
}
{
["text", "email", "password", "number", "date", "time"].includes(
p.kind ?? "text",
) && (
<div class={groupCls}>
{p.prefixText && (
<span class={cn(addonBase, "border-r-0 rounded-r-none")}>
{p.prefixText}
</span>
)}
<div class={cn(controlWrap, "flex-1")}>
{p.iconLeft && (
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-muted-foreground">
<Icon name={p.iconLeft} class={iconSize} />
</span>
)}
<input
id={fieldId}
name={p.name}
type={(p.kind as any) ?? "text"}
class={cn(
inputBase({
size: p.size ?? "md",
withIconLeft: !!p.iconLeft,
withIconRight:
!!p.iconRight || p.kind === "password",
}),
p.prefixText && "rounded-l-none",
p.suffixText && "rounded-r-none",
)}
data-control
placeholder={p.placeholder}
required={p.required}
disabled={p.disabled}
aria-invalid={!!p.error}
aria-describedby={
[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
value={
typeof p.value === "string" ||
typeof p.value === "number"
? String(p.value)
: undefined
}
/>
{p.kind === "password" && (
<button
type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-muted-foreground hover:text-foreground"
onclick={`const i = this.previousElementSibling; i.type = i.type === 'password' ? 'text' : 'password';`}
>
<Icon name="mdi:eye" class={iconSize} />
</button>
)}
{p.iconRight && (
<span class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-muted-foreground">
<Icon name={p.iconRight} class={iconSize} />
</span>
)}
</div>
{p.suffixText && (
<span class={cn(addonBase, "border-l-0 rounded-l-none")}>
{p.suffixText}
</span>
)}
</div>
)
}
{
p.kind === "file" && (
<div class={groupCls}>
{p.prefixText && (
<span class={cn(addonBase, "border-r-0 rounded-r-none")}>
{p.prefixText}
</span>
)}
<div class={cn(controlWrap, "flex-1")}>
{p.iconLeft && (
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-muted-foreground">
<Icon name={p.iconLeft} class={iconSize} />
</span>
)}
<input
id={fieldId}
name={p.name}
type={(p.kind as any) ?? "text"}
class={cn(
"block w-full border border-gray-200 shadow-sm rounded-lg text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 file:bg-gray-50 file:border-0 file:me-4 file:py-3 file:px-4 dark:file:bg-neutral-700 dark:file:text-neutral-400",
p.prefixText && "rounded-l-none",
p.suffixText && "rounded-r-none",
)}
data-control
multiple
placeholder={p.placeholder}
required={p.required}
disabled={p.disabled}
aria-invalid={!!p.error}
aria-describedby={
[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
value={
typeof p.value === "string" ||
typeof p.value === "number"
? String(p.value)
: undefined
}
/>
{p.iconRight && (
<span class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-muted-foreground">
<Icon name={p.iconRight} class={iconSize} />
</span>
)}
</div>
{p.suffixText && (
<span class={cn(addonBase, "border-l-0 rounded-l-none")}>
{p.suffixText}
</span>
)}
</div>
)
}
{
p.kind === "textarea" && (
<div class={controlWrap}>
<textarea
id={fieldId}
name={p.name}
rows={p.rows ?? 4}
placeholder={p.placeholder}
required={p.required}
disabled={p.disabled}
data-control
aria-invalid={!!p.error}
aria-describedby={
[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
class={cn(
"py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-lg sm:text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600",
p.size === "sm"
? "text-sm p-3"
: p.size === "lg"
? "text-base p-4"
: "text-sm p-3",
)}
data-initial={typeof p.value === "string" ? p.value : ""}
/>
</div>
)
}
{
p.kind === "select" && (
<div class={controlWrap}>
<select
id={fieldId}
name={p.name}
multiple={p.multiple}
required={p.required}
disabled={p.disabled}
data-control
aria-invalid={!!p.error}
aria-describedby={
[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
class={cn(
selectBase({ size: p.size ?? "md" }),
"appearance-none bg-[length:16px_16px] pr-9",
)}
>
{!p.multiple && p.placeholder && (
<option
value=""
disabled
selected={p.value ? undefined : true}
>
{p.placeholder}
</option>
)}
{(p.options ?? []).map((opt) => (
<option
value={opt.value}
selected={String(p.value) === opt.value}
disabled={opt.disabled}
>
{opt.label}
</option>
))}
</select>
<span class="pointer-events-none absolute inset-y-0 right-0 pr-3 flex items-center text-muted-foreground">
<Icon name="mdi:chevron-down" class={iconSize} />
</span>
</div>
)
}
{
p.kind === "checkbox" && (
<label class="flex items-center gap-3">
<input
id={fieldId}
name={p.name}
type="checkbox"
class="shrink-0 mt-0.5 border-gray-200 rounded-sm text-blue-600 focus:ring-blue-500 checked:border-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800"
checked={!!p.value}
disabled={p.disabled}
data-control
aria-invalid={!!p.error}
aria-describedby={
[descId, p.error ? errId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
/>
<div>
{p.label && (
<span class="text-sm text-foreground">{p.label}</span>
)}
{p.description && (
<p id={descId} class={helpCls}>
{p.description}
</p>
)}
</div>
</label>
)
}
{
p.kind === "radio" && (
<fieldset class="space-y-2">
{p.label && (
<legend class="text-sm font-medium text-foreground">
{p.label}
</legend>
)}
<div class="space-x-3">
{(p.options ?? []).map((opt) => (
<label class="inline-flex items-center gap-2">
<input
type="radio"
name={p.name}
value={opt.value}
checked={String(p.value) === opt.value}
class="shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 checked:border-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800"
disabled={opt.disabled || p.disabled}
data-control
/>
<span class="text-sm text-foreground">
{opt.label}
</span>
</label>
))}
</div>
</fieldset>
)
}
{
p.kind === "switch" && (
<div class="flex items-center gap-3">
<label class="relative inline-block w-11 h-6 cursor-pointer">
<input
id={fieldId}
name={p.name}
type="checkbox"
class="sr-only peer"
checked={!!p.value}
disabled={p.disabled}
data-control
/>
<span class="absolute inset-0 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out peer-checked:bg-blue-600 dark:bg-neutral-700 dark:peer-checked:bg-blue-500 peer-disabled:opacity-50 peer-disabled:pointer-events-none" />
<span class="absolute top-1/2 start-0.5 -translate-y-1/2 size-5 bg-white rounded-full shadow-xs transition-transform duration-200 ease-in-out peer-checked:translate-x-full dark:bg-neutral-400 dark:peer-checked:bg-white" />
</label>
{p.label && (
<label
for={fieldId}
class="text-sm text-gray-500 dark:text-neutral-400"
>
{p.label}
</label>
)}
</div>
)
}
{
p.description && !["checkbox", "radio"].includes(p.kind ?? "text") && (
<p id={descId} class={helpCls}>
{p.description}
</p>
)
}
<!-- Always-present error container (client fills it) -->
<p id={errId} class={errCls} hidden>{p.error ?? ""}</p>
</div>