From 558f653380dfe62f36d159a0fbdc88bc24c974ca Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 15:21:24 +0530 Subject: [PATCH 1/8] feat(templates): add template save/load functionality with versioning Add template management API to save and restore editor transformation stacks: - Add getTemplate() and loadTemplate() methods to ImageKitEditorRef - Implement v1 versioning system for backward compatibility - Export TRANSFORMATION_STATE_VERSION constant Add comprehensive testing infrastructure: - Add backward compatibility test suite with v1 template fixtures - Configure Vitest with coverage reporting - Add test step to CI workflow Update documentation: - Add template management guide with examples - Document version compatibility approach - Add TypeScript usage examples Other changes: - Bump version from 2.1.0 to 2.2.0 - Add coverage directory to .gitignore - Update React example with template save/load demo --- .github/workflows/ci.yaml | 3 +- .gitignore | 3 +- README.md | 157 +- examples/react-example/package.json | 5 +- examples/react-example/src/index.tsx | 237 +- package.json | 3 + packages/imagekit-editor-dev/package.json | 13 +- .../src/ImageKitEditor.tsx | 45 +- .../src/backward-compatibility.test.ts | 2549 +++++++++++++++++ packages/imagekit-editor-dev/src/index.tsx | 3 +- packages/imagekit-editor-dev/src/store.ts | 31 +- packages/imagekit-editor-dev/vite.config.ts | 16 + packages/imagekit-editor/package.json | 5 +- turbo.json | 4 + yarn.lock | 1730 ++++++++++- 15 files changed, 4767 insertions(+), 37 deletions(-) create mode 100644 packages/imagekit-editor-dev/src/backward-compatibility.test.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da9c2de..0756460 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,10 +22,11 @@ jobs: node-version: 20.x cache: yarn - - name: 📦 Install deps, build, pack + - name: 📦 Install deps, lint, test, build, pack run: | yarn install --frozen-lockfile yarn lint + yarn test yarn package env: CI: true diff --git a/.gitignore b/.gitignore index e1c4725..d208200 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ packages/imagekit-editor/*.tgz .yarn builds packages/imagekit-editor/README.md -.cursor \ No newline at end of file +.cursor +coverage \ No newline at end of file diff --git a/README.md b/README.md index 1358919..9a3d936 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A powerful, React-based image editor component powered by ImageKit transformatio - 🖼️ **Visual Image Editor**: Interactive UI for applying ImageKit transformations - 📝 **Transformation History**: Track and manage applied transformations using ImageKit's chain transformations +- 💾 **Template Management**: Save and restore editor templates with built-in serialization support - 🎨 **Multiple Transformation Types**: Support for resize, crop, focus, quality adjustments, and more - 🖥️ **Desktop Interface**: Modern interface built with Chakra UI for desktop environments - 🔧 **TypeScript Support**: Full TypeScript support with comprehensive type definitions @@ -141,9 +142,15 @@ interface ImageKitEditorRef { loadImage: (image: string | FileElement) => void; loadImages: (images: Array) => void; setCurrentImage: (imageSrc: string) => void; + getTemplate: () => Transformation[]; + loadTemplate: (template: Omit[]) => void; } ``` +**Template Management Methods:** +- `getTemplate()` - Returns the current editor template (transformation stack) +- `loadTemplate(template)` - Loads a previously saved template into the editor + ### Export Options You can configure export functionality in two ways: @@ -212,6 +219,124 @@ The `metadata` object can contain any contextual information your application ne ## Advanced Usage +### Template Management + +You can save and restore editor templates, enabling features like: +- Template library +- Preset transformation stacks +- Collaborative editing workflows +- Quick application of common transformations + +**Template Versioning:** All templates are versioned (currently `v1`) to ensure backward compatibility and safe schema evolution. + +#### Saving a Template + +```tsx +import { useRef } from 'react'; +import { ImageKitEditor, type ImageKitEditorRef, type Transformation } from '@imagekit/editor'; + +function MyComponent() { + const editorRef = useRef(null); + + const handleSaveTemplate = () => { + const template = editorRef.current?.getTemplate(); + if (template) { + // Remove the auto-generated 'id' field before saving + const templateToSave = template.map(({ id, ...rest }) => rest); + + // Save to localStorage + localStorage.setItem('editorTemplate', JSON.stringify(templateToSave)); + + // Or save to your backend + await api.saveTemplate(templateToSave); + } + }; + + return ( + + ); +} +``` + +#### Loading a Template + +```tsx +const handleLoadTemplate = () => { + // Load from localStorage + const saved = localStorage.getItem('editorTemplate'); + + // Or load from your backend + // const saved = await api.getTemplate(); + + if (saved) { + const template = JSON.parse(saved); + editorRef.current?.loadTemplate(template); + } +}; +``` + +#### Template Structure + +A template is an array of transformation objects with version information: + +```tsx +interface Transformation { + id: string; // Auto-generated, omit when saving + key: string; // e.g., 'adjust-background' + name: string; // e.g., 'Background' + type: 'transformation'; + value: Record; // Transformation parameters + version?: 'v1'; // Template version for compatibility +} + +// Version constant +import { TRANSFORMATION_STATE_VERSION } from '@imagekit/editor'; +console.log(TRANSFORMATION_STATE_VERSION); // 'v1' +``` + +**Example template:** +```json +[ + { + "key": "adjust-background", + "name": "Background", + "type": "transformation", + "value": { + "backgroundType": "color", + "background": "#FFFFFF" + }, + "version": "v1" + }, + { + "key": "resize_and_crop-resize_and_crop", + "name": "Resize and Crop", + "type": "transformation", + "value": { + "width": 800, + "height": 600, + "mode": "pad_resize" + }, + "version": "v1" + } +] +``` + +**Version Compatibility:** +- `v1` - Current version with all transformation features +- The `version` field is optional for backward compatibility +- Future versions will maintain backward compatibility where possible + ### Signed URLs For private images that require signed URLs, you can pass file metadata that will be available in the signer function: @@ -269,10 +394,40 @@ import type { ImageKitEditorProps, ImageKitEditorRef, FileElement, - Signer + Signer, + Transformation // For template management } from '@imagekit/editor'; + +// Version constant for template compatibility +import { TRANSFORMATION_STATE_VERSION } from '@imagekit/editor'; ``` +## Testing + +The package includes comprehensive tests to ensure schema stability and API consistency. Run tests: + +```bash +# Run tests once +yarn test + +# Watch mode +yarn test:watch + +# With UI +yarn test:ui +``` + +### Schema Versioning + +The transformation schema is locked down with tests to ensure: +- All transformation categories exist and are stable +- All transformation items have required properties +- Schemas validate correctly +- Template serialization/deserialization works consistently +- Version compatibility is maintained + +Current schema version: **v1** + ## Contributing We welcome contributions! Please see our [contributing guidelines](./CONTRIBUTING.md) for more details. diff --git a/examples/react-example/package.json b/examples/react-example/package.json index 7fac272..6930172 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@imagekit/editor": "2.1.0", + "@imagekit/editor": "workspace:*", "@types/node": "^20.11.24", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", @@ -18,7 +18,8 @@ "scripts": { "dev": "vite --port 3000", "start": "vite --port 3000", - "preview": "vite preview" + "preview": "vite preview", + "test": "echo \"No tests in this example\"" }, "eslintConfig": { "extends": [ diff --git a/examples/react-example/src/index.tsx b/examples/react-example/src/index.tsx index a083ba6..c68b0a5 100644 --- a/examples/react-example/src/index.tsx +++ b/examples/react-example/src/index.tsx @@ -1,6 +1,11 @@ import { Icon } from "@chakra-ui/react" -import { ImageKitEditor, type ImageKitEditorProps } from "@imagekit/editor" -import type { ImageKitEditorRef } from "@imagekit/editor/dist/ImageKitEditor" +import { + ImageKitEditor, + type ImageKitEditorProps, + type ImageKitEditorRef, + type Transformation, + TRANSFORMATION_STATE_VERSION, +} from "@imagekit/editor" import { PiDownload } from "@react-icons/all-files/pi/PiDownload" import React, { useCallback, useEffect } from "react" import ReactDOM from "react-dom" @@ -12,6 +17,10 @@ function App() { ImageKitEditorProps<{ requireSignedUrl: boolean; fileName: string }> >() const ref = React.useRef(null) + const [savedTemplate, setSavedTemplate] = React.useState< + Omit[] | null + >(null) + const [shouldLoadTemplate, setShouldLoadTemplate] = React.useState(false) /** * Function moved from EditorLayout component @@ -23,6 +32,75 @@ function App() { ref.current?.loadImage(randomImage) }, []) + /** + * Load template when editor becomes available + */ + React.useEffect(() => { + if (open && shouldLoadTemplate && ref.current && savedTemplate) { + ref.current.loadTemplate(savedTemplate) + console.log("Loaded template:", savedTemplate) + setShouldLoadTemplate(false) + } + }, [open, shouldLoadTemplate, savedTemplate]) + + /** + * Save the current editor template + */ + const handleSaveTemplate = useCallback(() => { + const template = ref.current?.getTemplate() + if (template) { + // Remove the 'id' field from each transformation for storage + const templateToSave = template.map(({ id, ...rest }: Transformation) => rest) + setSavedTemplate(templateToSave) + // Also save to localStorage for persistence + localStorage.setItem("editorTemplate", JSON.stringify(templateToSave)) + console.log("Saved template:", templateToSave) + alert(`✅ Saved template with ${templateToSave.length} transformation(s)!`) + } else { + alert("⚠️ No transformations to save") + } + }, []) + + /** + * Load previously saved template + */ + const handleLoadTemplate = useCallback(() => { + if (savedTemplate) { + // Flag to load template and open editor + setShouldLoadTemplate(true) + setOpen(true) + } else { + // Try to load from localStorage + const stored = localStorage.getItem("editorTemplate") + if (stored) { + try { + const parsed = JSON.parse(stored) + setSavedTemplate(parsed) + setShouldLoadTemplate(true) + setOpen(true) + console.log("Loaded template from localStorage:", parsed) + } catch (e) { + console.error("Failed to parse saved template:", e) + alert("❌ Failed to load saved template - invalid JSON") + } + } else { + alert("⚠️ No saved template found") + } + } + }, [savedTemplate]) + + /** + * Clear the saved template + */ + const handleClearTemplate = useCallback(() => { + if (confirm("Are you sure you want to clear the saved template?")) { + setSavedTemplate(null) + localStorage.removeItem("editorTemplate") + console.log("Cleared saved template") + alert("🗑️ Template cleared!") + } + }, []) + useEffect(() => { setEditorProps({ initialImages: [ @@ -59,11 +137,20 @@ function App() { exportOptions: [ { type: "button", - label: "Export", + label: "Export Images", icon: , isVisible: true, onClick: (images, currentImage) => { - console.log(images, currentImage) + console.log("Export images:", images, currentImage) + }, + }, + { + type: "button", + label: "Save Template", + icon: , + isVisible: true, + onClick: () => { + handleSaveTemplate() }, }, // { @@ -89,18 +176,148 @@ function App() { console.log("Signed URL", request.url) return Promise.resolve(request.url) }, - }) - }, [handleAddImage]) + }) }, [handleAddImage, handleSaveTemplate]) const toggle = () => { - setOpen((prev) => !prev) + setOpen((prev: boolean) => !prev) } return ( <> - +
+

ImageKit Editor - Template Management Demo

+

+ This demo shows how to save and restore editor templates using the + editor's ref methods. +

+ +
+ + + + + {savedTemplate && ( + + )} + + {savedTemplate && ( +
+

+ ✓ Saved Template +

+

+ Transformations: {savedTemplate.length} +

+

+ Schema Version: {TRANSFORMATION_STATE_VERSION} +

+

+ Types:{" "} + {Array.from( + new Set(savedTemplate.map((t) => t.type)) + ).join(", ")} +

+
+ + 📋 View Template JSON + +
+                  {JSON.stringify(savedTemplate, null, 2)}
+                
+
+
+ )} +
+ +
+

📖 How to use Template Features:

+
    +
  1. Click "Open ImageKit Editor" and apply some transformations
  2. +
  3. Click the "Save Template" button in the editor header
  4. +
  5. Close the editor
  6. +
  7. + Click "Load Saved Template" - it will open the editor with all transformations restored +
  8. +
  9. Use "Clear Template" to remove the saved template
  10. +
+

+ 💾 Persistent Storage: Templates are saved to localStorage, so they persist across page reloads! +

+

+ Note: Template IDs are automatically generated on load to ensure uniqueness and enable reusability. +

+
+
+ {open && editorProps && } ) diff --git a/package.json b/package.json index 68cf98d..9c4303c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ }, "devDependencies": { "@biomejs/biome": "2.1.1", + "@types/jsdom": "^28", + "@vitest/coverage-v8": "^4.0.18", "husky": "^9.1.7", + "jsdom": "^28.1.0", "lint-staged": "^16.1.2", "shx": "^0.4.0", "turbo": "^2.0.1" diff --git a/packages/imagekit-editor-dev/package.json b/packages/imagekit-editor-dev/package.json index 45bbed6..d1bd98f 100644 --- a/packages/imagekit-editor-dev/package.json +++ b/packages/imagekit-editor-dev/package.json @@ -1,6 +1,6 @@ { "name": "imagekit-editor-dev", - "version": "0.0.0", + "version": "2.2.0", "description": "AI Image Editor powered by ImageKit", "scripts": { "prepack": "yarn build", @@ -8,7 +8,11 @@ "dev": "DEBUG=* vite build --watch", "start": "vite build --watch", "analyze": "vite build --mode analyze", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage" }, "keywords": [], "author": { @@ -29,13 +33,16 @@ "@types/react-color": "^2", "@types/react-dom": "^17.0.2", "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^2.1.9", + "@vitest/ui": "^2.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "rollup-plugin-visualizer": "^5.12.0", "terser": "^5.43.1", "typescript": "4.9.3", "vite": "^6.3.5", - "vite-plugin-dts": "5.0.0-beta.3" + "vite-plugin-dts": "5.0.0-beta.3", + "vitest": "^2.0.0" }, "dependencies": { "@chakra-ui/icons": "1.1.1", diff --git a/packages/imagekit-editor-dev/src/ImageKitEditor.tsx b/packages/imagekit-editor-dev/src/ImageKitEditor.tsx index 675f583..d9bdf1b 100644 --- a/packages/imagekit-editor-dev/src/ImageKitEditor.tsx +++ b/packages/imagekit-editor-dev/src/ImageKitEditor.tsx @@ -9,14 +9,54 @@ import { type InputFileElement, type RequiredMetadata, type Signer, + type Transformation, useEditorStore, } from "./store" import { themeOverrides } from "./theme" export interface ImageKitEditorRef { + /** + * Loads a single image into the editor + * @param image - Image URL string or FileElement with metadata + */ loadImage: (image: string | InputFileElement) => void + + /** + * Loads multiple images into the editor + * @param images - Array of image URL strings or FileElements with metadata + */ loadImages: (images: Array) => void + + /** + * Switches the current active image + * @param imageSrc - URL of the image to set as current + */ setCurrentImage: (imageSrc: string) => void + + /** + * Gets the current editor template (transformation stack) + * @returns Array of transformation objects representing the template + * @example + * ```tsx + * const template = editorRef.current?.getTemplate() + * // Save to localStorage or backend + * localStorage.setItem('editorTemplate', JSON.stringify( + * template.map(({ id, ...rest }) => rest) + * )) + * ``` + */ + getTemplate: () => Transformation[] + + /** + * Loads a template (transformation stack) into the editor + * @param template - Array of transformation objects without the 'id' field + * @example + * ```tsx + * const saved = JSON.parse(localStorage.getItem('editorTemplate')) + * editorRef.current?.loadTemplate(saved) + * ``` + */ + loadTemplate: (template: Omit[]) => void } interface EditorProps { @@ -41,6 +81,7 @@ function ImageKitEditorImpl( transformations, initialize, destroy, + loadTemplate, } = useEditorStore() const handleOnClose = () => { @@ -73,8 +114,10 @@ function ImageKitEditorImpl( loadImage: addImage, loadImages: addImages, setCurrentImage, + getTemplate: () => transformations, + loadTemplate, }), - [addImage, addImages, setCurrentImage], + [addImage, addImages, setCurrentImage, transformations, loadTemplate], ) const mergedThemes = merge(defaultTheme, themeOverrides, theme) diff --git a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts new file mode 100644 index 0000000..4a644c0 --- /dev/null +++ b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts @@ -0,0 +1,2549 @@ +import { describe, expect, it } from "vitest" +import type { Transformation } from "./store" +import { TRANSFORMATION_STATE_VERSION } from "./store" +import { transformationFormatters, transformationSchema } from "./schema" + +/** + * V1 Template Fixtures + * These represent real saved templates from v1 of the editor. + * Tests ensure these templates continue to work even after UI/schema changes. + */ + +// Simple single transformation template +const V1_BASIC_TEMPLATE: Omit[] = [ + { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "color", + background: "#FFFFFF", + }, + version: "v1", + }, +] + +// Multiple common transformations +const V1_COMMON_TEMPLATE: Omit[] = [ + { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "pad_resize", + }, + version: "v1", + }, + { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "color", + background: "#E8E8E8", + }, + version: "v1", + }, + { + key: "adjust-rotate", + name: "Rotate", + type: "transformation", + value: { + rotate: 90, + }, + version: "v1", + }, +] + +// Complex template with gradient background +const V1_GRADIENT_TEMPLATE: Omit[] = [ + { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradient: { + from: "#FFFFFFFF", + to: "#00000000", + direction: "bottom", + stopPoint: 100, + }, + }, + version: "v1", + }, +] + +// AI transformations +const V1_AI_TEMPLATE: Omit[] = [ + { + key: "ai-bgremove", + name: "Remove Background", + type: "transformation", + value: { + bgremove: true, + }, + version: "v1", + }, + { + key: "ai-changebg", + name: "Change Background", + type: "transformation", + value: { + changebg: "beach sunset", + }, + version: "v1", + }, +] + +// Delivery optimizations +const V1_DELIVERY_TEMPLATE: Omit[] = [ + { + key: "delivery-quality", + name: "Quality", + type: "transformation", + value: { + quality: 80, + }, + version: "v1", + }, + { + key: "delivery-format", + name: "Format", + type: "transformation", + value: { + format: "webp", + }, + version: "v1", + }, +] + +// Layer transformations +const V1_LAYER_TEXT_TEMPLATE: Omit[] = [ + { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello World", + fontSize: 48, + fontColor: "#000000", + x: 50, + y: 50, + fontFamily: "arial", + }, + version: "v1", + }, +] + +// Advanced adjustments +const V1_ADVANCED_TEMPLATE: Omit[] = [ + { + key: "adjust-contrast", + name: "Contrast", + type: "transformation", + value: { + contrast: true, + }, + version: "v1", + }, + { + key: "adjust-blur", + name: "Blur", + type: "transformation", + value: { + blur: 10, + }, + version: "v1", + }, + { + key: "adjust-radius", + name: "Corner Radius", + type: "transformation", + value: { + radius: { + radius: 20, + }, + }, + version: "v1", + }, +] + +// Comprehensive template with many transformations +const V1_COMPREHENSIVE_TEMPLATE: Omit[] = [ + { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 1200, + height: 800, + mode: "pad_resize", + }, + version: "v1", + }, + { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "color", + background: "#FAFAFA", + }, + version: "v1", + }, + { + key: "adjust-radius", + name: "Corner Radius", + type: "transformation", + value: { + radius: { + radius: 15, + }, + }, + version: "v1", + }, + { + key: "adjust-border", + name: "Border", + type: "transformation", + value: { + border: 5, + borderColor: "#333333", + }, + version: "v1", + }, + { + key: "delivery-quality", + name: "Quality", + type: "transformation", + value: { + quality: 85, + }, + version: "v1", + }, + { + key: "delivery-format", + name: "Format", + type: "transformation", + value: { + format: "webp", + }, + version: "v1", + }, +] + +// Template without version field (backward compatibility) +const V1_UNVERSIONED_TEMPLATE: Omit[] = [ + { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "color", + background: "#FFFFFF", + }, + }, + { + key: "adjust-rotate", + name: "Rotate", + type: "transformation", + value: { + rotate: 180, + }, + }, +] + +/** + * Helper to find a transformation schema by key + */ +function findTransformationSchema(key: string) { + for (const category of transformationSchema) { + const item = category.items.find((item) => item.key === key) + if (item) { + return item + } + } + return null +} + +/** + * Helper to check if a transformation key exists in the schema + */ +function isTransformationKeyValid(key: string): boolean { + return findTransformationSchema(key) !== null +} + +/** + * Validates that a transformation can be processed by the editor + * - Key must exist in current schema + * - Structure must be valid (name, type, value) + * - Value must pass Zod schema validation + * Returns validation result with details + */ +function validateTransformation(t: Omit): { + valid: boolean + errors: string[] +} { + const errors: string[] = [] + + // Check basic structure + if (!t.name) { + errors.push("Missing 'name' field") + } + if (t.type !== "transformation") { + errors.push(`Invalid type: expected 'transformation', got '${t.type}'`) + } + if (!t.value) { + errors.push("Missing 'value' field") + } + + // Check if key exists in schema + const schemaItem = findTransformationSchema(t.key) + if (!schemaItem) { + errors.push(`Transformation key '${t.key}' not found in current schema`) + return { valid: false, errors } + } + + // Validate value against Zod schema + try { + const result = schemaItem.schema.safeParse(t.value) + if (!result.success) { + result.error.errors.forEach((err) => { + errors.push( + `Schema validation failed for '${err.path.join(".")}': ${err.message}` + ) + }) + } + } catch (error) { + errors.push(`Schema validation error: ${error}`) + } + + return { + valid: errors.length === 0, + errors, + } +} + +describe("Backward Compatibility - V1 Templates", () => { + describe("Version Constant", () => { + it("should have v1 as current version", () => { + expect(TRANSFORMATION_STATE_VERSION).toBe("v1") + }) + }) + + describe("V1 Basic Template", () => { + it("should parse basic template as valid JSON", () => { + const json = JSON.stringify(V1_BASIC_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(1) + }) + + it("should have valid transformation keys", () => { + V1_BASIC_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should have version field set to v1", () => { + V1_BASIC_TEMPLATE.forEach((t) => { + expect(t.version).toBe("v1") + }) + }) + + it("should pass Zod schema validation", () => { + V1_BASIC_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Common Template", () => { + it("should parse template as valid JSON", () => { + const json = JSON.stringify(V1_COMMON_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(3) + }) + + it("should have all valid transformation keys", () => { + V1_COMMON_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_COMMON_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Gradient Template", () => { + it("should parse template as valid JSON", () => { + const json = JSON.stringify(V1_GRADIENT_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + }) + + it("should preserve complex gradient values", () => { + const json = JSON.stringify(V1_GRADIENT_TEMPLATE) + const parsed = JSON.parse(json) + const gradient = parsed[0].value.backgroundGradient + expect(gradient.from).toBe("#FFFFFFFF") + expect(gradient.to).toBe("#00000000") + expect(gradient.direction).toBe("bottom") + expect(gradient.stopPoint).toBe(100) + }) + + it("should pass Zod schema validation", () => { + V1_GRADIENT_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 AI Template", () => { + it("should parse AI transformations as valid JSON", () => { + const json = JSON.stringify(V1_AI_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(2) + }) + + it("should have valid AI transformation keys", () => { + V1_AI_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_AI_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Delivery Template", () => { + it("should parse delivery optimizations as valid JSON", () => { + const json = JSON.stringify(V1_DELIVERY_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(2) + }) + + it("should have valid delivery transformation keys", () => { + V1_DELIVERY_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_DELIVERY_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Layer Text Template", () => { + it("should parse text layer as valid JSON", () => { + const json = JSON.stringify(V1_LAYER_TEXT_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + }) + + it("should preserve text layer values", () => { + const json = JSON.stringify(V1_LAYER_TEXT_TEMPLATE) + const parsed = JSON.parse(json) + expect(parsed[0].value.text).toBe("Hello World") + expect(parsed[0].value.fontSize).toBe(48) + expect(parsed[0].value.fontColor).toBe("#000000") + }) + + it("should have valid text layer key", () => { + expect(isTransformationKeyValid(V1_LAYER_TEXT_TEMPLATE[0].key)).toBe(true) + }) + }) + + describe("V1 Advanced Template", () => { + it("should parse advanced adjustments as valid JSON", () => { + const json = JSON.stringify(V1_ADVANCED_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(3) + }) + + it("should have all valid transformation keys", () => { + V1_ADVANCED_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_ADVANCED_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Comprehensive Template", () => { + it("should parse template with many transforms", () => { + const json = JSON.stringify(V1_COMPREHENSIVE_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(6) + }) + + it("should have all valid transformation keys", () => { + V1_COMPREHENSIVE_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_COMPREHENSIVE_TEMPLATE.forEach((t) => { + const result = validateTransformation(t) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + }) + + describe("V1 Unversioned Template (Backward Compatibility)", () => { + it("should parse template as valid JSON", () => { + const json = JSON.stringify(V1_UNVERSIONED_TEMPLATE) + const parsed = JSON.parse(json) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBe(2) + }) + + it("should handle missing version field", () => { + V1_UNVERSIONED_TEMPLATE.forEach((t) => { + expect(t.version).toBeUndefined() + }) + }) + + it("should have valid transformation keys even without version", () => { + V1_UNVERSIONED_TEMPLATE.forEach((t) => { + expect(isTransformationKeyValid(t.key)).toBe(true) + }) + }) + + it("should pass Zod schema validation", () => { + V1_UNVERSIONED_TEMPLATE.forEach((t) => { + const result = validateTransformation(t as Omit) + if (!result.valid) { + console.error(`Validation errors for ${t.key}:`, result.errors) + } + expect(result.valid).toBe(true) + expect(result.errors).toEqual([]) + }) + }) + + it("should be able to add version to unversioned state", () => { + const withVersion = V1_UNVERSIONED_TEMPLATE.map((t) => ({ + ...t, + version: TRANSFORMATION_STATE_VERSION, + })) + + withVersion.forEach((t) => { + expect(t.version).toBe("v1") + }) + }) + }) + + describe("Template Serialization Consistency", () => { + it("should preserve all properties during JSON round-trip", () => { + const original = V1_COMPREHENSIVE_TEMPLATE + const json = JSON.stringify(original) + const parsed = JSON.parse(json) + + expect(parsed.length).toBe(original.length) + parsed.forEach((t: Omit, i: number) => { + expect(t.key).toBe(original[i].key) + expect(t.name).toBe(original[i].name) + expect(t.type).toBe(original[i].type) + expect(t.version).toBe(original[i].version) + expect(JSON.stringify(t.value)).toBe(JSON.stringify(original[i].value)) + }) + }) + + it("should handle removal and addition of id field", () => { + const withId: Transformation = { + id: "test-123", + ...V1_BASIC_TEMPLATE[0], + } + + // Remove id for storage + const { id, ...forStorage } = withId + expect(forStorage.id).toBeUndefined() + + // Add id back when loading + const loaded = { + ...forStorage, + id: "new-id-456", + } + expect(loaded.id).toBe("new-id-456") + }) + }) + + describe("Schema Key Validation", () => { + it("should validate all v1 fixture keys exist in current schema", () => { + const allFixtures = [ + ...V1_BASIC_TEMPLATE, + ...V1_COMMON_TEMPLATE, + ...V1_GRADIENT_TEMPLATE, + ...V1_AI_TEMPLATE, + ...V1_DELIVERY_TEMPLATE, + ...V1_LAYER_TEXT_TEMPLATE, + ...V1_ADVANCED_TEMPLATE, + ...V1_COMPREHENSIVE_TEMPLATE, + ] + + const uniqueKeys = new Set(allFixtures.map((t) => t.key)) + const missingKeys: string[] = [] + + uniqueKeys.forEach((key) => { + if (!isTransformationKeyValid(key)) { + missingKeys.push(key) + } + }) + + expect(missingKeys).toEqual([]) + }) + }) + + describe("Validation Actually Works (Negative Tests)", () => { + it("should reject transformation with invalid key", () => { + const invalid: Omit = { + key: "nonexistent-transformation", + name: "Invalid", + type: "transformation", + value: {}, + version: "v1", + } + + const result = validateTransformation(invalid) + expect(result.valid).toBe(false) + expect(result.errors.length).toBeGreaterThan(0) + expect(result.errors.some((e) => e.includes("not found in current schema"))).toBe(true) + }) + + it("should reject transformation with wrong type", () => { + const invalid: any = { + key: "adjust-background", + name: "Background", + type: "wrong-type", + value: { background: "#FFF" }, + version: "v1", + } + + const result = validateTransformation(invalid) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.includes("Invalid type"))).toBe(true) + }) + + it("should reject transformation with invalid value structure", () => { + const invalid: Omit = { + key: "adjust-radius", + name: "Corner Radius", + type: "transformation", + value: { + radius: 999, // Should be an object with {radius: number} + }, + version: "v1", + } + + const result = validateTransformation(invalid) + expect(result.valid).toBe(false) + expect(result.errors.length).toBeGreaterThan(0) + }) + + it("should reject transformation with missing required fields", () => { + const invalid: Omit = { + key: "adjust-rotate", + name: "Rotate", + type: "transformation", + value: {}, // Missing required 'rotate' field + version: "v1", + } + + const result = validateTransformation(invalid) + expect(result.valid).toBe(false) + expect(result.errors.length).toBeGreaterThan(0) + }) + + it("should reject transformation with invalid data types", () => { + const invalid: Omit = { + key: "adjust-rotate", + name: "Rotate", + type: "transformation", + value: { + rotate: "not-a-number", // Should be a number + }, + version: "v1", + } + + const result = validateTransformation(invalid) + expect(result.valid).toBe(false) + expect(result.errors.length).toBeGreaterThan(0) + }) + }) + + /** + * Deep Schema Validation Tests + * These tests exercise custom validators and complex schema logic to achieve high coverage + */ + describe("Schema Validators - Width & Height", () => { + it("should validate width as number", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, height: 600, mode: "c-force" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate width as decimal", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 0.5, height: 600, mode: "c-force" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate width as expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: "iw_div_2", height: 600, mode: "c-force" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate height as expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, height: "ih_mul_1.5", mode: "c-force" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid width expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: "invalid_expr", height: 600, mode: "c-force" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Schema Validators - Color", () => { + it("should validate 6-digit hex color", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { backgroundType: "color", background: "#FF5533" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate 3-digit hex color", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { backgroundType: "color", background: "#F53" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate 8-digit hex color with alpha", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { backgroundType: "color", background: "#FF5533AA" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate hex without # prefix", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { backgroundType: "color", background: "FF5533" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid hex color", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { backgroundType: "color", background: "#GGGGGG" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Schema Validators - Aspect Ratio", () => { + it("should validate aspect ratio value format", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "16-9" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate aspect ratio with decimals", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "16.5-9.5" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate aspect ratio expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "iar_mul_1.5" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid aspect ratio", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "16:9" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject aspect ratio without width or height", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { aspectRatio: "16-9" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Schema Validators - Layer Positioning", () => { + it("should validate layer X as number", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionX: "100", fontSize: 24, radius: 0 }, + version: "v1", + } + const result = validateTransformation(template) + if (!result.valid) { + console.log("Layer X validation errors:", result.errors) + } + expect(result.valid).toBe(true) + }) + + it("should validate layer X as negative number", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionX: "-50", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate layer X as expression", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionX: "bw_div_2", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate layer Y as expression", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionY: "bh_sub_100", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Resize & Crop Complex Validations", () => { + it("should require mode when both width and height are specified", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, height: 600 }, // Missing mode + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate cm-pad_resize mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-pad_resize", + backgroundType: "color", + background: "#FFFFFF", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require both dimensions for blurred background in pad_resize", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + mode: "cm-pad_resize", + backgroundType: "blurred", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require both dimensions for generative_fill background", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + height: 600, + mode: "cm-pad_resize", + backgroundType: "generative_fill", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate cm-extract mode with focus object", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "object", + focusObject: "person", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusObject when extract mode has object focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "object", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require focusAnchor when extract mode has anchor focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "anchor", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate extract mode with topleft coordinates", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + x: "100", + y: "100", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require coordinates when extract uses coordinates focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate extract mode with center coordinates", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + xc: "400", + yc: "300", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate DPR with width", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + dprEnabled: true, + dpr: 2, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate DPR as auto", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + dprEnabled: true, + dpr: "auto", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject DPR without width or height", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + dpr: 2, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate c-maintain_ratio with focus anchor", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-maintain_ratio", + focus: "anchor", + focusAnchor: "center", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusAnchor in maintain_ratio with anchor focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-maintain_ratio", + focus: "anchor", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate all resize modes", () => { + const modes = [ + "c-maintain_ratio", + "cm-pad_resize", + "cm-extract", + "cm-pad_extract", + "c-force", + "c-at_max", + "c-at_max_enlarge", + "c-at_least", + ] + + modes.forEach((mode) => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, height: 600, mode }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + }) + + describe("Unsharpen Mask Validation", () => { + it("should validate complete unsharpen mask", () => { + const template: Omit = { + key: "adjust-unsharpen-mask", + name: "Unsharpen Mask", + type: "transformation", + value: { + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1.5, + unsharpenMaskAmount: 1.2, + unsharpenMaskThreshold: 0.1, + }, + version: "v1", + } + const result = validateTransformation(template) + if (!result.valid) { + console.log("Unsharpen mask errors:", result.errors) + } + expect(result.valid).toBe(true) + }) + + it("should require all fields when unsharpen mask is enabled", () => { + const template: Omit = { + key: "adjust-unsharpen-mask", + name: "Unsharpen Mask", + type: "transformation", + value: { + unsharpenMaskRadius: 2, + // Missing other required fields (sigma, amount, threshold) + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Additional Transformations Coverage", () => { + it("should validate shadow transformation", () => { + const template: Omit = { + key: "adjust-shadow", + name: "Shadow", + type: "transformation", + value: { + shadow: 5, + shadowBlur: 10, + shadowOffsetX: 5, + shadowOffsetY: 5, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate distort transformation", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "10", + y1: "10", + x2: "100", + y2: "10", + x3: "100", + y3: "100", + x4: "10", + y4: "100", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate border transformation", () => { + const template: Omit = { + key: "adjust-border", + name: "Border", + type: "transformation", + value: { + borderWidth: 10, + borderColor: "#000000", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate trim transformation", () => { + const template: Omit = { + key: "adjust-trim", + name: "Trim", + type: "transformation", + value: { + trimEnabled: true, + trim: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate color replace transformation", () => { + const template: Omit = { + key: "adjust-color-replace", + name: "Color Replace", + type: "transformation", + value: { + fromColor: "FF0000", + toColor: "00FF00", + tolerance: 20, + }, + version: "v1", + } + const result = validateTransformation(template) + if (!result.valid) { + console.log("Color replace errors:", result.errors) + } + expect(result.valid).toBe(true) + }) + + it("should validate sharpen transformation", () => { + const template: Omit = { + key: "adjust-sharpen", + name: "Sharpen", + type: "transformation", + value: { + sharpenEnabled: true, + sharpen: 5, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate flip transformation", () => { + const template: Omit = { + key: "adjust-flip", + name: "Flip", + type: "transformation", + value: { + flip: "both", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate opacity transformation", () => { + const template: Omit = { + key: "adjust-opacity", + name: "Opacity", + type: "transformation", + value: { + opacity: 50, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate AI drop shadow", () => { + const template: Omit = { + key: "ai-dropshadow", + name: "Drop Shadow", + type: "transformation", + value: { + dropshadow: true, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate AI upscale", () => { + const template: Omit = { + key: "ai-upscale", + name: "Upscale", + type: "transformation", + value: { + upscale: true, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate AI edit transformation", () => { + const template: Omit = { + key: "ai-edit", + name: "Edit Image", + type: "transformation", + value: { + edit: "replace dog with cat", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate image layer", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "https://example.com/image.jpg", + width: 200, + height: 200, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate complex text layer with all properties", () => { + const template: Omit = { + key: "layers-text", + name: "Text Layer", + type: "transformation", + value: { + text: "Hello World", + fontSize: 48, + fontFamily: "Arial", + color: "000000", + backgroundColor: "FFFFFF", + positionX: "100", + positionY: "200", + width: 400, + innerAlignment: "center", + opacity: 8, + rotation: 45, + radius: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate gradient transformation", () => { + const template: Omit = { + key: "adjust-gradient", + name: "Gradient", + type: "transformation", + value: { + gradientSwitch: true, + gradient: { + from: "#FF0000", + to: "#0000FF", + direction: "bottom", + stopPoint: 50, + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate grayscale transformation", () => { + const template: Omit = { + key: "adjust-grayscale", + name: "Grayscale", + type: "transformation", + value: { + grayscale: true, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Transformation Formatters", () => { + it("should format background with color", () => { + const values = { backgroundType: "color", background: "#FF5533" } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("FF5533") + }) + + it("should format background with dominant color", () => { + const values = { + backgroundType: "color", + backgroundDominantAuto: true, + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("dominant") + }) + + it("should format gradient with auto dominant", () => { + const values = { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + backgroundGradientPaletteSize: "3", + backgroundGradientMode: "linear", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("gradient_linear_3") + }) + + it("should format blurred background with negative brightness", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "10", + backgroundBlurBrightness: "-50", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + // Should create blurred background with intensity and brightness + expect(transforms.background).toBe("blurred_10_N50") + }) + }) + + describe("Validator Edge Cases - Reaching 100% Coverage", () => { + it("should handle empty string in layerX validator", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionX: "", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should handle undefined in layerX validator", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid layerX expression", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionX: "invalid_expr", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should handle empty string in layerY validator", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionY: "", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid layerY expression", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", positionY: "badexpr", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate line height as integer", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", lineHeight: "24", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate line height as expression", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", lineHeight: "ih_mul_1.5", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should handle empty line height", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", lineHeight: "", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject invalid line height", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { text: "Hello", lineHeight: "not_valid", fontSize: 24, radius: 0 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should handle empty aspect ratio", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should handle undefined aspect ratio", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800 }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Distort Perspective Validation - Full Coverage", () => { + it("should reject invalid perspective coordinates (non-numeric)", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "abc", + y1: "10", + x2: "100", + y2: "10", + x3: "100", + y3: "100", + x4: "10", + y4: "100", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject incomplete perspective coordinates", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "10", + y1: "10", + x2: "", + y2: "", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject invalid perspective coordinate arrangement", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "100", + y1: "100", + x2: "10", + y2: "10", + x3: "10", + y3: "10", + x4: "100", + y4: "100", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate arc distortion with positive degree", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + distortArcDegree: "45", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate arc distortion with negative degree", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + distortArcDegree: "-45", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate arc distortion with N prefix for negative", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + distortArcDegree: "N45", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject arc with zero degree", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + distortArcDegree: "0", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject arc with missing degree", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject arc with non-numeric degree", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "arc", + distortArcDegree: "abc", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate perspective with N prefix coordinates", () => { + const template: Omit = { + key: "adjust-distort", + name: "Distort", + type: "transformation", + value: { + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "10", + y1: "10", + x2: "100", + y2: "10", + x3: "100", + y3: "100", + x4: "N5", + y4: "100", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Background Field Contexts and Formatters", () => { + it("should format background for generative fill without prompt", () => { + const values = { + backgroundType: "generative_fill", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("genfill") + }) + + it("should format background for generative fill with simple prompt", () => { + const values = { + backgroundType: "generative_fill", + backgroundGenerativeFill: "beach", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("genfill-prompt-beach") + }) + + it("should format background for blurred with auto intensity and brightness", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "auto", + backgroundBlurBrightness: "50", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("blurred_auto_50") + }) + + it("should format background for blurred with auto intensity only", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "auto", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("blurred_auto") + }) + + it("should format background for blurred with numeric intensity only", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "5", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("blurred_5") + }) + + it("should format background for blurred with numeric intensity and brightness", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "5", + backgroundBlurBrightness: "25", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("blurred_5_25") + }) + + it("should format background for blurred fallback", () => { + const values = { + backgroundType: "blurred", + backgroundBlurIntensity: "invalid", + } + const transforms: Record = {} + transformationFormatters.background(values, transforms) + expect(transforms.background).toBe("blurred") + }) + + it("should validate gradient with manual colors", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradient: { + from: "#FF0000", + to: "#0000FF", + direction: "top", + stopPoint: 75, + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate background with auto gradient different modes", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + backgroundGradientMode: "radial", + backgroundGradientPaletteSize: "4", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Additional Resize & Crop Edge Cases", () => { + it("should validate cm-pad_extract mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-pad_extract", + backgroundType: "color", + background: "#FFFFFF", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate c-at_least mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-at_least", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate width with various expression operators", () => { + const operators = ["add", "sub", "mul", "div", "mod", "pow"] + operators.forEach((op) => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: `iw_${op}_2` }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + it("should validate height with various base dimensions", () => { + const bases = ["ih", "bh", "ch"] + bases.forEach((base) => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { height: `${base}_mul_0.5` }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + it("should validate aspect ratio with car expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { width: 800, aspectRatio: "car_mul_1.2" }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should handle object focus in maintain_ratio mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-maintain_ratio", + focus: "object", + focusObject: "car", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusObject in maintain_ratio with object focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-maintain_ratio", + focus: "object", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate extract with center coordinates only xc", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + xc: "400", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate extract with topleft coordinates only x", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + x: "100", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Typography and Text Layer Advanced", () => { + it("should validate text with typography array", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello", + fontSize: 24, + typography: ["bold", "italic"], + radius: 0, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate text with flip array", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello", + fontSize: 24, + flip: ["horizontal"], + radius: 0, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate text with max radius", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello", + fontSize: 24, + radius: "max", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate text layer with all alignment options", () => { + const alignments: Array<"left" | "right" | "center"> = ["left", "right", "center"] + alignments.forEach((align) => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello", + fontSize: 24, + innerAlignment: align, + radius: 0, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + it("should validate text opacity boundary values", () => { + const template: Omit = { + key: "layers-text", + name: "Text", + type: "transformation", + value: { + text: "Hello", + fontSize: 24, + opacity: 1, + radius: 0, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Image Layer Complex Validations", () => { + it("should validate image layer with border using expression", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "bw_div_10", + borderColor: "FF0000", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject image layer border with invalid expression", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "invalid_expr", + borderColor: "FF0000", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with unsharpen mask enabled", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1, + unsharpenMaskAmount: 1.5, + unsharpenMaskThreshold: 0.05, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require all unsharpen mask fields when enabled for image layer", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + unsharpenMask: true, + unsharpenMaskRadius: 2, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with DPR", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + width: 200, + dprEnabled: true, + dpr: 2, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject image layer DPR without dimensions", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + dpr: 2, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with focus object", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "object", + focusObject: "person", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusObject when image layer has object focus", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "object", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with focus anchor", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "anchor", + focusAnchor: "center", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusAnchor when image layer has anchor focus", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "anchor", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with topleft coordinates", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + x: "50", + y: "50", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require at least one topleft coordinate for image layer", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with center coordinates", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + xc: "100", + yc: "100", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require at least one center coordinate for image layer", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + crop: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate image layer with distort perspective", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + distort: true, + distortType: "perspective", + distortPerspective: { + x1: "10", + y1: "10", + x2: "100", + y2: "10", + x3: "100", + y3: "100", + x4: "10", + y4: "100", + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate image layer with arc distortion", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + distort: true, + distortType: "arc", + distortArcDegree: "30", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate image layer with all overlay effects", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + width: 200, + height: 200, + blur: 5, + shadow: true, + shadowBlur: 10, + shadowOffsetX: 5, + shadowOffsetY: 5, + grayscale: true, + opacity: 80, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Common Number and Expression Validator Coverage", () => { + it("should validate positive number", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate ih expression", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "ih_div_20", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate bh expression", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "bh_mul_0.05", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate ch expression", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "ch_add_10", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject negative number for common validator", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: -5, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should reject invalid expression format", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + borderWidth: "invalid_format", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) +}) diff --git a/packages/imagekit-editor-dev/src/index.tsx b/packages/imagekit-editor-dev/src/index.tsx index 5d74dfd..18ce1c9 100644 --- a/packages/imagekit-editor-dev/src/index.tsx +++ b/packages/imagekit-editor-dev/src/index.tsx @@ -1,4 +1,5 @@ export type { ImageKitEditorProps, ImageKitEditorRef } from "./ImageKitEditor" export { ImageKitEditor } from "./ImageKitEditor" export { DEFAULT_FOCUS_OBJECTS } from "./schema" -export type { FileElement, Signer } from "./store" +export type { FileElement, Signer, Transformation } from "./store" +export { TRANSFORMATION_STATE_VERSION } from "./store" diff --git a/packages/imagekit-editor-dev/src/store.ts b/packages/imagekit-editor-dev/src/store.ts index b0cdd83..e2c1628 100644 --- a/packages/imagekit-editor-dev/src/store.ts +++ b/packages/imagekit-editor-dev/src/store.ts @@ -15,12 +15,15 @@ import { } from "./schema" import { extractImagePath } from "./utils" +export const TRANSFORMATION_STATE_VERSION = "v1" as const + export interface Transformation { id: string key: string name: string type: "transformation" value: IKTransformation + version?: typeof TRANSFORMATION_STATE_VERSION } export type RequiredMetadata = { requireSignedUrl: boolean } @@ -104,7 +107,7 @@ export type EditorActions< addImage: (imageSrc: string | InputFileElement) => void addImages: (imageSrcs: Array>) => void removeImage: (imageSrc: string) => void - setTransformations: (transformations: Omit[]) => void + loadTemplate: (template: Omit[]) => void moveTransformation: ( activeId: UniqueIdentifier, overId: UniqueIdentifier, @@ -302,12 +305,30 @@ const useEditorStore = create()( }) }, - setTransformations: (transformations) => { - const transformationsWithIds = transformations.map((transformation) => ({ + loadTemplate: (template) => { + const transformationsWithIds = template.map((transformation, index) => ({ ...transformation, - id: `transformation-${Date.now()}`, + id: `transformation-${Date.now()}-${index}`, + version: TRANSFORMATION_STATE_VERSION, + })) + + const visibleTransformations: Record = {} + transformationsWithIds.forEach((t) => { + visibleTransformations[t.id] = true + }) + + set((state) => ({ + transformations: transformationsWithIds, + visibleTransformations: { + ...state.visibleTransformations, + ...visibleTransformations, + }, + _internalState: { + sidebarState: "none", + selectedTransformationKey: null, + transformationToEdit: null, + }, })) - set({ transformations: transformationsWithIds }) }, moveTransformation: (activeId, overId) => { diff --git a/packages/imagekit-editor-dev/vite.config.ts b/packages/imagekit-editor-dev/vite.config.ts index 0a8cdda..15d7a68 100644 --- a/packages/imagekit-editor-dev/vite.config.ts +++ b/packages/imagekit-editor-dev/vite.config.ts @@ -16,6 +16,22 @@ export default defineConfig({ outDir: "../imagekit-editor/dist/types", }), ], + test: { + globals: true, + environment: "jsdom", + setupFiles: [], + include: ["src/**/*.{test,spec}.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "src/**/*.{test,spec}.{ts,tsx}", + "src/index.tsx", + "node_modules/**", + ], + }, + }, build: { lib: { entry: path.resolve(__dirname, "src/index.tsx"), diff --git a/packages/imagekit-editor/package.json b/packages/imagekit-editor/package.json index 230658f..84f66d6 100644 --- a/packages/imagekit-editor/package.json +++ b/packages/imagekit-editor/package.json @@ -1,6 +1,6 @@ { "name": "@imagekit/editor", - "version": "2.1.0", + "version": "2.2.0", "description": "Image Editor powered by ImageKit", "main": "dist/index.cjs.js", "module": "dist/index.es.js", @@ -18,6 +18,9 @@ "url": "https://github.com/imagekit-developer/imagekit-editor/issues" }, "homepage": "https://imagekit.io", + "scripts": { + "test": "echo \"No tests in this package\"" + }, "peerDependencies": { "@chakra-ui/icons": "1.1.1", "@chakra-ui/react": "~1.8.9", diff --git a/turbo.json b/turbo.json index d2abe72..2895ca5 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,10 @@ "cache": false, "persistent": true, "dependsOn": [] + }, + "test": { + "cache": false, + "outputs": [] } } } diff --git a/yarn.lock b/yarn.lock index 5b64324..ee5faf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,14 @@ __metadata: version: 8 cacheKey: 10c0 -"@ampproject/remapping@npm:^2.2.0": +"@acemir/cssom@npm:^0.9.31": + version: 0.9.31 + resolution: "@acemir/cssom@npm:0.9.31" + checksum: 10c0/cbfff98812642104ec3b37de1ad3a53f216ddc437e7b9276a23f46f2453844ea3c3f46c200bc4656a2f747fb26567560b3cc5183d549d119a758926551b5f566 + languageName: node + linkType: hard + +"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -15,6 +22,39 @@ __metadata: languageName: node linkType: hard +"@asamuzakjp/css-color@npm:^5.0.1": + version: 5.0.1 + resolution: "@asamuzakjp/css-color@npm:5.0.1" + dependencies: + "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + lru-cache: "npm:^11.2.6" + checksum: 10c0/3e8d74a3b7f3005a325cb8e7f3da1aa32aeac4cd9ce387826dc25b16eaab4dc0e4a6faded8ccc1895959141f4a4a70e8bc38723347b89667b7b224990d16683c + languageName: node + linkType: hard + +"@asamuzakjp/dom-selector@npm:^6.8.1": + version: 6.8.1 + resolution: "@asamuzakjp/dom-selector@npm:6.8.1" + dependencies: + "@asamuzakjp/nwsapi": "npm:^2.3.9" + bidi-js: "npm:^1.0.3" + css-tree: "npm:^3.1.0" + is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.2.6" + checksum: 10c0/635de2c3b11971c07e2d491fd2833d2499bafbab05b616f5d38041031718879c404456644f60c45e9ba4ca2423e5bb48bf3c46179b0c58a0ea68eaae8c61e85f + languageName: node + linkType: hard + +"@asamuzakjp/nwsapi@npm:^2.3.9": + version: 2.3.9 + resolution: "@asamuzakjp/nwsapi@npm:2.3.9" + checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -140,6 +180,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -168,6 +215,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/333b2aa761264b91577a74bee86141ef733f9f9f6d4fc52548e4847dc35dfbf821f58c46832c637bfa761a6d9909d6a68f7d1ed59e17e4ffbb958dc510c17b62 + languageName: node + linkType: hard + "@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.4, @babel/parser@npm:^7.27.5": version: 7.27.5 resolution: "@babel/parser@npm:7.27.5" @@ -252,6 +310,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10c0/23cc3466e83bcbfab8b9bd0edaafdb5d4efdb88b82b3be6728bbade5ba2f0996f84f63b1c5f7a8c0d67efded28300898a5f930b171bb40b311bca2029c4e9b4f + languageName: node + linkType: hard + "@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6": version: 7.27.6 resolution: "@babel/types@npm:7.27.6" @@ -262,6 +330,20 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: 10c0/1eb1dc93cc17fb7abdcef21a6e7b867d6aa99a7ec88ec8207402b23d9083ab22a8011213f04b2cf26d535f1d22dc26139b7929e6c2134c254bd1e14ba5e678c3 + languageName: node + linkType: hard + "@biomejs/biome@npm:2.1.1": version: 2.1.1 resolution: "@biomejs/biome@npm:2.1.1" @@ -353,6 +435,17 @@ __metadata: languageName: node linkType: hard +"@bramus/specificity@npm:^2.4.2": + version: 2.4.2 + resolution: "@bramus/specificity@npm:2.4.2" + dependencies: + css-tree: "npm:^3.0.0" + bin: + specificity: bin/cli.js + checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72 + languageName: node + linkType: hard + "@chakra-ui/accordion@npm:1.4.12": version: 1.4.12 resolution: "@chakra-ui/accordion@npm:1.4.12" @@ -1221,6 +1314,59 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^6.0.2": + version: 6.0.2 + resolution: "@csstools/color-helpers@npm:6.0.2" + checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789 + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^3.1.1": + version: 3.1.1 + resolution: "@csstools/css-calc@npm:3.1.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/6efcc016d988edf66e54c7bad03e352d61752cbd1b56c7557fd013868aab23505052ded8f912cd4034e216943ea1e04c957d81012489e3eddc14a57b386510ef + languageName: node + linkType: hard + +"@csstools/css-color-parser@npm:^4.0.2": + version: 4.0.2 + resolution: "@csstools/css-color-parser@npm:4.0.2" + dependencies: + "@csstools/color-helpers": "npm:^6.0.2" + "@csstools/css-calc": "npm:^3.1.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/487cf507ef4630f74bd67d84298294ed269900b206ade015a968d20047e07ff46f235b72e26fe0c6b949a03f8f9f00a22c363da49c1b06ca60b32d0188e546be + languageName: node + linkType: hard + +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d + languageName: node + linkType: hard + +"@csstools/css-syntax-patches-for-csstree@npm:^1.0.28": + version: 1.1.0 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.0" + checksum: 10c0/ef84e09ead31d204e238eb674016b34a54083344348b4e4fd63cb03486dcaa5b53feeff74a6c246763973cca0eb3213a70f49ca8545ce26a3b3d9c97255f4dd1 + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f + languageName: node + linkType: hard + "@ctrl/tinycolor@npm:^3.4.0": version: 3.6.1 resolution: "@ctrl/tinycolor@npm:3.6.1" @@ -1439,6 +1585,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/aix-ppc64@npm:0.25.5" @@ -1446,6 +1599,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm64@npm:0.25.5" @@ -1453,6 +1613,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm@npm:0.25.5" @@ -1460,6 +1627,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-x64@npm:0.25.5" @@ -1467,6 +1641,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-arm64@npm:0.25.5" @@ -1474,6 +1655,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-x64@npm:0.25.5" @@ -1481,6 +1669,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-arm64@npm:0.25.5" @@ -1488,6 +1683,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-x64@npm:0.25.5" @@ -1495,6 +1697,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm64@npm:0.25.5" @@ -1502,6 +1711,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm@npm:0.25.5" @@ -1509,6 +1725,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ia32@npm:0.25.5" @@ -1516,6 +1739,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-loong64@npm:0.25.5" @@ -1523,6 +1753,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-mips64el@npm:0.25.5" @@ -1530,6 +1767,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ppc64@npm:0.25.5" @@ -1537,6 +1781,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-riscv64@npm:0.25.5" @@ -1544,6 +1795,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-s390x@npm:0.25.5" @@ -1551,6 +1809,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-x64@npm:0.25.5" @@ -1565,6 +1830,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-x64@npm:0.25.5" @@ -1579,6 +1851,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-x64@npm:0.25.5" @@ -1586,6 +1865,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/sunos-x64@npm:0.25.5" @@ -1593,6 +1879,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-arm64@npm:0.25.5" @@ -1600,6 +1893,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-ia32@npm:0.25.5" @@ -1607,6 +1907,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-x64@npm:0.25.5" @@ -1614,6 +1921,18 @@ __metadata: languageName: node linkType: hard +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.6.0": + version: 1.15.0 + resolution: "@exodus/bytes@npm:1.15.0" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/b48aad9729653385d6ed055c28cfcf0b1b1481cf5d83f4375c12abd7988f1d20f69c80b5f95d4a1cc24d9abe32b9efc352a812d53884c26efea172aca8b6356d + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.7.3": version: 1.7.3 resolution: "@floating-ui/core@npm:1.7.3" @@ -1651,7 +1970,7 @@ __metadata: languageName: node linkType: hard -"@imagekit/editor@npm:2.1.0, @imagekit/editor@workspace:packages/imagekit-editor": +"@imagekit/editor@workspace:*, @imagekit/editor@workspace:packages/imagekit-editor": version: 0.0.0-use.local resolution: "@imagekit/editor@workspace:packages/imagekit-editor" peerDependencies: @@ -1696,6 +2015,13 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -1738,6 +2064,23 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.31": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -1921,6 +2264,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" @@ -2009,6 +2359,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-android-arm64@npm:4.44.0" @@ -2016,6 +2373,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.44.0" @@ -2023,6 +2387,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-darwin-x64@npm:4.44.0" @@ -2030,6 +2401,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-freebsd-arm64@npm:4.44.0" @@ -2037,6 +2415,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-freebsd-x64@npm:4.44.0" @@ -2044,6 +2429,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0" @@ -2051,6 +2443,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.44.0" @@ -2058,6 +2457,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.44.0" @@ -2065,6 +2471,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.44.0" @@ -2072,6 +2485,27 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0" @@ -2086,6 +2520,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.44.0" @@ -2093,6 +2541,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.44.0" @@ -2100,6 +2555,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.44.0" @@ -2107,6 +2569,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.44.0" @@ -2114,6 +2583,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.44.0" @@ -2121,10 +2597,38 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.44.0": - version: 4.44.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.44.0" - conditions: os=win32 & cpu=arm64 +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.44.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -2135,6 +2639,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.44.0" @@ -2142,6 +2660,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:3.59.0": version: 3.59.0 resolution: "@rushstack/node-core-library@npm:3.59.0" @@ -2246,6 +2771,18 @@ __metadata: languageName: node linkType: hard +"@types/jsdom@npm:^28": + version: 28.0.0 + resolution: "@types/jsdom@npm:28.0.0" + dependencies: + "@types/node": "npm:*" + "@types/tough-cookie": "npm:*" + parse5: "npm:^7.0.0" + undici-types: "npm:^7.21.0" + checksum: 10c0/7b4b06dee1c611e37766ae2c5f92b0a881e3a2da8e38cc34999e812ab030b54b7250b0b9cc9af24dbeadc0fc2d341cc4e0adc5e5ca7d624d134ced1414a1ea5e + languageName: node + linkType: hard + "@types/lodash.mergewith@npm:4.6.6": version: 4.6.6 resolution: "@types/lodash.mergewith@npm:4.6.6" @@ -2262,6 +2799,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*": + version: 25.4.0 + resolution: "@types/node@npm:25.4.0" + dependencies: + undici-types: "npm:~7.18.0" + checksum: 10c0/da81e8b0a3a57964b1b5f85d134bfefc1b923fd67ed41756842348a049d7915b72e8773f5598d6929b9cb8119c2427993c55d364fd93bd572a3450e58b98a60e + languageName: node + linkType: hard + "@types/node@npm:^20.11.24": version: 20.19.1 resolution: "@types/node@npm:20.19.1" @@ -2341,6 +2887,13 @@ __metadata: languageName: node linkType: hard +"@types/tough-cookie@npm:*": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473 + languageName: node + linkType: hard + "@types/warning@npm:^3.0.0": version: 3.0.3 resolution: "@types/warning@npm:3.0.3" @@ -2364,6 +2917,173 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^2.1.9": + version: 2.1.9 + resolution: "@vitest/coverage-v8@npm:2.1.9" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.7" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.12" + magicast: "npm:^0.3.5" + std-env: "npm:^3.8.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + "@vitest/browser": 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/ccf5871954a630453af9393e84ff40a0f8a4515e988ea32c7ebac5db7c79f17535a12c1c2567cbb78ea01a1eb99abdde94e297f6b6ccd5f7f7fc9b8b01c5963c + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:^4.0.18": + version: 4.0.18 + resolution: "@vitest/coverage-v8@npm:4.0.18" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.0.18" + ast-v8-to-istanbul: "npm:^0.3.10" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.1" + obug: "npm:^2.1.1" + std-env: "npm:^3.10.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + "@vitest/browser": 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/e23e0da86f0b2a020c51562bc40ebdc7fc7553c24f8071dfb39a6df0161badbd5eaf2eebbf8ceaef18933a18c1934ff52d1c0c4bde77bb87e0c1feb0c8cbee4d + languageName: node + linkType: hard + +"@vitest/expect@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/expect@npm:2.1.9" + dependencies: + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/98d1cf02917316bebef9e4720723e38298a1c12b3c8f3a81f259bb822de4288edf594e69ff64f0b88afbda6d04d7a4f0c2f720f3fec16b4c45f5e2669f09fdbb + languageName: node + linkType: hard + +"@vitest/mocker@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/mocker@npm:2.1.9" + dependencies: + "@vitest/spy": "npm:2.1.9" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.12" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f734490d8d1206a7f44dfdfca459282f5921d73efa72935bb1dc45307578defd38a4131b14853316373ec364cbe910dbc74594ed4137e0da35aa4d9bb716f190 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.1.9, @vitest/pretty-format@npm:^2.1.9": + version: 2.1.9 + resolution: "@vitest/pretty-format@npm:2.1.9" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/155f9ede5090eabed2a73361094bb35ed4ec6769ae3546d2a2af139166569aec41bb80e031c25ff2da22b71dd4ed51e5468e66a05e6aeda5f14b32e30bc18f00 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/pretty-format@npm:4.0.18" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/0086b8c88eeca896d8e4b98fcdef452c8041a1b63eb9e85d3e0bcc96c8aa76d8e9e0b6990ebb0bb0a697c4ebab347e7735888b24f507dbff2742ddce7723fd94 + languageName: node + linkType: hard + +"@vitest/runner@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/runner@npm:2.1.9" + dependencies: + "@vitest/utils": "npm:2.1.9" + pathe: "npm:^1.1.2" + checksum: 10c0/e81f176badb12a815cbbd9bd97e19f7437a0b64e8934d680024b0f768d8670d59cad698ef0e3dada5241b6731d77a7bb3cd2c7cb29f751fd4dd35eb11c42963a + languageName: node + linkType: hard + +"@vitest/snapshot@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/snapshot@npm:2.1.9" + dependencies: + "@vitest/pretty-format": "npm:2.1.9" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + checksum: 10c0/394974b3a1fe96186a3c87f933b2f7f1f7b7cc42f9c781d80271dbb4c987809bf035fecd7398b8a3a2d54169e3ecb49655e38a0131d0e7fea5ce88960613b526 + languageName: node + linkType: hard + +"@vitest/spy@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/spy@npm:2.1.9" + dependencies: + tinyspy: "npm:^3.0.2" + checksum: 10c0/12a59b5095e20188b819a1d797e0a513d991b4e6a57db679927c43b362a3eff52d823b34e855a6dd9e73c9fa138dcc5ef52210841a93db5cbf047957a60ca83c + languageName: node + linkType: hard + +"@vitest/ui@npm:^2.0.0": + version: 2.1.9 + resolution: "@vitest/ui@npm:2.1.9" + dependencies: + "@vitest/utils": "npm:2.1.9" + fflate: "npm:^0.8.2" + flatted: "npm:^3.3.1" + pathe: "npm:^1.1.2" + sirv: "npm:^3.0.0" + tinyglobby: "npm:^0.2.10" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + vitest: 2.1.9 + checksum: 10c0/b091f5afd5e7327d1dfc37e26af16d58066bd6c37ec0a1547796f1843eff3170c59062243475fb250ca36d8d7c7293ab78b36b2d112d7839ba8331625ab9b1d3 + languageName: node + linkType: hard + +"@vitest/utils@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/utils@npm:2.1.9" + dependencies: + "@vitest/pretty-format": "npm:2.1.9" + loupe: "npm:^3.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/81a346cd72b47941f55411f5df4cc230e5f740d1e97e0d3f771b27f007266fc1f28d0438582f6409ea571bc0030ed37f684c64c58d1947d6298d770c21026fdf + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/utils@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/4a3c43c1421eb90f38576926496f6c80056167ba111e63f77cf118983902673737a1a38880b890d7c06ec0a12475024587344ee502b3c43093781533022f2aeb + languageName: node + linkType: hard + "@volar/language-core@npm:2.4.17": version: 2.4.17 resolution: "@volar/language-core@npm:2.4.17" @@ -2483,6 +3203,24 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + +"ast-v8-to-istanbul@npm:^0.3.10": + version: 0.3.12 + resolution: "ast-v8-to-istanbul@npm:0.3.12" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10c0/bad6ba222b1073c165c8d65dbf366193d4a90536dabe37f93a3df162269b1c9473975756e4c048f708c235efccc26f8e5321c547b7e9563b64b21b2e0f27cbc9 + languageName: node + linkType: hard + "babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" @@ -2501,6 +3239,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + "base64-arraybuffer@npm:^1.0.2": version: 1.0.2 resolution: "base64-arraybuffer@npm:1.0.2" @@ -2508,6 +3253,15 @@ __metadata: languageName: node linkType: hard +"bidi-js@npm:^1.0.3": + version: 1.0.3 + resolution: "bidi-js@npm:1.0.3" + dependencies: + require-from-string: "npm:^2.0.2" + checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 + languageName: node + linkType: hard + "brace-expansion@npm:^2.0.1": version: 2.0.1 resolution: "brace-expansion@npm:2.0.1" @@ -2517,6 +3271,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a + languageName: node + linkType: hard + "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -2547,6 +3310,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 + languageName: node + linkType: hard + "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -2581,6 +3351,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.2": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/b360fd4d38861622e5010c2f709736988b05c7f31042305fa3f4e9911f6adb80ccfb4e302068bf8ed10e835c2e2520cba0f5edc13d878b886987e5aa62483f53 + languageName: node + linkType: hard + "chalk@npm:^5.4.1": version: 5.4.1 resolution: "chalk@npm:5.4.1" @@ -2588,6 +3371,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.3 + resolution: "check-error@npm:2.1.3" + checksum: 10c0/878e99038fb6476316b74668cd6a498c7e66df3efe48158fa40db80a06ba4258742ac3ee2229c4a2a98c5e73f5dff84eb3e50ceb6b65bbd8f831eafc8338607d + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -2782,6 +3572,28 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.0, css-tree@npm:^3.1.0": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + +"cssstyle@npm:^6.0.1": + version: 6.2.0 + resolution: "cssstyle@npm:6.2.0" + dependencies: + "@asamuzakjp/css-color": "npm:^5.0.1" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.28" + css-tree: "npm:^3.1.0" + lru-cache: "npm:^11.2.6" + checksum: 10c0/d5e61973a8c1b4fb9727edddfb9f2677c9a91b1db63787fc0c8bed639a227a97fcf930e5aabc3c64c8280d63169c632015a39da4a84083d1731c949437d0a2a2 + languageName: node + linkType: hard + "csstype@npm:3.0.9": version: 3.0.9 resolution: "csstype@npm:3.0.9" @@ -2796,6 +3608,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" + dependencies: + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.4": version: 4.3.7 resolution: "debug@npm:4.3.7" @@ -2808,6 +3630,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.1.1, debug@npm:^4.3.7": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "debug@npm:^4.4.0, debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" @@ -2820,6 +3654,20 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "define-lazy-prop@npm:^2.0.0": version: 2.0.0 resolution: "define-lazy-prop@npm:2.0.0" @@ -2897,6 +3745,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^6.0.0": + version: 6.0.1 + resolution: "entities@npm:6.0.1" + checksum: 10c0/ed836ddac5acb34341094eb495185d527bd70e8632b6c0d59548cbfa23defdbae70b96f9a405c82904efa421230b5b3fd2283752447d737beffd3f3e6ee74414 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -2927,6 +3782,93 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.4": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b + languageName: node + linkType: hard + +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de + languageName: node + linkType: hard + "esbuild@npm:^0.25.0": version: 0.25.5 resolution: "esbuild@npm:0.25.5" @@ -3034,6 +3976,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "eventemitter3@npm:^5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" @@ -3056,6 +4007,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.2 resolution: "exponential-backoff@npm:3.1.2" @@ -3118,6 +4076,25 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10c0/03448d630c0a583abea594835a9fdb2aaf7d67787055a761515bf4ed862913cfd693b4c4ffd5c3f3b355a70cf1e19033e9ae5aedcca103188aaff91b8bd6e293 + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -3134,6 +4111,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.3.1": + version: 3.4.1 + resolution: "flatted@npm:3.4.1" + checksum: 10c0/3987a7f1e39bc7215cece001354313b462cdb4fb2dde0df4f7acd9e5016fbae56ee6fb3f0870b2150145033be8bda4f01af6f87a00946049651131bbfca7dfa6 + languageName: node + linkType: hard + "focus-lock@npm:^0.9.1": version: 0.9.2 resolution: "focus-lock@npm:0.9.2" @@ -3300,6 +4284,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.4.1": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828 + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -3314,6 +4314,13 @@ __metadata: languageName: node linkType: hard +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + "hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -3339,6 +4346,22 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^6.0.0": + version: 6.0.0 + resolution: "html-encoding-sniffer@npm:6.0.0" + dependencies: + "@exodus/bytes": "npm:^1.6.0" + checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "html2canvas@npm:^1.4.1": version: 1.4.1 resolution: "html2canvas@npm:1.4.1" @@ -3356,7 +4379,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -3366,7 +4389,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -3416,6 +4439,8 @@ __metadata: "@types/react-color": "npm:^2" "@types/react-dom": "npm:^17.0.2" "@vitejs/plugin-react": "npm:^4.5.2" + "@vitest/coverage-v8": "npm:^2.1.9" + "@vitest/ui": "npm:^2.0.0" framer-motion: "npm:6.5.1" imagekit-javascript: "npm:^3.0.2" lodash: "npm:^4.17.21" @@ -3429,6 +4454,7 @@ __metadata: typescript: "npm:4.9.3" vite: "npm:^6.3.5" vite-plugin-dts: "npm:5.0.0-beta.3" + vitest: "npm:^2.0.0" zod: "npm:^3.25.0" zustand: "npm:4.5.7" peerDependencies: @@ -3442,7 +4468,10 @@ __metadata: resolution: "imagekit-editor@workspace:." dependencies: "@biomejs/biome": "npm:2.1.1" + "@types/jsdom": "npm:^28" + "@vitest/coverage-v8": "npm:^4.0.18" husky: "npm:^9.1.7" + jsdom: "npm:^28.1.0" lint-staged: "npm:^16.1.2" react-select: "npm:^5.2.1" shx: "npm:^0.4.0" @@ -3569,6 +4598,13 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + "is-stream@npm:^1.1.0": version: 1.1.0 resolution: "is-stream@npm:1.1.0" @@ -3599,6 +4635,45 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.7, istanbul-reports@npm:^3.2.0": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/d596317cfd9c22e1394f22a8d8ba0303d2074fe2e971887b32d870e4b33f8464b10f8ccbe6847808f7db485f084eba09e6c2ed706b3a978e4b52f07085b8f9bc + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -3619,6 +4694,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10c0/a93498747812ba3e0c8626f95f75ab29319f2a13613a0de9e610700405760931624433a0de59eb7c27ff8836e526768fb20783861b86ef89be96676f2c996b64 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -3633,6 +4715,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^28.1.0": + version: 28.1.0 + resolution: "jsdom@npm:28.1.0" + dependencies: + "@acemir/cssom": "npm:^0.9.31" + "@asamuzakjp/dom-selector": "npm:^6.8.1" + "@bramus/specificity": "npm:^2.4.2" + "@exodus/bytes": "npm:^1.11.0" + cssstyle: "npm:^6.0.1" + data-urls: "npm:^7.0.0" + decimal.js: "npm:^10.6.0" + html-encoding-sniffer: "npm:^6.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.6" + is-potential-custom-element-name: "npm:^1.0.1" + parse5: "npm:^8.0.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^6.0.0" + undici: "npm:^7.21.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/341ecb4005be2dab3247dacc349a20285d7991b5cee3382301fcd69a4294b705b4147e7d9ae1ddfab466ba4b3aace97ded4f7b070de285262221cb2782965b25 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -3802,6 +4918,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.2": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -3809,6 +4932,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.2.6": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -3827,6 +4957,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + "magic-string@npm:^0.30.17": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -3836,6 +4975,37 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + source-map-js: "npm:^1.2.0" + checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64 + languageName: node + linkType: hard + +"magicast@npm:^0.5.1": + version: 0.5.2 + resolution: "magicast@npm:0.5.2" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -3855,6 +5025,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "memoize-one@npm:^6.0.0": version: 6.0.0 resolution: "memoize-one@npm:6.0.0" @@ -3886,6 +5063,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 + languageName: node + linkType: hard + "minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -3999,6 +5185,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -4090,6 +5283,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.1.1": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 + languageName: node + linkType: hard + "once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -4161,6 +5361,24 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.0.0": + version: 7.3.0 + resolution: "parse5@npm:7.3.0" + dependencies: + entities: "npm:^6.0.0" + checksum: 10c0/7fd2e4e247e85241d6f2a464d0085eed599a26d7b0a5233790c49f53473232eb85350e8133344d9b3fd58b89339e7ad7270fe1f89d28abe50674ec97b87f80b5 + languageName: node + linkType: hard + +"parse5@npm:^8.0.0": + version: 8.0.0 + resolution: "parse5@npm:8.0.0" + dependencies: + entities: "npm:^6.0.0" + checksum: 10c0/8279892dcd77b2f2229707f60eb039e303adf0288812b2a8fd5acf506a4d432da833c6c5d07a6554bef722c2367a81ef4a1f7e9336564379a7dba3e798bf16b3 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -4206,6 +5424,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 + languageName: node + linkType: hard + "pathe@npm:^2.0.1, pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" @@ -4213,6 +5438,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.1 + resolution: "pathval@npm:2.0.1" + checksum: 10c0/460f4709479fbf2c45903a65655fc8f0a5f6d808f989173aeef5fdea4ff4f303dc13f7870303999add60ec49d4c14733895c0a869392e9866f1091fa64fd7581 + languageName: node + linkType: hard + "picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -4234,6 +5466,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 + languageName: node + linkType: hard + "pidtree@npm:^0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" @@ -4277,6 +5516,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.43": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c + languageName: node + linkType: hard + "postcss@npm:^8.5.3": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -4326,7 +5576,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": +"punycode@npm:^2.1.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -4393,7 +5643,7 @@ __metadata: version: 0.0.0-use.local resolution: "react-example@workspace:examples/react-example" dependencies: - "@imagekit/editor": "npm:2.1.0" + "@imagekit/editor": "workspace:*" "@types/node": "npm:^20.11.24" "@types/react": "npm:^17.0.2" "@types/react-dom": "npm:^17.0.2" @@ -4563,6 +5813,13 @@ __metadata: languageName: node linkType: hard +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -4695,6 +5952,96 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.59.0 + resolution: "rollup@npm:4.59.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/f38742da34cfee5e899302615fa157aa77cb6a2a1495e5e3ce4cc9c540d3262e235bbe60caa31562bbfe492b01fdb3e7a8c43c39d842d3293bcf843123b766fc + languageName: node + linkType: hard + "rollup@npm:^4.34.9": version: 4.44.0 resolution: "rollup@npm:4.44.0" @@ -4786,6 +6133,15 @@ __metadata: languageName: node linkType: hard +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + "scheduler@npm:^0.20.2": version: 0.20.2 resolution: "scheduler@npm:0.20.2" @@ -4823,6 +6179,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + languageName: node + linkType: hard + "semver@npm:~7.3.0": version: 7.3.8 resolution: "semver@npm:7.3.8" @@ -4892,6 +6257,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -4906,6 +6278,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.0": + version: 3.0.2 + resolution: "sirv@npm:3.0.2" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/5930e4397afdb14fbae13751c3be983af4bda5c9aadec832607dc2af15a7162f7d518c71b30e83ae3644b9a24cea041543cc969e5fe2b80af6ce8ea3174b2d04 + languageName: node + linkType: hard + "slice-ansi@npm:^5.0.0": version: 5.0.0 resolution: "slice-ansi@npm:5.0.0" @@ -4954,7 +6337,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -5015,6 +6398,20 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"std-env@npm:^3.10.0, std-env@npm:^3.8.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f + languageName: node + linkType: hard + "string-argv@npm:^0.3.2, string-argv@npm:~0.3.1": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -5104,6 +6501,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -5111,6 +6517,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.4.3 resolution: "tar@npm:7.4.3" @@ -5139,6 +6552,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.2 + resolution: "test-exclude@npm:7.0.2" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^10.2.2" + checksum: 10c0/b79b855af9168c6a362146015ccf40f5e3a25e307304ba9bea930818507f6319d230380d5d7b5baa659c981ccc11f1bd21b6f012f85606353dec07e02dee67c9 + languageName: node + linkType: hard + "text-segmentation@npm:^1.0.3": version: 1.0.3 resolution: "text-segmentation@npm:1.0.3" @@ -5155,6 +6579,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + "tinycolor2@npm:1.4.2": version: 1.4.2 resolution: "tinycolor2@npm:1.4.2" @@ -5162,6 +6593,23 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^0.3.1": + version: 0.3.2 + resolution: "tinyexec@npm:0.3.2" + checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.10": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -5172,6 +6620,52 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.1": + version: 1.1.1 + resolution: "tinypool@npm:1.1.1" + checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b + languageName: node + linkType: hard + +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 10c0/7f78a4b997e5ba0f5ecb75e7ed786f30bab9063716e7dff24dd84013fb338802e43d176cb21ed12480561f5649a82184cf31efb296601a29d38145b1cdb4c192 + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.2": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10c0/55ffad24e346622b59292e097c2ee30a63919d5acb7ceca87fc0d1c223090089890587b426e20054733f97a58f20af2c349fb7cc193697203868ab7ba00bcea0 + languageName: node + linkType: hard + +"tldts-core@npm:^7.0.25": + version: 7.0.25 + resolution: "tldts-core@npm:7.0.25" + checksum: 10c0/fd07a555a27a6f11ed87365845d80bf7755d490cd52adf6cd474cbb9148fdaa97c79f58a8ebbc19ccfec0578e76ced5f5ae3b88e85338529291918f5fe815914 + languageName: node + linkType: hard + +"tldts@npm:^7.0.5": + version: 7.0.25 + resolution: "tldts@npm:7.0.25" + dependencies: + tldts-core: "npm:^7.0.25" + bin: + tldts: bin/cli.js + checksum: 10c0/8affd92849a4b0e290c5b211dce58ddee79d5e6d7079fc592afb97d95decd90028194b844c0b3cfb9254de02fb73e01f89500f3e47073dd96a3266223ca221b9 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -5195,6 +6689,31 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + +"tough-cookie@npm:^6.0.0": + version: 6.0.0 + resolution: "tough-cookie@npm:6.0.0" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10c0/7b17a461e9c2ac0d0bea13ab57b93b4346d0b8c00db174c963af1e46e4ea8d04148d2a55f2358fc857db0c0c65208a98e319d0c60693e32e0c559a9d9cf20cb5 + languageName: node + linkType: hard + +"tr46@npm:^6.0.0": + version: 6.0.0 + resolution: "tr46@npm:6.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e + languageName: node + linkType: hard + "tslib@npm:^1.0.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -5327,6 +6846,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:^7.21.0": + version: 7.22.0 + resolution: "undici-types@npm:7.22.0" + checksum: 10c0/5e6f2513c41d07404c719eb7c1c499b8d4cde042f1269b3bc2be335f059e6ef53eb04f316696c728d5e8064c4d522b98f63d7ae8938adf6317351e07ae9c943e + languageName: node + linkType: hard + "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -5334,6 +6860,20 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d + languageName: node + linkType: hard + +"undici@npm:^7.21.0": + version: 7.22.0 + resolution: "undici@npm:7.22.0" + checksum: 10c0/09777c06f3f18f761f03e3a4c9c04fd9fcca8ad02ccea43602ee4adf73fcba082806f1afb637f6ea714ef6279c5323c25b16d435814c63db720f63bfc20d316b + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -5504,6 +7044,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.9": + version: 2.1.9 + resolution: "vite-node@npm:2.1.9" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.7" + es-module-lexer: "npm:^1.5.4" + pathe: "npm:^1.1.2" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/0d3589f9f4e9cff696b5b49681fdb75d1638c75053728be52b4013f70792f38cb0120a9c15e3a4b22bdd6b795ad7c2da13bcaf47242d439f0906049e73bdd756 + languageName: node + linkType: hard + "vite-plugin-dts@npm:5.0.0-beta.3": version: 5.0.0-beta.3 resolution: "vite-plugin-dts@npm:5.0.0-beta.3" @@ -5524,6 +7079,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.4.21 + resolution: "vite@npm:5.4.21" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.43" + rollup: "npm:^4.20.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/468336a1409f728b464160cbf02672e72271fb688d0e605e776b74a89d27e1029509eef3a3a6c755928d8011e474dbf234824d054d07960be5f23cd176bc72de + languageName: node + linkType: hard + "vite@npm:^6.3.5": version: 6.3.5 resolution: "vite@npm:6.3.5" @@ -5579,6 +7177,56 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.0.0": + version: 2.1.9 + resolution: "vitest@npm:2.1.9" + dependencies: + "@vitest/expect": "npm:2.1.9" + "@vitest/mocker": "npm:2.1.9" + "@vitest/pretty-format": "npm:^2.1.9" + "@vitest/runner": "npm:2.1.9" + "@vitest/snapshot": "npm:2.1.9" + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + std-env: "npm:^3.8.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.1.9" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.9 + "@vitest/ui": 2.1.9 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/e339e16dccacf4589ff43cb1f38c7b4d14427956ae8ef48702af6820a9842347c2b6c77356aeddb040329759ca508a3cb2b104ddf78103ea5bc98ab8f2c3a54e + languageName: node + linkType: hard + "vscode-uri@npm:^3.0.8": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" @@ -5586,6 +7234,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + "warning@npm:^4.0.3": version: 4.0.3 resolution: "warning@npm:4.0.3" @@ -5595,6 +7252,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46 + languageName: node + linkType: hard + "webpack-virtual-modules@npm:^0.6.2": version: 0.6.2 resolution: "webpack-virtual-modules@npm:0.6.2" @@ -5602,6 +7266,24 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4 + languageName: node + linkType: hard + +"whatwg-url@npm:^16.0.0": + version: 16.0.1 + resolution: "whatwg-url@npm:16.0.1" + dependencies: + "@exodus/bytes": "npm:^1.11.0" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2 + languageName: node + linkType: hard + "which@npm:^1.2.9": version: 1.3.1 resolution: "which@npm:1.3.1" @@ -5635,6 +7317,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -5675,6 +7369,20 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From 93f2cb24412852c03f23b3188005eec4185ce2d6 Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 15:25:31 +0530 Subject: [PATCH 2/8] lint fix --- examples/react-example/src/index.tsx | 70 +++++++++++++------ .../src/ImageKitEditor.tsx | 8 +-- .../src/backward-compatibility.test.ts | 57 +++++++++++---- packages/imagekit-editor-dev/src/store.ts | 6 +- 4 files changed, 101 insertions(+), 40 deletions(-) diff --git a/examples/react-example/src/index.tsx b/examples/react-example/src/index.tsx index c68b0a5..a1b3bef 100644 --- a/examples/react-example/src/index.tsx +++ b/examples/react-example/src/index.tsx @@ -3,8 +3,8 @@ import { ImageKitEditor, type ImageKitEditorProps, type ImageKitEditorRef, - type Transformation, TRANSFORMATION_STATE_VERSION, + type Transformation, } from "@imagekit/editor" import { PiDownload } from "@react-icons/all-files/pi/PiDownload" import React, { useCallback, useEffect } from "react" @@ -50,12 +50,16 @@ function App() { const template = ref.current?.getTemplate() if (template) { // Remove the 'id' field from each transformation for storage - const templateToSave = template.map(({ id, ...rest }: Transformation) => rest) + const templateToSave = template.map( + ({ id, ...rest }: Transformation) => rest, + ) setSavedTemplate(templateToSave) // Also save to localStorage for persistence localStorage.setItem("editorTemplate", JSON.stringify(templateToSave)) console.log("Saved template:", templateToSave) - alert(`✅ Saved template with ${templateToSave.length} transformation(s)!`) + alert( + `✅ Saved template with ${templateToSave.length} transformation(s)!`, + ) } else { alert("⚠️ No transformations to save") } @@ -176,7 +180,8 @@ function App() { console.log("Signed URL", request.url) return Promise.resolve(request.url) }, - }) }, [handleAddImage, handleSaveTemplate]) + }) + }, [handleAddImage, handleSaveTemplate]) const toggle = () => { setOpen((prev: boolean) => !prev) @@ -213,12 +218,14 @@ function App() { padding: "10px 20px", fontSize: "16px", marginRight: "10px", - cursor: savedTemplate || localStorage.getItem("editorTemplate") - ? "pointer" - : "not-allowed", - opacity: savedTemplate || localStorage.getItem("editorTemplate") - ? 1 - : 0.5, + cursor: + savedTemplate || localStorage.getItem("editorTemplate") + ? "pointer" + : "not-allowed", + opacity: + savedTemplate || localStorage.getItem("editorTemplate") + ? 1 + : 0.5, }} > Load Saved Template @@ -264,12 +271,18 @@ function App() {

Types:{" "} - {Array.from( - new Set(savedTemplate.map((t) => t.type)) - ).join(", ")} + {Array.from(new Set(savedTemplate.map((t) => t.type))).join( + ", ", + )}

- + 📋 View Template JSON
📖 How to use Template Features:
           
  1. Click "Open ImageKit Editor" and apply some transformations
  2. -
  3. Click the "Save Template" button in the editor header
  4. +
  5. + Click the "Save Template" button in the editor + header +
  6. Close the editor
  7. - Click "Load Saved Template" - it will open the editor with all transformations restored + Click "Load Saved Template" - it will open the + editor with all transformations restored +
  8. +
  9. + Use "Clear Template" to remove the saved template
  10. -
  11. Use "Clear Template" to remove the saved template
-

- 💾 Persistent Storage: Templates are saved to localStorage, so they persist across page reloads! +

+ 💾 Persistent Storage: Templates are saved to + localStorage, so they persist across page reloads!

- Note: Template IDs are automatically generated on load to ensure uniqueness and enable reusability. + Note: Template IDs are automatically generated on + load to ensure uniqueness and enable reusability.

diff --git a/packages/imagekit-editor-dev/src/ImageKitEditor.tsx b/packages/imagekit-editor-dev/src/ImageKitEditor.tsx index d9bdf1b..89689e7 100644 --- a/packages/imagekit-editor-dev/src/ImageKitEditor.tsx +++ b/packages/imagekit-editor-dev/src/ImageKitEditor.tsx @@ -20,19 +20,19 @@ export interface ImageKitEditorRef { * @param image - Image URL string or FileElement with metadata */ loadImage: (image: string | InputFileElement) => void - + /** * Loads multiple images into the editor * @param images - Array of image URL strings or FileElements with metadata */ loadImages: (images: Array) => void - + /** * Switches the current active image * @param imageSrc - URL of the image to set as current */ setCurrentImage: (imageSrc: string) => void - + /** * Gets the current editor template (transformation stack) * @returns Array of transformation objects representing the template @@ -46,7 +46,7 @@ export interface ImageKitEditorRef { * ``` */ getTemplate: () => Transformation[] - + /** * Loads a template (transformation stack) into the editor * @param template - Array of transformation objects without the 'id' field diff --git a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts index 4a644c0..8e1fbc2 100644 --- a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts +++ b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest" +import { transformationFormatters, transformationSchema } from "./schema" import type { Transformation } from "./store" import { TRANSFORMATION_STATE_VERSION } from "./store" -import { transformationFormatters, transformationSchema } from "./schema" /** * V1 Template Fixtures @@ -313,7 +313,7 @@ function validateTransformation(t: Omit): { if (!result.success) { result.error.errors.forEach((err) => { errors.push( - `Schema validation failed for '${err.path.join(".")}': ${err.message}` + `Schema validation failed for '${err.path.join(".")}': ${err.message}`, ) }) } @@ -611,7 +611,7 @@ describe("Backward Compatibility - V1 Templates", () => { } // Remove id for storage - const { id, ...forStorage } = withId + const { id: _id, ...forStorage } = withId expect(forStorage.id).toBeUndefined() // Add id back when loading @@ -662,11 +662,13 @@ describe("Backward Compatibility - V1 Templates", () => { const result = validateTransformation(invalid) expect(result.valid).toBe(false) expect(result.errors.length).toBeGreaterThan(0) - expect(result.errors.some((e) => e.includes("not found in current schema"))).toBe(true) + expect( + result.errors.some((e) => e.includes("not found in current schema")), + ).toBe(true) }) it("should reject transformation with wrong type", () => { - const invalid: any = { + const invalid: Record = { key: "adjust-background", name: "Background", type: "wrong-type", @@ -685,7 +687,7 @@ describe("Backward Compatibility - V1 Templates", () => { name: "Corner Radius", type: "transformation", value: { - radius: 999, // Should be an object with {radius: number} + radius: 999, // Should be an object with {radius: number} }, version: "v1", } @@ -933,7 +935,12 @@ describe("Backward Compatibility - V1 Templates", () => { key: "layers-text", name: "Text", type: "transformation", - value: { text: "Hello", positionX: "bw_div_2", fontSize: 24, radius: 0 }, + value: { + text: "Hello", + positionX: "bw_div_2", + fontSize: 24, + radius: 0, + }, version: "v1", } expect(validateTransformation(template).valid).toBe(true) @@ -944,7 +951,12 @@ describe("Backward Compatibility - V1 Templates", () => { key: "layers-text", name: "Text", type: "transformation", - value: { text: "Hello", positionY: "bh_sub_100", fontSize: 24, radius: 0 }, + value: { + text: "Hello", + positionY: "bh_sub_100", + fontSize: 24, + radius: 0, + }, version: "v1", } expect(validateTransformation(template).valid).toBe(true) @@ -1227,7 +1239,7 @@ describe("Backward Compatibility - V1 Templates", () => { unsharpenMaskAmount: 1.2, unsharpenMaskThreshold: 0.1, }, - version: "v1", + version: "v1", } const result = validateTransformation(template) if (!result.valid) { @@ -1561,7 +1573,12 @@ describe("Backward Compatibility - V1 Templates", () => { key: "layers-text", name: "Text", type: "transformation", - value: { text: "Hello", positionX: "invalid_expr", fontSize: 24, radius: 0 }, + value: { + text: "Hello", + positionX: "invalid_expr", + fontSize: 24, + radius: 0, + }, version: "v1", } expect(validateTransformation(template).valid).toBe(false) @@ -1605,7 +1622,12 @@ describe("Backward Compatibility - V1 Templates", () => { key: "layers-text", name: "Text", type: "transformation", - value: { text: "Hello", lineHeight: "ih_mul_1.5", fontSize: 24, radius: 0 }, + value: { + text: "Hello", + lineHeight: "ih_mul_1.5", + fontSize: 24, + radius: 0, + }, version: "v1", } expect(validateTransformation(template).valid).toBe(true) @@ -1627,7 +1649,12 @@ describe("Backward Compatibility - V1 Templates", () => { key: "layers-text", name: "Text", type: "transformation", - value: { text: "Hello", lineHeight: "not_valid", fontSize: 24, radius: 0 }, + value: { + text: "Hello", + lineHeight: "not_valid", + fontSize: 24, + radius: 0, + }, version: "v1", } expect(validateTransformation(template).valid).toBe(false) @@ -2138,7 +2165,11 @@ describe("Backward Compatibility - V1 Templates", () => { }) it("should validate text layer with all alignment options", () => { - const alignments: Array<"left" | "right" | "center"> = ["left", "right", "center"] + const alignments: Array<"left" | "right" | "center"> = [ + "left", + "right", + "center", + ] alignments.forEach((align) => { const template: Omit = { key: "layers-text", diff --git a/packages/imagekit-editor-dev/src/store.ts b/packages/imagekit-editor-dev/src/store.ts index e2c1628..2220a89 100644 --- a/packages/imagekit-editor-dev/src/store.ts +++ b/packages/imagekit-editor-dev/src/store.ts @@ -311,13 +311,13 @@ const useEditorStore = create()( id: `transformation-${Date.now()}-${index}`, version: TRANSFORMATION_STATE_VERSION, })) - + const visibleTransformations: Record = {} transformationsWithIds.forEach((t) => { visibleTransformations[t.id] = true }) - - set((state) => ({ + + set((state) => ({ transformations: transformationsWithIds, visibleTransformations: { ...state.visibleTransformations, From 9f91d943fa2d59bd02d197ed236540e9e90b5bde Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 15:31:34 +0530 Subject: [PATCH 3/8] feat: add test script to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9c4303c..5046f3a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dev": "turbo run dev", "start": "turbo run start", "build": "turbo run build", + "test": "turbo run test", "version": "yarn workspace @imagekit/editor version", "package": "yarn build && shx cp README.md ./packages/imagekit-editor/ && yarn workspace @imagekit/editor pack --out ../../builds/imagekit-editor-%v.tgz", "release": "yarn build && shx cp README.md ./packages/imagekit-editor/ && yarn workspace @imagekit/editor pack --out ../../builds/imagekit-editor-%v.tgz && yarn workspace @imagekit/editor publish", From b6b2bdd2164addd2089c684b35b0d444e4e4502d Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 16:59:13 +0530 Subject: [PATCH 4/8] test: add coverage tests for background and resize/crop field visibility logic --- .../src/backward-compatibility.test.ts | 445 ++++++++++++++++ .../src/schema/field-config.test.ts | 496 ++++++++++++++++++ packages/imagekit-editor-dev/vite.config.ts | 10 +- 3 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 packages/imagekit-editor-dev/src/schema/field-config.test.ts diff --git a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts index 8e1fbc2..62799e9 100644 --- a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts +++ b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts @@ -2577,4 +2577,449 @@ describe("Backward Compatibility - V1 Templates", () => { expect(validateTransformation(template).valid).toBe(false) }) }) + + describe("Height Validator Coverage", () => { + it("should reject invalid height expression", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 100, + height: "invalid_height_expr", + mode: "cm-pad_resize", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + + + describe("Unsharpen Mask Error Coverage", () => { + it("should require sigma when unsharpen mask is enabled", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + unsharpenMask: true, + unsharpenMaskRadius: 2, + // Missing sigma and other required fields + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require amount when unsharpen mask is enabled", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1.5, + // Missing amount + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require threshold when unsharpen mask is enabled", () => { + const template: Omit = { + key: "layers-image", + name: "Image Layer", + type: "transformation", + value: { + imageUrl: "overlay.png", + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1.5, + unsharpenMaskAmount: 1.2, + // Missing threshold + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Background Gradient Auto Coverage", () => { + it("should validate background gradient with radial mode", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + backgroundGradientMode: "radial", + backgroundGradientPaletteSize: "2", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate background gradient with linear mode", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + backgroundGradientMode: "linear", + backgroundGradientPaletteSize: "4", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate manual background gradient", () => { + const template: Omit = { + key: "adjust-background", + name: "Background", + type: "transformation", + value: { + backgroundType: "gradient", + backgroundGradientAutoDominant: false, + backgroundGradient: { + type: "linear", + angle: "90", + stops: [ + { color: "#FF0000", stopPoint: 0 }, + { color: "#0000FF", stopPoint: 100 }, + ], + }, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Resize Mode Conversion Coverage", () => { + it("should validate c-at_max_enlarge mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-at_max_enlarge", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate c-force mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "c-force", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should validate c-at_max mode", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + mode: "c-at_max", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + + + + + describe("Maintain Ratio Focus Validations", () => { + it("should validate maintain_ratio with anchor focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + mode: "c-maintain_ratio", + focus: "anchor", + focusAnchor: "center", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusAnchor for maintain_ratio with anchor focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + mode: "c-maintain_ratio", + focus: "anchor", + // Missing focusAnchor + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should validate maintain_ratio with object focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + mode: "c-maintain_ratio", + focus: "object", + focusObject: "person", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require focusObject for maintain_ratio with object focus", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + mode: "c-maintain_ratio", + focus: "object", + // Missing focusObject + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + }) + + describe("Pad Resize Background Validation Errors", () => { + it("should require width when using blurred background", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + // width missing + height: 600, + mode: "cm-pad_resize", + backgroundType: "blurred", + backgroundBlurIntensity: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require height when using blurred background", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + // height missing + mode: "cm-pad_resize", + backgroundType: "blurred", + backgroundBlurIntensity: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require width when using generative fill", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + // width missing + height: 600, + mode: "cm-pad_resize", + backgroundType: "generative_fill", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should require height when using generative fill", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + // height missing + mode: "cm-pad_resize", + backgroundType: "generative_fill", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should pass validation with both dimensions for blurred background", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-pad_resize", + backgroundType: "blurred", + backgroundBlurIntensity: 10, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should pass validation with both dimensions for generative fill", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-pad_resize", + backgroundType: "generative_fill", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) + + describe("Final Coverage Gaps - Missing Validations", () => { + it("should reject aspect ratio without width or height", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + aspectRatio: "16-9", + // No width or height + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should accept aspect ratio with width", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + aspectRatio: "16-9", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should require at least one center coordinate for cm-extract with coordinates", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + // Missing both xc and yc + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should accept center coordinates with at least xc for cm-extract", () => { + const template: Omit = { + key: "resize_and_crop-resize_and_crop", + name: "Resize and Crop", + type: "transformation", + value: { + width: 800, + height: 600, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + xc: "400", + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + + it("should reject unsharpen mask with threshold = 0 as invalid", () => { + const template: Omit = { + key: "adjust-unsharpen-mask", + name: "Unsharpen Mask", + type: "transformation", + value: { + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1, + unsharpenMaskAmount: 0.5, + unsharpenMaskThreshold: 0, // Falsy value + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(false) + }) + + it("should accept unsharpen mask with valid positive threshold", () => { + const template: Omit = { + key: "adjust-unsharpen-mask", + name: "Unsharpen Mask", + type: "transformation", + value: { + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1, + unsharpenMaskAmount: 0.5, + unsharpenMaskThreshold: 0.05, + }, + version: "v1", + } + expect(validateTransformation(template).valid).toBe(true) + }) + }) }) diff --git a/packages/imagekit-editor-dev/src/schema/field-config.test.ts b/packages/imagekit-editor-dev/src/schema/field-config.test.ts new file mode 100644 index 0000000..4d7748e --- /dev/null +++ b/packages/imagekit-editor-dev/src/schema/field-config.test.ts @@ -0,0 +1,496 @@ +import { describe, expect, it } from "vitest" +import { backgroundTransformations } from "./background" +import { + getDefaultTransformationFromMode, + resizeAndCropTransformations, +} from "./resizeAndCrop" + +describe("Field Configuration Tests", () => { + describe("Background Fields - Visibility Logic", () => { + describe("background field (color picker)", () => { + it("should be visible for root_image when type is color and auto is off", () => { + const field = backgroundTransformations.background({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "color", + backgroundDominantAuto: false, + }) + + expect(visible).toBe(true) + }) + + it("should be hidden for root_image when auto dominant is enabled", () => { + const field = backgroundTransformations.background({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "color", + backgroundDominantAuto: true, + }) + + expect(visible).toBe(false) + }) + + it("should be visible for pad_resize when type is color and auto is off", () => { + const field = backgroundTransformations.background({ + transformationGroup: "background", + context: "pad_resize", + }) + + const visible = field.isVisible?.({ + backgroundType: "color", + backgroundDominantAuto: false, + }) + + expect(visible).toBe(true) + }) + + it("should be visible for pad_extract when type is color and auto is off", () => { + const field = backgroundTransformations.background({ + transformationGroup: "background", + context: "pad_extract", + }) + + const visible = field.isVisible?.({ + backgroundType: "color", + backgroundDominantAuto: false, + }) + + expect(visible).toBe(true) + }) + }) + + describe("backgroundGradientMode field", () => { + it("should be visible when type is gradient and auto dominant is true", () => { + const field = backgroundTransformations.backgroundGradientMode({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + }) + + expect(visible).toBe(true) + }) + + it("should be hidden when auto dominant is false", () => { + const field = backgroundTransformations.backgroundGradientMode({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "gradient", + backgroundGradientAutoDominant: false, + }) + + expect(visible).toBe(false) + }) + }) + + describe("backgroundGradientPaletteSize field", () => { + it("should be visible when type is gradient and auto dominant is true", () => { + const field = backgroundTransformations.backgroundGradientPaletteSize({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + }) + + expect(visible).toBe(true) + }) + + it("should be hidden when background type is not gradient", () => { + const field = backgroundTransformations.backgroundGradientPaletteSize({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "color", + backgroundGradientAutoDominant: true, + }) + + expect(visible).toBe(false) + }) + }) + + describe("backgroundGradient field (manual gradient)", () => { + it("should be visible when type is gradient and auto dominant is false", () => { + const field = backgroundTransformations.backgroundGradient({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "gradient", + backgroundGradientAutoDominant: false, + }) + + expect(visible).toBe(true) + }) + + it("should be hidden when auto dominant is true", () => { + const field = backgroundTransformations.backgroundGradient({ + transformationGroup: "background", + context: "root_image", + }) + + const visible = field.isVisible?.({ + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + }) + + expect(visible).toBe(false) + }) + }) + }) + + describe("Resize and Crop Fields - Visibility and Helpers", () => { + describe("coordinate field visibility", () => { + it("should show x field for topleft coordinates in extract mode", () => { + const xField = resizeAndCropTransformations.find((f) => f.name === "x") + + const visible = xField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + }) + + expect(visible).toBe(true) + }) + + it("should hide x field when coordinate method is not topleft", () => { + const xField = resizeAndCropTransformations.find((f) => f.name === "x") + + const visible = xField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + }) + + expect(visible).toBe(false) + }) + + it("should show y field for topleft coordinates in extract mode", () => { + const yField = resizeAndCropTransformations.find((f) => f.name === "y") + + const visible = yField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "topleft", + }) + + expect(visible).toBe(true) + }) + + it("should show xc field for center coordinates in extract mode", () => { + const xcField = resizeAndCropTransformations.find((f) => f.name === "xc") + + const visible = xcField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + }) + + expect(visible).toBe(true) + }) + + it("should show yc field for center coordinates in extract mode", () => { + const ycField = resizeAndCropTransformations.find((f) => f.name === "yc") + + const visible = ycField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + coordinateMethod: "center", + }) + + expect(visible).toBe(true) + }) + }) + + describe("focus field visibility", () => { + it("should show focusAnchor when focus is anchor", () => { + const focusAnchorField = resizeAndCropTransformations.find( + (f) => f.name === "focusAnchor", + ) + + const visible = focusAnchorField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "anchor", + }) + + expect(visible).toBe(true) + }) + + it("should show focusObject when focus is object", () => { + const focusObjectField = resizeAndCropTransformations.find( + (f) => f.name === "focusObject", + ) + + const visible = focusObjectField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "object", + }) + + expect(visible).toBe(true) + }) + + it("should show coordinateMethod when focus is coordinates", () => { + const coordinateMethodField = resizeAndCropTransformations.find( + (f) => f.name === "coordinateMethod", + ) + + const visible = coordinateMethodField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "coordinates", + }) + + expect(visible).toBe(true) + }) + }) + + describe("mode-specific field visibility", () => { + it("should show focus field for extract mode", () => { + const focusFields = resizeAndCropTransformations.filter( + (f) => f.name === "focus", + ) + // Find the one for extract mode + const extractFocusField = focusFields.find((f) => + f.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + }), + ) + + expect(extractFocusField).toBeDefined() + }) + + it("should show focus field for maintain_ratio crop", () => { + const focusFields = resizeAndCropTransformations.filter( + (f) => f.name === "focus", + ) + // Find the one for maintain_ratio mode + const maintainRatioFocusField = focusFields.find((f) => + f.isVisible?.({ + width: 100, + height: 100, + mode: "c-maintain_ratio", + }), + ) + + expect(maintainRatioFocusField).toBeDefined() + }) + }) + }) + + describe("Helper Functions - getDefaultTransformationfromMode", () => { + it("should return cropMode pad_resize for cm-pad_resize", () => { + const result = getDefaultTransformationFromMode("cm-pad_resize") + expect(result).toEqual({ cropMode: "pad_resize" }) + }) + + it("should return cropMode extract for cm-extract", () => { + const result = getDefaultTransformationFromMode("cm-extract") + expect(result).toEqual({ cropMode: "extract" }) + }) + + it("should return cropMode pad_extract for cm-pad_extract", () => { + const result = getDefaultTransformationFromMode("cm-pad_extract") + expect(result).toEqual({ cropMode: "pad_extract" }) + }) + + it("should return crop maintain_ratio for c-maintain_ratio", () => { + const result = getDefaultTransformationFromMode("c-maintain_ratio") + expect(result).toEqual({ crop: "maintain_ratio" }) + }) + + it("should return crop force for c-force", () => { + const result = getDefaultTransformationFromMode("c-force") + expect(result).toEqual({ crop: "force" }) + }) + + it("should return crop at_max for c-at_max", () => { + const result = getDefaultTransformationFromMode("c-at_max") + expect(result).toEqual({ crop: "at_max" }) + }) + + it("should return crop at_max_enlarge for c-at_max_enlarge", () => { + const result = getDefaultTransformationFromMode("c-at_max_enlarge") + expect(result).toEqual({ crop: "at_max_enlarge" }) + }) + + it("should return crop at_least for c-at_least", () => { + const result = getDefaultTransformationFromMode("c-at_least") + expect(result).toEqual({ crop: "at_least" }) + }) + + it("should return empty object for unknown mode", () => { + const result = getDefaultTransformationFromMode("unknown-mode") + expect(result).toEqual({}) + }) + }) + + describe("Additional Field Visibility Coverage", () => { + it("should show DPR field when enabled and width exists", () => { + const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + + const visible = dprField?.isVisible?.({ + dprEnabled: true, + width: 100, + }) + + expect(visible).toBe(true) + }) + + it("should show DPR field when enabled and height exists", () => { + const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + + const visible = dprField?.isVisible?.({ + dprEnabled: true, + height: 100, + }) + + expect(visible).toBe(true) + }) + + it("should hide DPR field when not enabled", () => { + const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + + const visible = dprField?.isVisible?.({ + dprEnabled: false, + width: 100, + }) + + expect(visible).toBe(false) + }) + + it("should show zoom field for face focus in extract mode", () => { + const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + + const visible = zoomField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "face", + }) + + expect(visible).toBe(true) + }) + + it("should show zoom field for object focus in maintain_ratio", () => { + const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + + const visible = zoomField?.isVisible?.({ + width: 100, + height: 100, + mode: "c-maintain_ratio", + focus: "object", + }) + + expect(visible).toBe(true) + }) + + it("should hide zoom field for anchor focus", () => { + const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + + const visible = zoomField?.isVisible?.({ + width: 100, + height: 100, + mode: "cm-extract", + focus: "anchor", + }) + + expect(visible).toBe(false) + }) + + it("should show focus field for c-force mode", () => { + const focusFields = resizeAndCropTransformations.filter( + (f) => f.name === "focus", + ) + // Find the one for force mode (has only auto option) + const forceField = focusFields.find((f) => + f.isVisible?.({ + width: 100, + height: 100, + mode: "c-force", + }), + ) + + expect(forceField).toBeDefined() + expect(forceField?.fieldProps?.options).toHaveLength(1) + expect(forceField?.fieldProps?.options?.[0].value).toBe("auto") + }) + + it("should test pad_resize background field wrapper", () => { + // Find background fields that are visible for pad_resize + const backgroundFields = resizeAndCropTransformations.filter( + (f) => + f.transformationGroup === "background" || + f.name === "backgroundType" || + f.name === "background", + ) + + // At least one should be visible for pad_resize with dimensions + const visibleForPadResize = backgroundFields.some((f) => + f.isVisible?.({ + width: 100, + height: 100, + mode: "cm-pad_resize", + backgroundType: "color", + }), + ) + + expect(visibleForPadResize).toBe(true) + }) + + it("should test pad_extract background field wrapper", () => { + // Find background fields that are visible for pad_extract + const backgroundFields = resizeAndCropTransformations.filter( + (f) => + f.transformationGroup === "background" || + f.name === "backgroundType" || + f.name === "background", + ) + + // At least one should be visible for pad_extract with dimensions + const visibleForPadExtract = backgroundFields.some((f) => + f.isVisible?.({ + width: 100, + height: 100, + mode: "cm-pad_extract", + backgroundType: "color", + }), + ) + + expect(visibleForPadExtract).toBe(true) + }) + }) +}) diff --git a/packages/imagekit-editor-dev/vite.config.ts b/packages/imagekit-editor-dev/vite.config.ts index 15d7a68..70de203 100644 --- a/packages/imagekit-editor-dev/vite.config.ts +++ b/packages/imagekit-editor-dev/vite.config.ts @@ -24,12 +24,18 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], - include: ["src/**/*.{ts,tsx}"], + include: ["src/schema/**/*.{ts,tsx}"], exclude: [ "src/**/*.{test,spec}.{ts,tsx}", - "src/index.tsx", "node_modules/**", ], + thresholds: { + // Only enforced on src/schema files - focusing on validation logic + lines: 85, // Realistic threshold given UI visibility code + branches: 85, + statements: 85, + perFile: false, // Global threshold across all schema files + }, }, }, build: { From e7044a2f30fac5aa008ab604d975c96e56b7d4cf Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 16:59:29 +0530 Subject: [PATCH 5/8] ci: update test command to run coverage tests --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0756460..3ae2a6c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: run: | yarn install --frozen-lockfile yarn lint - yarn test + yarn test:coverage yarn package env: CI: true From aee07842deaae4aa1a7f3b0d6ad885c51e9026fb Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 17:07:34 +0530 Subject: [PATCH 6/8] test: add validation tests for backward compatibility and transformation formatters --- .../src/backward-compatibility.test.ts | 83 +++++ .../src/schema/formatters.test.ts | 320 ++++++++++++++++++ .../src/schema/transformation.ts | 19 -- 3 files changed, 403 insertions(+), 19 deletions(-) create mode 100644 packages/imagekit-editor-dev/src/schema/formatters.test.ts diff --git a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts index 62799e9..eb4eb29 100644 --- a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts +++ b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts @@ -3005,6 +3005,25 @@ describe("Backward Compatibility - V1 Templates", () => { expect(validateTransformation(template).valid).toBe(false) }) + it("should reject unsharpen mask with missing threshold", () => { + const template: Omit = { + key: "adjust-unsharpen-mask", + name: "Unsharpen Mask", + type: "transformation", + value: { + unsharpenMask: true, + unsharpenMaskRadius: 2, + unsharpenMaskSigma: 1, + unsharpenMaskAmount: 0.5, + // Missing unsharpenMaskThreshold entirely + }, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + expect(result.errors?.some(e => e.includes("Threshold"))).toBe(true) + }) + it("should accept unsharpen mask with valid positive threshold", () => { const template: Omit = { key: "adjust-unsharpen-mask", @@ -3022,4 +3041,68 @@ describe("Backward Compatibility - V1 Templates", () => { expect(validateTransformation(template).valid).toBe(true) }) }) + + describe("Empty Transformation Validation - At Least One Value Required", () => { + it("should reject contrast transformation with no values", () => { + const template: Omit = { + key: "adjust-contrast", + name: "Contrast", + type: "transformation", + value: {}, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + expect(result.errors?.some(e => e.includes("At least one value"))).toBe(true) + }) + + it("should reject shadow transformation with no values", () => { + const template: Omit = { + key: "adjust-shadow", + name: "Shadow", + type: "transformation", + value: {}, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + expect(result.errors?.some(e => e.includes("At least one value"))).toBe(true) + }) + + it("should reject grayscale transformation with no values", () => { + const template: Omit = { + key: "adjust-grayscale", + name: "Grayscale", + type: "transformation", + value: {}, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + }) + + it("should reject radius transformation with no values", () => { + const template: Omit = { + key: "adjust-radius", + name: "Radius", + type: "transformation", + value: {}, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + }) + + it("should reject trim transformation with no values", () => { + const template: Omit = { + key: "adjust-trim", + name: "Trim", + type: "transformation", + value: {}, + version: "v1", + } + const result = validateTransformation(template) + expect(result.valid).toBe(false) + }) + }) }) diff --git a/packages/imagekit-editor-dev/src/schema/formatters.test.ts b/packages/imagekit-editor-dev/src/schema/formatters.test.ts new file mode 100644 index 0000000..557fc95 --- /dev/null +++ b/packages/imagekit-editor-dev/src/schema/formatters.test.ts @@ -0,0 +1,320 @@ +import { describe, expect, it } from "vitest" +import { transformationFormatters } from "./index" + +describe("Transformation Formatters", () => { + describe("background formatter", () => { + it("should format color background with dominant auto", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "color", + backgroundDominantAuto: true, + }, + transforms, + ) + expect(transforms.background).toBe("dominant") + }) + + it("should format gradient background with auto dominant", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + backgroundGradientPaletteSize: "4", + backgroundGradientMode: "contrast", + }, + transforms, + ) + expect(transforms.background).toBe("gradient_contrast_4") + }) + + it("should format gradient background with default values", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "gradient", + backgroundGradientAutoDominant: true, + }, + transforms, + ) + expect(transforms.background).toBe("gradient_dominant_2") + }) + + it("should format manual gradient background", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "gradient", + backgroundGradientAutoDominant: false, + backgroundGradient: { + from: "#FF0000", + to: "#0000FF", + direction: "top", + stopPoint: 50, + }, + }, + transforms, + ) + expect(transforms.raw).toContain("e-gradient") + expect(transforms.raw).toContain("from-FF0000") + expect(transforms.raw).toContain("to-0000FF") + }) + + it("should format blurred background with auto intensity", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "auto", + }, + transforms, + ) + expect(transforms.background).toBe("blurred_auto") + }) + + it("should format blurred background with auto intensity and brightness", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "auto", + backgroundBlurBrightness: "50", + }, + transforms, + ) + expect(transforms.background).toBe("blurred_auto_50") + }) + + it("should format blurred background with numeric intensity", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "10", + }, + transforms, + ) + expect(transforms.background).toBe("blurred_10") + }) + + it("should format blurred background with intensity and brightness", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "10", + backgroundBlurBrightness: "20", + }, + transforms, + ) + expect(transforms.background).toBe("blurred_10_20") + }) + + it("should handle negative blur brightness", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "10", + backgroundBlurBrightness: "-20", + }, + transforms, + ) + expect(transforms.background).toBe("blurred_10_N20") + }) + + it("should format generative fill background without prompt", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "generative_fill", + }, + transforms, + ) + expect(transforms.background).toBe("genfill") + }) + + it("should format generative fill with simple text prompt", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "generative_fill", + backgroundGenerativeFill: "beach", + }, + transforms, + ) + expect(transforms.background).toBe("genfill-prompt-beach") + }) + + it("should format generative fill with complex prompt", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "generative_fill", + backgroundGenerativeFill: "beach with palm trees!", + }, + transforms, + ) + expect(transforms.background).toContain("genfill-prompte-") + }) + + it("should format color background with manual color", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "color", + backgroundDominantAuto: false, + background: "#FF5733", + }, + transforms, + ) + expect(transforms.background).toBe("FF5733") + }) + + it("should default to blurred when intensity is invalid", () => { + const transforms: Record = {} + transformationFormatters.background( + { + backgroundType: "blurred", + backgroundBlurIntensity: "invalid", + }, + transforms, + ) + expect(transforms.background).toBe("blurred") + }) + }) + + describe("focus formatter", () => { + it("should format focus with anchor", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "anchor", + focusAnchor: "top_left", + }, + transforms, + ) + expect(transforms.focus).toBe("top_left") + }) + + it("should format focus with object", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "object", + focusObject: "face", + }, + transforms, + ) + expect(transforms.focus).toBe("face") + }) + + it("should format focus with auto", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "auto", + }, + transforms, + ) + expect(transforms.focus).toBe("auto") + }) + + it("should format focus with center coordinates", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "coordinates", + coordinateMethod: "center", + xc: "100", + yc: "200", + }, + transforms, + ) + expect(transforms.xc).toBe("100") + expect(transforms.yc).toBe("200") + }) + + it("should format focus with topleft coordinates", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "coordinates", + coordinateMethod: "topleft", + x: "50", + y: "75", + }, + transforms, + ) + expect(transforms.x).toBe("50") + expect(transforms.y).toBe("75") + }) + + it("should format focus with zoom", () => { + const transforms: Record = {} + transformationFormatters.focus( + { + focus: "auto", + zoom: 150, + }, + transforms, + ) + expect(transforms.zoom).toBe(1.5) + }) + }) + + describe("shadow formatter", () => { + it("should format shadow with all parameters", () => { + const transforms: Record = {} + transformationFormatters.shadow( + { + shadow: true, + shadowBlur: 10, + shadowSaturation: 50, + shadowOffsetX: 5, + shadowOffsetY: 8, + }, + transforms, + ) + expect(transforms.shadow).toBe("bl-10_st-50_x-5_y-8") + }) + + it("should skip shadow when disabled", () => { + const transforms: Record = {} + transformationFormatters.shadow( + { + shadow: false, + }, + transforms, + ) + expect(transforms.shadow).toBeUndefined() + }) + + it("should handle negative shadow offsets", () => { + const transforms: Record = {} + transformationFormatters.shadow( + { + shadow: true, + shadowOffsetX: -5, + shadowOffsetY: -10, + }, + transforms, + ) + expect(transforms.shadow).toContain("x-N5") + expect(transforms.shadow).toContain("y-N10") + }) + + it("should format shadow with only blur", () => { + const transforms: Record = {} + transformationFormatters.shadow( + { + shadow: true, + shadowBlur: 15, + }, + transforms, + ) + expect(transforms.shadow).toBe("bl-15") + }) + }) +}) diff --git a/packages/imagekit-editor-dev/src/schema/transformation.ts b/packages/imagekit-editor-dev/src/schema/transformation.ts index e188df5..6b05c4e 100644 --- a/packages/imagekit-editor-dev/src/schema/transformation.ts +++ b/packages/imagekit-editor-dev/src/schema/transformation.ts @@ -146,25 +146,6 @@ export const commonNumberAndExpressionValidator = z }) }) -const overlayBlockExpr = z - .string() - .regex(/^(?:bh|bw|bar)_(?:add|sub|mul|div|mod|pow)_(?:\d+(\.\d{1,2})?)$/, { - message: "String must be a valid expression string.", - }) - -export const overlayBlockExprValidator = z.any().superRefine((val, ctx) => { - if (commonNumber.safeParse(val).success) { - return - } - if (overlayBlockExpr.safeParse(val).success) { - return - } - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Must be a positive number or a valid expression string.", - }) -}) - const lineHeightInteger = z.coerce.string().regex(/^\d+$/) const lineHeightExpr = z From ed70e312f8ea969b40b3778bbe24976350322797 Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 17:08:04 +0530 Subject: [PATCH 7/8] refactor: clean up code formatting and improve readability in tests and configuration --- .../src/backward-compatibility.test.ts | 16 ++++------ .../src/schema/field-config.test.ts | 32 ++++++++++++++----- packages/imagekit-editor-dev/vite.config.ts | 5 +-- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts index eb4eb29..7807c90 100644 --- a/packages/imagekit-editor-dev/src/backward-compatibility.test.ts +++ b/packages/imagekit-editor-dev/src/backward-compatibility.test.ts @@ -2595,8 +2595,6 @@ describe("Backward Compatibility - V1 Templates", () => { }) }) - - describe("Unsharpen Mask Error Coverage", () => { it("should require sigma when unsharpen mask is enabled", () => { const template: Omit = { @@ -2752,10 +2750,6 @@ describe("Backward Compatibility - V1 Templates", () => { }) }) - - - - describe("Maintain Ratio Focus Validations", () => { it("should validate maintain_ratio with anchor focus", () => { const template: Omit = { @@ -3021,7 +3015,7 @@ describe("Backward Compatibility - V1 Templates", () => { } const result = validateTransformation(template) expect(result.valid).toBe(false) - expect(result.errors?.some(e => e.includes("Threshold"))).toBe(true) + expect(result.errors?.some((e) => e.includes("Threshold"))).toBe(true) }) it("should accept unsharpen mask with valid positive threshold", () => { @@ -3053,7 +3047,9 @@ describe("Backward Compatibility - V1 Templates", () => { } const result = validateTransformation(template) expect(result.valid).toBe(false) - expect(result.errors?.some(e => e.includes("At least one value"))).toBe(true) + expect(result.errors?.some((e) => e.includes("At least one value"))).toBe( + true, + ) }) it("should reject shadow transformation with no values", () => { @@ -3066,7 +3062,9 @@ describe("Backward Compatibility - V1 Templates", () => { } const result = validateTransformation(template) expect(result.valid).toBe(false) - expect(result.errors?.some(e => e.includes("At least one value"))).toBe(true) + expect(result.errors?.some((e) => e.includes("At least one value"))).toBe( + true, + ) }) it("should reject grayscale transformation with no values", () => { diff --git a/packages/imagekit-editor-dev/src/schema/field-config.test.ts b/packages/imagekit-editor-dev/src/schema/field-config.test.ts index 4d7748e..80882eb 100644 --- a/packages/imagekit-editor-dev/src/schema/field-config.test.ts +++ b/packages/imagekit-editor-dev/src/schema/field-config.test.ts @@ -201,7 +201,9 @@ describe("Field Configuration Tests", () => { }) it("should show xc field for center coordinates in extract mode", () => { - const xcField = resizeAndCropTransformations.find((f) => f.name === "xc") + const xcField = resizeAndCropTransformations.find( + (f) => f.name === "xc", + ) const visible = xcField?.isVisible?.({ width: 100, @@ -215,7 +217,9 @@ describe("Field Configuration Tests", () => { }) it("should show yc field for center coordinates in extract mode", () => { - const ycField = resizeAndCropTransformations.find((f) => f.name === "yc") + const ycField = resizeAndCropTransformations.find( + (f) => f.name === "yc", + ) const visible = ycField?.isVisible?.({ width: 100, @@ -360,7 +364,9 @@ describe("Field Configuration Tests", () => { describe("Additional Field Visibility Coverage", () => { it("should show DPR field when enabled and width exists", () => { - const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + const dprField = resizeAndCropTransformations.find( + (f) => f.name === "dpr", + ) const visible = dprField?.isVisible?.({ dprEnabled: true, @@ -371,7 +377,9 @@ describe("Field Configuration Tests", () => { }) it("should show DPR field when enabled and height exists", () => { - const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + const dprField = resizeAndCropTransformations.find( + (f) => f.name === "dpr", + ) const visible = dprField?.isVisible?.({ dprEnabled: true, @@ -382,7 +390,9 @@ describe("Field Configuration Tests", () => { }) it("should hide DPR field when not enabled", () => { - const dprField = resizeAndCropTransformations.find((f) => f.name === "dpr") + const dprField = resizeAndCropTransformations.find( + (f) => f.name === "dpr", + ) const visible = dprField?.isVisible?.({ dprEnabled: false, @@ -393,7 +403,9 @@ describe("Field Configuration Tests", () => { }) it("should show zoom field for face focus in extract mode", () => { - const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + const zoomField = resizeAndCropTransformations.find( + (f) => f.name === "zoom", + ) const visible = zoomField?.isVisible?.({ width: 100, @@ -406,7 +418,9 @@ describe("Field Configuration Tests", () => { }) it("should show zoom field for object focus in maintain_ratio", () => { - const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + const zoomField = resizeAndCropTransformations.find( + (f) => f.name === "zoom", + ) const visible = zoomField?.isVisible?.({ width: 100, @@ -419,7 +433,9 @@ describe("Field Configuration Tests", () => { }) it("should hide zoom field for anchor focus", () => { - const zoomField = resizeAndCropTransformations.find((f) => f.name === "zoom") + const zoomField = resizeAndCropTransformations.find( + (f) => f.name === "zoom", + ) const visible = zoomField?.isVisible?.({ width: 100, diff --git a/packages/imagekit-editor-dev/vite.config.ts b/packages/imagekit-editor-dev/vite.config.ts index 70de203..84f1f2d 100644 --- a/packages/imagekit-editor-dev/vite.config.ts +++ b/packages/imagekit-editor-dev/vite.config.ts @@ -25,10 +25,7 @@ export default defineConfig({ provider: "v8", reporter: ["text", "json", "html"], include: ["src/schema/**/*.{ts,tsx}"], - exclude: [ - "src/**/*.{test,spec}.{ts,tsx}", - "node_modules/**", - ], + exclude: ["src/**/*.{test,spec}.{ts,tsx}", "node_modules/**"], thresholds: { // Only enforced on src/schema files - focusing on validation logic lines: 85, // Realistic threshold given UI visibility code From 2aa122fb596687ef6999874c74ec75236faf8755 Mon Sep 17 00:00:00 2001 From: Manu Chaudhary Date: Tue, 10 Mar 2026 17:09:20 +0530 Subject: [PATCH 8/8] fix: increase coverage thresholds for lines, branches, and statements in Vite config --- packages/imagekit-editor-dev/vite.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/imagekit-editor-dev/vite.config.ts b/packages/imagekit-editor-dev/vite.config.ts index 84f1f2d..40053ea 100644 --- a/packages/imagekit-editor-dev/vite.config.ts +++ b/packages/imagekit-editor-dev/vite.config.ts @@ -28,9 +28,9 @@ export default defineConfig({ exclude: ["src/**/*.{test,spec}.{ts,tsx}", "node_modules/**"], thresholds: { // Only enforced on src/schema files - focusing on validation logic - lines: 85, // Realistic threshold given UI visibility code - branches: 85, - statements: 85, + lines: 90, // Realistic threshold given UI visibility code + branches: 90, + statements: 90, perFile: false, // Global threshold across all schema files }, },