// Composants UI partagés const { useEffect, useRef, useState } = React; // Reveal-on-scroll wrapper const Reveal = ({ children, delay = 0, className = '' }) => { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) {setShown(true);io.disconnect();} }); }, { threshold: 0.12, rootMargin: '0px 0px -80px 0px' }); io.observe(el); return () => io.disconnect(); }, []); return (
{children}
); }; // Eyebrow label const Eyebrow = ({ children, onInk, solo, style }) => (
{children}
); // Button const Btn = ({ kind = 'primary', children, onClick, onInk }) => ; // Inline link const InlineLink = ({ children, onClick }) => ; // Stats ribbon const Stats = () =>
{[ { num: '+38', sup: '%', lbl: 'Valorisation moyenne du foncier' }, { num: '120', sup: '+', lbl: 'Lots aménagés en Morbihan' }, { num: '15', sup: 'j', lbl: 'Pour une étude de potentiel' }, { num: '100', sup: '%', lbl: "D'accompagnement clé en main" }]. map((s, i) =>
{s.num}{s.sup}
{s.lbl}
)}
; // Avant / Après slider const BeforeAfter = ({ beforeSrc, afterSrc }) => { const wrap = useRef(null); const [pos, setPos] = useState(50); const dragging = useRef(false); const handle = (clientX) => { if (!wrap.current) return; const r = wrap.current.getBoundingClientRect(); const x = clientX - r.left; const p = Math.max(0, Math.min(100, x / r.width * 100)); setPos(p); }; useEffect(() => { const move = (e) => { if (!dragging.current) return; const x = e.touches ? e.touches[0].clientX : e.clientX; handle(x); }; const up = () => {dragging.current = false;}; window.addEventListener('mousemove', move); window.addEventListener('touchmove', move); window.addEventListener('mouseup', up); window.addEventListener('touchend', up); return () => { window.removeEventListener('mousemove', move); window.removeEventListener('touchmove', move); window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up); }; }, []); return (
{dragging.current = true;handle(e.clientX);}} onTouchStart={(e) => {dragging.current = true;handle(e.touches[0].clientX);}}> Avant
Après
Avant Après
); }; // Section heading bloc à 2 colonnes const SectionHead = ({ eyebrow, title, lede, onInk }) =>
{eyebrow && {eyebrow}}

{lede &&

{lede}

}
; Object.assign(window, { Reveal, Eyebrow, Btn, InlineLink, Stats, BeforeAfter, SectionHead });