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>
468 lines
21 KiB
Markdown
468 lines
21 KiB
Markdown
# 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.
|