// ============================================================ // Tribeca Lisboa — UI primitives (faithful to reference UI kit) // icons · logo · tags · card label · status badge · buttons // ============================================================ const { useState } = React; // Standalone-export resolver: returns the inlined blob URL when present // (window.__resources keyed by path), otherwise the original path. window.R = (p) => (window.__resources && window.__resources[p]) || p; const CAT = { film: { bg: 'var(--film-bg)', hl: 'var(--film-hl)', label: 'Film' }, podcasts: { bg: 'var(--podcasts-bg)', hl: 'var(--podcasts-hl)', label: 'Podcasts' }, talks: { bg: 'var(--talks-bg)', hl: 'var(--talks-hl)', label: 'Talks' }, vibes: { bg: 'var(--vibes-bg)', hl: 'var(--vibes-hl)', label: 'Vibes' }, nightlive: { bg: 'var(--nightlive-bg)', hl: 'var(--nightlive-hl)', label: 'Night Live' }, immersive: { bg: 'var(--immersive-bg)', hl: 'var(--immersive-hl)', label: 'Immersive' }, awards: { bg: 'var(--awards-bg)', hl: 'var(--awards-hl)', label: 'Awards' }, industry: { bg: 'var(--industry-bg)', hl: 'var(--industry-hl)', label: 'Industry' }, }; const ASSET = 'ds/assets'; // ---- Icons (1px line, 16px, currentColor) ----------------- function IconArrow({ size = 16 }) { return ( ); } function IconPin({ size = 16 }) { return ( ); } function IconInstagram({ size = 24 }) { return ( ); } function Hamburger({ color = '#fff' }) { return (
{[0, 1, 2].map(i => )}
); } // ---- Logo (extracted Figma wordmark) ---------------------- function Logo({ variant = 'white', width = 96 }) { const src = variant === 'white' ? `${ASSET}/logo-tribeca-white.svg` : `${ASSET}/logo-tribeca-black.svg`; return Tribeca Festival Lisboa; } // ---- Tag (category pill) ----------------------------------- function Tag({ cat, children, solid }) { const c = CAT[cat] || CAT.film; return ( {children || c.label} ); } // ---- Card label (square block with category wordmark mark) - function CardLabel({ cat }) { const c = CAT[cat] || CAT.industry; return ( {c.label} ); } // ---- Status badge (LIVE / SOON) ---------------------------- function StatusBadge({ kind }) { const live = kind === 'LIVE'; return ( {kind} ); } // ---- Buttons ----------------------------------------------- function PrimaryButton({ children, onClick, full = true }) { const [h, setH] = useState(false); return ( ); } function SecondaryButton({ children, onClick, dark = false }) { const [h, setH] = useState(false); const base = dark ? '#fff' : '#000'; const onbg = dark ? '#000' : '#fff'; return ( ); } function TertiaryButton({ children, onClick, color = '#000' }) { const [h, setH] = useState(false); return ( ); } function ButtonLink({ children, onClick, color = '#fff' }) { return ( ); } // ---- Reveal: subtle scroll-triggered fade + rise ------------ // Observes against the nearest scroll container (the iOS frame's // content area). Honors prefers-reduced-motion. `delay` (ms) lets // sibling reveals cascade for a layered, cinematic rhythm. const PREFERS_REDUCED = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; function Reveal({ children, delay = 0, y = 24, dur = 820, style, as = 'div', className }) { const ref = React.useRef(null); const [inView, setInView] = React.useState(PREFERS_REDUCED); React.useEffect(() => { if (PREFERS_REDUCED) return; const el = ref.current; if (!el) return; // nearest scrollable ancestor = scroll context let root = el.parentElement; while (root && root !== document.body) { const oy = getComputedStyle(root).overflowY; if (oy === 'auto' || oy === 'scroll') break; root = root.parentElement; } const scroller = (root && root !== document.body) ? root : null; const target = scroller || window; let alive = true, raf; const inViewNow = () => { const er = el.getBoundingClientRect(); const sr = scroller ? scroller.getBoundingClientRect() : { top: 0 }; const vh = scroller ? scroller.clientHeight : (window.innerHeight || document.documentElement.clientHeight); const relTop = er.top - sr.top; // element top within the scroll viewport return relTop < vh - 24 && (er.bottom - sr.top) > 0; }; const reveal = () => { if (!alive) return; alive = false; cancelAnimationFrame(raf); target.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); setInView(true); }; const onScroll = () => { if (alive && inViewNow()) reveal(); }; // rAF poll (covers programmatic scroll when painted) + scroll/resize // listeners (covers real + dispatched scroll) + deferred settle checks. const tick = () => { if (!alive) return; if (inViewNow()) { reveal(); return; } raf = requestAnimationFrame(tick); }; target.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); tick(); const t1 = setTimeout(onScroll, 250); const t2 = setTimeout(onScroll, 700); return () => { alive = false; cancelAnimationFrame(raf); clearTimeout(t1); clearTimeout(t2); target.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, []); const Comp = as; return ( {children} ); } Object.assign(window, { CAT, ASSET, IconArrow, IconPin, IconInstagram, Hamburger, Logo, Tag, CardLabel, StatusBadge, PrimaryButton, SecondaryButton, TertiaryButton, ButtonLink, Reveal, PREFERS_REDUCED, });