nuxt-ui-elements 0.1.32 → 0.1.33

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 (22) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/composables/useUploadManager/index.d.ts +4 -4
  3. package/dist/runtime/composables/useUploadManager/index.js +112 -87
  4. package/dist/runtime/composables/useUploadManager/plugins/image-compressor.d.ts +1 -1
  5. package/dist/runtime/composables/useUploadManager/plugins/image-compressor.js +2 -2
  6. package/dist/runtime/composables/useUploadManager/plugins/storage/azure-datalake.d.ts +12 -1
  7. package/dist/runtime/composables/useUploadManager/plugins/storage/azure-datalake.js +62 -32
  8. package/dist/runtime/composables/useUploadManager/plugins/thumbnail-generator.d.ts +4 -3
  9. package/dist/runtime/composables/useUploadManager/plugins/thumbnail-generator.js +87 -56
  10. package/dist/runtime/composables/useUploadManager/plugins/video-compressor.d.ts +1 -1
  11. package/dist/runtime/composables/useUploadManager/plugins/video-compressor.js +17 -7
  12. package/dist/runtime/composables/useUploadManager/types.d.ts +128 -5
  13. package/dist/runtime/composables/useUploadManager/types.js +6 -0
  14. package/dist/runtime/composables/useUploadManager/utils.d.ts +23 -0
  15. package/dist/runtime/composables/useUploadManager/utils.js +45 -0
  16. package/dist/runtime/composables/useUploadManager/validators/allowed-file-types.d.ts +1 -1
  17. package/dist/runtime/composables/useUploadManager/validators/allowed-file-types.js +7 -3
  18. package/dist/runtime/composables/useUploadManager/validators/max-file-size.d.ts +2 -2
  19. package/dist/runtime/composables/useUploadManager/validators/max-file-size.js +7 -3
  20. package/dist/runtime/composables/useUploadManager/validators/max-files.d.ts +1 -1
  21. package/dist/runtime/composables/useUploadManager/validators/max-files.js +7 -3
  22. package/package.json +6 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ui-elements",
3
3
  "configKey": "uiElements",
4
- "version": "0.1.32",
4
+ "version": "0.1.33",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -184,8 +184,8 @@ export declare const useUploadManager: <TUploadResult = any>(_options?: UploadOp
184
184
  };
185
185
  })[]>>;
186
186
  totalProgress: import("vue").ComputedRef<number>;
187
- addFiles: (newFiles: File[]) => Promise<(UploadFile | null)[]>;
188
- addFile: (file: File) => Promise<UploadFile | null>;
187
+ addFiles: (newFiles: File[]) => Promise<UploadFile[]>;
188
+ addFile: (file: File) => Promise<UploadFile>;
189
189
  onGetRemoteFile: (fn: GetRemoteFileFn) => void;
190
190
  onUpload: (fn: UploadFn<TUploadResult>) => void;
191
191
  removeFile: (fileId: string) => Promise<void>;
@@ -453,14 +453,14 @@ export declare const useUploadManager: <TUploadResult = any>(_options?: UploadOp
453
453
  } | undefined;
454
454
  uploadResult?: import("vue").UnwrapRef<TUploadResult> | undefined;
455
455
  meta: Record<string, unknown>;
456
- } | undefined;
456
+ };
457
457
  upload: () => Promise<void>;
458
458
  reset: () => void;
459
459
  status: import("vue").Ref<UploadStatus, UploadStatus>;
460
460
  getFileData: (fileId: string) => Promise<Blob>;
461
461
  getFileURL: (fileId: string) => Promise<string>;
462
462
  getFileStream: (fileId: string) => Promise<ReadableStream<Uint8Array>>;
463
- replaceFileData: (fileId: string, newData: Blob, newName?: string) => Promise<void>;
463
+ replaceFileData: (fileId: string, newData: Blob, newName?: string, shouldAutoUpload?: boolean) => Promise<UploadFile>;
464
464
  updateFile: (fileId: string, updatedFile: Partial<UploadFile<TUploadResult>>) => void;
465
465
  initializeExistingFiles: (initialFiles: Array<Partial<UploadFile>>) => Promise<void>;
466
466
  addPlugin: (plugin: UploaderPlugin<any, any>) => void;
@@ -1,7 +1,11 @@
1
1
  import mitt from "mitt";
2
- import { computed, readonly, ref } from "vue";
3
- import { ValidatorAllowedFileTypes, ValidatorMaxfileSize, ValidatorMaxFiles } from "./validators/index.js";
2
+ import { computed, onBeforeUnmount, readonly, ref } from "vue";
3
+ import { ValidatorAllowedFileTypes, ValidatorMaxFileSize, ValidatorMaxFiles } from "./validators/index.js";
4
4
  import { PluginThumbnailGenerator, PluginImageCompressor } from "./plugins/index.js";
5
+ import { createPluginContext, createFileError, cleanupObjectURLs } from "./utils.js";
6
+ function isStoragePlugin(plugin) {
7
+ return plugin !== null && "upload" in plugin.hooks;
8
+ }
5
9
  function getExtension(fullFileName) {
6
10
  const lastDot = fullFileName.lastIndexOf(".");
7
11
  if (lastDot === -1 || lastDot === fullFileName.length - 1) {
@@ -10,6 +14,7 @@ function getExtension(fullFileName) {
10
14
  return fullFileName.slice(lastDot + 1).toLocaleLowerCase();
11
15
  }
12
16
  const defaultOptions = {
17
+ storage: void 0,
13
18
  plugins: [],
14
19
  maxFileSize: false,
15
20
  allowedFileTypes: false,
@@ -23,6 +28,7 @@ export const useUploadManager = (_options = {}) => {
23
28
  const files = ref([]);
24
29
  const emitter = mitt();
25
30
  const status = ref("waiting");
31
+ const createdObjectURLs = /* @__PURE__ */ new Map();
26
32
  const pluginEmitFunctions = /* @__PURE__ */ new Map();
27
33
  const getPluginEmitFn = (pluginId) => {
28
34
  let emitFn = pluginEmitFunctions.get(pluginId);
@@ -46,45 +52,53 @@ export const useUploadManager = (_options = {}) => {
46
52
  const sum = files.value.reduce((acc, file) => acc + file.progress.percentage, 0);
47
53
  return Math.round(sum / files.value.length);
48
54
  });
49
- const isStoragePlugin = (plugin) => {
50
- return !!(plugin.hooks.upload || plugin.hooks.getRemoteFile || plugin.hooks.remove);
51
- };
52
55
  const getStoragePlugin = () => {
56
+ if (options.storage) {
57
+ return options.storage;
58
+ }
53
59
  if (!options.plugins) return null;
54
60
  for (let i = options.plugins.length - 1; i >= 0; i--) {
55
- const plugin = options.plugins[i];
56
- if (plugin && isStoragePlugin(plugin)) {
61
+ const plugin = options.plugins[i] || null;
62
+ if (isStoragePlugin(plugin)) {
63
+ if (import.meta.dev) {
64
+ console.warn(
65
+ `[useUploadManager] Storage plugin "${plugin.id}" found in plugins array.
66
+ This is deprecated. Use the 'storage' option instead:
67
+
68
+ useUploadManager({ storage: ${plugin.id}(...) })`
69
+ );
70
+ }
57
71
  return plugin;
58
72
  }
59
73
  }
60
74
  return null;
61
75
  };
62
76
  const addPlugin = (plugin) => {
63
- if (isStoragePlugin(plugin)) {
64
- const existingStorage = options.plugins?.find((p) => isStoragePlugin(p));
65
- if (existingStorage && import.meta.dev) {
77
+ const hasUploadHook = "upload" in plugin.hooks;
78
+ if (hasUploadHook) {
79
+ if (import.meta.dev) {
66
80
  console.warn(
67
- `[useUploadManager] Multiple storage plugins detected!
81
+ `[useUploadManager] Storage plugin "${plugin.id}" should use the 'storage' option instead of 'plugins':
68
82
 
69
- You're trying to add "${plugin.id}" but "${existingStorage.id}" is already registered.
70
- The LAST storage plugin ("${plugin.id}") will be used for uploads.
71
-
72
- \u{1F4A1} If you need multiple storage destinations, create separate uploader instances:
73
-
74
- const s3Uploader = useUploadManager({ plugins: [PluginS3Storage({ ... })] })
75
- const azureUploader = useUploadManager({ plugins: [PluginAzureStorage({ ... })] })
83
+ useUploadManager({
84
+ storage: ${plugin.id}({ ... }), // \u2713 Correct
85
+ plugins: [...] // Only for validators/processors
86
+ })
76
87
  `
77
88
  );
78
89
  }
79
90
  }
80
- ;
81
- options.plugins?.push(plugin);
91
+ if (options.plugins) {
92
+ options.plugins.push(plugin);
93
+ } else {
94
+ options.plugins = [plugin];
95
+ }
82
96
  };
83
97
  if (options.maxFiles !== false && options.maxFiles !== void 0) {
84
98
  addPlugin(ValidatorMaxFiles({ maxFiles: options.maxFiles }));
85
99
  }
86
100
  if (options.maxFileSize !== false && options.maxFileSize !== void 0) {
87
- addPlugin(ValidatorMaxfileSize({ maxFileSize: options.maxFileSize }));
101
+ addPlugin(ValidatorMaxFileSize({ maxFileSize: options.maxFileSize }));
88
102
  }
89
103
  if (options.allowedFileTypes !== false && options.allowedFileTypes !== void 0 && options.allowedFileTypes.length > 0) {
90
104
  addPlugin(ValidatorAllowedFileTypes({ allowedFileTypes: options.allowedFileTypes }));
@@ -93,8 +107,8 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
93
107
  const thumbOpts = options.thumbnails === true ? {} : options.thumbnails || {};
94
108
  addPlugin(
95
109
  PluginThumbnailGenerator({
96
- width: thumbOpts.width ?? 128,
97
- height: thumbOpts.height ?? 128,
110
+ maxWidth: thumbOpts.width ?? 128,
111
+ maxHeight: thumbOpts.height ?? 128,
98
112
  quality: thumbOpts.quality ?? 1
99
113
  })
100
114
  );
@@ -131,14 +145,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
131
145
  const storagePlugin = getStoragePlugin();
132
146
  let remoteFileData;
133
147
  if (storagePlugin?.hooks.getRemoteFile) {
134
- const context = {
135
- files: files.value,
136
- options,
137
- emit: (event, payload) => {
138
- const prefixedEvent = `${storagePlugin.id}:${String(event)}`;
139
- emitter.emit(prefixedEvent, payload);
140
- }
141
- };
148
+ const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
142
149
  remoteFileData = await storagePlugin.hooks.getRemoteFile(file.id, context);
143
150
  } else {
144
151
  remoteFileData = await getRemoteFileFn(file.id);
@@ -183,46 +190,55 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
183
190
  extension
184
191
  }
185
192
  };
186
- const validatedFile = await runPluginStage("validate", uploadFile);
187
- if (!validatedFile) return null;
188
- const preprocessedFile = await runPluginStage("preprocess", validatedFile);
189
- const fileToAdd = preprocessedFile || validatedFile;
190
- files.value.push(fileToAdd);
191
- emitter.emit("file:added", fileToAdd);
192
- if (options.autoProceed) {
193
- upload();
193
+ try {
194
+ const validatedFile = await runPluginStage("validate", uploadFile);
195
+ if (!validatedFile) {
196
+ throw new Error(`File validation failed for ${file.name}`);
197
+ }
198
+ const preprocessedFile = await runPluginStage("preprocess", validatedFile);
199
+ const fileToAdd = preprocessedFile || validatedFile;
200
+ files.value.push(fileToAdd);
201
+ emitter.emit("file:added", fileToAdd);
202
+ if (options.autoProceed) {
203
+ upload();
204
+ }
205
+ return validatedFile;
206
+ } catch (err) {
207
+ const error = createFileError(uploadFile, err);
208
+ const fileWithError = { ...uploadFile, status: "error", error };
209
+ files.value.push(fileWithError);
210
+ emitter.emit("file:error", { file: fileWithError, error });
211
+ throw err;
194
212
  }
195
- return validatedFile;
196
213
  };
197
- const addFiles = (newFiles) => {
198
- return Promise.all(newFiles.map((file) => addFile(file)));
214
+ const addFiles = async (newFiles) => {
215
+ const results = await Promise.allSettled(newFiles.map((file) => addFile(file)));
216
+ const addedFiles = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
217
+ return addedFiles;
199
218
  };
200
219
  const removeFile = async (fileId) => {
201
- const file = getFile(fileId);
220
+ const file = files.value.find((f) => f.id === fileId);
202
221
  if (!file) return;
203
222
  if (file.remoteUrl) {
204
223
  const storagePlugin = getStoragePlugin();
205
224
  if (storagePlugin?.hooks.remove) {
206
225
  try {
207
- const context = {
208
- files: files.value,
209
- options,
210
- emit: (event, payload) => {
211
- const prefixedEvent = `${storagePlugin.id}:${String(event)}`;
212
- emitter.emit(prefixedEvent, payload);
213
- }
214
- };
226
+ const context = createPluginContext(storagePlugin.id, files.value, options, emitter);
215
227
  await storagePlugin.hooks.remove(file, context);
216
228
  } catch (error) {
217
229
  console.error(`Storage plugin remove error:`, error);
218
230
  }
219
231
  }
220
232
  }
233
+ cleanupObjectURLs(createdObjectURLs, file.id);
221
234
  files.value = files.value.filter((f) => f.id !== fileId);
222
235
  emitter.emit("file:removed", file);
223
236
  };
224
237
  const removeFiles = (fileIds) => {
225
238
  const removedFiles = files.value.filter((f) => fileIds.includes(f.id));
239
+ removedFiles.forEach((file) => {
240
+ cleanupObjectURLs(createdObjectURLs, file.id);
241
+ });
226
242
  files.value = files.value.filter((f) => !fileIds.includes(f.id));
227
243
  removedFiles.forEach((file) => {
228
244
  emitter.emit("file:removed", file);
@@ -231,6 +247,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
231
247
  };
232
248
  const clearFiles = () => {
233
249
  const allFiles = [...files.value];
250
+ cleanupObjectURLs(createdObjectURLs);
234
251
  files.value = [];
235
252
  allFiles.forEach((file) => {
236
253
  emitter.emit("file:removed", file);
@@ -239,9 +256,6 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
239
256
  };
240
257
  const getFileData = async (fileId) => {
241
258
  const file = getFile(fileId);
242
- if (!file) {
243
- throw new Error(`File not found: ${fileId}`);
244
- }
245
259
  if (file.size > 100 * 1024 * 1024) {
246
260
  console.warn(
247
261
  `getFileData: Loading large file (${(file.size / 1024 / 1024).toFixed(2)}MB) into memory. Consider using getFileURL() or getFileStream() for better performance.`
@@ -258,19 +272,19 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
258
272
  };
259
273
  const getFileURL = async (fileId) => {
260
274
  const file = getFile(fileId);
261
- if (!file) {
262
- throw new Error(`File not found: ${fileId}`);
263
- }
264
275
  if (file.source === "local") {
265
- return URL.createObjectURL(file.data);
276
+ const existingURL = createdObjectURLs.get(file.id);
277
+ if (existingURL) {
278
+ return existingURL;
279
+ }
280
+ const objectURL = URL.createObjectURL(file.data);
281
+ createdObjectURLs.set(file.id, objectURL);
282
+ return objectURL;
266
283
  }
267
284
  return file.remoteUrl;
268
285
  };
269
286
  const getFileStream = async (fileId) => {
270
287
  const file = getFile(fileId);
271
- if (!file) {
272
- throw new Error(`File not found: ${fileId}`);
273
- }
274
288
  if (file.source === "local") {
275
289
  return file.data.stream();
276
290
  }
@@ -280,11 +294,9 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
280
294
  }
281
295
  return response.body;
282
296
  };
283
- const replaceFileData = async (fileId, newData, newName) => {
297
+ const replaceFileData = async (fileId, newData, newName, shouldAutoUpload) => {
284
298
  const file = getFile(fileId);
285
- if (!file) {
286
- throw new Error(`File not found: ${fileId}`);
287
- }
299
+ cleanupObjectURLs(createdObjectURLs, fileId);
288
300
  const updatedFile = {
289
301
  ...file,
290
302
  source: "local",
@@ -294,12 +306,25 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
294
306
  status: "waiting",
295
307
  // Mark as needing upload
296
308
  progress: { percentage: 0 },
297
- remoteUrl: void 0
309
+ remoteUrl: void 0,
298
310
  // Clear old remote URL
311
+ meta: {}
312
+ // Clear old metadata (thumbnails, dimensions, etc.)
299
313
  };
314
+ const preprocessedFile = await runPluginStage("preprocess", updatedFile);
315
+ const finalFile = preprocessedFile || updatedFile;
300
316
  const index = files.value.findIndex((f) => f.id === fileId);
301
- files.value[index] = updatedFile;
302
- emitter.emit("file:added", updatedFile);
317
+ if (index === -1) {
318
+ throw new Error(`File not found: ${fileId}`);
319
+ }
320
+ files.value[index] = finalFile;
321
+ emitter.emit("file:replaced", finalFile);
322
+ emitter.emit("file:added", finalFile);
323
+ const shouldUpload = shouldAutoUpload ?? options.autoProceed;
324
+ if (shouldUpload) {
325
+ upload();
326
+ }
327
+ return finalFile;
303
328
  };
304
329
  const reorderFile = (oldIndex, newIndex) => {
305
330
  if (oldIndex === newIndex) {
@@ -321,7 +346,11 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
321
346
  emitter.emit("files:reorder", { oldIndex, newIndex });
322
347
  };
323
348
  const getFile = (fileId) => {
324
- return files.value.find((f) => f.id === fileId);
349
+ const file = files.value.find((f) => f.id === fileId);
350
+ if (!file) {
351
+ throw new Error(`File not found: ${fileId}`);
352
+ }
353
+ return file;
325
354
  };
326
355
  const upload = async () => {
327
356
  const filesToUpload = files.value.filter((f) => f.status === "waiting");
@@ -330,14 +359,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
330
359
  try {
331
360
  const processedFile = await runPluginStage("process", file);
332
361
  if (!processedFile) {
333
- const error = {
334
- message: "File processing failed",
335
- details: {
336
- fileName: file.name,
337
- fileSize: file.size,
338
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
339
- }
340
- };
362
+ const error = createFileError(file, new Error("File processing failed"));
341
363
  updateFile(file.id, { status: "error", error });
342
364
  emitter.emit("file:error", { file, error });
343
365
  continue;
@@ -352,6 +374,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
352
374
  };
353
375
  const storagePlugin = getStoragePlugin();
354
376
  let uploadResult;
377
+ let remoteUrl;
355
378
  if (storagePlugin?.hooks.upload) {
356
379
  const context = {
357
380
  files: files.value,
@@ -359,23 +382,18 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
359
382
  onProgress,
360
383
  emit: getPluginEmitFn(storagePlugin.id)
361
384
  };
362
- uploadResult = await storagePlugin.hooks.upload(processedFile, context);
385
+ const result = await storagePlugin.hooks.upload(processedFile, context);
386
+ uploadResult = result;
387
+ remoteUrl = result.url;
363
388
  } else {
364
389
  uploadResult = await uploadFn(processedFile, onProgress);
390
+ remoteUrl = typeof uploadResult === "string" ? uploadResult : void 0;
365
391
  }
366
- const remoteUrl = uploadResult?.url;
367
392
  const currentFile = files.value.find((f) => f.id === processedFile.id);
368
393
  const preview = currentFile?.preview || remoteUrl;
369
394
  updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview });
370
395
  } catch (err) {
371
- const error = {
372
- message: err instanceof Error ? err.message : "Unknown upload error",
373
- details: {
374
- fileName: file.name,
375
- fileSize: file.size,
376
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
377
- }
378
- };
396
+ const error = createFileError(file, err);
379
397
  updateFile(file.id, { status: "error", error });
380
398
  emitter.emit("file:error", { file, error });
381
399
  }
@@ -384,8 +402,15 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
384
402
  emitter.emit("upload:complete", completed);
385
403
  };
386
404
  const reset = () => {
405
+ cleanupObjectURLs(createdObjectURLs);
387
406
  files.value = [];
388
407
  };
408
+ onBeforeUnmount(() => {
409
+ createdObjectURLs.forEach((url) => {
410
+ URL.revokeObjectURL(url);
411
+ });
412
+ createdObjectURLs.clear();
413
+ });
389
414
  const callPluginHook = async (hook, stage, file, context) => {
390
415
  switch (stage) {
391
416
  case "validate":
@@ -52,5 +52,5 @@ interface ImageCompressorOptions {
52
52
  */
53
53
  preserveMetadata?: boolean;
54
54
  }
55
- export declare const PluginImageCompressor: (options: ImageCompressorOptions) => import("../types.js").Plugin<any, ImageCompressorEvents>;
55
+ export declare const PluginImageCompressor: (options: ImageCompressorOptions) => import("../types.js").ProcessingPlugin<any, ImageCompressorEvents>;
56
56
  export {};
@@ -1,5 +1,5 @@
1
- import { defineUploaderPlugin } from "../types.js";
2
- export const PluginImageCompressor = defineUploaderPlugin((pluginOptions) => {
1
+ import { defineProcessingPlugin } from "../types.js";
2
+ export const PluginImageCompressor = defineProcessingPlugin((pluginOptions) => {
3
3
  const {
4
4
  maxWidth = 1920,
5
5
  maxHeight = 1920,
@@ -30,6 +30,17 @@ export interface AzureDataLakeOptions {
30
30
  * @default true
31
31
  */
32
32
  autoCreateDirectory?: boolean;
33
+ /**
34
+ * Number of retry attempts for failed operations
35
+ * @default 3
36
+ */
37
+ retries?: number;
38
+ /**
39
+ * Initial delay between retries in milliseconds
40
+ * Uses exponential backoff: delay * (2 ^ attempt)
41
+ * @default 1000 (1 second)
42
+ */
43
+ retryDelay?: number;
33
44
  }
34
45
  export interface AzureUploadResult {
35
46
  /**
@@ -41,4 +52,4 @@ export interface AzureUploadResult {
41
52
  */
42
53
  blobPath: string;
43
54
  }
44
- export declare const PluginAzureDataLake: (options: AzureDataLakeOptions) => import("../../types.js").Plugin<any, Record<string, never>>;
55
+ export declare const PluginAzureDataLake: (options: AzureDataLakeOptions) => import("../../types.js").StoragePlugin<AzureUploadResult, Record<string, never>>;
@@ -1,10 +1,34 @@
1
1
  import { ref } from "vue";
2
2
  import { DataLakeDirectoryClient } from "@azure/storage-file-datalake";
3
- import { defineUploaderPlugin } from "../../types.js";
4
- export const PluginAzureDataLake = defineUploaderPlugin((options) => {
3
+ import { defineStoragePlugin } from "../../types.js";
4
+ export const PluginAzureDataLake = defineStoragePlugin((options) => {
5
5
  const sasURL = ref(options.sasURL || "");
6
6
  let refreshPromise = null;
7
7
  const directoryCheckedCache = /* @__PURE__ */ new Set();
8
+ const maxRetries = options.retries ?? 3;
9
+ const initialRetryDelay = options.retryDelay ?? 1e3;
10
+ async function withRetry(operation, operationName) {
11
+ let lastError;
12
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
13
+ try {
14
+ return await operation();
15
+ } catch (error) {
16
+ lastError = error;
17
+ if (attempt === maxRetries) {
18
+ break;
19
+ }
20
+ const delay = initialRetryDelay * Math.pow(2, attempt);
21
+ if (import.meta.dev) {
22
+ console.warn(
23
+ `[Azure Storage] ${operationName} failed (attempt ${attempt + 1}/${maxRetries + 1}). Retrying in ${delay}ms...`,
24
+ error
25
+ );
26
+ }
27
+ await new Promise((resolve) => setTimeout(resolve, delay));
28
+ }
29
+ }
30
+ throw new Error(`[Azure Storage] ${operationName} failed after ${maxRetries + 1} attempts: ${lastError?.message}`);
31
+ }
8
32
  if (options.getSASUrl && !options.sasURL) {
9
33
  options.getSASUrl().then((url) => {
10
34
  sasURL.value = url;
@@ -61,46 +85,52 @@ export const PluginAzureDataLake = defineUploaderPlugin((options) => {
61
85
  if (file.source !== "local" || file.data === null) {
62
86
  throw new Error("Cannot upload remote file - no local data available");
63
87
  }
64
- const fileClient = await getFileClient(file.id);
65
- await fileClient.upload(file.data, {
66
- metadata: {
67
- ...options.metadata,
68
- mimeType: file.mimeType,
69
- size: String(file.size),
70
- originalName: file.name
71
- },
72
- pathHttpHeaders: {
73
- ...options.pathHttpHeaders,
74
- contentType: file.mimeType
75
- },
76
- onProgress: ({ loadedBytes }) => {
77
- const uploadedPercentage = Math.round(loadedBytes / file.size * 100);
78
- context.onProgress(uploadedPercentage);
79
- }
80
- });
81
- return {
82
- url: fileClient.url,
83
- blobPath: fileClient.name
84
- };
88
+ return withRetry(async () => {
89
+ const fileClient = await getFileClient(file.id);
90
+ await fileClient.upload(file.data, {
91
+ metadata: {
92
+ ...options.metadata,
93
+ mimeType: file.mimeType,
94
+ size: String(file.size),
95
+ originalName: file.name
96
+ },
97
+ pathHttpHeaders: {
98
+ ...options.pathHttpHeaders,
99
+ contentType: file.mimeType
100
+ },
101
+ onProgress: ({ loadedBytes }) => {
102
+ const uploadedPercentage = Math.round(loadedBytes / file.size * 100);
103
+ context.onProgress(uploadedPercentage);
104
+ }
105
+ });
106
+ return {
107
+ url: fileClient.url,
108
+ blobPath: fileClient.name
109
+ };
110
+ }, `Upload file "${file.name}"`);
85
111
  },
86
112
  /**
87
113
  * Get remote file metadata from Azure
88
114
  */
89
115
  async getRemoteFile(fileId, _context) {
90
- const fileClient = await getFileClient(fileId);
91
- const properties = await fileClient.getProperties();
92
- return {
93
- size: properties.contentLength || 0,
94
- mimeType: properties.contentType || "application/octet-stream",
95
- remoteUrl: fileClient.url
96
- };
116
+ return withRetry(async () => {
117
+ const fileClient = await getFileClient(fileId);
118
+ const properties = await fileClient.getProperties();
119
+ return {
120
+ size: properties.contentLength || 0,
121
+ mimeType: properties.contentType || "application/octet-stream",
122
+ remoteUrl: fileClient.url
123
+ };
124
+ }, `Get remote file "${fileId}"`);
97
125
  },
98
126
  /**
99
127
  * Delete file from Azure Blob Storage
100
128
  */
101
129
  async remove(file, _context) {
102
- const fileClient = await getFileClient(file.id);
103
- await fileClient.deleteIfExists();
130
+ return withRetry(async () => {
131
+ const fileClient = await getFileClient(file.id);
132
+ await fileClient.deleteIfExists();
133
+ }, `Delete file "${file.name}"`);
104
134
  }
105
135
  }
106
136
  };
@@ -1,7 +1,8 @@
1
1
  interface ThumbnailGeneratorOptions {
2
- width?: number;
3
- height?: number;
2
+ maxWidth?: number;
3
+ maxHeight?: number;
4
4
  quality?: number;
5
+ videoCaptureTime?: number;
5
6
  }
6
- export declare const PluginThumbnailGenerator: (options: ThumbnailGeneratorOptions) => import("../types.js").Plugin<any, Record<string, never>>;
7
+ export declare const PluginThumbnailGenerator: (options: ThumbnailGeneratorOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
7
8
  export {};
@@ -1,66 +1,97 @@
1
- import { defineUploaderPlugin } from "../types.js";
2
- export const PluginThumbnailGenerator = defineUploaderPlugin((pluginOptions) => {
1
+ import { defineProcessingPlugin } from "../types.js";
2
+ import { calculateThumbnailDimensions } from "../utils.js";
3
+ export const PluginThumbnailGenerator = defineProcessingPlugin((pluginOptions) => {
3
4
  return {
4
5
  id: "thumbnail-generator",
5
6
  hooks: {
6
- preprocess: async (file, _context) => {
7
- const { width = 100, height = 100, quality = 0.7 } = pluginOptions;
8
- const sourceUrl = file.source === "local" ? URL.createObjectURL(file.data) : file.remoteUrl;
9
- if (file.mimeType.startsWith("image/")) {
10
- const image = new Image();
11
- image.crossOrigin = "anonymous";
12
- image.src = sourceUrl;
13
- await new Promise((resolve) => {
14
- image.onload = resolve;
15
- });
16
- const aspectRatio = image.width / image.height;
17
- let targetWidth = width;
18
- let targetHeight = height;
19
- if (aspectRatio > 1) {
20
- targetHeight = width / aspectRatio;
21
- } else {
22
- targetWidth = height * aspectRatio;
23
- }
24
- const canvas = document.createElement("canvas");
25
- canvas.width = targetWidth;
26
- canvas.height = targetHeight;
27
- const ctx = canvas.getContext("2d");
28
- if (ctx) {
29
- ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
30
- const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
31
- file.preview = thumbnailPreviewUrl;
32
- }
33
- } else if (file.mimeType.startsWith("video/")) {
34
- const video = document.createElement("video");
35
- video.src = sourceUrl;
36
- video.crossOrigin = "anonymous";
37
- video.currentTime = 1;
38
- await new Promise((resolve) => {
39
- video.onloadeddata = () => {
40
- video.onseeked = resolve;
41
- video.currentTime = 1;
42
- };
43
- });
44
- const aspectRatio = video.videoWidth / video.videoHeight;
45
- let targetWidth = width;
46
- let targetHeight = height;
47
- if (aspectRatio > 1) {
48
- targetHeight = width / aspectRatio;
49
- } else {
50
- targetWidth = height * aspectRatio;
51
- }
52
- const canvas = document.createElement("canvas");
53
- canvas.width = targetWidth;
54
- canvas.height = targetHeight;
55
- const ctx = canvas.getContext("2d");
56
- if (ctx) {
57
- ctx.drawImage(video, 0, 0, targetWidth, targetHeight);
58
- const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
59
- file.preview = thumbnailPreviewUrl;
7
+ preprocess: async (file, context) => {
8
+ const { maxWidth = 200, maxHeight = 200, quality = 0.7, videoCaptureTime = 1 } = pluginOptions;
9
+ if (!file.mimeType.startsWith("image/") && !file.mimeType.startsWith("video/")) {
10
+ return file;
11
+ }
12
+ if (file.mimeType === "image/gif") {
13
+ return file;
14
+ }
15
+ if (file.mimeType === "image/svg+xml") {
16
+ return file;
17
+ }
18
+ if (file.source !== "local" || !file.data) {
19
+ return file;
20
+ }
21
+ const sourceUrl = URL.createObjectURL(file.data);
22
+ try {
23
+ if (file.mimeType.startsWith("image/")) {
24
+ const thumbnailUrl = await generateImageThumbnail(sourceUrl, maxWidth, maxHeight, quality);
25
+ file.meta.thumbnail = thumbnailUrl;
26
+ } else if (file.mimeType.startsWith("video/")) {
27
+ const thumbnailUrl = await generateVideoThumbnail(sourceUrl, maxWidth, maxHeight, quality, videoCaptureTime);
28
+ file.meta.thumbnail = thumbnailUrl;
60
29
  }
30
+ } catch (error) {
31
+ console.warn(`[ThumbnailGenerator] Failed for ${file.name}:`, error);
32
+ } finally {
33
+ URL.revokeObjectURL(sourceUrl);
61
34
  }
62
35
  return file;
63
36
  }
64
37
  }
65
38
  };
66
39
  });
40
+ async function generateImageThumbnail(sourceUrl, maxWidth, maxHeight, quality) {
41
+ return new Promise((resolve, reject) => {
42
+ const image = new Image();
43
+ image.onload = () => {
44
+ try {
45
+ const { width, height } = calculateThumbnailDimensions(image.width, image.height, maxWidth, maxHeight);
46
+ const canvas = document.createElement("canvas");
47
+ canvas.width = width;
48
+ canvas.height = height;
49
+ const ctx = canvas.getContext("2d");
50
+ if (!ctx) {
51
+ throw new Error("Failed to get canvas context");
52
+ }
53
+ ctx.drawImage(image, 0, 0, width, height);
54
+ const thumbnailUrl = canvas.toDataURL("image/jpeg", quality);
55
+ resolve(thumbnailUrl);
56
+ } catch (error) {
57
+ reject(error);
58
+ }
59
+ };
60
+ image.onerror = () => {
61
+ reject(new Error("Failed to load image"));
62
+ };
63
+ image.src = sourceUrl;
64
+ });
65
+ }
66
+ async function generateVideoThumbnail(sourceUrl, maxWidth, maxHeight, quality, captureTime) {
67
+ return new Promise((resolve, reject) => {
68
+ const video = document.createElement("video");
69
+ video.preload = "metadata";
70
+ video.muted = true;
71
+ video.onloadedmetadata = () => {
72
+ const seekTime = Math.min(captureTime, video.duration * 0.1);
73
+ video.currentTime = seekTime;
74
+ };
75
+ video.onseeked = () => {
76
+ try {
77
+ const { width, height } = calculateThumbnailDimensions(video.videoWidth, video.videoHeight, maxWidth, maxHeight);
78
+ const canvas = document.createElement("canvas");
79
+ canvas.width = width;
80
+ canvas.height = height;
81
+ const ctx = canvas.getContext("2d");
82
+ if (!ctx) {
83
+ throw new Error("Failed to get canvas context");
84
+ }
85
+ ctx.drawImage(video, 0, 0, width, height);
86
+ const thumbnailUrl = canvas.toDataURL("image/jpeg", quality);
87
+ resolve(thumbnailUrl);
88
+ } catch (error) {
89
+ reject(error);
90
+ }
91
+ };
92
+ video.onerror = () => {
93
+ reject(new Error("Failed to load video"));
94
+ };
95
+ video.src = sourceUrl;
96
+ });
97
+ }
@@ -68,5 +68,5 @@ type VideoCompressorEvents = {
68
68
  error: Error;
69
69
  };
70
70
  };
71
- export declare const PluginVideoCompressor: (options: VideoCompressorOptions) => import("../types.js").Plugin<any, VideoCompressorEvents>;
71
+ export declare const PluginVideoCompressor: (options: VideoCompressorOptions) => import("../types.js").ProcessingPlugin<any, VideoCompressorEvents>;
72
72
  export {};
@@ -1,7 +1,7 @@
1
- import { defineUploaderPlugin } from "../types.js";
1
+ import { defineProcessingPlugin } from "../types.js";
2
2
  import { useFFMpeg } from "../../useFFMpeg.js";
3
- import { watchEffect } from "vue";
4
- export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) => {
3
+ import { watch } from "vue";
4
+ export const PluginVideoCompressor = defineProcessingPlugin((pluginOptions = {}) => {
5
5
  return {
6
6
  id: "video-compressor",
7
7
  hooks: {
@@ -32,14 +32,19 @@ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) =
32
32
  });
33
33
  return file;
34
34
  }
35
+ let inputUrl;
36
+ let stopProgressWatch;
35
37
  try {
36
38
  context.emit("start", { file, originalSize: file.size });
37
- const inputUrl = URL.createObjectURL(file.data);
39
+ inputUrl = URL.createObjectURL(file.data);
38
40
  const ffmpeg = useFFMpeg({ inputUrl });
39
41
  await ffmpeg.load();
40
- watchEffect(() => {
41
- context.emit("progress", { file, percentage: Math.round(ffmpeg.progress.value * 100) });
42
- });
42
+ stopProgressWatch = watch(
43
+ () => ffmpeg.progress.value,
44
+ (progress) => {
45
+ context.emit("progress", { file, percentage: Math.round(progress * 100) });
46
+ }
47
+ );
43
48
  const convertOptions = ["-c:v", codec];
44
49
  if (bitrate) {
45
50
  convertOptions.push("-b:v", bitrate);
@@ -92,6 +97,11 @@ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) =
92
97
  context.emit("error", { file, error });
93
98
  console.error(`Video compression error for ${file.name}:`, error);
94
99
  return file;
100
+ } finally {
101
+ stopProgressWatch?.();
102
+ if (inputUrl) {
103
+ URL.revokeObjectURL(inputUrl);
104
+ }
95
105
  }
96
106
  }
97
107
  }
@@ -24,7 +24,7 @@ export interface FileError {
24
24
  * - 'onedrive': OneDrive picker
25
25
  * - ... add more as needed
26
26
  */
27
- export type FileSource = 'local' | 'storage' | 'instagram' | 'dropbox' | 'google-drive' | 'onedrive';
27
+ export type FileSource = "local" | "storage" | "instagram" | "dropbox" | "google-drive" | "onedrive";
28
28
  /**
29
29
  * Base properties shared by both local and remote upload files
30
30
  */
@@ -86,7 +86,7 @@ export interface BaseUploadFile<TUploadResult = any> {
86
86
  */
87
87
  export interface LocalUploadFile<TUploadResult = any> extends BaseUploadFile<TUploadResult> {
88
88
  /** Always 'local' for files selected from user's device */
89
- source: 'local';
89
+ source: "local";
90
90
  /**
91
91
  * The actual file data (File from input or Blob)
92
92
  * Available for processing, compression, thumbnail generation, etc.
@@ -125,7 +125,7 @@ export interface RemoteUploadFile<TUploadResult = any> extends BaseUploadFile<TU
125
125
  * - 'storage': File from your storage (previously uploaded)
126
126
  * - 'instagram', 'dropbox', etc.: File from cloud picker
127
127
  */
128
- source: Exclude<FileSource, 'local'>;
128
+ source: Exclude<FileSource, "local">;
129
129
  /**
130
130
  * Always null for remote files (no local data available)
131
131
  * File exists remotely and is accessed via remoteUrl
@@ -157,9 +157,38 @@ export type UploadFn<TUploadResult = any> = (file: UploadFile<TUploadResult>, on
157
157
  export type GetRemoteFileFn = (fileId: string) => Promise<MinimumRemoteFileAttributes>;
158
158
  export interface UploadOptions {
159
159
  /**
160
- * Custom plugins to add (in addition to built-in plugins)
160
+ * Storage plugin for uploading files (only one storage plugin can be active)
161
+ *
162
+ * Storage plugins handle the actual upload, download, and deletion of files
163
+ * from remote storage (Azure, S3, GCS, etc.)
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * storage: PluginAzureDataLake({
168
+ * sasURL: 'https://...',
169
+ * path: 'uploads'
170
+ * })
171
+ * ```
161
172
  */
162
- plugins?: Plugin<any, any>[];
173
+ storage?: StoragePlugin<any, any>;
174
+ /**
175
+ * Processing and validation plugins (validators, compressors, etc.)
176
+ *
177
+ * These plugins run during the file lifecycle:
178
+ * - validate: Check file before adding
179
+ * - preprocess: Generate thumbnails/previews immediately
180
+ * - process: Compress/transform before upload
181
+ * - complete: Post-upload processing
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * plugins: [
186
+ * ValidatorMaxFiles({ maxFiles: 10 }),
187
+ * PluginImageCompressor({ quality: 0.8 })
188
+ * ]
189
+ * ```
190
+ */
191
+ plugins?: ProcessingPlugin<any, any>[];
163
192
  /**
164
193
  * Validate maximum number of files
165
194
  * - false: disabled
@@ -219,6 +248,7 @@ export interface ImageCompressionOptions {
219
248
  type CoreUploaderEvents<TUploadResult = any> = {
220
249
  "file:added": Readonly<UploadFile<TUploadResult>>;
221
250
  "file:removed": Readonly<UploadFile<TUploadResult>>;
251
+ "file:replaced": Readonly<UploadFile<TUploadResult>>;
222
252
  "file:processing": Readonly<UploadFile<TUploadResult>>;
223
253
  "file:error": {
224
254
  file: Readonly<UploadFile<TUploadResult>>;
@@ -279,6 +309,26 @@ export type UploadHook<TUploadResult = any, TPluginEvents extends Record<string,
279
309
  export type GetRemoteFileHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (fileId: string, context: PluginContext<TPluginEvents>) => Promise<MinimumRemoteFileAttributes>;
280
310
  export type RemoveHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (file: UploadFile, context: PluginContext<TPluginEvents>) => Promise<void>;
281
311
  export type PluginLifecycleStage = "validate" | "preprocess" | "process" | "upload" | "complete";
312
+ /**
313
+ * Processing plugin hooks (validators, compressors, thumbnail generators)
314
+ */
315
+ export type ProcessingPluginHooks<TPluginEvents extends Record<string, any> = Record<string, never>> = {
316
+ validate?: ValidationHook<TPluginEvents>;
317
+ preprocess?: ProcessingHook<TPluginEvents>;
318
+ process?: ProcessingHook<TPluginEvents>;
319
+ complete?: ProcessingHook<TPluginEvents>;
320
+ };
321
+ /**
322
+ * Storage plugin hooks (upload, download, delete from remote storage)
323
+ */
324
+ export type StoragePluginHooks<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> = {
325
+ upload: UploadHook<TUploadResult, TPluginEvents>;
326
+ getRemoteFile?: GetRemoteFileHook<TPluginEvents>;
327
+ remove?: RemoveHook<TPluginEvents>;
328
+ };
329
+ /**
330
+ * All possible plugin hooks (for internal use)
331
+ */
282
332
  export type PluginHooks<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> = {
283
333
  validate?: ValidationHook<TPluginEvents>;
284
334
  preprocess?: ProcessingHook<TPluginEvents>;
@@ -288,16 +338,89 @@ export type PluginHooks<TUploadResult = any, TPluginEvents extends Record<string
288
338
  remove?: RemoveHook<TPluginEvents>;
289
339
  complete?: ProcessingHook<TPluginEvents>;
290
340
  };
341
+ /**
342
+ * Processing plugin (validators, compressors, thumbnail generators)
343
+ *
344
+ * These plugins transform or validate files without handling storage.
345
+ */
346
+ export interface ProcessingPlugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
347
+ id: string;
348
+ hooks: ProcessingPluginHooks<TPluginEvents>;
349
+ options?: UploadOptions;
350
+ events?: TPluginEvents;
351
+ }
352
+ /**
353
+ * Storage plugin (Azure, S3, GCS, etc.)
354
+ *
355
+ * Storage plugins handle uploading, downloading, and deleting files from remote storage.
356
+ * Only one storage plugin can be active at a time.
357
+ */
358
+ export interface StoragePlugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
359
+ id: string;
360
+ hooks: StoragePluginHooks<TUploadResult, TPluginEvents>;
361
+ options?: UploadOptions;
362
+ events?: TPluginEvents;
363
+ }
364
+ /**
365
+ * Base plugin interface (for internal use - supports both types)
366
+ */
291
367
  export interface Plugin<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> {
292
368
  id: string;
293
369
  hooks: PluginHooks<TUploadResult, TPluginEvents>;
294
370
  options?: UploadOptions;
295
371
  events?: TPluginEvents;
296
372
  }
373
+ /**
374
+ * Define a processing plugin (validators, compressors, thumbnail generators)
375
+ *
376
+ * @example Validator
377
+ * ```typescript
378
+ * export const ValidatorMaxFiles = defineProcessingPlugin<ValidatorOptions>((options) => ({
379
+ * id: 'validator-max-files',
380
+ * hooks: {
381
+ * validate: async (file, context) => {
382
+ * if (context.files.length >= options.maxFiles) {
383
+ * throw { message: 'Too many files' }
384
+ * }
385
+ * return file
386
+ * }
387
+ * }
388
+ * }))
389
+ * ```
390
+ */
391
+ export declare function defineProcessingPlugin<TPluginOptions = unknown, TPluginEvents extends Record<string, any> = Record<string, never>>(factory: (options: TPluginOptions) => ProcessingPlugin<any, TPluginEvents>): (options: TPluginOptions) => ProcessingPlugin<any, TPluginEvents>;
392
+ /**
393
+ * Define a storage plugin (Azure, S3, GCS, etc.)
394
+ *
395
+ * Storage plugins MUST implement the `upload` hook and should return an object with a `url` property.
396
+ *
397
+ * @example Azure Storage
398
+ * ```typescript
399
+ * export const PluginAzureDataLake = defineStoragePlugin<AzureOptions, AzureEvents>((options) => ({
400
+ * id: 'azure-datalake-storage',
401
+ * hooks: {
402
+ * upload: async (file, context) => {
403
+ * const fileClient = await getFileClient(file.id)
404
+ * await fileClient.upload(file.data, { onProgress: context.onProgress })
405
+ * return { url: fileClient.url, blobPath: fileClient.name }
406
+ * },
407
+ * getRemoteFile: async (fileId, context) => {
408
+ * // ... fetch file metadata ...
409
+ * },
410
+ * remove: async (file, context) => {
411
+ * // ... delete file ...
412
+ * }
413
+ * }
414
+ * }))
415
+ * ```
416
+ */
417
+ export declare function defineStoragePlugin<TPluginOptions = unknown, TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>>(factory: (options: TPluginOptions) => StoragePlugin<TUploadResult, TPluginEvents>): (options: TPluginOptions) => StoragePlugin<TUploadResult, TPluginEvents>;
297
418
  /**
298
419
  * Define an uploader plugin with type safety, context access, and custom events.
299
420
  * This is the universal plugin factory for all plugin types (storage, validators, processors).
300
421
  *
422
+ * @deprecated Use defineProcessingPlugin or defineStoragePlugin instead for better type safety
423
+ *
301
424
  * Hooks receive context as a parameter, making it clear when context is available.
302
425
  * Context includes current files, options, and an emit function for custom events.
303
426
  *
@@ -1,3 +1,9 @@
1
+ export function defineProcessingPlugin(factory) {
2
+ return factory;
3
+ }
4
+ export function defineStoragePlugin(factory) {
5
+ return factory;
6
+ }
1
7
  export function defineUploaderPlugin(factory) {
2
8
  return factory;
3
9
  }
@@ -0,0 +1,23 @@
1
+ import type { PluginContext, UploadFile, FileError, UploadOptions } from "./types.js";
2
+ import type { Emitter } from "mitt";
3
+ /**
4
+ * Create a plugin context object with consistent structure
5
+ */
6
+ export declare function createPluginContext<TPluginEvents extends Record<string, any> = Record<string, never>>(pluginId: string, files: UploadFile[], options: UploadOptions, emitter: Emitter<any>): PluginContext<TPluginEvents>;
7
+ /**
8
+ * Create a consistent file error object
9
+ */
10
+ export declare function createFileError(file: UploadFile, error: unknown): FileError;
11
+ /**
12
+ * Calculate thumbnail dimensions while maintaining aspect ratio
13
+ */
14
+ export declare function calculateThumbnailDimensions(originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number): {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ /**
19
+ * Cleanup object URLs to prevent memory leaks
20
+ * @param urlMap Map of file IDs to object URLs
21
+ * @param fileId Optional file ID to cleanup specific URL, or cleanup all if not provided
22
+ */
23
+ export declare function cleanupObjectURLs(urlMap: Map<string, string>, fileId?: string): void;
@@ -0,0 +1,45 @@
1
+ export function createPluginContext(pluginId, files, options, emitter) {
2
+ return {
3
+ files,
4
+ options,
5
+ emit: (event, payload) => {
6
+ const prefixedEvent = `${pluginId}:${String(event)}`;
7
+ emitter.emit(prefixedEvent, payload);
8
+ }
9
+ };
10
+ }
11
+ export function createFileError(file, error) {
12
+ return {
13
+ message: error instanceof Error ? error.message : String(error),
14
+ details: {
15
+ fileName: file.name,
16
+ fileSize: file.size,
17
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
18
+ }
19
+ };
20
+ }
21
+ export function calculateThumbnailDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
22
+ const aspectRatio = originalWidth / originalHeight;
23
+ let width = maxWidth;
24
+ let height = maxHeight;
25
+ if (aspectRatio > 1) {
26
+ height = maxWidth / aspectRatio;
27
+ } else {
28
+ width = maxHeight * aspectRatio;
29
+ }
30
+ return { width, height };
31
+ }
32
+ export function cleanupObjectURLs(urlMap, fileId) {
33
+ if (fileId) {
34
+ const url = urlMap.get(fileId);
35
+ if (url) {
36
+ URL.revokeObjectURL(url);
37
+ urlMap.delete(fileId);
38
+ }
39
+ } else {
40
+ for (const url of urlMap.values()) {
41
+ URL.revokeObjectURL(url);
42
+ }
43
+ urlMap.clear();
44
+ }
45
+ }
@@ -1,5 +1,5 @@
1
1
  interface ValidatorAllowedFileTypesOptions {
2
2
  allowedFileTypes?: string[];
3
3
  }
4
- export declare const ValidatorAllowedFileTypes: (options: ValidatorAllowedFileTypesOptions) => import("../types.js").Plugin<any, Record<string, never>>;
4
+ export declare const ValidatorAllowedFileTypes: (options: ValidatorAllowedFileTypesOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
5
5
  export {};
@@ -1,11 +1,15 @@
1
- import { defineUploaderPlugin } from "../types.js";
2
- export const ValidatorAllowedFileTypes = defineUploaderPlugin((options) => {
1
+ import { defineProcessingPlugin } from "../types.js";
2
+ export const ValidatorAllowedFileTypes = defineProcessingPlugin((options) => {
3
3
  return {
4
4
  id: "validator-allowed-file-types",
5
5
  hooks: {
6
6
  validate: async (file, _context) => {
7
- if (options.allowedFileTypes && options.allowedFileTypes.includes(file.mimeType) || (options.allowedFileTypes && options.allowedFileTypes.length) === 0)
7
+ if (!options.allowedFileTypes || options.allowedFileTypes.length === 0) {
8
8
  return file;
9
+ }
10
+ if (options.allowedFileTypes.includes(file.mimeType)) {
11
+ return file;
12
+ }
9
13
  throw { message: `File type ${file.mimeType} is not allowed` };
10
14
  }
11
15
  }
@@ -1,5 +1,5 @@
1
- interface ValidatorMaxfileSizeOptions {
1
+ interface ValidatorMaxFileSizeOptions {
2
2
  maxFileSize?: number;
3
3
  }
4
- export declare const ValidatorMaxfileSize: (options: ValidatorMaxfileSizeOptions) => import("../types.js").Plugin<any, Record<string, never>>;
4
+ export declare const ValidatorMaxFileSize: (options: ValidatorMaxFileSizeOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
5
5
  export {};
@@ -1,11 +1,15 @@
1
- import { defineUploaderPlugin } from "../types.js";
2
- export const ValidatorMaxfileSize = defineUploaderPlugin((options) => {
1
+ import { defineProcessingPlugin } from "../types.js";
2
+ export const ValidatorMaxFileSize = defineProcessingPlugin((options) => {
3
3
  return {
4
4
  id: "validator-max-file-size",
5
5
  hooks: {
6
6
  validate: async (file, _context) => {
7
- if (options.maxFileSize && options.maxFileSize !== Infinity && file.size <= options.maxFileSize || options.maxFileSize && options.maxFileSize === Infinity)
7
+ if (!options.maxFileSize || options.maxFileSize === Infinity) {
8
8
  return file;
9
+ }
10
+ if (file.size <= options.maxFileSize) {
11
+ return file;
12
+ }
9
13
  throw { message: `File size exceeds the maximum limit of ${options.maxFileSize} bytes` };
10
14
  }
11
15
  }
@@ -1,5 +1,5 @@
1
1
  interface ValidatorMaxFilesOptions {
2
2
  maxFiles?: number;
3
3
  }
4
- export declare const ValidatorMaxFiles: (options: ValidatorMaxFilesOptions) => import("../types.js").Plugin<any, Record<string, never>>;
4
+ export declare const ValidatorMaxFiles: (options: ValidatorMaxFilesOptions) => import("../types.js").ProcessingPlugin<any, Record<string, never>>;
5
5
  export {};
@@ -1,11 +1,15 @@
1
- import { defineUploaderPlugin } from "../types.js";
2
- export const ValidatorMaxFiles = defineUploaderPlugin((options) => {
1
+ import { defineProcessingPlugin } from "../types.js";
2
+ export const ValidatorMaxFiles = defineProcessingPlugin((options) => {
3
3
  return {
4
4
  id: "validator-max-files",
5
5
  hooks: {
6
6
  validate: async (file, context) => {
7
- if (options.maxFiles && options.maxFiles !== Infinity && context.files.length < options.maxFiles || options.maxFiles && options.maxFiles === Infinity)
7
+ if (options.maxFiles === void 0 || options.maxFiles === Infinity) {
8
8
  return file;
9
+ }
10
+ if (context.files.length < options.maxFiles) {
11
+ return file;
12
+ }
9
13
  throw { message: `Maximum number of files (${options.maxFiles}) exceeded` };
10
14
  }
11
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-elements",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "A collection of beautiful, animated UI components for Nuxt applications",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/genu/nuxt-ui-elements.git",
@@ -45,6 +45,7 @@
45
45
  "@types/culori": "^4.0.1",
46
46
  "@types/node": "latest",
47
47
  "@vitest/coverage-v8": "^4.0.17",
48
+ "better-sqlite3": "^12.6.0",
48
49
  "changelogen": "^0.6.2",
49
50
  "eslint": "^9.39.2",
50
51
  "eslint-config-prettier": "10.1.8",
@@ -73,6 +74,10 @@
73
74
  "dev": "pnpm dev:prepare && nuxi dev playground",
74
75
  "dev:build": "nuxi build playground",
75
76
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
77
+ "docs:dev": "nuxi dev docs",
78
+ "docs:build": "nuxi build docs",
79
+ "docs:generate": "nuxi generate docs",
80
+ "docs:preview": "nuxi preview docs",
76
81
  "lint": "eslint .",
77
82
  "lint:fix": "eslint . --fix",
78
83
  "format": "prettier --write .",