/* ui.jsx — shared primitives: scroll-reveal, media hook, small helpers */ const { useState, useEffect, useRef, useCallback } = React; /* Reveal-on-scroll wrapper */ function Reveal({ children, className = '', delay = 0, tag = 'div', ...rest }) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } }); }, { threshold: 0.18 }); io.observe(el); return () => io.disconnect(); }, []); const Tag = tag; const d = delay ? ` d${delay}` : ''; return {children}; } /* live media list, re-reads on the global 'r50:media' event */ function useMediaList(filter) { const [items, setItems] = useState([]); const load = useCallback(async () => { const all = await R50.getMedia(); setItems(filter ? all.filter(filter) : all); }, [filter]); useEffect(() => { load(); const h = () => load(); window.addEventListener('r50:media', h); return () => window.removeEventListener('r50:media', h); }, [load]); return items; } function bumpMedia() { window.dispatchEvent(new Event('r50:media')); } /* read a file from an or drop, with basic guard rails */ function pickFile(onFile, { accept } = {}) { const input = document.createElement('input'); input.type = 'file'; if (accept) input.accept = accept; input.onchange = () => { if (input.files && input.files[0]) onFile(input.files[0]); }; input.click(); } const SECTION_LINKS = [ ['Invitation', 'invitation'], ['Details', 'details'], ['RSVP', 'rsvp'], ['Guestbook', 'guestbook'], ['Gallery', 'gallery'], ]; Object.assign(window, { Reveal, useMediaList, bumpMedia, pickFile, SECTION_LINKS });