feat(para-quien): la bifurcación dibujada junto al split sí/no
La ruta del 'no' se revela por opacidad (no por pathLength): el stroke dasharray '1 8' sobrevive y el camino queda realmente punteado, en lugar de que framer-motion lo pise con el dasharray que usa para dibujar el trazo. El test afina la aserción al valor '1 8' para no contar la inyección de pathLength en las dos rutas sólidas.
This commit is contained in:
1 parent
2fa1e73764
commit
ca949f35c2
3 files changed
+101
-2
No files matched your search
@@ -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 = (
|
||||
<>
|
||||
<circle cx={110} cy={70} r={5} fill="currentColor" className="text-dorado" />
|
||||
<circle cx={110} cy={70} r={9} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={1} strokeOpacity={0.4} />
|
||||
</>
|
||||
);
|
||||
|
||||
// Plain <path> under reduced motion — motion.path would hide the drawing.
|
||||
if (reduced) {
|
||||
return (
|
||||
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
||||
<path d={TRONCO} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2.5} strokeLinecap="round" />
|
||||
<path d={RUTA_NO} fill="none" stroke="currentColor" className="text-gris-medio" strokeWidth={2} strokeLinecap="round" strokeDasharray="1 8" />
|
||||
<path d={RUTA_SI} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
|
||||
{punto}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
||||
<motion.path d={TRONCO} fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={2.5} strokeLinecap="round" {...dibuja(0)} />
|
||||
<motion.path d={RUTA_NO} fill="none" stroke="currentColor" className="text-gris-medio" strokeWidth={2} strokeLinecap="round" strokeDasharray="1 8" {...punteada} />
|
||||
<motion.path d={RUTA_SI} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" {...dibuja(0.55)} />
|
||||
{punto}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { IlustracionBifurcacion } from '../IlustracionBifurcacion';
|
||||
|
||||
describe('<IlustracionBifurcacion />', () => {
|
||||
it('is decorative: hidden from assistive tech', () => {
|
||||
const { container } = render(<IlustracionBifurcacion />);
|
||||
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(<IlustracionBifurcacion />);
|
||||
expect(container.querySelectorAll('path')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('tells the two routes apart: one solid, one dashed', () => {
|
||||
const { container } = render(<IlustracionBifurcacion />);
|
||||
// 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(<IlustracionBifurcacion />);
|
||||
expect(container.querySelectorAll('circle').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<section className="bg-white relative overflow-hidden">
|
||||
<Container className="relative pt-24 md:pt-32 pb-12 md:pb-16">
|
||||
<Container className="relative pt-24 md:pt-32 pb-12 md:pb-16 md:flex md:items-center md:justify-between md:gap-12">
|
||||
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||
className="font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] max-w-4xl tracking-tight">{paraQuien.title}</motion.h2>
|
||||
className="font-sans font-black text-4xl md:text-6xl text-azul-profundo leading-[0.95] max-w-3xl tracking-tight">{paraQuien.title}</motion.h2>
|
||||
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||
className="mt-8 md:mt-0 shrink-0">
|
||||
<IlustracionBifurcacion className="w-56 md:w-72 h-auto mx-auto" />
|
||||
</motion.div>
|
||||
</Container>
|
||||
{/* Pantalla partida: luz contra oscuridad, para dramatizar la bifurcación
|
||||
"sí es para ti / no es para ti" — sale del ancho del Container a
|
||||
|
||||
Reference in new issue
Block a user