@classytic/fluid 0.2.4 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -62
  3. package/dist/api-pagination-CJ0vR_w6.d.mts +34 -0
  4. package/dist/api-pagination-DBTE0yk4.mjs +190 -0
  5. package/dist/chunk-DQk6qfdC.mjs +18 -0
  6. package/dist/client/calendar.d.mts +105 -0
  7. package/dist/client/calendar.mjs +202 -0
  8. package/dist/client/core.d.mts +1614 -0
  9. package/dist/client/core.mjs +2779 -0
  10. package/dist/client/error.d.mts +125 -0
  11. package/dist/client/error.mjs +166 -0
  12. package/dist/client/hooks.d.mts +162 -0
  13. package/dist/client/hooks.mjs +447 -0
  14. package/dist/client/table.d.mts +84 -0
  15. package/dist/client/table.mjs +373 -0
  16. package/dist/client/theme.d.mts +6 -0
  17. package/dist/client/theme.mjs +65 -0
  18. package/dist/command.d.mts +134 -0
  19. package/dist/command.mjs +132 -0
  20. package/dist/compact.d.mts +359 -0
  21. package/dist/compact.mjs +892 -0
  22. package/dist/dashboard.d.mts +778 -0
  23. package/dist/dashboard.mjs +1617 -0
  24. package/dist/filter-utils-DqMmy_v-.mjs +72 -0
  25. package/dist/filter-utils-IZ0GtuPo.d.mts +40 -0
  26. package/dist/forms.d.mts +1549 -0
  27. package/dist/forms.mjs +3740 -0
  28. package/dist/index.d.mts +296 -0
  29. package/dist/index.mjs +432 -0
  30. package/dist/layouts.d.mts +215 -0
  31. package/dist/layouts.mjs +460 -0
  32. package/dist/search-context-DR7DBs7S.mjs +19 -0
  33. package/dist/search.d.mts +254 -0
  34. package/dist/search.mjs +523 -0
  35. package/dist/sheet-wrapper-C13Y-Q6w.mjs +211 -0
  36. package/dist/use-base-search-BGgWnWaF.d.mts +35 -0
  37. package/dist/use-debounce-xmZucz5e.mjs +53 -0
  38. package/dist/use-keyboard-shortcut-Bl6YM5Q7.mjs +82 -0
  39. package/dist/use-keyboard-shortcut-_mRCh3QO.d.mts +24 -0
  40. package/dist/use-media-query-BnVNIKT4.mjs +17 -0
  41. package/dist/use-mobile-BX3SQVo2.mjs +20 -0
  42. package/dist/use-scroll-detection-CsgsQYvy.mjs +43 -0
  43. package/dist/utils-CDue7cEt.d.mts +6 -0
  44. package/dist/utils-DQ5SCVoW.mjs +10 -0
  45. package/package.json +85 -45
  46. package/styles.css +2 -2
  47. package/dist/chunk-GUHK2DTW.js +0 -15
  48. package/dist/chunk-GUHK2DTW.js.map +0 -1
  49. package/dist/chunk-H3NFL3GJ.js +0 -57
  50. package/dist/chunk-H3NFL3GJ.js.map +0 -1
  51. package/dist/chunk-J2YRTQE4.js +0 -293
  52. package/dist/chunk-J2YRTQE4.js.map +0 -1
  53. package/dist/compact.d.ts +0 -217
  54. package/dist/compact.js +0 -986
  55. package/dist/compact.js.map +0 -1
  56. package/dist/dashboard.d.ts +0 -387
  57. package/dist/dashboard.js +0 -1032
  58. package/dist/dashboard.js.map +0 -1
  59. package/dist/index.d.ts +0 -2140
  60. package/dist/index.js +0 -6422
  61. package/dist/index.js.map +0 -1
  62. package/dist/layout.d.ts +0 -25
  63. package/dist/layout.js +0 -4
  64. package/dist/layout.js.map +0 -1
  65. package/dist/search.d.ts +0 -172
  66. package/dist/search.js +0 -341
  67. package/dist/search.js.map +0 -1
  68. package/dist/use-base-search-AS5Z3SAy.d.ts +0 -64
  69. package/dist/utils-Cbsgs0XP.d.ts +0 -5
package/dist/forms.mjs ADDED
@@ -0,0 +1,3740 @@
1
+ "use client";
2
+
3
+ import { t as cn } from "./utils-DQ5SCVoW.mjs";
4
+ import { n as useDebouncedCallback } from "./use-debounce-xmZucz5e.mjs";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import * as React$1 from "react";
7
+ import { isValidElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
+ import { CalendarIcon, Check, ChevronDown, Eye, EyeOff, FileIcon, FileImage, FileVideo, Loader2, Minus, Plus, Tag, Trash2, Upload, Wand2, X } from "lucide-react";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Button } from "@/components/ui/button";
11
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupTextarea } from "@/components/ui/input-group";
12
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
13
+ import { Badge } from "@/components/ui/badge";
14
+ import { Input } from "@/components/ui/input";
15
+ import { format, isValid, set } from "date-fns";
16
+ import { Controller } from "react-hook-form";
17
+ import { Field, FieldContent, FieldDescription, FieldError as FieldError$1, FieldLabel } from "@/components/ui/field";
18
+ import { Textarea } from "@/components/ui/textarea";
19
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
20
+ import { Checkbox } from "@/components/ui/checkbox";
21
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
22
+ import { Switch } from "@/components/ui/switch";
23
+ import { Calendar } from "@/components/ui/calendar";
24
+ import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput as ComboboxInput$1, ComboboxItem, ComboboxLabel, ComboboxList, useComboboxAnchor } from "@/components/ui/combobox";
25
+ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
26
+ import { Progress } from "@/components/ui/progress";
27
+ import { Separator } from "@/components/ui/separator";
28
+
29
+ //#region src/components/form/form-input.tsx
30
+ /**
31
+ * FormInput - Text input with react-hook-form integration
32
+ *
33
+ * Features:
34
+ * - Works with react-hook-form Controller
35
+ * - Supports input groups with icons/addons
36
+ * - Value transformation (input/output)
37
+ * - Can be used standalone without form
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * // With react-hook-form
42
+ * <FormInput
43
+ * control={form.control}
44
+ * name="email"
45
+ * type="email"
46
+ * label="Email"
47
+ * placeholder="user@example.com"
48
+ * required
49
+ * />
50
+ *
51
+ * // With icon
52
+ * <FormInput
53
+ * control={form.control}
54
+ * name="search"
55
+ * iconLeft={<SearchIcon />}
56
+ * placeholder="Search..."
57
+ * />
58
+ * ```
59
+ */
60
+ function FormInput({ control, name, label, placeholder, description, helperText, required, disabled, readOnly, type = "text", className, labelClassName, inputClassName, inputGroupClassName, iconLeft, iconRight, addonLeft, addonRight, onValueChange, transform, value, onChange, min, max, step, minLength, maxLength, pattern, autoComplete, autoFocus, inputMode, enterKeyHint }) {
61
+ const descriptionText = description || helperText;
62
+ const handleChange = (e, field) => {
63
+ const newValue = transform?.output ? transform.output(e.target.value) : e.target.value;
64
+ if (field) field.onChange(newValue);
65
+ else if (onChange) onChange(newValue);
66
+ onValueChange?.(newValue);
67
+ };
68
+ const hasInputGroup = iconLeft || iconRight || addonLeft || addonRight;
69
+ const renderInput = (field, isDisabled, fieldState) => {
70
+ const safeValue = (field ? transform?.input ? transform.input(field.value) : field.value : transform?.input ? transform.input(value) : value) ?? "";
71
+ const inputProps = {
72
+ id: name,
73
+ name: field?.name || name,
74
+ type,
75
+ disabled: isDisabled,
76
+ readOnly,
77
+ placeholder,
78
+ value: safeValue,
79
+ onChange: (e) => handleChange(e, field),
80
+ onBlur: field?.onBlur,
81
+ "aria-invalid": fieldState?.invalid || void 0,
82
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0
83
+ };
84
+ if (min !== void 0) inputProps.min = min;
85
+ if (max !== void 0) inputProps.max = max;
86
+ if (step !== void 0) inputProps.step = step;
87
+ if (minLength !== void 0) inputProps.minLength = minLength;
88
+ if (maxLength !== void 0) inputProps.maxLength = maxLength;
89
+ if (pattern !== void 0) inputProps.pattern = pattern;
90
+ if (autoComplete !== void 0) inputProps.autoComplete = autoComplete;
91
+ if (autoFocus !== void 0) inputProps.autoFocus = autoFocus;
92
+ if (inputMode !== void 0) inputProps.inputMode = inputMode;
93
+ if (enterKeyHint !== void 0) inputProps.enterKeyHint = enterKeyHint;
94
+ if (field?.ref) inputProps.ref = field.ref;
95
+ if (hasInputGroup) return /* @__PURE__ */ jsxs(InputGroup, {
96
+ className: cn(inputGroupClassName),
97
+ "data-disabled": isDisabled,
98
+ children: [
99
+ (addonLeft || iconLeft) && /* @__PURE__ */ jsx(InputGroupAddon, {
100
+ align: "inline-start",
101
+ children: addonLeft || iconLeft
102
+ }),
103
+ /* @__PURE__ */ jsx(InputGroupInput, {
104
+ ...inputProps,
105
+ className: inputClassName
106
+ }),
107
+ (addonRight || iconRight) && /* @__PURE__ */ jsx(InputGroupAddon, {
108
+ align: "inline-end",
109
+ children: addonRight || iconRight
110
+ })
111
+ ]
112
+ });
113
+ return /* @__PURE__ */ jsx(Input, {
114
+ ...inputProps,
115
+ className: inputClassName
116
+ });
117
+ };
118
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
119
+ className,
120
+ "data-disabled": disabled,
121
+ children: [
122
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
123
+ htmlFor: name,
124
+ className: labelClassName,
125
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
126
+ className: "text-destructive ml-1",
127
+ "aria-hidden": "true",
128
+ children: "*"
129
+ }), /* @__PURE__ */ jsx("span", {
130
+ className: "sr-only",
131
+ children: "(required)"
132
+ })] })]
133
+ }),
134
+ renderInput(void 0, disabled, void 0),
135
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
136
+ id: `${name}-description`,
137
+ children: descriptionText
138
+ })
139
+ ]
140
+ });
141
+ return /* @__PURE__ */ jsx(Controller, {
142
+ name,
143
+ control,
144
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
145
+ className,
146
+ "data-disabled": disabled,
147
+ "data-invalid": fieldState.invalid,
148
+ children: [
149
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
150
+ htmlFor: name,
151
+ className: labelClassName,
152
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
153
+ className: "text-destructive ml-1",
154
+ "aria-hidden": "true",
155
+ children: "*"
156
+ }), /* @__PURE__ */ jsx("span", {
157
+ className: "sr-only",
158
+ children: "(required)"
159
+ })] })]
160
+ }),
161
+ renderInput(field, disabled, fieldState),
162
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
163
+ id: `${name}-description`,
164
+ children: descriptionText
165
+ }),
166
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
167
+ id: `${name}-error`,
168
+ errors: [fieldState.error]
169
+ })
170
+ ]
171
+ })
172
+ });
173
+ }
174
+
175
+ //#endregion
176
+ //#region src/components/form/form-textarea.tsx
177
+ /**
178
+ * FormTextarea - Textarea with react-hook-form integration
179
+ *
180
+ * @example
181
+ * ```tsx
182
+ * <FormTextarea
183
+ * control={form.control}
184
+ * name="bio"
185
+ * label="Biography"
186
+ * placeholder="Tell us about yourself..."
187
+ * rows={5}
188
+ * />
189
+ * ```
190
+ */
191
+ function FormTextarea({ control, name, label, description, helperText, required, disabled, readOnly, placeholder, value: propValue, onChange: propOnChange, onValueChange, className, labelClassName, textareaClassName, inputGroupClassName, iconLeft, iconRight, addonLeft, addonRight, rows = 3, minLength, maxLength, autoComplete, autoFocus }) {
192
+ const descriptionText = description || helperText;
193
+ const hasInputGroup = iconLeft || iconRight || addonLeft || addonRight;
194
+ const [localValue, setLocalValue] = useState(propValue || "");
195
+ useEffect(() => {
196
+ if (propValue !== void 0) setLocalValue(propValue);
197
+ }, [propValue]);
198
+ const handleDirectValueChange = (value) => {
199
+ setLocalValue(value);
200
+ propOnChange?.(value);
201
+ onValueChange?.(value);
202
+ };
203
+ const renderTextarea = (field, isDisabled, fieldState) => {
204
+ const value = field ? field.value : localValue;
205
+ const handleChange = (e) => {
206
+ const newValue = e.target.value;
207
+ if (field) field.onChange(e);
208
+ else handleDirectValueChange(newValue);
209
+ onValueChange?.(newValue);
210
+ };
211
+ const textareaProps = {
212
+ id: name,
213
+ name: field?.name || name,
214
+ value: value ?? "",
215
+ placeholder,
216
+ disabled: isDisabled,
217
+ readOnly,
218
+ rows,
219
+ onChange: handleChange,
220
+ onBlur: field?.onBlur,
221
+ "aria-invalid": fieldState?.invalid || void 0,
222
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0
223
+ };
224
+ if (minLength !== void 0) textareaProps.minLength = minLength;
225
+ if (maxLength !== void 0) textareaProps.maxLength = maxLength;
226
+ if (autoComplete !== void 0) textareaProps.autoComplete = autoComplete;
227
+ if (autoFocus !== void 0) textareaProps.autoFocus = autoFocus;
228
+ if (field?.ref) textareaProps.ref = field.ref;
229
+ if (hasInputGroup) return /* @__PURE__ */ jsxs(InputGroup, {
230
+ className: cn(inputGroupClassName),
231
+ "data-disabled": isDisabled,
232
+ children: [
233
+ (addonLeft || iconLeft) && /* @__PURE__ */ jsx(InputGroupAddon, {
234
+ align: "inline-start",
235
+ children: addonLeft || iconLeft
236
+ }),
237
+ /* @__PURE__ */ jsx(InputGroupTextarea, {
238
+ ...textareaProps,
239
+ className: cn("overflow-auto resize-none", textareaClassName)
240
+ }),
241
+ (addonRight || iconRight) && /* @__PURE__ */ jsx(InputGroupAddon, {
242
+ align: "inline-end",
243
+ children: addonRight || iconRight
244
+ })
245
+ ]
246
+ });
247
+ return /* @__PURE__ */ jsx(Textarea, {
248
+ ...textareaProps,
249
+ className: cn("overflow-auto resize-none", textareaClassName)
250
+ });
251
+ };
252
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
253
+ className,
254
+ "data-disabled": disabled,
255
+ children: [
256
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
257
+ htmlFor: name,
258
+ className: labelClassName,
259
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
260
+ className: "text-destructive ml-1",
261
+ "aria-hidden": "true",
262
+ children: "*"
263
+ }), /* @__PURE__ */ jsx("span", {
264
+ className: "sr-only",
265
+ children: "(required)"
266
+ })] })]
267
+ }),
268
+ renderTextarea(void 0, disabled, void 0),
269
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
270
+ id: `${name}-description`,
271
+ children: descriptionText
272
+ })
273
+ ]
274
+ });
275
+ return /* @__PURE__ */ jsx(Controller, {
276
+ name,
277
+ control,
278
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
279
+ className,
280
+ "data-disabled": disabled,
281
+ "data-invalid": fieldState.invalid,
282
+ children: [
283
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
284
+ htmlFor: name,
285
+ className: labelClassName,
286
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
287
+ className: "text-destructive ml-1",
288
+ "aria-hidden": "true",
289
+ children: "*"
290
+ }), /* @__PURE__ */ jsx("span", {
291
+ className: "sr-only",
292
+ children: "(required)"
293
+ })] })]
294
+ }),
295
+ renderTextarea(field, disabled, fieldState),
296
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
297
+ id: `${name}-description`,
298
+ children: descriptionText
299
+ }),
300
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
301
+ id: `${name}-error`,
302
+ errors: [fieldState.error]
303
+ })
304
+ ]
305
+ })
306
+ });
307
+ }
308
+
309
+ //#endregion
310
+ //#region src/components/form/select-input.tsx
311
+ /**
312
+ * SelectInput - Select dropdown with react-hook-form integration
313
+ *
314
+ * Features:
315
+ * - Flat and grouped options
316
+ * - Custom placeholder
317
+ * - Optional "All" option (use placeholder text to match)
318
+ * - Custom styling
319
+ *
320
+ * Note: For "All" options with value="", the placeholder will be shown when selected.
321
+ * Make sure your placeholder text matches what you want to display for "All".
322
+ *
323
+ * @example
324
+ * ```tsx
325
+ * // Basic usage
326
+ * <SelectInput
327
+ * control={form.control}
328
+ * name="role"
329
+ * label="Role"
330
+ * items={[
331
+ * { value: "admin", label: "Admin" },
332
+ * { value: "user", label: "User" },
333
+ * ]}
334
+ * />
335
+ *
336
+ * // With "All" option - placeholder shows when "All" is selected
337
+ * <SelectInput
338
+ * name="status"
339
+ * label="Status"
340
+ * items={[
341
+ * { value: "", label: "All" }, // This will show placeholder when selected
342
+ * { value: "active", label: "Active" },
343
+ * ]}
344
+ * placeholder="All" // Make sure this matches the "All" label
345
+ * />
346
+ * ```
347
+ */
348
+ function SelectInput({ control, items = [], groups = [], name, label, placeholder = "Select option", allOption, description, helperText, required, disabled, className, labelClassName, triggerClassName, contentClassName, itemClassName, Icon, onValueChange, value: propValue, defaultOpen, side = "bottom", sideOffset = 4, align = "start", alignOffset = 0 }) {
349
+ const descriptionText = description || helperText;
350
+ const CLEAR_VALUE = "__CLEAR__";
351
+ const displayItems = useMemo(() => {
352
+ const filtered = items.filter((item) => item.value !== void 0 && item.value !== null);
353
+ return allOption ? [allOption, ...filtered] : filtered;
354
+ }, [items, allOption]);
355
+ const filteredGroups = useMemo(() => {
356
+ return groups.map((group) => ({
357
+ ...group,
358
+ items: group.items.filter((item) => item.value !== void 0 && item.value !== null)
359
+ })).filter((group) => group.items.length > 0);
360
+ }, [groups]);
361
+ const [localValue, setLocalValue] = useState(propValue?.toString() || "");
362
+ useEffect(() => {
363
+ if (propValue !== void 0) setLocalValue(propValue.toString());
364
+ }, [propValue]);
365
+ const labelMap = useMemo(() => {
366
+ const map = /* @__PURE__ */ new Map();
367
+ for (const item of displayItems) map.set(item.value?.toString() ?? "", item.label);
368
+ for (const group of filteredGroups) for (const item of group.items) map.set(item.value?.toString() ?? "", item.label);
369
+ return map;
370
+ }, [displayItems, filteredGroups]);
371
+ const renderSelect = (field, isDisabled, fieldState) => {
372
+ const rawValue = field ? field.value?.toString() : localValue;
373
+ const handleChange = (newValue) => {
374
+ const actualValue = newValue === CLEAR_VALUE ? "" : newValue;
375
+ if (field) field.onChange(actualValue);
376
+ else setLocalValue(actualValue);
377
+ onValueChange?.(actualValue);
378
+ };
379
+ const getItemValue = (item, fallback) => {
380
+ const val = item.value !== void 0 && item.value !== null ? item.value.toString() : fallback;
381
+ return val === "" ? CLEAR_VALUE : val;
382
+ };
383
+ const getItemLabel = (item) => {
384
+ return item.label;
385
+ };
386
+ const getUniqueReactKey = (item, fallback, seen) => {
387
+ const baseKey = `${getItemValue(item, fallback)}::${getItemLabel(item)}`;
388
+ const nextCount = (seen.get(baseKey) ?? 0) + 1;
389
+ seen.set(baseKey, nextCount);
390
+ return nextCount === 1 ? baseKey : `${baseKey}::${nextCount}`;
391
+ };
392
+ const renderGroupedContent = () => {
393
+ if (filteredGroups.length === 0) return null;
394
+ return filteredGroups.map((group, groupIdx) => {
395
+ const seenKeys = /* @__PURE__ */ new Map();
396
+ return /* @__PURE__ */ jsxs(SelectGroup, { children: [group.label && /* @__PURE__ */ jsx(SelectLabel, { children: group.label }), group.items.map((item, idx) => {
397
+ return /* @__PURE__ */ jsx(SelectItem, {
398
+ value: getItemValue(item, `item-${groupIdx}-${idx}`),
399
+ className: cn("cursor-pointer", itemClassName),
400
+ disabled: item.disabled,
401
+ children: getItemLabel(item)
402
+ }, getUniqueReactKey(item, `item-${groupIdx}-${idx}`, seenKeys));
403
+ })] }, `group-${groupIdx}`);
404
+ });
405
+ };
406
+ const renderFlatContent = () => {
407
+ if (displayItems.length === 0) return /* @__PURE__ */ jsx("div", {
408
+ className: "py-6 text-center text-sm text-muted-foreground",
409
+ children: "No options available"
410
+ });
411
+ const seenKeys = /* @__PURE__ */ new Map();
412
+ return displayItems.map((item, idx) => {
413
+ return /* @__PURE__ */ jsx(SelectItem, {
414
+ value: getItemValue(item, `item-${idx}`),
415
+ className: cn("cursor-pointer", itemClassName),
416
+ disabled: item.disabled,
417
+ children: getItemLabel(item)
418
+ }, getUniqueReactKey(item, `item-${idx}`, seenKeys));
419
+ });
420
+ };
421
+ const selectValue = rawValue === "" ? CLEAR_VALUE : rawValue ? rawValue : void 0;
422
+ const selectedLabel = (() => {
423
+ if (!rawValue && rawValue !== "") return void 0;
424
+ return labelMap.get(rawValue ?? "");
425
+ })();
426
+ return /* @__PURE__ */ jsxs(Select, {
427
+ onValueChange: handleChange,
428
+ value: selectValue,
429
+ disabled: isDisabled,
430
+ defaultOpen,
431
+ children: [/* @__PURE__ */ jsxs(SelectTrigger, {
432
+ className: cn("w-full", triggerClassName),
433
+ "aria-invalid": fieldState?.invalid || void 0,
434
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
435
+ children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4 text-primary" }), /* @__PURE__ */ jsx(SelectValue, {
436
+ placeholder,
437
+ children: selectedLabel || null
438
+ })]
439
+ }), /* @__PURE__ */ jsx(SelectContent, {
440
+ className: cn(contentClassName),
441
+ side,
442
+ sideOffset,
443
+ align,
444
+ alignOffset,
445
+ children: filteredGroups.length > 0 ? renderGroupedContent() : renderFlatContent()
446
+ })]
447
+ });
448
+ };
449
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
450
+ className,
451
+ "data-disabled": disabled,
452
+ children: [
453
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
454
+ htmlFor: name,
455
+ className: labelClassName,
456
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
457
+ className: "text-destructive ml-1",
458
+ "aria-hidden": "true",
459
+ children: "*"
460
+ }), /* @__PURE__ */ jsx("span", {
461
+ className: "sr-only",
462
+ children: "(required)"
463
+ })] })]
464
+ }),
465
+ renderSelect(void 0, disabled, void 0),
466
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
467
+ id: `${name}-description`,
468
+ children: descriptionText
469
+ })
470
+ ]
471
+ });
472
+ return /* @__PURE__ */ jsx(Controller, {
473
+ name,
474
+ control,
475
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
476
+ className,
477
+ "data-disabled": disabled,
478
+ "data-invalid": fieldState.invalid,
479
+ children: [
480
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
481
+ htmlFor: name,
482
+ className: labelClassName,
483
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
484
+ className: "text-destructive ml-1",
485
+ "aria-hidden": "true",
486
+ children: "*"
487
+ }), /* @__PURE__ */ jsx("span", {
488
+ className: "sr-only",
489
+ children: "(required)"
490
+ })] })]
491
+ }),
492
+ renderSelect(field, disabled, fieldState),
493
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
494
+ id: `${name}-description`,
495
+ children: descriptionText
496
+ }),
497
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
498
+ id: `${name}-error`,
499
+ errors: [fieldState.error]
500
+ })
501
+ ]
502
+ })
503
+ });
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/components/form/checkbox-input.tsx
508
+ /**
509
+ * CheckboxInput - Checkbox group with react-hook-form integration
510
+ *
511
+ * Features:
512
+ * - Multiple checkbox items
513
+ * - Array value support
514
+ * - Can be used standalone
515
+ *
516
+ * @example
517
+ * ```tsx
518
+ * <CheckboxInput
519
+ * control={form.control}
520
+ * name="features"
521
+ * label="Features"
522
+ * items={[
523
+ * { id: "wifi", label: "WiFi" },
524
+ * { id: "parking", label: "Parking" },
525
+ * { id: "pool", label: "Pool" },
526
+ * ]}
527
+ * />
528
+ * ```
529
+ */
530
+ function CheckboxInput({ control, name, label, description, helperText, required, disabled, items = [], value: propValue, onChange: propOnChange, onValueChange, className, labelClassName, checkboxClassName, itemClassName, itemLabelClassName }) {
531
+ const descriptionText = description || helperText;
532
+ const [localValues, setLocalValues] = useState(propValue || []);
533
+ useEffect(() => {
534
+ if (propValue !== void 0) setLocalValues(propValue);
535
+ }, [propValue]);
536
+ const handleDirectValueChange = (newValues) => {
537
+ setLocalValues(newValues);
538
+ propOnChange?.(newValues);
539
+ onValueChange?.(newValues);
540
+ };
541
+ const renderCheckboxes = (field, isDisabled, fieldState) => {
542
+ const values = field?.value || localValues || [];
543
+ const handleCheckedChange = (itemId, checked) => {
544
+ const newValues = checked ? [...values, itemId] : values.filter((value) => value !== itemId);
545
+ if (field) {
546
+ field.onChange(newValues);
547
+ onValueChange?.(newValues);
548
+ } else handleDirectValueChange(newValues);
549
+ };
550
+ return /* @__PURE__ */ jsx("div", {
551
+ "data-slot": "checkbox-group",
552
+ className: "space-y-2",
553
+ role: "group",
554
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
555
+ children: items.map((item) => /* @__PURE__ */ jsxs("div", {
556
+ className: cn("flex items-center gap-2", itemClassName),
557
+ children: [/* @__PURE__ */ jsx(Checkbox, {
558
+ id: `${name}-${item.id}`,
559
+ className: checkboxClassName,
560
+ checked: values.includes(item.id),
561
+ disabled: isDisabled || item.disabled,
562
+ onCheckedChange: (checked) => handleCheckedChange(item.id, checked),
563
+ "aria-invalid": fieldState?.invalid || void 0
564
+ }), /* @__PURE__ */ jsx("label", {
565
+ htmlFor: `${name}-${item.id}`,
566
+ className: cn("text-sm font-normal cursor-pointer leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", itemLabelClassName),
567
+ children: item.label
568
+ })]
569
+ }, item.id))
570
+ });
571
+ };
572
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
573
+ className,
574
+ "data-disabled": disabled,
575
+ children: [
576
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
577
+ htmlFor: name,
578
+ className: labelClassName,
579
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
580
+ className: "text-destructive ml-1",
581
+ "aria-hidden": "true",
582
+ children: "*"
583
+ }), /* @__PURE__ */ jsx("span", {
584
+ className: "sr-only",
585
+ children: "(required)"
586
+ })] })]
587
+ }),
588
+ renderCheckboxes(void 0, disabled, void 0),
589
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
590
+ id: `${name}-description`,
591
+ children: descriptionText
592
+ })
593
+ ]
594
+ });
595
+ return /* @__PURE__ */ jsx(Controller, {
596
+ name,
597
+ control,
598
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
599
+ className,
600
+ "data-disabled": disabled,
601
+ "data-invalid": fieldState.invalid,
602
+ children: [
603
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
604
+ htmlFor: name,
605
+ className: labelClassName,
606
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
607
+ className: "text-destructive ml-1",
608
+ "aria-hidden": "true",
609
+ children: "*"
610
+ }), /* @__PURE__ */ jsx("span", {
611
+ className: "sr-only",
612
+ children: "(required)"
613
+ })] })]
614
+ }),
615
+ renderCheckboxes(field, disabled, fieldState),
616
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
617
+ id: `${name}-description`,
618
+ children: descriptionText
619
+ }),
620
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
621
+ id: `${name}-error`,
622
+ errors: [fieldState.error]
623
+ })
624
+ ]
625
+ })
626
+ });
627
+ }
628
+
629
+ //#endregion
630
+ //#region src/components/form/radio-input.tsx
631
+ /**
632
+ * RadioInput - Radio group with react-hook-form integration
633
+ *
634
+ * @example
635
+ * ```tsx
636
+ * <RadioInput
637
+ * control={form.control}
638
+ * name="plan"
639
+ * label="Select Plan"
640
+ * choices={[
641
+ * { value: "free", label: "Free" },
642
+ * { value: "pro", label: "Pro" },
643
+ * { value: "enterprise", label: "Enterprise" },
644
+ * ]}
645
+ * orientation="horizontal"
646
+ * />
647
+ * ```
648
+ */
649
+ function RadioInput({ control, name, label, description, helperText, required, disabled, choices = [], items, orientation = "vertical", value: propValue, onChange: propOnChange, onValueChange, className, labelClassName, radioGroupClassName, radioItemClassName, radioLabelClassName }) {
650
+ const descriptionText = description || helperText;
651
+ const radioOptions = items || choices;
652
+ const [localValue, setLocalValue] = useState(propValue || "");
653
+ useEffect(() => {
654
+ if (propValue !== void 0) setLocalValue(propValue);
655
+ }, [propValue]);
656
+ const handleDirectValueChange = (newValue) => {
657
+ setLocalValue(newValue);
658
+ propOnChange?.(newValue);
659
+ onValueChange?.(newValue);
660
+ };
661
+ const renderRadioGroup = (field, isDisabled, fieldState) => {
662
+ const value = field ? field.value : localValue;
663
+ const handleValueChange = (newValue) => {
664
+ if (field) {
665
+ field.onChange(newValue);
666
+ onValueChange?.(newValue);
667
+ } else handleDirectValueChange(newValue);
668
+ };
669
+ return /* @__PURE__ */ jsx(RadioGroup, {
670
+ value,
671
+ onValueChange: handleValueChange,
672
+ disabled: isDisabled,
673
+ "aria-invalid": fieldState?.invalid || void 0,
674
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
675
+ className: cn(orientation === "horizontal" ? "flex flex-row flex-wrap gap-4" : "flex flex-col gap-3", radioGroupClassName),
676
+ children: radioOptions.map((choice) => /* @__PURE__ */ jsxs("div", {
677
+ className: cn("flex items-center gap-2", radioItemClassName),
678
+ children: [/* @__PURE__ */ jsx(RadioGroupItem, {
679
+ id: `${name}-${choice.value}`,
680
+ value: choice.value,
681
+ disabled: isDisabled || choice.disabled
682
+ }), /* @__PURE__ */ jsx("label", {
683
+ htmlFor: `${name}-${choice.value}`,
684
+ className: cn("text-sm font-normal cursor-pointer leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", radioLabelClassName),
685
+ children: choice.label
686
+ })]
687
+ }, choice.value))
688
+ });
689
+ };
690
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
691
+ className,
692
+ "data-disabled": disabled,
693
+ children: [
694
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
695
+ htmlFor: name,
696
+ className: labelClassName,
697
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
698
+ className: "text-destructive ml-1",
699
+ "aria-hidden": "true",
700
+ children: "*"
701
+ }), /* @__PURE__ */ jsx("span", {
702
+ className: "sr-only",
703
+ children: "(required)"
704
+ })] })]
705
+ }),
706
+ renderRadioGroup(void 0, disabled, void 0),
707
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
708
+ id: `${name}-description`,
709
+ children: descriptionText
710
+ })
711
+ ]
712
+ });
713
+ return /* @__PURE__ */ jsx(Controller, {
714
+ name,
715
+ control,
716
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
717
+ className,
718
+ "data-disabled": disabled,
719
+ "data-invalid": fieldState.invalid,
720
+ children: [
721
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
722
+ htmlFor: name,
723
+ className: labelClassName,
724
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
725
+ className: "text-destructive ml-1",
726
+ "aria-hidden": "true",
727
+ children: "*"
728
+ }), /* @__PURE__ */ jsx("span", {
729
+ className: "sr-only",
730
+ children: "(required)"
731
+ })] })]
732
+ }),
733
+ renderRadioGroup(field, disabled, fieldState),
734
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
735
+ id: `${name}-description`,
736
+ children: descriptionText
737
+ }),
738
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
739
+ id: `${name}-error`,
740
+ errors: [fieldState.error]
741
+ })
742
+ ]
743
+ })
744
+ });
745
+ }
746
+
747
+ //#endregion
748
+ //#region src/components/form/switch-input.tsx
749
+ /**
750
+ * SwitchInput - Toggle switch with react-hook-form integration
751
+ *
752
+ * @example
753
+ * ```tsx
754
+ * <SwitchInput
755
+ * control={form.control}
756
+ * name="notifications"
757
+ * label="Enable notifications"
758
+ * description="Receive email updates"
759
+ * />
760
+ * ```
761
+ */
762
+ function SwitchInput({ control, name, label, description, helperText, required, disabled, orientation = "horizontal", value: propValue, onChange: propOnChange, onValueChange, className, labelClassName, switchClassName }) {
763
+ const descriptionText = description || helperText;
764
+ const [localValue, setLocalValue] = useState(propValue || false);
765
+ useEffect(() => {
766
+ if (propValue !== void 0) setLocalValue(propValue);
767
+ }, [propValue]);
768
+ const handleDirectValueChange = (newValue) => {
769
+ setLocalValue(newValue);
770
+ propOnChange?.(newValue);
771
+ onValueChange?.(newValue);
772
+ };
773
+ const renderSwitch = (field, isDisabled, fieldState) => {
774
+ const value = field ? field.value ?? false : localValue ?? false;
775
+ const handleCheckedChange = (checked) => {
776
+ if (field) {
777
+ field.onChange(checked);
778
+ onValueChange?.(checked);
779
+ } else handleDirectValueChange(checked);
780
+ };
781
+ return /* @__PURE__ */ jsx(Switch, {
782
+ id: name,
783
+ checked: value,
784
+ onCheckedChange: handleCheckedChange,
785
+ disabled: isDisabled,
786
+ className: switchClassName,
787
+ "aria-invalid": fieldState?.invalid || void 0,
788
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0
789
+ });
790
+ };
791
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
792
+ className,
793
+ "data-disabled": disabled,
794
+ orientation,
795
+ children: [renderSwitch(void 0, disabled, void 0), /* @__PURE__ */ jsxs(FieldContent, { children: [label && /* @__PURE__ */ jsxs(FieldLabel, {
796
+ htmlFor: name,
797
+ className: labelClassName,
798
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
799
+ className: "text-destructive ml-1",
800
+ "aria-hidden": "true",
801
+ children: "*"
802
+ }), /* @__PURE__ */ jsx("span", {
803
+ className: "sr-only",
804
+ children: "(required)"
805
+ })] })]
806
+ }), descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
807
+ id: `${name}-description`,
808
+ children: descriptionText
809
+ })] })]
810
+ });
811
+ return /* @__PURE__ */ jsx(Controller, {
812
+ name,
813
+ control,
814
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
815
+ className,
816
+ "data-disabled": disabled,
817
+ "data-invalid": fieldState.invalid,
818
+ orientation,
819
+ children: [renderSwitch(field, disabled, fieldState), /* @__PURE__ */ jsxs(FieldContent, { children: [
820
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
821
+ htmlFor: name,
822
+ className: labelClassName,
823
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
824
+ className: "text-destructive ml-1",
825
+ "aria-hidden": "true",
826
+ children: "*"
827
+ }), /* @__PURE__ */ jsx("span", {
828
+ className: "sr-only",
829
+ children: "(required)"
830
+ })] })]
831
+ }),
832
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
833
+ id: `${name}-description`,
834
+ children: descriptionText
835
+ }),
836
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
837
+ id: `${name}-error`,
838
+ errors: [fieldState.error]
839
+ })
840
+ ] })]
841
+ })
842
+ });
843
+ }
844
+
845
+ //#endregion
846
+ //#region src/components/form/date-input.tsx
847
+ /**
848
+ * Convert various date formats to Date object
849
+ */
850
+ function toDate(val) {
851
+ if (!val) return void 0;
852
+ if (val instanceof Date) return val;
853
+ if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) {
854
+ const [y, m, d] = val.split("-").map(Number);
855
+ const dt = new Date(y, (m || 1) - 1, d || 1);
856
+ return isNaN(dt.getTime()) ? void 0 : dt;
857
+ }
858
+ const dt = new Date(val);
859
+ return isNaN(dt.getTime()) ? void 0 : dt;
860
+ }
861
+ /**
862
+ * DateInput - Date picker with react-hook-form integration
863
+ *
864
+ * @example
865
+ * ```tsx
866
+ * <DateInput
867
+ * control={form.control}
868
+ * name="birthDate"
869
+ * label="Date of Birth"
870
+ * minDate="1900-01-01"
871
+ * maxDate={new Date()}
872
+ * />
873
+ * ```
874
+ */
875
+ function DateInput({ control, name, label, description, helperText, placeholder = "Pick a date", required, disabled, className, labelClassName, inputClassName, minDate, maxDate, onValueChange, value: propValue, onChange: propOnChange, Icon = CalendarIcon, allowClear = true }) {
876
+ const descriptionText = description || helperText;
877
+ const isDateDisabled = (date) => {
878
+ if (!date) return false;
879
+ const minDateObj = toDate(minDate);
880
+ const maxDateObj = toDate(maxDate);
881
+ if (minDateObj && date < minDateObj) return true;
882
+ if (maxDateObj && date > maxDateObj) return true;
883
+ return false;
884
+ };
885
+ const renderDateInput = (field, isDisabled, fieldState) => {
886
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
887
+ const selected = toDate(field ? field.value : propValue);
888
+ const [open, setOpen] = useState(false);
889
+ const handleSelect = (date) => {
890
+ if (field) field.onChange(date);
891
+ else if (propOnChange) propOnChange(date);
892
+ onValueChange?.(date);
893
+ setOpen(false);
894
+ };
895
+ const handleClear = (e) => {
896
+ e?.stopPropagation?.();
897
+ if (field) field.onChange(void 0);
898
+ else if (propOnChange) propOnChange(void 0);
899
+ onValueChange?.(void 0);
900
+ };
901
+ const displayText = selected ? selected.toLocaleDateString("en-US", {
902
+ month: "short",
903
+ day: "numeric",
904
+ year: "numeric"
905
+ }) : placeholder;
906
+ return /* @__PURE__ */ jsxs(Popover, {
907
+ open,
908
+ onOpenChange: setOpen,
909
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsxs(Button, {
910
+ type: "button",
911
+ variant: "outline",
912
+ className: cn("w-full justify-start text-left font-normal", !selected && "text-muted-foreground", fieldState?.invalid && "border-destructive", inputClassName),
913
+ disabled: isDisabled,
914
+ "aria-invalid": fieldState?.invalid || void 0,
915
+ "aria-describedby": ariaDescribedBy,
916
+ children: [
917
+ /* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4" }),
918
+ displayText,
919
+ allowClear && selected && !isDisabled && /* @__PURE__ */ jsx(X, {
920
+ className: "ml-auto h-4 w-4 opacity-50 hover:opacity-100",
921
+ onClick: handleClear
922
+ })
923
+ ]
924
+ }) }), /* @__PURE__ */ jsx(PopoverContent, {
925
+ className: "w-auto p-0",
926
+ align: "start",
927
+ children: /* @__PURE__ */ jsx(Calendar, {
928
+ mode: "single",
929
+ defaultMonth: selected,
930
+ selected,
931
+ onSelect: handleSelect,
932
+ disabled: isDateDisabled,
933
+ initialFocus: true
934
+ })
935
+ })]
936
+ });
937
+ };
938
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
939
+ className,
940
+ "data-disabled": disabled,
941
+ children: [
942
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
943
+ htmlFor: name,
944
+ className: labelClassName,
945
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
946
+ className: "text-destructive ml-1",
947
+ "aria-hidden": "true",
948
+ children: "*"
949
+ }), /* @__PURE__ */ jsx("span", {
950
+ className: "sr-only",
951
+ children: "(required)"
952
+ })] })]
953
+ }),
954
+ renderDateInput(void 0, disabled),
955
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
956
+ id: `${name}-description`,
957
+ children: descriptionText
958
+ })
959
+ ]
960
+ });
961
+ return /* @__PURE__ */ jsx(Controller, {
962
+ name,
963
+ control,
964
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
965
+ className,
966
+ "data-disabled": disabled,
967
+ "data-invalid": fieldState.invalid,
968
+ children: [
969
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
970
+ htmlFor: name,
971
+ className: labelClassName,
972
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
973
+ className: "text-destructive ml-1",
974
+ "aria-hidden": "true",
975
+ children: "*"
976
+ }), /* @__PURE__ */ jsx("span", {
977
+ className: "sr-only",
978
+ children: "(required)"
979
+ })] })]
980
+ }),
981
+ renderDateInput(field, disabled, fieldState),
982
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
983
+ id: `${name}-description`,
984
+ children: descriptionText
985
+ }),
986
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
987
+ id: `${name}-error`,
988
+ errors: [fieldState.error]
989
+ })
990
+ ]
991
+ })
992
+ });
993
+ }
994
+
995
+ //#endregion
996
+ //#region src/components/form/tag-input.tsx
997
+ /**
998
+ * TagInput - Tag/chip input with react-hook-form integration
999
+ *
1000
+ * @example
1001
+ * ```tsx
1002
+ * <TagInput
1003
+ * control={form.control}
1004
+ * name="tags"
1005
+ * label="Tags"
1006
+ * placeholder="Add tags..."
1007
+ * maxTags={10}
1008
+ * suggestions={["react", "typescript", "nextjs"]}
1009
+ * />
1010
+ * ```
1011
+ */
1012
+ function TagInput({ control, name, label, description, helperText, placeholder = "Add tag...", required, disabled, className, labelClassName, inputClassName, maxTags, allowDuplicates = false, suggestions = [], suggestionLimit = 8, value: propValue = [], onChange: propOnChange, onValueChange, delimiter = ",", validateTag, transformTag, formatTag, ...props }) {
1013
+ const descriptionText = description || helperText;
1014
+ const [inputValue, setInputValue] = useState("");
1015
+ const inputRef = useRef(null);
1016
+ const parseMultipleTags = useCallback((input) => {
1017
+ if (!input.trim()) return [];
1018
+ return input.split(delimiter).map((tag) => tag.trim()).filter((tag) => tag.length > 0).map((tag) => transformTag ? transformTag(tag) : tag).filter((tag) => !validateTag || validateTag(tag));
1019
+ }, [
1020
+ delimiter,
1021
+ validateTag,
1022
+ transformTag
1023
+ ]);
1024
+ const handleAddMultipleTags = useCallback((tags, newTags, field) => {
1025
+ if (!newTags || newTags.length === 0) return tags;
1026
+ const updatedTags = [...tags];
1027
+ let addedCount = 0;
1028
+ for (const newTag of newTags) {
1029
+ if (!newTag.trim()) continue;
1030
+ const trimmedTag = newTag.trim();
1031
+ if (!allowDuplicates && updatedTags.includes(trimmedTag)) continue;
1032
+ if (maxTags && updatedTags.length >= maxTags) break;
1033
+ updatedTags.push(trimmedTag);
1034
+ addedCount++;
1035
+ }
1036
+ if (addedCount > 0) {
1037
+ if (field) field.onChange(updatedTags);
1038
+ else if (propOnChange) propOnChange(updatedTags);
1039
+ onValueChange?.(updatedTags);
1040
+ }
1041
+ return updatedTags;
1042
+ }, [
1043
+ allowDuplicates,
1044
+ maxTags,
1045
+ propOnChange,
1046
+ onValueChange
1047
+ ]);
1048
+ const handleAddTag = useCallback((tags, newTag, field) => {
1049
+ const updatedTags = handleAddMultipleTags(tags, parseMultipleTags(newTag), field);
1050
+ setInputValue("");
1051
+ return updatedTags;
1052
+ }, [parseMultipleTags, handleAddMultipleTags]);
1053
+ const handleRemoveTag = useCallback((tags, indexToRemove, field) => {
1054
+ const updatedTags = tags.filter((_, index) => index !== indexToRemove);
1055
+ if (field) field.onChange(updatedTags);
1056
+ else if (propOnChange) propOnChange(updatedTags);
1057
+ onValueChange?.(updatedTags);
1058
+ return updatedTags;
1059
+ }, [propOnChange, onValueChange]);
1060
+ const handleInputChange = useCallback((e, tags, field) => {
1061
+ const value = e.target.value;
1062
+ if (value.includes(delimiter)) {
1063
+ const parts = value.split(delimiter);
1064
+ const completeTags = parts.slice(0, -1);
1065
+ const remainingInput = parts[parts.length - 1] || "";
1066
+ if (completeTags.length > 0) handleAddMultipleTags(tags, completeTags.map((t) => t.trim()).filter((t) => t), field);
1067
+ setInputValue(remainingInput);
1068
+ } else setInputValue(value);
1069
+ }, [delimiter, handleAddMultipleTags]);
1070
+ const handlePaste = useCallback((e, tags, field) => {
1071
+ e.preventDefault();
1072
+ const pastedTags = parseMultipleTags(e.clipboardData.getData("text"));
1073
+ if (pastedTags.length > 0) {
1074
+ handleAddMultipleTags(tags, pastedTags, field);
1075
+ setInputValue("");
1076
+ }
1077
+ }, [parseMultipleTags, handleAddMultipleTags]);
1078
+ const handleKeyDown = useCallback((e, tags, field) => {
1079
+ if (e.key === "Enter" || e.key === delimiter) {
1080
+ e.preventDefault();
1081
+ handleAddTag(tags, inputValue, field);
1082
+ } else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
1083
+ e.preventDefault();
1084
+ handleRemoveTag(tags, tags.length - 1, field);
1085
+ }
1086
+ }, [
1087
+ delimiter,
1088
+ inputValue,
1089
+ handleAddTag,
1090
+ handleRemoveTag
1091
+ ]);
1092
+ const getPlaceholder = useCallback((tagsCount) => {
1093
+ if (tagsCount === 0) return placeholder.includes("comma") ? placeholder : `${placeholder} (separate with ${delimiter} for multiple)`;
1094
+ return "Add another...";
1095
+ }, [placeholder, delimiter]);
1096
+ const canAddMoreTags = useCallback((tagsLength) => {
1097
+ return !maxTags || tagsLength < maxTags;
1098
+ }, [maxTags]);
1099
+ const renderTagInput = ({ field, disabled: isDisabled, error }) => {
1100
+ const tags = field ? field.value || [] : propValue || [];
1101
+ const showInput = !isDisabled && canAddMoreTags(tags.length);
1102
+ const normalizedInputRaw = (inputValue || "").trim();
1103
+ const normalizedInput = (transformTag ? transformTag(normalizedInputRaw) : normalizedInputRaw).toLowerCase().trim();
1104
+ const filteredSuggestions = useMemo(() => {
1105
+ if (!normalizedInput) return [];
1106
+ const existingSet = new Set(tags.map((t) => t.toLowerCase()));
1107
+ return suggestions.filter(Boolean).map((s) => transformTag ? transformTag(s) : s).filter((s) => s.toLowerCase().includes(normalizedInput)).filter((s) => allowDuplicates || !existingSet.has(s.toLowerCase())).slice(0, suggestionLimit);
1108
+ }, [
1109
+ normalizedInput,
1110
+ tags,
1111
+ suggestions,
1112
+ transformTag,
1113
+ allowDuplicates,
1114
+ suggestionLimit
1115
+ ]);
1116
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1117
+ tags.length > 0 && /* @__PURE__ */ jsx("div", {
1118
+ className: cn("flex flex-wrap gap-1.5 mb-3", "p-2.5 rounded-md bg-muted/30 border border-border/50"),
1119
+ children: tags.map((tag, index) => {
1120
+ const displayTag = formatTag ? formatTag(tag) : tag;
1121
+ return /* @__PURE__ */ jsxs(Badge, {
1122
+ variant: "secondary",
1123
+ className: cn("group flex items-center gap-1.5 px-2.5 py-1", "bg-background border border-border shadow-sm", "hover:border-primary/50 transition-all duration-200", "animate-in fade-in-0 zoom-in-95", isDisabled && "opacity-60"),
1124
+ children: [
1125
+ /* @__PURE__ */ jsx(Tag, { className: "h-3 w-3 text-muted-foreground" }),
1126
+ /* @__PURE__ */ jsx("span", {
1127
+ className: "max-w-[200px] truncate text-sm font-medium",
1128
+ title: displayTag,
1129
+ children: displayTag
1130
+ }),
1131
+ !isDisabled && /* @__PURE__ */ jsx(Button, {
1132
+ type: "button",
1133
+ variant: "ghost",
1134
+ size: "sm",
1135
+ className: cn("h-4 w-4 p-0 ml-0.5", "text-muted-foreground/60 hover:text-destructive", "hover:bg-destructive/10 rounded-sm", "transition-colors"),
1136
+ onClick: (e) => {
1137
+ e.stopPropagation();
1138
+ handleRemoveTag(tags, index, field);
1139
+ },
1140
+ "aria-label": `Remove ${displayTag}`,
1141
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1142
+ })
1143
+ ]
1144
+ }, `${tag}-${index}`);
1145
+ })
1146
+ }),
1147
+ showInput && /* @__PURE__ */ jsxs("div", {
1148
+ className: "space-y-3",
1149
+ children: [/* @__PURE__ */ jsxs(InputGroup, {
1150
+ className: cn(error && "border-destructive"),
1151
+ children: [
1152
+ /* @__PURE__ */ jsx(InputGroupAddon, {
1153
+ align: "inline-start",
1154
+ children: /* @__PURE__ */ jsx(Tag, { className: "h-4 w-4" })
1155
+ }),
1156
+ /* @__PURE__ */ jsx(InputGroupInput, {
1157
+ ref: inputRef,
1158
+ type: "text",
1159
+ value: inputValue,
1160
+ onChange: (e) => handleInputChange(e, tags, field),
1161
+ onKeyDown: (e) => handleKeyDown(e, tags, field),
1162
+ onPaste: (e) => handlePaste(e, tags, field),
1163
+ placeholder: getPlaceholder(tags.length),
1164
+ disabled: isDisabled,
1165
+ "aria-label": "Add new tag",
1166
+ "aria-invalid": !!error,
1167
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
1168
+ className: inputClassName,
1169
+ ...props
1170
+ }),
1171
+ inputValue.trim() && /* @__PURE__ */ jsx(InputGroupAddon, {
1172
+ align: "inline-end",
1173
+ children: /* @__PURE__ */ jsx(Button, {
1174
+ type: "button",
1175
+ variant: "ghost",
1176
+ size: "icon",
1177
+ className: "h-6 w-6 text-primary hover:text-primary hover:bg-primary/10",
1178
+ onClick: () => handleAddTag(tags, inputValue, field),
1179
+ "aria-label": "Add tag",
1180
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
1181
+ })
1182
+ })
1183
+ ]
1184
+ }), filteredSuggestions.length > 0 && /* @__PURE__ */ jsxs("div", {
1185
+ className: "flex flex-wrap gap-1.5 p-2 rounded-md bg-muted/30 border border-dashed",
1186
+ children: [/* @__PURE__ */ jsx("span", {
1187
+ className: "text-xs text-muted-foreground font-medium w-full mb-1",
1188
+ children: "Suggestions:"
1189
+ }), filteredSuggestions.map((sug) => /* @__PURE__ */ jsxs(Button, {
1190
+ type: "button",
1191
+ variant: "outline",
1192
+ size: "sm",
1193
+ className: cn("h-7 px-2.5 text-xs", "hover:border-primary/50 hover:bg-primary/5", "transition-all duration-200"),
1194
+ onClick: (e) => {
1195
+ e.stopPropagation();
1196
+ handleAddMultipleTags(tags, [sug], field);
1197
+ setInputValue("");
1198
+ },
1199
+ "aria-label": `Add ${formatTag ? formatTag(sug) : sug}`,
1200
+ children: [formatTag ? formatTag(sug) : sug, /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3 ml-1.5 opacity-70" })]
1201
+ }, sug))]
1202
+ })]
1203
+ }),
1204
+ maxTags && /* @__PURE__ */ jsxs("div", {
1205
+ className: cn("text-xs font-medium mt-2 flex items-center gap-1.5", tags.length >= maxTags ? "text-destructive" : "text-muted-foreground"),
1206
+ children: [
1207
+ /* @__PURE__ */ jsx("div", { className: cn("h-1.5 w-1.5 rounded-full", tags.length >= maxTags ? "bg-destructive" : "bg-muted-foreground/50") }),
1208
+ tags.length,
1209
+ "/",
1210
+ maxTags,
1211
+ " tags"
1212
+ ]
1213
+ })
1214
+ ] });
1215
+ };
1216
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
1217
+ className,
1218
+ "data-disabled": disabled,
1219
+ children: [label && /* @__PURE__ */ jsxs(FieldLabel, {
1220
+ htmlFor: name,
1221
+ className: labelClassName,
1222
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1223
+ className: "text-destructive ml-1",
1224
+ "aria-hidden": "true",
1225
+ children: "*"
1226
+ }), /* @__PURE__ */ jsx("span", {
1227
+ className: "sr-only",
1228
+ children: "(required)"
1229
+ })] })]
1230
+ }), /* @__PURE__ */ jsxs(FieldContent, { children: [renderTagInput({
1231
+ field: null,
1232
+ disabled,
1233
+ error: null
1234
+ }), descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1235
+ id: `${name}-description`,
1236
+ children: descriptionText
1237
+ })] })]
1238
+ });
1239
+ return /* @__PURE__ */ jsx(Controller, {
1240
+ name,
1241
+ control,
1242
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
1243
+ className,
1244
+ "data-disabled": disabled,
1245
+ "data-invalid": fieldState.invalid,
1246
+ children: [label && /* @__PURE__ */ jsxs(FieldLabel, {
1247
+ htmlFor: name,
1248
+ className: labelClassName,
1249
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1250
+ className: "text-destructive ml-1",
1251
+ "aria-hidden": "true",
1252
+ children: "*"
1253
+ }), /* @__PURE__ */ jsx("span", {
1254
+ className: "sr-only",
1255
+ children: "(required)"
1256
+ })] })]
1257
+ }), /* @__PURE__ */ jsxs(FieldContent, { children: [
1258
+ renderTagInput({
1259
+ field,
1260
+ disabled,
1261
+ error: fieldState?.error?.message
1262
+ }),
1263
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1264
+ id: `${name}-description`,
1265
+ children: descriptionText
1266
+ }),
1267
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
1268
+ id: `${name}-error`,
1269
+ errors: [fieldState.error]
1270
+ })
1271
+ ] })]
1272
+ })
1273
+ });
1274
+ }
1275
+
1276
+ //#endregion
1277
+ //#region src/components/form/tag-choice-input.tsx
1278
+ function TagChoiceInputInternal({ name, label, description, placeholder = "Select...", required, disabled, className, items = [], value = [], onValueChange, error }) {
1279
+ const [open, setOpen] = useState(false);
1280
+ const [searchQuery, setSearchQuery] = useState("");
1281
+ const searchRef = useRef(null);
1282
+ const selectedValues = value || [];
1283
+ useEffect(() => {
1284
+ if (open) {
1285
+ const t = setTimeout(() => searchRef.current?.focus(), 50);
1286
+ return () => clearTimeout(t);
1287
+ } else setSearchQuery("");
1288
+ }, [open]);
1289
+ const handleSelect = (itemValue) => {
1290
+ const newValue = selectedValues.includes(itemValue) ? selectedValues.filter((v) => v !== itemValue) : [...selectedValues, itemValue];
1291
+ onValueChange?.(newValue);
1292
+ };
1293
+ const handleRemove = (e, valueToRemove) => {
1294
+ e.preventDefault();
1295
+ e.stopPropagation();
1296
+ onValueChange?.(selectedValues.filter((v) => v !== valueToRemove));
1297
+ };
1298
+ const filteredItems = items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()));
1299
+ return /* @__PURE__ */ jsxs(Field, {
1300
+ className,
1301
+ "data-disabled": disabled,
1302
+ "data-invalid": !!error,
1303
+ children: [label && /* @__PURE__ */ jsxs(FieldLabel, { children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1304
+ className: "text-destructive ml-1",
1305
+ "aria-hidden": "true",
1306
+ children: "*"
1307
+ }), /* @__PURE__ */ jsx("span", {
1308
+ className: "sr-only",
1309
+ children: "(required)"
1310
+ })] })] }), /* @__PURE__ */ jsxs(FieldContent, { children: [
1311
+ /* @__PURE__ */ jsxs(Popover, {
1312
+ open,
1313
+ onOpenChange: setOpen,
1314
+ modal: true,
1315
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
1316
+ render: /* @__PURE__ */ jsx("button", {
1317
+ type: "button",
1318
+ role: "combobox",
1319
+ "aria-expanded": open,
1320
+ "aria-invalid": !!error || void 0,
1321
+ "aria-describedby": [description ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
1322
+ disabled,
1323
+ className: cn("flex w-full min-h-10 items-center rounded-md border border-input bg-background text-left", "ring-offset-background transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "hover:bg-accent/50", error && "border-destructive", disabled && "opacity-50 cursor-not-allowed")
1324
+ }),
1325
+ children: [/* @__PURE__ */ jsx("div", {
1326
+ className: "flex flex-1 flex-wrap items-center gap-1 px-3 py-1.5 min-h-[2.25rem]",
1327
+ children: selectedValues.length > 0 ? selectedValues.map((val) => {
1328
+ const item = items.find((i) => i.value === val);
1329
+ return /* @__PURE__ */ jsxs(Badge, {
1330
+ variant: "secondary",
1331
+ className: "flex items-center gap-1 px-2 py-0.5 text-xs font-medium shrink-0",
1332
+ children: [item?.label || val, !disabled && /* @__PURE__ */ jsx("span", {
1333
+ role: "button",
1334
+ tabIndex: -1,
1335
+ className: "ml-0.5 rounded-sm hover:bg-foreground/20 cursor-pointer",
1336
+ onPointerDown: (e) => handleRemove(e, val),
1337
+ "aria-label": `Remove ${item?.label || val}`,
1338
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1339
+ })]
1340
+ }, val);
1341
+ }) : /* @__PURE__ */ jsx("span", {
1342
+ className: "text-sm text-muted-foreground",
1343
+ children: placeholder
1344
+ })
1345
+ }), /* @__PURE__ */ jsx("div", {
1346
+ className: "flex items-center px-2 shrink-0",
1347
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-4 w-4 text-muted-foreground transition-transform duration-200", open && "rotate-180") })
1348
+ })]
1349
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
1350
+ className: "w-(--anchor-width) p-0",
1351
+ align: "start",
1352
+ children: [
1353
+ /* @__PURE__ */ jsx("div", {
1354
+ className: "p-2 border-b",
1355
+ children: /* @__PURE__ */ jsx("input", {
1356
+ ref: searchRef,
1357
+ placeholder: "Search options...",
1358
+ value: searchQuery,
1359
+ onChange: (e) => setSearchQuery(e.target.value),
1360
+ className: "flex h-8 w-full rounded-md bg-transparent px-2 py-1 text-sm outline-none placeholder:text-muted-foreground"
1361
+ })
1362
+ }),
1363
+ /* @__PURE__ */ jsx("div", {
1364
+ className: "max-h-[200px] overflow-y-auto p-1",
1365
+ children: filteredItems.length === 0 ? /* @__PURE__ */ jsx("div", {
1366
+ className: "py-4 text-center text-sm text-muted-foreground",
1367
+ children: "No options found"
1368
+ }) : filteredItems.map((item) => {
1369
+ const isSelected = selectedValues.includes(item.value);
1370
+ return /* @__PURE__ */ jsxs("button", {
1371
+ type: "button",
1372
+ onClick: () => handleSelect(item.value),
1373
+ disabled: item.disabled,
1374
+ className: cn("w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-sm cursor-pointer", "hover:bg-accent hover:text-accent-foreground transition-colors", isSelected && "bg-accent/50 font-medium", item.disabled && "opacity-50 cursor-not-allowed"),
1375
+ children: [/* @__PURE__ */ jsx("div", {
1376
+ className: cn("flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border", isSelected ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground/30"),
1377
+ children: isSelected && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
1378
+ }), /* @__PURE__ */ jsx("span", { children: item.label })]
1379
+ }, item.value);
1380
+ })
1381
+ }),
1382
+ selectedValues.length > 0 && /* @__PURE__ */ jsxs("div", {
1383
+ className: "border-t px-2 py-1.5 text-xs text-muted-foreground",
1384
+ children: [selectedValues.length, " selected"]
1385
+ })
1386
+ ]
1387
+ })]
1388
+ }),
1389
+ description && /* @__PURE__ */ jsx(FieldDescription, {
1390
+ id: `${name}-description`,
1391
+ children: description
1392
+ }),
1393
+ error && /* @__PURE__ */ jsx(FieldError$1, {
1394
+ id: `${name}-error`,
1395
+ errors: [{ message: error }]
1396
+ })
1397
+ ] })]
1398
+ });
1399
+ }
1400
+ TagChoiceInputInternal.displayName = "TagChoiceInputInternal";
1401
+ /**
1402
+ * TagChoiceInput - Multi-select with inline tag display
1403
+ *
1404
+ * Selected items appear as badges inside the trigger area.
1405
+ * The popover dropdown stays anchored without layout jumps.
1406
+ */
1407
+ function TagChoiceInput({ control, name, label, description, helperText, placeholder = "Select...", required, disabled, className, items = [], value: propValue = [], onValueChange }) {
1408
+ const descriptionText = description || helperText;
1409
+ if (control) return /* @__PURE__ */ jsx(Controller, {
1410
+ name,
1411
+ control,
1412
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsx(TagChoiceInputInternal, {
1413
+ name,
1414
+ label,
1415
+ description: descriptionText,
1416
+ placeholder,
1417
+ required,
1418
+ disabled,
1419
+ className,
1420
+ items,
1421
+ value: field.value || [],
1422
+ onValueChange: (val) => {
1423
+ field.onChange(val);
1424
+ onValueChange?.(val);
1425
+ },
1426
+ error: fieldState?.error?.message
1427
+ })
1428
+ });
1429
+ return /* @__PURE__ */ jsx(TagChoiceInputInternal, {
1430
+ name,
1431
+ label,
1432
+ description: descriptionText,
1433
+ placeholder,
1434
+ required,
1435
+ disabled,
1436
+ className,
1437
+ items,
1438
+ value: propValue,
1439
+ onValueChange
1440
+ });
1441
+ }
1442
+ TagChoiceInput.displayName = "TagChoiceInput";
1443
+
1444
+ //#endregion
1445
+ //#region src/components/form/combobox-input.tsx
1446
+ /**
1447
+ * ComboboxInput - Searchable select with react-hook-form integration using Base UI
1448
+ *
1449
+ * Passes `{ value, label }` objects to Base UI Combobox so it auto-resolves
1450
+ * labels for display and filtering. The form value is always the string `value`.
1451
+ *
1452
+ * @example
1453
+ * ```tsx
1454
+ * // With react-hook-form
1455
+ * <ComboboxInput
1456
+ * control={form.control}
1457
+ * name="country"
1458
+ * label="Country"
1459
+ * items={[
1460
+ * { value: "us", label: "United States" },
1461
+ * { value: "uk", label: "United Kingdom" },
1462
+ * ]}
1463
+ * placeholder="Select a country..."
1464
+ * />
1465
+ *
1466
+ * // Standalone
1467
+ * <ComboboxInput
1468
+ * name="country"
1469
+ * label="Country"
1470
+ * items={items}
1471
+ * value={selectedCountry}
1472
+ * onValueChange={setSelectedCountry}
1473
+ * />
1474
+ * ```
1475
+ */
1476
+ function ComboboxInput({ control, name, label, placeholder = "Select...", emptyText = "No items found.", description, helperText, required, disabled, items = [], className, labelClassName, inputClassName, onValueChange, renderOption, value: propValue, onChange: propOnChange }) {
1477
+ const descriptionText = description || helperText;
1478
+ const handleValueChange = useCallback((newItem, field) => {
1479
+ const safeValue = newItem?.value || "";
1480
+ if (field) field.onChange(safeValue);
1481
+ else if (propOnChange) propOnChange(safeValue);
1482
+ onValueChange?.(safeValue);
1483
+ }, [propOnChange, onValueChange]);
1484
+ const renderCombobox = (currentValue, field, isDisabled, fieldState) => {
1485
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
1486
+ return /* @__PURE__ */ jsxs(Combobox, {
1487
+ items,
1488
+ value: currentValue ? items.find((item) => item.value === currentValue) ?? null : null,
1489
+ onValueChange: (item) => handleValueChange(item, field),
1490
+ disabled: isDisabled,
1491
+ children: [/* @__PURE__ */ jsx(ComboboxInput$1, {
1492
+ placeholder,
1493
+ className: cn(inputClassName),
1494
+ "aria-describedby": ariaDescribedBy,
1495
+ "aria-invalid": fieldState?.invalid || void 0
1496
+ }), /* @__PURE__ */ jsxs(ComboboxContent, { children: [/* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyText }), /* @__PURE__ */ jsx(ComboboxList, { children: (item) => {
1497
+ if (!item) return null;
1498
+ return /* @__PURE__ */ jsx(ComboboxItem, {
1499
+ value: item,
1500
+ disabled: item.disabled,
1501
+ children: renderOption ? renderOption(item) : item.label
1502
+ }, item.value);
1503
+ } })] })]
1504
+ });
1505
+ };
1506
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
1507
+ className,
1508
+ "data-disabled": disabled,
1509
+ children: [
1510
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
1511
+ htmlFor: name,
1512
+ className: labelClassName,
1513
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1514
+ className: "text-destructive ml-1",
1515
+ "aria-hidden": "true",
1516
+ children: "*"
1517
+ }), /* @__PURE__ */ jsx("span", {
1518
+ className: "sr-only",
1519
+ children: "(required)"
1520
+ })] })]
1521
+ }),
1522
+ renderCombobox(propValue, void 0, disabled),
1523
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1524
+ id: `${name}-description`,
1525
+ children: descriptionText
1526
+ })
1527
+ ]
1528
+ });
1529
+ return /* @__PURE__ */ jsx(Controller, {
1530
+ name,
1531
+ control,
1532
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
1533
+ className,
1534
+ "data-disabled": disabled,
1535
+ "data-invalid": fieldState.invalid,
1536
+ children: [
1537
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
1538
+ htmlFor: name,
1539
+ className: labelClassName,
1540
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1541
+ className: "text-destructive ml-1",
1542
+ "aria-hidden": "true",
1543
+ children: "*"
1544
+ }), /* @__PURE__ */ jsx("span", {
1545
+ className: "sr-only",
1546
+ children: "(required)"
1547
+ })] })]
1548
+ }),
1549
+ renderCombobox(field.value, field, disabled, fieldState),
1550
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1551
+ id: `${name}-description`,
1552
+ children: descriptionText
1553
+ }),
1554
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
1555
+ id: `${name}-error`,
1556
+ errors: [fieldState.error]
1557
+ })
1558
+ ]
1559
+ })
1560
+ });
1561
+ }
1562
+
1563
+ //#endregion
1564
+ //#region src/components/form/slug-field.tsx
1565
+ /**
1566
+ * Generates a URL-friendly slug from a string
1567
+ */
1568
+ function generateSlug(text) {
1569
+ if (!text) return "";
1570
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1571
+ }
1572
+ /**
1573
+ * SlugField - URL slug input with auto-generation
1574
+ *
1575
+ * @example
1576
+ * ```tsx
1577
+ * <SlugField
1578
+ * control={form.control}
1579
+ * name="slug"
1580
+ * label="URL Slug"
1581
+ * sourceValue={form.watch("title")}
1582
+ * description="This will be used in the page URL"
1583
+ * required
1584
+ * />
1585
+ * ```
1586
+ */
1587
+ function SlugField({ control, name, description, helperText, required, label, placeholder = "my-page-slug", disabled, sourceValue, onGenerate, className, inputClassName, labelClassName, onValueChange, value, onChange, error, ref }) {
1588
+ const descriptionText = description || helperText;
1589
+ const handleGenerate = (_currentValue, fieldOnChange) => {
1590
+ const newSlug = onGenerate ? onGenerate(sourceValue || "") : generateSlug(sourceValue);
1591
+ fieldOnChange?.(newSlug);
1592
+ onValueChange?.(newSlug);
1593
+ };
1594
+ const renderInput = (fieldValue, fieldOnChange, isDisabled, fieldState) => {
1595
+ return /* @__PURE__ */ jsxs(InputGroup, { children: [/* @__PURE__ */ jsx(InputGroupInput, {
1596
+ ref,
1597
+ id: name,
1598
+ type: "text",
1599
+ disabled: isDisabled,
1600
+ placeholder,
1601
+ value: fieldValue || "",
1602
+ onChange: (e) => {
1603
+ const newValue = e.target.value;
1604
+ fieldOnChange?.(newValue);
1605
+ onValueChange?.(newValue);
1606
+ },
1607
+ "aria-invalid": fieldState?.invalid,
1608
+ "aria-describedby": [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
1609
+ className: inputClassName
1610
+ }), /* @__PURE__ */ jsx(InputGroupAddon, {
1611
+ align: "inline-end",
1612
+ children: /* @__PURE__ */ jsxs(InputGroupButton, {
1613
+ type: "button",
1614
+ size: "sm",
1615
+ onClick: () => handleGenerate(fieldValue, fieldOnChange),
1616
+ disabled: isDisabled || !sourceValue,
1617
+ title: "Generate slug from source",
1618
+ children: [/* @__PURE__ */ jsx(Wand2, { className: "h-4 w-4" }), "Generate"]
1619
+ })
1620
+ })] });
1621
+ };
1622
+ if (control && name) return /* @__PURE__ */ jsx(Controller, {
1623
+ name,
1624
+ control,
1625
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
1626
+ className,
1627
+ "data-disabled": disabled,
1628
+ "data-invalid": fieldState.invalid,
1629
+ children: [
1630
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
1631
+ htmlFor: name,
1632
+ className: labelClassName,
1633
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1634
+ className: "text-destructive ml-1",
1635
+ "aria-hidden": "true",
1636
+ children: "*"
1637
+ }), /* @__PURE__ */ jsx("span", {
1638
+ className: "sr-only",
1639
+ children: "(required)"
1640
+ })] })]
1641
+ }),
1642
+ renderInput(field.value, field.onChange, disabled, fieldState),
1643
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1644
+ id: `${name}-description`,
1645
+ children: descriptionText
1646
+ }),
1647
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
1648
+ id: `${name}-error`,
1649
+ errors: [fieldState.error]
1650
+ })
1651
+ ]
1652
+ })
1653
+ });
1654
+ const handleDirectChange = (newValue) => {
1655
+ onChange?.({ target: { value: newValue } });
1656
+ onValueChange?.(newValue);
1657
+ };
1658
+ return /* @__PURE__ */ jsxs(Field, {
1659
+ className,
1660
+ "data-disabled": disabled,
1661
+ children: [
1662
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
1663
+ htmlFor: name,
1664
+ className: labelClassName,
1665
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1666
+ className: "text-destructive ml-1",
1667
+ "aria-hidden": "true",
1668
+ children: "*"
1669
+ }), /* @__PURE__ */ jsx("span", {
1670
+ className: "sr-only",
1671
+ children: "(required)"
1672
+ })] })]
1673
+ }),
1674
+ renderInput(value, handleDirectChange, disabled, { error }),
1675
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
1676
+ id: `${name}-description`,
1677
+ children: descriptionText
1678
+ }),
1679
+ error && /* @__PURE__ */ jsx(FieldError$1, {
1680
+ id: `${name}-error`,
1681
+ errors: [error]
1682
+ })
1683
+ ]
1684
+ });
1685
+ }
1686
+
1687
+ //#endregion
1688
+ //#region src/components/form/form-error-summary.tsx
1689
+ const flattenErrors = (errors, prefix = "") => {
1690
+ const flattened = [];
1691
+ Object.entries(errors).forEach(([key, value]) => {
1692
+ const path = prefix ? `${prefix}.${key}` : key;
1693
+ if (value && typeof value === "object" && "message" in value) flattened.push({
1694
+ field: path,
1695
+ message: String(value.message)
1696
+ });
1697
+ else if (value && typeof value === "object") flattened.push(...flattenErrors(value, path));
1698
+ });
1699
+ return flattened;
1700
+ };
1701
+ /**
1702
+ * FormErrorSummary - Displays a summary of form validation errors
1703
+ *
1704
+ * Takes react-hook-form errors object and displays all errors in a list.
1705
+ * Automatically flattens nested errors for complex form structures.
1706
+ */
1707
+ function FormErrorSummary({ errors, className, title = "Please fix the following errors:" }) {
1708
+ if (!errors || Object.keys(errors).length === 0) return null;
1709
+ const errorList = flattenErrors(errors);
1710
+ if (errorList.length === 0) return null;
1711
+ return /* @__PURE__ */ jsxs("div", {
1712
+ className: cn("bg-destructive/15 p-3 rounded-md border border-destructive/20", className),
1713
+ children: [/* @__PURE__ */ jsx("h4", {
1714
+ className: "text-sm font-medium text-destructive mb-2",
1715
+ children: title
1716
+ }), /* @__PURE__ */ jsx("ul", {
1717
+ className: "text-sm text-destructive space-y-1",
1718
+ children: errorList.map(({ field, message }, index) => /* @__PURE__ */ jsxs("li", {
1719
+ className: "flex items-start gap-2",
1720
+ children: [/* @__PURE__ */ jsx("span", {
1721
+ className: "text-destructive",
1722
+ children: "•"
1723
+ }), /* @__PURE__ */ jsxs("span", { children: [
1724
+ /* @__PURE__ */ jsxs("strong", { children: [field, ":"] }),
1725
+ " ",
1726
+ message
1727
+ ] })]
1728
+ }, `${field}-${index}`))
1729
+ })]
1730
+ });
1731
+ }
1732
+
1733
+ //#endregion
1734
+ //#region src/components/form/date-range-filter.tsx
1735
+ /**
1736
+ * DateRangeFilter - A date range picker for filtering
1737
+ *
1738
+ * Features:
1739
+ * - Only applies filter when explicitly submitted
1740
+ * - Supports clearing filters
1741
+ * - Uses Popover to show calendar on click
1742
+ * - Configurable min/max dates
1743
+ */
1744
+ function DateRangeFilter({ initialStartDate, initialEndDate, onFilter, onClear, className, buttonClassName, placeholder = "Pick a date range", minDate, maxDate, alignRight = true, numberOfMonths = 1 }) {
1745
+ const [dateRange, setDateRange] = React$1.useState(initialStartDate || initialEndDate ? {
1746
+ from: initialStartDate,
1747
+ to: initialEndDate
1748
+ } : void 0);
1749
+ const [appliedRange, setAppliedRange] = React$1.useState(dateRange);
1750
+ const [open, setOpen] = React$1.useState(false);
1751
+ React$1.useEffect(() => {
1752
+ const newRange = initialStartDate || initialEndDate ? {
1753
+ from: initialStartDate,
1754
+ to: initialEndDate
1755
+ } : void 0;
1756
+ setDateRange(newRange);
1757
+ setAppliedRange(newRange);
1758
+ }, [initialStartDate, initialEndDate]);
1759
+ const handleApplyFilter = () => {
1760
+ setAppliedRange(dateRange);
1761
+ onFilter?.(dateRange?.from || null, dateRange?.to || null);
1762
+ setOpen(false);
1763
+ };
1764
+ const handleClearFilter = (e) => {
1765
+ e?.stopPropagation?.();
1766
+ setDateRange(void 0);
1767
+ setAppliedRange(void 0);
1768
+ onClear?.();
1769
+ setOpen(false);
1770
+ };
1771
+ const getDisplayText = () => {
1772
+ if (!appliedRange?.from && !appliedRange?.to) return placeholder;
1773
+ const formatDate = (date) => {
1774
+ return date.toLocaleDateString("en-US", {
1775
+ month: "short",
1776
+ day: "numeric",
1777
+ year: "numeric"
1778
+ });
1779
+ };
1780
+ if (appliedRange.from && appliedRange.to) return `${formatDate(appliedRange.from)} - ${formatDate(appliedRange.to)}`;
1781
+ if (appliedRange.from) return `From ${formatDate(appliedRange.from)}`;
1782
+ if (appliedRange.to) return `Until ${formatDate(appliedRange.to)}`;
1783
+ };
1784
+ return /* @__PURE__ */ jsx("div", {
1785
+ className,
1786
+ children: /* @__PURE__ */ jsxs(Popover, {
1787
+ open,
1788
+ onOpenChange: setOpen,
1789
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsxs(Button, {
1790
+ type: "button",
1791
+ variant: "outline",
1792
+ className: cn("w-full justify-start font-normal", !(appliedRange?.from || appliedRange?.to) && "text-muted-foreground", buttonClassName),
1793
+ children: [
1794
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }),
1795
+ getDisplayText(),
1796
+ (appliedRange?.from || appliedRange?.to) && /* @__PURE__ */ jsx(X, {
1797
+ className: "ml-auto h-4 w-4 opacity-50 hover:opacity-100",
1798
+ onClick: handleClearFilter
1799
+ })
1800
+ ]
1801
+ }) }), /* @__PURE__ */ jsx(PopoverContent, {
1802
+ className: "w-auto p-0",
1803
+ align: alignRight ? "end" : "start",
1804
+ side: "bottom",
1805
+ children: /* @__PURE__ */ jsxs("div", {
1806
+ className: "p-3 space-y-4",
1807
+ children: [/* @__PURE__ */ jsx(Calendar, {
1808
+ mode: "range",
1809
+ defaultMonth: dateRange?.from,
1810
+ selected: dateRange,
1811
+ onSelect: setDateRange,
1812
+ disabled: (date) => {
1813
+ if (minDate && date < minDate) return true;
1814
+ if (maxDate && date > maxDate) return true;
1815
+ return false;
1816
+ },
1817
+ numberOfMonths,
1818
+ initialFocus: true
1819
+ }), /* @__PURE__ */ jsxs("div", {
1820
+ className: "flex justify-between gap-2 pt-2 border-t",
1821
+ children: [/* @__PURE__ */ jsx(Button, {
1822
+ type: "button",
1823
+ variant: "outline",
1824
+ size: "sm",
1825
+ onClick: () => handleClearFilter(),
1826
+ children: "Clear"
1827
+ }), /* @__PURE__ */ jsx(Button, {
1828
+ type: "button",
1829
+ size: "sm",
1830
+ onClick: handleApplyFilter,
1831
+ disabled: !dateRange?.from && !dateRange?.to,
1832
+ children: "Apply Filter"
1833
+ })]
1834
+ })]
1835
+ })
1836
+ })]
1837
+ })
1838
+ });
1839
+ }
1840
+
1841
+ //#endregion
1842
+ //#region src/components/form/date-range-input.tsx
1843
+ function DateRangePopover({ dateRange, placeholder, disabled, buttonClassName, calendarClassName, allowClear, showBadge, Icon, formatDateRange, isDateDisabled, handleDateRangeSelect, handleClear, minDate, maxDate, ariaDescribedBy, ariaInvalid }) {
1844
+ const hasValue = dateRange?.from || dateRange?.to;
1845
+ return /* @__PURE__ */ jsxs("div", {
1846
+ className: "relative w-full",
1847
+ children: [/* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsxs(Button, {
1848
+ type: "button",
1849
+ disabled,
1850
+ variant: "outline",
1851
+ "aria-invalid": ariaInvalid || void 0,
1852
+ "aria-describedby": ariaDescribedBy,
1853
+ className: cn("w-full justify-start gap-2 font-normal", !hasValue && "text-muted-foreground", hasValue && allowClear && "pr-10", ariaInvalid && "border-destructive", buttonClassName),
1854
+ children: [/* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0" }), hasValue ? /* @__PURE__ */ jsxs("span", {
1855
+ className: "flex-1 truncate text-left",
1856
+ children: [formatDateRange(dateRange), showBadge && dateRange?.from && dateRange?.to && (() => {
1857
+ const from = dateRange.from instanceof Date ? dateRange.from : new Date(dateRange.from);
1858
+ const to = dateRange.to instanceof Date ? dateRange.to : new Date(dateRange.to);
1859
+ const days = Math.ceil((to.getTime() - from.getTime()) / (1e3 * 60 * 60 * 24)) + 1;
1860
+ return /* @__PURE__ */ jsxs("span", {
1861
+ className: "ml-2 text-xs bg-secondary text-secondary-foreground py-0.5 px-2 rounded-full",
1862
+ children: [
1863
+ days,
1864
+ " ",
1865
+ days === 1 ? "day" : "days"
1866
+ ]
1867
+ });
1868
+ })()]
1869
+ }) : /* @__PURE__ */ jsx("span", {
1870
+ className: "text-left",
1871
+ children: placeholder
1872
+ })]
1873
+ }) }), /* @__PURE__ */ jsxs(PopoverContent, {
1874
+ className: cn("w-auto p-0", calendarClassName),
1875
+ align: "start",
1876
+ children: [/* @__PURE__ */ jsx(Calendar, {
1877
+ mode: "range",
1878
+ selected: dateRange,
1879
+ onSelect: handleDateRangeSelect,
1880
+ disabled: isDateDisabled,
1881
+ initialFocus: true,
1882
+ ...minDate && { fromDate: minDate },
1883
+ ...maxDate && { toDate: maxDate }
1884
+ }), /* @__PURE__ */ jsx("div", {
1885
+ className: "p-3 border-t border-border",
1886
+ children: /* @__PURE__ */ jsx(Button, {
1887
+ variant: "ghost",
1888
+ size: "sm",
1889
+ onClick: handleClear,
1890
+ disabled: !hasValue,
1891
+ className: "w-full",
1892
+ type: "button",
1893
+ children: "Clear Date Range"
1894
+ })
1895
+ })]
1896
+ })] }), hasValue && allowClear && !disabled && /* @__PURE__ */ jsx("button", {
1897
+ type: "button",
1898
+ onClick: handleClear,
1899
+ className: "absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-muted transition-colors",
1900
+ "aria-label": "Clear date range",
1901
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-muted-foreground hover:text-foreground" })
1902
+ })]
1903
+ });
1904
+ }
1905
+ const defaultTransform = {
1906
+ input: (value) => value ? {
1907
+ from: value.from ? new Date(value.from) : void 0,
1908
+ to: value.to ? new Date(value.to) : void 0
1909
+ } : {
1910
+ from: void 0,
1911
+ to: void 0
1912
+ },
1913
+ output: (range) => ({
1914
+ from: range?.from ? range.from instanceof Date ? range.from.toISOString() : range.from : void 0,
1915
+ to: range?.to ? range.to instanceof Date ? range.to.toISOString() : range.to : void 0
1916
+ })
1917
+ };
1918
+ /**
1919
+ * DateRangeInput - Date range picker with react-hook-form integration
1920
+ *
1921
+ * @example
1922
+ * ```tsx
1923
+ * <DateRangeInput
1924
+ * control={control}
1925
+ * name="dateRange"
1926
+ * label="Select Dates"
1927
+ * placeholder="Pick a date range"
1928
+ * />
1929
+ * ```
1930
+ */
1931
+ function DateRangeInput({ control, name, label, description, placeholder = "Select date range", required, disabled, className, labelClassName, buttonClassName, calendarClassName, minDate, maxDate, disabledDates, disabledDays, onValueChange, validateDateRange, clearErrors, descriptionComponent, allowClear = true, showBadge = false, Icon = CalendarIcon, transform = defaultTransform }) {
1932
+ const [localDateRange, setLocalDateRange] = useState({
1933
+ from: void 0,
1934
+ to: void 0
1935
+ });
1936
+ const formatDateRange = (range) => {
1937
+ if (!range || !range.from && !range.to) return "";
1938
+ const fromDate = range.from instanceof Date ? range.from : range.from ? new Date(range.from) : null;
1939
+ const toDate = range.to instanceof Date ? range.to : range.to ? new Date(range.to) : null;
1940
+ const fromFormatted = fromDate && isValid(fromDate) ? format(fromDate, "MMM d") : "";
1941
+ const toFormatted = toDate && isValid(toDate) ? format(toDate, "MMM d") : "";
1942
+ if (fromFormatted && toFormatted) return `${fromFormatted} - ${toFormatted}`;
1943
+ else if (fromFormatted) return `From ${fromFormatted}`;
1944
+ else if (toFormatted) return `Until ${toFormatted}`;
1945
+ return "";
1946
+ };
1947
+ const isDateDisabled = (date) => {
1948
+ if (!date || !isValid(date)) return true;
1949
+ if (disabled) return true;
1950
+ if (minDate && date < minDate) return true;
1951
+ if (maxDate && date > maxDate) return true;
1952
+ if (disabledDates?.some((d) => d.getTime() === date.getTime())) return true;
1953
+ if (disabledDays?.includes(date.getDay())) return true;
1954
+ return false;
1955
+ };
1956
+ const handleDateRangeSelect = (range, field) => {
1957
+ if (disabled) return;
1958
+ const safeRange = range || {
1959
+ from: void 0,
1960
+ to: void 0
1961
+ };
1962
+ if (validateDateRange && !validateDateRange(safeRange)) return;
1963
+ if (field) {
1964
+ const transformedValue = transform.output(safeRange);
1965
+ field.onChange(transformedValue);
1966
+ } else setLocalDateRange(safeRange);
1967
+ onValueChange?.(safeRange);
1968
+ if (clearErrors && name) clearErrors(name);
1969
+ };
1970
+ const handleClear = (field, e) => {
1971
+ e.stopPropagation();
1972
+ const emptyRange = {
1973
+ from: void 0,
1974
+ to: void 0
1975
+ };
1976
+ if (field) field.onChange(emptyRange);
1977
+ else setLocalDateRange(emptyRange);
1978
+ onValueChange?.(emptyRange);
1979
+ if (clearErrors && name) clearErrors(name);
1980
+ };
1981
+ if (control) return /* @__PURE__ */ jsx(Controller, {
1982
+ control,
1983
+ name,
1984
+ render: ({ field, fieldState }) => {
1985
+ const dateRange = transform.input(field.value);
1986
+ return /* @__PURE__ */ jsxs(Field, {
1987
+ className: cn("w-full", className),
1988
+ "data-invalid": !!fieldState.error,
1989
+ children: [
1990
+ label && /* @__PURE__ */ jsxs("div", {
1991
+ className: "flex items-center gap-2 mb-2",
1992
+ children: [/* @__PURE__ */ jsxs(FieldLabel, {
1993
+ className: cn("grow", labelClassName),
1994
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1995
+ className: "text-destructive ml-1",
1996
+ "aria-hidden": "true",
1997
+ children: "*"
1998
+ }), /* @__PURE__ */ jsx("span", {
1999
+ className: "sr-only",
2000
+ children: "(required)"
2001
+ })] })]
2002
+ }), /* @__PURE__ */ jsx("span", {
2003
+ className: "text-muted-foreground shrink-0",
2004
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
2005
+ })]
2006
+ }),
2007
+ /* @__PURE__ */ jsx(DateRangePopover, {
2008
+ dateRange,
2009
+ placeholder,
2010
+ disabled,
2011
+ buttonClassName,
2012
+ calendarClassName,
2013
+ allowClear,
2014
+ showBadge,
2015
+ Icon,
2016
+ formatDateRange,
2017
+ isDateDisabled,
2018
+ handleDateRangeSelect: (range) => handleDateRangeSelect(range, field),
2019
+ handleClear: (e) => handleClear(field, e),
2020
+ minDate,
2021
+ maxDate,
2022
+ ariaDescribedBy: [description ? `${name}-description` : void 0, fieldState.error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0,
2023
+ ariaInvalid: !!fieldState.error
2024
+ }),
2025
+ description && !descriptionComponent && /* @__PURE__ */ jsx(FieldDescription, {
2026
+ id: `${name}-description`,
2027
+ children: description
2028
+ }),
2029
+ descriptionComponent && /* @__PURE__ */ jsx("div", {
2030
+ className: "text-sm text-muted-foreground",
2031
+ children: descriptionComponent
2032
+ }),
2033
+ /* @__PURE__ */ jsx(FieldError$1, {
2034
+ id: `${name}-error`,
2035
+ errors: fieldState.error ? [fieldState.error] : void 0
2036
+ })
2037
+ ]
2038
+ });
2039
+ }
2040
+ });
2041
+ return /* @__PURE__ */ jsxs("div", {
2042
+ className: cn("w-full", className),
2043
+ children: [
2044
+ label && /* @__PURE__ */ jsxs("div", {
2045
+ className: "flex items-center gap-2 mb-2",
2046
+ children: [/* @__PURE__ */ jsxs("div", {
2047
+ className: cn("text-sm font-medium grow", labelClassName),
2048
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2049
+ className: "text-destructive ml-1",
2050
+ "aria-hidden": "true",
2051
+ children: "*"
2052
+ }), /* @__PURE__ */ jsx("span", {
2053
+ className: "sr-only",
2054
+ children: "(required)"
2055
+ })] })]
2056
+ }), /* @__PURE__ */ jsx("span", {
2057
+ className: "text-muted-foreground shrink-0",
2058
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
2059
+ })]
2060
+ }),
2061
+ /* @__PURE__ */ jsx(DateRangePopover, {
2062
+ dateRange: localDateRange,
2063
+ placeholder,
2064
+ disabled,
2065
+ buttonClassName,
2066
+ calendarClassName,
2067
+ allowClear,
2068
+ showBadge,
2069
+ Icon,
2070
+ formatDateRange,
2071
+ isDateDisabled,
2072
+ handleDateRangeSelect: (range) => handleDateRangeSelect(range),
2073
+ handleClear: (e) => handleClear(null, e),
2074
+ minDate,
2075
+ maxDate
2076
+ }),
2077
+ description && /* @__PURE__ */ jsx("div", {
2078
+ className: "text-sm text-muted-foreground mt-1",
2079
+ children: description
2080
+ }),
2081
+ descriptionComponent && /* @__PURE__ */ jsx("div", {
2082
+ className: "text-sm text-muted-foreground",
2083
+ children: descriptionComponent
2084
+ })
2085
+ ]
2086
+ });
2087
+ }
2088
+
2089
+ //#endregion
2090
+ //#region src/components/form/otp-input.tsx
2091
+ const DEFAULT_SEPARATORS = {
2092
+ 4: [1],
2093
+ 6: [2],
2094
+ 8: [3]
2095
+ };
2096
+ function buildGroups(length, separatorAfter) {
2097
+ const separatorSet = new Set(separatorAfter);
2098
+ const groups = [];
2099
+ let current = [];
2100
+ for (let i = 0; i < length; i++) {
2101
+ current.push(i);
2102
+ if (separatorSet.has(i) && i < length - 1) {
2103
+ groups.push(current);
2104
+ current = [];
2105
+ }
2106
+ }
2107
+ if (current.length > 0) groups.push(current);
2108
+ return groups;
2109
+ }
2110
+ /**
2111
+ * OTPInput - OTP/PIN code input with react-hook-form integration
2112
+ *
2113
+ * @example
2114
+ * ```tsx
2115
+ * <OTPInput
2116
+ * control={form.control}
2117
+ * name="otp"
2118
+ * label="Verification Code"
2119
+ * length={6}
2120
+ * autoSubmit
2121
+ * onComplete={(code) => verify(code)}
2122
+ * />
2123
+ * ```
2124
+ */
2125
+ function OTPInput({ control, name, label, description, required, disabled, length = 6, separatorAfter, pattern, autoSubmit, value: propValue, onChange: propOnChange, onComplete, className, slotClassName }) {
2126
+ const groups = buildGroups(length, separatorAfter ?? DEFAULT_SEPARATORS[length] ?? []);
2127
+ const handleChange = useCallback((newValue, field) => {
2128
+ if (field) field.onChange(newValue);
2129
+ else propOnChange?.(newValue);
2130
+ if (autoSubmit && newValue.length === length) onComplete?.(newValue);
2131
+ }, [
2132
+ propOnChange,
2133
+ onComplete,
2134
+ autoSubmit,
2135
+ length
2136
+ ]);
2137
+ const renderOTP = (currentValue, field, isDisabled, fieldState) => {
2138
+ const ariaDescribedBy = [description ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
2139
+ return /* @__PURE__ */ jsx(InputOTP, {
2140
+ maxLength: length,
2141
+ value: currentValue ?? "",
2142
+ onChange: (v) => handleChange(v, field),
2143
+ disabled: isDisabled,
2144
+ pattern,
2145
+ "aria-invalid": fieldState?.invalid || void 0,
2146
+ "aria-describedby": ariaDescribedBy,
2147
+ children: groups.map((group, gi) => /* @__PURE__ */ jsxs("span", {
2148
+ className: "flex items-center gap-1",
2149
+ children: [gi > 0 && /* @__PURE__ */ jsx(InputOTPSeparator, {}), /* @__PURE__ */ jsx(InputOTPGroup, { children: group.map((idx) => /* @__PURE__ */ jsx(InputOTPSlot, {
2150
+ index: idx,
2151
+ className: slotClassName
2152
+ }, idx)) })]
2153
+ }, gi))
2154
+ });
2155
+ };
2156
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
2157
+ className,
2158
+ "data-disabled": disabled,
2159
+ children: [
2160
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2161
+ htmlFor: name,
2162
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2163
+ className: "text-destructive ml-1",
2164
+ "aria-hidden": "true",
2165
+ children: "*"
2166
+ }), /* @__PURE__ */ jsx("span", {
2167
+ className: "sr-only",
2168
+ children: "(required)"
2169
+ })] })]
2170
+ }),
2171
+ renderOTP(propValue, void 0, disabled),
2172
+ description && /* @__PURE__ */ jsx(FieldDescription, {
2173
+ id: `${name}-description`,
2174
+ children: description
2175
+ })
2176
+ ]
2177
+ });
2178
+ return /* @__PURE__ */ jsx(Controller, {
2179
+ name,
2180
+ control,
2181
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
2182
+ className,
2183
+ "data-disabled": disabled,
2184
+ "data-invalid": fieldState.invalid,
2185
+ children: [
2186
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2187
+ htmlFor: name,
2188
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2189
+ className: "text-destructive ml-1",
2190
+ "aria-hidden": "true",
2191
+ children: "*"
2192
+ }), /* @__PURE__ */ jsx("span", {
2193
+ className: "sr-only",
2194
+ children: "(required)"
2195
+ })] })]
2196
+ }),
2197
+ renderOTP(field.value, field, disabled, fieldState),
2198
+ description && /* @__PURE__ */ jsx(FieldDescription, {
2199
+ id: `${name}-description`,
2200
+ children: description
2201
+ }),
2202
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
2203
+ id: `${name}-error`,
2204
+ errors: [fieldState.error]
2205
+ })
2206
+ ]
2207
+ })
2208
+ });
2209
+ }
2210
+
2211
+ //#endregion
2212
+ //#region src/components/form/multi-select.tsx
2213
+ /**
2214
+ * MultiSelect - Chips-based multi-select with Base UI Combobox
2215
+ *
2216
+ * @example
2217
+ * ```tsx
2218
+ * <MultiSelect
2219
+ * control={form.control}
2220
+ * name="tags"
2221
+ * label="Tags"
2222
+ * items={[
2223
+ * { value: "react", label: "React" },
2224
+ * { value: "vue", label: "Vue" },
2225
+ * ]}
2226
+ * maxSelections={5}
2227
+ * />
2228
+ * ```
2229
+ */
2230
+ function MultiSelect({ control, name, label, placeholder = "Select...", emptyText = "No items found.", description, helperText, required, disabled, items = [], groups, maxSelections, clearable = true, className, labelClassName, inputClassName, renderOption, value: propValue, onChange: propOnChange, onValueChange }) {
2231
+ const descriptionText = description || helperText;
2232
+ const allItems = useMemo(() => {
2233
+ if (groups) return groups.flatMap((g) => g.items);
2234
+ return items;
2235
+ }, [items, groups]);
2236
+ const lookup = useMemo(() => {
2237
+ const map = /* @__PURE__ */ new Map();
2238
+ for (const item of allItems) map.set(item.value, item);
2239
+ return map;
2240
+ }, [allItems]);
2241
+ const handleValueChange = useCallback((newItems, field) => {
2242
+ const values = newItems.map((i) => i.value);
2243
+ if (field) field.onChange(values);
2244
+ else propOnChange?.(values);
2245
+ onValueChange?.(values);
2246
+ }, [propOnChange, onValueChange]);
2247
+ const renderMultiSelect = (currentValue, field, isDisabled, error) => {
2248
+ const selectedValues = currentValue ?? [];
2249
+ const selectedItems = selectedValues.map((v) => lookup.get(v)).filter(Boolean);
2250
+ const atMax = maxSelections != null && selectedValues.length >= maxSelections;
2251
+ return /* @__PURE__ */ jsxs(Combobox, {
2252
+ multiple: true,
2253
+ items: allItems,
2254
+ value: selectedItems,
2255
+ onValueChange: (items) => handleValueChange(items, field),
2256
+ disabled: isDisabled,
2257
+ children: [/* @__PURE__ */ jsx(MultiSelectChips, {
2258
+ selectedItems,
2259
+ placeholder,
2260
+ inputClassName,
2261
+ disabled: isDisabled,
2262
+ atMax,
2263
+ clearable,
2264
+ onClear: () => handleValueChange([], field),
2265
+ error,
2266
+ ariaDescribedBy: [descriptionText ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0
2267
+ }), /* @__PURE__ */ jsx(MultiSelectDropdown, {
2268
+ groups,
2269
+ items,
2270
+ emptyText,
2271
+ renderOption,
2272
+ selectedValues,
2273
+ atMax
2274
+ })]
2275
+ });
2276
+ };
2277
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
2278
+ className,
2279
+ "data-disabled": disabled,
2280
+ children: [
2281
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2282
+ htmlFor: name,
2283
+ className: labelClassName,
2284
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2285
+ className: "text-destructive ml-1",
2286
+ "aria-hidden": "true",
2287
+ children: "*"
2288
+ }), /* @__PURE__ */ jsx("span", {
2289
+ className: "sr-only",
2290
+ children: "(required)"
2291
+ })] })]
2292
+ }),
2293
+ renderMultiSelect(propValue, void 0, disabled),
2294
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2295
+ id: `${name}-description`,
2296
+ children: descriptionText
2297
+ })
2298
+ ]
2299
+ });
2300
+ return /* @__PURE__ */ jsx(Controller, {
2301
+ name,
2302
+ control,
2303
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
2304
+ className,
2305
+ "data-disabled": disabled,
2306
+ "data-invalid": fieldState.invalid,
2307
+ children: [
2308
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2309
+ htmlFor: name,
2310
+ className: labelClassName,
2311
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2312
+ className: "text-destructive ml-1",
2313
+ "aria-hidden": "true",
2314
+ children: "*"
2315
+ }), /* @__PURE__ */ jsx("span", {
2316
+ className: "sr-only",
2317
+ children: "(required)"
2318
+ })] })]
2319
+ }),
2320
+ renderMultiSelect(field.value, field, disabled, fieldState.invalid),
2321
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2322
+ id: `${name}-description`,
2323
+ children: descriptionText
2324
+ }),
2325
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
2326
+ id: `${name}-error`,
2327
+ errors: [fieldState.error]
2328
+ })
2329
+ ]
2330
+ })
2331
+ });
2332
+ }
2333
+ function MultiSelectChips({ selectedItems, placeholder, inputClassName, disabled, atMax, clearable, onClear, error, ariaDescribedBy }) {
2334
+ return /* @__PURE__ */ jsxs("div", {
2335
+ className: "relative",
2336
+ children: [/* @__PURE__ */ jsxs(ComboboxChips, {
2337
+ ref: useComboboxAnchor(),
2338
+ "aria-describedby": ariaDescribedBy,
2339
+ className: cn("min-h-[2.5rem] flex-wrap", error && "border-destructive", inputClassName),
2340
+ children: [selectedItems.map((item) => /* @__PURE__ */ jsx(ComboboxChip, {
2341
+ value: item,
2342
+ showRemove: true,
2343
+ children: item.label
2344
+ }, item.value)), /* @__PURE__ */ jsx(ComboboxChipsInput, {
2345
+ placeholder: selectedItems.length === 0 ? placeholder : "",
2346
+ disabled: disabled || atMax
2347
+ })]
2348
+ }), clearable && selectedItems.length > 0 && !disabled && /* @__PURE__ */ jsx("button", {
2349
+ type: "button",
2350
+ onClick: onClear,
2351
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground/60 hover:text-foreground transition-colors",
2352
+ "aria-label": "Clear all",
2353
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2354
+ })]
2355
+ });
2356
+ }
2357
+ function MultiSelectDropdown({ groups, items, emptyText, renderOption, selectedValues, atMax }) {
2358
+ return /* @__PURE__ */ jsxs(ComboboxContent, { children: [/* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyText }), /* @__PURE__ */ jsx(ComboboxList, { children: groups ? groups.map((group) => /* @__PURE__ */ jsxs(ComboboxGroup, { children: [/* @__PURE__ */ jsx(ComboboxLabel, { children: group.label }), group.items.map((item) => /* @__PURE__ */ jsx(ComboboxItem, {
2359
+ value: item,
2360
+ disabled: item.disabled || atMax && !selectedValues.includes(item.value),
2361
+ children: renderOption ? renderOption(item) : item.label
2362
+ }, item.value))] }, group.label)) : (item) => {
2363
+ if (!item) return null;
2364
+ return /* @__PURE__ */ jsx(ComboboxItem, {
2365
+ value: item,
2366
+ disabled: item.disabled || atMax && !selectedValues.includes(item.value),
2367
+ children: renderOption ? renderOption(item) : item.label
2368
+ }, item.value);
2369
+ } })] });
2370
+ }
2371
+
2372
+ //#endregion
2373
+ //#region src/components/form/async-combobox.tsx
2374
+ const noFilter$1 = (items) => items;
2375
+ /**
2376
+ * AsyncCombobox - Searchable select with debounced async search
2377
+ *
2378
+ * @example
2379
+ * ```tsx
2380
+ * <AsyncCombobox
2381
+ * control={form.control}
2382
+ * name="city"
2383
+ * label="City"
2384
+ * onSearch={async (q) => {
2385
+ * const res = await fetch(`/api/cities?search=${q}`);
2386
+ * return res.json();
2387
+ * }}
2388
+ * placeholder="Search cities..."
2389
+ * />
2390
+ * ```
2391
+ */
2392
+ function AsyncCombobox({ control, name, label, placeholder = "Search...", emptyText = "No results found.", description, helperText, required, disabled, onSearch, debounceMs = 300, minChars = 1, initialItems = [], isLoading: externalLoading, hasMore, onLoadMore, className, labelClassName, inputClassName, renderOption, value: propValue, onChange: propOnChange, onValueChange }) {
2393
+ const descriptionText = description || helperText;
2394
+ const [items, setItems] = useState(initialItems);
2395
+ const [internalLoading, setInternalLoading] = useState(false);
2396
+ const resolvedMapRef = useRef(/* @__PURE__ */ new Map());
2397
+ const requestIdRef = useRef(0);
2398
+ useEffect(() => {
2399
+ for (const item of initialItems) resolvedMapRef.current.set(item.value, item);
2400
+ }, [initialItems]);
2401
+ const loading = internalLoading || externalLoading;
2402
+ const debouncedSearch = useDebouncedCallback(async (query) => {
2403
+ if (query.length < minChars) {
2404
+ setItems(initialItems);
2405
+ setInternalLoading(false);
2406
+ return;
2407
+ }
2408
+ const id = ++requestIdRef.current;
2409
+ try {
2410
+ const results = await onSearch(query);
2411
+ if (id !== requestIdRef.current) return;
2412
+ for (const item of results) resolvedMapRef.current.set(item.value, item);
2413
+ setItems(results);
2414
+ } catch {
2415
+ if (id !== requestIdRef.current) return;
2416
+ setItems([]);
2417
+ } finally {
2418
+ if (id === requestIdRef.current) setInternalLoading(false);
2419
+ }
2420
+ }, debounceMs);
2421
+ const handleInputChange = useCallback((e) => {
2422
+ const query = e.target.value;
2423
+ if (query.length >= minChars) {
2424
+ setInternalLoading(true);
2425
+ debouncedSearch(query);
2426
+ } else setItems(initialItems);
2427
+ }, [
2428
+ minChars,
2429
+ debouncedSearch,
2430
+ initialItems
2431
+ ]);
2432
+ const handleValueChange = useCallback((newItem, field) => {
2433
+ const val = newItem?.value ?? "";
2434
+ if (field) field.onChange(val);
2435
+ else propOnChange?.(val);
2436
+ onValueChange?.(val);
2437
+ }, [propOnChange, onValueChange]);
2438
+ const renderCombobox = (currentValue, field, isDisabled, fieldState) => {
2439
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
2440
+ return /* @__PURE__ */ jsxs(Combobox, {
2441
+ items,
2442
+ value: currentValue ? resolvedMapRef.current.get(currentValue) ?? items.find((i) => i.value === currentValue) ?? null : null,
2443
+ onValueChange: (item) => handleValueChange(item, field),
2444
+ disabled: isDisabled,
2445
+ filter: noFilter$1,
2446
+ children: [/* @__PURE__ */ jsx(ComboboxInput$1, {
2447
+ placeholder,
2448
+ className: cn(inputClassName),
2449
+ onInput: handleInputChange,
2450
+ "aria-describedby": ariaDescribedBy,
2451
+ "aria-invalid": fieldState?.invalid || void 0
2452
+ }), /* @__PURE__ */ jsx(ComboboxContent, { children: loading ? /* @__PURE__ */ jsx("div", {
2453
+ className: "flex items-center justify-center py-6",
2454
+ children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })
2455
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2456
+ /* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyText }),
2457
+ /* @__PURE__ */ jsx(ComboboxList, { children: (item) => {
2458
+ if (!item) return null;
2459
+ return /* @__PURE__ */ jsx(ComboboxItem, {
2460
+ value: item,
2461
+ disabled: item.disabled,
2462
+ children: renderOption ? renderOption(item) : item.label
2463
+ }, item.value);
2464
+ } }),
2465
+ hasMore && onLoadMore && /* @__PURE__ */ jsx("button", {
2466
+ type: "button",
2467
+ onClick: onLoadMore,
2468
+ className: "w-full py-2 text-center text-sm text-primary hover:bg-accent transition-colors",
2469
+ children: "Load more..."
2470
+ })
2471
+ ] }) })]
2472
+ });
2473
+ };
2474
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
2475
+ className,
2476
+ "data-disabled": disabled,
2477
+ children: [
2478
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2479
+ htmlFor: name,
2480
+ className: labelClassName,
2481
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2482
+ className: "text-destructive ml-1",
2483
+ "aria-hidden": "true",
2484
+ children: "*"
2485
+ }), /* @__PURE__ */ jsx("span", {
2486
+ className: "sr-only",
2487
+ children: "(required)"
2488
+ })] })]
2489
+ }),
2490
+ renderCombobox(propValue, void 0, disabled),
2491
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2492
+ id: `${name}-description`,
2493
+ children: descriptionText
2494
+ })
2495
+ ]
2496
+ });
2497
+ return /* @__PURE__ */ jsx(Controller, {
2498
+ name,
2499
+ control,
2500
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
2501
+ className,
2502
+ "data-disabled": disabled,
2503
+ "data-invalid": fieldState.invalid,
2504
+ children: [
2505
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2506
+ htmlFor: name,
2507
+ className: labelClassName,
2508
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2509
+ className: "text-destructive ml-1",
2510
+ "aria-hidden": "true",
2511
+ children: "*"
2512
+ }), /* @__PURE__ */ jsx("span", {
2513
+ className: "sr-only",
2514
+ children: "(required)"
2515
+ })] })]
2516
+ }),
2517
+ renderCombobox(field.value, field, disabled, fieldState),
2518
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2519
+ id: `${name}-description`,
2520
+ children: descriptionText
2521
+ }),
2522
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
2523
+ id: `${name}-error`,
2524
+ errors: [fieldState.error]
2525
+ })
2526
+ ]
2527
+ })
2528
+ });
2529
+ }
2530
+
2531
+ //#endregion
2532
+ //#region src/components/form/async-multi-select.tsx
2533
+ const noFilter = (items) => items;
2534
+ /**
2535
+ * AsyncMultiSelect - Async search + chips-based multi-select
2536
+ *
2537
+ * @example
2538
+ * ```tsx
2539
+ * <AsyncMultiSelect
2540
+ * control={form.control}
2541
+ * name="teamMembers"
2542
+ * label="Team Members"
2543
+ * onSearch={async (q) => {
2544
+ * const res = await fetch(`/api/users?search=${q}`);
2545
+ * const users = await res.json();
2546
+ * return users.map(u => ({ value: u.id, label: u.name }));
2547
+ * }}
2548
+ * maxSelections={5}
2549
+ * />
2550
+ * ```
2551
+ */
2552
+ function AsyncMultiSelect({ control, name, label, placeholder = "Search...", emptyText = "No results found.", description, helperText, required, disabled, onSearch, debounceMs = 300, minChars = 1, initialItems = [], isLoading: externalLoading, maxSelections, clearable = true, className, labelClassName, inputClassName, renderOption, value: propValue, onChange: propOnChange, onValueChange }) {
2553
+ const descriptionText = description || helperText;
2554
+ const [items, setItems] = useState(initialItems);
2555
+ const [internalLoading, setInternalLoading] = useState(false);
2556
+ const resolvedMapRef = useRef(/* @__PURE__ */ new Map());
2557
+ const requestIdRef = useRef(0);
2558
+ useEffect(() => {
2559
+ for (const item of initialItems) resolvedMapRef.current.set(item.value, item);
2560
+ }, [initialItems]);
2561
+ const loading = internalLoading || externalLoading;
2562
+ const debouncedSearch = useDebouncedCallback(async (query) => {
2563
+ if (query.length < minChars) {
2564
+ setItems(initialItems);
2565
+ setInternalLoading(false);
2566
+ return;
2567
+ }
2568
+ const id = ++requestIdRef.current;
2569
+ try {
2570
+ const results = await onSearch(query);
2571
+ if (id !== requestIdRef.current) return;
2572
+ for (const item of results) resolvedMapRef.current.set(item.value, item);
2573
+ setItems(results);
2574
+ } catch {
2575
+ if (id !== requestIdRef.current) return;
2576
+ setItems([]);
2577
+ } finally {
2578
+ if (id === requestIdRef.current) setInternalLoading(false);
2579
+ }
2580
+ }, debounceMs);
2581
+ const handleInputChange = useCallback((e) => {
2582
+ const query = e.target.value;
2583
+ if (query.length >= minChars) {
2584
+ setInternalLoading(true);
2585
+ debouncedSearch(query);
2586
+ } else setItems(initialItems);
2587
+ }, [
2588
+ minChars,
2589
+ debouncedSearch,
2590
+ initialItems
2591
+ ]);
2592
+ const handleValueChange = useCallback((newItems, field) => {
2593
+ const values = newItems.map((i) => i.value);
2594
+ if (field) field.onChange(values);
2595
+ else propOnChange?.(values);
2596
+ onValueChange?.(values);
2597
+ }, [propOnChange, onValueChange]);
2598
+ const renderMultiSelect = (currentValue, field, isDisabled, error) => {
2599
+ const selectedValues = currentValue ?? [];
2600
+ const selectedItems = selectedValues.map((v) => resolvedMapRef.current.get(v)).filter(Boolean);
2601
+ const atMax = maxSelections != null && selectedValues.length >= maxSelections;
2602
+ return /* @__PURE__ */ jsxs(Combobox, {
2603
+ multiple: true,
2604
+ items,
2605
+ value: selectedItems,
2606
+ onValueChange: (items) => handleValueChange(items, field),
2607
+ disabled: isDisabled,
2608
+ filter: noFilter,
2609
+ children: [/* @__PURE__ */ jsx(AsyncChipsArea, {
2610
+ selectedItems,
2611
+ placeholder,
2612
+ inputClassName,
2613
+ disabled: isDisabled,
2614
+ atMax,
2615
+ clearable,
2616
+ onClear: () => handleValueChange([], field),
2617
+ onInputChange: handleInputChange,
2618
+ error,
2619
+ ariaDescribedBy: [descriptionText ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0
2620
+ }), /* @__PURE__ */ jsx(ComboboxContent, { children: loading ? /* @__PURE__ */ jsx("div", {
2621
+ className: "flex items-center justify-center py-6",
2622
+ children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })
2623
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyText }), /* @__PURE__ */ jsx(ComboboxList, { children: (item) => {
2624
+ if (!item) return null;
2625
+ return /* @__PURE__ */ jsx(ComboboxItem, {
2626
+ value: item,
2627
+ disabled: item.disabled || atMax && !selectedValues.includes(item.value),
2628
+ children: renderOption ? renderOption(item) : item.label
2629
+ }, item.value);
2630
+ } })] }) })]
2631
+ });
2632
+ };
2633
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
2634
+ className,
2635
+ "data-disabled": disabled,
2636
+ children: [
2637
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2638
+ htmlFor: name,
2639
+ className: labelClassName,
2640
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2641
+ className: "text-destructive ml-1",
2642
+ "aria-hidden": "true",
2643
+ children: "*"
2644
+ }), /* @__PURE__ */ jsx("span", {
2645
+ className: "sr-only",
2646
+ children: "(required)"
2647
+ })] })]
2648
+ }),
2649
+ renderMultiSelect(propValue, void 0, disabled),
2650
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2651
+ id: `${name}-description`,
2652
+ children: descriptionText
2653
+ })
2654
+ ]
2655
+ });
2656
+ return /* @__PURE__ */ jsx(Controller, {
2657
+ name,
2658
+ control,
2659
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
2660
+ className,
2661
+ "data-disabled": disabled,
2662
+ "data-invalid": fieldState.invalid,
2663
+ children: [
2664
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2665
+ htmlFor: name,
2666
+ className: labelClassName,
2667
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2668
+ className: "text-destructive ml-1",
2669
+ "aria-hidden": "true",
2670
+ children: "*"
2671
+ }), /* @__PURE__ */ jsx("span", {
2672
+ className: "sr-only",
2673
+ children: "(required)"
2674
+ })] })]
2675
+ }),
2676
+ renderMultiSelect(field.value, field, disabled, fieldState.invalid),
2677
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2678
+ id: `${name}-description`,
2679
+ children: descriptionText
2680
+ }),
2681
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
2682
+ id: `${name}-error`,
2683
+ errors: [fieldState.error]
2684
+ })
2685
+ ]
2686
+ })
2687
+ });
2688
+ }
2689
+ function AsyncChipsArea({ selectedItems, placeholder, inputClassName, disabled, atMax, clearable, onClear, onInputChange, error, ariaDescribedBy }) {
2690
+ return /* @__PURE__ */ jsxs("div", {
2691
+ className: "relative",
2692
+ children: [/* @__PURE__ */ jsxs(ComboboxChips, {
2693
+ ref: useComboboxAnchor(),
2694
+ "aria-describedby": ariaDescribedBy,
2695
+ className: cn("min-h-[2.5rem] flex-wrap", error && "border-destructive", inputClassName),
2696
+ children: [selectedItems.map((item) => /* @__PURE__ */ jsx(ComboboxChip, {
2697
+ value: item,
2698
+ showRemove: true,
2699
+ children: item.label
2700
+ }, item.value)), /* @__PURE__ */ jsx(ComboboxChipsInput, {
2701
+ placeholder: selectedItems.length === 0 ? placeholder : "",
2702
+ disabled: disabled || atMax,
2703
+ onInput: onInputChange
2704
+ })]
2705
+ }), clearable && selectedItems.length > 0 && !disabled && /* @__PURE__ */ jsx("button", {
2706
+ type: "button",
2707
+ onClick: onClear,
2708
+ className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground/60 hover:text-foreground transition-colors",
2709
+ "aria-label": "Clear all",
2710
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2711
+ })]
2712
+ });
2713
+ }
2714
+
2715
+ //#endregion
2716
+ //#region src/components/form/file-upload-input.tsx
2717
+ function formatFileSize(bytes) {
2718
+ if (bytes === 0) return "0 B";
2719
+ const units = [
2720
+ "B",
2721
+ "KB",
2722
+ "MB",
2723
+ "GB"
2724
+ ];
2725
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
2726
+ return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
2727
+ }
2728
+ function validateFile(file, opts) {
2729
+ if (opts.accept) {
2730
+ if (!opts.accept.split(",").map((t) => t.trim()).some((type) => {
2731
+ if (type.endsWith("/*")) return file.type.startsWith(type.replace("/*", "/"));
2732
+ return file.type === type || file.name.endsWith(type);
2733
+ })) return `File type ${file.type || "unknown"} not accepted`;
2734
+ }
2735
+ if (opts.maxFileSize && file.size > opts.maxFileSize) return `File too large (max ${formatFileSize(opts.maxFileSize)})`;
2736
+ if (opts.minFileSize && file.size < opts.minFileSize) return `File too small (min ${formatFileSize(opts.minFileSize)})`;
2737
+ return null;
2738
+ }
2739
+ function getFileIcon(file) {
2740
+ if (file.type.startsWith("image/")) return FileImage;
2741
+ if (file.type.startsWith("video/")) return FileVideo;
2742
+ return FileIcon;
2743
+ }
2744
+ /**
2745
+ * FileUploadInput - Drag-and-drop file input with validation and preview
2746
+ *
2747
+ * @example
2748
+ * ```tsx
2749
+ * <FileUploadInput
2750
+ * control={form.control}
2751
+ * name="avatar"
2752
+ * label="Profile Picture"
2753
+ * accept="image/*"
2754
+ * maxFileSize={5 * 1024 * 1024}
2755
+ * onFilesAdded={(files) => startUpload(files)}
2756
+ * uploadProgress={uploadProgress}
2757
+ * />
2758
+ * ```
2759
+ */
2760
+ function FileUploadInput({ control, name, label, description, required, disabled, multiple = false, accept, maxFiles, maxFileSize, minFileSize, uploadProgress, uploadErrors, dropzoneText, dropzoneHint, showPreview = true, compact = false, onFilesAdded, onFileRemoved, className, dropzoneClassName, value: propValue, onChange: propOnChange, onValueChange }) {
2761
+ const descriptionText = description;
2762
+ const renderUpload = (currentFiles, fieldOnChange, isDisabled, error) => {
2763
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
2764
+ return /* @__PURE__ */ jsx(FileUploadInternal, {
2765
+ name,
2766
+ disabled: isDisabled,
2767
+ multiple,
2768
+ accept,
2769
+ maxFiles,
2770
+ maxFileSize,
2771
+ minFileSize,
2772
+ uploadProgress,
2773
+ uploadErrors,
2774
+ dropzoneText,
2775
+ dropzoneHint,
2776
+ showPreview,
2777
+ compact,
2778
+ onFilesAdded,
2779
+ onFileRemoved,
2780
+ dropzoneClassName,
2781
+ files: currentFiles ?? [],
2782
+ onChange: (files) => {
2783
+ if (fieldOnChange) fieldOnChange(files);
2784
+ else propOnChange?.(files);
2785
+ onValueChange?.(files);
2786
+ },
2787
+ error,
2788
+ ariaDescribedBy
2789
+ });
2790
+ };
2791
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
2792
+ className,
2793
+ "data-disabled": disabled,
2794
+ children: [
2795
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2796
+ htmlFor: name,
2797
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2798
+ className: "text-destructive ml-1",
2799
+ "aria-hidden": "true",
2800
+ children: "*"
2801
+ }), /* @__PURE__ */ jsx("span", {
2802
+ className: "sr-only",
2803
+ children: "(required)"
2804
+ })] })]
2805
+ }),
2806
+ renderUpload(propValue, void 0, disabled),
2807
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2808
+ id: `${name}-description`,
2809
+ children: descriptionText
2810
+ })
2811
+ ]
2812
+ });
2813
+ return /* @__PURE__ */ jsx(Controller, {
2814
+ name,
2815
+ control,
2816
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
2817
+ className,
2818
+ "data-disabled": disabled,
2819
+ "data-invalid": fieldState.invalid,
2820
+ children: [
2821
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
2822
+ htmlFor: name,
2823
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2824
+ className: "text-destructive ml-1",
2825
+ "aria-hidden": "true",
2826
+ children: "*"
2827
+ }), /* @__PURE__ */ jsx("span", {
2828
+ className: "sr-only",
2829
+ children: "(required)"
2830
+ })] })]
2831
+ }),
2832
+ renderUpload(field.value, field.onChange, disabled, fieldState.invalid),
2833
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
2834
+ id: `${name}-description`,
2835
+ children: descriptionText
2836
+ }),
2837
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
2838
+ id: `${name}-error`,
2839
+ errors: [fieldState.error]
2840
+ })
2841
+ ]
2842
+ })
2843
+ });
2844
+ }
2845
+ function FileUploadInternal({ name, disabled, multiple, accept, maxFiles, maxFileSize, minFileSize, uploadProgress, uploadErrors, dropzoneText, dropzoneHint, showPreview, compact, onFilesAdded, onFileRemoved, dropzoneClassName, files, onChange, error, ariaDescribedBy }) {
2846
+ const [isDragOver, setIsDragOver] = useState(false);
2847
+ const inputRef = useRef(null);
2848
+ const previewUrlsRef = useRef(/* @__PURE__ */ new Map());
2849
+ useEffect(() => {
2850
+ return () => {
2851
+ for (const url of previewUrlsRef.current.values()) URL.revokeObjectURL(url);
2852
+ };
2853
+ }, []);
2854
+ const getPreviewUrl = (file) => {
2855
+ const key = `${file.name}-${file.size}-${file.lastModified}`;
2856
+ if (!previewUrlsRef.current.has(key)) previewUrlsRef.current.set(key, URL.createObjectURL(file));
2857
+ return previewUrlsRef.current.get(key);
2858
+ };
2859
+ const addFiles = useCallback((newFiles) => {
2860
+ const validFiles = newFiles.filter((f) => !validateFile(f, {
2861
+ accept,
2862
+ maxFileSize,
2863
+ minFileSize
2864
+ }));
2865
+ const maxAllowed = maxFiles ? maxFiles - files.length : validFiles.length;
2866
+ const filesToAdd = validFiles.slice(0, Math.max(0, maxAllowed));
2867
+ if (filesToAdd.length > 0) {
2868
+ onChange(multiple ? [...files, ...filesToAdd] : filesToAdd.slice(0, 1));
2869
+ onFilesAdded?.(filesToAdd);
2870
+ }
2871
+ }, [
2872
+ files,
2873
+ accept,
2874
+ maxFileSize,
2875
+ minFileSize,
2876
+ maxFiles,
2877
+ multiple,
2878
+ onChange,
2879
+ onFilesAdded
2880
+ ]);
2881
+ const removeFile = useCallback((index) => {
2882
+ const file = files[index];
2883
+ onChange(files.filter((_, i) => i !== index));
2884
+ if (file) onFileRemoved?.(file, index);
2885
+ }, [
2886
+ files,
2887
+ onChange,
2888
+ onFileRemoved
2889
+ ]);
2890
+ const handleDrop = (e) => {
2891
+ e.preventDefault();
2892
+ setIsDragOver(false);
2893
+ if (disabled) return;
2894
+ addFiles(Array.from(e.dataTransfer.files));
2895
+ };
2896
+ const handleFileInput = (e) => {
2897
+ if (e.target.files) {
2898
+ addFiles(Array.from(e.target.files));
2899
+ e.target.value = "";
2900
+ }
2901
+ };
2902
+ const defaultText = multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse";
2903
+ const defaultHint = [
2904
+ accept && `Accepted: ${accept}`,
2905
+ maxFileSize && `Max: ${formatFileSize(maxFileSize)}`,
2906
+ maxFiles && `Up to ${maxFiles} file${maxFiles > 1 ? "s" : ""}`
2907
+ ].filter(Boolean).join(" · ");
2908
+ return /* @__PURE__ */ jsxs("div", {
2909
+ className: "space-y-3",
2910
+ children: [/* @__PURE__ */ jsxs("div", {
2911
+ role: "button",
2912
+ tabIndex: disabled ? -1 : 0,
2913
+ onClick: () => !disabled && inputRef.current?.click(),
2914
+ onKeyDown: (e) => {
2915
+ if ((e.key === "Enter" || e.key === " ") && !disabled) {
2916
+ e.preventDefault();
2917
+ inputRef.current?.click();
2918
+ }
2919
+ },
2920
+ onDragOver: (e) => {
2921
+ e.preventDefault();
2922
+ if (!disabled) setIsDragOver(true);
2923
+ },
2924
+ onDragLeave: () => setIsDragOver(false),
2925
+ onDrop: handleDrop,
2926
+ className: cn("relative border-2 border-dashed rounded-lg transition-colors cursor-pointer", "hover:border-primary/50 hover:bg-muted/50", isDragOver && "border-primary bg-primary/5", error && "border-destructive", disabled && "opacity-50 cursor-not-allowed", compact ? "px-4 py-3" : "px-6 py-8", dropzoneClassName),
2927
+ children: [/* @__PURE__ */ jsx("input", {
2928
+ ref: inputRef,
2929
+ type: "file",
2930
+ accept,
2931
+ multiple,
2932
+ onChange: handleFileInput,
2933
+ className: "sr-only",
2934
+ disabled,
2935
+ "aria-label": name,
2936
+ "aria-invalid": error || void 0,
2937
+ "aria-describedby": ariaDescribedBy
2938
+ }), compact ? /* @__PURE__ */ jsxs("div", {
2939
+ className: "flex items-center gap-3",
2940
+ children: [/* @__PURE__ */ jsx(Upload, { className: "h-4 w-4 text-muted-foreground shrink-0" }), /* @__PURE__ */ jsx("span", {
2941
+ className: "text-sm text-muted-foreground",
2942
+ children: dropzoneText || defaultText
2943
+ })]
2944
+ }) : /* @__PURE__ */ jsxs("div", {
2945
+ className: "flex flex-col items-center gap-2 text-center",
2946
+ children: [
2947
+ /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8 text-muted-foreground" }),
2948
+ /* @__PURE__ */ jsx("p", {
2949
+ className: "text-sm font-medium",
2950
+ children: dropzoneText || defaultText
2951
+ }),
2952
+ (dropzoneHint || defaultHint) && /* @__PURE__ */ jsx("p", {
2953
+ className: "text-xs text-muted-foreground",
2954
+ children: dropzoneHint || defaultHint
2955
+ })
2956
+ ]
2957
+ })]
2958
+ }), files.length > 0 && /* @__PURE__ */ jsx("div", {
2959
+ className: "space-y-2",
2960
+ children: files.map((file, i) => {
2961
+ const FileTypeIcon = getFileIcon(file);
2962
+ const progress = uploadProgress?.[file.name];
2963
+ const fileError = uploadErrors?.[file.name];
2964
+ const isImage = showPreview && file.type.startsWith("image/");
2965
+ return /* @__PURE__ */ jsxs("div", {
2966
+ className: cn("flex items-center gap-3 rounded-md border p-2.5", fileError && "border-destructive/50 bg-destructive/5"),
2967
+ children: [
2968
+ isImage ? /* @__PURE__ */ jsx("img", {
2969
+ src: getPreviewUrl(file),
2970
+ alt: file.name,
2971
+ className: "h-10 w-10 rounded object-cover shrink-0"
2972
+ }) : /* @__PURE__ */ jsx(FileTypeIcon, { className: "h-8 w-8 text-muted-foreground shrink-0" }),
2973
+ /* @__PURE__ */ jsxs("div", {
2974
+ className: "flex-1 min-w-0",
2975
+ children: [
2976
+ /* @__PURE__ */ jsx("p", {
2977
+ className: "text-sm font-medium truncate",
2978
+ children: file.name
2979
+ }),
2980
+ /* @__PURE__ */ jsx("p", {
2981
+ className: "text-xs text-muted-foreground",
2982
+ children: formatFileSize(file.size)
2983
+ }),
2984
+ progress != null && /* @__PURE__ */ jsx(Progress, {
2985
+ value: progress,
2986
+ className: "mt-1.5 h-1"
2987
+ }),
2988
+ fileError && /* @__PURE__ */ jsx("p", {
2989
+ className: "text-xs text-destructive mt-1",
2990
+ children: fileError
2991
+ })
2992
+ ]
2993
+ }),
2994
+ !disabled && /* @__PURE__ */ jsx(Button, {
2995
+ type: "button",
2996
+ variant: "ghost",
2997
+ size: "icon-sm",
2998
+ className: "shrink-0 text-muted-foreground hover:text-destructive",
2999
+ onClick: (e) => {
3000
+ e.stopPropagation();
3001
+ removeFile(i);
3002
+ },
3003
+ "aria-label": `Remove ${file.name}`,
3004
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
3005
+ })
3006
+ ]
3007
+ }, `${file.name}-${file.size}-${i}`);
3008
+ })
3009
+ })]
3010
+ });
3011
+ }
3012
+
3013
+ //#endregion
3014
+ //#region src/components/form/date-time-input.tsx
3015
+ function generateHourOptions(hourFormat) {
3016
+ const hours = [];
3017
+ if (hourFormat === 24) for (let h = 0; h < 24; h++) hours.push(String(h).padStart(2, "0"));
3018
+ else for (let h = 1; h <= 12; h++) hours.push(String(h).padStart(2, "0"));
3019
+ return hours;
3020
+ }
3021
+ function generateMinuteOptions(interval) {
3022
+ const minutes = [];
3023
+ for (let m = 0; m < 60; m += interval) minutes.push(String(m).padStart(2, "0"));
3024
+ return minutes;
3025
+ }
3026
+ function getHour12(hour24) {
3027
+ const period = hour24 >= 12 ? "PM" : "AM";
3028
+ let hour12 = hour24 % 12;
3029
+ if (hour12 === 0) hour12 = 12;
3030
+ return {
3031
+ hour12,
3032
+ period
3033
+ };
3034
+ }
3035
+ function toHour24(hour12, period) {
3036
+ if (period === "AM") return hour12 === 12 ? 0 : hour12;
3037
+ return hour12 === 12 ? 12 : hour12 + 12;
3038
+ }
3039
+ /**
3040
+ * DateTimeInput - Date + time picker with Popover and Calendar
3041
+ *
3042
+ * @example
3043
+ * ```tsx
3044
+ * <DateTimeInput
3045
+ * control={form.control}
3046
+ * name="scheduledAt"
3047
+ * label="Schedule Date"
3048
+ * mode="datetime"
3049
+ * minDate={new Date()}
3050
+ * timeSlotInterval={15}
3051
+ * />
3052
+ * ```
3053
+ */
3054
+ function DateTimeInput({ control, name, label, description, required, disabled, placeholder, mode = "datetime", minDate, maxDate, timeSlotInterval = 30, hourFormat = 24, clearable = true, className, labelClassName, value: propValue, onChange: propOnChange, onValueChange }) {
3055
+ const descriptionText = description;
3056
+ const defaultPlaceholder = mode === "datetime" ? "Pick a date and time" : "Pick a date";
3057
+ const renderDateTimePicker = (currentValue, fieldOnChange, isDisabled, error) => {
3058
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, error ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
3059
+ return /* @__PURE__ */ jsx(DateTimePickerInternal, {
3060
+ value: currentValue,
3061
+ onChange: (v) => {
3062
+ if (fieldOnChange) fieldOnChange(v);
3063
+ else propOnChange?.(v);
3064
+ onValueChange?.(v);
3065
+ },
3066
+ disabled: isDisabled,
3067
+ placeholder: placeholder || defaultPlaceholder,
3068
+ mode,
3069
+ minDate,
3070
+ maxDate,
3071
+ timeSlotInterval,
3072
+ hourFormat,
3073
+ clearable,
3074
+ error,
3075
+ ariaDescribedBy
3076
+ });
3077
+ };
3078
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
3079
+ className,
3080
+ "data-disabled": disabled,
3081
+ children: [
3082
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
3083
+ htmlFor: name,
3084
+ className: labelClassName,
3085
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3086
+ className: "text-destructive ml-1",
3087
+ "aria-hidden": "true",
3088
+ children: "*"
3089
+ }), /* @__PURE__ */ jsx("span", {
3090
+ className: "sr-only",
3091
+ children: "(required)"
3092
+ })] })]
3093
+ }),
3094
+ renderDateTimePicker(propValue, void 0, disabled),
3095
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
3096
+ id: `${name}-description`,
3097
+ children: descriptionText
3098
+ })
3099
+ ]
3100
+ });
3101
+ return /* @__PURE__ */ jsx(Controller, {
3102
+ name,
3103
+ control,
3104
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
3105
+ className,
3106
+ "data-disabled": disabled,
3107
+ "data-invalid": fieldState.invalid,
3108
+ children: [
3109
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
3110
+ htmlFor: name,
3111
+ className: labelClassName,
3112
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3113
+ className: "text-destructive ml-1",
3114
+ "aria-hidden": "true",
3115
+ children: "*"
3116
+ }), /* @__PURE__ */ jsx("span", {
3117
+ className: "sr-only",
3118
+ children: "(required)"
3119
+ })] })]
3120
+ }),
3121
+ renderDateTimePicker(field.value, field.onChange, disabled, fieldState.invalid),
3122
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
3123
+ id: `${name}-description`,
3124
+ children: descriptionText
3125
+ }),
3126
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
3127
+ id: `${name}-error`,
3128
+ errors: [fieldState.error]
3129
+ })
3130
+ ]
3131
+ })
3132
+ });
3133
+ }
3134
+ function DateTimePickerInternal({ value, onChange, disabled, placeholder, mode, minDate, maxDate, timeSlotInterval, hourFormat, clearable, error, ariaDescribedBy }) {
3135
+ const [open, setOpen] = useState(false);
3136
+ const hourOptions = useMemo(() => generateHourOptions(hourFormat), [hourFormat]);
3137
+ const minuteOptions = useMemo(() => generateMinuteOptions(timeSlotInterval), [timeSlotInterval]);
3138
+ const handleDateSelect = useCallback((date) => {
3139
+ if (!date) {
3140
+ onChange(void 0);
3141
+ if (mode === "date") setOpen(false);
3142
+ return;
3143
+ }
3144
+ if (mode === "datetime" && value) onChange(set(date, {
3145
+ hours: value.getHours(),
3146
+ minutes: value.getMinutes(),
3147
+ seconds: 0,
3148
+ milliseconds: 0
3149
+ }));
3150
+ else if (mode === "datetime") onChange(set(date, {
3151
+ hours: 12,
3152
+ minutes: 0,
3153
+ seconds: 0,
3154
+ milliseconds: 0
3155
+ }));
3156
+ else {
3157
+ onChange(date);
3158
+ setOpen(false);
3159
+ }
3160
+ }, [
3161
+ mode,
3162
+ value,
3163
+ onChange
3164
+ ]);
3165
+ const handleHourChange = useCallback((hourStr) => {
3166
+ if (!value) return;
3167
+ const hour = parseInt(hourStr, 10);
3168
+ onChange(set(value, { hours: hourFormat === 12 ? toHour24(hour, getHour12(value.getHours()).period) : hour }));
3169
+ }, [
3170
+ value,
3171
+ hourFormat,
3172
+ onChange
3173
+ ]);
3174
+ const handleMinuteChange = useCallback((minuteStr) => {
3175
+ if (!value) return;
3176
+ onChange(set(value, { minutes: parseInt(minuteStr, 10) }));
3177
+ }, [value, onChange]);
3178
+ const handlePeriodChange = useCallback((period) => {
3179
+ if (!value || hourFormat !== 12) return;
3180
+ const { hour12 } = getHour12(value.getHours());
3181
+ onChange(set(value, { hours: toHour24(hour12, period) }));
3182
+ }, [
3183
+ value,
3184
+ hourFormat,
3185
+ onChange
3186
+ ]);
3187
+ const handleClear = (e) => {
3188
+ e.stopPropagation();
3189
+ onChange(void 0);
3190
+ };
3191
+ const disabledDates = useMemo(() => {
3192
+ const constraints = [];
3193
+ if (minDate) constraints.push({ before: minDate });
3194
+ if (maxDate) constraints.push({ after: maxDate });
3195
+ return constraints.length > 0 ? constraints : void 0;
3196
+ }, [minDate, maxDate]);
3197
+ const displayText = value ? mode === "datetime" ? format(value, hourFormat === 12 ? "MMM d, yyyy hh:mm a" : "MMM d, yyyy HH:mm") : format(value, "MMM d, yyyy") : null;
3198
+ const currentHour = value ? hourFormat === 12 ? String(getHour12(value.getHours()).hour12).padStart(2, "0") : String(value.getHours()).padStart(2, "0") : void 0;
3199
+ const currentMinute = value ? String(value.getMinutes()).padStart(2, "0") : void 0;
3200
+ const currentPeriod = value ? getHour12(value.getHours()).period : void 0;
3201
+ return /* @__PURE__ */ jsxs(Popover, {
3202
+ open,
3203
+ onOpenChange: setOpen,
3204
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsxs(Button, {
3205
+ type: "button",
3206
+ variant: "outline",
3207
+ disabled,
3208
+ "aria-invalid": error || void 0,
3209
+ "aria-describedby": ariaDescribedBy,
3210
+ className: cn("w-full justify-start text-left font-normal", !value && "text-muted-foreground", error && "border-destructive"),
3211
+ children: [
3212
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }),
3213
+ displayText || placeholder,
3214
+ clearable && value && !disabled && /* @__PURE__ */ jsx(X, {
3215
+ className: "ml-auto h-4 w-4 opacity-50 hover:opacity-100",
3216
+ onClick: handleClear
3217
+ })
3218
+ ]
3219
+ }) }), /* @__PURE__ */ jsxs(PopoverContent, {
3220
+ className: "w-auto p-0",
3221
+ align: "start",
3222
+ children: [/* @__PURE__ */ jsx(Calendar, {
3223
+ mode: "single",
3224
+ selected: value,
3225
+ onSelect: handleDateSelect,
3226
+ defaultMonth: value,
3227
+ disabled: disabledDates
3228
+ }), mode === "datetime" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, {}), /* @__PURE__ */ jsxs("div", {
3229
+ className: "flex items-center gap-2 p-3",
3230
+ children: [
3231
+ /* @__PURE__ */ jsx("span", {
3232
+ className: "text-sm text-muted-foreground shrink-0",
3233
+ children: "Time:"
3234
+ }),
3235
+ /* @__PURE__ */ jsxs(Select, {
3236
+ value: currentHour,
3237
+ onValueChange: handleHourChange,
3238
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
3239
+ className: "w-[70px] h-8",
3240
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "HH" })
3241
+ }), /* @__PURE__ */ jsx(SelectContent, { children: hourOptions.map((h) => /* @__PURE__ */ jsx(SelectItem, {
3242
+ value: h,
3243
+ children: h
3244
+ }, h)) })]
3245
+ }),
3246
+ /* @__PURE__ */ jsx("span", {
3247
+ className: "text-sm font-medium",
3248
+ children: ":"
3249
+ }),
3250
+ /* @__PURE__ */ jsxs(Select, {
3251
+ value: currentMinute,
3252
+ onValueChange: handleMinuteChange,
3253
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
3254
+ className: "w-[70px] h-8",
3255
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "MM" })
3256
+ }), /* @__PURE__ */ jsx(SelectContent, { children: minuteOptions.map((m) => /* @__PURE__ */ jsx(SelectItem, {
3257
+ value: m,
3258
+ children: m
3259
+ }, m)) })]
3260
+ }),
3261
+ hourFormat === 12 && /* @__PURE__ */ jsxs(Select, {
3262
+ value: currentPeriod,
3263
+ onValueChange: handlePeriodChange,
3264
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
3265
+ className: "w-[70px] h-8",
3266
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "AM" })
3267
+ }), /* @__PURE__ */ jsxs(SelectContent, { children: [/* @__PURE__ */ jsx(SelectItem, {
3268
+ value: "AM",
3269
+ children: "AM"
3270
+ }), /* @__PURE__ */ jsx(SelectItem, {
3271
+ value: "PM",
3272
+ children: "PM"
3273
+ })] })]
3274
+ })
3275
+ ]
3276
+ })] })]
3277
+ })]
3278
+ });
3279
+ }
3280
+
3281
+ //#endregion
3282
+ //#region src/components/form/password-input.tsx
3283
+ /**
3284
+ * PasswordInput - Password field with visibility toggle
3285
+ *
3286
+ * Thin wrapper around FormInput. Inherits all FormInput features
3287
+ * (RHF integration, InputGroup, validation, transforms).
3288
+ *
3289
+ * @example
3290
+ * ```tsx
3291
+ * <PasswordInput
3292
+ * control={form.control}
3293
+ * name="password"
3294
+ * label="Password"
3295
+ * placeholder="Enter password"
3296
+ * required
3297
+ * />
3298
+ * ```
3299
+ */
3300
+ function PasswordInput({ showToggle = true, autoComplete = "current-password", ...props }) {
3301
+ const [visible, setVisible] = useState(false);
3302
+ return /* @__PURE__ */ jsx(FormInput, {
3303
+ ...props,
3304
+ type: visible ? "text" : "password",
3305
+ autoComplete,
3306
+ addonRight: showToggle ? /* @__PURE__ */ jsx(InputGroupButton, {
3307
+ type: "button",
3308
+ size: "icon-xs",
3309
+ "aria-label": visible ? "Hide password" : "Show password",
3310
+ onClick: () => setVisible((v) => !v),
3311
+ children: visible ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" })
3312
+ }) : void 0
3313
+ });
3314
+ }
3315
+
3316
+ //#endregion
3317
+ //#region src/components/form/number-input.tsx
3318
+ /**
3319
+ * NumberInput - Numeric input with optional stepper buttons
3320
+ *
3321
+ * Features:
3322
+ * - +/- stepper buttons with min/max clamping
3323
+ * - Works with react-hook-form Controller
3324
+ * - Can be used standalone without form
3325
+ * - Native spinner hidden when stepper is shown
3326
+ *
3327
+ * @example
3328
+ * ```tsx
3329
+ * <NumberInput
3330
+ * control={form.control}
3331
+ * name="quantity"
3332
+ * label="Quantity"
3333
+ * min={1}
3334
+ * max={100}
3335
+ * step={1}
3336
+ * />
3337
+ * ```
3338
+ */
3339
+ function NumberInput({ control, name, label, placeholder, description, helperText, required, disabled, min, max, step = 1, showStepper = true, value: propValue, onChange: propOnChange, onValueChange, className, labelClassName, inputClassName }) {
3340
+ const descriptionText = description || helperText;
3341
+ const clamp = useCallback((val) => {
3342
+ let result = val;
3343
+ if (min !== void 0 && result < min) result = min;
3344
+ if (max !== void 0 && result > max) result = max;
3345
+ return result;
3346
+ }, [min, max]);
3347
+ const parseValue = (raw) => {
3348
+ if (raw === "" || raw === null || raw === void 0) return void 0;
3349
+ const n = Number(raw);
3350
+ return isNaN(n) ? void 0 : n;
3351
+ };
3352
+ const emitChange = (value, fieldOnChange) => {
3353
+ if (fieldOnChange) fieldOnChange(value);
3354
+ else propOnChange?.(value);
3355
+ onValueChange?.(value);
3356
+ };
3357
+ const handleInputChange = (e, fieldOnChange) => {
3358
+ const raw = e.target.value;
3359
+ if (raw === "" || raw === "-") {
3360
+ if (raw === "") emitChange(void 0, fieldOnChange);
3361
+ return;
3362
+ }
3363
+ const num = parseFloat(raw);
3364
+ if (!isNaN(num)) emitChange(num, fieldOnChange);
3365
+ };
3366
+ const handleStep = (direction, currentValue, fieldOnChange) => {
3367
+ emitChange(clamp((parseValue(currentValue) ?? min ?? 0) + direction * step), fieldOnChange);
3368
+ };
3369
+ const renderNumberInput = (currentValue, fieldOnChange, isDisabled, fieldState) => {
3370
+ const displayValue = currentValue !== void 0 && currentValue !== null ? String(currentValue) : "";
3371
+ const numValue = parseValue(currentValue);
3372
+ const atMin = min !== void 0 && numValue !== void 0 && numValue <= min;
3373
+ const atMax = max !== void 0 && numValue !== void 0 && numValue >= max;
3374
+ const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
3375
+ const commonProps = {
3376
+ id: name,
3377
+ type: "number",
3378
+ inputMode: "numeric",
3379
+ value: displayValue,
3380
+ onChange: (e) => handleInputChange(e, fieldOnChange),
3381
+ disabled: isDisabled,
3382
+ placeholder,
3383
+ min,
3384
+ max,
3385
+ step,
3386
+ "aria-invalid": fieldState?.invalid || void 0,
3387
+ "aria-describedby": ariaDescribedBy
3388
+ };
3389
+ if (!showStepper) return /* @__PURE__ */ jsx(Input, {
3390
+ ...commonProps,
3391
+ className: inputClassName
3392
+ });
3393
+ return /* @__PURE__ */ jsxs(InputGroup, {
3394
+ "data-disabled": isDisabled,
3395
+ children: [
3396
+ /* @__PURE__ */ jsx(InputGroupAddon, {
3397
+ align: "inline-start",
3398
+ children: /* @__PURE__ */ jsx(InputGroupButton, {
3399
+ type: "button",
3400
+ size: "icon-xs",
3401
+ disabled: isDisabled || atMin,
3402
+ onClick: () => handleStep(-1, currentValue, fieldOnChange),
3403
+ "aria-label": "Decrease",
3404
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-3.5 w-3.5" })
3405
+ })
3406
+ }),
3407
+ /* @__PURE__ */ jsx(InputGroupInput, {
3408
+ ...commonProps,
3409
+ className: cn("text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", inputClassName)
3410
+ }),
3411
+ /* @__PURE__ */ jsx(InputGroupAddon, {
3412
+ align: "inline-end",
3413
+ children: /* @__PURE__ */ jsx(InputGroupButton, {
3414
+ type: "button",
3415
+ size: "icon-xs",
3416
+ disabled: isDisabled || atMax,
3417
+ onClick: () => handleStep(1, currentValue, fieldOnChange),
3418
+ "aria-label": "Increase",
3419
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" })
3420
+ })
3421
+ })
3422
+ ]
3423
+ });
3424
+ };
3425
+ if (!control) return /* @__PURE__ */ jsxs(Field, {
3426
+ className,
3427
+ "data-disabled": disabled,
3428
+ children: [
3429
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
3430
+ htmlFor: name,
3431
+ className: labelClassName,
3432
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3433
+ className: "text-destructive ml-1",
3434
+ "aria-hidden": "true",
3435
+ children: "*"
3436
+ }), /* @__PURE__ */ jsx("span", {
3437
+ className: "sr-only",
3438
+ children: "(required)"
3439
+ })] })]
3440
+ }),
3441
+ renderNumberInput(propValue, void 0, disabled),
3442
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
3443
+ id: `${name}-description`,
3444
+ children: descriptionText
3445
+ })
3446
+ ]
3447
+ });
3448
+ return /* @__PURE__ */ jsx(Controller, {
3449
+ name,
3450
+ control,
3451
+ render: ({ field, fieldState }) => /* @__PURE__ */ jsxs(Field, {
3452
+ className,
3453
+ "data-disabled": disabled,
3454
+ "data-invalid": fieldState.invalid,
3455
+ children: [
3456
+ label && /* @__PURE__ */ jsxs(FieldLabel, {
3457
+ htmlFor: name,
3458
+ className: labelClassName,
3459
+ children: [label, required && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3460
+ className: "text-destructive ml-1",
3461
+ "aria-hidden": "true",
3462
+ children: "*"
3463
+ }), /* @__PURE__ */ jsx("span", {
3464
+ className: "sr-only",
3465
+ children: "(required)"
3466
+ })] })]
3467
+ }),
3468
+ renderNumberInput(field.value, field.onChange, disabled, fieldState),
3469
+ descriptionText && /* @__PURE__ */ jsx(FieldDescription, {
3470
+ id: `${name}-description`,
3471
+ children: descriptionText
3472
+ }),
3473
+ fieldState.invalid && fieldState.error && /* @__PURE__ */ jsx(FieldError$1, {
3474
+ id: `${name}-error`,
3475
+ errors: [fieldState.error]
3476
+ })
3477
+ ]
3478
+ })
3479
+ });
3480
+ }
3481
+
3482
+ //#endregion
3483
+ //#region src/components/form/form-section.tsx
3484
+ function renderIcon(icon) {
3485
+ if (!icon) return null;
3486
+ if (isValidElement(icon)) return icon;
3487
+ if (typeof icon === "function") return /* @__PURE__ */ jsx(icon, { className: "h-4 w-4" });
3488
+ return icon;
3489
+ }
3490
+ /**
3491
+ * FormSection - Organizes form fields into logical groups with variants
3492
+ *
3493
+ * @example
3494
+ * ```tsx
3495
+ * <FormSection title="Personal Info" variant="card" collapsible>
3496
+ * <FormInput name="name" label="Name" />
3497
+ * <FormInput name="email" label="Email" />
3498
+ * </FormSection>
3499
+ * ```
3500
+ */
3501
+ function FormSection({ title, description, children, variant = "default", collapsible = false, defaultOpen = true, icon, className }) {
3502
+ const iconEl = renderIcon(icon);
3503
+ const header = /* @__PURE__ */ jsxs("div", {
3504
+ className: "flex items-center gap-2",
3505
+ children: [iconEl && /* @__PURE__ */ jsx("span", {
3506
+ className: "text-muted-foreground",
3507
+ children: iconEl
3508
+ }), /* @__PURE__ */ jsxs("div", {
3509
+ className: "flex-1 space-y-0.5",
3510
+ children: [title && /* @__PURE__ */ jsx("h3", {
3511
+ className: "text-base font-semibold",
3512
+ children: title
3513
+ }), description && /* @__PURE__ */ jsx("p", {
3514
+ className: "text-sm text-muted-foreground",
3515
+ children: description
3516
+ })]
3517
+ })]
3518
+ });
3519
+ if (variant === "card") {
3520
+ if (collapsible) return /* @__PURE__ */ jsxs("details", {
3521
+ open: defaultOpen,
3522
+ className: cn("group border rounded-lg", className),
3523
+ children: [/* @__PURE__ */ jsxs("summary", {
3524
+ className: "flex cursor-pointer list-none items-center justify-between px-6 py-4 [&::-webkit-details-marker]:hidden",
3525
+ children: [header, /* @__PURE__ */ jsx("svg", {
3526
+ className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180",
3527
+ xmlns: "http://www.w3.org/2000/svg",
3528
+ viewBox: "0 0 24 24",
3529
+ fill: "none",
3530
+ stroke: "currentColor",
3531
+ strokeWidth: "2",
3532
+ strokeLinecap: "round",
3533
+ strokeLinejoin: "round",
3534
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
3535
+ })]
3536
+ }), /* @__PURE__ */ jsx("div", {
3537
+ className: "px-6 pb-4",
3538
+ children
3539
+ })]
3540
+ });
3541
+ return /* @__PURE__ */ jsxs(Card, {
3542
+ className: cn("border-muted", className),
3543
+ children: [/* @__PURE__ */ jsxs(CardHeader, {
3544
+ className: "space-y-1",
3545
+ children: [/* @__PURE__ */ jsxs(CardTitle, {
3546
+ className: "text-lg flex items-center gap-2",
3547
+ children: [iconEl && /* @__PURE__ */ jsx("span", {
3548
+ className: "text-muted-foreground",
3549
+ children: iconEl
3550
+ }), title]
3551
+ }), description && /* @__PURE__ */ jsx(CardDescription, {
3552
+ className: "text-sm",
3553
+ children: description
3554
+ })]
3555
+ }), /* @__PURE__ */ jsx(CardContent, { children })]
3556
+ });
3557
+ }
3558
+ if (variant === "subtle") {
3559
+ if (collapsible) return /* @__PURE__ */ jsxs("details", {
3560
+ open: defaultOpen,
3561
+ className: cn("group", className),
3562
+ children: [/* @__PURE__ */ jsxs("summary", {
3563
+ className: "flex cursor-pointer list-none items-center justify-between rounded-md px-0 py-2 hover:bg-muted/50 [&::-webkit-details-marker]:hidden",
3564
+ children: [header, /* @__PURE__ */ jsx("svg", {
3565
+ className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180",
3566
+ xmlns: "http://www.w3.org/2000/svg",
3567
+ viewBox: "0 0 24 24",
3568
+ fill: "none",
3569
+ stroke: "currentColor",
3570
+ strokeWidth: "2",
3571
+ strokeLinecap: "round",
3572
+ strokeLinejoin: "round",
3573
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
3574
+ })]
3575
+ }), /* @__PURE__ */ jsx("div", {
3576
+ className: "py-4",
3577
+ children
3578
+ })]
3579
+ });
3580
+ return /* @__PURE__ */ jsxs("div", {
3581
+ className: cn("space-y-4", className),
3582
+ children: [(title || description) && /* @__PURE__ */ jsxs("div", {
3583
+ className: "space-y-1",
3584
+ children: [title && /* @__PURE__ */ jsxs("h3", {
3585
+ className: "text-base font-semibold flex items-center gap-2",
3586
+ children: [iconEl && /* @__PURE__ */ jsx("span", {
3587
+ className: "text-muted-foreground",
3588
+ children: iconEl
3589
+ }), title]
3590
+ }), description && /* @__PURE__ */ jsx("p", {
3591
+ className: "text-sm text-muted-foreground",
3592
+ children: description
3593
+ })]
3594
+ }), children]
3595
+ });
3596
+ }
3597
+ if (collapsible) return /* @__PURE__ */ jsxs("details", {
3598
+ open: defaultOpen,
3599
+ className: cn("group border rounded-lg", className),
3600
+ children: [/* @__PURE__ */ jsxs("summary", {
3601
+ className: "flex cursor-pointer list-none items-center justify-between p-4 [&::-webkit-details-marker]:hidden",
3602
+ children: [header, /* @__PURE__ */ jsx("svg", {
3603
+ className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180",
3604
+ xmlns: "http://www.w3.org/2000/svg",
3605
+ viewBox: "0 0 24 24",
3606
+ fill: "none",
3607
+ stroke: "currentColor",
3608
+ strokeWidth: "2",
3609
+ strokeLinecap: "round",
3610
+ strokeLinejoin: "round",
3611
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
3612
+ })]
3613
+ }), /* @__PURE__ */ jsx("div", {
3614
+ className: "p-4 pt-0",
3615
+ children
3616
+ })]
3617
+ });
3618
+ return /* @__PURE__ */ jsxs("div", {
3619
+ className: cn("space-y-4", className),
3620
+ children: [(title || description) && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
3621
+ className: "space-y-1.5",
3622
+ children: [title && /* @__PURE__ */ jsxs("h3", {
3623
+ className: "text-base font-semibold flex items-center gap-2",
3624
+ children: [iconEl && /* @__PURE__ */ jsx("span", {
3625
+ className: "text-muted-foreground",
3626
+ children: iconEl
3627
+ }), title]
3628
+ }), description && /* @__PURE__ */ jsx("p", {
3629
+ className: "text-sm text-muted-foreground leading-relaxed",
3630
+ children: description
3631
+ })]
3632
+ }), /* @__PURE__ */ jsx(Separator, { className: "my-4" })] }), children]
3633
+ });
3634
+ }
3635
+ const COLS_CLASS = {
3636
+ 1: "grid-cols-1",
3637
+ 2: "grid-cols-1 md:grid-cols-2",
3638
+ 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
3639
+ 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
3640
+ };
3641
+ /**
3642
+ * FormGrid - Responsive grid layout for form fields
3643
+ *
3644
+ * @example
3645
+ * ```tsx
3646
+ * <FormGrid cols={2}>
3647
+ * <FormInput name="firstName" label="First Name" />
3648
+ * <FormInput name="lastName" label="Last Name" />
3649
+ * </FormGrid>
3650
+ * ```
3651
+ */
3652
+ function FormGrid({ children, cols = 2, className }) {
3653
+ return /* @__PURE__ */ jsx("div", {
3654
+ className: cn("grid gap-4", COLS_CLASS[cols] ?? COLS_CLASS[2], className),
3655
+ children
3656
+ });
3657
+ }
3658
+
3659
+ //#endregion
3660
+ //#region src/components/form/form-field-array.tsx
3661
+ /**
3662
+ * FormFieldArray - Container for dynamic field arrays with add/remove
3663
+ *
3664
+ * @example
3665
+ * ```tsx
3666
+ * <FormFieldArray
3667
+ * title="Team Members"
3668
+ * onAdd={() => append({ name: "", role: "" })}
3669
+ * itemCount={fields.length}
3670
+ * maxItems={10}
3671
+ * >
3672
+ * {fields.map((field, index) => (
3673
+ * <FormFieldArrayItem
3674
+ * key={field.id}
3675
+ * title={`Member ${index + 1}`}
3676
+ * onRemove={() => remove(index)}
3677
+ * >
3678
+ * <FormInput control={control} name={`members.${index}.name`} label="Name" />
3679
+ * </FormFieldArrayItem>
3680
+ * ))}
3681
+ * </FormFieldArray>
3682
+ * ```
3683
+ */
3684
+ function FormFieldArray({ title, description, children, onAdd, addLabel = "Add Item", emptyMessage = "No items yet.", itemCount = 0, maxItems, className }) {
3685
+ const canAdd = maxItems == null || itemCount < maxItems;
3686
+ return /* @__PURE__ */ jsxs("div", {
3687
+ className: cn("space-y-4", className),
3688
+ children: [/* @__PURE__ */ jsxs("div", {
3689
+ className: "flex items-start justify-between gap-4",
3690
+ children: [/* @__PURE__ */ jsxs("div", {
3691
+ className: "space-y-1",
3692
+ children: [title && /* @__PURE__ */ jsx("h4", {
3693
+ className: "text-sm font-medium",
3694
+ children: title
3695
+ }), description && /* @__PURE__ */ jsx("p", {
3696
+ className: "text-xs text-muted-foreground",
3697
+ children: description
3698
+ })]
3699
+ }), onAdd && canAdd && /* @__PURE__ */ jsxs(Button, {
3700
+ type: "button",
3701
+ variant: "outline",
3702
+ size: "sm",
3703
+ onClick: onAdd,
3704
+ children: [/* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }), addLabel]
3705
+ })]
3706
+ }), itemCount === 0 && emptyMessage ? /* @__PURE__ */ jsx("div", {
3707
+ className: "text-center py-8 text-sm text-muted-foreground border border-dashed rounded-lg",
3708
+ children: emptyMessage
3709
+ }) : /* @__PURE__ */ jsx("div", {
3710
+ className: "space-y-3",
3711
+ children
3712
+ })]
3713
+ });
3714
+ }
3715
+ /**
3716
+ * FormFieldArrayItem - Individual item within a FormFieldArray
3717
+ */
3718
+ function FormFieldArrayItem({ children, onRemove, title, className }) {
3719
+ return /* @__PURE__ */ jsxs("div", {
3720
+ className: cn("relative border rounded-lg p-4 space-y-4 bg-muted/20 hover:bg-muted/30 transition-colors", className),
3721
+ children: [/* @__PURE__ */ jsxs("div", {
3722
+ className: "flex items-center justify-between gap-4 mb-2",
3723
+ children: [title && /* @__PURE__ */ jsx("h5", {
3724
+ className: "text-sm font-medium text-muted-foreground",
3725
+ children: title
3726
+ }), onRemove && /* @__PURE__ */ jsx(Button, {
3727
+ type: "button",
3728
+ variant: "ghost",
3729
+ size: "icon-sm",
3730
+ className: "shrink-0 text-destructive hover:bg-destructive/10",
3731
+ onClick: onRemove,
3732
+ "aria-label": "Remove item",
3733
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3734
+ })]
3735
+ }), children]
3736
+ });
3737
+ }
3738
+
3739
+ //#endregion
3740
+ export { AsyncCombobox, AsyncMultiSelect, CheckboxInput, ComboboxInput, DateInput, DateRangeFilter, DateRangeInput, DateTimeInput, FileUploadInput, FormErrorSummary, FormFieldArray, FormFieldArrayItem, FormGrid, FormInput, FormSection, FormTextarea, MultiSelect, NumberInput, OTPInput, PasswordInput, RadioInput, SelectInput, SlugField, SwitchInput, TagChoiceInput, TagInput, generateSlug };