@useupup/core 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +8 -0
  3. package/README.md +75 -0
  4. package/dist/browser-SXR426OQ.cjs +10 -0
  5. package/dist/browser-WD6T5ABJ.js +1 -0
  6. package/dist/chunk-2H5ICKFT.cjs +122 -0
  7. package/dist/chunk-6HF3I2JS.cjs +39 -0
  8. package/dist/chunk-6S5DQDQH.js +212 -0
  9. package/dist/chunk-BMQC27OE.cjs +92 -0
  10. package/dist/chunk-BSGQGINR.cjs +219 -0
  11. package/dist/chunk-CPKZ2CXD.cjs +179 -0
  12. package/dist/chunk-DTKDIMKT.cjs +573 -0
  13. package/dist/chunk-DYE5XJ6Z.cjs +2134 -0
  14. package/dist/chunk-E3ZCNOXG.cjs +74 -0
  15. package/dist/chunk-FEQY2WYY.cjs +2377 -0
  16. package/dist/chunk-FMURBCZ7.js +120 -0
  17. package/dist/chunk-FYQQRULL.js +37 -0
  18. package/dist/chunk-GE4LIVXP.js +133 -0
  19. package/dist/chunk-HFR5HZJZ.js +56 -0
  20. package/dist/chunk-I7ULPCA2.js +72 -0
  21. package/dist/chunk-MHYXDLZR.js +569 -0
  22. package/dist/chunk-N2SLHVAW.cjs +61 -0
  23. package/dist/chunk-OOPDEOTM.js +169 -0
  24. package/dist/chunk-PMBR3OUW.js +2359 -0
  25. package/dist/chunk-QK7HZIVY.cjs +144 -0
  26. package/dist/chunk-T2AQHY7V.cjs +59 -0
  27. package/dist/chunk-TRCP5FUR.js +90 -0
  28. package/dist/chunk-U7KL5M2H.cjs +374 -0
  29. package/dist/chunk-UXNPM54W.js +2108 -0
  30. package/dist/chunk-VV6QCQLL.js +365 -0
  31. package/dist/chunk-ZMS2HXLB.js +56 -0
  32. package/dist/context-shapes-123dd3b6.d.cts +207 -0
  33. package/dist/context-shapes-27b24c8f.d.ts +207 -0
  34. package/dist/contracts-pipeline-cf177270.d.cts +78 -0
  35. package/dist/contracts-pipeline-cf177270.d.ts +78 -0
  36. package/dist/contracts-strategies-378a16b7.d.cts +168 -0
  37. package/dist/contracts-strategies-378a16b7.d.ts +168 -0
  38. package/dist/contracts.cjs +181 -0
  39. package/dist/contracts.d.cts +11 -0
  40. package/dist/contracts.d.ts +11 -0
  41. package/dist/contracts.js +4 -0
  42. package/dist/core-141335ac.d.ts +651 -0
  43. package/dist/core-bea2ceca.d.cts +651 -0
  44. package/dist/create-worker-provider-AA5SRII6.js +58 -0
  45. package/dist/create-worker-provider-KJNUSSTW.cjs +60 -0
  46. package/dist/eligibility-ONLNJ6FB.cjs +10 -0
  47. package/dist/eligibility-ULFC3E4G.js +8 -0
  48. package/dist/error-code-map-9c44a425.d.ts +62 -0
  49. package/dist/error-code-map-dd5becfa.d.cts +62 -0
  50. package/dist/i18n/index.cjs +90 -0
  51. package/dist/i18n/index.d.cts +3 -0
  52. package/dist/i18n/index.d.ts +3 -0
  53. package/dist/i18n/index.js +1 -0
  54. package/dist/index.cjs +1770 -0
  55. package/dist/index.d.cts +280 -0
  56. package/dist/index.d.ts +280 -0
  57. package/dist/index.js +1589 -0
  58. package/dist/internal.cjs +2407 -0
  59. package/dist/internal.d.cts +595 -0
  60. package/dist/internal.d.ts +595 -0
  61. package/dist/internal.js +2275 -0
  62. package/dist/pipeline/index.cjs +10 -0
  63. package/dist/pipeline/index.d.cts +11 -0
  64. package/dist/pipeline/index.d.ts +11 -0
  65. package/dist/pipeline/index.js +1 -0
  66. package/dist/pipeline-worker.cjs +199 -0
  67. package/dist/pipeline-worker.d.cts +2 -0
  68. package/dist/pipeline-worker.d.ts +2 -0
  69. package/dist/pipeline-worker.js +197 -0
  70. package/dist/resolve-locale-81feef21.d.ts +20 -0
  71. package/dist/resolve-locale-9cb63600.d.cts +20 -0
  72. package/dist/resolve-theme-01758105.d.ts +53 -0
  73. package/dist/resolve-theme-66cf8d98.d.cts +53 -0
  74. package/dist/server-mode-drive-controller-b5f8ee12.d.cts +180 -0
  75. package/dist/server-mode-drive-controller-b5f8ee12.d.ts +180 -0
  76. package/dist/steps/compress.cjs +66 -0
  77. package/dist/steps/compress.d.cts +10 -0
  78. package/dist/steps/compress.d.ts +10 -0
  79. package/dist/steps/compress.js +64 -0
  80. package/dist/steps/exif.cjs +44 -0
  81. package/dist/steps/exif.d.cts +5 -0
  82. package/dist/steps/exif.d.ts +5 -0
  83. package/dist/steps/exif.js +42 -0
  84. package/dist/steps/hash.cjs +44 -0
  85. package/dist/steps/hash.d.cts +5 -0
  86. package/dist/steps/hash.d.ts +5 -0
  87. package/dist/steps/hash.js +42 -0
  88. package/dist/steps/heic.cjs +59 -0
  89. package/dist/steps/heic.d.cts +5 -0
  90. package/dist/steps/heic.d.ts +5 -0
  91. package/dist/steps/heic.js +57 -0
  92. package/dist/steps/thumbnail.cjs +54 -0
  93. package/dist/steps/thumbnail.d.cts +10 -0
  94. package/dist/steps/thumbnail.d.ts +10 -0
  95. package/dist/steps/thumbnail.js +52 -0
  96. package/dist/strategies/index.cjs +82 -0
  97. package/dist/strategies/index.d.cts +187 -0
  98. package/dist/strategies/index.d.ts +187 -0
  99. package/dist/strategies/index.js +60 -0
  100. package/dist/strategies/tus-upload.cjs +14 -0
  101. package/dist/strategies/tus-upload.d.cts +15 -0
  102. package/dist/strategies/tus-upload.d.ts +15 -0
  103. package/dist/strategies/tus-upload.js +5 -0
  104. package/dist/theme/index.cjs +38 -0
  105. package/dist/theme/index.d.cts +3 -0
  106. package/dist/theme/index.d.ts +3 -0
  107. package/dist/theme/index.js +1 -0
  108. package/dist/theme-store-2e56a674.d.cts +39 -0
  109. package/dist/theme-store-f13e0315.d.ts +39 -0
  110. package/dist/token-endpoint-5fdb4a35.d.ts +20 -0
  111. package/dist/token-endpoint-a7d15ab6.d.cts +20 -0
  112. package/dist/ui-translations-c5d00392.d.cts +455 -0
  113. package/dist/ui-translations-c5d00392.d.ts +455 -0
  114. package/dist/uploader-props-72242002.d.ts +340 -0
  115. package/dist/uploader-props-7f956e60.d.cts +340 -0
  116. package/dist/vars-d3421f6c.d.cts +240 -0
  117. package/dist/vars-d3421f6c.d.ts +240 -0
  118. package/package.json +182 -0
package/dist/index.js ADDED
@@ -0,0 +1,1589 @@
1
+ import { EventEmitter, PluginManager, FileManager, IndexedDBStorage, CrashRecoveryManager, UploadManager, validateFileRestrictions, isUploadActive } from './chunk-PMBR3OUW.js';
2
+ export { ACCEPT_PRESETS, BoxPlugin, DropboxPlugin, GoogleDrivePlugin, OneDrivePlugin, PopupOAuthPlugin, resolveAccept } from './chunk-PMBR3OUW.js';
3
+ import { PipelineEngine } from './chunk-FYQQRULL.js';
4
+ import { abortPersistedMultipartSessions, ServerCredentials, MultipartUpload } from './chunk-MHYXDLZR.js';
5
+ import { loadSession, fileFingerprint, TokenEndpointCredentials, DirectUpload } from './chunk-OOPDEOTM.js';
6
+ import { TusUpload } from './chunk-TRCP5FUR.js';
7
+ import { FileSource, UploadStatus } from './chunk-ZMS2HXLB.js';
8
+ export { FileSource, NON_S3_STORAGE_PROVIDERS, StorageProvider, UploadStatus } from './chunk-ZMS2HXLB.js';
9
+ import { UpupError, UpupConfigError } from './chunk-GE4LIVXP.js';
10
+ export { UpupAuthError, UpupConfigError, UpupError, UpupErrorCode, UpupNetworkError, UpupQuotaError, UpupStorageError, UpupValidationError, uploadErrorFromResponse } from './chunk-GE4LIVXP.js';
11
+ import { createTranslator, resolveLocaleBundle, enUS } from './chunk-UXNPM54W.js';
12
+ export { LOCALE_CODES, LOCALE_META, LOCALE_REGISTRY, arSA, createTranslator, deDE, enUS, errorCodeToMessageKey, esES, flattenTranslatorToUiTranslations, formatUiMessage, frFR, jaJP, koKR, normalizeBcp47, pluralUiMessage, zhCN, zhTW } from './chunk-UXNPM54W.js';
13
+ export { UPUP_VAR_PREFIX, darkPreset, flattenSlotsToClassNames, lightPreset, resolveTheme, tokensToVars } from './chunk-VV6QCQLL.js';
14
+
15
+ // src/pipeline/build-auto-pipeline.ts
16
+ async function buildAutoPipeline(options) {
17
+ const steps = [];
18
+ if (options.heicConversion) {
19
+ const { heicStep } = await import('./steps/heic.js');
20
+ steps.push(heicStep());
21
+ }
22
+ if (options.stripExifData) {
23
+ const { exifStep } = await import('./steps/exif.js');
24
+ steps.push(exifStep());
25
+ }
26
+ if (options.imageCompression) {
27
+ const { compressStep } = await import('./steps/compress.js');
28
+ const opts = typeof options.imageCompression === "object" ? options.imageCompression : {};
29
+ steps.push(compressStep(opts));
30
+ }
31
+ if (options.thumbnailGenerator) {
32
+ const { thumbnailStep } = await import('./steps/thumbnail.js');
33
+ const opts = typeof options.thumbnailGenerator === "object" ? options.thumbnailGenerator : {};
34
+ steps.push(thumbnailStep(opts));
35
+ }
36
+ if (options.checksumVerification) {
37
+ const { hashStep } = await import('./steps/hash.js');
38
+ steps.push(hashStep());
39
+ }
40
+ return steps;
41
+ }
42
+
43
+ // src/resolve-upload-config.ts
44
+ function resolveUploadConfig(options) {
45
+ const tusOptions = options.resumable?.protocol === "tus" ? options.resumable : null;
46
+ const configuredTargetCount = [
47
+ options.uploadEndpoint,
48
+ options.serverUrl,
49
+ tusOptions?.endpoint
50
+ ].filter(Boolean).length;
51
+ if (configuredTargetCount > 1) {
52
+ throw new UpupConfigError(
53
+ 'Configure exactly one upload target: uploadEndpoint, serverUrl, or resumable.protocol="tus" with endpoint.',
54
+ "AMBIGUOUS_UPLOAD_TARGET"
55
+ );
56
+ }
57
+ const credentials = options.serverUrl ? new ServerCredentials({
58
+ serverUrl: options.serverUrl
59
+ }) : options.uploadEndpoint ? new TokenEndpointCredentials({
60
+ url: options.uploadEndpoint
61
+ }) : {
62
+ getPresignedUrl: () => {
63
+ throw new UpupConfigError(
64
+ "Tus uploads do not use presigned upload URLs."
65
+ );
66
+ }
67
+ };
68
+ const directUpload = new DirectUpload();
69
+ const tusUpload = tusOptions ? new TusUpload(tusOptions) : null;
70
+ const multipartUpload = options.resumable?.protocol === "multipart" ? new MultipartUpload({
71
+ credentials,
72
+ chunkSizeBytes: options.resumable.chunkSizeBytes,
73
+ // Cross-reload resume is ON by default: an interrupted
74
+ // multipart upload keeps its server-side parts so the next
75
+ // attempt continues instead of restarting. Integrators who
76
+ // want the old auto-abort behavior set persist: false (and
77
+ // should set an S3 AbortIncompleteMultipartUpload lifecycle
78
+ // rule either way).
79
+ persist: options.resumable.persist ?? true,
80
+ sessionScope: options.serverUrl,
81
+ retryDelays: options.resumable.retryDelays,
82
+ partTimeoutMs: options.resumable.partTimeoutMs,
83
+ maxConcurrentParts: options.resumable.maxConcurrentParts
84
+ }) : null;
85
+ const multipartThreshold = options.resumable?.protocol === "multipart" ? options.resumable.thresholdBytes ?? 5 * 1024 * 1024 : Number.POSITIVE_INFINITY;
86
+ return {
87
+ credentials,
88
+ uploadStrategy: directUpload,
89
+ resolveUploadStrategy: (file) => {
90
+ if (tusUpload) {
91
+ return { uploadStrategy: tusUpload, presign: false };
92
+ }
93
+ if (multipartUpload && file.size >= multipartThreshold) {
94
+ return { uploadStrategy: multipartUpload, presign: false };
95
+ }
96
+ return { uploadStrategy: directUpload, presign: true };
97
+ },
98
+ maxConcurrentUploads: options.maxConcurrentUploads ?? 3,
99
+ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
100
+ ...options.fastAbortThreshold !== void 0 ? { fastAbortThreshold: options.fastAbortThreshold } : {},
101
+ ...options.isSuccessfulCall !== void 0 ? { isSuccessfulCall: options.isSuccessfulCall } : {}
102
+ };
103
+ }
104
+
105
+ // src/crash-recovery-serializer.ts
106
+ function toCrashRecoveryFileSnapshot(id, file) {
107
+ const snapshot = {
108
+ file,
109
+ id: file.id,
110
+ name: file.name,
111
+ type: file.type,
112
+ lastModified: file.lastModified,
113
+ source: file.source,
114
+ status: file.status,
115
+ metadata: file.metadata
116
+ };
117
+ if (file.url && !file.url.startsWith("blob:")) {
118
+ snapshot.url = file.url;
119
+ }
120
+ for (const key of [
121
+ "relativePath",
122
+ "key",
123
+ "etag",
124
+ "fileHash",
125
+ "checksumSHA256"
126
+ ]) {
127
+ if (file[key] !== void 0) {
128
+ Object.assign(snapshot, { [key]: file[key] });
129
+ }
130
+ }
131
+ const legacyThumbnail = file.thumbnail;
132
+ if (legacyThumbnail) {
133
+ snapshot.thumbnail = legacyThumbnail;
134
+ }
135
+ return snapshot;
136
+ }
137
+ function serializeCrashRecovery(files, status) {
138
+ return {
139
+ files: [...files.entries()].map(([id, file]) => [
140
+ id,
141
+ toCrashRecoveryFileSnapshot(id, file)
142
+ ]),
143
+ status
144
+ };
145
+ }
146
+ function reviveCrashRecoverySnapshot(snapshot) {
147
+ if (!isRecord(snapshot) || !Array.isArray(snapshot.files)) {
148
+ return null;
149
+ }
150
+ const status = isUploadStatus(snapshot.status) ? snapshot.status : "IDLE" /* IDLE */;
151
+ const files = snapshot.files.map((entry, index) => {
152
+ if (!Array.isArray(entry) || entry.length < 2) return null;
153
+ const id = typeof entry[0] === "string" ? entry[0] : `recovered-${index}`;
154
+ const file = reviveCrashRecoveryFile(id, entry[1], status);
155
+ return file ? [id, file] : null;
156
+ }).filter((entry) => entry != null);
157
+ return { files, status };
158
+ }
159
+ function reviveCrashRecoveryFile(id, value, fallbackStatus) {
160
+ const isFileLike = typeof File !== "undefined" && value instanceof File;
161
+ const wrappedBlob = isRecord(value) && typeof Blob !== "undefined" && value.file instanceof Blob ? value.file : null;
162
+ const props = isRecord(value) && wrappedBlob ? value : isRecord(value) ? value : {};
163
+ const nameFromProps = typeof props.name === "string" && props.name.trim() !== "" ? props.name : void 0;
164
+ const file = wrappedBlob ? toRecoverableFile(wrappedBlob, nameFromProps ?? id, props) : isFileLike ? value : null;
165
+ if (!file) return null;
166
+ const metadata = isRecord(props.metadata) ? props.metadata : {};
167
+ const uploadStatus = isUploadStatus(props.status) ? props.status : fallbackStatus;
168
+ const source = Object.values(FileSource).includes(
169
+ props.source
170
+ ) ? props.source : "local" /* LOCAL */;
171
+ const uploadFile = Object.assign(file, {
172
+ id: typeof props.id === "string" ? props.id : id,
173
+ source,
174
+ status: uploadStatus,
175
+ metadata
176
+ });
177
+ const url = typeof props.url === "string" && !props.url.startsWith("blob:") ? props.url : createObjectUrl(file);
178
+ if (url) {
179
+ uploadFile.url = url;
180
+ }
181
+ for (const key of [
182
+ "relativePath",
183
+ "key",
184
+ "etag",
185
+ "fileHash",
186
+ "checksumSHA256"
187
+ ]) {
188
+ if (typeof props[key] === "string") {
189
+ Object.assign(uploadFile, { [key]: props[key] });
190
+ }
191
+ }
192
+ if (isRecord(props.thumbnail)) {
193
+ const uploadFileRecord = uploadFile;
194
+ uploadFileRecord.thumbnail = props.thumbnail;
195
+ }
196
+ return uploadFile;
197
+ }
198
+ function createObjectUrl(file) {
199
+ return typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
200
+ }
201
+ function toRecoverableFile(blob, name, props) {
202
+ if (typeof File === "undefined") return null;
203
+ if (blob instanceof File && blob.name) {
204
+ return blob;
205
+ }
206
+ const type = typeof props.type === "string" ? props.type : blob.type;
207
+ const lastModified = typeof props.lastModified === "number" ? props.lastModified : Date.now();
208
+ return new File([blob], name, { type, lastModified });
209
+ }
210
+ function isRecord(value) {
211
+ return typeof value === "object" && value !== null;
212
+ }
213
+ function isUploadStatus(value) {
214
+ return Object.values(UploadStatus).includes(value);
215
+ }
216
+
217
+ // src/core.ts
218
+ var UpupCore = class {
219
+ constructor(options) {
220
+ this.emitter = new EventEmitter();
221
+ this.pluginManager = new PluginManager();
222
+ this.pipelineEngine = null;
223
+ this.uploadManager = null;
224
+ this._status = "IDLE" /* IDLE */;
225
+ this._error = null;
226
+ this.crashRecovery = null;
227
+ // Only set when WE created the IndexedDBStorage (not user-supplied
228
+ // storage, whose lifecycle belongs to the caller) so destroy() can
229
+ // release its cached IndexedDB connection.
230
+ this.crashRecoveryStorage = null;
231
+ this.crashRecoveryUnsubscribe = null;
232
+ /** Every crash-recovery storage write flows through this one promise chain.
233
+ * Blob snapshots of large files make IndexedDB puts slow; fire-and-forget
234
+ * save/clear calls complete out of order, so a save started before
235
+ * completion can land after the completion clear and resurrect a finished
236
+ * upload as a resumable one. FIFO ordering makes the last requested op the
237
+ * last applied op. */
238
+ this.crashRecoveryChain = Promise.resolve();
239
+ /** True while a snapshot sync is queued but not yet run. Progress ticks
240
+ * arrive far faster than a large snapshot writes; coalescing them into the
241
+ * one queued sync (which reads state at write time) keeps it to at most
242
+ * one write in flight plus one pending, instead of N stacked blob puts. */
243
+ this.crashRecoverySyncQueued = false;
244
+ this.fileOverrides = /* @__PURE__ */ new Map();
245
+ this.pauseRequested = false;
246
+ this.cancelRequested = false;
247
+ this.destroyed = false;
248
+ /** True only while the CURRENT pause was made by the offline handler —
249
+ * the one case `online` may resume. A pause the user chose stays theirs. */
250
+ this.offlinePausedRun = false;
251
+ this.detachNetworkListeners = null;
252
+ this.activeRun = null;
253
+ this.workerProvider = null;
254
+ this.options = { ...options };
255
+ this.fileManager = new FileManager(this.fileManagerOptions());
256
+ if (options.pipeline) {
257
+ this.pipelineEngine = new PipelineEngine(options.pipeline);
258
+ }
259
+ if (options.plugins) {
260
+ for (const plugin of options.plugins) {
261
+ this.use(plugin);
262
+ }
263
+ }
264
+ this.configureCrashRecovery(options.crashRecovery);
265
+ this.installNetworkAwareness();
266
+ }
267
+ /** Connectivity reactions (`networkAware`, default on): `offline` pauses
268
+ * an active run — with `persist` on, that keeps the multipart session
269
+ * alive instead of burning whole-run retries against a dead network —
270
+ * and `online` resumes it, but ONLY when this handler made the pause.
271
+ * SSR/node-safe: without a window this installs nothing. */
272
+ installNetworkAwareness() {
273
+ if (this.options.networkAware === false) return;
274
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
275
+ return;
276
+ }
277
+ const onOffline = () => {
278
+ if (this.destroyed || !isUploadActive(this._status)) return;
279
+ this.pause();
280
+ this.offlinePausedRun = true;
281
+ };
282
+ const onOnline = () => {
283
+ if (!this.offlinePausedRun) return;
284
+ setTimeout(() => {
285
+ if (!this.offlinePausedRun || this.destroyed) return;
286
+ const onLine = typeof navigator === "undefined" ? void 0 : navigator.onLine;
287
+ if (onLine === false) {
288
+ return;
289
+ }
290
+ this.offlinePausedRun = false;
291
+ if (this._status !== "PAUSED" /* PAUSED */) return;
292
+ this.resume();
293
+ }, 0);
294
+ };
295
+ window.addEventListener("offline", onOffline);
296
+ window.addEventListener("online", onOnline);
297
+ this.detachNetworkListeners = () => {
298
+ window.removeEventListener("offline", onOffline);
299
+ window.removeEventListener("online", onOnline);
300
+ };
301
+ }
302
+ /** The one projection of CoreOptions the FileManager consumes — construction
303
+ * and updateOptions() must stay in lockstep, so both call this. */
304
+ fileManagerOptions() {
305
+ return {
306
+ allowedFileTypes: this.options.allowedFileTypes,
307
+ limit: this.options.limit,
308
+ maxFileSize: this.options.maxFileSize,
309
+ minFileSize: this.options.minFileSize,
310
+ maxTotalFileSize: this.options.maxTotalFileSize,
311
+ contentDeduplication: this.options.contentDeduplication,
312
+ onBeforeFileAdded: this.options.onBeforeFileAdded
313
+ };
314
+ }
315
+ get files() {
316
+ return this.fileManager.getFiles();
317
+ }
318
+ get status() {
319
+ return this._status;
320
+ }
321
+ get error() {
322
+ return this._error;
323
+ }
324
+ get progress() {
325
+ const files = [...this.files.values()];
326
+ const total = files.length;
327
+ const completed = files.filter((f) => f.key != null).length;
328
+ return {
329
+ totalFiles: total,
330
+ completedFiles: completed,
331
+ percentage: total > 0 ? Math.round(completed / total * 100) : 0
332
+ };
333
+ }
334
+ /**
335
+ * Crash-recovery persistence is best-effort by design — a failed save/clear
336
+ * must never break an upload. But full silence hides a dead durability
337
+ * opt-in (F-734): surface failures dev-only, matching the worker-fallback
338
+ * convention in maybeCreateWorkerProvider().
339
+ */
340
+ warnCrashRecoveryFailure(op) {
341
+ return (err) => {
342
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
343
+ console.warn(`[upup] crash-recovery ${op} failed`, err);
344
+ }
345
+ };
346
+ }
347
+ configureCrashRecovery(crashRecovery) {
348
+ if (!crashRecovery || this.crashRecovery) return;
349
+ const crashOptions = typeof crashRecovery === "object" ? crashRecovery : {};
350
+ let storage = crashOptions.storage;
351
+ if (!storage) {
352
+ this.crashRecoveryStorage = new IndexedDBStorage();
353
+ storage = this.crashRecoveryStorage;
354
+ }
355
+ this.crashRecovery = new CrashRecoveryManager(storage);
356
+ this.crashRecoveryUnsubscribe = this.on("state-change", () => {
357
+ if (this.destroyed || this.files.size === 0) return;
358
+ this.queueCrashRecoverySync();
359
+ });
360
+ }
361
+ /** Queue a snapshot write that reads files/status at WRITE time, not at
362
+ * event time — so the write that runs last always reflects the state that
363
+ * came last, and a completion observed while a save is queued turns that
364
+ * queued write into the clear instead of racing it. */
365
+ queueCrashRecoverySync() {
366
+ if (this.crashRecoverySyncQueued) return;
367
+ this.crashRecoverySyncQueued = true;
368
+ void this.enqueueCrashRecoveryOp(async () => {
369
+ this.crashRecoverySyncQueued = false;
370
+ const manager = this.crashRecovery;
371
+ if (!manager || this.destroyed || this.files.size === 0) return;
372
+ if (this._status === "SUCCESSFUL" /* SUCCESSFUL */) {
373
+ await manager.clear().catch(this.warnCrashRecoveryFailure("clear"));
374
+ return;
375
+ }
376
+ await manager.save(serializeCrashRecovery(this.files, this._status)).catch(this.warnCrashRecoveryFailure("save"));
377
+ });
378
+ }
379
+ /** Queue an unconditional clear behind any in-flight snapshot write. The
380
+ * default manager is captured at call time so a clear requested before
381
+ * disable/destroy still applies after the field is nulled. */
382
+ queueCrashRecoveryClear(manager = this.crashRecovery) {
383
+ if (!manager) return Promise.resolve();
384
+ return this.enqueueCrashRecoveryOp(() => manager.clear());
385
+ }
386
+ enqueueCrashRecoveryOp(op) {
387
+ const run = this.crashRecoveryChain.then(op);
388
+ this.crashRecoveryChain = run.then(
389
+ () => void 0,
390
+ () => void 0
391
+ );
392
+ return run;
393
+ }
394
+ disableCrashRecovery() {
395
+ const manager = this.crashRecovery;
396
+ this.crashRecoveryUnsubscribe?.();
397
+ this.crashRecoveryUnsubscribe = null;
398
+ this.crashRecovery = null;
399
+ this.queueCrashRecoveryClear(manager).catch(
400
+ this.warnCrashRecoveryFailure("clear")
401
+ );
402
+ }
403
+ use(plugin) {
404
+ this.pluginManager.register(plugin);
405
+ plugin.init?.(this.emitter);
406
+ this.emitter.emit("plugin-registered", { name: plugin.name });
407
+ return this;
408
+ }
409
+ registerExtension(name, methods) {
410
+ this.pluginManager.registerExtension(name, methods);
411
+ }
412
+ getPlugin(name) {
413
+ return this.pluginManager.getPlugin(name);
414
+ }
415
+ getExtension(name) {
416
+ return this.pluginManager.getExtension(name);
417
+ }
418
+ get ext() {
419
+ return this.pluginManager.getExtensions();
420
+ }
421
+ async addFiles(files, overrides) {
422
+ if (this.destroyed)
423
+ throw new UpupError(
424
+ "UpupCore: addFiles() after destroy()",
425
+ "BAD_REQUEST" /* BAD_REQUEST */
426
+ );
427
+ try {
428
+ const added = await this.fileManager.addFiles(files);
429
+ if (added.length > 0) {
430
+ if (overrides) {
431
+ for (const file of added) {
432
+ this.fileOverrides.set(file.id, overrides);
433
+ }
434
+ }
435
+ this.emitter.emit("files-added", added);
436
+ const wasTerminal = this._status === "SUCCESSFUL" /* SUCCESSFUL */ || this._status === "FAILED" /* FAILED */;
437
+ if (wasTerminal && added.some((f) => f.key == null)) {
438
+ this._status = "IDLE" /* IDLE */;
439
+ this._error = null;
440
+ this.emitter.emit("state-change", {
441
+ files: this.files,
442
+ status: this._status
443
+ });
444
+ } else {
445
+ this.emitter.emit("state-change", { files: this.files });
446
+ }
447
+ }
448
+ const rejectedCount = files.length - added.length;
449
+ if (rejectedCount > 0) {
450
+ this.emitter.emit("file-rejected", { count: rejectedCount });
451
+ }
452
+ } catch (error) {
453
+ this.emitter.emit("restriction-failed", { error });
454
+ throw error;
455
+ }
456
+ }
457
+ /**
458
+ * Discard the persisted multipart sessions of files the user just threw
459
+ * away (cancel / removeFile / removeAll), aborting them server-side on the
460
+ * way out. With `resumable.persist` on, a failed or cancelled upload
461
+ * deliberately keeps its server-side parts alive for a later resume — so
462
+ * an explicit discard is the ONE place that has to say "no, really, drop
463
+ * it", or the user pays storage for parts nothing will ever finish.
464
+ *
465
+ * Fire-and-forget and fully swallowed: cancelling must never throw or wait.
466
+ */
467
+ discardPersistedMultipartSessions(files) {
468
+ const resumable = this.options.resumable;
469
+ if (resumable?.protocol !== "multipart") return;
470
+ if (resumable.persist === false) return;
471
+ const pending = files.filter((file) => file.key == null);
472
+ if (pending.length === 0) return;
473
+ try {
474
+ const { credentials } = resolveUploadConfig(this.options);
475
+ abortPersistedMultipartSessions(pending, credentials);
476
+ } catch {
477
+ }
478
+ }
479
+ removeFile(id) {
480
+ const file = this.fileManager.removeFile(id);
481
+ if (file) {
482
+ this.discardPersistedMultipartSessions([file]);
483
+ this.fileOverrides.delete(id);
484
+ this.emitter.emit("file-removed", file);
485
+ this.emitter.emit("state-change", { files: this.files });
486
+ }
487
+ }
488
+ removeAll() {
489
+ this.discardPersistedMultipartSessions([...this.files.values()]);
490
+ this.fileManager.removeAll();
491
+ this.fileOverrides.clear();
492
+ this.queueCrashRecoveryClear().catch(
493
+ this.warnCrashRecoveryFailure("clear")
494
+ );
495
+ this.emitter.emit("state-change", { files: this.files });
496
+ this.emitter.emit("files-cleared", {});
497
+ }
498
+ async setFiles(files) {
499
+ if (this.destroyed)
500
+ throw new UpupError(
501
+ "UpupCore: setFiles() after destroy()",
502
+ "BAD_REQUEST" /* BAD_REQUEST */
503
+ );
504
+ await this.fileManager.setFiles(files);
505
+ this.emitter.emit("state-change", { files: this.files });
506
+ this.emitter.emit("files-set", { count: this.files.size });
507
+ }
508
+ /** Update options after construction (e.g. when React props change). */
509
+ updateOptions(partial) {
510
+ const hadCrashRecovery = this.crashRecovery != null;
511
+ const PIPELINE_FLAGS = [
512
+ "heicConversion",
513
+ "stripExifData",
514
+ "imageCompression",
515
+ "thumbnailGenerator",
516
+ "checksumVerification"
517
+ ];
518
+ const pipelineFlagChanged = PIPELINE_FLAGS.some((k) => k in partial);
519
+ Object.assign(this.options, partial);
520
+ this.fileManager.updateOptions(this.fileManagerOptions());
521
+ if (pipelineFlagChanged && !this.options.pipeline) {
522
+ this.pipelineEngine = null;
523
+ }
524
+ if ("crashRecovery" in partial) {
525
+ if (partial.crashRecovery) {
526
+ this.configureCrashRecovery(partial.crashRecovery);
527
+ } else if (hadCrashRecovery) {
528
+ this.disableCrashRecovery();
529
+ }
530
+ }
531
+ this.emitter.emit("options-updated", { partial });
532
+ }
533
+ reorderFiles(fileIds) {
534
+ this.fileManager.reorderFiles(fileIds);
535
+ this.emitter.emit("state-change", { files: this.files });
536
+ this.emitter.emit("files-reordered", { fileIds });
537
+ }
538
+ async maybeCreateWorkerProvider(stepCount) {
539
+ const { isWorkerEligible } = await import('./eligibility-ULFC3E4G.js');
540
+ if (!isWorkerEligible(
541
+ this.options,
542
+ typeof Worker !== "undefined",
543
+ stepCount
544
+ ))
545
+ return null;
546
+ try {
547
+ const [{ createWorkerProvider }, { BrowserRuntime }] = await Promise.all([
548
+ import('./create-worker-provider-AA5SRII6.js'),
549
+ import('./browser-WD6T5ABJ.js')
550
+ ]);
551
+ return createWorkerProvider(
552
+ BrowserRuntime,
553
+ this.options.workerTimeoutMs !== void 0 ? { timeoutMs: this.options.workerTimeoutMs } : {}
554
+ );
555
+ } catch (err) {
556
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
557
+ console.warn(
558
+ "[upup] worker offload unavailable, falling back to main thread",
559
+ err
560
+ );
561
+ }
562
+ return null;
563
+ }
564
+ }
565
+ hasUploadTarget() {
566
+ return Boolean(
567
+ this.options.uploadEndpoint || this.options.serverUrl || this.options.resumable?.protocol === "tus"
568
+ );
569
+ }
570
+ createUploadManager() {
571
+ const config = resolveUploadConfig(this.options);
572
+ return new UploadManager({
573
+ ...config,
574
+ onFileStart: (file) => {
575
+ const uploading = this.fileManager.updateFile(file.id, {
576
+ status: "UPLOADING" /* UPLOADING */
577
+ });
578
+ if (!uploading) return;
579
+ this.emitter.emit("file-upload-start", { file: uploading });
580
+ this.emitter.emit("state-change", { files: this.files });
581
+ },
582
+ onProgress: (fileId, loaded, total) => {
583
+ this.emitter.emit("upload-progress", { fileId, loaded, total });
584
+ this.emitter.emit("state-change", { progress: this.progress });
585
+ },
586
+ onFileComplete: (file, result) => {
587
+ if (this.pauseRequested || this.cancelRequested || this.destroyed)
588
+ return;
589
+ const updated = this.fileManager.updateFile(file.id, {
590
+ key: result.key,
591
+ status: "SUCCESSFUL" /* SUCCESSFUL */
592
+ });
593
+ if (!updated) return;
594
+ this.emitter.emit("upload-success", { file: updated, result });
595
+ this.emitter.emit("state-change", { files: this.files });
596
+ },
597
+ onFileError: (file, error) => {
598
+ if (this.pauseRequested || this.cancelRequested || this.destroyed)
599
+ return;
600
+ const failed = this.fileManager.updateFile(file.id, {
601
+ status: "FAILED" /* FAILED */
602
+ });
603
+ if (!failed) return;
604
+ this.emitter.emit("upload-error", { file: failed, error });
605
+ this.emitter.emit("state-change", { files: this.files });
606
+ }
607
+ });
608
+ }
609
+ markFilesReady(files) {
610
+ return files.map((file) => {
611
+ const overrides = this.fileOverrides.get(file.id);
612
+ return this.fileManager.updateFile(file.id, {
613
+ status: "READY" /* READY */,
614
+ metadata: {
615
+ ...file.metadata,
616
+ ...this.options.metadata,
617
+ ...overrides?.metadata
618
+ }
619
+ });
620
+ }).filter((file) => file !== void 0);
621
+ }
622
+ updatePendingFileStatuses(status) {
623
+ for (const file of [...this.files.values()]) {
624
+ if (file.key == null && (file.status === "READY" /* READY */ || file.status === "UPLOADING" /* UPLOADING */ || file.status === "PROCESSING" /* PROCESSING */ || file.status === "PAUSED" /* PAUSED */)) {
625
+ this.fileManager.updateFile(file.id, { status });
626
+ }
627
+ }
628
+ }
629
+ markUnsuccessfulFilesFailed() {
630
+ for (const file of [...this.files.values()]) {
631
+ if (file.key == null && file.status !== "SUCCESSFUL" /* SUCCESSFUL */) {
632
+ this.fileManager.updateFile(file.id, {
633
+ status: "FAILED" /* FAILED */
634
+ });
635
+ }
636
+ }
637
+ }
638
+ async uploadFiles(files) {
639
+ const targetFiles = this.markFilesReady(files);
640
+ this.emitter.emit("state-change", { files: this.files });
641
+ if (!this.hasUploadTarget()) {
642
+ throw new UpupConfigError(
643
+ "No upload target configured. Use selected files directly for local-only flows, or configure uploadEndpoint, serverUrl, or an external Tus endpoint before calling upload()."
644
+ );
645
+ }
646
+ this.uploadManager = this.createUploadManager();
647
+ await this.uploadManager.uploadAll(targetFiles);
648
+ this.uploadManager = null;
649
+ return [...this.files.values()];
650
+ }
651
+ validateFiles(files) {
652
+ return Promise.resolve(
653
+ files.map((file) => {
654
+ const errors = validateFileRestrictions(file, this.options);
655
+ return { file, valid: errors.length === 0, errors };
656
+ })
657
+ );
658
+ }
659
+ async upload() {
660
+ if (this.destroyed)
661
+ throw new UpupError(
662
+ "UpupCore: upload() after destroy()",
663
+ "BAD_REQUEST" /* BAD_REQUEST */
664
+ );
665
+ if (this.activeRun) return this.activeRun;
666
+ this.activeRun = this.runUpload();
667
+ try {
668
+ return await this.activeRun;
669
+ } finally {
670
+ this.activeRun = null;
671
+ }
672
+ }
673
+ resetRunFlags() {
674
+ this.pauseRequested = false;
675
+ this.cancelRequested = false;
676
+ }
677
+ async runUpload() {
678
+ this.resetRunFlags();
679
+ this._status = "PROCESSING" /* PROCESSING */;
680
+ this.emitter.emit("upload-start", {});
681
+ this.emitter.emit("state-change", { status: this._status });
682
+ try {
683
+ if (!this.pipelineEngine) {
684
+ const autoSteps = await buildAutoPipeline(this.options);
685
+ if (autoSteps.length > 0) {
686
+ this.pipelineEngine = new PipelineEngine(autoSteps);
687
+ }
688
+ }
689
+ if (this.pipelineEngine) {
690
+ const translator = createTranslator({
691
+ bundle: resolveLocaleBundle(this.options.locale) ?? enUS,
692
+ fallback: enUS
693
+ });
694
+ const provider = await this.maybeCreateWorkerProvider(
695
+ this.pipelineEngine.stepCount
696
+ );
697
+ this.workerProvider = provider;
698
+ try {
699
+ const context = {
700
+ files: this.files,
701
+ options: this.options,
702
+ emit: (event, data) => {
703
+ this.emitter.emit(event, data);
704
+ },
705
+ t: (key, vars) => translator(
706
+ key,
707
+ vars
708
+ ),
709
+ worker: provider ? {
710
+ execute: (task) => provider.execute(task)
711
+ } : void 0
712
+ };
713
+ const processed = await this.pipelineEngine.processAll(
714
+ [...this.files.values()].filter((f) => f.key == null),
715
+ context
716
+ );
717
+ this.fileManager.applyProcessed(processed);
718
+ } finally {
719
+ provider?.terminate();
720
+ this.workerProvider = null;
721
+ }
722
+ }
723
+ if (this.destroyed || this.cancelRequested) {
724
+ this._status = "IDLE" /* IDLE */;
725
+ this._error = null;
726
+ this.emitter.emit("state-change", { status: this._status });
727
+ return [...this.files.values()];
728
+ }
729
+ if (this.pauseRequested) {
730
+ this._status = "PAUSED" /* PAUSED */;
731
+ this.emitter.emit("state-change", { status: this._status });
732
+ return [...this.files.values()];
733
+ }
734
+ this._status = "UPLOADING" /* UPLOADING */;
735
+ this.emitter.emit("state-change", { status: this._status });
736
+ await this.uploadFiles(
737
+ [...this.files.values()].filter((f) => f.key == null)
738
+ );
739
+ this._status = "SUCCESSFUL" /* SUCCESSFUL */;
740
+ this._error = null;
741
+ this.emitter.emit("upload-all-complete", [...this.files.values()]);
742
+ this.emitter.emit("state-change", { status: this._status });
743
+ this.queueCrashRecoveryClear().catch(
744
+ this.warnCrashRecoveryFailure("clear")
745
+ );
746
+ return [...this.files.values()];
747
+ } catch (error) {
748
+ const err = error instanceof Error ? error : new Error(String(error));
749
+ this.uploadManager = null;
750
+ if (this.pauseRequested) {
751
+ this._status = "PAUSED" /* PAUSED */;
752
+ this._error = null;
753
+ this.emitter.emit("state-change", { status: this._status });
754
+ return [...this.files.values()];
755
+ }
756
+ if (this.cancelRequested || this.destroyed) {
757
+ this._status = "IDLE" /* IDLE */;
758
+ this._error = null;
759
+ this.emitter.emit("state-change", { status: this._status });
760
+ return [...this.files.values()];
761
+ }
762
+ this.terminalRunFailure(err);
763
+ throw err;
764
+ }
765
+ }
766
+ /**
767
+ * The single terminal-failure path shared by all three run flavors
768
+ * (runUpload / runRetry / resume). They previously diverged (F-729): only
769
+ * runUpload marked leftover files FAILED, and resume never invoked
770
+ * options.onError — a retry/resume failure looked different to consumers
771
+ * than the identical first-run failure for no reason.
772
+ */
773
+ terminalRunFailure(err) {
774
+ this.uploadManager = null;
775
+ this._status = "FAILED" /* FAILED */;
776
+ this._error = err;
777
+ this.markUnsuccessfulFilesFailed();
778
+ this.options.onError?.(err);
779
+ this.emitter.emit("upload-error", { error: err });
780
+ this.emitter.emit("state-change", {
781
+ status: this._status,
782
+ error: err,
783
+ files: this.files
784
+ });
785
+ }
786
+ replaceFile(id, file) {
787
+ const next = this.fileManager.replaceFile(id, file);
788
+ this.emitter.emit("file-replaced", { file: next });
789
+ this.emitter.emit("state-change", { files: this.files });
790
+ }
791
+ /**
792
+ * Pause in-flight uploads by aborting the current abort controller.
793
+ * Note: This cancels active HTTP requests. Resume will re-upload
794
+ * files that were in progress — true pause/resume of partial uploads
795
+ * requires multipart upload support.
796
+ */
797
+ pause() {
798
+ this.offlinePausedRun = false;
799
+ this.pauseRequested = true;
800
+ if (this.uploadManager) {
801
+ this.uploadManager.abort();
802
+ this.uploadManager = null;
803
+ }
804
+ this.updatePendingFileStatuses("PAUSED" /* PAUSED */);
805
+ this._status = "PAUSED" /* PAUSED */;
806
+ this.emitter.emit("upload-pause", {});
807
+ this.emitter.emit("state-change", { status: this._status });
808
+ }
809
+ /**
810
+ * Resume uploads after a pause. Re-uploads files that did not
811
+ * complete successfully (those without a `key` set).
812
+ */
813
+ resume() {
814
+ if (this.destroyed)
815
+ throw new UpupError(
816
+ "UpupCore: resume() after destroy()",
817
+ "BAD_REQUEST" /* BAD_REQUEST */
818
+ );
819
+ if (this.activeRun) return;
820
+ this.pauseRequested = false;
821
+ this.offlinePausedRun = false;
822
+ this._status = "UPLOADING" /* UPLOADING */;
823
+ this.emitter.emit("upload-resume", {});
824
+ this.emitter.emit("state-change", { status: this._status });
825
+ const incomplete = [...this.files.values()].filter((f) => f.key == null);
826
+ if (incomplete.length > 0 && this.hasUploadTarget()) {
827
+ const run = this.uploadFiles(incomplete);
828
+ this.activeRun = run;
829
+ run.then(() => {
830
+ const allComplete = [...this.files.values()].every(
831
+ (file) => file.key != null
832
+ );
833
+ this._status = allComplete ? "SUCCESSFUL" /* SUCCESSFUL */ : "IDLE" /* IDLE */;
834
+ this.emitter.emit("state-change", { status: this._status });
835
+ }).catch((err) => {
836
+ if (this.pauseRequested || this.cancelRequested || this.destroyed)
837
+ return;
838
+ this.terminalRunFailure(
839
+ err instanceof Error ? err : new Error(String(err))
840
+ );
841
+ }).finally(() => {
842
+ this.activeRun = null;
843
+ });
844
+ return;
845
+ }
846
+ if (incomplete.length === 0 && this.files.size > 0) {
847
+ const allComplete = [...this.files.values()].every(
848
+ (file) => file.key != null
849
+ );
850
+ this._status = allComplete ? "SUCCESSFUL" /* SUCCESSFUL */ : "IDLE" /* IDLE */;
851
+ if (allComplete) {
852
+ this.emitter.emit("upload-all-complete", [
853
+ ...this.files.values()
854
+ ]);
855
+ this.queueCrashRecoveryClear().catch(
856
+ this.warnCrashRecoveryFailure("clear")
857
+ );
858
+ }
859
+ this.emitter.emit("state-change", { status: this._status });
860
+ }
861
+ }
862
+ cancel() {
863
+ this.offlinePausedRun = false;
864
+ this.cancelRequested = true;
865
+ if (this.uploadManager) {
866
+ this.uploadManager.abort();
867
+ this.uploadManager = null;
868
+ }
869
+ this.discardPersistedMultipartSessions([...this.files.values()]);
870
+ this.updatePendingFileStatuses("IDLE" /* IDLE */);
871
+ this._status = "IDLE" /* IDLE */;
872
+ this._error = null;
873
+ this.emitter.emit("upload-cancel", {});
874
+ this.emitter.emit("state-change", { status: this._status });
875
+ }
876
+ async retry(fileId) {
877
+ if (this.destroyed)
878
+ throw new UpupError(
879
+ "UpupCore: retry() after destroy()",
880
+ "BAD_REQUEST" /* BAD_REQUEST */
881
+ );
882
+ if (this.activeRun) return this.activeRun;
883
+ this.activeRun = this.runRetry(fileId);
884
+ try {
885
+ return await this.activeRun;
886
+ } finally {
887
+ this.activeRun = null;
888
+ }
889
+ }
890
+ async runRetry(fileId) {
891
+ this.emitter.emit("retry", { fileId });
892
+ const target = fileId ? this.files.get(fileId) : void 0;
893
+ const files = fileId ? target ? [target] : [] : [...this.files.values()].filter(
894
+ (file) => file.key == null || file.status === "FAILED" /* FAILED */
895
+ );
896
+ if (files.length === 0) {
897
+ return [...this.files.values()];
898
+ }
899
+ this._status = "UPLOADING" /* UPLOADING */;
900
+ this._error = null;
901
+ this.emitter.emit("upload-start", { retry: true, fileId });
902
+ this.emitter.emit("state-change", { status: this._status });
903
+ try {
904
+ const uploaded = await this.uploadFiles(files);
905
+ const allComplete = [...this.files.values()].every(
906
+ (file) => file.key != null
907
+ );
908
+ this._status = allComplete ? "SUCCESSFUL" /* SUCCESSFUL */ : "IDLE" /* IDLE */;
909
+ if (allComplete) {
910
+ this.emitter.emit("upload-all-complete", [
911
+ ...this.files.values()
912
+ ]);
913
+ this.queueCrashRecoveryClear().catch(
914
+ this.warnCrashRecoveryFailure("clear")
915
+ );
916
+ }
917
+ this.emitter.emit("state-change", { status: this._status });
918
+ return uploaded;
919
+ } catch (error) {
920
+ const err = error instanceof Error ? error : new Error(String(error));
921
+ this.terminalRunFailure(err);
922
+ throw err;
923
+ }
924
+ }
925
+ on(event, handler) {
926
+ return this.emitter.on(event, handler);
927
+ }
928
+ off(event, handler) {
929
+ this.emitter.off(event, handler);
930
+ }
931
+ emit(event, data) {
932
+ this.emitter.emit(event, data);
933
+ }
934
+ getSnapshot() {
935
+ return {
936
+ files: [...this.files.entries()],
937
+ status: this._status
938
+ };
939
+ }
940
+ restore(snapshot) {
941
+ this.fileManager.restore(snapshot.files);
942
+ this._status = snapshot.status;
943
+ this.emitter.emit("state-change", {
944
+ files: this.files,
945
+ status: this._status
946
+ });
947
+ this.emitter.emit("snapshot-restored", {
948
+ count: snapshot.files.length,
949
+ status: snapshot.status
950
+ });
951
+ }
952
+ async restoreFromCrashRecovery() {
953
+ if (!this.crashRecovery) return false;
954
+ const snapshot = await this.crashRecovery.restore();
955
+ const restored = reviveCrashRecoverySnapshot(snapshot);
956
+ if (restored && restored.files.length > 0) {
957
+ const wasActive = restored.status === "PROCESSING" /* PROCESSING */ || restored.status === "UPLOADING" /* UPLOADING */;
958
+ const liveFiles = restored.files.filter(
959
+ ([, file]) => !this.isStaleMultipartRestore(file)
960
+ );
961
+ if (liveFiles.length === 0) {
962
+ this.queueCrashRecoveryClear().catch(
963
+ this.warnCrashRecoveryFailure("clear")
964
+ );
965
+ return false;
966
+ }
967
+ const normalized = {
968
+ files: liveFiles.map(([id, file]) => {
969
+ if (wasActive && file.key == null && (file.status === "PROCESSING" /* PROCESSING */ || file.status === "UPLOADING" /* UPLOADING */ || file.status === "READY" /* READY */ || file.status === "IDLE" /* IDLE */)) {
970
+ return [
971
+ id,
972
+ Object.assign(file, {
973
+ status: "PAUSED" /* PAUSED */
974
+ })
975
+ ];
976
+ }
977
+ return [id, file];
978
+ }),
979
+ status: wasActive ? "PAUSED" /* PAUSED */ : restored.status
980
+ };
981
+ this.restore(normalized);
982
+ this.seedRestoredProgress(normalized.files);
983
+ this.emitter.emit("crash-recovery-restored", {});
984
+ this.maybeScheduleAutoResume(normalized.status);
985
+ return true;
986
+ }
987
+ return false;
988
+ }
989
+ /** `resumable.autoResume` continues a crash-restored upload without the
990
+ * explicit Resume click. Off by default on purpose: closing a tab is an
991
+ * ambiguous signal — as often "cancel" as "oops" — so silently resuming
992
+ * a transfer the user may have meant to kill is an integrator's call,
993
+ * never the library's. Deferred a tick so the host finishes mounting
994
+ * (the same guard autoUpload's timer carries, F-148), and re-checked at
995
+ * fire time: the user may have discarded the restore in between. */
996
+ maybeScheduleAutoResume(restoredStatus) {
997
+ const resumable = this.options.resumable;
998
+ if (resumable?.protocol !== "multipart" || !resumable.autoResume) {
999
+ return;
1000
+ }
1001
+ if (restoredStatus !== "PAUSED" /* PAUSED */) return;
1002
+ setTimeout(() => {
1003
+ if (this.destroyed) return;
1004
+ if (this._status !== "PAUSED" /* PAUSED */) return;
1005
+ this.resume();
1006
+ }, 0);
1007
+ }
1008
+ /** A restored PAUSED file rendered "0 B of N" even when most of it was
1009
+ * already in storage — the persisted multipart session knows the real
1010
+ * offset, but nothing replayed it into the progress pipeline until the
1011
+ * user actually hit resume. Emit it here so the restored UI opens at the
1012
+ * byte count the resume will continue from. Same session gates as
1013
+ * tryResume (scope), minus the server round-trip: this is display-only
1014
+ * and must not spend a request; the authoritative recheck still happens
1015
+ * on resume. */
1016
+ /** A finished multipart upload can die in the window between its
1017
+ * synchronous session removal and the asynchronous IndexedDB snapshot
1018
+ * clear — the next visit then restores a file the server already
1019
+ * assembled, offering a Resume that would re-upload all of it. The
1020
+ * session store is the tiebreaker: init writes the session synchronously
1021
+ * before the first byte moves and completion removes it synchronously,
1022
+ * so an UPLOADING-status file of multipart size with no session is a
1023
+ * stale snapshot, not a crashed upload. PROCESSING files never had a
1024
+ * session and stay restorable; so does everything below the multipart
1025
+ * threshold or outside persist mode. */
1026
+ isStaleMultipartRestore(file) {
1027
+ const resumable = this.options.resumable;
1028
+ if (resumable?.protocol !== "multipart") return false;
1029
+ if (!(resumable.persist ?? true)) return false;
1030
+ if (!(file instanceof File)) return false;
1031
+ if (file.status !== "UPLOADING" /* UPLOADING */) return false;
1032
+ if (file.key != null) return false;
1033
+ if (file.size < (resumable.thresholdBytes ?? 5 * 1024 * 1024)) {
1034
+ return false;
1035
+ }
1036
+ return loadSession(fileFingerprint(file)) == null;
1037
+ }
1038
+ seedRestoredProgress(restored) {
1039
+ for (const [, file] of restored) {
1040
+ if (file.status !== "PAUSED" /* PAUSED */) continue;
1041
+ if (!(file instanceof File)) continue;
1042
+ const session = loadSession(fileFingerprint(file));
1043
+ if (!session || session.scope !== this.options.serverUrl) continue;
1044
+ const loaded = Math.min(session.uploadedBytes ?? 0, file.size);
1045
+ if (loaded <= 0) continue;
1046
+ this.emitter.emit("upload-progress", {
1047
+ fileId: file.id,
1048
+ loaded,
1049
+ total: file.size
1050
+ });
1051
+ }
1052
+ }
1053
+ async clearCrashRecovery() {
1054
+ await this.queueCrashRecoveryClear();
1055
+ }
1056
+ destroy() {
1057
+ this.destroyed = true;
1058
+ this.detachNetworkListeners?.();
1059
+ this.detachNetworkListeners = null;
1060
+ this.uploadManager?.abort();
1061
+ this.uploadManager = null;
1062
+ this.workerProvider?.terminate();
1063
+ this.workerProvider = null;
1064
+ this.emitter.emit("destroyed", {});
1065
+ this.crashRecoveryUnsubscribe?.();
1066
+ this.crashRecoveryUnsubscribe = null;
1067
+ this.crashRecoveryStorage?.close();
1068
+ this.crashRecoveryStorage = null;
1069
+ this.crashRecovery = null;
1070
+ this.pipelineEngine = null;
1071
+ this.fileOverrides.clear();
1072
+ this.emitter.removeAllListeners();
1073
+ this.pluginManager.destroy();
1074
+ this.fileManager.removeAll();
1075
+ this._status = "IDLE" /* IDLE */;
1076
+ this._error = null;
1077
+ }
1078
+ };
1079
+
1080
+ // src/drives/drive-browser-descriptors.ts
1081
+ var GOOGLE_DRIVE_DESCRIPTOR = {
1082
+ pluginId: "google-drive",
1083
+ eventPrefix: "google-drive",
1084
+ rootFolderName: "Drive",
1085
+ rootFolderId: "root",
1086
+ folderKey: "id",
1087
+ loadFilesRootArg: void 0,
1088
+ folderIdField: "folderId",
1089
+ auth: "gis",
1090
+ selectFolder: "cached-children"
1091
+ };
1092
+ var ONE_DRIVE_DESCRIPTOR = {
1093
+ pluginId: "one-drive",
1094
+ eventPrefix: "one-drive",
1095
+ rootFolderName: "OneDrive",
1096
+ rootFolderId: "root",
1097
+ folderKey: "id",
1098
+ loadFilesRootArg: void 0,
1099
+ folderIdField: "folderId",
1100
+ auth: "popup",
1101
+ selectFolder: "load-all",
1102
+ loadAllRootArg: "root"
1103
+ };
1104
+ var DROPBOX_DESCRIPTOR = {
1105
+ pluginId: "dropbox",
1106
+ eventPrefix: "dropbox",
1107
+ rootFolderName: "Dropbox",
1108
+ rootFolderId: "root",
1109
+ folderKey: "path",
1110
+ loadFilesRootArg: "",
1111
+ folderIdField: "path",
1112
+ auth: "popup",
1113
+ selectFolder: "load-all",
1114
+ loadAllRootArg: ""
1115
+ };
1116
+ var BOX_DESCRIPTOR = {
1117
+ pluginId: "box",
1118
+ eventPrefix: "box",
1119
+ rootFolderName: "Box",
1120
+ rootFolderId: "0",
1121
+ folderKey: "id",
1122
+ loadFilesRootArg: "0",
1123
+ folderIdField: "folderId",
1124
+ auth: "popup",
1125
+ selectFolder: "load-all",
1126
+ loadAllRootArg: "0"
1127
+ };
1128
+
1129
+ // src/icons/file-type-icons.ts
1130
+ var FT_STROKE = {
1131
+ fill: "none",
1132
+ stroke: "currentColor",
1133
+ "stroke-width": "2",
1134
+ "stroke-linecap": "round",
1135
+ "stroke-linejoin": "round"
1136
+ };
1137
+ var FILE_TYPE_EXTENSIONS = [
1138
+ "bmp",
1139
+ "css",
1140
+ "csv",
1141
+ "docx",
1142
+ "html",
1143
+ "jpg",
1144
+ "js",
1145
+ "jsx",
1146
+ "pdf",
1147
+ "png",
1148
+ "php",
1149
+ "ppt",
1150
+ "rs",
1151
+ "sql",
1152
+ "svg",
1153
+ "ts",
1154
+ "tsx",
1155
+ "txt",
1156
+ "vue",
1157
+ "xls",
1158
+ "xml",
1159
+ "zip"
1160
+ ];
1161
+ var FILE_TYPE_ICONS = {
1162
+ "file-bmp": {
1163
+ viewBox: "0 0 24 24",
1164
+ defaultSize: 48,
1165
+ attrs: { ...FT_STROKE },
1166
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M18 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M4 21h1.5a1.5 1.5 0 0 0 0 -3h-1.5h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6z"></path><path d="M10 21v-6l2.5 3l2.5 -3v6"></path>`
1167
+ },
1168
+ "file-css": {
1169
+ viewBox: "0 0 24 24",
1170
+ defaultSize: 48,
1171
+ attrs: { ...FT_STROKE },
1172
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M8 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path><path d="M11 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>`
1173
+ },
1174
+ "file-csv": {
1175
+ viewBox: "0 0 24 24",
1176
+ defaultSize: 48,
1177
+ attrs: { ...FT_STROKE },
1178
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path><path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M16 15l2 6l2 -6"></path>`
1179
+ },
1180
+ "file-docx": {
1181
+ viewBox: "0 0 24 24",
1182
+ defaultSize: 48,
1183
+ attrs: { ...FT_STROKE },
1184
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M2 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"></path><path d="M17 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path><path d="M9.5 15a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1 -3 0v-3a1.5 1.5 0 0 1 1.5 -1.5z"></path><path d="M19.5 15l3 6"></path><path d="M19.5 21l3 -6"></path>`
1185
+ },
1186
+ "file-html": {
1187
+ viewBox: "0 0 24 24",
1188
+ defaultSize: 48,
1189
+ attrs: { ...FT_STROKE },
1190
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M2 21v-6"></path><path d="M5 15v6"></path><path d="M2 18h3"></path><path d="M20 15v6h2"></path><path d="M13 21v-6l2 3l2 -3v6"></path><path d="M7.5 15h3"></path><path d="M9 15v6"></path>`
1191
+ },
1192
+ "file-jpg": {
1193
+ viewBox: "0 0 24 24",
1194
+ defaultSize: 48,
1195
+ attrs: { ...FT_STROKE },
1196
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M11 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3"></path><path d="M5 15h3v4.5a1.5 1.5 0 0 1 -3 0"></path>`
1197
+ },
1198
+ "file-js": {
1199
+ viewBox: "0 0 24 24",
1200
+ defaultSize: 48,
1201
+ attrs: { ...FT_STROKE },
1202
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"></path><path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>`
1203
+ },
1204
+ "file-jsx": {
1205
+ viewBox: "0 0 24 24",
1206
+ defaultSize: 48,
1207
+ attrs: { ...FT_STROKE },
1208
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 15h3v4.5a1.5 1.5 0 0 1 -3 0"></path><path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M16 15l4 6"></path><path d="M16 21l4 -6"></path>`
1209
+ },
1210
+ "file-pdf": {
1211
+ viewBox: "0 0 24 24",
1212
+ defaultSize: 48,
1213
+ attrs: { ...FT_STROKE },
1214
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M17 18h2"></path><path d="M20 15h-3v6"></path><path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"></path>`
1215
+ },
1216
+ "file-png": {
1217
+ viewBox: "0 0 24 24",
1218
+ defaultSize: 48,
1219
+ attrs: { ...FT_STROKE },
1220
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3"></path><path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M11 21v-6l3 6v-6"></path>`
1221
+ },
1222
+ "file-php": {
1223
+ viewBox: "0 0 24 24",
1224
+ defaultSize: 48,
1225
+ attrs: { ...FT_STROKE },
1226
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M17 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M11 21v-6"></path><path d="M14 15v6"></path><path d="M11 18h3"></path>`
1227
+ },
1228
+ "file-ppt": {
1229
+ viewBox: "0 0 24 24",
1230
+ defaultSize: 48,
1231
+ attrs: { ...FT_STROKE },
1232
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M11 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M16.5 15h3"></path><path d="M18 15v6"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>`
1233
+ },
1234
+ "file-rs": {
1235
+ viewBox: "0 0 24 24",
1236
+ defaultSize: 48,
1237
+ attrs: { ...FT_STROKE },
1238
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path><path d="M3 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6m3 0l-2 -3"></path>`
1239
+ },
1240
+ "file-sql": {
1241
+ viewBox: "0 0 24 24",
1242
+ defaultSize: 48,
1243
+ attrs: { ...FT_STROKE },
1244
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M18 15v6h2"></path><path d="M13 15a2 2 0 0 1 2 2v2a2 2 0 1 1 -4 0v-2a2 2 0 0 1 2 -2z"></path><path d="M14 20l1.5 1.5"></path>`
1245
+ },
1246
+ "file-svg": {
1247
+ viewBox: "0 0 24 24",
1248
+ defaultSize: 48,
1249
+ attrs: { ...FT_STROKE },
1250
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M10 15l2 6l2 -6"></path><path d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3"></path>`
1251
+ },
1252
+ "file-ts": {
1253
+ viewBox: "0 0 24 24",
1254
+ defaultSize: 48,
1255
+ attrs: { ...FT_STROKE },
1256
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M3.5 15h3"></path><path d="M5 15v6"></path>`
1257
+ },
1258
+ "file-tsx": {
1259
+ viewBox: "0 0 24 24",
1260
+ defaultSize: 48,
1261
+ attrs: { ...FT_STROKE },
1262
+ inner: `<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M16 15l4 6"></path><path d="M16 21l4 -6"></path><path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M4.5 15h3"></path><path d="M6 15v6"></path>`
1263
+ },
1264
+ "file-txt": {
1265
+ viewBox: "0 0 24 24",
1266
+ defaultSize: 48,
1267
+ attrs: { ...FT_STROKE },
1268
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M16.5 15h3"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4.5 15h3"></path><path d="M6 15v6"></path><path d="M18 15v6"></path><path d="M10 15l4 6"></path><path d="M10 21l4 -6"></path>`
1269
+ },
1270
+ "file-vue": {
1271
+ viewBox: "0 0 24 24",
1272
+ defaultSize: 48,
1273
+ attrs: { ...FT_STROKE },
1274
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 15l2 6l2 -6"></path><path d="M11 15v4.5a1.5 1.5 0 0 0 3 0v-4.5"></path><path d="M20 15h-3v6h3"></path><path d="M17 18h2"></path>`
1275
+ },
1276
+ "file-xls": {
1277
+ viewBox: "0 0 24 24",
1278
+ defaultSize: 48,
1279
+ attrs: { ...FT_STROKE },
1280
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 15l4 6"></path><path d="M4 21l4 -6"></path><path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M11 15v6h3"></path>`
1281
+ },
1282
+ "file-xml": {
1283
+ viewBox: "0 0 24 24",
1284
+ defaultSize: 48,
1285
+ attrs: { ...FT_STROKE },
1286
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 15l4 6"></path><path d="M4 21l4 -6"></path><path d="M19 15v6h3"></path><path d="M11 21v-6l2.5 3l2.5 -3v6"></path>`
1287
+ },
1288
+ "file-zip": {
1289
+ viewBox: "0 0 24 24",
1290
+ defaultSize: 48,
1291
+ attrs: { ...FT_STROKE },
1292
+ inner: `<path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M16 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path><path d="M12 15v6"></path><path d="M5 15h3l-3 6h3"></path>`
1293
+ }
1294
+ };
1295
+
1296
+ // src/icons/registry.ts
1297
+ var STROKE_ATTRS = {
1298
+ fill: "none",
1299
+ stroke: "currentColor",
1300
+ "stroke-width": "2",
1301
+ "stroke-linecap": "round",
1302
+ "stroke-linejoin": "round"
1303
+ };
1304
+ var FILLED_ATTRS = { fill: "currentColor", stroke: "none" };
1305
+ var BRAND_FILLRULE = { fill: "none", "fill-rule": "nonzero" };
1306
+ var ICONS = {
1307
+ // ── Brand/source (viewBox 0 0 32 32) ──
1308
+ "my-device": {
1309
+ viewBox: "0 0 32 32",
1310
+ defaultSize: 32,
1311
+ className: "upup-text-[#0ea5e9]",
1312
+ inner: `
1313
+ <path d="M8.45 22.087l-1.305-6.674h17.678l-1.572 6.674H8.45zm4.975-12.412l1.083 1.765a.823.823 0 00.715.386h7.951V13.5H8.587V9.675h4.838zM26.043 13.5h-1.195v-2.598c0-.463-.336-.75-.798-.75h-8.356l-1.082-1.766A.823.823 0 0013.897 8H7.728c-.462 0-.815.256-.815.718V13.5h-.956a.97.97 0 00-.746.37.972.972 0 00-.19.81l1.724 8.565c.095.44.484.755.933.755H24c.44 0 .824-.3.929-.727l2.043-8.568a.972.972 0 00-.176-.825.967.967 0 00-.753-.38z" fill="currentcolor" fill-rule="evenodd" />
1314
+ `
1315
+ },
1316
+ box: {
1317
+ viewBox: "0 0 32 32",
1318
+ defaultSize: 32,
1319
+ className: "upup-text-[#0061d5]",
1320
+ inner: `
1321
+ <g fill="currentcolor" fill-rule="nonzero">
1322
+ <path d="m16.4 13.5c-1.6 0-3 0.9-3.7 2.2-0.7-1.3-2.1-2.2-3.7-2.2-1 0-1.8 0.3-2.5 0.8v-3.6c-0.1-0.3-0.5-0.7-1-0.7s-0.8 0.4-0.8 0.8v7c0 2.3 1.9 4.2 4.2 4.2 1.6 0 3-0.9 3.7-2.2 0.7 1.3 2.1 2.2 3.7 2.2 2.3 0 4.2-1.9 4.2-4.2 0.1-2.4-1.8-4.3-4.1-4.3m-7.5 6.8c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5m7.5 0c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5" />
1323
+ <path d="m27.2 20.6l-2.3-2.8 2.3-2.8c0.3-0.4 0.2-0.9-0.2-1.2s-1-0.2-1.3 0.2l-2 2.4-2-2.4c-0.3-0.4-0.9-0.4-1.3-0.2-0.4 0.3-0.5 0.8-0.2 1.2l2.3 2.8-2.3 2.8c-0.3 0.4-0.2 0.9 0.2 1.2s1 0.2 1.3-0.2l2-2.4 2 2.4c0.3 0.4 0.9 0.4 1.3 0.2 0.4-0.3 0.4-0.8 0.2-1.2" />
1324
+ </g>
1325
+ `
1326
+ },
1327
+ dropbox: {
1328
+ viewBox: "0 0 32 32",
1329
+ defaultSize: 32,
1330
+ className: "upup-text-[#0061fe]",
1331
+ inner: `
1332
+ <path d="M10.5 7.5L5 10.955l5.5 3.454 5.5-3.454 5.5 3.454 5.5-3.454L21.5 7.5 16 10.955zM10.5 21.319L5 17.864l5.5-3.455 5.5 3.455zM16 17.864l5.5-3.455 5.5 3.455-5.5 3.455zM16 25.925l-5.5-3.455 5.5-3.454 5.5 3.454z" fill="currentcolor" fill-rule="nonzero" />
1333
+ `
1334
+ },
1335
+ "google-drive": {
1336
+ viewBox: "0 0 32 32",
1337
+ defaultSize: 32,
1338
+ attrs: { ...BRAND_FILLRULE },
1339
+ inner: `
1340
+ <g fill-rule="nonzero" fill="none">
1341
+ <path d="M6.663 22.284l.97 1.62c.202.34.492.609.832.804l3.465-5.798H5c0 .378.1.755.302 1.096l1.361 2.278z" fill="#0066DA" />
1342
+ <path d="M16 12.09l-3.465-5.798c-.34.195-.63.463-.832.804l-6.4 10.718A2.15 2.15 0 005 18.91h6.93L16 12.09z" fill="#00AC47" />
1343
+ <path d="M23.535 24.708c.34-.195.63-.463.832-.804l.403-.67 1.928-3.228c.201-.34.302-.718.302-1.096h-6.93l1.474 2.802 1.991 2.996z" fill="#EA4335" />
1344
+ <path d="M16 12.09l3.465-5.798A2.274 2.274 0 0018.331 6h-4.662c-.403 0-.794.11-1.134.292L16 12.09z" fill="#00832D" />
1345
+ <path d="M20.07 18.91h-8.14l-3.465 5.798c.34.195.73.292 1.134.292h12.802c.403 0 .794-.11 1.134-.292L20.07 18.91z" fill="#2684FC" />
1346
+ <path d="M23.497 12.455l-3.2-5.359a2.252 2.252 0 00-.832-.804L16 12.09l4.07 6.82h6.917c0-.377-.1-.755-.302-1.096l-3.188-5.359z" fill="#FFBA00" />
1347
+ </g>
1348
+ `
1349
+ },
1350
+ "one-drive": {
1351
+ viewBox: "0 0 32 32",
1352
+ defaultSize: 32,
1353
+ attrs: { ...BRAND_FILLRULE },
1354
+ inner: `
1355
+ <g fill="none" fill-rule="nonzero">
1356
+ <path d="M13.39 12.888l4.618 2.747 2.752-1.15a4.478 4.478 0 012.073-.352 6.858 6.858 0 00-5.527-5.04 6.895 6.895 0 00-6.876 2.982l.07-.002a5.5 5.5 0 012.89.815z" fill="#0364B8" />
1357
+ <path d="M13.39 12.887v.001a5.5 5.5 0 00-2.89-.815l-.07.002a5.502 5.502 0 00-4.822 2.964 5.43 5.43 0 00.38 5.62l4.073-1.702 1.81-.757 4.032-1.685 2.105-.88-4.619-2.748z" fill="#0078D4" />
1358
+ <path d="M22.833 14.133a4.479 4.479 0 00-2.073.352l-2.752 1.15.798.475 2.616 1.556 1.141.68 3.902 2.321a4.413 4.413 0 00-.022-4.25 4.471 4.471 0 00-3.61-2.284z" fill="#1490DF" />
1359
+ <path d="M22.563 18.346l-1.141-.68-2.616-1.556-.798-.475-2.105.88L11.87 18.2l-1.81.757-4.073 1.702A5.503 5.503 0 0010.5 23h12.031a4.472 4.472 0 003.934-2.333l-3.902-2.321z" fill="#28A8EA" />
1360
+ </g>
1361
+ `
1362
+ },
1363
+ link: {
1364
+ viewBox: "0 0 32 32",
1365
+ defaultSize: 32,
1366
+ inner: `<path d="M23.637 15.312l-2.474 2.464a3.582 3.582 0 01-.577.491c-.907.657-1.897.986-2.968.986a4.925 4.925 0 01-3.959-1.971c-.248-.329-.164-.902.165-1.149.33-.247.907-.164 1.155.164 1.072 1.478 3.133 1.724 4.618.656a.642.642 0 00.33-.328l2.473-2.463c1.238-1.313 1.238-3.366-.082-4.597a3.348 3.348 0 00-4.618 0l-1.402 1.395a.799.799 0 01-1.154 0 .79.79 0 010-1.15l1.402-1.394a4.843 4.843 0 016.843 0c2.062 1.805 2.144 5.007.248 6.896zm-8.081 5.664l-1.402 1.395a3.348 3.348 0 01-4.618 0c-1.319-1.23-1.319-3.365-.082-4.596l2.475-2.464.328-.328c.743-.492 1.567-.739 2.475-.657.906.165 1.648.574 2.143 1.314.248.329.825.411 1.155.165.33-.248.412-.822.165-1.15-.825-1.068-1.98-1.724-3.216-1.888-1.238-.247-2.556.082-3.628.902l-.495.493-2.474 2.464c-1.897 1.969-1.814 5.09.083 6.977.99.904 2.226 1.396 3.463 1.396s2.473-.492 3.463-1.395l1.402-1.396a.79.79 0 000-1.15c-.33-.328-.908-.41-1.237-.082z" fill="#FF753E" fill-rule="nonzero" />`
1367
+ },
1368
+ camera: {
1369
+ viewBox: "0 0 32 32",
1370
+ defaultSize: 32,
1371
+ inner: `
1372
+ <path d="M23.5 9.5c1.417 0 2.5 1.083 2.5 2.5v9.167c0 1.416-1.083 2.5-2.5 2.5h-15c-1.417 0-2.5-1.084-2.5-2.5V12c0-1.417 1.083-2.5 2.5-2.5h2.917l1.416-2.167C13 7.167 13.25 7 13.5 7h5c.25 0 .5.167.667.333L20.583 9.5H23.5zM16 11.417a4.706 4.706 0 00-4.75 4.75 4.704 4.704 0 004.75 4.75 4.703 4.703 0 004.75-4.75c0-2.663-2.09-4.75-4.75-4.75zm0 7.825c-1.744 0-3.076-1.332-3.076-3.074 0-1.745 1.333-3.077 3.076-3.077 1.744 0 3.074 1.333 3.074 3.076s-1.33 3.075-3.074 3.075z" fill="#02B383" fill-rule="nonzero" />
1373
+ `
1374
+ },
1375
+ audio: {
1376
+ viewBox: "0 0 32 32",
1377
+ defaultSize: 32,
1378
+ className: "upup-text-[#8030a3]",
1379
+ inner: `
1380
+ <path d="M21.143 12.297c.473 0 .857.383.857.857v2.572c0 3.016-2.24 5.513-5.143 5.931v2.64h2.572a.857.857 0 110 1.714H12.57a.857.857 0 110-1.714h2.572v-2.64C12.24 21.24 10 18.742 10 15.726v-2.572a.857.857 0 111.714 0v2.572A4.29 4.29 0 0016 20.01a4.29 4.29 0 004.286-4.285v-2.572c0-.474.384-.857.857-.857zM16 6.5a3 3 0 013 3v6a3 3 0 01-6 0v-6a3 3 0 013-3z" fill="currentcolor" fill-rule="nonzero" />
1381
+ `
1382
+ },
1383
+ "screen-capture": {
1384
+ viewBox: "0 0 32 32",
1385
+ defaultSize: 32,
1386
+ className: "upup-text-[#2c3e50]",
1387
+ inner: `
1388
+ <g fill="currentcolor" fill-rule="evenodd">
1389
+ <path d="M24.182 9H7.818C6.81 9 6 9.742 6 10.667v10c0 .916.81 1.666 1.818 1.666h4.546V24h7.272v-1.667h4.546c1 0 1.809-.75 1.809-1.666l.009-10C26 9.742 25.182 9 24.182 9zM24 21H8V11h16v10z" />
1390
+ <circle cx="16" cy="16" r="2" />
1391
+ </g>
1392
+ `
1393
+ },
1394
+ // ── Stroke UI (viewBox 0 0 24 24) ──
1395
+ upload: {
1396
+ viewBox: "0 0 24 24",
1397
+ defaultSize: 24,
1398
+ attrs: { ...STROKE_ATTRS },
1399
+ inner: `
1400
+ <path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
1401
+ <polyline points="7 9 12 4 17 9" />
1402
+ <line x1="12" y1="4" x2="12" y2="16" />
1403
+ `
1404
+ },
1405
+ loader: {
1406
+ viewBox: "0 0 24 24",
1407
+ defaultSize: 24,
1408
+ attrs: { ...STROKE_ATTRS },
1409
+ inner: `
1410
+ <line x1="12" y1="2" x2="12" y2="6" /><line x1="12" y1="18" x2="12" y2="22" />
1411
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76" /><line x1="16.24" y1="16.24" x2="19.07" y2="19.07" />
1412
+ <line x1="2" y1="12" x2="6" y2="12" /><line x1="18" y1="12" x2="22" y2="12" />
1413
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24" /><line x1="16.24" y1="7.76" x2="19.07" y2="4.93" />
1414
+ `
1415
+ },
1416
+ x: {
1417
+ viewBox: "0 0 24 24",
1418
+ defaultSize: 24,
1419
+ attrs: { ...STROKE_ATTRS },
1420
+ inner: `
1421
+ <line x1="18" y1="6" x2="6" y2="18" />
1422
+ <line x1="6" y1="6" x2="18" y2="18" />
1423
+ `
1424
+ },
1425
+ // Tabler 'chevron-left' — drive-browser "go back one folder" affordance.
1426
+ "chevron-left": {
1427
+ viewBox: "0 0 24 24",
1428
+ defaultSize: 24,
1429
+ attrs: { ...STROKE_ATTRS },
1430
+ inner: `
1431
+ <path d="M15 6l-6 6l6 6" />
1432
+ `
1433
+ },
1434
+ // Tabler 'trash' (matches react-icons/tb TbTrash exactly) — default for icons.FileDeleteIcon.
1435
+ trash: {
1436
+ viewBox: "0 0 24 24",
1437
+ defaultSize: 24,
1438
+ attrs: { ...STROKE_ATTRS },
1439
+ inner: `
1440
+ <path d="M4 7l16 0" />
1441
+ <path d="M10 11l0 6" />
1442
+ <path d="M14 11l0 6" />
1443
+ <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
1444
+ <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
1445
+ `
1446
+ },
1447
+ // Tabler 'camera-rotate' (matches react-icons/tb TbCameraRotate exactly) — default for icons.CameraRotateIcon.
1448
+ "camera-rotate": {
1449
+ viewBox: "0 0 24 24",
1450
+ defaultSize: 24,
1451
+ attrs: { ...STROKE_ATTRS },
1452
+ inner: `
1453
+ <path d="M5 7h1a2 2 0 0 0 2 -2a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2" />
1454
+ <path d="M11.245 15.904a3 3 0 0 0 3.755 -2.904m-2.25 -2.905a3 3 0 0 0 -3.75 2.905" />
1455
+ <path d="M14 13h2v2" />
1456
+ <path d="M10 13h-2v-2" />
1457
+ `
1458
+ },
1459
+ // Tabler 'capture' (matches react-icons/tb TbCapture exactly) — default for icons.CameraCaptureIcon.
1460
+ capture: {
1461
+ viewBox: "0 0 24 24",
1462
+ defaultSize: 24,
1463
+ attrs: { ...STROKE_ATTRS },
1464
+ inner: `
1465
+ <path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
1466
+ <path d="M4 16v2a2 2 0 0 0 2 2h2" />
1467
+ <path d="M16 4h2a2 2 0 0 1 2 2v2" />
1468
+ <path d="M16 20h2a2 2 0 0 0 2 -2v-2" />
1469
+ <path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
1470
+ `
1471
+ },
1472
+ // Tabler 'plus' (matches react-icons/tb TbPlus exactly) — default for icons.ContainerAddMoreIcon.
1473
+ plus: {
1474
+ viewBox: "0 0 24 24",
1475
+ defaultSize: 24,
1476
+ attrs: { ...STROKE_ATTRS },
1477
+ inner: `
1478
+ <path d="M12 5l0 14" />
1479
+ <path d="M5 12l14 0" />
1480
+ `
1481
+ },
1482
+ "layout-grid": {
1483
+ viewBox: "0 0 24 24",
1484
+ defaultSize: 24,
1485
+ attrs: { ...STROKE_ATTRS },
1486
+ inner: `
1487
+ <rect x="4" y="4" width="6" height="6" rx="1" /><rect x="14" y="4" width="6" height="6" rx="1" />
1488
+ <rect x="4" y="14" width="6" height="6" rx="1" /><rect x="14" y="14" width="6" height="6" rx="1" />
1489
+ `
1490
+ },
1491
+ "layout-list": {
1492
+ viewBox: "0 0 24 24",
1493
+ defaultSize: 24,
1494
+ attrs: { ...STROKE_ATTRS },
1495
+ inner: `
1496
+ <rect x="4" y="4" width="16" height="6" rx="1" /><rect x="4" y="14" width="16" height="6" rx="1" />
1497
+ `
1498
+ },
1499
+ folder: {
1500
+ viewBox: "0 0 24 24",
1501
+ defaultSize: 24,
1502
+ attrs: { ...STROKE_ATTRS },
1503
+ inner: `
1504
+ <path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" />
1505
+ `
1506
+ },
1507
+ // Tabler 'folder-filled' — solid folder glyph for the drive browser, tinted
1508
+ // amber at the call site so folders read unmistakably as folders.
1509
+ "folder-filled": {
1510
+ viewBox: "0 0 24 24",
1511
+ defaultSize: 24,
1512
+ attrs: { ...FILLED_ATTRS },
1513
+ inner: `
1514
+ <path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" />
1515
+ `
1516
+ },
1517
+ search: {
1518
+ viewBox: "0 0 24 24",
1519
+ defaultSize: 24,
1520
+ attrs: { ...STROKE_ATTRS },
1521
+ inner: `
1522
+ <circle cx="10" cy="10" r="7" /><line x1="21" y1="21" x2="15" y2="15" />
1523
+ `
1524
+ },
1525
+ user: {
1526
+ viewBox: "0 0 24 24",
1527
+ defaultSize: 24,
1528
+ attrs: { ...STROKE_ATTRS },
1529
+ inner: `
1530
+ <path d="M12 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
1531
+ `
1532
+ },
1533
+ file: {
1534
+ viewBox: "0 0 24 24",
1535
+ defaultSize: 24,
1536
+ attrs: { ...STROKE_ATTRS },
1537
+ inner: `
1538
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" />
1539
+ `
1540
+ },
1541
+ // Tabler 'files' — stacked pages, used for the "N files max" limit caption.
1542
+ "stacked-files": {
1543
+ viewBox: "0 0 24 24",
1544
+ defaultSize: 24,
1545
+ attrs: { ...STROKE_ATTRS },
1546
+ inner: `
1547
+ <path d="M15 3v4a1 1 0 0 0 1 1h4" />
1548
+ <path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" />
1549
+ <path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" />
1550
+ `
1551
+ },
1552
+ // Tabler 'database' — storage cylinder, used for the "SIZE each" limit caption
1553
+ // (a drive/storage glyph — deliberately NOT a speedometer).
1554
+ storage: {
1555
+ viewBox: "0 0 24 24",
1556
+ defaultSize: 24,
1557
+ attrs: { ...STROKE_ATTRS },
1558
+ inner: `
1559
+ <ellipse cx="12" cy="6" rx="8" ry="3" />
1560
+ <path d="M4 6v6a8 3 0 0 0 16 0v-6" />
1561
+ <path d="M4 12v6a8 3 0 0 0 16 0v-6" />
1562
+ `
1563
+ },
1564
+ // ── Filled UI (viewBox 0 0 24 24) ──
1565
+ "player-play": {
1566
+ viewBox: "0 0 24 24",
1567
+ defaultSize: 24,
1568
+ attrs: { ...FILLED_ATTRS },
1569
+ inner: `<path d="M6 4l15 8-15 8z" />`
1570
+ },
1571
+ "player-pause": {
1572
+ viewBox: "0 0 24 24",
1573
+ defaultSize: 24,
1574
+ attrs: { ...FILLED_ATTRS },
1575
+ inner: `
1576
+ <rect x="6" y="4" width="4" height="16" rx="1" /><rect x="14" y="4" width="4" height="16" rx="1" />
1577
+ `
1578
+ },
1579
+ // ── File-type typed glyphs (Tabler TbFileType*) ──
1580
+ ...FILE_TYPE_ICONS
1581
+ };
1582
+
1583
+ // src/icons/file-type-icon.ts
1584
+ function fileTypeIconName(extension) {
1585
+ const key = `file-${extension.toLowerCase()}`;
1586
+ return key in ICONS ? key : "file";
1587
+ }
1588
+
1589
+ export { BOX_DESCRIPTOR, DROPBOX_DESCRIPTOR, FILE_TYPE_EXTENSIONS, GOOGLE_DRIVE_DESCRIPTOR, ICONS, ONE_DRIVE_DESCRIPTOR, UpupCore, fileTypeIconName };