diff --git a/components/primitives/IlustracionBifurcacion.tsx b/components/primitives/IlustracionBifurcacion.tsx new file mode 100644 index 0000000..5b4757d --- /dev/null +++ b/components/primitives/IlustracionBifurcacion.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { useEnVista } from '@/lib/motion'; + +// A shared trunk splitting at a marked decision point: one route climbs, gold +// and continuous; the other flattens, grey and dashed. It draws the section's +// own bifurcation — "sí es para ti / no es para ti" — without illustrating a +// judgement about the reader. +const TRONCO = 'M10 100 C 50 96, 80 84, 110 70'; +const RUTA_SI = 'M110 70 C 145 58, 175 30, 210 18'; +const RUTA_NO = 'M110 70 C 145 82, 175 92, 210 90'; + +export function IlustracionBifurcacion({ className = '' }: { className?: string }) { + const { ref, enVista, reduced } = useEnVista(); + + const punto = ( + <> + + + + ); + + // Plain under reduced motion — motion.path would hide the drawing. + if (reduced) { + return ( + + ); + } + + const dibuja = (delay: number) => ({ + initial: { pathLength: 0 }, + animate: enVista ? { pathLength: 1 } : { pathLength: 0 }, + transition: { duration: 1.1, delay, ease: [0.22, 1, 0.36, 1] as const }, + }); + + // The "no" route is dashed, and a dashed stroke cannot share Framer Motion's + // pathLength reveal: pathLength drives `stroke-dasharray` itself and would + // overwrite the "1 8" dashes. It fades in by opacity instead, so the dashes + // it is drawn with are the dashes it keeps. + const punteada = { + initial: { opacity: 0 }, + animate: enVista ? { opacity: 1 } : { opacity: 0 }, + transition: { duration: 0.8, delay: 0.55, ease: [0.22, 1, 0.36, 1] as const }, + }; + + return ( + + ); +} diff --git a/components/primitives/__tests__/IlustracionBifurcacion.test.tsx b/components/primitives/__tests__/IlustracionBifurcacion.test.tsx new file mode 100644 index 0000000..adf6d06 --- /dev/null +++ b/components/primitives/__tests__/IlustracionBifurcacion.test.tsx @@ -0,0 +1,34 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { IlustracionBifurcacion } from '../IlustracionBifurcacion'; + +describe('', () => { + it('is decorative: hidden from assistive tech', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg).toHaveAttribute('aria-hidden', 'true'); + expect(svg).toHaveAttribute('focusable', 'false'); + }); + + it('draws one shared trunk and two diverging routes', () => { + const { container } = render(); + expect(container.querySelectorAll('path')).toHaveLength(3); + }); + + it('tells the two routes apart: one solid, one dashed', () => { + const { container } = render(); + // Framer Motion's pathLength reveal injects its own `stroke-dasharray` + // attribute on every animated path, so we can't count the attribute's + // presence. The genuinely dashed route carries the real "1 8" value — + // that is what marks it apart from the two solid routes. + const punteadas = Array.from(container.querySelectorAll('path')).filter( + (p) => p.getAttribute('stroke-dasharray') === '1 8', + ); + expect(punteadas).toHaveLength(1); + }); + + it('marks the decision point', () => { + const { container } = render(); + expect(container.querySelectorAll('circle').length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/components/sections/ParaQuien.tsx b/components/sections/ParaQuien.tsx index b7e06e3..4a9ccb6 100644 --- a/components/sections/ParaQuien.tsx +++ b/components/sections/ParaQuien.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion'; import { Container } from '@/components/primitives/Container'; +import { IlustracionBifurcacion } from '@/components/primitives/IlustracionBifurcacion'; import { paraQuien } from '@/lib/content'; import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion'; @@ -9,9 +10,13 @@ export function ParaQuien() { const reduced = usePrefersReducedMotion(); return (
- + {paraQuien.title} + className="font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] max-w-3xl tracking-tight">{paraQuien.title} + + + {/* Pantalla partida: luz contra oscuridad, para dramatizar la bifurcación "sí es para ti / no es para ti" — sale del ancho del Container a