docs(plan): coherencia de ilustraciones + hilo al centro, 4 tareas
This commit is contained in:
1 parent
35c2700b4f
commit
8d87ef8757
1 file changed
+531
@@ -0,0 +1,531 @@
|
||||
# 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").
|
||||
Reference in new issue
Block a user