uploaderkit 1.0.0 → 2.0.0

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +16 -0
  3. package/README.es.md +1140 -0
  4. package/README.md +1135 -0
  5. package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
  6. package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
  7. package/dist/adapters/gcs.cjs +83 -0
  8. package/dist/adapters/gcs.cjs.map +1 -0
  9. package/dist/adapters/gcs.d.cts +31 -0
  10. package/dist/adapters/gcs.d.ts +31 -0
  11. package/dist/adapters/gcs.js +81 -0
  12. package/dist/adapters/gcs.js.map +1 -0
  13. package/dist/adapters/memory.cjs +27 -0
  14. package/dist/adapters/memory.cjs.map +1 -0
  15. package/dist/adapters/memory.d.cts +18 -0
  16. package/dist/adapters/memory.d.ts +18 -0
  17. package/dist/adapters/memory.js +25 -0
  18. package/dist/adapters/memory.js.map +1 -0
  19. package/dist/adapters/s3.cjs +75 -0
  20. package/dist/adapters/s3.cjs.map +1 -0
  21. package/dist/adapters/s3.d.cts +27 -0
  22. package/dist/adapters/s3.d.ts +27 -0
  23. package/dist/adapters/s3.js +73 -0
  24. package/dist/adapters/s3.js.map +1 -0
  25. package/dist/chunk-3FI44IOW.js +150 -0
  26. package/dist/chunk-3FI44IOW.js.map +1 -0
  27. package/dist/chunk-H7BRW5IY.js +308 -0
  28. package/dist/chunk-H7BRW5IY.js.map +1 -0
  29. package/dist/chunk-PDKAF4GX.js +707 -0
  30. package/dist/chunk-PDKAF4GX.js.map +1 -0
  31. package/dist/chunk-PTEX7F4R.js +309 -0
  32. package/dist/chunk-PTEX7F4R.js.map +1 -0
  33. package/dist/chunk-T5YZG6JW.js +511 -0
  34. package/dist/chunk-T5YZG6JW.js.map +1 -0
  35. package/dist/index.cjs +220 -27
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +57 -170
  38. package/dist/index.d.ts +57 -170
  39. package/dist/index.js +1 -324
  40. package/dist/index.js.map +1 -1
  41. package/dist/presets.cjs +1665 -0
  42. package/dist/presets.cjs.map +1 -0
  43. package/dist/presets.d.cts +100 -0
  44. package/dist/presets.d.ts +100 -0
  45. package/dist/presets.js +405 -0
  46. package/dist/presets.js.map +1 -0
  47. package/dist/react.cjs +981 -0
  48. package/dist/react.cjs.map +1 -0
  49. package/dist/react.d.cts +106 -0
  50. package/dist/react.d.ts +106 -0
  51. package/dist/react.js +5 -0
  52. package/dist/react.js.map +1 -0
  53. package/dist/server/express.cjs +179 -0
  54. package/dist/server/express.cjs.map +1 -0
  55. package/dist/server/express.d.cts +64 -0
  56. package/dist/server/express.d.ts +64 -0
  57. package/dist/server/express.js +111 -0
  58. package/dist/server/express.js.map +1 -0
  59. package/dist/server/next.cjs +165 -0
  60. package/dist/server/next.cjs.map +1 -0
  61. package/dist/server/next.d.cts +38 -0
  62. package/dist/server/next.d.ts +38 -0
  63. package/dist/server/next.js +107 -0
  64. package/dist/server/next.js.map +1 -0
  65. package/dist/server.cjs +550 -0
  66. package/dist/server.cjs.map +1 -0
  67. package/dist/server.d.cts +19 -0
  68. package/dist/server.d.ts +19 -0
  69. package/dist/server.js +80 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/storage-CYkSHWZX.d.cts +133 -0
  72. package/dist/storage-Qc9epG0G.d.ts +133 -0
  73. package/dist/types-BSlJJwti.d.cts +341 -0
  74. package/dist/types-BSlJJwti.d.ts +341 -0
  75. package/dist/ui.cjs +2325 -0
  76. package/dist/ui.cjs.map +1 -0
  77. package/dist/ui.d.cts +331 -0
  78. package/dist/ui.d.ts +331 -0
  79. package/dist/ui.js +749 -0
  80. package/dist/ui.js.map +1 -0
  81. package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
  82. package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
  83. package/dist/useUploader-BiBdS-7y.d.cts +117 -0
  84. package/dist/useUploader-CQHpj_oI.d.ts +117 -0
  85. package/package.json +189 -3
  86. package/tailwind.css +80 -0
package/dist/ui.js ADDED
@@ -0,0 +1,749 @@
1
+ import { useSlottedUploader } from './chunk-PTEX7F4R.js';
2
+ import { useOverlayTransition, useOverlayLayer, useFocusTrap, lockBodyScroll, unlockBodyScroll, cn, useCoarsePointer, Dropzone, UploadCloudIcon, FileViewer } from './chunk-PDKAF4GX.js';
3
+ export { Dropzone, FileViewer, cn } from './chunk-PDKAF4GX.js';
4
+ import { useUploaderLabels, warnDev, useUploader } from './chunk-H7BRW5IY.js';
5
+ export { UploaderProvider, useUploaderLabels } from './chunk-H7BRW5IY.js';
6
+ import { formatFileSize, getFileExtension } from './chunk-T5YZG6JW.js';
7
+ import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
8
+ import { createPortal } from 'react-dom';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+
11
+ var ConfirmDialog = ({
12
+ open,
13
+ title,
14
+ message,
15
+ variant = "danger",
16
+ confirmLabel,
17
+ cancelLabel,
18
+ onConfirm,
19
+ onCancel,
20
+ labels
21
+ }) => {
22
+ const copy = useUploaderLabels(labels);
23
+ const panelRef = useRef(null);
24
+ const { mounted, entered } = useOverlayTransition(open);
25
+ const layer = useOverlayLayer(open);
26
+ useFocusTrap(open, panelRef);
27
+ const cancelRef = useRef(onCancel);
28
+ cancelRef.current = onCancel;
29
+ useEffect(() => {
30
+ if (!open) return;
31
+ const onKey = (event) => {
32
+ if (event.key === "Escape") {
33
+ if (!layer.isTopmost()) return;
34
+ event.stopPropagation();
35
+ cancelRef.current();
36
+ }
37
+ };
38
+ window.addEventListener("keydown", onKey, true);
39
+ lockBodyScroll();
40
+ return () => {
41
+ window.removeEventListener("keydown", onKey, true);
42
+ unlockBodyScroll();
43
+ };
44
+ }, [open, layer]);
45
+ if (!mounted) return null;
46
+ return createPortal(
47
+ /* @__PURE__ */ jsxs(
48
+ "div",
49
+ {
50
+ className: cn(
51
+ "fixed inset-0 z-1000 flex items-end justify-center p-4 sm:items-center",
52
+ !open && "pointer-events-none"
53
+ ),
54
+ onClick: onCancel,
55
+ children: [
56
+ /* @__PURE__ */ jsx(
57
+ "div",
58
+ {
59
+ "aria-hidden": true,
60
+ className: cn(
61
+ "bg-ui-overlay fixed inset-0 transition-opacity duration-200 ease-out",
62
+ entered ? "opacity-100" : "opacity-0"
63
+ )
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs(
67
+ "div",
68
+ {
69
+ ref: panelRef,
70
+ role: "alertdialog",
71
+ "aria-modal": "true",
72
+ "aria-label": title,
73
+ tabIndex: -1,
74
+ onClick: (event) => event.stopPropagation(),
75
+ className: cn(
76
+ "bg-ui-surface rounded-ui-xl relative w-full max-w-md p-6 shadow-2xl outline-none",
77
+ "transition duration-200 ease-out",
78
+ entered ? "translate-y-0 scale-100 opacity-100" : "translate-y-3 scale-[0.97] opacity-0"
79
+ ),
80
+ children: [
81
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4", children: [
82
+ /* @__PURE__ */ jsx(
83
+ "span",
84
+ {
85
+ className: cn(
86
+ "flex size-11 shrink-0 items-center justify-center rounded-full",
87
+ variant === "danger" ? "bg-ui-danger-soft" : "bg-ui-primary-soft"
88
+ ),
89
+ children: /* @__PURE__ */ jsxs(
90
+ "svg",
91
+ {
92
+ viewBox: "0 0 24 24",
93
+ className: cn(
94
+ "size-6",
95
+ variant === "danger" ? "text-ui-danger" : "text-ui-primary"
96
+ ),
97
+ fill: "none",
98
+ stroke: "currentColor",
99
+ strokeWidth: "2",
100
+ strokeLinecap: "round",
101
+ strokeLinejoin: "round",
102
+ "aria-hidden": true,
103
+ children: [
104
+ /* @__PURE__ */ jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
105
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
106
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
107
+ ]
108
+ }
109
+ )
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
113
+ /* @__PURE__ */ jsx("h2", { className: "text-ui-fg text-base font-semibold", children: title }),
114
+ /* @__PURE__ */ jsx("div", { className: "text-ui-muted mt-1 text-sm", children: message })
115
+ ] })
116
+ ] }),
117
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", children: [
118
+ /* @__PURE__ */ jsx(
119
+ "button",
120
+ {
121
+ type: "button",
122
+ "data-autofocus": true,
123
+ onClick: onCancel,
124
+ className: "border-ui-border-strong text-ui-fg hover:bg-ui-surface-muted focus-visible:ring-ui-ring rounded-ui h-10 cursor-pointer border px-4 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none sm:w-auto",
125
+ children: cancelLabel ?? copy.cancel
126
+ }
127
+ ),
128
+ /* @__PURE__ */ jsx(
129
+ "button",
130
+ {
131
+ type: "button",
132
+ onClick: onConfirm,
133
+ className: cn(
134
+ "focus-visible:ring-ui-ring rounded-ui h-10 cursor-pointer px-4 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none sm:w-auto",
135
+ variant === "danger" ? "bg-ui-danger text-ui-danger-fg hover:bg-ui-danger-hover" : "bg-ui-primary text-ui-primary-fg hover:bg-ui-primary-hover"
136
+ ),
137
+ children: confirmLabel ?? copy.confirm
138
+ }
139
+ )
140
+ ] })
141
+ ]
142
+ }
143
+ )
144
+ ]
145
+ }
146
+ ),
147
+ document.body
148
+ );
149
+ };
150
+ var FileTypeBadge = ({
151
+ fileName,
152
+ preview,
153
+ size = "md"
154
+ }) => {
155
+ const box = size === "sm" ? "h-8 w-8" : "h-10 w-10";
156
+ if (!fileName) {
157
+ return /* @__PURE__ */ jsx(
158
+ "span",
159
+ {
160
+ "aria-hidden": "true",
161
+ className: cn(
162
+ "border-ui-border rounded-ui shrink-0 border border-dashed",
163
+ box
164
+ )
165
+ }
166
+ );
167
+ }
168
+ return preview ? /* @__PURE__ */ jsx(
169
+ "img",
170
+ {
171
+ src: preview,
172
+ alt: "",
173
+ className: cn("rounded-ui shrink-0 object-cover", box)
174
+ }
175
+ ) : /* @__PURE__ */ jsx(
176
+ "span",
177
+ {
178
+ className: cn(
179
+ "bg-ui-surface-muted text-ui-muted rounded-ui flex shrink-0 items-center justify-center font-medium uppercase",
180
+ size === "sm" ? "text-[10px]" : "text-xs",
181
+ box
182
+ ),
183
+ children: getFileExtension(fileName)
184
+ }
185
+ );
186
+ };
187
+ var ProgressBar = ({ percent, className }) => /* @__PURE__ */ jsx(
188
+ "div",
189
+ {
190
+ className: cn(
191
+ "bg-ui-surface-muted mt-1 h-1 w-full overflow-hidden rounded",
192
+ className
193
+ ),
194
+ children: /* @__PURE__ */ jsx(
195
+ "div",
196
+ {
197
+ className: "bg-ui-primary h-full transition-all",
198
+ style: { width: `${percent}%` }
199
+ }
200
+ )
201
+ }
202
+ );
203
+ var FileItem = ({
204
+ file,
205
+ onRemove,
206
+ onAbort,
207
+ onView,
208
+ size = "md",
209
+ labels
210
+ }) => {
211
+ const copy = useUploaderLabels(labels);
212
+ const uploading = file.status === "uploading";
213
+ return /* @__PURE__ */ jsxs(
214
+ "div",
215
+ {
216
+ className: cn(
217
+ "rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm transition-colors",
218
+ size === "sm" ? "p-1.5" : "p-2",
219
+ file.status === "error" ? "border-ui-danger-soft-fg/20 bg-ui-danger-soft" : "border-ui-border bg-ui-surface"
220
+ ),
221
+ children: [
222
+ /* @__PURE__ */ jsx(
223
+ FileTypeBadge,
224
+ {
225
+ fileName: file.file.name,
226
+ preview: file.preview,
227
+ size
228
+ }
229
+ ),
230
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
231
+ /* @__PURE__ */ jsx("p", { className: "text-ui-fg truncate font-medium", children: file.file.name }),
232
+ file.status === "error" ? /* @__PURE__ */ jsx("p", { className: "text-ui-danger text-xs", children: file.error }) : /* @__PURE__ */ jsxs("p", { className: "text-ui-muted text-xs", children: [
233
+ formatFileSize(file.file.size),
234
+ file.status === "success" && ` \xB7 ${copy.ready}`,
235
+ file.status === "idle" && ` \xB7 ${copy.staged}`
236
+ ] }),
237
+ uploading && /* @__PURE__ */ jsx(ProgressBar, { percent: file.progress })
238
+ ] }),
239
+ onView && /* @__PURE__ */ jsx(
240
+ "button",
241
+ {
242
+ type: "button",
243
+ onClick: onView,
244
+ className: "text-ui-primary hover:bg-ui-primary-soft focus-visible:ring-ui-ring rounded-ui shrink-0 cursor-pointer px-2 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
245
+ children: copy.view
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsx(
249
+ "button",
250
+ {
251
+ type: "button",
252
+ onClick: uploading ? onAbort : onRemove,
253
+ className: "text-ui-muted hover:bg-ui-surface-muted hover:text-ui-fg focus-visible:ring-ui-ring rounded-ui shrink-0 cursor-pointer px-2 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
254
+ children: uploading ? copy.cancel : copy.remove
255
+ }
256
+ )
257
+ ]
258
+ }
259
+ );
260
+ };
261
+ var SlotRow = ({
262
+ slot,
263
+ onPick,
264
+ onRemove,
265
+ onAbort,
266
+ onView,
267
+ size = "md",
268
+ labels
269
+ }) => {
270
+ const copy = useUploaderLabels(labels);
271
+ const inputRef = useRef(null);
272
+ const [dropping, setDropping] = useState(false);
273
+ const depth = useRef(0);
274
+ const { definition, filled, pending } = slot;
275
+ const uploading = pending?.status === "uploading";
276
+ const staged = pending?.status === "idle";
277
+ const occupied = Boolean(filled) || staged;
278
+ const droppable = !uploading;
279
+ const onDrop = (event) => {
280
+ event.preventDefault();
281
+ event.stopPropagation();
282
+ depth.current = 0;
283
+ setDropping(false);
284
+ if (!droppable) return;
285
+ const file = event.dataTransfer.files[0];
286
+ if (file) onPick(file);
287
+ };
288
+ const action = "focus-visible:ring-ui-ring rounded-ui shrink-0 cursor-pointer px-2 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none";
289
+ return /* @__PURE__ */ jsxs(
290
+ "div",
291
+ {
292
+ onDragEnter: () => {
293
+ depth.current += 1;
294
+ if (droppable) setDropping(true);
295
+ },
296
+ onDragOver: (event) => {
297
+ event.preventDefault();
298
+ event.stopPropagation();
299
+ },
300
+ onDragLeave: () => {
301
+ depth.current = Math.max(0, depth.current - 1);
302
+ if (depth.current === 0) setDropping(false);
303
+ },
304
+ onDrop,
305
+ className: cn(
306
+ "rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm transition-all duration-200",
307
+ size === "sm" ? "p-1.5" : "p-2",
308
+ dropping ? "border-ui-primary bg-ui-primary-soft ring-ui-ring scale-[1.01] ring-2" : "border-ui-border"
309
+ ),
310
+ children: [
311
+ /* @__PURE__ */ jsx(
312
+ "span",
313
+ {
314
+ className: cn(
315
+ "h-2.5 w-2.5 shrink-0 rounded-full transition-colors",
316
+ pending?.status === "error" ? "bg-ui-danger" : uploading ? "bg-ui-primary animate-pulse" : staged ? "bg-ui-warning" : filled ? "bg-ui-success" : "bg-ui-border-strong"
317
+ )
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsx(
321
+ FileTypeBadge,
322
+ {
323
+ fileName: pending?.file.name ?? filled?.stored.fileName,
324
+ preview: pending?.preview,
325
+ size
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
329
+ /* @__PURE__ */ jsx("p", { className: "text-ui-fg truncate font-medium", children: definition.label }),
330
+ pending?.status === "error" ? /* @__PURE__ */ jsx("p", { className: "text-ui-danger text-xs", children: pending.error }) : pending ? (
331
+ // The name is the STORAGE one: `assign` renames the pick to
332
+ // `{slot}.{ext}` before it reaches the machine.
333
+ /* @__PURE__ */ jsxs("p", { className: "text-ui-muted truncate text-xs", children: [
334
+ pending.file.name,
335
+ " \xB7 ",
336
+ formatFileSize(pending.file.size),
337
+ staged && ` \xB7 ${copy.staged}`
338
+ ] })
339
+ ) : filled ? /* @__PURE__ */ jsx(
340
+ "button",
341
+ {
342
+ type: "button",
343
+ onClick: onView,
344
+ className: "text-ui-muted hover:text-ui-primary block max-w-full cursor-pointer truncate text-left text-xs hover:underline",
345
+ children: filled.stored.fileName
346
+ }
347
+ ) : /* @__PURE__ */ jsx("p", { className: "text-ui-placeholder text-xs", children: definition.hint ?? definition.extensions.map((e) => `.${e}`).join(" \xB7 ") }),
348
+ uploading && /* @__PURE__ */ jsx(ProgressBar, { percent: pending.progress })
349
+ ] }),
350
+ /* @__PURE__ */ jsx(
351
+ "input",
352
+ {
353
+ ref: inputRef,
354
+ type: "file",
355
+ accept: definition.extensions.map((e) => `.${e}`).join(","),
356
+ className: "hidden",
357
+ onChange: (event) => {
358
+ const file = event.target.files?.[0];
359
+ if (file) onPick(file);
360
+ event.target.value = "";
361
+ }
362
+ }
363
+ ),
364
+ dropping && /* @__PURE__ */ jsx("span", { className: "bg-ui-primary text-ui-primary-fg rounded-full px-2.5 py-1 text-[11px] font-medium whitespace-nowrap", children: occupied ? copy.dropToReplace : copy.upload }),
365
+ !dropping && filled && !uploading && /* @__PURE__ */ jsx(
366
+ "button",
367
+ {
368
+ type: "button",
369
+ onClick: onView,
370
+ className: cn(action, "text-ui-primary hover:bg-ui-primary-soft"),
371
+ children: copy.view
372
+ }
373
+ ),
374
+ uploading ? /* @__PURE__ */ jsx(
375
+ "button",
376
+ {
377
+ type: "button",
378
+ onClick: onAbort,
379
+ className: cn(action, "text-ui-muted hover:bg-ui-surface-muted"),
380
+ children: copy.cancel
381
+ }
382
+ ) : dropping ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
383
+ /* @__PURE__ */ jsx(
384
+ "button",
385
+ {
386
+ type: "button",
387
+ onClick: () => inputRef.current?.click(),
388
+ className: cn(action, "text-ui-primary hover:bg-ui-primary-soft"),
389
+ children: occupied ? copy.replace : copy.upload
390
+ }
391
+ ),
392
+ occupied && /* @__PURE__ */ jsx(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ onClick: onRemove,
397
+ className: cn(action, "text-ui-muted hover:bg-ui-surface-muted"),
398
+ children: copy.remove
399
+ }
400
+ )
401
+ ] })
402
+ ]
403
+ }
404
+ );
405
+ };
406
+ var useFileViewer = ({
407
+ resolveUrl
408
+ } = {}) => {
409
+ const [viewing, setViewing] = useState(null);
410
+ const close = useCallback(() => setViewing(null), []);
411
+ const open = useCallback((file) => setViewing(file), []);
412
+ return {
413
+ viewing,
414
+ open,
415
+ close,
416
+ viewerProps: useMemo(
417
+ () => ({ file: viewing, onClose: close, resolveUrl }),
418
+ [viewing, close, resolveUrl]
419
+ )
420
+ };
421
+ };
422
+ var SlottedUploader = ({
423
+ uploadOn = "select",
424
+ title,
425
+ description,
426
+ hideDropzone = false,
427
+ confirmRemove,
428
+ confirmReplace,
429
+ removeStrategy,
430
+ resolveViewUrl,
431
+ size = "md",
432
+ icon,
433
+ shortcut,
434
+ controllerRef,
435
+ onPendingChange,
436
+ className,
437
+ ...options
438
+ }) => {
439
+ const slotted = useSlottedUploader({
440
+ ...options,
441
+ uploadOn: uploadOn === "select" ? "select" : "manual"
442
+ });
443
+ if (uploadOn === "submit" && !controllerRef) {
444
+ warnDev(
445
+ "slotted-submit-without-controller",
446
+ "uploadOn: 'submit' has no send of its own here \u2014 pass `controllerRef` and call upload() from the form's submit, or staged slots never leave."
447
+ );
448
+ }
449
+ const copy = useUploaderLabels(options.labels);
450
+ const coarse = useCoarsePointer();
451
+ const viewer = useFileViewer({ resolveUrl: resolveViewUrl });
452
+ const [pending, setPending] = useState(null);
453
+ const { upload, hasPending, isUploading } = slotted;
454
+ useEffect(() => {
455
+ if (!controllerRef) return;
456
+ controllerRef.current = { upload, hasPending, isUploading };
457
+ return () => {
458
+ controllerRef.current = null;
459
+ };
460
+ }, [controllerRef, upload, hasPending, isUploading]);
461
+ useEffect(() => {
462
+ onPendingChange?.(hasPending);
463
+ }, [onPendingChange, hasPending]);
464
+ const pick = (slot, file) => {
465
+ if (slot.filled && confirmReplace) {
466
+ setPending({ kind: "replace", slot, file });
467
+ return;
468
+ }
469
+ void slotted.addToSlot(slot.definition.id, file);
470
+ };
471
+ const forgetSlot = (slot) => {
472
+ const stored = slot.filled?.stored;
473
+ if (stored && removeStrategy && !options.scopes.get(options.scope).keepOnRemove) {
474
+ removeStrategy(stored, options.scope, options.entityId).then((deleted) => {
475
+ if (!deleted) options.onError?.(copy.removeFailed);
476
+ }).catch(() => options.onError?.(copy.removeFailed));
477
+ }
478
+ slotted.removeSlot(slot.definition.id);
479
+ };
480
+ const remove = (slot) => {
481
+ if (slot.filled && confirmRemove) {
482
+ setPending({ kind: "remove", slot });
483
+ return;
484
+ }
485
+ forgetSlot(slot);
486
+ };
487
+ const override = pending?.kind === "replace" ? typeof confirmReplace === "object" ? confirmReplace : {} : typeof confirmRemove === "object" ? confirmRemove : {};
488
+ const dialogTitle = override.title ?? (pending?.kind === "replace" ? copy.confirmReplaceTitle : copy.confirmRemoveTitle);
489
+ const dialogMessage = override.message ?? (pending?.kind === "replace" ? copy.confirmReplaceMessage(
490
+ pending.slot.filled?.stored.fileName ?? "",
491
+ pending.file.name
492
+ ) : copy.confirmRemoveMessage(pending?.slot.filled?.stored.fileName ?? ""));
493
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
494
+ title && /* @__PURE__ */ jsx("p", { className: "text-ui-fg text-sm font-medium", children: title }),
495
+ description && /* @__PURE__ */ jsx("p", { className: "text-ui-muted text-xs", children: description }),
496
+ slotted.slots.map((slot) => /* @__PURE__ */ jsx(
497
+ SlotRow,
498
+ {
499
+ slot,
500
+ onPick: (file) => pick(slot, file),
501
+ onRemove: () => remove(slot),
502
+ onAbort: () => slotted.abort(slot.definition.id),
503
+ onView: () => slot.filled && viewer.open(slot.filled.stored),
504
+ size,
505
+ labels: options.labels
506
+ },
507
+ slot.definition.id
508
+ )),
509
+ !hideDropzone && /* @__PURE__ */ jsxs(
510
+ Dropzone,
511
+ {
512
+ accept: slotted.accept,
513
+ multiple: true,
514
+ disabled: slotted.isUploading,
515
+ size,
516
+ shortcut,
517
+ onFiles: (files) => void slotted.addFiles(files),
518
+ children: [
519
+ icon !== null && /* @__PURE__ */ jsx("span", { className: "text-ui-placeholder group-hover/dz:text-ui-primary group-data-dragging/dz:text-ui-primary mb-1.5 inline-flex justify-center transition-all duration-200 group-hover/dz:-translate-y-0.5 group-data-dragging/dz:-translate-y-1 group-data-dragging/dz:scale-110", children: icon ?? /* @__PURE__ */ jsx(
520
+ UploadCloudIcon,
521
+ {
522
+ className: size === "sm" ? "h-5 w-5" : "h-7 w-7"
523
+ }
524
+ ) }),
525
+ /* @__PURE__ */ jsx("p", { className: "text-ui-muted group-hover/dz:text-ui-fg text-sm font-medium transition-colors duration-200", children: coarse ? copy.bulkTapPrompt : copy.bulkDropPrompt })
526
+ ]
527
+ }
528
+ ),
529
+ /* @__PURE__ */ jsx(FileViewer, { ...viewer.viewerProps, labels: options.labels }),
530
+ /* @__PURE__ */ jsx(
531
+ ConfirmDialog,
532
+ {
533
+ open: pending !== null,
534
+ title: dialogTitle,
535
+ message: dialogMessage,
536
+ onConfirm: () => {
537
+ if (pending?.kind === "replace") {
538
+ void slotted.addToSlot(pending.slot.definition.id, pending.file);
539
+ } else if (pending) {
540
+ forgetSlot(pending.slot);
541
+ }
542
+ setPending(null);
543
+ },
544
+ onCancel: () => setPending(null),
545
+ labels: options.labels
546
+ }
547
+ )
548
+ ] });
549
+ };
550
+ var StoredFileItem = ({
551
+ file,
552
+ onView,
553
+ onRemove,
554
+ size = "md",
555
+ labels
556
+ }) => {
557
+ const copy = useUploaderLabels(labels);
558
+ return /* @__PURE__ */ jsxs(
559
+ "div",
560
+ {
561
+ className: cn(
562
+ "border-ui-border bg-ui-surface-subtle rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm",
563
+ size === "sm" ? "p-1.5" : "p-2"
564
+ ),
565
+ children: [
566
+ /* @__PURE__ */ jsx(FileTypeBadge, { fileName: file.fileName, size }),
567
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
568
+ /* @__PURE__ */ jsx("p", { className: "text-ui-fg truncate font-medium", children: file.fileName }),
569
+ /* @__PURE__ */ jsx("p", { className: "text-ui-muted text-xs", children: formatFileSize(file.size) })
570
+ ] }),
571
+ /* @__PURE__ */ jsx(
572
+ "button",
573
+ {
574
+ type: "button",
575
+ onClick: onView,
576
+ className: "text-ui-primary hover:bg-ui-primary-soft focus-visible:ring-ui-ring rounded-ui shrink-0 cursor-pointer px-2 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
577
+ children: copy.view
578
+ }
579
+ ),
580
+ onRemove && /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ onClick: () => onRemove(file),
585
+ className: "text-ui-muted hover:bg-ui-surface-muted hover:text-ui-fg focus-visible:ring-ui-ring rounded-ui shrink-0 cursor-pointer px-2 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
586
+ children: copy.remove
587
+ }
588
+ )
589
+ ]
590
+ }
591
+ );
592
+ };
593
+ var Uploader = ({
594
+ uploadOn = "select",
595
+ label,
596
+ description,
597
+ stored = [],
598
+ filesPosition = "above",
599
+ renderFiles,
600
+ onRemoveStored,
601
+ confirmRemove,
602
+ resolveViewUrl,
603
+ capture,
604
+ size = "md",
605
+ icon,
606
+ shortcut,
607
+ controllerRef,
608
+ onPendingChange,
609
+ removeStrategy,
610
+ disabled = false,
611
+ className,
612
+ ...options
613
+ }) => {
614
+ const uploader = useUploader({
615
+ ...options,
616
+ uploadOn: uploadOn === "select" ? "select" : "manual"
617
+ });
618
+ const copy = useUploaderLabels(options.labels);
619
+ const viewer = useFileViewer({ resolveUrl: resolveViewUrl });
620
+ const [removing, setRemoving] = useState(null);
621
+ const { files, removeFile } = uploader;
622
+ const manual = uploadOn === "manual";
623
+ if (uploadOn === "submit" && !controllerRef) {
624
+ warnDev(
625
+ "uploader-submit-without-controller",
626
+ "uploadOn: 'submit' has no send of its own \u2014 pass `controllerRef` and call upload() from the form's submit, or use 'manual' for the zone's button."
627
+ );
628
+ }
629
+ const coarse = useCoarsePointer();
630
+ useEffect(() => {
631
+ if (stored.length === 0) return;
632
+ const keys = new Set(stored.map((file) => file.key));
633
+ files.filter((f) => f.status === "success" && f.stored && keys.has(f.stored.key)).forEach((f) => removeFile(f.id));
634
+ }, [stored, files, removeFile]);
635
+ const { upload, hasPending, isUploading } = uploader;
636
+ useEffect(() => {
637
+ if (!controllerRef) return;
638
+ controllerRef.current = { upload, hasPending, isUploading };
639
+ return () => {
640
+ controllerRef.current = null;
641
+ };
642
+ }, [controllerRef, upload, hasPending, isUploading]);
643
+ useEffect(() => {
644
+ onPendingChange?.(hasPending);
645
+ }, [onPendingChange, hasPending]);
646
+ const forgetStored = (file) => {
647
+ if (removeStrategy && !options.scopes.get(options.scope).keepOnRemove) {
648
+ removeStrategy(file, options.scope, options.entityId).then((deleted) => {
649
+ if (!deleted) options.onError?.(copy.removeFailed);
650
+ }).catch(() => options.onError?.(copy.removeFailed));
651
+ }
652
+ onRemoveStored?.(file);
653
+ };
654
+ const requestRemove = onRemoveStored || removeStrategy ? (file) => confirmRemove ? setRemoving(file) : forgetStored(file) : void 0;
655
+ const confirmCopy = typeof confirmRemove === "object" ? confirmRemove : {};
656
+ const defaultRows = /* @__PURE__ */ jsxs(Fragment, { children: [
657
+ stored.map((file) => /* @__PURE__ */ jsx(
658
+ StoredFileItem,
659
+ {
660
+ file,
661
+ onView: () => viewer.open(file),
662
+ onRemove: requestRemove,
663
+ size,
664
+ labels: options.labels
665
+ },
666
+ file.key
667
+ )),
668
+ files.map((file) => {
669
+ const { stored: settled } = file;
670
+ return /* @__PURE__ */ jsx(
671
+ FileItem,
672
+ {
673
+ file,
674
+ onRemove: () => removeFile(file.id),
675
+ onAbort: () => uploader.abort(file.id),
676
+ onView: settled ? () => viewer.open(settled) : void 0,
677
+ size,
678
+ labels: options.labels
679
+ },
680
+ file.id
681
+ );
682
+ })
683
+ ] });
684
+ const fileList = renderFiles ? renderFiles({
685
+ stored,
686
+ staged: files,
687
+ nodes: defaultRows,
688
+ isEmpty: stored.length === 0 && files.length === 0,
689
+ view: viewer.open,
690
+ remove: removeFile,
691
+ removeStored: requestRemove
692
+ }) : defaultRows;
693
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
694
+ label && /* @__PURE__ */ jsx("p", { className: "text-ui-fg text-sm font-medium", children: label }),
695
+ filesPosition === "above" && fileList,
696
+ /* @__PURE__ */ jsxs(
697
+ Dropzone,
698
+ {
699
+ accept: uploader.accept,
700
+ multiple: options.multiple,
701
+ disabled: disabled || uploader.isUploading,
702
+ capture,
703
+ size,
704
+ shortcut,
705
+ onFiles: (incoming) => void uploader.addFiles(incoming),
706
+ children: [
707
+ icon !== null && /* @__PURE__ */ jsx("span", { className: "text-ui-placeholder group-hover/dz:text-ui-primary group-data-dragging/dz:text-ui-primary mb-1.5 inline-flex justify-center transition-all duration-200 group-hover/dz:-translate-y-0.5 group-data-dragging/dz:-translate-y-1 group-data-dragging/dz:scale-110", children: icon ?? /* @__PURE__ */ jsx(
708
+ UploadCloudIcon,
709
+ {
710
+ className: size === "sm" ? "h-5 w-5" : "h-7 w-7"
711
+ }
712
+ ) }),
713
+ /* @__PURE__ */ jsx("p", { className: "text-ui-muted group-hover/dz:text-ui-fg text-sm font-medium transition-colors duration-200", children: coarse ? copy.tapPrompt : copy.dropPrompt }),
714
+ /* @__PURE__ */ jsx("p", { className: "text-ui-placeholder mt-1 text-xs", children: description ?? uploader.accept.replaceAll(",", " \xB7 ") })
715
+ ]
716
+ }
717
+ ),
718
+ filesPosition === "below" && fileList,
719
+ manual && uploader.hasPending && /* @__PURE__ */ jsx(
720
+ "button",
721
+ {
722
+ type: "button",
723
+ onClick: () => void uploader.upload(),
724
+ disabled: disabled || uploader.isUploading,
725
+ className: "bg-ui-primary text-ui-primary-fg hover:bg-ui-primary-hover focus-visible:ring-ui-ring rounded-ui h-10 w-full cursor-pointer px-4 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
726
+ children: copy.uploadAll
727
+ }
728
+ ),
729
+ /* @__PURE__ */ jsx(FileViewer, { ...viewer.viewerProps, labels: options.labels }),
730
+ /* @__PURE__ */ jsx(
731
+ ConfirmDialog,
732
+ {
733
+ open: removing !== null,
734
+ title: confirmCopy.title ?? copy.confirmRemoveTitle,
735
+ message: confirmCopy.message ?? copy.confirmRemoveMessage(removing?.fileName ?? ""),
736
+ onConfirm: () => {
737
+ if (removing) forgetStored(removing);
738
+ setRemoving(null);
739
+ },
740
+ onCancel: () => setRemoving(null),
741
+ labels: options.labels
742
+ }
743
+ )
744
+ ] });
745
+ };
746
+
747
+ export { ConfirmDialog, FileItem, SlottedUploader, Uploader, useFileViewer };
748
+ //# sourceMappingURL=ui.js.map
749
+ //# sourceMappingURL=ui.js.map