@jhits/plugin-images 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/package.json +8 -9
  2. package/src/api/fallback/route.ts +0 -69
  3. package/src/api/index.ts +0 -10
  4. package/src/api/list/index.ts +0 -96
  5. package/src/api/resolve/route.ts +0 -241
  6. package/src/api/router.ts +0 -85
  7. package/src/api/upload/index.ts +0 -88
  8. package/src/api/uploads/[filename]/route.ts +0 -93
  9. package/src/api-server.ts +0 -11
  10. package/src/assets/noimagefound.jpg +0 -0
  11. package/src/components/BackgroundImage.d.ts +0 -11
  12. package/src/components/BackgroundImage.d.ts.map +0 -1
  13. package/src/components/BackgroundImage.js +0 -35
  14. package/src/components/BackgroundImage.tsx +0 -92
  15. package/src/components/GlobalImageEditor/config.d.ts +0 -9
  16. package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
  17. package/src/components/GlobalImageEditor/config.js +0 -18
  18. package/src/components/GlobalImageEditor/config.ts +0 -21
  19. package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
  20. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
  21. package/src/components/GlobalImageEditor/eventHandlers.js +0 -206
  22. package/src/components/GlobalImageEditor/eventHandlers.ts +0 -267
  23. package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
  24. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
  25. package/src/components/GlobalImageEditor/imageDetection.js +0 -130
  26. package/src/components/GlobalImageEditor/imageDetection.ts +0 -160
  27. package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
  28. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
  29. package/src/components/GlobalImageEditor/imageSetup.js +0 -261
  30. package/src/components/GlobalImageEditor/imageSetup.ts +0 -306
  31. package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
  32. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
  33. package/src/components/GlobalImageEditor/saveLogic.js +0 -99
  34. package/src/components/GlobalImageEditor/saveLogic.ts +0 -133
  35. package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
  36. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
  37. package/src/components/GlobalImageEditor/stylingDetection.js +0 -110
  38. package/src/components/GlobalImageEditor/stylingDetection.ts +0 -122
  39. package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
  40. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
  41. package/src/components/GlobalImageEditor/transformParsing.js +0 -68
  42. package/src/components/GlobalImageEditor/transformParsing.ts +0 -83
  43. package/src/components/GlobalImageEditor/types.d.ts +0 -36
  44. package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
  45. package/src/components/GlobalImageEditor/types.js +0 -4
  46. package/src/components/GlobalImageEditor/types.ts +0 -39
  47. package/src/components/GlobalImageEditor.d.ts +0 -8
  48. package/src/components/GlobalImageEditor.d.ts.map +0 -1
  49. package/src/components/GlobalImageEditor.js +0 -227
  50. package/src/components/GlobalImageEditor.tsx +0 -327
  51. package/src/components/Image.d.ts +0 -22
  52. package/src/components/Image.d.ts.map +0 -1
  53. package/src/components/Image.js +0 -229
  54. package/src/components/Image.tsx +0 -343
  55. package/src/components/ImageBrowserModal.d.ts +0 -13
  56. package/src/components/ImageBrowserModal.d.ts.map +0 -1
  57. package/src/components/ImageBrowserModal.js +0 -504
  58. package/src/components/ImageBrowserModal.tsx +0 -837
  59. package/src/components/ImageEditor.d.ts +0 -27
  60. package/src/components/ImageEditor.d.ts.map +0 -1
  61. package/src/components/ImageEditor.js +0 -173
  62. package/src/components/ImageEditor.tsx +0 -323
  63. package/src/components/ImageEffectsPanel.tsx +0 -116
  64. package/src/components/ImagePicker.d.ts +0 -3
  65. package/src/components/ImagePicker.d.ts.map +0 -1
  66. package/src/components/ImagePicker.js +0 -143
  67. package/src/components/ImagePicker.tsx +0 -265
  68. package/src/components/ImagesPluginInit.d.ts +0 -24
  69. package/src/components/ImagesPluginInit.d.ts.map +0 -1
  70. package/src/components/ImagesPluginInit.js +0 -28
  71. package/src/components/ImagesPluginInit.tsx +0 -31
  72. package/src/components/index.ts +0 -10
  73. package/src/config.ts +0 -179
  74. package/src/hooks/useImagePicker.d.ts +0 -20
  75. package/src/hooks/useImagePicker.d.ts.map +0 -1
  76. package/src/hooks/useImagePicker.js +0 -322
  77. package/src/hooks/useImagePicker.ts +0 -344
  78. package/src/index.d.ts +0 -23
  79. package/src/index.d.ts.map +0 -1
  80. package/src/index.js +0 -28
  81. package/src/index.server.ts +0 -12
  82. package/src/index.tsx +0 -56
  83. package/src/init.d.ts +0 -33
  84. package/src/init.d.ts.map +0 -1
  85. package/src/init.js +0 -43
  86. package/src/init.tsx +0 -58
  87. package/src/types/index.d.ts +0 -80
  88. package/src/types/index.d.ts.map +0 -1
  89. package/src/types/index.js +0 -4
  90. package/src/types/index.ts +0 -84
  91. package/src/utils/fallback.d.ts +0 -27
  92. package/src/utils/fallback.d.ts.map +0 -1
  93. package/src/utils/fallback.js +0 -63
  94. package/src/utils/fallback.ts +0 -73
  95. package/src/utils/transforms.d.ts +0 -26
  96. package/src/utils/transforms.d.ts.map +0 -1
  97. package/src/utils/transforms.js +0 -38
  98. package/src/utils/transforms.ts +0 -54
  99. package/src/views/ImageManager.d.ts +0 -10
  100. package/src/views/ImageManager.d.ts.map +0 -1
  101. package/src/views/ImageManager.js +0 -9
  102. 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
- }