diff --git a/.claude/skills/react-doctor/SKILL.md b/.claude/skills/react-doctor/SKILL.md deleted file mode 100644 index d9e4c46..0000000 --- a/.claude/skills/react-doctor/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: react-doctor -description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook. -version: "1.2.0" ---- - -# React Doctor - -Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. - -## After making React code changes: - -Run `npx react-doctor@latest --verbose --scope changed` and check the score did not regress. - -If the score dropped, fix the regressions before committing. - -## For general cleanup or code improvement: - -Run `npx react-doctor@latest --verbose` (the default `--scope full`) to scan the full codebase. Fix issues by severity — errors first, then warnings. - -## For a focused UI design audit: - -Run `npx react-doctor@latest design --verbose`. This selects only design-tagged UI composition, typography, interaction, accessibility, and motion rules, including focused rules that remain opt-in during a general health scan. - -## /doctor — full local triage workflow - -When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it: - -```bash -curl --fail --silent --show-error \ - --header 'Cache-Control: no-cache' \ - https://www.react.doctor/prompts/react-doctor-agent.md -``` - -The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed. - -Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules//.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe. - -## Configuring or explaining rules - -When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), read [references/explain.md](references/explain.md) and follow it. Start with `npx react-doctor@latest rules explain `, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`). - -## Command - -```bash -npx react-doctor@latest --verbose --scope changed -``` - -| Flag | Purpose | -| ----------------- | ---------------------------------------------------------------- | -| `.` | Scan current directory | -| `--verbose` | Show affected files and line numbers per rule | -| `--scope changed` | Only report issues introduced vs the base branch (default: full) | -| `--scope lines` | Only report issues on the changed lines | -| `--score` | Output only the numeric score | -| `design` | Run only the focused UI design diagnostics | diff --git a/.claude/skills/react-doctor/references/explain.md b/.claude/skills/react-doctor/references/explain.md deleted file mode 100644 index c3a1008..0000000 --- a/.claude/skills/react-doctor/references/explain.md +++ /dev/null @@ -1,73 +0,0 @@ -# Explaining and configuring rules - -Explain React Doctor rules and edit `doctor.config.*` safely. Use this when a user -wants to understand a rule or change which rules run — not for fixing diagnostics -(that is the main `react-doctor` skill / `/doctor`). - -Triggers: "why did this rule fire", "I disagree with this rule", "turn this rule off", -"stop flagging X", "too noisy", "disable design rules". - -## Workflow - -1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`). -2. Explain it before changing anything: - -```bash -npx react-doctor@latest rules explain react-doctor/no-array-index-as-key -``` - -3. Pick the narrowest control that matches the user's intent (see decision guide). -4. Apply it with a `rules` subcommand (edits your `doctor.config.*` or `package.json#reactDoctor` in place, preserving other fields and formatting). -5. Validate the change did what they wanted: - -```bash -npx react-doctor@latest --verbose --diff -``` - -## Commands - -```bash -npx react-doctor@latest rules list # every rule + its effective severity -npx react-doctor@latest rules list --configured # only what your config changed -npx react-doctor@latest rules list --category Performance # filter by category -npx react-doctor@latest rules explain # why it matters + how to configure -npx react-doctor@latest rules disable # rule never runs -npx react-doctor@latest rules enable # turn back on at its recommended severity -npx react-doctor@latest rules set warn # off | warn | error -npx react-doctor@latest rules category "React Native" off # whole category -npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …) -npx react-doctor@latest rules unignore-tag design -``` - -Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`). - -## Decision guide - -Match the control to the intent — prefer the narrowest one: - -- **User disagrees with one rule / it's a false positive for them** → `rules disable ` (sets `rules. = "off"`; the rule stops running everywhere). This is the default for "I don't want this rule". -- **Rule is fine but wrong severity** → `rules set warn` or `rules set error`. -- **A disabled-by-default rule they want on** → `rules enable `. -- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "" off`. -- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag `. -- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output. -- **Restore test or story findings to production health** → set `surfaces.score.includeFileContexts` or `surfaces.ciFailure.includeFileContexts` to `["test"]`, `["story"]`, or both. Other surface exclusions still apply. - -How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs. - -## Config shape - -Config lives in `doctor.config.ts` (or `.js`/`.mjs`/`.cjs`/`.json`/`.jsonc`), or the `reactDoctor` key in `package.json`. The `rules` commands edit whichever exists — TS/JS edits preserve formatting (via magicast) — and create `doctor.config.json` when none does, stamping `$schema`: - -```ts -// doctor.config.ts -export default { - rules: { "react-doctor/no-array-index-as-key": "off" }, - categories: { "React Native": "warn" }, - ignore: { tags: ["design"] }, -}; -``` - -## Educating the user - -When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules//.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues. diff --git a/CLAUDE.md b/docs/CLAUDE.md similarity index 86% rename from CLAUDE.md rename to docs/CLAUDE.md index 6490685..a3b1904 100644 --- a/CLAUDE.md +++ b/docs/CLAUDE.md @@ -28,9 +28,9 @@ npx tsc --noEmit # typecheck (no hay script propio) Landing one-page estática. Next.js 14 App Router con `output: 'export'` en `next.config.mjs` → **no hay backend, ni API routes, ni `next/image` optimizado, ni middleware**. El contacto son enlaces directos (WhatsApp / mailto / LinkedIn); no hay formulario. -**Una sola ruta.** `app/page.tsx` compone las 8 secciones en orden fijo y monta el JSON-LD (`ProfessionalService`) inline. `app/layout.tsx` carga Open Sans vía `next/font/google` exponiéndola como `--font-open-sans`, y concentra toda la metadata/OG/canonical. +**Una sola ruta.** `app/page.tsx` compone las 10 secciones en orden fijo y monta el JSON-LD (`ProfessionalService`) inline. `app/layout.tsx` carga Open Sans vía `next/font/google` exponiéndola como `--font-open-sans`, y concentra toda la metadata/OG/canonical. -**El copy vive en `lib/content.ts`, no en el JSX.** Es el source of truth: cada sección importa su objeto (`hero`, `fraseAncla`, `paraQuien`, `mecanismo`, …). Las frases que necesitan énfasis parcial están **partidas en segmentos** (`headline.line1` / `headline.effort` / `headline.clarity`) para que el componente pinte en dorado o subraye sin fragmentar strings dentro del JSX. Al cambiar copy, edita `lib/content.ts`; al cambiar énfasis visual, edita el componente. Los enlaces de contacto ahí llevan `TODO dueña` con placeholders — no inventes valores reales. +**El copy vive en `lib/content.ts`, no en el JSX.** Es el source of truth: cada sección importa su objeto (`hero`, `fraseAncla`, `paraQuien`, `proceso`, …). Las frases que necesitan énfasis parcial están **partidas en segmentos** (`headline.line1` / `headline.effort` / `headline.clarity`) para que el componente pinte en dorado o subraye sin fragmentar strings dentro del JSX. Al cambiar copy, edita `lib/content.ts`; al cambiar énfasis visual, edita el componente. Los enlaces de contacto ahí llevan `TODO dueña` con placeholders — no inventes valores reales. **Animación centralizada en `lib/motion.ts`.** Las secciones son client components (`'use client'`) que usan `fadeInUp` + `staggerContainer` con `viewportOnce`, y **siempre** combinan `usePrefersReducedMotion()` con `motionProps(reduced)` — ese spread neutraliza `whileInView`/`initial` cuando el usuario pide menos movimiento. Es requisito de a11y del spec; replícalo en cualquier sección nueva. diff --git a/docs/branding.jpeg b/docs/branding.jpeg deleted file mode 100755 index e6db084..0000000 Binary files a/docs/branding.jpeg and /dev/null differ diff --git a/docs/dueña-directrices.md b/docs/dueña-directrices.md deleted file mode 100644 index 9e5db08..0000000 --- a/docs/dueña-directrices.md +++ /dev/null @@ -1,300 +0,0 @@ -# Directrices de la dueña · creeer - -> **Source of truth para decisiones de marca, copy y diseño.** -> Esta es la voz y el criterio de la dueña de creeer. Toda decisión del proyecto debe pasar por este filtro. Si una propuesta contradice algo de aquí, **preguntar antes de avanzar**. - ---- - -## Quién es la dueña - -La dueña es **la mente detrás de creeer** — estratega de crecimiento con +30 años construyendo marcas. Su negocio real es **claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión** (no vende marketing, vende la transformación). - -El nombre **creeer** es un juego con "creer" (en ti, en mí, en el proyecto) + "career". - ---- - -## Mensajes verbatim de la dueña (contexto y voz) - -Estos son los mensajes que la dueña envió por WhatsApp durante el brainstorming, transcritos literalmente. **Preservar su voz y sus palabras.** - -### Sobre la sensación que quiere proyectar -> **"Sabes lo que quiero que proyectemos es seguridad pero cercanía."** - -*(3:03 p.m., 20/7/2026)* — Esta es la directriz maestra de tono. **No frío/distante, no agresivo puro. Autoridad accesible.** - -### Sobre el wordmark en minúsculas -> **"Por eso está en minúsculas."** - -*(3:04 p.m., 20/7/2026)* — La decisión de escribir `creeer` en minúsculas es **consciente y estratégica**: transmite cercanía, humildad, accesibilidad (frente al frío de los mayúsculas institucionales). **NUNCA usar `CREEER` ni `Creeer` en el wordmark** — siempre `creeer`. - -### Sobre la paleta y el estilo -> **"Creo que el blanco con azul."** -> **"Simple directo."** -> **"Limpio."** - -*(3:12 p.m., 20/7/2026)* — Tres directrices visuales: -1. **Blanco con azul** → fondo claro + navy (no dark mode como dirección principal) -2. **Simple directo** → sin decoración, sin elementos accesorios, copy directo -3. **Limpio** → mucho aire, blanco, tipografía hace el trabajo - -### Referencia a Alex Hormozi -La dueña mencionó a **Alex Hormozi** como referencia estética/comunicacional. Su ADN: -- Bold sans-serif gigante, a menudo uppercase -- Alto contraste (negro/blanco/un acento eléctrico — en creeer, dorado) -- Copy directo, corto, cuantificado (números, métricas, claims) -- Palabras clave en caja (background detrás de la palabra) -- Voz agresiva, results-driven, "sin paja" - -> ⚠️ **Tensión a equilibrar:** Hormozi es agresivo/directo, pero la dueña también pidió **cercanía + minúsculas + blanco+azul + limpio**. La síntesis es **"Hormozi en voz, creeer en alma"**: el punch visual de Hormozi (bold sans, palabras en caja) con el dorado tostado, el wordmark serif en minúsculas y la paleta cálida de creeer. - -### Sobre la tipografía y el símbolo del wordmark (28/jul/2026) - -Contexto: se le pasaron 3 opciones Open Sans / Carlito / Fira Sans. Verbatim: - -> **Erik (11:28):** "Open Sans / Carlito / Fira Sans — Son muy similares, pero tal vez haya detallitos que creen mejores." -> **Dueña (12:56):** "Toma la más común." -> **Dueña (12:57):** "Sólo cuida que la E que está en 3 líneas se lea como E. Por favor." -> **Dueña (12:57):** "Gracias." - -Dos directivas: -1. **Tipografía → Open Sans** ("la más común" de las tres). -2. **Símbolo del wordmark:** el `≡` (tres líneas) debe **leerse como E** — no como líneas abstractas ni ícono de menú. El wordmark tiene que leerse `creeer`. Implementación: glifo en SVG que forma una **E** con tres barras horizontales + espina vertical izquierda, en dorado. - -> ⚠️ **Contradicción pendiente con el manual.** El Manual de Identidad Visual v1.0 dibuja el `≡` como **tres barras sueltas, sin espina vertical**. El punto 2 de arriba (la espina) es una interpretación del mensaje de la dueña, no algo que el manual muestre. El logotipo trazado en `public/logo-creeer-principal.svg` sigue al manual. **Preguntar a la dueña cuál manda.** - -### Sobre la nueva imagen y el formato de la página (29/jul/2026) - -Contexto: mandó el Manual de Identidad Visual (`docs/branding.jpeg`) y una referencia de sitio. Verbatim: - -> **Dueña (10:13):** *[imagen del Manual de Identidad Visual v1.0]* -> **Dueña (10:13):** "esta es la nueva imagen, lo que ofrecemos es claridad." -> **Dueña (10:13):** "claridad estratégica" -> **Dueña (10:14):** "me gusta el formato de esta página," -> **Dueña (10:14):** "ya que haremos preguntas y vamos ir dando solución a sus principales dolores" -> **Dueña (10:14):** https://en-ti.com/ -> **Dueña (10:15):** "me falta definir la información, pero les voy a dejar un par de proyectos que hice, justamente dando esta claridad estratégica a 2 compañías." - -Tres directivas: -1. **El manual v1.0 es la imagen definitiva** y la oferta se nombra **"claridad estratégica"** (no "estrategia", no "marketing"). -2. **Adoptar la estructura de `https://en-ti.com/`** — confirmado con la dueña (29/jul/2026): le gustó **la estructura** de esa página. Es referencia de *estructura*, no de estética: la paleta, el logotipo y el tono los sigue fijando el manual + las decisiones bloqueadas. -3. **Pendiente de la dueña:** 2 casos de proyectos reales donde dio claridad estratégica. La información aún no está definida por ella; no inventar casos ni métricas. - -**Estructura de en-ti.com, verificada en el sitio (29/jul/2026):** hero declarativo con 2 CTAs → barra de métricas → soluciones en 6 tarjetas → fichas ampliadas → proceso de 5 pasos → "a quién servimos" (8 industrias) → logos de clientes → productos propios → muro de prensa → CTA repetido (*"Cotiza tu proyecto"*). - -> **Ojo al releer esto:** la página **no contiene ni una sola pregunta** — todos sus encabezados son enunciados. El patrón real de sus tarjetas es **nombrar el dolor y resolverlo en dos líneas**, p. ej. *"Recuperación de equipo crítico — Cuando un servidor o equipo especializado falla, diagnosticamos, restauramos y devolvemos la operación, sin reemplazar toda la inversión."* Lo de "haremos preguntas" es intención de la dueña para el copy, no algo que la referencia demuestre. - -### Sobre el avance con gráfico y movimiento (30/jul/2026) - -> **Dueña (Jazmín):** "súper, sólo le falta algo de gráfico y movimiento, denme chance de entregar algo y si pudieras en la tarde nos conectamos un rato para comentarles." - -Dos lecturas: -1. **Aprueba el rumbo actual** ("súper") y pide más **gráfico y movimiento** — refuerza seguir sumando elementos visuales/animados (en la línea de lo ya trabajado: fondos de puntos, blur-in del headline, spotlight en tarjetas, carrusel), siempre dentro de la paleta y el tono ya fijados. -2. **Pide tiempo** ("denme chance de entregar algo") antes de seguir — probablemente va a compartir algo (¿los 2 casos pendientes? ¿más contenido del deck?) — y propone **conectar en la tarde** para comentar con el equipo ("comentarles", plural). No agendar nada unilateralmente; esperar a que ella confirme la llamada. - ---- - -## Deck "Rumbo Estratégico · Propuesta Socio" (29/jul/2026) - -> **Fuente:** `creeer_Rumbo_Estrategico_Propuesta_Socio.pptx`, 14 diapositivas, autoría de la dueña (Jazmín Rodríguez). -> ⚠️ **Es un deck para un socio estratégico, no material público.** Las diapositivas 11–13 (roadmap con fechas internas, qué se le pide al socio, próximos pasos) **no van a la landing**. El propio deck menciona que habrá un "deck público" aparte. -> ⚠️ **La voz del deck es "nosotros" ("no vendemos marketing", "trabajamos con…") porque le habla a un socio.** La landing va en segunda persona ("tú") y primera del singular, según las decisiones bloqueadas. **No copiar el copy verbatim sin cambiar la persona.** - -### Los 6 puntos de inflexión (diapositiva 2) — material para tarjetas - -| Punto | Descripción verbatim | -|---|---| -| Crecer | Dar el siguiente salto sin saber por dónde | -| Consolidarse | Ordenar lo construido antes de escalar | -| Recuperar rentabilidad | El esfuerzo ya no se traduce en resultado | -| Simplificar | Demasiada complejidad operativa | -| Expandirse | Nuevos mercados, mismo mapa de decisión | -| Replantear el portafolio | Qué seguir, qué dejar ir | - -### Los dolores, en primera persona (diapositiva 3) — verbatim - -> "Trabajo más y gano casi lo mismo que hace dos años." -> "Tengo varios proyectos y no sé en cuál enfocar mi tiempo y mi dinero." -> "Mi empresa no puede crecer si yo no estoy en cada decisión." -> "Lo que me trajo hasta aquí ya no me sirve para el siguiente nivel." - -Remate de la diapositiva: **"Esto no se resuelve con más esfuerzo. Se resuelve con una estrategia clara."** - -Estos son los "principales dolores" que la dueña anunció por WhatsApp. **Ya están escritos en su voz — usarlos, no reescribirlos.** - -### Confirmaciones que aporta el deck - -- **El insight (diap. 4) ratifica el lead de `Disciplinas`:** *"No vendemos marketing. Vendemos claridad estratégica… El marketing es una de las disciplinas que usamos para transformar empresas — no el producto que vendemos."* → `Disciplinas` se queda como está, separado de cualquier bloque de soluciones. -- **`Mecanismo` coincide** con el proceso del deck (diap. 7): Inmersión → Desafío → Construcción. -- **`ParaQuien` se queda corto:** el deck (diap. 9) tiene **4 "SÍ ES PARA" + 4 "NO ES PARA"**; la landing sólo tiene 3 criterios afirmativos. -- **Encuadre clave (diap. 5):** *"El cliente no está en crisis: está en transición."* -- **Frase-ancla alternativa (diap. 14):** *"Cuando una empresa pierde claridad, deja de crecer por estrategia y empieza a crecer por inercia."* Convive con la de `lib/content.ts`; **decidir cuál manda.** - -### Alertas - -- **Diapositiva 10 = escasez.** "Generación Fundadora", 10 empresas, "precio y condiciones de fundador, no repetibles". Está **explícitamente prohibida en el hero** por decisión de la dueña, y hay un test negativo en `Hero.test.tsx` que falla con `cupos|cohorte|generación fundadora`. **Preguntar si puede vivir más abajo en la página o si queda fuera del todo.** -- **Los 2 casos siguen sin llegar.** El deck no los contiene. -- **Calendario (diap. 11):** lanzamiento última semana de agosto 2026; hoy la fase es "Validación" (pilotos con 5–10 empresarios). - ---- - -## Principios derivados (cómo aplicar los mensajes) - -| Directriz de la dueña | Implicación práctica | -|---|---| -| "Seguridad + cercanía" | Serif para autoridad + voz en "tú" + fondos cálidos para cercanía | -| "minúsculas" | Wordmark SIEMPRE `creeer`. Eyebrows y CTAs pueden ser uppercase (system-ui) pero el wordmark jamás | -| "blanco con azul" | Fondo principal blanco/blanco cálido, navy como tinta y CTA | -| "Simple directo" | Sin elementos decorativos accesorios. Copy corto, una idea por línea | -| "Limpio" | Mucho aire (whitespace), máximo 2 familias tipográficas, paleta reducida | -| Referencia Hormozi | Permitido bold sans uppercase + palabras en caja + métricas, **siempre que** no rompan cercanía | - ---- - -## Posicionamiento estratégico (de su documento de nicho) - -Esto NO es copy de landing — es la brújula estratégica que la dueña ya validó. Toda la messaging debe ser coherente con esto. - -### Lo que creeer NO es -- ❌ Un diplomado de marketing -- ❌ Una consultoría genérica -- ❌ Clases/asesoría por hora -- ❌ Venta de disciplinas (Comunicación/Digital/Experiencias) como promesa - -### Lo que creeer SÍ es -- ✅ **Claridad estratégica** para empresarios con tracción en punto de inflexión -- ✅ Transformación, no horas -- ✅ Inmersión profunda → desafiar supuestos → estrategia ejecutable - -### Cliente ideal -Empresarios que **ya lograron tracción**, pero llegaron al punto donde **trabajar más ya no basta** — necesitan mejores decisiones estratégicas. NO cualquier PYME. NO empresas en crisis. **Empresas en transición.** - -### Frases-ancla aprobadas (de su documento) -- *"Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad."* -- *"Cuando el esfuerzo ya no alcanza, la estrategia se vuelve indispensable."* -- *"El tiempo no se compra, se invierte."* (institucional) -- *"Devuélvele claridad al rumbo de tu empresa."* (promesa) - -### Tagline de marca -*"creeer en ti, creeer en mí, creeer en el proyecto."* - -### Pilares (disciplinas, NO la promesa) -**Comunicación · Digital · Experiencias** (+ Integración y acción). Son las herramientas, no el producto. **Viven en sección "cómo", no en el hero.** - ---- - -## Sistema de diseño oficial — Manual de Identidad Visual v1.0 (mayo 2026) - -> **Fuente:** `docs/branding.jpeg` — Manual de Identidad Visual aprobado por la dueña (28/jul/2026). **Reemplaza** todas las decisiones previas de paleta, tipografía y logotipo del brainstorming y del pptx. - -### Filosofía de marca -> *"Transformamos la complejidad en claridad estratégica para construir ecosistemas empresariales que crecen con dirección y propósito."* - -Proceso visual: **Complejidad** (múltiples variables) → **Enfoque** (priorizamos y alineamos lo esencial) → **Claridad** (una estrategia, una dirección, resultados extraordinarios). - -### Símbolo principal -- **Tres líneas horizontales** (≡) que representan el proceso de transformar lo complejo en lo esencial -- Las líneas van de muchas/desordenadas (complejidad) a pocas/alineadas (claridad) -- Significado: "una estrategia clara, con dirección y propósito" - -### Logotipo -- Wordmark: **`cre≡er`** — la "e" central se reemplaza por el **símbolo de tres líneas** (≡) en dorado -- SIEMPRE en minúsculas -- Tagline debajo: `CLARIDAD · DIRECCIÓN · CRECIMIENTO` -- Eslogan: *"Estrategia que transforma."* - -**Versiones del logotipo:** -| Versión | Descripción | -|---------|-------------| -| Principal | `cre≡er` + tagline "CLARIDAD · DIRECCIÓN · CRECIMIENTO" | -| Secundaria | `cre≡er` sin tagline | -| Ícono | Solo el símbolo ≡ (tres líneas) | -| Negativa | Fondo navy, logotipo en blanco + dorado | -| Negativo (ícono) | Símbolo ≡ en dorado sobre navy | -| Escala de grises | Todo en grises | - -**Tamaño mínimo:** impreso 20 mm, digital 120 px. - -### Paleta de colores - -**Colores principales:** -| Nombre | Hex | Pantone | RGB | Uso | -|--------|-----|---------|-----|-----| -| **Azul Profundo** | `#0A1D3D` | 2965 C | 10, 29, 61 | Texto principal, fondos oscuros, CTAs | -| **Dorado Estratégico** | `#C4942E` | 872 C | 196, 154, 46 | Acentos, títulos, símbolo ≡, énfasis | - -**Colores neutros:** -| Nombre | Hex | RGB | Uso | -|--------|-----|-----|-----| -| Azul Claro | `#C8EEF5` | 222, 226, 245 | Fondos alternativos, secciones suaves | -| Gris Claro | `#F2F2F2` | 242, 242, 242 | Fondos neutros, separadores | -| Gris Medio | `#AAAAAA` | 166, 166, 166 | Texto secundario, placeholders | -| Blanco | `#FFFFFF` | 255, 255, 255 | Fondo principal | - -**Regla:** NO amarillo eléctrico tipo Hormozi (rompe cercanía). - -### Tipografía - -**Manual de marca (impreso/presentación):** Aptos (principal) + Aptos Narrow (secundaria). -**Implementación web (landing):** **Open Sans** — vía `next/font/google`, pesos 400/600/700/800 + itálicas. - -> ⚠️ **Por qué no Aptos en web:** Aptos es proprietaria de Microsoft; su EULA prohíbe distribuirla o servirla en un sitio público ("No Distribution Rights" + "publish for others to copy"). La dueña, informada del riesgo, inicialmente autorizó usarla; pero al faltar los pesos regular/semibold en la conversión a `.woff2`, decidió pasar a la alternativa open-source más cercana. **Open Sans** comparte diseñador con Aptos (Steve Matteson) y es OFL (libre para web). Decisión: 28/jul/2026. - -### Iconografía -- Estilo: **líneas simples, geométricas, consistentes en grosor** -- Iconos definidos: Análisis, Enfoque, Estrategia, Crecimiento, Alineación, Resultados -- Colores: Azul Profundo + Dorado Estratégico - -### Recursos visuales -- **Patrón de puntos** (grid de puntos dorados) -- **Líneas y divisores** (horizontales, en dorado o navy) -- **Recurso gráfico** (las tres líneas del símbolo en diversas orientaciones y escalas) - -### Patrones de alineación -- `center` → hero / títulos de impacto -- `right` → momentos asimétricos -- `left` → contenido, citas, cierre -- Mucho aire/whitespace alrededor de los textos - ---- - -## Restricciones fijadas (decisiones bloqueadas) - -Estas decisiones **no se reabren sin consultar a la dueña.** - -### Copy -- Voz en **segunda persona ("tú")** — nunca "el empresario", siempre "tu negocio" -- Sin jerga corporativa -- "Simple, directo, limpio" aplica al copy también - -### Frases-ancla aprobadas -- *"Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad."* -- *"Cuando el esfuerzo ya no alcanza, la estrategia se vuelve indispensable."* -- *"El tiempo no se compra, se invierte."* (institucional) -- *"Devuélvele claridad al rumbo de tu empresa."* (promesa) -- *"Estrategia que transforma."* (eslogan oficial) - -### Taglines -- **Eslogan oficial:** *"Estrategia que transforma."* -- **Tagline de marca:** *"CLARIDAD · DIRECCIÓN · CRECIMIENTO"* -- **Tagline emocional:** *"creeer en ti, creeer en mí, creeer en el proyecto."* - -### Estructura -- One-page (single page) -- Sin backend, contacto por enlaces directos (WhatsApp, email, LinkedIn) -- Sin escasez en el hero (la dueña pidió quitar cupos/cohorte del hero) - -### Pendiente de decisión de la dueña -- **Hero final:** HV-1 (bold sans sin cajas) vs HV-2 (palabras en caja) — ver `docs/hero-finalists.md` -- ~~**Tipografía web:** Aptos vs Inter~~ → **RESUELTO (28/jul/2026): Open Sans**. Ver sección "Tipografía" arriba. - ---- - -## Cómo usar este archivo - -Cuando trabajes en creeer (copy, diseño, código, decisiones de producto): - -1. **Lee esto primero** — es la voz de la dueña. -2. **Pasa cada decisión por el filtro** de "seguridad + cercanía" + "simple, directo, limpio". -3. **Si una propuesta contradice algo de aquí**, detente y pregunta antes de avanzar. -4. **Si la dueña envía un mensaje nuevo**, agrégalo a la sección "Mensajes verbatim" con fecha y contexto. -5. **Las decisiones bloqueadas** (paleta, wordmark, voz) no se reabren sin su aprobación explícita. diff --git a/docs/hero-finalists.md b/docs/hero-finalists.md deleted file mode 100644 index 01ca285..0000000 --- a/docs/hero-finalists.md +++ /dev/null @@ -1,118 +0,0 @@ -# Hero finalists · creeer - -Dos candidatos finales para el hero de la landing. **Mismo copy, misma paleta, mismo wordmark, mismo layout centrado.** La diferencia es el **tono del headline**: - -- **HV-1** → punch Hormozi (headline en Inter 900 uppercase) -- **V-a** → pureza editorial (headline en Georgia italic, todo serif) - -La dueña puede elegir uno. Ambos respetan: wordmark `cre`**`e`**`er.` en Georgia italic, voz en "tú", paleta del pptx, "simple, directo, limpio", sin escasez en el hero. - ---- - -## Base común - -**Wordmark:** `cre`**`e`**`er.` en **Georgia 700 italic**, minúscula, con la **"e" central** (2ª de las 3) en peso 900 + dorado `#B8901f`, y punto final dorado. *(El wordmark es la firma de marca — siempre serif italic, sin importar el hero elegido.)* - -**Eyebrow:** `— tu estratega de crecimiento —` (small caps, dorado tostado) - -**Headline copy:** -> Tu negocio no necesita más esfuerzo. -> Necesita más claridad. - -**CTA:** `Define tu siguiente movimiento →` (navy fill, white text, chunky) - -**Paleta:** -- Blanco `#FFFFFF` (fondo) -- Navy `#0A1F44` (texto + CTA) -- Dorado tostado `#B8901f` (acentos) - -**Sin escasez:** NO se muestran cupos, cohorte ni "Generación Fundadora" en el hero (decisión de la dueña). Esos elementos, si aparecen, viven en sección posterior de la página. - -**Voz:** segunda persona ("tú"/"tu negocio"), directa, sin jerga corporativa. - ---- - -## HV-1 · Hormozi ligero (punch) - -**Tratamiento:** -- Headline en **Inter 900 uppercase** con tracking negativo (`letter-spacing: -1.5px`) -- "más **esfuerzo**" en color dorado tostado `#B8901f` -- "más **claridad**" con **subrayado dorado** grueso (5px solid) -- Wordmark sigue en Georgia italic (contraste serif/sans intencional) - -**Sensación:** punto medio exacto entre la limpieza del pptx y el punch Hormozi. El contraste tipográfico (wordmark serif + headline sans) da energía sin perder clase. - -**Trade-off:** -- ✅ Punch visual, más conversión -- ✅ Headline memorable, retención media -- ✅ Mantiene el wordmark serif (marca intacta) -- ⚠️ Más agresivo que V-a; algo menos de cercanía - -**Mejor si la dueña prioriza:** impacto, conversión, diferenciación. - ---- - -## V-a · Editorial minimal (cercanía) - -**Tratamiento:** -- **TODO en Georgia serif italic** — wordmark + eyebrow + headline + CTA -- "más **esfuerzo**" en color dorado tostado, italic -- "más **claridad**" subrayada en dorado -- Sin métricas, sin subhead, sin elementos accesorios. Solo 4 bloques: wordmark → eyebrow → headline → CTA - -**Sensación:** calma editorial pura. Máximo aire. "Simple, directo, limpio" llevado al extremo. La cercanía que pidió la dueña en su forma más pura. - -**Trade-off:** -- ✅ Máxima cercanía y limpieza -- ✅ Coherencia tipográfica total (todo serif) -- ✅ Elegancia atemporal -- ⚠️ Menos punch de conversión que HV-1 -- ⚠️ Podría sentirse "demasiado tranquilo" para vender - -**Mejor si la dueña prioriza:** "seguridad + cercanía", "simple, directo, limpio", prestigio editorial. - ---- - -## Spectrum visual - -| Hero | Cercanía | Punch Hormozi | Estilo | -|---|---|---|---| -| **V-a** (todo serif italic) | **Alta** | Bajo | Editorial puro — calma, elegancia | -| **HV-1** (serif + sans Hormozi) | Media | **Medio-alto** | Hybrid — punch + marca | - ---- - -## Cómo decidir (pregunta para la dueña) - -> ¿Prefieres que el hero **respire y se sienta cálido** (V-a), o que **tenga punch e impacte** (HV-1)? - -Ambos funcionan. La decisión es de tono, no de calidad. El wordmark, la paleta y el copy son idénticos — solo cambia la energía del headline. - ---- - -## Mockups en vivo - -Servidor visual companion (activo durante la sesión de brainstorming): -``` -http://localhost:55739/?key=7cb508aff417782d47d2fdf9c54062393af1d0d1e6e2d19090228dbf67df333b -``` - -Archivos fuente de los mockups: -- `.superpowers/brainstorm/2447-1784580629/content/three-georgia-heroes.html` (#2 V-a) -- `.superpowers/brainstorm/2447-1784580629/content/heroes-wordmark-restored.html` (HV-1 con wordmark Georgia) -- `.superpowers/brainstorm/2447-1784580629/content/va-large-locked.html` (V-a en grande) - ---- - -## Nota técnica (para implementación) - -Independientemente de cuál se elija, la implementación es la misma base Next.js + Tailwind + Framer Motion: -- Componente `` con el wordmark, eyebrow, headline y CTA -- Animación de entrada escalonada (Framer Motion): wordmark → eyebrow → headline → CTA -- Fuentes: **Georgia** (system serif, no requiere import) + **Inter** (Google Fonts, solo si se usa HV-1) - -**Diferencia entre HV-1 y V-a en código:** -- V-a: headline usa `font-family: Georgia, serif; font-style: italic` -- HV-1: headline usa `font-family: 'Inter', sans-serif; font-weight: 900; text-transform: uppercase` - -Cambiar entre ambos es **trivial** (un `className` condicional). Se puede dejar como prop del componente `` para que la dueña decida en QA. diff --git a/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md b/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md deleted file mode 100644 index 87045b0..0000000 --- a/docs/superpowers/plans/2026-07-20-creeer-landing-implementation.md +++ /dev/null @@ -1,1435 +0,0 @@ -# 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 ( -
- - - - - - - -