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.
- package/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.es.md +1140 -0
- package/README.md +1135 -0
- package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
- package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
- package/dist/adapters/gcs.cjs +83 -0
- package/dist/adapters/gcs.cjs.map +1 -0
- package/dist/adapters/gcs.d.cts +31 -0
- package/dist/adapters/gcs.d.ts +31 -0
- package/dist/adapters/gcs.js +81 -0
- package/dist/adapters/gcs.js.map +1 -0
- package/dist/adapters/memory.cjs +27 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.d.cts +18 -0
- package/dist/adapters/memory.d.ts +18 -0
- package/dist/adapters/memory.js +25 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/s3.cjs +75 -0
- package/dist/adapters/s3.cjs.map +1 -0
- package/dist/adapters/s3.d.cts +27 -0
- package/dist/adapters/s3.d.ts +27 -0
- package/dist/adapters/s3.js +73 -0
- package/dist/adapters/s3.js.map +1 -0
- package/dist/chunk-3FI44IOW.js +150 -0
- package/dist/chunk-3FI44IOW.js.map +1 -0
- package/dist/chunk-H7BRW5IY.js +308 -0
- package/dist/chunk-H7BRW5IY.js.map +1 -0
- package/dist/chunk-PDKAF4GX.js +707 -0
- package/dist/chunk-PDKAF4GX.js.map +1 -0
- package/dist/chunk-PTEX7F4R.js +309 -0
- package/dist/chunk-PTEX7F4R.js.map +1 -0
- package/dist/chunk-T5YZG6JW.js +511 -0
- package/dist/chunk-T5YZG6JW.js.map +1 -0
- package/dist/index.cjs +220 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -170
- package/dist/index.d.ts +57 -170
- package/dist/index.js +1 -324
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +1665 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +100 -0
- package/dist/presets.d.ts +100 -0
- package/dist/presets.js +405 -0
- package/dist/presets.js.map +1 -0
- package/dist/react.cjs +981 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +106 -0
- package/dist/react.d.ts +106 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server/express.cjs +179 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.d.cts +64 -0
- package/dist/server/express.d.ts +64 -0
- package/dist/server/express.js +111 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/next.cjs +165 -0
- package/dist/server/next.cjs.map +1 -0
- package/dist/server/next.d.cts +38 -0
- package/dist/server/next.d.ts +38 -0
- package/dist/server/next.js +107 -0
- package/dist/server/next.js.map +1 -0
- package/dist/server.cjs +550 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +19 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +80 -0
- package/dist/server.js.map +1 -0
- package/dist/storage-CYkSHWZX.d.cts +133 -0
- package/dist/storage-Qc9epG0G.d.ts +133 -0
- package/dist/types-BSlJJwti.d.cts +341 -0
- package/dist/types-BSlJJwti.d.ts +341 -0
- package/dist/ui.cjs +2325 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +331 -0
- package/dist/ui.d.ts +331 -0
- package/dist/ui.js +749 -0
- package/dist/ui.js.map +1 -0
- package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
- package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
- package/dist/useUploader-BiBdS-7y.d.cts +117 -0
- package/dist/useUploader-CQHpj_oI.d.ts +117 -0
- package/package.json +189 -3
- package/tailwind.css +80 -0
package/dist/ui.cjs
ADDED
|
@@ -0,0 +1,2325 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var reactDom = require('react-dom');
|
|
6
|
+
|
|
7
|
+
// src/react/UploaderProvider.tsx
|
|
8
|
+
|
|
9
|
+
// src/labels.ts
|
|
10
|
+
var ES_LABELS = {
|
|
11
|
+
dropPrompt: "Selecciona un archivo o arr\xE1stralo aqu\xED",
|
|
12
|
+
tapPrompt: "Toca para elegir un archivo",
|
|
13
|
+
bulkDropPrompt: "Arrastra varios archivos \u2014 cada uno cae en su documento",
|
|
14
|
+
bulkTapPrompt: "Toca para agregar varios archivos",
|
|
15
|
+
uploadAll: "Subir archivos",
|
|
16
|
+
view: "Ver",
|
|
17
|
+
remove: "Quitar",
|
|
18
|
+
cancel: "Cancelar",
|
|
19
|
+
ready: "Listo",
|
|
20
|
+
staged: "Listo para subir",
|
|
21
|
+
upload: "Subir",
|
|
22
|
+
replace: "Reemplazar",
|
|
23
|
+
dropToReplace: "Suelta para reemplazar",
|
|
24
|
+
previous: "Anterior",
|
|
25
|
+
next: "Siguiente",
|
|
26
|
+
uploadFailed: "No se pudo subir el archivo",
|
|
27
|
+
uploadNetworkFailed: "No se pudo subir el archivo. Revisa tu conexi\xF3n",
|
|
28
|
+
removeFailed: "No se pudo borrar el archivo del almacenamiento",
|
|
29
|
+
readFailed: "No se pudo cargar el archivo",
|
|
30
|
+
maxFilesReached: (max) => `M\xE1ximo ${max} archivo(s)`,
|
|
31
|
+
slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: formato no permitido. Se aceptan: ${extensions.join(", ")}`,
|
|
32
|
+
fileWithoutSlot: (fileName) => `"${fileName}" no corresponde a ning\xFAn documento`,
|
|
33
|
+
viewerLoading: "Cargando\u2026",
|
|
34
|
+
viewerError: "No se pudo cargar la vista previa",
|
|
35
|
+
viewerErrorHint: "El archivo puede haberse movido o el enlace expir\xF3. Puedes reintentar o descargarlo.",
|
|
36
|
+
viewerRetry: "Reintentar",
|
|
37
|
+
openInTab: "Abrir en pesta\xF1a",
|
|
38
|
+
downloadShort: "Descargar",
|
|
39
|
+
close: "Cerrar",
|
|
40
|
+
noPreview: "Este formato no tiene vista previa",
|
|
41
|
+
download: (fileName) => `Descargar ${fileName}`,
|
|
42
|
+
filePreview: "Archivo",
|
|
43
|
+
confirm: "Confirmar",
|
|
44
|
+
confirmRemoveTitle: "Quitar archivo",
|
|
45
|
+
confirmRemoveMessage: (fileName) => `\xBFQuitar "${fileName}"? Podr\xE1s subir otro despu\xE9s.`,
|
|
46
|
+
confirmReplaceTitle: "Reemplazar archivo",
|
|
47
|
+
confirmReplaceMessage: (current, incoming) => `\xBFReemplazar "${current}" por "${incoming}"?`,
|
|
48
|
+
dropAnywhereTitle: "Suelta los archivos aqu\xED",
|
|
49
|
+
dropAnywhereHint: "Cualquier lugar de la pantalla cuenta",
|
|
50
|
+
avatarChange: "Cambiar foto",
|
|
51
|
+
avatarDropHere: "Suelta aqu\xED",
|
|
52
|
+
avatarRemove: "Quitar foto",
|
|
53
|
+
galleryAdd: "Agregar",
|
|
54
|
+
fileHasNoExtension: "El archivo no tiene extensi\xF3n",
|
|
55
|
+
formatNotAllowed: (extensions) => `Formato no permitido. Se aceptan: ${extensions.join(", ")}`,
|
|
56
|
+
fileIsEmpty: "El archivo est\xE1 vac\xEDo",
|
|
57
|
+
fileTooLarge: (size, max) => `El archivo pesa ${size} y el m\xE1ximo es ${max}`,
|
|
58
|
+
fileIncomplete: "El archivo est\xE1 incompleto",
|
|
59
|
+
contentDoesNotMatchExtension: "El contenido del archivo no corresponde a su extensi\xF3n",
|
|
60
|
+
requestIncomplete: "Solicitud incompleta",
|
|
61
|
+
unauthorized: "No autorizado",
|
|
62
|
+
fileRequired: "Archivo requerido",
|
|
63
|
+
unknownScope: "Destino de archivo no v\xE1lido",
|
|
64
|
+
processingFailed: "No se pudo procesar el archivo",
|
|
65
|
+
unnamedFile: "archivo"
|
|
66
|
+
};
|
|
67
|
+
var DEFAULT_LABELS = {
|
|
68
|
+
dropPrompt: "Choose a file or drag it here",
|
|
69
|
+
tapPrompt: "Tap to choose a file",
|
|
70
|
+
bulkDropPrompt: "Drop several files \u2014 each lands on its document",
|
|
71
|
+
bulkTapPrompt: "Tap to add several files",
|
|
72
|
+
uploadAll: "Upload files",
|
|
73
|
+
view: "View",
|
|
74
|
+
remove: "Remove",
|
|
75
|
+
cancel: "Cancel",
|
|
76
|
+
ready: "Done",
|
|
77
|
+
staged: "Ready to upload",
|
|
78
|
+
upload: "Upload",
|
|
79
|
+
replace: "Replace",
|
|
80
|
+
dropToReplace: "Drop to replace",
|
|
81
|
+
previous: "Previous",
|
|
82
|
+
next: "Next",
|
|
83
|
+
uploadFailed: "The file could not be uploaded",
|
|
84
|
+
uploadNetworkFailed: "The file could not be uploaded. Check your connection",
|
|
85
|
+
removeFailed: "The file could not be removed from storage",
|
|
86
|
+
readFailed: "The file could not be loaded",
|
|
87
|
+
maxFilesReached: (max) => `At most ${max} file(s)`,
|
|
88
|
+
slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
89
|
+
fileWithoutSlot: (fileName) => `"${fileName}" does not match any document`,
|
|
90
|
+
viewerLoading: "Loading\u2026",
|
|
91
|
+
viewerError: "The preview could not be loaded",
|
|
92
|
+
viewerErrorHint: "The file may have moved or the link expired. You can retry or download it.",
|
|
93
|
+
viewerRetry: "Retry",
|
|
94
|
+
openInTab: "Open in tab",
|
|
95
|
+
downloadShort: "Download",
|
|
96
|
+
close: "Close",
|
|
97
|
+
noPreview: "This format has no preview",
|
|
98
|
+
download: (fileName) => `Download ${fileName}`,
|
|
99
|
+
filePreview: "File",
|
|
100
|
+
confirm: "Confirm",
|
|
101
|
+
confirmRemoveTitle: "Remove file",
|
|
102
|
+
confirmRemoveMessage: (fileName) => `Remove "${fileName}"? You can upload another one later.`,
|
|
103
|
+
confirmReplaceTitle: "Replace file",
|
|
104
|
+
confirmReplaceMessage: (current, incoming) => `Replace "${current}" with "${incoming}"?`,
|
|
105
|
+
dropAnywhereTitle: "Drop the files here",
|
|
106
|
+
dropAnywhereHint: "Anywhere on the screen counts",
|
|
107
|
+
avatarChange: "Change photo",
|
|
108
|
+
avatarDropHere: "Drop here",
|
|
109
|
+
avatarRemove: "Remove photo",
|
|
110
|
+
galleryAdd: "Add",
|
|
111
|
+
fileHasNoExtension: "The file has no extension",
|
|
112
|
+
formatNotAllowed: (extensions) => `Format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
113
|
+
fileIsEmpty: "The file is empty",
|
|
114
|
+
fileTooLarge: (size, max) => `The file weighs ${size} and the maximum is ${max}`,
|
|
115
|
+
fileIncomplete: "The file is incomplete",
|
|
116
|
+
contentDoesNotMatchExtension: "The file contents do not match its extension",
|
|
117
|
+
requestIncomplete: "Incomplete request",
|
|
118
|
+
unauthorized: "Unauthorized",
|
|
119
|
+
fileRequired: "A file is required",
|
|
120
|
+
unknownScope: "Unknown file destination",
|
|
121
|
+
processingFailed: "The file could not be processed",
|
|
122
|
+
unnamedFile: "file"
|
|
123
|
+
};
|
|
124
|
+
var resolveLabels = (labels, base = DEFAULT_LABELS) => labels ? { ...base, ...labels } : base;
|
|
125
|
+
({ ...DEFAULT_LABELS });
|
|
126
|
+
var LabelsContext = react.createContext(DEFAULT_LABELS);
|
|
127
|
+
var UploaderProvider = ({
|
|
128
|
+
children,
|
|
129
|
+
language = "en",
|
|
130
|
+
labels
|
|
131
|
+
}) => {
|
|
132
|
+
const value = react.useMemo(
|
|
133
|
+
() => resolveLabels(labels, language === "es" ? ES_LABELS : DEFAULT_LABELS),
|
|
134
|
+
[language, labels]
|
|
135
|
+
);
|
|
136
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LabelsContext.Provider, { value, children });
|
|
137
|
+
};
|
|
138
|
+
var useUploaderLabels = (overrides) => {
|
|
139
|
+
const base = react.useContext(LabelsContext);
|
|
140
|
+
return react.useMemo(
|
|
141
|
+
() => overrides ? { ...base, ...overrides } : base,
|
|
142
|
+
[base, overrides]
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/ui/cn.ts
|
|
147
|
+
var cn = (...values) => values.filter(Boolean).join(" ");
|
|
148
|
+
var sequence = 0;
|
|
149
|
+
var stack = [];
|
|
150
|
+
var useOverlayLayer = (active2) => {
|
|
151
|
+
const idRef = react.useRef(0);
|
|
152
|
+
if (idRef.current === 0) idRef.current = ++sequence;
|
|
153
|
+
react.useEffect(() => {
|
|
154
|
+
if (!active2) return;
|
|
155
|
+
const id = idRef.current;
|
|
156
|
+
stack.push(id);
|
|
157
|
+
return () => {
|
|
158
|
+
const at = stack.lastIndexOf(id);
|
|
159
|
+
if (at !== -1) stack.splice(at, 1);
|
|
160
|
+
};
|
|
161
|
+
}, [active2]);
|
|
162
|
+
return react.useMemo(
|
|
163
|
+
() => ({ isTopmost: () => stack[stack.length - 1] === idRef.current }),
|
|
164
|
+
[]
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/ui/scrollLock.ts
|
|
169
|
+
var KEY = /* @__PURE__ */ Symbol.for("uploaderkit.bodyScrollLock");
|
|
170
|
+
var state = () => {
|
|
171
|
+
const host = globalThis;
|
|
172
|
+
return host[KEY] ??= { count: 0, saved: "" };
|
|
173
|
+
};
|
|
174
|
+
var lockBodyScroll = () => {
|
|
175
|
+
const lock = state();
|
|
176
|
+
if (lock.count === 0) {
|
|
177
|
+
lock.saved = document.body.style.overflow;
|
|
178
|
+
document.body.style.overflow = "hidden";
|
|
179
|
+
}
|
|
180
|
+
lock.count += 1;
|
|
181
|
+
};
|
|
182
|
+
var unlockBodyScroll = () => {
|
|
183
|
+
const lock = state();
|
|
184
|
+
if (lock.count === 0) return;
|
|
185
|
+
lock.count -= 1;
|
|
186
|
+
if (lock.count === 0) document.body.style.overflow = lock.saved;
|
|
187
|
+
};
|
|
188
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
189
|
+
var useFocusTrap = (active2, ref) => {
|
|
190
|
+
react.useEffect(() => {
|
|
191
|
+
if (!active2) return;
|
|
192
|
+
const restoreTo = document.activeElement;
|
|
193
|
+
const panel = ref.current;
|
|
194
|
+
const initial = panel?.querySelector("[data-autofocus]") ?? panel;
|
|
195
|
+
initial?.focus({ preventScroll: true });
|
|
196
|
+
const onKeyDown = (event) => {
|
|
197
|
+
if (event.key !== "Tab" || !ref.current) return;
|
|
198
|
+
const focusables = Array.from(
|
|
199
|
+
ref.current.querySelectorAll(FOCUSABLE)
|
|
200
|
+
).filter((el) => el.offsetParent !== null);
|
|
201
|
+
if (focusables.length === 0) {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const first = focusables[0];
|
|
206
|
+
const last = focusables[focusables.length - 1];
|
|
207
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
last.focus();
|
|
210
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
first.focus();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
document.addEventListener("keydown", onKeyDown);
|
|
216
|
+
return () => {
|
|
217
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
218
|
+
restoreTo?.focus({ preventScroll: true });
|
|
219
|
+
};
|
|
220
|
+
}, [active2, ref]);
|
|
221
|
+
};
|
|
222
|
+
var OVERLAY_ANIMATION_MS = 200;
|
|
223
|
+
var useOverlayTransition = (open) => {
|
|
224
|
+
const [entered, setEntered] = react.useState(false);
|
|
225
|
+
const [exiting, setExiting] = react.useState(false);
|
|
226
|
+
const wasOpen = react.useRef(false);
|
|
227
|
+
react.useEffect(() => {
|
|
228
|
+
if (open) {
|
|
229
|
+
wasOpen.current = true;
|
|
230
|
+
setExiting(false);
|
|
231
|
+
const raf = requestAnimationFrame(() => setEntered(true));
|
|
232
|
+
return () => cancelAnimationFrame(raf);
|
|
233
|
+
}
|
|
234
|
+
setEntered(false);
|
|
235
|
+
if (!wasOpen.current) return;
|
|
236
|
+
wasOpen.current = false;
|
|
237
|
+
setExiting(true);
|
|
238
|
+
const timer = setTimeout(() => setExiting(false), OVERLAY_ANIMATION_MS);
|
|
239
|
+
return () => clearTimeout(timer);
|
|
240
|
+
}, [open]);
|
|
241
|
+
return { mounted: open || exiting, entered };
|
|
242
|
+
};
|
|
243
|
+
var ConfirmDialog = ({
|
|
244
|
+
open,
|
|
245
|
+
title,
|
|
246
|
+
message,
|
|
247
|
+
variant = "danger",
|
|
248
|
+
confirmLabel,
|
|
249
|
+
cancelLabel,
|
|
250
|
+
onConfirm,
|
|
251
|
+
onCancel,
|
|
252
|
+
labels
|
|
253
|
+
}) => {
|
|
254
|
+
const copy = useUploaderLabels(labels);
|
|
255
|
+
const panelRef = react.useRef(null);
|
|
256
|
+
const { mounted, entered } = useOverlayTransition(open);
|
|
257
|
+
const layer = useOverlayLayer(open);
|
|
258
|
+
useFocusTrap(open, panelRef);
|
|
259
|
+
const cancelRef = react.useRef(onCancel);
|
|
260
|
+
cancelRef.current = onCancel;
|
|
261
|
+
react.useEffect(() => {
|
|
262
|
+
if (!open) return;
|
|
263
|
+
const onKey = (event) => {
|
|
264
|
+
if (event.key === "Escape") {
|
|
265
|
+
if (!layer.isTopmost()) return;
|
|
266
|
+
event.stopPropagation();
|
|
267
|
+
cancelRef.current();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
window.addEventListener("keydown", onKey, true);
|
|
271
|
+
lockBodyScroll();
|
|
272
|
+
return () => {
|
|
273
|
+
window.removeEventListener("keydown", onKey, true);
|
|
274
|
+
unlockBodyScroll();
|
|
275
|
+
};
|
|
276
|
+
}, [open, layer]);
|
|
277
|
+
if (!mounted) return null;
|
|
278
|
+
return reactDom.createPortal(
|
|
279
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
280
|
+
"div",
|
|
281
|
+
{
|
|
282
|
+
className: cn(
|
|
283
|
+
"fixed inset-0 z-1000 flex items-end justify-center p-4 sm:items-center",
|
|
284
|
+
!open && "pointer-events-none"
|
|
285
|
+
),
|
|
286
|
+
onClick: onCancel,
|
|
287
|
+
children: [
|
|
288
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
289
|
+
"div",
|
|
290
|
+
{
|
|
291
|
+
"aria-hidden": true,
|
|
292
|
+
className: cn(
|
|
293
|
+
"bg-ui-overlay fixed inset-0 transition-opacity duration-200 ease-out",
|
|
294
|
+
entered ? "opacity-100" : "opacity-0"
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
299
|
+
"div",
|
|
300
|
+
{
|
|
301
|
+
ref: panelRef,
|
|
302
|
+
role: "alertdialog",
|
|
303
|
+
"aria-modal": "true",
|
|
304
|
+
"aria-label": title,
|
|
305
|
+
tabIndex: -1,
|
|
306
|
+
onClick: (event) => event.stopPropagation(),
|
|
307
|
+
className: cn(
|
|
308
|
+
"bg-ui-surface rounded-ui-xl relative w-full max-w-md p-6 shadow-2xl outline-none",
|
|
309
|
+
"transition duration-200 ease-out",
|
|
310
|
+
entered ? "translate-y-0 scale-100 opacity-100" : "translate-y-3 scale-[0.97] opacity-0"
|
|
311
|
+
),
|
|
312
|
+
children: [
|
|
313
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-4", children: [
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
315
|
+
"span",
|
|
316
|
+
{
|
|
317
|
+
className: cn(
|
|
318
|
+
"flex size-11 shrink-0 items-center justify-center rounded-full",
|
|
319
|
+
variant === "danger" ? "bg-ui-danger-soft" : "bg-ui-primary-soft"
|
|
320
|
+
),
|
|
321
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
322
|
+
"svg",
|
|
323
|
+
{
|
|
324
|
+
viewBox: "0 0 24 24",
|
|
325
|
+
className: cn(
|
|
326
|
+
"size-6",
|
|
327
|
+
variant === "danger" ? "text-ui-danger" : "text-ui-primary"
|
|
328
|
+
),
|
|
329
|
+
fill: "none",
|
|
330
|
+
stroke: "currentColor",
|
|
331
|
+
strokeWidth: "2",
|
|
332
|
+
strokeLinecap: "round",
|
|
333
|
+
strokeLinejoin: "round",
|
|
334
|
+
"aria-hidden": true,
|
|
335
|
+
children: [
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.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" }),
|
|
337
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
|
|
338
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
),
|
|
344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
345
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-ui-fg text-base font-semibold", children: title }),
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-ui-muted mt-1 text-sm", children: message })
|
|
347
|
+
] })
|
|
348
|
+
] }),
|
|
349
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", children: [
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
351
|
+
"button",
|
|
352
|
+
{
|
|
353
|
+
type: "button",
|
|
354
|
+
"data-autofocus": true,
|
|
355
|
+
onClick: onCancel,
|
|
356
|
+
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",
|
|
357
|
+
children: cancelLabel ?? copy.cancel
|
|
358
|
+
}
|
|
359
|
+
),
|
|
360
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
361
|
+
"button",
|
|
362
|
+
{
|
|
363
|
+
type: "button",
|
|
364
|
+
onClick: onConfirm,
|
|
365
|
+
className: cn(
|
|
366
|
+
"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",
|
|
367
|
+
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"
|
|
368
|
+
),
|
|
369
|
+
children: confirmLabel ?? copy.confirm
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
] })
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
),
|
|
379
|
+
document.body
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
var Kbd = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "border-ui-border bg-ui-surface text-ui-muted rounded-md border px-1.5 py-0.5 font-sans text-[10px] font-medium shadow-sm", children });
|
|
383
|
+
var useCoarsePointer = () => {
|
|
384
|
+
const [coarse, setCoarse] = react.useState(false);
|
|
385
|
+
react.useEffect(() => {
|
|
386
|
+
const query = window.matchMedia("(pointer: coarse)");
|
|
387
|
+
setCoarse(query.matches);
|
|
388
|
+
const onChange = (event) => setCoarse(event.matches);
|
|
389
|
+
query.addEventListener("change", onChange);
|
|
390
|
+
return () => query.removeEventListener("change", onChange);
|
|
391
|
+
}, []);
|
|
392
|
+
return coarse;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/warn.ts
|
|
396
|
+
var seen = /* @__PURE__ */ new Set();
|
|
397
|
+
var warnDev = (key, message) => {
|
|
398
|
+
if (process.env.NODE_ENV === "production") return;
|
|
399
|
+
if (seen.has(key)) return;
|
|
400
|
+
seen.add(key);
|
|
401
|
+
console.warn(`[uploaderkit] ${message}`);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/ui/useShortcut.ts
|
|
405
|
+
var parseShortcut = (combo) => {
|
|
406
|
+
const parts = combo.toLowerCase().split("+");
|
|
407
|
+
return {
|
|
408
|
+
mod: parts.includes("mod"),
|
|
409
|
+
shift: parts.includes("shift"),
|
|
410
|
+
alt: parts.includes("alt"),
|
|
411
|
+
key: parts[parts.length - 1]
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
var IS_MAC = typeof navigator !== "undefined" && /mac/i.test(navigator.platform);
|
|
415
|
+
var formatShortcut = (combo) => {
|
|
416
|
+
const spec = parseShortcut(combo);
|
|
417
|
+
const key = spec.key.length === 1 ? spec.key.toUpperCase() : spec.key;
|
|
418
|
+
const parts = [];
|
|
419
|
+
if (spec.mod) parts.push(IS_MAC ? "\u2318" : "Ctrl");
|
|
420
|
+
if (spec.alt) parts.push(IS_MAC ? "\u2325" : "Alt");
|
|
421
|
+
if (spec.shift) parts.push(IS_MAC ? "\u21E7" : "Shift");
|
|
422
|
+
parts.push(key);
|
|
423
|
+
return parts.join(IS_MAC ? "" : "+");
|
|
424
|
+
};
|
|
425
|
+
var active = /* @__PURE__ */ new Map();
|
|
426
|
+
var useShortcut = (combo, handler, disabled = false) => {
|
|
427
|
+
const handlerRef = react.useRef(handler);
|
|
428
|
+
handlerRef.current = handler;
|
|
429
|
+
react.useEffect(() => {
|
|
430
|
+
if (!combo || disabled) return;
|
|
431
|
+
const spec = parseShortcut(combo);
|
|
432
|
+
active.set(combo, (active.get(combo) ?? 0) + 1);
|
|
433
|
+
if ((active.get(combo) ?? 0) > 1) {
|
|
434
|
+
warnDev(
|
|
435
|
+
`shortcut-collision:${combo}`,
|
|
436
|
+
`two surfaces registered the shortcut "${combo}" at the same time \u2014 give each its own combo or scope one out.`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const onKey = (event) => {
|
|
440
|
+
if (event.repeat) return;
|
|
441
|
+
const target = event.target;
|
|
442
|
+
if (target instanceof Element && target.closest("input, textarea, select, [contenteditable]")) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (event.key.toLowerCase() !== spec.key) return;
|
|
446
|
+
if (spec.mod !== (event.metaKey || event.ctrlKey)) return;
|
|
447
|
+
if (spec.shift !== event.shiftKey) return;
|
|
448
|
+
if (spec.alt !== event.altKey) return;
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
handlerRef.current();
|
|
451
|
+
};
|
|
452
|
+
window.addEventListener("keydown", onKey);
|
|
453
|
+
return () => {
|
|
454
|
+
window.removeEventListener("keydown", onKey);
|
|
455
|
+
const count = (active.get(combo) ?? 1) - 1;
|
|
456
|
+
if (count <= 0) active.delete(combo);
|
|
457
|
+
else active.set(combo, count);
|
|
458
|
+
};
|
|
459
|
+
}, [combo, disabled]);
|
|
460
|
+
};
|
|
461
|
+
var Dropzone = ({
|
|
462
|
+
accept,
|
|
463
|
+
multiple = false,
|
|
464
|
+
disabled = false,
|
|
465
|
+
capture,
|
|
466
|
+
size = "md",
|
|
467
|
+
shortcut,
|
|
468
|
+
onFiles,
|
|
469
|
+
children,
|
|
470
|
+
className
|
|
471
|
+
}) => {
|
|
472
|
+
const inputRef = react.useRef(null);
|
|
473
|
+
const [dragging, setDragging] = react.useState(false);
|
|
474
|
+
const depth = react.useRef(0);
|
|
475
|
+
const coarse = useCoarsePointer();
|
|
476
|
+
useShortcut(shortcut, () => inputRef.current?.click(), disabled);
|
|
477
|
+
const emit = react.useCallback(
|
|
478
|
+
(list) => {
|
|
479
|
+
if (disabled || !list || list.length === 0) return;
|
|
480
|
+
onFiles(Array.from(list));
|
|
481
|
+
},
|
|
482
|
+
[disabled, onFiles]
|
|
483
|
+
);
|
|
484
|
+
const onDrop = react.useCallback(
|
|
485
|
+
(event) => {
|
|
486
|
+
event.preventDefault();
|
|
487
|
+
depth.current = 0;
|
|
488
|
+
setDragging(false);
|
|
489
|
+
emit(event.dataTransfer.files);
|
|
490
|
+
},
|
|
491
|
+
[emit]
|
|
492
|
+
);
|
|
493
|
+
const onPaste = react.useCallback(
|
|
494
|
+
(event) => {
|
|
495
|
+
if (event.clipboardData.files.length === 0) return;
|
|
496
|
+
event.preventDefault();
|
|
497
|
+
emit(event.clipboardData.files);
|
|
498
|
+
},
|
|
499
|
+
[emit]
|
|
500
|
+
);
|
|
501
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
502
|
+
"div",
|
|
503
|
+
{
|
|
504
|
+
role: "button",
|
|
505
|
+
tabIndex: disabled ? -1 : 0,
|
|
506
|
+
"aria-disabled": disabled,
|
|
507
|
+
"data-dragging": dragging || void 0,
|
|
508
|
+
onClick: () => !disabled && inputRef.current?.click(),
|
|
509
|
+
onKeyDown: (event) => {
|
|
510
|
+
if (disabled) return;
|
|
511
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
inputRef.current?.click();
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
onDragEnter: () => {
|
|
517
|
+
depth.current += 1;
|
|
518
|
+
if (!disabled) setDragging(true);
|
|
519
|
+
},
|
|
520
|
+
onDragOver: (event) => event.preventDefault(),
|
|
521
|
+
onDragLeave: () => {
|
|
522
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
523
|
+
if (depth.current === 0) setDragging(false);
|
|
524
|
+
},
|
|
525
|
+
onDrop,
|
|
526
|
+
onPaste,
|
|
527
|
+
className: cn(
|
|
528
|
+
// `group/dz` + `data-dragging` let the children (the icon, the
|
|
529
|
+
// prompt) react to hover and drag without owning any state.
|
|
530
|
+
// `touch-manipulation` kills the 300ms tap delay; the active scale
|
|
531
|
+
// is the press feedback a touch screen needs in place of hover.
|
|
532
|
+
"group/dz rounded-ui-lg focus-visible:ring-ui-ring block w-full cursor-pointer touch-manipulation border-2 border-dashed text-center transition-all duration-200 focus-visible:ring-2 focus-visible:outline-none",
|
|
533
|
+
size === "sm" ? "p-3" : "p-5",
|
|
534
|
+
dragging ? "border-ui-primary bg-ui-primary-soft shadow-ui-primary/15 scale-[1.02] shadow-lg" : "border-ui-border-strong hover:border-ui-primary/60 hover:bg-ui-primary-soft/40 hover:shadow-ui-primary/10 bg-ui-surface-subtle/50 hover:shadow-md",
|
|
535
|
+
!disabled && "active:scale-[0.98]",
|
|
536
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
537
|
+
className
|
|
538
|
+
),
|
|
539
|
+
children: [
|
|
540
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
541
|
+
"input",
|
|
542
|
+
{
|
|
543
|
+
ref: inputRef,
|
|
544
|
+
type: "file",
|
|
545
|
+
accept,
|
|
546
|
+
multiple,
|
|
547
|
+
disabled,
|
|
548
|
+
...capture ? { capture } : {},
|
|
549
|
+
className: "hidden",
|
|
550
|
+
onClick: (event) => event.stopPropagation(),
|
|
551
|
+
onChange: (event) => {
|
|
552
|
+
emit(event.target.files);
|
|
553
|
+
event.target.value = "";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
children,
|
|
558
|
+
shortcut && !coarse && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-2 block", children: /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: formatShortcut(shortcut) }) })
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/constants.ts
|
|
565
|
+
var KB = 1024;
|
|
566
|
+
var MB = 1024 * KB;
|
|
567
|
+
var MAGIC_NUMBERS = {
|
|
568
|
+
png: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
569
|
+
jpg: [255, 216, 255],
|
|
570
|
+
jpeg: [255, 216, 255],
|
|
571
|
+
gif: [71, 73, 70, 56],
|
|
572
|
+
webp: [82, 73, 70, 70],
|
|
573
|
+
// RIFF; bytes 8-11 spell WEBP
|
|
574
|
+
ico: [0, 0, 1, 0],
|
|
575
|
+
pdf: [37, 80, 68, 70],
|
|
576
|
+
// %PDF
|
|
577
|
+
// OOXML files are ZIP containers, so docx/xlsx share the PK signature.
|
|
578
|
+
docx: [80, 75, 3, 4],
|
|
579
|
+
xlsx: [80, 75, 3, 4],
|
|
580
|
+
doc: [208, 207, 17, 224],
|
|
581
|
+
xls: [208, 207, 17, 224],
|
|
582
|
+
// DER/ASN.1 sequence header.
|
|
583
|
+
cer: [48, 130],
|
|
584
|
+
crt: [48, 130],
|
|
585
|
+
key: [48, 130],
|
|
586
|
+
pem: [45, 45, 45, 45, 45],
|
|
587
|
+
// -----
|
|
588
|
+
webm: [26, 69, 223, 163],
|
|
589
|
+
avi: [82, 73, 70, 70],
|
|
590
|
+
mp3: [73, 68, 51],
|
|
591
|
+
// ID3
|
|
592
|
+
wav: [82, 73, 70, 70],
|
|
593
|
+
ogg: [79, 103, 103, 83],
|
|
594
|
+
aac: [255, 241]
|
|
595
|
+
};
|
|
596
|
+
var MIME_TYPES = {
|
|
597
|
+
png: "image/png",
|
|
598
|
+
jpg: "image/jpeg",
|
|
599
|
+
jpeg: "image/jpeg",
|
|
600
|
+
svg: "image/svg+xml",
|
|
601
|
+
webp: "image/webp",
|
|
602
|
+
gif: "image/gif",
|
|
603
|
+
ico: "image/x-icon",
|
|
604
|
+
heic: "image/heic",
|
|
605
|
+
cer: "application/x-x509-ca-cert",
|
|
606
|
+
crt: "application/x-x509-ca-cert",
|
|
607
|
+
pem: "application/x-pem-file",
|
|
608
|
+
key: "application/x-pem-file",
|
|
609
|
+
pdf: "application/pdf",
|
|
610
|
+
doc: "application/msword",
|
|
611
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
612
|
+
xls: "application/vnd.ms-excel",
|
|
613
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
614
|
+
csv: "text/csv",
|
|
615
|
+
txt: "text/plain",
|
|
616
|
+
xml: "application/xml",
|
|
617
|
+
json: "application/json",
|
|
618
|
+
mp4: "video/mp4",
|
|
619
|
+
mov: "video/quicktime",
|
|
620
|
+
avi: "video/x-msvideo",
|
|
621
|
+
webm: "video/webm",
|
|
622
|
+
mp3: "audio/mpeg",
|
|
623
|
+
wav: "audio/wav",
|
|
624
|
+
ogg: "audio/ogg",
|
|
625
|
+
aac: "audio/aac"
|
|
626
|
+
};
|
|
627
|
+
var FILE_CATEGORY_CONFIG = {
|
|
628
|
+
image: {
|
|
629
|
+
extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
|
|
630
|
+
maxBytes: 5 * MB,
|
|
631
|
+
validateMagicNumbers: true,
|
|
632
|
+
label: "image",
|
|
633
|
+
accept: "image/*"
|
|
634
|
+
},
|
|
635
|
+
certificate: {
|
|
636
|
+
extensions: ["cer", "crt", "pem"],
|
|
637
|
+
maxBytes: 50 * KB,
|
|
638
|
+
validateMagicNumbers: true,
|
|
639
|
+
label: "certificate",
|
|
640
|
+
accept: ".cer,.crt,.pem"
|
|
641
|
+
},
|
|
642
|
+
key: {
|
|
643
|
+
extensions: ["key"],
|
|
644
|
+
maxBytes: 50 * KB,
|
|
645
|
+
validateMagicNumbers: true,
|
|
646
|
+
label: "private key",
|
|
647
|
+
accept: ".key"
|
|
648
|
+
},
|
|
649
|
+
pdf: {
|
|
650
|
+
extensions: ["pdf"],
|
|
651
|
+
maxBytes: 20 * MB,
|
|
652
|
+
validateMagicNumbers: true,
|
|
653
|
+
label: "PDF",
|
|
654
|
+
accept: ".pdf"
|
|
655
|
+
},
|
|
656
|
+
document: {
|
|
657
|
+
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
|
|
658
|
+
maxBytes: 20 * MB,
|
|
659
|
+
validateMagicNumbers: false,
|
|
660
|
+
label: "document",
|
|
661
|
+
accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
|
|
662
|
+
},
|
|
663
|
+
data: {
|
|
664
|
+
extensions: ["xml", "json"],
|
|
665
|
+
maxBytes: 5 * MB,
|
|
666
|
+
validateMagicNumbers: false,
|
|
667
|
+
label: "data file",
|
|
668
|
+
accept: ".xml,.json"
|
|
669
|
+
},
|
|
670
|
+
video: {
|
|
671
|
+
extensions: ["mp4", "mov", "avi", "webm"],
|
|
672
|
+
maxBytes: 100 * MB,
|
|
673
|
+
validateMagicNumbers: false,
|
|
674
|
+
label: "video",
|
|
675
|
+
accept: "video/*"
|
|
676
|
+
},
|
|
677
|
+
audio: {
|
|
678
|
+
extensions: ["mp3", "wav", "ogg", "aac"],
|
|
679
|
+
maxBytes: 20 * MB,
|
|
680
|
+
validateMagicNumbers: false,
|
|
681
|
+
label: "audio",
|
|
682
|
+
accept: "audio/*"
|
|
683
|
+
},
|
|
684
|
+
any: {
|
|
685
|
+
extensions: [],
|
|
686
|
+
maxBytes: 20 * MB,
|
|
687
|
+
validateMagicNumbers: false,
|
|
688
|
+
label: "file",
|
|
689
|
+
accept: "*/*"
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/file.ts
|
|
694
|
+
var getFileExtension = (fileName) => {
|
|
695
|
+
const dot = fileName.lastIndexOf(".");
|
|
696
|
+
if (dot < 1 || dot === fileName.length - 1) return "";
|
|
697
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
698
|
+
};
|
|
699
|
+
var getMimeType = (fileName) => {
|
|
700
|
+
const ext = getFileExtension(fileName);
|
|
701
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
702
|
+
};
|
|
703
|
+
var isKnownExtension = (value) => value in MIME_TYPES;
|
|
704
|
+
var formatFileSize = (bytes) => {
|
|
705
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
706
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
707
|
+
let size = bytes / 1024;
|
|
708
|
+
let unit = 0;
|
|
709
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
710
|
+
size /= 1024;
|
|
711
|
+
unit++;
|
|
712
|
+
}
|
|
713
|
+
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
714
|
+
};
|
|
715
|
+
var toAcceptAttribute = (extensions) => extensions.length === 0 ? "*/*" : extensions.map((extension) => `.${extension}`).join(",");
|
|
716
|
+
var FileTypeBadge = ({
|
|
717
|
+
fileName,
|
|
718
|
+
preview,
|
|
719
|
+
size = "md"
|
|
720
|
+
}) => {
|
|
721
|
+
const box = size === "sm" ? "h-8 w-8" : "h-10 w-10";
|
|
722
|
+
if (!fileName) {
|
|
723
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
724
|
+
"span",
|
|
725
|
+
{
|
|
726
|
+
"aria-hidden": "true",
|
|
727
|
+
className: cn(
|
|
728
|
+
"border-ui-border rounded-ui shrink-0 border border-dashed",
|
|
729
|
+
box
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return preview ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
735
|
+
"img",
|
|
736
|
+
{
|
|
737
|
+
src: preview,
|
|
738
|
+
alt: "",
|
|
739
|
+
className: cn("rounded-ui shrink-0 object-cover", box)
|
|
740
|
+
}
|
|
741
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
742
|
+
"span",
|
|
743
|
+
{
|
|
744
|
+
className: cn(
|
|
745
|
+
"bg-ui-surface-muted text-ui-muted rounded-ui flex shrink-0 items-center justify-center font-medium uppercase",
|
|
746
|
+
size === "sm" ? "text-[10px]" : "text-xs",
|
|
747
|
+
box
|
|
748
|
+
),
|
|
749
|
+
children: getFileExtension(fileName)
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
};
|
|
753
|
+
var ProgressBar = ({ percent, className }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
754
|
+
"div",
|
|
755
|
+
{
|
|
756
|
+
className: cn(
|
|
757
|
+
"bg-ui-surface-muted mt-1 h-1 w-full overflow-hidden rounded",
|
|
758
|
+
className
|
|
759
|
+
),
|
|
760
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
761
|
+
"div",
|
|
762
|
+
{
|
|
763
|
+
className: "bg-ui-primary h-full transition-all",
|
|
764
|
+
style: { width: `${percent}%` }
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
var FileItem = ({
|
|
770
|
+
file,
|
|
771
|
+
onRemove,
|
|
772
|
+
onAbort,
|
|
773
|
+
onView,
|
|
774
|
+
size = "md",
|
|
775
|
+
labels
|
|
776
|
+
}) => {
|
|
777
|
+
const copy = useUploaderLabels(labels);
|
|
778
|
+
const uploading = file.status === "uploading";
|
|
779
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
780
|
+
"div",
|
|
781
|
+
{
|
|
782
|
+
className: cn(
|
|
783
|
+
"rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm transition-colors",
|
|
784
|
+
size === "sm" ? "p-1.5" : "p-2",
|
|
785
|
+
file.status === "error" ? "border-ui-danger-soft-fg/20 bg-ui-danger-soft" : "border-ui-border bg-ui-surface"
|
|
786
|
+
),
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
789
|
+
FileTypeBadge,
|
|
790
|
+
{
|
|
791
|
+
fileName: file.file.name,
|
|
792
|
+
preview: file.preview,
|
|
793
|
+
size
|
|
794
|
+
}
|
|
795
|
+
),
|
|
796
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
797
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg truncate font-medium", children: file.file.name }),
|
|
798
|
+
file.status === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-danger text-xs", children: file.error }) : /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-ui-muted text-xs", children: [
|
|
799
|
+
formatFileSize(file.file.size),
|
|
800
|
+
file.status === "success" && ` \xB7 ${copy.ready}`,
|
|
801
|
+
file.status === "idle" && ` \xB7 ${copy.staged}`
|
|
802
|
+
] }),
|
|
803
|
+
uploading && /* @__PURE__ */ jsxRuntime.jsx(ProgressBar, { percent: file.progress })
|
|
804
|
+
] }),
|
|
805
|
+
onView && /* @__PURE__ */ jsxRuntime.jsx(
|
|
806
|
+
"button",
|
|
807
|
+
{
|
|
808
|
+
type: "button",
|
|
809
|
+
onClick: onView,
|
|
810
|
+
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",
|
|
811
|
+
children: copy.view
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
815
|
+
"button",
|
|
816
|
+
{
|
|
817
|
+
type: "button",
|
|
818
|
+
onClick: uploading ? onAbort : onRemove,
|
|
819
|
+
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",
|
|
820
|
+
children: uploading ? copy.cancel : copy.remove
|
|
821
|
+
}
|
|
822
|
+
)
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
};
|
|
827
|
+
var UploadCloudIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
828
|
+
"svg",
|
|
829
|
+
{
|
|
830
|
+
viewBox: "0 0 24 24",
|
|
831
|
+
fill: "none",
|
|
832
|
+
stroke: "currentColor",
|
|
833
|
+
strokeWidth: "1.75",
|
|
834
|
+
strokeLinecap: "round",
|
|
835
|
+
strokeLinejoin: "round",
|
|
836
|
+
className,
|
|
837
|
+
"aria-hidden": true,
|
|
838
|
+
children: [
|
|
839
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 14.9A7 7 0 1 1 15.7 8h1.8a4.5 4.5 0 0 1 2.5 8.2" }),
|
|
840
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 12v9" }),
|
|
841
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m8 16 4-4 4 4" })
|
|
842
|
+
]
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
var DownloadIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
846
|
+
"svg",
|
|
847
|
+
{
|
|
848
|
+
viewBox: "0 0 24 24",
|
|
849
|
+
fill: "none",
|
|
850
|
+
stroke: "currentColor",
|
|
851
|
+
strokeWidth: "1.75",
|
|
852
|
+
strokeLinecap: "round",
|
|
853
|
+
strokeLinejoin: "round",
|
|
854
|
+
className,
|
|
855
|
+
"aria-hidden": true,
|
|
856
|
+
children: [
|
|
857
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3v12" }),
|
|
858
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m7 11 5 5 5-5" }),
|
|
859
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 20h16" })
|
|
860
|
+
]
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
var ExternalLinkIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
864
|
+
"svg",
|
|
865
|
+
{
|
|
866
|
+
viewBox: "0 0 24 24",
|
|
867
|
+
fill: "none",
|
|
868
|
+
stroke: "currentColor",
|
|
869
|
+
strokeWidth: "1.75",
|
|
870
|
+
strokeLinecap: "round",
|
|
871
|
+
strokeLinejoin: "round",
|
|
872
|
+
className,
|
|
873
|
+
"aria-hidden": true,
|
|
874
|
+
children: [
|
|
875
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 4h6v6" }),
|
|
876
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 4 10 14" }),
|
|
877
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" })
|
|
878
|
+
]
|
|
879
|
+
}
|
|
880
|
+
);
|
|
881
|
+
var FileWarningIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
882
|
+
"svg",
|
|
883
|
+
{
|
|
884
|
+
viewBox: "0 0 24 24",
|
|
885
|
+
fill: "none",
|
|
886
|
+
stroke: "currentColor",
|
|
887
|
+
strokeWidth: "1.5",
|
|
888
|
+
strokeLinecap: "round",
|
|
889
|
+
strokeLinejoin: "round",
|
|
890
|
+
className,
|
|
891
|
+
"aria-hidden": true,
|
|
892
|
+
children: [
|
|
893
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
894
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3v5h5" }),
|
|
895
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11v4" }),
|
|
896
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 18h.01" })
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
var EMPTY_STATE_CLASS = "mx-4 flex max-w-sm flex-col items-center text-center";
|
|
901
|
+
var EMPTY_TITLE_CLASS = "text-sm font-medium text-white";
|
|
902
|
+
var EMPTY_HINT_CLASS = "mt-1.5 text-xs leading-relaxed text-white/60";
|
|
903
|
+
var EMPTY_PRIMARY_CLASS = "focus-visible:ring-ui-ring rounded-ui cursor-pointer bg-white/20 px-4 py-2 text-sm text-white transition-colors hover:bg-white/30 focus-visible:ring-2 focus-visible:outline-none";
|
|
904
|
+
var EMPTY_SECONDARY_CLASS = "focus-visible:ring-ui-ring rounded-ui cursor-pointer border border-white/20 px-4 py-2 text-sm text-white/80 transition-colors hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:outline-none";
|
|
905
|
+
var ACTION_CLASS = "focus-visible:ring-ui-ring rounded-ui inline-flex shrink-0 cursor-pointer items-center gap-1.5 bg-white/10 px-3 py-1.5 text-xs whitespace-nowrap text-white transition-colors hover:bg-white/20 focus-visible:ring-2 focus-visible:outline-none";
|
|
906
|
+
var kindOf = (file) => {
|
|
907
|
+
const mime = file.mimeType || getMimeType(file.fileName ?? file.url);
|
|
908
|
+
if (mime.startsWith("image/")) return "image";
|
|
909
|
+
if (mime === "application/pdf") return "pdf";
|
|
910
|
+
return "other";
|
|
911
|
+
};
|
|
912
|
+
var isIosWebkit = () => {
|
|
913
|
+
const ua = navigator.userAgent;
|
|
914
|
+
return /iPad|iPhone|iPod/.test(ua) || ua.includes("Macintosh") && navigator.maxTouchPoints > 1;
|
|
915
|
+
};
|
|
916
|
+
var browserEmbedsPdf = () => {
|
|
917
|
+
if (typeof navigator === "undefined") return false;
|
|
918
|
+
if (navigator.pdfViewerEnabled === false) return false;
|
|
919
|
+
return !isIosWebkit();
|
|
920
|
+
};
|
|
921
|
+
var Chevron = ({ direction }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
922
|
+
"svg",
|
|
923
|
+
{
|
|
924
|
+
viewBox: "0 0 24 24",
|
|
925
|
+
fill: "none",
|
|
926
|
+
stroke: "currentColor",
|
|
927
|
+
strokeWidth: "2.5",
|
|
928
|
+
strokeLinecap: "round",
|
|
929
|
+
strokeLinejoin: "round",
|
|
930
|
+
className: "h-5 w-5",
|
|
931
|
+
"aria-hidden": true,
|
|
932
|
+
children: direction === "left" ? /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m15 18-6-6 6-6" }) : /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m9 18 6-6-6-6" })
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
var FileViewer = ({
|
|
936
|
+
file,
|
|
937
|
+
onClose,
|
|
938
|
+
files,
|
|
939
|
+
resolveUrl,
|
|
940
|
+
renderError,
|
|
941
|
+
labels
|
|
942
|
+
}) => {
|
|
943
|
+
const copy = useUploaderLabels(labels);
|
|
944
|
+
const coarse = useCoarsePointer();
|
|
945
|
+
const [url, setUrl] = react.useState(null);
|
|
946
|
+
const [failed, setFailed] = react.useState(false);
|
|
947
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
948
|
+
const retry = react.useCallback(() => setAttempt((previous) => previous + 1), []);
|
|
949
|
+
const panelRef = react.useRef(null);
|
|
950
|
+
const open = file !== null;
|
|
951
|
+
const { mounted, entered } = useOverlayTransition(open);
|
|
952
|
+
const layer = useOverlayLayer(open);
|
|
953
|
+
useFocusTrap(open, panelRef);
|
|
954
|
+
const list = files && files.length > 0 ? files : null;
|
|
955
|
+
const [cursor, setCursor] = react.useState(0);
|
|
956
|
+
react.useEffect(() => {
|
|
957
|
+
if (!file || !list) return;
|
|
958
|
+
const index = list.findIndex((entry) => entry.url === file.url);
|
|
959
|
+
setCursor(index >= 0 ? index : 0);
|
|
960
|
+
}, [file]);
|
|
961
|
+
const current = open ? list ? list[cursor] ?? file : file : null;
|
|
962
|
+
const lastViewRef = react.useRef(null);
|
|
963
|
+
if (current) lastViewRef.current = current;
|
|
964
|
+
const navigate = react.useCallback(
|
|
965
|
+
(delta) => {
|
|
966
|
+
if (!list) return;
|
|
967
|
+
setCursor(
|
|
968
|
+
(previous) => Math.min(list.length - 1, Math.max(0, previous + delta))
|
|
969
|
+
);
|
|
970
|
+
},
|
|
971
|
+
[list]
|
|
972
|
+
);
|
|
973
|
+
const objectUrlRef = react.useRef(null);
|
|
974
|
+
const releaseObjectUrl = react.useCallback(() => {
|
|
975
|
+
if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
|
|
976
|
+
objectUrlRef.current = null;
|
|
977
|
+
}, []);
|
|
978
|
+
const resolveRef = react.useRef(resolveUrl);
|
|
979
|
+
resolveRef.current = resolveUrl;
|
|
980
|
+
const currentUrl = current?.url;
|
|
981
|
+
react.useEffect(() => {
|
|
982
|
+
releaseObjectUrl();
|
|
983
|
+
if (!current) {
|
|
984
|
+
setUrl(null);
|
|
985
|
+
setFailed(false);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
let alive = true;
|
|
989
|
+
setUrl(null);
|
|
990
|
+
setFailed(false);
|
|
991
|
+
void (async () => {
|
|
992
|
+
try {
|
|
993
|
+
const resolve = resolveRef.current;
|
|
994
|
+
const resolved = resolve ? await resolve(current) : current.url;
|
|
995
|
+
const minted = resolved.startsWith("blob:");
|
|
996
|
+
if (!alive) {
|
|
997
|
+
if (minted) URL.revokeObjectURL(resolved);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (minted) objectUrlRef.current = resolved;
|
|
1001
|
+
setUrl(resolved);
|
|
1002
|
+
} catch {
|
|
1003
|
+
if (alive) {
|
|
1004
|
+
setUrl(null);
|
|
1005
|
+
setFailed(true);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
})();
|
|
1009
|
+
return () => {
|
|
1010
|
+
alive = false;
|
|
1011
|
+
};
|
|
1012
|
+
}, [currentUrl, attempt]);
|
|
1013
|
+
react.useEffect(() => releaseObjectUrl, [releaseObjectUrl]);
|
|
1014
|
+
const closeRef = react.useRef(onClose);
|
|
1015
|
+
closeRef.current = onClose;
|
|
1016
|
+
const downloadRef = react.useRef(null);
|
|
1017
|
+
const openTabRef = react.useRef(null);
|
|
1018
|
+
react.useEffect(() => {
|
|
1019
|
+
if (!open) return;
|
|
1020
|
+
const onKey = (event) => {
|
|
1021
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1022
|
+
if (event.key === "Escape") {
|
|
1023
|
+
if (!layer.isTopmost()) return;
|
|
1024
|
+
event.stopPropagation();
|
|
1025
|
+
closeRef.current();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (event.key === "ArrowLeft") navigate(-1);
|
|
1029
|
+
if (event.key === "ArrowRight") navigate(1);
|
|
1030
|
+
const key = event.key.toLowerCase();
|
|
1031
|
+
if (key === "d") downloadRef.current?.click();
|
|
1032
|
+
if (key === "o") openTabRef.current?.click();
|
|
1033
|
+
};
|
|
1034
|
+
window.addEventListener("keydown", onKey, true);
|
|
1035
|
+
lockBodyScroll();
|
|
1036
|
+
return () => {
|
|
1037
|
+
window.removeEventListener("keydown", onKey, true);
|
|
1038
|
+
unlockBodyScroll();
|
|
1039
|
+
};
|
|
1040
|
+
}, [open, navigate, layer]);
|
|
1041
|
+
const [canEmbedPdf] = react.useState(browserEmbedsPdf);
|
|
1042
|
+
const view = current ?? lastViewRef.current;
|
|
1043
|
+
if (!mounted || !view) return null;
|
|
1044
|
+
const kind = kindOf(view);
|
|
1045
|
+
const showsPdf = kind === "pdf" && canEmbedPdf && !failed && !!url;
|
|
1046
|
+
const hasGallery = list !== null && list.length > 1;
|
|
1047
|
+
const arrow = "absolute top-1/2 z-10 flex h-11 w-11 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-white/10 text-white backdrop-blur-sm transition-all duration-200 hover:bg-white/25 focus-visible:ring-2 focus-visible:ring-ui-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-25";
|
|
1048
|
+
return reactDom.createPortal(
|
|
1049
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1050
|
+
"div",
|
|
1051
|
+
{
|
|
1052
|
+
className: cn(
|
|
1053
|
+
"fixed inset-0 z-1000 flex flex-col bg-black/85 transition-opacity duration-200 ease-out",
|
|
1054
|
+
entered ? "opacity-100" : "opacity-0",
|
|
1055
|
+
!open && "pointer-events-none"
|
|
1056
|
+
),
|
|
1057
|
+
onClick: onClose,
|
|
1058
|
+
role: "dialog",
|
|
1059
|
+
"aria-modal": "true",
|
|
1060
|
+
"aria-label": view.fileName ?? copy.filePreview,
|
|
1061
|
+
children: [
|
|
1062
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1063
|
+
"div",
|
|
1064
|
+
{
|
|
1065
|
+
className: "flex items-center justify-between gap-2 px-3 py-2.5 sm:gap-3 sm:px-4 sm:py-3",
|
|
1066
|
+
onClick: (event) => event.stopPropagation(),
|
|
1067
|
+
children: [
|
|
1068
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "min-w-0 flex-1 truncate text-xs font-medium text-white sm:text-sm", children: view.fileName ?? copy.filePreview }),
|
|
1069
|
+
hasGallery && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 rounded-full bg-white/10 px-2.5 py-1 text-[11px] font-medium text-white tabular-nums", children: [
|
|
1070
|
+
cursor + 1,
|
|
1071
|
+
" / ",
|
|
1072
|
+
list.length
|
|
1073
|
+
] }),
|
|
1074
|
+
url && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1075
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1076
|
+
"a",
|
|
1077
|
+
{
|
|
1078
|
+
ref: downloadRef,
|
|
1079
|
+
href: url,
|
|
1080
|
+
download: view.fileName,
|
|
1081
|
+
"aria-label": copy.downloadShort,
|
|
1082
|
+
title: copy.downloadShort,
|
|
1083
|
+
className: ACTION_CLASS,
|
|
1084
|
+
children: coarse ? /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, { className: "h-4 w-4" }) : copy.downloadShort
|
|
1085
|
+
}
|
|
1086
|
+
),
|
|
1087
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1088
|
+
"a",
|
|
1089
|
+
{
|
|
1090
|
+
ref: openTabRef,
|
|
1091
|
+
href: url,
|
|
1092
|
+
target: "_blank",
|
|
1093
|
+
rel: "noreferrer",
|
|
1094
|
+
"aria-label": copy.openInTab,
|
|
1095
|
+
title: copy.openInTab,
|
|
1096
|
+
className: ACTION_CLASS,
|
|
1097
|
+
children: coarse ? /* @__PURE__ */ jsxRuntime.jsx(ExternalLinkIcon, { className: "h-4 w-4" }) : copy.openInTab
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
] }),
|
|
1101
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1102
|
+
"button",
|
|
1103
|
+
{
|
|
1104
|
+
type: "button",
|
|
1105
|
+
onClick: onClose,
|
|
1106
|
+
"aria-label": copy.close,
|
|
1107
|
+
title: copy.close,
|
|
1108
|
+
className: ACTION_CLASS,
|
|
1109
|
+
children: coarse ? "\u2715" : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1110
|
+
"\u2715 ",
|
|
1111
|
+
copy.close
|
|
1112
|
+
] })
|
|
1113
|
+
}
|
|
1114
|
+
)
|
|
1115
|
+
]
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1119
|
+
"div",
|
|
1120
|
+
{
|
|
1121
|
+
ref: panelRef,
|
|
1122
|
+
tabIndex: -1,
|
|
1123
|
+
className: cn(
|
|
1124
|
+
"relative flex min-h-0 flex-1 items-center justify-center p-3 outline-none sm:p-4",
|
|
1125
|
+
showsPdf && "items-stretch"
|
|
1126
|
+
),
|
|
1127
|
+
onClick: (event) => event.stopPropagation(),
|
|
1128
|
+
children: [
|
|
1129
|
+
hasGallery && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1130
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1131
|
+
"button",
|
|
1132
|
+
{
|
|
1133
|
+
type: "button",
|
|
1134
|
+
onClick: () => navigate(-1),
|
|
1135
|
+
disabled: cursor === 0,
|
|
1136
|
+
"aria-label": copy.previous,
|
|
1137
|
+
className: cn(arrow, "left-2 sm:left-4"),
|
|
1138
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { direction: "left" })
|
|
1139
|
+
}
|
|
1140
|
+
),
|
|
1141
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1142
|
+
"button",
|
|
1143
|
+
{
|
|
1144
|
+
type: "button",
|
|
1145
|
+
onClick: () => navigate(1),
|
|
1146
|
+
disabled: cursor === list.length - 1,
|
|
1147
|
+
"aria-label": copy.next,
|
|
1148
|
+
className: cn(arrow, "right-2 sm:right-4"),
|
|
1149
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { direction: "right" })
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
1152
|
+
] }),
|
|
1153
|
+
failed ? renderError?.({ file: view, retry }) ?? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: EMPTY_STATE_CLASS, children: [
|
|
1154
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/5 text-white/70 ring-1 ring-white/10", children: /* @__PURE__ */ jsxRuntime.jsx(FileWarningIcon, { className: "h-8 w-8" }) }),
|
|
1155
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_TITLE_CLASS, children: copy.viewerError }),
|
|
1156
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_HINT_CLASS, children: copy.viewerErrorHint }),
|
|
1157
|
+
view.fileName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 max-w-full truncate font-mono text-[11px] text-white/40", children: view.fileName }),
|
|
1158
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 flex flex-wrap justify-center gap-2", children: [
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1160
|
+
"button",
|
|
1161
|
+
{
|
|
1162
|
+
type: "button",
|
|
1163
|
+
onClick: retry,
|
|
1164
|
+
className: EMPTY_PRIMARY_CLASS,
|
|
1165
|
+
children: copy.viewerRetry
|
|
1166
|
+
}
|
|
1167
|
+
),
|
|
1168
|
+
url && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1169
|
+
"a",
|
|
1170
|
+
{
|
|
1171
|
+
href: url,
|
|
1172
|
+
download: view.fileName,
|
|
1173
|
+
className: EMPTY_SECONDARY_CLASS,
|
|
1174
|
+
children: copy.downloadShort
|
|
1175
|
+
}
|
|
1176
|
+
)
|
|
1177
|
+
] })
|
|
1178
|
+
] }) : !url ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white/70", children: copy.viewerLoading }) : kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1179
|
+
"img",
|
|
1180
|
+
{
|
|
1181
|
+
src: url,
|
|
1182
|
+
alt: view.fileName ?? "",
|
|
1183
|
+
onError: () => setFailed(true),
|
|
1184
|
+
className: cn(
|
|
1185
|
+
"rounded-ui-lg max-h-full max-w-full object-contain shadow-2xl",
|
|
1186
|
+
"transition-transform duration-200 ease-out",
|
|
1187
|
+
entered ? "scale-100" : "scale-[0.97]"
|
|
1188
|
+
)
|
|
1189
|
+
}
|
|
1190
|
+
) : showsPdf ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1191
|
+
"iframe",
|
|
1192
|
+
{
|
|
1193
|
+
src: url,
|
|
1194
|
+
title: view.fileName ?? "PDF",
|
|
1195
|
+
onError: () => setFailed(true),
|
|
1196
|
+
className: "rounded-ui-lg h-full w-full max-w-4xl bg-white shadow-2xl"
|
|
1197
|
+
}
|
|
1198
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: EMPTY_STATE_CLASS, children: [
|
|
1199
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/5 text-white/70 ring-1 ring-white/10", children: /* @__PURE__ */ jsxRuntime.jsx(FileWarningIcon, { className: "h-8 w-8" }) }),
|
|
1200
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_TITLE_CLASS, children: copy.noPreview }),
|
|
1201
|
+
view.fileName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 max-w-full truncate font-mono text-[11px] text-white/40", children: view.fileName }),
|
|
1202
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1203
|
+
"a",
|
|
1204
|
+
{
|
|
1205
|
+
href: url,
|
|
1206
|
+
download: view.fileName,
|
|
1207
|
+
target: "_blank",
|
|
1208
|
+
rel: "noreferrer",
|
|
1209
|
+
className: cn(EMPTY_PRIMARY_CLASS, "mt-5 inline-block"),
|
|
1210
|
+
children: copy.download(view.fileName ?? copy.filePreview)
|
|
1211
|
+
}
|
|
1212
|
+
)
|
|
1213
|
+
] })
|
|
1214
|
+
]
|
|
1215
|
+
}
|
|
1216
|
+
),
|
|
1217
|
+
!coarse && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1218
|
+
"div",
|
|
1219
|
+
{
|
|
1220
|
+
className: "flex items-center justify-center gap-4 pb-3 text-[11px] text-white/60",
|
|
1221
|
+
onClick: (event) => event.stopPropagation(),
|
|
1222
|
+
children: [
|
|
1223
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1224
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Esc" }),
|
|
1225
|
+
" ",
|
|
1226
|
+
copy.close
|
|
1227
|
+
] }),
|
|
1228
|
+
url && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1229
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1230
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "D" }),
|
|
1231
|
+
" ",
|
|
1232
|
+
copy.downloadShort
|
|
1233
|
+
] }),
|
|
1234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1235
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "O" }),
|
|
1236
|
+
" ",
|
|
1237
|
+
copy.openInTab
|
|
1238
|
+
] })
|
|
1239
|
+
] }),
|
|
1240
|
+
hasGallery && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1241
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "\u2190" }),
|
|
1242
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "\u2192" }),
|
|
1243
|
+
" ",
|
|
1244
|
+
copy.previous,
|
|
1245
|
+
" / ",
|
|
1246
|
+
copy.next
|
|
1247
|
+
] })
|
|
1248
|
+
]
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
]
|
|
1252
|
+
}
|
|
1253
|
+
),
|
|
1254
|
+
document.body
|
|
1255
|
+
);
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// src/react/compressImage.ts
|
|
1259
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
1260
|
+
var compressImage = async (file, { maxWidth, maxHeight, quality = 0.8 }) => {
|
|
1261
|
+
if (!COMPRESSIBLE.has(file.type)) return file;
|
|
1262
|
+
if (typeof createImageBitmap !== "function") return file;
|
|
1263
|
+
try {
|
|
1264
|
+
const bitmap = await createImageBitmap(file);
|
|
1265
|
+
const scale = Math.min(
|
|
1266
|
+
1,
|
|
1267
|
+
(maxWidth ?? Infinity) / bitmap.width,
|
|
1268
|
+
(maxHeight ?? Infinity) / bitmap.height
|
|
1269
|
+
);
|
|
1270
|
+
const canvas = document.createElement("canvas");
|
|
1271
|
+
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
1272
|
+
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
1273
|
+
const context = canvas.getContext("2d");
|
|
1274
|
+
if (!context) return file;
|
|
1275
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
1276
|
+
bitmap.close();
|
|
1277
|
+
const blob = await new Promise(
|
|
1278
|
+
(resolve) => canvas.toBlob(resolve, file.type, quality)
|
|
1279
|
+
);
|
|
1280
|
+
if (!blob || blob.size >= file.size) return file;
|
|
1281
|
+
return new File([blob], file.name, { type: file.type });
|
|
1282
|
+
} catch {
|
|
1283
|
+
return file;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// src/react/slots.ts
|
|
1288
|
+
var matchSlotByExtension = (filled) => (slots, fileName) => {
|
|
1289
|
+
const extension = getFileExtension(fileName);
|
|
1290
|
+
const candidates = slots.filter((slot) => slot.extensions.includes(extension));
|
|
1291
|
+
if (candidates.length === 0) return null;
|
|
1292
|
+
return (candidates.find((slot) => !filled.has(slot.id)) ?? candidates[0]).id;
|
|
1293
|
+
};
|
|
1294
|
+
var matchSlotByName = (slots, fileName) => {
|
|
1295
|
+
const base = fileName.toLowerCase();
|
|
1296
|
+
return slots.find((slot) => base.startsWith(slot.id.toLowerCase()))?.id ?? null;
|
|
1297
|
+
};
|
|
1298
|
+
var slotFileName = (slotId, originalName) => `${slotId}.${getFileExtension(originalName)}`;
|
|
1299
|
+
|
|
1300
|
+
// src/validation.ts
|
|
1301
|
+
var ok = { valid: true };
|
|
1302
|
+
var fail = (code, message) => ({ valid: false, code, message });
|
|
1303
|
+
var validateExtension = (fileName, allowed, labels) => {
|
|
1304
|
+
if (allowed.length === 0) return ok;
|
|
1305
|
+
const copy = resolveLabels(labels);
|
|
1306
|
+
const extension = getFileExtension(fileName);
|
|
1307
|
+
if (!extension) {
|
|
1308
|
+
return fail("extension-not-allowed", copy.fileHasNoExtension);
|
|
1309
|
+
}
|
|
1310
|
+
if (!allowed.includes(extension)) {
|
|
1311
|
+
return fail("extension-not-allowed", copy.formatNotAllowed(allowed));
|
|
1312
|
+
}
|
|
1313
|
+
return ok;
|
|
1314
|
+
};
|
|
1315
|
+
var validateSize = (size, maxBytes, labels) => {
|
|
1316
|
+
const copy = resolveLabels(labels);
|
|
1317
|
+
if (size === 0) return fail("empty-file", copy.fileIsEmpty);
|
|
1318
|
+
if (size > maxBytes) {
|
|
1319
|
+
return fail(
|
|
1320
|
+
"too-large",
|
|
1321
|
+
copy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
return ok;
|
|
1325
|
+
};
|
|
1326
|
+
var validateMagicNumbers = async (file, labels) => {
|
|
1327
|
+
const copy = resolveLabels(labels);
|
|
1328
|
+
const extension = getFileExtension(file.name);
|
|
1329
|
+
if (!isKnownExtension(extension)) return ok;
|
|
1330
|
+
const expected = MAGIC_NUMBERS[extension];
|
|
1331
|
+
if (!expected) return ok;
|
|
1332
|
+
const head = new Uint8Array(await file.arrayBuffer()).slice(
|
|
1333
|
+
0,
|
|
1334
|
+
expected.length
|
|
1335
|
+
);
|
|
1336
|
+
if (head.length < expected.length) {
|
|
1337
|
+
return fail("magic-number-mismatch", copy.fileIncomplete);
|
|
1338
|
+
}
|
|
1339
|
+
const matches = expected.every((byte, index) => head[index] === byte);
|
|
1340
|
+
return matches ? ok : fail("magic-number-mismatch", copy.contentDoesNotMatchExtension);
|
|
1341
|
+
};
|
|
1342
|
+
var validateFile = async (file, options = {}) => {
|
|
1343
|
+
const {
|
|
1344
|
+
maxBytes = Infinity,
|
|
1345
|
+
allowedExtensions = [],
|
|
1346
|
+
validateMagicNumbers: checkMagic = false,
|
|
1347
|
+
labels,
|
|
1348
|
+
customValidation
|
|
1349
|
+
} = options;
|
|
1350
|
+
const size = validateSize(file.size, maxBytes, labels);
|
|
1351
|
+
if (!size.valid) return size;
|
|
1352
|
+
const extension = validateExtension(file.name, allowedExtensions, labels);
|
|
1353
|
+
if (!extension.valid) return extension;
|
|
1354
|
+
if (checkMagic) {
|
|
1355
|
+
const magic = await validateMagicNumbers(file, labels);
|
|
1356
|
+
if (!magic.valid) return magic;
|
|
1357
|
+
}
|
|
1358
|
+
if (customValidation) return customValidation(file);
|
|
1359
|
+
return ok;
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/scopes.ts
|
|
1363
|
+
var ScopeError = class extends Error {
|
|
1364
|
+
name = "ScopeError";
|
|
1365
|
+
};
|
|
1366
|
+
var PROBE_ENTITY = "__entity__";
|
|
1367
|
+
var probeFile = (name) => ({
|
|
1368
|
+
name,
|
|
1369
|
+
size: 0,
|
|
1370
|
+
type: "",
|
|
1371
|
+
arrayBuffer: async () => new ArrayBuffer(0)
|
|
1372
|
+
});
|
|
1373
|
+
var hasStableKey = (scope) => scope.path(PROBE_ENTITY, probeFile("a.png")) === scope.path(PROBE_ENTITY, probeFile("b.png"));
|
|
1374
|
+
var resolveReplaceMode = (scope) => {
|
|
1375
|
+
if (scope.replace !== void 0) return scope.replace;
|
|
1376
|
+
if ((scope.maxFiles ?? 1) > 1) return "key";
|
|
1377
|
+
return hasStableKey(scope) ? "key" : "entity";
|
|
1378
|
+
};
|
|
1379
|
+
var validateForScope = async (registry, name, file, labels) => {
|
|
1380
|
+
const scope = registry.get(name);
|
|
1381
|
+
const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
|
|
1382
|
+
return validateFile(file, {
|
|
1383
|
+
maxBytes: scope.maxBytes,
|
|
1384
|
+
allowedExtensions: scope.accept,
|
|
1385
|
+
validateMagicNumbers: category?.validateMagicNumbers ?? true,
|
|
1386
|
+
labels
|
|
1387
|
+
});
|
|
1388
|
+
};
|
|
1389
|
+
var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
1390
|
+
var previewOf = (file) => file.type.startsWith("image/") && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
1391
|
+
var revokePreview = (file) => {
|
|
1392
|
+
if (file.preview) URL.revokeObjectURL(file.preview);
|
|
1393
|
+
};
|
|
1394
|
+
var normalizeRetry = (retry) => {
|
|
1395
|
+
const options = typeof retry === "number" ? { attempts: retry } : retry ?? {};
|
|
1396
|
+
return {
|
|
1397
|
+
attempts: Math.max(1, options.attempts ?? 1),
|
|
1398
|
+
backoffMs: options.backoffMs ?? 500
|
|
1399
|
+
};
|
|
1400
|
+
};
|
|
1401
|
+
var wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
1402
|
+
const abort = () => {
|
|
1403
|
+
clearTimeout(timer);
|
|
1404
|
+
const error = new Error("Upload cancelled");
|
|
1405
|
+
error.name = "AbortError";
|
|
1406
|
+
reject(error);
|
|
1407
|
+
};
|
|
1408
|
+
const timer = setTimeout(() => {
|
|
1409
|
+
signal.removeEventListener("abort", abort);
|
|
1410
|
+
resolve();
|
|
1411
|
+
}, ms);
|
|
1412
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1413
|
+
});
|
|
1414
|
+
var runPool = async (items, limit, run) => {
|
|
1415
|
+
const results = new Array(items.length);
|
|
1416
|
+
let cursor = 0;
|
|
1417
|
+
const workers = Array.from(
|
|
1418
|
+
{ length: Math.min(Math.max(1, limit), items.length) },
|
|
1419
|
+
async () => {
|
|
1420
|
+
while (cursor < items.length) {
|
|
1421
|
+
const index = cursor++;
|
|
1422
|
+
results[index] = await run(items[index]);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
);
|
|
1426
|
+
await Promise.all(workers);
|
|
1427
|
+
return results;
|
|
1428
|
+
};
|
|
1429
|
+
var useUploader = ({
|
|
1430
|
+
scopes,
|
|
1431
|
+
scope,
|
|
1432
|
+
entityId,
|
|
1433
|
+
strategy,
|
|
1434
|
+
multiple: multipleOption,
|
|
1435
|
+
maxFiles: maxFilesOption,
|
|
1436
|
+
uploadOn = "manual",
|
|
1437
|
+
onUploadStart,
|
|
1438
|
+
onUploaded,
|
|
1439
|
+
onError,
|
|
1440
|
+
retry,
|
|
1441
|
+
concurrency = Infinity,
|
|
1442
|
+
rename,
|
|
1443
|
+
labels
|
|
1444
|
+
}) => {
|
|
1445
|
+
const scopeMaxFiles = scopes.get(scope).maxFiles;
|
|
1446
|
+
const maxFiles = maxFilesOption ?? scopeMaxFiles;
|
|
1447
|
+
const multiple = multipleOption ?? (maxFiles !== void 0 && maxFiles > 1);
|
|
1448
|
+
const [files, setFiles] = react.useState([]);
|
|
1449
|
+
const controllers = react.useRef(/* @__PURE__ */ new Map());
|
|
1450
|
+
const filesRef = react.useRef([]);
|
|
1451
|
+
filesRef.current = files;
|
|
1452
|
+
const copy = useUploaderLabels(labels);
|
|
1453
|
+
const { attempts, backoffMs } = normalizeRetry(retry);
|
|
1454
|
+
const patch = react.useCallback((id, changes) => {
|
|
1455
|
+
setFiles(
|
|
1456
|
+
(previous) => previous.map((file) => file.id === id ? { ...file, ...changes } : file)
|
|
1457
|
+
);
|
|
1458
|
+
}, []);
|
|
1459
|
+
const uploadOne = react.useCallback(
|
|
1460
|
+
async (target) => {
|
|
1461
|
+
if (!strategy) {
|
|
1462
|
+
warnDev(
|
|
1463
|
+
"upload-without-strategy",
|
|
1464
|
+
"upload() ran without a `strategy` \u2014 files stay local. Pass `strategy` (e.g. createXhrUploadStrategy) to useUploader."
|
|
1465
|
+
);
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
const config = scopes.get(scope);
|
|
1469
|
+
const controller = new AbortController();
|
|
1470
|
+
controllers.current.set(target.id, controller);
|
|
1471
|
+
patch(target.id, { status: "uploading", progress: 0 });
|
|
1472
|
+
try {
|
|
1473
|
+
const payload = config.compress ? await compressImage(target.file, config.compress) : target.file;
|
|
1474
|
+
for (let attempt = 1; ; attempt++) {
|
|
1475
|
+
try {
|
|
1476
|
+
const stored = await strategy(payload, scope, entityId, {
|
|
1477
|
+
onProgress: (percent) => patch(target.id, { progress: percent }),
|
|
1478
|
+
signal: controller.signal
|
|
1479
|
+
});
|
|
1480
|
+
patch(target.id, { status: "success", progress: 100, stored });
|
|
1481
|
+
return stored;
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
1484
|
+
if (aborted || attempt >= attempts) throw error;
|
|
1485
|
+
patch(target.id, { progress: 0 });
|
|
1486
|
+
await wait(backoffMs * 2 ** (attempt - 1), controller.signal);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
1491
|
+
const message = error instanceof Error && error.message ? error.message : copy.uploadFailed;
|
|
1492
|
+
patch(target.id, {
|
|
1493
|
+
status: aborted ? "idle" : "error",
|
|
1494
|
+
progress: 0,
|
|
1495
|
+
...aborted ? {} : { error: message }
|
|
1496
|
+
});
|
|
1497
|
+
if (!aborted) onError?.(message);
|
|
1498
|
+
return null;
|
|
1499
|
+
} finally {
|
|
1500
|
+
controllers.current.delete(target.id);
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
[
|
|
1504
|
+
strategy,
|
|
1505
|
+
scopes,
|
|
1506
|
+
scope,
|
|
1507
|
+
entityId,
|
|
1508
|
+
patch,
|
|
1509
|
+
onError,
|
|
1510
|
+
attempts,
|
|
1511
|
+
backoffMs,
|
|
1512
|
+
copy
|
|
1513
|
+
]
|
|
1514
|
+
);
|
|
1515
|
+
const uploadBatch = react.useCallback(
|
|
1516
|
+
async (targets) => {
|
|
1517
|
+
if (targets.length === 0) return [];
|
|
1518
|
+
onUploadStart?.(targets);
|
|
1519
|
+
const results = await runPool(targets, concurrency, uploadOne);
|
|
1520
|
+
const stored = results.filter((r) => r !== null);
|
|
1521
|
+
if (stored.length > 0) onUploaded?.(stored);
|
|
1522
|
+
return stored;
|
|
1523
|
+
},
|
|
1524
|
+
[uploadOne, onUploadStart, onUploaded, concurrency]
|
|
1525
|
+
);
|
|
1526
|
+
const addFiles = react.useCallback(
|
|
1527
|
+
async (incoming) => {
|
|
1528
|
+
let batch = Array.from(incoming);
|
|
1529
|
+
if (!multiple && batch.length > 1) {
|
|
1530
|
+
warnDev(
|
|
1531
|
+
"single-mode-multi-drop",
|
|
1532
|
+
`addFiles received ${batch.length} files but \`multiple\` is false \u2014 only the first is kept. Pass multiple: true if that is not what you meant.`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (!multiple) batch = batch.slice(0, 1);
|
|
1536
|
+
if (maxFiles !== void 0) {
|
|
1537
|
+
const room = maxFiles - (multiple ? filesRef.current.length : 0);
|
|
1538
|
+
if (batch.length > room) {
|
|
1539
|
+
batch = batch.slice(0, Math.max(0, room));
|
|
1540
|
+
onError?.(copy.maxFilesReached(maxFiles));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (batch.length === 0) return;
|
|
1544
|
+
if (rename) {
|
|
1545
|
+
batch = batch.map(
|
|
1546
|
+
(file) => new File([file], rename(file), { type: file.type })
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
const next = [];
|
|
1550
|
+
for (const file of batch) {
|
|
1551
|
+
const result = await validateForScope(scopes, scope, file, copy);
|
|
1552
|
+
next.push({
|
|
1553
|
+
id: createId(),
|
|
1554
|
+
file,
|
|
1555
|
+
status: result.valid ? "idle" : "error",
|
|
1556
|
+
progress: 0,
|
|
1557
|
+
...result.valid ? { preview: previewOf(file) } : { error: result.message }
|
|
1558
|
+
});
|
|
1559
|
+
if (!result.valid) onError?.(next[next.length - 1].error);
|
|
1560
|
+
}
|
|
1561
|
+
setFiles((previous) => {
|
|
1562
|
+
if (multiple) return [...previous, ...next];
|
|
1563
|
+
previous.forEach(revokePreview);
|
|
1564
|
+
return next;
|
|
1565
|
+
});
|
|
1566
|
+
if (uploadOn === "select") {
|
|
1567
|
+
await uploadBatch(next.filter((file) => file.status === "idle"));
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
[
|
|
1571
|
+
multiple,
|
|
1572
|
+
maxFiles,
|
|
1573
|
+
scopes,
|
|
1574
|
+
scope,
|
|
1575
|
+
uploadOn,
|
|
1576
|
+
uploadBatch,
|
|
1577
|
+
onError,
|
|
1578
|
+
rename,
|
|
1579
|
+
copy
|
|
1580
|
+
]
|
|
1581
|
+
);
|
|
1582
|
+
const upload = react.useCallback(
|
|
1583
|
+
() => uploadBatch(filesRef.current.filter((f) => f.status === "idle")),
|
|
1584
|
+
[uploadBatch]
|
|
1585
|
+
);
|
|
1586
|
+
const abort = react.useCallback((id) => {
|
|
1587
|
+
if (id) {
|
|
1588
|
+
controllers.current.get(id)?.abort();
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
1592
|
+
}, []);
|
|
1593
|
+
const removeFile = react.useCallback(
|
|
1594
|
+
(id) => {
|
|
1595
|
+
abort(id);
|
|
1596
|
+
setFiles((previous) => {
|
|
1597
|
+
const target = previous.find((file) => file.id === id);
|
|
1598
|
+
if (target) revokePreview(target);
|
|
1599
|
+
return previous.filter((file) => file.id !== id);
|
|
1600
|
+
});
|
|
1601
|
+
},
|
|
1602
|
+
[abort]
|
|
1603
|
+
);
|
|
1604
|
+
const clear = react.useCallback(() => {
|
|
1605
|
+
abort();
|
|
1606
|
+
setFiles((previous) => {
|
|
1607
|
+
previous.forEach(revokePreview);
|
|
1608
|
+
return [];
|
|
1609
|
+
});
|
|
1610
|
+
}, [abort]);
|
|
1611
|
+
react.useEffect(
|
|
1612
|
+
() => () => {
|
|
1613
|
+
filesRef.current.forEach(revokePreview);
|
|
1614
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
1615
|
+
},
|
|
1616
|
+
[]
|
|
1617
|
+
);
|
|
1618
|
+
const accept = react.useMemo(() => scopes.accept(scope), [scopes, scope]);
|
|
1619
|
+
return {
|
|
1620
|
+
files,
|
|
1621
|
+
accept,
|
|
1622
|
+
isUploading: files.some((file) => file.status === "uploading"),
|
|
1623
|
+
hasPending: files.some((file) => file.status === "idle"),
|
|
1624
|
+
addFiles,
|
|
1625
|
+
upload,
|
|
1626
|
+
abort,
|
|
1627
|
+
removeFile,
|
|
1628
|
+
clear
|
|
1629
|
+
};
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
// src/react/useSlottedUploader.ts
|
|
1633
|
+
var useSlottedUploader = ({
|
|
1634
|
+
scopes,
|
|
1635
|
+
scope,
|
|
1636
|
+
entityId,
|
|
1637
|
+
strategy,
|
|
1638
|
+
slots,
|
|
1639
|
+
value,
|
|
1640
|
+
onChange,
|
|
1641
|
+
matchBy = "extension",
|
|
1642
|
+
match,
|
|
1643
|
+
uploadOn = "select",
|
|
1644
|
+
onUploadStart,
|
|
1645
|
+
removeStrategy,
|
|
1646
|
+
onRemoveStored,
|
|
1647
|
+
onError,
|
|
1648
|
+
retry,
|
|
1649
|
+
concurrency,
|
|
1650
|
+
labels
|
|
1651
|
+
}) => {
|
|
1652
|
+
const copy = useUploaderLabels(labels);
|
|
1653
|
+
react.useMemo(() => {
|
|
1654
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1655
|
+
for (const slot of slots) {
|
|
1656
|
+
if (ids.has(slot.id)) {
|
|
1657
|
+
throw new ScopeError(
|
|
1658
|
+
`useSlottedUploader: duplicate slot id "${slot.id}" \u2014 slot ids are storage keys and must be unique`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
ids.add(slot.id);
|
|
1662
|
+
}
|
|
1663
|
+
if (slots.length > 1 && resolveReplaceMode(scopes.get(scope)) === "entity") {
|
|
1664
|
+
throw new ScopeError(
|
|
1665
|
+
`useSlottedUploader: scope "${scope}" replaces per entity, so filling one of its ${slots.length} slots would delete the others. Declare "maxFiles" on the scope (one per slot, at least), or set replace: 'key'.`
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
const accepted = scopes.get(scope).accept;
|
|
1669
|
+
for (const slot of slots) {
|
|
1670
|
+
const outside = slot.extensions.filter(
|
|
1671
|
+
(extension) => !accepted.includes(extension)
|
|
1672
|
+
);
|
|
1673
|
+
if (outside.length > 0) {
|
|
1674
|
+
warnDev(
|
|
1675
|
+
`slot-outside-scope:${slot.id}`,
|
|
1676
|
+
`slot "${slot.id}" accepts ${outside.join(", ")} but scope "${scope}" does not \u2014 those uploads will be rejected by validation. Align the slot's extensions with the scope's accept list.`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}, [slots, scopes, scope]);
|
|
1681
|
+
const valueRef = react.useRef(value);
|
|
1682
|
+
valueRef.current = value;
|
|
1683
|
+
const onChangeRef = react.useRef(onChange);
|
|
1684
|
+
onChangeRef.current = onChange;
|
|
1685
|
+
const landedRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1686
|
+
for (const [slot, item] of landedRef.current) {
|
|
1687
|
+
const absorbed = value.some(
|
|
1688
|
+
(entry) => entry.slot === slot && entry.stored.key === item.stored.key && entry.stored.uploadedAt === item.stored.uploadedAt
|
|
1689
|
+
);
|
|
1690
|
+
if (absorbed) landedRef.current.delete(slot);
|
|
1691
|
+
}
|
|
1692
|
+
const uploader = useUploader({
|
|
1693
|
+
scopes,
|
|
1694
|
+
scope,
|
|
1695
|
+
entityId,
|
|
1696
|
+
strategy,
|
|
1697
|
+
multiple: true,
|
|
1698
|
+
uploadOn,
|
|
1699
|
+
onUploadStart,
|
|
1700
|
+
onError,
|
|
1701
|
+
...retry !== void 0 ? { retry } : {},
|
|
1702
|
+
...concurrency !== void 0 ? { concurrency } : {},
|
|
1703
|
+
...labels ? { labels } : {},
|
|
1704
|
+
onUploaded: (stored) => {
|
|
1705
|
+
for (const item of stored) {
|
|
1706
|
+
const slot = slots.find((s) => item.fileName.startsWith(`${s.id}.`));
|
|
1707
|
+
if (slot)
|
|
1708
|
+
landedRef.current.set(slot.id, { slot: slot.id, stored: item });
|
|
1709
|
+
}
|
|
1710
|
+
const merged = new Map(valueRef.current.map((entry) => [entry.slot, entry]));
|
|
1711
|
+
landedRef.current.forEach((entry, slot) => merged.set(slot, entry));
|
|
1712
|
+
void onChangeRef.current([...merged.values()]);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
const resolveSlot = react.useMemo(() => {
|
|
1716
|
+
if (match) return match;
|
|
1717
|
+
if (matchBy === "name") return matchSlotByName;
|
|
1718
|
+
return (all, fileName) => matchSlotByExtension(new Set(valueRef.current.map((item) => item.slot)))(
|
|
1719
|
+
all,
|
|
1720
|
+
fileName
|
|
1721
|
+
);
|
|
1722
|
+
}, [match, matchBy]);
|
|
1723
|
+
const assign = react.useCallback(
|
|
1724
|
+
(slotId, file) => {
|
|
1725
|
+
const slot = slots.find((s) => s.id === slotId);
|
|
1726
|
+
if (!slot) return null;
|
|
1727
|
+
const extension = getFileExtension(file.name);
|
|
1728
|
+
if (!slot.extensions.includes(extension)) {
|
|
1729
|
+
onError?.(copy.slotFormatNotAllowed(slot.label, slot.extensions));
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
return new File([file], slotFileName(slotId, file.name), {
|
|
1733
|
+
type: file.type
|
|
1734
|
+
});
|
|
1735
|
+
},
|
|
1736
|
+
[slots, onError, copy]
|
|
1737
|
+
);
|
|
1738
|
+
const addToSlot = react.useCallback(
|
|
1739
|
+
async (slotId, file) => {
|
|
1740
|
+
const renamed = assign(slotId, file);
|
|
1741
|
+
if (renamed) await uploader.addFiles([renamed]);
|
|
1742
|
+
},
|
|
1743
|
+
[assign, uploader]
|
|
1744
|
+
);
|
|
1745
|
+
const addFiles = react.useCallback(
|
|
1746
|
+
async (incoming) => {
|
|
1747
|
+
const batch = [];
|
|
1748
|
+
for (const file of Array.from(incoming)) {
|
|
1749
|
+
const slotId = resolveSlot(slots, file.name);
|
|
1750
|
+
if (!slotId) {
|
|
1751
|
+
onError?.(copy.fileWithoutSlot(file.name));
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const renamed = assign(slotId, file);
|
|
1755
|
+
if (renamed) batch.push(renamed);
|
|
1756
|
+
}
|
|
1757
|
+
if (batch.length > 0) await uploader.addFiles(batch);
|
|
1758
|
+
},
|
|
1759
|
+
[slots, resolveSlot, assign, uploader, onError, copy]
|
|
1760
|
+
);
|
|
1761
|
+
const removeSlot = react.useCallback(
|
|
1762
|
+
(slotId) => {
|
|
1763
|
+
uploader.files.filter((file) => file.file.name.startsWith(`${slotId}.`)).forEach((file) => uploader.removeFile(file.id));
|
|
1764
|
+
landedRef.current.delete(slotId);
|
|
1765
|
+
const forgotten = valueRef.current.find((item) => item.slot === slotId);
|
|
1766
|
+
if (forgotten) {
|
|
1767
|
+
if (removeStrategy && !scopes.get(scope).keepOnRemove) {
|
|
1768
|
+
removeStrategy(forgotten.stored, scope, entityId).then((deleted) => {
|
|
1769
|
+
if (!deleted) onError?.(copy.removeFailed);
|
|
1770
|
+
}).catch(() => onError?.(copy.removeFailed));
|
|
1771
|
+
}
|
|
1772
|
+
onRemoveStored?.(forgotten);
|
|
1773
|
+
}
|
|
1774
|
+
void onChangeRef.current(
|
|
1775
|
+
valueRef.current.filter((item) => item.slot !== slotId)
|
|
1776
|
+
);
|
|
1777
|
+
},
|
|
1778
|
+
[
|
|
1779
|
+
uploader,
|
|
1780
|
+
removeStrategy,
|
|
1781
|
+
scopes,
|
|
1782
|
+
scope,
|
|
1783
|
+
entityId,
|
|
1784
|
+
onRemoveStored,
|
|
1785
|
+
onError,
|
|
1786
|
+
copy
|
|
1787
|
+
]
|
|
1788
|
+
);
|
|
1789
|
+
const abort = react.useCallback(
|
|
1790
|
+
(slotId) => {
|
|
1791
|
+
if (!slotId) {
|
|
1792
|
+
uploader.abort();
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
uploader.files.filter((file) => file.file.name.startsWith(`${slotId}.`)).forEach((file) => uploader.abort(file.id));
|
|
1796
|
+
},
|
|
1797
|
+
[uploader]
|
|
1798
|
+
);
|
|
1799
|
+
const slotStates = react.useMemo(
|
|
1800
|
+
() => slots.map((definition) => ({
|
|
1801
|
+
definition,
|
|
1802
|
+
filled: value.find((item) => item.slot === definition.id),
|
|
1803
|
+
pending: uploader.files.find(
|
|
1804
|
+
(file) => file.file.name.startsWith(`${definition.id}.`) && file.status !== "success"
|
|
1805
|
+
)
|
|
1806
|
+
})),
|
|
1807
|
+
[slots, value, uploader.files]
|
|
1808
|
+
);
|
|
1809
|
+
const accept = react.useMemo(
|
|
1810
|
+
() => toAcceptAttribute([...new Set(slots.flatMap((s) => s.extensions))]),
|
|
1811
|
+
[slots]
|
|
1812
|
+
);
|
|
1813
|
+
const upload = react.useCallback(async () => {
|
|
1814
|
+
await uploader.upload();
|
|
1815
|
+
}, [uploader]);
|
|
1816
|
+
return {
|
|
1817
|
+
slots: slotStates,
|
|
1818
|
+
accept,
|
|
1819
|
+
isUploading: uploader.isUploading,
|
|
1820
|
+
hasPending: uploader.hasPending,
|
|
1821
|
+
addFiles,
|
|
1822
|
+
addToSlot,
|
|
1823
|
+
removeSlot,
|
|
1824
|
+
abort,
|
|
1825
|
+
upload
|
|
1826
|
+
};
|
|
1827
|
+
};
|
|
1828
|
+
var SlotRow = ({
|
|
1829
|
+
slot,
|
|
1830
|
+
onPick,
|
|
1831
|
+
onRemove,
|
|
1832
|
+
onAbort,
|
|
1833
|
+
onView,
|
|
1834
|
+
size = "md",
|
|
1835
|
+
labels
|
|
1836
|
+
}) => {
|
|
1837
|
+
const copy = useUploaderLabels(labels);
|
|
1838
|
+
const inputRef = react.useRef(null);
|
|
1839
|
+
const [dropping, setDropping] = react.useState(false);
|
|
1840
|
+
const depth = react.useRef(0);
|
|
1841
|
+
const { definition, filled, pending } = slot;
|
|
1842
|
+
const uploading = pending?.status === "uploading";
|
|
1843
|
+
const staged = pending?.status === "idle";
|
|
1844
|
+
const occupied = Boolean(filled) || staged;
|
|
1845
|
+
const droppable = !uploading;
|
|
1846
|
+
const onDrop = (event) => {
|
|
1847
|
+
event.preventDefault();
|
|
1848
|
+
event.stopPropagation();
|
|
1849
|
+
depth.current = 0;
|
|
1850
|
+
setDropping(false);
|
|
1851
|
+
if (!droppable) return;
|
|
1852
|
+
const file = event.dataTransfer.files[0];
|
|
1853
|
+
if (file) onPick(file);
|
|
1854
|
+
};
|
|
1855
|
+
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";
|
|
1856
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1857
|
+
"div",
|
|
1858
|
+
{
|
|
1859
|
+
onDragEnter: () => {
|
|
1860
|
+
depth.current += 1;
|
|
1861
|
+
if (droppable) setDropping(true);
|
|
1862
|
+
},
|
|
1863
|
+
onDragOver: (event) => {
|
|
1864
|
+
event.preventDefault();
|
|
1865
|
+
event.stopPropagation();
|
|
1866
|
+
},
|
|
1867
|
+
onDragLeave: () => {
|
|
1868
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
1869
|
+
if (depth.current === 0) setDropping(false);
|
|
1870
|
+
},
|
|
1871
|
+
onDrop,
|
|
1872
|
+
className: cn(
|
|
1873
|
+
"rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm transition-all duration-200",
|
|
1874
|
+
size === "sm" ? "p-1.5" : "p-2",
|
|
1875
|
+
dropping ? "border-ui-primary bg-ui-primary-soft ring-ui-ring scale-[1.01] ring-2" : "border-ui-border"
|
|
1876
|
+
),
|
|
1877
|
+
children: [
|
|
1878
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1879
|
+
"span",
|
|
1880
|
+
{
|
|
1881
|
+
className: cn(
|
|
1882
|
+
"h-2.5 w-2.5 shrink-0 rounded-full transition-colors",
|
|
1883
|
+
pending?.status === "error" ? "bg-ui-danger" : uploading ? "bg-ui-primary animate-pulse" : staged ? "bg-ui-warning" : filled ? "bg-ui-success" : "bg-ui-border-strong"
|
|
1884
|
+
)
|
|
1885
|
+
}
|
|
1886
|
+
),
|
|
1887
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1888
|
+
FileTypeBadge,
|
|
1889
|
+
{
|
|
1890
|
+
fileName: pending?.file.name ?? filled?.stored.fileName,
|
|
1891
|
+
preview: pending?.preview,
|
|
1892
|
+
size
|
|
1893
|
+
}
|
|
1894
|
+
),
|
|
1895
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1896
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg truncate font-medium", children: definition.label }),
|
|
1897
|
+
pending?.status === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-danger text-xs", children: pending.error }) : pending ? (
|
|
1898
|
+
// The name is the STORAGE one: `assign` renames the pick to
|
|
1899
|
+
// `{slot}.{ext}` before it reaches the machine.
|
|
1900
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-ui-muted truncate text-xs", children: [
|
|
1901
|
+
pending.file.name,
|
|
1902
|
+
" \xB7 ",
|
|
1903
|
+
formatFileSize(pending.file.size),
|
|
1904
|
+
staged && ` \xB7 ${copy.staged}`
|
|
1905
|
+
] })
|
|
1906
|
+
) : filled ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1907
|
+
"button",
|
|
1908
|
+
{
|
|
1909
|
+
type: "button",
|
|
1910
|
+
onClick: onView,
|
|
1911
|
+
className: "text-ui-muted hover:text-ui-primary block max-w-full cursor-pointer truncate text-left text-xs hover:underline",
|
|
1912
|
+
children: filled.stored.fileName
|
|
1913
|
+
}
|
|
1914
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-placeholder text-xs", children: definition.hint ?? definition.extensions.map((e) => `.${e}`).join(" \xB7 ") }),
|
|
1915
|
+
uploading && /* @__PURE__ */ jsxRuntime.jsx(ProgressBar, { percent: pending.progress })
|
|
1916
|
+
] }),
|
|
1917
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1918
|
+
"input",
|
|
1919
|
+
{
|
|
1920
|
+
ref: inputRef,
|
|
1921
|
+
type: "file",
|
|
1922
|
+
accept: definition.extensions.map((e) => `.${e}`).join(","),
|
|
1923
|
+
className: "hidden",
|
|
1924
|
+
onChange: (event) => {
|
|
1925
|
+
const file = event.target.files?.[0];
|
|
1926
|
+
if (file) onPick(file);
|
|
1927
|
+
event.target.value = "";
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
),
|
|
1931
|
+
dropping && /* @__PURE__ */ jsxRuntime.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 }),
|
|
1932
|
+
!dropping && filled && !uploading && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1933
|
+
"button",
|
|
1934
|
+
{
|
|
1935
|
+
type: "button",
|
|
1936
|
+
onClick: onView,
|
|
1937
|
+
className: cn(action, "text-ui-primary hover:bg-ui-primary-soft"),
|
|
1938
|
+
children: copy.view
|
|
1939
|
+
}
|
|
1940
|
+
),
|
|
1941
|
+
uploading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1942
|
+
"button",
|
|
1943
|
+
{
|
|
1944
|
+
type: "button",
|
|
1945
|
+
onClick: onAbort,
|
|
1946
|
+
className: cn(action, "text-ui-muted hover:bg-ui-surface-muted"),
|
|
1947
|
+
children: copy.cancel
|
|
1948
|
+
}
|
|
1949
|
+
) : dropping ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1950
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1951
|
+
"button",
|
|
1952
|
+
{
|
|
1953
|
+
type: "button",
|
|
1954
|
+
onClick: () => inputRef.current?.click(),
|
|
1955
|
+
className: cn(action, "text-ui-primary hover:bg-ui-primary-soft"),
|
|
1956
|
+
children: occupied ? copy.replace : copy.upload
|
|
1957
|
+
}
|
|
1958
|
+
),
|
|
1959
|
+
occupied && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1960
|
+
"button",
|
|
1961
|
+
{
|
|
1962
|
+
type: "button",
|
|
1963
|
+
onClick: onRemove,
|
|
1964
|
+
className: cn(action, "text-ui-muted hover:bg-ui-surface-muted"),
|
|
1965
|
+
children: copy.remove
|
|
1966
|
+
}
|
|
1967
|
+
)
|
|
1968
|
+
] })
|
|
1969
|
+
]
|
|
1970
|
+
}
|
|
1971
|
+
);
|
|
1972
|
+
};
|
|
1973
|
+
var useFileViewer = ({
|
|
1974
|
+
resolveUrl
|
|
1975
|
+
} = {}) => {
|
|
1976
|
+
const [viewing, setViewing] = react.useState(null);
|
|
1977
|
+
const close = react.useCallback(() => setViewing(null), []);
|
|
1978
|
+
const open = react.useCallback((file) => setViewing(file), []);
|
|
1979
|
+
return {
|
|
1980
|
+
viewing,
|
|
1981
|
+
open,
|
|
1982
|
+
close,
|
|
1983
|
+
viewerProps: react.useMemo(
|
|
1984
|
+
() => ({ file: viewing, onClose: close, resolveUrl }),
|
|
1985
|
+
[viewing, close, resolveUrl]
|
|
1986
|
+
)
|
|
1987
|
+
};
|
|
1988
|
+
};
|
|
1989
|
+
var SlottedUploader = ({
|
|
1990
|
+
uploadOn = "select",
|
|
1991
|
+
title,
|
|
1992
|
+
description,
|
|
1993
|
+
hideDropzone = false,
|
|
1994
|
+
confirmRemove,
|
|
1995
|
+
confirmReplace,
|
|
1996
|
+
removeStrategy,
|
|
1997
|
+
resolveViewUrl,
|
|
1998
|
+
size = "md",
|
|
1999
|
+
icon,
|
|
2000
|
+
shortcut,
|
|
2001
|
+
controllerRef,
|
|
2002
|
+
onPendingChange,
|
|
2003
|
+
className,
|
|
2004
|
+
...options
|
|
2005
|
+
}) => {
|
|
2006
|
+
const slotted = useSlottedUploader({
|
|
2007
|
+
...options,
|
|
2008
|
+
uploadOn: uploadOn === "select" ? "select" : "manual"
|
|
2009
|
+
});
|
|
2010
|
+
if (uploadOn === "submit" && !controllerRef) {
|
|
2011
|
+
warnDev(
|
|
2012
|
+
"slotted-submit-without-controller",
|
|
2013
|
+
"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."
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
const copy = useUploaderLabels(options.labels);
|
|
2017
|
+
const coarse = useCoarsePointer();
|
|
2018
|
+
const viewer = useFileViewer({ resolveUrl: resolveViewUrl });
|
|
2019
|
+
const [pending, setPending] = react.useState(null);
|
|
2020
|
+
const { upload, hasPending, isUploading } = slotted;
|
|
2021
|
+
react.useEffect(() => {
|
|
2022
|
+
if (!controllerRef) return;
|
|
2023
|
+
controllerRef.current = { upload, hasPending, isUploading };
|
|
2024
|
+
return () => {
|
|
2025
|
+
controllerRef.current = null;
|
|
2026
|
+
};
|
|
2027
|
+
}, [controllerRef, upload, hasPending, isUploading]);
|
|
2028
|
+
react.useEffect(() => {
|
|
2029
|
+
onPendingChange?.(hasPending);
|
|
2030
|
+
}, [onPendingChange, hasPending]);
|
|
2031
|
+
const pick = (slot, file) => {
|
|
2032
|
+
if (slot.filled && confirmReplace) {
|
|
2033
|
+
setPending({ kind: "replace", slot, file });
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
void slotted.addToSlot(slot.definition.id, file);
|
|
2037
|
+
};
|
|
2038
|
+
const forgetSlot = (slot) => {
|
|
2039
|
+
const stored = slot.filled?.stored;
|
|
2040
|
+
if (stored && removeStrategy && !options.scopes.get(options.scope).keepOnRemove) {
|
|
2041
|
+
removeStrategy(stored, options.scope, options.entityId).then((deleted) => {
|
|
2042
|
+
if (!deleted) options.onError?.(copy.removeFailed);
|
|
2043
|
+
}).catch(() => options.onError?.(copy.removeFailed));
|
|
2044
|
+
}
|
|
2045
|
+
slotted.removeSlot(slot.definition.id);
|
|
2046
|
+
};
|
|
2047
|
+
const remove = (slot) => {
|
|
2048
|
+
if (slot.filled && confirmRemove) {
|
|
2049
|
+
setPending({ kind: "remove", slot });
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
forgetSlot(slot);
|
|
2053
|
+
};
|
|
2054
|
+
const override = pending?.kind === "replace" ? typeof confirmReplace === "object" ? confirmReplace : {} : typeof confirmRemove === "object" ? confirmRemove : {};
|
|
2055
|
+
const dialogTitle = override.title ?? (pending?.kind === "replace" ? copy.confirmReplaceTitle : copy.confirmRemoveTitle);
|
|
2056
|
+
const dialogMessage = override.message ?? (pending?.kind === "replace" ? copy.confirmReplaceMessage(
|
|
2057
|
+
pending.slot.filled?.stored.fileName ?? "",
|
|
2058
|
+
pending.file.name
|
|
2059
|
+
) : copy.confirmRemoveMessage(pending?.slot.filled?.stored.fileName ?? ""));
|
|
2060
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-2", className), children: [
|
|
2061
|
+
title && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg text-sm font-medium", children: title }),
|
|
2062
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-muted text-xs", children: description }),
|
|
2063
|
+
slotted.slots.map((slot) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2064
|
+
SlotRow,
|
|
2065
|
+
{
|
|
2066
|
+
slot,
|
|
2067
|
+
onPick: (file) => pick(slot, file),
|
|
2068
|
+
onRemove: () => remove(slot),
|
|
2069
|
+
onAbort: () => slotted.abort(slot.definition.id),
|
|
2070
|
+
onView: () => slot.filled && viewer.open(slot.filled.stored),
|
|
2071
|
+
size,
|
|
2072
|
+
labels: options.labels
|
|
2073
|
+
},
|
|
2074
|
+
slot.definition.id
|
|
2075
|
+
)),
|
|
2076
|
+
!hideDropzone && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2077
|
+
Dropzone,
|
|
2078
|
+
{
|
|
2079
|
+
accept: slotted.accept,
|
|
2080
|
+
multiple: true,
|
|
2081
|
+
disabled: slotted.isUploading,
|
|
2082
|
+
size,
|
|
2083
|
+
shortcut,
|
|
2084
|
+
onFiles: (files) => void slotted.addFiles(files),
|
|
2085
|
+
children: [
|
|
2086
|
+
icon !== null && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
|
|
2087
|
+
UploadCloudIcon,
|
|
2088
|
+
{
|
|
2089
|
+
className: size === "sm" ? "h-5 w-5" : "h-7 w-7"
|
|
2090
|
+
}
|
|
2091
|
+
) }),
|
|
2092
|
+
/* @__PURE__ */ jsxRuntime.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 })
|
|
2093
|
+
]
|
|
2094
|
+
}
|
|
2095
|
+
),
|
|
2096
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileViewer, { ...viewer.viewerProps, labels: options.labels }),
|
|
2097
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2098
|
+
ConfirmDialog,
|
|
2099
|
+
{
|
|
2100
|
+
open: pending !== null,
|
|
2101
|
+
title: dialogTitle,
|
|
2102
|
+
message: dialogMessage,
|
|
2103
|
+
onConfirm: () => {
|
|
2104
|
+
if (pending?.kind === "replace") {
|
|
2105
|
+
void slotted.addToSlot(pending.slot.definition.id, pending.file);
|
|
2106
|
+
} else if (pending) {
|
|
2107
|
+
forgetSlot(pending.slot);
|
|
2108
|
+
}
|
|
2109
|
+
setPending(null);
|
|
2110
|
+
},
|
|
2111
|
+
onCancel: () => setPending(null),
|
|
2112
|
+
labels: options.labels
|
|
2113
|
+
}
|
|
2114
|
+
)
|
|
2115
|
+
] });
|
|
2116
|
+
};
|
|
2117
|
+
var StoredFileItem = ({
|
|
2118
|
+
file,
|
|
2119
|
+
onView,
|
|
2120
|
+
onRemove,
|
|
2121
|
+
size = "md",
|
|
2122
|
+
labels
|
|
2123
|
+
}) => {
|
|
2124
|
+
const copy = useUploaderLabels(labels);
|
|
2125
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2126
|
+
"div",
|
|
2127
|
+
{
|
|
2128
|
+
className: cn(
|
|
2129
|
+
"border-ui-border bg-ui-surface-subtle rounded-ui animate-ui-fade-in flex items-center gap-3 border text-sm",
|
|
2130
|
+
size === "sm" ? "p-1.5" : "p-2"
|
|
2131
|
+
),
|
|
2132
|
+
children: [
|
|
2133
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileTypeBadge, { fileName: file.fileName, size }),
|
|
2134
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2135
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg truncate font-medium", children: file.fileName }),
|
|
2136
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-muted text-xs", children: formatFileSize(file.size) })
|
|
2137
|
+
] }),
|
|
2138
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2139
|
+
"button",
|
|
2140
|
+
{
|
|
2141
|
+
type: "button",
|
|
2142
|
+
onClick: onView,
|
|
2143
|
+
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",
|
|
2144
|
+
children: copy.view
|
|
2145
|
+
}
|
|
2146
|
+
),
|
|
2147
|
+
onRemove && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2148
|
+
"button",
|
|
2149
|
+
{
|
|
2150
|
+
type: "button",
|
|
2151
|
+
onClick: () => onRemove(file),
|
|
2152
|
+
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",
|
|
2153
|
+
children: copy.remove
|
|
2154
|
+
}
|
|
2155
|
+
)
|
|
2156
|
+
]
|
|
2157
|
+
}
|
|
2158
|
+
);
|
|
2159
|
+
};
|
|
2160
|
+
var Uploader = ({
|
|
2161
|
+
uploadOn = "select",
|
|
2162
|
+
label,
|
|
2163
|
+
description,
|
|
2164
|
+
stored = [],
|
|
2165
|
+
filesPosition = "above",
|
|
2166
|
+
renderFiles,
|
|
2167
|
+
onRemoveStored,
|
|
2168
|
+
confirmRemove,
|
|
2169
|
+
resolveViewUrl,
|
|
2170
|
+
capture,
|
|
2171
|
+
size = "md",
|
|
2172
|
+
icon,
|
|
2173
|
+
shortcut,
|
|
2174
|
+
controllerRef,
|
|
2175
|
+
onPendingChange,
|
|
2176
|
+
removeStrategy,
|
|
2177
|
+
disabled = false,
|
|
2178
|
+
className,
|
|
2179
|
+
...options
|
|
2180
|
+
}) => {
|
|
2181
|
+
const uploader = useUploader({
|
|
2182
|
+
...options,
|
|
2183
|
+
uploadOn: uploadOn === "select" ? "select" : "manual"
|
|
2184
|
+
});
|
|
2185
|
+
const copy = useUploaderLabels(options.labels);
|
|
2186
|
+
const viewer = useFileViewer({ resolveUrl: resolveViewUrl });
|
|
2187
|
+
const [removing, setRemoving] = react.useState(null);
|
|
2188
|
+
const { files, removeFile } = uploader;
|
|
2189
|
+
const manual = uploadOn === "manual";
|
|
2190
|
+
if (uploadOn === "submit" && !controllerRef) {
|
|
2191
|
+
warnDev(
|
|
2192
|
+
"uploader-submit-without-controller",
|
|
2193
|
+
"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."
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
const coarse = useCoarsePointer();
|
|
2197
|
+
react.useEffect(() => {
|
|
2198
|
+
if (stored.length === 0) return;
|
|
2199
|
+
const keys = new Set(stored.map((file) => file.key));
|
|
2200
|
+
files.filter((f) => f.status === "success" && f.stored && keys.has(f.stored.key)).forEach((f) => removeFile(f.id));
|
|
2201
|
+
}, [stored, files, removeFile]);
|
|
2202
|
+
const { upload, hasPending, isUploading } = uploader;
|
|
2203
|
+
react.useEffect(() => {
|
|
2204
|
+
if (!controllerRef) return;
|
|
2205
|
+
controllerRef.current = { upload, hasPending, isUploading };
|
|
2206
|
+
return () => {
|
|
2207
|
+
controllerRef.current = null;
|
|
2208
|
+
};
|
|
2209
|
+
}, [controllerRef, upload, hasPending, isUploading]);
|
|
2210
|
+
react.useEffect(() => {
|
|
2211
|
+
onPendingChange?.(hasPending);
|
|
2212
|
+
}, [onPendingChange, hasPending]);
|
|
2213
|
+
const forgetStored = (file) => {
|
|
2214
|
+
if (removeStrategy && !options.scopes.get(options.scope).keepOnRemove) {
|
|
2215
|
+
removeStrategy(file, options.scope, options.entityId).then((deleted) => {
|
|
2216
|
+
if (!deleted) options.onError?.(copy.removeFailed);
|
|
2217
|
+
}).catch(() => options.onError?.(copy.removeFailed));
|
|
2218
|
+
}
|
|
2219
|
+
onRemoveStored?.(file);
|
|
2220
|
+
};
|
|
2221
|
+
const requestRemove = onRemoveStored || removeStrategy ? (file) => confirmRemove ? setRemoving(file) : forgetStored(file) : void 0;
|
|
2222
|
+
const confirmCopy = typeof confirmRemove === "object" ? confirmRemove : {};
|
|
2223
|
+
const defaultRows = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2224
|
+
stored.map((file) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2225
|
+
StoredFileItem,
|
|
2226
|
+
{
|
|
2227
|
+
file,
|
|
2228
|
+
onView: () => viewer.open(file),
|
|
2229
|
+
onRemove: requestRemove,
|
|
2230
|
+
size,
|
|
2231
|
+
labels: options.labels
|
|
2232
|
+
},
|
|
2233
|
+
file.key
|
|
2234
|
+
)),
|
|
2235
|
+
files.map((file) => {
|
|
2236
|
+
const { stored: settled } = file;
|
|
2237
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2238
|
+
FileItem,
|
|
2239
|
+
{
|
|
2240
|
+
file,
|
|
2241
|
+
onRemove: () => removeFile(file.id),
|
|
2242
|
+
onAbort: () => uploader.abort(file.id),
|
|
2243
|
+
onView: settled ? () => viewer.open(settled) : void 0,
|
|
2244
|
+
size,
|
|
2245
|
+
labels: options.labels
|
|
2246
|
+
},
|
|
2247
|
+
file.id
|
|
2248
|
+
);
|
|
2249
|
+
})
|
|
2250
|
+
] });
|
|
2251
|
+
const fileList = renderFiles ? renderFiles({
|
|
2252
|
+
stored,
|
|
2253
|
+
staged: files,
|
|
2254
|
+
nodes: defaultRows,
|
|
2255
|
+
isEmpty: stored.length === 0 && files.length === 0,
|
|
2256
|
+
view: viewer.open,
|
|
2257
|
+
remove: removeFile,
|
|
2258
|
+
removeStored: requestRemove
|
|
2259
|
+
}) : defaultRows;
|
|
2260
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-2", className), children: [
|
|
2261
|
+
label && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-fg text-sm font-medium", children: label }),
|
|
2262
|
+
filesPosition === "above" && fileList,
|
|
2263
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2264
|
+
Dropzone,
|
|
2265
|
+
{
|
|
2266
|
+
accept: uploader.accept,
|
|
2267
|
+
multiple: options.multiple,
|
|
2268
|
+
disabled: disabled || uploader.isUploading,
|
|
2269
|
+
capture,
|
|
2270
|
+
size,
|
|
2271
|
+
shortcut,
|
|
2272
|
+
onFiles: (incoming) => void uploader.addFiles(incoming),
|
|
2273
|
+
children: [
|
|
2274
|
+
icon !== null && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
|
|
2275
|
+
UploadCloudIcon,
|
|
2276
|
+
{
|
|
2277
|
+
className: size === "sm" ? "h-5 w-5" : "h-7 w-7"
|
|
2278
|
+
}
|
|
2279
|
+
) }),
|
|
2280
|
+
/* @__PURE__ */ jsxRuntime.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 }),
|
|
2281
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-placeholder mt-1 text-xs", children: description ?? uploader.accept.replaceAll(",", " \xB7 ") })
|
|
2282
|
+
]
|
|
2283
|
+
}
|
|
2284
|
+
),
|
|
2285
|
+
filesPosition === "below" && fileList,
|
|
2286
|
+
manual && uploader.hasPending && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2287
|
+
"button",
|
|
2288
|
+
{
|
|
2289
|
+
type: "button",
|
|
2290
|
+
onClick: () => void uploader.upload(),
|
|
2291
|
+
disabled: disabled || uploader.isUploading,
|
|
2292
|
+
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",
|
|
2293
|
+
children: copy.uploadAll
|
|
2294
|
+
}
|
|
2295
|
+
),
|
|
2296
|
+
/* @__PURE__ */ jsxRuntime.jsx(FileViewer, { ...viewer.viewerProps, labels: options.labels }),
|
|
2297
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2298
|
+
ConfirmDialog,
|
|
2299
|
+
{
|
|
2300
|
+
open: removing !== null,
|
|
2301
|
+
title: confirmCopy.title ?? copy.confirmRemoveTitle,
|
|
2302
|
+
message: confirmCopy.message ?? copy.confirmRemoveMessage(removing?.fileName ?? ""),
|
|
2303
|
+
onConfirm: () => {
|
|
2304
|
+
if (removing) forgetStored(removing);
|
|
2305
|
+
setRemoving(null);
|
|
2306
|
+
},
|
|
2307
|
+
onCancel: () => setRemoving(null),
|
|
2308
|
+
labels: options.labels
|
|
2309
|
+
}
|
|
2310
|
+
)
|
|
2311
|
+
] });
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
exports.ConfirmDialog = ConfirmDialog;
|
|
2315
|
+
exports.Dropzone = Dropzone;
|
|
2316
|
+
exports.FileItem = FileItem;
|
|
2317
|
+
exports.FileViewer = FileViewer;
|
|
2318
|
+
exports.SlottedUploader = SlottedUploader;
|
|
2319
|
+
exports.Uploader = Uploader;
|
|
2320
|
+
exports.UploaderProvider = UploaderProvider;
|
|
2321
|
+
exports.cn = cn;
|
|
2322
|
+
exports.useFileViewer = useFileViewer;
|
|
2323
|
+
exports.useUploaderLabels = useUploaderLabels;
|
|
2324
|
+
//# sourceMappingURL=ui.cjs.map
|
|
2325
|
+
//# sourceMappingURL=ui.cjs.map
|