fix(ecommerce): panel de zoom dinámico y CTA móvil con revelado progresivo
- El panel de zoom lateral ahora calcula tamaño/posición contra el viewport real (window.innerWidth/innerHeight) en cada mousemove, en vez de un breakpoint fijo (lg:block) con ancho fijo -- a resoluciones altas con poco espacio a la derecha de la imagen terminaba saliéndose de la pantalla. Si no entra un panel de al menos PANEL_MIN_SIZE, se cae automáticamente al overlay in-place (antes atado a "por debajo de lg", ahora a "no hay espacio real"). - La barra fija de "Añadir al carrito" en mobile quedaba fija desde el primer render, tapando permanentemente parte de la imagen del producto. Ahora el botón real (ProductPurchase) también se muestra en mobile, y la barra sticky solo aparece cuando ese botón sale del viewport (IntersectionObserver) -- mismo patrón que el "Buy Now" sticky de Amazon. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
This commit is contained in:
1 parent
d605417452
commit
9d4553f3a4
3 files changed
+84
-15
No files matched your search
@@ -7,7 +7,17 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
|
||||
interface Offset { x: number; y: number }
|
||||
interface DragState { pointerId: number; startX: number; startY: number; originX: number; originY: number }
|
||||
interface HoverZoomState { lensLeft: number; lensTop: number; lensSize: number; bgX: number; bgY: number }
|
||||
interface HoverZoomState {
|
||||
lensLeft: number;
|
||||
lensTop: number;
|
||||
lensSize: number;
|
||||
bgX: number;
|
||||
bgY: number;
|
||||
showPanel: boolean;
|
||||
panelLeft: number;
|
||||
panelTop: number;
|
||||
panelSize: number;
|
||||
}
|
||||
|
||||
// Zoom estilo "lupa" de Amazon: un recuadro semitransparente sobre la
|
||||
// imagen (la lupa) marca la region que se esta mirando, y un panel
|
||||
@@ -26,8 +36,21 @@ interface HoverZoomState { lensLeft: number; lensTop: number; lensSize: number;
|
||||
// de accion en una foto vertical). background-size en un solo valor
|
||||
// (ancho%, alto en "auto") preserva el aspect ratio real de la foto
|
||||
// completa.
|
||||
//
|
||||
// El tamaño/posicion del panel se calculan en cada mousemove contra el
|
||||
// viewport real (window.innerWidth/innerHeight), no con un breakpoint
|
||||
// fijo tipo "lg:block" -- a resoluciones altas con la ventana angosta
|
||||
// (o el layout de la pagina dejando poco espacio a la derecha de la
|
||||
// imagen) un tamaño fijo terminaba saliéndose de la pantalla. Si no
|
||||
// entra un panel de al menos PANEL_MIN_SIZE, se cae automáticamente al
|
||||
// overlay in-place (misma imagen completa como fondo, dentro de la
|
||||
// propia caja) en vez de mostrar un panel cortado.
|
||||
const HOVER_ZOOM_SCALE = 2.2;
|
||||
const LENS_RATIO = 1 / HOVER_ZOOM_SCALE;
|
||||
const PANEL_MARGIN = 16;
|
||||
const PANEL_MAX_SIZE = 480;
|
||||
const PANEL_MIN_SIZE = 220;
|
||||
const VIEWPORT_EDGE_PADDING = 16;
|
||||
|
||||
export function ProductGallery({ images, name }: { images: string[]; name: string }) {
|
||||
const [selected, setSelected] = useState(images[0] ?? "");
|
||||
@@ -44,12 +67,22 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
|
||||
const cursorY = event.clientY - rect.top;
|
||||
const lensLeft = Math.min(Math.max(cursorX - lensSize / 2, 0), rect.width - lensSize);
|
||||
const lensTop = Math.min(Math.max(cursorY - lensSize / 2, 0), rect.height - lensSize);
|
||||
|
||||
const availableWidth = window.innerWidth - rect.right - PANEL_MARGIN - VIEWPORT_EDGE_PADDING;
|
||||
const availableHeight = window.innerHeight - VIEWPORT_EDGE_PADDING * 2;
|
||||
const panelSize = Math.min(PANEL_MAX_SIZE, availableWidth, availableHeight);
|
||||
const panelTop = Math.min(Math.max(rect.top, VIEWPORT_EDGE_PADDING), window.innerHeight - panelSize - VIEWPORT_EDGE_PADDING);
|
||||
|
||||
setHoverZoom({
|
||||
lensLeft,
|
||||
lensTop,
|
||||
lensSize,
|
||||
bgX: (lensLeft / (rect.width - lensSize)) * 100,
|
||||
bgY: (lensTop / (rect.height - lensSize)) * 100,
|
||||
showPanel: panelSize >= PANEL_MIN_SIZE,
|
||||
panelLeft: rect.right + PANEL_MARGIN,
|
||||
panelTop,
|
||||
panelSize,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,25 +130,25 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
{/* Lupa: recuadro que marca la region ampliada, visible desde
|
||||
lg (donde también hay lugar para el panel de al lado). Por
|
||||
debajo de lg se usa el overlay in-place (misma imagen
|
||||
completa como fondo, sin lupa ni panel aparte) porque no
|
||||
hay espacio horizontal para un panel flotante. */}
|
||||
{hoverZoom && (
|
||||
{/* Lupa: recuadro que marca la region ampliada -- solo cuando
|
||||
hay lugar real para el panel de al lado (showPanel,
|
||||
calculado contra el viewport real, no un breakpoint
|
||||
fijo). Sin espacio para el panel se usa el overlay
|
||||
in-place como fallback, para cualquier resolucion. */}
|
||||
{hoverZoom?.showPanel && (
|
||||
<div
|
||||
aria-hidden
|
||||
data-testid="gallery-lens"
|
||||
className="pointer-events-none absolute hidden border-2 border-white bg-white/25 shadow-lg lg:block"
|
||||
className="pointer-events-none absolute border-2 border-white bg-white/25 shadow-lg"
|
||||
style={{ left: hoverZoom.lensLeft, top: hoverZoom.lensTop, width: hoverZoom.lensSize, height: hoverZoom.lensSize }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
aria-hidden
|
||||
data-testid="gallery-inplace-overlay"
|
||||
className="pointer-events-none absolute inset-0 bg-slate-50 transition-opacity duration-150 ease-out lg:hidden"
|
||||
className="pointer-events-none absolute inset-0 bg-slate-50 transition-opacity duration-150 ease-out"
|
||||
style={{
|
||||
opacity: hoverZoom ? 1 : 0,
|
||||
opacity: hoverZoom && !hoverZoom.showPanel ? 1 : 0,
|
||||
backgroundImage: `url(${selected})`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
|
||||
@@ -144,12 +177,16 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{hoverZoom && (
|
||||
{hoverZoom?.showPanel && (
|
||||
<div
|
||||
aria-hidden
|
||||
data-testid="gallery-zoom-panel"
|
||||
className="pointer-events-none absolute left-full top-0 z-30 ml-4 hidden aspect-square w-[26rem] max-w-[40vw] overflow-hidden rounded-[2rem] border border-slate-200 bg-white shadow-2xl lg:block"
|
||||
className="pointer-events-none fixed z-30 overflow-hidden rounded-[2rem] border border-slate-200 bg-white shadow-2xl"
|
||||
style={{
|
||||
left: hoverZoom.panelLeft,
|
||||
top: hoverZoom.panelTop,
|
||||
width: hoverZoom.panelSize,
|
||||
height: hoverZoom.panelSize,
|
||||
backgroundImage: `url(${selected})`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: `${HOVER_ZOOM_SCALE * 100}%`,
|
||||
|
||||
Reference in new issue
Block a user