diff --git a/components/primitives/IlustracionNudo.tsx b/components/primitives/IlustracionNudo.tsx new file mode 100644 index 0000000..a3ba20b --- /dev/null +++ b/components/primitives/IlustracionNudo.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { useEnVista } from '@/lib/motion'; + +// Four strands looping over each other into a knot that never opens — the +// section's own subject drawn: effort that folds back on itself instead of +// going anywhere. The strands draw themselves in sequence, which reads as the +// knot tightening rather than as decoration appearing. +const HEBRAS = [ + { d: 'M30 90 C 60 20, 130 20, 140 70 C 148 112, 78 118, 70 78 C 63 44, 120 38, 150 66', ancho: 2.5 }, + { d: 'M20 60 C 70 110, 120 110, 160 60', ancho: 2 }, + { d: 'M40 110 C 80 40, 110 130, 155 95', ancho: 2 }, + { d: 'M25 40 C 90 70, 100 100, 165 40', ancho: 1.5 }, +]; + +export function IlustracionNudo({ className = '' }: { className?: string }) { + const { ref, enVista, reduced } = useEnVista(); + + // Plain under reduced motion — motion.path would hide the drawing. + if (reduced) { + return ( + + ); + } + + return ( + + ); +} diff --git a/components/primitives/__tests__/IlustracionNudo.test.tsx b/components/primitives/__tests__/IlustracionNudo.test.tsx new file mode 100644 index 0000000..4bb4842 --- /dev/null +++ b/components/primitives/__tests__/IlustracionNudo.test.tsx @@ -0,0 +1,48 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect, afterEach } from 'vitest'; +import { IlustracionNudo } from '../IlustracionNudo'; + +describe('', () => { + const matchMediaOriginal = window.matchMedia; + afterEach(() => { + window.matchMedia = matchMediaOriginal; + }); + + 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('tangles several strands rather than drawing one line', () => { + const { container } = render(); + expect(container.querySelectorAll('path').length).toBeGreaterThanOrEqual(3); + }); + + it('renders plain, fully-visible strands under reduced motion instead of motion.path\'s dash scaffolding', () => { + // Force the reduced-motion branch. motion.path applies stroke-dasharray/ + // stroke-dashoffset even with no explicit initial/animate resolution, + // which leaves the drawing invisible for reduced-motion users — see + // lib/motion.ts. The reduced branch must use plain elements + // instead, so none of them should carry that dash scaffolding. + window.matchMedia = (query: string) => + ({ + matches: true, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, + }) as unknown as MediaQueryList; + + const { container } = render(); + const paths = container.querySelectorAll('path'); + expect(paths.length).toBeGreaterThanOrEqual(3); + paths.forEach((path) => { + expect(path).not.toHaveAttribute('stroke-dasharray'); + }); + }); +}); diff --git a/components/sections/Dolores.tsx b/components/sections/Dolores.tsx index 2465aa4..4632bf4 100644 --- a/components/sections/Dolores.tsx +++ b/components/sections/Dolores.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion'; import { Container } from '@/components/primitives/Container'; +import { IlustracionNudo } from '@/components/primitives/IlustracionNudo'; import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight'; import { dolores } from '@/lib/content'; import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion'; @@ -9,8 +10,9 @@ import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, moti export function Dolores() { const reduced = usePrefersReducedMotion(); return ( -
- +
+ + {dolores.eyebrow}