@jhits/plugin-images 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +3 -2
  2. package/src/api/fallback/route.ts +69 -0
  3. package/src/api/index.ts +10 -0
  4. package/src/api/list/index.ts +96 -0
  5. package/src/api/resolve/route.ts +241 -0
  6. package/src/api/router.ts +85 -0
  7. package/src/api/upload/index.ts +88 -0
  8. package/src/api/uploads/[filename]/route.ts +93 -0
  9. package/src/api-server.ts +11 -0
  10. package/src/assets/noimagefound.jpg +0 -0
  11. package/src/components/BackgroundImage.d.ts +11 -0
  12. package/src/components/BackgroundImage.d.ts.map +1 -0
  13. package/src/components/BackgroundImage.tsx +92 -0
  14. package/src/components/GlobalImageEditor/config.d.ts +9 -0
  15. package/src/components/GlobalImageEditor/config.d.ts.map +1 -0
  16. package/src/components/GlobalImageEditor/config.ts +21 -0
  17. package/src/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
  18. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
  19. package/src/components/GlobalImageEditor/eventHandlers.ts +267 -0
  20. package/src/components/GlobalImageEditor/imageDetection.d.ts +16 -0
  21. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
  22. package/src/components/GlobalImageEditor/imageDetection.ts +160 -0
  23. package/src/components/GlobalImageEditor/imageSetup.d.ts +9 -0
  24. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
  25. package/src/components/GlobalImageEditor/imageSetup.ts +306 -0
  26. package/src/components/GlobalImageEditor/saveLogic.d.ts +26 -0
  27. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
  28. package/src/components/GlobalImageEditor/saveLogic.ts +133 -0
  29. package/src/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
  30. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
  31. package/src/components/GlobalImageEditor/stylingDetection.ts +122 -0
  32. package/src/components/GlobalImageEditor/transformParsing.d.ts +16 -0
  33. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
  34. package/src/components/GlobalImageEditor/transformParsing.ts +83 -0
  35. package/src/components/GlobalImageEditor/types.d.ts +36 -0
  36. package/src/components/GlobalImageEditor/types.d.ts.map +1 -0
  37. package/src/components/GlobalImageEditor/types.ts +39 -0
  38. package/src/components/GlobalImageEditor.d.ts +8 -0
  39. package/src/components/GlobalImageEditor.d.ts.map +1 -0
  40. package/src/components/GlobalImageEditor.tsx +327 -0
  41. package/src/components/Image.d.ts +22 -0
  42. package/src/components/Image.d.ts.map +1 -0
  43. package/src/components/Image.tsx +343 -0
  44. package/src/components/ImageBrowserModal.d.ts +13 -0
  45. package/src/components/ImageBrowserModal.d.ts.map +1 -0
  46. package/src/components/ImageBrowserModal.tsx +837 -0
  47. package/src/components/ImageEditor.d.ts +27 -0
  48. package/src/components/ImageEditor.d.ts.map +1 -0
  49. package/src/components/ImageEditor.tsx +323 -0
  50. package/src/components/ImageEffectsPanel.tsx +116 -0
  51. package/src/components/ImagePicker.d.ts +3 -0
  52. package/src/components/ImagePicker.d.ts.map +1 -0
  53. package/src/components/ImagePicker.tsx +265 -0
  54. package/src/components/ImagesPluginInit.d.ts +24 -0
  55. package/src/components/ImagesPluginInit.d.ts.map +1 -0
  56. package/src/components/ImagesPluginInit.tsx +31 -0
  57. package/src/components/index.ts +10 -0
  58. package/src/config.ts +179 -0
  59. package/src/hooks/useImagePicker.d.ts +20 -0
  60. package/src/hooks/useImagePicker.d.ts.map +1 -0
  61. package/src/hooks/useImagePicker.ts +344 -0
  62. package/src/index.server.ts +12 -0
  63. package/src/index.tsx +56 -0
  64. package/src/init.tsx +58 -0
  65. package/src/types/index.d.ts +80 -0
  66. package/src/types/index.d.ts.map +1 -0
  67. package/src/types/index.ts +84 -0
  68. package/src/utils/fallback.d.ts +27 -0
  69. package/src/utils/fallback.d.ts.map +1 -0
  70. package/src/utils/fallback.ts +73 -0
  71. package/src/utils/transforms.d.ts +26 -0
  72. package/src/utils/transforms.d.ts.map +1 -0
  73. package/src/utils/transforms.ts +54 -0
  74. package/src/views/ImageManager.d.ts +10 -0
  75. package/src/views/ImageManager.d.ts.map +1 -0
  76. package/src/views/ImageManager.tsx +30 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Image Uploads API Route
3
+ * Serves uploaded images for the plugin-images system
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { readFile, unlink } from 'fs/promises';
8
+ import path from 'path';
9
+
10
+ export async function GET(
11
+ request: NextRequest,
12
+ { params }: { params: Promise<{ filename: string }> }
13
+ ) {
14
+ const { filename } = await params;
15
+
16
+ // Security: Prevent directory traversal (only allow the filename)
17
+ const sanitizedFilename = path.basename(filename);
18
+ const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
19
+
20
+ // If filename doesn't have an extension, try common image extensions
21
+ let filePath = path.join(uploadsDir, sanitizedFilename);
22
+ const hasExtension = path.extname(sanitizedFilename).length > 0;
23
+
24
+ if (!hasExtension) {
25
+ // Try common image extensions
26
+ const extensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg'];
27
+ let found = false;
28
+ for (const ext of extensions) {
29
+ const testPath = filePath + ext;
30
+ try {
31
+ await readFile(testPath);
32
+ filePath = testPath;
33
+ found = true;
34
+ break;
35
+ } catch {
36
+ // Continue to next extension
37
+ }
38
+ }
39
+ if (!found) {
40
+ // If no extension found, try the original filename
41
+ filePath = path.join(uploadsDir, sanitizedFilename);
42
+ }
43
+ }
44
+
45
+ try {
46
+ const fileBuffer = await readFile(filePath);
47
+
48
+ // Determine content type based on extension
49
+ const ext = path.extname(filePath).toLowerCase();
50
+ let contentType = 'application/octet-stream';
51
+ if (ext === '.png') contentType = 'image/png';
52
+ else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg';
53
+ else if (ext === '.gif') contentType = 'image/gif';
54
+ else if (ext === '.webp') contentType = 'image/webp';
55
+ else if (ext === '.svg') contentType = 'image/svg+xml';
56
+
57
+ return new NextResponse(fileBuffer, {
58
+ headers: {
59
+ 'Content-Type': contentType,
60
+ 'Cache-Control': 'public, max-age=31536000, immutable',
61
+ },
62
+ });
63
+ } catch (e) {
64
+ // Don't log errors for missing files - it's expected for some requests
65
+ return new NextResponse('File not found', { status: 404 });
66
+ }
67
+ }
68
+
69
+ export async function DELETE(
70
+ request: NextRequest,
71
+ { params }: { params: Promise<{ filename: string }> }
72
+ ) {
73
+ const { filename } = await params;
74
+
75
+ // Security: Prevent directory traversal (only allow the filename)
76
+ const sanitizedFilename = path.basename(filename);
77
+ const filePath = path.join(process.cwd(), 'data', 'uploads', sanitizedFilename);
78
+
79
+ try {
80
+ await unlink(filePath);
81
+ return NextResponse.json({ success: true, message: 'File deleted' });
82
+ } catch (e: any) {
83
+ if (e.code === 'ENOENT') {
84
+ return NextResponse.json({ error: 'File not found' }, { status: 404 });
85
+ }
86
+ console.error('File deletion error:', e);
87
+ return NextResponse.json(
88
+ { error: 'Failed to delete file', detail: e.message },
89
+ { status: 500 }
90
+ );
91
+ }
92
+ }
93
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Plugin Images - Server-Only API Exports
3
+ * This file is only imported in server-side code (API routes)
4
+ *
5
+ * IMPORTANT: This file uses Node.js modules (fs, path, etc.) and should NEVER
6
+ * be imported in client-side code. Only use in server-side API routes.
7
+ */
8
+
9
+ // Re-export everything from the API index
10
+ export * from './api';
11
+
Binary file
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export interface BackgroundImageProps {
3
+ id: string;
4
+ className?: string;
5
+ style?: React.CSSProperties;
6
+ children?: React.ReactNode;
7
+ backgroundSize?: 'cover' | 'contain' | 'auto' | string;
8
+ backgroundPosition?: string;
9
+ }
10
+ export declare function BackgroundImage({ id, className, style, children, backgroundSize, backgroundPosition, }: BackgroundImageProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=BackgroundImage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundImage.d.ts","sourceRoot":"","sources":["BackgroundImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,MAAM,WAAW,oBAAoB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,eAAe,CAAC,EAC5B,EAAE,EACF,SAAc,EACd,KAAU,EACV,QAAQ,EACR,cAAwB,EACxB,kBAA6B,GAChC,EAAE,oBAAoB,2CAqEtB"}
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { Image } from './Image';
5
+ import { Edit2 } from 'lucide-react';
6
+
7
+ export interface BackgroundImageProps {
8
+ id: string;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ children?: React.ReactNode;
12
+ backgroundSize?: 'cover' | 'contain' | 'auto' | string;
13
+ backgroundPosition?: string;
14
+ }
15
+
16
+ export function BackgroundImage({
17
+ id,
18
+ className = '',
19
+ style = {},
20
+ children,
21
+ backgroundSize = 'cover',
22
+ backgroundPosition = 'center',
23
+ }: BackgroundImageProps) {
24
+ const [isAdmin, setIsAdmin] = useState(false);
25
+ const [isLoading, setIsLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ const checkUser = async () => {
29
+ try {
30
+ const res = await fetch('/api/me');
31
+ const data = await res.json();
32
+ if (data.loggedIn && ['admin', 'dev'].includes(data.user?.role)) {
33
+ setIsAdmin(true);
34
+ }
35
+ } catch (error) {
36
+ console.error('Auth error:', error);
37
+ } finally {
38
+ setIsLoading(false);
39
+ }
40
+ };
41
+ checkUser();
42
+ }, []);
43
+
44
+ const handleEditClick = (e: React.MouseEvent) => {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ window.dispatchEvent(new CustomEvent('open-image-editor', { detail: { id } }));
48
+ };
49
+
50
+ return (
51
+ <div
52
+ className={`group relative overflow-hidden ${className}`}
53
+ style={{ ...style, minHeight: style.minHeight ?? '300px' }} // Ensure visibility
54
+ data-background-id={id}
55
+ >
56
+ {/* 1. THE BACKGROUND LAYER
57
+ We force 'fill' and 'w-full h-full' to ensure the internal
58
+ Image component triggers its 'shouldFill' logic correctly.
59
+ */}
60
+ <div className="absolute inset-0 z-0">
61
+ <Image
62
+ id={id}
63
+ alt=""
64
+ fill
65
+ priority
66
+ className="w-full h-full"
67
+ objectFit={backgroundSize as any}
68
+ objectPosition={backgroundPosition}
69
+ editable={false}
70
+ />
71
+ </div>
72
+
73
+ {/* 2. CONTENT LAYER */}
74
+ <div className="relative z-10 w-full h-full">
75
+ {children}
76
+ </div>
77
+
78
+ {/* 3. ADMIN UI */}
79
+ {!isLoading && isAdmin && (
80
+ <button
81
+ onClick={handleEditClick}
82
+ className="absolute bottom-8 left-8 z-50 flex items-center gap-3 px-6 py-3 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-full shadow-2xl border border-neutral-200 dark:border-neutral-700 opacity-0 translate-y-4 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out"
83
+ >
84
+ <Edit2 size={16} className="text-primary" />
85
+ <span className="text-[11px] font-black uppercase tracking-widest">
86
+ Edit Background
87
+ </span>
88
+ </button>
89
+ )}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Configuration utilities for GlobalImageEditor
3
+ */
4
+ import type { PluginConfig } from './types';
5
+ /**
6
+ * Read plugin configuration from window global
7
+ */
8
+ export declare function getPluginConfig(): PluginConfig;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;GAEG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAW9C"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Configuration utilities for GlobalImageEditor
3
+ */
4
+
5
+ import type { PluginConfig } from './types';
6
+
7
+ /**
8
+ * Read plugin configuration from window global
9
+ */
10
+ export function getPluginConfig(): PluginConfig {
11
+ if (typeof window === 'undefined') {
12
+ return { enabled: false };
13
+ }
14
+
15
+ const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__?.['plugin-images'];
16
+ return {
17
+ enabled: pluginProps?.enabled !== undefined ? pluginProps.enabled : true,
18
+ className: pluginProps?.className,
19
+ overlayClassName: pluginProps?.overlayClassName,
20
+ };
21
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Event handlers for image changes and effects
3
+ */
4
+ import type { SelectedImage } from './types';
5
+ import type { ImageMetadata } from '../../types';
6
+ /**
7
+ * Apply image changes to DOM and save to API
8
+ */
9
+ export declare function handleImageChange(image: ImageMetadata | null, selectedImage: SelectedImage, onClose: () => void): Promise<void>;
10
+ /**
11
+ * Handle brightness change
12
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
13
+ */
14
+ export declare function handleBrightnessChange(brightness: number, selectedImage: SelectedImage, setSelectedImage: (image: SelectedImage) => void, saveImmediately?: boolean): Promise<void>;
15
+ /**
16
+ * Handle blur change
17
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
18
+ */
19
+ export declare function handleBlurChange(blur: number, selectedImage: SelectedImage, setSelectedImage: (image: SelectedImage) => void, saveImmediately?: boolean): Promise<void>;
20
+ //# sourceMappingURL=eventHandlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventHandlers.d.ts","sourceRoot":"","sources":["eventHandlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD;;GAEG;AACH,wBAAsB,iBAAiB,CACnC,KAAK,EAAE,aAAa,GAAG,IAAI,EAC3B,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,MAAM,IAAI,GACpB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAoID;;;GAGG;AACH,wBAAsB,sBAAsB,CACxC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAChD,eAAe,GAAE,OAAc,GAChC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAClC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAChD,eAAe,GAAE,OAAc,GAChC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Event handlers for image changes and effects
3
+ */
4
+
5
+ import type { SelectedImage } from './types';
6
+ import type { ImageMetadata } from '../../types';
7
+ import { getFilename, saveTransformToAPI, normalizePosition } from './saveLogic';
8
+
9
+ /**
10
+ * Apply image changes to DOM and save to API
11
+ */
12
+ export async function handleImageChange(
13
+ image: ImageMetadata | null,
14
+ selectedImage: SelectedImage,
15
+ onClose: () => void
16
+ ): Promise<void> {
17
+ if (!image) return;
18
+
19
+ const { element } = selectedImage;
20
+ const semanticId = element.getAttribute('data-image-id') ||
21
+ element.getAttribute('data-background-image-id');
22
+
23
+ if (!semanticId) {
24
+ console.error('[GlobalImageEditor] No semantic ID found for image');
25
+ return;
26
+ }
27
+
28
+ // Extract filename from image URL or use image.id
29
+ const filename = image.url.split('/').pop()?.split('?')[0] || image.id;
30
+ const normalizedPositionX = normalizePosition(selectedImage.positionX);
31
+ const normalizedPositionY = normalizePosition(selectedImage.positionY);
32
+
33
+ // Save the mapping between semantic ID and filename, including effects and transforms
34
+ await saveTransformToAPI(
35
+ semanticId,
36
+ filename,
37
+ selectedImage.scale,
38
+ normalizedPositionX,
39
+ normalizedPositionY,
40
+ selectedImage.brightness,
41
+ selectedImage.blur
42
+ );
43
+
44
+ // Update image src immediately - handle background images differently
45
+ if (selectedImage.isBackground) {
46
+ updateBackgroundImage(element, image.url, selectedImage, normalizedPositionX, normalizedPositionY);
47
+ } else {
48
+ updateRegularImage(element, image.url, semanticId, selectedImage, normalizedPositionX, normalizedPositionY);
49
+ }
50
+
51
+ // Dispatch a custom event to notify Image components to re-resolve
52
+ window.dispatchEvent(new CustomEvent('image-mapping-updated', {
53
+ detail: {
54
+ id: semanticId,
55
+ filename,
56
+ brightness: selectedImage.brightness,
57
+ blur: selectedImage.blur,
58
+ scale: selectedImage.scale,
59
+ positionX: selectedImage.positionX,
60
+ positionY: selectedImage.positionY,
61
+ }
62
+ }));
63
+
64
+ onClose();
65
+ }
66
+
67
+ /**
68
+ * Update background image element
69
+ */
70
+ function updateBackgroundImage(
71
+ element: HTMLElement,
72
+ imageUrl: string,
73
+ selectedImage: SelectedImage,
74
+ normalizedPositionX: number,
75
+ normalizedPositionY: number
76
+ ): void {
77
+ // For background images, update the nested Image component
78
+ const imgWrapper = element.querySelector('[data-image-id]');
79
+ if (imgWrapper) {
80
+ const img = imgWrapper.querySelector('img');
81
+ if (img) {
82
+ img.src = imageUrl;
83
+ img.setAttribute('data-edited-src', imageUrl);
84
+ img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
85
+ img.style.transformOrigin = 'center center';
86
+ }
87
+ } else {
88
+ // Fallback: update background-image style if no nested Image component
89
+ const bgImage = window.getComputedStyle(element).backgroundImage;
90
+ if (bgImage && bgImage !== 'none') {
91
+ element.style.backgroundImage = `url(${imageUrl})`;
92
+ }
93
+ }
94
+
95
+ // Store attributes on the background container
96
+ const semanticId = element.getAttribute('data-image-id') || element.getAttribute('data-background-image-id') || '';
97
+ element.setAttribute('data-background-image-id', semanticId);
98
+ element.setAttribute('data-edited-src', imageUrl);
99
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
100
+ element.setAttribute('data-blur', selectedImage.blur.toString());
101
+ element.setAttribute('data-scale', selectedImage.scale.toString());
102
+ element.setAttribute('data-position-x', normalizedPositionX.toString());
103
+ element.setAttribute('data-position-y', normalizedPositionY.toString());
104
+
105
+ // Apply filter to the wrapper element
106
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
107
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
108
+ } else {
109
+ element.style.filter = '';
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Update regular image element
115
+ */
116
+ function updateRegularImage(
117
+ element: HTMLElement,
118
+ imageUrl: string,
119
+ semanticId: string,
120
+ selectedImage: SelectedImage,
121
+ normalizedPositionX: number,
122
+ normalizedPositionY: number
123
+ ): void {
124
+ if (element.tagName === 'IMG') {
125
+ (element as HTMLImageElement).src = imageUrl;
126
+ element.setAttribute('data-edited-src', imageUrl);
127
+ element.setAttribute('data-image-id', semanticId);
128
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
129
+ element.setAttribute('data-blur', selectedImage.blur.toString());
130
+
131
+ // Apply filter
132
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
133
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
134
+ } else {
135
+ element.style.filter = '';
136
+ }
137
+ } else {
138
+ // Next.js Image wrapper - find the actual img element
139
+ const img = element.querySelector('img');
140
+ if (img) {
141
+ img.src = imageUrl;
142
+ img.setAttribute('data-edited-src', imageUrl);
143
+ img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
144
+ img.style.transformOrigin = 'center center';
145
+ }
146
+ element.setAttribute('data-image-id', semanticId);
147
+ element.setAttribute('data-edited-src', imageUrl);
148
+ element.setAttribute('data-brightness', selectedImage.brightness.toString());
149
+ element.setAttribute('data-blur', selectedImage.blur.toString());
150
+ element.setAttribute('data-scale', selectedImage.scale.toString());
151
+ element.setAttribute('data-position-x', normalizedPositionX.toString());
152
+ element.setAttribute('data-position-y', normalizedPositionY.toString());
153
+
154
+ // Apply filter to wrapper
155
+ if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
156
+ element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
157
+ } else {
158
+ element.style.filter = '';
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get filename from element for saving effects
165
+ */
166
+ async function getFilenameFromElement(
167
+ semanticId: string,
168
+ selectedImage: SelectedImage
169
+ ): Promise<string> {
170
+ const filename = await getFilename(semanticId, selectedImage);
171
+ if (filename) return filename;
172
+
173
+ const { element } = selectedImage;
174
+ if (selectedImage.isBackground) {
175
+ const bgImage = window.getComputedStyle(element).backgroundImage;
176
+ const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
177
+ if (urlMatch && urlMatch[1]) {
178
+ return urlMatch[1].split('/api/uploads/')[1]?.split('?')[0] || semanticId;
179
+ }
180
+ } else {
181
+ let imgSrc: string | null = null;
182
+ if (element instanceof HTMLImageElement && element.src) {
183
+ imgSrc = element.src;
184
+ } else {
185
+ const img = element.querySelector('img') as HTMLImageElement | null;
186
+ if (img?.src) {
187
+ imgSrc = img.src;
188
+ }
189
+ }
190
+ if (imgSrc) {
191
+ return imgSrc.split('/api/uploads/')[1]?.split('?')[0] || semanticId;
192
+ }
193
+ }
194
+ return semanticId;
195
+ }
196
+
197
+ /**
198
+ * Handle brightness change
199
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
200
+ */
201
+ export async function handleBrightnessChange(
202
+ brightness: number,
203
+ selectedImage: SelectedImage,
204
+ setSelectedImage: (image: SelectedImage) => void,
205
+ saveImmediately: boolean = true
206
+ ): Promise<void> {
207
+ const { element } = selectedImage;
208
+ const semanticId = element.getAttribute('data-image-id') ||
209
+ element.getAttribute('data-background-image-id');
210
+
211
+ if (!semanticId) return;
212
+
213
+ const currentFilter = element.style.filter || '';
214
+ const blurMatch = currentFilter.match(/blur\((\d+)px\)/);
215
+ const blur = blurMatch ? parseInt(blurMatch[1]) : selectedImage.blur;
216
+
217
+ // Update the selectedImage state
218
+ setSelectedImage({
219
+ ...selectedImage,
220
+ brightness,
221
+ });
222
+
223
+ element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
224
+ element.setAttribute('data-brightness', brightness.toString());
225
+
226
+ // Save the effect immediately only if requested (not when in editor)
227
+ if (saveImmediately) {
228
+ const filename = await getFilenameFromElement(semanticId, selectedImage);
229
+ await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Handle blur change
235
+ * @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
236
+ */
237
+ export async function handleBlurChange(
238
+ blur: number,
239
+ selectedImage: SelectedImage,
240
+ setSelectedImage: (image: SelectedImage) => void,
241
+ saveImmediately: boolean = true
242
+ ): Promise<void> {
243
+ const { element } = selectedImage;
244
+ const semanticId = element.getAttribute('data-image-id') ||
245
+ element.getAttribute('data-background-image-id');
246
+
247
+ if (!semanticId) return;
248
+
249
+ const currentFilter = element.style.filter || '';
250
+ const brightnessMatch = currentFilter.match(/brightness\((\d+)%\)/);
251
+ const brightness = brightnessMatch ? parseInt(brightnessMatch[1]) : selectedImage.brightness;
252
+
253
+ // Update the selectedImage state
254
+ setSelectedImage({
255
+ ...selectedImage,
256
+ blur,
257
+ });
258
+
259
+ element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
260
+ element.setAttribute('data-blur', blur.toString());
261
+
262
+ // Save the effect immediately only if requested (not when in editor)
263
+ if (saveImmediately) {
264
+ const filename = await getFilenameFromElement(semanticId, selectedImage);
265
+ await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
266
+ }
267
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Image detection and parsing utilities
3
+ */
4
+ import type { SelectedImage } from './types';
5
+ /**
6
+ * Extract image source from element (handles Next.js Image wrapper)
7
+ */
8
+ export declare function getImageSource(element: HTMLElement, isBackground: boolean): {
9
+ currentSrc: string;
10
+ actualImgElement: HTMLImageElement | null;
11
+ };
12
+ /**
13
+ * Parse image data from element to create SelectedImage
14
+ */
15
+ export declare function parseImageData(element: HTMLElement, id: string, currentBrightness?: number, currentBlur?: number): SelectedImage | null;
16
+ //# sourceMappingURL=imageDetection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageDetection.d.ts","sourceRoot":"","sources":["imageDetection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAI7C;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,GAAG;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC7C,CAmCA;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,WAAW,EACpB,EAAE,EAAE,MAAM,EACV,iBAAiB,CAAC,EAAE,MAAM,EAC1B,WAAW,CAAC,EAAE,MAAM,GACrB,aAAa,GAAG,IAAI,CAoGtB"}