# creeer Landing Page 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:** Build the one-page `creeer` landing site (7 sections) as a statically-exported Next.js 14 App Router site, matching the approved design spec exactly. **Architecture:** A single `app/page.tsx` assembles seven independent section components (`components/sections/*.tsx`), each wrapped in a shared `MotionSection` scroll-reveal wrapper that respects `prefers-reduced-motion`. A shared `Wordmark` component renders the brand mark everywhere it appears. No backend, no client-side state beyond motion — content is static JSX. **Tech Stack:** Next.js 14 (App Router) + TypeScript + Tailwind CSS 3 + Framer Motion 11, `output: export`. Tests: Vitest + React Testing Library + jsdom. ## Global Constraints - Idioma: español, `lang="es"` en ``. - Wordmark SIEMPRE minúsculas: `creeer` — nunca `CREEER` ni `Creeer`. - Paleta exacta (no aproximar): `bg-base #FBFAF7`, `bg-pure #FFFFFF`, `bg-dark #0A1F44`, `text-primary #0A1F44`, `text-on-dark #F5EBD0`, `accent-gold #B8901f`, `accent-gold-bright #D4AF37`, `text-muted #5a5168`, `text-muted-dark #9aa6c2`. NO amarillo eléctrico tipo Hormozi. - Sin backend, sin formulario — contacto solo por enlaces directos (WhatsApp/email/LinkedIn). - `next.config.js` debe tener `output: 'export'` **y** `images.unoptimized: true` (la optimización de imágenes de Next no funciona en export estático). - Container max-width: `1152px`. - Voz: segunda persona ("tú"/"tu negocio"), sin jerga corporativa. - Toda animación Framer Motion debe desactivarse con `useReducedMotion()`. - Contraste WCAG 2.2 AA: `accent-gold` (#B8901f) solo para texto grande/decorativo, nunca body text sobre fondo claro. - Un único `

` en toda la página (vive en el Hero). - Datos de contacto reales (WhatsApp/email/LinkedIn), foto de la dueña y elección final HV-1 vs V-a están **pendientes de la dueña** (ver spec §10). Este plan usa placeholders explícitos marcados como tales — no bloquean el build. --- ### Task 1: Next.js scaffold, Tailwind tokens, fonts, root layout **Files:** - Create: `package.json` - Create: `tsconfig.json` - Create: `next.config.js` - Create: `postcss.config.js` - Create: `tailwind.config.ts` - Create: `.eslintrc.json` - Create: `vitest.config.ts` - Create: `vitest.setup.ts` - Create: `test/mocks/next-font-google.ts` - Create: `app/layout.tsx` - Create: `app/layout.test.tsx` - Create: `app/globals.css` - Create: `app/page.tsx` (placeholder, replaced in Task 11) - Create: `components/MotionProvider.tsx` **Interfaces:** - Produces: Tailwind color tokens (`bg-base`, `bg-pure`, `bg-dark`, `text-primary`, `text-on-dark`, `accent-gold`, `accent-gold-bright`, `text-muted`, `text-muted-dark`), `font-serif` (Georgia), `font-sans` (Inter via `--font-inter`), `max-w-container` (1152px). All later tasks consume these. - Produces: working `npm test` (Vitest) and `npm run build` (static export) pipelines. - Produces: `MotionProvider({ children: ReactNode })` at `components/MotionProvider.tsx`, wrapping the app in `` per spec §8.3 (bundle-size). Consumed by `app/layout.tsx` here, and required context for Tasks 3 and 4, which must use framer-motion's `m.*` components (not `motion.*`) so the `strict` flag catches accidental full-bundle imports. - [ ] **Step 1: Write `package.json`** ```json { "name": "creeer", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "test": "vitest run", "test:watch": "vitest" }, "dependencies": { "framer-motion": "^11.3.19", "next": "^14.2.5", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@types/node": "^20.14.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", "jsdom": "^24.1.1", "postcss": "^8.4.40", "tailwindcss": "^3.4.7", "typescript": "^5.5.4", "vitest": "^2.0.5" } } ``` - [ ] **Step 2: Write `tsconfig.json`** ```json { "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` - [ ] **Step 3: Write `next.config.js`** ```js /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', images: { unoptimized: true, }, }; module.exports = nextConfig; ``` - [ ] **Step 4: Write `postcss.config.js`** ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` - [ ] **Step 5: Write `tailwind.config.ts`** ```ts import type { Config } from 'tailwindcss'; const config: Config = { content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'], theme: { extend: { colors: { 'bg-base': '#FBFAF7', 'bg-pure': '#FFFFFF', 'bg-dark': '#0A1F44', 'text-primary': '#0A1F44', 'text-on-dark': '#F5EBD0', 'accent-gold': '#B8901f', 'accent-gold-bright': '#D4AF37', 'text-muted': '#5a5168', 'text-muted-dark': '#9aa6c2', }, fontFamily: { serif: ['Georgia', 'serif'], sans: ['var(--font-inter)', 'system-ui', 'sans-serif'], }, maxWidth: { container: '1152px', }, }, }, plugins: [], }; export default config; ``` - [ ] **Step 6: Write `.eslintrc.json`** ```json { "extends": "next/core-web-vitals" } ``` - [ ] **Step 7: Write `test/mocks/next-font-google.ts`** Vitest doesn't run through Next's compiler, so `next/font/google` can't resolve its font manifest. Alias it to this mock so components importing it stay testable. ```ts export function Inter() { return { className: 'font-inter-mock', variable: '--font-inter', }; } ``` - [ ] **Step 8: Write `vitest.config.ts`** ```ts import path from 'path'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./vitest.setup.ts'], globals: true, }, resolve: { alias: { '@': path.resolve(__dirname, '.'), 'next/font/google': path.resolve( __dirname, 'test/mocks/next-font-google.ts' ), }, }, }); ``` - [ ] **Step 9: Write `vitest.setup.ts`** ```ts import '@testing-library/jest-dom/vitest'; class MockIntersectionObserver { observe() {} unobserve() {} disconnect() {} } // @ts-expect-error jsdom doesn't implement IntersectionObserver global.IntersectionObserver = MockIntersectionObserver; Object.defineProperty(window, 'matchMedia', { writable: true, value: (query: string) => ({ matches: false, media: query, onchange: null, addListener: () => {}, removeListener: () => {}, addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => false, }), }); ``` - [ ] **Step 10: Write `components/MotionProvider.tsx`** Wraps the app in Framer Motion's `LazyMotion` so every animated component can import the lightweight `m.*` primitives instead of the full `motion.*` bundle (spec §8.3: "usar `LazyMotion` + `domAnimation` para reducir bundle"). `strict` makes framer-motion throw if a `motion.*` component is ever used underneath by mistake. ```tsx 'use client'; import type { ReactNode } from 'react'; import { LazyMotion, domAnimation } from 'framer-motion'; export function MotionProvider({ children }: { children: ReactNode }) { return ( {children} ); } ``` - [ ] **Step 11: Write `app/globals.css`** ```css @tailwind base; @tailwind components; @tailwind utilities; :root { color-scheme: light; } *:focus-visible { outline: 3px solid #b8901f; outline-offset: 2px; } ``` - [ ] **Step 12: Write `app/layout.tsx`** ```tsx import type { Metadata } from 'next'; import type { ReactNode } from 'react'; import { Inter } from 'next/font/google'; import { MotionProvider } from '@/components/MotionProvider'; import './globals.css'; const inter = Inter({ subsets: ['latin'], weight: ['400', '600', '700', '900'], variable: '--font-inter', display: 'swap', }); export const metadata: Metadata = { title: 'creeer · tu estratega de crecimiento', description: 'Claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión. Define tu siguiente movimiento.', }; export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ); } ``` - [ ] **Step 13: Write the failing test `app/layout.test.tsx`** ```tsx import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import RootLayout, { metadata } from './layout'; describe('RootLayout', () => { it('sets the approved SEO title and description', () => { expect(metadata.title).toBe('creeer · tu estratega de crecimiento'); expect(metadata.description).toContain('Claridad estratégica'); }); it('declares Spanish as the document language', () => { const { container } = render(

contenido

); expect(container.querySelector('html')).toHaveAttribute('lang', 'es'); }); }); ``` - [ ] **Step 14: Write the placeholder `app/page.tsx`** Temporary stub — replaced entirely in Task 11 once all sections exist. ```tsx export default function Home() { return (

creeer

); } ``` - [ ] **Step 15: Install dependencies** Run: `npm install` Expected: installs without errors, creates `node_modules/` and `package-lock.json`. - [ ] **Step 16: Run the test to verify it passes** Run: `npm test -- app/layout.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 17: Verify the static export build works** Run: `npm run build` Expected: build succeeds, output includes a line for route `/`, and `out/index.html` exists afterward. - [ ] **Step 18: Commit** ```bash git add package.json package-lock.json tsconfig.json next.config.js postcss.config.js tailwind.config.ts .eslintrc.json vitest.config.ts vitest.setup.ts test/ app/ components/MotionProvider.tsx git commit -m "chore: scaffold Next.js app with Tailwind tokens, fonts, and test runner" ``` --- ### Task 2: Wordmark component **Files:** - Create: `components/Wordmark.tsx` - Create: `components/Wordmark.test.tsx` **Interfaces:** - Consumes: Tailwind tokens from Task 1 (`text-primary`, `text-on-dark`, `accent-gold`, `font-serif`). - Produces: `Wordmark({ size?: 'base' | 'lg', onDark?: boolean })` at `components/Wordmark.tsx`. Consumed by Task 4 (Hero). - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import { Wordmark } from './Wordmark'; describe('Wordmark', () => { it('renders the middle "e" in gold with heavier weight than the rest', () => { const { container } = render(); const goldE = container.querySelector('.font-black.text-accent-gold'); expect(goldE).not.toBeNull(); expect(goldE?.textContent).toBe('e'); }); it('renders the trailing period in gold', () => { const { container } = render(); const spans = container.querySelectorAll('span.text-accent-gold'); const period = Array.from(spans).find((el) => el.textContent === '.'); expect(period).not.toBeUndefined(); }); it('never renders the wordmark in uppercase', () => { const { container } = render(); expect(container.textContent?.toLowerCase()).toBe(container.textContent); }); it('switches to the on-dark ink color when onDark is true', () => { const { container } = render(); expect(container.firstElementChild?.className).toContain( 'text-text-on-dark' ); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/Wordmark.test.tsx` Expected: FAIL with "Cannot find module './Wordmark'". - [ ] **Step 3: Write `components/Wordmark.tsx`** ```tsx export function Wordmark({ size = 'base', onDark = false, }: { size?: 'base' | 'lg'; onDark?: boolean; }) { const inkColor = onDark ? 'text-text-on-dark' : 'text-text-primary'; const sizeClass = size === 'lg' ? 'text-4xl md:text-5xl' : 'text-2xl'; return ( cre e er . ); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/Wordmark.test.tsx` Expected: PASS, 4 tests passing. - [ ] **Step 5: Commit** ```bash git add components/Wordmark.tsx components/Wordmark.test.tsx git commit -m "feat: add Wordmark component with gold middle-e brand rule" ``` --- ### Task 3: MotionSection scroll-reveal wrapper **Files:** - Create: `components/MotionSection.tsx` - Create: `components/MotionSection.test.tsx` **Interfaces:** - Consumes: `framer-motion`'s `m` and `useReducedMotion` (the `m.*` lightweight primitives, not `motion.*` — see `MotionProvider` in Task 1, which enforces this via `strict`). - Produces: `MotionSection({ children: ReactNode, className?: string })` at `components/MotionSection.tsx`, rendering a `
`. Consumed by Tasks 5–10 (all sections except Hero, which has its own staggered entrance). - [ ] **Step 1: Write the failing tests** ```tsx import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render } from '@testing-library/react'; import { LazyMotion, domAnimation } from 'framer-motion'; import { MotionSection } from './MotionSection'; function mockReducedMotion(matches: boolean) { vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({ matches, media: query, onchange: null, addListener: () => {}, removeListener: () => {}, addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => false, })); } function renderWithMotion(ui: React.ReactElement) { return render( {ui} ); } afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe('MotionSection', () => { it('renders a plain section with no inline animation styles when reduced motion is preferred', () => { mockReducedMotion(true); const { container } = renderWithMotion(

contenido

); const section = container.querySelector('section'); expect(section).not.toBeNull(); expect(section?.getAttribute('style')).toBeNull(); }); it('applies an initial opacity of 0 before entering the viewport when motion is allowed', () => { mockReducedMotion(false); const { container } = renderWithMotion(

contenido

); expect(container.querySelector('section')?.getAttribute('style')).toContain( 'opacity: 0' ); }); it('always renders its children regardless of motion preference', () => { mockReducedMotion(true); const { getByText } = renderWithMotion(

contenido visible

); expect(getByText('contenido visible')).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/MotionSection.test.tsx` Expected: FAIL with "Cannot find module './MotionSection'". - [ ] **Step 3: Write `components/MotionSection.tsx`** ```tsx 'use client'; import type { ReactNode } from 'react'; import { m, useReducedMotion } from 'framer-motion'; const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1]; export function MotionSection({ children, className, }: { children: ReactNode; className?: string; }) { const shouldReduceMotion = useReducedMotion(); if (shouldReduceMotion) { return
{children}
; } return ( {children} ); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/MotionSection.test.tsx` Expected: PASS, 3 tests passing. Note: this component renders `m.section` without a `LazyMotion` ancestor in production only through `app/layout.tsx`'s `MotionProvider` (Task 1) — outside of a real render tree, `m` components need that provider to have their animation features loaded, which is why the test above wraps every render in its own ``. - [ ] **Step 5: Commit** ```bash git add components/MotionSection.tsx components/MotionSection.test.tsx git commit -m "feat: add MotionSection scroll-reveal wrapper with reduced-motion support" ``` --- ### Task 4: Hero section (01) **Files:** - Create: `components/sections/Hero.tsx` - Create: `components/sections/Hero.test.tsx` **Interfaces:** - Consumes: `Wordmark` (Task 2). - Produces: `Hero({ variant?: 'hormozi' | 'editorial' })` at `components/sections/Hero.tsx`, default `'editorial'`. Renders the page's single `

` and the CTA linking to `#contacto`. Consumed by Task 11. **Note on the default:** the spec leaves HV-1 vs V-a as the dueña's decision (§4, §10). `variant` defaults to `'editorial'` (V-a) because it's the closer match to the master tone "seguridad + cercanía" — swap the default or the call site in Task 11 the moment she decides. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Hero } from './Hero'; describe('Hero', () => { it('renders the editorial variant in Georgia italic by default', () => { render(); const headline = screen.getByRole('heading', { level: 1 }); expect(headline.className).toContain('font-serif'); expect(headline.className).toContain('italic'); }); it('renders the hormozi variant in uppercase sans when requested', () => { render(); const headline = screen.getByRole('heading', { level: 1 }); expect(headline.className).toContain('uppercase'); expect(headline.className).not.toContain('font-serif'); }); it('renders exactly one h1 with the approved headline copy', () => { render(); const headings = screen.getAllByRole('heading', { level: 1 }); expect(headings).toHaveLength(1); expect(headings[0].textContent).toContain('más esfuerzo'); expect(headings[0].textContent).toContain('más claridad'); }); it('renders the CTA linking to the contact section', () => { render(); const cta = screen.getByRole('link', { name: /Define tu siguiente movimiento/, }); expect(cta).toHaveAttribute('href', '#contacto'); }); it('never shows scarcity language in the hero', () => { render(); const text = screen.getByRole('heading', { level: 1 }).textContent ?? ''; expect(text).not.toMatch(/cupo|cohorte|generaci[oó]n fundadora/i); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/Hero.test.tsx` Expected: FAIL with "Cannot find module './Hero'". - [ ] **Step 3: Write `components/sections/Hero.tsx`** ```tsx 'use client'; import { m, useReducedMotion } from 'framer-motion'; import { Wordmark } from '@/components/Wordmark'; const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1]; const STAGGER_DELAYS = [0, 0.1, 0.2, 0.3]; type HeroVariant = 'hormozi' | 'editorial'; function HeroHeadline({ variant }: { variant: HeroVariant }) { if (variant === 'hormozi') { return (

Tu negocio no necesita más esfuerzo.
Necesita{' '} más claridad .

); } return (

Tu negocio no necesita más esfuerzo.
Necesita{' '} más claridad .

); } export function Hero({ variant = 'editorial' }: { variant?: HeroVariant }) { const shouldReduceMotion = useReducedMotion(); const item = (delay: number) => shouldReduceMotion ? {} : { initial: { opacity: 0, y: 16 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.5, delay, ease: EASE_OUT_EXPO }, }; return (
— tu estratega de crecimiento — Define tu siguiente movimiento →
); } ``` Note: Hero's tests only assert `className`/text/`href`, none of which depend on the animation feature bundle being loaded, so `Hero.test.tsx` renders the component directly with no `LazyMotion` wrapper needed (unlike `MotionSection.test.tsx`). - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/Hero.test.tsx` Expected: PASS, 5 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/Hero.tsx components/sections/Hero.test.tsx git commit -m "feat: add Hero section with hormozi/editorial variant switch" ``` --- ### Task 5: AnchorQuote section (02) **Files:** - Create: `components/sections/AnchorQuote.tsx` - Create: `components/sections/AnchorQuote.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `AnchorQuote()` at `components/sections/AnchorQuote.tsx`. Consumed by Task 11. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { AnchorQuote } from './AnchorQuote'; describe('AnchorQuote', () => { it('renders the approved anchor quote verbatim', () => { render(); expect( screen.getByText(/no dejan de crecer por falta de claridad/) ).toBeInTheDocument(); }); it('does not introduce a second h1 on the page', () => { render(); expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/AnchorQuote.test.tsx` Expected: FAIL with "Cannot find module './AnchorQuote'". - [ ] **Step 3: Write `components/sections/AnchorQuote.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; export function AnchorQuote() { return (
“Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad.”
); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/AnchorQuote.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/AnchorQuote.tsx components/sections/AnchorQuote.test.tsx git commit -m "feat: add AnchorQuote section" ``` --- ### Task 6: ForWhom section (03) **Files:** - Create: `components/sections/ForWhom.tsx` - Create: `components/sections/ForWhom.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `ForWhom()` at `components/sections/ForWhom.tsx`. Consumed by Task 11. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { ForWhom } from './ForWhom'; describe('ForWhom', () => { it('renders exactly the three approved qualifying chips in order', () => { render(); const items = screen.getAllByRole('listitem'); expect(items.map((item) => item.textContent)).toEqual([ '✓ tienes tracción', '✓ más esfuerzo no rinde', '✓ necesitas claridad', ]); }); it('uses an h2, never a second h1', () => { render(); expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/ForWhom.test.tsx` Expected: FAIL with "Cannot find module './ForWhom'". - [ ] **Step 3: Write `components/sections/ForWhom.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; const CHIPS = ['tienes tracción', 'más esfuerzo no rinde', 'necesitas claridad']; export function ForWhom() { return (

¿Llegaste a tu punto de inflexión?

Esto es para ti si...

    {CHIPS.map((chip) => (
  • ✓ {chip}
  • ))}
); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/ForWhom.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/ForWhom.tsx components/sections/ForWhom.test.tsx git commit -m "feat: add ForWhom qualifying section" ``` --- ### Task 7: HowIWork section (04, dark) **Files:** - Create: `components/sections/HowIWork.tsx` - Create: `components/sections/HowIWork.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `HowIWork()` at `components/sections/HowIWork.tsx`. Consumed by Task 11. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { HowIWork } from './HowIWork'; describe('HowIWork', () => { it('renders the three mechanism steps in order, numbered, with no icons', () => { render(); const items = screen.getAllByRole('listitem'); expect(items.map((item) => item.textContent)).toEqual([ '01Inmersión', '02Desafiar supuestos', '03Estrategia ejecutable', ]); expect(screen.queryAllByRole('img')).toHaveLength(0); }); it('uses the dark section background', () => { const { container } = render(); expect(container.querySelector('section')?.className).toContain( 'bg-bg-dark' ); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/HowIWork.test.tsx` Expected: FAIL with "Cannot find module './HowIWork'". - [ ] **Step 3: Write `components/sections/HowIWork.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; const STEPS = [ { number: '01', title: 'Inmersión' }, { number: '02', title: 'Desafiar supuestos' }, { number: '03', title: 'Estrategia ejecutable' }, ]; export function HowIWork() { return (

Cómo trabajo

    {STEPS.map((step) => (
  1. {step.number}

    {step.title}

  2. ))}
); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/HowIWork.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/HowIWork.tsx components/sections/HowIWork.test.tsx git commit -m "feat: add HowIWork dark mechanism section" ``` --- ### Task 8: Disciplines section (05) **Files:** - Create: `components/sections/Disciplines.tsx` - Create: `components/sections/Disciplines.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `Disciplines()` at `components/sections/Disciplines.tsx`. Consumed by Task 11. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Disciplines } from './Disciplines'; describe('Disciplines', () => { it('renders the three disciplines as tools, not as the core promise', () => { render(); expect(screen.getByText('Comunicación')).toBeInTheDocument(); expect(screen.getByText('Digital')).toBeInTheDocument(); expect(screen.getByText('Experiencias')).toBeInTheDocument(); }); it('gives each card a gold top border', () => { const { container } = render(); expect(container.querySelectorAll('.border-t-4')).toHaveLength(3); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/Disciplines.test.tsx` Expected: FAIL with "Cannot find module './Disciplines'". - [ ] **Step 3: Write `components/sections/Disciplines.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; const DISCIPLINES = ['Comunicación', 'Digital', 'Experiencias']; export function Disciplines() { return (

Las herramientas

{DISCIPLINES.map((discipline) => (

{discipline}

))}
); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/Disciplines.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/Disciplines.tsx components/sections/Disciplines.test.tsx git commit -m "feat: add Disciplines section" ``` --- ### Task 9: About section (06) **Files:** - Create: `components/sections/About.tsx` - Create: `components/sections/About.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `About()` at `components/sections/About.tsx`. Consumed by Task 11. **Note:** no professional photo exists yet (spec §5, §10 — pending). Renders a navy/gold gradient placeholder labeled for screen readers; swap the `div` for a Next `...` the moment a photo is provided (`images.unoptimized: true` from Task 1 already makes this drop-in safe under static export). - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { About } from './About'; describe('About', () => { it('labels the portrait placeholder for screen readers until a real photo is provided', () => { render(); expect( screen.getByRole('img', { name: /Retrato de la estratega/ }) ).toBeInTheDocument(); }); it('renders the first-person bio and the institutional quote', () => { render(); expect(screen.getByText(/Hoy ayudo a empresarios/)).toBeInTheDocument(); expect( screen.getByText(/El tiempo no se compra, se invierte/) ).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/About.test.tsx` Expected: FAIL with "Cannot find module './About'". - [ ] **Step 3: Write `components/sections/About.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; export function About() { return (

La mente detrás de creeer

+30 años construyendo marcas. Hoy ayudo a empresarios con tracción a encontrar la claridad estratégica que los lleva a su siguiente movimiento.

“El tiempo no se compra, se invierte.”

); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/About.test.tsx` Expected: PASS, 2 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/About.tsx components/sections/About.test.tsx git commit -m "feat: add About section with portrait placeholder" ``` --- ### Task 10: Closing section (07, dark) + contact links **Files:** - Create: `components/sections/Closing.tsx` - Create: `components/sections/Closing.test.tsx` **Interfaces:** - Consumes: `MotionSection` (Task 3). - Produces: `Closing()` at `components/sections/Closing.tsx`, rendering the `id="contacto"` anchor target that Hero's CTA (Task 4) scrolls to. Consumed by Task 11. **Note:** WhatsApp number, email, and LinkedIn URL are placeholders pending the dueña's real contact details (spec §10). Replace the three values in `CONTACT_LINKS` the moment she provides them — no other code changes needed. - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Closing } from './Closing'; describe('Closing', () => { it('exposes the three contact channels with accessible labels', () => { render(); expect( screen.getByRole('link', { name: 'Escribir por WhatsApp' }) ).toHaveAttribute('href', 'https://wa.me/521234567890'); expect( screen.getByRole('link', { name: 'Escribir por correo electrónico' }) ).toHaveAttribute('href', 'mailto:hola@creeer.com'); expect( screen.getByRole('link', { name: 'Ver perfil de LinkedIn' }) ).toHaveAttribute('href', 'https://www.linkedin.com/in/creeer'); }); it('points the closing CTA at the primary contact channel', () => { render(); const cta = screen.getByRole('link', { name: 'Define tu siguiente movimiento →', }); expect(cta).toHaveAttribute('href', 'https://wa.me/521234567890'); }); it('provides the #contacto anchor target for the hero CTA to scroll to', () => { const { container } = render(); expect(container.querySelector('#contacto')).not.toBeNull(); }); it('uses the dark section background', () => { const { container } = render(); expect(container.querySelector('section')?.className).toContain( 'bg-bg-dark' ); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- components/sections/Closing.test.tsx` Expected: FAIL with "Cannot find module './Closing'". - [ ] **Step 3: Write `components/sections/Closing.tsx`** ```tsx import { MotionSection } from '@/components/MotionSection'; const CONTACT_LINKS = [ { label: 'WhatsApp', href: 'https://wa.me/521234567890', ariaLabel: 'Escribir por WhatsApp', }, { label: 'Email', href: 'mailto:hola@creeer.com', ariaLabel: 'Escribir por correo electrónico', }, { label: 'LinkedIn', href: 'https://www.linkedin.com/in/creeer', ariaLabel: 'Ver perfil de LinkedIn', }, ]; export function Closing() { return (

“Devuélvele claridad al rumbo de tu empresa.”

Define tu siguiente movimiento →
); } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npm test -- components/sections/Closing.test.tsx` Expected: PASS, 4 tests passing. - [ ] **Step 5: Commit** ```bash git add components/sections/Closing.tsx components/sections/Closing.test.tsx git commit -m "feat: add Closing section with contact links" ``` --- ### Task 11: Assemble the page, structured data, and verify the build **Files:** - Modify: `app/page.tsx` (replace the Task 1 placeholder) - Create: `app/page.test.tsx` **Interfaces:** - Consumes: `Hero` (Task 4), `AnchorQuote` (Task 5), `ForWhom` (Task 6), `HowIWork` (Task 7), `Disciplines` (Task 8), `About` (Task 9), `Closing` (Task 10). - [ ] **Step 1: Write the failing tests** ```tsx import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import Home from './page'; describe('Home page', () => { it('renders exactly one h1 across the whole page', () => { render(); expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1); }); it('renders the seven sections in spec order', () => { const { container } = render(); expect(container.querySelectorAll('section')).toHaveLength(7); }); it('repeats the primary CTA copy at the top and the bottom of the page', () => { render(); expect( screen.getAllByText('Define tu siguiente movimiento →') ).toHaveLength(2); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npm test -- app/page.test.tsx` Expected: FAIL — the placeholder page only renders one `

` and zero `
` elements, so the section-count and CTA-repetition assertions fail. - [ ] **Step 3: Replace `app/page.tsx`** ```tsx import { Hero } from '@/components/sections/Hero'; import { AnchorQuote } from '@/components/sections/AnchorQuote'; import { ForWhom } from '@/components/sections/ForWhom'; import { HowIWork } from '@/components/sections/HowIWork'; import { Disciplines } from '@/components/sections/Disciplines'; import { About } from '@/components/sections/About'; import { Closing } from '@/components/sections/Closing'; const STRUCTURED_DATA = { '@context': 'https://schema.org', '@type': 'ProfessionalService', name: 'creeer', description: 'Claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión.', url: 'https://creeer.com', }; export default function Home() { return (