/* 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 && (
)}
{/* 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