feat(primitivas): capa gráfica y de movimiento reutilizable
Diez primitivas que dan a la landing el "gráfico y movimiento" que pidió la dueña, todas con guarda de prefers-reduced-motion: - FondoPuntos / PatronPuntos — el patrón de puntos del manual - TarjetaSpotlight, TextoDesenfoque, ContadorNumero — realce y reveal - Carrusel — deslizable, con contador, autoplay pausable - Icono, SimboloTresLineas, IlustracionInflexion — línea, sin relleno - FondoFoto — duotono azul/dorado sobre foto useAutoStep en lib/motion.ts comparte la forma del autoplay del carrusel para indicadores de paso que no son carrusel. IlustracionInflexion documenta un detalle que muerde: motion.path aplica su propio andamiaje de dasharray aunque no reciba initial/animate, así que la rama de reduced motion renderiza <path> planos en vez de intentar neutralizar los animados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9758a92f9
commit
5b400cb161
19 files changed
+881
No files matched your search
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
||||
|
||||
// A literal small illustration of the section's own idea: a business grows,
|
||||
// hits its inflection point (the dot), and from there either drifts flat
|
||||
// without direction (grey, dashed) or keeps climbing with a clear strategy
|
||||
// (dorado). Draws itself in once scrolled into view; a decorative aside, not
|
||||
// data, so it stays purely presentational.
|
||||
export function IlustracionInflexion({ className = '' }: { className?: string }) {
|
||||
const reduced = usePrefersReducedMotion();
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
const [inView, setInView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || reduced) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setInView(true);
|
||||
observer.unobserve(el);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.4 },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [reduced]);
|
||||
|
||||
// `motion.path` applies its own dash-based pathLength scaffolding even with
|
||||
// no initial/animate props, which left the lines invisible for
|
||||
// reduced-motion users — so that branch renders plain, non-motion <path>s
|
||||
// instead of trying to neutralise the animated ones.
|
||||
if (reduced) {
|
||||
return (
|
||||
<svg viewBox="0 0 320 180" className={className} aria-hidden="true" focusable="false">
|
||||
<path d="M20 140 C 70 150, 110 40, 165 60" fill="none" stroke="currentColor" className="text-azul-profundo" strokeWidth={3} strokeLinecap="round" />
|
||||
<path d="M165 60 C 210 76, 250 92, 300 88" fill="none" stroke="currentColor" className="text-gris-medio" strokeWidth={2.5} strokeLinecap="round" strokeDasharray="1 9" />
|
||||
<path d="M165 60 C 205 48, 245 8, 300 14" fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
|
||||
<circle cx="165" cy="60" r="6" fill="currentColor" className="text-dorado" />
|
||||
<circle cx="165" cy="60" r="10" fill="none" stroke="currentColor" className="text-dorado" strokeWidth={1} strokeOpacity={0.4} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const draw = (delay: number) => ({
|
||||
initial: { pathLength: 0 },
|
||||
animate: inView ? { pathLength: 1 } : { pathLength: 0 },
|
||||
transition: { duration: 1.1, delay, ease: [0.22, 1, 0.36, 1] as const },
|
||||
});
|
||||
|
||||
return (
|
||||
<svg ref={ref} viewBox="0 0 320 180" className={className} aria-hidden="true" focusable="false">
|
||||
<motion.path
|
||||
d="M20 140 C 70 150, 110 40, 165 60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
className="text-azul-profundo"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
{...draw(0)}
|
||||
/>
|
||||
<motion.path
|
||||
d="M165 60 C 210 76, 250 92, 300 88"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
className="text-gris-medio"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="1 9"
|
||||
{...draw(0.5)}
|
||||
/>
|
||||
<motion.path
|
||||
d="M165 60 C 205 48, 245 8, 300 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
className="text-dorado"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
{...draw(0.5)}
|
||||
/>
|
||||
<circle cx="165" cy="60" r="6" fill="currentColor" className="text-dorado" />
|
||||
<circle cx="165" cy="60" r="10" fill="none" stroke="currentColor" className="text-dorado" strokeWidth={1} strokeOpacity={0.4} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user