// desidium-helpers.jsx — shared primitives, exported to window
const { useState, useEffect, useRef } = React;
// Site languages, in switcher order. Slugs must match Polylang's (see inc/polylang.php)
// and the keys used in content.js / models-data.js / journal-data.js.
const LANGS = ["en", "es", "pt"];
// pick a localized string: L({en,es,pt}, lang)
function L(obj, lang) { return obj ? (obj[lang] ?? obj.en) : ""; }
// EN / ES / PT switcher. `className` is appended to .lang-toggle ("", "big", "foot").
function LangToggle({ lang, setLang, className = "" }) {
return (
{LANGS.map((code, i) => (
{i > 0 && /}
))}
);
}
// Scroll-reveal wrapper. Adds .is-in when scrolled into view.
// `motion=false` -> always visible (reduced motion / tweak off).
function Reveal({ children, motion = true, delay = 0, y = 26, as = "div", className = "", style = {} }) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
if (!motion) { setSeen(true); return; }
const el = ref.current; if (!el) return;
const io = new IntersectionObserver((es) => {
es.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } });
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
io.observe(el);
return () => io.disconnect();
}, [motion]);
const Tag = as;
const baseT = motion ? `opacity .9s cubic-bezier(.22,.61,.36,1) ${delay}ms, transform 1.05s cubic-bezier(.22,.61,.36,1) ${delay}ms` : "none";
return (
{children}
);
}
// Cinematic film placeholder (the hero video stand-in).
function Film({ label = "Hero film · placeholder", showPlay = true, showScrub = true, time = "00:00 / 01:20", className = "", style = {} }) {
return (
{showPlay &&
}
{label}
{showScrub &&
{time}
}
);
}
// Portrait placeholder for roster / divisions. Striped, with mono caption.
function Portrait({ caption = "Portrait", src = "", position = "center", showCaption = true, tall = false, className = "", style = {} }) {
const base = (window.desidiumWp && window.desidiumWp.themeUri) || "";
return (
{src &&

}
{showCaption &&
{caption}
}
);
}
// Drag-to-compare before/after. Two muted, looped, roughly-synced videos; a draggable
// divider clips the "before" clip. Empty srcs -> striped placeholder (like Portrait).
function BeforeAfter({ beforeSrc = "", afterSrc = "", beforeLabel = "Before", afterLabel = "After", hint = "Drag to compare", fit = "cover", className = "" }) {
const base = (window.desidiumWp && window.desidiumWp.themeUri) || "";
const [pos, setPos] = useState(50);
const wrapRef = useRef(null);
const beforeRef = useRef(null);
const afterRef = useRef(null);
const dragging = useRef(false);
const hasVideo = !!(beforeSrc && afterSrc);
const moveTo = (clientX) => {
const el = wrapRef.current; if (!el) return;
const r = el.getBoundingClientRect();
setPos(Math.max(0, Math.min(100, ((clientX - r.left) / r.width) * 100)));
};
useEffect(() => {
if (!hasVideo) return;
const onMove = (e) => { if (!dragging.current) return; moveTo(e.touches ? e.touches[0].clientX : e.clientX); };
const onUp = () => { dragging.current = false; };
window.addEventListener("mousemove", onMove);
window.addEventListener("touchmove", onMove, { passive: true });
window.addEventListener("mouseup", onUp);
window.addEventListener("touchend", onUp);
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("touchmove", onMove);
window.removeEventListener("mouseup", onUp);
window.removeEventListener("touchend", onUp);
};
}, [hasVideo]);
useEffect(() => {
if (!hasVideo) return;
const a = afterRef.current, b = beforeRef.current;
if (!a || !b) return;
const sync = () => { if (Math.abs(a.currentTime - b.currentTime) > 0.12) { try { b.currentTime = a.currentTime; } catch (e) {} } };
a.addEventListener("timeupdate", sync);
return () => a.removeEventListener("timeupdate", sync);
}, [hasVideo]);
const startDrag = (clientX) => { dragging.current = true; moveTo(clientX); };
return (
startDrag(e.clientX) : undefined}
onTouchStart={hasVideo ? (e) => startDrag(e.touches[0].clientX) : undefined}>
{hasVideo ? (
{fit === "contain" && (
)}
{ if (e.key === "ArrowLeft") setPos((p) => Math.max(0, p - 4)); if (e.key === "ArrowRight") setPos((p) => Math.min(100, p + 4)); }}>
‹ ›
{beforeLabel}
{afterLabel}
) : (
{beforeLabel} / {afterLabel}
)}
);
}
// Labelled