@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,15 +1,25 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Dialog } from "primereact/dialog";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "../components/ui/dialog";
3
3
  import { useEditContext } from "./client/editContext";
4
4
  import { useEffect, useRef, useState } from "react";
5
- import { Button } from "primereact/button";
5
+ import { Button } from "../components/ui/button";
6
6
  import DialogButtons from "./ui/DialogButtons";
7
7
  import { classNames } from "primereact/utils";
8
- export function PictureCropper({ field, onClose, variantName: selectedVariantName, }) {
8
+ import { RotateCcw } from "lucide-react";
9
+ import { SimpleTabs } from "./ui/SimpleTabs";
10
+ import { AspectRatioSelector } from "./AspectRatioSelector";
11
+ export function PictureCropper({ field, onClose, variantName: initialVariantName, }) {
9
12
  const [pictureValue, setPictureValue] = useState();
10
13
  const [isValid, setIsValid] = useState(true);
11
14
  const [rawValue, setRawValue] = useState();
12
15
  const [actualImageDimensions, setActualImageDimensions] = useState(null);
16
+ const [activeTabIndex, setActiveTabIndex] = useState(0);
17
+ // Track selected aspect ratio for each variant
18
+ const [selectedAspectRatio, setSelectedAspectRatio] = useState(null);
19
+ // Track crop rectangles for each variant separately
20
+ const [variantRects, setVariantRects] = useState(new Map());
21
+ // Cache image dimensions by URL to avoid showing loading indicator for already loaded images
22
+ const imageDimensionsCache = useRef(new Map());
13
23
  const imageRef = useRef(null);
14
24
  const [rect, setRect] = useState();
15
25
  const rectRef = useRef(rect);
@@ -23,6 +33,29 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
23
33
  const EDGE_THRESHOLD = 10; // pixels from edge to detect resize
24
34
  const [resizeEdge, setResizeEdge] = useState(null);
25
35
  const resizeEdgeRef = useRef(null);
36
+ // Helper function to create default region for aspect ratio locked variants
37
+ const createDefaultRegion = () => {
38
+ if (!selectedAspectRatio || !actualImageDimensions) {
39
+ return undefined;
40
+ }
41
+ // Always use the actual loaded image dimensions for aspect ratio calculations
42
+ const sourceWidth = actualImageDimensions.width;
43
+ const sourceHeight = actualImageDimensions.height;
44
+ const sourceAspectRatio = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
45
+ const targetAspectRatio = selectedAspectRatio;
46
+ // Calculate maximum size that fits the target aspect ratio within the source image
47
+ let width = 1.0; // Start with full width
48
+ let height = (width / targetAspectRatio) * sourceAspectRatio;
49
+ // If height exceeds bounds, start with full height instead
50
+ if (height > 1.0) {
51
+ height = 1.0;
52
+ width = (height * targetAspectRatio) / sourceAspectRatio;
53
+ }
54
+ // Center the rectangle
55
+ const x = (1.0 - width) / 2;
56
+ const y = (1.0 - height) / 2;
57
+ return { x, y, width, height };
58
+ };
26
59
  const getResizeEdge = (pos, rect, bounds) => {
27
60
  if (!rect || !bounds)
28
61
  return null;
@@ -62,28 +95,106 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
62
95
  : { Variants: [] };
63
96
  setRawValue(raw);
64
97
  }, [field]);
65
- const selectedVariant = pictureValue && pictureValue.variants
66
- ? pictureValue.variants?.find((x) => x.name == selectedVariantName)
98
+ // Set initial tab based on the provided variant name
99
+ useEffect(() => {
100
+ if (pictureValue?.variants && initialVariantName) {
101
+ const initialIndex = pictureValue.variants.findIndex((variant) => variant.name === initialVariantName);
102
+ if (initialIndex >= 0) {
103
+ setActiveTabIndex(initialIndex);
104
+ }
105
+ }
106
+ }, [pictureValue, initialVariantName]);
107
+ const selectedVariant = pictureValue &&
108
+ pictureValue.variants &&
109
+ pictureValue.variants[activeTabIndex]
110
+ ? pictureValue.variants[activeTabIndex]
67
111
  : null;
112
+ const selectedVariantName = selectedVariant?.name || "";
113
+ // Update selected aspect ratio when switching variants
114
+ useEffect(() => {
115
+ if (selectedVariant) {
116
+ // If variant has locked aspect ratio, use that
117
+ if (selectedVariant.aspectRatioLock) {
118
+ setSelectedAspectRatio(selectedVariant.aspectRatioLock);
119
+ }
120
+ else {
121
+ // Default to free form if no lock
122
+ setSelectedAspectRatio(null);
123
+ }
124
+ }
125
+ }, [selectedVariant]);
126
+ // Manage image dimensions when the image source changes between variants
127
+ const currentImageSrc = selectedVariant?.originalSrc ?? selectedVariant?.src;
128
+ const previousImageSrcRef = useRef(undefined);
68
129
  useEffect(() => {
130
+ if (currentImageSrc && currentImageSrc !== previousImageSrcRef.current) {
131
+ // Check if we have cached dimensions for this image
132
+ const cachedDimensions = imageDimensionsCache.current.get(currentImageSrc);
133
+ if (cachedDimensions) {
134
+ setActualImageDimensions(cachedDimensions);
135
+ }
136
+ else {
137
+ // Only reset to null if we don't have cached dimensions
138
+ setActualImageDimensions(null);
139
+ }
140
+ previousImageSrcRef.current = currentImageSrc;
141
+ }
142
+ }, [currentImageSrc]);
143
+ // Load crop rectangle when switching variants
144
+ useEffect(() => {
145
+ if (!selectedVariantName)
146
+ return;
147
+ // Check if we have a current working rect for this variant
148
+ const existingRect = variantRects.get(selectedVariantName);
149
+ // If we have a valid rect, use it
150
+ if (existingRect !== undefined) {
151
+ setRect(existingRect);
152
+ return;
153
+ }
154
+ // Initialize rect for this variant if not already done
155
+ let initialRect;
156
+ // Try to load from saved region if this variant hasn't been touched yet
69
157
  if (selectedVariant?.region &&
70
- (!selectedVariant.aspectRatioLock ||
71
- Math.abs(selectedVariant.aspectRatioLock -
158
+ (!selectedAspectRatio ||
159
+ Math.abs(selectedAspectRatio -
72
160
  (selectedVariant.region.width * selectedVariant.width) /
73
161
  (selectedVariant.region.height * selectedVariant.height)) < 0.1) &&
74
162
  selectedVariant.region.width > 0 &&
75
163
  selectedVariant.region.height > 0) {
76
- setRect({
164
+ initialRect = {
77
165
  width: selectedVariant.region.width,
78
166
  height: selectedVariant.region.height,
79
167
  y: selectedVariant.region.y,
80
168
  x: selectedVariant.region.x,
81
- });
169
+ };
82
170
  }
83
171
  else {
84
- setRect(undefined);
172
+ // Try to create a default region if aspect ratio is locked
173
+ initialRect = createDefaultRegion();
85
174
  }
86
- }, [selectedVariant]);
175
+ // Store in variantRects and set as current rect
176
+ setVariantRects((prev) => new Map(prev).set(selectedVariantName, initialRect));
177
+ setRect(initialRect);
178
+ }, [selectedVariant, selectedVariantName]);
179
+ // Create default region when image dimensions become available
180
+ useEffect(() => {
181
+ if (selectedVariant &&
182
+ actualImageDimensions &&
183
+ !selectedVariant?.region &&
184
+ selectedAspectRatio) {
185
+ const defaultRect = createDefaultRegion();
186
+ setVariantRects((prev) => new Map(prev).set(selectedVariant.name, defaultRect));
187
+ setRect(defaultRect);
188
+ }
189
+ }, [actualImageDimensions, selectedVariant, selectedAspectRatio]);
190
+ // Update working state when rect changes (but only if user is actively editing)
191
+ const isUserEditingRef = useRef(false);
192
+ useEffect(() => {
193
+ if (selectedVariantName && isUserEditingRef.current) {
194
+ setVariantRects((prev) => new Map(prev).set(selectedVariantName, rect));
195
+ isUserEditingRef.current = false;
196
+ }
197
+ }, [rect, selectedVariantName]);
87
198
  useEffect(() => {
88
199
  if (!rect)
89
200
  return;
@@ -101,13 +212,18 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
101
212
  if (!selectedVariantName)
102
213
  return;
103
214
  if (selectedVariant) {
104
- let selected = rawValue.Variants?.find((x) => x.Name == selectedVariantName);
215
+ // Create a deep copy of rawValue to avoid mutations
216
+ const newRawValue = JSON.parse(JSON.stringify(rawValue));
217
+ let selected = newRawValue.Variants?.find((x) => x.Name == selectedVariantName);
105
218
  if (!selected) {
106
219
  selected = {
107
220
  Name: selectedVariantName,
108
221
  MediaId: selectedVariant.mediaId,
109
222
  };
110
- rawValue.Variants?.push(selected);
223
+ if (!newRawValue.Variants) {
224
+ newRawValue.Variants = [];
225
+ }
226
+ newRawValue.Variants.push(selected);
111
227
  }
112
228
  if (selected) {
113
229
  if (rect)
@@ -119,7 +235,7 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
119
235
  };
120
236
  else
121
237
  selected.Region = undefined;
122
- setRawValue(rawValue);
238
+ setRawValue(newRawValue);
123
239
  }
124
240
  }
125
241
  }, [rect]);
@@ -152,68 +268,256 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
152
268
  y: (ev.clientY - bounds.top) / bounds.height,
153
269
  };
154
270
  let newRect = { ...rect };
155
- const aspectRatio = selectedVariant.aspectRatioLock;
156
- const actualWidth = selectedVariant.width > 0
157
- ? selectedVariant.width
158
- : actualImageDimensions?.width || 0;
159
- const actualHeight = selectedVariant.height > 0
160
- ? selectedVariant.height
161
- : actualImageDimensions?.height || 0;
162
- const originalAspectRatio = actualHeight > 0 ? actualWidth / actualHeight : 1;
163
- if (resizeEdgeRef.current.includes("w")) {
164
- const newWidth = rect.width + rect.x - pos.x;
165
- if (newWidth > 0) {
166
- newRect.width = newWidth;
167
- newRect.x = pos.x;
168
- if (aspectRatio) {
169
- newRect.height =
170
- (newRect.width / aspectRatio) * originalAspectRatio;
171
- }
271
+ const aspectRatio = selectedAspectRatio;
272
+ const sourceWidth = actualImageDimensions?.width || 0;
273
+ const sourceHeight = actualImageDimensions?.height || 0;
274
+ const sourceAspectRatio = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
275
+ // Handle resize based on edge - structure to avoid conflicts between corner handles
276
+ if (resizeEdgeRef.current === "w") {
277
+ // Pure west edge - resize from left, keep right edge fixed
278
+ const rightEdge = rect.x + rect.width;
279
+ const newWidth = Math.max(0.01, rightEdge - pos.x);
280
+ newRect.width = newWidth;
281
+ newRect.x = rightEdge - newWidth;
282
+ if (aspectRatio) {
283
+ const newHeight = (newWidth / aspectRatio) * sourceAspectRatio;
284
+ const centerY = rect.y + rect.height / 2;
285
+ newRect.y = centerY - newHeight / 2;
286
+ newRect.height = newHeight;
172
287
  }
173
288
  }
174
- if (resizeEdgeRef.current.includes("e")) {
289
+ else if (resizeEdgeRef.current === "e") {
290
+ // Pure east edge - resize from right, keep left edge fixed
175
291
  const newWidth = pos.x - rect.x;
176
292
  if (newWidth > 0) {
177
293
  newRect.width = newWidth;
178
294
  if (aspectRatio) {
179
- newRect.height =
180
- (newRect.width / aspectRatio) * originalAspectRatio;
295
+ const newHeight = (newWidth / aspectRatio) * sourceAspectRatio;
296
+ const centerY = rect.y + rect.height / 2;
297
+ newRect.y = centerY - newHeight / 2;
298
+ newRect.height = newHeight;
181
299
  }
182
300
  }
183
301
  }
184
- if (resizeEdgeRef.current.includes("n")) {
185
- const newHeight = rect.height + rect.y - pos.y;
302
+ else if (resizeEdgeRef.current === "n") {
303
+ // Pure north edge - resize from top, keep bottom edge fixed
304
+ const bottomEdge = rect.y + rect.height;
305
+ const newHeight = bottomEdge - pos.y;
186
306
  if (newHeight > 0) {
187
307
  newRect.height = newHeight;
188
308
  newRect.y = pos.y;
189
309
  if (aspectRatio) {
190
- newRect.width =
191
- (newRect.height * aspectRatio) / originalAspectRatio;
310
+ const newWidth = (newHeight * aspectRatio) / sourceAspectRatio;
311
+ const centerX = rect.x + rect.width / 2;
312
+ newRect.x = centerX - newWidth / 2;
313
+ newRect.width = newWidth;
192
314
  }
193
315
  }
194
316
  }
195
- if (resizeEdgeRef.current.includes("s")) {
317
+ else if (resizeEdgeRef.current === "s") {
318
+ // Pure south edge - resize from bottom, keep top edge fixed
196
319
  const newHeight = pos.y - rect.y;
197
320
  if (newHeight > 0) {
198
321
  newRect.height = newHeight;
199
322
  if (aspectRatio) {
200
- newRect.width =
201
- (newRect.height * aspectRatio) / originalAspectRatio;
323
+ const newWidth = (newHeight * aspectRatio) / sourceAspectRatio;
324
+ const centerX = rect.x + rect.width / 2;
325
+ newRect.x = centerX - newWidth / 2;
326
+ newRect.width = newWidth;
327
+ }
328
+ }
329
+ }
330
+ else if (resizeEdgeRef.current.includes("w") &&
331
+ resizeEdgeRef.current.includes("n")) {
332
+ // Northwest corner - keep southeast corner fixed
333
+ const rightEdge = rect.x + rect.width;
334
+ const bottomEdge = rect.y + rect.height;
335
+ const newWidth = Math.max(0.01, rightEdge - pos.x);
336
+ const newHeight = Math.max(0.01, bottomEdge - pos.y);
337
+ newRect.width = newWidth;
338
+ newRect.height = newHeight;
339
+ newRect.x = rightEdge - newWidth;
340
+ newRect.y = bottomEdge - newHeight;
341
+ if (aspectRatio) {
342
+ // Use mouse movement direction to determine primary dimension
343
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
344
+ newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
345
+ newRect.y = bottomEdge - newRect.height;
346
+ }
347
+ else {
348
+ newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
349
+ newRect.x = rightEdge - newRect.width;
202
350
  }
203
351
  }
204
352
  }
205
- // Constrain to bounds
206
- if (newRect.x < 0)
207
- newRect.x = 0;
208
- if (newRect.y < 0)
209
- newRect.y = 0;
210
- if (newRect.x + newRect.width > 1)
211
- newRect.width = 1 - newRect.x;
212
- if (newRect.y + newRect.height > 1)
213
- newRect.height = 1 - newRect.y;
353
+ else if (resizeEdgeRef.current.includes("w") &&
354
+ resizeEdgeRef.current.includes("s")) {
355
+ // Southwest corner - keep northeast corner fixed
356
+ const rightEdge = rect.x + rect.width;
357
+ const newWidth = Math.max(0.01, rightEdge - pos.x);
358
+ const newHeight = Math.max(0.01, pos.y - rect.y);
359
+ newRect.width = newWidth;
360
+ newRect.height = newHeight;
361
+ newRect.x = rightEdge - newWidth;
362
+ if (aspectRatio) {
363
+ // Use mouse movement direction to determine primary dimension
364
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
365
+ newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
366
+ }
367
+ else {
368
+ newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
369
+ newRect.x = rightEdge - newRect.width;
370
+ }
371
+ }
372
+ }
373
+ else if (resizeEdgeRef.current.includes("e") &&
374
+ resizeEdgeRef.current.includes("n")) {
375
+ // Northeast corner - keep southwest corner fixed
376
+ const bottomEdge = rect.y + rect.height;
377
+ const newWidth = Math.max(0.01, pos.x - rect.x);
378
+ const newHeight = Math.max(0.01, bottomEdge - pos.y);
379
+ newRect.width = newWidth;
380
+ newRect.height = newHeight;
381
+ newRect.y = bottomEdge - newHeight;
382
+ if (aspectRatio) {
383
+ // Use mouse movement direction to determine primary dimension
384
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
385
+ newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
386
+ newRect.y = bottomEdge - newRect.height;
387
+ }
388
+ else {
389
+ newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
390
+ }
391
+ }
392
+ }
393
+ else if (resizeEdgeRef.current.includes("e") &&
394
+ resizeEdgeRef.current.includes("s")) {
395
+ // Southeast corner - keep northwest corner fixed
396
+ const newWidth = Math.max(0.01, pos.x - rect.x);
397
+ const newHeight = Math.max(0.01, pos.y - rect.y);
398
+ newRect.width = newWidth;
399
+ newRect.height = newHeight;
400
+ if (aspectRatio) {
401
+ // Use mouse movement direction to determine primary dimension
402
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
403
+ newRect.height = (newWidth / aspectRatio) * sourceAspectRatio;
404
+ }
405
+ else {
406
+ newRect.width = (newHeight * aspectRatio) / sourceAspectRatio;
407
+ }
408
+ }
409
+ }
410
+ // Constrain to bounds while maintaining aspect ratio
411
+ // For west edge resizes, maintain the right edge position when hitting left bound
412
+ if (newRect.x < 0) {
413
+ if (resizeEdgeRef.current?.includes("w")) {
414
+ const rightEdge = rect.x + rect.width; // Original right edge position
415
+ newRect.x = 0;
416
+ newRect.width = rightEdge; // Width = distance from left bound to original right edge
417
+ // Recalculate height if aspect ratio is locked
418
+ if (aspectRatio) {
419
+ const newHeight = (rightEdge / aspectRatio) * sourceAspectRatio;
420
+ if (resizeEdgeRef.current === "w") {
421
+ const centerY = rect.y + rect.height / 2;
422
+ newRect.y = centerY - newHeight / 2;
423
+ }
424
+ newRect.height = newHeight;
425
+ }
426
+ }
427
+ else {
428
+ newRect.x = 0;
429
+ }
430
+ }
431
+ // For north edge resizes, maintain the bottom edge position when hitting top bound
432
+ if (newRect.y < 0) {
433
+ if (resizeEdgeRef.current?.includes("n")) {
434
+ const bottomEdge = rect.y + rect.height;
435
+ newRect.y = 0;
436
+ newRect.height = bottomEdge; // Keep bottom edge fixed
437
+ // Recalculate width if aspect ratio is locked
438
+ if (aspectRatio) {
439
+ const newWidth = (newRect.height * aspectRatio) / sourceAspectRatio;
440
+ if (resizeEdgeRef.current === "n") {
441
+ const centerX = rect.x + rect.width / 2;
442
+ newRect.x = centerX - newWidth / 2;
443
+ }
444
+ newRect.width = newWidth;
445
+ }
446
+ }
447
+ else {
448
+ newRect.y = 0;
449
+ }
450
+ }
451
+ // Apply bounds constraints and maintain aspect ratio
452
+ let boundedRect = { ...newRect };
453
+ if (boundedRect.x + boundedRect.width > 1) {
454
+ boundedRect.width = 1 - boundedRect.x;
455
+ // Re-apply aspect ratio if it was locked
456
+ if (aspectRatio) {
457
+ const newHeight = (boundedRect.width / aspectRatio) * sourceAspectRatio;
458
+ // Maintain anchoring behavior based on resize edge
459
+ if (resizeEdgeRef.current === "w" ||
460
+ resizeEdgeRef.current === "e") {
461
+ // For horizontal resizes, keep centered vertically
462
+ const centerY = rect.y + rect.height / 2;
463
+ boundedRect.y = centerY - newHeight / 2;
464
+ }
465
+ boundedRect.height = newHeight;
466
+ }
467
+ }
468
+ if (boundedRect.y + boundedRect.height > 1) {
469
+ boundedRect.height = 1 - boundedRect.y;
470
+ // Re-apply aspect ratio if it was locked
471
+ if (aspectRatio) {
472
+ const newWidth = (boundedRect.height * aspectRatio) / sourceAspectRatio;
473
+ // Maintain anchoring behavior based on resize edge
474
+ if (resizeEdgeRef.current === "n" ||
475
+ resizeEdgeRef.current === "s") {
476
+ // For vertical resizes, keep centered horizontally
477
+ const centerX = rect.x + rect.width / 2;
478
+ boundedRect.x = centerX - newWidth / 2;
479
+ }
480
+ boundedRect.width = newWidth;
481
+ }
482
+ }
483
+ // Final check - if aspect ratio re-calculation pushes us out of bounds again,
484
+ // we need to find the maximum size that fits both constraints
485
+ if (aspectRatio &&
486
+ (boundedRect.x + boundedRect.width > 1 ||
487
+ boundedRect.y + boundedRect.height > 1)) {
488
+ const maxWidth = 1 - boundedRect.x;
489
+ const maxHeight = 1 - boundedRect.y;
490
+ const maxHeightFromWidth = (maxWidth / aspectRatio) * sourceAspectRatio;
491
+ const maxWidthFromHeight = (maxHeight * aspectRatio) / sourceAspectRatio;
492
+ if (maxHeightFromWidth <= maxHeight) {
493
+ // Width is the limiting factor
494
+ boundedRect.width = maxWidth;
495
+ boundedRect.height = maxHeightFromWidth;
496
+ // Maintain anchoring for horizontal resizes
497
+ if (resizeEdgeRef.current === "w" ||
498
+ resizeEdgeRef.current === "e") {
499
+ const centerY = rect.y + rect.height / 2;
500
+ boundedRect.y = Math.max(0, Math.min(1 - boundedRect.height, centerY - boundedRect.height / 2));
501
+ }
502
+ }
503
+ else {
504
+ // Height is the limiting factor
505
+ boundedRect.height = maxHeight;
506
+ boundedRect.width = maxWidthFromHeight;
507
+ // Maintain anchoring for vertical resizes
508
+ if (resizeEdgeRef.current === "n" ||
509
+ resizeEdgeRef.current === "s") {
510
+ const centerX = rect.x + rect.width / 2;
511
+ boundedRect.x = Math.max(0, Math.min(1 - boundedRect.width, centerX - boundedRect.width / 2));
512
+ }
513
+ }
514
+ }
515
+ newRect = boundedRect;
516
+ isUserEditingRef.current = true;
214
517
  setRect(newRect);
215
518
  }
216
519
  else if (movingRef.current) {
520
+ isUserEditingRef.current = true;
217
521
  setRect({
218
522
  ...rect,
219
523
  x: Math.max(0, Math.min(1 - rectRef.current.width, (ev.clientX - bounds.left) / bounds.width - offset.current.x)),
@@ -221,14 +525,10 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
221
525
  });
222
526
  }
223
527
  else {
224
- const actualWidth = selectedVariant.width > 0
225
- ? selectedVariant.width
226
- : actualImageDimensions?.width || 0;
227
- const actualHeight = selectedVariant.height > 0
228
- ? selectedVariant.height
229
- : actualImageDimensions?.height || 0;
230
- const originalAspectRatio = actualHeight > 0 ? actualWidth / actualHeight : 1;
231
- const aspectRatio = selectedVariant.aspectRatioLock;
528
+ const sourceWidth = actualImageDimensions?.width || 0;
529
+ const sourceHeight = actualImageDimensions?.height || 0;
530
+ const sourceAspectRatio = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
531
+ const aspectRatio = selectedAspectRatio;
232
532
  const currentX = (ev.clientX - bounds.left) / bounds.width;
233
533
  const currentY = (ev.clientY - bounds.top) / bounds.height;
234
534
  const minX = Math.min(rect.x, currentX);
@@ -239,26 +539,47 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
239
539
  let height = maxY - minY;
240
540
  if (aspectRatio) {
241
541
  if (Math.abs(deltaX) > Math.abs(deltaY)) {
242
- height = (width / aspectRatio) * originalAspectRatio;
542
+ height = (width / aspectRatio) * sourceAspectRatio;
243
543
  }
244
544
  else {
245
- width = (height * aspectRatio) / originalAspectRatio;
545
+ width = (height * aspectRatio) / sourceAspectRatio;
246
546
  }
247
547
  }
248
- if (width + minX > 1) {
249
- width = 1 - minX;
250
- if (aspectRatio)
251
- height = (width / aspectRatio) * originalAspectRatio;
548
+ // Apply bounds constraints and maintain aspect ratio
549
+ if (aspectRatio) {
550
+ const maxWidth = 1 - minX;
551
+ const maxHeight = 1 - minY;
552
+ const maxHeightFromWidth = (maxWidth / aspectRatio) * sourceAspectRatio;
553
+ const maxWidthFromHeight = (maxHeight * aspectRatio) / sourceAspectRatio;
554
+ if (maxHeightFromWidth <= maxHeight) {
555
+ // Width is the limiting factor
556
+ if (width > maxWidth) {
557
+ width = maxWidth;
558
+ height = maxHeightFromWidth;
559
+ }
560
+ }
561
+ else {
562
+ // Height is the limiting factor
563
+ if (height > maxHeight) {
564
+ height = maxHeight;
565
+ width = maxWidthFromHeight;
566
+ }
567
+ }
252
568
  }
253
- if (height + minY > 1) {
254
- height = 1 - minY;
255
- if (aspectRatio)
256
- width = (height * aspectRatio) / originalAspectRatio;
569
+ else {
570
+ // No aspect ratio lock, just apply simple bounds
571
+ if (width + minX > 1) {
572
+ width = 1 - minX;
573
+ }
574
+ if (height + minY > 1) {
575
+ height = 1 - minY;
576
+ }
257
577
  }
258
578
  // Ensure final dimensions are non-negative
259
579
  width = Math.max(0, width);
260
580
  height = Math.max(0, height);
261
581
  const newRect = { x: minX, y: minY, width, height };
582
+ isUserEditingRef.current = true;
262
583
  setRect(newRect);
263
584
  }
264
585
  ev.preventDefault();
@@ -267,6 +588,7 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
267
588
  };
268
589
  const handleMouseUp = () => {
269
590
  if (!rectRef.current || !rectRef.current.width || !rectRef.current.height) {
591
+ isUserEditingRef.current = true;
270
592
  setRect(undefined);
271
593
  }
272
594
  movingRef.current = false;
@@ -304,6 +626,7 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
304
626
  };
305
627
  }
306
628
  else {
629
+ isUserEditingRef.current = true;
307
630
  setRect({
308
631
  x: (ev.clientX - bounds.left) / bounds.width,
309
632
  y: (ev.clientY - bounds.top) / bounds.height,
@@ -314,56 +637,86 @@ export function PictureCropper({ field, onClose, variantName: selectedVariantNam
314
637
  setStartPos({ x: ev.clientX, y: ev.clientY });
315
638
  window.addEventListener("mouseup", handleMouseUp);
316
639
  };
317
- if (!selectedVariant)
640
+ if (!pictureValue?.variants || pictureValue.variants.length === 0) {
318
641
  return null;
642
+ }
643
+ // Create tabs for each variant
644
+ const variantTabs = pictureValue.variants.map((variant, index) => ({
645
+ id: `variant-${index}`,
646
+ label: variant.name,
647
+ content: null, // Content will be rendered separately
648
+ }));
319
649
  const imageBounds = imageRef.current?.getBoundingClientRect();
320
- const actualWidth = selectedVariant.width > 0
650
+ const sourceWidth = actualImageDimensions?.width || 0;
651
+ const sourceHeight = actualImageDimensions?.height || 0;
652
+ // For output dimensions, prefer variant dimensions if specified
653
+ const outputWidth = selectedVariant?.width && selectedVariant.width > 0
321
654
  ? selectedVariant.width
322
- : actualImageDimensions?.width || 0;
323
- const actualHeight = selectedVariant.height > 0
655
+ : sourceWidth;
656
+ const outputHeight = selectedVariant?.height && selectedVariant.height > 0
324
657
  ? selectedVariant.height
325
- : actualImageDimensions?.height || 0;
326
- const scale = imageBounds && actualWidth > 0 ? actualWidth / imageBounds.width : 1;
327
- const widthPx = rect ? Math.round(rect.width * actualWidth) : 0;
328
- const heightPx = rect ? Math.round(rect.height * actualHeight) : 0;
329
- return (_jsx(_Fragment, { children: _jsx(Dialog, { header: "Crop " + field.name + " - " + selectedVariantName, pt: { content: { style: { paddingLeft: "0" } } }, visible: true, style: { width: "75vw", height: "75vh" }, onHide: onClose, children: _jsxs("div", { className: "justify flex h-full flex-col gap-1", children: [_jsxs("div", { className: "flex flex-1 gap-2", children: [_jsxs("div", { className: "flex w-56 flex-col gap-3 bg-gray-100 p-4 text-sm", children: [_jsx(LabelAndValue, { label: "Variant:", value: selectedVariantName }), _jsx(LabelAndValue, { label: "Image Dimensions:", value: selectedVariant.width > 0 && selectedVariant.height > 0 ? (_jsxs(_Fragment, { children: [selectedVariant.width, " x ", selectedVariant.height] })) : actualImageDimensions ? (_jsxs(_Fragment, { children: [actualImageDimensions.width, " x", " ", actualImageDimensions.height, " (actual)"] })) : (_jsx("span", { className: "text-gray-500", children: "Loading..." })) }), selectedVariant.aspectRatioLock && (_jsx(LabelAndValue, { label: "Required Aspect Ratio:", value: selectedVariant.aspectRatioLockText })), selectedVariant.minWidth && (_jsx(LabelAndValue, { label: "Minimum Width:", value: selectedVariant.minWidth })), selectedVariant.minHeight && (_jsx(LabelAndValue, { label: "Minimum Height:", value: selectedVariant.minHeight })), rect && (_jsxs(_Fragment, { children: [_jsx(LabelAndValue, { label: "Selection:", value: _jsxs(_Fragment, { children: [widthPx, " x ", heightPx] }) }), selectedVariant.minWidth &&
330
- widthPx < selectedVariant.minWidth && (_jsx("div", { className: "text-red-500", children: "Minimum width not met!" })), selectedVariant.minHeight &&
331
- heightPx < selectedVariant.minHeight && (_jsx("div", { className: "text-red-500", children: "Minimum height not met!" }))] }))] }), _jsx("div", { className: "relative flex-1 p-3", children: _jsx("div", { className: "absolute inset-0 top-3 flex items-center justify-center select-none", children: _jsxs("div", { ref: imageRef, className: "relative max-h-full cursor-crosshair", style: {
332
- ...(actualWidth > 0 && actualHeight > 0
333
- ? { aspectRatio: `${actualWidth}/${actualHeight}` }
334
- : {}),
335
- }, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, children: [_jsx("img", { className: "max-h-full max-w-full object-scale-down", src: selectedVariant.originalSrc ?? selectedVariant.src, onLoad: (e) => {
336
- const img = e.target;
337
- setActualImageDimensions({
338
- width: img.naturalWidth,
339
- height: img.naturalHeight,
658
+ : sourceHeight;
659
+ const scale = imageBounds && outputWidth > 0 ? outputWidth / imageBounds.width : 1;
660
+ const widthPx = rect ? Math.round(rect.width * outputWidth) : 0;
661
+ const heightPx = rect ? Math.round(rect.height * outputHeight) : 0;
662
+ return (_jsx(_Fragment, { children: _jsx(Dialog, { open: true, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: "h-[75vh] max-h-none w-[75vw] max-w-none", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { children: ["Crop ", field.name] }) }), _jsxs("div", { className: "justify flex h-full flex-col", children: [_jsxs("div", { className: "flex flex-1 gap-2", children: [_jsxs("div", { className: "flex w-72 flex-col gap-3 p-6 text-sm", children: [_jsx("div", { className: "mb-4", children: _jsx(SimpleTabs, { tabs: variantTabs, activeTab: activeTabIndex, setActiveTab: setActiveTabIndex, className: "border-gray-2 border-b text-xs", tabClassName: "flex-1 text-center" }) }), selectedVariant && (_jsxs(_Fragment, { children: [_jsx("div", { className: "mb-4", children: _jsx(AspectRatioSelector, { selectedRatio: selectedAspectRatio, onRatioChange: (ratio) => {
663
+ setSelectedAspectRatio(ratio);
664
+ // Reset the crop rect to default when aspect ratio changes
665
+ const defaultRect = createDefaultRegion();
666
+ isUserEditingRef.current = true;
667
+ setRect(defaultRect);
668
+ }, lockedRatio: selectedVariant.aspectRatioLock }) }), _jsx(LabelAndValue, { label: "Image Dimensions", value: selectedVariant.width > 0 &&
669
+ selectedVariant.height > 0 ? (_jsxs(_Fragment, { children: [selectedVariant.width, " x ", selectedVariant.height] })) : actualImageDimensions ? (_jsxs(_Fragment, { children: [actualImageDimensions.width, " x", " ", actualImageDimensions.height, " (actual)"] })) : (_jsx("span", { className: "text-gray-500", children: "Loading..." })) }), selectedVariant.minWidth && (_jsx(LabelAndValue, { label: "Minimum Width:", value: selectedVariant.minWidth })), selectedVariant.minHeight && (_jsx(LabelAndValue, { label: "Minimum Height:", value: selectedVariant.minHeight })), rect && (_jsxs(_Fragment, { children: [_jsx(LabelAndValue, { label: "Selection", value: _jsxs(_Fragment, { children: [widthPx, " x ", heightPx] }) }), selectedVariant.minWidth &&
670
+ widthPx < selectedVariant.minWidth && (_jsx("div", { className: "text-red-500", children: "Minimum width not met!" })), selectedVariant.minHeight &&
671
+ heightPx < selectedVariant.minHeight && (_jsx("div", { className: "text-red-500", children: "Minimum height not met!" }))] }))] }))] }), _jsx("div", { className: "bg-gray-4 relative flex-1", children: selectedVariant && (_jsxs("div", { className: "absolute inset-3 flex items-center justify-center select-none", children: [!actualImageDimensions && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "text-gray-500", children: "Loading image..." }) })), !actualImageDimensions && (_jsx("img", { className: "pointer-events-none absolute opacity-0", src: selectedVariant.originalSrc ?? selectedVariant.src, onLoad: (e) => {
672
+ const img = e.target;
673
+ const dimensions = {
674
+ width: img.naturalWidth,
675
+ height: img.naturalHeight,
676
+ };
677
+ // Cache the dimensions for this image URL
678
+ const imgSrc = selectedVariant.originalSrc ?? selectedVariant.src;
679
+ if (imgSrc) {
680
+ imageDimensionsCache.current.set(imgSrc, dimensions);
681
+ }
682
+ setActualImageDimensions(dimensions);
683
+ } })), actualImageDimensions && (_jsxs("div", { ref: imageRef, className: "relative max-h-full cursor-crosshair", style: {
684
+ aspectRatio: `${sourceWidth}/${sourceHeight}`,
685
+ }, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, children: [_jsx("img", { className: "max-h-full max-w-full object-scale-down", src: selectedVariant.originalSrc ?? selectedVariant.src }), rect && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pointer-events-none absolute inset-0 bg-black/40", style: {
686
+ 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%)`,
687
+ } }), _jsxs("div", { className: classNames("absolute cursor-move border-2 border-dashed", isValid
688
+ ? "border-theme-secondary"
689
+ : "border-red-400"), style: {
690
+ left: rect.x * 100 + "%",
691
+ top: rect.y * 100 + "%",
692
+ width: rect.width * 100 + "%",
693
+ height: rect.height * 100 + "%",
694
+ cursor: resizeEdge
695
+ ? resizeEdge.length === 1
696
+ ? `${resizeEdge}-resize`
697
+ : `${resizeEdge}-resize`
698
+ : "move",
699
+ }, children: [_jsx("div", { className: "absolute -top-1.5 -left-1.5 h-3 w-3 cursor-nw-resize rounded-full bg-white/90 shadow-lg" }), _jsx("div", { className: "absolute -top-1.5 -right-1.5 h-3 w-3 cursor-ne-resize rounded-full bg-white/90 shadow-lg" }), _jsx("div", { className: "absolute -bottom-1.5 -left-1.5 h-3 w-3 cursor-sw-resize rounded-full bg-white/90 shadow-lg" }), _jsx("div", { className: "absolute -right-1.5 -bottom-1.5 h-3 w-3 cursor-se-resize rounded-full bg-white/90 shadow-lg" }), _jsx("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" }), _jsx("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" }), _jsx("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" }), _jsx("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" }), rect.width > 0.05 && (_jsxs("div", { className: classNames("absolute right-1 bottom-1 rounded p-2 text-xs text-nowrap", isValid
700
+ ? "bg-black/40 text-white"
701
+ : "bg-red-400/40 text-white"), children: [widthPx, " x ", heightPx] }))] })] }))] }))] })) })] }), _jsxs(DialogButtons, { children: [_jsx(Button, { onClick: onClose, variant: "ghost", children: "Cancel" }), _jsxs(Button, { onClick: () => {
702
+ // If there's an aspect ratio lock, restore the default centered, maximum-sized region
703
+ const defaultRect = createDefaultRegion();
704
+ isUserEditingRef.current = true;
705
+ setRect(defaultRect);
706
+ }, variant: "outline", children: [_jsx(RotateCcw, { strokeWidth: 1 }), "Reset"] }), _jsx(Button, { disabled: !isValid, onClick: () => {
707
+ if (pictureValue) {
708
+ if (field) {
709
+ editContext?.operations.editField({
710
+ field: field.descriptor,
711
+ rawValue: JSON.stringify(rawValue),
712
+ refresh: "immediate",
340
713
  });
341
- } }), rect && (_jsx("div", { className: classNames("absolute cursor-move border text-xs opacity-70", isValid
342
- ? "border-blue-400 bg-blue-200 text-blue-500"
343
- : "border-red-400 bg-red-200 text-red-500"), style: {
344
- left: rect.x * 100 + "%",
345
- top: rect.y * 100 + "%",
346
- width: widthPx / scale + "px",
347
- height: heightPx / scale + "px",
348
- cursor: resizeEdge
349
- ? resizeEdge.length === 1
350
- ? `${resizeEdge}-resize`
351
- : `${resizeEdge}-resize`
352
- : "move",
353
- }, children: widthPx / scale > 50 && (_jsxs("div", { className: "absolute right-2 bottom-1 text-nowrap", style: { textShadow: "white 1px 1px" }, children: [widthPx, " x ", heightPx] })) }))] }) }) })] }), _jsxs(DialogButtons, { children: [_jsx(Button, { onClick: () => setRect(undefined), children: "Reset" }), _jsx(Button, { size: "small", disabled: !isValid, onClick: () => {
354
- if (pictureValue) {
355
- if (field) {
356
- editContext?.operations.editField({
357
- field: field.descriptor,
358
- rawValue: JSON.stringify(rawValue),
359
- refresh: "immediate",
360
- });
361
- }
362
- }
363
- onClose();
364
- }, children: "Ok" }), _jsx(Button, { onClick: onClose, size: "small", children: "Cancel" })] })] }) }) }));
714
+ }
715
+ }
716
+ onClose();
717
+ }, children: "Save" })] })] })] }) }) }));
365
718
  }
366
719
  function LabelAndValue({ label, value, }) {
367
- return (_jsxs("div", { children: [_jsx("div", { className: "font-bold", children: label }), _jsx("div", { children: value })] }));
720
+ return (_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: label }), _jsx("div", { className: "text-xs font-light", children: value })] }));
368
721
  }
369
722
  //# sourceMappingURL=PictureCropper.js.map