nuxt-ui-elements 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ui-elements",
3
3
  "configKey": "uiElements",
4
- "version": "0.1.30",
4
+ "version": "0.1.32",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -154,12 +154,12 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
154
154
  size: remoteFileData.size,
155
155
  mimeType: remoteFileData.mimeType,
156
156
  remoteUrl: remoteFileData.remoteUrl,
157
+ preview: remoteFileData.preview || file.preview || remoteFileData.remoteUrl,
158
+ // Use preview from storage, passed-in value, or fallback to remoteUrl
157
159
  source: "storage"
158
160
  // File loaded from remote storage
159
161
  };
160
- const processedFile = await runPluginStage("process", existingFile);
161
- if (!processedFile) return null;
162
- return processedFile;
162
+ return existingFile;
163
163
  })
164
164
  );
165
165
  const filteredFiles = initializedfiles.filter((f) => f !== null);
@@ -185,14 +185,14 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
185
185
  };
186
186
  const validatedFile = await runPluginStage("validate", uploadFile);
187
187
  if (!validatedFile) return null;
188
- const processedFile = await runPluginStage("process", validatedFile);
189
- if (!processedFile) return null;
190
- files.value.push(processedFile);
191
- emitter.emit("file:added", processedFile);
188
+ const preprocessedFile = await runPluginStage("preprocess", validatedFile);
189
+ const fileToAdd = preprocessedFile || validatedFile;
190
+ files.value.push(fileToAdd);
191
+ emitter.emit("file:added", fileToAdd);
192
192
  if (options.autoProceed) {
193
193
  upload();
194
194
  }
195
- return processedFile;
195
+ return validatedFile;
196
196
  };
197
197
  const addFiles = (newFiles) => {
198
198
  return Promise.all(newFiles.map((file) => addFile(file)));
@@ -328,10 +328,27 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
328
328
  emitter.emit("upload:start", filesToUpload);
329
329
  for (const file of filesToUpload) {
330
330
  try {
331
- updateFile(file.id, { status: "uploading" });
331
+ const processedFile = await runPluginStage("process", file);
332
+ 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
+ };
341
+ updateFile(file.id, { status: "error", error });
342
+ emitter.emit("file:error", { file, error });
343
+ continue;
344
+ }
345
+ if (processedFile.id !== file.id) {
346
+ files.value = files.value.map((f) => f.id === file.id ? processedFile : f);
347
+ }
348
+ updateFile(processedFile.id, { status: "uploading" });
332
349
  const onProgress = (progress) => {
333
- updateFile(file.id, { progress: { percentage: progress } });
334
- emitter.emit("upload:progress", { file, progress });
350
+ updateFile(processedFile.id, { progress: { percentage: progress } });
351
+ emitter.emit("upload:progress", { file: processedFile, progress });
335
352
  };
336
353
  const storagePlugin = getStoragePlugin();
337
354
  let uploadResult;
@@ -342,12 +359,14 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
342
359
  onProgress,
343
360
  emit: getPluginEmitFn(storagePlugin.id)
344
361
  };
345
- uploadResult = await storagePlugin.hooks.upload(file, context);
362
+ uploadResult = await storagePlugin.hooks.upload(processedFile, context);
346
363
  } else {
347
- uploadResult = await uploadFn(file, onProgress);
364
+ uploadResult = await uploadFn(processedFile, onProgress);
348
365
  }
349
366
  const remoteUrl = uploadResult?.url;
350
- updateFile(file.id, { status: "complete", uploadResult, remoteUrl });
367
+ const currentFile = files.value.find((f) => f.id === processedFile.id);
368
+ const preview = currentFile?.preview || remoteUrl;
369
+ updateFile(processedFile.id, { status: "complete", uploadResult, remoteUrl, preview });
351
370
  } catch (err) {
352
371
  const error = {
353
372
  message: err instanceof Error ? err.message : "Unknown upload error",
@@ -372,6 +391,7 @@ The LAST storage plugin ("${plugin.id}") will be used for uploads.
372
391
  case "validate":
373
392
  await hook(file, context);
374
393
  return file;
394
+ case "preprocess":
375
395
  case "process":
376
396
  case "complete":
377
397
  if (!file) throw new Error("File is required for this hook type");
@@ -3,7 +3,7 @@ export const PluginThumbnailGenerator = defineUploaderPlugin((pluginOptions) =>
3
3
  return {
4
4
  id: "thumbnail-generator",
5
5
  hooks: {
6
- process: async (file, _context) => {
6
+ preprocess: async (file, _context) => {
7
7
  const { width = 100, height = 100, quality = 0.7 } = pluginOptions;
8
8
  const sourceUrl = file.source === "local" ? URL.createObjectURL(file.data) : file.remoteUrl;
9
9
  if (file.mimeType.startsWith("image/")) {
@@ -1,7 +1,7 @@
1
1
  import { defineUploaderPlugin } from "../types.js";
2
2
  import { useFFMpeg } from "../../useFFMpeg.js";
3
3
  import { watchEffect } from "vue";
4
- export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions) => {
4
+ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions = {}) => {
5
5
  return {
6
6
  id: "video-compressor",
7
7
  hooks: {
@@ -35,10 +35,7 @@ export const PluginVideoCompressor = defineUploaderPlugin((pluginOptions) => {
35
35
  try {
36
36
  context.emit("start", { file, originalSize: file.size });
37
37
  const inputUrl = URL.createObjectURL(file.data);
38
- const ffmpeg = useFFMpeg({
39
- inputUrl,
40
- convertOptions: []
41
- });
38
+ const ffmpeg = useFFMpeg({ inputUrl });
42
39
  await ffmpeg.load();
43
40
  watchEffect(() => {
44
41
  context.emit("progress", { file, percentage: Math.round(ffmpeg.progress.value * 100) });
@@ -29,33 +29,112 @@ export type FileSource = 'local' | 'storage' | 'instagram' | 'dropbox' | 'google
29
29
  * Base properties shared by both local and remote upload files
30
30
  */
31
31
  export interface BaseUploadFile<TUploadResult = any> {
32
+ /** Unique identifier for the file */
32
33
  id: string;
34
+ /** Original filename (e.g., "vacation-photo.jpg") */
33
35
  name: string;
36
+ /** File size in bytes */
34
37
  size: number;
38
+ /** MIME type (e.g., "image/jpeg", "video/mp4") */
35
39
  mimeType: string;
40
+ /**
41
+ * Current upload status
42
+ */
36
43
  status: FileStatus;
44
+ /**
45
+ * Preview URL for displaying in UI (always populated after initialization)
46
+ * - For local files: Generated by thumbnail plugin as data URL, or remoteUrl after upload
47
+ * - For remote files: Provided by backend/storage, or falls back to remoteUrl
48
+ * - No manual fallback needed: Always available for display
49
+ */
37
50
  preview?: string;
51
+ /** Upload progress tracking */
38
52
  progress: FileProgress;
53
+ /** Error information if upload fails */
39
54
  error?: FileError;
55
+ /**
56
+ * Result returned by the upload function/storage plugin
57
+ * Can contain additional metadata like etag, key, etc.
58
+ */
40
59
  uploadResult?: TUploadResult;
60
+ /**
61
+ * Custom metadata for storing additional file information
62
+ * Plugins can add data here (e.g., { extension: 'jpg', originalWidth: 4000 })
63
+ */
41
64
  meta: Record<string, unknown>;
42
65
  }
43
66
  /**
44
67
  * Local upload file - originates from user's device
45
- * Has local data (File/Blob) and may get a remoteUrl after upload
68
+ *
69
+ * Lifecycle:
70
+ * 1. User selects file → created with data, no remoteUrl
71
+ * 2. Validation → runs validate hooks
72
+ * 3. Preprocessing → generates thumbnails (preview set here)
73
+ * 4. User clicks upload → runs process hooks (compression)
74
+ * 5. Upload → remoteUrl set after successful upload
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const localFile: LocalUploadFile = {
79
+ * source: 'local',
80
+ * data: fileBlob, // Has local data
81
+ * remoteUrl: undefined, // Not uploaded yet
82
+ * preview: 'data:image/jpeg;base64,...', // Generated by thumbnail plugin
83
+ * ...
84
+ * }
85
+ * ```
46
86
  */
47
87
  export interface LocalUploadFile<TUploadResult = any> extends BaseUploadFile<TUploadResult> {
88
+ /** Always 'local' for files selected from user's device */
48
89
  source: 'local';
90
+ /**
91
+ * The actual file data (File from input or Blob)
92
+ * Available for processing, compression, thumbnail generation, etc.
93
+ */
49
94
  data: File | Blob;
95
+ /**
96
+ * URL where file was uploaded (set after successful upload)
97
+ * undefined before upload, populated after upload completes
98
+ * Use to check if file has been uploaded: `if (file.remoteUrl) { ... }`
99
+ */
50
100
  remoteUrl?: string;
51
101
  }
52
102
  /**
53
- * Remote upload file - originates from remote source (cloud pickers, etc.)
54
- * Has remoteUrl but no local data
103
+ * Remote upload file - originates from remote source
104
+ *
105
+ * These files already exist remotely and don't need uploading.
106
+ * Common sources:
107
+ * - 'storage': Previously uploaded files being re-loaded
108
+ * - 'instagram', 'dropbox', etc.: Files from cloud pickers
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const remoteFile: RemoteUploadFile = {
113
+ * source: 'storage',
114
+ * data: null, // No local data - file is remote
115
+ * remoteUrl: 'https://storage.com/file.jpg', // Always present
116
+ * preview: 'https://storage.com/thumbnails/file.jpg', // Optional, from backend
117
+ * status: 'complete', // Already uploaded
118
+ * ...
119
+ * }
120
+ * ```
55
121
  */
56
122
  export interface RemoteUploadFile<TUploadResult = any> extends BaseUploadFile<TUploadResult> {
123
+ /**
124
+ * Source of the remote file
125
+ * - 'storage': File from your storage (previously uploaded)
126
+ * - 'instagram', 'dropbox', etc.: File from cloud picker
127
+ */
57
128
  source: Exclude<FileSource, 'local'>;
129
+ /**
130
+ * Always null for remote files (no local data available)
131
+ * File exists remotely and is accessed via remoteUrl
132
+ */
58
133
  data: null;
134
+ /**
135
+ * URL to the remote file (always present for remote files)
136
+ * Use this to display/download the file
137
+ */
59
138
  remoteUrl: string;
60
139
  }
61
140
  /**
@@ -199,9 +278,10 @@ export type UploadHook<TUploadResult = any, TPluginEvents extends Record<string,
199
278
  }>;
200
279
  export type GetRemoteFileHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (fileId: string, context: PluginContext<TPluginEvents>) => Promise<MinimumRemoteFileAttributes>;
201
280
  export type RemoveHook<TPluginEvents extends Record<string, any> = Record<string, never>> = (file: UploadFile, context: PluginContext<TPluginEvents>) => Promise<void>;
202
- export type PluginLifecycleStage = "validate" | "process" | "upload" | "complete";
281
+ export type PluginLifecycleStage = "validate" | "preprocess" | "process" | "upload" | "complete";
203
282
  export type PluginHooks<TUploadResult = any, TPluginEvents extends Record<string, any> = Record<string, never>> = {
204
283
  validate?: ValidationHook<TPluginEvents>;
284
+ preprocess?: ProcessingHook<TPluginEvents>;
205
285
  process?: ProcessingHook<TPluginEvents>;
206
286
  upload?: UploadHook<TUploadResult, TPluginEvents>;
207
287
  getRemoteFile?: GetRemoteFileHook<TPluginEvents>;
@@ -280,5 +360,6 @@ type MinimumRemoteFileAttributes = {
280
360
  size: number;
281
361
  mimeType: string;
282
362
  remoteUrl: string;
363
+ preview?: string;
283
364
  };
284
365
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-elements",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",