/* Sipho website — shared chrome + helpers. Loaded on every page. Exposes Icon, Nav, Footer, CountUp, Reveal, SectionHead to window. */ const { useState, useEffect, useRef } = React; /* ---------- Lucide icon helper ---------- */ function Icon({ name, className }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || !window.lucide) return; el.innerHTML = ''; window.lucide.createIcons(); }, [name]); return ; } /* ---------- Scroll reveal ---------- Visible state is the BASE style (see .reveal in CSS) so content can never be trapped invisible. The entrance animation is driven by adding classes via classList in a one-time effect — NOT via React state — so re-renders never restart or interrupt the transition. */ function Reveal({ children, as = 'div', delay = 0, className = '', ...rest }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; el.classList.add('reveal-pre'); let done = false, t = null; const cleanup = () => { window.removeEventListener('scroll', check, true); window.removeEventListener('resize', check); if (t) clearTimeout(t); }; const reveal = () => { if (done) return; done = true; el.classList.add('reveal-in'); cleanup(); }; const check = () => { const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || 800) * 0.94) requestAnimationFrame(reveal); }; requestAnimationFrame(check); window.addEventListener('scroll', check, true); window.addEventListener('resize', check); t = setTimeout(reveal, 2200); // safety: never leave content hidden return cleanup; }, []); const Tag = as; return ( {children} ); } /* ---------- Count up on view ---------- */ function CountUp({ to, decimals = 0, suffix = '', prefix = '' }) { const [val, setVal] = useState(0); const ref = useRef(null); useEffect(() => { let raf, started = false; if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setVal(to); return; } const el = ref.current; if (!el) { setVal(to); return; } let t = null; const run = () => { if (started) return; started = true; window.removeEventListener('scroll', check, true); if (t) clearTimeout(t); const start = performance.now(), dur = 1200; const tick = (now) => { const p = Math.min(1, (now - start) / dur); const e = 1 - Math.pow(1 - p, 3); setVal(to * e); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); }; const check = () => { const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || 800) * 0.9) run(); }; check(); window.addEventListener('scroll', check, true); t = setTimeout(run, 2200); return () => { window.removeEventListener('scroll', check, true); if (t) clearTimeout(t); cancelAnimationFrame(raf); }; }, [to]); const loc = (typeof langLocale === 'function') ? langLocale() : 'en-US'; return {prefix}{val.toLocaleString(loc, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}; } /* ---------- Section head ---------- */ function SectionHead({ eyebrow, title, lead, center }) { return ( {eyebrow &&

{tr(eyebrow)}

}

{tr(title)}

{lead &&

{tr(lead)}

}
); } /* ---------- NAV ---------- */ const NAV_ITEMS = [ { href: 'index.html', label: 'Home', key: 'home' }, { href: 'product.html', label: 'Product', key: 'product' }, { href: 'impact.html', label: 'Impact', key: 'impact' }, { href: 'history.html', label: 'History', key: 'history' }, { href: 'about.html', label: 'Our story', key: 'about' }, { href: 'contact.html', label: 'Contact', key: 'contact' }, ]; function Nav() { useLang(); const active = window.SIPHO_PAGE || 'home'; const [open, setOpen] = useState(false); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return (
Sipho
{tr('Want to join us?')}
{open && (
{NAV_ITEMS.map(it => ( {tr(it.label)} ))} {tr('Want to join us?')}
)}
); } /* ---------- FOOTER ---------- */ function Footer() { useLang(); const cols = [ { h: 'Product', links: [['Reusable plunger for medical syringes', 'product.html'], ['How it works', 'product.html#how'], ['Compatibility', 'product.html#solution'], ['Development status', 'product.html#status']] }, { h: 'Company', links: [['Our story', 'about.html'], ['The team', 'about.html#team'], ['History', 'history.html'], ['Contact', 'contact.html']] }, { h: 'Evidence', links: [['Impact & CO₂e', 'impact.html'], ['Lifecycle', 'impact.html#lifecycle'], ['Transparency', 'impact.html#transparency'], ['Test log', 'product.html#status']] }, ]; return ( ); } Object.assign(window, { Icon, Reveal, CountUp, SectionHead, Nav, Footer });