feat(dolores): el nudo que no se deshace
This commit is contained in:
1 parent
0239d4ac15
commit
2fa1e73764
3 files changed
+100
-2
No files matched your search
@@ -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 <path> under reduced motion — motion.path would hide the drawing.
|
||||||
|
if (reduced) {
|
||||||
|
return (
|
||||||
|
<svg ref={ref} viewBox="0 0 190 140" className={className} aria-hidden="true" focusable="false">
|
||||||
|
{HEBRAS.map((h) => (
|
||||||
|
<path key={h.d} d={h.d} fill="none" stroke="currentColor" strokeWidth={h.ancho} strokeLinecap="round" />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg ref={ref} viewBox="0 0 190 140" className={className} aria-hidden="true" focusable="false">
|
||||||
|
{HEBRAS.map((h, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={h.d}
|
||||||
|
d={h.d}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={h.ancho}
|
||||||
|
strokeLinecap="round"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={enVista ? { pathLength: 1 } : { pathLength: 0 }}
|
||||||
|
transition={{ duration: 1.3, delay: i * 0.18, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { IlustracionNudo } from '../IlustracionNudo';
|
||||||
|
|
||||||
|
describe('<IlustracionNudo />', () => {
|
||||||
|
const matchMediaOriginal = window.matchMedia;
|
||||||
|
afterEach(() => {
|
||||||
|
window.matchMedia = matchMediaOriginal;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is decorative: hidden from assistive tech', () => {
|
||||||
|
const { container } = render(<IlustracionNudo />);
|
||||||
|
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(<IlustracionNudo />);
|
||||||
|
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 <path> 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(<IlustracionNudo />);
|
||||||
|
const paths = container.querySelectorAll('path');
|
||||||
|
expect(paths.length).toBeGreaterThanOrEqual(3);
|
||||||
|
paths.forEach((path) => {
|
||||||
|
expect(path).not.toHaveAttribute('stroke-dasharray');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Container } from '@/components/primitives/Container';
|
import { Container } from '@/components/primitives/Container';
|
||||||
|
import { IlustracionNudo } from '@/components/primitives/IlustracionNudo';
|
||||||
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
|
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
|
||||||
import { dolores } from '@/lib/content';
|
import { dolores } from '@/lib/content';
|
||||||
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
|
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
|
||||||
@@ -9,8 +10,9 @@ import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, moti
|
|||||||
export function Dolores() {
|
export function Dolores() {
|
||||||
const reduced = usePrefersReducedMotion();
|
const reduced = usePrefersReducedMotion();
|
||||||
return (
|
return (
|
||||||
<section className="bg-white">
|
<section className="bg-white relative overflow-hidden">
|
||||||
<Container className="py-24 md:py-32">
|
<IlustracionNudo className="pointer-events-none select-none absolute -top-6 right-4 w-64 md:w-96 text-azul-profundo/[0.08]" />
|
||||||
|
<Container className="relative py-24 md:py-32">
|
||||||
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{dolores.eyebrow}</motion.p>
|
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{dolores.eyebrow}</motion.p>
|
||||||
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
|
|||||||
Reference in new issue
Block a user