// 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 && {caption}} {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" && ( ) : (
{beforeLabel} / {afterLabel}
)}
); } // Labelled