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