@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.
Files changed (87) hide show
  1. package/dist/components/ui/badge.d.ts +1 -1
  2. package/dist/components/ui/button.d.ts +2 -2
  3. package/dist/components/ui/switch.js +1 -1
  4. package/dist/components/ui/switch.js.map +1 -1
  5. package/dist/config/config.js +18 -2
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/editor/AspectRatioSelector.d.ts +13 -0
  8. package/dist/editor/AspectRatioSelector.js +71 -0
  9. package/dist/editor/AspectRatioSelector.js.map +1 -0
  10. package/dist/editor/ConfirmationDialog.js +4 -5
  11. package/dist/editor/ConfirmationDialog.js.map +1 -1
  12. package/dist/editor/PictureCropper.d.ts +1 -1
  13. package/dist/editor/PictureCropper.js +466 -113
  14. package/dist/editor/PictureCropper.js.map +1 -1
  15. package/dist/editor/Terminal.js +5 -4
  16. package/dist/editor/Terminal.js.map +1 -1
  17. package/dist/editor/ai/AiTerminal.js +20 -2
  18. package/dist/editor/ai/AiTerminal.js.map +1 -1
  19. package/dist/editor/client/EditorClient.js +14 -3
  20. package/dist/editor/client/EditorClient.js.map +1 -1
  21. package/dist/editor/client/editContext.d.ts +3 -0
  22. package/dist/editor/client/editContext.js.map +1 -1
  23. package/dist/editor/commands/componentCommands.js +13 -5
  24. package/dist/editor/commands/componentCommands.js.map +1 -1
  25. package/dist/editor/media-selector/Preview.js +1 -1
  26. package/dist/editor/media-selector/Preview.js.map +1 -1
  27. package/dist/editor/page-editor-chrome/FrameMenu.js +48 -24
  28. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  29. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
  30. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +6 -6
  31. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  32. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +26 -4
  33. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  34. package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
  35. package/dist/editor/page-viewer/EditorForm.js +16 -2
  36. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  37. package/dist/editor/page-viewer/EditorFormPopup.d.ts +11 -0
  38. package/dist/editor/page-viewer/EditorFormPopup.js +41 -0
  39. package/dist/editor/page-viewer/EditorFormPopup.js.map +1 -0
  40. package/dist/editor/page-viewer/PageViewer.js +4 -6
  41. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  42. package/dist/editor/services/contextService.d.ts +26 -0
  43. package/dist/editor/services/contextService.js +102 -0
  44. package/dist/editor/services/contextService.js.map +1 -0
  45. package/dist/editor/sidebar/Completions.d.ts +1 -0
  46. package/dist/editor/sidebar/Completions.js +54 -0
  47. package/dist/editor/sidebar/Completions.js.map +1 -0
  48. package/dist/editor/sidebar/Validation.js +3 -3
  49. package/dist/editor/sidebar/Validation.js.map +1 -1
  50. package/dist/editor/ui/PerfectTree.js +17 -3
  51. package/dist/editor/ui/PerfectTree.js.map +1 -1
  52. package/dist/editor/ui/SimpleTabs.d.ts +2 -1
  53. package/dist/editor/ui/SimpleTabs.js +2 -2
  54. package/dist/editor/ui/SimpleTabs.js.map +1 -1
  55. package/dist/revision.d.ts +2 -2
  56. package/dist/revision.js +2 -2
  57. package/dist/styles.css +134 -30
  58. package/dist/types.d.ts +1 -0
  59. package/package.json +1 -1
  60. package/src/components/ui/switch.tsx +1 -1
  61. package/src/config/config.tsx +18 -1
  62. package/src/editor/AspectRatioSelector.tsx +146 -0
  63. package/src/editor/ConfirmationDialog.tsx +36 -45
  64. package/src/editor/PictureCropper.tsx +724 -233
  65. package/src/editor/Terminal.tsx +9 -8
  66. package/src/editor/ai/AiTerminal.tsx +58 -15
  67. package/src/editor/client/EditorClient.tsx +26 -1
  68. package/src/editor/client/editContext.ts +7 -0
  69. package/src/editor/commands/componentCommands.tsx +14 -9
  70. package/src/editor/media-selector/Preview.tsx +7 -5
  71. package/src/editor/page-editor-chrome/FrameMenu.tsx +70 -15
  72. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +9 -3
  73. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +31 -5
  74. package/src/editor/page-viewer/EditorForm.tsx +21 -1
  75. package/src/editor/page-viewer/EditorFormPopup.tsx +104 -0
  76. package/src/editor/page-viewer/PageViewer.tsx +3 -11
  77. package/src/editor/services/contextService.ts +146 -0
  78. package/src/editor/sidebar/Completions.tsx +160 -0
  79. package/src/editor/sidebar/Validation.tsx +9 -10
  80. package/src/editor/ui/PerfectTree.tsx +19 -3
  81. package/src/editor/ui/SimpleTabs.tsx +4 -1
  82. package/src/revision.ts +2 -2
  83. package/src/types.ts +1 -0
  84. package/dist/editor/menubar/BrowseHistory.d.ts +0 -6
  85. package/dist/editor/menubar/BrowseHistory.js +0 -11
  86. package/dist/editor/menubar/BrowseHistory.js.map +0 -1
  87. package/src/editor/menubar/BrowseHistory.tsx +0 -28
@@ -1,21 +1,30 @@
1
- import { Dialog } from "primereact/dialog";
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 "primereact/button";
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: selectedVariantName,
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 && pictureValue.variants
91
- ? pictureValue.variants?.find(
92
- (x: PictureVariant) => x.name == selectedVariantName,
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
- (!selectedVariant.aspectRatioLock ||
215
+ (!selectedAspectRatio ||
100
216
  Math.abs(
101
- selectedVariant.aspectRatioLock -
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
- setRect({
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
- setRect(undefined);
231
+ // Try to create a default region if aspect ratio is locked
232
+ initialRect = createDefaultRegion();
116
233
  }
117
- }, [selectedVariant]);
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
- let selected = rawValue.Variants?.find(
135
- (x) => x.Name == selectedVariantName,
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
- rawValue.Variants?.push(selected);
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(rawValue);
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 = selectedVariant.aspectRatioLock;
191
- const actualWidth =
192
- selectedVariant.width > 0
193
- ? selectedVariant.width
194
- : actualImageDimensions?.width || 0;
195
- const actualHeight =
196
- selectedVariant.height > 0
197
- ? selectedVariant.height
198
- : actualImageDimensions?.height || 0;
199
- const originalAspectRatio =
200
- actualHeight > 0 ? actualWidth / actualHeight : 1;
201
-
202
- if (resizeEdgeRef.current.includes("w")) {
203
- const newWidth = rect.width + rect.x - pos.x;
204
- if (newWidth > 0) {
205
- newRect.width = newWidth;
206
- newRect.x = pos.x;
207
- if (aspectRatio) {
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
- if (resizeEdgeRef.current.includes("e")) {
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
- newRect.height =
219
- (newRect.width / aspectRatio) * originalAspectRatio;
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
- if (resizeEdgeRef.current.includes("n")) {
224
- const newHeight = rect.height + rect.y - pos.y;
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
- newRect.width =
230
- (newRect.height * aspectRatio) / originalAspectRatio;
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
- if (resizeEdgeRef.current.includes("s")) {
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
- newRect.width =
240
- (newRect.height * aspectRatio) / originalAspectRatio;
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
- // Constrain to bounds
246
- if (newRect.x < 0) newRect.x = 0;
247
- if (newRect.y < 0) newRect.y = 0;
248
- if (newRect.x + newRect.width > 1) newRect.width = 1 - newRect.x;
249
- if (newRect.y + newRect.height > 1) newRect.height = 1 - newRect.y;
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 actualWidth =
272
- selectedVariant.width > 0
273
- ? selectedVariant.width
274
- : actualImageDimensions?.width || 0;
275
- const actualHeight =
276
- selectedVariant.height > 0
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) * originalAspectRatio;
661
+ height = (width / aspectRatio) * sourceAspectRatio;
298
662
  } else {
299
- width = (height * aspectRatio) / originalAspectRatio;
663
+ width = (height * aspectRatio) / sourceAspectRatio;
300
664
  }
301
665
  }
302
666
 
303
- if (width + minX > 1) {
304
- width = 1 - minX;
305
- if (aspectRatio) height = (width / aspectRatio) * originalAspectRatio;
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
- if (height + minY > 1) {
309
- height = 1 - minY;
310
- if (aspectRatio) width = (height * aspectRatio) / originalAspectRatio;
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 (!selectedVariant) return null;
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 actualWidth =
384
- selectedVariant.width > 0
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
- : actualImageDimensions?.width || 0;
387
- const actualHeight =
388
- selectedVariant.height > 0
788
+ : sourceWidth;
789
+ const outputHeight =
790
+ selectedVariant?.height && selectedVariant.height > 0
389
791
  ? selectedVariant.height
390
- : actualImageDimensions?.height || 0;
792
+ : sourceHeight;
793
+
391
794
  const scale =
392
- imageBounds && actualWidth > 0 ? actualWidth / imageBounds.width : 1;
393
- const widthPx = rect ? Math.round(rect.width * actualWidth) : 0;
394
- const heightPx = rect ? Math.round(rect.height * actualHeight) : 0;
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
- header={"Crop " + field.name + " - " + selectedVariantName}
400
- pt={{ content: { style: { paddingLeft: "0" } } }}
401
- visible={true}
402
- style={{ width: "75vw", height: "75vh" }}
403
- onHide={onClose}
404
- >
405
- <div className="justify flex h-full flex-col gap-1">
406
- <div className="flex flex-1 gap-2">
407
- <div className="flex w-56 flex-col gap-3 bg-gray-100 p-4 text-sm">
408
- <LabelAndValue label="Variant:" value={selectedVariantName} />
409
- <LabelAndValue
410
- label="Image Dimensions:"
411
- value={
412
- selectedVariant.width > 0 && selectedVariant.height > 0 ? (
413
- <>
414
- {selectedVariant.width} x {selectedVariant.height}
415
- </>
416
- ) : actualImageDimensions ? (
417
- <>
418
- {actualImageDimensions.width} x{" "}
419
- {actualImageDimensions.height} (actual)
420
- </>
421
- ) : (
422
- <span className="text-gray-500">Loading...</span>
423
- )
424
- }
425
- />
426
- {selectedVariant.aspectRatioLock && (
427
- <LabelAndValue
428
- label="Required Aspect Ratio:"
429
- value={selectedVariant.aspectRatioLockText}
430
- />
431
- )}
432
- {selectedVariant.minWidth && (
433
- <LabelAndValue
434
- label="Minimum Width:"
435
- value={selectedVariant.minWidth}
436
- />
437
- )}
438
- {selectedVariant.minHeight && (
439
- <LabelAndValue
440
- label="Minimum Height:"
441
- value={selectedVariant.minHeight}
442
- />
443
- )}
444
- {rect && (
445
- <>
446
- <LabelAndValue
447
- label="Selection:"
448
- value={
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
- {widthPx} x {heightPx}
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
- {selectedVariant.minHeight &&
459
- heightPx < selectedVariant.minHeight && (
460
- <div className="text-red-500">
461
- Minimum height not met!
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
- </div>
467
- <div className="relative flex-1 p-3">
468
- <div className="absolute inset-0 top-3 flex items-center justify-center select-none">
469
- <div
470
- ref={imageRef}
471
- className="relative max-h-full cursor-crosshair"
472
- style={{
473
- ...(actualWidth > 0 && actualHeight > 0
474
- ? { aspectRatio: `${actualWidth}/${actualHeight}` }
475
- : {}),
476
- }}
477
- onMouseDown={handleMouseDown}
478
- onMouseMove={handleMouseMove}
479
- >
480
- <img
481
- className="max-h-full max-w-full object-scale-down"
482
- src={selectedVariant.originalSrc ?? selectedVariant.src}
483
- onLoad={(e) => {
484
- const img = e.target as HTMLImageElement;
485
- setActualImageDimensions({
486
- width: img.naturalWidth,
487
- height: img.naturalHeight,
488
- });
489
- }}
490
- />
491
- {rect && (
492
- <div
493
- className={classNames(
494
- "absolute cursor-move border text-xs opacity-70",
495
- isValid
496
- ? "border-blue-400 bg-blue-200 text-blue-500"
497
- : "border-red-400 bg-red-200 text-red-500",
498
- )}
499
- style={{
500
- left: rect.x * 100 + "%",
501
- top: rect.y * 100 + "%",
502
- width: widthPx / scale + "px",
503
- height: heightPx / scale + "px",
504
- cursor: resizeEdge
505
- ? resizeEdge.length === 1
506
- ? `${resizeEdge}-resize`
507
- : `${resizeEdge}-resize`
508
- : "move",
509
- }}
510
- >
511
- {widthPx / scale > 50 && (
512
- <div
513
- className="absolute right-2 bottom-1 text-nowrap"
514
- style={{ textShadow: "white 1px 1px" }}
515
- >
516
- {widthPx} x {heightPx}
517
- </div>
518
- )}
519
- </div>
520
- )}
521
- </div>
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
- </div>
525
- <DialogButtons>
526
- <Button onClick={() => setRect(undefined)}>Reset</Button>
527
- <Button
528
- size="small"
529
- disabled={!isValid}
530
- onClick={() => {
531
- if (pictureValue) {
532
- if (field) {
533
- editContext?.operations.editField({
534
- field: field.descriptor,
535
- rawValue: JSON.stringify(rawValue),
536
- refresh: "immediate",
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
- onClose();
541
- }}
542
- >
543
- Ok
544
- </Button>
545
- <Button onClick={onClose} size="small">
546
- Cancel
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-bold">{label}</div>
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
  }