Primitivas y contenido del rediseño (escala tipográfica, Revelar, MarcaSeccion, Seccion, Antetitulo, SimboloMarca, Resplandor, ClaridadHero, sección Proceso de ocho etapas, lib/content.ts reestructurado en seis secciones con alias para las viejas). Mecanismo y FormaTrabajar se retiran. Encima, lo de esta sesión: - recorrido: creeer vuelve a responder cada situación en vez de repetirla (el alias apuntaba respuesta a situacion); respuestas en plural. Créditos de las seis fotos de México. - contacto: WhatsApp, email (con mailto:) y LinkedIn reales; iconos de las redes en el cierre y el footer (IconoRed, trazos de Simple Icons, CC0). - enlaces: lo externo abre en otra pestaña con noopener y aviso para lectores de pantalla (lib/enlaces.ts); mailto y anclas se quedan. - Proceso: las ocho tarjetas no aparecían nunca en móvil (amount 0.3 sobre una lista de ~3000px); ahora cada una se revela sola. El ≡ de la 08 ya no tapa un entregable, título equilibrado, etiqueta a 12px, sin elevar al hover lo que no es clicable. - Disciplinas: la frase «No son la promesa — son los medios» gana peso y se agrupa con título e ilustración; sin cortes huérfanos en móvil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import type { Variants } from 'framer-motion';
|
|
|
|
// EaseOutExpo — smooth editorial deceleration
|
|
export const easeOutExpo = [0.22, 1, 0.36, 1] as const;
|
|
|
|
export const fadeInUp: Variants = {
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: easeOutExpo } },
|
|
};
|
|
|
|
export const staggerContainer: Variants = {
|
|
hidden: {},
|
|
visible: { transition: { staggerChildren: 0.1 } },
|
|
};
|
|
|
|
export const viewportOnce = { once: true, amount: 0.3 } as const;
|
|
|
|
// Hook: returns true if user prefers reduced motion. Components use this to
|
|
// disable Framer Motion animations (per spec, a11y requirement).
|
|
export function usePrefersReducedMotion() {
|
|
const [reduced, setReduced] = useState(false);
|
|
useEffect(() => {
|
|
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
setReduced(mq.matches);
|
|
const handler = () => setReduced(mq.matches);
|
|
mq.addEventListener('change', handler);
|
|
return () => mq.removeEventListener('change', handler);
|
|
}, []);
|
|
return reduced;
|
|
}
|
|
|
|
// Shared "draw once when scrolled into view" wiring for the SVG illustrations.
|
|
// Returns the ref to attach, whether the element has been seen, and the
|
|
// reduced-motion flag the caller needs anyway to pick its render branch.
|
|
// Observes nothing under reduced motion — those branches render statically.
|
|
export function useEnVista(umbral = 0.4) {
|
|
const reduced = usePrefersReducedMotion();
|
|
const ref = useRef<SVGSVGElement>(null);
|
|
const [enVista, setEnVista] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el || reduced) return;
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting) {
|
|
setEnVista(true);
|
|
observer.unobserve(el);
|
|
}
|
|
},
|
|
{ threshold: umbral },
|
|
);
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, [reduced, umbral]);
|
|
|
|
return { ref, enVista, reduced };
|
|
}
|
|
|
|
// Pass to <motion.*> as motion-reduced guard. When reduced, variants resolve to visible immediately.
|
|
export const motionProps = (reduced: boolean) =>
|
|
reduced
|
|
? { initial: 'visible' as const, animate: 'visible' as const, whileInView: undefined, viewport: undefined }
|
|
: {};
|
|
|
|
// Cycles through `count` steps on its own (ComoEmpezar): each
|
|
// manual change restarts the dwell timer, hovering pauses it, and it never
|
|
// advances at all for reduced-motion users.
|
|
export function useAutoStep(count: number, intervalMs = 3500) {
|
|
const reduced = usePrefersReducedMotion();
|
|
const [active, setActive] = useState(0);
|
|
const [paused, setPaused] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (reduced || paused || count <= 1) return;
|
|
const id = setTimeout(() => setActive((a) => (a + 1) % count), intervalMs);
|
|
return () => clearTimeout(id);
|
|
}, [active, reduced, paused, count, intervalMs]);
|
|
|
|
return {
|
|
active,
|
|
setActive,
|
|
hoverProps: { onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false) },
|
|
};
|
|
}
|