fix(ecommerce): zoom-hover del producto revela la imagen completa
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 24s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 10m35s

El hover-zoom escalaba (transform:scale) la misma <Image> ya recortada
por object-cover -- para fotos verticales (ej. figura de acción de pie)
eso dejaba partes del producto (la cabeza) fuera del recorte desde el
propio render, así que ningún nivel de zoom las mostraba. Se reemplaza
por un overlay con background-image usando la foto completa
(background-size en un solo valor preserva el aspect ratio real) y
background-position siguiendo el cursor, que sí permite recorrer toda
la imagen.

De paso: las miniaturas ahora cambian la imagen principal también con
hover (onMouseEnter), no solo con click.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-13 19:21:39 +00:00
1 parent 2fa404fe7b
commit 9ab779e781
1 file changed
+21 -4
@@ -14,6 +14,16 @@ interface HoverZoomOrigin { x: number; y: number }
// continua en touch, asi que en mobile simplemente no aparece el efecto,
// sin romper nada). Independiente del zoom interactivo del modal
// (click -> Dialog con scale/drag), que sigue igual.
//
// El overlay de hover usa background-image (no transform:scale sobre el
// <Image object-cover>) a proposito: escalar la imagen ya recortada por
// object-cover nunca iba a revelar las partes recortadas (ej. la cabeza
// de una figura de accion en una foto vertical) -- el recorte pasa antes
// del zoom, en el propio render. Con background-size en un solo valor
// (ancho%, alto en "auto") se preserva el aspect ratio real de la foto
// completa, y background-position 0%-100% permite recorrer TODA la
// imagen con el cursor, no solo la porcion que object-cover dejaba
// visible en reposo.
const HOVER_ZOOM_SCALE = 2.2;
export function ProductGallery({ images, name }: { images: string[]; name: string }) {
@@ -57,7 +67,7 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
return (
<div className="grid gap-4 lg:grid-cols-[86px_1fr]">
<div className="order-2 flex gap-3 overflow-x-auto lg:order-1 lg:flex-col">
{images.map((image) => <button type="button" key={image} onClick={() => { setSelected(image); reset(); }} className={`relative aspect-square w-20 shrink-0 overflow-hidden rounded-2xl border-2 ${selected === image ? "border-fuchsia-500" : "border-transparent"}`}><Image src={image} alt="" fill sizes="80px" className="object-cover" /></button>)}
{images.map((image) => <button type="button" key={image} onClick={() => { setSelected(image); reset(); }} onMouseEnter={() => { setSelected(image); reset(); }} className={`relative aspect-square w-20 shrink-0 overflow-hidden rounded-2xl border-2 ${selected === image ? "border-fuchsia-500" : "border-transparent"}`}><Image src={image} alt="" fill sizes="80px" className="object-cover" /></button>)}
</div>
<Dialog open={open} onOpenChange={changeOpen}>
<DialogTrigger asChild>
@@ -73,10 +83,17 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
fill
priority
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover transition-transform duration-150 ease-out"
className="object-cover"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-slate-50 transition-opacity duration-150 ease-out"
style={{
transform: `scale(${hoverZoom ? HOVER_ZOOM_SCALE : 1})`,
transformOrigin: hoverZoom ? `${hoverZoom.x}% ${hoverZoom.y}%` : "center",
opacity: hoverZoom ? 1 : 0,
backgroundImage: `url(${selected})`,
backgroundRepeat: "no-repeat",
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
backgroundPosition: hoverZoom ? `${hoverZoom.x}% ${hoverZoom.y}%` : "center",
}}
/>
<span className="absolute bottom-5 right-5 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-xs font-black shadow-xl"><ZoomIn className="size-4" />Zoom interactivo</span>