@jhits/plugin-images 0.0.8 → 0.0.10
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.
- package/package.json +15 -9
- package/src/api/fallback/route.ts +0 -69
- package/src/api/index.ts +0 -10
- package/src/api/list/index.ts +0 -96
- package/src/api/resolve/route.ts +0 -241
- package/src/api/router.ts +0 -85
- package/src/api/upload/index.ts +0 -88
- package/src/api/uploads/[filename]/route.ts +0 -93
- package/src/api-server.ts +0 -11
- package/src/assets/noimagefound.jpg +0 -0
- package/src/components/BackgroundImage.d.ts +0 -11
- package/src/components/BackgroundImage.d.ts.map +0 -1
- package/src/components/BackgroundImage.js +0 -35
- package/src/components/BackgroundImage.tsx +0 -92
- package/src/components/GlobalImageEditor/config.d.ts +0 -9
- package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/config.js +0 -18
- package/src/components/GlobalImageEditor/config.ts +0 -21
- package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
- package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/eventHandlers.js +0 -206
- package/src/components/GlobalImageEditor/eventHandlers.ts +0 -267
- package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
- package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageDetection.js +0 -130
- package/src/components/GlobalImageEditor/imageDetection.ts +0 -160
- package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
- package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/imageSetup.js +0 -261
- package/src/components/GlobalImageEditor/imageSetup.ts +0 -306
- package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
- package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/saveLogic.js +0 -99
- package/src/components/GlobalImageEditor/saveLogic.ts +0 -133
- package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
- package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/stylingDetection.js +0 -110
- package/src/components/GlobalImageEditor/stylingDetection.ts +0 -122
- package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
- package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/transformParsing.js +0 -68
- package/src/components/GlobalImageEditor/transformParsing.ts +0 -83
- package/src/components/GlobalImageEditor/types.d.ts +0 -36
- package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
- package/src/components/GlobalImageEditor/types.js +0 -4
- package/src/components/GlobalImageEditor/types.ts +0 -39
- package/src/components/GlobalImageEditor.d.ts +0 -8
- package/src/components/GlobalImageEditor.d.ts.map +0 -1
- package/src/components/GlobalImageEditor.js +0 -227
- package/src/components/GlobalImageEditor.tsx +0 -327
- package/src/components/Image.d.ts +0 -22
- package/src/components/Image.d.ts.map +0 -1
- package/src/components/Image.js +0 -229
- package/src/components/Image.tsx +0 -343
- package/src/components/ImageBrowserModal.d.ts +0 -13
- package/src/components/ImageBrowserModal.d.ts.map +0 -1
- package/src/components/ImageBrowserModal.js +0 -504
- package/src/components/ImageBrowserModal.tsx +0 -837
- package/src/components/ImageEditor.d.ts +0 -27
- package/src/components/ImageEditor.d.ts.map +0 -1
- package/src/components/ImageEditor.js +0 -173
- package/src/components/ImageEditor.tsx +0 -323
- package/src/components/ImageEffectsPanel.tsx +0 -116
- package/src/components/ImagePicker.d.ts +0 -3
- package/src/components/ImagePicker.d.ts.map +0 -1
- package/src/components/ImagePicker.js +0 -143
- package/src/components/ImagePicker.tsx +0 -265
- package/src/components/ImagesPluginInit.d.ts +0 -24
- package/src/components/ImagesPluginInit.d.ts.map +0 -1
- package/src/components/ImagesPluginInit.js +0 -28
- package/src/components/ImagesPluginInit.tsx +0 -31
- package/src/components/index.ts +0 -10
- package/src/config.ts +0 -179
- package/src/hooks/useImagePicker.d.ts +0 -20
- package/src/hooks/useImagePicker.d.ts.map +0 -1
- package/src/hooks/useImagePicker.js +0 -322
- package/src/hooks/useImagePicker.ts +0 -344
- package/src/index.d.ts +0 -23
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -28
- package/src/index.server.ts +0 -12
- package/src/index.tsx +0 -56
- package/src/init.d.ts +0 -33
- package/src/init.d.ts.map +0 -1
- package/src/init.js +0 -43
- package/src/init.tsx +0 -58
- package/src/types/index.d.ts +0 -80
- package/src/types/index.d.ts.map +0 -1
- package/src/types/index.js +0 -4
- package/src/types/index.ts +0 -84
- package/src/utils/fallback.d.ts +0 -27
- package/src/utils/fallback.d.ts.map +0 -1
- package/src/utils/fallback.js +0 -63
- package/src/utils/fallback.ts +0 -73
- package/src/utils/transforms.d.ts +0 -26
- package/src/utils/transforms.d.ts.map +0 -1
- package/src/utils/transforms.js +0 -38
- package/src/utils/transforms.ts +0 -54
- package/src/views/ImageManager.d.ts +0 -10
- package/src/views/ImageManager.d.ts.map +0 -1
- package/src/views/ImageManager.js +0 -9
- package/src/views/ImageManager.tsx +0 -30
|
@@ -1,27 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useRef, useEffect, useCallback, useImperativeHandle, forwardRef, useMemo } from 'react';
|
|
4
|
-
import { ZoomIn, RotateCcw, Maximize2, Sun, Droplets } from 'lucide-react';
|
|
5
|
-
import { getImageTransform, getImageFilter } from '../utils/transforms';
|
|
6
|
-
export const ImageEditor = forwardRef(({ imageUrl, scale, positionX, positionY, brightness = 100, blur = 0, onScaleChange, onPositionChange, onBrightnessChange, onBlurChange, aspectRatio = '16/9', borderRadius = 'rounded-xl', imageId, }, ref) => {
|
|
7
|
-
const containerRef = useRef(null);
|
|
8
|
-
const imageRef = useRef(null);
|
|
9
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
10
|
-
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
11
|
-
const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 });
|
|
12
|
-
const [baseScale, setBaseScale] = useState(0);
|
|
13
|
-
// Local state for brightness and blur (only applied on save)
|
|
14
|
-
const [localBrightness, setLocalBrightness] = useState(brightness);
|
|
15
|
-
const [localBlur, setLocalBlur] = useState(blur);
|
|
16
|
-
// Keep internal values in a ref to avoid stale closure issues during the final save
|
|
17
|
-
const currentValues = useRef({ scale, positionX, positionY, brightness, blur });
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
currentValues.current = { scale, positionX, positionY, brightness: localBrightness, blur: localBlur };
|
|
20
|
-
}, [scale, positionX, positionY, localBrightness, localBlur]);
|
|
21
|
-
// Reset local values when props change (when editor opens with new values)
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
setLocalBrightness(brightness);
|
|
24
|
-
setLocalBlur(blur);
|
|
25
|
-
}, [brightness, blur]);
|
|
26
|
-
// EXPOSE TO PARENT: Return current values so Parent can handle the DB Save
|
|
27
|
-
useImperativeHandle(ref, () => ({
|
|
28
|
-
flushSave: async () => {
|
|
29
|
-
// Read the latest values directly from props/state (they're in the dependency array, so they're current)
|
|
30
|
-
// The ref is kept in sync via useEffect, but read directly from props to avoid any timing issues
|
|
31
|
-
const latestScale = scale;
|
|
32
|
-
const latestPositionX = positionX;
|
|
33
|
-
const latestPositionY = positionY;
|
|
34
|
-
const latestBrightness = localBrightness;
|
|
35
|
-
const latestBlur = localBlur;
|
|
36
|
-
// Update the ref to ensure it's in sync (for any other code that might read it)
|
|
37
|
-
currentValues.current = {
|
|
38
|
-
scale: latestScale,
|
|
39
|
-
positionX: latestPositionX,
|
|
40
|
-
positionY: latestPositionY,
|
|
41
|
-
brightness: latestBrightness,
|
|
42
|
-
blur: latestBlur
|
|
43
|
-
};
|
|
44
|
-
// DON'T call onBrightnessChange/onBlurChange here - they trigger saves
|
|
45
|
-
// The parent (via onEditorSave) will handle saving all values together
|
|
46
|
-
// This prevents duplicate saves
|
|
47
|
-
return {
|
|
48
|
-
scale: Math.max(0.1, Math.min(5.0, latestScale)),
|
|
49
|
-
positionX: latestPositionX,
|
|
50
|
-
positionY: latestPositionY,
|
|
51
|
-
brightness: latestBrightness,
|
|
52
|
-
blur: latestBlur
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
}), [scale, positionX, positionY, localBrightness, localBlur]);
|
|
56
|
-
const getClampedPosition = useCallback((x, y, currentScale) => {
|
|
57
|
-
if (!containerRef.current || !imageRef.current || baseScale === 0)
|
|
58
|
-
return { x, y };
|
|
59
|
-
const totalScale = baseScale * currentScale;
|
|
60
|
-
const containerWidth = containerRef.current.offsetWidth;
|
|
61
|
-
const containerHeight = containerRef.current.offsetHeight;
|
|
62
|
-
// With minWidth/minHeight, the image maintains its natural aspect ratio
|
|
63
|
-
// but is scaled to cover the container. The scaled dimensions are based on natural size.
|
|
64
|
-
const scaledWidth = imageRef.current.naturalWidth * totalScale;
|
|
65
|
-
const scaledHeight = imageRef.current.naturalHeight * totalScale;
|
|
66
|
-
const overflowX = Math.max(0, (scaledWidth - containerWidth) / 2);
|
|
67
|
-
const overflowY = Math.max(0, (scaledHeight - containerHeight) / 2);
|
|
68
|
-
// Convert overflow limits to percentage of CONTAINER (not natural size)
|
|
69
|
-
// Position values are container-relative, so limits must be too
|
|
70
|
-
const limitX = containerWidth > 0 ? (overflowX / containerWidth) * 100 : 0;
|
|
71
|
-
const limitY = containerHeight > 0 ? (overflowY / containerHeight) * 100 : 0;
|
|
72
|
-
return { x: Math.max(-limitX, Math.min(limitX, x)), y: Math.max(-limitY, Math.min(limitY, y)) };
|
|
73
|
-
}, [baseScale]);
|
|
74
|
-
const handleScaleChange = (newScale) => {
|
|
75
|
-
const s = Math.max(0.1, Math.min(5.0, newScale));
|
|
76
|
-
onScaleChange(s);
|
|
77
|
-
const clamped = getClampedPosition(positionX, positionY, s);
|
|
78
|
-
onPositionChange(clamped.x, clamped.y);
|
|
79
|
-
};
|
|
80
|
-
const calculateBaseScale = useCallback(() => {
|
|
81
|
-
var _a, _b;
|
|
82
|
-
if (!imageRef.current || !containerRef.current)
|
|
83
|
-
return;
|
|
84
|
-
const img = imageRef.current;
|
|
85
|
-
const container = containerRef.current;
|
|
86
|
-
const containerWidth = container.offsetWidth;
|
|
87
|
-
const containerHeight = container.offsetHeight;
|
|
88
|
-
// Ensure container has dimensions before calculating
|
|
89
|
-
if (containerWidth === 0 || containerHeight === 0) {
|
|
90
|
-
// Try to get dimensions from computed styles or parent
|
|
91
|
-
const parentWidth = (_a = container.parentElement) === null || _a === void 0 ? void 0 : _a.clientWidth;
|
|
92
|
-
const parentHeight = (_b = container.parentElement) === null || _b === void 0 ? void 0 : _b.clientHeight;
|
|
93
|
-
if (parentWidth && parentHeight && img.naturalWidth > 0) {
|
|
94
|
-
const widthRatio = parentWidth / img.naturalWidth;
|
|
95
|
-
const heightRatio = parentHeight / img.naturalHeight;
|
|
96
|
-
const calculatedBaseScale = Math.max(widthRatio, heightRatio);
|
|
97
|
-
setBaseScale(calculatedBaseScale);
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (img.naturalWidth === 0 || img.naturalHeight === 0)
|
|
102
|
-
return;
|
|
103
|
-
setBaseScale(Math.max(containerWidth / img.naturalWidth, containerHeight / img.naturalHeight));
|
|
104
|
-
}, []);
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (!containerRef.current)
|
|
107
|
-
return;
|
|
108
|
-
const obs = new ResizeObserver(calculateBaseScale);
|
|
109
|
-
obs.observe(containerRef.current);
|
|
110
|
-
return () => obs.disconnect();
|
|
111
|
-
}, [calculateBaseScale]);
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (!isDragging)
|
|
114
|
-
return;
|
|
115
|
-
const move = (e) => {
|
|
116
|
-
if (!containerRef.current || !imageRef.current || baseScale === 0)
|
|
117
|
-
return;
|
|
118
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
119
|
-
// Calculate movement in screen pixels
|
|
120
|
-
const mouseDeltaX = e.clientX - dragStart.x;
|
|
121
|
-
const mouseDeltaY = e.clientY - dragStart.y;
|
|
122
|
-
// Convert screen pixel movement to percentage of CONTAINER
|
|
123
|
-
// Since the image is now width: 100%; height: 100%, percentages are container-relative
|
|
124
|
-
// This makes position values consistent across different container sizes
|
|
125
|
-
const dx = containerRef.current && containerRef.current.offsetWidth > 0
|
|
126
|
-
? (mouseDeltaX / containerRef.current.offsetWidth) * 100
|
|
127
|
-
: 0;
|
|
128
|
-
const dy = containerRef.current && containerRef.current.offsetHeight > 0
|
|
129
|
-
? (mouseDeltaY / containerRef.current.offsetHeight) * 100
|
|
130
|
-
: 0;
|
|
131
|
-
const newPositionX = dragStartPosition.x + dx;
|
|
132
|
-
const newPositionY = dragStartPosition.y + dy;
|
|
133
|
-
const clamped = getClampedPosition(newPositionX, newPositionY, scale);
|
|
134
|
-
onPositionChange(clamped.x, clamped.y);
|
|
135
|
-
};
|
|
136
|
-
const up = () => setIsDragging(false);
|
|
137
|
-
window.addEventListener('mousemove', move);
|
|
138
|
-
window.addEventListener('mouseup', up);
|
|
139
|
-
return () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); };
|
|
140
|
-
}, [isDragging, dragStart, dragStartPosition, scale, baseScale, getClampedPosition, onPositionChange]);
|
|
141
|
-
const aspectValue = useMemo(() => {
|
|
142
|
-
if (aspectRatio === 'auto')
|
|
143
|
-
return undefined;
|
|
144
|
-
const [w, h] = aspectRatio.split('/').map(Number);
|
|
145
|
-
return w / h;
|
|
146
|
-
}, [aspectRatio]);
|
|
147
|
-
return (_jsxs("div", { className: "flex flex-col lg:flex-row gap-6 h-full", children: [_jsx("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", children: _jsx("div", { ref: containerRef, 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`, style: { aspectRatio: aspectValue, height: aspectValue && aspectValue < 1 ? '400px' : 'auto', width: aspectValue && aspectValue < 1 ? 'auto' : '100%', cursor: isDragging ? 'grabbing' : 'grab' }, onMouseDown: (e) => {
|
|
148
|
-
setIsDragging(true);
|
|
149
|
-
setDragStart({ x: e.clientX, y: e.clientY });
|
|
150
|
-
// Convert stored position to visual percentage for dragging
|
|
151
|
-
// stored = visual, so we can use positionX directly as visual percentage
|
|
152
|
-
setDragStartPosition({ x: positionX, y: positionY });
|
|
153
|
-
e.preventDefault();
|
|
154
|
-
}, children: _jsx("img", { ref: imageRef, src: imageUrl, alt: "Editor", onLoad: calculateBaseScale, className: "absolute max-w-none select-none", style: {
|
|
155
|
-
top: '50%',
|
|
156
|
-
left: '50%',
|
|
157
|
-
width: 'auto', // Allow image to maintain natural aspect ratio
|
|
158
|
-
height: 'auto', // Allow image to maintain natural aspect ratio
|
|
159
|
-
minWidth: '100%', // Ensure image covers container width
|
|
160
|
-
minHeight: '100%', // Ensure image covers container height
|
|
161
|
-
filter: getImageFilter(localBrightness, localBlur),
|
|
162
|
-
transform: baseScale > 0 ? getImageTransform({ scale, positionX, positionY, baseScale }, true) : 'translate(-50%, -50%)',
|
|
163
|
-
transformOrigin: 'center center'
|
|
164
|
-
}, draggable: false }) }) }), _jsx("div", { className: "w-full lg:w-80 space-y-4", children: _jsxs("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", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsxs("label", { className: "text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2", children: [_jsx(ZoomIn, { size: 14, className: "text-neutral-600 dark:text-neutral-300" }), " Zoom Level"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-xs font-mono font-bold text-primary dark:text-primary", children: [Math.round(scale * 100), "%"] }), _jsx("button", { onClick: () => handleScaleChange(1), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors", title: "Reset zoom", children: _jsx(RotateCcw, { size: 12, className: "text-neutral-500 dark:text-neutral-400" }) })] })] }), _jsx("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" })] }), onBrightnessChange && (_jsxs("div", { children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsxs("label", { className: "text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2", children: [_jsx(Sun, { size: 14, className: "text-neutral-600 dark:text-neutral-300" }), " Brightness"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-xs font-mono font-bold text-primary dark:text-primary", children: [Math.round(localBrightness), "%"] }), _jsx("button", { onClick: () => setLocalBrightness(100), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors", title: "Reset brightness", children: _jsx(RotateCcw, { size: 12, className: "text-neutral-500 dark:text-neutral-400" }) })] })] }), _jsx("input", { type: "range", min: "0", max: "200", step: "1", value: localBrightness, onChange: (e) => setLocalBrightness(parseInt(e.target.value)), className: "w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-primary" })] })), onBlurChange && (_jsxs("div", { children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsxs("label", { className: "text-[10px] font-black uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 flex items-center gap-2", children: [_jsx(Droplets, { size: 14, className: "text-neutral-600 dark:text-neutral-300" }), " Blur"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-xs font-mono font-bold text-primary dark:text-primary", children: [localBlur, "px"] }), _jsx("button", { onClick: () => setLocalBlur(0), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors", title: "Reset blur", children: _jsx(RotateCcw, { size: 12, className: "text-neutral-500 dark:text-neutral-400" }) })] })] }), _jsx("input", { type: "range", min: "0", max: "20", step: "0.5", value: localBlur, onChange: (e) => setLocalBlur(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" })] })), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("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", children: [_jsx(Maximize2, { size: 16, className: "text-neutral-700 dark:text-neutral-300" }), _jsx("span", { className: "text-[9px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Center" })] }), _jsxs("button", { onClick: () => {
|
|
165
|
-
handleScaleChange(1);
|
|
166
|
-
if (onBrightnessChange)
|
|
167
|
-
setLocalBrightness(100);
|
|
168
|
-
if (onBlurChange)
|
|
169
|
-
setLocalBlur(0);
|
|
170
|
-
onPositionChange(0, 0);
|
|
171
|
-
}, 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", children: [_jsx(RotateCcw, { size: 16, className: "text-neutral-700 dark:text-neutral-300" }), _jsx("span", { className: "text-[9px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Reset All" })] })] })] }) })] }));
|
|
172
|
-
});
|
|
173
|
-
ImageEditor.displayName = 'ImageEditor';
|
|
@@ -1,323 +0,0 @@
|
|
|
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';
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
}
|