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);
+42 -8
View File
@@ -32,18 +32,52 @@ export const fraseAncla = {
},
};
// The six inflection points, verbatim from the dueña's deck
// "Rumbo Estratégico · Propuesta Socio" (slide 2).
// The six inflection points, from the dueña's deck "Rumbo Estratégico ·
// Propuesta Socio" (slide 2). The deck states each one in the infinitive
// ("Dar el siguiente salto sin saber por dónde"); here it is stated in the
// second person, the way she reads them aloud herself — "estás atrapado en tu
// compañía", "creciste muy rápido pero quieres expandirte" (call of 31/jul,
// 6:01–7:15). `respuesta` is new copy and still needs her sign-off; 01 and 05
// are the two exchanges she improvised on that call, written out.
//
// `situacion` describes the state of the business. It must not drift into how
// the reader feels — that is `dolores`, one section down, in the client's own
// first-person words, and the two would collapse into each other.
export const puntosInflexion = {
eyebrow: 'el contexto',
title: 'Todos los negocios consolidados llegan a un punto de inflexión',
lead: '¿Te reconoces?',
items: [
{ name: 'Crecer', desc: 'Dar el siguiente salto sin saber por dónde.' },
{ name: 'Consolidarse', desc: 'Ordenar lo construido antes de escalar.' },
{ name: 'Recuperar rentabilidad', desc: 'El esfuerzo ya no se traduce en resultado.' },
{ name: 'Simplificar', desc: 'Demasiada complejidad operativa.' },
{ name: 'Expandirse', desc: 'Nuevos mercados, mismo mapa de decisión.' },
{ name: 'Replantear el portafolio', desc: 'Qué seguir, qué dejar ir.' },
{
name: 'Crecer',
situacion: 'Ya demostraste que sabes crecer. El siguiente salto no tiene mapa.',
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez.',
},
{
name: 'Consolidarse',
situacion: 'Construiste rápido. Escalar así multiplicaría el desorden.',
respuesta: 'Ordenamos lo que ya existe antes de ponerle más peso encima. Escalas sobre algo que aguanta.',
},
{
name: 'Recuperar rentabilidad',
situacion: 'Tu esfuerzo dejó de traducirse en resultado.',
respuesta: 'Cuestiono lo que todos dan por hecho en tu operación. El margen no siempre está en vender más.',
},
{
name: 'Simplificar',
situacion: 'Tu operación se volvió más compleja que tu negocio.',
respuesta: 'Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti.',
},
{
name: 'Expandirse',
situacion: 'Vas a mercados nuevos con el mismo mapa de decisión.',
respuesta: 'Reviso tu proceso y ubico la oportunidad real antes de mover una ficha. Expandirte deja de ser una apuesta.',
},
{
name: 'Replantear el portafolio',
situacion: 'Tienes varios frentes abiertos y ninguno con su lugar claro.',
respuesta: 'Construimos, juntos, el criterio para decidir qué sigue y qué se deja ir. Por escrito, no en tu cabeza.',
},
],
};