@jhits/plugin-images 0.0.6 → 0.0.7

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 (170) hide show
  1. package/dist/api/fallback/route.d.ts +7 -0
  2. package/dist/api/fallback/route.d.ts.map +1 -0
  3. package/dist/api/fallback/route.js +65 -0
  4. package/dist/api/index.d.ts +9 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +8 -0
  7. package/dist/api/list/index.d.ts +21 -0
  8. package/dist/api/list/index.d.ts.map +1 -0
  9. package/dist/api/list/index.js +80 -0
  10. package/dist/api/resolve/route.d.ts +39 -0
  11. package/dist/api/resolve/route.d.ts.map +1 -0
  12. package/dist/api/resolve/route.js +213 -0
  13. package/dist/api/router.d.ts +14 -0
  14. package/dist/api/router.d.ts.map +1 -0
  15. package/dist/api/router.js +67 -0
  16. package/dist/api/upload/index.d.ts +20 -0
  17. package/dist/api/upload/index.d.ts.map +1 -0
  18. package/dist/api/upload/index.js +65 -0
  19. package/dist/api/uploads/[filename]/route.d.ts +21 -0
  20. package/dist/api/uploads/[filename]/route.d.ts.map +1 -0
  21. package/dist/api/uploads/[filename]/route.js +80 -0
  22. package/dist/api-server.d.ts +9 -0
  23. package/dist/api-server.d.ts.map +1 -0
  24. package/dist/api-server.js +9 -0
  25. package/dist/components/BackgroundImage.d.ts +11 -0
  26. package/dist/components/BackgroundImage.d.ts.map +1 -0
  27. package/dist/components/BackgroundImage.js +33 -0
  28. package/dist/components/GlobalImageEditor/config.d.ts +9 -0
  29. package/dist/components/GlobalImageEditor/config.d.ts.map +1 -0
  30. package/dist/components/GlobalImageEditor/config.js +17 -0
  31. package/dist/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
  32. package/dist/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
  33. package/dist/components/GlobalImageEditor/eventHandlers.js +210 -0
  34. package/dist/components/GlobalImageEditor/imageDetection.d.ts +16 -0
  35. package/dist/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
  36. package/dist/components/GlobalImageEditor/imageDetection.js +135 -0
  37. package/dist/components/GlobalImageEditor/imageSetup.d.ts +9 -0
  38. package/dist/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
  39. package/dist/components/GlobalImageEditor/imageSetup.js +260 -0
  40. package/dist/components/GlobalImageEditor/saveLogic.d.ts +26 -0
  41. package/dist/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
  42. package/dist/components/GlobalImageEditor/saveLogic.js +98 -0
  43. package/dist/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
  44. package/dist/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
  45. package/dist/components/GlobalImageEditor/stylingDetection.js +110 -0
  46. package/dist/components/GlobalImageEditor/transformParsing.d.ts +16 -0
  47. package/dist/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
  48. package/dist/components/GlobalImageEditor/transformParsing.js +68 -0
  49. package/dist/components/GlobalImageEditor/types.d.ts +36 -0
  50. package/dist/components/GlobalImageEditor/types.d.ts.map +1 -0
  51. package/dist/components/GlobalImageEditor/types.js +4 -0
  52. package/dist/components/GlobalImageEditor.d.ts +8 -0
  53. package/dist/components/GlobalImageEditor.d.ts.map +1 -0
  54. package/dist/components/GlobalImageEditor.js +232 -0
  55. package/dist/components/Image.d.ts +22 -0
  56. package/dist/components/Image.d.ts.map +1 -0
  57. package/dist/components/Image.js +227 -0
  58. package/dist/components/ImageBrowserModal.d.ts +13 -0
  59. package/dist/components/ImageBrowserModal.d.ts.map +1 -0
  60. package/dist/components/ImageBrowserModal.js +507 -0
  61. package/dist/components/ImageEditor.d.ts +27 -0
  62. package/dist/components/ImageEditor.d.ts.map +1 -0
  63. package/dist/components/ImageEditor.js +172 -0
  64. package/dist/components/ImageEffectsPanel.d.ts +10 -0
  65. package/dist/components/ImageEffectsPanel.d.ts.map +1 -0
  66. package/dist/components/ImageEffectsPanel.js +11 -0
  67. package/dist/components/ImagePicker.d.ts +3 -0
  68. package/dist/components/ImagePicker.d.ts.map +1 -0
  69. package/dist/components/ImagePicker.js +142 -0
  70. package/dist/components/ImagesPluginInit.d.ts +24 -0
  71. package/dist/components/ImagesPluginInit.d.ts.map +1 -0
  72. package/dist/components/ImagesPluginInit.js +28 -0
  73. package/dist/components/index.d.ts +9 -0
  74. package/dist/components/index.d.ts.map +1 -0
  75. package/dist/components/index.js +7 -0
  76. package/dist/config.d.ts +14 -0
  77. package/dist/config.d.ts.map +1 -0
  78. package/dist/config.js +172 -0
  79. package/dist/hooks/useImagePicker.d.ts +20 -0
  80. package/dist/hooks/useImagePicker.d.ts.map +1 -0
  81. package/dist/hooks/useImagePicker.js +320 -0
  82. package/dist/index.d.ts +23 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +28 -0
  85. package/dist/index.server.d.ts +11 -0
  86. package/dist/index.server.d.ts.map +1 -0
  87. package/dist/index.server.js +10 -0
  88. package/dist/init.d.ts +33 -0
  89. package/dist/init.d.ts.map +1 -0
  90. package/dist/init.js +43 -0
  91. package/dist/types/index.d.ts +80 -0
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +4 -0
  94. package/dist/utils/fallback.d.ts +27 -0
  95. package/dist/utils/fallback.d.ts.map +1 -0
  96. package/dist/utils/fallback.js +63 -0
  97. package/dist/utils/transforms.d.ts +26 -0
  98. package/dist/utils/transforms.d.ts.map +1 -0
  99. package/dist/utils/transforms.js +38 -0
  100. package/dist/views/ImageManager.d.ts +10 -0
  101. package/dist/views/ImageManager.d.ts.map +1 -0
  102. package/dist/views/ImageManager.js +9 -0
  103. package/package.json +8 -8
  104. package/src/assets/noimagefound.jpg +0 -0
  105. package/src/components/BackgroundImage.d.ts +11 -0
  106. package/src/components/BackgroundImage.d.ts.map +1 -0
  107. package/src/components/BackgroundImage.js +35 -0
  108. package/src/components/GlobalImageEditor/config.d.ts +9 -0
  109. package/src/components/GlobalImageEditor/config.d.ts.map +1 -0
  110. package/src/components/GlobalImageEditor/config.js +18 -0
  111. package/src/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
  112. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
  113. package/src/components/GlobalImageEditor/eventHandlers.js +206 -0
  114. package/src/components/GlobalImageEditor/imageDetection.d.ts +16 -0
  115. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
  116. package/src/components/GlobalImageEditor/imageDetection.js +130 -0
  117. package/src/components/GlobalImageEditor/imageSetup.d.ts +9 -0
  118. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
  119. package/src/components/GlobalImageEditor/imageSetup.js +261 -0
  120. package/src/components/GlobalImageEditor/saveLogic.d.ts +26 -0
  121. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
  122. package/src/components/GlobalImageEditor/saveLogic.js +99 -0
  123. package/src/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
  124. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
  125. package/src/components/GlobalImageEditor/stylingDetection.js +110 -0
  126. package/src/components/GlobalImageEditor/transformParsing.d.ts +16 -0
  127. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
  128. package/src/components/GlobalImageEditor/transformParsing.js +68 -0
  129. package/src/components/GlobalImageEditor/types.d.ts +36 -0
  130. package/src/components/GlobalImageEditor/types.d.ts.map +1 -0
  131. package/src/components/GlobalImageEditor/types.js +4 -0
  132. package/src/components/GlobalImageEditor.d.ts +8 -0
  133. package/src/components/GlobalImageEditor.d.ts.map +1 -0
  134. package/src/components/GlobalImageEditor.js +227 -0
  135. package/src/components/Image.d.ts +22 -0
  136. package/src/components/Image.d.ts.map +1 -0
  137. package/src/components/Image.js +229 -0
  138. package/src/components/ImageBrowserModal.d.ts +13 -0
  139. package/src/components/ImageBrowserModal.d.ts.map +1 -0
  140. package/src/components/ImageBrowserModal.js +504 -0
  141. package/src/components/ImageEditor.d.ts +27 -0
  142. package/src/components/ImageEditor.d.ts.map +1 -0
  143. package/src/components/ImageEditor.js +173 -0
  144. package/src/components/ImagePicker.d.ts +3 -0
  145. package/src/components/ImagePicker.d.ts.map +1 -0
  146. package/src/components/ImagePicker.js +143 -0
  147. package/src/components/ImagesPluginInit.d.ts +24 -0
  148. package/src/components/ImagesPluginInit.d.ts.map +1 -0
  149. package/src/components/ImagesPluginInit.js +28 -0
  150. package/src/hooks/useImagePicker.d.ts +20 -0
  151. package/src/hooks/useImagePicker.d.ts.map +1 -0
  152. package/src/hooks/useImagePicker.js +322 -0
  153. package/src/index.d.ts +23 -0
  154. package/src/index.d.ts.map +1 -0
  155. package/src/index.js +28 -0
  156. package/src/init.d.ts +33 -0
  157. package/src/init.d.ts.map +1 -0
  158. package/src/init.js +43 -0
  159. package/src/types/index.d.ts +80 -0
  160. package/src/types/index.d.ts.map +1 -0
  161. package/src/types/index.js +4 -0
  162. package/src/utils/fallback.d.ts +27 -0
  163. package/src/utils/fallback.d.ts.map +1 -0
  164. package/src/utils/fallback.js +63 -0
  165. package/src/utils/transforms.d.ts +26 -0
  166. package/src/utils/transforms.d.ts.map +1 -0
  167. package/src/utils/transforms.js +38 -0
  168. package/src/views/ImageManager.d.ts +10 -0
  169. package/src/views/ImageManager.d.ts.map +1 -0
  170. package/src/views/ImageManager.js +9 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Global Image Editor Component
3
+ * Allows clicking on any image in the client app to edit it (admin/dev only)
4
+ *
5
+ * Reads configuration from window.__JHITS_PLUGIN_PROPS__['plugin-images']
6
+ */
7
+ 'use client';
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
10
+ import { X } from 'lucide-react';
11
+ import { ImagePicker } from './ImagePicker';
12
+ import { getPluginConfig } from './GlobalImageEditor/config';
13
+ import { parseImageData } from './GlobalImageEditor/imageDetection';
14
+ import { setupImageHandlers } from './GlobalImageEditor/imageSetup';
15
+ import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './GlobalImageEditor/saveLogic';
16
+ import { handleImageChange, handleBrightnessChange, handleBlurChange } from './GlobalImageEditor/eventHandlers';
17
+ export function GlobalImageEditor() {
18
+ // Configuration
19
+ const config = useMemo(() => getPluginConfig(), []);
20
+ // State
21
+ const [selectedImage, setSelectedImage] = useState(null);
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const [userRole, setUserRole] = useState(null);
24
+ const [isLoading, setIsLoading] = useState(true);
25
+ // Refs for save debouncing
26
+ const saveTransformTimeoutRef = useRef(null);
27
+ const pendingTransformRef = useRef(null);
28
+ // Check if user is admin/dev
29
+ useEffect(() => {
30
+ const checkUser = async () => {
31
+ try {
32
+ const res = await fetch('/api/me');
33
+ const data = await res.json();
34
+ if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
35
+ setUserRole(data.user.role);
36
+ }
37
+ }
38
+ catch (error) {
39
+ console.error('Failed to check user role:', error);
40
+ }
41
+ finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+ checkUser();
46
+ }, []);
47
+ // Listen for open-image-editor custom event from Image/BackgroundImage components
48
+ useEffect(() => {
49
+ if (!userRole)
50
+ return;
51
+ const handleOpenEditor = async (e) => {
52
+ const { id, currentBrightness, currentBlur } = e.detail || {};
53
+ if (!id)
54
+ return;
55
+ // Find the element by data-image-id or data-background-image-id
56
+ let element = document.querySelector(`[data-image-id="${id}"], [data-background-image-id="${id}"]`);
57
+ if (!element) {
58
+ element = document.querySelector(`[data-background-image-component="true"][data-background-image-id="${id}"]`);
59
+ }
60
+ if (!element) {
61
+ console.error('[GlobalImageEditor] Element not found for id:', id);
62
+ return;
63
+ }
64
+ // Extract semantic ID
65
+ const semanticId = element.getAttribute('data-image-id') ||
66
+ element.getAttribute('data-background-image-id') ||
67
+ id;
68
+ // Parse image data
69
+ const imageData = parseImageData(element, semanticId, currentBrightness, currentBlur);
70
+ if (!imageData) {
71
+ console.error('[GlobalImageEditor] Failed to parse image data');
72
+ return;
73
+ }
74
+ // Store semantic ID on element if not already set
75
+ if (imageData.isBackground && !element.hasAttribute('data-background-image-id')) {
76
+ element.setAttribute('data-background-image-id', semanticId);
77
+ }
78
+ else if (!imageData.isBackground && !element.hasAttribute('data-image-id')) {
79
+ element.setAttribute('data-image-id', semanticId);
80
+ }
81
+ setSelectedImage(imageData);
82
+ setIsOpen(true);
83
+ };
84
+ window.addEventListener('open-image-editor', handleOpenEditor);
85
+ return () => {
86
+ window.removeEventListener('open-image-editor', handleOpenEditor);
87
+ };
88
+ }, [userRole]);
89
+ // Add click handlers to all images
90
+ useEffect(() => {
91
+ if (isLoading || !userRole) {
92
+ return;
93
+ }
94
+ const cleanup = setupImageHandlers((imageData) => {
95
+ setSelectedImage(imageData);
96
+ setIsOpen(true);
97
+ });
98
+ // Setup immediately and also after a short delay to catch dynamically loaded images
99
+ const timeoutId = setTimeout(() => {
100
+ const cleanup2 = setupImageHandlers((imageData) => {
101
+ setSelectedImage(imageData);
102
+ setIsOpen(true);
103
+ });
104
+ // Note: This creates a new cleanup, but the first one will handle the initial setup
105
+ }, 500);
106
+ return () => {
107
+ clearTimeout(timeoutId);
108
+ cleanup();
109
+ };
110
+ }, [isLoading, userRole]);
111
+ // Save image transform (scale/position) to API with debouncing
112
+ const saveImageTransformHandler = useCallback(async (scale, positionX, positionY, immediate = false, brightnessOverride, blurOverride) => {
113
+ if (!selectedImage)
114
+ return;
115
+ const { element } = selectedImage;
116
+ const semanticId = element.getAttribute('data-image-id') ||
117
+ element.getAttribute('data-background-image-id');
118
+ if (!semanticId)
119
+ return;
120
+ const filename = await getFilename(semanticId, selectedImage);
121
+ if (!filename)
122
+ return;
123
+ // Store pending transform
124
+ pendingTransformRef.current = {
125
+ scale,
126
+ positionX,
127
+ positionY,
128
+ semanticId,
129
+ filename
130
+ };
131
+ // Clear existing timeout
132
+ if (saveTransformTimeoutRef.current) {
133
+ clearTimeout(saveTransformTimeoutRef.current);
134
+ saveTransformTimeoutRef.current = null;
135
+ }
136
+ // If immediate save is requested (e.g., when "Done" is clicked), save right away
137
+ if (immediate) {
138
+ const normalizedPositionX = normalizePosition(positionX);
139
+ const normalizedPositionY = normalizePosition(positionY);
140
+ // Use override values if provided, otherwise use selectedImage values
141
+ // IMPORTANT: Overrides contain the latest values from the editor
142
+ const finalBrightness = brightnessOverride !== undefined ? brightnessOverride : (selectedImage?.brightness ?? 100);
143
+ const finalBlur = blurOverride !== undefined ? blurOverride : (selectedImage?.blur ?? 0);
144
+ await saveTransformToAPI(semanticId, filename, scale, normalizedPositionX, normalizedPositionY, finalBrightness, finalBlur);
145
+ pendingTransformRef.current = null;
146
+ return;
147
+ }
148
+ // Debounce: save after 300ms of no changes
149
+ saveTransformTimeoutRef.current = setTimeout(async () => {
150
+ const pending = pendingTransformRef.current;
151
+ if (pending && selectedImage) {
152
+ await flushPendingSave(pending, selectedImage);
153
+ }
154
+ }, 300);
155
+ }, [selectedImage]);
156
+ // Cleanup timeout on unmount
157
+ useEffect(() => {
158
+ return () => {
159
+ if (saveTransformTimeoutRef.current) {
160
+ clearTimeout(saveTransformTimeoutRef.current);
161
+ }
162
+ };
163
+ }, []);
164
+ // Event handlers
165
+ const handleImageChangeWrapper = useCallback(async (image) => {
166
+ if (!selectedImage)
167
+ return;
168
+ await handleImageChange(image, selectedImage, handleClose);
169
+ }, [selectedImage]);
170
+ const handleBrightnessChangeWrapper = useCallback(async (brightness) => {
171
+ if (!selectedImage)
172
+ return;
173
+ // Don't save immediately when in editor - will be saved when "Done" is pressed
174
+ await handleBrightnessChange(brightness, selectedImage, setSelectedImage, false);
175
+ }, [selectedImage]);
176
+ const handleBlurChangeWrapper = useCallback(async (blur) => {
177
+ if (!selectedImage)
178
+ return;
179
+ // Don't save immediately when in editor - will be saved when "Done" is pressed
180
+ await handleBlurChange(blur, selectedImage, setSelectedImage, false);
181
+ }, [selectedImage]);
182
+ const handleClose = useCallback(() => {
183
+ setIsOpen(false);
184
+ setSelectedImage(null);
185
+ }, []);
186
+ // Don't render if disabled or user is not admin/dev
187
+ if (!config.enabled || isLoading || !userRole) {
188
+ return null;
189
+ }
190
+ if (!isOpen || !selectedImage) {
191
+ return null;
192
+ }
193
+ return (_jsx("div", { className: `fixed inset-0 z-50 flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`, "data-image-editor": "true", onClick: (e) => {
194
+ if (e.target === e.currentTarget) {
195
+ handleClose();
196
+ }
197
+ }, children: _jsxs("div", { className: `relative bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] flex flex-col ${config.className || ''}`, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "flex items-center justify-between p-6 border-b dark:border-neutral-800", children: [_jsx("h2", { className: "text-2xl font-bold dark:text-white", children: "Edit Image" }), _jsx("button", { onClick: handleClose, className: "p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors", "aria-label": "Close editor", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsx("div", { className: "p-6", children: _jsx(ImagePicker, { value: selectedImage.element.getAttribute('data-image-id') || selectedImage.element.getAttribute('data-background-image-id') || selectedImage.originalSrc, onChange: handleImageChangeWrapper, brightness: selectedImage.brightness, blur: selectedImage.blur, scale: selectedImage.scale, positionX: selectedImage.positionX, positionY: selectedImage.positionY, onBrightnessChange: handleBrightnessChangeWrapper, onBlurChange: handleBlurChangeWrapper, onScaleChange: async (newScale) => {
198
+ if (selectedImage) {
199
+ const updated = { ...selectedImage, scale: newScale };
200
+ setSelectedImage(updated);
201
+ await saveImageTransformHandler(newScale, updated.positionX, updated.positionY);
202
+ }
203
+ }, onPositionXChange: async (newPositionX) => {
204
+ if (selectedImage) {
205
+ const updated = { ...selectedImage, positionX: newPositionX };
206
+ setSelectedImage(updated);
207
+ await saveImageTransformHandler(updated.scale, newPositionX, updated.positionY);
208
+ }
209
+ }, onPositionYChange: async (newPositionY) => {
210
+ if (selectedImage) {
211
+ const updated = { ...selectedImage, positionY: newPositionY };
212
+ setSelectedImage(updated);
213
+ await saveImageTransformHandler(updated.scale, updated.positionX, newPositionY);
214
+ }
215
+ }, onEditorSave: async (finalScale, finalPositionX, finalPositionY, finalBrightness, finalBlur) => {
216
+ if (selectedImage) {
217
+ // Update selectedImage with final values
218
+ // Use provided brightness/blur if available, otherwise use current values
219
+ const updated = {
220
+ ...selectedImage,
221
+ scale: finalScale,
222
+ positionX: finalPositionX,
223
+ positionY: finalPositionY,
224
+ brightness: finalBrightness !== undefined ? finalBrightness : selectedImage.brightness,
225
+ blur: finalBlur !== undefined ? finalBlur : selectedImage.blur
226
+ };
227
+ setSelectedImage(updated);
228
+ // Save with the final brightness and blur values
229
+ await saveImageTransformHandler(finalScale, finalPositionX, finalPositionY, true, updated.brightness, updated.blur);
230
+ }
231
+ }, showEffects: true, darkMode: false, aspectRatio: selectedImage.aspectRatio, borderRadius: selectedImage.borderRadius, objectFit: selectedImage.objectFit, objectPosition: selectedImage.objectPosition }) })] }) }));
232
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ export interface PluginImageProps {
3
+ id: string;
4
+ alt: string;
5
+ width?: number;
6
+ height?: number;
7
+ className?: string;
8
+ fill?: boolean;
9
+ sizes?: string;
10
+ priority?: boolean;
11
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
12
+ objectPosition?: string;
13
+ style?: React.CSSProperties;
14
+ editable?: boolean;
15
+ scale?: number;
16
+ positionX?: number;
17
+ positionY?: number;
18
+ brightness?: number;
19
+ blur?: number;
20
+ }
21
+ export declare function Image({ id, alt, width, height, className, fill, sizes, priority, objectFit, objectPosition, style, editable, ...props }: PluginImageProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=Image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAKjF,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,KAAK,CAAC,EAClB,EAAE,EACF,GAAG,EACH,KAAK,EACL,MAAM,EACN,SAAc,EACd,IAAY,EACZ,KAAK,EACL,QAAgB,EAChB,SAAmB,EACnB,cAAyB,EACzB,KAAK,EACL,QAAe,EACf,GAAG,KAAK,EACX,EAAE,gBAAgB,2CA2SlB"}
@@ -0,0 +1,227 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import NextImage from 'next/image';
4
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
5
+ import { Edit2, Loader2 } from 'lucide-react';
6
+ import { getImageTransform, getImageFilter } from '../utils/transforms';
7
+ import { getFallbackImageUrl } from '../utils/fallback';
8
+ export function Image({ id, alt, width, height, className = '', fill = false, sizes, priority = false, objectFit = 'cover', objectPosition = 'center', style, editable = true, ...props // Using rest for override props
9
+ }) {
10
+ // 1. State management (Local only, used if props are undefined)
11
+ const [apiData, setApiData] = useState({
12
+ filename: null,
13
+ brightness: 100,
14
+ blur: 0,
15
+ scale: 1.0,
16
+ positionX: 0,
17
+ positionY: 0,
18
+ });
19
+ const [isResolving, setIsResolving] = useState(true);
20
+ const [baseScale, setBaseScale] = useState(1);
21
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
22
+ const [imageError, setImageError] = useState(false);
23
+ const imageRef = useRef(null);
24
+ const containerRef = useRef(null);
25
+ // 2. Derive "Effective" values (Prop > Local State)
26
+ const effective = useMemo(() => ({
27
+ brightness: props.brightness ?? apiData.brightness,
28
+ blur: props.blur ?? apiData.blur,
29
+ scale: props.scale ?? apiData.scale,
30
+ positionX: props.positionX ?? apiData.positionX,
31
+ positionY: props.positionY ?? apiData.positionY,
32
+ filename: apiData.filename || id
33
+ }), [props, apiData, id]);
34
+ // 3. Optimized Image Resolution
35
+ const fetchImageMetadata = useCallback(async (targetId) => {
36
+ const isLikelyPath = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(targetId) || /^\d+-/.test(targetId);
37
+ if (isLikelyPath) {
38
+ setApiData(prev => ({ ...prev, filename: targetId }));
39
+ setIsResolving(false);
40
+ return;
41
+ }
42
+ try {
43
+ const res = await fetch(`/api/plugin-images/resolve?id=${encodeURIComponent(targetId)}`);
44
+ if (!res.ok)
45
+ throw new Error();
46
+ const data = await res.json();
47
+ const apiPositionX = data.positionX ?? 0;
48
+ const apiPositionY = data.positionY ?? 0;
49
+ const apiScale = data.scale ?? 1.0;
50
+ setApiData({
51
+ filename: data.filename || targetId,
52
+ brightness: data.brightness ?? 100,
53
+ blur: data.blur ?? 0,
54
+ scale: apiScale,
55
+ positionX: apiPositionX,
56
+ positionY: apiPositionY,
57
+ });
58
+ // Reset error state when resolution succeeds (in case it was set during pre-check)
59
+ setImageError(false);
60
+ }
61
+ catch {
62
+ setApiData(prev => ({ ...prev, filename: targetId }));
63
+ }
64
+ finally {
65
+ setIsResolving(false);
66
+ }
67
+ }, []);
68
+ // 4. Auth Check (Single mount only)
69
+ useEffect(() => {
70
+ if (!editable)
71
+ return;
72
+ fetch('/api/me')
73
+ .then(res => res.json())
74
+ .then(data => {
75
+ if (data.loggedIn && ['admin', 'dev'].includes(data.user?.role)) {
76
+ setIsAuthenticated(true);
77
+ }
78
+ })
79
+ .catch(() => setIsAuthenticated(false));
80
+ }, [editable]);
81
+ // 5. Stable Event Listener (Prevents closure staleness)
82
+ // Only fetch from API if props are not provided (parent controls the values)
83
+ const hasProps = props.scale !== undefined || props.positionX !== undefined || props.positionY !== undefined || props.brightness !== undefined || props.blur !== undefined;
84
+ useEffect(() => {
85
+ fetchImageMetadata(id);
86
+ // Only listen to events if props are not provided
87
+ if (hasProps)
88
+ return;
89
+ const handleUpdate = (e) => {
90
+ if (e.detail?.id === id) {
91
+ // Only update if the event contains new data
92
+ const eventData = e.detail;
93
+ if (eventData.scale !== undefined || eventData.positionX !== undefined || eventData.positionY !== undefined || eventData.brightness !== undefined || eventData.blur !== undefined) {
94
+ // Update local state directly from event to avoid unnecessary API call
95
+ setApiData(prev => ({
96
+ ...prev,
97
+ brightness: eventData.brightness ?? prev.brightness,
98
+ blur: eventData.blur ?? prev.blur,
99
+ scale: eventData.scale ?? prev.scale,
100
+ positionX: eventData.positionX ?? prev.positionX,
101
+ positionY: eventData.positionY ?? prev.positionY,
102
+ }));
103
+ }
104
+ }
105
+ };
106
+ window.addEventListener('image-mapping-updated', handleUpdate);
107
+ return () => window.removeEventListener('image-mapping-updated', handleUpdate);
108
+ }, [id, fetchImageMetadata, hasProps]);
109
+ // 6. Layout & Transforms
110
+ const calculateBaseScale = useCallback(() => {
111
+ if (!imageRef.current || !containerRef.current)
112
+ return;
113
+ const containerWidth = containerRef.current.offsetWidth;
114
+ const containerHeight = containerRef.current.offsetHeight;
115
+ // Ensure container has dimensions before calculating
116
+ if (containerWidth === 0 || containerHeight === 0) {
117
+ // Try to get dimensions from computed styles or parent
118
+ const parentWidth = containerRef.current.parentElement?.clientWidth;
119
+ const parentHeight = containerRef.current.parentElement?.clientHeight;
120
+ if (parentWidth && parentHeight && imageRef.current.naturalWidth > 0) {
121
+ const widthRatio = parentWidth / imageRef.current.naturalWidth;
122
+ const heightRatio = parentHeight / imageRef.current.naturalHeight;
123
+ const calculatedBaseScale = Math.max(widthRatio, heightRatio);
124
+ setBaseScale(calculatedBaseScale);
125
+ }
126
+ return;
127
+ }
128
+ if (imageRef.current.naturalWidth === 0 || imageRef.current.naturalHeight === 0)
129
+ return;
130
+ const widthRatio = containerWidth / imageRef.current.naturalWidth;
131
+ const heightRatio = containerHeight / imageRef.current.naturalHeight;
132
+ const calculatedBaseScale = Math.max(widthRatio, heightRatio);
133
+ setBaseScale(calculatedBaseScale);
134
+ }, []);
135
+ // Only construct image URL if we have a valid filename (not a semantic ID)
136
+ // Check if filename looks like an actual file (has extension or starts with timestamp)
137
+ const filename = effective.filename;
138
+ const hasFileExtension = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(filename);
139
+ const looksLikeTimestamp = /^\d+-/.test(filename);
140
+ const isValidFilename = hasFileExtension || looksLikeTimestamp;
141
+ // Don't construct URL until resolution is complete AND we have a valid filename
142
+ const shouldConstructUrl = !isResolving && isValidFilename;
143
+ const baseSrc = shouldConstructUrl ? `/api/uploads/${encodeURIComponent(filename)}` : null;
144
+ const src = imageError ? getFallbackImageUrl() : (baseSrc || getFallbackImageUrl());
145
+ const shouldFill = fill || className.includes('w-full') || className.includes('h-full');
146
+ const hasTransforms = effective.scale !== 1 || effective.positionX !== 0 || effective.positionY !== 0;
147
+ // Pre-check image URL to detect errors early (only for valid filenames after resolution)
148
+ useEffect(() => {
149
+ // Don't check if:
150
+ // 1. Already in error state
151
+ // 2. Still resolving
152
+ // 3. No valid base src
153
+ // 4. Base src is the fallback URL
154
+ if (imageError || isResolving || !baseSrc || baseSrc === getFallbackImageUrl() || !isValidFilename)
155
+ return;
156
+ const testImg = document.createElement('img');
157
+ const handleError = () => {
158
+ setImageError(true);
159
+ };
160
+ const handleLoad = () => {
161
+ setImageError(false);
162
+ };
163
+ testImg.onerror = handleError;
164
+ testImg.onload = handleLoad;
165
+ testImg.src = baseSrc;
166
+ return () => {
167
+ testImg.onerror = null;
168
+ testImg.onload = null;
169
+ };
170
+ }, [baseSrc, imageError, isResolving, isValidFilename]);
171
+ // Recalculate baseScale when container is resized
172
+ useEffect(() => {
173
+ if (!hasTransforms || !shouldFill || !containerRef.current)
174
+ return;
175
+ const resizeObserver = new ResizeObserver(() => {
176
+ calculateBaseScale();
177
+ });
178
+ resizeObserver.observe(containerRef.current);
179
+ return () => resizeObserver.disconnect();
180
+ }, [hasTransforms, shouldFill, calculateBaseScale]);
181
+ const transformStyle = hasTransforms && baseScale > 0 ? (() => {
182
+ return getImageTransform({
183
+ scale: effective.scale,
184
+ positionX: effective.positionX,
185
+ positionY: effective.positionY,
186
+ baseScale
187
+ }, true);
188
+ })() : undefined;
189
+ const filterStyle = getImageFilter(effective.brightness, effective.blur);
190
+ return (_jsxs("div", { ref: containerRef, className: `group/image relative overflow-hidden transition-all duration-300 ${className}`, style: {
191
+ ...style,
192
+ filter: filterStyle || style?.filter,
193
+ // Ensure container has explicit positioning context
194
+ position: 'relative',
195
+ }, "data-image-id": id, children: [isResolving && (_jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 animate-pulse", children: _jsx(Loader2, { className: "w-5 h-5 animate-spin text-neutral-400" }) })), hasTransforms && shouldFill ? (baseSrc ? (_jsx("img", { ref: imageRef, src: src, alt: alt, loading: priority ? 'eager' : 'lazy', onLoad: calculateBaseScale, onError: () => setImageError(true), className: "absolute max-w-none select-none", style: {
196
+ top: '50%',
197
+ left: '50%',
198
+ width: 'auto', // Allow image to maintain natural aspect ratio
199
+ height: 'auto', // Allow image to maintain natural aspect ratio
200
+ minWidth: '100%', // Ensure image covers container width
201
+ minHeight: '100%', // Ensure image covers container height
202
+ transform: baseScale > 0 && transformStyle
203
+ ? transformStyle
204
+ : 'translate(-50%, -50%)',
205
+ transformOrigin: 'center center',
206
+ position: 'absolute',
207
+ }, draggable: false })) : null) : imageError || !baseSrc ? (
208
+ // Fallback to regular img tag when error occurs or no valid filename yet
209
+ _jsx("img", { src: getFallbackImageUrl(), alt: alt, loading: priority ? 'eager' : 'lazy', className: `transition-all duration-500 ${editable ? 'group-hover/image:scale-105' : ''}`, style: {
210
+ objectFit,
211
+ objectPosition,
212
+ width: shouldFill ? '100%' : width,
213
+ height: shouldFill ? '100%' : height,
214
+ ...(transformStyle ? { transform: transformStyle } : {}),
215
+ ...style,
216
+ } })) : (_jsx(NextImage, { src: src, alt: alt, width: !shouldFill ? width : undefined, height: !shouldFill ? height : undefined, fill: shouldFill, sizes: sizes || (shouldFill ? "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" : undefined), priority: priority, loading: priority ? 'eager' : undefined, className: `transition-all duration-500 ${editable ? 'group-hover/image:scale-105' : ''}`, style: {
217
+ objectFit,
218
+ objectPosition,
219
+ ...(transformStyle ? { transform: transformStyle } : {}),
220
+ ...style,
221
+ } })), editable && isAuthenticated && (_jsx("button", { onClick: (e) => {
222
+ e.preventDefault();
223
+ window.dispatchEvent(new CustomEvent('open-image-editor', {
224
+ detail: { id, currentBrightness: effective.brightness, currentBlur: effective.blur }
225
+ }));
226
+ }, className: "absolute inset-0 z-30 flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-all duration-300 bg-neutral-900/40 backdrop-blur-[2px]", children: _jsxs("div", { className: "flex items-center gap-2 px-4 py-2 bg-white dark:bg-neutral-800 rounded-full shadow-xl", children: [_jsx(Edit2, { size: 14, className: "text-primary" }), _jsx("span", { className: "text-[10px] font-bold uppercase tracking-widest", children: "Edit" })] }) }))] }));
227
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import type { ImageMetadata } from '../types';
3
+ export interface ImageBrowserModalProps {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ onSelectImage: (image: ImageMetadata) => void;
7
+ selectedImageId?: string;
8
+ darkMode?: boolean;
9
+ onUpload?: (file: File) => Promise<void>;
10
+ uploading?: boolean;
11
+ }
12
+ export declare function ImageBrowserModal({ isOpen, onClose, onSelectImage, selectedImageId, darkMode, onUpload, uploading, }: ImageBrowserModalProps): React.ReactPortal | null;
13
+ //# sourceMappingURL=ImageBrowserModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageBrowserModal.d.ts","sourceRoot":"","sources":["../../src/components/ImageBrowserModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAGjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AA8P9C,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AAID,wBAAgB,iBAAiB,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,aAAa,EACb,eAAe,EACf,QAAgB,EAChB,QAAQ,EACR,SAAiB,GACpB,EAAE,sBAAsB,4BA6iBxB"}