@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.
- package/dist/api/fallback/route.d.ts +7 -0
- package/dist/api/fallback/route.d.ts.map +1 -0
- package/dist/api/fallback/route.js +65 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/list/index.d.ts +21 -0
- package/dist/api/list/index.d.ts.map +1 -0
- package/dist/api/list/index.js +80 -0
- package/dist/api/resolve/route.d.ts +39 -0
- package/dist/api/resolve/route.d.ts.map +1 -0
- package/dist/api/resolve/route.js +213 -0
- package/dist/api/router.d.ts +14 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +67 -0
- package/dist/api/upload/index.d.ts +20 -0
- package/dist/api/upload/index.d.ts.map +1 -0
- package/dist/api/upload/index.js +65 -0
- package/dist/api/uploads/[filename]/route.d.ts +21 -0
- package/dist/api/uploads/[filename]/route.d.ts.map +1 -0
- package/dist/api/uploads/[filename]/route.js +80 -0
- package/dist/api-server.d.ts +9 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +9 -0
- package/dist/components/BackgroundImage.d.ts +11 -0
- package/dist/components/BackgroundImage.d.ts.map +1 -0
- package/dist/components/BackgroundImage.js +33 -0
- package/dist/components/GlobalImageEditor/config.d.ts +9 -0
- package/dist/components/GlobalImageEditor/config.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/config.js +17 -0
- package/dist/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
- package/dist/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/eventHandlers.js +210 -0
- package/dist/components/GlobalImageEditor/imageDetection.d.ts +16 -0
- package/dist/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/imageDetection.js +135 -0
- package/dist/components/GlobalImageEditor/imageSetup.d.ts +9 -0
- package/dist/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/imageSetup.js +260 -0
- package/dist/components/GlobalImageEditor/saveLogic.d.ts +26 -0
- package/dist/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/saveLogic.js +98 -0
- package/dist/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
- package/dist/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/stylingDetection.js +110 -0
- package/dist/components/GlobalImageEditor/transformParsing.d.ts +16 -0
- package/dist/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/transformParsing.js +68 -0
- package/dist/components/GlobalImageEditor/types.d.ts +36 -0
- package/dist/components/GlobalImageEditor/types.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor/types.js +4 -0
- package/dist/components/GlobalImageEditor.d.ts +8 -0
- package/dist/components/GlobalImageEditor.d.ts.map +1 -0
- package/dist/components/GlobalImageEditor.js +232 -0
- package/dist/components/Image.d.ts +22 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +227 -0
- package/dist/components/ImageBrowserModal.d.ts +13 -0
- package/dist/components/ImageBrowserModal.d.ts.map +1 -0
- package/dist/components/ImageBrowserModal.js +507 -0
- package/dist/components/ImageEditor.d.ts +27 -0
- package/dist/components/ImageEditor.d.ts.map +1 -0
- package/dist/components/ImageEditor.js +172 -0
- package/dist/components/ImageEffectsPanel.d.ts +10 -0
- package/dist/components/ImageEffectsPanel.d.ts.map +1 -0
- package/dist/components/ImageEffectsPanel.js +11 -0
- package/dist/components/ImagePicker.d.ts +3 -0
- package/dist/components/ImagePicker.d.ts.map +1 -0
- package/dist/components/ImagePicker.js +142 -0
- package/dist/components/ImagesPluginInit.d.ts +24 -0
- package/dist/components/ImagesPluginInit.d.ts.map +1 -0
- package/dist/components/ImagesPluginInit.js +28 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +172 -0
- package/dist/hooks/useImagePicker.d.ts +20 -0
- package/dist/hooks/useImagePicker.d.ts.map +1 -0
- package/dist/hooks/useImagePicker.js +320 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.server.d.ts +11 -0
- package/dist/index.server.d.ts.map +1 -0
- package/dist/index.server.js +10 -0
- package/dist/init.d.ts +33 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +43 -0
- package/dist/types/index.d.ts +80 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/fallback.d.ts +27 -0
- package/dist/utils/fallback.d.ts.map +1 -0
- package/dist/utils/fallback.js +63 -0
- package/dist/utils/transforms.d.ts +26 -0
- package/dist/utils/transforms.d.ts.map +1 -0
- package/dist/utils/transforms.js +38 -0
- package/dist/views/ImageManager.d.ts +10 -0
- package/dist/views/ImageManager.d.ts.map +1 -0
- package/dist/views/ImageManager.js +9 -0
- package/package.json +26 -22
- package/src/assets/noimagefound.jpg +0 -0
- package/src/components/BackgroundImage.d.ts +11 -0
- package/src/components/BackgroundImage.d.ts.map +1 -0
- package/src/components/BackgroundImage.js +35 -0
- package/src/components/GlobalImageEditor/config.d.ts +9 -0
- package/src/components/GlobalImageEditor/config.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/config.js +18 -0
- package/src/components/GlobalImageEditor/eventHandlers.d.ts +20 -0
- package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/eventHandlers.js +206 -0
- package/src/components/GlobalImageEditor/imageDetection.d.ts +16 -0
- package/src/components/GlobalImageEditor/imageDetection.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/imageDetection.js +130 -0
- package/src/components/GlobalImageEditor/imageSetup.d.ts +9 -0
- package/src/components/GlobalImageEditor/imageSetup.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/imageSetup.js +261 -0
- package/src/components/GlobalImageEditor/saveLogic.d.ts +26 -0
- package/src/components/GlobalImageEditor/saveLogic.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/saveLogic.js +99 -0
- package/src/components/GlobalImageEditor/stylingDetection.d.ts +9 -0
- package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/stylingDetection.js +110 -0
- package/src/components/GlobalImageEditor/transformParsing.d.ts +16 -0
- package/src/components/GlobalImageEditor/transformParsing.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/transformParsing.js +68 -0
- package/src/components/GlobalImageEditor/types.d.ts +36 -0
- package/src/components/GlobalImageEditor/types.d.ts.map +1 -0
- package/src/components/GlobalImageEditor/types.js +4 -0
- package/src/components/GlobalImageEditor.d.ts +8 -0
- package/src/components/GlobalImageEditor.d.ts.map +1 -0
- package/src/components/GlobalImageEditor.js +227 -0
- package/src/components/GlobalImageEditor.tsx +2 -2
- package/src/components/Image.d.ts +22 -0
- package/src/components/Image.d.ts.map +1 -0
- package/src/components/Image.js +229 -0
- package/src/components/ImageBrowserModal.d.ts +13 -0
- package/src/components/ImageBrowserModal.d.ts.map +1 -0
- package/src/components/ImageBrowserModal.js +504 -0
- package/src/components/ImageBrowserModal.tsx +18 -5
- package/src/components/ImageEditor.d.ts +27 -0
- package/src/components/ImageEditor.d.ts.map +1 -0
- package/src/components/ImageEditor.js +173 -0
- package/src/components/ImagePicker.d.ts +3 -0
- package/src/components/ImagePicker.d.ts.map +1 -0
- package/src/components/ImagePicker.js +143 -0
- package/src/components/ImagePicker.tsx +53 -15
- package/src/components/ImagesPluginInit.d.ts +24 -0
- package/src/components/ImagesPluginInit.d.ts.map +1 -0
- package/src/components/ImagesPluginInit.js +28 -0
- package/src/hooks/useImagePicker.d.ts +20 -0
- package/src/hooks/useImagePicker.d.ts.map +1 -0
- package/src/hooks/useImagePicker.js +322 -0
- package/src/hooks/useImagePicker.ts +28 -6
- package/src/index.d.ts +23 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +28 -0
- package/src/init.d.ts +33 -0
- package/src/init.d.ts.map +1 -0
- package/src/init.js +43 -0
- package/src/types/index.d.ts +80 -0
- package/src/types/index.d.ts.map +1 -0
- package/src/types/index.js +4 -0
- package/src/utils/fallback.d.ts +27 -0
- package/src/utils/fallback.d.ts.map +1 -0
- package/src/utils/fallback.js +63 -0
- package/src/utils/transforms.d.ts +26 -0
- package/src/utils/transforms.d.ts.map +1 -0
- package/src/utils/transforms.js +38 -0
- package/src/views/ImageManager.d.ts +10 -0
- package/src/views/ImageManager.d.ts.map +1 -0
- package/src/views/ImageManager.js +9 -0
- package/src/components/GlobalImageEditor/GlobalImageEditor.tsx +0 -374
|
@@ -0,0 +1,173 @@
|
|
|
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';
|
|
@@ -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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Search, Image as ImageIcon, Settings } from 'lucide-react';
|
|
6
|
+
import { ImageEditor } from './ImageEditor';
|
|
7
|
+
import { ImageBrowserModal } from './ImageBrowserModal';
|
|
8
|
+
import { useImagePicker } from '../hooks/useImagePicker';
|
|
9
|
+
import { getImageTransform, getImageFilter } from '../utils/transforms';
|
|
10
|
+
import { getFallbackImageUrl } from '../utils/fallback';
|
|
11
|
+
export function ImagePicker({ value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0, aspectRatio = '16/9', borderRadius = 'rounded-xl', onBrightnessChange, onBlurChange, onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor = false, }) {
|
|
12
|
+
const [transforms, setTransforms] = useState({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
|
|
13
|
+
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
14
|
+
const [isBrowserOpen, setIsBrowserOpen] = useState(false);
|
|
15
|
+
const [previewBaseScale, setPreviewBaseScale] = useState(null);
|
|
16
|
+
const [previewImageError, setPreviewImageError] = useState(false);
|
|
17
|
+
const [mounted, setMounted] = useState(false);
|
|
18
|
+
const [portalTarget, setPortalTarget] = useState(null);
|
|
19
|
+
const previewImageRef = useRef(null);
|
|
20
|
+
const previewContainerRef = useRef(null);
|
|
21
|
+
const editorRef = useRef(null);
|
|
22
|
+
const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
|
|
23
|
+
// Reset preview error when selected image changes
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setPreviewImageError(false);
|
|
26
|
+
}, [selectedImage === null || selectedImage === void 0 ? void 0 : selectedImage.id, selectedImage === null || selectedImage === void 0 ? void 0 : selectedImage.url]);
|
|
27
|
+
// Handle SSR & portal target - ensure we only render portal on client
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
var _a;
|
|
30
|
+
setMounted(true);
|
|
31
|
+
if (typeof document === 'undefined')
|
|
32
|
+
return;
|
|
33
|
+
// If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
|
|
34
|
+
// This ensures the browser/editor modals share the same stacking context
|
|
35
|
+
const editorContainer = (_a = previewContainerRef.current) === null || _a === void 0 ? void 0 : _a.closest('[data-image-editor="true"]');
|
|
36
|
+
if (editorContainer) {
|
|
37
|
+
setPortalTarget(editorContainer);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
setPortalTarget(document.body);
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
// Auto-open editor if requested (e.g., when opening from edit button)
|
|
44
|
+
// Only auto-open once when the prop first becomes true, not on every render
|
|
45
|
+
const hasAutoOpenedRef = useRef(false);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (autoOpenEditor && selectedImage && !isEditorOpen && !hasAutoOpenedRef.current) {
|
|
48
|
+
// Small delay to ensure ImagePicker is fully rendered
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
setIsEditorOpen(true);
|
|
51
|
+
hasAutoOpenedRef.current = true;
|
|
52
|
+
}, 100);
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
// Reset the flag when autoOpenEditor becomes false
|
|
56
|
+
if (!autoOpenEditor) {
|
|
57
|
+
hasAutoOpenedRef.current = false;
|
|
58
|
+
}
|
|
59
|
+
}, [autoOpenEditor, selectedImage, isEditorOpen]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isEditorOpen)
|
|
62
|
+
setTransforms({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
|
|
63
|
+
}, [scale, positionX, positionY, isEditorOpen]);
|
|
64
|
+
const calculatePreviewBaseScale = useCallback(() => {
|
|
65
|
+
if (!previewImageRef.current || !previewContainerRef.current)
|
|
66
|
+
return;
|
|
67
|
+
const fit = Math.max(previewContainerRef.current.offsetWidth / previewImageRef.current.naturalWidth, previewContainerRef.current.offsetHeight / previewImageRef.current.naturalHeight);
|
|
68
|
+
setPreviewBaseScale(fit);
|
|
69
|
+
}, []);
|
|
70
|
+
// Handle editor save - delegate to parent callbacks instead of saving directly
|
|
71
|
+
// This ensures all saves go through a single location (GlobalImageEditor.saveImageTransform)
|
|
72
|
+
const handleEditorSave = async () => {
|
|
73
|
+
if (!editorRef.current || !selectedImage)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
// 1. Get current values from Editor UI (this will also call onBrightnessChange and onBlurChange)
|
|
77
|
+
const final = await editorRef.current.flushSave();
|
|
78
|
+
// 2. Normalize position values - if they're -50% (centering value), treat as 0
|
|
79
|
+
const normalizedPositionX = final.positionX === -50 ? 0 : final.positionX;
|
|
80
|
+
const normalizedPositionY = final.positionY === -50 ? 0 : final.positionY;
|
|
81
|
+
// 3. If onEditorSave is provided, use it exclusively to prevent duplicate saves
|
|
82
|
+
// Otherwise, update parent state through individual callbacks
|
|
83
|
+
console.log('[ImagePicker] handleEditorSave - final values:', final, 'has onEditorSave:', !!onEditorSave);
|
|
84
|
+
if (onEditorSave) {
|
|
85
|
+
// onEditorSave handles everything - don't call individual handlers to avoid duplicates
|
|
86
|
+
console.log('[ImagePicker] Calling onEditorSave with:', { scale: final.scale, positionX: normalizedPositionX, positionY: normalizedPositionY, brightness: final.brightness, blur: final.blur });
|
|
87
|
+
onEditorSave(final.scale, normalizedPositionX, normalizedPositionY, final.brightness, final.blur);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Fallback: update parent state through individual callbacks
|
|
91
|
+
// Since onEditorSave is not provided, call all callbacks and they should handle saving
|
|
92
|
+
// The last one (onPositionYChange) will trigger the final save
|
|
93
|
+
console.log('[ImagePicker] No onEditorSave, using individual callbacks');
|
|
94
|
+
// Update scale first
|
|
95
|
+
onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(final.scale);
|
|
96
|
+
// Update positions - these will trigger saves
|
|
97
|
+
onPositionXChange === null || onPositionXChange === void 0 ? void 0 : onPositionXChange(normalizedPositionX);
|
|
98
|
+
// Last callback - this should trigger the final save with all values
|
|
99
|
+
// We need to ensure this saves immediately with all final values including brightness/blur
|
|
100
|
+
onPositionYChange === null || onPositionYChange === void 0 ? void 0 : onPositionYChange(normalizedPositionY);
|
|
101
|
+
}
|
|
102
|
+
// 5. Close the editor
|
|
103
|
+
setIsEditorOpen(false);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('[ImagePicker] Failed to get editor values:', error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const aspectValue = useMemo(() => {
|
|
110
|
+
if (aspectRatio === 'auto')
|
|
111
|
+
return undefined;
|
|
112
|
+
const [w, h] = aspectRatio.split('/').map(Number);
|
|
113
|
+
return w / h;
|
|
114
|
+
}, [aspectRatio]);
|
|
115
|
+
return (_jsxs("div", { className: "space-y-6", children: [selectedImage ? (_jsx("div", { className: "relative group max-w-md mx-auto", children: _jsxs("div", { className: `relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`, style: { aspectRatio: aspectValue, width: '100%' }, children: [_jsx("div", { ref: previewContainerRef, className: "relative w-full h-full overflow-hidden", children: previewImageError ? (_jsx("img", { src: getFallbackImageUrl(), alt: selectedImage.filename || 'Image not found', className: "absolute max-w-none", style: {
|
|
116
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
117
|
+
minWidth: '100%', minHeight: '100%',
|
|
118
|
+
transform: 'translate(-50%, -50%)',
|
|
119
|
+
transformOrigin: 'center center',
|
|
120
|
+
objectFit: 'cover',
|
|
121
|
+
} })) : (_jsx("img", { ref: previewImageRef, src: selectedImage.url, alt: selectedImage.filename, className: "absolute max-w-none", onLoad: calculatePreviewBaseScale, onError: () => setPreviewImageError(true), style: {
|
|
122
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
123
|
+
minWidth: '100%', minHeight: '100%',
|
|
124
|
+
filter: getImageFilter(brightness, blur),
|
|
125
|
+
transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
|
|
126
|
+
transformOrigin: 'center center',
|
|
127
|
+
} })) }), _jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" })] }) })) : (_jsxs("div", { onClick: () => setIsBrowserOpen(true), className: "aspect-video bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-950 rounded-2xl border-2 border-dashed border-neutral-300 dark:border-neutral-800 flex flex-col items-center justify-center text-neutral-400 hover:border-primary hover:text-primary cursor-pointer transition-all duration-300 hover:shadow-lg group", children: [_jsx("div", { className: "p-4 bg-white/50 dark:bg-neutral-800/50 rounded-full mb-3 group-hover:scale-110 transition-transform duration-300", children: _jsx(ImageIcon, { size: 28, className: "group-hover:scale-110 transition-transform duration-300" }) }), _jsx("span", { className: "text-xs font-bold uppercase tracking-[0.15em] group-hover:tracking-[0.2em] transition-all duration-300", children: "Select Image" })] })), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [_jsxs("button", { onClick: () => setIsBrowserOpen(true), className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]", children: [_jsx(Search, { size: 16 }), "Browse"] }), _jsxs("button", { onClick: () => setIsEditorOpen(true), disabled: !selectedImage, className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm disabled:hover:scale-100", children: [_jsx(Settings, { size: 16 }), "Edit"] })] }), isEditorOpen && selectedImage && mounted && portalTarget && createPortal(_jsx("div", { className: "fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200", children: _jsxs("div", { className: "w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300", children: [_jsx("div", { className: "flex-1 overflow-hidden p-4 lg:p-6", children: _jsx(ImageEditor, { ref: editorRef, imageUrl: selectedImage.url, scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, brightness: brightness, blur: blur, onScaleChange: (s) => setTransforms(t => (Object.assign(Object.assign({}, t), { scale: s }))), onPositionChange: (x, y) => {
|
|
128
|
+
// Only update local state during drag - don't trigger saves
|
|
129
|
+
// Saves will happen when editor closes via onEditorSave
|
|
130
|
+
setTransforms(t => (Object.assign(Object.assign({}, t), { positionX: x, positionY: y })));
|
|
131
|
+
}, onBrightnessChange: onBrightnessChange, onBlurChange: onBlurChange, aspectRatio: aspectRatio, borderRadius: borderRadius }) }), _jsxs("div", { className: "px-6 py-5 border-t border-neutral-200 dark:border-neutral-800 bg-neutral-50/80 dark:bg-neutral-900/80 backdrop-blur-sm flex justify-end gap-3", children: [_jsx("button", { onClick: () => setIsEditorOpen(false), className: "px-6 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl font-semibold text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]", children: "Cancel" }), _jsx("button", { onClick: handleEditorSave, className: "px-8 py-2.5 bg-primary text-white rounded-xl font-semibold shadow-md hover:shadow-lg hover:bg-primary/90 transition-all duration-200 active:scale-[0.98]", children: "Done" })] })] }) }), portalTarget), _jsx(ImageBrowserModal, { isOpen: isBrowserOpen, onClose: () => setIsBrowserOpen(false), onSelectImage: (image) => {
|
|
132
|
+
setSelectedImage(image);
|
|
133
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(image);
|
|
134
|
+
setIsBrowserOpen(false);
|
|
135
|
+
}, selectedImageId: (() => {
|
|
136
|
+
// Use resolved image's filename/URL if available, otherwise fall back to value
|
|
137
|
+
// This ensures semantic IDs are resolved to actual filenames for matching
|
|
138
|
+
if (selectedImage) {
|
|
139
|
+
return selectedImage.filename || selectedImage.url || selectedImage.id || value;
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
})(), darkMode: false })] }));
|
|
143
|
+
}
|
|
@@ -9,6 +9,7 @@ import { ImageEditor, type ImageEditorHandle } from './ImageEditor';
|
|
|
9
9
|
import { ImageBrowserModal } from './ImageBrowserModal';
|
|
10
10
|
import { useImagePicker } from '../hooks/useImagePicker';
|
|
11
11
|
import { getImageTransform, getImageFilter } from '../utils/transforms';
|
|
12
|
+
import { getFallbackImageUrl } from '../utils/fallback';
|
|
12
13
|
|
|
13
14
|
export function ImagePicker({
|
|
14
15
|
value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0,
|
|
@@ -20,16 +21,34 @@ export function ImagePicker({
|
|
|
20
21
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
21
22
|
const [isBrowserOpen, setIsBrowserOpen] = useState(false);
|
|
22
23
|
const [previewBaseScale, setPreviewBaseScale] = useState<number | null>(null);
|
|
24
|
+
const [previewImageError, setPreviewImageError] = useState(false);
|
|
23
25
|
const [mounted, setMounted] = useState(false);
|
|
26
|
+
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
|
24
27
|
const previewImageRef = useRef<HTMLImageElement>(null);
|
|
25
28
|
const previewContainerRef = useRef<HTMLDivElement>(null);
|
|
26
29
|
const editorRef = useRef<ImageEditorHandle | null>(null);
|
|
27
30
|
|
|
28
31
|
const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
|
|
29
32
|
|
|
30
|
-
//
|
|
33
|
+
// Reset preview error when selected image changes
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setPreviewImageError(false);
|
|
36
|
+
}, [selectedImage?.id, selectedImage?.url]);
|
|
37
|
+
|
|
38
|
+
// Handle SSR & portal target - ensure we only render portal on client
|
|
31
39
|
useEffect(() => {
|
|
32
40
|
setMounted(true);
|
|
41
|
+
|
|
42
|
+
if (typeof document === 'undefined') return;
|
|
43
|
+
|
|
44
|
+
// If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
|
|
45
|
+
// This ensures the browser/editor modals share the same stacking context
|
|
46
|
+
const editorContainer = previewContainerRef.current?.closest('[data-image-editor="true"]') as HTMLElement | null;
|
|
47
|
+
if (editorContainer) {
|
|
48
|
+
setPortalTarget(editorContainer);
|
|
49
|
+
} else {
|
|
50
|
+
setPortalTarget(document.body);
|
|
51
|
+
}
|
|
33
52
|
}, []);
|
|
34
53
|
|
|
35
54
|
// Auto-open editor if requested (e.g., when opening from edit button)
|
|
@@ -115,17 +134,36 @@ export function ImagePicker({
|
|
|
115
134
|
<div className="relative group max-w-md mx-auto">
|
|
116
135
|
<div className={`relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`} style={{ aspectRatio: aspectValue, width: '100%' }}>
|
|
117
136
|
<div ref={previewContainerRef} className="relative w-full h-full overflow-hidden">
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
{previewImageError ? (
|
|
138
|
+
<img
|
|
139
|
+
src={getFallbackImageUrl()}
|
|
140
|
+
alt={selectedImage.filename || 'Image not found'}
|
|
141
|
+
className="absolute max-w-none"
|
|
142
|
+
style={{
|
|
143
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
144
|
+
minWidth: '100%', minHeight: '100%',
|
|
145
|
+
transform: 'translate(-50%, -50%)',
|
|
146
|
+
transformOrigin: 'center center',
|
|
147
|
+
objectFit: 'cover',
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<img
|
|
152
|
+
ref={previewImageRef}
|
|
153
|
+
src={selectedImage.url}
|
|
154
|
+
alt={selectedImage.filename}
|
|
155
|
+
className="absolute max-w-none"
|
|
156
|
+
onLoad={calculatePreviewBaseScale}
|
|
157
|
+
onError={() => setPreviewImageError(true)}
|
|
158
|
+
style={{
|
|
159
|
+
top: '50%', left: '50%', width: 'auto', height: 'auto',
|
|
160
|
+
minWidth: '100%', minHeight: '100%',
|
|
161
|
+
filter: getImageFilter(brightness, blur),
|
|
162
|
+
transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
|
|
163
|
+
transformOrigin: 'center center',
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
129
167
|
</div>
|
|
130
168
|
{/* Overlay on hover */}
|
|
131
169
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" />
|
|
@@ -161,8 +199,8 @@ export function ImagePicker({
|
|
|
161
199
|
</button>
|
|
162
200
|
</div>
|
|
163
201
|
|
|
164
|
-
{isEditorOpen && selectedImage && mounted && createPortal(
|
|
165
|
-
<div className="fixed inset-0 z-[
|
|
202
|
+
{isEditorOpen && selectedImage && mounted && portalTarget && createPortal(
|
|
203
|
+
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200">
|
|
166
204
|
<div className="w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300">
|
|
167
205
|
<div className="flex-1 overflow-hidden p-4 lg:p-6">
|
|
168
206
|
<ImageEditor
|
|
@@ -201,7 +239,7 @@ export function ImagePicker({
|
|
|
201
239
|
</div>
|
|
202
240
|
</div>
|
|
203
241
|
</div>,
|
|
204
|
-
|
|
242
|
+
portalTarget
|
|
205
243
|
)}
|
|
206
244
|
|
|
207
245
|
<ImageBrowserModal
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images Plugin Initialization Component
|
|
3
|
+
*
|
|
4
|
+
* This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
|
|
5
|
+
* and renders the GlobalImageEditor if enabled.
|
|
6
|
+
*
|
|
7
|
+
* Render this once in your app layout after calling initImagesPlugin().
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Images Plugin Initialization Component
|
|
11
|
+
*
|
|
12
|
+
* Renders the global image editor if enabled in the plugin configuration.
|
|
13
|
+
* This component should be rendered in your app layout.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { ImagesPluginInit } from '@jhits/plugin-images';
|
|
18
|
+
*
|
|
19
|
+
* // After calling initImagesPlugin() in a useEffect or script
|
|
20
|
+
* <ImagesPluginInit />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function ImagesPluginInit(): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=ImagesPluginInit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImagesPluginInit.d.ts","sourceRoot":"","sources":["ImagesPluginInit.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,4CAE/B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images Plugin Initialization Component
|
|
3
|
+
*
|
|
4
|
+
* This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
|
|
5
|
+
* and renders the GlobalImageEditor if enabled.
|
|
6
|
+
*
|
|
7
|
+
* Render this once in your app layout after calling initImagesPlugin().
|
|
8
|
+
*/
|
|
9
|
+
'use client';
|
|
10
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
+
import { GlobalImageEditor } from './GlobalImageEditor';
|
|
12
|
+
/**
|
|
13
|
+
* Images Plugin Initialization Component
|
|
14
|
+
*
|
|
15
|
+
* Renders the global image editor if enabled in the plugin configuration.
|
|
16
|
+
* This component should be rendered in your app layout.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { ImagesPluginInit } from '@jhits/plugin-images';
|
|
21
|
+
*
|
|
22
|
+
* // After calling initImagesPlugin() in a useEffect or script
|
|
23
|
+
* <ImagesPluginInit />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function ImagesPluginInit() {
|
|
27
|
+
return _jsx(GlobalImageEditor, {});
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for Image Picker Logic
|
|
3
|
+
*/
|
|
4
|
+
import type { ImageMetadata } from '../types';
|
|
5
|
+
export interface UseImagePickerOptions {
|
|
6
|
+
value?: string;
|
|
7
|
+
images: ImageMetadata[];
|
|
8
|
+
}
|
|
9
|
+
export declare function useImagePicker({ value, images }: UseImagePickerOptions): {
|
|
10
|
+
selectedImage: ImageMetadata | null;
|
|
11
|
+
setSelectedImage: import("react").Dispatch<import("react").SetStateAction<ImageMetadata | null>>;
|
|
12
|
+
uploading: boolean;
|
|
13
|
+
fileInputRef: import("react").RefObject<HTMLInputElement | null>;
|
|
14
|
+
handleFileSelect: (e?: React.ChangeEvent<HTMLInputElement> | {
|
|
15
|
+
target: {
|
|
16
|
+
files: File[] | null;
|
|
17
|
+
};
|
|
18
|
+
}) => Promise<any>;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=useImagePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useImagePicker.d.ts","sourceRoot":"","sources":["useImagePicker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,qBAAqB;;;;;2BA6R/B,KAAK,CAAC,WAAW,CAAC,gBAAgB,CAAC,GAAG;QAAE,MAAM,EAAE;YAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;SAAE,CAAA;KAAE;EA8CjH"}
|