diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index d255148..483457b 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -1,19 +1,41 @@ +'use client'; + +import { useState } from 'react'; +import { useScroll, useMotionValueEvent } from 'framer-motion'; import { Wordmark } from '@/components/primitives/Wordmark'; import { CTA } from '@/components/primitives/CTA'; import { Container } from '@/components/primitives/Container'; +import { ProgresoLectura } from '@/components/primitives/ProgresoLectura'; import { header } from '@/lib/content'; +// The header tightens once the reader leaves the hero: less chrome, more page. +// Client component because the compaction needs scroll state on the
+// itself; there is no server-rendered data here to lose. export function Header() { + const [compacto, setCompacto] = useState(false); + const { scrollY } = useScroll(); + + useMotionValueEvent(scrollY, 'change', (y) => { + // Hysteresis: compact past 120px, expand back below 60px, so a header + // sitting exactly on the threshold doesn't flicker. + setCompacto((actual) => (actual ? y > 60 : y > 120)); + }); + return ( -
- - - +
+ + + +
); } diff --git a/components/layout/__tests__/Header.test.tsx b/components/layout/__tests__/Header.test.tsx index 97306b7..37932a3 100644 --- a/components/layout/__tests__/Header.test.tsx +++ b/components/layout/__tests__/Header.test.tsx @@ -14,4 +14,15 @@ describe('
', () => { const cta = screen.getByRole('link', { name: /Hablemos/i }); expect(cta).toHaveAttribute('href', '#contacto'); }); + it('shows reading progress', () => { + const { container } = render(
); + expect(container.querySelector('.bg-dorado')).toBeInTheDocument(); + }); + + it('starts roomy, before any scrolling has happened', () => { + const { container } = render(
); + // The header compacts on scroll; at rest it must be in its tall state, or + // the wordmark would land already shrunken on first paint. + expect(container.querySelector('header')?.className).toContain('py-3'); + }); }); diff --git a/components/primitives/ProgresoLectura.tsx b/components/primitives/ProgresoLectura.tsx new file mode 100644 index 0000000..a837c1e --- /dev/null +++ b/components/primitives/ProgresoLectura.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { motion, useScroll, useSpring } from 'framer-motion'; +import { usePrefersReducedMotion } from '@/lib/motion'; + +// Reading progress along the bottom edge of the header. Pure motion feedback: +// under reduced motion it renders nothing at all, because a static progress +// bar communicates nothing — unlike an illustration, there is no meaningful +// still frame of it. +export function ProgresoLectura({ className = '' }: { className?: string }) { + const reduced = usePrefersReducedMotion(); + const { scrollYProgress } = useScroll(); + const avance = useSpring(scrollYProgress, { stiffness: 120, damping: 30, restDelta: 0.001 }); + + if (reduced) return null; + + return ( +