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:
1 parent
a9d1f44df2
commit
c1a5e79c63
3 files changed
+93
-43
No files matched your search
@@ -3,27 +3,11 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Container } from '@/components/primitives/Container';
|
import { Container } from '@/components/primitives/Container';
|
||||||
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
|
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
|
||||||
import { Carrusel } from '@/components/primitives/Carrusel';
|
import { ParDialogo } from '@/components/primitives/ParDialogo';
|
||||||
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
|
|
||||||
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
|
import { IlustracionInflexion } from '@/components/primitives/IlustracionInflexion';
|
||||||
import { puntosInflexion } from '@/lib/content';
|
import { puntosInflexion } from '@/lib/content';
|
||||||
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
|
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() {
|
export function PuntosInflexion() {
|
||||||
const reduced = usePrefersReducedMotion();
|
const reduced = usePrefersReducedMotion();
|
||||||
return (
|
return (
|
||||||
@@ -43,22 +27,18 @@ export function PuntosInflexion() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Swipeable carousel on mobile ("01 / 06" counter, as on en-ti.com); fixed grid from md up. */}
|
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
<div className="mt-12 md:hidden">
|
className="mt-14 text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.lead}</motion.p>
|
||||||
<Carrusel
|
|
||||||
ariaLabel="Los seis puntos de inflexión"
|
{/* The conversation. One <li> per exchange, read top to bottom — no
|
||||||
mostrarContador
|
carousel at any breakpoint, because "te bajas" is the whole shape
|
||||||
slides={puntosInflexion.items.map((item, i) => <Tarjeta key={item.name} n={i + 1} {...item} />)}
|
the dueña asked for. The stagger reaches ParDialogo's two halves
|
||||||
slideClassName="pr-4"
|
through React context, so each pair arrives in two beats: the
|
||||||
/>
|
situation, then the answer. */}
|
||||||
</div>
|
|
||||||
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
<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) => (
|
{puntosInflexion.items.map((item, i) => (
|
||||||
<motion.li key={item.name} variants={fadeInUp}
|
<ParDialogo key={item.name} n={i + 1} {...item} />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</motion.ul>
|
</motion.ul>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,22 +1,58 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { PuntosInflexion } from '../PuntosInflexion';
|
import { PuntosInflexion } from '../PuntosInflexion';
|
||||||
|
import { puntosInflexion } from '@/lib/content';
|
||||||
|
|
||||||
describe('<PuntosInflexion />', () => {
|
describe('<PuntosInflexion />', () => {
|
||||||
it('renders the section title', () => {
|
it('renders the section title', () => {
|
||||||
render(<PuntosInflexion />);
|
render(<PuntosInflexion />);
|
||||||
expect(screen.getByText(/llegan a un punto de inflexión/i)).toBeInTheDocument();
|
expect(screen.getByText(/llegan a un punto de inflexión/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the six inflection points from the dueña deck', () => {
|
it('renders the six inflection points from the dueña deck', () => {
|
||||||
render(<PuntosInflexion />);
|
render(<PuntosInflexion />);
|
||||||
// Each point renders twice in the markup — once in the mobile carousel,
|
// Each point now renders once, not twice: the mobile carousel is gone and
|
||||||
// once in the desktop grid — with CSS `hidden`/`md:` toggling which one
|
// the conversation stacks vertically at every breakpoint.
|
||||||
// is actually visible at a given breakpoint.
|
|
||||||
for (const name of ['Crecer', 'Consolidarse', 'Recuperar rentabilidad', 'Simplificar', 'Expandirse', 'Replantear el portafolio']) {
|
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);
|
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', () => {
|
it('does NOT introduce scarcity', () => {
|
||||||
const { container } = render(<PuntosInflexion />);
|
const { container } = render(<PuntosInflexion />);
|
||||||
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
|
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
|
||||||
|
|||||||
+42
-8
@@ -32,18 +32,52 @@ export const fraseAncla = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// The six inflection points, verbatim from the dueña's deck
|
// The six inflection points, from the dueña's deck "Rumbo Estratégico ·
|
||||||
// "Rumbo Estratégico · Propuesta Socio" (slide 2).
|
// 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 = {
|
export const puntosInflexion = {
|
||||||
eyebrow: 'el contexto',
|
eyebrow: 'el contexto',
|
||||||
title: 'Todos los negocios consolidados llegan a un punto de inflexión',
|
title: 'Todos los negocios consolidados llegan a un punto de inflexión',
|
||||||
|
lead: '¿Te reconoces?',
|
||||||
items: [
|
items: [
|
||||||
{ name: 'Crecer', desc: 'Dar el siguiente salto sin saber por dónde.' },
|
{
|
||||||
{ name: 'Consolidarse', desc: 'Ordenar lo construido antes de escalar.' },
|
name: 'Crecer',
|
||||||
{ name: 'Recuperar rentabilidad', desc: 'El esfuerzo ya no se traduce en resultado.' },
|
situacion: 'Ya demostraste que sabes crecer. El siguiente salto no tiene mapa.',
|
||||||
{ name: 'Simplificar', desc: 'Demasiada complejidad operativa.' },
|
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez.',
|
||||||
{ name: 'Expandirse', desc: 'Nuevos mercados, mismo mapa de decisión.' },
|
},
|
||||||
{ name: 'Replantear el portafolio', desc: 'Qué seguir, qué dejar ir.' },
|
{
|
||||||
|
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.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user