// Shared atoms: Placeholder image, icons, Reveal wrapper, nav link factory. // Each component is lightweight — no external deps beyond React. const Ph = ({ label, variant = "", className = "", style, src, alt, position = "center" }) => (
{src && {alt} {label && !src &&
{label}
}
); const Icon = ({ name, size = 16 }) => { const s = { width: size, height: size, display: "inline-block", verticalAlign: "middle" }; const p = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case "phone": return ; case "arrow": return ; case "clock": return ; case "pin": return ; case "mail": return ; case "star": return ; case "leaf": return ; case "x": return ; case "award": return ; case "utensils": return ; case "wine": return ; case "chat": return ; default: return null; } }; // Apparition au défilement — IntersectionObserver ajoute la classe 'in'. // Les blocs démarrent à opacity:0 : si l'observateur ne se déclenche jamais, le // contenu resterait invisible. Deux filets de sécurité ci-dessous. const Reveal = ({ children, delay = 0, as: Tag = "div", className = "", ...rest }) => { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const show = () => el.classList.add("in"); // 1. Navigateur sans IntersectionObserver → on affiche tout de suite. if (typeof IntersectionObserver === "undefined") { show(); return; } const io = new IntersectionObserver( (entries) => entries.forEach(e => e.isIntersecting && show()), { threshold: 0.15, rootMargin: "0px 0px -60px 0px" } ); io.observe(el); // 2. Déjà visible au chargement (haut de page, grand écran) → on n'attend pas // le premier callback, qui peut tarder d'une frame ou deux. const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || 0) && r.bottom > 0) show(); return () => io.disconnect(); }, []); const delayClass = delay ? ` reveal-delay-${delay}` : ""; return {children}; }; // Parallax — translateY based on scroll relative to element top const Parallax = ({ children, speed = 0.15, className = "" }) => { const ref = React.useRef(null); const innerRef = React.useRef(null); React.useEffect(() => { const el = ref.current, inner = innerRef.current; if (!el || !inner) return; let raf = 0; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; const r = el.getBoundingClientRect(); const vh = window.innerHeight; const progress = (r.top + r.height/2 - vh/2) / vh; // -1..1 ish inner.style.transform = `translate3d(0, ${progress * speed * 100}px, 0)`; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [speed]); return (
{children}
); }; Object.assign(window, { Ph, Icon, Reveal, Parallax });