@alpaca-editor/core 1.0.3978 → 1.0.3980
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/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/config/config.js +18 -2
- package/dist/config/config.js.map +1 -1
- package/dist/editor/AspectRatioSelector.d.ts +13 -0
- package/dist/editor/AspectRatioSelector.js +71 -0
- package/dist/editor/AspectRatioSelector.js.map +1 -0
- package/dist/editor/ConfirmationDialog.js +4 -5
- package/dist/editor/ConfirmationDialog.js.map +1 -1
- package/dist/editor/PictureCropper.d.ts +1 -1
- package/dist/editor/PictureCropper.js +466 -113
- package/dist/editor/PictureCropper.js.map +1 -1
- package/dist/editor/Terminal.js +5 -4
- package/dist/editor/Terminal.js.map +1 -1
- package/dist/editor/ai/AiTerminal.js +20 -2
- package/dist/editor/ai/AiTerminal.js.map +1 -1
- package/dist/editor/client/EditorClient.js +14 -3
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +3 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +13 -5
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/media-selector/Preview.js +1 -1
- package/dist/editor/media-selector/Preview.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +48 -24
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +6 -6
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js +26 -4
- package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
- package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
- package/dist/editor/page-viewer/EditorForm.js +16 -2
- package/dist/editor/page-viewer/EditorForm.js.map +1 -1
- package/dist/editor/page-viewer/EditorFormPopup.d.ts +11 -0
- package/dist/editor/page-viewer/EditorFormPopup.js +41 -0
- package/dist/editor/page-viewer/EditorFormPopup.js.map +1 -0
- package/dist/editor/page-viewer/PageViewer.js +4 -6
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/services/contextService.d.ts +26 -0
- package/dist/editor/services/contextService.js +102 -0
- package/dist/editor/services/contextService.js.map +1 -0
- package/dist/editor/sidebar/Completions.d.ts +1 -0
- package/dist/editor/sidebar/Completions.js +54 -0
- package/dist/editor/sidebar/Completions.js.map +1 -0
- package/dist/editor/sidebar/Validation.js +3 -3
- package/dist/editor/sidebar/Validation.js.map +1 -1
- package/dist/editor/ui/PerfectTree.js +17 -3
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/ui/SimpleTabs.d.ts +2 -1
- package/dist/editor/ui/SimpleTabs.js +2 -2
- package/dist/editor/ui/SimpleTabs.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +134 -30
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/config/config.tsx +18 -1
- package/src/editor/AspectRatioSelector.tsx +146 -0
- package/src/editor/ConfirmationDialog.tsx +36 -45
- package/src/editor/PictureCropper.tsx +724 -233
- package/src/editor/Terminal.tsx +9 -8
- package/src/editor/ai/AiTerminal.tsx +58 -15
- package/src/editor/client/EditorClient.tsx +26 -1
- package/src/editor/client/editContext.ts +7 -0
- package/src/editor/commands/componentCommands.tsx +14 -9
- package/src/editor/media-selector/Preview.tsx +7 -5
- package/src/editor/page-editor-chrome/FrameMenu.tsx +70 -15
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -3
- package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +31 -5
- package/src/editor/page-viewer/EditorForm.tsx +21 -1
- package/src/editor/page-viewer/EditorFormPopup.tsx +104 -0
- package/src/editor/page-viewer/PageViewer.tsx +3 -11
- package/src/editor/services/contextService.ts +146 -0
- package/src/editor/sidebar/Completions.tsx +160 -0
- package/src/editor/sidebar/Validation.tsx +9 -10
- package/src/editor/ui/PerfectTree.tsx +19 -3
- package/src/editor/ui/SimpleTabs.tsx +4 -1
- package/src/revision.ts +2 -2
- package/src/types.ts +1 -0
- package/dist/editor/menubar/BrowseHistory.d.ts +0 -6
- package/dist/editor/menubar/BrowseHistory.js +0 -11
- package/dist/editor/menubar/BrowseHistory.js.map +0 -1
- package/src/editor/menubar/BrowseHistory.tsx +0 -28
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogContent,
|
|
4
|
+
DialogHeader,
|
|
5
|
+
DialogTitle,
|
|
6
|
+
} from "../components/ui/dialog";
|
|
2
7
|
import { useEditContext } from "./client/editContext";
|
|
3
8
|
import { ReactNode, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { Button } from "
|
|
9
|
+
import { Button } from "../components/ui/button";
|
|
5
10
|
import DialogButtons from "./ui/DialogButtons";
|
|
6
11
|
import { Rect } from "./utils";
|
|
7
12
|
import { classNames } from "primereact/utils";
|
|
8
13
|
import {
|
|
9
14
|
PictureField,
|
|
10
15
|
PictureRawValue,
|
|
16
|
+
PictureRawVariant,
|
|
11
17
|
PictureValue,
|
|
12
18
|
PictureVariant,
|
|
13
19
|
} from "./fieldTypes";
|
|
20
|
+
import { RotateCcw } from "lucide-react";
|
|
21
|
+
import { SimpleTabs, Tab } from "./ui/SimpleTabs";
|
|
22
|
+
import { AspectRatioSelector } from "./AspectRatioSelector";
|
|
14
23
|
|
|
15
24
|
export function PictureCropper({
|
|
16
25
|
field,
|
|
17
26
|
onClose,
|
|
18
|
-
variantName:
|
|
27
|
+
variantName: initialVariantName,
|
|
19
28
|
}: {
|
|
20
29
|
field: PictureField;
|
|
21
30
|
variantName: string;
|
|
@@ -28,6 +37,22 @@ export function PictureCropper({
|
|
|
28
37
|
width: number;
|
|
29
38
|
height: number;
|
|
30
39
|
} | null>(null);
|
|
40
|
+
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
|
41
|
+
|
|
42
|
+
// Track selected aspect ratio for each variant
|
|
43
|
+
const [selectedAspectRatio, setSelectedAspectRatio] = useState<number | null>(
|
|
44
|
+
null,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Track crop rectangles for each variant separately
|
|
48
|
+
const [variantRects, setVariantRects] = useState<
|
|
49
|
+
Map<string, Rect | undefined>
|
|
50
|
+
>(new Map());
|
|
51
|
+
|
|
52
|
+
// Cache image dimensions by URL to avoid showing loading indicator for already loaded images
|
|
53
|
+
const imageDimensionsCache = useRef<
|
|
54
|
+
Map<string, { width: number; height: number }>
|
|
55
|
+
>(new Map());
|
|
31
56
|
|
|
32
57
|
const imageRef = useRef<HTMLDivElement>(null);
|
|
33
58
|
const [rect, setRect] = useState<Rect>();
|
|
@@ -46,6 +71,35 @@ export function PictureCropper({
|
|
|
46
71
|
const [resizeEdge, setResizeEdge] = useState<string | null>(null);
|
|
47
72
|
const resizeEdgeRef = useRef<string | null>(null);
|
|
48
73
|
|
|
74
|
+
// Helper function to create default region for aspect ratio locked variants
|
|
75
|
+
const createDefaultRegion = (): Rect | undefined => {
|
|
76
|
+
if (!selectedAspectRatio || !actualImageDimensions) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Always use the actual loaded image dimensions for aspect ratio calculations
|
|
81
|
+
const sourceWidth = actualImageDimensions.width;
|
|
82
|
+
const sourceHeight = actualImageDimensions.height;
|
|
83
|
+
const sourceAspectRatio = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
|
|
84
|
+
const targetAspectRatio = selectedAspectRatio;
|
|
85
|
+
|
|
86
|
+
// Calculate maximum size that fits the target aspect ratio within the source image
|
|
87
|
+
let width = 1.0; // Start with full width
|
|
88
|
+
let height = (width / targetAspectRatio) * sourceAspectRatio;
|
|
89
|
+
|
|
90
|
+
// If height exceeds bounds, start with full height instead
|
|
91
|
+
if (height > 1.0) {
|
|
92
|
+
height = 1.0;
|
|
93
|
+
width = (height * targetAspectRatio) / sourceAspectRatio;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Center the rectangle
|
|
97
|
+
const x = (1.0 - width) / 2;
|
|
98
|
+
const y = (1.0 - height) / 2;
|
|
99
|
+
|
|
100
|
+
return { x, y, width, height };
|
|
101
|
+
};
|
|
102
|
+
|
|
49
103
|
const getResizeEdge = (
|
|
50
104
|
pos: { x: number; y: number },
|
|
51
105
|
rect?: Rect,
|
|
@@ -86,35 +140,130 @@ export function PictureCropper({
|
|
|
86
140
|
setRawValue(raw);
|
|
87
141
|
}, [field]);
|
|
88
142
|
|
|
143
|
+
// Set initial tab based on the provided variant name
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (pictureValue?.variants && initialVariantName) {
|
|
146
|
+
const initialIndex = pictureValue.variants.findIndex(
|
|
147
|
+
(variant) => variant.name === initialVariantName,
|
|
148
|
+
);
|
|
149
|
+
if (initialIndex >= 0) {
|
|
150
|
+
setActiveTabIndex(initialIndex);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}, [pictureValue, initialVariantName]);
|
|
154
|
+
|
|
89
155
|
const selectedVariant =
|
|
90
|
-
pictureValue &&
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
156
|
+
pictureValue &&
|
|
157
|
+
pictureValue.variants &&
|
|
158
|
+
pictureValue.variants[activeTabIndex]
|
|
159
|
+
? pictureValue.variants[activeTabIndex]
|
|
94
160
|
: null;
|
|
95
161
|
|
|
162
|
+
const selectedVariantName = selectedVariant?.name || "";
|
|
163
|
+
|
|
164
|
+
// Update selected aspect ratio when switching variants
|
|
96
165
|
useEffect(() => {
|
|
166
|
+
if (selectedVariant) {
|
|
167
|
+
// If variant has locked aspect ratio, use that
|
|
168
|
+
if (selectedVariant.aspectRatioLock) {
|
|
169
|
+
setSelectedAspectRatio(selectedVariant.aspectRatioLock);
|
|
170
|
+
} else {
|
|
171
|
+
// Default to free form if no lock
|
|
172
|
+
setSelectedAspectRatio(null);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, [selectedVariant]);
|
|
176
|
+
|
|
177
|
+
// Manage image dimensions when the image source changes between variants
|
|
178
|
+
const currentImageSrc = selectedVariant?.originalSrc ?? selectedVariant?.src;
|
|
179
|
+
const previousImageSrcRef = useRef<string | undefined>(undefined);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (currentImageSrc && currentImageSrc !== previousImageSrcRef.current) {
|
|
183
|
+
// Check if we have cached dimensions for this image
|
|
184
|
+
const cachedDimensions =
|
|
185
|
+
imageDimensionsCache.current.get(currentImageSrc);
|
|
186
|
+
if (cachedDimensions) {
|
|
187
|
+
setActualImageDimensions(cachedDimensions);
|
|
188
|
+
} else {
|
|
189
|
+
// Only reset to null if we don't have cached dimensions
|
|
190
|
+
setActualImageDimensions(null);
|
|
191
|
+
}
|
|
192
|
+
previousImageSrcRef.current = currentImageSrc;
|
|
193
|
+
}
|
|
194
|
+
}, [currentImageSrc]);
|
|
195
|
+
|
|
196
|
+
// Load crop rectangle when switching variants
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!selectedVariantName) return;
|
|
199
|
+
|
|
200
|
+
// Check if we have a current working rect for this variant
|
|
201
|
+
const existingRect = variantRects.get(selectedVariantName);
|
|
202
|
+
|
|
203
|
+
// If we have a valid rect, use it
|
|
204
|
+
if (existingRect !== undefined) {
|
|
205
|
+
setRect(existingRect);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Initialize rect for this variant if not already done
|
|
210
|
+
let initialRect: Rect | undefined;
|
|
211
|
+
|
|
212
|
+
// Try to load from saved region if this variant hasn't been touched yet
|
|
97
213
|
if (
|
|
98
214
|
selectedVariant?.region &&
|
|
99
|
-
(!
|
|
215
|
+
(!selectedAspectRatio ||
|
|
100
216
|
Math.abs(
|
|
101
|
-
|
|
217
|
+
selectedAspectRatio -
|
|
102
218
|
(selectedVariant.region.width * selectedVariant.width) /
|
|
103
219
|
(selectedVariant.region.height * selectedVariant.height),
|
|
104
220
|
) < 0.1) &&
|
|
105
221
|
selectedVariant.region.width > 0 &&
|
|
106
222
|
selectedVariant.region.height > 0
|
|
107
223
|
) {
|
|
108
|
-
|
|
224
|
+
initialRect = {
|
|
109
225
|
width: selectedVariant.region.width,
|
|
110
226
|
height: selectedVariant.region.height,
|
|
111
227
|
y: selectedVariant.region.y,
|
|
112
228
|
x: selectedVariant.region.x,
|
|
113
|
-
}
|
|
229
|
+
};
|
|
114
230
|
} else {
|
|
115
|
-
|
|
231
|
+
// Try to create a default region if aspect ratio is locked
|
|
232
|
+
initialRect = createDefaultRegion();
|
|
116
233
|
}
|
|
117
|
-
|
|
234
|
+
|
|
235
|
+
// Store in variantRects and set as current rect
|
|
236
|
+
setVariantRects((prev) =>
|
|
237
|
+
new Map(prev).set(selectedVariantName, initialRect),
|
|
238
|
+
);
|
|
239
|
+
setRect(initialRect);
|
|
240
|
+
}, [selectedVariant, selectedVariantName]);
|
|
241
|
+
|
|
242
|
+
// Create default region when image dimensions become available
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (
|
|
245
|
+
selectedVariant &&
|
|
246
|
+
actualImageDimensions &&
|
|
247
|
+
!selectedVariant?.region &&
|
|
248
|
+
selectedAspectRatio
|
|
249
|
+
) {
|
|
250
|
+
const defaultRect = createDefaultRegion();
|
|
251
|
+
setVariantRects((prev) =>
|
|
252
|
+
new Map(prev).set(selectedVariant.name, defaultRect),
|
|
253
|
+
);
|
|
254
|
+
setRect(defaultRect);
|
|
255
|
+
}
|
|
256
|
+
}, [actualImageDimensions, selectedVariant, selectedAspectRatio]);
|
|
257
|
+
|
|
258
|
+
// Update working state when rect changes (but only if user is actively editing)
|
|
259
|
+
const isUserEditingRef = useRef(false);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (selectedVariantName && isUserEditingRef.current) {
|
|
263
|
+
setVariantRects((prev) => new Map(prev).set(selectedVariantName, rect));
|
|
264
|
+
isUserEditingRef.current = false;
|
|
265
|
+
}
|
|
266
|
+
}, [rect, selectedVariantName]);
|
|
118
267
|
|
|
119
268
|
useEffect(() => {
|
|
120
269
|
if (!rect) return;
|
|
@@ -131,15 +280,21 @@ export function PictureCropper({
|
|
|
131
280
|
if (!rawValue) return;
|
|
132
281
|
if (!selectedVariantName) return;
|
|
133
282
|
if (selectedVariant) {
|
|
134
|
-
|
|
135
|
-
|
|
283
|
+
// Create a deep copy of rawValue to avoid mutations
|
|
284
|
+
const newRawValue = JSON.parse(JSON.stringify(rawValue));
|
|
285
|
+
|
|
286
|
+
let selected = newRawValue.Variants?.find(
|
|
287
|
+
(x: PictureRawVariant) => x.Name == selectedVariantName,
|
|
136
288
|
);
|
|
137
289
|
if (!selected) {
|
|
138
290
|
selected = {
|
|
139
291
|
Name: selectedVariantName,
|
|
140
292
|
MediaId: selectedVariant.mediaId!,
|
|
141
293
|
};
|
|
142
|
-
|
|
294
|
+
if (!newRawValue.Variants) {
|
|
295
|
+
newRawValue.Variants = [];
|
|
296
|
+
}
|
|
297
|
+
newRawValue.Variants.push(selected);
|
|
143
298
|
}
|
|
144
299
|
if (selected) {
|
|
145
300
|
if (rect)
|
|
@@ -150,7 +305,7 @@ export function PictureCropper({
|
|
|
150
305
|
Height: rect.height,
|
|
151
306
|
};
|
|
152
307
|
else selected.Region = undefined;
|
|
153
|
-
setRawValue(
|
|
308
|
+
setRawValue(newRawValue);
|
|
154
309
|
}
|
|
155
310
|
}
|
|
156
311
|
}, [rect]);
|
|
@@ -187,69 +342,284 @@ export function PictureCropper({
|
|
|
187
342
|
};
|
|
188
343
|
|
|
189
344
|
let newRect = { ...rect };
|
|
190
|
-
const aspectRatio =
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
newRect.
|
|
207
|
-
|
|
208
|
-
newRect.height =
|
|
209
|
-
(newRect.width / aspectRatio) * originalAspectRatio;
|
|
210
|
-
}
|
|
345
|
+
const aspectRatio = selectedAspectRatio;
|
|
346
|
+
const sourceWidth = actualImageDimensions?.width || 0;
|
|
347
|
+
const sourceHeight = actualImageDimensions?.height || 0;
|
|
348
|
+
const sourceAspectRatio =
|
|
349
|
+
sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
|
|
350
|
+
|
|
351
|
+
// Handle resize based on edge - structure to avoid conflicts between corner handles
|
|
352
|
+
if (resizeEdgeRef.current === "w") {
|
|
353
|
+
// Pure west edge - resize from left, keep right edge fixed
|
|
354
|
+
const rightEdge = rect.x + rect.width;
|
|
355
|
+
const newWidth = Math.max(0.01, rightEdge - pos.x);
|
|
356
|
+
newRect.width = newWidth;
|
|
357
|
+
newRect.x = rightEdge - newWidth;
|
|
358
|
+
if (aspectRatio) {
|
|
359
|
+
const newHeight = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
360
|
+
const centerY = rect.y + rect.height / 2;
|
|
361
|
+
newRect.y = centerY - newHeight / 2;
|
|
362
|
+
newRect.height = newHeight;
|
|
211
363
|
}
|
|
212
|
-
}
|
|
213
|
-
|
|
364
|
+
} else if (resizeEdgeRef.current === "e") {
|
|
365
|
+
// Pure east edge - resize from right, keep left edge fixed
|
|
214
366
|
const newWidth = pos.x - rect.x;
|
|
215
367
|
if (newWidth > 0) {
|
|
216
368
|
newRect.width = newWidth;
|
|
217
369
|
if (aspectRatio) {
|
|
218
|
-
|
|
219
|
-
|
|
370
|
+
const newHeight = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
371
|
+
const centerY = rect.y + rect.height / 2;
|
|
372
|
+
newRect.y = centerY - newHeight / 2;
|
|
373
|
+
newRect.height = newHeight;
|
|
220
374
|
}
|
|
221
375
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const
|
|
376
|
+
} else if (resizeEdgeRef.current === "n") {
|
|
377
|
+
// Pure north edge - resize from top, keep bottom edge fixed
|
|
378
|
+
const bottomEdge = rect.y + rect.height;
|
|
379
|
+
const newHeight = bottomEdge - pos.y;
|
|
225
380
|
if (newHeight > 0) {
|
|
226
381
|
newRect.height = newHeight;
|
|
227
382
|
newRect.y = pos.y;
|
|
228
383
|
if (aspectRatio) {
|
|
229
|
-
|
|
230
|
-
|
|
384
|
+
const newWidth = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
385
|
+
const centerX = rect.x + rect.width / 2;
|
|
386
|
+
newRect.x = centerX - newWidth / 2;
|
|
387
|
+
newRect.width = newWidth;
|
|
231
388
|
}
|
|
232
389
|
}
|
|
233
|
-
}
|
|
234
|
-
|
|
390
|
+
} else if (resizeEdgeRef.current === "s") {
|
|
391
|
+
// Pure south edge - resize from bottom, keep top edge fixed
|
|
235
392
|
const newHeight = pos.y - rect.y;
|
|
236
393
|
if (newHeight > 0) {
|
|
237
394
|
newRect.height = newHeight;
|
|
238
395
|
if (aspectRatio) {
|
|
239
|
-
|
|
240
|
-
|
|
396
|
+
const newWidth = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
397
|
+
const centerX = rect.x + rect.width / 2;
|
|
398
|
+
newRect.x = centerX - newWidth / 2;
|
|
399
|
+
newRect.width = newWidth;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} else if (
|
|
403
|
+
resizeEdgeRef.current.includes("w") &&
|
|
404
|
+
resizeEdgeRef.current.includes("n")
|
|
405
|
+
) {
|
|
406
|
+
// Northwest corner - keep southeast corner fixed
|
|
407
|
+
const rightEdge = rect.x + rect.width;
|
|
408
|
+
const bottomEdge = rect.y + rect.height;
|
|
409
|
+
const newWidth = Math.max(0.01, rightEdge - pos.x);
|
|
410
|
+
const newHeight = Math.max(0.01, bottomEdge - pos.y);
|
|
411
|
+
newRect.width = newWidth;
|
|
412
|
+
newRect.height = newHeight;
|
|
413
|
+
newRect.x = rightEdge - newWidth;
|
|
414
|
+
newRect.y = bottomEdge - newHeight;
|
|
415
|
+
if (aspectRatio) {
|
|
416
|
+
// Use mouse movement direction to determine primary dimension
|
|
417
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
418
|
+
newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
419
|
+
newRect.y = bottomEdge - newRect.height;
|
|
420
|
+
} else {
|
|
421
|
+
newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
422
|
+
newRect.x = rightEdge - newRect.width;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else if (
|
|
426
|
+
resizeEdgeRef.current.includes("w") &&
|
|
427
|
+
resizeEdgeRef.current.includes("s")
|
|
428
|
+
) {
|
|
429
|
+
// Southwest corner - keep northeast corner fixed
|
|
430
|
+
const rightEdge = rect.x + rect.width;
|
|
431
|
+
const newWidth = Math.max(0.01, rightEdge - pos.x);
|
|
432
|
+
const newHeight = Math.max(0.01, pos.y - rect.y);
|
|
433
|
+
newRect.width = newWidth;
|
|
434
|
+
newRect.height = newHeight;
|
|
435
|
+
newRect.x = rightEdge - newWidth;
|
|
436
|
+
if (aspectRatio) {
|
|
437
|
+
// Use mouse movement direction to determine primary dimension
|
|
438
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
439
|
+
newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
440
|
+
} else {
|
|
441
|
+
newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
442
|
+
newRect.x = rightEdge - newRect.width;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} else if (
|
|
446
|
+
resizeEdgeRef.current.includes("e") &&
|
|
447
|
+
resizeEdgeRef.current.includes("n")
|
|
448
|
+
) {
|
|
449
|
+
// Northeast corner - keep southwest corner fixed
|
|
450
|
+
const bottomEdge = rect.y + rect.height;
|
|
451
|
+
const newWidth = Math.max(0.01, pos.x - rect.x);
|
|
452
|
+
const newHeight = Math.max(0.01, bottomEdge - pos.y);
|
|
453
|
+
newRect.width = newWidth;
|
|
454
|
+
newRect.height = newHeight;
|
|
455
|
+
newRect.y = bottomEdge - newHeight;
|
|
456
|
+
if (aspectRatio) {
|
|
457
|
+
// Use mouse movement direction to determine primary dimension
|
|
458
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
459
|
+
newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
460
|
+
newRect.y = bottomEdge - newRect.height;
|
|
461
|
+
} else {
|
|
462
|
+
newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else if (
|
|
466
|
+
resizeEdgeRef.current.includes("e") &&
|
|
467
|
+
resizeEdgeRef.current.includes("s")
|
|
468
|
+
) {
|
|
469
|
+
// Southeast corner - keep northwest corner fixed
|
|
470
|
+
const newWidth = Math.max(0.01, pos.x - rect.x);
|
|
471
|
+
const newHeight = Math.max(0.01, pos.y - rect.y);
|
|
472
|
+
newRect.width = newWidth;
|
|
473
|
+
newRect.height = newHeight;
|
|
474
|
+
if (aspectRatio) {
|
|
475
|
+
// Use mouse movement direction to determine primary dimension
|
|
476
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
477
|
+
newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
|
|
478
|
+
} else {
|
|
479
|
+
newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Constrain to bounds while maintaining aspect ratio
|
|
485
|
+
// For west edge resizes, maintain the right edge position when hitting left bound
|
|
486
|
+
if (newRect.x < 0) {
|
|
487
|
+
if (resizeEdgeRef.current?.includes("w")) {
|
|
488
|
+
const rightEdge = rect.x + rect.width; // Original right edge position
|
|
489
|
+
newRect.x = 0;
|
|
490
|
+
newRect.width = rightEdge; // Width = distance from left bound to original right edge
|
|
491
|
+
// Recalculate height if aspect ratio is locked
|
|
492
|
+
if (aspectRatio) {
|
|
493
|
+
const newHeight = (rightEdge / aspectRatio) * sourceAspectRatio;
|
|
494
|
+
if (resizeEdgeRef.current === "w") {
|
|
495
|
+
const centerY = rect.y + rect.height / 2;
|
|
496
|
+
newRect.y = centerY - newHeight / 2;
|
|
497
|
+
}
|
|
498
|
+
newRect.height = newHeight;
|
|
241
499
|
}
|
|
500
|
+
} else {
|
|
501
|
+
newRect.x = 0;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// For north edge resizes, maintain the bottom edge position when hitting top bound
|
|
505
|
+
if (newRect.y < 0) {
|
|
506
|
+
if (resizeEdgeRef.current?.includes("n")) {
|
|
507
|
+
const bottomEdge = rect.y + rect.height;
|
|
508
|
+
newRect.y = 0;
|
|
509
|
+
newRect.height = bottomEdge; // Keep bottom edge fixed
|
|
510
|
+
// Recalculate width if aspect ratio is locked
|
|
511
|
+
if (aspectRatio) {
|
|
512
|
+
const newWidth =
|
|
513
|
+
(newRect.height * aspectRatio) / sourceAspectRatio;
|
|
514
|
+
if (resizeEdgeRef.current === "n") {
|
|
515
|
+
const centerX = rect.x + rect.width / 2;
|
|
516
|
+
newRect.x = centerX - newWidth / 2;
|
|
517
|
+
}
|
|
518
|
+
newRect.width = newWidth;
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
newRect.y = 0;
|
|
242
522
|
}
|
|
243
523
|
}
|
|
244
524
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
525
|
+
// Apply bounds constraints and maintain aspect ratio
|
|
526
|
+
let boundedRect = { ...newRect };
|
|
527
|
+
if (boundedRect.x + boundedRect.width > 1) {
|
|
528
|
+
boundedRect.width = 1 - boundedRect.x;
|
|
529
|
+
// Re-apply aspect ratio if it was locked
|
|
530
|
+
if (aspectRatio) {
|
|
531
|
+
const newHeight =
|
|
532
|
+
(boundedRect.width / aspectRatio) * sourceAspectRatio;
|
|
533
|
+
// Maintain anchoring behavior based on resize edge
|
|
534
|
+
if (
|
|
535
|
+
resizeEdgeRef.current === "w" ||
|
|
536
|
+
resizeEdgeRef.current === "e"
|
|
537
|
+
) {
|
|
538
|
+
// For horizontal resizes, keep centered vertically
|
|
539
|
+
const centerY = rect.y + rect.height / 2;
|
|
540
|
+
boundedRect.y = centerY - newHeight / 2;
|
|
541
|
+
}
|
|
542
|
+
boundedRect.height = newHeight;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (boundedRect.y + boundedRect.height > 1) {
|
|
546
|
+
boundedRect.height = 1 - boundedRect.y;
|
|
547
|
+
// Re-apply aspect ratio if it was locked
|
|
548
|
+
if (aspectRatio) {
|
|
549
|
+
const newWidth =
|
|
550
|
+
(boundedRect.height * aspectRatio) / sourceAspectRatio;
|
|
551
|
+
// Maintain anchoring behavior based on resize edge
|
|
552
|
+
if (
|
|
553
|
+
resizeEdgeRef.current === "n" ||
|
|
554
|
+
resizeEdgeRef.current === "s"
|
|
555
|
+
) {
|
|
556
|
+
// For vertical resizes, keep centered horizontally
|
|
557
|
+
const centerX = rect.x + rect.width / 2;
|
|
558
|
+
boundedRect.x = centerX - newWidth / 2;
|
|
559
|
+
}
|
|
560
|
+
boundedRect.width = newWidth;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Final check - if aspect ratio re-calculation pushes us out of bounds again,
|
|
565
|
+
// we need to find the maximum size that fits both constraints
|
|
566
|
+
if (
|
|
567
|
+
aspectRatio &&
|
|
568
|
+
(boundedRect.x + boundedRect.width > 1 ||
|
|
569
|
+
boundedRect.y + boundedRect.height > 1)
|
|
570
|
+
) {
|
|
571
|
+
const maxWidth = 1 - boundedRect.x;
|
|
572
|
+
const maxHeight = 1 - boundedRect.y;
|
|
573
|
+
const maxHeightFromWidth =
|
|
574
|
+
(maxWidth / aspectRatio) * sourceAspectRatio;
|
|
575
|
+
const maxWidthFromHeight =
|
|
576
|
+
(maxHeight * aspectRatio) / sourceAspectRatio;
|
|
250
577
|
|
|
578
|
+
if (maxHeightFromWidth <= maxHeight) {
|
|
579
|
+
// Width is the limiting factor
|
|
580
|
+
boundedRect.width = maxWidth;
|
|
581
|
+
boundedRect.height = maxHeightFromWidth;
|
|
582
|
+
// Maintain anchoring for horizontal resizes
|
|
583
|
+
if (
|
|
584
|
+
resizeEdgeRef.current === "w" ||
|
|
585
|
+
resizeEdgeRef.current === "e"
|
|
586
|
+
) {
|
|
587
|
+
const centerY = rect.y + rect.height / 2;
|
|
588
|
+
boundedRect.y = Math.max(
|
|
589
|
+
0,
|
|
590
|
+
Math.min(
|
|
591
|
+
1 - boundedRect.height,
|
|
592
|
+
centerY - boundedRect.height / 2,
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
// Height is the limiting factor
|
|
598
|
+
boundedRect.height = maxHeight;
|
|
599
|
+
boundedRect.width = maxWidthFromHeight;
|
|
600
|
+
// Maintain anchoring for vertical resizes
|
|
601
|
+
if (
|
|
602
|
+
resizeEdgeRef.current === "n" ||
|
|
603
|
+
resizeEdgeRef.current === "s"
|
|
604
|
+
) {
|
|
605
|
+
const centerX = rect.x + rect.width / 2;
|
|
606
|
+
boundedRect.x = Math.max(
|
|
607
|
+
0,
|
|
608
|
+
Math.min(
|
|
609
|
+
1 - boundedRect.width,
|
|
610
|
+
centerX - boundedRect.width / 2,
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
newRect = boundedRect;
|
|
618
|
+
|
|
619
|
+
isUserEditingRef.current = true;
|
|
251
620
|
setRect(newRect);
|
|
252
621
|
} else if (movingRef.current) {
|
|
622
|
+
isUserEditingRef.current = true;
|
|
253
623
|
setRect({
|
|
254
624
|
...rect,
|
|
255
625
|
x: Math.max(
|
|
@@ -268,18 +638,12 @@ export function PictureCropper({
|
|
|
268
638
|
),
|
|
269
639
|
});
|
|
270
640
|
} else {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
? selectedVariant.height
|
|
278
|
-
: actualImageDimensions?.height || 0;
|
|
279
|
-
const originalAspectRatio =
|
|
280
|
-
actualHeight > 0 ? actualWidth / actualHeight : 1;
|
|
281
|
-
|
|
282
|
-
const aspectRatio = selectedVariant.aspectRatioLock;
|
|
641
|
+
const sourceWidth = actualImageDimensions?.width || 0;
|
|
642
|
+
const sourceHeight = actualImageDimensions?.height || 0;
|
|
643
|
+
const sourceAspectRatio =
|
|
644
|
+
sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
|
|
645
|
+
|
|
646
|
+
const aspectRatio = selectedAspectRatio;
|
|
283
647
|
|
|
284
648
|
const currentX = (ev.clientX - bounds.left) / bounds.width;
|
|
285
649
|
const currentY = (ev.clientY - bounds.top) / bounds.height;
|
|
@@ -294,20 +658,42 @@ export function PictureCropper({
|
|
|
294
658
|
|
|
295
659
|
if (aspectRatio) {
|
|
296
660
|
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
297
|
-
height = (width / aspectRatio) *
|
|
661
|
+
height = (width / aspectRatio) * sourceAspectRatio;
|
|
298
662
|
} else {
|
|
299
|
-
width = (height * aspectRatio) /
|
|
663
|
+
width = (height * aspectRatio) / sourceAspectRatio;
|
|
300
664
|
}
|
|
301
665
|
}
|
|
302
666
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
667
|
+
// Apply bounds constraints and maintain aspect ratio
|
|
668
|
+
if (aspectRatio) {
|
|
669
|
+
const maxWidth = 1 - minX;
|
|
670
|
+
const maxHeight = 1 - minY;
|
|
671
|
+
const maxHeightFromWidth =
|
|
672
|
+
(maxWidth / aspectRatio) * sourceAspectRatio;
|
|
673
|
+
const maxWidthFromHeight =
|
|
674
|
+
(maxHeight * aspectRatio) / sourceAspectRatio;
|
|
307
675
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
676
|
+
if (maxHeightFromWidth <= maxHeight) {
|
|
677
|
+
// Width is the limiting factor
|
|
678
|
+
if (width > maxWidth) {
|
|
679
|
+
width = maxWidth;
|
|
680
|
+
height = maxHeightFromWidth;
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
// Height is the limiting factor
|
|
684
|
+
if (height > maxHeight) {
|
|
685
|
+
height = maxHeight;
|
|
686
|
+
width = maxWidthFromHeight;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
// No aspect ratio lock, just apply simple bounds
|
|
691
|
+
if (width + minX > 1) {
|
|
692
|
+
width = 1 - minX;
|
|
693
|
+
}
|
|
694
|
+
if (height + minY > 1) {
|
|
695
|
+
height = 1 - minY;
|
|
696
|
+
}
|
|
311
697
|
}
|
|
312
698
|
|
|
313
699
|
// Ensure final dimensions are non-negative
|
|
@@ -315,6 +701,7 @@ export function PictureCropper({
|
|
|
315
701
|
height = Math.max(0, height);
|
|
316
702
|
|
|
317
703
|
const newRect = { x: minX, y: minY, width, height };
|
|
704
|
+
isUserEditingRef.current = true;
|
|
318
705
|
setRect(newRect);
|
|
319
706
|
}
|
|
320
707
|
ev.preventDefault();
|
|
@@ -324,6 +711,7 @@ export function PictureCropper({
|
|
|
324
711
|
|
|
325
712
|
const handleMouseUp = () => {
|
|
326
713
|
if (!rectRef.current || !rectRef.current.width || !rectRef.current.height) {
|
|
714
|
+
isUserEditingRef.current = true;
|
|
327
715
|
setRect(undefined);
|
|
328
716
|
}
|
|
329
717
|
movingRef.current = false;
|
|
@@ -364,6 +752,7 @@ export function PictureCropper({
|
|
|
364
752
|
y: pos.y - rect!.y,
|
|
365
753
|
};
|
|
366
754
|
} else {
|
|
755
|
+
isUserEditingRef.current = true;
|
|
367
756
|
setRect({
|
|
368
757
|
x: (ev.clientX - bounds.left) / bounds.width,
|
|
369
758
|
y: (ev.clientY - bounds.top) / bounds.height,
|
|
@@ -377,176 +766,278 @@ export function PictureCropper({
|
|
|
377
766
|
window.addEventListener("mouseup", handleMouseUp);
|
|
378
767
|
};
|
|
379
768
|
|
|
380
|
-
if (!
|
|
769
|
+
if (!pictureValue?.variants || pictureValue.variants.length === 0) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Create tabs for each variant
|
|
774
|
+
const variantTabs: Tab[] = pictureValue.variants.map((variant, index) => ({
|
|
775
|
+
id: `variant-${index}`,
|
|
776
|
+
label: variant.name,
|
|
777
|
+
content: null, // Content will be rendered separately
|
|
778
|
+
}));
|
|
381
779
|
|
|
382
780
|
const imageBounds = imageRef.current?.getBoundingClientRect();
|
|
383
|
-
const
|
|
384
|
-
|
|
781
|
+
const sourceWidth = actualImageDimensions?.width || 0;
|
|
782
|
+
const sourceHeight = actualImageDimensions?.height || 0;
|
|
783
|
+
|
|
784
|
+
// For output dimensions, prefer variant dimensions if specified
|
|
785
|
+
const outputWidth =
|
|
786
|
+
selectedVariant?.width && selectedVariant.width > 0
|
|
385
787
|
? selectedVariant.width
|
|
386
|
-
:
|
|
387
|
-
const
|
|
388
|
-
selectedVariant.height > 0
|
|
788
|
+
: sourceWidth;
|
|
789
|
+
const outputHeight =
|
|
790
|
+
selectedVariant?.height && selectedVariant.height > 0
|
|
389
791
|
? selectedVariant.height
|
|
390
|
-
:
|
|
792
|
+
: sourceHeight;
|
|
793
|
+
|
|
391
794
|
const scale =
|
|
392
|
-
imageBounds &&
|
|
393
|
-
const widthPx = rect ? Math.round(rect.width *
|
|
394
|
-
const heightPx = rect ? Math.round(rect.height *
|
|
795
|
+
imageBounds && outputWidth > 0 ? outputWidth / imageBounds.width : 1;
|
|
796
|
+
const widthPx = rect ? Math.round(rect.width * outputWidth) : 0;
|
|
797
|
+
const heightPx = rect ? Math.round(rect.height * outputHeight) : 0;
|
|
395
798
|
|
|
396
799
|
return (
|
|
397
800
|
<>
|
|
398
|
-
<Dialog
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
801
|
+
<Dialog open={true} onOpenChange={(open) => !open && onClose()}>
|
|
802
|
+
<DialogContent className="h-[75vh] max-h-none w-[75vw] max-w-none">
|
|
803
|
+
<DialogHeader>
|
|
804
|
+
<DialogTitle>Crop {field.name}</DialogTitle>
|
|
805
|
+
</DialogHeader>
|
|
806
|
+
<div className="justify flex h-full flex-col">
|
|
807
|
+
<div className="flex flex-1 gap-2">
|
|
808
|
+
<div className="flex w-72 flex-col gap-3 p-6 text-sm">
|
|
809
|
+
{/* Variant Tabs */}
|
|
810
|
+
<div className="mb-4">
|
|
811
|
+
<SimpleTabs
|
|
812
|
+
tabs={variantTabs}
|
|
813
|
+
activeTab={activeTabIndex}
|
|
814
|
+
setActiveTab={setActiveTabIndex}
|
|
815
|
+
className="border-gray-2 border-b text-xs"
|
|
816
|
+
tabClassName="flex-1 text-center"
|
|
817
|
+
/>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
{selectedVariant && (
|
|
821
|
+
<>
|
|
822
|
+
<div className="mb-4">
|
|
823
|
+
<AspectRatioSelector
|
|
824
|
+
selectedRatio={selectedAspectRatio}
|
|
825
|
+
onRatioChange={(ratio) => {
|
|
826
|
+
setSelectedAspectRatio(ratio);
|
|
827
|
+
// Reset the crop rect to default when aspect ratio changes
|
|
828
|
+
const defaultRect = createDefaultRegion();
|
|
829
|
+
isUserEditingRef.current = true;
|
|
830
|
+
setRect(defaultRect);
|
|
831
|
+
}}
|
|
832
|
+
lockedRatio={selectedVariant.aspectRatioLock}
|
|
833
|
+
/>
|
|
834
|
+
</div>
|
|
835
|
+
<LabelAndValue
|
|
836
|
+
label="Image Dimensions"
|
|
837
|
+
value={
|
|
838
|
+
selectedVariant.width > 0 &&
|
|
839
|
+
selectedVariant.height > 0 ? (
|
|
840
|
+
<>
|
|
841
|
+
{selectedVariant.width} x {selectedVariant.height}
|
|
842
|
+
</>
|
|
843
|
+
) : actualImageDimensions ? (
|
|
844
|
+
<>
|
|
845
|
+
{actualImageDimensions.width} x{" "}
|
|
846
|
+
{actualImageDimensions.height} (actual)
|
|
847
|
+
</>
|
|
848
|
+
) : (
|
|
849
|
+
<span className="text-gray-500">Loading...</span>
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
/>
|
|
853
|
+
|
|
854
|
+
{selectedVariant.minWidth && (
|
|
855
|
+
<LabelAndValue
|
|
856
|
+
label="Minimum Width:"
|
|
857
|
+
value={selectedVariant.minWidth}
|
|
858
|
+
/>
|
|
859
|
+
)}
|
|
860
|
+
{selectedVariant.minHeight && (
|
|
861
|
+
<LabelAndValue
|
|
862
|
+
label="Minimum Height:"
|
|
863
|
+
value={selectedVariant.minHeight}
|
|
864
|
+
/>
|
|
865
|
+
)}
|
|
866
|
+
{rect && (
|
|
449
867
|
<>
|
|
450
|
-
|
|
868
|
+
<LabelAndValue
|
|
869
|
+
label="Selection"
|
|
870
|
+
value={
|
|
871
|
+
<>
|
|
872
|
+
{widthPx} x {heightPx}
|
|
873
|
+
</>
|
|
874
|
+
}
|
|
875
|
+
/>
|
|
876
|
+
{selectedVariant.minWidth &&
|
|
877
|
+
widthPx < selectedVariant.minWidth && (
|
|
878
|
+
<div className="text-red-500">
|
|
879
|
+
Minimum width not met!
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
{selectedVariant.minHeight &&
|
|
883
|
+
heightPx < selectedVariant.minHeight && (
|
|
884
|
+
<div className="text-red-500">
|
|
885
|
+
Minimum height not met!
|
|
886
|
+
</div>
|
|
887
|
+
)}
|
|
451
888
|
</>
|
|
452
|
-
}
|
|
453
|
-
/>
|
|
454
|
-
{selectedVariant.minWidth &&
|
|
455
|
-
widthPx < selectedVariant.minWidth && (
|
|
456
|
-
<div className="text-red-500">Minimum width not met!</div>
|
|
457
889
|
)}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
890
|
+
</>
|
|
891
|
+
)}
|
|
892
|
+
</div>
|
|
893
|
+
<div className="bg-gray-4 relative flex-1">
|
|
894
|
+
{selectedVariant && (
|
|
895
|
+
<div className="absolute inset-3 flex items-center justify-center select-none">
|
|
896
|
+
{/* Loading indicator when dimensions are not available */}
|
|
897
|
+
{!actualImageDimensions && (
|
|
898
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
899
|
+
<div className="text-gray-500">Loading image...</div>
|
|
462
900
|
</div>
|
|
463
901
|
)}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
902
|
+
{/* Hidden image for loading dimensions */}
|
|
903
|
+
{!actualImageDimensions && (
|
|
904
|
+
<img
|
|
905
|
+
className="pointer-events-none absolute opacity-0"
|
|
906
|
+
src={selectedVariant.originalSrc ?? selectedVariant.src}
|
|
907
|
+
onLoad={(e) => {
|
|
908
|
+
const img = e.target as HTMLImageElement;
|
|
909
|
+
const dimensions = {
|
|
910
|
+
width: img.naturalWidth,
|
|
911
|
+
height: img.naturalHeight,
|
|
912
|
+
};
|
|
913
|
+
// Cache the dimensions for this image URL
|
|
914
|
+
const imgSrc =
|
|
915
|
+
selectedVariant.originalSrc ?? selectedVariant.src;
|
|
916
|
+
if (imgSrc) {
|
|
917
|
+
imageDimensionsCache.current.set(
|
|
918
|
+
imgSrc,
|
|
919
|
+
dimensions,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
setActualImageDimensions(dimensions);
|
|
923
|
+
}}
|
|
924
|
+
/>
|
|
925
|
+
)}
|
|
926
|
+
{/* Only render the interactive image container when we have dimensions */}
|
|
927
|
+
{actualImageDimensions && (
|
|
928
|
+
<div
|
|
929
|
+
ref={imageRef}
|
|
930
|
+
className="relative max-h-full cursor-crosshair"
|
|
931
|
+
style={{
|
|
932
|
+
aspectRatio: `${sourceWidth}/${sourceHeight}`,
|
|
933
|
+
}}
|
|
934
|
+
onMouseDown={handleMouseDown}
|
|
935
|
+
onMouseMove={handleMouseMove}
|
|
936
|
+
>
|
|
937
|
+
<img
|
|
938
|
+
className="max-h-full max-w-full object-scale-down"
|
|
939
|
+
src={
|
|
940
|
+
selectedVariant.originalSrc ?? selectedVariant.src
|
|
941
|
+
}
|
|
942
|
+
/>
|
|
943
|
+
{rect && (
|
|
944
|
+
<>
|
|
945
|
+
{/* Gray mask overlay */}
|
|
946
|
+
<div
|
|
947
|
+
className="pointer-events-none absolute inset-0 bg-black/40"
|
|
948
|
+
style={{
|
|
949
|
+
clipPath: `polygon(0% 0%, 0% 100%, ${rect.x * 100}% 100%, ${rect.x * 100}% ${rect.y * 100}%, ${(rect.x + rect.width) * 100}% ${rect.y * 100}%, ${(rect.x + rect.width) * 100}% ${(rect.y + rect.height) * 100}%, ${rect.x * 100}% ${(rect.y + rect.height) * 100}%, ${rect.x * 100}% 100%, 100% 100%, 100% 0%)`,
|
|
950
|
+
}}
|
|
951
|
+
/>
|
|
952
|
+
|
|
953
|
+
{/* Crop area border and interaction */}
|
|
954
|
+
<div
|
|
955
|
+
className={classNames(
|
|
956
|
+
"absolute cursor-move border-2 border-dashed",
|
|
957
|
+
isValid
|
|
958
|
+
? "border-theme-secondary"
|
|
959
|
+
: "border-red-400",
|
|
960
|
+
)}
|
|
961
|
+
style={{
|
|
962
|
+
left: rect.x * 100 + "%",
|
|
963
|
+
top: rect.y * 100 + "%",
|
|
964
|
+
width: rect.width * 100 + "%",
|
|
965
|
+
height: rect.height * 100 + "%",
|
|
966
|
+
cursor: resizeEdge
|
|
967
|
+
? resizeEdge.length === 1
|
|
968
|
+
? `${resizeEdge}-resize`
|
|
969
|
+
: `${resizeEdge}-resize`
|
|
970
|
+
: "move",
|
|
971
|
+
}}
|
|
972
|
+
>
|
|
973
|
+
{/* Resize handles */}
|
|
974
|
+
<div className="absolute -top-1.5 -left-1.5 h-3 w-3 cursor-nw-resize rounded-full bg-white/90 shadow-lg" />
|
|
975
|
+
<div className="absolute -top-1.5 -right-1.5 h-3 w-3 cursor-ne-resize rounded-full bg-white/90 shadow-lg" />
|
|
976
|
+
<div className="absolute -bottom-1.5 -left-1.5 h-3 w-3 cursor-sw-resize rounded-full bg-white/90 shadow-lg" />
|
|
977
|
+
<div className="absolute -right-1.5 -bottom-1.5 h-3 w-3 cursor-se-resize rounded-full bg-white/90 shadow-lg" />
|
|
978
|
+
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 transform cursor-n-resize rounded-full bg-white/90 shadow-lg" />
|
|
979
|
+
<div className="absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 transform cursor-s-resize rounded-full bg-white/90 shadow-lg" />
|
|
980
|
+
<div className="absolute top-1/2 -left-1.5 h-3 w-3 -translate-y-1/2 transform cursor-w-resize rounded-full bg-white/90 shadow-lg" />
|
|
981
|
+
<div className="absolute top-1/2 -right-1.5 h-3 w-3 -translate-y-1/2 transform cursor-e-resize rounded-full bg-white/90 shadow-lg" />
|
|
982
|
+
|
|
983
|
+
{/* Dimensions text */}
|
|
984
|
+
{rect.width > 0.05 && (
|
|
985
|
+
<div
|
|
986
|
+
className={classNames(
|
|
987
|
+
"absolute right-1 bottom-1 rounded p-2 text-xs text-nowrap",
|
|
988
|
+
isValid
|
|
989
|
+
? "bg-black/40 text-white"
|
|
990
|
+
: "bg-red-400/40 text-white",
|
|
991
|
+
)}
|
|
992
|
+
>
|
|
993
|
+
{widthPx} x {heightPx}
|
|
994
|
+
</div>
|
|
995
|
+
)}
|
|
996
|
+
</div>
|
|
997
|
+
</>
|
|
998
|
+
)}
|
|
999
|
+
</div>
|
|
1000
|
+
)}
|
|
1001
|
+
</div>
|
|
1002
|
+
)}
|
|
522
1003
|
</div>
|
|
523
1004
|
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1005
|
+
<DialogButtons>
|
|
1006
|
+
<Button onClick={onClose} variant="ghost">
|
|
1007
|
+
Cancel
|
|
1008
|
+
</Button>
|
|
1009
|
+
<Button
|
|
1010
|
+
onClick={() => {
|
|
1011
|
+
// If there's an aspect ratio lock, restore the default centered, maximum-sized region
|
|
1012
|
+
const defaultRect = createDefaultRegion();
|
|
1013
|
+
isUserEditingRef.current = true;
|
|
1014
|
+
setRect(defaultRect);
|
|
1015
|
+
}}
|
|
1016
|
+
variant="outline"
|
|
1017
|
+
>
|
|
1018
|
+
<RotateCcw strokeWidth={1} />
|
|
1019
|
+
Reset
|
|
1020
|
+
</Button>
|
|
1021
|
+
<Button
|
|
1022
|
+
disabled={!isValid}
|
|
1023
|
+
onClick={() => {
|
|
1024
|
+
if (pictureValue) {
|
|
1025
|
+
if (field) {
|
|
1026
|
+
editContext?.operations.editField({
|
|
1027
|
+
field: field.descriptor,
|
|
1028
|
+
rawValue: JSON.stringify(rawValue),
|
|
1029
|
+
refresh: "immediate",
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
538
1032
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
</
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
</Button>
|
|
548
|
-
</DialogButtons>
|
|
549
|
-
</div>
|
|
1033
|
+
onClose();
|
|
1034
|
+
}}
|
|
1035
|
+
>
|
|
1036
|
+
Save
|
|
1037
|
+
</Button>
|
|
1038
|
+
</DialogButtons>
|
|
1039
|
+
</div>
|
|
1040
|
+
</DialogContent>
|
|
550
1041
|
</Dialog>
|
|
551
1042
|
</>
|
|
552
1043
|
);
|
|
@@ -561,8 +1052,8 @@ function LabelAndValue({
|
|
|
561
1052
|
}) {
|
|
562
1053
|
return (
|
|
563
1054
|
<div>
|
|
564
|
-
<div className="font-
|
|
565
|
-
<div>{value}</div>
|
|
1055
|
+
<div className="font-medium">{label}</div>
|
|
1056
|
+
<div className="text-xs font-light">{value}</div>
|
|
566
1057
|
</div>
|
|
567
1058
|
);
|
|
568
1059
|
}
|