// ============================================================
// 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
;
}
// ---- 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 (
);
}
// ---- 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,
});