/* Shared components for the prototype. Exposed on window for other babel scripts. */
const { useState, useEffect, useRef, useCallback } = React;
/* ---- hooks ---- */
function useInView(opts) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
const el = ref.current; if (!el) return;
if (typeof IntersectionObserver === "undefined") { setSeen(true); return; }
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.unobserve(e.target); } });
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px", ...(opts || {}) });
io.observe(el);
return () => io.disconnect();
}, []);
return [ref, seen];
}
/* ---- Reveal: scroll-in wrapper ---- */
function Reveal({ children, delay, as, className = "", ...rest }) {
const [ref, seen] = useInView();
const Tag = as || "div";
return (
{children}
);
}
/* ---- animated counter ---- */
function Counter({ to, suffix = "", text, dur = 1600 }) {
const [ref, seen] = useInView({ threshold: 0.5 });
const [val, setVal] = useState(0);
useEffect(() => {
if (!seen || text) return;
let raf, start;
const step = (ts) => {
if (!start) start = ts;
const p = Math.min((ts - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
setVal(Math.round(eased * to));
if (p < 1) raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [seen, to, text, dur]);
return (
{text ? text : <>{val}{suffix}>}
);
}
/* ---- icons (minimal, geometric) ---- */
const Arrow = (p) => (
);
const Play = (p) => (
);
/* ---- image placeholder ---- */
function Ph({ motif, tone = "ph--sage", ratio, className = "", style }) {
return (
Bild · {motif}
);
}
/* ---- section heading ---- */
function SecHead({ eyebrow, title, children, center, light, className = "" }) {
return (
{eyebrow && {eyebrow}}
{title}
{children && {children}
}
);
}
const SunDivider = () => (
);
/* ---- Header ---- */
function Header({ route }) {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => { setOpen(false); }, [route]);
const items = window.SITE.nav;
const go = (e, href) => { setOpen(false); };
return (
);
}
/* ---- Footer ---- */
function Footer() {
const items = window.SITE.nav;
const Social = ({ t }) => (
{t}
);
return (
);
}
/* ---- CTA band reused across pages ---- */
function CtaBand({ title, text, primary, secondary }) {
return (
);
}
Object.assign(window, {
useInView, Reveal, Counter, Arrow, Play, Ph, SecHead, SunDivider, Header, Footer, CtaBand,
});