From d4bff33e47d37e6d8934ff836e8d78400b74ae6a Mon Sep 17 00:00:00 2001 From: erikgh Date: Mon, 3 Aug 2026 16:07:00 -0600 Subject: [PATCH] feat(dolores): cada frase se resuelve al llegar, palabra a palabra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometí en la vista previa que entrarían «de una en una al bajar» y lo que monté las traía a las cuatro a la vez: un stagger de contenedor se dispara con la sección al 30% en pantalla, y la sección mide 1.200px, así que el lector veía aparecer todas las frases mucho antes de llegar a ellas. Ahora cada cita usa TextoDesenfoque, que lleva su propio observador: cada una espera su turno y entonces se resuelve desde un desenfoque, palabra a palabra. Es el dolor entrando en foco, y es el mismo recurso que ya usan el hero y la frase ancla — vocabulario del proyecto, no un invento nuevo. Verificado midiendo la opacidad de la palabra animada de cada cita: al asomar la sección, las dos primeras están resueltas y las dos últimas a cero; a media sección, las cuatro. Antes de corregir el selector medía el span envoltorio y daba unos en todas, que no significaba nada. Co-Authored-By: Claude Opus 5 --- components/sections/Dolores.tsx | 21 ++++++++++++------- .../sections/__tests__/Dolores.test.tsx | 11 ++++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/components/sections/Dolores.tsx b/components/sections/Dolores.tsx index 2b001ca..517805a 100644 --- a/components/sections/Dolores.tsx +++ b/components/sections/Dolores.tsx @@ -3,8 +3,9 @@ import { motion } from 'framer-motion'; import { Container } from '@/components/primitives/Container'; import { IlustracionNudo } from '@/components/primitives/IlustracionNudo'; +import { TextoDesenfoque } from '@/components/primitives/TextoDesenfoque'; import { dolores } from '@/lib/content'; -import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion'; +import { fadeInUp, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion'; // The four quotes are the client's own words, from slide 3 of the dueña's deck. // @@ -39,18 +40,24 @@ export function Dolores() { {dolores.title} - + {/* No stagger on the list. A container stagger fires all four the moment + the section is 30% on screen, and the section is 1.200px tall — the + reader would watch every sentence appear long before scrolling to + it, which is not what "se apilan al bajar" means. TextoDesenfoque + carries its own observer, so each quote waits for its own arrival + and then resolves out of blur, word by word: the pain coming into + focus. Same device as the hero and the anchor phrase. */} +
    {dolores.quotes.map((quote, i) => ( - +
  • “ - {quote} + ”
    - +
  • ))} - +
{/* Flush left and still, against four sentences that are not. */} ', () => { it('renders the four pains verbatim, in the client\'s first person', () => { - render(); + const { container } = render(); + // The quotes resolve word by word, so each word is its own span and no + // single element holds the whole sentence as its own text — getByText + // would never find it. The blockquote's textContent is what the reader + // ends up with, and that is what has to be verbatim. + const citas = Array.from(container.querySelectorAll('blockquote')).map((b) => + (b.textContent ?? '').replace(/\s+/g, ' '), + ); for (const dolor of dolores.quotes) { - expect(screen.getAllByText(new RegExp(dolor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))).length).toBeGreaterThan(0); + expect(citas.some((c) => c.includes(dolor))).toBe(true); } });