Files
Creeer-Web/components/primitives/EscenaDialogo.tsx
T
erikghandClaude Opus 5 253ff26fa2 fix(recorrido): un fotograma, una cosa
La pregunta y la respuesta compartían fotograma: la tarjeta de creeer
entraba debajo de la del lector y se veían las dos a la vez. El flujo que
pidió la dueña es pregunta → scroll → respuesta → scroll → pregunta, con
una sola cosa en pantalla cada vez.

Las dos tarjetas pasan a ocupar la misma celda de la rejilla, así que la
segunda toma el escenario que deja la primera en vez de apilarse bajo
ella, y entran desde márgenes opuestos para que el cambio se lea como un
relevo. Verificado tiempo a tiempo: los doce muestran exactamente una.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 12:18:23 -06:00

112 lines
4.4 KiB
TypeScript

'use client';
import { motion, useTransform, type MotionValue } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
// One beat of the pinned conversation, staged as two calling cards laid on a
// table: the reader's, in white, and creeer's, in navy, handed back.
//
// The cards are driven by STATE, not by scroll position. Mapping opacity
// straight onto scroll made every intermediate position a valid frame, so
// stopping mid-swipe left creeer's card as a half-transparent grey rectangle —
// it read as a rendering fault. Scroll now only decides which beat we are on;
// the cards animate to that beat over time and always land somewhere finished,
// wherever the reader lets go.
//
// Every scene stays mounted and stacked in the same place. Mounting only the
// active one would animate more cheaply and would strip five sixths of the
// section out of the static export.
const EASE = [0.22, 1, 0.36, 1] as const;
export function EscenaDialogo({
i,
total,
progreso,
activa,
respondida,
n,
name,
situacion,
respuesta,
}: {
i: number;
total: number;
progreso: MotionValue<number>;
activa: boolean;
respondida: boolean;
n: string;
name: string;
situacion: string;
respuesta: string;
}) {
const paso = 1 / total;
// The one thing still tied to scroll. A drifting numeral never looks broken
// at any position it can be caught in, so it costs nothing and keeps the
// stage from feeling frozen between beats.
const cifraY = useTransform(progreso, [i * paso - paso, (i + 1) * paso + paso], [60, -60]);
return (
<motion.li
data-activa={activa ? 'true' : 'false'}
animate={{ opacity: activa ? 1 : 0, y: activa ? 0 : 26 }}
transition={{ duration: 0.45, ease: EASE }}
className="absolute inset-x-0 top-1/2 -translate-y-1/2"
style={{ pointerEvents: activa ? 'auto' : 'none' }}
>
<motion.span
aria-hidden="true"
style={{ y: cifraY }}
className="pointer-events-none absolute -top-32 right-0 select-none font-sans font-black leading-none text-azul-profundo/[0.045] text-[9rem] md:text-[17rem]"
>
{n}
</motion.span>
{/* One card per frame — never both. Both live in the same grid cell so
the second can take the stage the first leaves, instead of stacking
under it: the question is asked, it goes, and then the answer is the
only thing on screen. They come in from opposite sides so the swap
reads as one card replacing another, not as one fading into the same
spot. */}
<div className="grid">
<motion.div
animate={{
opacity: respondida ? 0 : 1,
y: respondida ? -52 : 0,
rotate: respondida ? -3.4 : -0.8,
scale: respondida ? 0.96 : 1,
}}
transition={{ duration: 0.5, ease: EASE }}
style={{ pointerEvents: respondida ? 'none' : 'auto' }}
className="col-start-1 row-start-1 self-center justify-self-start max-w-xl rounded-xl border border-azul-profundo/10 bg-white p-7 shadow-[0_28px_70px_-32px_rgba(10,29,61,0.5)] md:p-9"
>
<p className="text-[0.65rem] font-black uppercase tracking-[0.34em] text-dorado">
{n} · {name}
</p>
<p className="mt-4 font-sans font-black leading-[1.12] text-azul-profundo text-2xl md:text-4xl">
«{situacion}»
</p>
</motion.div>
{/* creeer's card, handed back: navy against the reader's white, tilted
the other way, and coming in from the opposite margin. */}
<motion.div
animate={{
opacity: respondida ? 1 : 0,
y: respondida ? 0 : 64,
rotate: respondida ? 1.6 : 5,
scale: respondida ? 1 : 0.95,
}}
transition={{ duration: 0.55, ease: EASE }}
style={{ pointerEvents: respondida ? 'auto' : 'none' }}
className="col-start-1 row-start-1 self-center justify-self-end max-w-xl rounded-xl bg-azul-profundo p-7 shadow-[0_34px_80px_-30px_rgba(10,29,61,0.75)] md:mr-6 md:p-9"
>
<span aria-hidden="true" className="absolute left-0 top-7 h-10 w-[3px] rounded-r bg-dorado md:top-9" />
<Wordmark size="sm" onDark />
<p className="mt-4 text-base leading-relaxed text-white/75 md:text-xl">{respuesta}</p>
</motion.div>
</div>
</motion.li>
);
}