docs(recorrido): spec y plan del recorrido de diálogo
Sale de la llamada del 31/jul: "te va contestando y te va como llevando de uno a otro". Los seis puntos de inflexión dejan de ser tarjetas mudas y pasan a ser una conversación. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2383e63bc5
commit
170482a32b
2 files changed
+577
No files matched your search
@@ -0,0 +1,467 @@
|
||||
# El recorrido de diálogo — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Convertir `PuntosInflexion` de una rejilla de seis tarjetas mudas en una conversación: cada punto de inflexión enuncia la situación del lector y creeer contesta qué hace al respecto.
|
||||
|
||||
**Architecture:** Nace un primitivo `ParDialogo` que encapsula un intercambio (situación + respuesta + su separación visual) y no sabe nada de la sección que lo monta. `lib/content.ts` cambia la forma de `puntosInflexion.items` de `{ name, desc }` a `{ name, situacion, respuesta }`. `PuntosInflexion` conserva su cabecera y sustituye el cuerpo por una `<ul>` de seis `ParDialogo`. El carrusel móvil desaparece y con él el primitivo `Carrusel`, que se queda sin consumidores.
|
||||
|
||||
**Tech Stack:** Next.js 14 App Router (`output: 'export'`), React 18, TypeScript, Tailwind, Framer Motion 11, Vitest + jsdom + Testing Library.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-03-recorrido-dialogo-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Gestor de paquetes: `pnpm`** (`pnpm@11.7.0` vía corepack). No usar npm.
|
||||
- **Rama nueva por cada cambio.** Este trabajo va en `feat/recorrido-dialogo`. Nunca trabajar directo en `main`.
|
||||
- **No hacer commits sin aprobación explícita del usuario.** Los pasos de commit de este plan están escritos, pero **hay que pedir el visto bueno antes de ejecutarlos**.
|
||||
- **El copy vive en `lib/content.ts`, no en el JSX.** Las comillas angulares que envuelven la situación son puntuación del componente, no copy.
|
||||
- **El wordmark nunca se escribe a mano en el JSX.** Solo `<Wordmark />` dibuja `cre≡er`.
|
||||
- **Proyecto en español** — copy, docs y commits. **Los comentarios de código, en inglés.**
|
||||
- **Toda sección animada** combina `usePrefersReducedMotion()` con `motionProps(reduced)`. Es requisito de accesibilidad del spec.
|
||||
- **Tokens de color de `tailwind.config.ts`** (`azul-profundo`, `dorado`, `gris-claro`, `gris-medio`), nunca hex sueltos.
|
||||
- **Sin escasez:** ningún texto puede contener `cupos`, `cohorte` ni `generación fundadora`.
|
||||
- **Ningún conector puede depender de `pathLength`** sobre un SVG con `preserveAspectRatio="none"`. Chromium recorre el guion en espacio de pantalla y normaliza la unidad desde el espacio de usuario, así que el trazo llega partido. Usar bordes o formas rellenas.
|
||||
|
||||
## File Structure
|
||||
|
||||
| Archivo | Responsabilidad |
|
||||
|---------|-----------------|
|
||||
| `components/primitives/ParDialogo.tsx` | **Nuevo.** Un intercambio: la situación del lector y la respuesta de creeer, con las dos voces distinguibles sin depender del color |
|
||||
| `components/primitives/__tests__/ParDialogo.test.tsx` | **Nuevo.** Contrato del primitivo |
|
||||
| `lib/content.ts` | `puntosInflexion.items` pasa a `{ name, situacion, respuesta }`; se añade `lead` |
|
||||
| `components/sections/PuntosInflexion.tsx` | Cabecera intacta; el cuerpo pasa de rejilla + carrusel a `<ul>` de `ParDialogo` |
|
||||
| `components/sections/__tests__/PuntosInflexion.test.tsx` | Se actualiza: ya no hay tarjetas ni duplicado por carrusel |
|
||||
| `components/primitives/Carrusel.tsx` | **Se retira.** Sin consumidores tras el cambio |
|
||||
| `components/primitives/__tests__/Carrusel.test.tsx` | **Se retira** con él |
|
||||
|
||||
---
|
||||
|
||||
### Task 0: Abrir la rama
|
||||
|
||||
**Files:** ninguno.
|
||||
|
||||
- [ ] **Step 1: Crear la rama de trabajo**
|
||||
|
||||
El repositorio arrastra la retirada del hilo conductor sin commitear en `feat/hilo-conductor-visual`. Antes de empezar, confirmar con el usuario qué hacer con esos cambios (commitearlos allí o arrastrarlos). Después:
|
||||
|
||||
```bash
|
||||
git checkout -b feat/recorrido-dialogo
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: la rama nueva existe y `git status` muestra solo lo que el usuario haya decidido arrastrar.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `ParDialogo`, el intercambio
|
||||
|
||||
**Files:**
|
||||
- Create: `components/primitives/ParDialogo.tsx`
|
||||
- Test: `components/primitives/__tests__/ParDialogo.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Wordmark` de `@/components/primitives/Wordmark` (props `size?: 'sm'|'md'|'lg'|'xl'`, `onDark?: boolean`, `showTagline?: boolean`); `fadeInUp` de `@/lib/motion`.
|
||||
- Produces: `ParDialogo({ n, name, situacion, respuesta }: { n: number; name: string; situacion: string; respuesta: string })`. Renderiza un `<li>`, así que **el que lo monta debe darle una `<ul>` o `<ol>` como padre**. La Task 2 depende de esa firma exacta.
|
||||
|
||||
- [ ] **Step 1: Escribir el test que falla**
|
||||
|
||||
Crear `components/primitives/__tests__/ParDialogo.test.tsx`:
|
||||
|
||||
```tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ParDialogo } from '../ParDialogo';
|
||||
|
||||
const par = {
|
||||
n: 1,
|
||||
name: 'Crecer',
|
||||
situacion: 'El siguiente salto no tiene mapa.',
|
||||
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro.',
|
||||
};
|
||||
|
||||
describe('<ParDialogo />', () => {
|
||||
it('renders both voices of the exchange', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getByText(/El siguiente salto no tiene mapa/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Me sumerjo hasta entender tu empresa desde dentro/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('attributes the answer to creeer, not to the reader', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
// Without a speaker label the answer reads as more description of the
|
||||
// problem. The wordmark is that label — and <Wordmark /> is the only
|
||||
// thing allowed to draw cre≡er, never hand-typed in the JSX.
|
||||
expect(screen.getByLabelText('creeer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('labels the situation with its inflection point', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getByText(/01 · Crecer/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tells the two voices apart by more than colour', () => {
|
||||
const { container } = render(<ul><ParDialogo {...par} /></ul>);
|
||||
// A reader who cannot separate gold from navy still has the speaker
|
||||
// label, the column, and the rule hanging off each block. Colour alone
|
||||
// would fail the spec's accessibility criterion.
|
||||
const bloques = container.querySelectorAll('li > div');
|
||||
expect(bloques).toHaveLength(2);
|
||||
bloques.forEach((b) => expect(b.className).toMatch(/border-l-2/));
|
||||
});
|
||||
|
||||
it('is a list item, so the section can mount six of them as a list', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Correr el test y verificar que falla**
|
||||
|
||||
Run: `pnpm test components/primitives/__tests__/ParDialogo.test.tsx`
|
||||
Expected: FAIL — `Failed to resolve import "../ParDialogo"`.
|
||||
|
||||
- [ ] **Step 3: Escribir el componente**
|
||||
|
||||
Crear `components/primitives/ParDialogo.tsx`:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Wordmark } from '@/components/primitives/Wordmark';
|
||||
import { fadeInUp } from '@/lib/motion';
|
||||
|
||||
// One exchange of the page's conversation: the reader's situation, then what
|
||||
// creeer does about it.
|
||||
//
|
||||
// The sides never swap from one pair to the next. Each voice keeps its own
|
||||
// margin, the way a conversation reads on a page; what alternates going down
|
||||
// is situación → respuesta, not left → right. Flipping sides per pair would
|
||||
// cost the reader the one cue that tells them who is speaking.
|
||||
//
|
||||
// Three cues carry that distinction, never colour alone: the speaker label,
|
||||
// the column, and the rule hanging off each block. The answer also drops half
|
||||
// a step below the situation on wide screens, so the eye reads it as a reply
|
||||
// rather than as a second column of the same thought.
|
||||
//
|
||||
// No motion.li here on purpose: Framer propagates variants through React
|
||||
// context, so the two motion.div children still inherit the stagger from the
|
||||
// <ul> the section wraps them in, even across this plain <li>.
|
||||
|
||||
export function ParDialogo({
|
||||
n,
|
||||
name,
|
||||
situacion,
|
||||
respuesta,
|
||||
}: {
|
||||
n: number;
|
||||
name: string;
|
||||
situacion: string;
|
||||
respuesta: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="grid gap-5 md:grid-cols-2 md:gap-12">
|
||||
<motion.div variants={fadeInUp} className="border-l-2 border-azul-profundo/25 pl-5">
|
||||
<p className="text-xs font-black uppercase tracking-[0.2em] text-dorado">
|
||||
{String(n).padStart(2, '0')} · {name}
|
||||
</p>
|
||||
<p className="mt-3 font-sans font-bold text-xl md:text-2xl text-azul-profundo leading-snug">
|
||||
«{situacion}»
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div variants={fadeInUp} className="border-l-2 border-dorado pl-5 md:mt-10">
|
||||
<Wordmark size="sm" />
|
||||
<p className="mt-3 text-gris-medio leading-relaxed">{respuesta}</p>
|
||||
</motion.div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Correr el test y verificar que pasa**
|
||||
|
||||
Run: `pnpm test components/primitives/__tests__/ParDialogo.test.tsx`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 5: Typecheck**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: sin salida.
|
||||
|
||||
- [ ] **Step 6: Commit** *(pedir aprobación primero — ver Global Constraints)*
|
||||
|
||||
```bash
|
||||
git add components/primitives/ParDialogo.tsx components/primitives/__tests__/ParDialogo.test.tsx
|
||||
git commit -m "feat(recorrido): ParDialogo — la situación y la respuesta como un intercambio"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: El copy y la sección
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/content.ts:35-48` (el bloque `puntosInflexion`)
|
||||
- Modify: `components/sections/PuntosInflexion.tsx`
|
||||
- Test: `components/sections/__tests__/PuntosInflexion.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ParDialogo({ n, name, situacion, respuesta })` de la Task 1.
|
||||
- Produces: `puntosInflexion` con forma `{ eyebrow: string; title: string; lead: string; items: { name: string; situacion: string; respuesta: string }[] }`. **`desc` desaparece**: ningún otro archivo lo lee (verificado — `PuntosInflexion.tsx` era su único consumidor).
|
||||
|
||||
- [ ] **Step 1: Escribir los tests que fallan**
|
||||
|
||||
Reemplazar el contenido de `components/sections/__tests__/PuntosInflexion.test.tsx`:
|
||||
|
||||
```tsx
|
||||
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 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.getByText(new RegExp(name))).toBeInTheDocument();
|
||||
}
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('answers every situation instead of only naming it', () => {
|
||||
render(<PuntosInflexion />);
|
||||
// This is 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', () => {
|
||||
render(<PuntosInflexion />);
|
||||
// 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.
|
||||
for (const { situacion } of puntosInflexion.items) {
|
||||
expect(situacion).toMatch(/\b(tu|tus|te|ya|creciste|construiste|sabes|puedes|quieres|dejó|volvió|abiertos|mismo)\b/i);
|
||||
}
|
||||
});
|
||||
|
||||
it('does NOT introduce scarcity', () => {
|
||||
const { container } = render(<PuntosInflexion />);
|
||||
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Correr los tests y verificar que fallan**
|
||||
|
||||
Run: `pnpm test components/sections/__tests__/PuntosInflexion.test.tsx`
|
||||
Expected: FAIL — `getAllByLabelText('creeer')` no encuentra nada y `puntosInflexion.items[0].respuesta` es `undefined`.
|
||||
|
||||
- [ ] **Step 3: Reescribir el copy en `lib/content.ts`**
|
||||
|
||||
Sustituir el bloque `puntosInflexion` entero (líneas 35–48) por:
|
||||
|
||||
```ts
|
||||
// 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`, four sections down, 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',
|
||||
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: 'El 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: 'La operación se volvió más compleja que el negocio.',
|
||||
respuesta: 'Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti.',
|
||||
},
|
||||
{
|
||||
name: 'Expandirse',
|
||||
situacion: 'Mercados nuevos, 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: '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.',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Reescribir `PuntosInflexion.tsx`**
|
||||
|
||||
Sustituir el archivo entero por:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Container } from '@/components/primitives/Container';
|
||||
import { FondoPuntos } from '@/components/primitives/FondoPuntos';
|
||||
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';
|
||||
|
||||
export function PuntosInflexion() {
|
||||
const reduced = usePrefersReducedMotion();
|
||||
return (
|
||||
<section className="bg-gris-claro relative overflow-hidden">
|
||||
<FondoPuntos className="-bottom-6 -right-12 w-80 md:w-[32rem] text-azul-profundo/10" distance={32} />
|
||||
<Container className="relative py-16 md:py-24">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
|
||||
<div>
|
||||
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{puntosInflexion.eyebrow}</motion.p>
|
||||
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||
className="mt-4 font-sans font-black text-seccion text-azul-profundo max-w-3xl">{puntosInflexion.title}</motion.h2>
|
||||
</div>
|
||||
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||
className="mt-8 md:mt-0 shrink-0">
|
||||
<IlustracionInflexion className="w-56 md:w-72 h-auto mx-auto" />
|
||||
</motion.div>
|
||||
</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-8 list-none p-0 flex flex-col gap-12 md:gap-16">
|
||||
{puntosInflexion.items.map((item, i) => (
|
||||
<ParDialogo key={item.name} n={i + 1} {...item} />
|
||||
))}
|
||||
</motion.ul>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Correr los tests y verificar que pasan**
|
||||
|
||||
Run: `pnpm test components/sections/__tests__/PuntosInflexion.test.tsx`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 6: Correr la suite entera y el typecheck**
|
||||
|
||||
Run: `pnpm test && npx tsc --noEmit`
|
||||
Expected: todo verde. `Carrusel.test.tsx` sigue pasando por su cuenta — el primitivo aún existe, solo dejó de usarse.
|
||||
|
||||
- [ ] **Step 7: Commit** *(pedir aprobación primero)*
|
||||
|
||||
```bash
|
||||
git add lib/content.ts components/sections/PuntosInflexion.tsx components/sections/__tests__/PuntosInflexion.test.tsx
|
||||
git commit -m "feat(recorrido): los seis puntos dejan de ser tarjetas y pasan a contestar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Retirar el carrusel
|
||||
|
||||
**Files:**
|
||||
- Delete: `components/primitives/Carrusel.tsx`
|
||||
- Delete: `components/primitives/__tests__/Carrusel.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nada.
|
||||
- Produces: nada. Es una retirada.
|
||||
|
||||
- [ ] **Step 1: Verificar que de verdad quedó sin consumidores**
|
||||
|
||||
Run: `grep -rn "Carrusel" --include="*.tsx" --include="*.ts" components app lib`
|
||||
Expected: solo `components/primitives/Carrusel.tsx` y su propio test. Si aparece cualquier otro archivo, **parar**: el primitivo sigue en uso y no se retira.
|
||||
|
||||
- [ ] **Step 2: Retirarlo**
|
||||
|
||||
```bash
|
||||
git rm components/primitives/Carrusel.tsx components/primitives/__tests__/Carrusel.test.tsx
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verificar que nada se rompió**
|
||||
|
||||
Run: `pnpm test && npx tsc --noEmit && pnpm lint`
|
||||
Expected: todo verde, con la suite más corta por los tests del carrusel que se fueron.
|
||||
|
||||
- [ ] **Step 4: Commit** *(pedir aprobación primero)*
|
||||
|
||||
```bash
|
||||
git add -A components/primitives
|
||||
git commit -m "chore: retira Carrusel, sin consumidores desde que el recorrido se apila"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Verificación en navegador
|
||||
|
||||
**Files:** ninguno. Es la comprobación de los criterios de aceptación del spec que jsdom no puede dar, porque **jsdom no hace layout**. Dos de los peores fallos de este proyecto —una banda que alargaba el documento 5.800px y otra que llegaba partida en cuatro— pasaron toda la suite en verde.
|
||||
|
||||
- [ ] **Step 1: Build de producción**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: export estático sin errores.
|
||||
|
||||
- [ ] **Step 2: Levantar el servidor**
|
||||
|
||||
Run: `pnpm dev`
|
||||
Expected: sirve en `http://localhost:3000`. **No correr `pnpm build` a la vez que `pnpm dev`**: comparten `.next` y el servidor acaba sirviendo una página corrupta (se midió un documento de 60.569px por esto).
|
||||
|
||||
- [ ] **Step 3: Revisar a 1440×900 y a 390×844**
|
||||
|
||||
Comprobar contra los criterios del spec:
|
||||
- los seis pares se leen de arriba abajo como conversación;
|
||||
- cada respuesta se distingue como voz de creeer sin depender solo del color;
|
||||
- en 390px no hay desbordes horizontales ni carrusel;
|
||||
- la sección no deja huecos verticales desproporcionados frente a sus vecinas (`Dolores` viene justo después).
|
||||
|
||||
- [ ] **Step 4: Revisar con `prefers-reduced-motion: reduce`**
|
||||
|
||||
Cargar la página con la preferencia activada y confirmar que **ningún par queda invisible** y que todas las reglas laterales se dibujan. Es el fallo clásico de este proyecto: las ramas de movimiento reducido que se quedan en `opacity: 0`.
|
||||
|
||||
- [ ] **Step 5: Mirar `Dolores` y `PuntosInflexion` juntas**
|
||||
|
||||
El spec lo pide explícitamente. Las situaciones describen el estado del negocio; los dolores, cómo se siente. Si al leerlas seguidas suenan a lo mismo, informarlo — **no reescribir `dolores` por cuenta propia**, es texto verbatim de la dueña.
|
||||
|
||||
- [ ] **Step 6: Informar al usuario**
|
||||
|
||||
Resumir qué se verificó y qué queda pendiente del visto bueno de la dueña: las seis respuestas son redacción nueva y las situaciones reescriben líneas marcadas como verbatim de su deck.
|
||||
@@ -0,0 +1,110 @@
|
||||
# El recorrido: la página que te contesta
|
||||
|
||||
**Fecha:** 2026-08-03
|
||||
**Origen:** llamada de Zoom con la dueña del 31/jul/2026 (`Grabación 2026-07-31 171150.mp4`, minutos 5:30–7:40).
|
||||
**Rama:** `feat/recorrido-dialogo`.
|
||||
|
||||
---
|
||||
|
||||
## 1. De dónde sale esto
|
||||
|
||||
En esa llamada Jazmín comparte pantalla, recorre `en-ti.com` como referencia y describe una idea para la página de creeer. Transcripción del tramo:
|
||||
|
||||
> **[06:01]** Pero también estaba pensando que podríamos hacer esto. O sea, dices: empiezas aquí y dices "estás atrapado en tu compañía y no estás creciendo", y ya te bajas, ¿no? Y entonces te bajas y contestas: "esto es para ti". Aquí desafiamos o cuestionamos absolutamente todo lo que has hecho y lo replanteamos, te generamos… y luego otra vez te regresas, ¿no?
|
||||
>
|
||||
> **[06:39]** O la siguiente: vas otra vez de cómo está, y aquí preguntas otra cosa. "No estás creciendo, creciste muy rápido pero quieres expandirte y sientes que no…" Quieres expandirte pero llegaste a un punto de inflexión — y ya contesta creeer y dice "hacemos una inmersión, revisamos tu proceso y vemos las áreas de oportunidad, o cuáles son los siguientes pasos para esa expansión".
|
||||
>
|
||||
> **[07:15]** ¿Sí me explico? Y luego te regresas, y luego otro. O sea, como que te va contestando y te va como llevando de uno a otro.
|
||||
|
||||
**Advertencia sobre la fuente.** Es transcripción automática (Whisper-small sobre el audio de Zoom), no taquigrafía. Tres pasajes quedan en duda: *"y sientes que no sientes"* está mal transcrito y falta el final de la frase; *"hacemos una inversión"* se lee aquí como **inmersión**, que es el paso 01 de `mecanismo` y encaja con el resto de la frase; y hay una repetición por solape de ventanas alrededor de 07:14. El resto se da por fiable. **Pendiente:** que la dueña o Erik confirmen la fidelidad antes de que esto entre en `docs/dueña-directrices.md`.
|
||||
|
||||
## 2. El problema
|
||||
|
||||
La página tiene hoy las piezas de lo que ella describe, pero sueltas y mudas:
|
||||
|
||||
- `PuntosInflexion` enumera seis situaciones en una rejilla de tarjetas. Ninguna dice qué hace creeer al respecto.
|
||||
- `Mecanismo` dice qué hace creeer —inmersión, desafiar supuestos, estrategia ejecutable— pero desligado de la situación de quien lee.
|
||||
- `ParaQuien` dice "esto es para ti si…" como lista de requisitos, no como respuesta.
|
||||
|
||||
El lector se reconoce en una tarjeta y ahí se acaba. Nada le contesta. Eso es exactamente lo que ella pide: *"te va contestando y te va como llevando de uno a otro"*.
|
||||
|
||||
## 3. Concepto: el diálogo
|
||||
|
||||
`PuntosInflexion` deja de ser una rejilla y pasa a ser una **secuencia de seis pares**. Cada par es un intercambio:
|
||||
|
||||
- **la situación**, en segunda persona, con la voz del lector;
|
||||
- **la respuesta de creeer**, en primera persona, con la voz de la estratega.
|
||||
|
||||
Alternan al bajar, unidos por un conector, de modo que la sección se lee como una conversación y no como un catálogo. Todo el texto queda en el DOM: nada se esconde tras un clic, ni por SEO ni por accesibilidad.
|
||||
|
||||
La segunda persona no es invención: es como los enuncia ella misma en la llamada (*"estás atrapado…", "creciste muy rápido pero quieres expandirte…"*).
|
||||
|
||||
## 4. El contenido
|
||||
|
||||
Los seis puntos de inflexión se conservan —son de la diapositiva 2 de su deck— y cada uno gana su par.
|
||||
|
||||
| # | Punto | Situación (tú) | Respuesta (cre≡er) |
|
||||
|---|-------|----------------|--------------------|
|
||||
| 01 | Crecer | Ya demostraste que sabes crecer. El siguiente salto no tiene mapa. | Me sumerjo hasta entender tu empresa desde dentro. Sales sabiendo por dónde, no intentándolo por todos lados a la vez. |
|
||||
| 02 | Consolidarse | Construiste rápido. Escalar así multiplicaría el desorden. | Ordenamos lo que ya existe antes de ponerle más peso encima. Escalas sobre algo que aguanta. |
|
||||
| 03 | Recuperar rentabilidad | El esfuerzo dejó de traducirse en resultado. | Cuestiono lo que todos dan por hecho en tu operación. El margen no siempre está en vender más. |
|
||||
| 04 | Simplificar | La operación se volvió más compleja que el negocio. | Separo lo que solo tú puedes decidir de lo que la empresa debería resolver sin ti. |
|
||||
| 05 | Expandirse | Mercados nuevos, el mismo mapa de decisión. | Reviso tu proceso y ubico la oportunidad real antes de mover una ficha. Expandirte deja de ser una apuesta. |
|
||||
| 06 | Replantear el portafolio | Varios frentes abiertos y ninguno con su lugar claro. | Construimos, juntos, el criterio para decidir qué sigue y qué se deja ir. Por escrito, no en tu cabeza. |
|
||||
|
||||
Los pares **01** y **05** son los dos ejemplos que ella da en el audio, redactados.
|
||||
|
||||
### Frontera con `Dolores`
|
||||
|
||||
`Dolores` son cuatro citas en primera persona sobre lo que el lector **siente** ("Trabajo más y gano casi lo mismo que hace dos años"). Las seis situaciones de aquí describen el estado del **negocio**, no el ánimo. Esa frontera es deliberada y hay que sostenerla al redactar: si una situación empieza a sonar a queja, invade `Dolores`.
|
||||
|
||||
Las dos secciones van seguidas en la página. Conviene mirarlas juntas una vez implementado esto.
|
||||
|
||||
### Riesgo de repetición
|
||||
|
||||
Las tres respuestas posibles salen todas del mismo mecanismo. Con seis pares hay riesgo real de que las seis digan lo mismo con distintas palabras. Mitigación: cada respuesta abre por el movimiento concreto de esa situación y cierra por lo que cambia para el lector, no por la actividad. Si aun así sobran, **recortar a cuatro es decisión de la dueña** — los seis son suyos.
|
||||
|
||||
## 5. Arquitectura
|
||||
|
||||
| Archivo | Cambio |
|
||||
|---------|--------|
|
||||
| `lib/content.ts` | `puntosInflexion.items` cambia de `{ name, desc }` a `{ name, situacion, respuesta }` |
|
||||
| `components/primitives/ParDialogo.tsx` | **Nuevo.** Un par: situación, respuesta y el conector entre ambas |
|
||||
| `components/primitives/__tests__/ParDialogo.test.tsx` | **Nuevo** |
|
||||
| `components/sections/PuntosInflexion.tsx` | De rejilla + carrusel a secuencia de `ParDialogo` |
|
||||
| `components/sections/__tests__/PuntosInflexion.test.tsx` | Actualizar: ya no hay tarjetas ni carrusel |
|
||||
| `components/primitives/Carrusel.tsx` + su test | **Retirar.** Queda sin consumidores (ver abajo) |
|
||||
|
||||
**`ParDialogo`** recibe `n`, `name`, `situacion` y `respuesta`, y no sabe nada de la sección que lo monta. Es la única pieza que conoce la forma del intercambio.
|
||||
|
||||
Los lados **no alternan par a par**: la situación va siempre al mismo lado y la respuesta siempre al contrario, como en una conversación, donde cada interlocutor mantiene su margen. Lo que alterna al bajar es situación → respuesta → situación → respuesta. Voltear los lados en cada par rompería la lectura de quién habla.
|
||||
|
||||
**`PuntosInflexion`** conserva su cabecera actual —eyebrow, título e `IlustracionInflexion`— y sustituye el cuerpo.
|
||||
|
||||
**El carrusel móvil desaparece**: el diálogo se apila en vertical y no necesita deslizamiento — además, *"te bajas"* es literalmente lo que pide la dueña. Esto deja a `Carrusel` **sin ningún consumidor**: hoy `PuntosInflexion` es el único. Se retira junto con su test, como se hizo con `SimboloTresLineas`; el historial de git lo conserva si vuelve a hacer falta. Dejarlo montado sin uso repetiría un huérfano que ya se limpió una vez en este proyecto.
|
||||
|
||||
**Movimiento.** Cada par entra con `fadeInUp` dentro de un `staggerContainer`, con `viewportOnce`, y **siempre** combinando `usePrefersReducedMotion()` con `motionProps(reduced)`, como el resto de las secciones.
|
||||
|
||||
**Los conectores se dibujan con bordes o con formas rellenas, nunca con `pathLength` sobre un SVG escalado de forma no uniforme.** Esa combinación se rompe en Chromium —el guion se recorre en espacio de pantalla pero se normaliza desde el espacio de usuario, y el trazo llega partido en trozos— y es lo que acaba de retirarse con el hilo conductor.
|
||||
|
||||
**Tests.** Verifican copy y contrato de marca, no estilos, como el resto del proyecto: que aparezcan las seis situaciones y las seis respuestas; que cada respuesta esté atribuida a creeer y cada situación al lector; que con `prefers-reduced-motion` no quede nada invisible.
|
||||
|
||||
## 6. Decisiones que necesitan el visto bueno de la dueña
|
||||
|
||||
1. **Se reescriben líneas marcadas como verbatim de su deck.** Los `desc` actuales ("Dar el siguiente salto sin saber por dónde") pasan a segunda persona y dejan de ser literales de la diapositiva 2. Se hace porque es como ella misma los enuncia en la llamada, pero es su texto.
|
||||
2. **Ella dice "nosotros", la página dice "yo".** En el audio: *"desafiamos", "cuestionamos", "hacemos una inmersión"*. La web está en primera persona del singular porque es una estratega sola, y así está `mecanismo`. Aquí se mantiene el singular salvo donde el trabajo es conjunto ("Construimos, juntos"). Pasar todo a plural sería un cambio global de voz, no de esta sección.
|
||||
3. **Las seis respuestas son redacción nueva**, no salidas de ningún material suyo. Hay que validarlas una por una.
|
||||
|
||||
## 7. Fuera de alcance
|
||||
|
||||
La otra mitad de ese tramo de audio: el claim del hero (*"a la mejor, si ponemos 'oye, damos claridad'"*), la **diana con un punto**, y jugar con los puntitos de la marca. Ahí ella está pensando en voz alta y no cierra nada. Merece su propio spec y su propia decisión.
|
||||
|
||||
## 8. Criterios de aceptación
|
||||
|
||||
- [ ] Los seis pares se leen de arriba abajo como una conversación: situación, respuesta, situación, respuesta.
|
||||
- [ ] Todo el texto está en el DOM sin interacción: nada tras un clic, un hover o un deslizamiento.
|
||||
- [ ] Cada respuesta se distingue visualmente como voz de creeer, y cada situación como voz del lector, sin depender solo del color.
|
||||
- [ ] En móvil (390px) la secuencia se lee sin desbordes horizontales y sin carrusel.
|
||||
- [ ] Con `prefers-reduced-motion: reduce` no queda ningún par invisible ni ningún conector sin dibujar.
|
||||
- [ ] `pnpm test`, `npx tsc --noEmit`, `pnpm lint` y `pnpm build` limpios.
|
||||
- [ ] Ningún conector depende de `pathLength` sobre un SVG con `preserveAspectRatio="none"`.
|
||||
Reference in new issue
Block a user