@jhits/plugin-images 0.0.11 → 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,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":["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"}
@@ -0,0 +1,323 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect, useCallback, useImperativeHandle, forwardRef, useMemo } from 'react';
4
+ import { ZoomIn, RotateCcw, Move, Maximize2, Sun, Droplets } from 'lucide-react';
5
+ import { getImageTransform, getImageFilter } from '../utils/transforms';
6
+
7
+ export interface ImageEditorProps {
8
+ imageUrl: string;
9
+ scale: number;
10
+ positionX: number;
11
+ positionY: number;
12
+ brightness?: number;
13
+ blur?: number;
14
+ onScaleChange: (scale: number) => void;
15
+ onPositionChange: (x: number, y: number) => void;
16
+ onBrightnessChange?: (brightness: number) => void;
17
+ onBlurChange?: (blur: number) => void;
18
+ aspectRatio?: string;
19
+ borderRadius?: string;
20
+ imageId?: string;
21
+ }
22
+
23
+ export interface ImageEditorHandle {
24
+ flushSave: () => Promise<{ scale: number; positionX: number; positionY: number; brightness: number; blur: number }>;
25
+ }
26
+
27
+ export const ImageEditor = forwardRef<ImageEditorHandle, ImageEditorProps>(({
28
+ imageUrl,
29
+ scale,
30
+ positionX,
31
+ positionY,
32
+ brightness = 100,
33
+ blur = 0,
34
+ onScaleChange,
35
+ onPositionChange,
36
+ onBrightnessChange,
37
+ onBlurChange,
38
+ aspectRatio = '16/9',
39
+ borderRadius = 'rounded-xl',
40
+ imageId,
41
+ }, ref) => {
42
+ const containerRef = useRef<HTMLDivElement>(null);
43
+ const imageRef = useRef<HTMLImageElement>(null);
44
+ const [isDragging, setIsDragging] = useState(false);
45
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
46
+ const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 });
47
+ const [baseScale, setBaseScale] = useState(0);
48
+
49
+ // Local state for brightness and blur (only applied on save)
50
+ const [localBrightness, setLocalBrightness] = useState(brightness);
51
+ const [localBlur, setLocalBlur] = useState(blur);
52
+
53
+ // Keep internal values in a ref to avoid stale closure issues during the final save
54
+ const currentValues = useRef({ scale, positionX, positionY, brightness, blur });
55
+ useEffect(() => {
56
+ currentValues.current = { scale, positionX, positionY, brightness: localBrightness, blur: localBlur };
57
+ }, [scale, positionX, positionY, localBrightness, localBlur]);
58
+
59
+ // Reset local values when props change (when editor opens with new values)
60
+ useEffect(() => {
61
+ setLocalBrightness(brightness);
62
+ setLocalBlur(blur);
63
+ }, [brightness, blur]);
64
+
65
+ // EXPOSE TO PARENT: Return current values so Parent can handle the DB Save
66
+ useImperativeHandle(ref, () => ({
67
+ flushSave: async () => {
68
+ // Read the latest values directly from props/state (they're in the dependency array, so they're current)
69
+ // The ref is kept in sync via useEffect, but read directly from props to avoid any timing issues
70
+ const latestScale = scale;
71
+ const latestPositionX = positionX;
72
+ const latestPositionY = positionY;
73
+ const latestBrightness = localBrightness;
74
+ const latestBlur = localBlur;
75
+
76
+ // Update the ref to ensure it's in sync (for any other code that might read it)
77
+ currentValues.current = {
78
+ scale: latestScale,
79
+ positionX: latestPositionX,
80
+ positionY: latestPositionY,
81
+ brightness: latestBrightness,
82
+ blur: latestBlur
83
+ };
84
+
85
+ // DON'T call onBrightnessChange/onBlurChange here - they trigger saves
86
+ // The parent (via onEditorSave) will handle saving all values together
87
+ // This prevents duplicate saves
88
+
89
+ return {
90
+ scale: Math.max(0.1, Math.min(5.0, latestScale)),
91
+ positionX: latestPositionX,
92
+ positionY: latestPositionY,
93
+ brightness: latestBrightness,
94
+ blur: latestBlur
95
+ };
96
+ }
97
+ }), [scale, positionX, positionY, localBrightness, localBlur]);
98
+
99
+ const getClampedPosition = useCallback((x: number, y: number, currentScale: number) => {
100
+ if (!containerRef.current || !imageRef.current || baseScale === 0) return { x, y };
101
+ const totalScale = baseScale * currentScale;
102
+ const containerWidth = containerRef.current.offsetWidth;
103
+ const containerHeight = containerRef.current.offsetHeight;
104
+
105
+ // With minWidth/minHeight, the image maintains its natural aspect ratio
106
+ // but is scaled to cover the container. The scaled dimensions are based on natural size.
107
+ const scaledWidth = imageRef.current.naturalWidth * totalScale;
108
+ const scaledHeight = imageRef.current.naturalHeight * totalScale;
109
+ const overflowX = Math.max(0, (scaledWidth - containerWidth) / 2);
110
+ const overflowY = Math.max(0, (scaledHeight - containerHeight) / 2);
111
+
112
+ // Convert overflow limits to percentage of CONTAINER (not natural size)
113
+ // Position values are container-relative, so limits must be too
114
+ const limitX = containerWidth > 0 ? (overflowX / containerWidth) * 100 : 0;
115
+ const limitY = containerHeight > 0 ? (overflowY / containerHeight) * 100 : 0;
116
+
117
+ return { x: Math.max(-limitX, Math.min(limitX, x)), y: Math.max(-limitY, Math.min(limitY, y)) };
118
+ }, [baseScale]);
119
+
120
+ const handleScaleChange = (newScale: number) => {
121
+ const s = Math.max(0.1, Math.min(5.0, newScale));
122
+ onScaleChange(s);
123
+ const clamped = getClampedPosition(positionX, positionY, s);
124
+ onPositionChange(clamped.x, clamped.y);
125
+ };
126
+
127
+ const calculateBaseScale = useCallback(() => {
128
+ if (!imageRef.current || !containerRef.current) return;
129
+ const img = imageRef.current;
130
+ const container = containerRef.current;
131
+ const containerWidth = container.offsetWidth;
132
+ const containerHeight = container.offsetHeight;
133
+
134
+ // Ensure container has dimensions before calculating
135
+ if (containerWidth === 0 || containerHeight === 0) {
136
+ // Try to get dimensions from computed styles or parent
137
+ const parentWidth = container.parentElement?.clientWidth;
138
+ const parentHeight = container.parentElement?.clientHeight;
139
+
140
+ if (parentWidth && parentHeight && img.naturalWidth > 0) {
141
+ const widthRatio = parentWidth / img.naturalWidth;
142
+ const heightRatio = parentHeight / img.naturalHeight;
143
+ const calculatedBaseScale = Math.max(widthRatio, heightRatio);
144
+ setBaseScale(calculatedBaseScale);
145
+ }
146
+ return;
147
+ }
148
+
149
+ if (img.naturalWidth === 0 || img.naturalHeight === 0) return;
150
+ setBaseScale(Math.max(containerWidth / img.naturalWidth, containerHeight / img.naturalHeight));
151
+ }, []);
152
+
153
+ useEffect(() => {
154
+ if (!containerRef.current) return;
155
+ const obs = new ResizeObserver(calculateBaseScale);
156
+ obs.observe(containerRef.current);
157
+ return () => obs.disconnect();
158
+ }, [calculateBaseScale]);
159
+
160
+ useEffect(() => {
161
+ if (!isDragging) return;
162
+ const move = (e: MouseEvent) => {
163
+ if (!containerRef.current || !imageRef.current || baseScale === 0) return;
164
+ const rect = containerRef.current.getBoundingClientRect();
165
+
166
+ // Calculate movement in screen pixels
167
+ const mouseDeltaX = e.clientX - dragStart.x;
168
+ const mouseDeltaY = e.clientY - dragStart.y;
169
+
170
+ // Convert screen pixel movement to percentage of CONTAINER
171
+ // Since the image is now width: 100%; height: 100%, percentages are container-relative
172
+ // This makes position values consistent across different container sizes
173
+ const dx = containerRef.current && containerRef.current.offsetWidth > 0
174
+ ? (mouseDeltaX / containerRef.current.offsetWidth) * 100
175
+ : 0;
176
+ const dy = containerRef.current && containerRef.current.offsetHeight > 0
177
+ ? (mouseDeltaY / containerRef.current.offsetHeight) * 100
178
+ : 0;
179
+
180
+ const newPositionX = dragStartPosition.x + dx;
181
+ const newPositionY = dragStartPosition.y + dy;
182
+ const clamped = getClampedPosition(newPositionX, newPositionY, scale);
183
+ onPositionChange(clamped.x, clamped.y);
184
+ };
185
+ const up = () => setIsDragging(false);
186
+ window.addEventListener('mousemove', move);
187
+ window.addEventListener('mouseup', up);
188
+ return () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); };
189
+ }, [isDragging, dragStart, dragStartPosition, scale, baseScale, getClampedPosition, onPositionChange]);
190
+
191
+ const aspectValue = useMemo(() => {
192
+ if (aspectRatio === 'auto') return undefined;
193
+ const [w, h] = aspectRatio.split('/').map(Number);
194
+ return w / h;
195
+ }, [aspectRatio]);
196
+
197
+ return (
198
+ <div className="flex flex-col lg:flex-row gap-6 h-full">
199
+ <div className="flex-1 bg-neutral-50 dark:bg-neutral-950 rounded-2xl p-4 lg:p-6 flex items-center justify-center border border-neutral-300 dark:border-neutral-700 relative group/canvas min-w-0">
200
+ <div
201
+ ref={containerRef}
202
+ className={`relative shadow-2xl overflow-hidden ${borderRadius} bg-neutral-100 dark:bg-neutral-900 w-full max-w-full border border-neutral-200 dark:border-neutral-800`}
203
+ style={{ aspectRatio: aspectValue, height: aspectValue && aspectValue < 1 ? '400px' : 'auto', width: aspectValue && aspectValue < 1 ? 'auto' : '100%', cursor: isDragging ? 'grabbing' : 'grab' }}
204
+ onMouseDown={(e) => {
205
+ setIsDragging(true);
206
+ setDragStart({ x: e.clientX, y: e.clientY });
207
+ // Convert stored position to visual percentage for dragging
208
+ // stored = visual, so we can use positionX directly as visual percentage
209
+ setDragStartPosition({ x: positionX, y: positionY });
210
+ e.preventDefault();
211
+ }}
212
+ >
213
+ <img
214
+ ref={imageRef} src={imageUrl} alt="Editor" onLoad={calculateBaseScale}
215
+ className="absolute max-w-none select-none"
216
+ style={{
217
+ top: '50%',
218
+ left: '50%',
219
+ width: 'auto', // Allow image to maintain natural aspect ratio
220
+ height: 'auto', // Allow image to maintain natural aspect ratio
221
+ minWidth: '100%', // Ensure image covers container width
222
+ minHeight: '100%', // Ensure image covers container height
223
+ filter: getImageFilter(localBrightness, localBlur),
224
+ transform: baseScale > 0 ? getImageTransform({ scale, positionX, positionY, baseScale }, true) : 'translate(-50%, -50%)',
225
+ transformOrigin: 'center center'
226
+ }}
227
+ draggable={false}
228
+ />
229
+ </div>
230
+ </div>
231
+
232
+ <div className="w-full lg:w-80 space-y-4">
233
+ <div className="p-6 bg-white dark:bg-neutral-900 rounded-3xl border border-neutral-200 dark:border-neutral-800 shadow-lg dark:shadow-neutral-950/50 space-y-6">
234
+ <div>
235
+ <div className="flex justify-between items-center mb-4">
236
+ <label className="text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2"><ZoomIn size={14} className="text-neutral-600 dark:text-neutral-300" /> Zoom Level</label>
237
+ <div className="flex items-center gap-2">
238
+ <span className="text-xs font-mono font-bold text-primary dark:text-primary">{Math.round(scale * 100)}%</span>
239
+ <button
240
+ onClick={() => handleScaleChange(1)}
241
+ className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
242
+ title="Reset zoom"
243
+ >
244
+ <RotateCcw size={12} className="text-neutral-500 dark:text-neutral-400" />
245
+ </button>
246
+ </div>
247
+ </div>
248
+ <input type="range" min="0.1" max="5" step="0.01" value={scale} onChange={(e) => handleScaleChange(parseFloat(e.target.value))} className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary" />
249
+ </div>
250
+
251
+ {onBrightnessChange && (
252
+ <div>
253
+ <div className="flex justify-between items-center mb-4">
254
+ <label className="text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2"><Sun size={14} className="text-neutral-600 dark:text-neutral-300" /> Brightness</label>
255
+ <div className="flex items-center gap-2">
256
+ <span className="text-xs font-mono font-bold text-primary dark:text-primary">{Math.round(localBrightness)}%</span>
257
+ <button
258
+ onClick={() => setLocalBrightness(100)}
259
+ className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
260
+ title="Reset brightness"
261
+ >
262
+ <RotateCcw size={12} className="text-neutral-500 dark:text-neutral-400" />
263
+ </button>
264
+ </div>
265
+ </div>
266
+ <input
267
+ type="range"
268
+ min="0"
269
+ max="200"
270
+ step="1"
271
+ value={localBrightness}
272
+ onChange={(e) => setLocalBrightness(parseInt(e.target.value))}
273
+ className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary"
274
+ />
275
+ </div>
276
+ )}
277
+
278
+ {onBlurChange && (
279
+ <div>
280
+ <div className="flex justify-between items-center mb-4">
281
+ <label className="text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2"><Droplets size={14} className="text-neutral-600 dark:text-neutral-300" /> Blur</label>
282
+ <div className="flex items-center gap-2">
283
+ <span className="text-xs font-mono font-bold text-primary dark:text-primary">{localBlur}px</span>
284
+ <button
285
+ onClick={() => setLocalBlur(0)}
286
+ className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
287
+ title="Reset blur"
288
+ >
289
+ <RotateCcw size={12} className="text-neutral-500 dark:text-neutral-400" />
290
+ </button>
291
+ </div>
292
+ </div>
293
+ <input
294
+ type="range"
295
+ min="0"
296
+ max="20"
297
+ step="0.5"
298
+ value={localBlur}
299
+ onChange={(e) => setLocalBlur(parseFloat(e.target.value))}
300
+ className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary"
301
+ />
302
+ </div>
303
+ )}
304
+
305
+ <div className="grid grid-cols-2 gap-3">
306
+ <button onClick={() => onPositionChange(0, 0)} className="flex flex-col items-center gap-2 p-4 bg-neutral-50 dark:bg-neutral-800 rounded-2xl hover:bg-primary/10 dark:hover:bg-primary/20 transition-all border border-transparent hover:border-primary/20 dark:hover:border-primary/30">
307
+ <Maximize2 size={16} className="text-neutral-700 dark:text-neutral-300" /><span className="text-[9px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400">Center</span>
308
+ </button>
309
+ <button onClick={() => {
310
+ handleScaleChange(1);
311
+ if (onBrightnessChange) setLocalBrightness(100);
312
+ if (onBlurChange) setLocalBlur(0);
313
+ onPositionChange(0, 0);
314
+ }} className="flex flex-col items-center gap-2 p-4 bg-neutral-50 dark:bg-neutral-800 rounded-2xl hover:bg-primary/10 dark:hover:bg-primary/20 transition-all border border-transparent hover:border-primary/20 dark:hover:border-primary/30">
315
+ <RotateCcw size={16} className="text-neutral-700 dark:text-neutral-300" /><span className="text-[9px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400">Reset All</span>
316
+ </button>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
322
+ });
323
+ ImageEditor.displayName = 'ImageEditor';
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { SlidersHorizontal, X, RotateCcw, Sun, Wind } from 'lucide-react';
5
+
6
+ export interface ImageEffectsPanelProps {
7
+ brightness: number;
8
+ blur: number;
9
+ onBrightnessChange: (brightness: number | undefined) => void;
10
+ onBlurChange: (blur: number | undefined) => void;
11
+ onClose?: () => void;
12
+ darkMode?: boolean;
13
+ }
14
+
15
+ export function ImageEffectsPanel({
16
+ brightness,
17
+ blur,
18
+ onBrightnessChange,
19
+ onBlurChange,
20
+ onClose,
21
+ }: ImageEffectsPanelProps) {
22
+
23
+ // Helper to reset specific effects to defaults
24
+ const handleReset = () => {
25
+ onBrightnessChange(100);
26
+ onBlurChange(0);
27
+ };
28
+
29
+ return (
30
+ <div className="flex flex-col gap-5 p-5 bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 shadow-sm transition-all">
31
+ {/* Header */}
32
+ <div className="flex items-center justify-between pb-2 border-b border-neutral-100 dark:border-neutral-800">
33
+ <div className="flex items-center gap-2">
34
+ <div className="p-1.5 bg-primary/10 rounded-lg">
35
+ <SlidersHorizontal size={14} className="text-primary" />
36
+ </div>
37
+ <h4 className="text-[11px] text-neutral-900 dark:text-neutral-100 uppercase font-black tracking-widest">
38
+ Effects Studio
39
+ </h4>
40
+ </div>
41
+ <div className="flex items-center gap-1">
42
+ <button
43
+ onClick={handleReset}
44
+ title="Reset all effects"
45
+ className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg text-neutral-400 hover:text-primary transition-all"
46
+ >
47
+ <RotateCcw size={14} />
48
+ </button>
49
+ {onClose && (
50
+ <button
51
+ onClick={onClose}
52
+ className="p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg text-neutral-400 transition-all"
53
+ >
54
+ <X size={16} />
55
+ </button>
56
+ )}
57
+ </div>
58
+ </div>
59
+
60
+ {/* Brightness Slider */}
61
+ <div className="space-y-3">
62
+ <div className="flex items-center justify-between">
63
+ <div className="flex items-center gap-2 text-neutral-600 dark:text-neutral-400">
64
+ <Sun size={14} />
65
+ <label className="text-xs font-bold uppercase tracking-tight">
66
+ Brightness
67
+ </label>
68
+ </div>
69
+ <span className="text-[10px] font-mono font-bold px-2 py-0.5 bg-neutral-100 dark:bg-neutral-800 rounded-full text-primary">
70
+ {brightness}%
71
+ </span>
72
+ </div>
73
+ <input
74
+ type="range"
75
+ min="0"
76
+ max="200"
77
+ step="1"
78
+ value={brightness}
79
+ onChange={(e) => onBrightnessChange(Number(e.target.value))}
80
+ className="w-full h-1.5 bg-neutral-100 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary hover:accent-primary/80 transition-all"
81
+ />
82
+ </div>
83
+
84
+ {/* Blur Slider */}
85
+ <div className="space-y-3">
86
+ <div className="flex items-center justify-between">
87
+ <div className="flex items-center gap-2 text-neutral-600 dark:text-neutral-400">
88
+ <Wind size={14} />
89
+ <label className="text-xs font-bold uppercase tracking-tight">
90
+ Blur Intensity
91
+ </label>
92
+ </div>
93
+ <span className="text-[10px] font-mono font-bold px-2 py-0.5 bg-neutral-100 dark:bg-neutral-800 rounded-full text-primary">
94
+ {blur}px
95
+ </span>
96
+ </div>
97
+ <input
98
+ type="range"
99
+ min="0"
100
+ max="20"
101
+ step="0.5"
102
+ value={blur}
103
+ onChange={(e) => onBlurChange(Number(e.target.value))}
104
+ className="w-full h-1.5 bg-neutral-100 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary hover:accent-primary/80 transition-all"
105
+ />
106
+ </div>
107
+
108
+ {/* Visual Feedback Note */}
109
+ <div className="mt-2 p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-xl border border-dashed border-neutral-200 dark:border-neutral-700">
110
+ <p className="text-[9px] leading-relaxed text-neutral-400 uppercase font-medium text-center italic">
111
+ Changes are applied in real-time to the preview above
112
+ </p>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,3 @@
1
+ import type { ImagePickerProps } from '../types';
2
+ export declare function ImagePicker({ value, onChange, brightness, blur, scale, positionX, positionY, aspectRatio, borderRadius, onBrightnessChange, onBlurChange, onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor, }: ImagePickerProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=ImagePicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImagePicker.d.ts","sourceRoot":"","sources":["ImagePicker.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAQjD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EAAE,QAAQ,EAAE,UAAgB,EAAE,IAAQ,EAAE,KAAW,EAAE,SAAa,EAAE,SAAa,EACtF,WAAoB,EAAE,YAA2B,EAAE,kBAAkB,EAAE,YAAY,EACnF,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAsB,GAC5F,EAAE,gBAAgB,2CAuPlB"}