Fixxed bugs From form and Fields
This commit is contained in:
Generated
+14
@@ -19,6 +19,7 @@
|
|||||||
"@iconify/json": "^2.2.373",
|
"@iconify/json": "^2.2.373",
|
||||||
"@iconify/utils": "^3.0.1",
|
"@iconify/utils": "^3.0.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
@@ -2686,6 +2687,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/class-variance-authority": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://polar.sh/cva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cli-boxes": {
|
"node_modules/cli-boxes": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"@iconify/json": "^2.2.373",
|
"@iconify/json": "^2.2.373",
|
||||||
"@iconify/utils": "^3.0.1",
|
"@iconify/utils": "^3.0.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const helpCls = "mt-2 text-sm text-gray-500 dark:text-neutral-500";
|
|||||||
const errCls = "text-sm text-red-600 mt-2";
|
const errCls = "text-sm text-red-600 mt-2";
|
||||||
const groupCls = "relative flex items-stretch";
|
const groupCls = "relative flex items-stretch";
|
||||||
const addonBase =
|
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";
|
"px-4 inline-flex items-center min-w-fit rounded-md border 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";
|
const controlWrap = "relative";
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,8 +144,6 @@ const controlWrap = "relative";
|
|||||||
}),
|
}),
|
||||||
p.prefixText && "rounded-l-none",
|
p.prefixText && "rounded-l-none",
|
||||||
p.suffixText && "rounded-r-none",
|
p.suffixText && "rounded-r-none",
|
||||||
p.iconLeft && "rounded-l-none",
|
|
||||||
p.iconRight && "rounded-r-none",
|
|
||||||
p.error &&
|
p.error &&
|
||||||
"border-danger focus-visible:ring-danger/40",
|
"border-danger focus-visible:ring-danger/40",
|
||||||
)}
|
)}
|
||||||
@@ -217,8 +215,6 @@ const controlWrap = "relative";
|
|||||||
"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",
|
"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.prefixText && "rounded-l-none",
|
||||||
p.suffixText && "rounded-r-none",
|
p.suffixText && "rounded-r-none",
|
||||||
p.iconLeft && "rounded-l-none",
|
|
||||||
p.iconRight && "rounded-r-none",
|
|
||||||
p.error &&
|
p.error &&
|
||||||
"border-danger focus-visible:ring-danger/40",
|
"border-danger focus-visible:ring-danger/40",
|
||||||
)}
|
)}
|
||||||
@@ -281,8 +277,6 @@ const controlWrap = "relative";
|
|||||||
: p.size === "lg"
|
: p.size === "lg"
|
||||||
? "text-base p-4"
|
? "text-base p-4"
|
||||||
: "text-sm p-3",
|
: "text-sm p-3",
|
||||||
p.iconLeft && "rounded-l-none",
|
|
||||||
p.iconRight && "rounded-r-none",
|
|
||||||
p.error && "border-danger focus-visible:ring-danger/40",
|
p.error && "border-danger focus-visible:ring-danger/40",
|
||||||
)}
|
)}
|
||||||
data-initial={typeof p.value === "string" ? p.value : ""}
|
data-initial={typeof p.value === "string" ? p.value : ""}
|
||||||
@@ -309,9 +303,7 @@ const controlWrap = "relative";
|
|||||||
}
|
}
|
||||||
class={cn(
|
class={cn(
|
||||||
selectBase({ size: p.size ?? "md" }),
|
selectBase({ size: p.size ?? "md" }),
|
||||||
"appearance-none bg-[length:16px_16px] pr-9",
|
"appearance-none bg-none pr-9",
|
||||||
p.iconLeft && "rounded-l-none",
|
|
||||||
p.iconRight && "rounded-r-none",
|
|
||||||
p.error && "border-danger focus-visible:ring-danger/40",
|
p.error && "border-danger focus-visible:ring-danger/40",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ interface Props extends HTMLAttributes<"div"> {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
options: Option[];
|
options: Option[];
|
||||||
value?: string[]; // initial selected values
|
value?: string[];
|
||||||
placeholder?: string; // shown when empty
|
placeholder?: string;
|
||||||
searchable?: boolean; // show search box in dropdown
|
searchable?: boolean;
|
||||||
clearable?: boolean; // show clear (x) button
|
clearable?: boolean;
|
||||||
maxSelections?: number; // optional selection cap
|
maxSelections?: number;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
|
display?: "text" | "chips";
|
||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +33,9 @@ const p = Astro.props as Props;
|
|||||||
const fieldId = p.id ?? p.name;
|
const fieldId = p.id ?? p.name;
|
||||||
const descId = p.description ? `${fieldId}-desc` : undefined;
|
const descId = p.description ? `${fieldId}-desc` : undefined;
|
||||||
const errId = `${fieldId}-err`;
|
const errId = `${fieldId}-err`;
|
||||||
|
|
||||||
const selected = new Set(p.value ?? []);
|
const selected = new Set(p.value ?? []);
|
||||||
const summary = selected.size
|
const summarySSR = selected.size
|
||||||
? Array.from(selected)
|
? Array.from(selected)
|
||||||
.map((v) => p.options.find((o) => o.value === v)?.label ?? v)
|
.map((v) => p.options.find((o) => o.value === v)?.label ?? v)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -47,11 +49,15 @@ const sizeCls =
|
|||||||
: "h-10 px-3 text-sm";
|
: "h-10 px-3 text-sm";
|
||||||
|
|
||||||
const controlCls =
|
const controlCls =
|
||||||
"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";
|
"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";
|
||||||
|
|
||||||
const chipBase =
|
const chipBase =
|
||||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs bg-muted text-foreground";
|
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs bg-muted text-foreground";
|
||||||
|
|
||||||
const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
||||||
|
const displayMode = p.display ?? "text";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class={cn("space-y-1.5", p.class)} data-field={p.name} id={containerId}>
|
<div class={cn("space-y-1.5", p.class)} data-field={p.name} id={containerId}>
|
||||||
@@ -67,9 +73,6 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
{
|
|
||||||
/** Visible control is a readonly input so validation styles apply here */
|
|
||||||
}
|
|
||||||
<input
|
<input
|
||||||
id={fieldId}
|
id={fieldId}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -79,21 +82,28 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
aria-describedby={[descId, p.error ? errId : undefined]
|
aria-describedby={[descId, p.error ? errId : undefined]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ") || undefined}
|
.join(" ") || undefined}
|
||||||
class={cn(controlCls, sizeCls, "pr-10 cursor-pointer")}
|
class={cn(
|
||||||
|
controlCls,
|
||||||
|
sizeCls,
|
||||||
|
"pr-10 cursor-pointer",
|
||||||
|
displayMode === "chips" && "pl-3",
|
||||||
|
)}
|
||||||
placeholder={p.placeholder ?? "Select..."}
|
placeholder={p.placeholder ?? "Select..."}
|
||||||
value={summary}
|
value={displayMode === "text" ? summarySSR : ""}
|
||||||
readonly
|
readonly
|
||||||
data-control
|
data-control
|
||||||
{...p.disabled ? { disabled: true } : {}}
|
{...p.disabled ? { disabled: true } : {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/** Chevron / Clear */}
|
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center gap-1 pr-2">
|
<div class="absolute inset-y-0 right-0 flex items-center gap-1 pr-2">
|
||||||
{
|
{
|
||||||
p.clearable !== false && selected.size > 0 && !p.disabled && (
|
p.clearable !== false && !p.disabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-1 rounded hover:bg-muted"
|
class={cn(
|
||||||
|
"p-1 rounded hover:bg-muted",
|
||||||
|
selected.size === 0 && "hidden",
|
||||||
|
)}
|
||||||
data-ms-clear
|
data-ms-clear
|
||||||
aria-label="Clear selection"
|
aria-label="Clear selection"
|
||||||
>
|
>
|
||||||
@@ -111,28 +121,15 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
<div
|
||||||
/** Selected chips (optional; comment out if you prefer only summary text) */
|
data-ms-chips
|
||||||
}
|
class={cn(
|
||||||
{
|
"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 flex flex-wrap gap-1 pr-14",
|
||||||
selected.size > 0 && (
|
displayMode === "chips" ? "" : "hidden",
|
||||||
<div class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 flex flex-wrap gap-1 pr-14">
|
|
||||||
{Array.from(selected)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((v) => {
|
|
||||||
const lab =
|
|
||||||
p.options.find((o) => o.value === v)?.label ??
|
|
||||||
v;
|
|
||||||
return <span class={chipBase}>{lab}</span>;
|
|
||||||
})}
|
|
||||||
{selected.size > 3 && (
|
|
||||||
<span class={chipBase}>+{selected.size - 3}</span>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/** Dropdown */}
|
|
||||||
<div
|
<div
|
||||||
class="absolute border-border bg-popover text-popover-foreground shadow-lg hidden mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700"
|
class="absolute border-border bg-popover text-popover-foreground shadow-lg hidden mt-2 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700"
|
||||||
id={`${containerId}-panel`}
|
id={`${containerId}-panel`}
|
||||||
@@ -198,19 +195,25 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-primary hover:underline disabled:opacity-50"
|
class="text-sm text-primary hover:underline disabled:opacity-50"
|
||||||
data-ms-select-all>Select all</button
|
data-ms-select-all
|
||||||
>
|
>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-muted-foreground hover:underline"
|
class="text-sm text-muted-foreground hover:underline"
|
||||||
data-ms-cancel>Cancel</button
|
data-ms-cancel
|
||||||
>
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-primary hover:underline"
|
class="text-sm text-primary hover:underline"
|
||||||
data-ms-apply>Apply</button
|
data-ms-apply
|
||||||
>
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,6 +239,7 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
fieldId,
|
fieldId,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
maxSelections: p.maxSelections ?? null,
|
maxSelections: p.maxSelections ?? null,
|
||||||
|
displayMode,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
const root = document.getElementById(containerId);
|
const root = document.getElementById(containerId);
|
||||||
@@ -248,39 +252,72 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
const btnCan = root.querySelector("[data-ms-cancel]");
|
const btnCan = root.querySelector("[data-ms-cancel]");
|
||||||
const btnApp = root.querySelector("[data-ms-apply]");
|
const btnApp = root.querySelector("[data-ms-apply]");
|
||||||
const sch = root.querySelector("[data-ms-search]");
|
const sch = root.querySelector("[data-ms-search]");
|
||||||
|
const chips = root.querySelector("[data-ms-chips]");
|
||||||
|
|
||||||
const checks = Array.from(
|
const checks = Array.from(
|
||||||
root.querySelectorAll(
|
root.querySelectorAll(
|
||||||
`input[type="checkbox"][name="${CSS.escape(name)}"]`,
|
`input[type="checkbox"][name="${CSS.escape(name)}"]`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let open = false;
|
let open = false;
|
||||||
|
const origPlaceholder = input.getAttribute("placeholder") || "Select...";
|
||||||
|
|
||||||
function setExpanded(
|
function setExpanded(v, { focusSearch = false } = {}) {
|
||||||
v,
|
|
||||||
{ focusSearch = false, refocusInput = false } = {},
|
|
||||||
) {
|
|
||||||
open = v;
|
open = v;
|
||||||
input.setAttribute("aria-expanded", String(open));
|
input.setAttribute("aria-expanded", String(open));
|
||||||
panel.classList.toggle("hidden", !open);
|
panel.classList.toggle("hidden", !open);
|
||||||
if (open && focusSearch) sch?.focus();
|
if (open && focusSearch) sch?.focus();
|
||||||
if (!open && refocusInput) input.focus();
|
}
|
||||||
|
|
||||||
|
function makeChip(label) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className =
|
||||||
|
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs bg-muted text-foreground";
|
||||||
|
span.textContent = label;
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleClear(show) {
|
||||||
|
if (!btnClr) return;
|
||||||
|
btnClr.classList.toggle("hidden", !show);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSummary() {
|
function updateSummary() {
|
||||||
const selected = checks.filter((c) => c.checked);
|
const selected = checks.filter((c) => c.checked);
|
||||||
const labels = selected
|
const labels = selected.map(
|
||||||
.slice(0, 3)
|
|
||||||
.map(
|
|
||||||
(c) =>
|
(c) =>
|
||||||
c
|
c
|
||||||
.closest("[data-ms-item]")
|
.closest("[data-ms-item]")
|
||||||
?.querySelector("span")
|
?.querySelector("span")
|
||||||
?.textContent?.trim() || c.value,
|
?.textContent?.trim() || c.value,
|
||||||
);
|
);
|
||||||
const extra = Math.max(0, selected.length - 3);
|
|
||||||
input.value = selected.length
|
if (displayMode === "text") {
|
||||||
? labels.join(", ") + (extra ? ` +${extra}` : "")
|
const preview = labels.slice(0, 3).join(", ");
|
||||||
|
const extra = Math.max(0, labels.length - 3);
|
||||||
|
input.value = labels.length
|
||||||
|
? preview + (extra ? ` +${extra}` : "")
|
||||||
: "";
|
: "";
|
||||||
|
input.placeholder = labels.length ? "" : origPlaceholder;
|
||||||
|
if (chips) {
|
||||||
|
chips.innerHTML = "";
|
||||||
|
chips.classList.add("hidden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.value = "";
|
||||||
|
input.placeholder = labels.length ? "" : origPlaceholder;
|
||||||
|
if (!chips) return;
|
||||||
|
chips.innerHTML = "";
|
||||||
|
const max = 3;
|
||||||
|
labels
|
||||||
|
.slice(0, max)
|
||||||
|
.forEach((lab) => chips.appendChild(makeChip(lab)));
|
||||||
|
const extra = labels.length - max;
|
||||||
|
if (extra > 0) chips.appendChild(makeChip(`+${extra}`));
|
||||||
|
chips.classList.toggle("hidden", labels.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleClear(labels.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceMax() {
|
function enforceMax() {
|
||||||
@@ -308,7 +345,6 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
updateSummary();
|
updateSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open/close
|
|
||||||
btnTgl?.addEventListener("click", () =>
|
btnTgl?.addEventListener("click", () =>
|
||||||
setExpanded(!open, { focusSearch: !open }),
|
setExpanded(!open, { focusSearch: !open }),
|
||||||
);
|
);
|
||||||
@@ -322,21 +358,20 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
|
||||||
btnClr?.addEventListener("click", (e) => {
|
btnClr?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
clearAll();
|
clearAll();
|
||||||
});
|
});
|
||||||
btnAll?.addEventListener("click", selectAll);
|
btnAll?.addEventListener("click", selectAll);
|
||||||
btnCan?.addEventListener("click", () => setExpanded(false)); // no refocus
|
btnCan?.addEventListener("click", () => setExpanded(false));
|
||||||
btnApp?.addEventListener("click", () => setExpanded(false)); // no refocus
|
btnApp?.addEventListener("click", () => setExpanded(false));
|
||||||
|
|
||||||
list.addEventListener("change", () => {
|
list.addEventListener("change", () => {
|
||||||
enforceMax();
|
enforceMax();
|
||||||
updateSummary();
|
updateSummary();
|
||||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search filter
|
|
||||||
sch?.addEventListener("input", () => {
|
sch?.addEventListener("input", () => {
|
||||||
const q = sch.value.trim().toLowerCase();
|
const q = sch.value.trim().toLowerCase();
|
||||||
root.querySelectorAll("[data-ms-item]").forEach((li) => {
|
root.querySelectorAll("[data-ms-item]").forEach((li) => {
|
||||||
@@ -345,12 +380,10 @@ const containerId = `ms-${Math.random().toString(36).slice(2)}`;
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on outside click without refocusing input
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!root.contains(e.target)) setExpanded(false); // note: no refocusInput
|
if (!root.contains(e.target)) setExpanded(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init
|
|
||||||
enforceMax();
|
enforceMax();
|
||||||
updateSummary();
|
updateSummary();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+166
-90
@@ -1,105 +1,155 @@
|
|||||||
---
|
---
|
||||||
import Form from "../components/Form.astro";
|
import { Form, Field, MultiSelect } from "@wrnexus/astro";
|
||||||
import Field from "../components/Field.astro";
|
|
||||||
import Base from "../layouts/Base.astro";
|
import Base from "../layouts/Base.astro";
|
||||||
import MultiSelect from "../components/MultiSelect.astro";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base class="p-4">
|
<Base class="p-4">
|
||||||
<Form
|
<Form
|
||||||
title="Create Project"
|
title="Job Application"
|
||||||
description="Project metadata, constraints, and attachments."
|
description="Tell us about yourself. Fields marked * are required."
|
||||||
columns={2}
|
columns={2}
|
||||||
schemaName="schema"
|
schemaName="schema"
|
||||||
onSubmit="submit"
|
onSubmit="submit"
|
||||||
submitLabel="Create Project"
|
submitLabel="Submit Application"
|
||||||
submitIcon="mdi:check"
|
submitIcon="mdi:send"
|
||||||
actionsAlign="center"
|
actionsAlign="end"
|
||||||
resetOnSubmit
|
resetOnSubmit
|
||||||
validateOn="blur,change,input"
|
validateOn="input,blur,change"
|
||||||
>
|
>
|
||||||
|
<!-- Applicant -->
|
||||||
<Field
|
<Field
|
||||||
name="projectName"
|
name="firstName"
|
||||||
label="Project name"
|
label="First name"
|
||||||
required
|
required
|
||||||
iconLeft="mdi:briefcase"
|
iconLeft="mdi:account"
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="slug"
|
name="lastName"
|
||||||
label="Slug"
|
label="Last name"
|
||||||
prefixText="https://app.example.com/p/"
|
|
||||||
placeholder="acme-crm"
|
|
||||||
required
|
required
|
||||||
|
iconLeft="mdi:account"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
kind="email"
|
||||||
|
required
|
||||||
|
iconLeft="mdi:email"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="phone"
|
||||||
|
label="Phone"
|
||||||
|
placeholder="+91 98765 43210"
|
||||||
|
iconLeft="mdi:phone"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
name="portfolio"
|
||||||
|
label="Portfolio URL"
|
||||||
|
kind="text"
|
||||||
|
placeholder="https://example.com"
|
||||||
iconLeft="mdi:link-variant"
|
iconLeft="mdi:link-variant"
|
||||||
|
class="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Role details -->
|
||||||
<Field
|
<Field
|
||||||
name="teamSize"
|
name="role"
|
||||||
label="Team size"
|
label="Desired role"
|
||||||
kind="number"
|
kind="select"
|
||||||
placeholder="e.g., 8"
|
placeholder="Select a role"
|
||||||
|
options={[
|
||||||
|
{ value: "frontend", label: "Frontend Engineer" },
|
||||||
|
{ value: "backend", label: "Backend Engineer" },
|
||||||
|
{ value: "fullstack", label: "Full-stack Engineer" },
|
||||||
|
{ value: "designer", label: "Product Designer" },
|
||||||
|
]}
|
||||||
required
|
required
|
||||||
iconLeft="mdi:account-group"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name="budget"
|
|
||||||
label="Budget (USD)"
|
|
||||||
kind="number"
|
|
||||||
placeholder="Optional"
|
|
||||||
iconLeft="mdi:currency-usd"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field name="startDate" label="Start date" kind="date" required />
|
<Field
|
||||||
<Field name="kickoffTime" label="Kickoff time" kind="time" required />
|
name="experience"
|
||||||
|
label="Years of experience"
|
||||||
|
kind="number"
|
||||||
|
placeholder="e.g., 3"
|
||||||
|
suffixText="yrs"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
name="tags"
|
name="skills"
|
||||||
label="Tags"
|
label="Key skills"
|
||||||
|
placeholder="Pick skills"
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
maxSelections={6}
|
||||||
|
value={["javascript"]}
|
||||||
options={[
|
options={[
|
||||||
{ value: "web", label: "Web" },
|
{ value: "javascript", label: "JavaScript" },
|
||||||
{ value: "mobile", label: "Mobile" },
|
{ value: "typescript", label: "TypeScript" },
|
||||||
{ value: "api", label: "API" },
|
{ value: "react", label: "React" },
|
||||||
{ value: "ml", label: "ML" },
|
{ value: "node", label: "Node.js" },
|
||||||
{ value: "ops", label: "Ops" },
|
{ value: "astro", label: "Astro" },
|
||||||
|
{ value: "css", label: "CSS" },
|
||||||
|
{ value: "figma", label: "Figma" },
|
||||||
]}
|
]}
|
||||||
description="Use Ctrl/Cmd to pick multiple."
|
|
||||||
class="sm:col-span-2"
|
class="sm:col-span-2"
|
||||||
required
|
description="Choose up to 6."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name="visibility"
|
name="availableFrom"
|
||||||
label="Visibility"
|
label="Available from"
|
||||||
|
kind="date"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Field name="preferredCall" label="Preferred call time" kind="time" />
|
||||||
|
|
||||||
|
<!-- Work auth -->
|
||||||
|
<Field
|
||||||
|
name="workAuth"
|
||||||
|
label="Work Authorization"
|
||||||
kind="radio"
|
kind="radio"
|
||||||
value="private"
|
value="authorized"
|
||||||
class="sm:col-span-2"
|
class="sm:col-span-2"
|
||||||
options={[
|
options={[
|
||||||
{ value: "private", label: "Private" },
|
{ value: "authorized", label: "Authorized to work" },
|
||||||
{ value: "internal", label: "Internal" },
|
{ value: "requiresVisa", label: "Requires visa sponsorship" },
|
||||||
{ value: "public", label: "Public" },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Compensation -->
|
||||||
<Field
|
<Field
|
||||||
name="attachments"
|
name="salary"
|
||||||
label="Attachments"
|
label="Expected CTC"
|
||||||
|
kind="number"
|
||||||
|
prefixText="₹"
|
||||||
|
suffixText="LPA"
|
||||||
|
placeholder="e.g., 12"
|
||||||
|
class="sm:col-span-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Resume/Cover -->
|
||||||
|
<Field
|
||||||
|
name="resume"
|
||||||
|
label="Resume (PDF)"
|
||||||
kind="file"
|
kind="file"
|
||||||
multiple
|
|
||||||
class="sm:col-span-2"
|
class="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="description"
|
name="coverLetter"
|
||||||
label="Description"
|
label="Cover letter"
|
||||||
kind="textarea"
|
kind="textarea"
|
||||||
rows={5}
|
rows={6}
|
||||||
placeholder="What are we building?"
|
placeholder="A short note about why you’d be a great fit…"
|
||||||
class="sm:col-span-2"
|
class="sm:col-span-2"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name="notify"
|
name="agree"
|
||||||
label="Notify team on create"
|
label="I confirm the information provided is accurate"
|
||||||
kind="switch"
|
kind="checkbox"
|
||||||
class="sm:col-span-2"
|
class="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -108,46 +158,72 @@ import MultiSelect from "../components/MultiSelect.astro";
|
|||||||
<script>
|
<script>
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const fileArray = z.array(z.instanceof(File)).optional();
|
const phoneRegex = /^\+?[0-9\s().-]{7,20}$/i;
|
||||||
|
const urlRegex = /^https?:\/\/.+/i;
|
||||||
|
|
||||||
window.schema = z.object({
|
const resumeSchema = z
|
||||||
projectName: z.string().min(3, "Enter at least 3 characters."),
|
.union([z.instanceof(File), z.null()])
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.regex(
|
|
||||||
/^[a-z0-9-]{3,}$/i,
|
|
||||||
"Slug must be 3+ chars; letters, digits, hyphen.",
|
|
||||||
),
|
|
||||||
teamSize: z.coerce.number().int().min(1, "Team size must be ≥ 1."),
|
|
||||||
budget: z.coerce
|
|
||||||
.number()
|
|
||||||
.positive("Budget must be positive.")
|
|
||||||
.optional()
|
|
||||||
.or(z.nan().transform(() => undefined)),
|
|
||||||
startDate: z.string().min(1, "Start date is required."),
|
|
||||||
kickoffTime: z.string().min(1, "Kickoff time is required."),
|
|
||||||
tags: z.array(z.string()).min(1, "Pick at least one tag."),
|
|
||||||
visibility: z.enum(["private", "internal", "public"], {
|
|
||||||
required_error: "Choose a visibility.",
|
|
||||||
}),
|
|
||||||
// attachments -> array<File> (from our collector when multiple)
|
|
||||||
attachments: fileArray
|
|
||||||
.refine((arr) => !arr || arr.length <= 3, "Max 3 files.")
|
|
||||||
.refine(
|
.refine(
|
||||||
(arr) => !arr || arr.every((f) => f.size <= 5 * 1024 * 1024),
|
(f) => f === null || /pdf$/i.test(f.type),
|
||||||
"Files must be ≤ 5MB.",
|
"Resume must be a PDF.",
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(arr) =>
|
(f) => f === null || f.size <= 5 * 1024 * 1024,
|
||||||
!arr || arr.every((f) => /pdf|png|jpg|jpeg/i.test(f.type)),
|
"Max file size is 5MB.",
|
||||||
"Only PDF/PNG/JPG.",
|
)
|
||||||
),
|
.optional();
|
||||||
description: z.string().min(20, "Describe the project (20+ chars)."),
|
|
||||||
notify: z.boolean().optional(),
|
window.schema = z
|
||||||
|
.object({
|
||||||
|
firstName: z.string().min(2, "Enter at least 2 characters."),
|
||||||
|
lastName: z.string().min(2, "Enter at least 2 characters."),
|
||||||
|
email: z.string().email("Enter a valid email."),
|
||||||
|
phone: z
|
||||||
|
.string()
|
||||||
|
.regex(phoneRegex, "Enter a valid phone.")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
portfolio: z
|
||||||
|
.string()
|
||||||
|
.regex(urlRegex, "Enter a valid URL (http/https).")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
role: z.enum(["frontend", "backend", "fullstack", "designer"], {
|
||||||
|
required_error: "Select a role.",
|
||||||
|
}),
|
||||||
|
experience: z.coerce.number().int().min(0, "Must be 0 or more."),
|
||||||
|
skills: z
|
||||||
|
.array(z.string())
|
||||||
|
.min(1, "Pick at least one skill.")
|
||||||
|
.max(6, "Up to 6 skills."),
|
||||||
|
availableFrom: z.string().min(1, "Pick a date."),
|
||||||
|
preferredCall: z.string().optional().or(z.literal("")),
|
||||||
|
workAuth: z.enum(["authorized", "requiresVisa"]),
|
||||||
|
salary: z.coerce
|
||||||
|
.number()
|
||||||
|
.positive("Enter a positive number.")
|
||||||
|
.optional(),
|
||||||
|
resume: resumeSchema,
|
||||||
|
coverLetter: z
|
||||||
|
.string()
|
||||||
|
.max(1000, "Keep it under 1000 characters.")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
agree: z.literal(true, {
|
||||||
|
errorMap: () => ({ message: "You must confirm to proceed." }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.superRefine((obj, ctx) => {
|
||||||
|
if (obj.workAuth === "requiresVisa" && !obj.coverLetter) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["coverLetter"],
|
||||||
|
message: "Please add a note about your visa status.",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.submit = async (data: any, form: any) => {
|
window.submit = async (data: any, form: any) => {
|
||||||
console.log("Project Create submit:", data);
|
console.log("Application:", data);
|
||||||
await new Promise((r) => setTimeout(r, 800));
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user