payload-plugin-aspect-preview 0.1.1 → 0.2.1

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.
@@ -46,9 +46,11 @@ function resolveCropPixels(crop, imgW, imgH) {
46
46
  h: crop.height
47
47
  };
48
48
  }
49
- // Editor stores focalX/focalY as percentages of the FULL image.
50
- // Convert to percentages within the crop region.
51
- function toCropRelativeFocal(focalX, focalY, crop, imgW, imgH) {
49
+ // Editor passes focalX/focalY as percentages of the FULL image; convert to
50
+ // percentages within the crop region. Pixel-based sibling of focal.ts'
51
+ // `toCropRelativeFocal` identical result, but expressed in pixels here
52
+ // because this component already resolves the crop to pixels.
53
+ function focalWithinCropPixels(focalX, focalY, crop, imgW, imgH) {
52
54
  const relX = (focalX / 100 * imgW - crop.x) / crop.w;
53
55
  const relY = (focalY / 100 * imgH - crop.y) / crop.h;
54
56
  return {
@@ -67,7 +69,7 @@ function computeCroppedBackgroundStyle(frameW, frameH, imgW, imgH, crop, focalX,
67
69
  // Scale the crop region to cover the frame (same math as object-fit:cover)
68
70
  const scale = Math.max(frameW / crop.w, frameH / crop.h);
69
71
  // Focal point inside the crop region, expressed as %
70
- const relFocal = toCropRelativeFocal(focalX, focalY, crop, imgW, imgH);
72
+ const relFocal = focalWithinCropPixels(focalX, focalY, crop, imgW, imgH);
71
73
  // Where the crop region's top-left lands in the frame, per object-position math
72
74
  const cropDisplayW = crop.w * scale;
73
75
  const cropDisplayH = crop.h * scale;
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useConfig, useDocumentInfo, useForm, useUploadEdits } from '@payloadcms/ui';
3
+ import { useConfig, useDocumentInfo, useField, useForm, useUploadEdits } from '@payloadcms/ui';
4
4
  import React, { useCallback, useEffect, useRef, useState } from 'react';
5
5
  import 'react-image-crop/dist/ReactCrop.css';
6
6
  import ReactCrop from 'react-image-crop';
7
7
  import { DEFAULT_ASPECT_RATIOS } from '../defaults';
8
+ import { toCropRelativeFocal, toFullImageFocal } from '../focal';
8
9
  import { AspectRatioPreviewGrid } from './AspectRatioPreviewGrid';
9
10
  export const FocalPointEditor = ()=>{
10
11
  const { data } = useDocumentInfo();
@@ -20,16 +21,36 @@ export const FocalPointEditor = ()=>{
20
21
  }) : undefined;
21
22
  const aspectRatios = entityConfig?.custom?.aspectPreview?.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
22
23
  const hasUserEditedRef = useRef(false);
24
+ // The freshly-picked File lives in the shared `file` form field (set by the
25
+ // upload component) before the document is saved. Reading it here lets us
26
+ // show the editor + preview immediately on selection, saving a save-round-trip.
27
+ const { value: pendingFile } = useField({
28
+ path: 'file'
29
+ });
30
+ const pendingUrl = React.useMemo(()=>pendingFile instanceof File && pendingFile.type.startsWith('image/') ? URL.createObjectURL(pendingFile) : undefined, [
31
+ pendingFile
32
+ ]);
33
+ // Revoke the object URL when it changes or the editor unmounts.
34
+ useEffect(()=>{
35
+ if (!pendingUrl) return;
36
+ return ()=>URL.revokeObjectURL(pendingUrl);
37
+ }, [
38
+ pendingUrl
39
+ ]);
23
40
  // Bust the browser cache when the document is re-saved so the regenerated
24
41
  // cropped image (same URL, new bytes) shows up in the preview grid.
25
42
  const rawUrl = data?.url;
26
43
  const updatedAtTag = data?.updatedAt ? String(data.updatedAt) : undefined;
27
44
  const imageUrl = React.useMemo(()=>{
45
+ // A pending selection wins over the saved doc — it's what the user is
46
+ // about to save. Blob URLs take no query string, so skip cache-busting.
47
+ if (pendingUrl) return pendingUrl;
28
48
  if (!rawUrl) return undefined;
29
49
  if (!updatedAtTag) return rawUrl;
30
50
  const sep = rawUrl.includes('?') ? '&' : '?';
31
51
  return `${rawUrl}${sep}v=${encodeURIComponent(updatedAtTag)}`;
32
52
  }, [
53
+ pendingUrl,
33
54
  rawUrl,
34
55
  updatedAtTag
35
56
  ]);
@@ -44,9 +65,18 @@ export const FocalPointEditor = ()=>{
44
65
  width: uploadEdits.crop.width,
45
66
  height: uploadEdits.crop.height
46
67
  } : undefined);
47
- const [focalPoint, setFocalPoint] = useState({
48
- x: uploadEdits?.focalPoint?.x || data?.focalX || 50,
49
- y: uploadEdits?.focalPoint?.y || data?.focalY || 50
68
+ // Focal point is stored relative to the active crop region: a 0-100 position
69
+ // inside the crop, or inside the whole image when there is no crop. This is
70
+ // the same space Payload persists (it bakes the crop into the base image and
71
+ // reads focalX/focalY as percentages of the cropped result), so uploadEdits
72
+ // and data.focalX/Y are already in this space — no conversion on load.
73
+ // `typeof` (not `||`) so a valid edge focal of 0 doesn't fall through to 50.
74
+ const [focalPoint, setFocalPoint] = useState(()=>{
75
+ if (uploadEdits?.focalPoint) return uploadEdits.focalPoint;
76
+ return {
77
+ x: typeof data?.focalX === 'number' ? data.focalX : 50,
78
+ y: typeof data?.focalY === 'number' ? data.focalY : 50
79
+ };
50
80
  });
51
81
  const containerRef = useRef(null);
52
82
  const [isDragging, setIsDragging] = useState(false);
@@ -62,7 +92,9 @@ export const FocalPointEditor = ()=>{
62
92
  const docWidth = typeof data?.width === 'number' ? data.width : 0;
63
93
  const docHeight = typeof data?.height === 'number' ? data.height : 0;
64
94
  useEffect(()=>{
65
- if (docWidth > 0 && docHeight > 0) {
95
+ // For a pending selection the doc dimensions are stale/absent — always
96
+ // decode the picked image so crop pixel math uses the real size.
97
+ if (!pendingUrl && docWidth > 0 && docHeight > 0) {
66
98
  queueMicrotask(()=>setNaturalSize({
67
99
  width: docWidth,
68
100
  height: docHeight
@@ -80,42 +112,60 @@ export const FocalPointEditor = ()=>{
80
112
  img.src = imageUrl;
81
113
  }, [
82
114
  imageUrl,
115
+ pendingUrl,
83
116
  docWidth,
84
117
  docHeight
85
118
  ]);
86
- const [prevUpdatedAtTag, setPrevUpdatedAtTag] = useState(updatedAtTag);
87
- // Re-sync local state from the saved document each time `updatedAt` changes.
119
+ // Re-sync local state from the saved document once per save. The guard is a
120
+ // ref (not state) so it flips synchronously the effect cannot re-enter when
121
+ // the updateUploadEdits call below changes the upload-edits context. Driving
122
+ // the guard through state (and listing uploadEdits in the deps) is what
123
+ // previously re-entered on every context change and exceeded React's update
124
+ // depth on save.
125
+ const lastSyncedTagRef = useRef(updatedAtTag);
88
126
  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
- }
127
+ if (!updatedAtTag || updatedAtTag === lastSyncedTagRef.current) return;
128
+ lastSyncedTagRef.current = updatedAtTag;
129
+ hasUserEditedRef.current = false;
130
+ const nextFocal = {
131
+ x: typeof data?.focalX === 'number' ? data.focalX : 50,
132
+ y: typeof data?.focalY === 'number' ? data.focalY : 50
133
+ };
134
+ setFocalPoint(nextFocal);
135
+ // The crop is now baked into the saved image, so there is no pending crop
136
+ // and the focal is relative to the whole (already-cropped) image again.
137
+ setCrop(undefined);
138
+ updateUploadEdits({
139
+ crop: undefined,
140
+ focalPoint: nextFocal,
141
+ heightInPixels: undefined,
142
+ widthInPixels: undefined
143
+ });
109
144
  }, [
110
145
  updatedAtTag,
111
- prevUpdatedAtTag,
112
146
  data?.focalX,
113
147
  data?.focalY,
114
- uploadEdits?.crop,
115
- uploadEdits?.focalPoint,
116
148
  updateUploadEdits
117
149
  ]);
118
150
  const hasRealCrop = !!crop && crop.width > 0 && crop.height > 0 && (crop.width !== 100 || crop.height !== 100 || crop.x !== 0 || crop.y !== 0);
151
+ // The rectangle the focal point is measured against: the crop when one exists
152
+ // (in %), otherwise the whole image. Focal state is always a 0-100 position
153
+ // inside this rectangle; converting to/from full-image space happens only at
154
+ // the pointer and rendering boundaries.
155
+ const cropRect = React.useMemo(()=>hasRealCrop && crop && crop.unit === '%' ? {
156
+ x: crop.x,
157
+ y: crop.y,
158
+ width: crop.width,
159
+ height: crop.height
160
+ } : {
161
+ x: 0,
162
+ y: 0,
163
+ width: 100,
164
+ height: 100
165
+ }, [
166
+ hasRealCrop,
167
+ crop
168
+ ]);
119
169
  // Auto-save to uploadEdits whenever state changes
120
170
  useEffect(()=>{
121
171
  if (!hasUserEditedRef.current) return;
@@ -140,6 +190,8 @@ export const FocalPointEditor = ()=>{
140
190
  width: crop.width,
141
191
  height: crop.height
142
192
  },
193
+ // focalPoint is already relative to the crop region — exactly the
194
+ // space Payload reads it in for the cropped image.
143
195
  focalPoint,
144
196
  heightInPixels,
145
197
  widthInPixels
@@ -162,35 +214,28 @@ export const FocalPointEditor = ()=>{
162
214
  setModified,
163
215
  updateUploadEdits
164
216
  ]);
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
217
+ // Focal point drag handler. The pointer lands somewhere on the full image;
218
+ // convert that full-image % into the crop-relative space the focal lives in,
219
+ // then clamp to the crop (0-100 inside cropRect).
179
220
  const updateFocalFromEvent = useCallback((e)=>{
180
221
  const container = containerRef.current;
181
222
  if (!container) return;
182
223
  const rect = container.getBoundingClientRect();
183
224
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
184
225
  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);
226
+ const fullX = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width * 100));
227
+ const fullY = Math.max(0, Math.min(100, (clientY - rect.top) / rect.height * 100));
228
+ const rel = toCropRelativeFocal({
229
+ x: fullX,
230
+ y: fullY
231
+ }, cropRect);
190
232
  hasUserEditedRef.current = true;
191
- setFocalPoint(clamped);
233
+ setFocalPoint({
234
+ x: Math.max(0, Math.min(100, rel.x)),
235
+ y: Math.max(0, Math.min(100, rel.y))
236
+ });
192
237
  }, [
193
- clampFocalToCrop
238
+ cropRect
194
239
  ]);
195
240
  const handleMouseDown = useCallback((e)=>{
196
241
  e.preventDefault();
@@ -285,6 +330,10 @@ export const FocalPointEditor = ()=>{
285
330
  height: crop.height
286
331
  } : undefined;
287
332
  const cropUnitLabel = crop?.unit === 'px' ? 'px' : '%';
333
+ // Full-image position of the focal point (crop-relative → whole image). Used
334
+ // to place the crosshair absolutely and to feed the preview grid, which
335
+ // re-derives the crop-relative position itself.
336
+ const focalFullImage = toFullImageFocal(focalPoint, cropRect);
288
337
  return /*#__PURE__*/ _jsx("div", {
289
338
  className: "focal-editor",
290
339
  children: /*#__PURE__*/ _jsxs("div", {
@@ -331,10 +380,10 @@ export const FocalPointEditor = ()=>{
331
380
  value: Math.round(focalPoint.x),
332
381
  onChange: (e)=>{
333
382
  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
- });
383
+ setFocalPoint((prev)=>({
384
+ ...prev,
385
+ x: Math.max(0, Math.min(100, Number(e.target.value)))
386
+ }));
338
387
  }
339
388
  }),
340
389
  /*#__PURE__*/ _jsx("span", {
@@ -357,10 +406,10 @@ export const FocalPointEditor = ()=>{
357
406
  value: Math.round(focalPoint.y),
358
407
  onChange: (e)=>{
359
408
  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
- });
409
+ setFocalPoint((prev)=>({
410
+ ...prev,
411
+ y: Math.max(0, Math.min(100, Number(e.target.value)))
412
+ }));
364
413
  }
365
414
  }),
366
415
  /*#__PURE__*/ _jsx("span", {
@@ -591,8 +640,8 @@ export const FocalPointEditor = ()=>{
591
640
  children: mode === 'focal' && /*#__PURE__*/ _jsxs("div", {
592
641
  className: "focal-editor__crosshair",
593
642
  style: {
594
- left: `${focalPoint.x}%`,
595
- top: `${focalPoint.y}%`
643
+ left: `${focalFullImage.x}%`,
644
+ top: `${focalFullImage.y}%`
596
645
  },
597
646
  children: [
598
647
  /*#__PURE__*/ _jsx("div", {
@@ -614,8 +663,8 @@ export const FocalPointEditor = ()=>{
614
663
  className: "focal-editor__right",
615
664
  children: /*#__PURE__*/ _jsx(AspectRatioPreviewGrid, {
616
665
  url: imageUrl,
617
- focalX: focalPoint.x,
618
- focalY: focalPoint.y,
666
+ focalX: focalFullImage.x,
667
+ focalY: focalFullImage.y,
619
668
  aspectRatios: aspectRatios,
620
669
  crop: cropConfig
621
670
  })
@@ -1,2 +1,6 @@
1
1
  import type { AspectRatioConfig } from './types.js';
2
+ /**
3
+ * The aspect ratios previewed when {@link AspectPreviewPluginOptions.aspectRatios}
4
+ * is omitted: square, 4:3, 3:2, 16:9, 9:16, and a 1200×630 social-share target.
5
+ */
2
6
  export declare const DEFAULT_ASPECT_RATIOS: AspectRatioConfig[];
package/dist/defaults.js CHANGED
@@ -1,4 +1,7 @@
1
- export const DEFAULT_ASPECT_RATIOS = [
1
+ /**
2
+ * The aspect ratios previewed when {@link AspectPreviewPluginOptions.aspectRatios}
3
+ * is omitted: square, 4:3, 3:2, 16:9, 9:16, and a 1200×630 social-share target.
4
+ */ export const DEFAULT_ASPECT_RATIOS = [
2
5
  {
3
6
  name: 'Square',
4
7
  ratio: '1:1',
@@ -0,0 +1,17 @@
1
+ import type { CropConfig, FocalPointState } from './types.js';
2
+ /** The crop rectangle in percentage units — all this math needs. */
3
+ type PercentCrop = Pick<CropConfig, 'x' | 'y' | 'width' | 'height'>;
4
+ /**
5
+ * Payload stores the focal point relative to the CROP region: on save it bakes
6
+ * the crop into the base image, then interprets `focalX/focalY` as percentages
7
+ * of that cropped image. The editor, by contrast, works in full-image
8
+ * coordinates because the crosshair is drawn over the whole picture.
9
+ *
10
+ * These two pure inverses convert between the spaces. The math is percentage
11
+ * only — the image's pixel dimensions cancel out, so just the crop rectangle
12
+ * (in %) is required. Callers must guarantee a non-zero crop width/height.
13
+ */
14
+ export declare function toCropRelativeFocal(focal: FocalPointState, crop: PercentCrop): FocalPointState;
15
+ /** Inverse of {@link toCropRelativeFocal}: crop-relative % back to full-image %. */
16
+ export declare function toFullImageFocal(focal: FocalPointState, crop: PercentCrop): FocalPointState;
17
+ export {};
package/dist/focal.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Payload stores the focal point relative to the CROP region: on save it bakes
3
+ * the crop into the base image, then interprets `focalX/focalY` as percentages
4
+ * of that cropped image. The editor, by contrast, works in full-image
5
+ * coordinates because the crosshair is drawn over the whole picture.
6
+ *
7
+ * These two pure inverses convert between the spaces. The math is percentage
8
+ * only — the image's pixel dimensions cancel out, so just the crop rectangle
9
+ * (in %) is required. Callers must guarantee a non-zero crop width/height.
10
+ */ export function toCropRelativeFocal(focal, crop) {
11
+ return {
12
+ x: (focal.x - crop.x) / crop.width * 100,
13
+ y: (focal.y - crop.y) / crop.height * 100
14
+ };
15
+ }
16
+ /** Inverse of {@link toCropRelativeFocal}: crop-relative % back to full-image %. */ export function toFullImageFocal(focal, crop) {
17
+ return {
18
+ x: crop.x + focal.x / 100 * crop.width,
19
+ y: crop.y + focal.y / 100 * crop.height
20
+ };
21
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,28 @@
1
1
  import type { Plugin } from 'payload';
2
2
  import type { AspectPreviewPluginOptions } from './types.js';
3
+ /**
4
+ * Adds an inline focal-point + crop editor with a live aspect-ratio preview
5
+ * grid to Payload upload collections.
6
+ *
7
+ * For every collection listed in {@link AspectPreviewPluginOptions.collections}
8
+ * it appends an `aspectPreview` UI field that renders the editor and (unless
9
+ * {@link AspectPreviewPluginOptions.disabled}) overrides the collection's upload
10
+ * component to replace Payload's default crop drawer.
11
+ *
12
+ * @param options - see {@link AspectPreviewPluginOptions}.
13
+ * @returns a Payload {@link Plugin}.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // payload.config.ts
18
+ * import { aspectPreviewPlugin } from 'payload-plugin-aspect-preview'
19
+ *
20
+ * export default buildConfig({
21
+ * plugins: [
22
+ * // aspectRatios defaults to DEFAULT_ASPECT_RATIOS when omitted
23
+ * aspectPreviewPlugin({ collections: ['media'] }),
24
+ * ],
25
+ * })
26
+ * ```
27
+ */
3
28
  export declare const aspectPreviewPlugin: (options: AspectPreviewPluginOptions) => Plugin;
package/dist/plugin.js CHANGED
@@ -1,7 +1,31 @@
1
1
  import { DEFAULT_ASPECT_RATIOS } from './defaults.js';
2
2
  const FIELD_COMPONENT = 'payload-plugin-aspect-preview/client#FocalPointEditor';
3
3
  const UPLOAD_COMPONENT = 'payload-plugin-aspect-preview/client#CustomUpload';
4
- export const aspectPreviewPlugin = (options)=>(config)=>{
4
+ /**
5
+ * Adds an inline focal-point + crop editor with a live aspect-ratio preview
6
+ * grid to Payload upload collections.
7
+ *
8
+ * For every collection listed in {@link AspectPreviewPluginOptions.collections}
9
+ * it appends an `aspectPreview` UI field that renders the editor and (unless
10
+ * {@link AspectPreviewPluginOptions.disabled}) overrides the collection's upload
11
+ * component to replace Payload's default crop drawer.
12
+ *
13
+ * @param options - see {@link AspectPreviewPluginOptions}.
14
+ * @returns a Payload {@link Plugin}.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // payload.config.ts
19
+ * import { aspectPreviewPlugin } from 'payload-plugin-aspect-preview'
20
+ *
21
+ * export default buildConfig({
22
+ * plugins: [
23
+ * // aspectRatios defaults to DEFAULT_ASPECT_RATIOS when omitted
24
+ * aspectPreviewPlugin({ collections: ['media'] }),
25
+ * ],
26
+ * })
27
+ * ```
28
+ */ export const aspectPreviewPlugin = (options)=>(config)=>{
5
29
  const aspectRatios = options.aspectRatios ?? DEFAULT_ASPECT_RATIOS;
6
30
  const enabled = new Set(options.collections);
7
31
  return {
@@ -16,6 +40,9 @@ export const aspectPreviewPlugin = (options)=>(config)=>{
16
40
  name: 'aspectPreview',
17
41
  type: 'ui',
18
42
  admin: {
43
+ // It's an edit-view editor, not data — keep it out of the
44
+ // list view's columns.
45
+ disableListColumn: true,
19
46
  components: {
20
47
  Field: FIELD_COMPONENT
21
48
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * A single aspect ratio shown in the editor's live preview grid. Pass an array
3
+ * of these as {@link AspectPreviewPluginOptions.aspectRatios} to tailor the
4
+ * ratios to your frontend; see {@link DEFAULT_ASPECT_RATIOS} for the shipped set.
5
+ */
1
6
  export interface AspectRatioConfig {
2
7
  /** Display label, e.g. "Social Share". */
3
8
  name: string;
@@ -14,6 +19,7 @@ export interface AspectRatioConfig {
14
19
  /** Optional free-form grouping label. */
15
20
  category?: string;
16
21
  }
22
+ /** Options for {@link aspectPreviewPlugin}. */
17
23
  export interface AspectPreviewPluginOptions {
18
24
  /** Upload collections (by slug) to enhance with the inline editor. */
19
25
  collections: string[];
@@ -22,6 +28,7 @@ export interface AspectPreviewPluginOptions {
22
28
  /** Disable the plugin while keeping the schema field for DB consistency. */
23
29
  disabled?: boolean;
24
30
  }
31
+ /** A crop rectangle. `x`/`y`/`width`/`height` are in `unit` (percent or pixels). */
25
32
  export interface CropConfig {
26
33
  unit: 'px' | '%';
27
34
  x: number;
@@ -29,10 +36,12 @@ export interface CropConfig {
29
36
  width: number;
30
37
  height: number;
31
38
  }
39
+ /** Focal point as percentages (0-100) of its reference image. */
32
40
  export interface FocalPointState {
33
41
  x: number;
34
42
  y: number;
35
43
  }
44
+ /** The editor's pending edits, mirrored into Payload's upload-edits context. */
36
45
  export interface UploadEditsState {
37
46
  crop?: CropConfig;
38
47
  focalPoint?: FocalPointState;
package/dist/types.js CHANGED
@@ -1 +1,5 @@
1
- export { };
1
+ /**
2
+ * A single aspect ratio shown in the editor's live preview grid. Pass an array
3
+ * of these as {@link AspectPreviewPluginOptions.aspectRatios} to tailor the
4
+ * ratios to your frontend; see {@link DEFAULT_ASPECT_RATIOS} for the shipped set.
5
+ */ /** The editor's pending edits, mirrored into Payload's upload-edits context. */ export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-plugin-aspect-preview",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Single-screen crop, focal point, and live multi-aspect-ratio preview editor for Payload upload collections.",
5
5
  "keywords": [
6
6
  "payload",