payload-plugin-aspect-preview 0.1.0

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.
@@ -0,0 +1,629 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useConfig, useDocumentInfo, useForm, useUploadEdits } from '@payloadcms/ui';
4
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
5
+ import 'react-image-crop/dist/ReactCrop.css';
6
+ import ReactCrop from 'react-image-crop';
7
+ import { DEFAULT_ASPECT_RATIOS } from '../defaults';
8
+ import { AspectRatioPreviewGrid } from './AspectRatioPreviewGrid';
9
+ export const FocalPointEditor = ()=>{
10
+ const { data } = useDocumentInfo();
11
+ const { setModified } = useForm();
12
+ const { uploadEdits, updateUploadEdits } = useUploadEdits();
13
+ const { getEntityConfig } = useConfig();
14
+ // Read the ratios the plugin stashed in the collection's `custom` config.
15
+ // Falls back to the shipped defaults so the editor renders even if a
16
+ // consumer wired the field without options.
17
+ const collectionSlug = typeof data?.collection === 'string' ? data.collection : undefined;
18
+ const entityConfig = collectionSlug ? getEntityConfig({
19
+ collectionSlug
20
+ }) : undefined;
21
+ const aspectRatios = entityConfig?.custom?.aspectPreview?.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
22
+ const hasUserEditedRef = useRef(false);
23
+ // Bust the browser cache when the document is re-saved so the regenerated
24
+ // cropped image (same URL, new bytes) shows up in the preview grid.
25
+ const rawUrl = data?.url;
26
+ const updatedAtTag = data?.updatedAt ? String(data.updatedAt) : undefined;
27
+ const imageUrl = React.useMemo(()=>{
28
+ if (!rawUrl) return undefined;
29
+ if (!updatedAtTag) return rawUrl;
30
+ const sep = rawUrl.includes('?') ? '&' : '?';
31
+ return `${rawUrl}${sep}v=${encodeURIComponent(updatedAtTag)}`;
32
+ }, [
33
+ rawUrl,
34
+ updatedAtTag
35
+ ]);
36
+ const [mode, setMode] = useState('focal');
37
+ // Crop is undefined when none has been drawn yet. When the user enters crop
38
+ // mode without an existing crop, ReactCrop should wait for them to draw one
39
+ // — we don't pre-fill 100% any more.
40
+ const [crop, setCrop] = useState(uploadEdits?.crop ? {
41
+ unit: uploadEdits.crop.unit,
42
+ x: uploadEdits.crop.x,
43
+ y: uploadEdits.crop.y,
44
+ width: uploadEdits.crop.width,
45
+ height: uploadEdits.crop.height
46
+ } : undefined);
47
+ const [focalPoint, setFocalPoint] = useState({
48
+ x: uploadEdits?.focalPoint?.x || data?.focalX || 50,
49
+ y: uploadEdits?.focalPoint?.y || data?.focalY || 50
50
+ });
51
+ const containerRef = useRef(null);
52
+ const [isDragging, setIsDragging] = useState(false);
53
+ // Natural pixel dimensions of the underlying image. Needed because Payload's
54
+ // cropImage requires `widthInPixels` / `heightInPixels` in addition to the
55
+ // percent crop — without them sharp.extract receives NaN and the save fails.
56
+ const [naturalSize, setNaturalSize] = useState(()=>({
57
+ width: typeof data?.width === 'number' ? data.width : 0,
58
+ height: typeof data?.height === 'number' ? data.height : 0
59
+ }));
60
+ // Prefer the Payload document's authoritative width/height — that's what
61
+ // sharp reads on the server. Fall back to the browser-decoded natural size.
62
+ const docWidth = typeof data?.width === 'number' ? data.width : 0;
63
+ const docHeight = typeof data?.height === 'number' ? data.height : 0;
64
+ useEffect(()=>{
65
+ if (docWidth > 0 && docHeight > 0) {
66
+ queueMicrotask(()=>setNaturalSize({
67
+ width: docWidth,
68
+ height: docHeight
69
+ }));
70
+ return;
71
+ }
72
+ if (!imageUrl) return;
73
+ const img = new Image();
74
+ img.onload = ()=>{
75
+ setNaturalSize({
76
+ width: img.naturalWidth,
77
+ height: img.naturalHeight
78
+ });
79
+ };
80
+ img.src = imageUrl;
81
+ }, [
82
+ imageUrl,
83
+ docWidth,
84
+ docHeight
85
+ ]);
86
+ const [prevUpdatedAtTag, setPrevUpdatedAtTag] = useState(updatedAtTag);
87
+ // Re-sync local state from the saved document each time `updatedAt` changes.
88
+ useEffect(()=>{
89
+ if (updatedAtTag && updatedAtTag !== prevUpdatedAtTag) {
90
+ const nextFocal = {
91
+ x: typeof data?.focalX === 'number' ? data.focalX : 50,
92
+ y: typeof data?.focalY === 'number' ? data.focalY : 50
93
+ };
94
+ queueMicrotask(()=>{
95
+ setPrevUpdatedAtTag(updatedAtTag);
96
+ hasUserEditedRef.current = false;
97
+ setFocalPoint(nextFocal);
98
+ setCrop(undefined);
99
+ });
100
+ if (uploadEdits?.crop || uploadEdits?.focalPoint) {
101
+ updateUploadEdits({
102
+ crop: undefined,
103
+ focalPoint: nextFocal,
104
+ heightInPixels: undefined,
105
+ widthInPixels: undefined
106
+ });
107
+ }
108
+ }
109
+ }, [
110
+ updatedAtTag,
111
+ prevUpdatedAtTag,
112
+ data?.focalX,
113
+ data?.focalY,
114
+ uploadEdits?.crop,
115
+ uploadEdits?.focalPoint,
116
+ updateUploadEdits
117
+ ]);
118
+ const hasRealCrop = !!crop && crop.width > 0 && crop.height > 0 && (crop.width !== 100 || crop.height !== 100 || crop.x !== 0 || crop.y !== 0);
119
+ // Auto-save to uploadEdits whenever state changes
120
+ useEffect(()=>{
121
+ if (!hasUserEditedRef.current) return;
122
+ const timeout = setTimeout(()=>{
123
+ setModified(true);
124
+ if (hasRealCrop && crop && naturalSize.width > 0 && naturalSize.height > 0) {
125
+ // Payload's cropImage computes `left = floor(x% * imgW / 100)` and
126
+ // `top = floor(y% * imgH / 100)`, then calls sharp.extract with our
127
+ // widthInPixels / heightInPixels. Sharp rejects the call if
128
+ // left + width > imgW (likewise for height) — even by 1px.
129
+ // We must compute width/height with floor and clamp so they always
130
+ // fit inside the image from that floored offset.
131
+ const leftPx = Math.floor(crop.x / 100 * naturalSize.width);
132
+ const topPx = Math.floor(crop.y / 100 * naturalSize.height);
133
+ const widthInPixels = Math.max(1, Math.min(Math.floor(crop.width / 100 * naturalSize.width), naturalSize.width - leftPx));
134
+ const heightInPixels = Math.max(1, Math.min(Math.floor(crop.height / 100 * naturalSize.height), naturalSize.height - topPx));
135
+ updateUploadEdits({
136
+ crop: {
137
+ unit: '%',
138
+ x: crop.x,
139
+ y: crop.y,
140
+ width: crop.width,
141
+ height: crop.height
142
+ },
143
+ focalPoint,
144
+ heightInPixels,
145
+ widthInPixels
146
+ });
147
+ } else {
148
+ updateUploadEdits({
149
+ crop: undefined,
150
+ focalPoint,
151
+ heightInPixels: undefined,
152
+ widthInPixels: undefined
153
+ });
154
+ }
155
+ }, 100);
156
+ return ()=>clearTimeout(timeout);
157
+ }, [
158
+ crop,
159
+ hasRealCrop,
160
+ focalPoint,
161
+ naturalSize,
162
+ setModified,
163
+ updateUploadEdits
164
+ ]);
165
+ const clampFocalToCrop = useCallback((x, y)=>{
166
+ if (crop && crop.unit === '%' && hasRealCrop) {
167
+ x = Math.max(crop.x, Math.min(crop.x + crop.width, x));
168
+ y = Math.max(crop.y, Math.min(crop.y + crop.height, y));
169
+ }
170
+ return {
171
+ x,
172
+ y
173
+ };
174
+ }, [
175
+ crop,
176
+ hasRealCrop
177
+ ]);
178
+ // Focal point drag handlers
179
+ const updateFocalFromEvent = useCallback((e)=>{
180
+ const container = containerRef.current;
181
+ if (!container) return;
182
+ const rect = container.getBoundingClientRect();
183
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
184
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
185
+ let x = (clientX - rect.left) / rect.width * 100;
186
+ let y = (clientY - rect.top) / rect.height * 100;
187
+ x = Math.max(0, Math.min(100, x));
188
+ y = Math.max(0, Math.min(100, y));
189
+ const clamped = clampFocalToCrop(x, y);
190
+ hasUserEditedRef.current = true;
191
+ setFocalPoint(clamped);
192
+ }, [
193
+ clampFocalToCrop
194
+ ]);
195
+ const handleMouseDown = useCallback((e)=>{
196
+ e.preventDefault();
197
+ setIsDragging(true);
198
+ updateFocalFromEvent(e);
199
+ }, [
200
+ updateFocalFromEvent
201
+ ]);
202
+ const handleTouchStart = useCallback((e)=>{
203
+ setIsDragging(true);
204
+ updateFocalFromEvent(e);
205
+ }, [
206
+ updateFocalFromEvent
207
+ ]);
208
+ useEffect(()=>{
209
+ if (!isDragging) return;
210
+ const handleMove = (e)=>{
211
+ e.preventDefault();
212
+ updateFocalFromEvent(e);
213
+ };
214
+ const handleUp = ()=>setIsDragging(false);
215
+ window.addEventListener('mousemove', handleMove);
216
+ window.addEventListener('mouseup', handleUp);
217
+ window.addEventListener('touchmove', handleMove, {
218
+ passive: false
219
+ });
220
+ window.addEventListener('touchend', handleUp);
221
+ return ()=>{
222
+ window.removeEventListener('mousemove', handleMove);
223
+ window.removeEventListener('mouseup', handleUp);
224
+ window.removeEventListener('touchmove', handleMove);
225
+ window.removeEventListener('touchend', handleUp);
226
+ };
227
+ }, [
228
+ isDragging,
229
+ updateFocalFromEvent
230
+ ]);
231
+ // react-image-crop gives two values: `crop` (in the current unit, often px
232
+ // relative to the *displayed* image) and `percentCrop` (always 0-100%).
233
+ // Store percentCrop so preview math is display-size-independent.
234
+ const handleCropChange = useCallback((_crop, _percentCrop)=>{
235
+ hasUserEditedRef.current = true;
236
+ setCrop(_percentCrop);
237
+ }, []);
238
+ const handleReset = useCallback(()=>{
239
+ hasUserEditedRef.current = true;
240
+ setFocalPoint({
241
+ x: 50,
242
+ y: 50
243
+ });
244
+ setCrop(undefined);
245
+ }, []);
246
+ const handleCropFieldChange = useCallback((field, value)=>{
247
+ hasUserEditedRef.current = true;
248
+ setCrop((prev)=>{
249
+ // Bootstrap a crop on first edit
250
+ const base = prev ?? {
251
+ unit: '%',
252
+ x: 0,
253
+ y: 0,
254
+ width: 100,
255
+ height: 100
256
+ };
257
+ const next = {
258
+ ...base,
259
+ [field]: value
260
+ };
261
+ // Clamp to image bounds (percent units)
262
+ if (next.unit === '%') {
263
+ next.x = Math.max(0, Math.min(100, next.x));
264
+ next.y = Math.max(0, Math.min(100, next.y));
265
+ next.width = Math.max(0, Math.min(100 - next.x, next.width));
266
+ next.height = Math.max(0, Math.min(100 - next.y, next.height));
267
+ }
268
+ return next;
269
+ });
270
+ }, []);
271
+ if (!imageUrl) {
272
+ return /*#__PURE__*/ _jsx("div", {
273
+ className: "focal-editor",
274
+ children: /*#__PURE__*/ _jsx("div", {
275
+ className: "focal-editor__empty",
276
+ children: "Upload an image to edit focal point and preview aspect ratios."
277
+ })
278
+ });
279
+ }
280
+ const cropConfig = hasRealCrop && crop ? {
281
+ unit: crop.unit,
282
+ x: crop.x,
283
+ y: crop.y,
284
+ width: crop.width,
285
+ height: crop.height
286
+ } : undefined;
287
+ const cropUnitLabel = crop?.unit === 'px' ? 'px' : '%';
288
+ return /*#__PURE__*/ _jsx("div", {
289
+ className: "focal-editor",
290
+ children: /*#__PURE__*/ _jsxs("div", {
291
+ className: "focal-editor__layout",
292
+ children: [
293
+ /*#__PURE__*/ _jsxs("div", {
294
+ className: "focal-editor__left",
295
+ children: [
296
+ /*#__PURE__*/ _jsxs("div", {
297
+ className: "focal-editor__toolbar",
298
+ children: [
299
+ /*#__PURE__*/ _jsxs("div", {
300
+ className: "focal-editor__toolbar-inputs",
301
+ children: [
302
+ /*#__PURE__*/ _jsxs("div", {
303
+ className: "focal-editor__toolbar-col",
304
+ children: [
305
+ /*#__PURE__*/ _jsx("span", {
306
+ className: "focal-editor__row-label",
307
+ children: "Focal"
308
+ }),
309
+ /*#__PURE__*/ _jsxs("div", {
310
+ className: "focal-editor__subgroup",
311
+ children: [
312
+ /*#__PURE__*/ _jsx("span", {
313
+ className: "focal-editor__subgroup-label",
314
+ children: "Position"
315
+ }),
316
+ /*#__PURE__*/ _jsxs("div", {
317
+ className: "focal-editor__subgroup-inputs",
318
+ children: [
319
+ /*#__PURE__*/ _jsxs("div", {
320
+ className: "focal-editor__input-group",
321
+ children: [
322
+ /*#__PURE__*/ _jsx("label", {
323
+ htmlFor: "focal-x",
324
+ children: "X"
325
+ }),
326
+ /*#__PURE__*/ _jsx("input", {
327
+ id: "focal-x",
328
+ type: "number",
329
+ min: 0,
330
+ max: 100,
331
+ value: Math.round(focalPoint.x),
332
+ onChange: (e)=>{
333
+ hasUserEditedRef.current = true;
334
+ setFocalPoint((prev)=>{
335
+ const x = Math.max(0, Math.min(100, Number(e.target.value)));
336
+ return clampFocalToCrop(x, prev.y);
337
+ });
338
+ }
339
+ }),
340
+ /*#__PURE__*/ _jsx("span", {
341
+ children: "%"
342
+ })
343
+ ]
344
+ }),
345
+ /*#__PURE__*/ _jsxs("div", {
346
+ className: "focal-editor__input-group",
347
+ children: [
348
+ /*#__PURE__*/ _jsx("label", {
349
+ htmlFor: "focal-y",
350
+ children: "Y"
351
+ }),
352
+ /*#__PURE__*/ _jsx("input", {
353
+ id: "focal-y",
354
+ type: "number",
355
+ min: 0,
356
+ max: 100,
357
+ value: Math.round(focalPoint.y),
358
+ onChange: (e)=>{
359
+ hasUserEditedRef.current = true;
360
+ setFocalPoint((prev)=>{
361
+ const y = Math.max(0, Math.min(100, Number(e.target.value)));
362
+ return clampFocalToCrop(prev.x, y);
363
+ });
364
+ }
365
+ }),
366
+ /*#__PURE__*/ _jsx("span", {
367
+ children: "%"
368
+ })
369
+ ]
370
+ })
371
+ ]
372
+ })
373
+ ]
374
+ })
375
+ ]
376
+ }),
377
+ /*#__PURE__*/ _jsxs("div", {
378
+ className: "focal-editor__toolbar-col",
379
+ children: [
380
+ /*#__PURE__*/ _jsx("span", {
381
+ className: "focal-editor__row-label",
382
+ children: "Crop"
383
+ }),
384
+ /*#__PURE__*/ _jsxs("div", {
385
+ className: "focal-editor__toolbar-row",
386
+ children: [
387
+ /*#__PURE__*/ _jsxs("div", {
388
+ className: "focal-editor__subgroup",
389
+ children: [
390
+ /*#__PURE__*/ _jsx("span", {
391
+ className: "focal-editor__subgroup-label",
392
+ children: "Position"
393
+ }),
394
+ /*#__PURE__*/ _jsxs("div", {
395
+ className: "focal-editor__subgroup-inputs",
396
+ children: [
397
+ /*#__PURE__*/ _jsxs("div", {
398
+ className: "focal-editor__input-group",
399
+ children: [
400
+ /*#__PURE__*/ _jsx("label", {
401
+ htmlFor: "crop-x",
402
+ children: "X"
403
+ }),
404
+ /*#__PURE__*/ _jsx("input", {
405
+ id: "crop-x",
406
+ type: "number",
407
+ min: 0,
408
+ max: 100,
409
+ value: crop ? Math.round(crop.x) : 0,
410
+ onChange: (e)=>handleCropFieldChange('x', Number(e.target.value)),
411
+ disabled: !hasRealCrop
412
+ }),
413
+ /*#__PURE__*/ _jsx("span", {
414
+ children: cropUnitLabel
415
+ })
416
+ ]
417
+ }),
418
+ /*#__PURE__*/ _jsxs("div", {
419
+ className: "focal-editor__input-group",
420
+ children: [
421
+ /*#__PURE__*/ _jsx("label", {
422
+ htmlFor: "crop-y",
423
+ children: "Y"
424
+ }),
425
+ /*#__PURE__*/ _jsx("input", {
426
+ id: "crop-y",
427
+ type: "number",
428
+ min: 0,
429
+ max: 100,
430
+ value: crop ? Math.round(crop.y) : 0,
431
+ onChange: (e)=>handleCropFieldChange('y', Number(e.target.value)),
432
+ disabled: !hasRealCrop
433
+ }),
434
+ /*#__PURE__*/ _jsx("span", {
435
+ children: cropUnitLabel
436
+ })
437
+ ]
438
+ })
439
+ ]
440
+ })
441
+ ]
442
+ }),
443
+ /*#__PURE__*/ _jsxs("div", {
444
+ className: "focal-editor__subgroup",
445
+ children: [
446
+ /*#__PURE__*/ _jsx("span", {
447
+ className: "focal-editor__subgroup-label",
448
+ children: "Size"
449
+ }),
450
+ /*#__PURE__*/ _jsxs("div", {
451
+ className: "focal-editor__subgroup-inputs",
452
+ children: [
453
+ /*#__PURE__*/ _jsxs("div", {
454
+ className: "focal-editor__input-group",
455
+ children: [
456
+ /*#__PURE__*/ _jsx("label", {
457
+ htmlFor: "crop-w",
458
+ children: "W"
459
+ }),
460
+ /*#__PURE__*/ _jsx("input", {
461
+ id: "crop-w",
462
+ type: "number",
463
+ min: 0,
464
+ max: 100,
465
+ value: crop ? Math.round(crop.width) : 0,
466
+ onChange: (e)=>handleCropFieldChange('width', Number(e.target.value)),
467
+ disabled: !hasRealCrop
468
+ }),
469
+ /*#__PURE__*/ _jsx("span", {
470
+ children: cropUnitLabel
471
+ })
472
+ ]
473
+ }),
474
+ /*#__PURE__*/ _jsxs("div", {
475
+ className: "focal-editor__input-group",
476
+ children: [
477
+ /*#__PURE__*/ _jsx("label", {
478
+ htmlFor: "crop-h",
479
+ children: "H"
480
+ }),
481
+ /*#__PURE__*/ _jsx("input", {
482
+ id: "crop-h",
483
+ type: "number",
484
+ min: 0,
485
+ max: 100,
486
+ value: crop ? Math.round(crop.height) : 0,
487
+ onChange: (e)=>handleCropFieldChange('height', Number(e.target.value)),
488
+ disabled: !hasRealCrop
489
+ }),
490
+ /*#__PURE__*/ _jsx("span", {
491
+ children: cropUnitLabel
492
+ })
493
+ ]
494
+ })
495
+ ]
496
+ })
497
+ ]
498
+ })
499
+ ]
500
+ })
501
+ ]
502
+ })
503
+ ]
504
+ }),
505
+ /*#__PURE__*/ _jsxs("div", {
506
+ className: "focal-editor__toolbar-actions",
507
+ children: [
508
+ /*#__PURE__*/ _jsx("button", {
509
+ className: "focal-editor__btn",
510
+ onClick: handleReset,
511
+ type: "button",
512
+ children: "Reset"
513
+ }),
514
+ /*#__PURE__*/ _jsxs("div", {
515
+ className: "focal-editor__mode-toggle",
516
+ children: [
517
+ /*#__PURE__*/ _jsx("button", {
518
+ className: `focal-editor__mode-btn ${mode === 'focal' ? 'focal-editor__mode-btn--active' : ''}`,
519
+ onClick: ()=>setMode('focal'),
520
+ type: "button",
521
+ children: "Focal Point"
522
+ }),
523
+ /*#__PURE__*/ _jsx("button", {
524
+ className: `focal-editor__mode-btn ${mode === 'crop' ? 'focal-editor__mode-btn--active' : ''}`,
525
+ onClick: ()=>setMode('crop'),
526
+ type: "button",
527
+ children: "Crop"
528
+ })
529
+ ]
530
+ })
531
+ ]
532
+ })
533
+ ]
534
+ }),
535
+ /*#__PURE__*/ _jsx("div", {
536
+ className: "focal-editor__image-wrapper",
537
+ children: /*#__PURE__*/ _jsx(ReactCrop, {
538
+ crop: mode === 'crop' ? crop : undefined,
539
+ onChange: handleCropChange,
540
+ disabled: mode === 'focal',
541
+ className: `focal-editor__react-crop ${mode === 'focal' ? 'is-focal-mode' : 'is-crop-mode'}`,
542
+ children: /*#__PURE__*/ _jsxs("div", {
543
+ style: {
544
+ position: 'relative',
545
+ display: 'inline-block',
546
+ lineHeight: 0,
547
+ width: '100%'
548
+ },
549
+ children: [
550
+ /*#__PURE__*/ _jsx("img", {
551
+ src: imageUrl,
552
+ alt: "Edit",
553
+ draggable: false,
554
+ style: {
555
+ display: 'block',
556
+ width: '100%',
557
+ height: 'auto',
558
+ objectFit: 'contain'
559
+ },
560
+ onLoad: (e)=>{
561
+ const img = e.currentTarget;
562
+ const displayedW = img.clientWidth;
563
+ const displayedH = img.clientHeight;
564
+ // If a legacy px crop was loaded from uploadEdits, convert
565
+ // it to percentages now that we know the displayed size.
566
+ if (crop?.unit === 'px' && displayedW > 0 && displayedH > 0) {
567
+ setCrop({
568
+ unit: '%',
569
+ x: crop.x / displayedW * 100,
570
+ y: crop.y / displayedH * 100,
571
+ width: crop.width / displayedW * 100,
572
+ height: crop.height / displayedH * 100
573
+ });
574
+ }
575
+ }
576
+ }),
577
+ mode === 'focal' && crop && crop.unit === '%' && hasRealCrop && /*#__PURE__*/ _jsx("div", {
578
+ className: "focal-editor__crop-overlay",
579
+ style: {
580
+ left: `${crop.x}%`,
581
+ top: `${crop.y}%`,
582
+ width: `${crop.width}%`,
583
+ height: `${crop.height}%`
584
+ }
585
+ }),
586
+ /*#__PURE__*/ _jsx("div", {
587
+ ref: containerRef,
588
+ className: `focal-editor__focal-overlay ${mode === 'focal' ? 'is-active' : ''}`,
589
+ onMouseDown: handleMouseDown,
590
+ onTouchStart: handleTouchStart,
591
+ children: mode === 'focal' && /*#__PURE__*/ _jsxs("div", {
592
+ className: "focal-editor__crosshair",
593
+ style: {
594
+ left: `${focalPoint.x}%`,
595
+ top: `${focalPoint.y}%`
596
+ },
597
+ children: [
598
+ /*#__PURE__*/ _jsx("div", {
599
+ className: "focal-editor__crosshair-h"
600
+ }),
601
+ /*#__PURE__*/ _jsx("div", {
602
+ className: "focal-editor__crosshair-v"
603
+ })
604
+ ]
605
+ })
606
+ })
607
+ ]
608
+ })
609
+ })
610
+ })
611
+ ]
612
+ }),
613
+ /*#__PURE__*/ _jsx("div", {
614
+ className: "focal-editor__right",
615
+ children: /*#__PURE__*/ _jsx(AspectRatioPreviewGrid, {
616
+ url: imageUrl,
617
+ focalX: focalPoint.x,
618
+ focalY: focalPoint.y,
619
+ aspectRatios: aspectRatios,
620
+ crop: cropConfig
621
+ })
622
+ })
623
+ ]
624
+ })
625
+ });
626
+ };
627
+ export default FocalPointEditor;
628
+
629
+ //# sourceMappingURL=FocalPointEditor.js.map