/* global React */ const { useEffect, useRef, useState } = React; // ===== Logo ===== function ChicLogo({ size = "md", color, style }) { const cls = `chic-logo chic-logo--${size}`; return (
Centre
Hospitalier
International de
Calavi (CHIC)
); } // ===== Pills ===== function StatusPill({ status, dense }) { const open = status === "open"; return ( {dense ? (open ? "Ouvert" : "Bientôt") : (open ? "Disponible" : "Bientôt")} ); } // ===== Animated number counter (counts up when visible) ===== function Counter({ to = 0, duration = 1400, suffix = "", className = "", style }) { const [val, setVal] = useState(0); const ref = useRef(null); const started = useRef(false); useEffect(() => { const node = ref.current; if (!node) return; const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting && !started.current) { started.current = true; const start = performance.now(); const tick = t => { const p = Math.min(1, (t - start) / duration); // ease-out cubic const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(eased * to)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.4 }); obs.observe(node); return () => obs.disconnect(); }, [to, duration]); return {val.toLocaleString("fr-FR")}{suffix}; } // ===== Stat block ===== function Stat({ value, label, frac, prefix = "", suffix = "" }) { // value may include a fraction "openSpec/totalSpec" — split visually if (typeof value === "string" && value.includes("/")) { const [a, b] = value.split("/"); return (
/{b}
{label}
); } return (
{prefix}{suffix} {frac && /{frac}}
{label}
); } // ===== Reveal on scroll ===== function FadeUp({ children, delay = 0, as: As = "div", className = "", style }) { const [shown, setShown] = useState(false); const ref = useRef(null); useEffect(() => { const node = ref.current; if (!node) return; const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); obs.disconnect(); } }); }, { threshold: 0.1, rootMargin: "0px 0px -40px 0px" }); obs.observe(node); return () => obs.disconnect(); }, [delay]); return ( {children} ); } // ===== Crumbs ===== function Crumbs({ trail }) { return (
{trail.map((t, i) => ( {i > 0 && /} {i === trail.length - 1 ? {t.label} : {t.label}} ))}
); } // ===== Section header (eyebrow + title + lead + optional right) ===== function SectionHeader({ eyebrow, title, lead, right }) { return (
{eyebrow &&
{eyebrow}
} {title &&

{title}

} {lead &&

{lead}

}
{right &&
{right}
}
); } Object.assign(window, { ChicLogo, StatusPill, Counter, Stat, FadeUp, Crumbs, SectionHeader, });