@k3-universe/react-kit 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5393 -1357
  4. package/dist/kit/builder/form/components/FormBuilder.d.ts +21 -0
  5. package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
  6. package/dist/kit/builder/form/components/fields/FileField.d.ts +1 -1
  7. package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -1
  8. package/dist/kit/builder/stack-dialog/hooks.d.ts +2 -5
  9. package/dist/kit/builder/stack-dialog/hooks.d.ts.map +1 -1
  10. package/dist/kit/builder/stack-dialog/index.d.ts +3 -3
  11. package/dist/kit/builder/stack-dialog/index.d.ts.map +1 -1
  12. package/dist/kit/builder/stack-dialog/renderer.d.ts.map +1 -1
  13. package/dist/kit/builder/stack-dialog/types.d.ts +1 -0
  14. package/dist/kit/builder/stack-dialog/types.d.ts.map +1 -1
  15. package/dist/kit/components/fileuploader/FileUploader.d.ts +4 -0
  16. package/dist/kit/components/fileuploader/FileUploader.d.ts.map +1 -0
  17. package/dist/kit/components/fileuploader/index.d.ts +4 -0
  18. package/dist/kit/components/fileuploader/index.d.ts.map +1 -0
  19. package/dist/kit/components/fileuploader/types.d.ts +63 -0
  20. package/dist/kit/components/fileuploader/types.d.ts.map +1 -0
  21. package/dist/kit/themes/clean-slate.css +60 -0
  22. package/dist/kit/themes/default.css +60 -0
  23. package/dist/kit/themes/minimal-modern.css +60 -0
  24. package/dist/kit/themes/spotify.css +60 -0
  25. package/package.json +2 -1
  26. package/src/index.ts +2 -0
  27. package/src/kit/builder/form/components/FormBuilder.tsx +56 -0
  28. package/src/kit/builder/form/components/fields/FileField.tsx +17 -5
  29. package/src/kit/builder/stack-dialog/hooks.ts +2 -1
  30. package/src/kit/builder/stack-dialog/index.ts +8 -3
  31. package/src/kit/builder/stack-dialog/renderer.tsx +2 -2
  32. package/src/kit/builder/stack-dialog/types.ts +2 -0
  33. package/src/kit/components/fileuploader/FileUploader.tsx +488 -0
  34. package/src/kit/components/fileuploader/index.ts +3 -0
  35. package/src/kit/components/fileuploader/types.ts +73 -0
  36. package/src/kit/components/monthpicker/MonthPicker.tsx +2 -2
  37. package/src/kit/components/monthpicker/MonthRangePicker.tsx +2 -2
  38. package/src/stories/FileUploader.stories.tsx +166 -0
  39. package/src/stories/kit/builder/Form.ArrayLayouts.stories.tsx +1 -1
  40. package/src/stories/kit/builder/Form.Autocomplete.stories.tsx +5 -5
  41. package/src/stories/kit/builder/Form.DateTime.stories.tsx +1 -1
  42. package/src/stories/kit/builder/Form.Files.stories.tsx +125 -0
  43. package/src/stories/kit/builder/Form.Time.stories.tsx +1 -1
@@ -0,0 +1,488 @@
1
+ import { useCallback, useEffect, useRef, useState, memo } from "react";
2
+ import { useDropzone, type Accept } from "react-dropzone";
3
+ import { cn } from "../../../shadcn/lib/utils";
4
+ import { Button } from "../../../shadcn/ui/button";
5
+ import { Progress } from "../../../shadcn/ui/progress";
6
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../shadcn/ui/tooltip";
7
+ import {
8
+ Loader2,
9
+ UploadCloud,
10
+ Image as ImageIcon,
11
+ File as FileIcon,
12
+ FileText,
13
+ FileCode,
14
+ Video,
15
+ Music,
16
+ Archive,
17
+ CheckCircle2,
18
+ XCircle,
19
+ Download,
20
+ Trash2,
21
+ RotateCcw,
22
+ } from "lucide-react";
23
+ import type { FileRecord, FileUploaderProps } from "./types";
24
+
25
+ // Cache preview URLs per File instance without mutating the File object
26
+ const previewUrlMap: WeakMap<File, string> = new WeakMap();
27
+
28
+ function formatBytes(bytes?: number) {
29
+ if (!bytes && bytes !== 0) return "";
30
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
31
+ if (bytes === 0) return "0 Byte";
32
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
33
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
34
+ }
35
+
36
+ function isImageType(type?: string, name?: string) {
37
+ if (!type && name) {
38
+ const ext = name.toLowerCase().split(".").pop();
39
+ if (!ext) return false;
40
+ return ["png", "jpg", "jpeg", "webp", "gif", "bmp", "svg", "heic", "heif"].includes(ext);
41
+ }
42
+ return !!type?.startsWith("image/");
43
+ }
44
+
45
+ function pickIconByType(type?: string, name?: string) {
46
+ const t = (type || "").toLowerCase();
47
+ const ext = (name?.split(".").pop() || "").toLowerCase();
48
+ if (t.startsWith("image/") || ["png","jpg","jpeg","webp","gif","bmp","svg","heic","heif"].includes(ext)) return <ImageIcon className="h-8 w-8" />;
49
+ if (t.startsWith("video/") || ["mp4","mov","webm","mkv"].includes(ext)) return <Video className="h-8 w-8" />;
50
+ if (t.startsWith("audio/") || ["mp3","wav","aac","flac"].includes(ext)) return <Music className="h-8 w-8" />;
51
+ if (["zip","rar","7z","tar","gz"].includes(ext)) return <Archive className="h-8 w-8" />;
52
+ if (["txt","md","rtf"].includes(ext)) return <FileText className="h-8 w-8" />;
53
+ if (["js","ts","tsx","json","yml","yaml","xml","html","css"].includes(ext)) return <FileCode className="h-8 w-8" />;
54
+ return <FileIcon className="h-8 w-8" />;
55
+ }
56
+
57
+ function getPreviewUrl(file: FileRecord) {
58
+ if (file.thumbnailUrl) return file.thumbnailUrl;
59
+ if (file.url && isImageType(file.type, file.name)) return file.url;
60
+ if (file.file && isImageType(file.file.type, file.file.name)) {
61
+ const cached = previewUrlMap.get(file.file);
62
+ if (cached) return cached;
63
+ const created = URL.createObjectURL(file.file);
64
+ previewUrlMap.set(file.file, created);
65
+ return created;
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ export function FileUploader({
71
+ value,
72
+ defaultValue,
73
+ onChange,
74
+ uploader,
75
+ onUploadSuccess,
76
+ onUploadError,
77
+ onRemove,
78
+ onRetry,
79
+ onRetryAll,
80
+ multiple = true,
81
+ maxFiles,
82
+ accept,
83
+ layout = "grid",
84
+ disabled,
85
+ withDownload = true,
86
+ placeholder = "Drag and drop files here, or click to select",
87
+ className,
88
+ }: FileUploaderProps) {
89
+ const isControlled = value !== undefined;
90
+ const [files, setFiles] = useState<FileRecord[]>(() => defaultValue ?? []);
91
+
92
+ const prevUrlsRef = useRef<Set<string>>(new Set());
93
+ useEffect(() => {
94
+ return () => {
95
+ prevUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
96
+ prevUrlsRef.current.clear();
97
+ };
98
+ }, []);
99
+
100
+ const setFilesAndEmit = useCallback(
101
+ (updater: FileRecord[] | ((prev: FileRecord[]) => FileRecord[])) => {
102
+ setFiles((prev) => {
103
+ let next: FileRecord[];
104
+ if (typeof updater === "function") {
105
+ const fn = updater as (p: FileRecord[]) => FileRecord[];
106
+ next = fn(prev);
107
+ } else {
108
+ next = updater;
109
+ }
110
+ onChange?.(next);
111
+ return next;
112
+ });
113
+ },
114
+ [onChange],
115
+ );
116
+
117
+
118
+
119
+ useEffect(() => {
120
+ if (isControlled && value) setFiles(value);
121
+ if (isControlled && !value) setFiles([]);
122
+ }, [isControlled, value]);
123
+
124
+ // Normalize incoming items (default/controlled): if a file has a URL but no status, mark as success
125
+ useEffect(() => {
126
+ setFiles((prev) => {
127
+ let changed = false;
128
+ const next = prev.map((f) => {
129
+ if ((f.url || f.thumbnailUrl) && !f.status) {
130
+ changed = true;
131
+ return { ...f, status: "success" as const, progress: 100 };
132
+ }
133
+ return f;
134
+ });
135
+ return changed ? next : prev;
136
+ });
137
+ }, [isControlled]);
138
+
139
+ const handleRemove = useCallback(
140
+ async (idx: number) => {
141
+ const target = files[idx];
142
+ try {
143
+ await onRemove?.(target);
144
+ } catch {
145
+ // ignore removal error UX for now
146
+ }
147
+ setFilesAndEmit((prev) => prev.filter((_, i) => i !== idx));
148
+ },
149
+ [files, onRemove, setFilesAndEmit],
150
+ );
151
+
152
+ const startUpload = useCallback(
153
+ async (index: number, f: File) => {
154
+ if (!uploader) return;
155
+ try {
156
+ setFilesAndEmit((prev) => {
157
+ const n = [...prev];
158
+ n[index] = { ...n[index], status: "uploading", progress: 0, errorMessage: undefined };
159
+ return n;
160
+ });
161
+ const result = await uploader(f, (pct) => {
162
+ setFilesAndEmit((prev) => {
163
+ const n = [...prev];
164
+ if (!n[index]) return prev;
165
+ n[index] = { ...n[index], progress: Math.min(100, Math.max(0, Math.round(pct))), status: "uploading" };
166
+ return n;
167
+ });
168
+ });
169
+ setFilesAndEmit((prev) => {
170
+ const n = [...prev];
171
+ if (!n[index]) return prev;
172
+ n[index] = {
173
+ ...n[index],
174
+ ...result,
175
+ status: "success",
176
+ progress: 100,
177
+ };
178
+ return n;
179
+ });
180
+ const uploaded = (isControlled ? value : files)[index] ?? undefined;
181
+ if (uploaded) onUploadSuccess?.(uploaded);
182
+ } catch (err) {
183
+ setFilesAndEmit((prev) => {
184
+ const n = [...prev];
185
+ if (!n[index]) return prev;
186
+ const msg = err && typeof err === "object" && "message" in (err as Record<string, unknown>) ? String((err as { message?: unknown }).message) : "Upload failed";
187
+ n[index] = { ...n[index], status: "error", errorMessage: msg };
188
+ return n;
189
+ });
190
+ const failed = (isControlled ? value : files)[index] ?? undefined;
191
+ if (failed) onUploadError?.(failed, err);
192
+ }
193
+ },
194
+ [files, isControlled, onUploadError, onUploadSuccess, setFilesAndEmit, uploader, value],
195
+ );
196
+
197
+ const handleRetry = useCallback(
198
+ (idx: number) => {
199
+ const fr = files[idx];
200
+ if (!uploader || !fr?.file) return;
201
+ onRetry?.(fr);
202
+ void startUpload(idx, fr.file);
203
+ },
204
+ [files, onRetry, startUpload, uploader],
205
+ );
206
+
207
+ const handleRetryAll = useCallback(() => {
208
+ if (!uploader) return;
209
+ const failedFiles = files.filter((f) => f.status === "error" && !!f.file);
210
+ if (failedFiles.length === 0) return;
211
+ onRetryAll?.(failedFiles);
212
+ failedFiles.forEach((fr) => {
213
+ const idx = files.indexOf(fr);
214
+ if (idx >= 0 && fr.file) {
215
+ onRetry?.(fr);
216
+ void startUpload(idx, fr.file);
217
+ }
218
+ });
219
+ }, [files, onRetry, onRetryAll, startUpload, uploader]);
220
+
221
+ const onDrop = useCallback(
222
+ (acceptedFiles: File[]) => {
223
+ if (!acceptedFiles?.length) return;
224
+ setFilesAndEmit((prev) => {
225
+ const existing = [...prev];
226
+ const capacity = typeof maxFiles === "number" ? Math.max(0, maxFiles - existing.length) : acceptedFiles.length;
227
+ const incoming = acceptedFiles.slice(0, capacity).map<FileRecord>((f) => ({
228
+ id: undefined,
229
+ url: undefined,
230
+ thumbnailUrl: undefined,
231
+ file: f,
232
+ name: f.name,
233
+ size: f.size,
234
+ type: f.type,
235
+ status: uploader ? "uploading" : "idle",
236
+ progress: uploader ? 0 : undefined,
237
+ }));
238
+ const next = multiple ? [...existing, ...incoming] : [incoming[0]].filter(Boolean) as FileRecord[];
239
+ return next;
240
+ });
241
+
242
+ if (uploader) {
243
+ const baseIndex = (isControlled ? value : files)?.length ?? 0;
244
+ const capacity = typeof maxFiles === "number" ? Math.max(0, maxFiles - ((isControlled ? value : files)?.length ?? 0)) : acceptedFiles.length;
245
+ acceptedFiles.slice(0, capacity).forEach((f, i) => {
246
+ const index = multiple ? baseIndex + i : 0;
247
+ void startUpload(index, f);
248
+ });
249
+ }
250
+ },
251
+ [files, isControlled, maxFiles, multiple, startUpload, uploader, value, setFilesAndEmit],
252
+ );
253
+
254
+ const disabledBecauseFull = !!maxFiles && files.length >= maxFiles;
255
+
256
+ const { getRootProps, getInputProps, isDragActive, isDragReject, open } = useDropzone({
257
+ onDrop,
258
+ multiple,
259
+ maxFiles: multiple ? maxFiles : 1,
260
+ accept: accept as Accept | undefined,
261
+ noClick: true,
262
+ noKeyboard: true,
263
+ disabled: disabled || disabledBecauseFull,
264
+ });
265
+
266
+ useEffect(() => {
267
+ const urls = new Set<string>();
268
+ files.forEach((fr) => {
269
+ const preview = getPreviewUrl(fr);
270
+ if (preview) urls.add(preview);
271
+ });
272
+ prevUrlsRef.current.forEach((prev) => {
273
+ if (!urls.has(prev)) URL.revokeObjectURL(prev);
274
+ });
275
+ prevUrlsRef.current = urls;
276
+ }, [files]);
277
+
278
+ const rootClasses = cn(
279
+ "w-full border border-dashed rounded-md p-4 text-sm transition-colors bg-background",
280
+ "hover:border-foreground/50",
281
+ isDragActive && "border-primary",
282
+ isDragReject && "border-destructive",
283
+ (disabled || disabledBecauseFull) && "opacity-50 pointer-events-none",
284
+ );
285
+
286
+ // (renderThumb removed; inlined into FileItem for better memoization)
287
+
288
+ // Stable handler refs so children don't see new function identities each render
289
+ const removeRef = useRef(handleRemove);
290
+ useEffect(() => { removeRef.current = handleRemove; }, [handleRemove]);
291
+ const retryRef = useRef(handleRetry);
292
+ useEffect(() => { retryRef.current = handleRetry; }, [handleRetry]);
293
+ const onRemoveAt = useCallback((i: number) => { removeRef.current(i); }, []);
294
+ const onRetryAt = useCallback((i: number) => { retryRef.current(i); }, []);
295
+
296
+ type FileItemProps = {
297
+ fr: FileRecord;
298
+ idx: number;
299
+ layout: "grid" | "list";
300
+ withDownload: boolean;
301
+ uploaderPresent: boolean;
302
+ onRemove: (idx: number) => void;
303
+ onRetry: (idx: number) => void;
304
+ };
305
+
306
+ const FileItem = memo(function FileItem({ fr, idx, layout, withDownload, uploaderPresent, onRemove, onRetry }: FileItemProps) {
307
+ const name = fr.name;
308
+ const size = formatBytes(fr.size);
309
+ const error = fr.status === "error" ? fr.errorMessage : undefined;
310
+ const preview = getPreviewUrl(fr);
311
+ return (
312
+ <div className={cn(
313
+ "flex items-center gap-3 border rounded-md p-2 bg-card",
314
+ layout === "grid" ? "flex-col items-stretch" : "flex-row",
315
+ )}>
316
+ <div className={cn(layout === "grid" ? "self-center" : "")}>{
317
+ (
318
+ <div className={cn(
319
+ "relative overflow-hidden bg-muted/40 border rounded-md flex items-center justify-center",
320
+ layout === "grid" ? "h-28 w-28" : "h-16 w-16",
321
+ )}>
322
+ {preview ? (
323
+ <img src={preview} alt={fr.name} className="object-cover w-full h-full" />
324
+ ) : (
325
+ <div className="flex items-center justify-center text-muted-foreground">
326
+ {pickIconByType(fr.type, fr.name)}
327
+ </div>
328
+ )}
329
+ {fr.status === "uploading" ? (
330
+ <div className="absolute inset-0 bg-black/40 flex items-center justify-center">
331
+ <Loader2 className="h-6 w-6 text-white animate-spin" />
332
+ </div>
333
+ ) : null}
334
+ {fr.status === "success" ? (
335
+ <div className="absolute top-1 right-1 text-green-500">
336
+ <CheckCircle2 className="h-5 w-5 drop-shadow" />
337
+ </div>
338
+ ) : null}
339
+ {fr.status === "error" ? (
340
+ <div className="absolute top-1 right-1 text-red-500">
341
+ <XCircle className="h-5 w-5 drop-shadow" />
342
+ </div>
343
+ ) : null}
344
+ </div>
345
+ )
346
+ }</div>
347
+ <div className={cn("min-w-0 flex-1", layout === "grid" ? "mt-2" : "")}>
348
+ <div className="flex items-center justify-between gap-2">
349
+ <div className="min-w-0">
350
+ <div className="truncate font-medium" title={name}>{name}</div>
351
+ <div className="text-xs text-muted-foreground">{size}</div>
352
+ </div>
353
+ <div className="flex items-center gap-1">
354
+ {fr.status === "error" ? (
355
+ <Tooltip>
356
+ <TooltipTrigger asChild>
357
+ <Button
358
+ size="icon"
359
+ variant="ghost"
360
+ onClick={() => onRetry(idx)}
361
+ aria-label="Retry upload"
362
+ disabled={!uploaderPresent || !fr.file}
363
+ >
364
+ <RotateCcw className="h-4 w-4" />
365
+ </Button>
366
+ </TooltipTrigger>
367
+ <TooltipContent>Retry</TooltipContent>
368
+ </Tooltip>
369
+ ) : null}
370
+ {withDownload && (fr.url || fr.thumbnailUrl) ? (
371
+ <Tooltip>
372
+ <TooltipTrigger asChild>
373
+ <Button
374
+ size="icon"
375
+ variant="ghost"
376
+ onClick={() => {
377
+ const url = fr.url ?? fr.thumbnailUrl;
378
+ if (url) {
379
+ window.open(url, "_blank", "noopener,noreferrer");
380
+ }
381
+ }}
382
+ aria-label="Download"
383
+ >
384
+ <Download className="h-4 w-4" />
385
+ </Button>
386
+ </TooltipTrigger>
387
+ <TooltipContent>Download</TooltipContent>
388
+ </Tooltip>
389
+ ) : null}
390
+ <Tooltip>
391
+ <TooltipTrigger asChild>
392
+ <Button
393
+ size="icon"
394
+ variant="ghost"
395
+ onClick={() => onRemove(idx)}
396
+ aria-label="Remove"
397
+ >
398
+ <Trash2 className="h-4 w-4" />
399
+ </Button>
400
+ </TooltipTrigger>
401
+ <TooltipContent>Remove</TooltipContent>
402
+ </Tooltip>
403
+ </div>
404
+ </div>
405
+ {fr.status === "uploading" ? (
406
+ <div className="mt-2">
407
+ <Progress value={fr.progress ?? 0} />
408
+ </div>
409
+ ) : null}
410
+ {error ? (
411
+ <div className="mt-2 text-xs text-destructive">{error}</div>
412
+ ) : null}
413
+ </div>
414
+ </div>
415
+ );
416
+ }, (prev, next) => prev.fr === next.fr && prev.layout === next.layout && prev.withDownload === next.withDownload && prev.uploaderPresent === next.uploaderPresent && prev.idx === next.idx);
417
+
418
+ return (
419
+ <div className={cn("space-y-3", className)}>
420
+ <div {...getRootProps({ className: rootClasses })}>
421
+ <input {...getInputProps({ onClick: (e) => { (e.target as HTMLInputElement).value = ""; } })} />
422
+ <div className="flex items-center justify-between gap-3">
423
+ <div className="flex items-center gap-2 text-muted-foreground">
424
+ <Button
425
+ size="sm"
426
+ variant="secondary"
427
+ disabled={disabled || disabledBecauseFull}
428
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); open(); }}
429
+ >
430
+ Select files
431
+ </Button>
432
+ <UploadCloud className="h-5 w-5" />
433
+ <div>
434
+ <div className="font-medium">
435
+ {disabledBecauseFull ? "File limit reached" : isDragActive ? "Drop the files here" : placeholder}
436
+ </div>
437
+ <div className="text-xs">
438
+ {accept ? "Specific file types only" : "Any file type"}
439
+ {typeof maxFiles === "number" ? ` • Up to ${maxFiles} file${maxFiles > 1 ? "s" : ""}` : ""}
440
+ </div>
441
+ </div>
442
+ </div>
443
+ <div className="flex items-center gap-2">
444
+ <Button
445
+ size="sm"
446
+ variant="ghost"
447
+ onClick={handleRetryAll}
448
+ disabled={
449
+ !!disabled || !uploader || files.every((f) => f.status !== "error" || !f.file)
450
+ }
451
+ >
452
+ <RotateCcw className="mr-1 h-4 w-4" /> Retry failed
453
+ </Button>
454
+ </div>
455
+ </div>
456
+ </div>
457
+
458
+ <TooltipProvider>
459
+ <div
460
+ className={cn(
461
+ layout === "grid"
462
+ ? "flex flex-wrap gap-3"
463
+ : "flex flex-col gap-2",
464
+ )}
465
+ >
466
+ {files.length === 0 ? (
467
+ <div className="text-sm text-muted-foreground">No files</div>
468
+ ) : (
469
+ files.map((fr, i) => (
470
+ <FileItem
471
+ key={`${fr.name}-${i}`}
472
+ fr={fr}
473
+ idx={i}
474
+ layout={layout}
475
+ withDownload={withDownload}
476
+ uploaderPresent={!!uploader}
477
+ onRemove={onRemoveAt}
478
+ onRetry={onRetryAt}
479
+ />
480
+ ))
481
+ )}
482
+ </div>
483
+ </TooltipProvider>
484
+ </div>
485
+ );
486
+ }
487
+
488
+ export default FileUploader;
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export { default as FileUploader } from "./FileUploader";
3
+ export { FileUploader as default } from "./FileUploader";
@@ -0,0 +1,73 @@
1
+ import type { Accept } from "react-dropzone";
2
+
3
+ export type FileUploadStatus = "idle" | "uploading" | "success" | "error";
4
+
5
+ export type FileRecord = {
6
+ id?: string | number;
7
+ /** Server URL to access the uploaded file */
8
+ url?: string;
9
+ /** Optional dedicated thumbnail URL */
10
+ thumbnailUrl?: string;
11
+ /** Original File object (when chosen locally) */
12
+ file?: File | null;
13
+ name: string;
14
+ size?: number;
15
+ type?: string;
16
+ /** Upload status */
17
+ status?: FileUploadStatus;
18
+ /** 0-100 */
19
+ progress?: number;
20
+ errorMessage?: string;
21
+ /** Any server metadata you want to keep */
22
+ meta?: Record<string, unknown>;
23
+ };
24
+
25
+ export type FileUploaderLayout = "grid" | "list";
26
+
27
+ export type FileUploaderProps = {
28
+ /** Controlled value of files */
29
+ value?: FileRecord[];
30
+ /** Initial files (e.g., from server on edit forms) */
31
+ defaultValue?: FileRecord[];
32
+ /** Called whenever internal files list changes */
33
+ onChange?: (files: FileRecord[]) => void;
34
+
35
+ /**
36
+ * Uploader function invoked for each dropped/selected file.
37
+ * Use the provided onProgress callback to report upload progress (0-100).
38
+ * Resolve with server info like id/url/meta.
39
+ */
40
+ uploader?: (
41
+ file: File,
42
+ onProgress: (pct: number) => void,
43
+ ) => Promise<Partial<FileRecord>>;
44
+
45
+ /** Called after a single file successfully uploaded */
46
+ onUploadSuccess?: (file: FileRecord) => void;
47
+ /** Called after a single file failed to upload */
48
+ onUploadError?: (file: FileRecord, error: unknown) => void;
49
+ /** Called when a file is removed (useful to delete from server) */
50
+ onRemove?: (file: FileRecord) => void | Promise<void>;
51
+ /** Called when user retries an errored file upload */
52
+ onRetry?: (file: FileRecord) => void;
53
+ /** Called when user retries all failed uploads at once */
54
+ onRetryAll?: (files: FileRecord[]) => void;
55
+
56
+ /** Allow selecting multiple files */
57
+ multiple?: boolean;
58
+ /** Max number of files allowed in the list */
59
+ maxFiles?: number;
60
+ /** Accept file types (react-dropzone Accept map) */
61
+ accept?: Accept;
62
+
63
+ /** Layout variant */
64
+ layout?: FileUploaderLayout;
65
+ /** Disable interactions */
66
+ disabled?: boolean;
67
+ /** Show a download action for successfully uploaded files with url */
68
+ withDownload?: boolean;
69
+ /** Optional label or placeholder for the dropzone */
70
+ placeholder?: string;
71
+ /** Additional className */
72
+ className?: string;
73
+ };
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import * as React from 'react';
3
3
  import { ChevronLeft, ChevronRight } from 'lucide-react';
4
- import { buttonVariants } from '@/shadcn/ui/button';
5
- import { cn } from '@/shadcn/lib/utils';
4
+ import { buttonVariants } from '../../../shadcn/ui/button';
5
+ import { cn } from '../../../shadcn/lib/utils';
6
6
 
7
7
  type Month = {
8
8
  number: number;
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import * as React from 'react';
3
3
  import { ChevronLeft, ChevronRight } from 'lucide-react';
4
- import { Button, buttonVariants } from '@/shadcn/ui/button';
5
- import { cn } from '@/shadcn/lib/utils';
4
+ import { Button, buttonVariants } from '../../../shadcn/ui/button';
5
+ import { cn } from '../../../shadcn/lib/utils';
6
6
 
7
7
  const addMonths = (input: Date, months: number) => {
8
8
  const date = new Date(input);