chore(docs): retira documentación obsoleta y mueve CLAUDE.md a docs/

Fuera las directrices, los finalistas del hero, el branding en jpeg, los
specs y planes ya ejecutados o sustituidos, y la skill react-doctor.
CLAUDE.md pasa a docs/CLAUDE.md. Todo sigue en la historia de git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5.5 committed 2026-09-30 11:26:44 -06:00
1 parent 97fd0a0224
commit d028fa5804
18 files changed
+2 -7692

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,462 +0,0 @@
# creeer Landing Additions — Header + Cómo empezar + Footer — 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:** Add a minimal sticky Header, a new "Cómo empezar" on-ramp section (section 07), and a slim Footer to the existing 7-section `creeer` landing page.
**Architecture:** Three additive pieces reusing the existing primitives (`Wordmark`, `CTA`, `Container`), tokens, the `motion` + `motionProps(reduced)` pattern, and the copy-in-`lib/content.ts` convention. Header and Footer are static server components wrapping `<main>`; `ComoEmpezar` is a client section following the exact pattern of `Mecanismo.tsx`/`Disciplinas.tsx`. Cierre becomes section 08. No backend, no forms — the site stays a static export.
**Tech Stack:** Next.js 14 (App Router, `output: export`), React 18, TypeScript, Tailwind CSS 3.4, Framer Motion 11. Tests: Vitest + @testing-library/react.
## Global Constraints
(Copied from the spec `docs/superpowers/specs/2026-07-21-creeer-landing-additions-design.md` — apply to every task.)
- **Wordmark** always via the `Wordmark` primitive — never re-implemented, never uppercased.
- **Paleta (tokens):** `ink #0A1F44` · `gold #B8901f` (accents on LIGHT) · `gold-bright #D4AF37` (accents on DARK) · `base #FBFAF7` (light bg, class `bg-base`) · dark bg class `bg-bg-dark` · `cream` (text on dark) · `muted`/`muted-dark`.
- **Tipografía:** Georgia italic (`font-serif italic`) = wordmark + section titles; Inter = body/UI.
- **Sin backend, sin formularios** — contacto solo por enlaces directos (WhatsApp/mailto/LinkedIn).
- **Un único `<h1>`** en toda la página — vive en el Hero. New pieces use `<h2>`/`<h3>` or no heading.
- **Todo el copy** vive en `lib/content.ts` (nuevas claves `header`, `comoEmpezar`, `footer`).
- **Voz** en segunda persona, español.
- **Reduced motion:** every Framer Motion element gets `{...motionProps(reduced)}` after its `initial`/`whileInView`/`viewport` props (`reduced = usePrefersReducedMotion()`). Header/Footer are static (no motion).
- **`tsconfig.json` has no `target`** → avoid ES2018+ regex flags (`s`/dotAll) or `tsc --noEmit` fails.
## File Structure
```
components/
├── layout/
│ ├── Header.tsx # Task 1 — sticky bar (server component)
│ ├── Footer.tsx # Task 3 — slim footer strip (server component)
│ └── __tests__/
│ ├── Header.test.tsx # Task 1
│ └── Footer.test.tsx # Task 3
└── sections/
├── ComoEmpezar.tsx # Task 2 — section 07 (client, motion pattern)
└── __tests__/
└── ComoEmpezar.test.tsx # Task 2
lib/content.ts # Tasks 1/2/3 append `header` / `comoEmpezar` / `footer`
app/page.tsx # Tasks 1/2/3 wire the pieces; Task 4 anchor offset
components/sections/Cierre.tsx # Task 4 — add scroll-mt to the #contacto anchor
```
Each component task also wires itself into `app/page.tsx` (matching the repo's established incremental-compose pattern), so the page stays working after every task.
---
### Task 1: Header (minimal sticky)
**Files:**
- Create: `components/layout/Header.tsx`
- Create: `components/layout/__tests__/Header.test.tsx`
- Modify: `lib/content.ts` (append `header`)
- Modify: `app/page.tsx` (place `<Header />` before `<main>`)
**Interfaces:**
- Consumes: `Wordmark` (`size`, `onDark`), `CTA` (`href`, `label`, `variant`), `Container`, and `header.cta` from content.
- Produces: `Header()` at `components/layout/Header.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `header` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const header = {
cta: 'Hablemos', // short nav CTA → #contacto (the full CTA lives in Hero/Cierre)
};
```
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Header.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Header } from '../Header';
describe('<Header />', () => {
it('renders the wordmark', () => {
const { container } = render(<Header />);
expect(container.textContent).toContain('creeer');
});
it('renders a persistent CTA linking to the contact section', () => {
render(<Header />);
const cta = screen.getByRole('link', { name: /Hablemos/i });
expect(cta).toHaveAttribute('href', '#contacto');
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- Header`
Expected: FAIL with "Cannot find module '../Header'".
- [ ] **Step 4: Write `components/layout/Header.tsx`**
```tsx
import { Wordmark } from '@/components/primitives/Wordmark';
import { CTA } from '@/components/primitives/CTA';
import { Container } from '@/components/primitives/Container';
import { header } from '@/lib/content';
export function Header() {
return (
<header className="sticky top-0 z-50 border-b border-ink/10 bg-base/80 backdrop-blur">
<Container className="flex items-center justify-between py-3">
<a href="#top" aria-label="creeer — inicio">
<Wordmark size="md" />
</a>
<nav aria-label="Principal">
<CTA href="#contacto" label={header.cta} variant="light" />
</nav>
</Container>
</header>
);
}
```
Notes: server component (no `'use client'`, no motion — `position: sticky` is pure CSS). `#top` is the special top-of-document fragment (no element with that id is required). The `CTA` primitive appends the `→`, so `header.cta` holds just the word.
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- Header`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<Header />` into `app/page.tsx`**
Add the import after the existing section imports (top of the file):
```tsx
import { Header } from '@/components/layout/Header';
```
Then insert `<Header />` between the JSON-LD `<script>` and `<main>`:
```tsx
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<Header />
<main>
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/layout/Header.tsx components/layout/__tests__/Header.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: sticky Header with persistent contact CTA"
```
---
### Task 2: "Cómo empezar" section (07)
**Files:**
- Create: `components/sections/ComoEmpezar.tsx`
- Create: `components/sections/__tests__/ComoEmpezar.test.tsx`
- Modify: `lib/content.ts` (append `comoEmpezar`)
- Modify: `app/page.tsx` (place `<ComoEmpezar />` before `<Cierre />`)
**Interfaces:**
- Consumes: `Container`, `CTA`, `comoEmpezar` + `cierre.contact.whatsapp.href` from content, and `fadeInUp`/`staggerContainer`/`viewportOnce`/`usePrefersReducedMotion`/`motionProps` from `lib/motion`.
- Produces: `ComoEmpezar()` at `components/sections/ComoEmpezar.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `comoEmpezar` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const comoEmpezar = {
eyebrow: 'cómo empezar',
title: 'Tu siguiente movimiento empieza con una conversación.',
steps: [
{ n: '01', name: 'Escríbeme', desc: 'Cuéntame dónde está tu negocio hoy.' },
{ n: '02', name: 'Sesión de diagnóstico', desc: 'Una conversación para ubicar tu punto de inflexión.' },
{ n: '03', name: 'Estrategia ejecutable', desc: 'Sales con claridad y tu siguiente movimiento definido.' },
],
cta: 'Agenda tu sesión de diagnóstico',
whatsappText: 'Hola, quiero agendar una sesión de diagnóstico',
};
```
- [ ] **Step 2: Write the failing test `components/sections/__tests__/ComoEmpezar.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ComoEmpezar } from '../ComoEmpezar';
describe('<ComoEmpezar />', () => {
it('renders the on-ramp steps', () => {
render(<ComoEmpezar />);
expect(screen.getByText('Escríbeme')).toBeInTheDocument();
expect(screen.getByText('Sesión de diagnóstico')).toBeInTheDocument();
});
it('links the CTA to WhatsApp with a prefilled message', () => {
render(<ComoEmpezar />);
const cta = screen.getByRole('link', { name: /Agenda tu sesión de diagnóstico/i });
const href = cta.getAttribute('href') ?? '';
expect(href).toMatch(/^https:\/\/wa\.me\//);
expect(href).toContain('text=');
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- ComoEmpezar`
Expected: FAIL with "Cannot find module '../ComoEmpezar'".
- [ ] **Step 4: Write `components/sections/ComoEmpezar.tsx`**
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { comoEmpezar, cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function ComoEmpezar() {
const reduced = usePrefersReducedMotion();
const waHref = `${cierre.contact.whatsapp.href}?text=${encodeURIComponent(comoEmpezar.whatsappText)}`;
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{comoEmpezar.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{comoEmpezar.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 grid gap-10 md:grid-cols-3">
{comoEmpezar.steps.map((step) => (
<motion.li key={step.n} variants={fadeInUp} className="border-t border-gold/30 pt-6">
<div className="font-serif italic font-black text-5xl md:text-6xl text-gold leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl text-ink">{step.name}</h3>
<p className="mt-3 text-muted leading-relaxed">{step.desc}</p>
</motion.li>
))}
</motion.ol>
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12">
<CTA href={waHref} label={comoEmpezar.cta} variant="light" />
</motion.div>
</Container>
</section>
);
}
```
This mirrors `Mecanismo.tsx` exactly, but in the light-section palette (`bg-base`, accents in `gold` not `gold-bright`, `text-muted` for descriptions, `text-ink` for headings).
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- ComoEmpezar`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<ComoEmpezar />` into `app/page.tsx`**
Add the import with the other section imports:
```tsx
import { ComoEmpezar } from '@/components/sections/ComoEmpezar';
```
Insert `<ComoEmpezar />` between `<Estratega />` and `<Cierre />`:
```tsx
<Estratega />
<ComoEmpezar />
<Cierre />
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/sections/ComoEmpezar.tsx components/sections/__tests__/ComoEmpezar.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: Cómo empezar section (07) — 3-step on-ramp to WhatsApp"
```
---
### Task 3: Footer (slim strip)
**Files:**
- Create: `components/layout/Footer.tsx`
- Create: `components/layout/__tests__/Footer.test.tsx`
- Modify: `lib/content.ts` (append `footer`)
- Modify: `app/page.tsx` (place `<Footer />` after `</main>`)
**Interfaces:**
- Consumes: `Wordmark` (`size`, `onDark`), `Container`, and `footer.tagline`/`footer.legal` from content.
- Produces: `Footer()` at `components/layout/Footer.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `footer` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const footer = {
tagline: 'tu estratega de crecimiento',
legal: '© 2026 creeer',
};
```
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Footer.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Footer } from '../Footer';
describe('<Footer />', () => {
it('renders the tagline and the legal line', () => {
render(<Footer />);
expect(screen.getByText('tu estratega de crecimiento')).toBeInTheDocument();
expect(screen.getByText(/© 2026 creeer/)).toBeInTheDocument();
});
it('does not introduce a second h1', () => {
render(<Footer />);
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- Footer`
Expected: FAIL with "Cannot find module '../Footer'".
- [ ] **Step 4: Write `components/layout/Footer.tsx`**
```tsx
import { Wordmark } from '@/components/primitives/Wordmark';
import { Container } from '@/components/primitives/Container';
import { footer } from '@/lib/content';
export function Footer() {
return (
<footer className="bg-bg-dark text-cream border-t border-cream/10">
<Container className="flex flex-col items-center gap-3 py-10 text-center md:flex-row md:justify-between md:text-left">
<Wordmark size="sm" onDark />
<p className="text-sm text-muted-dark">{footer.tagline}</p>
<p className="text-xs text-muted-dark">{footer.legal}</p>
</Container>
</footer>
);
}
```
Slim dark strip (`bg-bg-dark text-cream`) with a subtle top divider. It deliberately does NOT repeat the contact links — `Cierre` already owns them, and a second dark block of contacts would be redundant.
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- Footer`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<Footer />` into `app/page.tsx`**
Add the import near the `Header` import:
```tsx
import { Footer } from '@/components/layout/Footer';
```
Insert `<Footer />` after the closing `</main>` and before the fragment close:
```tsx
</main>
<Footer />
</>
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/layout/Footer.tsx components/layout/__tests__/Footer.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: slim Footer strip with wordmark, tagline and legal line"
```
---
### Task 4: Sticky-header anchor offset + static build verification
**Files:**
- Modify: `components/sections/Cierre.tsx` (add `scroll-mt` to the `#contacto` anchor target)
**Interfaces:**
- Consumes: the composed page from Tasks 1–3.
- Produces: a verified static export; the `#contacto` anchor target that clears the sticky header.
- [ ] **Step 1: Locate the `#contacto` anchor in `components/sections/Cierre.tsx`**
Run: `grep -n 'id="contacto"' components/sections/Cierre.tsx`
Expected: one match — the element carrying `id="contacto"` (the closing section's outer element).
- [ ] **Step 2: Add `scroll-mt-24` to that element's className**
On the element that has `id="contacto"`, add `scroll-mt-24` to its `className` so the sticky header (~60px) never overlaps the contact anchor when the Header/Hero CTAs jump to it. Example — if the element reads:
```tsx
<section id="contacto" className="bg-bg-dark text-cream">
```
change it to:
```tsx
<section id="contacto" className="bg-bg-dark text-cream scroll-mt-24">
```
(Only add the one utility class; do not change anything else in `Cierre.tsx`.)
- [ ] **Step 3: Run the full suite and typecheck**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass (Cierre's existing tests still green — the class addition changes nothing they assert).
- [ ] **Step 4: Run lint**
Run: `npx next lint`
Expected: "No ESLint warnings or errors".
- [ ] **Step 5: Run the static build and verify the output**
Run: `npm run build`
Expected: build succeeds. Then verify the composed page:
(The exported HTML is minified onto few lines, so count occurrences with `grep -o … | wc -l`, not `grep -c` which counts lines.)
Run: `grep -o '<section' out/index.html | wc -l` → expect `8` (Hero, FraseAncla, ParaQuien, Mecanismo, Disciplinas, Estratega, ComoEmpezar, Cierre — one `<section>` each).
Run: `grep -o '<h1' out/index.html | wc -l` → expect `1` (single h1, still the Hero).
Run: `grep -o '<header' out/index.html | wc -l` → expect `1`.
Run: `grep -o '<footer' out/index.html | wc -l` → expect `1`.
Run: `grep -o 'wa.me' out/index.html | wc -l` → expect at least `2` (Cierre's WhatsApp link + ComoEmpezar's prefilled link).
- [ ] **Step 6: Commit**
```bash
git add components/sections/Cierre.tsx
git commit -m "fix: scroll-margin on #contacto so the sticky header never overlaps it"
```
---
## After this plan
- The header/footer/on-ramp are complete. Remaining follow-ups (own branches): testimonials/social proof, results/case studies, FAQ, real OG image + branded favicon, real portrait, and the dueña's real contact details (the `wa.me/520000000000` placeholder in `lib/content.ts` still applies to both Cierre and the new ComoEmpezar CTA).
- Merge `feat/header-comoempezar-footer` into `main` once reviewed.
@@ -1,531 +0,0 @@
# Coherencia de las ilustraciones + hilo conductor al centro — 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:** Que cada ilustración exprese el mensaje de su sección y la narrativa de marca (Complejidad → Enfoque → Claridad), y que el hilo dorado sea **visible bajando por el centro**, tras el texto.
**Architecture:** El hilo (`HiloConductor`) se mueve del canal izquierdo (donde vive hoy, `right-full`, `hidden 2xl:block`) al **centro** y se muestra en todos los breakpoints; para que no cruce/dañe la lectura, el contenido se levanta por encima de él subiendo `Container` a `relative z-20` (fondos de sección `z-auto` < hilo `z-10` < texto `z-20`). `IlustracionInflexion` se rehace como una curva que se dobla en el punto (sin fork, para dejar de duplicar a `ParaQuien`). Nudo/Bifurcación/Convergencia unifican sus grosores a la escala `1.5 / 2 / 3`. El nudo se reciente en `Dolores` para que el hilo central lo atraviese.
**Tech Stack:** Next.js 14 App Router (`output: 'export'`), TypeScript, Tailwind, Framer Motion, Vitest + jsdom + Testing Library.
## Global Constraints
- **Gestor de paquetes: `pnpm`.** Nunca `npm`.
- **Idioma:** copy, docs y mensajes de commit en **español**; comentarios de código en **inglés**.
- **No se toca el copy** de `lib/content.ts`. Ninguna tarea edita ese archivo.
- **No se reordenan las secciones** de `app/page.tsx`.
- **Colores por token**, nunca hex suelto: `text-dorado`, `text-azul-profundo`, `text-gris-medio`.
- **El wordmark `cre≡er` va siempre en minúsculas** y sólo se renderiza vía `<Wordmark />`.
- **No se agrega escasez** al hero. El test negativo `Hero.test.tsx` (`cupos|cohorte|generación fundadora`) debe seguir pasando.
- **Toda animación** pasa por `usePrefersReducedMotion()` / `useEnVista()`. La rama reducida SIEMPRE renderiza elementos SVG planos (`<path>`, `<line>`), nunca `motion.*` neutralizados (el bug del dash-scaffolding ya documentado).
- **Ilustraciones decorativas:** `aria-hidden="true"` y `focusable="false"` en el `<svg>`.
- **Escala tipográfica de trazo (nueva, fija):** base `2`, énfasis `3`, detalle fino `1.5`. Excepción única y documentada: `SecuenciaClaridad` resuelve en las barras reales del `≡` (grosor de `LINEAS_SIMBOLO`).
- **Verificación por tarea:** `pnpm test` en verde antes de cada commit. Al final del plan, además: `pnpm lint`, `npx tsc --noEmit` y `pnpm build`.
- **Rama de trabajo:** `feat/hilo-conductor-visual` (ya activa).
---
## Estructura de archivos
**Modificar:**
| Archivo | Responsabilidad del cambio |
|---|---|
| `components/primitives/HiloConductor.tsx` | Mover el hilo al centro, visible en todos los breakpoints, `z-10`, trazo más tenue/recto |
| `components/primitives/Container.tsx` | Añadir `relative z-20` para que el texto quede sobre el hilo |
| `components/primitives/IlustracionInflexion.tsx` | Rehacer: curva que se dobla en el punto (azul → dorada), sin fork, lienzo `220×120` |
| `components/primitives/IlustracionNudo.tsx` | Unificar grosores de las hebras a la escala |
| `components/primitives/IlustracionBifurcacion.tsx` | Grosor del tronco `2.5 → 2` |
| `components/primitives/IlustracionConvergencia.tsx` | Grosor de la estrategia `3.5 → 3` |
| `components/sections/Dolores.tsx` | Recentrar el nudo para que el hilo central lo atraviese |
**Crear:**
| Archivo | Responsabilidad |
|---|---|
| `components/primitives/__tests__/Container.test.tsx` | Fija el mecanismo de apilado (el contenido queda sobre el hilo) |
**Tests a actualizar:** `HiloConductor.test.tsx`, `IlustracionInflexion.test.tsx`, `IlustracionNudo.test.tsx`, `IlustracionBifurcacion.test.tsx`, `IlustracionConvergencia.test.tsx`, `Dolores.test.tsx`.
---
### Task 1: El hilo al centro, visible, tras el texto
Hoy el hilo vive en el canal izquierdo (`right-full`) y sólo aparece desde `2xl`. La dueña pidió que esté **en medio**. Se mueve al centro, se muestra en todos los breakpoints, y se pone **tras el texto** subiendo `Container` a `relative z-20` (fondos `z-auto` < hilo `z-10` < texto `z-20`): el hilo se lee como una columna tenue tras la lectura, nunca cruzándola.
**Files:**
- Modify: `components/primitives/HiloConductor.tsx`
- Modify: `components/primitives/Container.tsx`
- Modify: `components/primitives/__tests__/HiloConductor.test.tsx`
- Create: `components/primitives/__tests__/Container.test.tsx`
**Interfaces:**
- Consumes: `usePrefersReducedMotion`; `useScroll`, `useSpring`, `motion` de `framer-motion`.
- Produces: `HiloConductor` centrado y visible (mismo nombre de export, mismos consumidores: `app/page.tsx`). `Container` con `relative z-20` (mismo nombre de export).
- [ ] **Step 1: Actualizar los tests del hilo (deben fallar) y crear el test de Container**
En `components/primitives/__tests__/HiloConductor.test.tsx`, **sustituir** el test `sits in the margin band beside the column, never over it` por:
```tsx
it('runs down the centre of the column, not in a side gutter', () => {
const { container } = render(<HiloConductor />);
const cls = container.querySelector('svg')?.getAttribute('class') ?? '';
// `left-1/2 -translate-x-1/2` anchors the thread to the column's centre;
// the old `right-full` banished it to the left gutter, which is why the
// thread read as off to the side instead of as the page's spine.
expect(cls).toContain('left-1/2');
expect(cls).toContain('-translate-x-1/2');
expect(cls).not.toContain('right-full');
});
```
Y **sustituir** el test `is hidden below the breakpoint that guarantees a safe margin` por:
```tsx
it('renders on every breakpoint, not only on very wide screens', () => {
const { container } = render(<HiloConductor />);
const wrapperClass = container.firstElementChild?.className ?? '';
// The thread is the brand's through-line; it belongs in the middle on a
// phone too. Legibility is handled by layering it behind the text, not by
// hiding it below a breakpoint.
expect(wrapperClass).not.toContain('hidden');
expect(wrapperClass).not.toContain('2xl:block');
});
```
Los demás tests del archivo (decorativo, un solo path, `absolute` no `fixed`, `pointer-events-none`, nivel de apilado `z-\d+`, rama reducida con `<path>` plano) **se quedan sin cambios**.
Crear `components/primitives/__tests__/Container.test.tsx`:
```tsx
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Container } from '../Container';
describe('<Container />', () => {
it('lifts its content above the conductor thread', () => {
const { container } = render(<Container>contenido</Container>);
const cls = container.firstElementChild?.className ?? '';
// The thread sits at z-10; the section backgrounds are z-auto. Lifting the
// content column to z-20 is what puts the text *above* a centred thread,
// so the spine reads behind the reading instead of across it.
expect(cls).toContain('relative');
expect(cls).toMatch(/\bz-20\b/);
});
});
```
- [ ] **Step 2: Correr — deben fallar**
Run: `pnpm test --run components/primitives/__tests__/HiloConductor.test.tsx components/primitives/__tests__/Container.test.tsx`
Expected: FAIL — el svg del hilo aún tiene `right-full`/`hidden`, y Container no tiene `z-20`.
- [ ] **Step 3: Reescribir `HiloConductor.tsx`**
Contenido completo del archivo:
```tsx
'use client';
import { motion, useScroll, useSpring } from 'framer-motion';
import { usePrefersReducedMotion } from '@/lib/motion';
// The gold spine of the page: one continuous line down the vertical centre,
// drawing itself as the reader scrolls. It is what turns ten sections into a
// single route — *claridad · dirección · crecimiento* made visible, per the
// Manual de Identidad Visual.
//
// Centred, never in a side gutter: the brand's through-line belongs in the
// middle, on every breakpoint. It sits ABOVE the section backgrounds (z-10)
// but BELOW the text, which `Container` lifts to z-20 — so the thread reads
// as a faint watermark spine behind the reading, never crossing it.
//
// Page-level useScroll() with no target ref on purpose: no element measuring,
// no "container has a static position" warnings, one listener for the page.
const TRAZO =
'M50 0 C 42 200, 58 400, 50 600 C 42 800, 58 1000, 50 1200';
export function HiloConductor({ className = '' }: { className?: string }) {
const reduced = usePrefersReducedMotion();
const { scrollYProgress } = useScroll();
const avance = useSpring(scrollYProgress, { stiffness: 90, damping: 30, restDelta: 0.001 });
const wrapper = `pointer-events-none absolute inset-0 z-10 ${className}`;
// Plain <path> for reduced motion: motion.path applies dash scaffolding even
// without initial/animate and would leave the thread invisible.
const trazo = reduced ? (
<path d={TRAZO} fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" />
) : (
<motion.path
d={TRAZO}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
style={{ pathLength: avance }}
/>
);
return (
<div className={wrapper}>
{/* Mirrors Container's own centring (`mx-auto max-w-content`) so the
thread is measured from the real column edge and stays centred in it. */}
<div className="relative mx-auto h-full w-full max-w-content">
<svg
className="absolute inset-y-0 left-1/2 h-full w-14 -translate-x-1/2 text-dorado/25"
viewBox="0 0 100 1200"
preserveAspectRatio="none"
aria-hidden="true"
focusable="false"
>
{trazo}
</svg>
</div>
</div>
);
}
```
- [ ] **Step 4: Añadir `relative z-20` a `Container.tsx`**
Sustituir la línea de retorno por:
```tsx
return <Tag className={`relative z-20 mx-auto w-full max-w-content px-6 md:px-12 ${className}`}>{children}</Tag>;
```
- [ ] **Step 5: Correr — debe pasar**
Run: `pnpm test --run components/primitives/__tests__/HiloConductor.test.tsx components/primitives/__tests__/Container.test.tsx`
Expected: PASS (8 tests del hilo + 1 de Container).
- [ ] **Step 6: Correr la suite completa y commitear**
Run: `pnpm test`
Expected: PASS (los cambios de apilado no rompen tests de sección, que verifican copy/contrato, no clases de Container).
```bash
git add components/primitives/HiloConductor.tsx components/primitives/Container.tsx components/primitives/__tests__/HiloConductor.test.tsx components/primitives/__tests__/Container.test.tsx
git commit -m "feat(hilo): el conductor baja visible por el centro, tras el texto"
```
---
### Task 2: `IlustracionInflexion` — la curva que se dobla, sin fork
Hoy dibuja tres caminos divergentes (crecimiento / deriva / estrategia): el **mismo fork** que `IlustracionBifurcacion`. Se rehace como **una sola línea que se dobla** en el punto de inflexión — azul (el presente) antes del punto, dorada (la claridad) después — para que encarene el concepto de "punto de inflexión" y deje de duplicar a `ParaQuien`. Lienzo `220×120` (compartido con Bifurcación y Convergencia).
**Files:**
- Modify: `components/primitives/IlustracionInflexion.tsx`
- Modify: `components/primitives/__tests__/IlustracionInflexion.test.tsx`
**Interfaces:**
- Consumes: `useEnVista` de `lib/motion`.
- Produces: `IlustracionInflexion({ className?: string })` (mismo nombre; la consume `PuntosInflexion`).
- [ ] **Step 1: Reescribir el test (debe fallar)**
Contenido completo de `components/primitives/__tests__/IlustracionInflexion.test.tsx`:
```tsx
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { IlustracionInflexion } from '../IlustracionInflexion';
describe('<IlustracionInflexion />', () => {
it('is decorative: hidden from assistive tech', () => {
const { container } = render(<IlustracionInflexion />);
const svg = container.querySelector('svg');
expect(svg).toHaveAttribute('aria-hidden', 'true');
expect(svg).toHaveAttribute('focusable', 'false');
});
it('shares the unified canvas with the other scene illustrations', () => {
const { container } = render(<IlustracionInflexion />);
expect(container.querySelector('svg')).toHaveAttribute('viewBox', '0 0 220 120');
});
it('is a single bend at the inflection point, not a fork', () => {
const { container } = render(<IlustracionInflexion />);
// The previous version drew three diverging paths (growth / drift /
// strategy) — the same fork as ParaQuien. The rework is one continuous
// line that bends at the point: two segments, pre- and post-inflection.
expect(container.querySelectorAll('path')).toHaveLength(2);
});
it('marks the inflection point itself', () => {
const { container } = render(<IlustracionInflexion />);
expect(container.querySelectorAll('circle').length).toBeGreaterThanOrEqual(1);
});
it('bends from the present (azul) into clarity (dorado)', () => {
const { container } = render(<IlustracionInflexion />);
const clases = Array.from(container.querySelectorAll('path')).map((p) => p.getAttribute('class') ?? '');
expect(clases.some((c) => c.includes('text-azul-profundo'))).toBe(true);
expect(clases.some((c) => c.includes('text-dorado'))).toBe(true);
});
});
```
- [ ] **Step 2: Correr — debe fallar**
Run: `pnpm test --run components/primitives/__tests__/IlustracionInflexion.test.tsx`
Expected: FAIL — hoy hay 3 paths (no 2) y el viewBox es `0 0 320 180`.
- [ ] **Step 3: Reescribir `IlustracionInflexion.tsx`**
Contenido completo del archivo:
```tsx
'use client';
import { motion } from 'framer-motion';
import { useEnVista } from '@/lib/motion';
// The section's own subject, drawn: the inflection point itself. One line
// rises gently (the present, azul), bends at the marked point, and from there
// climbs steeply in dorado — clarity accelerating the climb. No fork, no
// "deriva": that bifurcation belongs to ParaQuien, not here.
const ANTES = 'M10 110 C 55 104, 85 88, 110 72';
const DESPUES = 'M110 72 C 145 60, 178 24, 210 12';
export function IlustracionInflexion({ className = '' }: { className?: string }) {
const { ref, enVista, reduced } = useEnVista();
const punto = (
<>
<circle cx={110} cy={72} r={6} fill="currentColor" className="text-dorado" />
<circle cx={110} cy={72} r={10} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={1} strokeOpacity={0.4} />
</>
);
// Plain <path> under reduced motion — motion.path applies its own dash
// scaffolding and would leave the drawing invisible.
if (reduced) {
return (
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
<path d={ANTES} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2} strokeLinecap="round" />
<path d={DESPUES} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
{punto}
</svg>
);
}
const dibuja = (delay: number) => ({
initial: { pathLength: 0 },
animate: enVista ? { pathLength: 1 } : { pathLength: 0 },
transition: { duration: 1.1, delay, ease: [0.22, 1, 0.36, 1] as const },
});
return (
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
<motion.path d={ANTES} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2} strokeLinecap="round" {...dibuja(0)} />
<motion.path d={DESPUES} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" {...dibuja(0.5)} />
{punto}
</svg>
);
}
```
- [ ] **Step 4: Correr — debe pasar**
Run: `pnpm test --run components/primitives/__tests__/IlustracionInflexion.test.tsx`
Expected: PASS (5 tests).
- [ ] **Step 5: Correr la suite completa y commitear**
Run: `pnpm test`
Expected: PASS.
```bash
git add components/primitives/IlustracionInflexion.tsx components/primitives/__tests__/IlustracionInflexion.test.tsx
git commit -m "feat(inflexion): la curva que se dobla en el punto, sin fork"
```
---
### Task 3: Unificar grosores a la escala `1.5 / 2 / 3`
`IlustracionNudo` (`2.5`), `IlustracionBifurcacion` (tronco `2.5`) e `IlustracionConvergencia` (estrategia `3.5`) quedan fuera de la escala. Un test de contrato exige que todo trazo pertenezca a `{1.5, 2, 3}` y impulsa los cambios.
**Files:**
- Modify: `components/primitives/IlustracionNudo.tsx`
- Modify: `components/primitives/IlustracionBifurcacion.tsx`
- Modify: `components/primitives/IlustracionConvergencia.tsx`
- Modify: `components/primitives/__tests__/IlustracionNudo.test.tsx`
- Modify: `components/primitives/__tests__/IlustracionBifurcacion.test.tsx`
- Modify: `components/primitives/__tests__/IlustracionConvergencia.test.tsx`
**Interfaces:**
- Sin cambios de interfaz pública. Sólo grosores.
- [ ] **Step 1: Añadir el test de escala (debe fallar)**
Añadir a `components/primitives/__tests__/IlustracionNudo.test.tsx`, dentro del `describe`:
```tsx
it('keeps every stroke within the shared line-art scale', () => {
const { container } = render(<IlustracionNudo />);
const anchos = Array.from(container.querySelectorAll('path')).map((p) => p.getAttribute('stroke-width'));
expect(anchos.length).toBeGreaterThan(0);
anchos.forEach((a) => expect(['1.5', '2', '3']).toContain(a));
});
```
Añadir a `components/primitives/__tests__/IlustracionBifurcacion.test.tsx`, dentro del `describe`:
```tsx
it('keeps every stroke within the shared line-art scale', () => {
const { container } = render(<IlustracionBifurcacion />);
const anchos = Array.from(container.querySelectorAll('path')).map((p) => p.getAttribute('stroke-width'));
expect(anchos.length).toBeGreaterThan(0);
anchos.forEach((a) => expect(['1.5', '2', '3']).toContain(a));
});
```
Añadir a `components/primitives/__tests__/IlustracionConvergencia.test.tsx`, dentro del `describe`:
```tsx
it('keeps every stroke within the shared line-art scale', () => {
const { container } = render(<IlustracionConvergencia />);
const anchos = Array.from(container.querySelectorAll('path')).map((p) => p.getAttribute('stroke-width'));
expect(anchos.length).toBeGreaterThan(0);
anchos.forEach((a) => expect(['1.5', '2', '3']).toContain(a));
});
```
- [ ] **Step 2: Correr — deben fallar**
Run: `pnpm test --run components/primitives/__tests__/IlustracionNudo.test.tsx components/primitives/__tests__/IlustracionBifurcacion.test.tsx components/primitives/__tests__/IlustracionConvergencia.test.tsx`
Expected: FAIL — `2.5` (Nudo, Bifurcación) y `3.5` (Convergencia) no están en `{1.5, 2, 3}`.
- [ ] **Step 3: Unificar los grosores**
En `components/primitives/IlustracionNudo.tsx`, sustituir el array `HEBRAS` por grosores de la escala (la primera hebra pasa de `2.5` a `3` —énfasis— para conservar la jerarquía visual del nudo):
```ts
const HEBRAS = [
{ d: 'M30 90 C 60 20, 130 20, 140 70 C 148 112, 78 118, 70 78 C 63 44, 120 38, 150 66', ancho: 3 },
{ d: 'M20 60 C 70 110, 120 110, 160 60', ancho: 2 },
{ d: 'M40 110 C 80 40, 110 130, 155 95', ancho: 2 },
{ d: 'M25 40 C 90 70, 100 100, 165 40', ancho: 1.5 },
];
```
En `components/primitives/IlustracionBifurcacion.tsx`, cambiar el `strokeWidth` del tronco de `2.5` a `2` **en las dos ramas** (reduced y animada):
```tsx
<path d={TRONCO} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2} strokeLinecap="round" />
```
```tsx
<motion.path d={TRONCO} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2} strokeLinecap="round" {...dibuja(0)} />
```
En `components/primitives/IlustracionConvergencia.tsx`, cambiar el `strokeWidth` de la estrategia de `3.5` a `3` **en las dos ramas**:
```tsx
<path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
```
```tsx
<motion.path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" {...dibuja(0.7)} />
```
- [ ] **Step 4: Correr — deben pasar**
Run: `pnpm test --run components/primitives/__tests__/IlustracionNudo.test.tsx components/primitives/__tests__/IlustracionBifurcacion.test.tsx components/primitives/__tests__/IlustracionConvergencia.test.tsx`
Expected: PASS.
- [ ] **Step 5: Correr la suite completa y commitear**
Run: `pnpm test`
Expected: PASS.
```bash
git add components/primitives/IlustracionNudo.tsx components/primitives/IlustracionBifurcacion.tsx components/primitives/IlustracionConvergencia.tsx components/primitives/__tests__/IlustracionNudo.test.tsx components/primitives/__tests__/IlustracionBifurcacion.test.tsx components/primitives/__tests__/IlustracionConvergencia.test.tsx
git commit -m "refactor(ilustraciones): grosores unificados a la escala 1.5/2/3"
```
---
### Task 4: El nudo, al centro para que el hilo lo atraviese (`Dolores`)
El nudo hoy vive arriba a la derecha (`-top-6 right-4`); el hilo central no lo toca. Se recentrea en `Dolores` para que el hilo (Task 1) lo atraviese —el dolor, cosido por el hilo que es lo que lo va a desatar—, conectando el nudo con el remate *"se resuelve con una estrategia clara"*.
**Files:**
- Modify: `components/sections/Dolores.tsx`
- Modify: `components/sections/__tests__/Dolores.test.tsx`
**Interfaces:**
- Consumes: `IlustracionNudo` (Task 3 ya dejó sus grosores listos).
- Sin cambios de interfaz pública.
- [ ] **Step 1: Añadir el test que falla**
Añadir a `components/sections/__tests__/Dolores.test.tsx`, dentro del `describe`:
```tsx
it('centres the knot so the conductor thread runs through it', () => {
const { container } = render(<Dolores />);
// The knot is the centred decorative svg of the section; the conductor
// thread (Task 1) runs down the centre, so only a centred knot gets
// traversed by it — stitching the pain to the resolution below.
const centrados = Array.from(container.querySelectorAll('svg')).filter((s) => {
const cls = s.getAttribute('class') ?? '';
return cls.includes('left-1/2') && cls.includes('-translate-x-1/2');
});
expect(centrados.length).toBeGreaterThanOrEqual(1);
});
```
- [ ] **Step 2: Correr — debe fallar**
Run: `pnpm test --run components/sections/__tests__/Dolores.test.tsx`
Expected: FAIL — el nudo está en `right-4`, no centrado.
- [ ] **Step 3: Recentrar el nudo en `Dolores`**
En `components/sections/Dolores.tsx`, sustituir la línea del `<IlustracionNudo ...>` por:
```tsx
<IlustracionNudo className="pointer-events-none select-none absolute top-0 left-1/2 -translate-x-1/2 w-64 md:w-[28rem] text-azul-profundo/[0.08]" />
```
- [ ] **Step 4: Correr — debe pasar**
Run: `pnpm test --run components/sections/__tests__/Dolores.test.tsx`
Expected: PASS.
- [ ] **Step 5: Correr la suite completa y commitear**
Run: `pnpm test`
Expected: PASS.
```bash
git add components/sections/Dolores.tsx components/sections/__tests__/Dolores.test.tsx
git commit -m "feat(dolores): el nudo al centro para que el hilo lo atraviese"
```
---
## Verificación final
- [ ] `pnpm test` — todos los tests en verde
- [ ] `pnpm lint` — sin warnings
- [ ] `npx tsc --noEmit` — sin errores
- [ ] `pnpm build` — el export estático compila
- [ ] `pnpm dev` y revisar a mano:
- (a) el hilo dorado baja **visible por el centro** en móvil y desktop, tras el texto (no lo cruza);
- (b) `IlustracionInflexion` es una curva que se dobla, **distinta** del fork de `ParaQuien`;
- (c) el nudo de `Dolores` está al centro y el hilo lo atraviesa;
- (d) los trazos de Nudo/Bifurcación/Convergencia se ven parejos (escala `1.5/2/3`).
- [ ] Con `prefers-reduced-motion: reduce`: ninguna ilustración invisible, el hilo se ve estático, la página se lee completa.
## Notas para quien ejecute
- **El apilado es la decisión clave del Task 1.** Si en `pnpm dev` el hilo, estando centrado, resta legibilidad a algún título (no debería: el texto queda a `z-20`, sobre el hilo a `z-10`), **no** vuelvas a esconderlo en el canal: baja su opacidad (`dorada/20`) o reduce `w-14` antes de tocar el posicionamiento. "Hilo visible al centro" es un pedido explícito de la dueña.
- **El lienzo del nudo se mantiene en `0 0 190 140`.** Es un ornamento de fondo que se renderiza a otra escala; forzarlo a `220×120` (como dice el spec §4) arriesga el dibujo sin aportar coherencia. La unificación de grosores (Task 3) ya cumple la intención del spec.
- **La "deriva gris punteada" de `Inflexion` (spec §4/§7) desaparece por diseño.** El rework "sin fork" (Task 2) no tiene ruta de deriva —encarnar el punto de inflexión como una curva que se dobla, no como una bifurcación—, así que el bug del punteado no tiene dónde aplicar y se resuelve por eliminación. La única ruta punteada que queda en la página (la del "no" en `ParaQuien`) ya se revela por opacidad desde el plan anterior. Por eso la verificación final no lista "deriva punteada de Inflexion".
- **No reabrir** las decisiones bloqueadas del `AGENTS.md` (paleta, wordmark, voz, "simple, directo, limpio").
File diff suppressed because it is too large. Load diff
@@ -1,467 +0,0 @@
# 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.