Flatten transparent product images to white and allow cropper unzoom.

PNG/WebP uploads no longer bake to black on JPEG; product croppers can zoom out with white padding, and SEO tools move into the product overflow menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-19 08:39:32 +03:30
co-authored by Cursor
parent b743c09e4d
commit 06e2442aaf
17 changed files with 470 additions and 102 deletions
+28 -4
View File
@@ -11,6 +11,11 @@ interface ImageCropperProps {
/** Crop aspect ratio. Defaults to square (1). */
aspect?: number
outputFormat?: 'jpeg' | 'png'
/** Fill color for JPEG and for crop areas outside the image. */
fillColor?: string
/** Minimum zoom. Values below 1 shrink the image inside the crop frame. */
minZoom?: number
maxZoom?: number
accept?: string
uploadLabel?: string
hint?: string
@@ -24,6 +29,9 @@ export function ImageCropper({
onChange,
aspect = 1,
outputFormat = 'jpeg',
fillColor,
minZoom = 1,
maxZoom = 3,
accept = 'image/*',
uploadLabel,
hint,
@@ -34,6 +42,8 @@ export function ImageCropper({
const resolvedUploadLabel = uploadLabel ?? t('products.form.thumbnailUpload')
const resolvedHint = hint ?? t('products.form.thumbnailHint')
const resolvedChangeLabel = changeLabel ?? t('products.form.thumbnailChange')
const resolvedFillColor = fillColor ?? (outputFormat === 'jpeg' ? '#ffffff' : undefined)
const cropSurfaceColor = resolvedFillColor ?? '#1e293b'
const [imageSrc, setImageSrc] = useState<string | null>(null)
const [crop, setCrop] = useState({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1)
@@ -65,7 +75,12 @@ export function ImageCropper({
async function applyCrop() {
if (!imageSrc || !croppedArea) return
const cropped = await getCroppedImage(imageSrc, croppedArea, outputFormat)
const cropped = await getCroppedImage(
imageSrc,
croppedArea,
outputFormat,
resolvedFillColor,
)
onChange(cropped)
setImageSrc(null)
setZoom(1)
@@ -128,16 +143,25 @@ export function ImageCropper({
{imageSrc && (
<div className={styles.cropPanel}>
<div className={styles.cropArea} style={frameStyle}>
<div
className={styles.cropArea}
style={{ ...frameStyle, background: cropSurfaceColor }}
>
<Cropper
key={String(aspect)}
image={imageSrc}
crop={crop}
zoom={zoom}
minZoom={minZoom}
maxZoom={maxZoom}
aspect={aspect}
onCropChange={setCrop}
onZoomChange={setZoom}
onCropComplete={onCropComplete}
restrictPosition={minZoom >= 1}
style={{
containerStyle: { background: cropSurfaceColor },
}}
/>
</div>
<div className={styles.cropControls}>
@@ -145,8 +169,8 @@ export function ImageCropper({
{t('products.form.zoom')}
<input
type="range"
min={1}
max={3}
min={minZoom}
max={maxZoom}
step={0.05}
value={zoom}
onChange={(e) => setZoom(Number(e.target.value))}