/* global React */ // Shared cosmetic helpers for the Jullia website UI kit. // Not design-system primitives — kit-local scaffolding only. // A low-key photo frame — src given, it shows real photography treated in the // atelier's mono hand (desaturated, deep shadow, a single-lamp vignette, a slow // one-time reveal scale). No src, it falls back to the original art-direction // placeholder, stating the intended shot instead. function Media({ label, caption, src, alt, ratio = '16 / 9', tone = 'inset', showCaption = true, style = {} }) { const bg = tone === 'raised' ? 'var(--surface-raised)' : tone === 'surface' ? 'var(--surface)' : 'var(--surface-inset)'; return (
{src && ( {alt )} {/* single-lamp vignette, drawn with value only */}
{showCaption && (
{!src && Image} {caption || label}
)}
); } // Reveal-on-scroll. The base state ([data-reveal]) is hidden; this reveals nodes by // setting the final values INLINE (so paint is guaranteed even if the composited CSS // transition never plays). IntersectionObserver drives the on-scroll fade as an // enhancement, and a double-rAF guarantees every tracked node ends visible on mount — // the transition still animates 0→1 for anything currently in view. function useReveal() { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const nodes = [...(el.hasAttribute('data-reveal') ? [el] : []), ...el.querySelectorAll('[data-reveal]')]; const reveal = (n) => { n.setAttribute('data-revealed', ''); n.style.opacity = '1'; n.style.transform = 'none'; }; let io; if ('IntersectionObserver' in window) { io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { reveal(e.target); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); nodes.forEach((n) => io.observe(n)); } // Guarantee visibility: reveal in-view nodes on the next frame (they fade in), // then reveal everything shortly after so nothing can stay hidden. const raf = requestAnimationFrame(() => { const vh = window.innerHeight || document.documentElement.clientHeight; nodes.forEach((n) => { if (n.getBoundingClientRect().top < vh * 0.95) reveal(n); }); }); const t = setTimeout(() => nodes.forEach(reveal), 900); return () => { if (io) io.disconnect(); cancelAnimationFrame(raf); clearTimeout(t); }; }, []); return ref; } // A rework-film placeholder. Reads as a video still with a play affordance; no // real footage is bundled. Click toggles a quiet "playing" caption. Swap the // inner surface for a real