upload-motion 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1256 @@
1
+
2
+ //#region src/core/format.ts
3
+ const UNITS = [
4
+ "B",
5
+ "KB",
6
+ "MB",
7
+ "GB",
8
+ "TB"
9
+ ];
10
+ /**
11
+ * `25 * 1024 * 1024` → `"25 MB"`. Uses 1024-based units so limits written the
12
+ * usual way (`n * 1024 * 1024`) read back exactly as configured.
13
+ */
14
+ function formatBytes(bytes, fractionDigits = 1) {
15
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
16
+ const exponent = Math.min(Math.floor(Math.log2(bytes) / 10), UNITS.length - 1);
17
+ const value = bytes / 1024 ** exponent;
18
+ const digits = exponent === 0 || value >= 100 || Number.isInteger(value) ? 0 : fractionDigits;
19
+ return `${value.toFixed(digits)} ${UNITS[exponent]}`;
20
+ }
21
+ function formatSpeed(bytesPerSecond) {
22
+ return `${formatBytes(bytesPerSecond)}/s`;
23
+ }
24
+ /** `5` → `"5 seconds"`, `90` → `"2 minutes"`, `3900` → `"1 hour"` */
25
+ function formatDuration(seconds) {
26
+ if (!Number.isFinite(seconds) || seconds < 0) return "";
27
+ const s = Math.max(1, Math.round(seconds));
28
+ if (s < 60) return `${s} ${s === 1 ? "second" : "seconds"}`;
29
+ const m = Math.round(s / 60);
30
+ if (m < 60) return `${m} ${m === 1 ? "minute" : "minutes"}`;
31
+ const h = Math.round(m / 60);
32
+ return `${h} ${h === 1 ? "hour" : "hours"}`;
33
+ }
34
+ function formatPercent(fraction) {
35
+ return `${Math.round(clamp01(fraction) * 100)}%`;
36
+ }
37
+ function clamp01(value) {
38
+ return value < 0 ? 0 : value > 1 ? 1 : Number.isFinite(value) ? value : 0;
39
+ }
40
+ //#endregion
41
+ //#region src/core/file-kind.ts
42
+ const EXTENSION_KIND = {
43
+ jpg: "image",
44
+ jpeg: "image",
45
+ png: "image",
46
+ webp: "image",
47
+ gif: "image",
48
+ svg: "image",
49
+ avif: "image",
50
+ bmp: "image",
51
+ ico: "image",
52
+ heic: "image",
53
+ heif: "image",
54
+ tif: "image",
55
+ tiff: "image",
56
+ mp4: "video",
57
+ mov: "video",
58
+ webm: "video",
59
+ mkv: "video",
60
+ avi: "video",
61
+ m4v: "video",
62
+ ogv: "video",
63
+ mp3: "audio",
64
+ wav: "audio",
65
+ ogg: "audio",
66
+ oga: "audio",
67
+ m4a: "audio",
68
+ aac: "audio",
69
+ flac: "audio",
70
+ opus: "audio",
71
+ pdf: "pdf",
72
+ doc: "document",
73
+ docx: "document",
74
+ odt: "document",
75
+ rtf: "document",
76
+ pages: "document",
77
+ xls: "spreadsheet",
78
+ xlsx: "spreadsheet",
79
+ ods: "spreadsheet",
80
+ csv: "spreadsheet",
81
+ tsv: "spreadsheet",
82
+ numbers: "spreadsheet",
83
+ ppt: "presentation",
84
+ pptx: "presentation",
85
+ odp: "presentation",
86
+ key: "presentation",
87
+ zip: "archive",
88
+ rar: "archive",
89
+ "7z": "archive",
90
+ tar: "archive",
91
+ gz: "archive",
92
+ tgz: "archive",
93
+ bz2: "archive",
94
+ xz: "archive",
95
+ js: "code",
96
+ mjs: "code",
97
+ cjs: "code",
98
+ jsx: "code",
99
+ ts: "code",
100
+ tsx: "code",
101
+ py: "code",
102
+ rb: "code",
103
+ go: "code",
104
+ rs: "code",
105
+ java: "code",
106
+ kt: "code",
107
+ swift: "code",
108
+ c: "code",
109
+ cpp: "code",
110
+ h: "code",
111
+ cs: "code",
112
+ php: "code",
113
+ html: "code",
114
+ css: "code",
115
+ scss: "code",
116
+ sh: "code",
117
+ sql: "code",
118
+ vue: "code",
119
+ svelte: "code",
120
+ json: "data",
121
+ xml: "data",
122
+ yaml: "data",
123
+ yml: "data",
124
+ toml: "data",
125
+ ndjson: "data",
126
+ parquet: "data",
127
+ txt: "text",
128
+ md: "text",
129
+ log: "text",
130
+ ini: "text"
131
+ };
132
+ const MIME_KIND = [
133
+ [/^image\//, "image"],
134
+ [/^video\//, "video"],
135
+ [/^audio\//, "audio"],
136
+ [/^application\/pdf$/, "pdf"],
137
+ [/(msword|wordprocessingml|opendocument\.text|rtf)/, "document"],
138
+ [/(ms-excel|spreadsheetml|opendocument\.spreadsheet|text\/csv)/, "spreadsheet"],
139
+ [/(ms-powerpoint|presentationml|opendocument\.presentation)/, "presentation"],
140
+ [/(zip|x-rar|x-7z|x-tar|gzip|x-bzip)/, "archive"],
141
+ [/(javascript|typescript|x-python|x-sh)/, "code"],
142
+ [/(json|xml|yaml)/, "data"],
143
+ [/^text\//, "text"]
144
+ ];
145
+ function getExtension(name) {
146
+ const dot = name.lastIndexOf(".");
147
+ if (dot <= 0 || dot === name.length - 1) return "";
148
+ return name.slice(dot + 1).toLowerCase();
149
+ }
150
+ /** Classifies a file by extension first (more specific), then MIME type. */
151
+ function getFileKind(file) {
152
+ const fromExt = EXTENSION_KIND[getExtension(file.name)];
153
+ if (fromExt) return fromExt;
154
+ const type = (file.type ?? "").toLowerCase();
155
+ if (type) {
156
+ for (const [pattern, kind] of MIME_KIND) if (pattern.test(type)) return kind;
157
+ }
158
+ return "unknown";
159
+ }
160
+ /** Short badge label such as `PDF`, `DOCX`, `MP4`. Falls back to the kind. */
161
+ function getFileLabel(file) {
162
+ const ext = getExtension(file.name);
163
+ if (ext && ext.length <= 5) return ext.toUpperCase();
164
+ const kind = getFileKind(file);
165
+ return kind === "unknown" ? "FILE" : kind.slice(0, 4).toUpperCase();
166
+ }
167
+ /** Kinds that browsers can render as an `<img>` without extra libraries. */
168
+ function isPreviewableImage(file) {
169
+ const type = (file.type ?? "").toLowerCase();
170
+ if (type) return /^image\/(png|jpe?g|webp|gif|svg\+xml|avif|bmp|x-icon|vnd\.microsoft\.icon)$/.test(type);
171
+ return [
172
+ "png",
173
+ "jpg",
174
+ "jpeg",
175
+ "webp",
176
+ "gif",
177
+ "svg",
178
+ "avif",
179
+ "bmp",
180
+ "ico"
181
+ ].includes(getExtension(file.name));
182
+ }
183
+ //#endregion
184
+ //#region src/core/preset-catalog.ts
185
+ const info = (entry) => entry;
186
+ const PRESET_INFO = {
187
+ "card-fan": info({
188
+ id: "card-fan",
189
+ name: "Card Fan",
190
+ description: "Three media cards fan out as files approach the window, curved arrows draw themselves toward the zone, and the deck re-deals on drop. Manual upload button with inline progress.",
191
+ category: "reference",
192
+ bestFor: [
193
+ "Media libraries",
194
+ "CMS",
195
+ "Marketing tools"
196
+ ],
197
+ defaults: { autoUpload: false }
198
+ }),
199
+ "wave-bar": info({
200
+ id: "wave-bar",
201
+ name: "Wave Bar",
202
+ description: "A compact upload pill: a soft fill sweeps behind the summary while a sine wave travels along the edge, then flattens into a line on success. Pause, cancel, expand and more actions built in.",
203
+ category: "reference",
204
+ bestFor: [
205
+ "Dashboards",
206
+ "Cloud storage",
207
+ "Background uploads"
208
+ ],
209
+ defaults: { progressVariant: "wave" }
210
+ }),
211
+ "hoop-shot": info({
212
+ id: "hoop-shot",
213
+ name: "Hoop Shot",
214
+ description: "A playful, physics-driven uploader: pull a file back, watch the dotted trajectory, release to shoot it into the hoop. Real gravity, rim bounces and bank shots – plus keyboard and \"Shoot all\" for instant perfect shots.",
215
+ category: "reference",
216
+ bestFor: [
217
+ "Consumer apps",
218
+ "Onboarding",
219
+ "Portfolios",
220
+ "Delight moments"
221
+ ],
222
+ defaults: { autoUpload: false }
223
+ }),
224
+ essential: info({
225
+ id: "essential",
226
+ name: "Essential",
227
+ description: "Restrained and professional: a quiet dashed zone, thin progress bars, gentle list transitions. Motion that informs rather than performs.",
228
+ category: "professional",
229
+ bestFor: [
230
+ "Admin panels",
231
+ "Forms",
232
+ "Enterprise SaaS"
233
+ ]
234
+ }),
235
+ "segment-ring": info({
236
+ id: "segment-ring",
237
+ name: "Segment Ring",
238
+ description: "One dial, many files: each upload owns an arc sized by its bytes and coloured by type, filling independently. The ring morphs into a checkmark when the batch completes.",
239
+ category: "professional",
240
+ bestFor: [
241
+ "Batch uploads",
242
+ "Analytics",
243
+ "Cloud storage"
244
+ ],
245
+ defaults: { progressVariant: "ring" }
246
+ }),
247
+ beam: info({
248
+ id: "beam",
249
+ name: "Gradient Beam",
250
+ description: "A light beam circles the zone’s border and speeds up when files approach. Progress bars are gradient beams with a travelling highlight. Made for dark, premium UIs.",
251
+ category: "expressive",
252
+ bestFor: [
253
+ "Developer tools",
254
+ "AI products",
255
+ "Dark dashboards"
256
+ ],
257
+ defaults: { progressVariant: "beam" }
258
+ }),
259
+ magnetic: info({
260
+ id: "magnetic",
261
+ name: "Magnetic",
262
+ description: "A dot field that reacts to your cursor: the core is pulled toward the pointer, a spotlight follows it and dots swell nearby. Dropped files snap into the list with a spring.",
263
+ category: "expressive",
264
+ bestFor: [
265
+ "Landing pages",
266
+ "Creative tools",
267
+ "Design apps"
268
+ ]
269
+ }),
270
+ portal: info({
271
+ id: "portal",
272
+ name: "Portal",
273
+ description: "Files spiral into a rotating portal from wherever you drop them. The portal’s rim fills with aggregate progress and flashes when everything lands.",
274
+ category: "expressive",
275
+ bestFor: [
276
+ "Consumer apps",
277
+ "Gaming",
278
+ "Creative platforms"
279
+ ]
280
+ }),
281
+ liquid: info({
282
+ id: "liquid",
283
+ name: "Liquid Fill",
284
+ description: "The zone is a vessel that fills with liquid as bytes arrive – layered waves, rising bubbles, and text that inverts as the level passes it. Turns to success colour when full.",
285
+ category: "expressive",
286
+ bestFor: [
287
+ "Consumer apps",
288
+ "Single large uploads",
289
+ "Mobile"
290
+ ]
291
+ }),
292
+ stack: info({
293
+ id: "stack",
294
+ name: "Dimensional Stack",
295
+ description: "Files form a 3D deck: the front card uploads while the rest wait in perspective. Finished cards fly to a tray; hover to fan the deck. A full list view is one click away.",
296
+ category: "expressive",
297
+ bestFor: [
298
+ "Document workflows",
299
+ "Portfolios",
300
+ "Review queues"
301
+ ]
302
+ }),
303
+ orbit: info({
304
+ id: "orbit",
305
+ name: "Orbit",
306
+ description: "Files orbit an upload core in three lanes. Orbits tighten as bytes transfer, and each finished file falls into the core with a pulse. Failed files drift to a halt.",
307
+ category: "expressive",
308
+ bestFor: [
309
+ "Landing pages",
310
+ "Multi-file batches",
311
+ "Consumer apps"
312
+ ]
313
+ }),
314
+ bloom: info({
315
+ id: "bloom",
316
+ name: "Image Bloom",
317
+ description: "A gallery grid where each image starts blurred and desaturated, then blooms into full colour from the centre as it uploads – finishing with a petal burst.",
318
+ category: "media",
319
+ bestFor: [
320
+ "Photo galleries",
321
+ "Social apps",
322
+ "E-commerce"
323
+ ]
324
+ }),
325
+ "page-reveal": info({
326
+ id: "page-reveal",
327
+ name: "Page Reveal",
328
+ description: "Documents become pages: a scan line inks each line as bytes upload – showing the real first lines of text, CSV, JSON and code files – then an “Uploaded” stamp lands.",
329
+ category: "media",
330
+ bestFor: [
331
+ "Document management",
332
+ "Legal & finance",
333
+ "Knowledge bases"
334
+ ]
335
+ }),
336
+ filmstrip: info({
337
+ id: "filmstrip",
338
+ name: "Filmstrip",
339
+ description: "Videos turn into filmstrips using frames extracted in the browser. Frames light up as the upload advances while a playhead tracks progress.",
340
+ category: "media",
341
+ bestFor: [
342
+ "Video platforms",
343
+ "Course builders",
344
+ "Social media tools"
345
+ ]
346
+ }),
347
+ waveform: info({
348
+ id: "waveform",
349
+ name: "Audio Waveform",
350
+ description: "Audio files show their true waveform, decoded locally, filling bar by bar with an equaliser bounce at the playhead. Includes local preview playback.",
351
+ category: "media",
352
+ bestFor: [
353
+ "Podcasts",
354
+ "Music platforms",
355
+ "Voice notes"
356
+ ]
357
+ }),
358
+ pixel: info({
359
+ id: "pixel",
360
+ name: "Pixel Assembly",
361
+ description: "Every file assembles from 64 pixels flying into place in a shuffled order driven by real progress. Images use their own pixels; other files build a tinted mosaic.",
362
+ category: "media",
363
+ bestFor: [
364
+ "Creative tools",
365
+ "Games",
366
+ "Retro / playful brands"
367
+ ]
368
+ }),
369
+ adaptive: info({
370
+ id: "adaptive",
371
+ name: "Adaptive Media",
372
+ description: "Picks the right motion per file: images bloom, videos become filmstrips, audio draws its waveform, documents reveal their pages. Override the mapping with `fileAnimations`.",
373
+ category: "media",
374
+ bestFor: [
375
+ "Mixed-media inboxes",
376
+ "Asset managers",
377
+ "Chat attachments"
378
+ ]
379
+ })
380
+ };
381
+ /** Built-in presets in gallery order. */
382
+ const presetCatalog = [
383
+ PRESET_INFO["card-fan"],
384
+ PRESET_INFO["wave-bar"],
385
+ PRESET_INFO["hoop-shot"],
386
+ PRESET_INFO.essential,
387
+ PRESET_INFO["segment-ring"],
388
+ PRESET_INFO.beam,
389
+ PRESET_INFO.magnetic,
390
+ PRESET_INFO.portal,
391
+ PRESET_INFO.liquid,
392
+ PRESET_INFO.stack,
393
+ PRESET_INFO.orbit,
394
+ PRESET_INFO.bloom,
395
+ PRESET_INFO["page-reveal"],
396
+ PRESET_INFO.filmstrip,
397
+ PRESET_INFO.waveform,
398
+ PRESET_INFO.pixel,
399
+ PRESET_INFO.adaptive
400
+ ];
401
+ //#endregion
402
+ //#region src/core/validation.ts
403
+ /** Normalises `accept` to lower-case tokens: `['image/*', '.pdf', 'application/json']`. */
404
+ function parseAccept(accept) {
405
+ if (!accept) return [];
406
+ return (typeof accept === "string" ? accept.split(",") : accept.flatMap((a) => a.split(","))).map((token) => token.trim().toLowerCase()).filter(Boolean);
407
+ }
408
+ /** Mirrors the browser's `accept` matching rules. An empty list accepts everything. */
409
+ function isFileAccepted(file, accept) {
410
+ if (accept.length === 0) return true;
411
+ const type = (file.type ?? "").toLowerCase();
412
+ const ext = getExtension(file.name);
413
+ return accept.some((token) => {
414
+ if (token.startsWith(".")) return ext !== "" && token.slice(1) === ext;
415
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
416
+ return type === token;
417
+ });
418
+ }
419
+ const MIME_NAMES = {
420
+ "application/pdf": "PDF",
421
+ "application/json": "JSON",
422
+ "application/zip": "ZIP",
423
+ "text/csv": "CSV",
424
+ "text/plain": "TXT",
425
+ "application/msword": "DOC",
426
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
427
+ "application/vnd.ms-excel": "XLS",
428
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
429
+ "application/vnd.ms-powerpoint": "PPT",
430
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX"
431
+ };
432
+ /** Human description of an accept list: `"Images, PDF"`. */
433
+ function describeAccept(accept) {
434
+ const words = accept.map((token) => {
435
+ if (token === "image/*") return "Images";
436
+ if (token === "video/*") return "Videos";
437
+ if (token === "audio/*") return "Audio";
438
+ if (token === "text/*") return "Text files";
439
+ if (token.startsWith(".")) return token.slice(1).toUpperCase();
440
+ if (MIME_NAMES[token]) return MIME_NAMES[token];
441
+ return (token.split("/")[1] ?? token).replace(/^(x-|vnd\.)/, "").split(/[.+-]/)[0].toUpperCase();
442
+ });
443
+ return Array.from(new Set(words)).join(", ");
444
+ }
445
+ function validateFile(file, rules, items) {
446
+ const errors = [];
447
+ if (!isFileAccepted(file, rules.accept)) errors.push({
448
+ code: "file-invalid-type",
449
+ message: `${file.name} isn't an accepted file type (${describeAccept(rules.accept)}).`
450
+ });
451
+ if (rules.maxSize !== void 0 && file.size > rules.maxSize) errors.push({
452
+ code: "file-too-large",
453
+ message: `${file.name} is ${formatBytes(file.size)}; the limit is ${formatBytes(rules.maxSize)}.`
454
+ });
455
+ if (rules.minSize !== void 0 && file.size < rules.minSize) errors.push({
456
+ code: "file-too-small",
457
+ message: `${file.name} is smaller than the ${formatBytes(rules.minSize)} minimum.`
458
+ });
459
+ if (rules.validate) {
460
+ const custom = rules.validate(file, { items });
461
+ if (custom) errors.push({
462
+ code: "custom",
463
+ message: custom
464
+ });
465
+ }
466
+ return errors;
467
+ }
468
+ //#endregion
469
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/typeof.js
470
+ function _typeof(o) {
471
+ "@babel/helpers - typeof";
472
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
473
+ return typeof o;
474
+ } : function(o) {
475
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
476
+ }, _typeof(o);
477
+ }
478
+ //#endregion
479
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/toPrimitive.js
480
+ function toPrimitive(t, r) {
481
+ if ("object" != _typeof(t) || !t) return t;
482
+ var e = t[Symbol.toPrimitive];
483
+ if (void 0 !== e) {
484
+ var i = e.call(t, r || "default");
485
+ if ("object" != _typeof(i)) return i;
486
+ throw new TypeError("@@toPrimitive must return a primitive value.");
487
+ }
488
+ return ("string" === r ? String : Number)(t);
489
+ }
490
+ //#endregion
491
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/toPropertyKey.js
492
+ function toPropertyKey(t) {
493
+ var i = toPrimitive(t, "string");
494
+ return "symbol" == _typeof(i) ? i : i + "";
495
+ }
496
+ //#endregion
497
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/defineProperty.js
498
+ function _defineProperty(e, r, t) {
499
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
500
+ value: t,
501
+ enumerable: !0,
502
+ configurable: !0,
503
+ writable: !0
504
+ }) : e[r] = t, e;
505
+ }
506
+ //#endregion
507
+ //#region src/core/errors.ts
508
+ const BRAND = Symbol.for("upload-motion.UploadError");
509
+ /** Error type produced by built-in transports and by the store when wrapping handler failures. */
510
+ var UploadError = class extends Error {
511
+ constructor(code, message, init = {}) {
512
+ super(message, init.cause === void 0 ? void 0 : { cause: init.cause });
513
+ _defineProperty(this, "code", void 0);
514
+ _defineProperty(this, "status", void 0);
515
+ _defineProperty(this, "retryable", void 0);
516
+ _defineProperty(this, "response", void 0);
517
+ _defineProperty(this, BRAND, true);
518
+ this.name = "UploadError";
519
+ this.code = code;
520
+ this.status = init.status;
521
+ this.response = init.response;
522
+ this.retryable = init.retryable ?? defaultRetryable(code, init.status);
523
+ }
524
+ };
525
+ function defaultRetryable(code, status) {
526
+ if (code === "network" || code === "timeout") return true;
527
+ if (code === "http" && status !== void 0) return status >= 500 || status === 408 || status === 429;
528
+ return code === "handler";
529
+ }
530
+ /** Works across bundle copies (no `instanceof` pitfalls). */
531
+ function isUploadError(value) {
532
+ return typeof value === "object" && value !== null && value[BRAND] === true;
533
+ }
534
+ function httpErrorMessage(status) {
535
+ if (status === 401 || status === 403) return `Not allowed to upload (HTTP ${status})`;
536
+ if (status === 404) return "Upload endpoint not found (HTTP 404)";
537
+ if (status === 408) return "The server timed out (HTTP 408)";
538
+ if (status === 413) return "File is too large for the server (HTTP 413)";
539
+ if (status === 415) return "The server rejected this file type (HTTP 415)";
540
+ if (status === 429) return "Too many uploads – try again shortly (HTTP 429)";
541
+ if (status >= 500) return `Server error (HTTP ${status})`;
542
+ return `Upload failed (HTTP ${status})`;
543
+ }
544
+ function toUploadError(error) {
545
+ if (isUploadError(error)) return error;
546
+ if (error instanceof DOMException && error.name === "AbortError") return new UploadError("aborted", "Upload aborted", {
547
+ cause: error,
548
+ retryable: false
549
+ });
550
+ return new UploadError("handler", error instanceof Error && error.message ? error.message : "Upload failed", { cause: error });
551
+ }
552
+ //#endregion
553
+ //#region src/core/store.ts
554
+ let idCounter = 0;
555
+ function createId(prefix) {
556
+ idCounter += 1;
557
+ return `${prefix}_${idCounter.toString(36)}${Math.random().toString(36).slice(2, 7)}`;
558
+ }
559
+ const now = () => Date.now();
560
+ const scheduleFrame = typeof requestAnimationFrame === "function" ? (cb) => {
561
+ const handle = requestAnimationFrame(cb);
562
+ return () => cancelAnimationFrame(handle);
563
+ } : (cb) => {
564
+ const handle = setTimeout(cb, 16);
565
+ return () => clearTimeout(handle);
566
+ };
567
+ const IN_FLIGHT = /* @__PURE__ */ new Set([
568
+ "queued",
569
+ "uploading",
570
+ "paused",
571
+ "success"
572
+ ]);
573
+ function computeTotals(items) {
574
+ const totals = {
575
+ count: items.length,
576
+ progress: 0,
577
+ loaded: 0,
578
+ size: 0,
579
+ speed: 0,
580
+ eta: null,
581
+ pending: 0,
582
+ queued: 0,
583
+ uploading: 0,
584
+ paused: 0,
585
+ success: 0,
586
+ error: 0,
587
+ canceled: 0,
588
+ active: 0,
589
+ isUploading: false,
590
+ isComplete: false
591
+ };
592
+ let weighted = 0;
593
+ let weightedLoaded = 0;
594
+ for (const item of items) {
595
+ totals[item.status] += 1;
596
+ if (!IN_FLIGHT.has(item.status)) continue;
597
+ const weight = Math.max(item.size, 1);
598
+ weighted += weight;
599
+ weightedLoaded += item.status === "success" ? weight : item.progress * weight;
600
+ totals.size += item.size;
601
+ totals.loaded += item.status === "success" ? item.size : item.loaded;
602
+ if (item.status === "uploading") totals.speed += item.speed;
603
+ }
604
+ totals.progress = weighted > 0 ? clamp01(weightedLoaded / weighted) : 0;
605
+ totals.active = totals.queued + totals.uploading;
606
+ totals.isUploading = totals.uploading > 0;
607
+ totals.eta = totals.speed > 0 ? Math.max(0, (totals.size - totals.loaded) / totals.speed) : null;
608
+ totals.isComplete = totals.success > 0 && totals.pending + totals.queued + totals.uploading + totals.paused === 0;
609
+ return totals;
610
+ }
611
+ function normalizeIds(ids, all) {
612
+ if (ids === void 0) return [...all];
613
+ return typeof ids === "string" ? [ids] : [...ids];
614
+ }
615
+ function createUploadStore(initialOptions = {}) {
616
+ let options = initialOptions;
617
+ const items = /* @__PURE__ */ new Map();
618
+ let order = [];
619
+ const runtimes = /* @__PURE__ */ new Map();
620
+ let rejections = [];
621
+ let snapshot = null;
622
+ const listeners = /* @__PURE__ */ new Set();
623
+ const eventHandlers = /* @__PURE__ */ new Map();
624
+ let cancelScheduled = null;
625
+ let completeArmed = false;
626
+ function notify() {
627
+ if (cancelScheduled) {
628
+ cancelScheduled();
629
+ cancelScheduled = null;
630
+ }
631
+ for (const listener of [...listeners]) listener();
632
+ }
633
+ /** `sync` for state transitions, batched for progress ticks. */
634
+ function commit(sync) {
635
+ snapshot = null;
636
+ if (sync) notify();
637
+ else if (!cancelScheduled) cancelScheduled = scheduleFrame(() => {
638
+ cancelScheduled = null;
639
+ notify();
640
+ });
641
+ if (options.onChange) options.onChange(order.map((id) => items.get(id)));
642
+ }
643
+ function emit(event, payload) {
644
+ const handlers = eventHandlers.get(event);
645
+ if (handlers) for (const handler of [...handlers]) handler(payload);
646
+ }
647
+ function patch(id, changes) {
648
+ const current = items.get(id);
649
+ if (!current) return void 0;
650
+ const next = {
651
+ ...current,
652
+ ...changes
653
+ };
654
+ items.set(id, next);
655
+ return next;
656
+ }
657
+ function runtime(id) {
658
+ let rt = runtimes.get(id);
659
+ if (!rt) {
660
+ rt = {
661
+ controller: null,
662
+ reason: null,
663
+ lastTime: 0,
664
+ lastLoaded: 0,
665
+ autoRetries: 0,
666
+ retryTimer: null
667
+ };
668
+ runtimes.set(id, rt);
669
+ }
670
+ return rt;
671
+ }
672
+ function clearRetryTimer(id) {
673
+ const rt = runtimes.get(id);
674
+ if (rt?.retryTimer) {
675
+ clearTimeout(rt.retryTimer);
676
+ rt.retryTimer = null;
677
+ }
678
+ }
679
+ function retryConfig() {
680
+ const retry = options.retry;
681
+ if (typeof retry === "number") return {
682
+ attempts: retry,
683
+ delay: 800
684
+ };
685
+ return {
686
+ attempts: retry?.attempts ?? 0,
687
+ delay: retry?.delay ?? 800
688
+ };
689
+ }
690
+ function pump() {
691
+ if (!options.upload) return;
692
+ const limit = Math.max(1, Math.floor(options.concurrency ?? 3));
693
+ let active = 0;
694
+ for (const id of order) if (items.get(id)?.status === "uploading") active += 1;
695
+ const t = now();
696
+ for (const id of order) {
697
+ if (active >= limit) break;
698
+ const item = items.get(id);
699
+ if (item?.status === "queued" && (item.retryAt === null || item.retryAt <= t)) {
700
+ run(id);
701
+ active += 1;
702
+ }
703
+ }
704
+ }
705
+ function checkComplete() {
706
+ if (!completeArmed) return;
707
+ const list = order.map((id) => items.get(id));
708
+ if (list.some((item) => item.status === "queued" || item.status === "uploading")) return;
709
+ completeArmed = false;
710
+ const settled = list.filter((item) => item.startedAt !== null);
711
+ emit("complete", settled);
712
+ options.onQueueComplete?.(settled);
713
+ }
714
+ function run(id) {
715
+ const handler = options.upload;
716
+ const initial = items.get(id);
717
+ if (!handler || !initial) return;
718
+ completeArmed = true;
719
+ const rt = runtime(id);
720
+ const controller = new AbortController();
721
+ rt.controller = controller;
722
+ rt.reason = null;
723
+ rt.lastTime = now();
724
+ rt.lastLoaded = 0;
725
+ const started = patch(id, {
726
+ status: "uploading",
727
+ progress: 0,
728
+ loaded: 0,
729
+ speed: 0,
730
+ eta: null,
731
+ attempts: initial.attempts + 1,
732
+ error: null,
733
+ startedAt: now(),
734
+ completedAt: null,
735
+ retryAt: null
736
+ });
737
+ commit(true);
738
+ emit("start", started);
739
+ options.onUploadStart?.(started);
740
+ let settled = false;
741
+ const onProgress = (loaded, total) => {
742
+ if (settled || controller.signal.aborted) return;
743
+ const current = items.get(id);
744
+ if (!current || current.status !== "uploading") return;
745
+ const denominator = total && total > 0 ? total : Math.max(current.size, 1);
746
+ const fraction = Math.max(current.progress, clamp01(loaded / denominator));
747
+ const fileLoaded = Math.round(fraction * current.size);
748
+ const t = now();
749
+ let speed = current.speed;
750
+ const dt = t - rt.lastTime;
751
+ if (dt >= 250) {
752
+ const instant = (fileLoaded - rt.lastLoaded) / dt * 1e3;
753
+ speed = speed > 0 ? speed * .6 + instant * .4 : instant;
754
+ rt.lastTime = t;
755
+ rt.lastLoaded = fileLoaded;
756
+ }
757
+ const eta = speed > 0 ? Math.max(0, (current.size - fileLoaded) / speed) : null;
758
+ const next = patch(id, {
759
+ progress: fraction,
760
+ loaded: fileLoaded,
761
+ speed,
762
+ eta
763
+ });
764
+ commit(false);
765
+ options.onUploadProgress?.(next);
766
+ };
767
+ const finish = (outcome) => {
768
+ if (settled) return;
769
+ settled = true;
770
+ rt.controller = null;
771
+ const reason = rt.reason;
772
+ rt.reason = null;
773
+ const current = items.get(id);
774
+ if (!current) {
775
+ pump();
776
+ checkComplete();
777
+ return;
778
+ }
779
+ if (reason === "pause") patch(id, {
780
+ status: "paused",
781
+ speed: 0,
782
+ eta: null
783
+ });
784
+ else if (reason === "cancel" || reason === "dispose") {
785
+ const canceled = patch(id, {
786
+ status: "canceled",
787
+ speed: 0,
788
+ eta: null
789
+ });
790
+ emit("cancel", canceled);
791
+ options.onUploadCancel?.(canceled);
792
+ } else if (outcome.ok) {
793
+ const done = patch(id, {
794
+ status: "success",
795
+ progress: 1,
796
+ loaded: current.size,
797
+ speed: 0,
798
+ eta: 0,
799
+ response: outcome.response,
800
+ completedAt: now()
801
+ });
802
+ rt.autoRetries = 0;
803
+ emit("success", done);
804
+ options.onUploadSuccess?.(done, outcome.response);
805
+ } else {
806
+ const error = toUploadError(outcome.error);
807
+ const retry = retryConfig();
808
+ if (error.code === "aborted") {
809
+ const canceled = patch(id, {
810
+ status: "canceled",
811
+ speed: 0,
812
+ eta: null,
813
+ error: null
814
+ });
815
+ emit("cancel", canceled);
816
+ options.onUploadCancel?.(canceled);
817
+ } else if (error.retryable && rt.autoRetries < retry.attempts) {
818
+ rt.autoRetries += 1;
819
+ const delay = retry.delay * 2 ** (rt.autoRetries - 1);
820
+ patch(id, {
821
+ status: "queued",
822
+ error,
823
+ speed: 0,
824
+ eta: null,
825
+ retryAt: now() + delay
826
+ });
827
+ rt.retryTimer = setTimeout(() => {
828
+ rt.retryTimer = null;
829
+ if (items.get(id)?.status === "queued") {
830
+ patch(id, { retryAt: null });
831
+ pump();
832
+ commit(true);
833
+ }
834
+ }, delay);
835
+ } else {
836
+ const failed = patch(id, {
837
+ status: "error",
838
+ error,
839
+ speed: 0,
840
+ eta: null
841
+ });
842
+ emit("error", failed);
843
+ options.onUploadError?.(failed, error);
844
+ }
845
+ }
846
+ commit(true);
847
+ pump();
848
+ checkComplete();
849
+ };
850
+ const context = {
851
+ file: started.file,
852
+ item: started,
853
+ signal: controller.signal,
854
+ attempt: started.attempts,
855
+ onProgress
856
+ };
857
+ Promise.resolve().then(() => handler(context)).then((response) => finish({
858
+ ok: true,
859
+ response
860
+ }), (error) => finish({
861
+ ok: false,
862
+ error
863
+ }));
864
+ }
865
+ function abortItem(id, reason) {
866
+ const rt = runtimes.get(id);
867
+ if (rt?.controller) {
868
+ rt.reason = reason;
869
+ rt.controller.abort(reason);
870
+ return true;
871
+ }
872
+ return false;
873
+ }
874
+ const store = {
875
+ getSnapshot() {
876
+ if (!snapshot) {
877
+ const list = order.map((id) => items.get(id));
878
+ const handler = options.upload;
879
+ snapshot = {
880
+ items: list,
881
+ rejections,
882
+ totals: computeTotals(list),
883
+ transport: handler ? handler.simulated ? "simulated" : "network" : "none"
884
+ };
885
+ }
886
+ return snapshot;
887
+ },
888
+ subscribe(listener) {
889
+ listeners.add(listener);
890
+ return () => listeners.delete(listener);
891
+ },
892
+ on(event, handler) {
893
+ let set = eventHandlers.get(event);
894
+ if (!set) {
895
+ set = /* @__PURE__ */ new Set();
896
+ eventHandlers.set(event, set);
897
+ }
898
+ set.add(handler);
899
+ return () => set.delete(handler);
900
+ },
901
+ setOptions(next) {
902
+ const transportChanged = Boolean(next.upload) !== Boolean(options.upload) || next.upload?.simulated !== options.upload?.simulated;
903
+ options = next;
904
+ if (transportChanged) commit(true);
905
+ pump();
906
+ },
907
+ getOptions() {
908
+ return options;
909
+ },
910
+ getInitialOptions() {
911
+ return initialOptions;
912
+ },
913
+ addFiles(input) {
914
+ const files = Array.from(input);
915
+ const accept = parseAccept(options.accept);
916
+ const single = options.multiple === false;
917
+ const maxFiles = single ? 1 : options.maxFiles ?? Infinity;
918
+ const currentList = order.map((id) => items.get(id));
919
+ let slots = single ? 1 : maxFiles - currentList.length;
920
+ const acceptedFiles = [];
921
+ const rejected = [];
922
+ files.forEach((file, index) => {
923
+ const errors = validateFile(file, {
924
+ accept,
925
+ maxSize: options.maxSize,
926
+ minSize: options.minSize,
927
+ validate: options.validate
928
+ }, currentList);
929
+ if (errors.length === 0 && (slots <= 0 || single && index > 0)) errors.push({
930
+ code: "too-many-files",
931
+ message: single ? `Only one file can be uploaded – ${file.name} was skipped.` : `You can add up to ${maxFiles} ${maxFiles === 1 ? "file" : "files"} – ${file.name} was skipped.`
932
+ });
933
+ if (errors.length > 0) rejected.push({
934
+ id: createId("rej"),
935
+ file,
936
+ errors,
937
+ at: now()
938
+ });
939
+ else {
940
+ acceptedFiles.push(file);
941
+ slots -= 1;
942
+ }
943
+ });
944
+ if (single && acceptedFiles.length > 0 && order.length > 0) store.remove([...order]);
945
+ const autoStart = Boolean(options.upload) && options.autoUpload !== false;
946
+ const accepted = acceptedFiles.map((file) => {
947
+ const item = {
948
+ id: createId("file"),
949
+ file,
950
+ name: file.name,
951
+ size: file.size,
952
+ type: file.type,
953
+ extension: getExtension(file.name),
954
+ kind: getFileKind(file),
955
+ status: autoStart ? "queued" : "pending",
956
+ progress: 0,
957
+ loaded: 0,
958
+ speed: 0,
959
+ eta: null,
960
+ attempts: 0,
961
+ error: null,
962
+ response: void 0,
963
+ addedAt: now(),
964
+ startedAt: null,
965
+ completedAt: null,
966
+ retryAt: null
967
+ };
968
+ items.set(item.id, item);
969
+ order.push(item.id);
970
+ return item;
971
+ });
972
+ if (rejected.length > 0) rejections = [...rejections, ...rejected].slice(-20);
973
+ if (accepted.length > 0 || rejected.length > 0) commit(true);
974
+ if (accepted.length > 0) {
975
+ emit("added", accepted);
976
+ options.onFilesAdded?.(accepted);
977
+ }
978
+ if (rejected.length > 0) {
979
+ emit("rejected", rejected);
980
+ options.onFilesRejected?.(rejected);
981
+ }
982
+ pump();
983
+ return {
984
+ accepted,
985
+ rejected
986
+ };
987
+ },
988
+ start(ids) {
989
+ let changed = false;
990
+ for (const id of normalizeIds(ids, order)) if (items.get(id)?.status === "pending" && options.upload) {
991
+ patch(id, { status: "queued" });
992
+ changed = true;
993
+ }
994
+ if (changed) commit(true);
995
+ pump();
996
+ },
997
+ cancel(ids) {
998
+ let changed = false;
999
+ for (const id of normalizeIds(ids, order)) {
1000
+ const item = items.get(id);
1001
+ if (!item) continue;
1002
+ clearRetryTimer(id);
1003
+ if (item.status === "uploading") abortItem(id, "cancel");
1004
+ else if (item.status === "queued" || item.status === "paused" || item.status === "pending") {
1005
+ const canceled = patch(id, {
1006
+ status: "canceled",
1007
+ speed: 0,
1008
+ eta: null,
1009
+ retryAt: null
1010
+ });
1011
+ emit("cancel", canceled);
1012
+ options.onUploadCancel?.(canceled);
1013
+ changed = true;
1014
+ }
1015
+ }
1016
+ if (changed) {
1017
+ commit(true);
1018
+ checkComplete();
1019
+ }
1020
+ },
1021
+ retry(ids) {
1022
+ let changed = false;
1023
+ for (const id of normalizeIds(ids, order)) {
1024
+ const item = items.get(id);
1025
+ if (!item || item.status !== "error" && item.status !== "canceled") continue;
1026
+ clearRetryTimer(id);
1027
+ runtime(id).autoRetries = 0;
1028
+ patch(id, {
1029
+ status: options.upload ? "queued" : "pending",
1030
+ error: null,
1031
+ progress: 0,
1032
+ loaded: 0,
1033
+ speed: 0,
1034
+ eta: null,
1035
+ retryAt: null
1036
+ });
1037
+ changed = true;
1038
+ }
1039
+ if (changed) commit(true);
1040
+ pump();
1041
+ },
1042
+ pause(ids) {
1043
+ let changed = false;
1044
+ for (const id of normalizeIds(ids, order)) {
1045
+ const item = items.get(id);
1046
+ if (!item) continue;
1047
+ if (item.status === "uploading") abortItem(id, "pause");
1048
+ else if (item.status === "queued") {
1049
+ clearRetryTimer(id);
1050
+ patch(id, {
1051
+ status: "paused",
1052
+ retryAt: null
1053
+ });
1054
+ changed = true;
1055
+ }
1056
+ }
1057
+ if (changed) commit(true);
1058
+ },
1059
+ resume(ids) {
1060
+ let changed = false;
1061
+ for (const id of normalizeIds(ids, order)) if (items.get(id)?.status === "paused") {
1062
+ patch(id, { status: options.upload ? "queued" : "pending" });
1063
+ changed = true;
1064
+ }
1065
+ if (changed) commit(true);
1066
+ pump();
1067
+ },
1068
+ remove(ids) {
1069
+ const list = typeof ids === "string" ? [ids] : ids;
1070
+ const removed = [];
1071
+ for (const id of list) {
1072
+ const item = items.get(id);
1073
+ if (!item) continue;
1074
+ clearRetryTimer(id);
1075
+ abortItem(id, "remove");
1076
+ items.delete(id);
1077
+ runtimes.delete(id);
1078
+ removed.push(item);
1079
+ }
1080
+ if (removed.length === 0) return;
1081
+ const removedIds = new Set(removed.map((item) => item.id));
1082
+ order = order.filter((id) => !removedIds.has(id));
1083
+ commit(true);
1084
+ for (const item of removed) {
1085
+ emit("removed", item);
1086
+ options.onFileRemoved?.(item);
1087
+ }
1088
+ pump();
1089
+ checkComplete();
1090
+ },
1091
+ clear(filter = "all") {
1092
+ const match = {
1093
+ all: () => true,
1094
+ completed: (item) => item.status === "success",
1095
+ failed: (item) => item.status === "error",
1096
+ canceled: (item) => item.status === "canceled"
1097
+ };
1098
+ const ids = order.filter((id) => match[filter](items.get(id)));
1099
+ if (filter === "all") rejections = [];
1100
+ if (ids.length > 0) store.remove(ids);
1101
+ else if (filter === "all") commit(true);
1102
+ },
1103
+ dismissRejection(id) {
1104
+ const next = id === void 0 ? [] : rejections.filter((rejection) => rejection.id !== id);
1105
+ if (next.length !== rejections.length) {
1106
+ rejections = next;
1107
+ commit(true);
1108
+ }
1109
+ },
1110
+ getItem(id) {
1111
+ return items.get(id);
1112
+ },
1113
+ dispose() {
1114
+ for (const id of order) {
1115
+ clearRetryTimer(id);
1116
+ abortItem(id, "dispose");
1117
+ }
1118
+ if (cancelScheduled) {
1119
+ cancelScheduled();
1120
+ cancelScheduled = null;
1121
+ }
1122
+ }
1123
+ };
1124
+ return store;
1125
+ }
1126
+ //#endregion
1127
+ //#region src/core/transports.ts
1128
+ function resolve(value, item) {
1129
+ return typeof value === "function" ? value(item) : value;
1130
+ }
1131
+ function defaultParse(xhr) {
1132
+ const text = xhr.responseText;
1133
+ if ((xhr.getResponseHeader("content-type") ?? "").includes("json") || /^\s*[[{]/.test(text)) try {
1134
+ return JSON.parse(text);
1135
+ } catch {
1136
+ return text;
1137
+ }
1138
+ return text;
1139
+ }
1140
+ /**
1141
+ * Real network transport built on `XMLHttpRequest`, the only browser API that
1142
+ * reports upload progress for request bodies on HTTP/1.1.
1143
+ */
1144
+ function createXhrUploader(options) {
1145
+ const { method = "POST", body = "form-data", fieldName = "file", withCredentials = false, timeout = 0, parseResponse = defaultParse, isSuccess = (status) => status >= 200 && status < 300 } = options;
1146
+ return async function xhrUpload({ file, item, signal, onProgress }) {
1147
+ const [url, headers, fields] = await Promise.all([
1148
+ resolve(options.url, item),
1149
+ options.headers ? resolve(options.headers, item) : void 0,
1150
+ options.formData ? resolve(options.formData, item) : void 0
1151
+ ]);
1152
+ if (signal.aborted) throw new UploadError("aborted", "Upload aborted", { retryable: false });
1153
+ return new Promise((resolvePromise, reject) => {
1154
+ const xhr = new XMLHttpRequest();
1155
+ xhr.open(method, url, true);
1156
+ xhr.withCredentials = withCredentials;
1157
+ xhr.timeout = timeout;
1158
+ for (const [key, value] of Object.entries(headers ?? {})) xhr.setRequestHeader(key, value);
1159
+ const onAbort = () => xhr.abort();
1160
+ signal.addEventListener("abort", onAbort, { once: true });
1161
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
1162
+ xhr.upload.onprogress = (event) => {
1163
+ if (event.lengthComputable) onProgress(event.loaded, event.total);
1164
+ };
1165
+ xhr.onload = () => {
1166
+ cleanup();
1167
+ if (isSuccess(xhr.status)) {
1168
+ try {
1169
+ resolvePromise(parseResponse(xhr));
1170
+ } catch (error) {
1171
+ reject(new UploadError("invalid-response", "Could not read the server response", {
1172
+ cause: error,
1173
+ retryable: false
1174
+ }));
1175
+ }
1176
+ return;
1177
+ }
1178
+ let response;
1179
+ try {
1180
+ response = defaultParse(xhr);
1181
+ } catch {
1182
+ response = void 0;
1183
+ }
1184
+ reject(new UploadError("http", (typeof response === "object" && response !== null && typeof response.error === "string" ? response.error : void 0) ?? httpErrorMessage(xhr.status), {
1185
+ status: xhr.status,
1186
+ response
1187
+ }));
1188
+ };
1189
+ xhr.onerror = () => {
1190
+ cleanup();
1191
+ reject(new UploadError("network", "Network error – check your connection and try again"));
1192
+ };
1193
+ xhr.ontimeout = () => {
1194
+ cleanup();
1195
+ reject(new UploadError("timeout", "The upload timed out"));
1196
+ };
1197
+ xhr.onabort = () => {
1198
+ cleanup();
1199
+ reject(new UploadError("aborted", "Upload aborted", { retryable: false }));
1200
+ };
1201
+ if (body === "raw") xhr.send(file);
1202
+ else {
1203
+ const form = new FormData();
1204
+ for (const [key, value] of Object.entries(fields ?? {})) form.append(key, value);
1205
+ form.append(fieldName, file, file.name);
1206
+ xhr.send(form);
1207
+ }
1208
+ });
1209
+ };
1210
+ }
1211
+ /**
1212
+ * Demo transport: no bytes leave the browser. The returned handler is flagged
1213
+ * `simulated: true` so built-in components display a "Simulated" badge.
1214
+ */
1215
+ function createSimulatedUploader(options = {}) {
1216
+ const { speed = [15e5, 6e6], minDuration = 1400, failureRate = 0, latency = 250 } = options;
1217
+ const handler = ({ file, signal, onProgress }) => new Promise((resolvePromise, reject) => {
1218
+ const bytesPerSecond = Array.isArray(speed) ? speed[0] + Math.random() * (speed[1] - speed[0]) : speed;
1219
+ const total = Math.max(file.size, 1);
1220
+ const duration = Math.max(minDuration, total / bytesPerSecond * 1e3);
1221
+ const failAt = Math.random() < failureRate ? .25 + Math.random() * .5 : Infinity;
1222
+ let elapsed = 0;
1223
+ let timer;
1224
+ const tickMs = 80;
1225
+ const abort = () => {
1226
+ if (timer !== void 0) clearTimeout(timer);
1227
+ reject(new UploadError("aborted", "Upload aborted", { retryable: false }));
1228
+ };
1229
+ if (signal.aborted) return abort();
1230
+ signal.addEventListener("abort", abort, { once: true });
1231
+ const tick = () => {
1232
+ elapsed += tickMs * (.55 + Math.random() * .9);
1233
+ const fraction = Math.min(elapsed / duration, 1);
1234
+ if (fraction >= failAt) {
1235
+ signal.removeEventListener("abort", abort);
1236
+ reject(new UploadError("network", "Simulated network failure"));
1237
+ return;
1238
+ }
1239
+ onProgress(Math.round(fraction * total), total);
1240
+ if (fraction >= 1) {
1241
+ signal.removeEventListener("abort", abort);
1242
+ resolvePromise({
1243
+ simulated: true,
1244
+ name: file.name,
1245
+ size: file.size
1246
+ });
1247
+ return;
1248
+ }
1249
+ timer = setTimeout(tick, tickMs);
1250
+ };
1251
+ timer = setTimeout(tick, latency);
1252
+ });
1253
+ return Object.assign(handler, { simulated: true });
1254
+ }
1255
+ //#endregion
1256
+ export { formatSpeed as C, formatPercent as S, getFileLabel as _, UploadError as a, formatBytes as b, toUploadError as c, parseAccept as d, validateFile as f, getFileKind as g, getExtension as h, createUploadStore as i, describeAccept as l, presetCatalog as m, createXhrUploader as n, httpErrorMessage as o, PRESET_INFO as p, computeTotals as r, isUploadError as s, createSimulatedUploader as t, isFileAccepted as u, isPreviewableImage as v, formatDuration as x, clamp01 as y };