nuxt-ui-elements 0.1.24 → 0.1.25

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 (30) hide show
  1. package/README.md +199 -160
  2. package/dist/module.d.mts +1 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +8 -1
  5. package/dist/runtime/composables/useUploader/index.d.ts +368 -0
  6. package/dist/runtime/composables/useUploader/index.js +274 -0
  7. package/dist/runtime/composables/useUploader/plugins/image-compressor.d.ts +49 -0
  8. package/dist/runtime/composables/useUploader/plugins/image-compressor.js +111 -0
  9. package/dist/runtime/composables/useUploader/plugins/index.d.ts +2 -0
  10. package/dist/runtime/composables/useUploader/plugins/index.js +2 -0
  11. package/dist/runtime/composables/useUploader/plugins/thumbnail-generator.d.ts +8 -0
  12. package/dist/runtime/composables/useUploader/plugins/thumbnail-generator.js +65 -0
  13. package/dist/runtime/composables/useUploader/types.d.ts +155 -0
  14. package/dist/runtime/composables/useUploader/types.js +0 -0
  15. package/dist/runtime/composables/useUploader/validators/allowed-file-types.d.ts +6 -0
  16. package/dist/runtime/composables/useUploader/validators/allowed-file-types.js +12 -0
  17. package/dist/runtime/composables/useUploader/validators/duplicate-file.d.ts +27 -0
  18. package/dist/runtime/composables/useUploader/validators/duplicate-file.js +26 -0
  19. package/dist/runtime/composables/useUploader/validators/index.d.ts +4 -0
  20. package/dist/runtime/composables/useUploader/validators/index.js +4 -0
  21. package/dist/runtime/composables/useUploader/validators/max-file-size.d.ts +6 -0
  22. package/dist/runtime/composables/useUploader/validators/max-file-size.js +12 -0
  23. package/dist/runtime/composables/useUploader/validators/max-files.d.ts +6 -0
  24. package/dist/runtime/composables/useUploader/validators/max-files.js +12 -0
  25. package/dist/runtime/index.d.ts +1 -0
  26. package/dist/runtime/index.js +1 -0
  27. package/dist/runtime/types/index.d.ts +1 -0
  28. package/dist/runtime/types/index.js +1 -0
  29. package/dist/types.d.mts +2 -0
  30. package/package.json +3 -2
@@ -0,0 +1,111 @@
1
+ export const PluginImageCompressor = (_, pluginOptions) => {
2
+ const {
3
+ maxWidth = 1920,
4
+ maxHeight = 1920,
5
+ quality = 0.85,
6
+ outputFormat = "auto",
7
+ minSizeToCompress = 1e5,
8
+ // 100KB
9
+ preserveMetadata = true
10
+ } = pluginOptions;
11
+ return {
12
+ id: "image-compressor",
13
+ hooks: {
14
+ process: async (file) => {
15
+ if (!file.mimeType.startsWith("image/")) {
16
+ return file;
17
+ }
18
+ if (file.mimeType === "image/gif") {
19
+ return file;
20
+ }
21
+ if (file.mimeType === "image/svg+xml") {
22
+ return file;
23
+ }
24
+ if (file.size < minSizeToCompress) {
25
+ return file;
26
+ }
27
+ try {
28
+ const sourceUrl = URL.createObjectURL(file.data);
29
+ const image = new Image();
30
+ image.src = sourceUrl;
31
+ await new Promise((resolve, reject) => {
32
+ image.onload = () => resolve();
33
+ image.onerror = () => reject(new Error("Failed to load image"));
34
+ });
35
+ const needsResize = image.width > maxWidth || image.height > maxHeight;
36
+ if (!needsResize && outputFormat === "auto") {
37
+ URL.revokeObjectURL(sourceUrl);
38
+ return file;
39
+ }
40
+ let targetWidth = image.width;
41
+ let targetHeight = image.height;
42
+ if (needsResize) {
43
+ const widthRatio = maxWidth / image.width;
44
+ const heightRatio = maxHeight / image.height;
45
+ const ratio = Math.min(widthRatio, heightRatio);
46
+ targetWidth = Math.round(image.width * ratio);
47
+ targetHeight = Math.round(image.height * ratio);
48
+ }
49
+ const canvas = document.createElement("canvas");
50
+ canvas.width = targetWidth;
51
+ canvas.height = targetHeight;
52
+ const ctx = canvas.getContext("2d");
53
+ if (!ctx) {
54
+ throw new Error("Could not get canvas context");
55
+ }
56
+ ctx.imageSmoothingEnabled = true;
57
+ ctx.imageSmoothingQuality = "high";
58
+ ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
59
+ let mimeType = file.mimeType;
60
+ if (outputFormat === "jpeg") {
61
+ mimeType = "image/jpeg";
62
+ } else if (outputFormat === "webp") {
63
+ mimeType = "image/webp";
64
+ } else if (outputFormat === "png") {
65
+ mimeType = "image/png";
66
+ }
67
+ const compressedBlob = await new Promise((resolve, reject) => {
68
+ canvas.toBlob(
69
+ (blob) => {
70
+ if (blob) {
71
+ resolve(blob);
72
+ } else {
73
+ reject(new Error("Failed to compress image"));
74
+ }
75
+ },
76
+ mimeType,
77
+ quality
78
+ );
79
+ });
80
+ URL.revokeObjectURL(sourceUrl);
81
+ if (compressedBlob.size < file.size) {
82
+ let newId = file.id;
83
+ if (outputFormat !== "auto" && outputFormat !== file.meta.extension) {
84
+ const extension = outputFormat === "jpeg" ? "jpg" : outputFormat;
85
+ newId = file.id.replace(/\.[^.]+$/, `.${extension}`);
86
+ }
87
+ file.meta.originalSize = file.size;
88
+ file.meta.compressionRatio = ((file.size - compressedBlob.size) / file.size * 100).toFixed(1);
89
+ return {
90
+ ...file,
91
+ id: newId,
92
+ data: compressedBlob,
93
+ size: compressedBlob.size,
94
+ mimeType,
95
+ meta: {
96
+ ...file.meta,
97
+ compressed: true,
98
+ originalSize: file.size,
99
+ compressionRatio: file.meta.compressionRatio
100
+ }
101
+ };
102
+ }
103
+ return file;
104
+ } catch (error) {
105
+ console.warn(`Image compression failed for ${file.name}:`, error);
106
+ return file;
107
+ }
108
+ }
109
+ }
110
+ };
111
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./thumbnail-generator.js";
2
+ export * from "./image-compressor.js";
@@ -0,0 +1,2 @@
1
+ export * from "./thumbnail-generator.js";
2
+ export * from "./image-compressor.js";
@@ -0,0 +1,8 @@
1
+ import type { PluginFn } from "../types.js";
2
+ interface ThumbnailGeneratorOptions {
3
+ width?: number;
4
+ height?: number;
5
+ quality?: number;
6
+ }
7
+ export declare const PluginThumbnailGenerator: PluginFn<ThumbnailGeneratorOptions>;
8
+ export {};
@@ -0,0 +1,65 @@
1
+ export const PluginThumbnailGenerator = (_options, pluginOptions) => {
2
+ return {
3
+ id: "thumbnail-generator",
4
+ hooks: {
5
+ process: async (file) => {
6
+ const { width = 100, height = 100, quality = 0.7 } = pluginOptions;
7
+ const sourceUrl = file.isRemote ? file.remoteUrl : URL.createObjectURL(file.data);
8
+ if (file.mimeType.startsWith("image/")) {
9
+ const image = new Image();
10
+ image.crossOrigin = "anonymous";
11
+ image.src = sourceUrl;
12
+ await new Promise((resolve) => {
13
+ image.onload = resolve;
14
+ });
15
+ const aspectRatio = image.width / image.height;
16
+ let targetWidth = width;
17
+ let targetHeight = height;
18
+ if (aspectRatio > 1) {
19
+ targetHeight = width / aspectRatio;
20
+ } else {
21
+ targetWidth = height * aspectRatio;
22
+ }
23
+ const canvas = document.createElement("canvas");
24
+ canvas.width = targetWidth;
25
+ canvas.height = targetHeight;
26
+ const ctx = canvas.getContext("2d");
27
+ if (ctx) {
28
+ ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
29
+ const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
30
+ file.preview = thumbnailPreviewUrl;
31
+ }
32
+ } else if (file.mimeType.startsWith("video/")) {
33
+ const video = document.createElement("video");
34
+ video.src = sourceUrl;
35
+ video.crossOrigin = "anonymous";
36
+ video.currentTime = 1;
37
+ await new Promise((resolve) => {
38
+ video.onloadeddata = () => {
39
+ video.onseeked = resolve;
40
+ video.currentTime = 1;
41
+ };
42
+ });
43
+ const aspectRatio = video.videoWidth / video.videoHeight;
44
+ let targetWidth = width;
45
+ let targetHeight = height;
46
+ if (aspectRatio > 1) {
47
+ targetHeight = width / aspectRatio;
48
+ } else {
49
+ targetWidth = height * aspectRatio;
50
+ }
51
+ const canvas = document.createElement("canvas");
52
+ canvas.width = targetWidth;
53
+ canvas.height = targetHeight;
54
+ const ctx = canvas.getContext("2d");
55
+ if (ctx) {
56
+ ctx.drawImage(video, 0, 0, targetWidth, targetHeight);
57
+ const thumbnailPreviewUrl = canvas.toDataURL("image/jpeg", quality);
58
+ file.preview = thumbnailPreviewUrl;
59
+ }
60
+ }
61
+ return file;
62
+ }
63
+ }
64
+ };
65
+ };
@@ -0,0 +1,155 @@
1
+ import type { Emitter } from "mitt";
2
+ /**
3
+ * PUBLIC API - Types users commonly need
4
+ * These are exported from the main package
5
+ */
6
+ export type FileStatus = "waiting" | "preprocessing" | "uploading" | "postprocessing" | "complete" | "error";
7
+ export type UploadStatus = "waiting" | "uploading";
8
+ export interface FileProgress {
9
+ percentage: number;
10
+ }
11
+ export interface FileError {
12
+ message: string;
13
+ details?: unknown;
14
+ }
15
+ export interface UploadFile<TUploadResult = any> {
16
+ id: string;
17
+ name: string;
18
+ size: number;
19
+ mimeType: string;
20
+ data: File | Blob;
21
+ status: FileStatus;
22
+ preview?: string;
23
+ progress: FileProgress;
24
+ error?: FileError;
25
+ uploadResult?: TUploadResult;
26
+ isRemote?: boolean;
27
+ remoteUrl?: string;
28
+ meta: Record<string, unknown>;
29
+ }
30
+ export type UploadFn<TUploadResult = any> = (file: UploadFile<TUploadResult>, onProgress: (progress: number) => void) => Promise<TUploadResult>;
31
+ export type GetRemoteFileFn = (fileId: string) => Promise<MinimumRemoteFileAttributes>;
32
+ export interface UploadOptions {
33
+ /**
34
+ * Custom plugins to add (in addition to built-in plugins)
35
+ */
36
+ plugins?: Plugin[];
37
+ /**
38
+ * Validate maximum number of files
39
+ * - false: disabled
40
+ * - number: enabled with limit
41
+ * @default false
42
+ */
43
+ maxFiles?: false | number;
44
+ /**
45
+ * Validate maximum file size in bytes
46
+ * - false: disabled
47
+ * - number: enabled with limit
48
+ * @default false
49
+ */
50
+ maxFileSize?: false | number;
51
+ /**
52
+ * Validate allowed file MIME types
53
+ * - false: disabled
54
+ * - string[]: enabled with allowed types
55
+ * @default false
56
+ */
57
+ allowedFileTypes?: false | string[];
58
+ /**
59
+ * Generate thumbnail previews for images/videos
60
+ * - false: disabled
61
+ * - true: enabled with defaults
62
+ * - object: enabled with custom options
63
+ * @default false
64
+ */
65
+ thumbnails?: false | true | ThumbnailOptions;
66
+ /**
67
+ * Compress images before upload
68
+ * - false: disabled
69
+ * - true: enabled with defaults
70
+ * - object: enabled with custom options
71
+ * @default false
72
+ */
73
+ imageCompression?: false | true | ImageCompressionOptions;
74
+ /**
75
+ * Automatically start upload after files are added
76
+ * @default false
77
+ */
78
+ autoProceed?: boolean;
79
+ }
80
+ export interface ThumbnailOptions {
81
+ width?: number;
82
+ height?: number;
83
+ quality?: number;
84
+ }
85
+ export interface ImageCompressionOptions {
86
+ maxWidth?: number;
87
+ maxHeight?: number;
88
+ quality?: number;
89
+ outputFormat?: "jpeg" | "webp" | "png" | "auto";
90
+ minSizeToCompress?: number;
91
+ preserveMetadata?: boolean;
92
+ }
93
+ export type UploaderEvents<TUploadResult = any> = {
94
+ "file:added": Readonly<UploadFile<TUploadResult>>;
95
+ "file:removed": Readonly<UploadFile<TUploadResult>>;
96
+ "file:processing": Readonly<UploadFile<TUploadResult>>;
97
+ "file:error": {
98
+ file: Readonly<UploadFile<TUploadResult>>;
99
+ error: FileError;
100
+ };
101
+ "upload:start": Array<Readonly<UploadFile<TUploadResult>>>;
102
+ "upload:complete": Array<Required<Readonly<UploadFile<TUploadResult>>>>;
103
+ "upload:error": FileError;
104
+ "upload:progress": {
105
+ file: Readonly<UploadFile<TUploadResult>>;
106
+ progress: number;
107
+ };
108
+ "files:reorder": {
109
+ oldIndex: number;
110
+ newIndex: number;
111
+ };
112
+ };
113
+ /**
114
+ * PLUGIN API - Types for building custom plugins
115
+ * Only needed if users want to create custom validators/processors
116
+ */
117
+ export type PluginContext = {
118
+ files: UploadFile[];
119
+ options: UploadOptions;
120
+ };
121
+ export type ValidationHook = (file: UploadFile, context: PluginContext) => Promise<true | UploadFile>;
122
+ export type ProcessingHook = (file: UploadFile, context: PluginContext) => Promise<UploadFile>;
123
+ export type SetupHook = (context: PluginContext) => Promise<void>;
124
+ export type PluginLifecycleStage = "validate" | "process" | "complete";
125
+ export type PluginHooks = {
126
+ validate?: ValidationHook;
127
+ process?: ProcessingHook;
128
+ complete?: ProcessingHook;
129
+ };
130
+ export interface Plugin {
131
+ id: string;
132
+ hooks: PluginHooks;
133
+ options?: UploadOptions;
134
+ }
135
+ export type PluginFn<TPluginOptions = unknown> = {
136
+ (context: PluginContext, pluginOptions: TPluginOptions): Plugin;
137
+ __pluginOptions?: TPluginOptions;
138
+ };
139
+ /**
140
+ * INTERNAL TYPES - Not commonly needed by users
141
+ * Kept exported for edge cases but not primary API
142
+ */
143
+ export type PreProcessor = (file: UploadFile) => Promise<UploadFile>;
144
+ export type Uploader = (file: Readonly<UploadFile>, emiter: Emitter<Pick<UploaderEvents, "upload:error" | "upload:progress">>) => Promise<string>;
145
+ export type Validator = (file: UploadFile) => Promise<boolean | FileError>;
146
+ export type Processor = (file: UploadFile) => Promise<File | Blob>;
147
+ export interface UploadBlob {
148
+ blobPath: string;
149
+ }
150
+ type MinimumRemoteFileAttributes = {
151
+ size: number;
152
+ mimeType: string;
153
+ remoteUrl: string;
154
+ };
155
+ export {};
File without changes
@@ -0,0 +1,6 @@
1
+ import type { PluginFn } from "../types.js";
2
+ interface ValidatorAllowedFileTypesOptions {
3
+ allowedFileTypes?: string[];
4
+ }
5
+ export declare const ValidatorAllowedFileTypes: PluginFn<ValidatorAllowedFileTypesOptions>;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ export const ValidatorAllowedFileTypes = (_, options) => {
2
+ return {
3
+ id: "validator-allowed-file-types",
4
+ hooks: {
5
+ validate: async (file) => {
6
+ if (options.allowedFileTypes && options.allowedFileTypes.includes(file.mimeType) || (options.allowedFileTypes && options.allowedFileTypes.length) === 0)
7
+ return file;
8
+ throw { message: `File type ${file.mimeType} is not allowed` };
9
+ }
10
+ }
11
+ };
12
+ };
@@ -0,0 +1,27 @@
1
+ import type { PluginFn } from "../types.js";
2
+ interface ValidatorDuplicateFileOptions {
3
+ /**
4
+ * Whether to allow duplicate files
5
+ * @default false
6
+ */
7
+ allowDuplicates?: boolean;
8
+ /**
9
+ * Custom error message for duplicates
10
+ */
11
+ errorMessage?: string;
12
+ }
13
+ /**
14
+ * Prevents uploading duplicate files based on name, size, and last modified date.
15
+ * Useful for preventing accidental double-uploads in social media scheduling.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const uploader = useUpload()
20
+ * uploader.addPlugin(ValidatorDuplicateFile, {
21
+ * allowDuplicates: false,
22
+ * errorMessage: 'This file has already been added'
23
+ * })
24
+ * ```
25
+ */
26
+ export declare const ValidatorDuplicateFile: PluginFn<ValidatorDuplicateFileOptions>;
27
+ export {};
@@ -0,0 +1,26 @@
1
+ export const ValidatorDuplicateFile = ({ files }, options) => {
2
+ const { allowDuplicates = false, errorMessage = "This file has already been added" } = options;
3
+ return {
4
+ id: "validator-duplicate-file",
5
+ hooks: {
6
+ validate: async (file) => {
7
+ if (allowDuplicates) {
8
+ return file;
9
+ }
10
+ const isDuplicate = files.some((existingFile) => {
11
+ const sameSize = existingFile.size === file.size;
12
+ const sameName = existingFile.name === file.name;
13
+ let sameDate = true;
14
+ if (file.data instanceof File && existingFile.data instanceof File) {
15
+ sameDate = existingFile.data.lastModified === file.data.lastModified;
16
+ }
17
+ return sameSize && sameName && sameDate;
18
+ });
19
+ if (isDuplicate) {
20
+ throw { message: errorMessage, details: { fileName: file.name } };
21
+ }
22
+ return file;
23
+ }
24
+ }
25
+ };
26
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./allowed-file-types.js";
2
+ export * from "./max-file-size.js";
3
+ export * from "./max-files.js";
4
+ export * from "./duplicate-file.js";
@@ -0,0 +1,4 @@
1
+ export * from "./allowed-file-types.js";
2
+ export * from "./max-file-size.js";
3
+ export * from "./max-files.js";
4
+ export * from "./duplicate-file.js";
@@ -0,0 +1,6 @@
1
+ import type { PluginFn } from "../types.js";
2
+ interface ValidatorMaxfileSizeOptions {
3
+ maxFileSize?: number;
4
+ }
5
+ export declare const ValidatorMaxfileSize: PluginFn<ValidatorMaxfileSizeOptions>;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ export const ValidatorMaxfileSize = (_, options) => {
2
+ return {
3
+ id: "validator-max-file-size",
4
+ hooks: {
5
+ validate: async (file) => {
6
+ if (options.maxFileSize && options.maxFileSize !== Infinity && file.size <= options.maxFileSize || options.maxFileSize && options.maxFileSize === Infinity)
7
+ return file;
8
+ throw { message: `File size exceeds the maximum limit of ${options.maxFileSize} bytes` };
9
+ }
10
+ }
11
+ };
12
+ };
@@ -0,0 +1,6 @@
1
+ import type { PluginFn } from "../types.js";
2
+ interface ValidatorMaxFilesOptions {
3
+ maxFiles?: number;
4
+ }
5
+ export declare const ValidatorMaxFiles: PluginFn<ValidatorMaxFilesOptions>;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ export const ValidatorMaxFiles = ({ files }, options) => {
2
+ return {
3
+ id: "validator-max-files",
4
+ hooks: {
5
+ validate: async (file) => {
6
+ if (options.maxFiles && options.maxFiles !== Infinity && files.length < options.maxFiles || options.maxFiles && options.maxFiles === Infinity)
7
+ return file;
8
+ throw { message: `Maximum number of files (${options.maxFiles}) exceeded` };
9
+ }
10
+ }
11
+ };
12
+ };
@@ -0,0 +1 @@
1
+ export * from "./types/index.js";
@@ -0,0 +1 @@
1
+ export * from "./types/index.js";
@@ -1,3 +1,4 @@
1
1
  export * from "../components/DialogConfirm.vue.js";
2
2
  export * from "../components/DialogAlert.vue.js";
3
3
  export * from "./tv.js";
4
+ export * from "../composables/useUploader/types.js";
@@ -1,3 +1,4 @@
1
1
  export * from "../components/DialogConfirm.vue";
2
2
  export * from "../components/DialogAlert.vue";
3
3
  export * from "./tv.js";
4
+ export * from "../composables/useUploader/types.js";
package/dist/types.d.mts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
3
  export { type ModuleOptions } from './module.mjs'
4
+
5
+ export * from '../dist/runtime/types/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-elements",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
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",
@@ -28,6 +28,7 @@
28
28
  "@internationalized/date": "^3.10.1",
29
29
  "@nuxt/kit": "^4.2.2",
30
30
  "@sindresorhus/slugify": "^3.0.0",
31
+ "mitt": "^3.0.1",
31
32
  "plur": "^6.0.0",
32
33
  "scule": "^1.3.0",
33
34
  "tailwind-variants": "^3.2.2"
@@ -37,7 +38,7 @@
37
38
  "@nuxt/eslint-config": "^1.12.1",
38
39
  "@nuxt/module-builder": "^1.0.2",
39
40
  "@nuxt/schema": "^4.2.2",
40
- "@nuxt/test-utils": "^3.22.0",
41
+ "@nuxt/test-utils": "^3.23.0",
41
42
  "@types/culori": "^4.0.1",
42
43
  "@types/node": "latest",
43
44
  "@vitest/coverage-v8": "^4.0.16",