payload-plugin-aspect-preview 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # payload-plugin-aspect-preview
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/payload-plugin-aspect-preview.svg)](https://www.npmjs.com/package/payload-plugin-aspect-preview)
4
+ [![license](https://img.shields.io/npm/l/payload-plugin-aspect-preview.svg)](./LICENSE.md)
5
+
3
6
  Single-screen crop, focal point, and **live multi-aspect-ratio preview** for Payload upload collections. Replaces Payload's separate crop/focal drawers with one editor that shows, in real time, how your image will look at every aspect ratio your frontend uses.
4
7
 
5
8
  ![edit_preview](./payload-plugin-aspect-preview.png)
@@ -226,5 +226,3 @@ export const AspectRatioPreviewGrid = ({ url, focalX, focalY, aspectRatios, crop
226
226
  });
227
227
  };
228
228
  export default AspectRatioPreviewGrid;
229
-
230
- //# sourceMappingURL=AspectRatioPreviewGrid.js.map
@@ -163,5 +163,3 @@ export const CustomUpload = ()=>{
163
163
  });
164
164
  };
165
165
  export default CustomUpload;
166
-
167
- //# sourceMappingURL=CustomUpload.js.map
@@ -625,5 +625,3 @@ export const FocalPointEditor = ()=>{
625
625
  });
626
626
  };
627
627
  export default FocalPointEditor;
628
-
629
- //# sourceMappingURL=FocalPointEditor.js.map
package/dist/defaults.js CHANGED
@@ -54,5 +54,3 @@ export const DEFAULT_ASPECT_RATIOS = [
54
54
  category: 'social'
55
55
  }
56
56
  ];
57
-
58
- //# sourceMappingURL=defaults.js.map
@@ -1,5 +1,3 @@
1
1
  export { FocalPointEditor } from '../components/FocalPointEditor.js';
2
2
  export { AspectRatioPreviewGrid } from '../components/AspectRatioPreviewGrid.js';
3
3
  export { CustomUpload } from '../components/CustomUpload.js';
4
-
5
- //# sourceMappingURL=client.js.map
@@ -1,3 +1 @@
1
1
  export { };
2
-
3
- //# sourceMappingURL=types.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,2 @@
1
1
  export { aspectPreviewPlugin } from './plugin.js';
2
2
  export { DEFAULT_ASPECT_RATIOS } from './defaults.js';
3
-
4
- //# sourceMappingURL=index.js.map
package/dist/plugin.js CHANGED
@@ -48,5 +48,3 @@ export const aspectPreviewPlugin = (options)=>(config)=>{
48
48
  })
49
49
  };
50
50
  };
51
-
52
- //# sourceMappingURL=plugin.js.map
package/dist/types.js CHANGED
@@ -1,3 +1 @@
1
1
  export { };
2
-
3
- //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "payload-plugin-aspect-preview",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Single-screen crop, focal point, and live multi-aspect-ratio preview editor for Payload upload collections.",
5
- "keywords": ["payload", "payload-plugin", "payloadcms", "focal-point", "crop", "aspect-ratio", "media"],
5
+ "keywords": [
6
+ "payload",
7
+ "payload-plugin",
8
+ "payloadcms",
9
+ "focal-point",
10
+ "crop",
11
+ "aspect-ratio",
12
+ "media"
13
+ ],
6
14
  "license": "MIT",
7
15
  "author": "Sebastian Kolbusz <s.kolbusz@outlook.com>",
8
16
  "repository": {
@@ -37,7 +45,9 @@
37
45
  "default": "./dist/exports/types.js"
38
46
  }
39
47
  },
40
- "files": ["dist"],
48
+ "files": [
49
+ "dist"
50
+ ],
41
51
  "scripts": {
42
52
  "build": "pnpm clean && pnpm copyfiles && pnpm build:types && pnpm build:swc",
43
53
  "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths --ignore \"**/*.spec.ts\"",
@@ -47,7 +57,9 @@
47
57
  "test": "vitest run",
48
58
  "test:watch": "vitest",
49
59
  "dev": "next dev dev --turbo",
50
- "prepublishOnly": "pnpm build"
60
+ "prepublishOnly": "pnpm build",
61
+ "lint": "eslint .",
62
+ "lint:fix": "eslint . --fix"
51
63
  },
52
64
  "dependencies": {
53
65
  "react-image-crop": "^11.1.2"
@@ -55,10 +67,10 @@
55
67
  "peerDependencies": {
56
68
  "@payloadcms/ui": "^3.85.0",
57
69
  "payload": "^3.85.0",
58
- "react": "^19.0.0",
59
- "react-dom": "^19.0.0"
70
+ "react": "^19.0.0"
60
71
  },
61
72
  "devDependencies": {
73
+ "@eslint/js": "^10.0.1",
62
74
  "@payloadcms/db-mongodb": "^3.85.0",
63
75
  "@payloadcms/next": "^3.85.0",
64
76
  "@payloadcms/richtext-lexical": "^3.85.0",
@@ -67,9 +79,9 @@
67
79
  "@swc/core": "^1.10.0",
68
80
  "@types/node": "^22.0.0",
69
81
  "@types/react": "^19.0.0",
70
- "@types/react-dom": "^19.0.0",
71
82
  "copyfiles": "^2.4.1",
72
- "cross-env": "^7.0.3",
83
+ "eslint": "^10.7.0",
84
+ "eslint-plugin-react-hooks": "^7.1.1",
73
85
  "next": "^16.0.0",
74
86
  "payload": "^3.85.0",
75
87
  "react": "^19.0.0",
@@ -77,6 +89,8 @@
77
89
  "rimraf": "^6.0.0",
78
90
  "sharp": "0.34.2",
79
91
  "typescript": "^5.7.0",
92
+ "typescript-eslint": "^8.63.0",
80
93
  "vitest": "^3.0.0"
81
- }
94
+ },
95
+ "packageManager": "pnpm@10.34.4"
82
96
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/components/AspectRatioPreviewGrid.tsx"],"sourcesContent":["'use client'\n\nimport React, { useEffect, useRef, useState } from 'react'\n\nimport type { AspectRatioConfig, CropConfig } from '../types'\n\ninterface AspectRatioPreviewGridProps {\n url: string\n focalX: number\n focalY: number\n aspectRatios: AspectRatioConfig[]\n crop?: CropConfig\n}\n\nfunction clamp(val: number, min: number, max: number) {\n return Math.max(min, Math.min(max, val))\n}\n\nfunction isValidNumber(n: number) {\n return typeof n === 'number' && !isNaN(n) && isFinite(n) && n > 0\n}\n\nfunction isValidCrop(crop: CropConfig | undefined, imgW: number, imgH: number) {\n if (!crop) return false\n\n let cropW: number\n let cropH: number\n\n if (crop.unit === '%') {\n cropW = crop.width\n cropH = crop.height\n if (!isValidNumber(cropW) || !isValidNumber(cropH)) return false\n if (cropW > 100 || cropH > 100) return false\n if (crop.x < 0 || crop.y < 0) return false\n if (crop.x + cropW > 100 + 1e-6) return false\n if (crop.y + cropH > 100 + 1e-6) return false\n } else {\n if (crop.width > imgW || crop.height > imgH || crop.x > imgW || crop.y > imgH) {\n return false\n }\n if (!isValidNumber(crop.width) || !isValidNumber(crop.height)) return false\n if (crop.x < 0 || crop.y < 0) return false\n cropW = crop.width\n cropH = crop.height\n }\n\n return cropW > 1e-6 && cropH > 1e-6\n}\n\nfunction resolveCropPixels(crop: CropConfig, imgW: number, imgH: number) {\n if (crop.unit === '%') {\n return {\n x: (crop.x / 100) * imgW,\n y: (crop.y / 100) * imgH,\n w: (crop.width / 100) * imgW,\n h: (crop.height / 100) * imgH,\n }\n }\n return { x: crop.x, y: crop.y, w: crop.width, h: crop.height }\n}\n\n// Editor stores focalX/focalY as percentages of the FULL image.\n// Convert to percentages within the crop region.\nfunction toCropRelativeFocal(\n focalX: number,\n focalY: number,\n crop: { x: number; y: number; w: number; h: number },\n imgW: number,\n imgH: number,\n) {\n const relX = ((focalX / 100) * imgW - crop.x) / crop.w\n const relY = ((focalY / 100) * imgH - crop.y) / crop.h\n return {\n x: clamp(relX * 100, 0, 100),\n y: clamp(relY * 100, 0, 100),\n }\n}\n\n// Compute a background-image style on the FRAME that simulates Payload's\n// server-side behavior: take the crop region from the original image, then\n// render it with object-fit:cover + object-position:focal% into the frame.\n//\n// Background-image is used instead of <img> because Payload's admin CSS\n// applies aggressive global rules to <img> (max-width:100%, height:auto)\n// that fight inline pixel sizing and cause squashing.\nfunction computeCroppedBackgroundStyle(\n frameW: number,\n frameH: number,\n imgW: number,\n imgH: number,\n crop: { x: number; y: number; w: number; h: number },\n focalX: number,\n focalY: number,\n): { backgroundSize: string; backgroundPosition: string } {\n // Scale the crop region to cover the frame (same math as object-fit:cover)\n const scale = Math.max(frameW / crop.w, frameH / crop.h)\n\n // Focal point inside the crop region, expressed as %\n const relFocal = toCropRelativeFocal(focalX, focalY, crop, imgW, imgH)\n\n // Where the crop region's top-left lands in the frame, per object-position math\n const cropDisplayW = crop.w * scale\n const cropDisplayH = crop.h * scale\n const cropLeft = (frameW - cropDisplayW) * (relFocal.x / 100)\n const cropTop = (frameH - cropDisplayH) * (relFocal.y / 100)\n\n // Position the FULL image so the crop region lands at cropLeft / cropTop\n const bgLeft = cropLeft - crop.x * scale\n const bgTop = cropTop - crop.y * scale\n\n const bgW = imgW * scale\n const bgH = imgH * scale\n\n return {\n backgroundSize: `${bgW}px ${bgH}px`,\n backgroundPosition: `${bgLeft}px ${bgTop}px`,\n }\n}\n\nconst PreviewCard: React.FC<{\n ratio: AspectRatioConfig\n url: string\n focalX: number\n focalY: number\n crop?: CropConfig\n imgW: number\n imgH: number\n}> = ({ ratio, url, focalX, focalY, crop, imgW, imgH }) => {\n const frameRef = useRef<HTMLDivElement>(null)\n const [frameSize, setFrameSize] = useState({ width: 0, height: 0 })\n\n useEffect(() => {\n const el = frameRef.current\n if (!el) return\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect\n setFrameSize({ width, height })\n }\n })\n observer.observe(el)\n return () => observer.disconnect()\n }, [])\n\n const frameStyle: React.CSSProperties = React.useMemo(() => {\n const base: React.CSSProperties = {\n aspectRatio: `${ratio.width} / ${ratio.height}`,\n backgroundImage: `url(\"${url}\")`,\n backgroundRepeat: 'no-repeat',\n }\n\n const haveDims =\n frameSize.width > 0 && frameSize.height > 0 && isValidNumber(imgW) && isValidNumber(imgH)\n\n // Fallback: no crop OR dimensions not yet known.\n // Use CSS-native cover + position — identical to the frontend's\n // `object-fit:cover; object-position: focalX% focalY%`.\n if (!haveDims || !isValidCrop(crop, imgW, imgH)) {\n return {\n ...base,\n backgroundSize: 'cover',\n backgroundPosition: `${focalX}% ${focalY}%`,\n }\n }\n\n const cropPx = resolveCropPixels(crop!, imgW, imgH)\n const { backgroundSize, backgroundPosition } = computeCroppedBackgroundStyle(\n frameSize.width,\n frameSize.height,\n imgW,\n imgH,\n cropPx,\n focalX,\n focalY,\n )\n return { ...base, backgroundSize, backgroundPosition }\n }, [frameSize, imgW, imgH, focalX, focalY, crop, ratio.width, ratio.height, url])\n\n return (\n <div className=\"focal-preview__card\">\n <div className=\"focal-preview__card-header\">\n <span className=\"focal-preview__card-name\">{ratio.name}</span>\n <span className=\"focal-preview__card-ratio\">{ratio.ratio}</span>\n </div>\n <div\n ref={frameRef}\n className=\"focal-preview__card-frame\"\n style={frameStyle}\n role=\"img\"\n aria-label={`Preview for ${ratio.name}`}\n >\n <div className=\"focal-preview__card-overlay\" />\n <div className=\"focal-preview__card-crosshair\" />\n </div>\n <div className=\"focal-preview__card-footer\">\n <span className=\"focal-preview__card-usage\">{ratio.usage}</span>\n <span className={`focal-preview__card-source focal-preview__card-source--${ratio.source}`}>\n {ratio.source === 'css' ? 'CSS' : 'Crop'}\n </span>\n </div>\n </div>\n )\n}\n\nexport const AspectRatioPreviewGrid: React.FC<AspectRatioPreviewGridProps> = ({\n url,\n focalX,\n focalY,\n aspectRatios,\n crop,\n}) => {\n const [imgDimensions, setImgDimensions] = useState({ width: 0, height: 0 })\n\n useEffect(() => {\n const img = new Image()\n img.onload = () => {\n setImgDimensions({ width: img.naturalWidth, height: img.naturalHeight })\n }\n img.onerror = () => {\n setImgDimensions({ width: 0, height: 0 })\n }\n img.src = url\n }, [url])\n\n return (\n <div className=\"focal-preview\">\n <div className=\"focal-preview__grid\">\n {aspectRatios.map((ratio) => (\n <PreviewCard\n key={ratio.name}\n ratio={ratio}\n url={url}\n focalX={focalX}\n focalY={focalY}\n crop={crop}\n imgW={imgDimensions.width}\n imgH={imgDimensions.height}\n />\n ))}\n </div>\n </div>\n )\n}\n\nexport default AspectRatioPreviewGrid\n"],"names":["React","useEffect","useRef","useState","clamp","val","min","max","Math","isValidNumber","n","isNaN","isFinite","isValidCrop","crop","imgW","imgH","cropW","cropH","unit","width","height","x","y","resolveCropPixels","w","h","toCropRelativeFocal","focalX","focalY","relX","relY","computeCroppedBackgroundStyle","frameW","frameH","scale","relFocal","cropDisplayW","cropDisplayH","cropLeft","cropTop","bgLeft","bgTop","bgW","bgH","backgroundSize","backgroundPosition","PreviewCard","ratio","url","frameRef","frameSize","setFrameSize","el","current","observer","ResizeObserver","entries","entry","contentRect","observe","disconnect","frameStyle","useMemo","base","aspectRatio","backgroundImage","backgroundRepeat","haveDims","cropPx","div","className","span","name","ref","style","role","aria-label","usage","source","AspectRatioPreviewGrid","aspectRatios","imgDimensions","setImgDimensions","img","Image","onload","naturalWidth","naturalHeight","onerror","src","map"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/components/CustomUpload.tsx"],"sourcesContent":["'use client'\n\nimport type { SanitizedUploadConfig } from 'payload'\n\nimport {\n Button,\n Drawer,\n Dropzone,\n FileDetails,\n PreviewSizes,\n Thumbnail,\n fieldBaseClass,\n useConfig,\n useDocumentInfo,\n useField,\n useForm,\n useModal,\n useTranslation,\n useUploadEdits,\n} from '@payloadcms/ui'\nimport { isImage } from 'payload/shared'\nimport React, { useCallback, useState } from 'react'\n\nimport '../styles.scss'\n\nconst baseClass = 'file-field'\nconst sizePreviewSlug = 'preview-sizes'\n\nconst validate = (value: File | undefined) => {\n if (!value && value !== undefined) {\n return 'A file is required.'\n }\n if (value && (!value.name || value.name === '')) {\n return 'A file name is required.'\n }\n return true\n}\n\nexport const CustomUpload: React.FC = () => {\n const { t } = useTranslation()\n const { getEntityConfig } = useConfig()\n const { data, docPermissions } = useDocumentInfo()\n const { setModified } = useForm()\n const { openModal } = useModal()\n const { resetUploadEdits } = useUploadEdits()\n\n const collectionSlug = data?.collection || 'media'\n const collectionConfig = getEntityConfig({ collectionSlug })\n const uploadConfig = collectionConfig?.upload as SanitizedUploadConfig | undefined\n\n const { setValue, value } = useField<File>({ path: 'file', validate })\n\n const [fileSrc, setFileSrc] = useState<string | undefined>(undefined)\n const [removedFile, setRemovedFile] = useState(false)\n\n const hasImageSizes = ((uploadConfig?.imageSizes?.length as number | undefined) || 0) > 0\n const imageCacheTag = uploadConfig?.cacheTags && data?.updatedAt\n\n const handleFileChange = useCallback(\n (file: File | null) => {\n if (file) {\n setFileSrc(URL.createObjectURL(file))\n }\n setValue(file)\n setModified(true)\n },\n [setValue, setModified],\n )\n\n const handleFileRemoval = useCallback(() => {\n setRemovedFile(true)\n setValue(null)\n setFileSrc(undefined)\n resetUploadEdits()\n setModified(true)\n }, [setValue, resetUploadEdits, setModified])\n\n const handleDropzoneChange = useCallback(\n (files: FileList | null) => {\n if (files && files.length > 0) {\n handleFileChange(files[0])\n }\n },\n [handleFileChange],\n )\n\n const hasExistingFile = data?.filename && !removedFile\n const hasNewFile = value && fileSrc\n\n return (\n <div className={[fieldBaseClass, baseClass].join(' ')}>\n {/* Existing file display — no edit button, editor is a separate UI field */}\n {hasExistingFile && (\n <FileDetails\n collectionSlug={collectionSlug}\n doc={data}\n enableAdjustments={false}\n handleRemove={docPermissions?.update ? handleFileRemoval : undefined}\n hasImageSizes={hasImageSizes}\n uploadConfig={uploadConfig!}\n imageCacheTag={imageCacheTag}\n />\n )}\n\n {/* Upload area */}\n {(!hasExistingFile || removedFile) && (\n <div className={`${baseClass}__upload`}>\n <Dropzone onChange={handleDropzoneChange}>\n <div className={`${baseClass}__dropzoneContent`}>\n <Button\n buttonStyle=\"pill\"\n onClick={() => document.getElementById('file-input')?.click()}\n size=\"small\"\n >\n {t('upload:selectFile')}\n </Button>\n <input\n id=\"file-input\"\n type=\"file\"\n hidden\n onChange={(e) => handleFileChange(e.target.files?.[0] || null)}\n />\n </div>\n </Dropzone>\n </div>\n )}\n\n {/* Selected file preview */}\n {hasNewFile && (\n <div className={`${baseClass}__file-selected`}>\n <div className={`${baseClass}__thumbnail-wrap`}>\n <Thumbnail\n collectionSlug={collectionSlug}\n fileSrc={isImage(value.type) ? fileSrc : undefined}\n />\n </div>\n <div className={`${baseClass}__file-adjustments`}>\n <input\n type=\"text\"\n value={value.name}\n onChange={(e) => {\n const renamed = new File([value], e.target.value, { type: value.type })\n handleFileChange(renamed)\n }}\n />\n <div className={`${baseClass}__upload-actions`}>\n {hasImageSizes && (\n <Button buttonStyle=\"pill\" size=\"small\" onClick={() => openModal(sizePreviewSlug)}>\n {t('upload:previewSizes')}\n </Button>\n )}\n </div>\n </div>\n <Button\n buttonStyle=\"icon-label\"\n icon=\"x\"\n onClick={handleFileRemoval}\n round\n tooltip={t('general:cancel')}\n />\n </div>\n )}\n\n {/* Preview Sizes Drawer */}\n {data && hasImageSizes && (\n <Drawer slug={sizePreviewSlug} title={t('upload:sizesFor', { label: data.filename })}>\n <PreviewSizes doc={data} imageCacheTag={imageCacheTag} uploadConfig={uploadConfig!} />\n </Drawer>\n )}\n </div>\n )\n}\n\nexport default CustomUpload\n"],"names":["Button","Drawer","Dropzone","FileDetails","PreviewSizes","Thumbnail","fieldBaseClass","useConfig","useDocumentInfo","useField","useForm","useModal","useTranslation","useUploadEdits","isImage","React","useCallback","useState","baseClass","sizePreviewSlug","validate","value","undefined","name","CustomUpload","t","getEntityConfig","data","docPermissions","setModified","openModal","resetUploadEdits","collectionSlug","collection","collectionConfig","uploadConfig","upload","setValue","path","fileSrc","setFileSrc","removedFile","setRemovedFile","hasImageSizes","imageSizes","length","imageCacheTag","cacheTags","updatedAt","handleFileChange","file","URL","createObjectURL","handleFileRemoval","handleDropzoneChange","files","hasExistingFile","filename","hasNewFile","div","className","join","doc","enableAdjustments","handleRemove","update","onChange","buttonStyle","onClick","document","getElementById","click","size","input","id","type","hidden","e","target","renamed","File","icon","round","tooltip","slug","title","label"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/components/FocalPointEditor.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useDocumentInfo, useForm, useUploadEdits } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport 'react-image-crop/dist/ReactCrop.css'\nimport ReactCrop, { type Crop } from 'react-image-crop'\n\nimport { DEFAULT_ASPECT_RATIOS } from '../defaults'\nimport type { AspectRatioConfig, CropConfig } from '../types'\n\nimport { AspectRatioPreviewGrid } from './AspectRatioPreviewGrid'\n\nexport const FocalPointEditor: React.FC = () => {\n const { data } = useDocumentInfo()\n const { setModified } = useForm()\n const { uploadEdits, updateUploadEdits } = useUploadEdits()\n const { getEntityConfig } = useConfig()\n\n // Read the ratios the plugin stashed in the collection's `custom` config.\n // Falls back to the shipped defaults so the editor renders even if a\n // consumer wired the field without options.\n const collectionSlug = typeof data?.collection === 'string' ? data.collection : undefined\n const entityConfig = collectionSlug ? getEntityConfig({ collectionSlug }) : undefined\n const aspectRatios =\n (((entityConfig as unknown as { custom?: { aspectPreview?: { aspectRatios?: AspectRatioConfig[] } } })?.custom\n ?.aspectPreview?.aspectRatios) as AspectRatioConfig[] | undefined) ?? DEFAULT_ASPECT_RATIOS\n\n const hasUserEditedRef = useRef(false)\n\n // Bust the browser cache when the document is re-saved so the regenerated\n // cropped image (same URL, new bytes) shows up in the preview grid.\n const rawUrl = data?.url as string | undefined\n const updatedAtTag = data?.updatedAt ? String(data.updatedAt) : undefined\n const imageUrl = React.useMemo(() => {\n if (!rawUrl) return undefined\n if (!updatedAtTag) return rawUrl\n const sep = rawUrl.includes('?') ? '&' : '?'\n return `${rawUrl}${sep}v=${encodeURIComponent(updatedAtTag)}`\n }, [rawUrl, updatedAtTag])\n\n const [mode, setMode] = useState<'crop' | 'focal'>('focal')\n // Crop is undefined when none has been drawn yet. When the user enters crop\n // mode without an existing crop, ReactCrop should wait for them to draw one\n // — we don't pre-fill 100% any more.\n const [crop, setCrop] = useState<Crop | undefined>(\n uploadEdits?.crop\n ? {\n unit: uploadEdits.crop.unit as 'px' | '%',\n x: uploadEdits.crop.x,\n y: uploadEdits.crop.y,\n width: uploadEdits.crop.width,\n height: uploadEdits.crop.height,\n }\n : undefined,\n )\n const [focalPoint, setFocalPoint] = useState({\n x: uploadEdits?.focalPoint?.x || data?.focalX || 50,\n y: uploadEdits?.focalPoint?.y || data?.focalY || 50,\n })\n const containerRef = useRef<HTMLDivElement>(null)\n const [isDragging, setIsDragging] = useState(false)\n // Natural pixel dimensions of the underlying image. Needed because Payload's\n // cropImage requires `widthInPixels` / `heightInPixels` in addition to the\n // percent crop — without them sharp.extract receives NaN and the save fails.\n const [naturalSize, setNaturalSize] = useState<{ width: number; height: number }>(() => ({\n width: typeof data?.width === 'number' ? data.width : 0,\n height: typeof data?.height === 'number' ? data.height : 0,\n }))\n\n // Prefer the Payload document's authoritative width/height — that's what\n // sharp reads on the server. Fall back to the browser-decoded natural size.\n const docWidth = typeof data?.width === 'number' ? data.width : 0\n const docHeight = typeof data?.height === 'number' ? data.height : 0\n\n useEffect(() => {\n if (docWidth > 0 && docHeight > 0) {\n queueMicrotask(() => setNaturalSize({ width: docWidth, height: docHeight }))\n return\n }\n if (!imageUrl) return\n const img = new Image()\n img.onload = () => {\n setNaturalSize({ width: img.naturalWidth, height: img.naturalHeight })\n }\n img.src = imageUrl\n }, [imageUrl, docWidth, docHeight])\n\n const [prevUpdatedAtTag, setPrevUpdatedAtTag] = useState(updatedAtTag)\n\n // Re-sync local state from the saved document each time `updatedAt` changes.\n useEffect(() => {\n if (updatedAtTag && updatedAtTag !== prevUpdatedAtTag) {\n const nextFocal = {\n x: typeof data?.focalX === 'number' ? data.focalX : 50,\n y: typeof data?.focalY === 'number' ? data.focalY : 50,\n }\n queueMicrotask(() => {\n setPrevUpdatedAtTag(updatedAtTag)\n hasUserEditedRef.current = false\n setFocalPoint(nextFocal)\n setCrop(undefined)\n })\n if (uploadEdits?.crop || uploadEdits?.focalPoint) {\n updateUploadEdits({\n crop: undefined,\n focalPoint: nextFocal,\n heightInPixels: undefined,\n widthInPixels: undefined,\n })\n }\n }\n }, [\n updatedAtTag,\n prevUpdatedAtTag,\n data?.focalX,\n data?.focalY,\n uploadEdits?.crop,\n uploadEdits?.focalPoint,\n updateUploadEdits\n ])\n\n const hasRealCrop =\n !!crop &&\n crop.width > 0 &&\n crop.height > 0 &&\n (crop.width !== 100 || crop.height !== 100 || crop.x !== 0 || crop.y !== 0)\n\n // Auto-save to uploadEdits whenever state changes\n useEffect(() => {\n if (!hasUserEditedRef.current) return\n\n const timeout = setTimeout(() => {\n setModified(true)\n\n if (hasRealCrop && crop && naturalSize.width > 0 && naturalSize.height > 0) {\n // Payload's cropImage computes `left = floor(x% * imgW / 100)` and\n // `top = floor(y% * imgH / 100)`, then calls sharp.extract with our\n // widthInPixels / heightInPixels. Sharp rejects the call if\n // left + width > imgW (likewise for height) — even by 1px.\n // We must compute width/height with floor and clamp so they always\n // fit inside the image from that floored offset.\n const leftPx = Math.floor((crop.x / 100) * naturalSize.width)\n const topPx = Math.floor((crop.y / 100) * naturalSize.height)\n const widthInPixels = Math.max(\n 1,\n Math.min(Math.floor((crop.width / 100) * naturalSize.width), naturalSize.width - leftPx),\n )\n const heightInPixels = Math.max(\n 1,\n Math.min(\n Math.floor((crop.height / 100) * naturalSize.height),\n naturalSize.height - topPx,\n ),\n )\n updateUploadEdits({\n crop: {\n unit: '%',\n x: crop.x,\n y: crop.y,\n width: crop.width,\n height: crop.height,\n },\n focalPoint,\n heightInPixels,\n widthInPixels,\n })\n } else {\n updateUploadEdits({\n crop: undefined,\n focalPoint,\n heightInPixels: undefined,\n widthInPixels: undefined,\n })\n }\n }, 100)\n return () => clearTimeout(timeout)\n }, [crop, hasRealCrop, focalPoint, naturalSize, setModified, updateUploadEdits])\n\n const clampFocalToCrop = useCallback(\n (x: number, y: number) => {\n if (crop && crop.unit === '%' && hasRealCrop) {\n x = Math.max(crop.x, Math.min(crop.x + crop.width, x))\n y = Math.max(crop.y, Math.min(crop.y + crop.height, y))\n }\n return { x, y }\n },\n [crop, hasRealCrop],\n )\n\n // Focal point drag handlers\n const updateFocalFromEvent = useCallback(\n (e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) => {\n const container = containerRef.current\n if (!container) return\n const rect = container.getBoundingClientRect()\n const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX\n const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY\n let x = ((clientX - rect.left) / rect.width) * 100\n let y = ((clientY - rect.top) / rect.height) * 100\n\n x = Math.max(0, Math.min(100, x))\n y = Math.max(0, Math.min(100, y))\n\n const clamped = clampFocalToCrop(x, y)\n hasUserEditedRef.current = true\n setFocalPoint(clamped)\n },\n [clampFocalToCrop],\n )\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n e.preventDefault()\n setIsDragging(true)\n updateFocalFromEvent(e)\n },\n [updateFocalFromEvent],\n )\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n setIsDragging(true)\n updateFocalFromEvent(e)\n },\n [updateFocalFromEvent],\n )\n\n useEffect(() => {\n if (!isDragging) return\n const handleMove = (e: MouseEvent | TouchEvent) => {\n e.preventDefault()\n updateFocalFromEvent(e)\n }\n const handleUp = () => setIsDragging(false)\n window.addEventListener('mousemove', handleMove)\n window.addEventListener('mouseup', handleUp)\n window.addEventListener('touchmove', handleMove, { passive: false })\n window.addEventListener('touchend', handleUp)\n return () => {\n window.removeEventListener('mousemove', handleMove)\n window.removeEventListener('mouseup', handleUp)\n window.removeEventListener('touchmove', handleMove)\n window.removeEventListener('touchend', handleUp)\n }\n }, [isDragging, updateFocalFromEvent])\n\n // react-image-crop gives two values: `crop` (in the current unit, often px\n // relative to the *displayed* image) and `percentCrop` (always 0-100%).\n // Store percentCrop so preview math is display-size-independent.\n const handleCropChange = useCallback((_crop: Crop, _percentCrop: Crop) => {\n hasUserEditedRef.current = true\n setCrop(_percentCrop)\n }, [])\n\n const handleReset = useCallback(() => {\n hasUserEditedRef.current = true\n setFocalPoint({ x: 50, y: 50 })\n setCrop(undefined)\n }, [])\n\n const handleCropFieldChange = useCallback(\n (field: 'x' | 'y' | 'width' | 'height', value: number) => {\n hasUserEditedRef.current = true\n setCrop((prev) => {\n // Bootstrap a crop on first edit\n const base: Crop = prev ?? { unit: '%', x: 0, y: 0, width: 100, height: 100 }\n const next: Crop = { ...base, [field]: value }\n // Clamp to image bounds (percent units)\n if (next.unit === '%') {\n next.x = Math.max(0, Math.min(100, next.x))\n next.y = Math.max(0, Math.min(100, next.y))\n next.width = Math.max(0, Math.min(100 - next.x, next.width))\n next.height = Math.max(0, Math.min(100 - next.y, next.height))\n }\n return next\n })\n },\n [],\n )\n\n if (!imageUrl) {\n return (\n <div className=\"focal-editor\">\n <div className=\"focal-editor__empty\">\n Upload an image to edit focal point and preview aspect ratios.\n </div>\n </div>\n )\n }\n\n const cropConfig: CropConfig | undefined =\n hasRealCrop && crop\n ? {\n unit: crop.unit as 'px' | '%',\n x: crop.x,\n y: crop.y,\n width: crop.width,\n height: crop.height,\n }\n : undefined\n\n const cropUnitLabel = crop?.unit === 'px' ? 'px' : '%'\n\n return (\n <div className=\"focal-editor\">\n <div className=\"focal-editor__layout\">\n {/* Left — sticky editor */}\n <div className=\"focal-editor__left\">\n {/* Toolbar: two rows of inputs on the left, button column on the right */}\n <div className=\"focal-editor__toolbar\">\n <div className=\"focal-editor__toolbar-inputs\">\n {/* Row 1 — focal point */}\n <div className=\"focal-editor__toolbar-col\">\n <span className=\"focal-editor__row-label\">Focal</span>\n <div className=\"focal-editor__subgroup\">\n <span className=\"focal-editor__subgroup-label\">Position</span>\n <div className=\"focal-editor__subgroup-inputs\">\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"focal-x\">X</label>\n <input\n id=\"focal-x\"\n type=\"number\"\n min={0}\n max={100}\n value={Math.round(focalPoint.x)}\n onChange={(e) => {\n hasUserEditedRef.current = true\n setFocalPoint((prev) => {\n const x = Math.max(0, Math.min(100, Number(e.target.value)))\n return clampFocalToCrop(x, prev.y)\n })\n }}\n />\n <span>%</span>\n </div>\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"focal-y\">Y</label>\n <input\n id=\"focal-y\"\n type=\"number\"\n min={0}\n max={100}\n value={Math.round(focalPoint.y)}\n onChange={(e) => {\n hasUserEditedRef.current = true\n setFocalPoint((prev) => {\n const y = Math.max(0, Math.min(100, Number(e.target.value)))\n return clampFocalToCrop(prev.x, y)\n })\n }}\n />\n <span>%</span>\n </div>\n </div>\n </div>\n </div>\n\n {/* Row 2 — crop */}\n <div className=\"focal-editor__toolbar-col\">\n <span className=\"focal-editor__row-label\">Crop</span>\n <div className=\"focal-editor__toolbar-row\">\n <div className=\"focal-editor__subgroup\">\n <span className=\"focal-editor__subgroup-label\">Position</span>\n <div className=\"focal-editor__subgroup-inputs\">\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"crop-x\">X</label>\n <input\n id=\"crop-x\"\n type=\"number\"\n min={0}\n max={100}\n value={crop ? Math.round(crop.x) : 0}\n onChange={(e) => handleCropFieldChange('x', Number(e.target.value))}\n disabled={!hasRealCrop}\n />\n <span>{cropUnitLabel}</span>\n </div>\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"crop-y\">Y</label>\n <input\n id=\"crop-y\"\n type=\"number\"\n min={0}\n max={100}\n value={crop ? Math.round(crop.y) : 0}\n onChange={(e) => handleCropFieldChange('y', Number(e.target.value))}\n disabled={!hasRealCrop}\n />\n <span>{cropUnitLabel}</span>\n </div>\n </div>\n </div>\n <div className=\"focal-editor__subgroup\">\n <span className=\"focal-editor__subgroup-label\">Size</span>\n <div className=\"focal-editor__subgroup-inputs\">\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"crop-w\">W</label>\n <input\n id=\"crop-w\"\n type=\"number\"\n min={0}\n max={100}\n value={crop ? Math.round(crop.width) : 0}\n onChange={(e) => handleCropFieldChange('width', Number(e.target.value))}\n disabled={!hasRealCrop}\n />\n <span>{cropUnitLabel}</span>\n </div>\n <div className=\"focal-editor__input-group\">\n <label htmlFor=\"crop-h\">H</label>\n <input\n id=\"crop-h\"\n type=\"number\"\n min={0}\n max={100}\n value={crop ? Math.round(crop.height) : 0}\n onChange={(e) => handleCropFieldChange('height', Number(e.target.value))}\n disabled={!hasRealCrop}\n />\n <span>{cropUnitLabel}</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div className=\"focal-editor__toolbar-actions\">\n <button className=\"focal-editor__btn\" onClick={handleReset} type=\"button\">\n Reset\n </button>\n <div className=\"focal-editor__mode-toggle\">\n <button\n className={`focal-editor__mode-btn ${mode === 'focal' ? 'focal-editor__mode-btn--active' : ''}`}\n onClick={() => setMode('focal')}\n type=\"button\"\n >\n Focal Point\n </button>\n <button\n className={`focal-editor__mode-btn ${mode === 'crop' ? 'focal-editor__mode-btn--active' : ''}`}\n onClick={() => setMode('crop')}\n type=\"button\"\n >\n Crop\n </button>\n </div>\n </div>\n </div>\n\n {/* Full-width image within left column */}\n <div className=\"focal-editor__image-wrapper\">\n <ReactCrop\n crop={mode === 'crop' ? crop : undefined}\n onChange={handleCropChange}\n disabled={mode === 'focal'}\n className={`focal-editor__react-crop ${mode === 'focal' ? 'is-focal-mode' : 'is-crop-mode'}`}\n >\n <div\n style={{\n position: 'relative',\n display: 'inline-block',\n lineHeight: 0,\n width: '100%',\n }}\n >\n {\n /**\n *\n * As it's a CMS, we don't want to use next/image here,\n * it also wouldn't bring any value as it's not a production\n * website, but an admin panel. So we can use a simple\n * img tag here.\n *\n **/\n }\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={imageUrl}\n alt=\"Edit\"\n draggable={false}\n style={{ display: 'block', width: '100%', height: 'auto', objectFit: 'contain' }}\n onLoad={(e) => {\n const img = e.currentTarget\n const displayedW = img.clientWidth\n const displayedH = img.clientHeight\n // If a legacy px crop was loaded from uploadEdits, convert\n // it to percentages now that we know the displayed size.\n if (crop?.unit === 'px' && displayedW > 0 && displayedH > 0) {\n setCrop({\n unit: '%',\n x: (crop.x / displayedW) * 100,\n y: (crop.y / displayedH) * 100,\n width: (crop.width / displayedW) * 100,\n height: (crop.height / displayedH) * 100,\n })\n }\n }}\n />\n {/* Crop reference overlay — visible in focal mode so the user\n knows which region the focal point is relative to. */}\n {mode === 'focal' && crop && crop.unit === '%' && hasRealCrop && (\n <div\n className=\"focal-editor__crop-overlay\"\n style={{\n left: `${crop.x}%`,\n top: `${crop.y}%`,\n width: `${crop.width}%`,\n height: `${crop.height}%`,\n }}\n />\n )}\n <div\n ref={containerRef}\n className={`focal-editor__focal-overlay ${mode === 'focal' ? 'is-active' : ''}`}\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n >\n {mode === 'focal' && (\n <div\n className=\"focal-editor__crosshair\"\n style={{\n left: `${focalPoint.x}%`,\n top: `${focalPoint.y}%`,\n }}\n >\n <div className=\"focal-editor__crosshair-h\" />\n <div className=\"focal-editor__crosshair-v\" />\n </div>\n )}\n </div>\n </div>\n </ReactCrop>\n </div>\n </div>\n\n {/* Right — CSS grid of aspect ratio previews */}\n <div className=\"focal-editor__right\">\n <AspectRatioPreviewGrid\n url={imageUrl}\n focalX={focalPoint.x}\n focalY={focalPoint.y}\n aspectRatios={aspectRatios}\n crop={cropConfig}\n />\n </div>\n </div>\n </div>\n )\n}\n\nexport default FocalPointEditor\n"],"names":["useConfig","useDocumentInfo","useForm","useUploadEdits","React","useCallback","useEffect","useRef","useState","ReactCrop","DEFAULT_ASPECT_RATIOS","AspectRatioPreviewGrid","FocalPointEditor","data","setModified","uploadEdits","updateUploadEdits","getEntityConfig","collectionSlug","collection","undefined","entityConfig","aspectRatios","custom","aspectPreview","hasUserEditedRef","rawUrl","url","updatedAtTag","updatedAt","String","imageUrl","useMemo","sep","includes","encodeURIComponent","mode","setMode","crop","setCrop","unit","x","y","width","height","focalPoint","setFocalPoint","focalX","focalY","containerRef","isDragging","setIsDragging","naturalSize","setNaturalSize","docWidth","docHeight","queueMicrotask","img","Image","onload","naturalWidth","naturalHeight","src","prevUpdatedAtTag","setPrevUpdatedAtTag","nextFocal","current","heightInPixels","widthInPixels","hasRealCrop","timeout","setTimeout","leftPx","Math","floor","topPx","max","min","clearTimeout","clampFocalToCrop","updateFocalFromEvent","e","container","rect","getBoundingClientRect","clientX","touches","clientY","left","top","clamped","handleMouseDown","preventDefault","handleTouchStart","handleMove","handleUp","window","addEventListener","passive","removeEventListener","handleCropChange","_crop","_percentCrop","handleReset","handleCropFieldChange","field","value","prev","base","next","div","className","cropConfig","cropUnitLabel","span","label","htmlFor","input","id","type","round","onChange","Number","target","disabled","button","onClick","style","position","display","lineHeight","alt","draggable","objectFit","onLoad","currentTarget","displayedW","clientWidth","displayedH","clientHeight","ref","onMouseDown","onTouchStart"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { AspectRatioConfig } from './types.js'\n\nexport const DEFAULT_ASPECT_RATIOS: AspectRatioConfig[] = [\n { name: 'Square', ratio: '1:1', width: 1, height: 1, source: 'crop', usage: 'Avatars, thumbnails', category: 'general' },\n { name: 'Standard', ratio: '4:3', width: 4, height: 3, source: 'css', usage: 'Standard cards', category: 'general' },\n { name: 'Classic', ratio: '3:2', width: 3, height: 2, source: 'css', usage: 'Photography', category: 'general' },\n { name: 'Widescreen', ratio: '16:9', width: 16, height: 9, source: 'css', usage: 'Hero / media blocks', category: 'general' },\n { name: 'Portrait', ratio: '9:16', width: 9, height: 16, source: 'css', usage: 'Mobile / stories', category: 'general' },\n { name: 'Social Share', ratio: '1.91:1', width: 1200, height: 630, source: 'crop', usage: 'Open Graph / social', category: 'social' },\n]\n"],"names":["DEFAULT_ASPECT_RATIOS","name","ratio","width","height","source","usage","category"],"mappings":"AAEA,OAAO,MAAMA,wBAA6C;IACxD;QAAEC,MAAM;QAAUC,OAAO;QAAOC,OAAO;QAAGC,QAAQ;QAAGC,QAAQ;QAAQC,OAAO;QAAuBC,UAAU;IAAU;IACvH;QAAEN,MAAM;QAAYC,OAAO;QAAOC,OAAO;QAAGC,QAAQ;QAAGC,QAAQ;QAAOC,OAAO;QAAkBC,UAAU;IAAU;IACnH;QAAEN,MAAM;QAAWC,OAAO;QAAOC,OAAO;QAAGC,QAAQ;QAAGC,QAAQ;QAAOC,OAAO;QAAeC,UAAU;IAAU;IAC/G;QAAEN,MAAM;QAAcC,OAAO;QAAQC,OAAO;QAAIC,QAAQ;QAAGC,QAAQ;QAAOC,OAAO;QAAuBC,UAAU;IAAU;IAC5H;QAAEN,MAAM;QAAYC,OAAO;QAAQC,OAAO;QAAGC,QAAQ;QAAIC,QAAQ;QAAOC,OAAO;QAAoBC,UAAU;IAAU;IACvH;QAAEN,MAAM;QAAgBC,OAAO;QAAUC,OAAO;QAAMC,QAAQ;QAAKC,QAAQ;QAAQC,OAAO;QAAuBC,UAAU;IAAS;CACrI,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { FocalPointEditor } from '../components/FocalPointEditor.js'\nexport { AspectRatioPreviewGrid } from '../components/AspectRatioPreviewGrid.js'\nexport { CustomUpload } from '../components/CustomUpload.js'\n"],"names":["FocalPointEditor","AspectRatioPreviewGrid","CustomUpload"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,oCAAmC;AACpE,SAASC,sBAAsB,QAAQ,0CAAyC;AAChF,SAASC,YAAY,QAAQ,gCAA+B"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/exports/types.ts"],"sourcesContent":["export type {\n AspectPreviewPluginOptions,\n AspectRatioConfig,\n CropConfig,\n FocalPointState,\n UploadEditsState,\n} from '../types.js'\n"],"names":[],"mappings":"AAAA,WAMoB"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { aspectPreviewPlugin } from './plugin.js'\nexport { DEFAULT_ASPECT_RATIOS } from './defaults.js'\nexport type {\n AspectPreviewPluginOptions,\n AspectRatioConfig,\n CropConfig,\n FocalPointState,\n UploadEditsState,\n} from './types.js'\n"],"names":["aspectPreviewPlugin","DEFAULT_ASPECT_RATIOS"],"mappings":"AAAA,SAASA,mBAAmB,QAAQ,cAAa;AACjD,SAASC,qBAAqB,QAAQ,gBAAe"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionConfig, Config, Plugin } from 'payload'\n\nimport { DEFAULT_ASPECT_RATIOS } from './defaults.js'\nimport type { AspectPreviewPluginOptions } from './types.js'\n\nconst FIELD_COMPONENT = 'payload-plugin-aspect-preview/client#FocalPointEditor'\nconst UPLOAD_COMPONENT = 'payload-plugin-aspect-preview/client#CustomUpload'\n\nexport const aspectPreviewPlugin =\n (options: AspectPreviewPluginOptions): Plugin =>\n (config: Config): Config => {\n const aspectRatios = options.aspectRatios ?? DEFAULT_ASPECT_RATIOS\n const enabled = new Set(options.collections)\n\n return {\n ...config,\n collections: config.collections?.map((collection) => {\n if (!enabled.has(collection.slug)) return collection\n\n const next: CollectionConfig = {\n ...collection,\n fields: [\n ...(collection.fields || []),\n {\n name: 'aspectPreview',\n type: 'ui',\n admin: {\n components: {\n Field: FIELD_COMPONENT,\n },\n },\n },\n ],\n custom: {\n ...collection.custom,\n aspectPreview: { aspectRatios },\n },\n }\n\n // The Upload override is what removes Payload's default crop/adjust\n // drawer so the inline editor is the only surface. Skip it when\n // disabled, but keep the field + custom above for schema consistency.\n if (!options.disabled) {\n next.admin = {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n edit: {\n ...collection.admin?.components?.edit,\n Upload: UPLOAD_COMPONENT,\n },\n },\n }\n }\n\n return next\n }),\n }\n }\n"],"names":["DEFAULT_ASPECT_RATIOS","FIELD_COMPONENT","UPLOAD_COMPONENT","aspectPreviewPlugin","options","config","aspectRatios","enabled","Set","collections","map","collection","has","slug","next","fields","name","type","admin","components","Field","custom","aspectPreview","disabled","edit","Upload"],"mappings":"AAEA,SAASA,qBAAqB,QAAQ,gBAAe;AAGrD,MAAMC,kBAAkB;AACxB,MAAMC,mBAAmB;AAEzB,OAAO,MAAMC,sBACX,CAACC,UACD,CAACC;QACC,MAAMC,eAAeF,QAAQE,YAAY,IAAIN;QAC7C,MAAMO,UAAU,IAAIC,IAAIJ,QAAQK,WAAW;QAE3C,OAAO;YACL,GAAGJ,MAAM;YACTI,aAAaJ,OAAOI,WAAW,EAAEC,IAAI,CAACC;gBACpC,IAAI,CAACJ,QAAQK,GAAG,CAACD,WAAWE,IAAI,GAAG,OAAOF;gBAE1C,MAAMG,OAAyB;oBAC7B,GAAGH,UAAU;oBACbI,QAAQ;2BACFJ,WAAWI,MAAM,IAAI,EAAE;wBAC3B;4BACEC,MAAM;4BACNC,MAAM;4BACNC,OAAO;gCACLC,YAAY;oCACVC,OAAOnB;gCACT;4BACF;wBACF;qBACD;oBACDoB,QAAQ;wBACN,GAAGV,WAAWU,MAAM;wBACpBC,eAAe;4BAAEhB;wBAAa;oBAChC;gBACF;gBAEA,oEAAoE;gBACpE,gEAAgE;gBAChE,sEAAsE;gBACtE,IAAI,CAACF,QAAQmB,QAAQ,EAAE;oBACrBT,KAAKI,KAAK,GAAG;wBACX,GAAGP,WAAWO,KAAK;wBACnBC,YAAY;4BACV,GAAGR,WAAWO,KAAK,EAAEC,UAAU;4BAC/BK,MAAM;gCACJ,GAAGb,WAAWO,KAAK,EAAEC,YAAYK,IAAI;gCACrCC,QAAQvB;4BACV;wBACF;oBACF;gBACF;gBAEA,OAAOY;YACT;QACF;IACF,EAAC"}
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface AspectRatioConfig {\n /** Display label, e.g. \"Social Share\". */\n name: string\n /** Display token, e.g. \"16:9\". */\n ratio: string\n /** Ratio numerator (or absolute px width for exact targets). */\n width: number\n /** Ratio denominator (or absolute px height for exact targets). */\n height: number\n /** How the frontend renders this ratio; drives a display badge only. Defaults to 'css'. */\n source?: 'css' | 'crop'\n /** Optional description shown in the preview card footer. */\n usage?: string\n /** Optional free-form grouping label. */\n category?: string\n}\n\nexport interface AspectPreviewPluginOptions {\n /** Upload collections (by slug) to enhance with the inline editor. */\n collections: string[]\n /** Aspect ratios shown in the live preview grid. Defaults to DEFAULT_ASPECT_RATIOS. */\n aspectRatios?: AspectRatioConfig[]\n /** Disable the plugin while keeping the schema field for DB consistency. */\n disabled?: boolean\n}\n\nexport interface CropConfig {\n unit: 'px' | '%'\n x: number\n y: number\n width: number\n height: number\n}\n\nexport interface FocalPointState {\n x: number\n y: number\n}\n\nexport interface UploadEditsState {\n crop?: CropConfig\n focalPoint?: FocalPointState\n}\n"],"names":[],"mappings":"AAuCA,WAGC"}