feat(grafico): HiloConductor — la línea dorada que cose la página
This commit is contained in:
1 parent
6783e32324
commit
a900bdd99d
3 files changed
+105
-1
No files matched your search
+3
-1
@@ -1,5 +1,6 @@
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { HiloConductor } from '@/components/primitives/HiloConductor';
|
||||
import { Hero } from '@/components/sections/Hero';
|
||||
import { FraseAncla } from '@/components/sections/FraseAncla';
|
||||
import { PuntosInflexion } from '@/components/sections/PuntosInflexion';
|
||||
@@ -24,7 +25,8 @@ export default function Home() {
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
<Header />
|
||||
<main>
|
||||
<main className="relative">
|
||||
<HiloConductor />
|
||||
<Hero variant="editorial" /> {/* dueña puede cambiar a "hormozi" */}
|
||||
<FraseAncla />
|
||||
<PuntosInflexion />
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useScroll, useSpring } from 'framer-motion';
|
||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
||||
|
||||
// One continuous gold line running the height of the page, drawing itself as
|
||||
// the reader scrolls. It is the spine of the layout: it stitches the sections
|
||||
// together so the landing reads as a route rather than a stack of blocks.
|
||||
//
|
||||
// Positioned `absolute`, never `fixed` — the thread belongs to the document
|
||||
// and scrolls with it. A fixed layer would read as an ornament stuck to the
|
||||
// window.
|
||||
//
|
||||
// Uses the page-level useScroll() with no target ref on purpose: no element
|
||||
// measuring, no "container has a static position" warnings, and one scroll
|
||||
// listener for the whole page.
|
||||
|
||||
const TRAZO =
|
||||
'M50 0 C 20 140, 80 260, 50 400 C 20 540, 80 660, 50 800 C 20 940, 80 1060, 50 1200';
|
||||
|
||||
export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
const reduced = usePrefersReducedMotion();
|
||||
const { scrollYProgress } = useScroll();
|
||||
const avance = useSpring(scrollYProgress, { stiffness: 90, damping: 30, restDelta: 0.001 });
|
||||
|
||||
const base = `pointer-events-none absolute inset-y-0 left-1/2 -translate-x-1/2 w-full max-w-content h-full ${className}`;
|
||||
|
||||
// The positioning/pointer-events classes live on a wrapping <div> rather
|
||||
// than the <svg> itself: per spec `SVGElement.className` is an
|
||||
// SVGAnimatedString, not a plain string, so tests (and any other reader of
|
||||
// `.className`) can't inspect it as text. A plain HTMLElement keeps that
|
||||
// contract simple.
|
||||
|
||||
// Plain <path> for reduced motion: motion.path applies dash scaffolding even
|
||||
// without initial/animate and would leave the thread invisible.
|
||||
if (reduced) {
|
||||
return (
|
||||
<div className={base}>
|
||||
<svg
|
||||
className="h-full w-full text-dorado/25"
|
||||
viewBox="0 0 100 1200"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d={TRAZO} fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={base}>
|
||||
<svg
|
||||
className="h-full w-full text-dorado/25"
|
||||
viewBox="0 0 100 1200"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<motion.path
|
||||
d={TRAZO}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
style={{ pathLength: avance }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HiloConductor } from '../HiloConductor';
|
||||
|
||||
describe('<HiloConductor />', () => {
|
||||
it('is decorative: hidden from assistive tech', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
const svg = container.querySelector('svg');
|
||||
expect(svg).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(svg).toHaveAttribute('focusable', 'false');
|
||||
});
|
||||
|
||||
it('draws a single continuous thread', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
expect(container.querySelectorAll('path')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('travels with the document instead of sticking to the viewport', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
// `fixed` would read as an ornament pinned to the window; the thread
|
||||
// belongs to the page and must scroll with it.
|
||||
expect(container.firstElementChild?.className).toContain('absolute');
|
||||
expect(container.firstElementChild?.className).not.toContain('fixed');
|
||||
});
|
||||
|
||||
it('stays out of the way of pointer input', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
expect(container.firstElementChild?.className).toContain('pointer-events-none');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user