@jhits/plugin-images 0.0.5 → 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 (175) 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 +26 -22
  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/GlobalImageEditor.tsx +2 -2
  136. package/src/components/Image.d.ts +22 -0
  137. package/src/components/Image.d.ts.map +1 -0
  138. package/src/components/Image.js +229 -0
  139. package/src/components/ImageBrowserModal.d.ts +13 -0
  140. package/src/components/ImageBrowserModal.d.ts.map +1 -0
  141. package/src/components/ImageBrowserModal.js +504 -0
  142. package/src/components/ImageBrowserModal.tsx +18 -5
  143. package/src/components/ImageEditor.d.ts +27 -0
  144. package/src/components/ImageEditor.d.ts.map +1 -0
  145. package/src/components/ImageEditor.js +173 -0
  146. package/src/components/ImagePicker.d.ts +3 -0
  147. package/src/components/ImagePicker.d.ts.map +1 -0
  148. package/src/components/ImagePicker.js +143 -0
  149. package/src/components/ImagePicker.tsx +53 -15
  150. package/src/components/ImagesPluginInit.d.ts +24 -0
  151. package/src/components/ImagesPluginInit.d.ts.map +1 -0
  152. package/src/components/ImagesPluginInit.js +28 -0
  153. package/src/hooks/useImagePicker.d.ts +20 -0
  154. package/src/hooks/useImagePicker.d.ts.map +1 -0
  155. package/src/hooks/useImagePicker.js +322 -0
  156. package/src/hooks/useImagePicker.ts +28 -6
  157. package/src/index.d.ts +23 -0
  158. package/src/index.d.ts.map +1 -0
  159. package/src/index.js +28 -0
  160. package/src/init.d.ts +33 -0
  161. package/src/init.d.ts.map +1 -0
  162. package/src/init.js +43 -0
  163. package/src/types/index.d.ts +80 -0
  164. package/src/types/index.d.ts.map +1 -0
  165. package/src/types/index.js +4 -0
  166. package/src/utils/fallback.d.ts +27 -0
  167. package/src/utils/fallback.d.ts.map +1 -0
  168. package/src/utils/fallback.js +63 -0
  169. package/src/utils/transforms.d.ts +26 -0
  170. package/src/utils/transforms.d.ts.map +1 -0
  171. package/src/utils/transforms.js +38 -0
  172. package/src/views/ImageManager.d.ts +10 -0
  173. package/src/views/ImageManager.d.ts.map +1 -0
  174. package/src/views/ImageManager.js +9 -0
  175. package/src/components/GlobalImageEditor/GlobalImageEditor.tsx +0 -374
@@ -0,0 +1,507 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Upload, Search, X, Image as ImageIcon, Check, Link as LinkIcon, Loader2, Grid3x3 } from 'lucide-react';
6
+ // Masonry Grid Component that maintains item positions
7
+ function MasonryGrid({ images, selectedImageId, onSelectImage, onClose, fixedColumns, }) {
8
+ const containerRef = useRef(null);
9
+ const [columnCount, setColumnCount] = useState(fixedColumns || 2);
10
+ // Calculate column count based on screen size (only if not fixed)
11
+ useEffect(() => {
12
+ if (fixedColumns) {
13
+ setColumnCount(fixedColumns);
14
+ return;
15
+ }
16
+ const updateColumnCount = () => {
17
+ const width = window.innerWidth;
18
+ let cols = 2;
19
+ if (width >= 1280)
20
+ cols = 6;
21
+ else if (width >= 1024)
22
+ cols = 5;
23
+ else if (width >= 768)
24
+ cols = 4;
25
+ else if (width >= 640)
26
+ cols = 3;
27
+ setColumnCount(cols);
28
+ };
29
+ updateColumnCount();
30
+ window.addEventListener('resize', updateColumnCount);
31
+ return () => window.removeEventListener('resize', updateColumnCount);
32
+ }, [fixedColumns]);
33
+ // Distribute images into columns using stable round-robin assignment
34
+ // This ensures items NEVER move once rendered - they always go to the same column
35
+ // based on their index, preventing reshuffling when new items are added
36
+ const columns = useMemo(() => {
37
+ const cols = Array.from({ length: columnCount }, () => []);
38
+ images.forEach((image, index) => {
39
+ // Round-robin: image at index N always goes to column (N % columnCount)
40
+ // This guarantees stable positions - items never move columns
41
+ const columnIndex = index % columnCount;
42
+ cols[columnIndex].push(image);
43
+ });
44
+ return cols;
45
+ }, [images, columnCount]);
46
+ return (_jsx("div", { ref: containerRef, className: "grid gap-4", style: {
47
+ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
48
+ }, children: columns.map((columnImages, columnIndex) => (_jsx("div", { className: "flex flex-col gap-4", children: columnImages.map((image, imageIndex) => {
49
+ // Normalize both selectedImageId and image identifiers for matching
50
+ let normalizedSelectedId = selectedImageId;
51
+ let normalizedImageUrl = image.url;
52
+ let normalizedImageId = image.id;
53
+ let normalizedImageFilename = image.filename;
54
+ // Extract filename from selectedImageId if it's a URL
55
+ if (selectedImageId) {
56
+ if (selectedImageId.includes('/')) {
57
+ const urlParts = selectedImageId.split('/');
58
+ normalizedSelectedId = urlParts[urlParts.length - 1]?.split('?')[0] || selectedImageId;
59
+ }
60
+ }
61
+ // Normalize image URL (remove protocol, domain, query params)
62
+ if (image.url) {
63
+ // Remove protocol and domain if present
64
+ normalizedImageUrl = image.url.replace(/^https?:\/\/[^\/]+/, '');
65
+ // Remove query params
66
+ normalizedImageUrl = normalizedImageUrl.split('?')[0];
67
+ // Extract just the filename
68
+ const urlParts = normalizedImageUrl.split('/');
69
+ normalizedImageUrl = urlParts[urlParts.length - 1] || normalizedImageUrl;
70
+ }
71
+ // Check if image is selected by comparing all possible combinations
72
+ const isSelected = Boolean(selectedImageId && (
73
+ // Direct matches
74
+ selectedImageId === image.id ||
75
+ selectedImageId === image.url ||
76
+ selectedImageId === image.filename ||
77
+ // Normalized filename matches
78
+ normalizedSelectedId === image.id ||
79
+ normalizedSelectedId === image.filename ||
80
+ normalizedSelectedId === normalizedImageUrl ||
81
+ normalizedSelectedId === normalizedImageId ||
82
+ normalizedSelectedId === normalizedImageFilename ||
83
+ // URL contains/ends with matches
84
+ (image.url && selectedImageId && (image.url.includes(selectedImageId) ||
85
+ image.url.endsWith(selectedImageId) ||
86
+ selectedImageId.includes(image.url) ||
87
+ selectedImageId.endsWith(image.url) ||
88
+ (normalizedSelectedId && (image.url.includes(normalizedSelectedId) ||
89
+ image.url.endsWith(normalizedSelectedId))))) ||
90
+ // Filename matches
91
+ (image.filename && (image.filename === normalizedSelectedId ||
92
+ normalizedSelectedId === image.filename)) ||
93
+ // ID matches
94
+ (image.id && (image.id === normalizedSelectedId ||
95
+ normalizedSelectedId === image.id))));
96
+ // Use a unique key combining column index, image index, and image id
97
+ // This ensures uniqueness even if image.id is duplicated
98
+ const uniqueKey = `${columnIndex}-${imageIndex}-${image.id}-${image.url}`;
99
+ return (_jsx(ImageCard, { image: image, isSelected: isSelected, onSelect: () => {
100
+ onSelectImage(image);
101
+ onClose();
102
+ } }, uniqueKey));
103
+ }) }, columnIndex))) }));
104
+ }
105
+ // Image Card Component for Masonry Layout
106
+ function ImageCard({ image, isSelected, onSelect }) {
107
+ const [imageLoaded, setImageLoaded] = useState(false);
108
+ const [imageError, setImageError] = useState(false);
109
+ const imgRef = useRef(null);
110
+ useEffect(() => {
111
+ // Check if image is already loaded (cached)
112
+ const img = imgRef.current;
113
+ if (img) {
114
+ if (img.complete && img.naturalWidth > 0) {
115
+ setImageLoaded(true);
116
+ }
117
+ else {
118
+ // Fallback: if image doesn't load within 5 seconds, show it anyway
119
+ const timeout = setTimeout(() => {
120
+ setImageLoaded(true);
121
+ }, 5000);
122
+ return () => clearTimeout(timeout);
123
+ }
124
+ }
125
+ }, [image.url]);
126
+ const handleLoad = () => {
127
+ setImageLoaded(true);
128
+ setImageError(false);
129
+ };
130
+ const handleError = (e) => {
131
+ console.error('[ImageCard] Image failed to load:', image.url, e);
132
+ setImageError(true);
133
+ setImageLoaded(false);
134
+ };
135
+ return (_jsxs("button", { onClick: onSelect, className: `group relative w-full rounded-xl overflow-hidden bg-neutral-100 dark:bg-neutral-800 transition-all duration-200 active:scale-[0.98] shadow-sm hover:shadow-lg block ${isSelected ? 'ring-2 ring-primary ring-offset-1 ring-offset-white dark:ring-offset-neutral-900' : ''}`, children: [!imageError ? (_jsxs("div", { className: "relative w-full group/image", children: [_jsx("img", { ref: imgRef, src: image.url, alt: image.filename, onLoad: handleLoad, onError: handleError, className: `w-full h-auto object-cover transition-all duration-300 group-hover/image:scale-105 block ${!imageLoaded ? 'opacity-0' : 'opacity-100'}`, loading: "lazy" }), !imageLoaded && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-neutral-200 dark:bg-neutral-800 z-10", children: _jsx(Loader2, { size: 20, className: "text-neutral-400 animate-spin" }) }))] })) : (_jsxs("div", { className: "w-full aspect-square flex flex-col items-center justify-center bg-neutral-200 dark:bg-neutral-800 text-neutral-400", children: [_jsx(ImageIcon, { size: 24, className: "mb-2" }), _jsx("p", { className: "text-xs font-medium", children: "Failed to load" })] })), isSelected && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute inset-0 border-2 border-primary rounded-xl pointer-events-none z-30 ring-2 ring-primary/20" }), _jsxs("div", { className: "absolute top-2 right-2 bg-primary text-white rounded-md px-2 py-1 shadow-md z-30 flex items-center gap-1.5 backdrop-blur-sm", children: [_jsx(Check, { size: 13, strokeWidth: 2.5, className: "flex-shrink-0" }), _jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wide", children: "Selected" })] }), _jsx("div", { className: "absolute inset-0 bg-primary/8 dark:bg-primary/12 rounded-xl pointer-events-none z-20" })] })), _jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-200 rounded-xl pointer-events-none" }), _jsx("div", { className: "absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/70 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-b-xl pointer-events-none", children: _jsx("p", { className: "text-[10px] text-white font-semibold truncate leading-tight", children: image.filename }) })] }));
136
+ }
137
+ const ITEMS_PER_PAGE = 100; // Increased from 24 to load more images per request
138
+ export function ImageBrowserModal({ isOpen, onClose, onSelectImage, selectedImageId, darkMode = false, onUpload, uploading = false, }) {
139
+ const [searchQuery, setSearchQuery] = useState('');
140
+ const [externalUrl, setExternalUrl] = useState('');
141
+ const [images, setImages] = useState([]);
142
+ const [loading, setLoading] = useState(false);
143
+ const [loadingMore, setLoadingMore] = useState(false);
144
+ const [page, setPage] = useState(1);
145
+ const [hasMore, setHasMore] = useState(true);
146
+ const [total, setTotal] = useState(0);
147
+ const [resolvedSelectedId, setResolvedSelectedId] = useState(selectedImageId);
148
+ const [internalUploading, setInternalUploading] = useState(false);
149
+ const [mounted, setMounted] = useState(false);
150
+ const [portalTarget, setPortalTarget] = useState(null);
151
+ // Handle SSR & portal target - ensure we only render portal on client
152
+ useEffect(() => {
153
+ setMounted(true);
154
+ if (typeof document === 'undefined')
155
+ return;
156
+ // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
157
+ // This ensures the browser modal shares the same stacking context as the global editor
158
+ const editorContainer = document.querySelector('[data-image-editor="true"]');
159
+ if (editorContainer) {
160
+ setPortalTarget(editorContainer);
161
+ }
162
+ else {
163
+ setPortalTarget(document.body);
164
+ }
165
+ }, []);
166
+ // Debug: Log when selectedImageId prop changes
167
+ useEffect(() => {
168
+ setResolvedSelectedId(selectedImageId);
169
+ }, [selectedImageId]);
170
+ // Load column count from localStorage, default to 3
171
+ const getStoredColumnCount = () => {
172
+ if (typeof window === 'undefined')
173
+ return 3;
174
+ try {
175
+ const stored = localStorage.getItem('plugin-images-column-count');
176
+ if (stored) {
177
+ const parsed = parseInt(stored, 10);
178
+ // Validate: must be between 3 and 9
179
+ if (!isNaN(parsed) && parsed >= 3 && parsed <= 9) {
180
+ return parsed;
181
+ }
182
+ }
183
+ }
184
+ catch (error) {
185
+ // Silently fail - use default
186
+ }
187
+ return 3;
188
+ };
189
+ const [columnCount, setColumnCount] = useState(getStoredColumnCount);
190
+ // Save column count to localStorage whenever it changes
191
+ useEffect(() => {
192
+ if (typeof window === 'undefined')
193
+ return;
194
+ try {
195
+ localStorage.setItem('plugin-images-column-count', columnCount.toString());
196
+ }
197
+ catch (error) {
198
+ // Silently fail
199
+ }
200
+ }, [columnCount]);
201
+ const modalRef = useRef(null);
202
+ const scrollContainerRef = useRef(null);
203
+ const fileInputRef = useRef(null);
204
+ const searchTimeoutRef = useRef(null);
205
+ // Load images from API with pagination
206
+ const loadImages = useCallback(async (pageNum = 1, append = false) => {
207
+ if (append) {
208
+ setLoadingMore(true);
209
+ }
210
+ else {
211
+ setLoading(true);
212
+ }
213
+ try {
214
+ const searchParam = searchQuery.trim() ? `&search=${encodeURIComponent(searchQuery.trim())}` : '';
215
+ const response = await fetch(`/api/plugin-images/list?page=${pageNum}&limit=${ITEMS_PER_PAGE}${searchParam}`);
216
+ if (response.ok) {
217
+ const data = await response.json();
218
+ const newImages = (data.images || []).map((img) => {
219
+ const imageUrl = img.url || `/api/uploads/${img.filename}`;
220
+ // Ensure URL is absolute if it's a relative path
221
+ const finalUrl = imageUrl.startsWith('http') ? imageUrl : imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
222
+ return {
223
+ ...img,
224
+ url: finalUrl
225
+ };
226
+ });
227
+ setTotal(data.total || 0);
228
+ if (append) {
229
+ setImages(prev => {
230
+ // Deduplicate: use a Map to track unique images by id
231
+ const imageMap = new Map();
232
+ // Add existing images
233
+ prev.forEach((img) => {
234
+ imageMap.set(img.id, img);
235
+ });
236
+ // Add new images (will overwrite duplicates)
237
+ newImages.forEach((img) => {
238
+ imageMap.set(img.id, img);
239
+ });
240
+ const updated = Array.from(imageMap.values());
241
+ // More images available if: we got a full page AND haven't loaded all images
242
+ setHasMore(newImages.length === ITEMS_PER_PAGE && updated.length < (data.total || 0));
243
+ return updated;
244
+ });
245
+ }
246
+ else {
247
+ // Deduplicate even for initial load
248
+ const imageMap = new Map();
249
+ newImages.forEach((img) => {
250
+ imageMap.set(img.id, img);
251
+ });
252
+ const deduplicated = Array.from(imageMap.values());
253
+ setImages(deduplicated);
254
+ // More images available if: we got a full page AND haven't loaded all images
255
+ setHasMore(deduplicated.length === ITEMS_PER_PAGE && deduplicated.length < (data.total || 0));
256
+ }
257
+ setPage(pageNum);
258
+ }
259
+ else {
260
+ console.error('[ImageBrowserModal] Failed to load images:', response.status);
261
+ }
262
+ }
263
+ catch (error) {
264
+ console.error('[ImageBrowserModal] Failed to load images:', error);
265
+ }
266
+ finally {
267
+ setLoading(false);
268
+ setLoadingMore(false);
269
+ }
270
+ }, [searchQuery]);
271
+ // Load multiple pages in parallel for faster initial load
272
+ const loadInitialImages = useCallback(async () => {
273
+ setLoading(true);
274
+ try {
275
+ const searchParam = searchQuery.trim() ? `&search=${encodeURIComponent(searchQuery.trim())}` : '';
276
+ // Load first 2-3 pages in parallel to fill the screen quickly
277
+ const pagesToLoad = 3;
278
+ const requests = Array.from({ length: pagesToLoad }, (_, i) => fetch(`/api/plugin-images/list?page=${i + 1}&limit=${ITEMS_PER_PAGE}${searchParam}`));
279
+ const responses = await Promise.all(requests);
280
+ const results = await Promise.all(responses.map(r => r.ok ? r.json() : { images: [], total: 0 }));
281
+ // Combine all images
282
+ const allImages = [];
283
+ let totalCount = 0;
284
+ results.forEach((data, index) => {
285
+ if (data.images && data.images.length > 0) {
286
+ const pageImages = data.images.map((img) => {
287
+ const imageUrl = img.url || `/api/uploads/${img.filename}`;
288
+ const finalUrl = imageUrl.startsWith('http') ? imageUrl : imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
289
+ return {
290
+ ...img,
291
+ url: finalUrl
292
+ };
293
+ });
294
+ allImages.push(...pageImages);
295
+ }
296
+ if (index === 0 && data.total) {
297
+ totalCount = data.total;
298
+ }
299
+ });
300
+ // Deduplicate
301
+ const imageMap = new Map();
302
+ allImages.forEach((img) => {
303
+ imageMap.set(img.id, img);
304
+ });
305
+ const deduplicated = Array.from(imageMap.values());
306
+ setImages(deduplicated);
307
+ setTotal(totalCount);
308
+ setHasMore(deduplicated.length < totalCount);
309
+ setPage(pagesToLoad);
310
+ }
311
+ catch (error) {
312
+ console.error('[ImageBrowserModal] Error loading initial images:', error);
313
+ // Fallback to single page load
314
+ loadImages(1, false);
315
+ }
316
+ finally {
317
+ setLoading(false);
318
+ }
319
+ }, [searchQuery, loadImages]);
320
+ // Load more images when scrolling near bottom
321
+ const handleScroll = useCallback(() => {
322
+ const container = scrollContainerRef.current;
323
+ if (!container || loadingMore || !hasMore || loading)
324
+ return;
325
+ const scrollTop = container.scrollTop;
326
+ const scrollHeight = container.scrollHeight;
327
+ const clientHeight = container.clientHeight;
328
+ // Calculate distance from bottom
329
+ const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
330
+ // Load more when within 200px of bottom
331
+ if (distanceFromBottom < 200) {
332
+ loadImages(page + 1, true);
333
+ }
334
+ }, [page, hasMore, loadingMore, loading, loadImages]);
335
+ // Check if viewport needs more images to fill the screen
336
+ const checkAndLoadMoreIfNeeded = useCallback(() => {
337
+ const container = scrollContainerRef.current;
338
+ if (!container || loading || loadingMore || !hasMore)
339
+ return;
340
+ // Check if content fills the viewport (with some margin)
341
+ const scrollHeight = container.scrollHeight;
342
+ const clientHeight = container.clientHeight;
343
+ const needsMoreContent = scrollHeight <= clientHeight + 100; // 100px margin
344
+ if (needsMoreContent) {
345
+ loadImages(page + 1, true);
346
+ }
347
+ }, [loading, loadingMore, hasMore, images.length, page, loadImages]);
348
+ // Auto-load more images when images change and viewport isn't filled
349
+ useEffect(() => {
350
+ if (!isOpen || images.length === 0 || loading || loadingMore)
351
+ return;
352
+ // Wait for DOM to update after images are set and rendered
353
+ const timeoutId = setTimeout(() => {
354
+ checkAndLoadMoreIfNeeded();
355
+ }, 200); // Delay to allow images to render and layout to settle
356
+ return () => clearTimeout(timeoutId);
357
+ }, [images, isOpen, loading, loadingMore, checkAndLoadMoreIfNeeded]);
358
+ // Check again after loading completes
359
+ useEffect(() => {
360
+ if (!isOpen || images.length === 0)
361
+ return;
362
+ if (!loading && !loadingMore) {
363
+ // Loading just finished, check if we need more
364
+ const timeoutId = setTimeout(() => {
365
+ checkAndLoadMoreIfNeeded();
366
+ }, 300); // Give images time to render
367
+ return () => clearTimeout(timeoutId);
368
+ }
369
+ }, [loading, loadingMore, isOpen, images.length, checkAndLoadMoreIfNeeded]);
370
+ // Also check when column count changes (more columns = need more images)
371
+ useEffect(() => {
372
+ if (!isOpen || images.length === 0 || loading || loadingMore)
373
+ return;
374
+ const timeoutId = setTimeout(() => {
375
+ checkAndLoadMoreIfNeeded();
376
+ }, 200);
377
+ return () => clearTimeout(timeoutId);
378
+ }, [columnCount, isOpen, loading, loadingMore, checkAndLoadMoreIfNeeded]);
379
+ // Initial load and reset on open
380
+ useEffect(() => {
381
+ if (isOpen) {
382
+ document.body.style.overflow = 'hidden';
383
+ setPage(1);
384
+ setImages([]);
385
+ setHasMore(true);
386
+ // Use parallel loading for faster initial load
387
+ loadInitialImages();
388
+ }
389
+ else {
390
+ document.body.style.overflow = 'unset';
391
+ setSearchQuery('');
392
+ setImages([]);
393
+ setPage(1);
394
+ }
395
+ return () => { document.body.style.overflow = 'unset'; };
396
+ }, [isOpen, loadInitialImages, selectedImageId]);
397
+ // Debounced search
398
+ useEffect(() => {
399
+ if (!isOpen)
400
+ return;
401
+ if (searchTimeoutRef.current) {
402
+ clearTimeout(searchTimeoutRef.current);
403
+ }
404
+ searchTimeoutRef.current = setTimeout(() => {
405
+ setPage(1);
406
+ setImages([]);
407
+ setHasMore(true);
408
+ // For search, use parallel loading if no search query (faster), otherwise single page
409
+ if (!searchQuery.trim()) {
410
+ loadInitialImages();
411
+ }
412
+ else {
413
+ loadImages(1, false);
414
+ }
415
+ }, 300);
416
+ return () => {
417
+ if (searchTimeoutRef.current) {
418
+ clearTimeout(searchTimeoutRef.current);
419
+ }
420
+ };
421
+ }, [searchQuery, isOpen, loadImages, loadInitialImages]);
422
+ // Reload after upload
423
+ useEffect(() => {
424
+ if (!uploading && isOpen && images.length > 0) {
425
+ // Reload first page to show newly uploaded image
426
+ loadImages(1, false);
427
+ }
428
+ }, [uploading, isOpen, loadImages]);
429
+ // Attach scroll listener
430
+ useEffect(() => {
431
+ const container = scrollContainerRef.current;
432
+ if (!container)
433
+ return;
434
+ container.addEventListener('scroll', handleScroll);
435
+ return () => container.removeEventListener('scroll', handleScroll);
436
+ }, [handleScroll]);
437
+ // Handle file upload (default handler if onUpload prop is not provided)
438
+ const handleFileUpload = async (file) => {
439
+ // If onUpload prop is provided, use it
440
+ if (onUpload) {
441
+ await onUpload(file);
442
+ return;
443
+ }
444
+ // Otherwise, handle upload internally
445
+ setInternalUploading(true);
446
+ try {
447
+ const formData = new FormData();
448
+ formData.append('file', file);
449
+ const response = await fetch('/api/plugin-images/upload', {
450
+ method: 'POST',
451
+ body: formData,
452
+ });
453
+ const data = await response.json();
454
+ if (data.success && data.image) {
455
+ // Reload images to show the newly uploaded image
456
+ await loadImages(1, false);
457
+ // Optionally select the newly uploaded image
458
+ // onSelectImage(data.image);
459
+ }
460
+ else {
461
+ console.error('[ImageBrowserModal] Upload failed:', data.error || 'Unknown error');
462
+ alert(data.error || 'Failed to upload image');
463
+ }
464
+ }
465
+ catch (error) {
466
+ console.error('[ImageBrowserModal] Upload error:', error);
467
+ alert('Failed to upload image');
468
+ }
469
+ finally {
470
+ setInternalUploading(false);
471
+ // Reset file input
472
+ if (fileInputRef.current) {
473
+ fileInputRef.current.value = '';
474
+ }
475
+ }
476
+ };
477
+ // 4. Handle External URL Logic
478
+ const handleUseExternalUrl = () => {
479
+ const url = externalUrl.trim();
480
+ if (!url)
481
+ return;
482
+ const externalImage = {
483
+ id: `external-${Date.now()}`,
484
+ filename: url.split('/').pop()?.split('?')[0] || 'external-image',
485
+ url: url,
486
+ size: 0,
487
+ mimeType: 'image/external',
488
+ uploadedAt: new Date().toISOString(),
489
+ };
490
+ onSelectImage(externalImage);
491
+ setExternalUrl('');
492
+ onClose();
493
+ };
494
+ if (!isOpen || !mounted || !portalTarget)
495
+ return null;
496
+ const modalContent = (_jsxs("div", { className: "fixed inset-0 z-[200] flex items-center justify-center p-4 animate-in fade-in duration-200", children: [_jsx("div", { className: "absolute inset-0 bg-neutral-950/70 backdrop-blur-md", onClick: onClose }), _jsxs("div", { ref: modalRef, className: `relative w-full max-w-6xl h-[90vh] bg-white dark:bg-neutral-900 rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300 ${darkMode ? 'dark' : ''}`, children: [_jsxs("div", { className: "px-6 py-5 border-b border-neutral-200 dark:border-neutral-800 bg-gradient-to-r from-neutral-50 to-white dark:from-neutral-900 dark:to-neutral-950 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-black uppercase tracking-tight dark:text-white", children: "Media Library" }), total > 0 && (_jsxs("p", { className: "text-xs text-neutral-500 font-semibold uppercase tracking-wider mt-1", children: [total, " ", total === 1 ? 'image' : 'images'] }))] }), _jsx("button", { onClick: onClose, className: "p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-all duration-200 hover:scale-110 active:scale-95", "aria-label": "Close", children: _jsx(X, { size: 20, className: "text-neutral-500 dark:text-neutral-300 hover:text-neutral-700 dark:hover:text-white transition-colors" }) })] }), _jsxs("div", { className: "px-6 py-4 bg-neutral-50/50 dark:bg-neutral-900/30 border-b border-neutral-200 dark:border-neutral-800 flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { size: 16, className: "absolute left-3.5 top-1/2 -translate-y-1/2 text-neutral-400" }), _jsx("input", { type: "text", placeholder: "Search images...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full pl-10 pr-4 py-2.5 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium focus:ring-2 focus:ring-primary/30 focus:border-primary outline-none transition-all shadow-sm" })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-3 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-xl px-4 py-2.5 shadow-sm", children: [_jsx(Grid3x3, { size: 16, className: "text-neutral-400 flex-shrink-0" }), _jsxs("div", { className: "flex items-center gap-3 min-w-[140px]", children: [_jsx("span", { className: "text-sm font-bold text-neutral-700 dark:text-neutral-300 min-w-[24px] text-right tabular-nums", children: columnCount }), _jsx("input", { type: "range", min: "3", max: "9", value: columnCount, onChange: (e) => setColumnCount(parseInt(e.target.value)), className: "flex-1 h-2 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary" })] }), _jsx("span", { className: "text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider", children: "cols" })] }), _jsxs("button", { onClick: () => fileInputRef.current?.click(), disabled: uploading || internalUploading, className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-primary text-white rounded-xl font-semibold text-sm hover:bg-primary/90 disabled:opacity-50 transition-all duration-200 shadow-md hover:shadow-lg active:scale-[0.98]", children: [(uploading || internalUploading) ? _jsx(Loader2, { size: 16, className: "animate-spin" }) : _jsx(Upload, { size: 16 }), "Upload"] })] })] }), _jsx("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto p-6", style: { scrollbarWidth: 'thin' }, children: loading && images.length === 0 ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center space-y-4", children: [_jsx(Loader2, { size: 48, className: "text-primary animate-spin" }), _jsx("p", { className: "text-sm font-semibold text-neutral-500 uppercase tracking-wider", children: "Loading Library..." })] })) : images.length === 0 ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center text-center py-16", children: [_jsx("div", { className: "p-5 bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900 rounded-2xl mb-4 shadow-inner", children: _jsx(ImageIcon, { size: 36, className: "text-neutral-400" }) }), _jsx("h3", { className: "text-lg font-bold dark:text-white mb-2", children: "No images found" }), _jsx("p", { className: "text-sm text-neutral-500 max-w-sm", children: searchQuery.trim()
497
+ ? 'Try a different search term or upload a new image.'
498
+ : 'Upload your first image to get started.' })] })) : (_jsxs(_Fragment, { children: [_jsx(MasonryGrid, { images: images, selectedImageId: resolvedSelectedId, onSelectImage: (image) => {
499
+ onSelectImage(image);
500
+ }, onClose: onClose, fixedColumns: columnCount }), loadingMore && (_jsx("div", { className: "flex justify-center items-center py-8", children: _jsx(Loader2, { size: 24, className: "text-primary animate-spin" }) })), !hasMore && images.length > 0 && (_jsx("div", { className: "text-center py-6", children: _jsxs("p", { className: "text-xs text-neutral-400 font-semibold uppercase tracking-wider", children: ["All ", total, " images loaded"] }) }))] })) }), _jsxs("div", { className: "px-6 py-4 bg-gradient-to-r from-neutral-50 to-white dark:from-neutral-900 dark:to-neutral-950 border-t border-neutral-200 dark:border-neutral-800 flex flex-col sm:flex-row items-center gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-neutral-400 min-w-max", children: [_jsx(LinkIcon, { size: 14 }), _jsx("span", { className: "text-[10px] font-bold uppercase tracking-wider", children: "External URL" })] }), _jsxs("div", { className: "flex-1 flex w-full gap-2", children: [_jsx("input", { type: "text", placeholder: "Paste image URL here...", value: externalUrl, onChange: (e) => setExternalUrl(e.target.value), onKeyDown: (e) => e.key === 'Enter' && handleUseExternalUrl(), className: "flex-1 px-3 py-2 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-lg text-xs font-medium focus:ring-2 focus:ring-primary/30 focus:border-primary outline-none transition-all shadow-sm" }), _jsx("button", { onClick: handleUseExternalUrl, className: "px-4 py-2 bg-neutral-900 dark:bg-white dark:text-neutral-900 text-white rounded-lg text-xs font-semibold hover:opacity-90 transition-all duration-200 active:scale-[0.98]", children: "Import" })] })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: (e) => {
501
+ const file = e.target.files?.[0];
502
+ if (file) {
503
+ handleFileUpload(file);
504
+ }
505
+ }, className: "hidden" })] })] }));
506
+ return createPortal(modalContent, portalTarget);
507
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ export interface ImageEditorProps {
3
+ imageUrl: string;
4
+ scale: number;
5
+ positionX: number;
6
+ positionY: number;
7
+ brightness?: number;
8
+ blur?: number;
9
+ onScaleChange: (scale: number) => void;
10
+ onPositionChange: (x: number, y: number) => void;
11
+ onBrightnessChange?: (brightness: number) => void;
12
+ onBlurChange?: (blur: number) => void;
13
+ aspectRatio?: string;
14
+ borderRadius?: string;
15
+ imageId?: string;
16
+ }
17
+ export interface ImageEditorHandle {
18
+ flushSave: () => Promise<{
19
+ scale: number;
20
+ positionX: number;
21
+ positionY: number;
22
+ brightness: number;
23
+ blur: number;
24
+ }>;
25
+ }
26
+ export declare const ImageEditor: React.ForwardRefExoticComponent<ImageEditorProps & React.RefAttributes<ImageEditorHandle>>;
27
+ //# sourceMappingURL=ImageEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageEditor.d.ts","sourceRoot":"","sources":["../../src/components/ImageEditor.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA6F,MAAM,OAAO,CAAC;AAIlH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvH;AAED,eAAO,MAAM,WAAW,4FAuStB,CAAC"}