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>
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useState, type ReactNode } from 'react';
|
|
import useEmblaCarousel from 'embla-carousel-react';
|
|
import Autoplay from 'embla-carousel-autoplay';
|
|
import { usePrefersReducedMotion } from '@/lib/motion';
|
|
|
|
// Thin Embla wrapper shared by the hero's pain-point rotator and the mobile
|
|
// row of inflection points. Autoplay pauses on hover/focus/interaction, and
|
|
// never runs at all for users who asked for less motion — it's still a fully
|
|
// usable carousel (drag, dots, keyboard) with it off, just static.
|
|
export function Carrusel({
|
|
slides,
|
|
autoplayMs,
|
|
className = '',
|
|
slideClassName = '',
|
|
ariaLabel,
|
|
mostrarContador = false,
|
|
}: {
|
|
slides: ReactNode[];
|
|
autoplayMs?: number;
|
|
className?: string;
|
|
slideClassName?: string;
|
|
ariaLabel: string;
|
|
// "01 / 06" + flechas prev/next, en vez de puntos — el patrón de
|
|
// en-ti.com ("desliza para explorar") para carruseles con más peso.
|
|
mostrarContador?: boolean;
|
|
}) {
|
|
const reduced = usePrefersReducedMotion();
|
|
const plugins = autoplayMs && !reduced ? [Autoplay({ delay: autoplayMs, stopOnInteraction: false, stopOnMouseEnter: true })] : [];
|
|
const [emblaRef, embla] = useEmblaCarousel({ loop: true }, plugins);
|
|
const [selected, setSelected] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!embla) return;
|
|
const onSelect = () => setSelected(embla.selectedScrollSnap());
|
|
embla.on('select', onSelect);
|
|
onSelect();
|
|
return () => {
|
|
embla.off('select', onSelect);
|
|
};
|
|
}, [embla]);
|
|
|
|
const scrollTo = useCallback((i: number) => embla?.scrollTo(i), [embla]);
|
|
const scrollPrev = useCallback(() => embla?.scrollPrev(), [embla]);
|
|
const scrollNext = useCallback(() => embla?.scrollNext(), [embla]);
|
|
|
|
return (
|
|
<div className={`min-w-0 ${className}`}>
|
|
<div className="overflow-hidden" ref={emblaRef} role="group" aria-label={ariaLabel}>
|
|
<div className="flex">
|
|
{slides.map((slide, i) => (
|
|
<div key={i} className={`min-w-0 shrink-0 grow-0 basis-full ${slideClassName}`}>
|
|
{slide}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{slides.length > 1 && mostrarContador && (
|
|
<div className="mt-6 flex items-center justify-center gap-5">
|
|
<button type="button" onClick={scrollPrev} aria-label="Anterior"
|
|
className="grid place-items-center w-9 h-9 rounded-full border border-azul-profundo/20 text-azul-profundo transition-colors hover:border-dorado hover:text-dorado">
|
|
←
|
|
</button>
|
|
<p className="font-sans font-bold text-sm tabular-nums text-azul-profundo tracking-wider">
|
|
{String(selected + 1).padStart(2, '0')} <span className="text-gris-medio font-normal">/ {String(slides.length).padStart(2, '0')}</span>
|
|
</p>
|
|
<button type="button" onClick={scrollNext} aria-label="Siguiente"
|
|
className="grid place-items-center w-9 h-9 rounded-full border border-azul-profundo/20 text-azul-profundo transition-colors hover:border-dorado hover:text-dorado">
|
|
→
|
|
</button>
|
|
</div>
|
|
)}
|
|
{slides.length > 1 && !mostrarContador && (
|
|
<div className="mt-5 flex justify-center gap-2">
|
|
{slides.map((_, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => scrollTo(i)}
|
|
aria-label={`Ir a la tarjeta ${i + 1} de ${slides.length}`}
|
|
aria-current={selected === i}
|
|
className={`h-2 rounded-full transition-all duration-300 ${selected === i ? 'w-6 bg-dorado' : 'w-2 bg-gris-medio/50 hover:bg-gris-medio'}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|