@page-speed/forms 0.5.2 → 0.5.4

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 (55) hide show
  1. package/dist/chunk-232KNGJI.js +207 -0
  2. package/dist/chunk-232KNGJI.js.map +1 -0
  3. package/dist/chunk-24RPM43T.js +373 -0
  4. package/dist/chunk-24RPM43T.js.map +1 -0
  5. package/dist/chunk-27JUYRDE.cjs +173 -0
  6. package/dist/chunk-27JUYRDE.cjs.map +1 -0
  7. package/dist/chunk-5NT5T5XY.js +4136 -0
  8. package/dist/chunk-5NT5T5XY.js.map +1 -0
  9. package/dist/chunk-AVAKC6R7.cjs +236 -0
  10. package/dist/chunk-AVAKC6R7.cjs.map +1 -0
  11. package/dist/chunk-DKLLPKZN.cjs +238 -0
  12. package/dist/chunk-DKLLPKZN.cjs.map +1 -0
  13. package/dist/chunk-EX6CRLKG.cjs +397 -0
  14. package/dist/chunk-EX6CRLKG.cjs.map +1 -0
  15. package/dist/chunk-H6NNFV64.js +127 -0
  16. package/dist/chunk-H6NNFV64.js.map +1 -0
  17. package/dist/chunk-JBEWTBFH.js +217 -0
  18. package/dist/chunk-JBEWTBFH.js.map +1 -0
  19. package/dist/chunk-JBEZLX3H.cjs +138 -0
  20. package/dist/chunk-JBEZLX3H.cjs.map +1 -0
  21. package/dist/chunk-VLGZG2VP.js +150 -0
  22. package/dist/chunk-VLGZG2VP.js.map +1 -0
  23. package/dist/chunk-ZYFTT6DB.cjs +4169 -0
  24. package/dist/chunk-ZYFTT6DB.cjs.map +1 -0
  25. package/dist/core.cjs +23 -733
  26. package/dist/core.cjs.map +1 -1
  27. package/dist/core.js +3 -716
  28. package/dist/core.js.map +1 -1
  29. package/dist/index.cjs +43 -738
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +3 -716
  32. package/dist/index.js.map +1 -1
  33. package/dist/inputs.cjs +44 -4359
  34. package/dist/inputs.cjs.map +1 -1
  35. package/dist/inputs.js +2 -4337
  36. package/dist/inputs.js.map +1 -1
  37. package/dist/integration.cjs +65 -4658
  38. package/dist/integration.cjs.map +1 -1
  39. package/dist/integration.d.cts +7 -1
  40. package/dist/integration.d.ts +7 -1
  41. package/dist/integration.js +42 -4635
  42. package/dist/integration.js.map +1 -1
  43. package/dist/validation-rules.cjs +75 -231
  44. package/dist/validation-rules.cjs.map +1 -1
  45. package/dist/validation-rules.js +1 -215
  46. package/dist/validation-rules.js.map +1 -1
  47. package/dist/validation-utils.cjs +43 -133
  48. package/dist/validation-utils.cjs.map +1 -1
  49. package/dist/validation-utils.js +1 -125
  50. package/dist/validation-utils.js.map +1 -1
  51. package/dist/validation.cjs +115 -364
  52. package/dist/validation.cjs.map +1 -1
  53. package/dist/validation.js +2 -339
  54. package/dist/validation.js.map +1 -1
  55. package/package.json +1 -1
package/dist/inputs.js CHANGED
@@ -1,4339 +1,4 @@
1
- import * as React25 from 'react';
2
- import { clsx } from 'clsx';
3
- import { twMerge } from 'tailwind-merge';
4
- import { Dialog as Dialog$1, Label as Label$1, Checkbox as Checkbox$1, RadioGroup as RadioGroup$1, Switch as Switch$1, Select as Select$1, Popover as Popover$1, Slot as Slot$1 } from 'radix-ui';
5
- import { Command as Command$1 } from 'cmdk';
6
- import { cva } from 'class-variance-authority';
7
- import { useDirection } from '@radix-ui/react-direction';
8
- import { Slot } from '@radix-ui/react-slot';
9
- import { getDefaultClassNames, DayPicker } from 'react-day-picker';
10
-
11
- // src/inputs/TextInput.tsx
12
- function cn(...inputs) {
13
- return twMerge(clsx(inputs));
14
- }
15
- var INPUT_AUTOFILL_RESET_CLASSES = "autofill:bg-transparent autofill:text-foreground [&:-webkit-autofill]:[-webkit-text-fill-color:hsl(var(--foreground))] [&:-webkit-autofill]:[caret-color:hsl(var(--foreground))] [&:-webkit-autofill]:[box-shadow:0_0_0px_1000px_hsl(var(--background))_inset] [&:-webkit-autofill:hover]:[box-shadow:0_0_0px_1000px_hsl(var(--background))_inset] [&:-webkit-autofill:focus]:[box-shadow:0_0_0px_1000px_hsl(var(--background))_inset] [&:-webkit-autofill]:[transition:background-color_9999s_ease-out,color_9999s_ease-out]";
16
-
17
- // src/components/ui/input.tsx
18
- var Input = React25.forwardRef(
19
- ({ className, type, ...props }, ref) => {
20
- return /* @__PURE__ */ React25.createElement(
21
- "input",
22
- {
23
- ref,
24
- type,
25
- "data-slot": "input",
26
- className: cn(
27
- // Core structure - no hardcoded colors, uses CSS variables
28
- "flex h-9 w-full min-w-0 rounded-md border border-input",
29
- "bg-transparent px-3 py-1 text-base shadow-sm",
30
- "transition-colors outline-none md:text-sm",
31
- // Focus state - uses ring-ring CSS variable (adapts to theme)
32
- "focus-visible:ring-1 focus-visible:ring-ring",
33
- // Error state - uses destructive CSS variables (adapts to theme)
34
- "aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive",
35
- // Disabled state - no color hardcoding
36
- "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
37
- // File input specific - inherits text color from parent
38
- "file:inline-flex file:h-7 file:border-0 file:bg-transparent",
39
- "file:text-sm file:font-medium",
40
- // Autofill reset - prevents browser from overriding our dynamic colors
41
- INPUT_AUTOFILL_RESET_CLASSES,
42
- className
43
- ),
44
- ...props
45
- }
46
- );
47
- }
48
- );
49
- Input.displayName = "Input";
50
-
51
- // src/inputs/TextInput.tsx
52
- function TextInput({
53
- name,
54
- value,
55
- onChange,
56
- onBlur,
57
- placeholder,
58
- disabled = false,
59
- required = false,
60
- error = false,
61
- className = "",
62
- type = "text",
63
- id = "text",
64
- ...props
65
- }) {
66
- const handleChange = (e) => {
67
- onChange(e.target.value);
68
- };
69
- const handleBlur = () => {
70
- onBlur?.();
71
- };
72
- const hasValue = String(value ?? "").trim().length > 0;
73
- return /* @__PURE__ */ React25.createElement(
74
- Input,
75
- {
76
- type,
77
- id,
78
- name,
79
- value: value ?? "",
80
- onChange: handleChange,
81
- onBlur: handleBlur,
82
- placeholder,
83
- disabled,
84
- required,
85
- className: cn(
86
- // Valid value indicator - ring-2 when has value and no error
87
- !error && hasValue && "ring-2 ring-ring",
88
- // Error state - handled by Input component via aria-invalid
89
- className
90
- ),
91
- "aria-invalid": error || props["aria-invalid"],
92
- "aria-describedby": props["aria-describedby"],
93
- "aria-required": required || props["aria-required"],
94
- ...props
95
- }
96
- );
97
- }
98
- TextInput.displayName = "TextInput";
99
- function Textarea({ className, ...props }) {
100
- return /* @__PURE__ */ React25.createElement(
101
- "textarea",
102
- {
103
- "data-slot": "textarea",
104
- className: cn(
105
- // Core structure - uses CSS variables only
106
- "flex field-sizing-content min-h-16 w-full rounded-md border border-input",
107
- "bg-transparent px-3 py-2 text-base shadow-xs",
108
- "transition-[color,box-shadow] outline-none md:text-sm",
109
- // Focus state - uses ring-ring CSS variable
110
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
111
- // Error state - uses destructive CSS variables
112
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
113
- // Disabled state
114
- "disabled:cursor-not-allowed disabled:opacity-50",
115
- className
116
- ),
117
- ...props
118
- }
119
- );
120
- }
121
-
122
- // src/inputs/TextArea.tsx
123
- function TextArea({
124
- name,
125
- value,
126
- onChange,
127
- onBlur,
128
- placeholder,
129
- disabled = false,
130
- required = false,
131
- error = false,
132
- className = "",
133
- rows = 3,
134
- cols,
135
- maxLength,
136
- minLength,
137
- wrap = "soft",
138
- ...props
139
- }) {
140
- const handleChange = (e) => {
141
- onChange(e.target.value);
142
- };
143
- const handleBlur = () => {
144
- onBlur?.();
145
- };
146
- const hasValue = String(value ?? "").trim().length > 0;
147
- return /* @__PURE__ */ React25.createElement(
148
- Textarea,
149
- {
150
- name,
151
- value: value ?? "",
152
- onChange: handleChange,
153
- onBlur: handleBlur,
154
- placeholder,
155
- disabled,
156
- required,
157
- className: cn(
158
- // Valid value indicator - ring-2 when has value and no error
159
- !error && hasValue && "ring-2 ring-ring",
160
- // Error state - handled by Textarea component via aria-invalid
161
- className
162
- ),
163
- rows,
164
- cols,
165
- maxLength,
166
- minLength,
167
- wrap,
168
- "aria-invalid": error || props["aria-invalid"],
169
- "aria-describedby": props["aria-describedby"],
170
- "aria-required": required || props["aria-required"],
171
- ...props
172
- }
173
- );
174
- }
175
- TextArea.displayName = "TextArea";
176
- function Checkbox({
177
- className,
178
- ...props
179
- }) {
180
- return /* @__PURE__ */ React25.createElement(
181
- Checkbox$1.Root,
182
- {
183
- "data-slot": "checkbox",
184
- className: cn(
185
- // Core structure - uses CSS variables
186
- "peer size-4 shrink-0 rounded-[4px] border border-input bg-transparent shadow-xs",
187
- "transition-shadow outline-none",
188
- // Checked state - uses primary CSS variables
189
- "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
190
- "data-[state=checked]:border-primary",
191
- // Focus state - uses ring-ring CSS variable
192
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
193
- // Error state - uses destructive CSS variables
194
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
195
- // Disabled state
196
- "disabled:cursor-not-allowed disabled:opacity-50",
197
- className
198
- ),
199
- ...props
200
- },
201
- /* @__PURE__ */ React25.createElement(
202
- Checkbox$1.Indicator,
203
- {
204
- "data-slot": "checkbox-indicator",
205
- className: "grid place-content-center text-current transition-none"
206
- },
207
- /* @__PURE__ */ React25.createElement(
208
- "svg",
209
- {
210
- className: "size-3.5",
211
- viewBox: "0 0 24 24",
212
- fill: "none",
213
- stroke: "currentColor",
214
- strokeWidth: "3",
215
- strokeLinecap: "round",
216
- strokeLinejoin: "round"
217
- },
218
- /* @__PURE__ */ React25.createElement("polyline", { points: "20 6 9 17 4 12" })
219
- )
220
- )
221
- );
222
- }
223
- function Label({
224
- className,
225
- ...props
226
- }) {
227
- return /* @__PURE__ */ React25.createElement(
228
- Label$1.Root,
229
- {
230
- "data-slot": "label",
231
- className: cn(
232
- "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
233
- className
234
- ),
235
- ...props
236
- }
237
- );
238
- }
239
-
240
- // src/components/ui/field.tsx
241
- var Field = React25.forwardRef(
242
- ({ className, orientation = "vertical", invalid = false, ...props }, ref) => {
243
- return /* @__PURE__ */ React25.createElement(
244
- "div",
245
- {
246
- ref,
247
- "data-slot": "field",
248
- "data-orientation": orientation,
249
- "data-invalid": invalid || void 0,
250
- className: cn(
251
- "flex",
252
- orientation === "horizontal" ? "items-center gap-2" : "flex-col gap-1.5",
253
- className
254
- ),
255
- ...props
256
- }
257
- );
258
- }
259
- );
260
- Field.displayName = "Field";
261
- var FieldGroup = React25.forwardRef(({ className, ...props }, ref) => {
262
- return /* @__PURE__ */ React25.createElement(
263
- "div",
264
- {
265
- ref,
266
- "data-slot": "field-group",
267
- className: cn("flex flex-col gap-4", className),
268
- ...props
269
- }
270
- );
271
- });
272
- FieldGroup.displayName = "FieldGroup";
273
- var FieldLabel = React25.forwardRef(({ className, required, children, ...props }, ref) => {
274
- return /* @__PURE__ */ React25.createElement(
275
- Label,
276
- {
277
- ref,
278
- "data-slot": "field-label",
279
- className: cn(
280
- "text-sm font-medium leading-none select-none",
281
- "peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
282
- className
283
- ),
284
- ...props
285
- },
286
- children,
287
- required && /* @__PURE__ */ React25.createElement("span", { className: "text-destructive ml-1" }, "*")
288
- );
289
- });
290
- FieldLabel.displayName = "FieldLabel";
291
- var FieldDescription = React25.forwardRef(({ className, ...props }, ref) => {
292
- return /* @__PURE__ */ React25.createElement(
293
- "p",
294
- {
295
- ref,
296
- "data-slot": "field-description",
297
- className: cn("text-sm opacity-70", className),
298
- ...props
299
- }
300
- );
301
- });
302
- FieldDescription.displayName = "FieldDescription";
303
- var FieldError = React25.forwardRef(({ className, ...props }, ref) => {
304
- return /* @__PURE__ */ React25.createElement(
305
- "p",
306
- {
307
- ref,
308
- "data-slot": "field-error",
309
- role: "alert",
310
- "aria-live": "polite",
311
- className: cn("text-sm text-destructive", className),
312
- ...props
313
- }
314
- );
315
- });
316
- FieldError.displayName = "FieldError";
317
-
318
- // src/inputs/Checkbox.tsx
319
- function Checkbox2({
320
- name,
321
- value,
322
- onChange,
323
- onBlur,
324
- disabled = false,
325
- required = false,
326
- error = false,
327
- className = "",
328
- label,
329
- description,
330
- useChoiceCard = false,
331
- ...props
332
- }) {
333
- const checkboxId = props.id || `checkbox-${name}`;
334
- const handleCheckedChange = (checked) => {
335
- onChange(checked);
336
- };
337
- const handleBlur = () => {
338
- onBlur?.();
339
- };
340
- const showChoiceCard = useChoiceCard || !!description;
341
- const checkbox = /* @__PURE__ */ React25.createElement(React25.Fragment, null, /* @__PURE__ */ React25.createElement(
342
- "input",
343
- {
344
- type: "checkbox",
345
- name,
346
- checked: value,
347
- onChange: () => {
348
- },
349
- disabled,
350
- required,
351
- tabIndex: -1,
352
- "aria-hidden": "true",
353
- style: {
354
- position: "absolute",
355
- width: "1px",
356
- height: "1px",
357
- padding: 0,
358
- margin: "-1px",
359
- overflow: "hidden",
360
- clip: "rect(0, 0, 0, 0)",
361
- whiteSpace: "nowrap",
362
- border: 0
363
- }
364
- }
365
- ), /* @__PURE__ */ React25.createElement(
366
- Checkbox,
367
- {
368
- id: checkboxId,
369
- checked: value,
370
- onCheckedChange: handleCheckedChange,
371
- onBlur: handleBlur,
372
- disabled,
373
- "aria-invalid": error || props["aria-invalid"],
374
- "aria-describedby": description ? `${checkboxId}-description` : props["aria-describedby"],
375
- "aria-required": required || props["aria-required"],
376
- ...props
377
- }
378
- ));
379
- if (!label) {
380
- return /* @__PURE__ */ React25.createElement(Field, { className }, checkbox);
381
- }
382
- return /* @__PURE__ */ React25.createElement(Field, { className: "gap-0", invalid: Boolean(error) }, /* @__PURE__ */ React25.createElement(
383
- FieldLabel,
384
- {
385
- htmlFor: checkboxId,
386
- className: cn(
387
- "flex gap-3 p-3 duration-200 select-auto font-normal leading-normal",
388
- showChoiceCard && "border rounded-lg hover:ring-2 hover:ring-ring/50",
389
- showChoiceCard && value && "ring-2 ring-ring",
390
- showChoiceCard && error && "border-destructive",
391
- disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
392
- className
393
- )
394
- },
395
- /* @__PURE__ */ React25.createElement(
396
- "div",
397
- {
398
- className: cn(
399
- "flex w-full gap-3",
400
- showChoiceCard ? "items-start" : "items-center"
401
- )
402
- },
403
- checkbox,
404
- /* @__PURE__ */ React25.createElement(Field, { className: "flex-1 gap-1" }, /* @__PURE__ */ React25.createElement("span", { className: "text-sm font-medium leading-none" }, label), description && /* @__PURE__ */ React25.createElement(
405
- FieldDescription,
406
- {
407
- id: `${checkboxId}-description`,
408
- className: "leading-snug"
409
- },
410
- description
411
- ))
412
- )
413
- ));
414
- }
415
- Checkbox2.displayName = "Checkbox";
416
- var LabelGroup = ({
417
- labelHtmlFor,
418
- required = false,
419
- variant = "label",
420
- secondaryId,
421
- secondary,
422
- primary,
423
- primaryClassName,
424
- secondaryClassName
425
- }) => {
426
- const primaryClasses = cn(
427
- "text-sm font-medium leading-snug",
428
- variant === "legend" ? "mb-1.5" : "mb-1 block",
429
- primaryClassName
430
- );
431
- const requiredIndicator = required && variant !== "label" ? /* @__PURE__ */ React25.createElement("span", { className: "text-destructive pl-0.5", "aria-label": "required" }, "*") : null;
432
- let primaryElement = null;
433
- if (primary) {
434
- if (variant === "label") {
435
- primaryElement = /* @__PURE__ */ React25.createElement(
436
- FieldLabel,
437
- {
438
- htmlFor: labelHtmlFor,
439
- required,
440
- className: primaryClasses
441
- },
442
- primary
443
- );
444
- } else if (variant === "legend") {
445
- primaryElement = /* @__PURE__ */ React25.createElement("legend", { "data-slot": "field-legend", className: primaryClasses }, primary, requiredIndicator);
446
- } else {
447
- primaryElement = /* @__PURE__ */ React25.createElement("div", { "data-slot": "field-label", className: primaryClasses }, primary, requiredIndicator);
448
- }
449
- }
450
- const secondaryElement = secondary ? /* @__PURE__ */ React25.createElement(
451
- FieldDescription,
452
- {
453
- id: secondaryId,
454
- className: cn("leading-normal font-normal", secondaryClassName)
455
- },
456
- secondary
457
- ) : null;
458
- if (!primaryElement && !secondaryElement) return null;
459
- if (variant === "legend") {
460
- return /* @__PURE__ */ React25.createElement(React25.Fragment, null, primaryElement, secondaryElement);
461
- }
462
- return /* @__PURE__ */ React25.createElement("div", { className: "flex flex-1 flex-col gap-0.5" }, primaryElement, secondaryElement);
463
- };
464
-
465
- // src/inputs/CheckboxGroup.tsx
466
- function CheckboxGroup({
467
- name,
468
- value = [],
469
- onChange,
470
- onBlur,
471
- disabled = false,
472
- required = false,
473
- error = false,
474
- className = "",
475
- layout = "stacked",
476
- label,
477
- description,
478
- options,
479
- showSelectAll = false,
480
- selectAllLabel = "Select all",
481
- minSelections,
482
- maxSelections,
483
- renderOption,
484
- gridColumns = 2,
485
- ...props
486
- }) {
487
- const enabledOptions = options.filter((opt) => !opt.disabled);
488
- const enabledValues = enabledOptions.map((opt) => opt.value);
489
- const selectedEnabledCount = value.filter(
490
- (v) => enabledValues.includes(v)
491
- ).length;
492
- const allSelected = selectedEnabledCount === enabledOptions.length;
493
- const someSelected = selectedEnabledCount > 0 && !allSelected;
494
- const useChoiceCard = React25.useMemo(() => {
495
- if (!options) return false;
496
- return options?.some((opt) => opt.description);
497
- }, [options]);
498
- const countableValue = React25.useMemo(() => {
499
- if (value?.length > 0) {
500
- return value.length;
501
- }
502
- return 0;
503
- }, [value]);
504
- const handleChange = (optionValue, checked) => {
505
- const newValues = checked ? [...value, optionValue] : value.filter((v) => v !== optionValue);
506
- if (maxSelections && checked && newValues.length > maxSelections) {
507
- return;
508
- }
509
- onChange(newValues);
510
- };
511
- const handleSelectAll = (checked) => {
512
- if (checked) {
513
- const allValues = enabledOptions.map((opt) => opt.value);
514
- onChange(allValues);
515
- } else {
516
- onChange([]);
517
- }
518
- };
519
- const handleBlur = () => {
520
- onBlur?.();
521
- };
522
- const maxReached = Boolean(maxSelections && countableValue >= maxSelections);
523
- const containerClass = React25.useMemo(() => {
524
- return cn(
525
- "w-full gap-3 grid grid-cols-1 border-0 m-0 p-0 min-w-0",
526
- (layout === "grid" || layout === "inline") && "md:grid-cols-2",
527
- className
528
- );
529
- }, [layout, className]);
530
- const groupDescriptionId = description ? `${name}-description` : void 0;
531
- const groupAriaDescribedBy = [props["aria-describedby"], groupDescriptionId].filter(Boolean).join(" ") || void 0;
532
- return /* @__PURE__ */ React25.createElement(
533
- "fieldset",
534
- {
535
- className: containerClass,
536
- role: "group",
537
- "aria-invalid": error || props["aria-invalid"],
538
- "aria-describedby": groupAriaDescribedBy,
539
- "aria-required": required || props["aria-required"],
540
- "aria-label": typeof label === "string" ? label : props["aria-label"]
541
- },
542
- /* @__PURE__ */ React25.createElement(
543
- LabelGroup,
544
- {
545
- labelHtmlFor: name,
546
- required,
547
- variant: "legend",
548
- secondaryId: groupDescriptionId,
549
- secondary: description,
550
- primary: label
551
- }
552
- ),
553
- showSelectAll && enabledOptions.length > 0 && /* @__PURE__ */ React25.createElement(
554
- Checkbox2,
555
- {
556
- name: `${name}-select-all`,
557
- id: `${name}-select-all`,
558
- value: allSelected,
559
- onChange: handleSelectAll,
560
- onBlur: handleBlur,
561
- indeterminate: someSelected,
562
- label: selectAllLabel,
563
- useChoiceCard,
564
- disabled,
565
- "aria-label": selectAllLabel
566
- }
567
- ),
568
- options.map((option) => {
569
- const isChecked = value.includes(option.value);
570
- const isDisabled = disabled || option.disabled || maxReached && !isChecked;
571
- return /* @__PURE__ */ React25.createElement(
572
- Checkbox2,
573
- {
574
- key: option.value,
575
- name,
576
- id: `${name}-${option.value}`,
577
- value: isChecked,
578
- onChange: (checked) => handleChange(option.value, checked),
579
- onBlur: handleBlur,
580
- disabled: isDisabled,
581
- required: required && minSelections ? value.length < minSelections : false,
582
- error,
583
- label: renderOption ? renderOption(option) : option.label,
584
- description: renderOption ? void 0 : option.description,
585
- useChoiceCard
586
- }
587
- );
588
- }),
589
- (minSelections || maxSelections) && /* @__PURE__ */ React25.createElement(
590
- FieldDescription,
591
- {
592
- className: cn(
593
- "p-2 rounded-lg border font-semibold mt-2 leading-snug",
594
- minSelections && countableValue < minSelections ? "border-destructive bg-destructive/80 text-destructive-foreground" : "border-border bg-card text-card-foreground"
595
- ),
596
- "aria-live": "polite"
597
- },
598
- minSelections && countableValue < minSelections && /* @__PURE__ */ React25.createElement("span", null, "Select at least ", minSelections, " option", minSelections !== 1 ? "s" : ""),
599
- maxSelections && /* @__PURE__ */ React25.createElement("span", null, countableValue, "/", maxSelections, " selected")
600
- )
601
- );
602
- }
603
- CheckboxGroup.displayName = "CheckboxGroup";
604
- function RadioGroup({
605
- className,
606
- ...props
607
- }) {
608
- return /* @__PURE__ */ React25.createElement(
609
- RadioGroup$1.Root,
610
- {
611
- "data-slot": "radio-group",
612
- className: cn("grid gap-3", className),
613
- ...props
614
- }
615
- );
616
- }
617
- function RadioGroupItem({
618
- className,
619
- ...props
620
- }) {
621
- return /* @__PURE__ */ React25.createElement(
622
- RadioGroup$1.Item,
623
- {
624
- "data-slot": "radio-group-item",
625
- className: cn(
626
- // Core structure - uses CSS variables
627
- "aspect-square size-4 shrink-0 rounded-full border border-input bg-transparent shadow-xs",
628
- "text-primary transition-[color,box-shadow] outline-none",
629
- // Focus state - uses ring-ring CSS variable
630
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
631
- // Error state - uses destructive CSS variables
632
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
633
- // Disabled state
634
- "disabled:cursor-not-allowed disabled:opacity-50",
635
- className
636
- ),
637
- ...props
638
- },
639
- /* @__PURE__ */ React25.createElement(
640
- RadioGroup$1.Indicator,
641
- {
642
- "data-slot": "radio-group-indicator",
643
- className: "relative flex items-center justify-center"
644
- },
645
- /* @__PURE__ */ React25.createElement(
646
- "svg",
647
- {
648
- className: "fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2",
649
- viewBox: "0 0 24 24"
650
- },
651
- /* @__PURE__ */ React25.createElement("circle", { cx: "12", cy: "12", r: "12" })
652
- )
653
- )
654
- );
655
- }
656
-
657
- // src/inputs/Radio.tsx
658
- function Radio({
659
- name,
660
- value,
661
- onChange,
662
- onBlur,
663
- disabled = false,
664
- required = false,
665
- error = false,
666
- className = "",
667
- layout = "stacked",
668
- label,
669
- description,
670
- options,
671
- ...props
672
- }) {
673
- const handleValueChange = (selectedValue) => {
674
- onChange(selectedValue);
675
- };
676
- const handleBlur = () => {
677
- onBlur?.();
678
- };
679
- const useChoiceCard = React25.useMemo(() => {
680
- return options.some((option) => option.description);
681
- }, [options]);
682
- const groupDescriptionId = description ? `${name}-description` : void 0;
683
- return /* @__PURE__ */ React25.createElement(Field, { className: cn("w-full", className), invalid: Boolean(error) }, (label || description) && /* @__PURE__ */ React25.createElement(Field, { className: "mb-3 gap-1" }, label && /* @__PURE__ */ React25.createElement("div", { className: "text-base font-medium leading-none" }, label), description && /* @__PURE__ */ React25.createElement(
684
- FieldDescription,
685
- {
686
- id: groupDescriptionId,
687
- className: "leading-snug"
688
- },
689
- description
690
- )), /* @__PURE__ */ React25.createElement(
691
- RadioGroup,
692
- {
693
- name,
694
- value,
695
- onValueChange: handleValueChange,
696
- onBlur: handleBlur,
697
- disabled,
698
- required,
699
- className: cn(
700
- "gap-3",
701
- layout === "grid" && "grid grid-cols-1 md:grid-cols-2",
702
- layout === "inline" && "flex flex-wrap"
703
- ),
704
- "aria-invalid": error || props["aria-invalid"],
705
- "aria-describedby": groupDescriptionId || props["aria-describedby"],
706
- "aria-required": required || props["aria-required"]
707
- },
708
- options.map((option) => {
709
- const isSelected = value === option.value;
710
- const isDisabled = disabled || option.disabled;
711
- const radioId = `${name}-${option.value}`;
712
- const hasDescription = !!option.description;
713
- return /* @__PURE__ */ React25.createElement(
714
- FieldLabel,
715
- {
716
- key: option.value,
717
- htmlFor: radioId,
718
- className: cn(
719
- "flex gap-3 p-3 duration-200 select-auto font-normal leading-normal",
720
- useChoiceCard && "border rounded-lg hover:ring-2 hover:ring-ring/50",
721
- useChoiceCard && isSelected && "ring-2 ring-ring",
722
- useChoiceCard && error && "border-destructive",
723
- isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
724
- )
725
- },
726
- /* @__PURE__ */ React25.createElement(
727
- Field,
728
- {
729
- orientation: "horizontal",
730
- className: cn(
731
- "flex w-full gap-3",
732
- useChoiceCard ? "items-start" : "items-center"
733
- )
734
- },
735
- /* @__PURE__ */ React25.createElement(
736
- RadioGroupItem,
737
- {
738
- value: option.value,
739
- id: radioId,
740
- disabled: isDisabled,
741
- className: "mt-0.5",
742
- "aria-describedby": hasDescription ? `${radioId}-description` : void 0
743
- }
744
- ),
745
- /* @__PURE__ */ React25.createElement(Field, { className: "flex-1 gap-1" }, /* @__PURE__ */ React25.createElement("span", { className: "text-sm font-medium leading-none" }, option.label), option.description && /* @__PURE__ */ React25.createElement(
746
- FieldDescription,
747
- {
748
- id: `${radioId}-description`,
749
- className: "leading-snug"
750
- },
751
- option.description
752
- ))
753
- )
754
- );
755
- })
756
- ));
757
- }
758
- Radio.displayName = "Radio";
759
- function Switch({
760
- className,
761
- size = "default",
762
- ...props
763
- }) {
764
- return /* @__PURE__ */ React25.createElement(
765
- Switch$1.Root,
766
- {
767
- "data-slot": "switch",
768
- "data-size": size,
769
- className: cn(
770
- // Core structure - uses CSS variables
771
- "peer group/switch inline-flex shrink-0 items-center rounded-full",
772
- "border border-transparent shadow-xs transition-all outline-none",
773
- // State-based backgrounds - use CSS variables
774
- "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
775
- // Focus state
776
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
777
- // Disabled state
778
- "disabled:cursor-not-allowed disabled:opacity-50",
779
- // Size variants
780
- "data-[size=default]:h-[1.15rem] data-[size=default]:w-8",
781
- "data-[size=sm]:h-3.5 data-[size=sm]:w-6",
782
- className
783
- ),
784
- ...props
785
- },
786
- /* @__PURE__ */ React25.createElement(
787
- Switch$1.Thumb,
788
- {
789
- "data-slot": "switch-thumb",
790
- className: cn(
791
- // Thumb appearance - inherits from parent theme
792
- "bg-background pointer-events-none block rounded-full ring-0 transition-transform",
793
- // Size variants
794
- "group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3",
795
- // Position based on state
796
- "data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
797
- )
798
- }
799
- )
800
- );
801
- }
802
-
803
- // src/inputs/Switch.tsx
804
- function Switch2({
805
- name,
806
- value,
807
- onChange,
808
- onBlur,
809
- disabled = false,
810
- required = false,
811
- error = false,
812
- className = "",
813
- label,
814
- description,
815
- size = "default",
816
- ...props
817
- }) {
818
- const switchId = props.id || `switch-${name}`;
819
- const handleCheckedChange = (checked) => {
820
- onChange(checked);
821
- };
822
- const handleBlur = () => {
823
- onBlur?.();
824
- };
825
- const switchElement = /* @__PURE__ */ React25.createElement(
826
- Switch,
827
- {
828
- id: switchId,
829
- checked: value,
830
- onCheckedChange: handleCheckedChange,
831
- onBlur: handleBlur,
832
- disabled,
833
- size,
834
- "aria-invalid": error || props["aria-invalid"],
835
- "aria-describedby": description ? `${switchId}-description` : props["aria-describedby"],
836
- "aria-required": required || props["aria-required"],
837
- ...props
838
- }
839
- );
840
- if (!label) {
841
- return /* @__PURE__ */ React25.createElement(Field, { className }, switchElement);
842
- }
843
- return /* @__PURE__ */ React25.createElement(Field, { className: "gap-0", invalid: Boolean(error) }, /* @__PURE__ */ React25.createElement(
844
- FieldLabel,
845
- {
846
- htmlFor: switchId,
847
- className: cn(
848
- "flex items-center gap-3 cursor-pointer select-auto font-normal leading-normal",
849
- disabled && "opacity-50 cursor-not-allowed",
850
- className
851
- )
852
- },
853
- switchElement,
854
- /* @__PURE__ */ React25.createElement(Field, { className: "gap-1" }, /* @__PURE__ */ React25.createElement("span", { className: "text-sm font-medium leading-none" }, label), description && /* @__PURE__ */ React25.createElement(
855
- FieldDescription,
856
- {
857
- id: `${switchId}-description`,
858
- className: "leading-snug"
859
- },
860
- description
861
- ))
862
- ));
863
- }
864
- Switch2.displayName = "Switch";
865
- function Select({
866
- ...props
867
- }) {
868
- return /* @__PURE__ */ React25.createElement(Select$1.Root, { "data-slot": "select", ...props });
869
- }
870
- function SelectGroup({
871
- ...props
872
- }) {
873
- return /* @__PURE__ */ React25.createElement(Select$1.Group, { "data-slot": "select-group", ...props });
874
- }
875
- function SelectValue({
876
- ...props
877
- }) {
878
- return /* @__PURE__ */ React25.createElement(Select$1.Value, { "data-slot": "select-value", ...props });
879
- }
880
- function SelectTrigger({
881
- className,
882
- size = "default",
883
- children,
884
- ...props
885
- }) {
886
- return /* @__PURE__ */ React25.createElement(
887
- Select$1.Trigger,
888
- {
889
- "data-slot": "select-trigger",
890
- "data-size": size,
891
- className: cn(
892
- // Core structure - uses CSS variables
893
- "flex w-fit items-center justify-between gap-2 rounded-md border border-input",
894
- "bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs",
895
- "transition-[color,box-shadow] outline-none",
896
- // Focus state
897
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
898
- // Error state
899
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
900
- // Disabled state
901
- "disabled:cursor-not-allowed disabled:opacity-50",
902
- // Size variants
903
- "data-[size=default]:h-9 data-[size=sm]:h-8",
904
- // Value styling
905
- "*:data-[slot=select-value]:line-clamp-1",
906
- "*:data-[slot=select-value]:flex",
907
- "*:data-[slot=select-value]:items-center",
908
- "*:data-[slot=select-value]:gap-2",
909
- // SVG styling
910
- "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
911
- className
912
- ),
913
- ...props
914
- },
915
- children,
916
- /* @__PURE__ */ React25.createElement(Select$1.Icon, { asChild: true }, /* @__PURE__ */ React25.createElement(
917
- "svg",
918
- {
919
- className: "size-4 opacity-50",
920
- viewBox: "0 0 24 24",
921
- fill: "none",
922
- stroke: "currentColor",
923
- strokeWidth: "2",
924
- strokeLinecap: "round",
925
- strokeLinejoin: "round"
926
- },
927
- /* @__PURE__ */ React25.createElement("polyline", { points: "6 9 12 15 18 9" })
928
- ))
929
- );
930
- }
931
- function SelectContent({
932
- className,
933
- children,
934
- position = "item-aligned",
935
- align = "center",
936
- ...props
937
- }) {
938
- return /* @__PURE__ */ React25.createElement(Select$1.Portal, null, /* @__PURE__ */ React25.createElement(
939
- Select$1.Content,
940
- {
941
- "data-slot": "select-content",
942
- className: cn(
943
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
944
- position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
945
- className
946
- ),
947
- position,
948
- align,
949
- ...props
950
- },
951
- /* @__PURE__ */ React25.createElement(SelectScrollUpButton, null),
952
- /* @__PURE__ */ React25.createElement(
953
- Select$1.Viewport,
954
- {
955
- className: cn(
956
- "p-1",
957
- position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
958
- )
959
- },
960
- children
961
- ),
962
- /* @__PURE__ */ React25.createElement(SelectScrollDownButton, null)
963
- ));
964
- }
965
- function SelectLabel({
966
- className,
967
- ...props
968
- }) {
969
- return /* @__PURE__ */ React25.createElement(
970
- Select$1.Label,
971
- {
972
- "data-slot": "select-label",
973
- className: cn("px-2 py-1.5 text-xs opacity-70", className),
974
- ...props
975
- }
976
- );
977
- }
978
- function SelectItem({
979
- className,
980
- children,
981
- ...props
982
- }) {
983
- return /* @__PURE__ */ React25.createElement(
984
- Select$1.Item,
985
- {
986
- "data-slot": "select-item",
987
- className: cn(
988
- // Core structure - inherits text color
989
- "relative flex w-full cursor-default items-center gap-2 rounded-sm",
990
- "py-1.5 pr-8 pl-2 text-sm outline-hidden select-none",
991
- // Focus state - uses accent CSS variable
992
- "focus:bg-accent focus:text-accent-foreground",
993
- // Disabled state
994
- "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
995
- // SVG styling
996
- "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
997
- // Span styling
998
- "*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
999
- className
1000
- ),
1001
- ...props
1002
- },
1003
- /* @__PURE__ */ React25.createElement(
1004
- "span",
1005
- {
1006
- "data-slot": "select-item-indicator",
1007
- className: "absolute right-2 flex size-3.5 items-center justify-center"
1008
- },
1009
- /* @__PURE__ */ React25.createElement(Select$1.ItemIndicator, null, /* @__PURE__ */ React25.createElement(
1010
- "svg",
1011
- {
1012
- className: "size-4",
1013
- viewBox: "0 0 24 24",
1014
- fill: "none",
1015
- stroke: "currentColor",
1016
- strokeWidth: "3",
1017
- strokeLinecap: "round",
1018
- strokeLinejoin: "round"
1019
- },
1020
- /* @__PURE__ */ React25.createElement("polyline", { points: "20 6 9 17 4 12" })
1021
- ))
1022
- ),
1023
- /* @__PURE__ */ React25.createElement(Select$1.ItemText, null, children)
1024
- );
1025
- }
1026
- function SelectScrollUpButton({
1027
- className,
1028
- ...props
1029
- }) {
1030
- return /* @__PURE__ */ React25.createElement(
1031
- Select$1.ScrollUpButton,
1032
- {
1033
- "data-slot": "select-scroll-up-button",
1034
- className: cn(
1035
- "flex cursor-default items-center justify-center py-1",
1036
- className
1037
- ),
1038
- ...props
1039
- },
1040
- /* @__PURE__ */ React25.createElement(
1041
- "svg",
1042
- {
1043
- className: "size-4",
1044
- viewBox: "0 0 24 24",
1045
- fill: "none",
1046
- stroke: "currentColor",
1047
- strokeWidth: "2",
1048
- strokeLinecap: "round",
1049
- strokeLinejoin: "round"
1050
- },
1051
- /* @__PURE__ */ React25.createElement("polyline", { points: "18 15 12 9 6 15" })
1052
- )
1053
- );
1054
- }
1055
- function SelectScrollDownButton({
1056
- className,
1057
- ...props
1058
- }) {
1059
- return /* @__PURE__ */ React25.createElement(
1060
- Select$1.ScrollDownButton,
1061
- {
1062
- "data-slot": "select-scroll-down-button",
1063
- className: cn(
1064
- "flex cursor-default items-center justify-center py-1",
1065
- className
1066
- ),
1067
- ...props
1068
- },
1069
- /* @__PURE__ */ React25.createElement(
1070
- "svg",
1071
- {
1072
- className: "size-4",
1073
- viewBox: "0 0 24 24",
1074
- fill: "none",
1075
- stroke: "currentColor",
1076
- strokeWidth: "2",
1077
- strokeLinecap: "round",
1078
- strokeLinejoin: "round"
1079
- },
1080
- /* @__PURE__ */ React25.createElement("polyline", { points: "6 9 12 15 18 9" })
1081
- )
1082
- );
1083
- }
1084
-
1085
- // src/inputs/Select.tsx
1086
- function Select2({
1087
- name,
1088
- value,
1089
- onChange,
1090
- onBlur,
1091
- onFocus,
1092
- disabled = false,
1093
- required = false,
1094
- error = false,
1095
- className = "",
1096
- placeholder = "Select...",
1097
- options = [],
1098
- optionGroups = [],
1099
- renderOption,
1100
- ...props
1101
- }) {
1102
- const [hasInteracted, setHasInteracted] = React25.useState(false);
1103
- const allOptions = React25.useMemo(() => {
1104
- if (optionGroups.length > 0) {
1105
- return optionGroups.flatMap((group) => group.options);
1106
- }
1107
- return options;
1108
- }, [options, optionGroups]);
1109
- const hasValue = Boolean(value);
1110
- const selectValue = value ? String(value) : void 0;
1111
- const handleValueChange = (newValue) => {
1112
- onChange(newValue);
1113
- };
1114
- const handleOpenChange = (open) => {
1115
- if (open) {
1116
- if (!hasInteracted) {
1117
- setHasInteracted(true);
1118
- }
1119
- onFocus?.();
1120
- } else if (hasInteracted) {
1121
- onBlur?.();
1122
- }
1123
- };
1124
- return /* @__PURE__ */ React25.createElement(React25.Fragment, null, /* @__PURE__ */ React25.createElement(
1125
- "input",
1126
- {
1127
- type: "hidden",
1128
- name,
1129
- value: value ?? "",
1130
- disabled,
1131
- required,
1132
- tabIndex: -1,
1133
- "aria-hidden": "true",
1134
- style: {
1135
- position: "absolute",
1136
- width: "1px",
1137
- height: "1px",
1138
- padding: "0",
1139
- margin: "-1px",
1140
- overflow: "hidden",
1141
- clip: "rect(0, 0, 0, 0)",
1142
- whiteSpace: "nowrap",
1143
- border: "0"
1144
- }
1145
- }
1146
- ), /* @__PURE__ */ React25.createElement(
1147
- Select,
1148
- {
1149
- value: selectValue,
1150
- onValueChange: handleValueChange,
1151
- onOpenChange: handleOpenChange,
1152
- disabled
1153
- },
1154
- /* @__PURE__ */ React25.createElement(
1155
- SelectTrigger,
1156
- {
1157
- className: cn(
1158
- // Valid value indicator - ring-2 when has value and no error
1159
- !error && hasValue && "ring-2 ring-ring",
1160
- // Error state - handled by SelectTrigger via aria-invalid
1161
- className
1162
- ),
1163
- "aria-invalid": error || props["aria-invalid"],
1164
- "aria-describedby": props["aria-describedby"],
1165
- "aria-required": required || props["aria-required"]
1166
- },
1167
- /* @__PURE__ */ React25.createElement(SelectValue, { placeholder })
1168
- ),
1169
- /* @__PURE__ */ React25.createElement(SelectContent, null, optionGroups.length > 0 ? (
1170
- // Render grouped options
1171
- optionGroups.map((group, groupIndex) => /* @__PURE__ */ React25.createElement(SelectGroup, { key: groupIndex }, /* @__PURE__ */ React25.createElement(SelectLabel, null, group.label), group.options.map((option) => /* @__PURE__ */ React25.createElement(
1172
- SelectItem,
1173
- {
1174
- key: option.value,
1175
- value: option.value,
1176
- disabled: option.disabled
1177
- },
1178
- renderOption ? renderOption(option) : option.label
1179
- ))))
1180
- ) : (
1181
- // Render flat options
1182
- allOptions.map((option) => /* @__PURE__ */ React25.createElement(
1183
- SelectItem,
1184
- {
1185
- key: option.value,
1186
- value: option.value,
1187
- disabled: option.disabled
1188
- },
1189
- renderOption ? renderOption(option) : option.label
1190
- ))
1191
- ))
1192
- ));
1193
- }
1194
- Select2.displayName = "Select";
1195
- var buttonVariants = cva(
1196
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
1197
- {
1198
- variants: {
1199
- variant: {
1200
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
1201
- destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20",
1202
- outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
1203
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
1204
- ghost: "hover:bg-accent hover:text-accent-foreground",
1205
- link: "text-primary underline-offset-4 hover:underline"
1206
- },
1207
- size: {
1208
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
1209
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
1210
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
1211
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
1212
- icon: "size-9",
1213
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
1214
- "icon-sm": "size-8",
1215
- "icon-lg": "size-10"
1216
- }
1217
- },
1218
- defaultVariants: {
1219
- variant: "default",
1220
- size: "default"
1221
- }
1222
- }
1223
- );
1224
- function Button({
1225
- className,
1226
- variant = "default",
1227
- size = "default",
1228
- asChild = false,
1229
- ...props
1230
- }) {
1231
- const Comp = asChild ? Slot$1.Root : "button";
1232
- return /* @__PURE__ */ React25.createElement(
1233
- Comp,
1234
- {
1235
- "data-slot": "button",
1236
- "data-variant": variant,
1237
- "data-size": size,
1238
- className: cn(buttonVariants({ variant, size, className })),
1239
- ...props
1240
- }
1241
- );
1242
- }
1243
-
1244
- // src/components/ui/dialog.tsx
1245
- function Dialog({
1246
- ...props
1247
- }) {
1248
- return /* @__PURE__ */ React25.createElement(Dialog$1.Root, { "data-slot": "dialog", ...props });
1249
- }
1250
- function DialogPortal({
1251
- ...props
1252
- }) {
1253
- return /* @__PURE__ */ React25.createElement(Dialog$1.Portal, { "data-slot": "dialog-portal", ...props });
1254
- }
1255
- function DialogClose({
1256
- ...props
1257
- }) {
1258
- return /* @__PURE__ */ React25.createElement(Dialog$1.Close, { "data-slot": "dialog-close", ...props });
1259
- }
1260
- var DialogOverlay = React25.forwardRef(({ className, ...props }, ref) => {
1261
- return /* @__PURE__ */ React25.createElement(
1262
- Dialog$1.Overlay,
1263
- {
1264
- ref,
1265
- "data-slot": "dialog-overlay",
1266
- className: cn(
1267
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
1268
- className
1269
- ),
1270
- ...props
1271
- }
1272
- );
1273
- });
1274
- DialogOverlay.displayName = Dialog$1.Overlay.displayName;
1275
- var DialogContent = React25.forwardRef(({ className, children, showCloseButton = true, ...props }, ref) => {
1276
- return /* @__PURE__ */ React25.createElement(DialogPortal, { "data-slot": "dialog-portal" }, /* @__PURE__ */ React25.createElement(DialogOverlay, null), /* @__PURE__ */ React25.createElement(
1277
- Dialog$1.Content,
1278
- {
1279
- ref,
1280
- "data-slot": "dialog-content",
1281
- className: cn(
1282
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
1283
- className
1284
- ),
1285
- ...props
1286
- },
1287
- children,
1288
- showCloseButton && /* @__PURE__ */ React25.createElement(
1289
- Dialog$1.Close,
1290
- {
1291
- "data-slot": "dialog-close",
1292
- className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
1293
- },
1294
- /* @__PURE__ */ React25.createElement(
1295
- "svg",
1296
- {
1297
- className: "size-4",
1298
- viewBox: "0 0 24 24",
1299
- fill: "none",
1300
- stroke: "currentColor",
1301
- strokeWidth: "2",
1302
- strokeLinecap: "round",
1303
- strokeLinejoin: "round"
1304
- },
1305
- /* @__PURE__ */ React25.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1306
- /* @__PURE__ */ React25.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1307
- ),
1308
- /* @__PURE__ */ React25.createElement("span", { className: "sr-only" }, "Close")
1309
- )
1310
- ));
1311
- });
1312
- DialogContent.displayName = Dialog$1.Content.displayName;
1313
- function DialogHeader({ className, ...props }) {
1314
- return /* @__PURE__ */ React25.createElement(
1315
- "div",
1316
- {
1317
- "data-slot": "dialog-header",
1318
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
1319
- ...props
1320
- }
1321
- );
1322
- }
1323
- function DialogTitle({
1324
- className,
1325
- ...props
1326
- }) {
1327
- return /* @__PURE__ */ React25.createElement(
1328
- Dialog$1.Title,
1329
- {
1330
- "data-slot": "dialog-title",
1331
- className: cn("text-lg leading-none font-semibold", className),
1332
- ...props
1333
- }
1334
- );
1335
- }
1336
-
1337
- // src/components/ui/command.tsx
1338
- function Command({
1339
- className,
1340
- ...props
1341
- }) {
1342
- return /* @__PURE__ */ React25.createElement(
1343
- Command$1,
1344
- {
1345
- "data-slot": "command",
1346
- className: cn(
1347
- "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
1348
- className
1349
- ),
1350
- ...props
1351
- }
1352
- );
1353
- }
1354
- function CommandInput({
1355
- className,
1356
- ...props
1357
- }) {
1358
- return /* @__PURE__ */ React25.createElement(
1359
- "div",
1360
- {
1361
- "data-slot": "command-input-wrapper",
1362
- className: "flex h-9 items-center gap-2 border-b px-3"
1363
- },
1364
- /* @__PURE__ */ React25.createElement(
1365
- "svg",
1366
- {
1367
- className: "size-4 shrink-0 opacity-50",
1368
- viewBox: "0 0 24 24",
1369
- fill: "none",
1370
- stroke: "currentColor",
1371
- strokeWidth: "2",
1372
- strokeLinecap: "round",
1373
- strokeLinejoin: "round"
1374
- },
1375
- /* @__PURE__ */ React25.createElement("circle", { cx: "11", cy: "11", r: "8" }),
1376
- /* @__PURE__ */ React25.createElement("path", { d: "m21 21-4.3-4.3" })
1377
- ),
1378
- /* @__PURE__ */ React25.createElement(
1379
- Command$1.Input,
1380
- {
1381
- "data-slot": "command-input",
1382
- className: cn(
1383
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
1384
- className
1385
- ),
1386
- ...props
1387
- }
1388
- )
1389
- );
1390
- }
1391
- function CommandList({
1392
- className,
1393
- ...props
1394
- }) {
1395
- return /* @__PURE__ */ React25.createElement(
1396
- Command$1.List,
1397
- {
1398
- "data-slot": "command-list",
1399
- className: cn(
1400
- "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
1401
- className
1402
- ),
1403
- ...props
1404
- }
1405
- );
1406
- }
1407
- function CommandEmpty({
1408
- ...props
1409
- }) {
1410
- return /* @__PURE__ */ React25.createElement(
1411
- Command$1.Empty,
1412
- {
1413
- "data-slot": "command-empty",
1414
- className: "py-6 text-center text-sm",
1415
- ...props
1416
- }
1417
- );
1418
- }
1419
- function CommandGroup({
1420
- className,
1421
- ...props
1422
- }) {
1423
- return /* @__PURE__ */ React25.createElement(
1424
- Command$1.Group,
1425
- {
1426
- "data-slot": "command-group",
1427
- className: cn(
1428
- "overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:opacity-70",
1429
- className
1430
- ),
1431
- ...props
1432
- }
1433
- );
1434
- }
1435
- function Popover({
1436
- ...props
1437
- }) {
1438
- return /* @__PURE__ */ React25.createElement(Popover$1.Root, { "data-slot": "popover", ...props });
1439
- }
1440
- function PopoverTrigger({
1441
- ...props
1442
- }) {
1443
- return /* @__PURE__ */ React25.createElement(Popover$1.Trigger, { "data-slot": "popover-trigger", ...props });
1444
- }
1445
- function PopoverContent({
1446
- className,
1447
- align = "center",
1448
- sideOffset = 4,
1449
- ...props
1450
- }) {
1451
- return /* @__PURE__ */ React25.createElement(Popover$1.Portal, null, /* @__PURE__ */ React25.createElement(
1452
- Popover$1.Content,
1453
- {
1454
- "data-slot": "popover-content",
1455
- align,
1456
- sideOffset,
1457
- className: cn(
1458
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
1459
- className
1460
- ),
1461
- ...props
1462
- }
1463
- ));
1464
- }
1465
-
1466
- // src/inputs/MultiSelect.tsx
1467
- function ensureResizeObserver() {
1468
- if (typeof window === "undefined") return;
1469
- const windowWithResizeObserver = window;
1470
- if (windowWithResizeObserver.ResizeObserver) return;
1471
- windowWithResizeObserver.ResizeObserver = class ResizeObserverMock {
1472
- observe() {
1473
- }
1474
- unobserve() {
1475
- }
1476
- disconnect() {
1477
- }
1478
- };
1479
- if (typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.scrollIntoView !== "function") {
1480
- HTMLElement.prototype.scrollIntoView = () => {
1481
- };
1482
- }
1483
- }
1484
- function optionLabelText(option) {
1485
- if (typeof option.label === "string") {
1486
- return option.label;
1487
- }
1488
- return String(option.label);
1489
- }
1490
- function MultiSelect({
1491
- name,
1492
- value = [],
1493
- onChange,
1494
- onBlur,
1495
- onFocus,
1496
- disabled = false,
1497
- required = false,
1498
- error = false,
1499
- className = "",
1500
- placeholder = "Select...",
1501
- searchable = true,
1502
- clearable = true,
1503
- loading = false,
1504
- maxSelections,
1505
- showSelectAll = false,
1506
- options = [],
1507
- optionGroups = [],
1508
- renderOption,
1509
- renderValue,
1510
- ...props
1511
- }) {
1512
- const [isOpen, setIsOpen] = React25.useState(false);
1513
- const [searchQuery, setSearchQuery] = React25.useState("");
1514
- const [focusedIndex, setFocusedIndex] = React25.useState(-1);
1515
- const [hasInteracted, setHasInteracted] = React25.useState(false);
1516
- const triggerRef = React25.useRef(null);
1517
- const dropdownId = `${name}-dropdown`;
1518
- const searchInputId = `${name}-search`;
1519
- ensureResizeObserver();
1520
- const allOptions = React25.useMemo(() => {
1521
- if (optionGroups.length > 0) {
1522
- return optionGroups.flatMap((group) => group.options);
1523
- }
1524
- return options;
1525
- }, [options, optionGroups]);
1526
- const filteredOptions = React25.useMemo(() => {
1527
- if (!searchQuery.trim()) {
1528
- return allOptions;
1529
- }
1530
- const query = searchQuery.toLowerCase();
1531
- return allOptions.filter(
1532
- (option) => optionLabelText(option).toLowerCase().includes(query)
1533
- );
1534
- }, [allOptions, searchQuery]);
1535
- const selectedOptions = React25.useMemo(() => {
1536
- return allOptions.filter((option) => value.includes(option.value));
1537
- }, [allOptions, value]);
1538
- const hasValue = value.length > 0;
1539
- const isMaxReached = React25.useMemo(() => {
1540
- return maxSelections !== void 0 && value.length >= maxSelections;
1541
- }, [maxSelections, value.length]);
1542
- const getEnabledOptions = React25.useCallback(() => {
1543
- return filteredOptions.filter(
1544
- (option) => !option.disabled && (!isMaxReached || value.includes(option.value))
1545
- );
1546
- }, [filteredOptions, isMaxReached, value]);
1547
- React25.useEffect(() => {
1548
- if (!isOpen) return;
1549
- if (!searchable) return;
1550
- const id = window.setTimeout(() => {
1551
- const searchInput = document.getElementById(
1552
- searchInputId
1553
- );
1554
- searchInput?.focus();
1555
- }, 0);
1556
- return () => {
1557
- window.clearTimeout(id);
1558
- };
1559
- }, [isOpen, searchable, searchInputId]);
1560
- const handleToggleOption = React25.useCallback(
1561
- (optionValue) => {
1562
- const isSelected = value.includes(optionValue);
1563
- if (isSelected) {
1564
- onChange(value.filter((entry) => entry !== optionValue));
1565
- } else if (!isMaxReached) {
1566
- onChange([...value, optionValue]);
1567
- }
1568
- setSearchQuery("");
1569
- },
1570
- [isMaxReached, onChange, value]
1571
- );
1572
- const handleSelectAll = React25.useCallback(() => {
1573
- const enabledOptions = filteredOptions.filter((option) => !option.disabled);
1574
- onChange(enabledOptions.map((option) => option.value));
1575
- setSearchQuery("");
1576
- }, [filteredOptions, onChange]);
1577
- const handleClearAll = React25.useCallback(
1578
- (e) => {
1579
- e.stopPropagation();
1580
- onChange([]);
1581
- setSearchQuery("");
1582
- setFocusedIndex(-1);
1583
- },
1584
- [onChange]
1585
- );
1586
- const handleRemoveValue = React25.useCallback(
1587
- (optionValue, e) => {
1588
- e.stopPropagation();
1589
- onChange(value.filter((entry) => entry !== optionValue));
1590
- },
1591
- [onChange, value]
1592
- );
1593
- const handleOpenChange = React25.useCallback(
1594
- (nextOpen) => {
1595
- if (disabled) {
1596
- setIsOpen(false);
1597
- return;
1598
- }
1599
- if (nextOpen) {
1600
- if (!hasInteracted) {
1601
- setHasInteracted(true);
1602
- }
1603
- setIsOpen(true);
1604
- onFocus?.();
1605
- return;
1606
- }
1607
- if (isOpen && hasInteracted) {
1608
- onBlur?.();
1609
- }
1610
- setIsOpen(false);
1611
- setSearchQuery("");
1612
- setFocusedIndex(-1);
1613
- },
1614
- [disabled, hasInteracted, isOpen, onBlur, onFocus]
1615
- );
1616
- const handleTriggerBlur = React25.useCallback(() => {
1617
- if (!isOpen) {
1618
- onBlur?.();
1619
- }
1620
- }, [isOpen, onBlur]);
1621
- const handleKeyDown = React25.useCallback(
1622
- (event) => {
1623
- if (disabled) return;
1624
- const enabledOptions = getEnabledOptions();
1625
- switch (event.key) {
1626
- case "ArrowDown": {
1627
- event.preventDefault();
1628
- if (!isOpen) {
1629
- setHasInteracted(true);
1630
- setIsOpen(true);
1631
- onFocus?.();
1632
- if (enabledOptions.length > 0) {
1633
- setFocusedIndex(filteredOptions.indexOf(enabledOptions[0]));
1634
- }
1635
- return;
1636
- }
1637
- if (enabledOptions.length === 0) return;
1638
- const currentOption = filteredOptions[focusedIndex];
1639
- const currentEnabledIndex = enabledOptions.findIndex(
1640
- (option) => option === currentOption
1641
- );
1642
- const nextEnabledIndex = currentEnabledIndex === -1 ? 0 : (currentEnabledIndex + 1) % enabledOptions.length;
1643
- setFocusedIndex(filteredOptions.indexOf(enabledOptions[nextEnabledIndex]));
1644
- break;
1645
- }
1646
- case "ArrowUp": {
1647
- event.preventDefault();
1648
- if (!isOpen || enabledOptions.length === 0) return;
1649
- const currentOption = filteredOptions[focusedIndex];
1650
- const currentEnabledIndex = enabledOptions.findIndex(
1651
- (option) => option === currentOption
1652
- );
1653
- const previousEnabledIndex = currentEnabledIndex === -1 ? enabledOptions.length - 1 : (currentEnabledIndex - 1 + enabledOptions.length) % enabledOptions.length;
1654
- setFocusedIndex(
1655
- filteredOptions.indexOf(enabledOptions[previousEnabledIndex])
1656
- );
1657
- break;
1658
- }
1659
- case "Enter": {
1660
- event.preventDefault();
1661
- if (isOpen && focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
1662
- const focusedOption = filteredOptions[focusedIndex];
1663
- const optionDisabled = focusedOption.disabled || isMaxReached && !value.includes(focusedOption.value);
1664
- if (!optionDisabled) {
1665
- handleToggleOption(focusedOption.value);
1666
- }
1667
- return;
1668
- }
1669
- if (!isOpen) {
1670
- setHasInteracted(true);
1671
- setIsOpen(true);
1672
- onFocus?.();
1673
- }
1674
- break;
1675
- }
1676
- case "Escape": {
1677
- if (!isOpen) return;
1678
- event.preventDefault();
1679
- setIsOpen(false);
1680
- setSearchQuery("");
1681
- setFocusedIndex(-1);
1682
- break;
1683
- }
1684
- case " ": {
1685
- if (isOpen && focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
1686
- event.preventDefault();
1687
- const focusedOption = filteredOptions[focusedIndex];
1688
- const optionDisabled = focusedOption.disabled || isMaxReached && !value.includes(focusedOption.value);
1689
- if (!optionDisabled) {
1690
- handleToggleOption(focusedOption.value);
1691
- }
1692
- return;
1693
- }
1694
- if (!isOpen && !searchable) {
1695
- event.preventDefault();
1696
- setHasInteracted(true);
1697
- setIsOpen(true);
1698
- onFocus?.();
1699
- }
1700
- break;
1701
- }
1702
- }
1703
- },
1704
- [
1705
- disabled,
1706
- filteredOptions,
1707
- focusedIndex,
1708
- getEnabledOptions,
1709
- handleToggleOption,
1710
- isMaxReached,
1711
- isOpen,
1712
- onFocus,
1713
- searchable,
1714
- value
1715
- ]
1716
- );
1717
- const combinedClassName = cn("relative w-full", className);
1718
- return /* @__PURE__ */ React25.createElement("div", { className: combinedClassName }, /* @__PURE__ */ React25.createElement(
1719
- "select",
1720
- {
1721
- name,
1722
- value,
1723
- onChange: () => {
1724
- },
1725
- disabled,
1726
- required,
1727
- "aria-hidden": "true",
1728
- tabIndex: -1,
1729
- style: { display: "none" },
1730
- multiple: true
1731
- },
1732
- /* @__PURE__ */ React25.createElement("option", { value: "" }, "Select..."),
1733
- allOptions.map((option) => /* @__PURE__ */ React25.createElement("option", { key: option.value, value: option.value }, optionLabelText(option)))
1734
- ), /* @__PURE__ */ React25.createElement(Popover, { open: isOpen, onOpenChange: handleOpenChange }, /* @__PURE__ */ React25.createElement(PopoverTrigger, { asChild: true }, /* @__PURE__ */ React25.createElement(
1735
- "div",
1736
- {
1737
- ref: triggerRef,
1738
- className: cn(
1739
- "flex min-h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm",
1740
- "cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1741
- !error && hasValue && "ring-2 ring-ring",
1742
- disabled && "cursor-not-allowed opacity-50 pointer-events-none",
1743
- error && "border-destructive ring-1 ring-destructive"
1744
- ),
1745
- onKeyDown: handleKeyDown,
1746
- onBlur: handleTriggerBlur,
1747
- role: "combobox",
1748
- "aria-expanded": isOpen,
1749
- "aria-controls": dropdownId,
1750
- "aria-invalid": error || props["aria-invalid"],
1751
- "aria-describedby": props["aria-describedby"],
1752
- "aria-required": required || props["aria-required"],
1753
- "aria-disabled": disabled,
1754
- tabIndex: disabled ? -1 : 0
1755
- },
1756
- /* @__PURE__ */ React25.createElement("div", { className: "flex flex-1 items-center overflow-hidden" }, selectedOptions.length > 0 ? /* @__PURE__ */ React25.createElement("div", { className: "flex flex-wrap gap-1" }, selectedOptions.map((option) => /* @__PURE__ */ React25.createElement(
1757
- "span",
1758
- {
1759
- key: option.value,
1760
- className: "inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium"
1761
- },
1762
- renderValue ? renderValue(option) : /* @__PURE__ */ React25.createElement(React25.Fragment, null, /* @__PURE__ */ React25.createElement("span", { className: "max-w-40 overflow-hidden text-ellipsis whitespace-nowrap" }, option.label), !disabled && /* @__PURE__ */ React25.createElement(
1763
- "button",
1764
- {
1765
- type: "button",
1766
- className: "flex h-3.5 w-3.5 items-center justify-center rounded-sm border-none bg-transparent p-0 text-[0.625rem] transition-opacity hover:opacity-70",
1767
- onClick: (e) => handleRemoveValue(option.value, e),
1768
- "aria-label": `Remove ${optionLabelText(option)}`,
1769
- tabIndex: -1
1770
- },
1771
- "\u2715"
1772
- ))
1773
- ))) : /* @__PURE__ */ React25.createElement("span", { className: "relative" }, placeholder)),
1774
- /* @__PURE__ */ React25.createElement("div", { className: "ml-2 flex items-center gap-1" }, loading && /* @__PURE__ */ React25.createElement("span", { className: "text-xs" }, "\u23F3"), clearable && value.length > 0 && !disabled && !loading && /* @__PURE__ */ React25.createElement(
1775
- "button",
1776
- {
1777
- type: "button",
1778
- className: "flex h-4 w-4 items-center justify-center rounded-sm border-none bg-transparent p-0 text-xs transition-opacity hover:opacity-70",
1779
- onClick: handleClearAll,
1780
- "aria-label": "Clear all selections",
1781
- tabIndex: -1
1782
- },
1783
- "\u2715"
1784
- ), /* @__PURE__ */ React25.createElement("span", { className: "text-xs leading-none", "aria-hidden": "true" }, isOpen ? "\u25B2" : "\u25BC"))
1785
- )), isOpen && /* @__PURE__ */ React25.createElement(
1786
- PopoverContent,
1787
- {
1788
- id: dropdownId,
1789
- align: "start",
1790
- sideOffset: 4,
1791
- className: "w-full min-w-[var(--radix-popover-trigger-width)] p-0",
1792
- onOpenAutoFocus: (event) => {
1793
- event.preventDefault();
1794
- }
1795
- },
1796
- /* @__PURE__ */ React25.createElement(
1797
- Command,
1798
- {
1799
- shouldFilter: false,
1800
- className: "max-h-80",
1801
- onKeyDown: handleKeyDown
1802
- },
1803
- searchable && /* @__PURE__ */ React25.createElement(
1804
- CommandInput,
1805
- {
1806
- id: searchInputId,
1807
- className: cn(INPUT_AUTOFILL_RESET_CLASSES),
1808
- placeholder: "Search...",
1809
- value: searchQuery,
1810
- onValueChange: (nextValue) => {
1811
- setSearchQuery(nextValue);
1812
- setFocusedIndex(0);
1813
- },
1814
- "aria-label": "Search options"
1815
- }
1816
- ),
1817
- showSelectAll && filteredOptions.length > 0 && /* @__PURE__ */ React25.createElement("div", { className: "flex gap-2 border-b border-input p-2" }, /* @__PURE__ */ React25.createElement(
1818
- "button",
1819
- {
1820
- type: "button",
1821
- className: "flex-1 rounded border border-input bg-transparent px-3 py-1.5 text-xs font-medium transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
1822
- onClick: handleSelectAll,
1823
- disabled
1824
- },
1825
- "Select All"
1826
- ), value.length > 0 && /* @__PURE__ */ React25.createElement(
1827
- "button",
1828
- {
1829
- type: "button",
1830
- className: "flex-1 rounded border border-input bg-transparent px-3 py-1.5 text-xs font-medium transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
1831
- onClick: handleClearAll,
1832
- disabled
1833
- },
1834
- "Clear All"
1835
- )),
1836
- isMaxReached && /* @__PURE__ */ React25.createElement("div", { className: "border-b border-destructive bg-destructive/80 px-2 py-1 text-xs font-medium text-destructive-foreground" }, "Maximum ", maxSelections, " selection", maxSelections !== 1 ? "s" : "", " ", "reached"),
1837
- /* @__PURE__ */ React25.createElement(CommandList, { role: "listbox", "aria-multiselectable": "true" }, /* @__PURE__ */ React25.createElement(CommandEmpty, null, "No options found"), optionGroups.length > 0 ? optionGroups.map((group, groupIndex) => {
1838
- const groupOptions = group.options.filter(
1839
- (option) => filteredOptions.includes(option)
1840
- );
1841
- if (groupOptions.length === 0) return null;
1842
- return /* @__PURE__ */ React25.createElement(
1843
- CommandGroup,
1844
- {
1845
- key: `${group.label}-${groupIndex}`,
1846
- heading: group.label
1847
- },
1848
- groupOptions.map((option) => {
1849
- const globalIndex = filteredOptions.indexOf(option);
1850
- const isSelected = value.includes(option.value);
1851
- const isFocused = globalIndex === focusedIndex;
1852
- const optionDisabled = option.disabled || isMaxReached && !isSelected;
1853
- return /* @__PURE__ */ React25.createElement(
1854
- "div",
1855
- {
1856
- key: option.value,
1857
- role: "option",
1858
- "aria-selected": isSelected,
1859
- "aria-disabled": optionDisabled,
1860
- onMouseEnter: () => {
1861
- setFocusedIndex(globalIndex);
1862
- },
1863
- onClick: () => {
1864
- if (!optionDisabled) {
1865
- handleToggleOption(option.value);
1866
- }
1867
- },
1868
- className: cn(
1869
- "relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent",
1870
- isFocused && "bg-accent",
1871
- isSelected && "bg-accent font-medium",
1872
- optionDisabled && "pointer-events-none opacity-50"
1873
- )
1874
- },
1875
- /* @__PURE__ */ React25.createElement("span", { className: "text-base leading-none" }, isSelected ? "\u2611" : "\u2610"),
1876
- /* @__PURE__ */ React25.createElement("span", { className: "flex-1" }, renderOption ? renderOption(option) : option.label)
1877
- );
1878
- })
1879
- );
1880
- }) : filteredOptions.map((option, index) => {
1881
- const isSelected = value.includes(option.value);
1882
- const isFocused = index === focusedIndex;
1883
- const optionDisabled = option.disabled || isMaxReached && !isSelected;
1884
- return /* @__PURE__ */ React25.createElement(
1885
- "div",
1886
- {
1887
- key: option.value,
1888
- role: "option",
1889
- "aria-selected": isSelected,
1890
- "aria-disabled": optionDisabled,
1891
- onMouseEnter: () => {
1892
- setFocusedIndex(index);
1893
- },
1894
- onClick: () => {
1895
- if (!optionDisabled) {
1896
- handleToggleOption(option.value);
1897
- }
1898
- },
1899
- className: cn(
1900
- "relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent",
1901
- isFocused && "bg-accent",
1902
- isSelected && "bg-accent font-medium",
1903
- optionDisabled && "pointer-events-none opacity-50"
1904
- )
1905
- },
1906
- /* @__PURE__ */ React25.createElement("span", { className: "text-base leading-none" }, isSelected ? "\u2611" : "\u2610"),
1907
- /* @__PURE__ */ React25.createElement("span", { className: "flex-1" }, renderOption ? renderOption(option) : option.label)
1908
- );
1909
- }))
1910
- )
1911
- )));
1912
- }
1913
- MultiSelect.displayName = "MultiSelect";
1914
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React25.useLayoutEffect : React25.useEffect;
1915
-
1916
- // src/hooks/use-as-ref.ts
1917
- function useAsRef(props) {
1918
- const ref = React25.useRef(props);
1919
- useIsomorphicLayoutEffect(() => {
1920
- ref.current = props;
1921
- });
1922
- return ref;
1923
- }
1924
- function useLazyRef(fn) {
1925
- const ref = React25.useRef(null);
1926
- if (ref.current === null) {
1927
- ref.current = fn();
1928
- }
1929
- return ref;
1930
- }
1931
-
1932
- // src/components/ui/file-upload.tsx
1933
- var ROOT_NAME = "FileUpload";
1934
- var DROPZONE_NAME = "FileUploadDropzone";
1935
- var LIST_NAME = "FileUploadList";
1936
- var ITEM_NAME = "FileUploadItem";
1937
- var ITEM_PREVIEW_NAME = "FileUploadItemPreview";
1938
- var ITEM_METADATA_NAME = "FileUploadItemMetadata";
1939
- var ITEM_DELETE_NAME = "FileUploadItemDelete";
1940
- function BaseFileIcon({
1941
- children,
1942
- className
1943
- }) {
1944
- return /* @__PURE__ */ React25.createElement(
1945
- "svg",
1946
- {
1947
- viewBox: "0 0 24 24",
1948
- fill: "none",
1949
- stroke: "currentColor",
1950
- strokeWidth: "2",
1951
- strokeLinecap: "round",
1952
- strokeLinejoin: "round",
1953
- className: cn("size-5", className),
1954
- "aria-hidden": "true"
1955
- },
1956
- children
1957
- );
1958
- }
1959
- function FileVideoIcon() {
1960
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("rect", { x: "8", y: "12", width: "6", height: "4", rx: "1" }), /* @__PURE__ */ React25.createElement("path", { d: "m14 13 3-1.5v5L14 15" }));
1961
- }
1962
- function FileAudioIcon() {
1963
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("path", { d: "M10 16a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z" }), /* @__PURE__ */ React25.createElement("path", { d: "M13 17V11l3-1" }));
1964
- }
1965
- function FileTextIcon() {
1966
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("line", { x1: "8", y1: "13", x2: "16", y2: "13" }), /* @__PURE__ */ React25.createElement("line", { x1: "8", y1: "17", x2: "14", y2: "17" }));
1967
- }
1968
- function FileCodeIcon() {
1969
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("polyline", { points: "11 14 9 16 11 18" }), /* @__PURE__ */ React25.createElement("polyline", { points: "13 14 15 16 13 18" }));
1970
- }
1971
- function FileArchiveIcon() {
1972
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("rect", { x: "9", y: "11", width: "6", height: "2" }), /* @__PURE__ */ React25.createElement("path", { d: "M12 13v5" }));
1973
- }
1974
- function FileCogIcon() {
1975
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }), /* @__PURE__ */ React25.createElement("circle", { cx: "12", cy: "16", r: "2" }), /* @__PURE__ */ React25.createElement("path", { d: "m12 12 .4.9m2.7 1.1 .9.4m-.9 2.7-.9.4m-2.7 1.1-.4.9m-2.3-.9-.4-.9m-2.7-1.1-.9-.4m.9-2.7.9-.4m2.7-1.1.4-.9" }));
1976
- }
1977
- function FileIcon() {
1978
- return /* @__PURE__ */ React25.createElement(BaseFileIcon, null, /* @__PURE__ */ React25.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React25.createElement("polyline", { points: "14 2 14 8 20 8" }));
1979
- }
1980
- function formatBytes(bytes) {
1981
- if (bytes === 0) return "0 B";
1982
- const sizes = ["B", "KB", "MB", "GB", "TB"];
1983
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
1984
- return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`;
1985
- }
1986
- function getFileIcon(file) {
1987
- const type = file.type;
1988
- const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
1989
- if (type.startsWith("video/")) {
1990
- return /* @__PURE__ */ React25.createElement(FileVideoIcon, null);
1991
- }
1992
- if (type.startsWith("audio/")) {
1993
- return /* @__PURE__ */ React25.createElement(FileAudioIcon, null);
1994
- }
1995
- if (type.startsWith("text/") || ["txt", "md", "rtf", "pdf"].includes(extension)) {
1996
- return /* @__PURE__ */ React25.createElement(FileTextIcon, null);
1997
- }
1998
- if ([
1999
- "html",
2000
- "css",
2001
- "js",
2002
- "jsx",
2003
- "ts",
2004
- "tsx",
2005
- "json",
2006
- "xml",
2007
- "php",
2008
- "py",
2009
- "rb",
2010
- "java",
2011
- "c",
2012
- "cpp",
2013
- "cs"
2014
- ].includes(extension)) {
2015
- return /* @__PURE__ */ React25.createElement(FileCodeIcon, null);
2016
- }
2017
- if (["zip", "rar", "7z", "tar", "gz", "bz2"].includes(extension)) {
2018
- return /* @__PURE__ */ React25.createElement(FileArchiveIcon, null);
2019
- }
2020
- if (["exe", "msi", "app", "apk", "deb", "rpm"].includes(extension) || type.startsWith("application/")) {
2021
- return /* @__PURE__ */ React25.createElement(FileCogIcon, null);
2022
- }
2023
- return /* @__PURE__ */ React25.createElement(FileIcon, null);
2024
- }
2025
- var StoreContext = React25.createContext(null);
2026
- function useStoreContext(consumerName) {
2027
- const context = React25.useContext(StoreContext);
2028
- if (!context) {
2029
- throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
2030
- }
2031
- return context;
2032
- }
2033
- function useStore(selector) {
2034
- const store = useStoreContext("useStore");
2035
- const lastValueRef = useLazyRef(
2036
- () => null
2037
- );
2038
- const getSnapshot = React25.useCallback(() => {
2039
- const state = store.getState();
2040
- const prevValue = lastValueRef.current;
2041
- if (prevValue && prevValue.state === state) {
2042
- return prevValue.value;
2043
- }
2044
- const nextValue = selector(state);
2045
- lastValueRef.current = { value: nextValue, state };
2046
- return nextValue;
2047
- }, [store, selector, lastValueRef]);
2048
- return React25.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
2049
- }
2050
- var FileUploadContext = React25.createContext(
2051
- null
2052
- );
2053
- function useFileUploadContext(consumerName) {
2054
- const context = React25.useContext(FileUploadContext);
2055
- if (!context) {
2056
- throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
2057
- }
2058
- return context;
2059
- }
2060
- function FileUpload(props) {
2061
- const {
2062
- value,
2063
- defaultValue,
2064
- onValueChange,
2065
- onAccept,
2066
- onFileAccept,
2067
- onFileReject,
2068
- onFileValidate,
2069
- onUpload,
2070
- accept,
2071
- maxFiles,
2072
- maxSize,
2073
- dir: dirProp,
2074
- label,
2075
- name,
2076
- asChild,
2077
- disabled = false,
2078
- invalid = false,
2079
- multiple = false,
2080
- required = false,
2081
- inputProps,
2082
- children,
2083
- className,
2084
- ...rootProps
2085
- } = props;
2086
- const inputId = React25.useId();
2087
- const dropzoneId = React25.useId();
2088
- const listId = React25.useId();
2089
- const labelId = React25.useId();
2090
- const dir = useDirection(dirProp);
2091
- const listeners = useLazyRef(() => /* @__PURE__ */ new Set()).current;
2092
- const files = useLazyRef(() => /* @__PURE__ */ new Map()).current;
2093
- const urlCache = useLazyRef(() => /* @__PURE__ */ new WeakMap()).current;
2094
- const inputRef = React25.useRef(null);
2095
- const isControlled = value !== void 0;
2096
- const propsRef = useAsRef({
2097
- onValueChange,
2098
- onAccept,
2099
- onFileAccept,
2100
- onFileReject,
2101
- onFileValidate,
2102
- onUpload
2103
- });
2104
- const store = React25.useMemo(() => {
2105
- let state = {
2106
- files,
2107
- dragOver: false,
2108
- invalid
2109
- };
2110
- function reducer(state2, action) {
2111
- switch (action.type) {
2112
- case "ADD_FILES": {
2113
- for (const file of action.files) {
2114
- files.set(file, {
2115
- file,
2116
- progress: 0,
2117
- status: "idle"
2118
- });
2119
- }
2120
- if (propsRef.current.onValueChange) {
2121
- const fileList = Array.from(files.values()).map(
2122
- (fileState) => fileState.file
2123
- );
2124
- propsRef.current.onValueChange(fileList);
2125
- }
2126
- return { ...state2, files };
2127
- }
2128
- case "SET_FILES": {
2129
- const newFileSet = new Set(action.files);
2130
- for (const existingFile of files.keys()) {
2131
- if (!newFileSet.has(existingFile)) {
2132
- files.delete(existingFile);
2133
- }
2134
- }
2135
- for (const file of action.files) {
2136
- const existingState = files.get(file);
2137
- if (!existingState) {
2138
- files.set(file, {
2139
- file,
2140
- progress: 0,
2141
- status: "idle"
2142
- });
2143
- }
2144
- }
2145
- return { ...state2, files };
2146
- }
2147
- case "SET_PROGRESS": {
2148
- const fileState = files.get(action.file);
2149
- if (fileState) {
2150
- files.set(action.file, {
2151
- ...fileState,
2152
- progress: action.progress,
2153
- status: "uploading"
2154
- });
2155
- }
2156
- return { ...state2, files };
2157
- }
2158
- case "SET_SUCCESS": {
2159
- const fileState = files.get(action.file);
2160
- if (fileState) {
2161
- files.set(action.file, {
2162
- ...fileState,
2163
- progress: 100,
2164
- status: "success"
2165
- });
2166
- }
2167
- return { ...state2, files };
2168
- }
2169
- case "SET_ERROR": {
2170
- const fileState = files.get(action.file);
2171
- if (fileState) {
2172
- files.set(action.file, {
2173
- ...fileState,
2174
- error: action.error,
2175
- status: "error"
2176
- });
2177
- }
2178
- return { ...state2, files };
2179
- }
2180
- case "REMOVE_FILE": {
2181
- const cachedUrl = urlCache.get(action.file);
2182
- if (cachedUrl) {
2183
- URL.revokeObjectURL(cachedUrl);
2184
- urlCache.delete(action.file);
2185
- }
2186
- files.delete(action.file);
2187
- if (propsRef.current.onValueChange) {
2188
- const fileList = Array.from(files.values()).map(
2189
- (fileState) => fileState.file
2190
- );
2191
- propsRef.current.onValueChange(fileList);
2192
- }
2193
- return { ...state2, files };
2194
- }
2195
- case "SET_DRAG_OVER": {
2196
- return { ...state2, dragOver: action.dragOver };
2197
- }
2198
- case "SET_INVALID": {
2199
- return { ...state2, invalid: action.invalid };
2200
- }
2201
- case "CLEAR": {
2202
- for (const file of files.keys()) {
2203
- const cachedUrl = urlCache.get(file);
2204
- if (cachedUrl) {
2205
- URL.revokeObjectURL(cachedUrl);
2206
- urlCache.delete(file);
2207
- }
2208
- }
2209
- files.clear();
2210
- if (propsRef.current.onValueChange) {
2211
- propsRef.current.onValueChange([]);
2212
- }
2213
- return { ...state2, files, invalid: false };
2214
- }
2215
- default:
2216
- return state2;
2217
- }
2218
- }
2219
- return {
2220
- getState: () => state,
2221
- dispatch: (action) => {
2222
- state = reducer(state, action);
2223
- for (const listener of listeners) {
2224
- listener();
2225
- }
2226
- },
2227
- subscribe: (listener) => {
2228
- listeners.add(listener);
2229
- return () => listeners.delete(listener);
2230
- }
2231
- };
2232
- }, [listeners, files, invalid, propsRef, urlCache]);
2233
- const acceptTypes = React25.useMemo(
2234
- () => accept?.split(",").map((t) => t.trim()) ?? null,
2235
- [accept]
2236
- );
2237
- const onProgress = useLazyRef(() => {
2238
- let frame = 0;
2239
- return (file, progress) => {
2240
- if (frame) return;
2241
- frame = requestAnimationFrame(() => {
2242
- frame = 0;
2243
- store.dispatch({
2244
- type: "SET_PROGRESS",
2245
- file,
2246
- progress: Math.min(Math.max(0, progress), 100)
2247
- });
2248
- });
2249
- };
2250
- }).current;
2251
- React25.useEffect(() => {
2252
- if (isControlled) {
2253
- store.dispatch({ type: "SET_FILES", files: value });
2254
- } else if (defaultValue && defaultValue.length > 0 && !store.getState().files.size) {
2255
- store.dispatch({ type: "SET_FILES", files: defaultValue });
2256
- }
2257
- }, [value, defaultValue, isControlled, store]);
2258
- React25.useEffect(() => {
2259
- return () => {
2260
- for (const file of files.keys()) {
2261
- const cachedUrl = urlCache.get(file);
2262
- if (cachedUrl) {
2263
- URL.revokeObjectURL(cachedUrl);
2264
- }
2265
- }
2266
- };
2267
- }, [files, urlCache]);
2268
- const onFilesUpload = React25.useCallback(
2269
- async (files2) => {
2270
- try {
2271
- for (const file of files2) {
2272
- store.dispatch({ type: "SET_PROGRESS", file, progress: 0 });
2273
- }
2274
- if (propsRef.current.onUpload) {
2275
- await propsRef.current.onUpload(files2, {
2276
- onProgress,
2277
- onSuccess: (file) => {
2278
- store.dispatch({ type: "SET_SUCCESS", file });
2279
- },
2280
- onError: (file, error) => {
2281
- store.dispatch({
2282
- type: "SET_ERROR",
2283
- file,
2284
- error: error.message ?? "Upload failed"
2285
- });
2286
- }
2287
- });
2288
- } else {
2289
- for (const file of files2) {
2290
- store.dispatch({ type: "SET_SUCCESS", file });
2291
- }
2292
- }
2293
- } catch (error) {
2294
- const errorMessage = error instanceof Error ? error.message : "Upload failed";
2295
- for (const file of files2) {
2296
- store.dispatch({
2297
- type: "SET_ERROR",
2298
- file,
2299
- error: errorMessage
2300
- });
2301
- }
2302
- }
2303
- },
2304
- [store, propsRef, onProgress]
2305
- );
2306
- const onFilesChange = React25.useCallback(
2307
- (originalFiles) => {
2308
- if (disabled) return;
2309
- let filesToProcess = [...originalFiles];
2310
- let invalid2 = false;
2311
- if (maxFiles) {
2312
- const currentCount = store.getState().files.size;
2313
- const remainingSlotCount = Math.max(0, maxFiles - currentCount);
2314
- if (remainingSlotCount < filesToProcess.length) {
2315
- const rejectedFiles2 = filesToProcess.slice(remainingSlotCount);
2316
- invalid2 = true;
2317
- filesToProcess = filesToProcess.slice(0, remainingSlotCount);
2318
- for (const file of rejectedFiles2) {
2319
- let rejectionMessage = `Maximum ${maxFiles} files allowed`;
2320
- if (propsRef.current.onFileValidate) {
2321
- const validationMessage = propsRef.current.onFileValidate(file);
2322
- if (validationMessage) {
2323
- rejectionMessage = validationMessage;
2324
- }
2325
- }
2326
- propsRef.current.onFileReject?.(file, rejectionMessage);
2327
- }
2328
- }
2329
- }
2330
- const acceptedFiles = [];
2331
- for (const file of filesToProcess) {
2332
- let rejected = false;
2333
- let rejectionMessage = "";
2334
- if (propsRef.current.onFileValidate) {
2335
- const validationMessage = propsRef.current.onFileValidate(file);
2336
- if (validationMessage) {
2337
- rejectionMessage = validationMessage;
2338
- propsRef.current.onFileReject?.(file, rejectionMessage);
2339
- rejected = true;
2340
- invalid2 = true;
2341
- continue;
2342
- }
2343
- }
2344
- if (acceptTypes) {
2345
- const fileType = file.type;
2346
- const fileExtension = `.${file.name.split(".").pop()}`;
2347
- if (!acceptTypes.some(
2348
- (type) => type === fileType || type === fileExtension || type.includes("/*") && fileType.startsWith(type.replace("/*", "/"))
2349
- )) {
2350
- rejectionMessage = "File type not accepted";
2351
- propsRef.current.onFileReject?.(file, rejectionMessage);
2352
- rejected = true;
2353
- invalid2 = true;
2354
- }
2355
- }
2356
- if (maxSize && file.size > maxSize) {
2357
- rejectionMessage = "File too large";
2358
- propsRef.current.onFileReject?.(file, rejectionMessage);
2359
- rejected = true;
2360
- invalid2 = true;
2361
- }
2362
- if (!rejected) {
2363
- acceptedFiles.push(file);
2364
- }
2365
- }
2366
- if (invalid2) {
2367
- store.dispatch({ type: "SET_INVALID", invalid: invalid2 });
2368
- setTimeout(() => {
2369
- store.dispatch({ type: "SET_INVALID", invalid: false });
2370
- }, 2e3);
2371
- }
2372
- if (acceptedFiles.length > 0) {
2373
- store.dispatch({ type: "ADD_FILES", files: acceptedFiles });
2374
- if (isControlled && propsRef.current.onValueChange) {
2375
- const currentFiles = Array.from(store.getState().files.values()).map(
2376
- (f) => f.file
2377
- );
2378
- propsRef.current.onValueChange([...currentFiles]);
2379
- }
2380
- if (propsRef.current.onAccept) {
2381
- propsRef.current.onAccept(acceptedFiles);
2382
- }
2383
- for (const file of acceptedFiles) {
2384
- propsRef.current.onFileAccept?.(file);
2385
- }
2386
- if (propsRef.current.onUpload) {
2387
- requestAnimationFrame(() => {
2388
- onFilesUpload(acceptedFiles);
2389
- });
2390
- }
2391
- }
2392
- },
2393
- [
2394
- store,
2395
- isControlled,
2396
- propsRef,
2397
- onFilesUpload,
2398
- maxFiles,
2399
- acceptTypes,
2400
- maxSize,
2401
- disabled
2402
- ]
2403
- );
2404
- const onInputChange = React25.useCallback(
2405
- (event) => {
2406
- const files2 = Array.from(event.target.files ?? []);
2407
- onFilesChange(files2);
2408
- event.target.value = "";
2409
- },
2410
- [onFilesChange]
2411
- );
2412
- const contextValue = React25.useMemo(
2413
- () => ({
2414
- dropzoneId,
2415
- inputId,
2416
- listId,
2417
- labelId,
2418
- dir,
2419
- disabled,
2420
- inputRef,
2421
- urlCache
2422
- }),
2423
- [dropzoneId, inputId, listId, labelId, dir, disabled, urlCache]
2424
- );
2425
- const RootPrimitive = asChild ? Slot : "div";
2426
- const inputAriaDescribedBy = [
2427
- contextValue.dropzoneId,
2428
- inputProps?.["aria-describedby"]
2429
- ].filter(Boolean).join(" ").trim();
2430
- return /* @__PURE__ */ React25.createElement(StoreContext.Provider, { value: store }, /* @__PURE__ */ React25.createElement(FileUploadContext.Provider, { value: contextValue }, /* @__PURE__ */ React25.createElement(
2431
- RootPrimitive,
2432
- {
2433
- "data-disabled": disabled ? "" : void 0,
2434
- "data-slot": "file-upload",
2435
- dir,
2436
- ...rootProps,
2437
- className: cn("relative flex flex-col gap-2", className)
2438
- },
2439
- children,
2440
- /* @__PURE__ */ React25.createElement(
2441
- "input",
2442
- {
2443
- type: "file",
2444
- id: inputId,
2445
- "aria-labelledby": inputProps?.["aria-labelledby"] ? `${labelId} ${inputProps["aria-labelledby"]}` : labelId,
2446
- "aria-describedby": inputAriaDescribedBy || void 0,
2447
- ref: inputRef,
2448
- tabIndex: -1,
2449
- accept,
2450
- name,
2451
- className: "sr-only",
2452
- disabled,
2453
- multiple,
2454
- required,
2455
- onChange: onInputChange,
2456
- ...inputProps
2457
- }
2458
- ),
2459
- /* @__PURE__ */ React25.createElement("div", { id: labelId, className: "sr-only" }, label ?? "File upload")
2460
- )));
2461
- }
2462
- function FileUploadDropzone(props) {
2463
- const {
2464
- asChild,
2465
- className,
2466
- onClick: onClickProp,
2467
- onDragOver: onDragOverProp,
2468
- onDragEnter: onDragEnterProp,
2469
- onDragLeave: onDragLeaveProp,
2470
- onDrop: onDropProp,
2471
- onPaste: onPasteProp,
2472
- onKeyDown: onKeyDownProp,
2473
- ...dropzoneProps
2474
- } = props;
2475
- const context = useFileUploadContext(DROPZONE_NAME);
2476
- const store = useStoreContext(DROPZONE_NAME);
2477
- const dragOver = useStore((state) => state.dragOver);
2478
- const invalid = useStore((state) => state.invalid);
2479
- const propsRef = useAsRef({
2480
- onClick: onClickProp,
2481
- onDragOver: onDragOverProp,
2482
- onDragEnter: onDragEnterProp,
2483
- onDragLeave: onDragLeaveProp,
2484
- onDrop: onDropProp,
2485
- onPaste: onPasteProp,
2486
- onKeyDown: onKeyDownProp
2487
- });
2488
- const onClick = React25.useCallback(
2489
- (event) => {
2490
- propsRef.current.onClick?.(event);
2491
- if (event.defaultPrevented) return;
2492
- const target = event.target;
2493
- const isFromTrigger = target instanceof HTMLElement && target.closest('[data-slot="file-upload-trigger"]');
2494
- if (!isFromTrigger) {
2495
- context.inputRef.current?.click();
2496
- }
2497
- },
2498
- [context.inputRef, propsRef]
2499
- );
2500
- const onDragOver = React25.useCallback(
2501
- (event) => {
2502
- propsRef.current.onDragOver?.(event);
2503
- if (event.defaultPrevented) return;
2504
- event.preventDefault();
2505
- store.dispatch({ type: "SET_DRAG_OVER", dragOver: true });
2506
- },
2507
- [store, propsRef]
2508
- );
2509
- const onDragEnter = React25.useCallback(
2510
- (event) => {
2511
- propsRef.current.onDragEnter?.(event);
2512
- if (event.defaultPrevented) return;
2513
- event.preventDefault();
2514
- store.dispatch({ type: "SET_DRAG_OVER", dragOver: true });
2515
- },
2516
- [store, propsRef]
2517
- );
2518
- const onDragLeave = React25.useCallback(
2519
- (event) => {
2520
- propsRef.current.onDragLeave?.(event);
2521
- if (event.defaultPrevented) return;
2522
- const relatedTarget = event.relatedTarget;
2523
- if (relatedTarget && relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) {
2524
- return;
2525
- }
2526
- event.preventDefault();
2527
- store.dispatch({ type: "SET_DRAG_OVER", dragOver: false });
2528
- },
2529
- [store, propsRef]
2530
- );
2531
- const onDrop = React25.useCallback(
2532
- (event) => {
2533
- propsRef.current.onDrop?.(event);
2534
- if (event.defaultPrevented) return;
2535
- if (context.disabled) return;
2536
- event.preventDefault();
2537
- store.dispatch({ type: "SET_DRAG_OVER", dragOver: false });
2538
- const files = Array.from(event.dataTransfer.files);
2539
- const inputElement = context.inputRef.current;
2540
- if (!inputElement) return;
2541
- if (typeof DataTransfer === "undefined") return;
2542
- const dataTransfer = new DataTransfer();
2543
- for (const file of files) {
2544
- dataTransfer.items.add(file);
2545
- }
2546
- inputElement.files = dataTransfer.files;
2547
- inputElement.dispatchEvent(new Event("change", { bubbles: true }));
2548
- },
2549
- [store, context.inputRef, propsRef]
2550
- );
2551
- const onPaste = React25.useCallback(
2552
- (event) => {
2553
- propsRef.current.onPaste?.(event);
2554
- if (event.defaultPrevented) return;
2555
- if (context.disabled) return;
2556
- event.preventDefault();
2557
- store.dispatch({ type: "SET_DRAG_OVER", dragOver: false });
2558
- const items = event.clipboardData?.items;
2559
- if (!items) return;
2560
- const files = [];
2561
- for (let i = 0; i < items.length; i++) {
2562
- const item = items[i];
2563
- if (item?.kind === "file") {
2564
- const file = item.getAsFile();
2565
- if (file) {
2566
- files.push(file);
2567
- }
2568
- }
2569
- }
2570
- if (files.length === 0) return;
2571
- const inputElement = context.inputRef.current;
2572
- if (!inputElement) return;
2573
- if (typeof DataTransfer === "undefined") return;
2574
- const dataTransfer = new DataTransfer();
2575
- for (const file of files) {
2576
- dataTransfer.items.add(file);
2577
- }
2578
- inputElement.files = dataTransfer.files;
2579
- inputElement.dispatchEvent(new Event("change", { bubbles: true }));
2580
- },
2581
- [store, context.inputRef, propsRef]
2582
- );
2583
- const onKeyDown = React25.useCallback(
2584
- (event) => {
2585
- propsRef.current.onKeyDown?.(event);
2586
- if (!event.defaultPrevented && (event.key === "Enter" || event.key === " ")) {
2587
- event.preventDefault();
2588
- context.inputRef.current?.click();
2589
- }
2590
- },
2591
- [context.inputRef, propsRef]
2592
- );
2593
- const DropzonePrimitive = asChild ? Slot : "div";
2594
- return /* @__PURE__ */ React25.createElement(
2595
- DropzonePrimitive,
2596
- {
2597
- role: "region",
2598
- id: context.dropzoneId,
2599
- "aria-controls": `${context.inputId} ${context.listId}`,
2600
- "aria-disabled": context.disabled,
2601
- "aria-invalid": invalid,
2602
- "data-disabled": context.disabled ? "" : void 0,
2603
- "data-dragging": dragOver ? "" : void 0,
2604
- "data-invalid": invalid ? "" : void 0,
2605
- "data-slot": "file-upload-dropzone",
2606
- dir: context.dir,
2607
- tabIndex: context.disabled ? -1 : 0,
2608
- ...dropzoneProps,
2609
- className: cn(
2610
- "relative flex select-none flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 outline-none transition-colors hover:bg-accent/30 focus-visible:border-ring/50 data-disabled:pointer-events-none data-dragging:border-primary/30 data-dragging:bg-accent/30 data-invalid:ring-destructive/20",
2611
- className
2612
- ),
2613
- onClick,
2614
- onDragEnter,
2615
- onDragLeave,
2616
- onDragOver,
2617
- onDrop,
2618
- onKeyDown,
2619
- onPaste
2620
- }
2621
- );
2622
- }
2623
- function FileUploadList(props) {
2624
- const {
2625
- className,
2626
- orientation = "vertical",
2627
- asChild,
2628
- forceMount,
2629
- ...listProps
2630
- } = props;
2631
- const context = useFileUploadContext(LIST_NAME);
2632
- const fileCount = useStore((state) => state.files.size);
2633
- const shouldRender = forceMount || fileCount > 0;
2634
- if (!shouldRender) return null;
2635
- const ListPrimitive = asChild ? Slot : "div";
2636
- return /* @__PURE__ */ React25.createElement(
2637
- ListPrimitive,
2638
- {
2639
- role: "list",
2640
- id: context.listId,
2641
- "aria-orientation": orientation,
2642
- "data-orientation": orientation,
2643
- "data-slot": "file-upload-list",
2644
- "data-state": shouldRender ? "active" : "inactive",
2645
- dir: context.dir,
2646
- ...listProps,
2647
- className: cn(
2648
- "data-[state=inactive]:fade-out-0 data-[state=active]:fade-in-0 data-[state=inactive]:slide-out-to-top-2 data-[state=active]:slide-in-from-top-2 flex flex-col gap-2 data-[state=active]:animate-in data-[state=inactive]:animate-out",
2649
- orientation === "horizontal" && "flex-row overflow-x-auto p-1.5",
2650
- className
2651
- )
2652
- }
2653
- );
2654
- }
2655
- var FileUploadItemContext = React25.createContext(null);
2656
- function useFileUploadItemContext(consumerName) {
2657
- const context = React25.useContext(FileUploadItemContext);
2658
- if (!context) {
2659
- throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
2660
- }
2661
- return context;
2662
- }
2663
- function FileUploadItem(props) {
2664
- const { value, asChild, className, ...itemProps } = props;
2665
- const id = React25.useId();
2666
- const statusId = `${id}-status`;
2667
- const nameId = `${id}-name`;
2668
- const sizeId = `${id}-size`;
2669
- const messageId = `${id}-message`;
2670
- const context = useFileUploadContext(ITEM_NAME);
2671
- const fileState = useStore((state) => state.files.get(value));
2672
- const fileCount = useStore((state) => state.files.size);
2673
- const fileIndex = useStore((state) => {
2674
- const files = Array.from(state.files.keys());
2675
- return files.indexOf(value) + 1;
2676
- });
2677
- const itemContext = React25.useMemo(
2678
- () => ({
2679
- id,
2680
- fileState,
2681
- nameId,
2682
- sizeId,
2683
- statusId,
2684
- messageId
2685
- }),
2686
- [id, fileState, statusId, nameId, sizeId, messageId]
2687
- );
2688
- if (!fileState) return null;
2689
- const statusText = fileState.error ? `Error: ${fileState.error}` : fileState.status === "uploading" ? `Uploading: ${fileState.progress}% complete` : fileState.status === "success" ? "Upload complete" : "Ready to upload";
2690
- const ItemPrimitive = asChild ? Slot : "div";
2691
- return /* @__PURE__ */ React25.createElement(FileUploadItemContext.Provider, { value: itemContext }, /* @__PURE__ */ React25.createElement(
2692
- ItemPrimitive,
2693
- {
2694
- role: "listitem",
2695
- id,
2696
- "aria-setsize": fileCount,
2697
- "aria-posinset": fileIndex,
2698
- "aria-describedby": `${nameId} ${sizeId} ${statusId} ${fileState.error ? messageId : ""}`,
2699
- "aria-labelledby": nameId,
2700
- "data-slot": "file-upload-item",
2701
- dir: context.dir,
2702
- ...itemProps,
2703
- className: cn(
2704
- "relative flex items-center gap-2.5 rounded-md border p-3",
2705
- className
2706
- )
2707
- },
2708
- props.children,
2709
- /* @__PURE__ */ React25.createElement("span", { id: statusId, className: "sr-only" }, statusText)
2710
- ));
2711
- }
2712
- function FileUploadItemPreview(props) {
2713
- const { render, asChild, children, className, ...previewProps } = props;
2714
- const itemContext = useFileUploadItemContext(ITEM_PREVIEW_NAME);
2715
- const context = useFileUploadContext(ITEM_PREVIEW_NAME);
2716
- const getDefaultRender = React25.useCallback(
2717
- (file) => {
2718
- if (itemContext.fileState?.file.type.startsWith("image/")) {
2719
- let url = context.urlCache.get(file);
2720
- if (!url) {
2721
- url = URL.createObjectURL(file);
2722
- context.urlCache.set(file, url);
2723
- }
2724
- return (
2725
- // biome-ignore lint/performance/noImgElement: dynamic file URLs from user uploads don't work well with Next.js Image optimization
2726
- /* @__PURE__ */ React25.createElement("img", { src: url, alt: file.name, className: "size-full object-cover" })
2727
- );
2728
- }
2729
- return getFileIcon(file);
2730
- },
2731
- [itemContext.fileState?.file.type, context.urlCache]
2732
- );
2733
- const onPreviewRender = React25.useCallback(
2734
- (file) => {
2735
- if (render) {
2736
- return render(file, () => getDefaultRender(file));
2737
- }
2738
- return getDefaultRender(file);
2739
- },
2740
- [render, getDefaultRender]
2741
- );
2742
- if (!itemContext.fileState) return null;
2743
- const ItemPreviewPrimitive = asChild ? Slot : "div";
2744
- return /* @__PURE__ */ React25.createElement(
2745
- ItemPreviewPrimitive,
2746
- {
2747
- "aria-labelledby": itemContext.nameId,
2748
- "data-slot": "file-upload-preview",
2749
- ...previewProps,
2750
- className: cn(
2751
- "relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded border bg-accent/50 [&>svg]:size-10",
2752
- className
2753
- )
2754
- },
2755
- onPreviewRender(itemContext.fileState.file),
2756
- children
2757
- );
2758
- }
2759
- function FileUploadItemMetadata(props) {
2760
- const {
2761
- asChild,
2762
- size = "default",
2763
- children,
2764
- className,
2765
- ...metadataProps
2766
- } = props;
2767
- const context = useFileUploadContext(ITEM_METADATA_NAME);
2768
- const itemContext = useFileUploadItemContext(ITEM_METADATA_NAME);
2769
- if (!itemContext.fileState) return null;
2770
- const ItemMetadataPrimitive = asChild ? Slot : "div";
2771
- return /* @__PURE__ */ React25.createElement(
2772
- ItemMetadataPrimitive,
2773
- {
2774
- "data-slot": "file-upload-metadata",
2775
- dir: context.dir,
2776
- ...metadataProps,
2777
- className: cn("flex min-w-0 flex-1 flex-col", className)
2778
- },
2779
- children ?? /* @__PURE__ */ React25.createElement(React25.Fragment, null, /* @__PURE__ */ React25.createElement(
2780
- "span",
2781
- {
2782
- id: itemContext.nameId,
2783
- className: cn(
2784
- "truncate font-medium text-sm",
2785
- size === "sm" && "font-normal text-[13px] leading-snug"
2786
- )
2787
- },
2788
- itemContext.fileState.file.name
2789
- ), /* @__PURE__ */ React25.createElement(
2790
- "span",
2791
- {
2792
- id: itemContext.sizeId,
2793
- className: cn(
2794
- "truncate text-xs opacity-70",
2795
- size === "sm" && "text-[11px] leading-snug"
2796
- )
2797
- },
2798
- formatBytes(itemContext.fileState.file.size)
2799
- ), itemContext.fileState.error && /* @__PURE__ */ React25.createElement(
2800
- "span",
2801
- {
2802
- id: itemContext.messageId,
2803
- className: "text-destructive text-xs"
2804
- },
2805
- itemContext.fileState.error
2806
- ))
2807
- );
2808
- }
2809
- function FileUploadItemDelete(props) {
2810
- const { asChild, onClick: onClickProp, ...deleteProps } = props;
2811
- const store = useStoreContext(ITEM_DELETE_NAME);
2812
- const itemContext = useFileUploadItemContext(ITEM_DELETE_NAME);
2813
- const onClick = React25.useCallback(
2814
- (event) => {
2815
- onClickProp?.(event);
2816
- if (!itemContext.fileState || event.defaultPrevented) return;
2817
- store.dispatch({
2818
- type: "REMOVE_FILE",
2819
- file: itemContext.fileState.file
2820
- });
2821
- },
2822
- [store, itemContext.fileState, onClickProp]
2823
- );
2824
- if (!itemContext.fileState) return null;
2825
- const ItemDeletePrimitive = asChild ? Slot : "button";
2826
- return /* @__PURE__ */ React25.createElement(
2827
- ItemDeletePrimitive,
2828
- {
2829
- type: "button",
2830
- "aria-controls": itemContext.id,
2831
- "aria-describedby": itemContext.nameId,
2832
- "data-slot": "file-upload-item-delete",
2833
- ...deleteProps,
2834
- onClick
2835
- }
2836
- );
2837
- }
2838
-
2839
- // src/inputs/FileInput.tsx
2840
- function FileInput({
2841
- name,
2842
- value = [],
2843
- onChange,
2844
- onBlur,
2845
- placeholder = "Choose file...",
2846
- disabled = false,
2847
- required = false,
2848
- error = false,
2849
- className = "",
2850
- accept,
2851
- maxSize = 5 * 1024 * 1024,
2852
- // 5MB default
2853
- maxFiles = 1,
2854
- multiple = false,
2855
- showPreview = true,
2856
- showProgress = true,
2857
- uploadProgress = {},
2858
- enableCropping = false,
2859
- cropAspectRatio,
2860
- onCropComplete,
2861
- onValidationError,
2862
- onFileRemove,
2863
- ...props
2864
- }) {
2865
- const normalizedValue = React25.useMemo(() => {
2866
- const safeValue = Array.isArray(value) ? value : [];
2867
- return multiple ? safeValue : safeValue.slice(0, 1);
2868
- }, [multiple, value]);
2869
- const [cropperOpen, setCropperOpen] = React25.useState(false);
2870
- const [imageToCrop, setImageToCrop] = React25.useState(null);
2871
- const [crop, setCrop] = React25.useState({ x: 0, y: 0 });
2872
- const [zoom, setZoom] = React25.useState(1);
2873
- const [croppedAreaPixels, setCroppedAreaPixels] = React25.useState(null);
2874
- const validateFile = React25.useCallback(
2875
- (file) => {
2876
- if (accept) {
2877
- const acceptedTypes = accept.split(",").map((type) => type.trim());
2878
- const isValidType = acceptedTypes.some((type) => {
2879
- if (type.startsWith(".")) {
2880
- return file.name.toLowerCase().endsWith(type.toLowerCase());
2881
- }
2882
- if (type.endsWith("/*")) {
2883
- const baseType = type.split("/")[0];
2884
- return file.type.startsWith(`${baseType}/`);
2885
- }
2886
- return file.type === type;
2887
- });
2888
- if (!isValidType) {
2889
- return {
2890
- file,
2891
- error: "type",
2892
- message: `File type "${file.type}" is not accepted. Accepted types: ${accept}`
2893
- };
2894
- }
2895
- }
2896
- if (file.size > maxSize) {
2897
- const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
2898
- const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
2899
- return {
2900
- file,
2901
- error: "size",
2902
- message: `File size ${fileSizeMB}MB exceeds maximum ${maxSizeMB}MB`
2903
- };
2904
- }
2905
- return null;
2906
- },
2907
- [accept, maxSize]
2908
- );
2909
- const mapRejectedFileError = React25.useCallback(
2910
- (file, message) => {
2911
- const normalizedMessage = message.toLowerCase();
2912
- if (normalizedMessage.includes("maximum") && normalizedMessage.includes("files")) {
2913
- return { file, error: "count", message };
2914
- }
2915
- if (normalizedMessage.includes("size") || normalizedMessage.includes("large")) {
2916
- return { file, error: "size", message };
2917
- }
2918
- if (normalizedMessage.includes("type") || normalizedMessage.includes("accept")) {
2919
- return { file, error: "type", message };
2920
- }
2921
- if (file.size > maxSize) {
2922
- return { file, error: "size", message };
2923
- }
2924
- return { file, error: "type", message };
2925
- },
2926
- [maxSize]
2927
- );
2928
- const handleFileValidate = React25.useCallback(
2929
- (file) => {
2930
- const validationError = validateFile(file);
2931
- return validationError?.message ?? null;
2932
- },
2933
- [validateFile]
2934
- );
2935
- const handleFileReject = React25.useCallback(
2936
- (file, message) => {
2937
- const validationError = mapRejectedFileError(file, message);
2938
- onValidationError?.([validationError]);
2939
- },
2940
- [mapRejectedFileError, onValidationError]
2941
- );
2942
- const handleBlur = React25.useCallback(() => {
2943
- onBlur?.();
2944
- }, [onBlur]);
2945
- const fileIdentity = React25.useCallback((file) => {
2946
- return `${file.name}-${file.size}-${file.lastModified}`;
2947
- }, []);
2948
- const handleValueChange = React25.useCallback(
2949
- (incomingFiles) => {
2950
- const nextFiles = multiple ? incomingFiles : incomingFiles.slice(-1);
2951
- if (onFileRemove && nextFiles.length < normalizedValue.length) {
2952
- const nextFileIds = new Set(nextFiles.map((file) => fileIdentity(file)));
2953
- normalizedValue.forEach((file, index) => {
2954
- if (!nextFileIds.has(fileIdentity(file))) {
2955
- onFileRemove(file, index);
2956
- }
2957
- });
2958
- }
2959
- if (enableCropping && !multiple) {
2960
- const nextImageFile = nextFiles[0];
2961
- const previousFile = normalizedValue[0];
2962
- const isNewSingleImage = Boolean(
2963
- nextImageFile && nextImageFile.type.startsWith("image/") && nextImageFile !== previousFile
2964
- );
2965
- if (isNewSingleImage) {
2966
- const previewUrl = URL.createObjectURL(nextImageFile);
2967
- setImageToCrop({ file: nextImageFile, url: previewUrl });
2968
- setCropperOpen(true);
2969
- return;
2970
- }
2971
- }
2972
- onChange(nextFiles);
2973
- },
2974
- [
2975
- enableCropping,
2976
- maxFiles,
2977
- multiple,
2978
- normalizedValue,
2979
- onChange,
2980
- onFileRemove,
2981
- fileIdentity
2982
- ]
2983
- );
2984
- const createCroppedImage = React25.useCallback(
2985
- async (imageUrl, cropArea) => {
2986
- return new Promise((resolve, reject) => {
2987
- const image = new Image();
2988
- image.onload = () => {
2989
- const canvas = document.createElement("canvas");
2990
- const ctx = canvas.getContext("2d");
2991
- if (!ctx) {
2992
- reject(new Error("Failed to get canvas context"));
2993
- return;
2994
- }
2995
- canvas.width = cropArea.width;
2996
- canvas.height = cropArea.height;
2997
- ctx.drawImage(
2998
- image,
2999
- cropArea.x,
3000
- cropArea.y,
3001
- cropArea.width,
3002
- cropArea.height,
3003
- 0,
3004
- 0,
3005
- cropArea.width,
3006
- cropArea.height
3007
- );
3008
- canvas.toBlob(
3009
- (blob) => {
3010
- if (blob) {
3011
- resolve(blob);
3012
- } else {
3013
- reject(new Error("Failed to create blob from canvas"));
3014
- }
3015
- },
3016
- "image/jpeg",
3017
- 0.95
3018
- );
3019
- };
3020
- image.onerror = () => {
3021
- reject(new Error("Failed to load image"));
3022
- };
3023
- image.src = imageUrl;
3024
- });
3025
- },
3026
- []
3027
- );
3028
- const handleCropSave = React25.useCallback(async () => {
3029
- if (!imageToCrop || !croppedAreaPixels) return;
3030
- try {
3031
- const croppedBlob = await createCroppedImage(
3032
- imageToCrop.url,
3033
- croppedAreaPixels
3034
- );
3035
- if (onCropComplete) {
3036
- onCropComplete(croppedBlob, imageToCrop.file);
3037
- }
3038
- const croppedFile = new File([croppedBlob], imageToCrop.file.name, {
3039
- type: "image/jpeg"
3040
- });
3041
- let updatedFiles;
3042
- if (!multiple) {
3043
- updatedFiles = [croppedFile];
3044
- } else {
3045
- const existingIndex = normalizedValue.findIndex(
3046
- (file) => file === imageToCrop.file
3047
- );
3048
- if (existingIndex === -1) {
3049
- updatedFiles = [...normalizedValue, croppedFile].slice(0, maxFiles);
3050
- } else {
3051
- updatedFiles = normalizedValue.map(
3052
- (file, index) => index === existingIndex ? croppedFile : file
3053
- );
3054
- }
3055
- }
3056
- onChange(updatedFiles);
3057
- setCropperOpen(false);
3058
- URL.revokeObjectURL(imageToCrop.url);
3059
- setImageToCrop(null);
3060
- setCrop({ x: 0, y: 0 });
3061
- setZoom(1);
3062
- setCroppedAreaPixels(null);
3063
- } catch (cropError) {
3064
- console.error("Failed to crop image:", cropError);
3065
- }
3066
- }, [
3067
- createCroppedImage,
3068
- croppedAreaPixels,
3069
- imageToCrop,
3070
- maxFiles,
3071
- multiple,
3072
- normalizedValue,
3073
- onChange,
3074
- onCropComplete
3075
- ]);
3076
- const handleCropCancel = React25.useCallback(() => {
3077
- if (imageToCrop) {
3078
- URL.revokeObjectURL(imageToCrop.url);
3079
- }
3080
- setCropperOpen(false);
3081
- setImageToCrop(null);
3082
- setCrop({ x: 0, y: 0 });
3083
- setZoom(1);
3084
- setCroppedAreaPixels(null);
3085
- }, [imageToCrop]);
3086
- const handleCrop = React25.useCallback((file) => {
3087
- if (!file.type.startsWith("image/")) return;
3088
- const previewUrl = URL.createObjectURL(file);
3089
- setImageToCrop({ file, url: previewUrl });
3090
- setCropperOpen(true);
3091
- }, []);
3092
- const onCropChange = React25.useCallback((nextCrop) => {
3093
- setCrop(nextCrop);
3094
- }, []);
3095
- const onZoomChange = React25.useCallback((nextZoom) => {
3096
- setZoom(nextZoom);
3097
- }, []);
3098
- const onCropCompleteInternal = React25.useCallback(
3099
- (_, nextCroppedAreaPixels) => {
3100
- setCroppedAreaPixels(nextCroppedAreaPixels);
3101
- },
3102
- []
3103
- );
3104
- const formatFileSize = React25.useCallback((bytes) => {
3105
- if (bytes === 0) return "0 Bytes";
3106
- const unit = 1024;
3107
- const units = ["Bytes", "KB", "MB", "GB"];
3108
- const index = Math.floor(Math.log(bytes) / Math.log(unit));
3109
- return Math.round(bytes / Math.pow(unit, index) * 100) / 100 + " " + units[index];
3110
- }, []);
3111
- React25.useEffect(() => {
3112
- return () => {
3113
- if (imageToCrop) {
3114
- URL.revokeObjectURL(imageToCrop.url);
3115
- }
3116
- };
3117
- }, [imageToCrop]);
3118
- const fileCountLabel = normalizedValue.length > 0 ? `${normalizedValue.length} file(s) selected` : placeholder;
3119
- return /* @__PURE__ */ React25.createElement(React25.Fragment, null, /* @__PURE__ */ React25.createElement(
3120
- FileUpload,
3121
- {
3122
- name,
3123
- value: normalizedValue,
3124
- onValueChange: handleValueChange,
3125
- onFileValidate: handleFileValidate,
3126
- onFileReject: handleFileReject,
3127
- accept,
3128
- maxSize,
3129
- maxFiles: multiple ? maxFiles : void 0,
3130
- multiple,
3131
- disabled,
3132
- required: required && normalizedValue.length === 0,
3133
- invalid: Boolean(error || props["aria-invalid"]),
3134
- label: "File upload",
3135
- className: cn(className),
3136
- inputProps: {
3137
- ...props,
3138
- onBlur: handleBlur,
3139
- style: { display: "none" },
3140
- "aria-invalid": error || props["aria-invalid"],
3141
- "aria-required": required || props["aria-required"],
3142
- "aria-describedby": props["aria-describedby"]
3143
- }
3144
- },
3145
- /* @__PURE__ */ React25.createElement(
3146
- FileUploadDropzone,
3147
- {
3148
- role: "button",
3149
- "aria-label": placeholder,
3150
- className: cn(
3151
- "flex min-h-32 w-full cursor-pointer items-center justify-center border-input bg-transparent p-6 transition-colors",
3152
- "hover:bg-accent/50 hover:border-ring",
3153
- "data-[dragging]:bg-accent data-[dragging]:border-ring",
3154
- disabled && "cursor-not-allowed opacity-50",
3155
- error && "border-destructive"
3156
- )
3157
- },
3158
- /* @__PURE__ */ React25.createElement("div", { className: "flex flex-col items-center gap-2 text-center" }, /* @__PURE__ */ React25.createElement(
3159
- "svg",
3160
- {
3161
- width: "48",
3162
- height: "48",
3163
- viewBox: "0 0 24 24",
3164
- fill: "none",
3165
- stroke: "currentColor",
3166
- strokeWidth: "2",
3167
- strokeLinecap: "round",
3168
- strokeLinejoin: "round",
3169
- "aria-hidden": "true"
3170
- },
3171
- /* @__PURE__ */ React25.createElement("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3172
- /* @__PURE__ */ React25.createElement("polyline", { points: "17 8 12 3 7 8" }),
3173
- /* @__PURE__ */ React25.createElement("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
3174
- ), /* @__PURE__ */ React25.createElement("p", { className: "text-sm font-medium" }, fileCountLabel), accept && /* @__PURE__ */ React25.createElement("p", { className: "text-xs" }, "Accepted: ", accept), /* @__PURE__ */ React25.createElement("p", { className: "text-xs" }, "Max size: ", formatFileSize(maxSize)))
3175
- ),
3176
- /* @__PURE__ */ React25.createElement(FileUploadList, { className: "mt-4" }, normalizedValue.map((file, index) => {
3177
- const progressValue = uploadProgress[file.name];
3178
- const hasProgress = showProgress && typeof progressValue === "number";
3179
- return /* @__PURE__ */ React25.createElement(
3180
- FileUploadItem,
3181
- {
3182
- key: `${file.name}-${index}`,
3183
- value: file,
3184
- className: "flex items-center gap-3 border-border bg-card text-card-foreground hover:bg-primary/50 transition-colors"
3185
- },
3186
- showPreview ? /* @__PURE__ */ React25.createElement(FileUploadItemPreview, { className: "h-12 w-12 rounded [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:size-6" }) : null,
3187
- /* @__PURE__ */ React25.createElement("div", { className: "flex min-w-0 flex-1 flex-col" }, /* @__PURE__ */ React25.createElement(FileUploadItemMetadata, { className: "min-w-0" }), /* @__PURE__ */ React25.createElement("span", { className: "text-xs" }, formatFileSize(file.size)), hasProgress ? /* @__PURE__ */ React25.createElement("div", { className: "mt-1 flex items-center gap-2" }, /* @__PURE__ */ React25.createElement(
3188
- "div",
3189
- {
3190
- className: "h-1.5 flex-1 overflow-hidden rounded-full bg-accent/40",
3191
- role: "progressbar",
3192
- "aria-valuenow": progressValue,
3193
- "aria-valuemin": 0,
3194
- "aria-valuemax": 100,
3195
- "aria-label": `Upload progress: ${progressValue}%`
3196
- },
3197
- /* @__PURE__ */ React25.createElement(
3198
- "div",
3199
- {
3200
- className: "h-full bg-primary transition-all",
3201
- style: { width: `${progressValue}%` }
3202
- }
3203
- )
3204
- ), /* @__PURE__ */ React25.createElement("span", { className: "text-xs" }, progressValue, "%")) : null),
3205
- enableCropping && file.type.startsWith("image/") ? /* @__PURE__ */ React25.createElement(
3206
- Button,
3207
- {
3208
- type: "button",
3209
- variant: "ghost",
3210
- size: "icon",
3211
- onClick: (event) => {
3212
- event.stopPropagation();
3213
- handleCrop(file);
3214
- },
3215
- disabled,
3216
- className: "h-8 w-8 p-0",
3217
- "aria-label": `Crop ${file.name}`
3218
- },
3219
- /* @__PURE__ */ React25.createElement(
3220
- "svg",
3221
- {
3222
- width: "20",
3223
- height: "20",
3224
- viewBox: "0 0 24 24",
3225
- fill: "none",
3226
- stroke: "currentColor",
3227
- strokeWidth: "2",
3228
- strokeLinecap: "round",
3229
- strokeLinejoin: "round",
3230
- "aria-hidden": "true"
3231
- },
3232
- /* @__PURE__ */ React25.createElement("path", { d: "M6.13 1L6 16a2 2 0 0 0 2 2h15" }),
3233
- /* @__PURE__ */ React25.createElement("path", { d: "M1 6.13L16 6a2 2 0 0 1 2 2v15" })
3234
- )
3235
- ) : null,
3236
- /* @__PURE__ */ React25.createElement(FileUploadItemDelete, { asChild: true }, /* @__PURE__ */ React25.createElement(
3237
- Button,
3238
- {
3239
- type: "button",
3240
- variant: "ghost",
3241
- size: "icon",
3242
- disabled,
3243
- className: "h-8 w-8 p-0",
3244
- "aria-label": `Remove ${file.name}`
3245
- },
3246
- /* @__PURE__ */ React25.createElement(
3247
- "svg",
3248
- {
3249
- width: "20",
3250
- height: "20",
3251
- viewBox: "0 0 24 24",
3252
- fill: "none",
3253
- stroke: "currentColor",
3254
- strokeWidth: "2",
3255
- strokeLinecap: "round",
3256
- strokeLinejoin: "round",
3257
- "aria-hidden": "true"
3258
- },
3259
- /* @__PURE__ */ React25.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3260
- /* @__PURE__ */ React25.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
3261
- )
3262
- ))
3263
- );
3264
- }))
3265
- ), /* @__PURE__ */ React25.createElement(
3266
- Dialog,
3267
- {
3268
- open: cropperOpen && Boolean(imageToCrop),
3269
- onOpenChange: (open) => {
3270
- if (!open) {
3271
- handleCropCancel();
3272
- }
3273
- }
3274
- },
3275
- imageToCrop ? /* @__PURE__ */ React25.createElement(
3276
- DialogContent,
3277
- {
3278
- showCloseButton: false,
3279
- className: "max-w-3xl gap-0 p-0",
3280
- "aria-describedby": void 0
3281
- },
3282
- /* @__PURE__ */ React25.createElement(DialogHeader, { className: "flex-row items-center justify-between border-b border-border px-4 py-3" }, /* @__PURE__ */ React25.createElement(DialogTitle, null, "Crop Image"), /* @__PURE__ */ React25.createElement(DialogClose, { asChild: true }, /* @__PURE__ */ React25.createElement(
3283
- Button,
3284
- {
3285
- type: "button",
3286
- variant: "ghost",
3287
- size: "icon",
3288
- className: "h-8 w-8 p-0",
3289
- "aria-label": "Close"
3290
- },
3291
- /* @__PURE__ */ React25.createElement(
3292
- "svg",
3293
- {
3294
- width: "16",
3295
- height: "16",
3296
- viewBox: "0 0 24 24",
3297
- fill: "none",
3298
- stroke: "currentColor",
3299
- strokeWidth: "2",
3300
- strokeLinecap: "round",
3301
- strokeLinejoin: "round",
3302
- "aria-hidden": "true"
3303
- },
3304
- /* @__PURE__ */ React25.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3305
- /* @__PURE__ */ React25.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
3306
- )
3307
- ))),
3308
- /* @__PURE__ */ React25.createElement("div", { className: "p-4" }, /* @__PURE__ */ React25.createElement(
3309
- "div",
3310
- {
3311
- className: "relative h-96 w-full overflow-hidden rounded-md bg-accent/40",
3312
- onMouseDown: (event) => {
3313
- event.preventDefault();
3314
- const startX = event.clientX - crop.x;
3315
- const startY = event.clientY - crop.y;
3316
- const handleMouseMove = (moveEvent) => {
3317
- onCropChange({
3318
- x: moveEvent.clientX - startX,
3319
- y: moveEvent.clientY - startY
3320
- });
3321
- };
3322
- const handleMouseUp = () => {
3323
- document.removeEventListener("mousemove", handleMouseMove);
3324
- document.removeEventListener("mouseup", handleMouseUp);
3325
- };
3326
- document.addEventListener("mousemove", handleMouseMove);
3327
- document.addEventListener("mouseup", handleMouseUp);
3328
- }
3329
- },
3330
- /* @__PURE__ */ React25.createElement(
3331
- "img",
3332
- {
3333
- src: imageToCrop.url,
3334
- alt: "Crop preview",
3335
- className: "absolute inset-0 h-full w-full object-contain",
3336
- style: {
3337
- transform: `translate(${crop.x}px, ${crop.y}px) scale(${zoom})`
3338
- },
3339
- draggable: false,
3340
- onLoad: (event) => {
3341
- const image = event.currentTarget;
3342
- const containerWidth = 600;
3343
- const containerHeight = 400;
3344
- const cropWidth = cropAspectRatio ? Math.min(
3345
- containerWidth * 0.8,
3346
- containerHeight * 0.8 * cropAspectRatio
3347
- ) : containerWidth * 0.8;
3348
- const cropHeight = cropAspectRatio ? cropWidth / cropAspectRatio : containerHeight * 0.8;
3349
- const imageWidth = image.naturalWidth;
3350
- const imageHeight = image.naturalHeight;
3351
- const scale = zoom;
3352
- const centerX = containerWidth / 2;
3353
- const centerY = containerHeight / 2;
3354
- const cropX = (centerX - crop.x - cropWidth / 2) / scale;
3355
- const cropY = (centerY - crop.y - cropHeight / 2) / scale;
3356
- onCropCompleteInternal(null, {
3357
- x: Math.max(0, cropX),
3358
- y: Math.max(0, cropY),
3359
- width: Math.min(cropWidth / scale, imageWidth),
3360
- height: Math.min(cropHeight / scale, imageHeight)
3361
- });
3362
- }
3363
- }
3364
- ),
3365
- /* @__PURE__ */ React25.createElement(
3366
- "div",
3367
- {
3368
- className: "pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded border-2 border-primary",
3369
- style: {
3370
- width: cropAspectRatio ? `${Math.min(80, 80 * cropAspectRatio)}%` : "80%",
3371
- aspectRatio: cropAspectRatio ? String(cropAspectRatio) : void 0
3372
- }
3373
- },
3374
- /* @__PURE__ */ React25.createElement("div", { className: "absolute inset-0 grid grid-cols-3 grid-rows-3" }, /* @__PURE__ */ React25.createElement("div", { className: "border-r border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-r border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-r border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-r border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-b border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-r border-primary/30" }), /* @__PURE__ */ React25.createElement("div", { className: "border-r border-primary/30" }), /* @__PURE__ */ React25.createElement("div", null))
3375
- )
3376
- ), /* @__PURE__ */ React25.createElement("div", { className: "mt-4 flex items-center gap-3" }, /* @__PURE__ */ React25.createElement(
3377
- "label",
3378
- {
3379
- htmlFor: "zoom-slider",
3380
- className: "whitespace-nowrap text-sm font-medium"
3381
- },
3382
- "Zoom: ",
3383
- zoom.toFixed(1),
3384
- "x"
3385
- ), /* @__PURE__ */ React25.createElement(
3386
- "input",
3387
- {
3388
- id: "zoom-slider",
3389
- type: "range",
3390
- min: "1",
3391
- max: "3",
3392
- step: "0.1",
3393
- value: zoom,
3394
- onChange: (event) => onZoomChange(parseFloat(event.target.value)),
3395
- className: "h-2 flex-1 cursor-pointer appearance-none rounded-lg bg-accent/60",
3396
- "aria-label": "Zoom level"
3397
- }
3398
- ))),
3399
- /* @__PURE__ */ React25.createElement("div", { className: "flex items-center justify-end gap-2 border-t border-border p-4" }, /* @__PURE__ */ React25.createElement(Button, { type: "button", variant: "outline", onClick: handleCropCancel }, "Cancel"), /* @__PURE__ */ React25.createElement(Button, { type: "button", onClick: handleCropSave }, "Save"))
3400
- ) : null
3401
- ));
3402
- }
3403
- FileInput.displayName = "FileInput";
3404
- function Calendar({
3405
- className,
3406
- classNames,
3407
- showOutsideDays = true,
3408
- captionLayout = "label",
3409
- buttonVariant = "ghost",
3410
- formatters,
3411
- components,
3412
- ...props
3413
- }) {
3414
- const defaultClassNames = getDefaultClassNames();
3415
- return /* @__PURE__ */ React25.createElement(
3416
- DayPicker,
3417
- {
3418
- showOutsideDays,
3419
- className: cn(
3420
- "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
3421
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
3422
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
3423
- className
3424
- ),
3425
- captionLayout,
3426
- formatters: {
3427
- formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
3428
- ...formatters
3429
- },
3430
- classNames: {
3431
- root: cn("w-fit", defaultClassNames.root),
3432
- months: cn(
3433
- "flex gap-4 flex-col md:flex-row relative",
3434
- defaultClassNames.months
3435
- ),
3436
- month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
3437
- nav: cn(
3438
- "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
3439
- defaultClassNames.nav
3440
- ),
3441
- button_previous: cn(
3442
- buttonVariants({ variant: buttonVariant }),
3443
- "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
3444
- defaultClassNames.button_previous
3445
- ),
3446
- button_next: cn(
3447
- buttonVariants({ variant: buttonVariant }),
3448
- "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
3449
- defaultClassNames.button_next
3450
- ),
3451
- month_caption: cn(
3452
- "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
3453
- defaultClassNames.month_caption
3454
- ),
3455
- dropdowns: cn(
3456
- "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
3457
- defaultClassNames.dropdowns
3458
- ),
3459
- dropdown_root: cn(
3460
- "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
3461
- defaultClassNames.dropdown_root
3462
- ),
3463
- dropdown: cn(
3464
- "absolute bg-popover inset-0 opacity-0",
3465
- defaultClassNames.dropdown
3466
- ),
3467
- caption_label: cn(
3468
- "select-none font-medium",
3469
- captionLayout === "label" ? "text-sm" : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:opacity-70 [&>svg]:size-3.5",
3470
- defaultClassNames.caption_label
3471
- ),
3472
- table: "w-full border-collapse",
3473
- weekdays: cn("flex", defaultClassNames.weekdays),
3474
- weekday: cn(
3475
- "opacity-70 rounded-md flex-1 font-normal text-[0.8rem] select-none",
3476
- defaultClassNames.weekday
3477
- ),
3478
- week: cn("flex w-full mt-2", defaultClassNames.week),
3479
- week_number_header: cn(
3480
- "select-none w-(--cell-size)",
3481
- defaultClassNames.week_number_header
3482
- ),
3483
- week_number: cn(
3484
- "text-[0.8rem] select-none opacity-70",
3485
- defaultClassNames.week_number
3486
- ),
3487
- day: cn(
3488
- "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
3489
- props.showWeekNumber ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" : "[&:first-child[data-selected=true]_button]:rounded-l-md",
3490
- defaultClassNames.day
3491
- ),
3492
- range_start: cn(
3493
- "rounded-l-md bg-accent",
3494
- defaultClassNames.range_start
3495
- ),
3496
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
3497
- range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
3498
- today: cn(
3499
- "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
3500
- defaultClassNames.today
3501
- ),
3502
- outside: cn(
3503
- "opacity-50",
3504
- defaultClassNames.outside
3505
- ),
3506
- disabled: cn(
3507
- "opacity-50",
3508
- defaultClassNames.disabled
3509
- ),
3510
- hidden: cn("invisible", defaultClassNames.hidden),
3511
- ...classNames
3512
- },
3513
- components: {
3514
- Root: ({ className: className2, rootRef, ...props2 }) => {
3515
- return /* @__PURE__ */ React25.createElement(
3516
- "div",
3517
- {
3518
- "data-slot": "calendar",
3519
- ref: rootRef,
3520
- className: cn(className2),
3521
- ...props2
3522
- }
3523
- );
3524
- },
3525
- Chevron: ({ className: className2, orientation, ...props2 }) => {
3526
- if (orientation === "left") {
3527
- return /* @__PURE__ */ React25.createElement(
3528
- "svg",
3529
- {
3530
- className: cn("size-4", className2),
3531
- viewBox: "0 0 24 24",
3532
- fill: "none",
3533
- stroke: "currentColor",
3534
- strokeWidth: "2",
3535
- strokeLinecap: "round",
3536
- strokeLinejoin: "round",
3537
- ...props2
3538
- },
3539
- /* @__PURE__ */ React25.createElement("polyline", { points: "15 18 9 12 15 6" })
3540
- );
3541
- }
3542
- if (orientation === "right") {
3543
- return /* @__PURE__ */ React25.createElement(
3544
- "svg",
3545
- {
3546
- className: cn("size-4", className2),
3547
- viewBox: "0 0 24 24",
3548
- fill: "none",
3549
- stroke: "currentColor",
3550
- strokeWidth: "2",
3551
- strokeLinecap: "round",
3552
- strokeLinejoin: "round",
3553
- ...props2
3554
- },
3555
- /* @__PURE__ */ React25.createElement("polyline", { points: "9 18 15 12 9 6" })
3556
- );
3557
- }
3558
- return /* @__PURE__ */ React25.createElement(
3559
- "svg",
3560
- {
3561
- className: cn("size-4", className2),
3562
- viewBox: "0 0 24 24",
3563
- fill: "none",
3564
- stroke: "currentColor",
3565
- strokeWidth: "2",
3566
- strokeLinecap: "round",
3567
- strokeLinejoin: "round",
3568
- ...props2
3569
- },
3570
- /* @__PURE__ */ React25.createElement("polyline", { points: "6 9 12 15 18 9" })
3571
- );
3572
- },
3573
- DayButton: CalendarDayButton,
3574
- WeekNumber: ({ children, ...props2 }) => {
3575
- return /* @__PURE__ */ React25.createElement("td", { ...props2 }, /* @__PURE__ */ React25.createElement("div", { className: "flex size-(--cell-size) items-center justify-center text-center" }, children));
3576
- },
3577
- ...components
3578
- },
3579
- ...props
3580
- }
3581
- );
3582
- }
3583
- function CalendarDayButton({
3584
- className,
3585
- day,
3586
- modifiers,
3587
- ...props
3588
- }) {
3589
- const defaultClassNames = getDefaultClassNames();
3590
- const ref = React25.useRef(null);
3591
- React25.useEffect(() => {
3592
- if (modifiers.focused) ref.current?.focus();
3593
- }, [modifiers.focused]);
3594
- return /* @__PURE__ */ React25.createElement(
3595
- Button,
3596
- {
3597
- ref,
3598
- variant: "ghost",
3599
- size: "icon",
3600
- "data-day": day.date.toLocaleDateString(),
3601
- "data-selected-single": modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle,
3602
- "data-range-start": modifiers.range_start,
3603
- "data-range-end": modifiers.range_end,
3604
- "data-range-middle": modifiers.range_middle,
3605
- className: cn(
3606
- // Core structure
3607
- "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal",
3608
- // Selected states - uses CSS variables
3609
- "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground",
3610
- "data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground",
3611
- "data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground",
3612
- "data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground",
3613
- // Focus state
3614
- "group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10",
3615
- "group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-[3px]",
3616
- // Rounding based on position
3617
- "data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md",
3618
- "data-[range-middle=true]:rounded-none",
3619
- "data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md",
3620
- // Nested span styling
3621
- "[&>span]:text-xs [&>span]:opacity-70",
3622
- defaultClassNames.day,
3623
- className
3624
- ),
3625
- ...props
3626
- }
3627
- );
3628
- }
3629
-
3630
- // src/inputs/DatePicker.tsx
3631
- function formatDate(date, format) {
3632
- if (!date) return "";
3633
- const d = new Date(date);
3634
- const month = String(d.getMonth() + 1).padStart(2, "0");
3635
- const day = String(d.getDate()).padStart(2, "0");
3636
- const year = d.getFullYear();
3637
- return format.replace("MM", month).replace("dd", day).replace("yyyy", String(year)).replace("yy", String(year).slice(2));
3638
- }
3639
- function DatePickerDayButton({
3640
- day,
3641
- modifiers,
3642
- className,
3643
- children,
3644
- ...props
3645
- }) {
3646
- return /* @__PURE__ */ React25.createElement(
3647
- "button",
3648
- {
3649
- type: "button",
3650
- className: cn(
3651
- "flex items-center justify-center h-8 w-8 rounded-md border-none bg-transparent cursor-pointer text-sm transition-colors",
3652
- "hover:bg-accent",
3653
- modifiers.selected && "bg-primary text-primary-foreground font-semibold",
3654
- !modifiers.selected && modifiers.today && "border border-primary",
3655
- modifiers.disabled && "cursor-not-allowed opacity-50 pointer-events-none",
3656
- className
3657
- ),
3658
- ...props
3659
- },
3660
- children ?? day.date.getDate()
3661
- );
3662
- }
3663
- function DatePicker({
3664
- name,
3665
- value,
3666
- onChange,
3667
- onBlur,
3668
- disabled = false,
3669
- required = false,
3670
- error = false,
3671
- className = "",
3672
- placeholder = "Select date...",
3673
- format = "MM/dd/yyyy",
3674
- minDate,
3675
- maxDate,
3676
- disabledDates = [],
3677
- isDateDisabled,
3678
- clearable = true,
3679
- showIcon = true,
3680
- ...props
3681
- }) {
3682
- const [isOpen, setIsOpen] = React25.useState(false);
3683
- const [hasInteracted, setHasInteracted] = React25.useState(false);
3684
- const [selectedMonth, setSelectedMonth] = React25.useState(
3685
- value || /* @__PURE__ */ new Date()
3686
- );
3687
- const inputRef = React25.useRef(null);
3688
- React25.useEffect(() => {
3689
- if (value) {
3690
- setSelectedMonth(value);
3691
- }
3692
- }, [value]);
3693
- const disabledMatchers = React25.useMemo(() => {
3694
- const matchers = [];
3695
- if (minDate) {
3696
- matchers.push({ before: minDate });
3697
- }
3698
- if (maxDate) {
3699
- matchers.push({ after: maxDate });
3700
- }
3701
- if (disabledDates.length > 0) {
3702
- matchers.push(disabledDates);
3703
- }
3704
- if (isDateDisabled) {
3705
- matchers.push(isDateDisabled);
3706
- }
3707
- return matchers;
3708
- }, [disabledDates, isDateDisabled, maxDate, minDate]);
3709
- const handleDateSelect = React25.useCallback(
3710
- (date) => {
3711
- if (!date) return;
3712
- onChange(date);
3713
- setSelectedMonth(date);
3714
- setIsOpen(false);
3715
- onBlur?.();
3716
- },
3717
- [onBlur, onChange]
3718
- );
3719
- const handleClear = React25.useCallback(
3720
- (e) => {
3721
- e.stopPropagation();
3722
- onChange(null);
3723
- setIsOpen(false);
3724
- inputRef.current?.focus();
3725
- },
3726
- [onChange]
3727
- );
3728
- const handleOpenChange = React25.useCallback(
3729
- (nextOpen) => {
3730
- if (disabled) {
3731
- setIsOpen(false);
3732
- return;
3733
- }
3734
- if (nextOpen) {
3735
- if (!hasInteracted) {
3736
- setHasInteracted(true);
3737
- }
3738
- setIsOpen(true);
3739
- return;
3740
- }
3741
- if (isOpen && hasInteracted) {
3742
- onBlur?.();
3743
- }
3744
- setIsOpen(false);
3745
- },
3746
- [disabled, hasInteracted, isOpen, onBlur]
3747
- );
3748
- const handleInputBlur = React25.useCallback(() => {
3749
- if (!isOpen) {
3750
- onBlur?.();
3751
- }
3752
- }, [isOpen, onBlur]);
3753
- const handleInputClick = React25.useCallback(() => {
3754
- if (!hasInteracted) {
3755
- setHasInteracted(true);
3756
- }
3757
- }, [hasInteracted]);
3758
- const hasValue = Boolean(value);
3759
- const displayValue = formatDate(value, format);
3760
- const combinedClassName = cn("relative", className);
3761
- return /* @__PURE__ */ React25.createElement("div", { className: combinedClassName }, /* @__PURE__ */ React25.createElement(
3762
- "input",
3763
- {
3764
- type: "hidden",
3765
- name,
3766
- value: value ? value.toISOString() : ""
3767
- }
3768
- ), /* @__PURE__ */ React25.createElement(Popover, { open: isOpen, onOpenChange: handleOpenChange }, /* @__PURE__ */ React25.createElement("div", { className: "relative" }, showIcon && /* @__PURE__ */ React25.createElement(
3769
- "span",
3770
- {
3771
- className: "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",
3772
- "aria-hidden": "true"
3773
- },
3774
- /* @__PURE__ */ React25.createElement(
3775
- "svg",
3776
- {
3777
- xmlns: "http://www.w3.org/2000/svg",
3778
- width: "18",
3779
- height: "18",
3780
- viewBox: "0 0 24 24",
3781
- fill: "none",
3782
- stroke: "currentColor",
3783
- strokeLinecap: "round",
3784
- strokeLinejoin: "round",
3785
- strokeWidth: "2"
3786
- },
3787
- /* @__PURE__ */ React25.createElement("path", { d: "M8 2v4m8-4v4m5 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8M3 10h18m-5 10l2 2l4-4" })
3788
- )
3789
- ), /* @__PURE__ */ React25.createElement(PopoverTrigger, { asChild: true }, /* @__PURE__ */ React25.createElement(
3790
- "input",
3791
- {
3792
- ref: inputRef,
3793
- id: props.id,
3794
- type: "text",
3795
- className: cn(
3796
- "flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-base shadow-sm transition-colors",
3797
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3798
- "disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
3799
- INPUT_AUTOFILL_RESET_CLASSES,
3800
- showIcon ? "pl-10" : "pl-3",
3801
- clearable && value ? "pr-10" : "pr-3",
3802
- !error && hasValue && "ring-2 ring-ring",
3803
- error && "border-destructive ring-1 ring-destructive"
3804
- ),
3805
- value: displayValue,
3806
- onClick: handleInputClick,
3807
- onBlur: handleInputBlur,
3808
- disabled,
3809
- required,
3810
- placeholder,
3811
- "aria-invalid": error || props["aria-invalid"] ? "true" : "false",
3812
- "aria-describedby": props["aria-describedby"],
3813
- "aria-required": required || props["aria-required"],
3814
- readOnly: true
3815
- }
3816
- )), clearable && value && !disabled && /* @__PURE__ */ React25.createElement(
3817
- "button",
3818
- {
3819
- type: "button",
3820
- className: "absolute right-3 top-1/2 -translate-y-1/2 transition-colors",
3821
- onClick: handleClear,
3822
- "aria-label": "Clear date",
3823
- tabIndex: -1
3824
- },
3825
- "\u2715"
3826
- )), !disabled && /* @__PURE__ */ React25.createElement(
3827
- PopoverContent,
3828
- {
3829
- align: "start",
3830
- sideOffset: 4,
3831
- className: "w-auto p-0",
3832
- onOpenAutoFocus: (event) => {
3833
- event.preventDefault();
3834
- }
3835
- },
3836
- /* @__PURE__ */ React25.createElement(
3837
- Calendar,
3838
- {
3839
- mode: "single",
3840
- selected: value ?? void 0,
3841
- onSelect: handleDateSelect,
3842
- month: selectedMonth,
3843
- onMonthChange: setSelectedMonth,
3844
- disabled: disabledMatchers,
3845
- showOutsideDays: true,
3846
- labels: {
3847
- labelGrid: () => "Calendar",
3848
- labelDayButton: (date) => formatDate(date, format),
3849
- labelPrevious: () => "Previous month",
3850
- labelNext: () => "Next month"
3851
- },
3852
- components: {
3853
- DayButton: DatePickerDayButton
3854
- },
3855
- classNames: {
3856
- today: "border border-primary rounded-md bg-transparent"
3857
- }
3858
- }
3859
- )
3860
- )));
3861
- }
3862
- DatePicker.displayName = "DatePicker";
3863
- function normalizeToNativeTime(value) {
3864
- if (!value) return "";
3865
- const twelveHourMatch = value.match(
3866
- /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)$/i
3867
- );
3868
- if (twelveHourMatch) {
3869
- const rawHour = parseInt(twelveHourMatch[1], 10);
3870
- const minute = parseInt(twelveHourMatch[2], 10);
3871
- const period = twelveHourMatch[4].toUpperCase();
3872
- if (Number.isNaN(rawHour) || Number.isNaN(minute) || rawHour < 1 || rawHour > 12 || minute < 0 || minute > 59) {
3873
- return "";
3874
- }
3875
- const normalizedHour = period === "PM" ? rawHour === 12 ? 12 : rawHour + 12 : rawHour === 12 ? 0 : rawHour;
3876
- return `${String(normalizedHour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
3877
- }
3878
- const twentyFourHourMatch = value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
3879
- if (twentyFourHourMatch) {
3880
- const hour = parseInt(twentyFourHourMatch[1], 10);
3881
- const minute = parseInt(twentyFourHourMatch[2], 10);
3882
- if (Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
3883
- return "";
3884
- }
3885
- return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
3886
- }
3887
- return "";
3888
- }
3889
- function formatFromNativeTime(nativeValue, use24Hour) {
3890
- if (!nativeValue) return "";
3891
- const [hourValue, minuteValue] = nativeValue.split(":");
3892
- const hour = parseInt(hourValue, 10);
3893
- const minute = parseInt(minuteValue, 10);
3894
- if (Number.isNaN(hour) || Number.isNaN(minute)) {
3895
- return "";
3896
- }
3897
- if (use24Hour) {
3898
- return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
3899
- }
3900
- const period = hour >= 12 ? "PM" : "AM";
3901
- const hour12 = hour % 12 || 12;
3902
- return `${hour12}:${String(minute).padStart(2, "0")} ${period}`;
3903
- }
3904
- function TimePicker({
3905
- name,
3906
- value,
3907
- onChange,
3908
- onBlur,
3909
- disabled = false,
3910
- required = false,
3911
- error = false,
3912
- className = "",
3913
- placeholder = "Select time...",
3914
- use24Hour = false,
3915
- minuteStep = 1,
3916
- clearable = true,
3917
- showIcon = true,
3918
- ...props
3919
- }) {
3920
- const inputRef = React25.useRef(null);
3921
- const [nativeValue, setNativeValue] = React25.useState(
3922
- normalizeToNativeTime(value)
3923
- );
3924
- React25.useEffect(() => {
3925
- setNativeValue(normalizeToNativeTime(value));
3926
- }, [value]);
3927
- const handleChange = (e) => {
3928
- const nextNativeValue = e.target.value;
3929
- setNativeValue(nextNativeValue);
3930
- onChange(formatFromNativeTime(nextNativeValue, use24Hour));
3931
- };
3932
- const handleClear = (e) => {
3933
- e.stopPropagation();
3934
- setNativeValue("");
3935
- onChange("");
3936
- inputRef.current?.focus();
3937
- };
3938
- const hasValue = Boolean(value);
3939
- const stepInSeconds = Math.max(1, minuteStep * 60);
3940
- return /* @__PURE__ */ React25.createElement("div", { className: cn("relative", className) }, /* @__PURE__ */ React25.createElement("input", { type: "hidden", name, value }), /* @__PURE__ */ React25.createElement("div", { className: "relative" }, showIcon && /* @__PURE__ */ React25.createElement(
3941
- "span",
3942
- {
3943
- className: "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",
3944
- "aria-hidden": "true"
3945
- },
3946
- /* @__PURE__ */ React25.createElement(
3947
- "svg",
3948
- {
3949
- xmlns: "http://www.w3.org/2000/svg",
3950
- width: "18",
3951
- height: "18",
3952
- viewBox: "0 0 24 24",
3953
- fill: "none",
3954
- stroke: "currentColor",
3955
- strokeLinecap: "round",
3956
- strokeLinejoin: "round",
3957
- strokeWidth: "2"
3958
- },
3959
- /* @__PURE__ */ React25.createElement("circle", { cx: "12", cy: "12", r: "10" }),
3960
- /* @__PURE__ */ React25.createElement("path", { d: "M12 6v6l4 2" })
3961
- )
3962
- ), /* @__PURE__ */ React25.createElement(
3963
- Input,
3964
- {
3965
- ref: inputRef,
3966
- type: "time",
3967
- className: cn(
3968
- "appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none",
3969
- INPUT_AUTOFILL_RESET_CLASSES,
3970
- showIcon ? "pl-10" : "pl-3",
3971
- clearable && value ? "pr-10" : "pr-3",
3972
- !error && hasValue && "ring-2 ring-ring",
3973
- error && "border-destructive ring-1 ring-destructive"
3974
- ),
3975
- value: nativeValue,
3976
- onChange: handleChange,
3977
- onBlur,
3978
- disabled,
3979
- required,
3980
- step: stepInSeconds,
3981
- placeholder,
3982
- "aria-invalid": error || props["aria-invalid"] ? "true" : "false",
3983
- "aria-describedby": props["aria-describedby"],
3984
- "aria-required": required || props["aria-required"],
3985
- ...props
3986
- }
3987
- ), clearable && value && !disabled && /* @__PURE__ */ React25.createElement(
3988
- Button,
3989
- {
3990
- type: "button",
3991
- variant: "ghost",
3992
- size: "icon",
3993
- className: "absolute right-1.5 top-1/2 h-7 w-7 -translate-y-1/2 p-0",
3994
- onClick: handleClear,
3995
- "aria-label": "Clear time",
3996
- tabIndex: -1
3997
- },
3998
- /* @__PURE__ */ React25.createElement(
3999
- "svg",
4000
- {
4001
- width: "14",
4002
- height: "14",
4003
- viewBox: "0 0 24 24",
4004
- fill: "none",
4005
- stroke: "currentColor",
4006
- strokeWidth: "2",
4007
- strokeLinecap: "round",
4008
- strokeLinejoin: "round",
4009
- "aria-hidden": "true"
4010
- },
4011
- /* @__PURE__ */ React25.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
4012
- /* @__PURE__ */ React25.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
4013
- )
4014
- )));
4015
- }
4016
- TimePicker.displayName = "TimePicker";
4017
- function formatDate2(date, format) {
4018
- if (!date) return "";
4019
- const d = new Date(date);
4020
- const month = String(d.getMonth() + 1).padStart(2, "0");
4021
- const day = String(d.getDate()).padStart(2, "0");
4022
- const year = d.getFullYear();
4023
- return format.replace("MM", month).replace("dd", day).replace("yyyy", String(year)).replace("yy", String(year).slice(2));
4024
- }
4025
- function toDayTimestamp(date) {
4026
- return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
4027
- }
4028
- function isSameDay(date, target) {
4029
- if (!target) return false;
4030
- return toDayTimestamp(date) === toDayTimestamp(target);
4031
- }
4032
- function isDateInRange(date, start, end) {
4033
- if (!start || !end) return false;
4034
- const value = toDayTimestamp(date);
4035
- const startTs = toDayTimestamp(start);
4036
- const endTs = toDayTimestamp(end);
4037
- return value >= Math.min(startTs, endTs) && value <= Math.max(startTs, endTs);
4038
- }
4039
- function DateRangePicker({
4040
- name,
4041
- value = { start: null, end: null },
4042
- onChange,
4043
- onBlur,
4044
- disabled = false,
4045
- required = false,
4046
- error = false,
4047
- className = "",
4048
- placeholder = "Select date range...",
4049
- format = "MM/dd/yyyy",
4050
- minDate,
4051
- maxDate,
4052
- disabledDates = [],
4053
- isDateDisabled,
4054
- clearable = true,
4055
- showIcon = true,
4056
- separator = " - ",
4057
- ...props
4058
- }) {
4059
- const [isOpen, setIsOpen] = React25.useState(false);
4060
- const [hasInteracted, setHasInteracted] = React25.useState(false);
4061
- const [selectedMonth, setSelectedMonth] = React25.useState(
4062
- value.start || /* @__PURE__ */ new Date()
4063
- );
4064
- const [rangeStart, setRangeStart] = React25.useState(value.start);
4065
- const [rangeEnd, setRangeEnd] = React25.useState(value.end);
4066
- const [hoverDate, setHoverDate] = React25.useState(null);
4067
- const inputRef = React25.useRef(null);
4068
- React25.useEffect(() => {
4069
- setRangeStart(value.start);
4070
- setRangeEnd(value.end);
4071
- if (value.start) {
4072
- setSelectedMonth(value.start);
4073
- }
4074
- }, [value]);
4075
- const disabledMatchers = React25.useMemo(() => {
4076
- const matchers = [];
4077
- if (minDate) {
4078
- matchers.push({ before: minDate });
4079
- }
4080
- if (maxDate) {
4081
- matchers.push({ after: maxDate });
4082
- }
4083
- if (disabledDates.length > 0) {
4084
- matchers.push(disabledDates);
4085
- }
4086
- if (isDateDisabled) {
4087
- matchers.push(isDateDisabled);
4088
- }
4089
- return matchers;
4090
- }, [disabledDates, isDateDisabled, maxDate, minDate]);
4091
- const handleDateSelect = React25.useCallback(
4092
- (date) => {
4093
- if (!rangeStart || rangeEnd) {
4094
- setRangeStart(date);
4095
- setRangeEnd(null);
4096
- setHoverDate(null);
4097
- setSelectedMonth(date);
4098
- onChange({ start: date, end: null });
4099
- onBlur?.();
4100
- return;
4101
- }
4102
- if (toDayTimestamp(date) < toDayTimestamp(rangeStart)) {
4103
- setRangeStart(date);
4104
- setRangeEnd(rangeStart);
4105
- setHoverDate(null);
4106
- setSelectedMonth(date);
4107
- onChange({ start: date, end: rangeStart });
4108
- setIsOpen(false);
4109
- onBlur?.();
4110
- return;
4111
- }
4112
- setRangeEnd(date);
4113
- setHoverDate(null);
4114
- setSelectedMonth(date);
4115
- onChange({ start: rangeStart, end: date });
4116
- setIsOpen(false);
4117
- onBlur?.();
4118
- },
4119
- [onBlur, onChange, rangeEnd, rangeStart]
4120
- );
4121
- const handleClear = React25.useCallback(
4122
- (e) => {
4123
- e.stopPropagation();
4124
- setRangeStart(null);
4125
- setRangeEnd(null);
4126
- setHoverDate(null);
4127
- setIsOpen(false);
4128
- onChange({ start: null, end: null });
4129
- inputRef.current?.focus();
4130
- },
4131
- [onChange]
4132
- );
4133
- const handleOpenChange = React25.useCallback(
4134
- (nextOpen) => {
4135
- if (disabled) {
4136
- setIsOpen(false);
4137
- return;
4138
- }
4139
- if (nextOpen) {
4140
- if (!hasInteracted) {
4141
- setHasInteracted(true);
4142
- }
4143
- setIsOpen(true);
4144
- return;
4145
- }
4146
- if (isOpen && hasInteracted) {
4147
- onBlur?.();
4148
- }
4149
- setHoverDate(null);
4150
- setIsOpen(false);
4151
- },
4152
- [disabled, hasInteracted, isOpen, onBlur]
4153
- );
4154
- const handleInputBlur = React25.useCallback(() => {
4155
- if (!isOpen) {
4156
- onBlur?.();
4157
- }
4158
- }, [isOpen, onBlur]);
4159
- const handleInputClick = React25.useCallback(() => {
4160
- if (!hasInteracted) {
4161
- setHasInteracted(true);
4162
- }
4163
- }, [hasInteracted]);
4164
- const RangeDayButton = React25.useCallback(
4165
- ({
4166
- day,
4167
- modifiers,
4168
- className: dayClassName,
4169
- children,
4170
- onClick,
4171
- onMouseEnter,
4172
- onMouseLeave,
4173
- ...rest
4174
- }) => {
4175
- const date = day.date;
4176
- const isStart = isSameDay(date, rangeStart);
4177
- const isEnd = isSameDay(date, rangeEnd);
4178
- const isRangeEndpoint = isStart || isEnd;
4179
- const isInCommittedRange = isDateInRange(date, rangeStart, rangeEnd);
4180
- const isInHoverRange = !!rangeStart && !rangeEnd && !!hoverDate && isDateInRange(date, rangeStart, hoverDate);
4181
- const isRangeHighlight = (isInCommittedRange || isInHoverRange) && !isRangeEndpoint;
4182
- const isToday = isSameDay(date, /* @__PURE__ */ new Date());
4183
- return /* @__PURE__ */ React25.createElement(
4184
- "button",
4185
- {
4186
- type: "button",
4187
- ...rest,
4188
- className: cn(
4189
- "flex items-center justify-center h-8 w-8 rounded-md border-none bg-transparent cursor-pointer text-sm transition-colors",
4190
- "hover:bg-accent",
4191
- isRangeEndpoint && "bg-primary text-primary-foreground font-semibold",
4192
- isRangeHighlight && "bg-accent",
4193
- !isRangeEndpoint && !isRangeHighlight && isToday && "border border-primary",
4194
- modifiers.disabled && "cursor-not-allowed opacity-50 pointer-events-none",
4195
- dayClassName
4196
- ),
4197
- onClick: (event) => {
4198
- onClick?.(event);
4199
- if (modifiers.disabled) return;
4200
- handleDateSelect(date);
4201
- },
4202
- onMouseEnter: (event) => {
4203
- onMouseEnter?.(event);
4204
- if (modifiers.disabled) {
4205
- setHoverDate(null);
4206
- return;
4207
- }
4208
- setHoverDate(date);
4209
- },
4210
- onMouseLeave: (event) => {
4211
- onMouseLeave?.(event);
4212
- setHoverDate(null);
4213
- }
4214
- },
4215
- children ?? date.getDate()
4216
- );
4217
- },
4218
- [handleDateSelect, hoverDate, rangeEnd, rangeStart]
4219
- );
4220
- const hasValue = Boolean(rangeStart || rangeEnd);
4221
- const selectedRange = rangeStart || rangeEnd ? {
4222
- from: rangeStart ?? void 0,
4223
- to: rangeEnd ?? void 0
4224
- } : void 0;
4225
- const displayValue = rangeStart && rangeEnd ? `${formatDate2(rangeStart, format)}${separator}${formatDate2(rangeEnd, format)}` : rangeStart ? formatDate2(rangeStart, format) : "";
4226
- const combinedClassName = cn("relative", className);
4227
- return /* @__PURE__ */ React25.createElement("div", { className: combinedClassName }, /* @__PURE__ */ React25.createElement(
4228
- "input",
4229
- {
4230
- type: "hidden",
4231
- name: `${name}[start]`,
4232
- value: rangeStart ? rangeStart.toISOString() : ""
4233
- }
4234
- ), /* @__PURE__ */ React25.createElement(
4235
- "input",
4236
- {
4237
- type: "hidden",
4238
- name: `${name}[end]`,
4239
- value: rangeEnd ? rangeEnd.toISOString() : ""
4240
- }
4241
- ), /* @__PURE__ */ React25.createElement(Popover, { open: isOpen, onOpenChange: handleOpenChange }, /* @__PURE__ */ React25.createElement("div", { className: "relative" }, showIcon && /* @__PURE__ */ React25.createElement(
4242
- "span",
4243
- {
4244
- className: "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",
4245
- "aria-hidden": "true"
4246
- },
4247
- /* @__PURE__ */ React25.createElement(
4248
- "svg",
4249
- {
4250
- xmlns: "http://www.w3.org/2000/svg",
4251
- width: "18",
4252
- height: "18",
4253
- viewBox: "0 0 24 24",
4254
- fill: "none",
4255
- stroke: "currentColor",
4256
- strokeLinecap: "round",
4257
- strokeLinejoin: "round",
4258
- strokeWidth: "2"
4259
- },
4260
- /* @__PURE__ */ React25.createElement("path", { d: "M8 2v4m8-4v4m5 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8M3 10h18m-5 10l2 2l4-4" })
4261
- )
4262
- ), /* @__PURE__ */ React25.createElement(PopoverTrigger, { asChild: true }, /* @__PURE__ */ React25.createElement(
4263
- "input",
4264
- {
4265
- ref: inputRef,
4266
- id: props.id,
4267
- type: "text",
4268
- className: cn(
4269
- "flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-base shadow-sm transition-colors",
4270
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
4271
- "disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
4272
- INPUT_AUTOFILL_RESET_CLASSES,
4273
- showIcon ? "pl-10" : "pl-3",
4274
- clearable && (rangeStart || rangeEnd) ? "pr-10" : "pr-3",
4275
- !error && hasValue && "ring-2 ring-ring",
4276
- error && "border-destructive ring-1 ring-destructive"
4277
- ),
4278
- value: displayValue,
4279
- onClick: handleInputClick,
4280
- onBlur: handleInputBlur,
4281
- disabled,
4282
- required,
4283
- placeholder,
4284
- "aria-invalid": error || props["aria-invalid"] ? "true" : "false",
4285
- "aria-describedby": props["aria-describedby"],
4286
- "aria-required": required || props["aria-required"],
4287
- readOnly: true
4288
- }
4289
- )), clearable && (rangeStart || rangeEnd) && !disabled && /* @__PURE__ */ React25.createElement(
4290
- "button",
4291
- {
4292
- type: "button",
4293
- className: "absolute right-3 top-1/2 -translate-y-1/2 transition-colors",
4294
- onClick: handleClear,
4295
- "aria-label": "Clear date range",
4296
- tabIndex: -1
4297
- },
4298
- "\u2715"
4299
- )), !disabled && /* @__PURE__ */ React25.createElement(
4300
- PopoverContent,
4301
- {
4302
- align: "start",
4303
- sideOffset: 4,
4304
- className: "w-auto p-0",
4305
- onOpenAutoFocus: (event) => {
4306
- event.preventDefault();
4307
- }
4308
- },
4309
- /* @__PURE__ */ React25.createElement(
4310
- Calendar,
4311
- {
4312
- mode: "range",
4313
- selected: selectedRange,
4314
- month: selectedMonth,
4315
- onMonthChange: setSelectedMonth,
4316
- disabled: disabledMatchers,
4317
- labels: {
4318
- labelGrid: () => "Calendar",
4319
- labelDayButton: (date) => formatDate2(date, format),
4320
- labelPrevious: () => "Previous month",
4321
- labelNext: () => "Next month"
4322
- },
4323
- components: {
4324
- DayButton: RangeDayButton
4325
- },
4326
- classNames: {
4327
- today: "border border-primary rounded-md bg-transparent"
4328
- },
4329
- showOutsideDays: true
4330
- }
4331
- ),
4332
- rangeStart && !rangeEnd && /* @__PURE__ */ React25.createElement("div", { className: "border-t border-input px-3 py-2 text-center text-xs opacity-70" }, "Select end date")
4333
- )));
4334
- }
4335
- DateRangePicker.displayName = "DateRangePicker";
4336
-
4337
- export { Checkbox2 as Checkbox, CheckboxGroup, DatePicker, DateRangePicker, FileInput, MultiSelect, Radio, Select2 as Select, Switch2 as Switch, TextArea, TextInput, TimePicker };
1
+ export { Checkbox, CheckboxGroup, DatePicker, DateRangePicker, FileInput, MultiSelect, Radio, Select, Switch, TextArea, TextInput, TimePicker } from './chunk-5NT5T5XY.js';
2
+ import './chunk-232KNGJI.js';
4338
3
  //# sourceMappingURL=inputs.js.map
4339
4
  //# sourceMappingURL=inputs.js.map