feat(recorrido): los seis puntos dejan de ser tarjetas y pasan a contestar

Cada punto de inflexión enuncia ahora la situación en segunda persona y
creeer responde qué hace al respecto. 01 y 05 son los dos intercambios
que la dueña improvisó en la llamada del 31/jul, redactados.

El test de segunda persona cazó cuatro situaciones impersonales que yo
había escrito ("El esfuerzo dejó de traducirse en resultado" no
interpela a nadie). Se corrigió el copy, no el test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-08-03 11:14:57 -06:00
1 parent a9d1f44df2
commit c1a5e79c63
3 files changed
+93 -43

No files matched your search

+11 -31
View File
@@ -3,27 +3,11 @@
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
import { Carrusel } from '@/components/primitives/Carrusel';
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
import { ParDialogo } from '@/components/primitives/ParDialogo';
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
import { puntosInflexion } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
function Tarjeta({ n, name, desc }: { n: number; name: string; desc: string }) {
return (
<TarjetaSpotlight className="relative h-full rounded-md border-t-2 border-dorado pt-5 pl-1 pr-1 pb-1 overflow-visible">
{/* An index, not a watermark. At 48px and 6% opacity these collided with
the titles and with the gold rule while carrying no contrast of their
own; sat on the rule they encode the sequence and stay out of the way. */}
<span aria-hidden="true" className="pointer-events-none absolute -top-1.5 right-0 bg-gris-claro pl-2 font-sans font-black text-xs tracking-[0.2em] text-dorado leading-none select-none">
{String(n).padStart(2, '0')}
</span>
<h3 className="relative font-sans font-bold text-xl text-azul-profundo">{name}</h3>
<p className="relative mt-2 text-gris-medio leading-relaxed">{desc}</p>
</TarjetaSpotlight>
);
}
export function PuntosInflexion() {
const reduced = usePrefersReducedMotion();
return (
@@ -43,22 +27,18 @@ export function PuntosInflexion() {
</motion.div>
</div>
{/* Swipeable carousel on mobile ("01 / 06" counter, as on en-ti.com); fixed grid from md up. */}
<div className="mt-12 md:hidden">
<Carrusel
ariaLabel="Los seis puntos de inflexión"
mostrarContador
slides={puntosInflexion.items.map((item, i) => <Tarjeta key={item.name} n={i + 1} {...item} />)}
slideClassName="pr-4"
/>
</div>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-14 text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.lead}</motion.p>
{/* The conversation. One <li> per exchange, read top to bottom — no
carousel at any breakpoint, because "te bajas" is the whole shape
the dueña asked for. The stagger reaches ParDialogo's two halves
through React context, so each pair arrives in two beats: the
situation, then the answer. */}
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 hidden md:grid md:grid-cols-3 md:gap-8 list-none p-0">
className="mt-8 list-none p-0 flex flex-col gap-12 md:gap-16">
{puntosInflexion.items.map((item, i) => (
<motion.li key={item.name} variants={fadeInUp}
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
<Tarjeta n={i + 1} {...item} />
</motion.li>
<ParDialogo key={item.name} n={i + 1} {...item} />
))}
</motion.ul>
</Container>
@@ -1,22 +1,58 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { PuntosInflexion } from '../PuntosInflexion';
import { puntosInflexion } from '@/lib/content';
describe('<PuntosInflexion />', () => {
it('renders the section title', () => {
render(<PuntosInflexion />);
expect(screen.getByText(/llegan a un punto de inflexión/i)).toBeInTheDocument();
});
it('renders the six inflection points from the dueña deck', () => {
render(<PuntosInflexion />);
// Each point renders twice in the markup — once in the mobile carousel,
// once in the desktop grid — with CSS `hidden`/`md:` toggling which one
// is actually visible at a given breakpoint.
// Each point now renders once, not twice: the mobile carousel is gone and
// the conversation stacks vertically at every breakpoint.
for (const name of ['Crecer', 'Consolidarse', 'Recuperar rentabilidad', 'Simplificar', 'Expandirse', 'Replantear el portafolio']) {
expect(screen.getAllByText(name).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(new RegExp(name))).toBeInTheDocument();
}
expect(screen.getAllByRole('listitem')).toHaveLength(6);
});
it('answers every situation instead of only naming it', () => {
render(<PuntosInflexion />);
// The whole point of the section, and the one thing that would silently
// regress back into the old mute grid: six situations, six answers, each
// spoken by creeer.
expect(screen.getAllByLabelText('creeer')).toHaveLength(6);
for (const { respuesta } of puntosInflexion.items) {
expect(screen.getByText(respuesta)).toBeInTheDocument();
}
});
it('states each situation in the second person, as the dueña reads them', () => {
// Her own phrasing in the 31/jul call: "estás atrapado en tu compañía",
// "creciste muy rápido pero quieres expandirte". The deck's infinitives
// ("Dar el siguiente salto") would put the reader outside their own story.
//
// Only real second-person markers count here. The first draft of this
// pattern also accepted `dejó|volvió|abiertos|mismo`, which let four
// impersonal lines through — "El esfuerzo dejó de traducirse en resultado"
// never addresses anyone. That was the test being bent to fit the copy.
for (const { situacion } of puntosInflexion.items) {
expect(situacion).toMatch(/\b(tu|tus|te|ti|tienes|vas|sabes|quieres|puedes|demostraste|construiste|creciste)\b/i);
}
});
it('keeps the situations about the business, not about how it feels', () => {
// `dolores` is four sections down and owns the feelings, in the client's
// own first-person words. If a situation starts saying "siento" or "estoy
// agotado", the two sections collapse into each other.
for (const { situacion } of puntosInflexion.items) {
expect(situacion).not.toMatch(/\b(siento|me siento|estoy|agotad|frustrad|harto)\b/i);
}
});
it('does NOT introduce scarcity', () => {
const { container } = render(<PuntosInflexion />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);