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/presets.cjs
ADDED
|
@@ -0,0 +1,1665 @@
|
|
|
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/presets/AvatarUploader.tsx
|
|
8
|
+
|
|
9
|
+
// src/labels.ts
|
|
10
|
+
var DEFAULT_LABELS = {
|
|
11
|
+
dropPrompt: "Choose a file or drag it here",
|
|
12
|
+
tapPrompt: "Tap to choose a file",
|
|
13
|
+
bulkDropPrompt: "Drop several files \u2014 each lands on its document",
|
|
14
|
+
bulkTapPrompt: "Tap to add several files",
|
|
15
|
+
uploadAll: "Upload files",
|
|
16
|
+
view: "View",
|
|
17
|
+
remove: "Remove",
|
|
18
|
+
cancel: "Cancel",
|
|
19
|
+
ready: "Done",
|
|
20
|
+
staged: "Ready to upload",
|
|
21
|
+
upload: "Upload",
|
|
22
|
+
replace: "Replace",
|
|
23
|
+
dropToReplace: "Drop to replace",
|
|
24
|
+
previous: "Previous",
|
|
25
|
+
next: "Next",
|
|
26
|
+
uploadFailed: "The file could not be uploaded",
|
|
27
|
+
uploadNetworkFailed: "The file could not be uploaded. Check your connection",
|
|
28
|
+
removeFailed: "The file could not be removed from storage",
|
|
29
|
+
readFailed: "The file could not be loaded",
|
|
30
|
+
maxFilesReached: (max) => `At most ${max} file(s)`,
|
|
31
|
+
slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
32
|
+
fileWithoutSlot: (fileName) => `"${fileName}" does not match any document`,
|
|
33
|
+
viewerLoading: "Loading\u2026",
|
|
34
|
+
viewerError: "The preview could not be loaded",
|
|
35
|
+
viewerErrorHint: "The file may have moved or the link expired. You can retry or download it.",
|
|
36
|
+
viewerRetry: "Retry",
|
|
37
|
+
openInTab: "Open in tab",
|
|
38
|
+
downloadShort: "Download",
|
|
39
|
+
close: "Close",
|
|
40
|
+
noPreview: "This format has no preview",
|
|
41
|
+
download: (fileName) => `Download ${fileName}`,
|
|
42
|
+
filePreview: "File",
|
|
43
|
+
confirm: "Confirm",
|
|
44
|
+
confirmRemoveTitle: "Remove file",
|
|
45
|
+
confirmRemoveMessage: (fileName) => `Remove "${fileName}"? You can upload another one later.`,
|
|
46
|
+
confirmReplaceTitle: "Replace file",
|
|
47
|
+
confirmReplaceMessage: (current, incoming) => `Replace "${current}" with "${incoming}"?`,
|
|
48
|
+
dropAnywhereTitle: "Drop the files here",
|
|
49
|
+
dropAnywhereHint: "Anywhere on the screen counts",
|
|
50
|
+
avatarChange: "Change photo",
|
|
51
|
+
avatarDropHere: "Drop here",
|
|
52
|
+
avatarRemove: "Remove photo",
|
|
53
|
+
galleryAdd: "Add",
|
|
54
|
+
fileHasNoExtension: "The file has no extension",
|
|
55
|
+
formatNotAllowed: (extensions) => `Format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
56
|
+
fileIsEmpty: "The file is empty",
|
|
57
|
+
fileTooLarge: (size, max) => `The file weighs ${size} and the maximum is ${max}`,
|
|
58
|
+
fileIncomplete: "The file is incomplete",
|
|
59
|
+
contentDoesNotMatchExtension: "The file contents do not match its extension",
|
|
60
|
+
requestIncomplete: "Incomplete request",
|
|
61
|
+
unauthorized: "Unauthorized",
|
|
62
|
+
fileRequired: "A file is required",
|
|
63
|
+
unknownScope: "Unknown file destination",
|
|
64
|
+
processingFailed: "The file could not be processed",
|
|
65
|
+
unnamedFile: "file"
|
|
66
|
+
};
|
|
67
|
+
var resolveLabels = (labels, base = DEFAULT_LABELS) => labels ? { ...base, ...labels } : base;
|
|
68
|
+
({ ...DEFAULT_LABELS });
|
|
69
|
+
var LabelsContext = react.createContext(DEFAULT_LABELS);
|
|
70
|
+
var useUploaderLabels = (overrides) => {
|
|
71
|
+
const base = react.useContext(LabelsContext);
|
|
72
|
+
return react.useMemo(
|
|
73
|
+
() => overrides ? { ...base, ...overrides } : base,
|
|
74
|
+
[base, overrides]
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/constants.ts
|
|
79
|
+
var KB = 1024;
|
|
80
|
+
var MB = 1024 * KB;
|
|
81
|
+
var MAGIC_NUMBERS = {
|
|
82
|
+
png: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
83
|
+
jpg: [255, 216, 255],
|
|
84
|
+
jpeg: [255, 216, 255],
|
|
85
|
+
gif: [71, 73, 70, 56],
|
|
86
|
+
webp: [82, 73, 70, 70],
|
|
87
|
+
// RIFF; bytes 8-11 spell WEBP
|
|
88
|
+
ico: [0, 0, 1, 0],
|
|
89
|
+
pdf: [37, 80, 68, 70],
|
|
90
|
+
// %PDF
|
|
91
|
+
// OOXML files are ZIP containers, so docx/xlsx share the PK signature.
|
|
92
|
+
docx: [80, 75, 3, 4],
|
|
93
|
+
xlsx: [80, 75, 3, 4],
|
|
94
|
+
doc: [208, 207, 17, 224],
|
|
95
|
+
xls: [208, 207, 17, 224],
|
|
96
|
+
// DER/ASN.1 sequence header.
|
|
97
|
+
cer: [48, 130],
|
|
98
|
+
crt: [48, 130],
|
|
99
|
+
key: [48, 130],
|
|
100
|
+
pem: [45, 45, 45, 45, 45],
|
|
101
|
+
// -----
|
|
102
|
+
webm: [26, 69, 223, 163],
|
|
103
|
+
avi: [82, 73, 70, 70],
|
|
104
|
+
mp3: [73, 68, 51],
|
|
105
|
+
// ID3
|
|
106
|
+
wav: [82, 73, 70, 70],
|
|
107
|
+
ogg: [79, 103, 103, 83],
|
|
108
|
+
aac: [255, 241]
|
|
109
|
+
};
|
|
110
|
+
var MIME_TYPES = {
|
|
111
|
+
png: "image/png",
|
|
112
|
+
jpg: "image/jpeg",
|
|
113
|
+
jpeg: "image/jpeg",
|
|
114
|
+
svg: "image/svg+xml",
|
|
115
|
+
webp: "image/webp",
|
|
116
|
+
gif: "image/gif",
|
|
117
|
+
ico: "image/x-icon",
|
|
118
|
+
heic: "image/heic",
|
|
119
|
+
cer: "application/x-x509-ca-cert",
|
|
120
|
+
crt: "application/x-x509-ca-cert",
|
|
121
|
+
pem: "application/x-pem-file",
|
|
122
|
+
key: "application/x-pem-file",
|
|
123
|
+
pdf: "application/pdf",
|
|
124
|
+
doc: "application/msword",
|
|
125
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
126
|
+
xls: "application/vnd.ms-excel",
|
|
127
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
128
|
+
csv: "text/csv",
|
|
129
|
+
txt: "text/plain",
|
|
130
|
+
xml: "application/xml",
|
|
131
|
+
json: "application/json",
|
|
132
|
+
mp4: "video/mp4",
|
|
133
|
+
mov: "video/quicktime",
|
|
134
|
+
avi: "video/x-msvideo",
|
|
135
|
+
webm: "video/webm",
|
|
136
|
+
mp3: "audio/mpeg",
|
|
137
|
+
wav: "audio/wav",
|
|
138
|
+
ogg: "audio/ogg",
|
|
139
|
+
aac: "audio/aac"
|
|
140
|
+
};
|
|
141
|
+
var FILE_CATEGORY_CONFIG = {
|
|
142
|
+
image: {
|
|
143
|
+
extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
|
|
144
|
+
maxBytes: 5 * MB,
|
|
145
|
+
validateMagicNumbers: true,
|
|
146
|
+
label: "image",
|
|
147
|
+
accept: "image/*"
|
|
148
|
+
},
|
|
149
|
+
certificate: {
|
|
150
|
+
extensions: ["cer", "crt", "pem"],
|
|
151
|
+
maxBytes: 50 * KB,
|
|
152
|
+
validateMagicNumbers: true,
|
|
153
|
+
label: "certificate",
|
|
154
|
+
accept: ".cer,.crt,.pem"
|
|
155
|
+
},
|
|
156
|
+
key: {
|
|
157
|
+
extensions: ["key"],
|
|
158
|
+
maxBytes: 50 * KB,
|
|
159
|
+
validateMagicNumbers: true,
|
|
160
|
+
label: "private key",
|
|
161
|
+
accept: ".key"
|
|
162
|
+
},
|
|
163
|
+
pdf: {
|
|
164
|
+
extensions: ["pdf"],
|
|
165
|
+
maxBytes: 20 * MB,
|
|
166
|
+
validateMagicNumbers: true,
|
|
167
|
+
label: "PDF",
|
|
168
|
+
accept: ".pdf"
|
|
169
|
+
},
|
|
170
|
+
document: {
|
|
171
|
+
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
|
|
172
|
+
maxBytes: 20 * MB,
|
|
173
|
+
validateMagicNumbers: false,
|
|
174
|
+
label: "document",
|
|
175
|
+
accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
|
|
176
|
+
},
|
|
177
|
+
data: {
|
|
178
|
+
extensions: ["xml", "json"],
|
|
179
|
+
maxBytes: 5 * MB,
|
|
180
|
+
validateMagicNumbers: false,
|
|
181
|
+
label: "data file",
|
|
182
|
+
accept: ".xml,.json"
|
|
183
|
+
},
|
|
184
|
+
video: {
|
|
185
|
+
extensions: ["mp4", "mov", "avi", "webm"],
|
|
186
|
+
maxBytes: 100 * MB,
|
|
187
|
+
validateMagicNumbers: false,
|
|
188
|
+
label: "video",
|
|
189
|
+
accept: "video/*"
|
|
190
|
+
},
|
|
191
|
+
audio: {
|
|
192
|
+
extensions: ["mp3", "wav", "ogg", "aac"],
|
|
193
|
+
maxBytes: 20 * MB,
|
|
194
|
+
validateMagicNumbers: false,
|
|
195
|
+
label: "audio",
|
|
196
|
+
accept: "audio/*"
|
|
197
|
+
},
|
|
198
|
+
any: {
|
|
199
|
+
extensions: [],
|
|
200
|
+
maxBytes: 20 * MB,
|
|
201
|
+
validateMagicNumbers: false,
|
|
202
|
+
label: "file",
|
|
203
|
+
accept: "*/*"
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/file.ts
|
|
208
|
+
var getFileExtension = (fileName) => {
|
|
209
|
+
const dot = fileName.lastIndexOf(".");
|
|
210
|
+
if (dot < 1 || dot === fileName.length - 1) return "";
|
|
211
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
212
|
+
};
|
|
213
|
+
var getMimeType = (fileName) => {
|
|
214
|
+
const ext = getFileExtension(fileName);
|
|
215
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
216
|
+
};
|
|
217
|
+
var isKnownExtension = (value) => value in MIME_TYPES;
|
|
218
|
+
var formatFileSize = (bytes) => {
|
|
219
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
220
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
221
|
+
let size = bytes / 1024;
|
|
222
|
+
let unit = 0;
|
|
223
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
224
|
+
size /= 1024;
|
|
225
|
+
unit++;
|
|
226
|
+
}
|
|
227
|
+
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/validation.ts
|
|
231
|
+
var ok = { valid: true };
|
|
232
|
+
var fail = (code, message) => ({ valid: false, code, message });
|
|
233
|
+
var validateExtension = (fileName, allowed, labels) => {
|
|
234
|
+
if (allowed.length === 0) return ok;
|
|
235
|
+
const copy = resolveLabels(labels);
|
|
236
|
+
const extension = getFileExtension(fileName);
|
|
237
|
+
if (!extension) {
|
|
238
|
+
return fail("extension-not-allowed", copy.fileHasNoExtension);
|
|
239
|
+
}
|
|
240
|
+
if (!allowed.includes(extension)) {
|
|
241
|
+
return fail("extension-not-allowed", copy.formatNotAllowed(allowed));
|
|
242
|
+
}
|
|
243
|
+
return ok;
|
|
244
|
+
};
|
|
245
|
+
var validateSize = (size, maxBytes, labels) => {
|
|
246
|
+
const copy = resolveLabels(labels);
|
|
247
|
+
if (size === 0) return fail("empty-file", copy.fileIsEmpty);
|
|
248
|
+
if (size > maxBytes) {
|
|
249
|
+
return fail(
|
|
250
|
+
"too-large",
|
|
251
|
+
copy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return ok;
|
|
255
|
+
};
|
|
256
|
+
var validateMagicNumbers = async (file, labels) => {
|
|
257
|
+
const copy = resolveLabels(labels);
|
|
258
|
+
const extension = getFileExtension(file.name);
|
|
259
|
+
if (!isKnownExtension(extension)) return ok;
|
|
260
|
+
const expected = MAGIC_NUMBERS[extension];
|
|
261
|
+
if (!expected) return ok;
|
|
262
|
+
const head = new Uint8Array(await file.arrayBuffer()).slice(
|
|
263
|
+
0,
|
|
264
|
+
expected.length
|
|
265
|
+
);
|
|
266
|
+
if (head.length < expected.length) {
|
|
267
|
+
return fail("magic-number-mismatch", copy.fileIncomplete);
|
|
268
|
+
}
|
|
269
|
+
const matches = expected.every((byte, index) => head[index] === byte);
|
|
270
|
+
return matches ? ok : fail("magic-number-mismatch", copy.contentDoesNotMatchExtension);
|
|
271
|
+
};
|
|
272
|
+
var validateFile = async (file, options = {}) => {
|
|
273
|
+
const {
|
|
274
|
+
maxBytes = Infinity,
|
|
275
|
+
allowedExtensions = [],
|
|
276
|
+
validateMagicNumbers: checkMagic = false,
|
|
277
|
+
labels,
|
|
278
|
+
customValidation
|
|
279
|
+
} = options;
|
|
280
|
+
const size = validateSize(file.size, maxBytes, labels);
|
|
281
|
+
if (!size.valid) return size;
|
|
282
|
+
const extension = validateExtension(file.name, allowedExtensions, labels);
|
|
283
|
+
if (!extension.valid) return extension;
|
|
284
|
+
if (checkMagic) {
|
|
285
|
+
const magic = await validateMagicNumbers(file, labels);
|
|
286
|
+
if (!magic.valid) return magic;
|
|
287
|
+
}
|
|
288
|
+
if (customValidation) return customValidation(file);
|
|
289
|
+
return ok;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// src/scopes.ts
|
|
293
|
+
var validateForScope = async (registry, name, file, labels) => {
|
|
294
|
+
const scope = registry.get(name);
|
|
295
|
+
const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
|
|
296
|
+
return validateFile(file, {
|
|
297
|
+
maxBytes: scope.maxBytes,
|
|
298
|
+
allowedExtensions: scope.accept,
|
|
299
|
+
validateMagicNumbers: category?.validateMagicNumbers ?? true,
|
|
300
|
+
labels
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/warn.ts
|
|
305
|
+
var seen = /* @__PURE__ */ new Set();
|
|
306
|
+
var warnDev = (key, message) => {
|
|
307
|
+
if (process.env.NODE_ENV === "production") return;
|
|
308
|
+
if (seen.has(key)) return;
|
|
309
|
+
seen.add(key);
|
|
310
|
+
console.warn(`[uploaderkit] ${message}`);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/react/compressImage.ts
|
|
314
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
315
|
+
var compressImage = async (file, { maxWidth, maxHeight, quality = 0.8 }) => {
|
|
316
|
+
if (!COMPRESSIBLE.has(file.type)) return file;
|
|
317
|
+
if (typeof createImageBitmap !== "function") return file;
|
|
318
|
+
try {
|
|
319
|
+
const bitmap = await createImageBitmap(file);
|
|
320
|
+
const scale = Math.min(
|
|
321
|
+
1,
|
|
322
|
+
(maxWidth ?? Infinity) / bitmap.width,
|
|
323
|
+
(maxHeight ?? Infinity) / bitmap.height
|
|
324
|
+
);
|
|
325
|
+
const canvas = document.createElement("canvas");
|
|
326
|
+
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
327
|
+
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
328
|
+
const context = canvas.getContext("2d");
|
|
329
|
+
if (!context) return file;
|
|
330
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
331
|
+
bitmap.close();
|
|
332
|
+
const blob = await new Promise(
|
|
333
|
+
(resolve) => canvas.toBlob(resolve, file.type, quality)
|
|
334
|
+
);
|
|
335
|
+
if (!blob || blob.size >= file.size) return file;
|
|
336
|
+
return new File([blob], file.name, { type: file.type });
|
|
337
|
+
} catch {
|
|
338
|
+
return file;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/react/useUploader.ts
|
|
343
|
+
var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
344
|
+
var previewOf = (file) => file.type.startsWith("image/") && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
345
|
+
var revokePreview = (file) => {
|
|
346
|
+
if (file.preview) URL.revokeObjectURL(file.preview);
|
|
347
|
+
};
|
|
348
|
+
var normalizeRetry = (retry) => {
|
|
349
|
+
const options = typeof retry === "number" ? { attempts: retry } : retry ?? {};
|
|
350
|
+
return {
|
|
351
|
+
attempts: Math.max(1, options.attempts ?? 1),
|
|
352
|
+
backoffMs: options.backoffMs ?? 500
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
var wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
356
|
+
const abort = () => {
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
const error = new Error("Upload cancelled");
|
|
359
|
+
error.name = "AbortError";
|
|
360
|
+
reject(error);
|
|
361
|
+
};
|
|
362
|
+
const timer = setTimeout(() => {
|
|
363
|
+
signal.removeEventListener("abort", abort);
|
|
364
|
+
resolve();
|
|
365
|
+
}, ms);
|
|
366
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
367
|
+
});
|
|
368
|
+
var runPool = async (items, limit, run) => {
|
|
369
|
+
const results = new Array(items.length);
|
|
370
|
+
let cursor = 0;
|
|
371
|
+
const workers = Array.from(
|
|
372
|
+
{ length: Math.min(Math.max(1, limit), items.length) },
|
|
373
|
+
async () => {
|
|
374
|
+
while (cursor < items.length) {
|
|
375
|
+
const index = cursor++;
|
|
376
|
+
results[index] = await run(items[index]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
await Promise.all(workers);
|
|
381
|
+
return results;
|
|
382
|
+
};
|
|
383
|
+
var useUploader = ({
|
|
384
|
+
scopes,
|
|
385
|
+
scope,
|
|
386
|
+
entityId,
|
|
387
|
+
strategy,
|
|
388
|
+
multiple: multipleOption,
|
|
389
|
+
maxFiles: maxFilesOption,
|
|
390
|
+
uploadOn = "manual",
|
|
391
|
+
onUploadStart,
|
|
392
|
+
onUploaded,
|
|
393
|
+
onError,
|
|
394
|
+
retry,
|
|
395
|
+
concurrency = Infinity,
|
|
396
|
+
rename,
|
|
397
|
+
labels
|
|
398
|
+
}) => {
|
|
399
|
+
const scopeMaxFiles = scopes.get(scope).maxFiles;
|
|
400
|
+
const maxFiles = maxFilesOption ?? scopeMaxFiles;
|
|
401
|
+
const multiple = multipleOption ?? (maxFiles !== void 0 && maxFiles > 1);
|
|
402
|
+
const [files, setFiles] = react.useState([]);
|
|
403
|
+
const controllers = react.useRef(/* @__PURE__ */ new Map());
|
|
404
|
+
const filesRef = react.useRef([]);
|
|
405
|
+
filesRef.current = files;
|
|
406
|
+
const copy = useUploaderLabels(labels);
|
|
407
|
+
const { attempts, backoffMs } = normalizeRetry(retry);
|
|
408
|
+
const patch = react.useCallback((id, changes) => {
|
|
409
|
+
setFiles(
|
|
410
|
+
(previous) => previous.map((file) => file.id === id ? { ...file, ...changes } : file)
|
|
411
|
+
);
|
|
412
|
+
}, []);
|
|
413
|
+
const uploadOne = react.useCallback(
|
|
414
|
+
async (target) => {
|
|
415
|
+
if (!strategy) {
|
|
416
|
+
warnDev(
|
|
417
|
+
"upload-without-strategy",
|
|
418
|
+
"upload() ran without a `strategy` \u2014 files stay local. Pass `strategy` (e.g. createXhrUploadStrategy) to useUploader."
|
|
419
|
+
);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const config = scopes.get(scope);
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
controllers.current.set(target.id, controller);
|
|
425
|
+
patch(target.id, { status: "uploading", progress: 0 });
|
|
426
|
+
try {
|
|
427
|
+
const payload = config.compress ? await compressImage(target.file, config.compress) : target.file;
|
|
428
|
+
for (let attempt = 1; ; attempt++) {
|
|
429
|
+
try {
|
|
430
|
+
const stored = await strategy(payload, scope, entityId, {
|
|
431
|
+
onProgress: (percent) => patch(target.id, { progress: percent }),
|
|
432
|
+
signal: controller.signal
|
|
433
|
+
});
|
|
434
|
+
patch(target.id, { status: "success", progress: 100, stored });
|
|
435
|
+
return stored;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
438
|
+
if (aborted || attempt >= attempts) throw error;
|
|
439
|
+
patch(target.id, { progress: 0 });
|
|
440
|
+
await wait(backoffMs * 2 ** (attempt - 1), controller.signal);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
445
|
+
const message = error instanceof Error && error.message ? error.message : copy.uploadFailed;
|
|
446
|
+
patch(target.id, {
|
|
447
|
+
status: aborted ? "idle" : "error",
|
|
448
|
+
progress: 0,
|
|
449
|
+
...aborted ? {} : { error: message }
|
|
450
|
+
});
|
|
451
|
+
if (!aborted) onError?.(message);
|
|
452
|
+
return null;
|
|
453
|
+
} finally {
|
|
454
|
+
controllers.current.delete(target.id);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
[
|
|
458
|
+
strategy,
|
|
459
|
+
scopes,
|
|
460
|
+
scope,
|
|
461
|
+
entityId,
|
|
462
|
+
patch,
|
|
463
|
+
onError,
|
|
464
|
+
attempts,
|
|
465
|
+
backoffMs,
|
|
466
|
+
copy
|
|
467
|
+
]
|
|
468
|
+
);
|
|
469
|
+
const uploadBatch = react.useCallback(
|
|
470
|
+
async (targets) => {
|
|
471
|
+
if (targets.length === 0) return [];
|
|
472
|
+
onUploadStart?.(targets);
|
|
473
|
+
const results = await runPool(targets, concurrency, uploadOne);
|
|
474
|
+
const stored = results.filter((r) => r !== null);
|
|
475
|
+
if (stored.length > 0) onUploaded?.(stored);
|
|
476
|
+
return stored;
|
|
477
|
+
},
|
|
478
|
+
[uploadOne, onUploadStart, onUploaded, concurrency]
|
|
479
|
+
);
|
|
480
|
+
const addFiles = react.useCallback(
|
|
481
|
+
async (incoming) => {
|
|
482
|
+
let batch = Array.from(incoming);
|
|
483
|
+
if (!multiple && batch.length > 1) {
|
|
484
|
+
warnDev(
|
|
485
|
+
"single-mode-multi-drop",
|
|
486
|
+
`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.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (!multiple) batch = batch.slice(0, 1);
|
|
490
|
+
if (maxFiles !== void 0) {
|
|
491
|
+
const room = maxFiles - (multiple ? filesRef.current.length : 0);
|
|
492
|
+
if (batch.length > room) {
|
|
493
|
+
batch = batch.slice(0, Math.max(0, room));
|
|
494
|
+
onError?.(copy.maxFilesReached(maxFiles));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (batch.length === 0) return;
|
|
498
|
+
if (rename) {
|
|
499
|
+
batch = batch.map(
|
|
500
|
+
(file) => new File([file], rename(file), { type: file.type })
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const next = [];
|
|
504
|
+
for (const file of batch) {
|
|
505
|
+
const result = await validateForScope(scopes, scope, file, copy);
|
|
506
|
+
next.push({
|
|
507
|
+
id: createId(),
|
|
508
|
+
file,
|
|
509
|
+
status: result.valid ? "idle" : "error",
|
|
510
|
+
progress: 0,
|
|
511
|
+
...result.valid ? { preview: previewOf(file) } : { error: result.message }
|
|
512
|
+
});
|
|
513
|
+
if (!result.valid) onError?.(next[next.length - 1].error);
|
|
514
|
+
}
|
|
515
|
+
setFiles((previous) => {
|
|
516
|
+
if (multiple) return [...previous, ...next];
|
|
517
|
+
previous.forEach(revokePreview);
|
|
518
|
+
return next;
|
|
519
|
+
});
|
|
520
|
+
if (uploadOn === "select") {
|
|
521
|
+
await uploadBatch(next.filter((file) => file.status === "idle"));
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
[
|
|
525
|
+
multiple,
|
|
526
|
+
maxFiles,
|
|
527
|
+
scopes,
|
|
528
|
+
scope,
|
|
529
|
+
uploadOn,
|
|
530
|
+
uploadBatch,
|
|
531
|
+
onError,
|
|
532
|
+
rename,
|
|
533
|
+
copy
|
|
534
|
+
]
|
|
535
|
+
);
|
|
536
|
+
const upload = react.useCallback(
|
|
537
|
+
() => uploadBatch(filesRef.current.filter((f) => f.status === "idle")),
|
|
538
|
+
[uploadBatch]
|
|
539
|
+
);
|
|
540
|
+
const abort = react.useCallback((id) => {
|
|
541
|
+
if (id) {
|
|
542
|
+
controllers.current.get(id)?.abort();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
546
|
+
}, []);
|
|
547
|
+
const removeFile = react.useCallback(
|
|
548
|
+
(id) => {
|
|
549
|
+
abort(id);
|
|
550
|
+
setFiles((previous) => {
|
|
551
|
+
const target = previous.find((file) => file.id === id);
|
|
552
|
+
if (target) revokePreview(target);
|
|
553
|
+
return previous.filter((file) => file.id !== id);
|
|
554
|
+
});
|
|
555
|
+
},
|
|
556
|
+
[abort]
|
|
557
|
+
);
|
|
558
|
+
const clear = react.useCallback(() => {
|
|
559
|
+
abort();
|
|
560
|
+
setFiles((previous) => {
|
|
561
|
+
previous.forEach(revokePreview);
|
|
562
|
+
return [];
|
|
563
|
+
});
|
|
564
|
+
}, [abort]);
|
|
565
|
+
react.useEffect(
|
|
566
|
+
() => () => {
|
|
567
|
+
filesRef.current.forEach(revokePreview);
|
|
568
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
569
|
+
},
|
|
570
|
+
[]
|
|
571
|
+
);
|
|
572
|
+
const accept = react.useMemo(() => scopes.accept(scope), [scopes, scope]);
|
|
573
|
+
return {
|
|
574
|
+
files,
|
|
575
|
+
accept,
|
|
576
|
+
isUploading: files.some((file) => file.status === "uploading"),
|
|
577
|
+
hasPending: files.some((file) => file.status === "idle"),
|
|
578
|
+
addFiles,
|
|
579
|
+
upload,
|
|
580
|
+
abort,
|
|
581
|
+
removeFile,
|
|
582
|
+
clear
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/ui/cn.ts
|
|
587
|
+
var cn = (...values) => values.filter(Boolean).join(" ");
|
|
588
|
+
var AvatarUploader = ({
|
|
589
|
+
scopes,
|
|
590
|
+
scope,
|
|
591
|
+
entityId,
|
|
592
|
+
strategy,
|
|
593
|
+
rename,
|
|
594
|
+
src,
|
|
595
|
+
fallback,
|
|
596
|
+
size = 96,
|
|
597
|
+
onUploaded,
|
|
598
|
+
onRemove,
|
|
599
|
+
onError,
|
|
600
|
+
alt = "",
|
|
601
|
+
disabled = false,
|
|
602
|
+
labels,
|
|
603
|
+
className
|
|
604
|
+
}) => {
|
|
605
|
+
const copy = useUploaderLabels(labels);
|
|
606
|
+
const input = react.useRef(null);
|
|
607
|
+
const [dropping, setDropping] = react.useState(false);
|
|
608
|
+
const depth = react.useRef(0);
|
|
609
|
+
const uploader = useUploader({
|
|
610
|
+
scopes,
|
|
611
|
+
scope,
|
|
612
|
+
entityId,
|
|
613
|
+
strategy,
|
|
614
|
+
uploadOn: "select",
|
|
615
|
+
maxFiles: 1,
|
|
616
|
+
...rename ? { rename } : {},
|
|
617
|
+
onUploaded: (stored) => {
|
|
618
|
+
if (stored[0]) onUploaded(stored[0]);
|
|
619
|
+
},
|
|
620
|
+
onError,
|
|
621
|
+
labels
|
|
622
|
+
});
|
|
623
|
+
const current = uploader.files[0];
|
|
624
|
+
const uploading = current?.status === "uploading";
|
|
625
|
+
const failed = current?.status === "error" ? current.error : void 0;
|
|
626
|
+
const shown = current?.preview ?? src ?? null;
|
|
627
|
+
const locked = disabled || uploading;
|
|
628
|
+
const onDrop = (event) => {
|
|
629
|
+
event.preventDefault();
|
|
630
|
+
depth.current = 0;
|
|
631
|
+
setDropping(false);
|
|
632
|
+
if (locked) return;
|
|
633
|
+
const file = event.dataTransfer.files[0];
|
|
634
|
+
if (file) void uploader.addFiles([file]);
|
|
635
|
+
};
|
|
636
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("inline-flex flex-col items-center gap-2", className), children: [
|
|
637
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { width: size, height: size }, children: [
|
|
638
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
639
|
+
"button",
|
|
640
|
+
{
|
|
641
|
+
type: "button",
|
|
642
|
+
onClick: () => !locked && input.current?.click(),
|
|
643
|
+
onDragEnter: () => {
|
|
644
|
+
depth.current += 1;
|
|
645
|
+
if (!locked) setDropping(true);
|
|
646
|
+
},
|
|
647
|
+
onDragOver: (event) => event.preventDefault(),
|
|
648
|
+
onDragLeave: () => {
|
|
649
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
650
|
+
if (depth.current === 0) setDropping(false);
|
|
651
|
+
},
|
|
652
|
+
onDrop,
|
|
653
|
+
disabled: locked,
|
|
654
|
+
"aria-busy": uploading,
|
|
655
|
+
"aria-label": copy.avatarChange,
|
|
656
|
+
className: cn(
|
|
657
|
+
"group ring-ui-border focus-visible:ring-ui-ring relative flex h-full w-full overflow-hidden rounded-full ring-2 ring-offset-2 transition-all duration-200 outline-none",
|
|
658
|
+
locked ? "cursor-not-allowed" : "hover:ring-ui-primary cursor-pointer",
|
|
659
|
+
dropping && "ring-ui-primary cursor-copy ring-4"
|
|
660
|
+
),
|
|
661
|
+
children: [
|
|
662
|
+
shown ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: shown, alt, className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-ui-surface-muted text-ui-muted flex h-full w-full items-center justify-center text-xl font-semibold", children: fallback }),
|
|
663
|
+
dropping ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-ui-primary/70 text-ui-primary-fg absolute inset-0 flex items-center justify-center text-xs font-semibold", children: copy.avatarDropHere }) : uploading ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white", children: [
|
|
664
|
+
current.progress,
|
|
665
|
+
"%"
|
|
666
|
+
] }) : locked ? null : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100", children: copy.avatarChange })
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
uploading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
671
|
+
"svg",
|
|
672
|
+
{
|
|
673
|
+
viewBox: "0 0 100 100",
|
|
674
|
+
className: "pointer-events-none absolute -inset-1.5 -rotate-90",
|
|
675
|
+
"aria-hidden": true,
|
|
676
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
677
|
+
"circle",
|
|
678
|
+
{
|
|
679
|
+
cx: "50",
|
|
680
|
+
cy: "50",
|
|
681
|
+
r: "48",
|
|
682
|
+
pathLength: 100,
|
|
683
|
+
fill: "none",
|
|
684
|
+
strokeWidth: 4,
|
|
685
|
+
strokeLinecap: "round",
|
|
686
|
+
strokeDasharray: 100,
|
|
687
|
+
strokeDashoffset: 100 - (current?.progress ?? 0),
|
|
688
|
+
className: "stroke-ui-primary transition-[stroke-dashoffset] duration-200"
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
) : null
|
|
693
|
+
] }),
|
|
694
|
+
failed ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-danger max-w-[16rem] text-center text-xs", children: failed }) : onRemove && src && !uploading && !disabled ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
695
|
+
"button",
|
|
696
|
+
{
|
|
697
|
+
type: "button",
|
|
698
|
+
onClick: onRemove,
|
|
699
|
+
className: "text-ui-danger cursor-pointer text-xs hover:underline",
|
|
700
|
+
children: copy.avatarRemove
|
|
701
|
+
}
|
|
702
|
+
) : null,
|
|
703
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
704
|
+
"input",
|
|
705
|
+
{
|
|
706
|
+
ref: input,
|
|
707
|
+
type: "file",
|
|
708
|
+
accept: uploader.accept,
|
|
709
|
+
className: "hidden",
|
|
710
|
+
disabled: locked,
|
|
711
|
+
onChange: (event) => {
|
|
712
|
+
const file = event.target.files?.[0];
|
|
713
|
+
if (file) void uploader.addFiles([file]);
|
|
714
|
+
event.target.value = "";
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
] });
|
|
719
|
+
};
|
|
720
|
+
var DocumentGlyph = () => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
721
|
+
"svg",
|
|
722
|
+
{
|
|
723
|
+
width: "40",
|
|
724
|
+
height: "40",
|
|
725
|
+
viewBox: "0 0 24 24",
|
|
726
|
+
fill: "none",
|
|
727
|
+
stroke: "currentColor",
|
|
728
|
+
strokeWidth: "1.6",
|
|
729
|
+
strokeLinecap: "round",
|
|
730
|
+
strokeLinejoin: "round",
|
|
731
|
+
"aria-hidden": true,
|
|
732
|
+
children: [
|
|
733
|
+
/* @__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" }),
|
|
734
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3v5h5" }),
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 18v-6" }),
|
|
736
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m9.5 14.5 2.5-2.5 2.5 2.5" })
|
|
737
|
+
]
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
var DropAnywhereOverlay = ({
|
|
741
|
+
open,
|
|
742
|
+
icon,
|
|
743
|
+
title,
|
|
744
|
+
hint,
|
|
745
|
+
labels,
|
|
746
|
+
className
|
|
747
|
+
}) => {
|
|
748
|
+
const copy = useUploaderLabels(labels);
|
|
749
|
+
if (!open || typeof document === "undefined") return null;
|
|
750
|
+
return reactDom.createPortal(
|
|
751
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
752
|
+
"div",
|
|
753
|
+
{
|
|
754
|
+
"aria-hidden": true,
|
|
755
|
+
className: "animate-ui-fade-in bg-ui-overlay pointer-events-none fixed inset-0 z-1010 flex items-center justify-center p-6",
|
|
756
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
757
|
+
"div",
|
|
758
|
+
{
|
|
759
|
+
className: cn(
|
|
760
|
+
"border-ui-primary bg-ui-surface text-ui-fg rounded-ui-xl flex flex-col items-center gap-2 border-2 border-dashed px-10 py-8 shadow-2xl",
|
|
761
|
+
className
|
|
762
|
+
),
|
|
763
|
+
children: [
|
|
764
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-primary", children: icon ?? /* @__PURE__ */ jsxRuntime.jsx(DocumentGlyph, {}) }),
|
|
765
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-base font-semibold", children: title ?? copy.dropAnywhereTitle }),
|
|
766
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-ui-muted text-sm", children: hint ?? copy.dropAnywhereHint })
|
|
767
|
+
]
|
|
768
|
+
}
|
|
769
|
+
)
|
|
770
|
+
}
|
|
771
|
+
),
|
|
772
|
+
document.body
|
|
773
|
+
);
|
|
774
|
+
};
|
|
775
|
+
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 });
|
|
776
|
+
var useCoarsePointer = () => {
|
|
777
|
+
const [coarse, setCoarse] = react.useState(false);
|
|
778
|
+
react.useEffect(() => {
|
|
779
|
+
const query = window.matchMedia("(pointer: coarse)");
|
|
780
|
+
setCoarse(query.matches);
|
|
781
|
+
const onChange = (event) => setCoarse(event.matches);
|
|
782
|
+
query.addEventListener("change", onChange);
|
|
783
|
+
return () => query.removeEventListener("change", onChange);
|
|
784
|
+
}, []);
|
|
785
|
+
return coarse;
|
|
786
|
+
};
|
|
787
|
+
var parseShortcut = (combo) => {
|
|
788
|
+
const parts = combo.toLowerCase().split("+");
|
|
789
|
+
return {
|
|
790
|
+
mod: parts.includes("mod"),
|
|
791
|
+
shift: parts.includes("shift"),
|
|
792
|
+
alt: parts.includes("alt"),
|
|
793
|
+
key: parts[parts.length - 1]
|
|
794
|
+
};
|
|
795
|
+
};
|
|
796
|
+
var IS_MAC = typeof navigator !== "undefined" && /mac/i.test(navigator.platform);
|
|
797
|
+
var formatShortcut = (combo) => {
|
|
798
|
+
const spec = parseShortcut(combo);
|
|
799
|
+
const key = spec.key.length === 1 ? spec.key.toUpperCase() : spec.key;
|
|
800
|
+
const parts = [];
|
|
801
|
+
if (spec.mod) parts.push(IS_MAC ? "\u2318" : "Ctrl");
|
|
802
|
+
if (spec.alt) parts.push(IS_MAC ? "\u2325" : "Alt");
|
|
803
|
+
if (spec.shift) parts.push(IS_MAC ? "\u21E7" : "Shift");
|
|
804
|
+
parts.push(key);
|
|
805
|
+
return parts.join(IS_MAC ? "" : "+");
|
|
806
|
+
};
|
|
807
|
+
var active = /* @__PURE__ */ new Map();
|
|
808
|
+
var useShortcut = (combo, handler, disabled = false) => {
|
|
809
|
+
const handlerRef = react.useRef(handler);
|
|
810
|
+
handlerRef.current = handler;
|
|
811
|
+
react.useEffect(() => {
|
|
812
|
+
if (!combo || disabled) return;
|
|
813
|
+
const spec = parseShortcut(combo);
|
|
814
|
+
active.set(combo, (active.get(combo) ?? 0) + 1);
|
|
815
|
+
if ((active.get(combo) ?? 0) > 1) {
|
|
816
|
+
warnDev(
|
|
817
|
+
`shortcut-collision:${combo}`,
|
|
818
|
+
`two surfaces registered the shortcut "${combo}" at the same time \u2014 give each its own combo or scope one out.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const onKey = (event) => {
|
|
822
|
+
if (event.repeat) return;
|
|
823
|
+
const target = event.target;
|
|
824
|
+
if (target instanceof Element && target.closest("input, textarea, select, [contenteditable]")) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (event.key.toLowerCase() !== spec.key) return;
|
|
828
|
+
if (spec.mod !== (event.metaKey || event.ctrlKey)) return;
|
|
829
|
+
if (spec.shift !== event.shiftKey) return;
|
|
830
|
+
if (spec.alt !== event.altKey) return;
|
|
831
|
+
event.preventDefault();
|
|
832
|
+
handlerRef.current();
|
|
833
|
+
};
|
|
834
|
+
window.addEventListener("keydown", onKey);
|
|
835
|
+
return () => {
|
|
836
|
+
window.removeEventListener("keydown", onKey);
|
|
837
|
+
const count = (active.get(combo) ?? 1) - 1;
|
|
838
|
+
if (count <= 0) active.delete(combo);
|
|
839
|
+
else active.set(combo, count);
|
|
840
|
+
};
|
|
841
|
+
}, [combo, disabled]);
|
|
842
|
+
};
|
|
843
|
+
var Dropzone = ({
|
|
844
|
+
accept,
|
|
845
|
+
multiple = false,
|
|
846
|
+
disabled = false,
|
|
847
|
+
capture,
|
|
848
|
+
size = "md",
|
|
849
|
+
shortcut,
|
|
850
|
+
onFiles,
|
|
851
|
+
children,
|
|
852
|
+
className
|
|
853
|
+
}) => {
|
|
854
|
+
const inputRef = react.useRef(null);
|
|
855
|
+
const [dragging, setDragging] = react.useState(false);
|
|
856
|
+
const depth = react.useRef(0);
|
|
857
|
+
const coarse = useCoarsePointer();
|
|
858
|
+
useShortcut(shortcut, () => inputRef.current?.click(), disabled);
|
|
859
|
+
const emit = react.useCallback(
|
|
860
|
+
(list) => {
|
|
861
|
+
if (disabled || !list || list.length === 0) return;
|
|
862
|
+
onFiles(Array.from(list));
|
|
863
|
+
},
|
|
864
|
+
[disabled, onFiles]
|
|
865
|
+
);
|
|
866
|
+
const onDrop = react.useCallback(
|
|
867
|
+
(event) => {
|
|
868
|
+
event.preventDefault();
|
|
869
|
+
depth.current = 0;
|
|
870
|
+
setDragging(false);
|
|
871
|
+
emit(event.dataTransfer.files);
|
|
872
|
+
},
|
|
873
|
+
[emit]
|
|
874
|
+
);
|
|
875
|
+
const onPaste = react.useCallback(
|
|
876
|
+
(event) => {
|
|
877
|
+
if (event.clipboardData.files.length === 0) return;
|
|
878
|
+
event.preventDefault();
|
|
879
|
+
emit(event.clipboardData.files);
|
|
880
|
+
},
|
|
881
|
+
[emit]
|
|
882
|
+
);
|
|
883
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
884
|
+
"div",
|
|
885
|
+
{
|
|
886
|
+
role: "button",
|
|
887
|
+
tabIndex: disabled ? -1 : 0,
|
|
888
|
+
"aria-disabled": disabled,
|
|
889
|
+
"data-dragging": dragging || void 0,
|
|
890
|
+
onClick: () => !disabled && inputRef.current?.click(),
|
|
891
|
+
onKeyDown: (event) => {
|
|
892
|
+
if (disabled) return;
|
|
893
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
894
|
+
event.preventDefault();
|
|
895
|
+
inputRef.current?.click();
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
onDragEnter: () => {
|
|
899
|
+
depth.current += 1;
|
|
900
|
+
if (!disabled) setDragging(true);
|
|
901
|
+
},
|
|
902
|
+
onDragOver: (event) => event.preventDefault(),
|
|
903
|
+
onDragLeave: () => {
|
|
904
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
905
|
+
if (depth.current === 0) setDragging(false);
|
|
906
|
+
},
|
|
907
|
+
onDrop,
|
|
908
|
+
onPaste,
|
|
909
|
+
className: cn(
|
|
910
|
+
// `group/dz` + `data-dragging` let the children (the icon, the
|
|
911
|
+
// prompt) react to hover and drag without owning any state.
|
|
912
|
+
// `touch-manipulation` kills the 300ms tap delay; the active scale
|
|
913
|
+
// is the press feedback a touch screen needs in place of hover.
|
|
914
|
+
"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",
|
|
915
|
+
size === "sm" ? "p-3" : "p-5",
|
|
916
|
+
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",
|
|
917
|
+
!disabled && "active:scale-[0.98]",
|
|
918
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
919
|
+
className
|
|
920
|
+
),
|
|
921
|
+
children: [
|
|
922
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
923
|
+
"input",
|
|
924
|
+
{
|
|
925
|
+
ref: inputRef,
|
|
926
|
+
type: "file",
|
|
927
|
+
accept,
|
|
928
|
+
multiple,
|
|
929
|
+
disabled,
|
|
930
|
+
...capture ? { capture } : {},
|
|
931
|
+
className: "hidden",
|
|
932
|
+
onClick: (event) => event.stopPropagation(),
|
|
933
|
+
onChange: (event) => {
|
|
934
|
+
emit(event.target.files);
|
|
935
|
+
event.target.value = "";
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
),
|
|
939
|
+
children,
|
|
940
|
+
shortcut && !coarse && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-2 block", children: /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: formatShortcut(shortcut) }) })
|
|
941
|
+
]
|
|
942
|
+
}
|
|
943
|
+
);
|
|
944
|
+
};
|
|
945
|
+
var DownloadIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
946
|
+
"svg",
|
|
947
|
+
{
|
|
948
|
+
viewBox: "0 0 24 24",
|
|
949
|
+
fill: "none",
|
|
950
|
+
stroke: "currentColor",
|
|
951
|
+
strokeWidth: "1.75",
|
|
952
|
+
strokeLinecap: "round",
|
|
953
|
+
strokeLinejoin: "round",
|
|
954
|
+
className,
|
|
955
|
+
"aria-hidden": true,
|
|
956
|
+
children: [
|
|
957
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3v12" }),
|
|
958
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m7 11 5 5 5-5" }),
|
|
959
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 20h16" })
|
|
960
|
+
]
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
var ExternalLinkIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
964
|
+
"svg",
|
|
965
|
+
{
|
|
966
|
+
viewBox: "0 0 24 24",
|
|
967
|
+
fill: "none",
|
|
968
|
+
stroke: "currentColor",
|
|
969
|
+
strokeWidth: "1.75",
|
|
970
|
+
strokeLinecap: "round",
|
|
971
|
+
strokeLinejoin: "round",
|
|
972
|
+
className,
|
|
973
|
+
"aria-hidden": true,
|
|
974
|
+
children: [
|
|
975
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 4h6v6" }),
|
|
976
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 4 10 14" }),
|
|
977
|
+
/* @__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" })
|
|
978
|
+
]
|
|
979
|
+
}
|
|
980
|
+
);
|
|
981
|
+
var FileWarningIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
982
|
+
"svg",
|
|
983
|
+
{
|
|
984
|
+
viewBox: "0 0 24 24",
|
|
985
|
+
fill: "none",
|
|
986
|
+
stroke: "currentColor",
|
|
987
|
+
strokeWidth: "1.5",
|
|
988
|
+
strokeLinecap: "round",
|
|
989
|
+
strokeLinejoin: "round",
|
|
990
|
+
className,
|
|
991
|
+
"aria-hidden": true,
|
|
992
|
+
children: [
|
|
993
|
+
/* @__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" }),
|
|
994
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3v5h5" }),
|
|
995
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11v4" }),
|
|
996
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 18h.01" })
|
|
997
|
+
]
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
var sequence = 0;
|
|
1001
|
+
var stack = [];
|
|
1002
|
+
var useOverlayLayer = (active2) => {
|
|
1003
|
+
const idRef = react.useRef(0);
|
|
1004
|
+
if (idRef.current === 0) idRef.current = ++sequence;
|
|
1005
|
+
react.useEffect(() => {
|
|
1006
|
+
if (!active2) return;
|
|
1007
|
+
const id = idRef.current;
|
|
1008
|
+
stack.push(id);
|
|
1009
|
+
return () => {
|
|
1010
|
+
const at = stack.lastIndexOf(id);
|
|
1011
|
+
if (at !== -1) stack.splice(at, 1);
|
|
1012
|
+
};
|
|
1013
|
+
}, [active2]);
|
|
1014
|
+
return react.useMemo(
|
|
1015
|
+
() => ({ isTopmost: () => stack[stack.length - 1] === idRef.current }),
|
|
1016
|
+
[]
|
|
1017
|
+
);
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
// src/ui/scrollLock.ts
|
|
1021
|
+
var KEY = /* @__PURE__ */ Symbol.for("uploaderkit.bodyScrollLock");
|
|
1022
|
+
var state = () => {
|
|
1023
|
+
const host = globalThis;
|
|
1024
|
+
return host[KEY] ??= { count: 0, saved: "" };
|
|
1025
|
+
};
|
|
1026
|
+
var lockBodyScroll = () => {
|
|
1027
|
+
const lock = state();
|
|
1028
|
+
if (lock.count === 0) {
|
|
1029
|
+
lock.saved = document.body.style.overflow;
|
|
1030
|
+
document.body.style.overflow = "hidden";
|
|
1031
|
+
}
|
|
1032
|
+
lock.count += 1;
|
|
1033
|
+
};
|
|
1034
|
+
var unlockBodyScroll = () => {
|
|
1035
|
+
const lock = state();
|
|
1036
|
+
if (lock.count === 0) return;
|
|
1037
|
+
lock.count -= 1;
|
|
1038
|
+
if (lock.count === 0) document.body.style.overflow = lock.saved;
|
|
1039
|
+
};
|
|
1040
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
1041
|
+
var useFocusTrap = (active2, ref) => {
|
|
1042
|
+
react.useEffect(() => {
|
|
1043
|
+
if (!active2) return;
|
|
1044
|
+
const restoreTo = document.activeElement;
|
|
1045
|
+
const panel = ref.current;
|
|
1046
|
+
const initial = panel?.querySelector("[data-autofocus]") ?? panel;
|
|
1047
|
+
initial?.focus({ preventScroll: true });
|
|
1048
|
+
const onKeyDown = (event) => {
|
|
1049
|
+
if (event.key !== "Tab" || !ref.current) return;
|
|
1050
|
+
const focusables = Array.from(
|
|
1051
|
+
ref.current.querySelectorAll(FOCUSABLE)
|
|
1052
|
+
).filter((el) => el.offsetParent !== null);
|
|
1053
|
+
if (focusables.length === 0) {
|
|
1054
|
+
event.preventDefault();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const first = focusables[0];
|
|
1058
|
+
const last = focusables[focusables.length - 1];
|
|
1059
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
1060
|
+
event.preventDefault();
|
|
1061
|
+
last.focus();
|
|
1062
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
1063
|
+
event.preventDefault();
|
|
1064
|
+
first.focus();
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1068
|
+
return () => {
|
|
1069
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1070
|
+
restoreTo?.focus({ preventScroll: true });
|
|
1071
|
+
};
|
|
1072
|
+
}, [active2, ref]);
|
|
1073
|
+
};
|
|
1074
|
+
var OVERLAY_ANIMATION_MS = 200;
|
|
1075
|
+
var useOverlayTransition = (open) => {
|
|
1076
|
+
const [entered, setEntered] = react.useState(false);
|
|
1077
|
+
const [exiting, setExiting] = react.useState(false);
|
|
1078
|
+
const wasOpen = react.useRef(false);
|
|
1079
|
+
react.useEffect(() => {
|
|
1080
|
+
if (open) {
|
|
1081
|
+
wasOpen.current = true;
|
|
1082
|
+
setExiting(false);
|
|
1083
|
+
const raf = requestAnimationFrame(() => setEntered(true));
|
|
1084
|
+
return () => cancelAnimationFrame(raf);
|
|
1085
|
+
}
|
|
1086
|
+
setEntered(false);
|
|
1087
|
+
if (!wasOpen.current) return;
|
|
1088
|
+
wasOpen.current = false;
|
|
1089
|
+
setExiting(true);
|
|
1090
|
+
const timer = setTimeout(() => setExiting(false), OVERLAY_ANIMATION_MS);
|
|
1091
|
+
return () => clearTimeout(timer);
|
|
1092
|
+
}, [open]);
|
|
1093
|
+
return { mounted: open || exiting, entered };
|
|
1094
|
+
};
|
|
1095
|
+
var EMPTY_STATE_CLASS = "mx-4 flex max-w-sm flex-col items-center text-center";
|
|
1096
|
+
var EMPTY_TITLE_CLASS = "text-sm font-medium text-white";
|
|
1097
|
+
var EMPTY_HINT_CLASS = "mt-1.5 text-xs leading-relaxed text-white/60";
|
|
1098
|
+
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";
|
|
1099
|
+
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";
|
|
1100
|
+
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";
|
|
1101
|
+
var kindOf = (file) => {
|
|
1102
|
+
const mime = file.mimeType || getMimeType(file.fileName ?? file.url);
|
|
1103
|
+
if (mime.startsWith("image/")) return "image";
|
|
1104
|
+
if (mime === "application/pdf") return "pdf";
|
|
1105
|
+
return "other";
|
|
1106
|
+
};
|
|
1107
|
+
var isIosWebkit = () => {
|
|
1108
|
+
const ua = navigator.userAgent;
|
|
1109
|
+
return /iPad|iPhone|iPod/.test(ua) || ua.includes("Macintosh") && navigator.maxTouchPoints > 1;
|
|
1110
|
+
};
|
|
1111
|
+
var browserEmbedsPdf = () => {
|
|
1112
|
+
if (typeof navigator === "undefined") return false;
|
|
1113
|
+
if (navigator.pdfViewerEnabled === false) return false;
|
|
1114
|
+
return !isIosWebkit();
|
|
1115
|
+
};
|
|
1116
|
+
var Chevron = ({ direction }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1117
|
+
"svg",
|
|
1118
|
+
{
|
|
1119
|
+
viewBox: "0 0 24 24",
|
|
1120
|
+
fill: "none",
|
|
1121
|
+
stroke: "currentColor",
|
|
1122
|
+
strokeWidth: "2.5",
|
|
1123
|
+
strokeLinecap: "round",
|
|
1124
|
+
strokeLinejoin: "round",
|
|
1125
|
+
className: "h-5 w-5",
|
|
1126
|
+
"aria-hidden": true,
|
|
1127
|
+
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" })
|
|
1128
|
+
}
|
|
1129
|
+
);
|
|
1130
|
+
var FileViewer = ({
|
|
1131
|
+
file,
|
|
1132
|
+
onClose,
|
|
1133
|
+
files,
|
|
1134
|
+
resolveUrl,
|
|
1135
|
+
renderError,
|
|
1136
|
+
labels
|
|
1137
|
+
}) => {
|
|
1138
|
+
const copy = useUploaderLabels(labels);
|
|
1139
|
+
const coarse = useCoarsePointer();
|
|
1140
|
+
const [url, setUrl] = react.useState(null);
|
|
1141
|
+
const [failed, setFailed] = react.useState(false);
|
|
1142
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
1143
|
+
const retry = react.useCallback(() => setAttempt((previous) => previous + 1), []);
|
|
1144
|
+
const panelRef = react.useRef(null);
|
|
1145
|
+
const open = file !== null;
|
|
1146
|
+
const { mounted, entered } = useOverlayTransition(open);
|
|
1147
|
+
const layer = useOverlayLayer(open);
|
|
1148
|
+
useFocusTrap(open, panelRef);
|
|
1149
|
+
const list = files && files.length > 0 ? files : null;
|
|
1150
|
+
const [cursor, setCursor] = react.useState(0);
|
|
1151
|
+
react.useEffect(() => {
|
|
1152
|
+
if (!file || !list) return;
|
|
1153
|
+
const index = list.findIndex((entry) => entry.url === file.url);
|
|
1154
|
+
setCursor(index >= 0 ? index : 0);
|
|
1155
|
+
}, [file]);
|
|
1156
|
+
const current = open ? list ? list[cursor] ?? file : file : null;
|
|
1157
|
+
const lastViewRef = react.useRef(null);
|
|
1158
|
+
if (current) lastViewRef.current = current;
|
|
1159
|
+
const navigate = react.useCallback(
|
|
1160
|
+
(delta) => {
|
|
1161
|
+
if (!list) return;
|
|
1162
|
+
setCursor(
|
|
1163
|
+
(previous) => Math.min(list.length - 1, Math.max(0, previous + delta))
|
|
1164
|
+
);
|
|
1165
|
+
},
|
|
1166
|
+
[list]
|
|
1167
|
+
);
|
|
1168
|
+
const objectUrlRef = react.useRef(null);
|
|
1169
|
+
const releaseObjectUrl = react.useCallback(() => {
|
|
1170
|
+
if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
|
|
1171
|
+
objectUrlRef.current = null;
|
|
1172
|
+
}, []);
|
|
1173
|
+
const resolveRef = react.useRef(resolveUrl);
|
|
1174
|
+
resolveRef.current = resolveUrl;
|
|
1175
|
+
const currentUrl = current?.url;
|
|
1176
|
+
react.useEffect(() => {
|
|
1177
|
+
releaseObjectUrl();
|
|
1178
|
+
if (!current) {
|
|
1179
|
+
setUrl(null);
|
|
1180
|
+
setFailed(false);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
let alive = true;
|
|
1184
|
+
setUrl(null);
|
|
1185
|
+
setFailed(false);
|
|
1186
|
+
void (async () => {
|
|
1187
|
+
try {
|
|
1188
|
+
const resolve = resolveRef.current;
|
|
1189
|
+
const resolved = resolve ? await resolve(current) : current.url;
|
|
1190
|
+
const minted = resolved.startsWith("blob:");
|
|
1191
|
+
if (!alive) {
|
|
1192
|
+
if (minted) URL.revokeObjectURL(resolved);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (minted) objectUrlRef.current = resolved;
|
|
1196
|
+
setUrl(resolved);
|
|
1197
|
+
} catch {
|
|
1198
|
+
if (alive) {
|
|
1199
|
+
setUrl(null);
|
|
1200
|
+
setFailed(true);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
})();
|
|
1204
|
+
return () => {
|
|
1205
|
+
alive = false;
|
|
1206
|
+
};
|
|
1207
|
+
}, [currentUrl, attempt]);
|
|
1208
|
+
react.useEffect(() => releaseObjectUrl, [releaseObjectUrl]);
|
|
1209
|
+
const closeRef = react.useRef(onClose);
|
|
1210
|
+
closeRef.current = onClose;
|
|
1211
|
+
const downloadRef = react.useRef(null);
|
|
1212
|
+
const openTabRef = react.useRef(null);
|
|
1213
|
+
react.useEffect(() => {
|
|
1214
|
+
if (!open) return;
|
|
1215
|
+
const onKey = (event) => {
|
|
1216
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1217
|
+
if (event.key === "Escape") {
|
|
1218
|
+
if (!layer.isTopmost()) return;
|
|
1219
|
+
event.stopPropagation();
|
|
1220
|
+
closeRef.current();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (event.key === "ArrowLeft") navigate(-1);
|
|
1224
|
+
if (event.key === "ArrowRight") navigate(1);
|
|
1225
|
+
const key = event.key.toLowerCase();
|
|
1226
|
+
if (key === "d") downloadRef.current?.click();
|
|
1227
|
+
if (key === "o") openTabRef.current?.click();
|
|
1228
|
+
};
|
|
1229
|
+
window.addEventListener("keydown", onKey, true);
|
|
1230
|
+
lockBodyScroll();
|
|
1231
|
+
return () => {
|
|
1232
|
+
window.removeEventListener("keydown", onKey, true);
|
|
1233
|
+
unlockBodyScroll();
|
|
1234
|
+
};
|
|
1235
|
+
}, [open, navigate, layer]);
|
|
1236
|
+
const [canEmbedPdf] = react.useState(browserEmbedsPdf);
|
|
1237
|
+
const view = current ?? lastViewRef.current;
|
|
1238
|
+
if (!mounted || !view) return null;
|
|
1239
|
+
const kind = kindOf(view);
|
|
1240
|
+
const showsPdf = kind === "pdf" && canEmbedPdf && !failed && !!url;
|
|
1241
|
+
const hasGallery = list !== null && list.length > 1;
|
|
1242
|
+
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";
|
|
1243
|
+
return reactDom.createPortal(
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1245
|
+
"div",
|
|
1246
|
+
{
|
|
1247
|
+
className: cn(
|
|
1248
|
+
"fixed inset-0 z-1000 flex flex-col bg-black/85 transition-opacity duration-200 ease-out",
|
|
1249
|
+
entered ? "opacity-100" : "opacity-0",
|
|
1250
|
+
!open && "pointer-events-none"
|
|
1251
|
+
),
|
|
1252
|
+
onClick: onClose,
|
|
1253
|
+
role: "dialog",
|
|
1254
|
+
"aria-modal": "true",
|
|
1255
|
+
"aria-label": view.fileName ?? copy.filePreview,
|
|
1256
|
+
children: [
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1258
|
+
"div",
|
|
1259
|
+
{
|
|
1260
|
+
className: "flex items-center justify-between gap-2 px-3 py-2.5 sm:gap-3 sm:px-4 sm:py-3",
|
|
1261
|
+
onClick: (event) => event.stopPropagation(),
|
|
1262
|
+
children: [
|
|
1263
|
+
/* @__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 }),
|
|
1264
|
+
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: [
|
|
1265
|
+
cursor + 1,
|
|
1266
|
+
" / ",
|
|
1267
|
+
list.length
|
|
1268
|
+
] }),
|
|
1269
|
+
url && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1271
|
+
"a",
|
|
1272
|
+
{
|
|
1273
|
+
ref: downloadRef,
|
|
1274
|
+
href: url,
|
|
1275
|
+
download: view.fileName,
|
|
1276
|
+
"aria-label": copy.downloadShort,
|
|
1277
|
+
title: copy.downloadShort,
|
|
1278
|
+
className: ACTION_CLASS,
|
|
1279
|
+
children: coarse ? /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, { className: "h-4 w-4" }) : copy.downloadShort
|
|
1280
|
+
}
|
|
1281
|
+
),
|
|
1282
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1283
|
+
"a",
|
|
1284
|
+
{
|
|
1285
|
+
ref: openTabRef,
|
|
1286
|
+
href: url,
|
|
1287
|
+
target: "_blank",
|
|
1288
|
+
rel: "noreferrer",
|
|
1289
|
+
"aria-label": copy.openInTab,
|
|
1290
|
+
title: copy.openInTab,
|
|
1291
|
+
className: ACTION_CLASS,
|
|
1292
|
+
children: coarse ? /* @__PURE__ */ jsxRuntime.jsx(ExternalLinkIcon, { className: "h-4 w-4" }) : copy.openInTab
|
|
1293
|
+
}
|
|
1294
|
+
)
|
|
1295
|
+
] }),
|
|
1296
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1297
|
+
"button",
|
|
1298
|
+
{
|
|
1299
|
+
type: "button",
|
|
1300
|
+
onClick: onClose,
|
|
1301
|
+
"aria-label": copy.close,
|
|
1302
|
+
title: copy.close,
|
|
1303
|
+
className: ACTION_CLASS,
|
|
1304
|
+
children: coarse ? "\u2715" : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1305
|
+
"\u2715 ",
|
|
1306
|
+
copy.close
|
|
1307
|
+
] })
|
|
1308
|
+
}
|
|
1309
|
+
)
|
|
1310
|
+
]
|
|
1311
|
+
}
|
|
1312
|
+
),
|
|
1313
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1314
|
+
"div",
|
|
1315
|
+
{
|
|
1316
|
+
ref: panelRef,
|
|
1317
|
+
tabIndex: -1,
|
|
1318
|
+
className: cn(
|
|
1319
|
+
"relative flex min-h-0 flex-1 items-center justify-center p-3 outline-none sm:p-4",
|
|
1320
|
+
showsPdf && "items-stretch"
|
|
1321
|
+
),
|
|
1322
|
+
onClick: (event) => event.stopPropagation(),
|
|
1323
|
+
children: [
|
|
1324
|
+
hasGallery && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1325
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1326
|
+
"button",
|
|
1327
|
+
{
|
|
1328
|
+
type: "button",
|
|
1329
|
+
onClick: () => navigate(-1),
|
|
1330
|
+
disabled: cursor === 0,
|
|
1331
|
+
"aria-label": copy.previous,
|
|
1332
|
+
className: cn(arrow, "left-2 sm:left-4"),
|
|
1333
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { direction: "left" })
|
|
1334
|
+
}
|
|
1335
|
+
),
|
|
1336
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1337
|
+
"button",
|
|
1338
|
+
{
|
|
1339
|
+
type: "button",
|
|
1340
|
+
onClick: () => navigate(1),
|
|
1341
|
+
disabled: cursor === list.length - 1,
|
|
1342
|
+
"aria-label": copy.next,
|
|
1343
|
+
className: cn(arrow, "right-2 sm:right-4"),
|
|
1344
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { direction: "right" })
|
|
1345
|
+
}
|
|
1346
|
+
)
|
|
1347
|
+
] }),
|
|
1348
|
+
failed ? renderError?.({ file: view, retry }) ?? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: EMPTY_STATE_CLASS, children: [
|
|
1349
|
+
/* @__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" }) }),
|
|
1350
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_TITLE_CLASS, children: copy.viewerError }),
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_HINT_CLASS, children: copy.viewerErrorHint }),
|
|
1352
|
+
view.fileName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 max-w-full truncate font-mono text-[11px] text-white/40", children: view.fileName }),
|
|
1353
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 flex flex-wrap justify-center gap-2", children: [
|
|
1354
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1355
|
+
"button",
|
|
1356
|
+
{
|
|
1357
|
+
type: "button",
|
|
1358
|
+
onClick: retry,
|
|
1359
|
+
className: EMPTY_PRIMARY_CLASS,
|
|
1360
|
+
children: copy.viewerRetry
|
|
1361
|
+
}
|
|
1362
|
+
),
|
|
1363
|
+
url && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1364
|
+
"a",
|
|
1365
|
+
{
|
|
1366
|
+
href: url,
|
|
1367
|
+
download: view.fileName,
|
|
1368
|
+
className: EMPTY_SECONDARY_CLASS,
|
|
1369
|
+
children: copy.downloadShort
|
|
1370
|
+
}
|
|
1371
|
+
)
|
|
1372
|
+
] })
|
|
1373
|
+
] }) : !url ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white/70", children: copy.viewerLoading }) : kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1374
|
+
"img",
|
|
1375
|
+
{
|
|
1376
|
+
src: url,
|
|
1377
|
+
alt: view.fileName ?? "",
|
|
1378
|
+
onError: () => setFailed(true),
|
|
1379
|
+
className: cn(
|
|
1380
|
+
"rounded-ui-lg max-h-full max-w-full object-contain shadow-2xl",
|
|
1381
|
+
"transition-transform duration-200 ease-out",
|
|
1382
|
+
entered ? "scale-100" : "scale-[0.97]"
|
|
1383
|
+
)
|
|
1384
|
+
}
|
|
1385
|
+
) : showsPdf ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1386
|
+
"iframe",
|
|
1387
|
+
{
|
|
1388
|
+
src: url,
|
|
1389
|
+
title: view.fileName ?? "PDF",
|
|
1390
|
+
onError: () => setFailed(true),
|
|
1391
|
+
className: "rounded-ui-lg h-full w-full max-w-4xl bg-white shadow-2xl"
|
|
1392
|
+
}
|
|
1393
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: EMPTY_STATE_CLASS, children: [
|
|
1394
|
+
/* @__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" }) }),
|
|
1395
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: EMPTY_TITLE_CLASS, children: copy.noPreview }),
|
|
1396
|
+
view.fileName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 max-w-full truncate font-mono text-[11px] text-white/40", children: view.fileName }),
|
|
1397
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1398
|
+
"a",
|
|
1399
|
+
{
|
|
1400
|
+
href: url,
|
|
1401
|
+
download: view.fileName,
|
|
1402
|
+
target: "_blank",
|
|
1403
|
+
rel: "noreferrer",
|
|
1404
|
+
className: cn(EMPTY_PRIMARY_CLASS, "mt-5 inline-block"),
|
|
1405
|
+
children: copy.download(view.fileName ?? copy.filePreview)
|
|
1406
|
+
}
|
|
1407
|
+
)
|
|
1408
|
+
] })
|
|
1409
|
+
]
|
|
1410
|
+
}
|
|
1411
|
+
),
|
|
1412
|
+
!coarse && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1413
|
+
"div",
|
|
1414
|
+
{
|
|
1415
|
+
className: "flex items-center justify-center gap-4 pb-3 text-[11px] text-white/60",
|
|
1416
|
+
onClick: (event) => event.stopPropagation(),
|
|
1417
|
+
children: [
|
|
1418
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1419
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Esc" }),
|
|
1420
|
+
" ",
|
|
1421
|
+
copy.close
|
|
1422
|
+
] }),
|
|
1423
|
+
url && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1425
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "D" }),
|
|
1426
|
+
" ",
|
|
1427
|
+
copy.downloadShort
|
|
1428
|
+
] }),
|
|
1429
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1430
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "O" }),
|
|
1431
|
+
" ",
|
|
1432
|
+
copy.openInTab
|
|
1433
|
+
] })
|
|
1434
|
+
] }),
|
|
1435
|
+
hasGallery && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
1436
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "\u2190" }),
|
|
1437
|
+
/* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "\u2192" }),
|
|
1438
|
+
" ",
|
|
1439
|
+
copy.previous,
|
|
1440
|
+
" / ",
|
|
1441
|
+
copy.next
|
|
1442
|
+
] })
|
|
1443
|
+
]
|
|
1444
|
+
}
|
|
1445
|
+
)
|
|
1446
|
+
]
|
|
1447
|
+
}
|
|
1448
|
+
),
|
|
1449
|
+
document.body
|
|
1450
|
+
);
|
|
1451
|
+
};
|
|
1452
|
+
var GalleryUploader = ({
|
|
1453
|
+
scopes,
|
|
1454
|
+
scope,
|
|
1455
|
+
entityId,
|
|
1456
|
+
strategy,
|
|
1457
|
+
maxFiles,
|
|
1458
|
+
rename,
|
|
1459
|
+
stored = [],
|
|
1460
|
+
onUploaded,
|
|
1461
|
+
onRemoveStored,
|
|
1462
|
+
onError,
|
|
1463
|
+
resolveViewUrl,
|
|
1464
|
+
columnsClassName = "grid-cols-2 sm:grid-cols-3 md:grid-cols-4",
|
|
1465
|
+
disabled = false,
|
|
1466
|
+
labels,
|
|
1467
|
+
className
|
|
1468
|
+
}) => {
|
|
1469
|
+
const copy = useUploaderLabels(labels);
|
|
1470
|
+
const [viewing, setViewing] = react.useState(null);
|
|
1471
|
+
const uploader = useUploader({
|
|
1472
|
+
scopes,
|
|
1473
|
+
scope,
|
|
1474
|
+
entityId,
|
|
1475
|
+
strategy,
|
|
1476
|
+
multiple: true,
|
|
1477
|
+
...maxFiles !== void 0 ? { maxFiles } : {},
|
|
1478
|
+
...rename ? { rename } : {},
|
|
1479
|
+
uploadOn: "select",
|
|
1480
|
+
onUploaded,
|
|
1481
|
+
onError,
|
|
1482
|
+
labels
|
|
1483
|
+
});
|
|
1484
|
+
const storedKeys = new Set(stored.map((file) => file.key));
|
|
1485
|
+
const inFlight = uploader.files.filter(
|
|
1486
|
+
(file) => !(file.stored && storedKeys.has(file.stored.key))
|
|
1487
|
+
);
|
|
1488
|
+
const viewable = [
|
|
1489
|
+
...stored,
|
|
1490
|
+
...inFlight.flatMap((file) => file.stored ? [file.stored] : [])
|
|
1491
|
+
];
|
|
1492
|
+
const tile = "group animate-ui-fade-in border-ui-border bg-ui-surface-muted rounded-ui-lg relative aspect-square overflow-hidden border shadow-sm transition-shadow duration-200 hover:shadow-md";
|
|
1493
|
+
const action = "bg-ui-surface/90 text-ui-fg hover:bg-ui-surface rounded-ui cursor-pointer px-2 py-1 text-[11px] font-medium shadow-sm transition-colors";
|
|
1494
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
|
|
1495
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("grid gap-3", columnsClassName), children: [
|
|
1496
|
+
stored.map((file) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: tile, children: [
|
|
1497
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1498
|
+
"img",
|
|
1499
|
+
{
|
|
1500
|
+
src: file.url,
|
|
1501
|
+
alt: file.fileName,
|
|
1502
|
+
className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
1503
|
+
}
|
|
1504
|
+
),
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 bottom-0 flex justify-between gap-1 bg-gradient-to-t from-black/60 to-transparent p-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100", children: [
|
|
1506
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1507
|
+
"button",
|
|
1508
|
+
{
|
|
1509
|
+
type: "button",
|
|
1510
|
+
onClick: () => setViewing(file),
|
|
1511
|
+
className: action,
|
|
1512
|
+
children: copy.view
|
|
1513
|
+
}
|
|
1514
|
+
),
|
|
1515
|
+
onRemoveStored && !disabled ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1516
|
+
"button",
|
|
1517
|
+
{
|
|
1518
|
+
type: "button",
|
|
1519
|
+
onClick: () => onRemoveStored(file),
|
|
1520
|
+
className: cn(action, "text-ui-danger"),
|
|
1521
|
+
children: copy.remove
|
|
1522
|
+
}
|
|
1523
|
+
) : null
|
|
1524
|
+
] })
|
|
1525
|
+
] }, file.key)),
|
|
1526
|
+
inFlight.map((file) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: tile, children: [
|
|
1527
|
+
file.preview ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1528
|
+
"img",
|
|
1529
|
+
{
|
|
1530
|
+
src: file.preview,
|
|
1531
|
+
alt: file.file.name,
|
|
1532
|
+
className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
1533
|
+
}
|
|
1534
|
+
) : null,
|
|
1535
|
+
file.status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/45", children: [
|
|
1536
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-semibold text-white", children: [
|
|
1537
|
+
file.progress,
|
|
1538
|
+
"%"
|
|
1539
|
+
] }),
|
|
1540
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 w-2/3 overflow-hidden rounded-full bg-white/30", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1541
|
+
"div",
|
|
1542
|
+
{
|
|
1543
|
+
className: "h-full bg-white transition-all duration-150",
|
|
1544
|
+
style: { width: `${file.progress}%` }
|
|
1545
|
+
}
|
|
1546
|
+
) })
|
|
1547
|
+
] }) : null,
|
|
1548
|
+
file.status === "error" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-ui-danger/70 absolute inset-0 flex items-center justify-center p-2", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-[11px] leading-tight text-white", children: file.error }) }) : null,
|
|
1549
|
+
file.status !== "uploading" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 bottom-0 flex justify-between gap-1 bg-gradient-to-t from-black/60 to-transparent p-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100", children: [
|
|
1550
|
+
file.stored ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1551
|
+
"button",
|
|
1552
|
+
{
|
|
1553
|
+
type: "button",
|
|
1554
|
+
onClick: () => file.stored && setViewing(file.stored),
|
|
1555
|
+
className: action,
|
|
1556
|
+
children: copy.view
|
|
1557
|
+
}
|
|
1558
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
|
|
1559
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
type: "button",
|
|
1563
|
+
onClick: () => uploader.removeFile(file.id),
|
|
1564
|
+
className: cn(action, "text-ui-danger"),
|
|
1565
|
+
children: copy.remove
|
|
1566
|
+
}
|
|
1567
|
+
)
|
|
1568
|
+
] }) : null
|
|
1569
|
+
] }, file.id)),
|
|
1570
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1571
|
+
Dropzone,
|
|
1572
|
+
{
|
|
1573
|
+
accept: uploader.accept,
|
|
1574
|
+
multiple: true,
|
|
1575
|
+
size: "sm",
|
|
1576
|
+
disabled,
|
|
1577
|
+
onFiles: (files) => void uploader.addFiles(files),
|
|
1578
|
+
className: "flex aspect-square flex-col items-center justify-center",
|
|
1579
|
+
children: [
|
|
1580
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-muted text-3xl leading-none font-light", children: "+" }),
|
|
1581
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-muted mt-1 text-xs", children: copy.galleryAdd })
|
|
1582
|
+
]
|
|
1583
|
+
}
|
|
1584
|
+
)
|
|
1585
|
+
] }),
|
|
1586
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1587
|
+
FileViewer,
|
|
1588
|
+
{
|
|
1589
|
+
file: viewing,
|
|
1590
|
+
files: viewable,
|
|
1591
|
+
onClose: () => setViewing(null),
|
|
1592
|
+
...resolveViewUrl ? { resolveUrl: resolveViewUrl } : {}
|
|
1593
|
+
}
|
|
1594
|
+
)
|
|
1595
|
+
] });
|
|
1596
|
+
};
|
|
1597
|
+
var matchesAccept = (file, accept) => {
|
|
1598
|
+
const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean);
|
|
1599
|
+
if (tokens.length === 0) return true;
|
|
1600
|
+
const name = file.name.toLowerCase();
|
|
1601
|
+
const type = file.type.toLowerCase();
|
|
1602
|
+
return tokens.some((token) => {
|
|
1603
|
+
if (token.startsWith(".")) return name.endsWith(token);
|
|
1604
|
+
if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
|
|
1605
|
+
return type === token;
|
|
1606
|
+
});
|
|
1607
|
+
};
|
|
1608
|
+
var useDropAnywhere = ({
|
|
1609
|
+
enabled = true,
|
|
1610
|
+
onFiles,
|
|
1611
|
+
accept,
|
|
1612
|
+
onRejected
|
|
1613
|
+
}) => {
|
|
1614
|
+
const [dragging, setDragging] = react.useState(false);
|
|
1615
|
+
const onFilesRef = react.useRef(onFiles);
|
|
1616
|
+
onFilesRef.current = onFiles;
|
|
1617
|
+
const onRejectedRef = react.useRef(onRejected);
|
|
1618
|
+
onRejectedRef.current = onRejected;
|
|
1619
|
+
react.useEffect(() => {
|
|
1620
|
+
if (!enabled) {
|
|
1621
|
+
setDragging(false);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
let depth = 0;
|
|
1625
|
+
const onEnter = (event) => {
|
|
1626
|
+
if (!event.dataTransfer?.types.includes("Files")) return;
|
|
1627
|
+
depth += 1;
|
|
1628
|
+
setDragging(true);
|
|
1629
|
+
};
|
|
1630
|
+
const onLeave = () => {
|
|
1631
|
+
depth = Math.max(0, depth - 1);
|
|
1632
|
+
if (depth === 0) setDragging(false);
|
|
1633
|
+
};
|
|
1634
|
+
const onOver = (event) => event.preventDefault();
|
|
1635
|
+
const onDrop = (event) => {
|
|
1636
|
+
event.preventDefault();
|
|
1637
|
+
depth = 0;
|
|
1638
|
+
setDragging(false);
|
|
1639
|
+
const files = Array.from(event.dataTransfer?.files ?? []);
|
|
1640
|
+
if (files.length === 0) return;
|
|
1641
|
+
const accepted = accept ? files.filter((file) => matchesAccept(file, accept)) : files;
|
|
1642
|
+
const rejected = files.filter((file) => !accepted.includes(file));
|
|
1643
|
+
if (accepted.length > 0) onFilesRef.current(accepted);
|
|
1644
|
+
if (rejected.length > 0) onRejectedRef.current?.(rejected);
|
|
1645
|
+
};
|
|
1646
|
+
window.addEventListener("dragenter", onEnter);
|
|
1647
|
+
window.addEventListener("dragleave", onLeave);
|
|
1648
|
+
window.addEventListener("dragover", onOver);
|
|
1649
|
+
window.addEventListener("drop", onDrop);
|
|
1650
|
+
return () => {
|
|
1651
|
+
window.removeEventListener("dragenter", onEnter);
|
|
1652
|
+
window.removeEventListener("dragleave", onLeave);
|
|
1653
|
+
window.removeEventListener("dragover", onOver);
|
|
1654
|
+
window.removeEventListener("drop", onDrop);
|
|
1655
|
+
};
|
|
1656
|
+
}, [enabled, accept]);
|
|
1657
|
+
return { dragging };
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
exports.AvatarUploader = AvatarUploader;
|
|
1661
|
+
exports.DropAnywhereOverlay = DropAnywhereOverlay;
|
|
1662
|
+
exports.GalleryUploader = GalleryUploader;
|
|
1663
|
+
exports.useDropAnywhere = useDropAnywhere;
|
|
1664
|
+
//# sourceMappingURL=presets.cjs.map
|
|
1665
|
+
//# sourceMappingURL=presets.cjs.map
|