docs: implementation plan for landing additions (+ short header CTA in spec)
This commit is contained in:
1 parent
4f73312526
commit
50f62bdf28
2 files changed
+467
-1
No files matched your search
@@ -0,0 +1,462 @@
|
|||||||
|
# creeer Landing Additions — Header + Cómo empezar + Footer — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a minimal sticky Header, a new "Cómo empezar" on-ramp section (section 07), and a slim Footer to the existing 7-section `creeer` landing page.
|
||||||
|
|
||||||
|
**Architecture:** Three additive pieces reusing the existing primitives (`Wordmark`, `CTA`, `Container`), tokens, the `motion` + `motionProps(reduced)` pattern, and the copy-in-`lib/content.ts` convention. Header and Footer are static server components wrapping `<main>`; `ComoEmpezar` is a client section following the exact pattern of `Mecanismo.tsx`/`Disciplinas.tsx`. Cierre becomes section 08. No backend, no forms — the site stays a static export.
|
||||||
|
|
||||||
|
**Tech Stack:** Next.js 14 (App Router, `output: export`), React 18, TypeScript, Tailwind CSS 3.4, Framer Motion 11. Tests: Vitest + @testing-library/react.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
(Copied from the spec `docs/superpowers/specs/2026-07-21-creeer-landing-additions-design.md` — apply to every task.)
|
||||||
|
|
||||||
|
- **Wordmark** always via the `Wordmark` primitive — never re-implemented, never uppercased.
|
||||||
|
- **Paleta (tokens):** `ink #0A1F44` · `gold #B8901f` (accents on LIGHT) · `gold-bright #D4AF37` (accents on DARK) · `base #FBFAF7` (light bg, class `bg-base`) · dark bg class `bg-bg-dark` · `cream` (text on dark) · `muted`/`muted-dark`.
|
||||||
|
- **Tipografía:** Georgia italic (`font-serif italic`) = wordmark + section titles; Inter = body/UI.
|
||||||
|
- **Sin backend, sin formularios** — contacto solo por enlaces directos (WhatsApp/mailto/LinkedIn).
|
||||||
|
- **Un único `<h1>`** en toda la página — vive en el Hero. New pieces use `<h2>`/`<h3>` or no heading.
|
||||||
|
- **Todo el copy** vive en `lib/content.ts` (nuevas claves `header`, `comoEmpezar`, `footer`).
|
||||||
|
- **Voz** en segunda persona, español.
|
||||||
|
- **Reduced motion:** every Framer Motion element gets `{...motionProps(reduced)}` after its `initial`/`whileInView`/`viewport` props (`reduced = usePrefersReducedMotion()`). Header/Footer are static (no motion).
|
||||||
|
- **`tsconfig.json` has no `target`** → avoid ES2018+ regex flags (`s`/dotAll) or `tsc --noEmit` fails.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── layout/
|
||||||
|
│ ├── Header.tsx # Task 1 — sticky bar (server component)
|
||||||
|
│ ├── Footer.tsx # Task 3 — slim footer strip (server component)
|
||||||
|
│ └── __tests__/
|
||||||
|
│ ├── Header.test.tsx # Task 1
|
||||||
|
│ └── Footer.test.tsx # Task 3
|
||||||
|
└── sections/
|
||||||
|
├── ComoEmpezar.tsx # Task 2 — section 07 (client, motion pattern)
|
||||||
|
└── __tests__/
|
||||||
|
└── ComoEmpezar.test.tsx # Task 2
|
||||||
|
lib/content.ts # Tasks 1/2/3 append `header` / `comoEmpezar` / `footer`
|
||||||
|
app/page.tsx # Tasks 1/2/3 wire the pieces; Task 4 anchor offset
|
||||||
|
components/sections/Cierre.tsx # Task 4 — add scroll-mt to the #contacto anchor
|
||||||
|
```
|
||||||
|
|
||||||
|
Each component task also wires itself into `app/page.tsx` (matching the repo's established incremental-compose pattern), so the page stays working after every task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Header (minimal sticky)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `components/layout/Header.tsx`
|
||||||
|
- Create: `components/layout/__tests__/Header.test.tsx`
|
||||||
|
- Modify: `lib/content.ts` (append `header`)
|
||||||
|
- Modify: `app/page.tsx` (place `<Header />` before `<main>`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `Wordmark` (`size`, `onDark`), `CTA` (`href`, `label`, `variant`), `Container`, and `header.cta` from content.
|
||||||
|
- Produces: `Header()` at `components/layout/Header.tsx` — no props. Consumed by `app/page.tsx`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append the `header` copy to `lib/content.ts`**
|
||||||
|
|
||||||
|
Add at the end of `lib/content.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const header = {
|
||||||
|
cta: 'Hablemos', // short nav CTA → #contacto (the full CTA lives in Hero/Cierre)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Header.test.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Header } from '../Header';
|
||||||
|
|
||||||
|
describe('<Header />', () => {
|
||||||
|
it('renders the wordmark', () => {
|
||||||
|
const { container } = render(<Header />);
|
||||||
|
expect(container.textContent).toContain('creeer');
|
||||||
|
});
|
||||||
|
it('renders a persistent CTA linking to the contact section', () => {
|
||||||
|
render(<Header />);
|
||||||
|
const cta = screen.getByRole('link', { name: /Hablemos/i });
|
||||||
|
expect(cta).toHaveAttribute('href', '#contacto');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm test -- Header`
|
||||||
|
Expected: FAIL with "Cannot find module '../Header'".
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `components/layout/Header.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Wordmark } from '@/components/primitives/Wordmark';
|
||||||
|
import { CTA } from '@/components/primitives/CTA';
|
||||||
|
import { Container } from '@/components/primitives/Container';
|
||||||
|
import { header } from '@/lib/content';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 border-b border-ink/10 bg-base/80 backdrop-blur">
|
||||||
|
<Container className="flex items-center justify-between py-3">
|
||||||
|
<a href="#top" aria-label="creeer — inicio">
|
||||||
|
<Wordmark size="md" />
|
||||||
|
</a>
|
||||||
|
<nav aria-label="Principal">
|
||||||
|
<CTA href="#contacto" label={header.cta} variant="light" />
|
||||||
|
</nav>
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes: server component (no `'use client'`, no motion — `position: sticky` is pure CSS). `#top` is the special top-of-document fragment (no element with that id is required). The `CTA` primitive appends the `→`, so `header.cta` holds just the word.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm test -- Header`
|
||||||
|
Expected: PASS, 2 tests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Wire `<Header />` into `app/page.tsx`**
|
||||||
|
|
||||||
|
Add the import after the existing section imports (top of the file):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Header } from '@/components/layout/Header';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then insert `<Header />` between the JSON-LD `<script>` and `<main>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Typecheck and run the full suite**
|
||||||
|
|
||||||
|
Run: `npx tsc --noEmit && npm test`
|
||||||
|
Expected: tsc clean; all tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add components/layout/Header.tsx components/layout/__tests__/Header.test.tsx lib/content.ts app/page.tsx
|
||||||
|
git commit -m "feat: sticky Header with persistent contact CTA"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: "Cómo empezar" section (07)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `components/sections/ComoEmpezar.tsx`
|
||||||
|
- Create: `components/sections/__tests__/ComoEmpezar.test.tsx`
|
||||||
|
- Modify: `lib/content.ts` (append `comoEmpezar`)
|
||||||
|
- Modify: `app/page.tsx` (place `<ComoEmpezar />` before `<Cierre />`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `Container`, `CTA`, `comoEmpezar` + `cierre.contact.whatsapp.href` from content, and `fadeInUp`/`staggerContainer`/`viewportOnce`/`usePrefersReducedMotion`/`motionProps` from `lib/motion`.
|
||||||
|
- Produces: `ComoEmpezar()` at `components/sections/ComoEmpezar.tsx` — no props. Consumed by `app/page.tsx`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append the `comoEmpezar` copy to `lib/content.ts`**
|
||||||
|
|
||||||
|
Add at the end of `lib/content.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const comoEmpezar = {
|
||||||
|
eyebrow: 'cómo empezar',
|
||||||
|
title: 'Tu siguiente movimiento empieza con una conversación.',
|
||||||
|
steps: [
|
||||||
|
{ n: '01', name: 'Escríbeme', desc: 'Cuéntame dónde está tu negocio hoy.' },
|
||||||
|
{ n: '02', name: 'Sesión de diagnóstico', desc: 'Una conversación para ubicar tu punto de inflexión.' },
|
||||||
|
{ n: '03', name: 'Estrategia ejecutable', desc: 'Sales con claridad y tu siguiente movimiento definido.' },
|
||||||
|
],
|
||||||
|
cta: 'Agenda tu sesión de diagnóstico',
|
||||||
|
whatsappText: 'Hola, quiero agendar una sesión de diagnóstico',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test `components/sections/__tests__/ComoEmpezar.test.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ComoEmpezar } from '../ComoEmpezar';
|
||||||
|
|
||||||
|
describe('<ComoEmpezar />', () => {
|
||||||
|
it('renders the on-ramp steps', () => {
|
||||||
|
render(<ComoEmpezar />);
|
||||||
|
expect(screen.getByText('Escríbeme')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Sesión de diagnóstico')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('links the CTA to WhatsApp with a prefilled message', () => {
|
||||||
|
render(<ComoEmpezar />);
|
||||||
|
const cta = screen.getByRole('link', { name: /Agenda tu sesión de diagnóstico/i });
|
||||||
|
const href = cta.getAttribute('href') ?? '';
|
||||||
|
expect(href).toMatch(/^https:\/\/wa\.me\//);
|
||||||
|
expect(href).toContain('text=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm test -- ComoEmpezar`
|
||||||
|
Expected: FAIL with "Cannot find module '../ComoEmpezar'".
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `components/sections/ComoEmpezar.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Container } from '@/components/primitives/Container';
|
||||||
|
import { CTA } from '@/components/primitives/CTA';
|
||||||
|
import { comoEmpezar, cierre } from '@/lib/content';
|
||||||
|
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
|
||||||
|
|
||||||
|
export function ComoEmpezar() {
|
||||||
|
const reduced = usePrefersReducedMotion();
|
||||||
|
const waHref = `${cierre.contact.whatsapp.href}?text=${encodeURIComponent(comoEmpezar.whatsappText)}`;
|
||||||
|
return (
|
||||||
|
<section className="bg-base">
|
||||||
|
<Container className="py-24 md:py-32">
|
||||||
|
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
|
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{comoEmpezar.eyebrow}</motion.p>
|
||||||
|
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
|
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{comoEmpezar.title}</motion.h2>
|
||||||
|
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
|
className="mt-12 grid gap-10 md:grid-cols-3">
|
||||||
|
{comoEmpezar.steps.map((step) => (
|
||||||
|
<motion.li key={step.n} variants={fadeInUp} className="border-t border-gold/30 pt-6">
|
||||||
|
<div className="font-serif italic font-black text-5xl md:text-6xl text-gold leading-none">{step.n}</div>
|
||||||
|
<h3 className="mt-4 font-sans font-bold text-xl text-ink">{step.name}</h3>
|
||||||
|
<p className="mt-3 text-muted leading-relaxed">{step.desc}</p>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</motion.ol>
|
||||||
|
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
|
className="mt-12">
|
||||||
|
<CTA href={waHref} label={comoEmpezar.cta} variant="light" />
|
||||||
|
</motion.div>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This mirrors `Mecanismo.tsx` exactly, but in the light-section palette (`bg-base`, accents in `gold` not `gold-bright`, `text-muted` for descriptions, `text-ink` for headings).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm test -- ComoEmpezar`
|
||||||
|
Expected: PASS, 2 tests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Wire `<ComoEmpezar />` into `app/page.tsx`**
|
||||||
|
|
||||||
|
Add the import with the other section imports:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ComoEmpezar } from '@/components/sections/ComoEmpezar';
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert `<ComoEmpezar />` between `<Estratega />` and `<Cierre />`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Estratega />
|
||||||
|
<ComoEmpezar />
|
||||||
|
<Cierre />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Typecheck and run the full suite**
|
||||||
|
|
||||||
|
Run: `npx tsc --noEmit && npm test`
|
||||||
|
Expected: tsc clean; all tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add components/sections/ComoEmpezar.tsx components/sections/__tests__/ComoEmpezar.test.tsx lib/content.ts app/page.tsx
|
||||||
|
git commit -m "feat: Cómo empezar section (07) — 3-step on-ramp to WhatsApp"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Footer (slim strip)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `components/layout/Footer.tsx`
|
||||||
|
- Create: `components/layout/__tests__/Footer.test.tsx`
|
||||||
|
- Modify: `lib/content.ts` (append `footer`)
|
||||||
|
- Modify: `app/page.tsx` (place `<Footer />` after `</main>`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `Wordmark` (`size`, `onDark`), `Container`, and `footer.tagline`/`footer.legal` from content.
|
||||||
|
- Produces: `Footer()` at `components/layout/Footer.tsx` — no props. Consumed by `app/page.tsx`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append the `footer` copy to `lib/content.ts`**
|
||||||
|
|
||||||
|
Add at the end of `lib/content.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const footer = {
|
||||||
|
tagline: 'tu estratega de crecimiento',
|
||||||
|
legal: '© 2026 creeer',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Footer.test.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Footer } from '../Footer';
|
||||||
|
|
||||||
|
describe('<Footer />', () => {
|
||||||
|
it('renders the tagline and the legal line', () => {
|
||||||
|
render(<Footer />);
|
||||||
|
expect(screen.getByText('tu estratega de crecimiento')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/© 2026 creeer/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('does not introduce a second h1', () => {
|
||||||
|
render(<Footer />);
|
||||||
|
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm test -- Footer`
|
||||||
|
Expected: FAIL with "Cannot find module '../Footer'".
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `components/layout/Footer.tsx`**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Wordmark } from '@/components/primitives/Wordmark';
|
||||||
|
import { Container } from '@/components/primitives/Container';
|
||||||
|
import { footer } from '@/lib/content';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-bg-dark text-cream border-t border-cream/10">
|
||||||
|
<Container className="flex flex-col items-center gap-3 py-10 text-center md:flex-row md:justify-between md:text-left">
|
||||||
|
<Wordmark size="sm" onDark />
|
||||||
|
<p className="text-sm text-muted-dark">{footer.tagline}</p>
|
||||||
|
<p className="text-xs text-muted-dark">{footer.legal}</p>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Slim dark strip (`bg-bg-dark text-cream`) with a subtle top divider. It deliberately does NOT repeat the contact links — `Cierre` already owns them, and a second dark block of contacts would be redundant.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm test -- Footer`
|
||||||
|
Expected: PASS, 2 tests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Wire `<Footer />` into `app/page.tsx`**
|
||||||
|
|
||||||
|
Add the import near the `Header` import:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Footer } from '@/components/layout/Footer';
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert `<Footer />` after the closing `</main>` and before the fragment close:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Typecheck and run the full suite**
|
||||||
|
|
||||||
|
Run: `npx tsc --noEmit && npm test`
|
||||||
|
Expected: tsc clean; all tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add components/layout/Footer.tsx components/layout/__tests__/Footer.test.tsx lib/content.ts app/page.tsx
|
||||||
|
git commit -m "feat: slim Footer strip with wordmark, tagline and legal line"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Sticky-header anchor offset + static build verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `components/sections/Cierre.tsx` (add `scroll-mt` to the `#contacto` anchor target)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the composed page from Tasks 1–3.
|
||||||
|
- Produces: a verified static export; the `#contacto` anchor target that clears the sticky header.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Locate the `#contacto` anchor in `components/sections/Cierre.tsx`**
|
||||||
|
|
||||||
|
Run: `grep -n 'id="contacto"' components/sections/Cierre.tsx`
|
||||||
|
Expected: one match — the element carrying `id="contacto"` (the closing section's outer element).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `scroll-mt-24` to that element's className**
|
||||||
|
|
||||||
|
On the element that has `id="contacto"`, add `scroll-mt-24` to its `className` so the sticky header (~60px) never overlaps the contact anchor when the Header/Hero CTAs jump to it. Example — if the element reads:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<section id="contacto" className="bg-bg-dark text-cream">
|
||||||
|
```
|
||||||
|
|
||||||
|
change it to:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<section id="contacto" className="bg-bg-dark text-cream scroll-mt-24">
|
||||||
|
```
|
||||||
|
|
||||||
|
(Only add the one utility class; do not change anything else in `Cierre.tsx`.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the full suite and typecheck**
|
||||||
|
|
||||||
|
Run: `npx tsc --noEmit && npm test`
|
||||||
|
Expected: tsc clean; all tests pass (Cierre's existing tests still green — the class addition changes nothing they assert).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run lint**
|
||||||
|
|
||||||
|
Run: `npx next lint`
|
||||||
|
Expected: "No ESLint warnings or errors".
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the static build and verify the output**
|
||||||
|
|
||||||
|
Run: `npm run build`
|
||||||
|
Expected: build succeeds. Then verify the composed page:
|
||||||
|
|
||||||
|
(The exported HTML is minified onto few lines, so count occurrences with `grep -o … | wc -l`, not `grep -c` which counts lines.)
|
||||||
|
|
||||||
|
Run: `grep -o '<section' out/index.html | wc -l` → expect `8` (Hero, FraseAncla, ParaQuien, Mecanismo, Disciplinas, Estratega, ComoEmpezar, Cierre — one `<section>` each).
|
||||||
|
Run: `grep -o '<h1' out/index.html | wc -l` → expect `1` (single h1, still the Hero).
|
||||||
|
Run: `grep -o '<header' out/index.html | wc -l` → expect `1`.
|
||||||
|
Run: `grep -o '<footer' out/index.html | wc -l` → expect `1`.
|
||||||
|
Run: `grep -o 'wa.me' out/index.html | wc -l` → expect at least `2` (Cierre's WhatsApp link + ComoEmpezar's prefilled link).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add components/sections/Cierre.tsx
|
||||||
|
git commit -m "fix: scroll-margin on #contacto so the sticky header never overlaps it"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After this plan
|
||||||
|
|
||||||
|
- The header/footer/on-ramp are complete. Remaining follow-ups (own branches): testimonials/social proof, results/case studies, FAQ, real OG image + branded favicon, real portrait, and the dueña's real contact details (the `wa.me/520000000000` placeholder in `lib/content.ts` still applies to both Cierre and the new ComoEmpezar CTA).
|
||||||
|
- Merge `feat/header-comoempezar-footer` into `main` once reviewed.
|
||||||
@@ -52,7 +52,7 @@ app/page.tsx # compose Header + ComoEmpezar + Footer
|
|||||||
- `<header>` with `sticky top-0 z-50`, light background (`bg-base/80` or `bg-pure/80`) + `backdrop-blur` + a subtle bottom border (`border-b border-ink/10`). The light bar stays legible while scrolling over both light and dark sections.
|
- `<header>` with `sticky top-0 z-50`, light background (`bg-base/80` or `bg-pure/80`) + `backdrop-blur` + a subtle bottom border (`border-b border-ink/10`). The light bar stays legible while scrolling over both light and dark sections.
|
||||||
- A `Container`-width inner row, `flex items-center justify-between`, vertical padding ~`py-3`/`py-4`.
|
- A `Container`-width inner row, `flex items-center justify-between`, vertical padding ~`py-3`/`py-4`.
|
||||||
- **Left:** `<Wordmark size="base" />` wrapped in an `<a href="#top">` (or the page top) — the wordmark ink color is the default (dark on the light bar).
|
- **Left:** `<Wordmark size="base" />` wrapped in an `<a href="#top">` (or the page top) — the wordmark ink color is the default (dark on the light bar).
|
||||||
- **Right:** the `CTA` primitive, `variant="light"`, label from `cierre.cta` (`"Define tu siguiente movimiento"` + `→`), `href="#contacto"`. On small screens the CTA stays but may use a slightly smaller padding/label; the wordmark + CTA layout is identical on mobile (no menu, no hamburger).
|
- **Right:** the `CTA` primitive, `variant="light"`, with a **short** label from a new `header.cta` content key (`"Hablemos"`, `→` appended by the primitive), `href="#contacto"`. Rationale: the full-length "Define tu siguiente movimiento" would overflow next to the wordmark on ~375px screens; a compact nav CTA fits mobile and desktop, and the full CTA still appears in the Hero and Cierre. Layout is identical on mobile (no menu, no hamburger).
|
||||||
- Semantic `<header>`; wrap the CTA link region in `<nav aria-label="Principal">`. No animation (nothing to gate on reduced motion).
|
- Semantic `<header>`; wrap the CTA link region in `<nav aria-label="Principal">`. No animation (nothing to gate on reduced motion).
|
||||||
|
|
||||||
**A11y:** the sticky header must not obscure anchor targets — ensure `#contacto` still scrolls into view (the closing section has ample padding; acceptable without extra scroll-margin, but `scroll-mt` on anchor targets may be added if visual testing shows clipping).
|
**A11y:** the sticky header must not obscure anchor targets — ensure `#contacto` still scrolls into view (the closing section has ample padding; acceptable without extra scroll-margin, but `scroll-mt` on anchor targets may be added if visual testing shows clipping).
|
||||||
@@ -94,6 +94,10 @@ app/page.tsx # compose Header + ComoEmpezar + Footer
|
|||||||
## Content additions (`lib/content.ts`)
|
## Content additions (`lib/content.ts`)
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
export const header = {
|
||||||
|
cta: 'Hablemos', // short nav CTA → #contacto (full CTA lives in Hero/Cierre)
|
||||||
|
};
|
||||||
|
|
||||||
export const comoEmpezar = {
|
export const comoEmpezar = {
|
||||||
eyebrow: 'cómo empezar',
|
eyebrow: 'cómo empezar',
|
||||||
title: 'Tu siguiente movimiento empieza con una conversación.', // Georgia-italic h2
|
title: 'Tu siguiente movimiento empieza con una conversación.', // Georgia-italic h2
|
||||||
|
|||||||
Reference in new issue
Block a user