// 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 &&

}
{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 (
);
};
Object.assign(window, { Ph, Icon, Reveal, Parallax });