From 7af36e2a936ae07afac6bfa58b3d51c3a46ca3c9 Mon Sep 17 00:00:00 2001 From: erikgh Date: Mon, 3 Aug 2026 15:01:37 -0600 Subject: [PATCH] feat(dolores): las cuatro frases dejan de estar ordenaditas y pasan a moverse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eran cuatro tarjetas alineadas describiendo desorden — lo contrario de lo que dicen. Ahora cruzan la sección en dos filas lentas y en direcciones opuestas, a sangre, mientras el remate en dorado no se mueve. Ese contraste es el argumento de la sección. Siguen legibles a propósito. Bajarlas a un susurro habría dado mejor textura y habría tirado a la basura la voz del cliente: son palabras suyas, verbatim de la diapositiva 3. El murmullo lo hace el movimiento. Cada frase se pinta cuatro veces —dos por fila para que el bucle no tenga costura, y dos filas— pero solo una llega a los lectores de pantalla. Sin eso recitarían la lista entera de dolores y luego la recitarían otra vez. Hay una prueba que lo exige. Con prefers-reduced-motion no hay rótulo: las cuatro frases se leen quietas a dos columnas. Co-Authored-By: Claude Opus 5 --- components/sections/Dolores.tsx | 107 ++++++++++++++---- .../sections/__tests__/Dolores.test.tsx | 25 +++- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/components/sections/Dolores.tsx b/components/sections/Dolores.tsx index c57007e..a092f6a 100644 --- a/components/sections/Dolores.tsx +++ b/components/sections/Dolores.tsx @@ -3,38 +3,107 @@ import { motion } from 'framer-motion'; import { Container } from '@/components/primitives/Container'; import { IlustracionNudo } from '@/components/primitives/IlustracionNudo'; -import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight'; 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. +// They used to sit in four tidy cards — the opposite of what they describe. Here +// they drift past instead: the noise you carry around, still going, while the +// resolution underneath does not move at all. The contrast between the two is +// the argument. +// +// They stay legible on purpose. Fading them to a whisper would have made a +// better texture and thrown away the client's own voice; the murmur is carried +// by the motion, not by hiding the words. + +const VUELTA_SEGUNDOS = 58; + +function Rotulo({ + frases, + haciaIzquierda, + mudo = false, +}: { + frases: string[]; + haciaIzquierda: boolean; + mudo?: boolean; +}) { + const tanda = (oculta: boolean) => ( +
+ {frases.map((frase) => ( + + “ + {frase} + ” + + ))} +
+ ); + + return ( +
+ {/* Two identical runs inside one track: translating the track by exactly + half its own width lands the second run where the first began, so the + loop has no seam. The duplicate is hidden from assistive tech — it is + the same four sentences twice. */} + + {tanda(false)} + {tanda(true)} + +
+ ); +} export function Dolores() { const reduced = usePrefersReducedMotion(); + // Rotated so the two rows never show the same sentence side by side. + const segunda = [...dolores.quotes.slice(2), ...dolores.quotes.slice(0, 2)]; + return (
{/* Smaller and a touch stronger than it was: sprawling at 8% across the whole title it read as a smudge over the headline rather than as a - knot. Stays centred so the conductor thread runs through it. */} + knot. Stays centred. */} - + + {dolores.eyebrow} {dolores.title} - - {dolores.quotes.map((quote) => ( - - -
- “ - {quote} - ” -
-
-
- ))} -
+
+ + {/* Full-bleed on purpose: the noise does not respect the column. */} + {reduced ? ( + +
    + {dolores.quotes.map((quote) => ( +
  • + “ + {quote} + ” +
  • + ))} +
+
+ ) : ( +
+ + {/* The second row is the same four sentences again, rotated so no + pair lines up. It is a visual echo, so it is silent: without + `mudo` a screen reader would read all four pains, then read them + all over again. */} + +
+ )} + + {dolores.remate.before}{' '} diff --git a/components/sections/__tests__/Dolores.test.tsx b/components/sections/__tests__/Dolores.test.tsx index 55cdaa5..54023b3 100644 --- a/components/sections/__tests__/Dolores.test.tsx +++ b/components/sections/__tests__/Dolores.test.tsx @@ -1,15 +1,30 @@ import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { Dolores } from '../Dolores'; +import { dolores } from '@/lib/content'; describe('', () => { it('renders the four pains verbatim, in the client\'s first person', () => { render(); - expect(screen.getByText(/Trabajo más y gano casi lo mismo que hace dos años\./)).toBeInTheDocument(); - expect(screen.getByText(/no sé en cuál enfocar mi tiempo y mi dinero\./)).toBeInTheDocument(); - expect(screen.getByText(/no puede crecer si yo no estoy en cada decisión\./)).toBeInTheDocument(); - expect(screen.getByText(/ya no me sirve para el siguiente nivel\./)).toBeInTheDocument(); - expect(screen.getAllByRole('listitem')).toHaveLength(4); + for (const dolor of dolores.quotes) { + expect(screen.getAllByText(new RegExp(dolor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))).length).toBeGreaterThan(0); + } + }); + + it('says each pain only once out loud, however many times it is painted', () => { + const { container } = render(); + // The drifting rows repeat the four sentences four times over: twice per + // row so the loop has no seam, and twice again because there are two rows. + // Only one of those runs may reach assistive tech — otherwise a screen + // reader recites the whole list of pains, and then recites it again. + for (const dolor of dolores.quotes) { + const apariciones = Array.from(container.querySelectorAll('span')).filter( + (el) => el.textContent?.includes(dolor), + ); + const audibles = apariciones.filter((el) => !el.closest('[aria-hidden="true"]')); + expect(apariciones.length).toBeGreaterThan(1); + expect(audibles).toHaveLength(1); + } }); it('closes with the resolution, not with more effort', () => { const { container } = render();