// TLB primitives — editorial, monochrome, strict. // Reads from window.TLB_TOKENS. Reads tweak state from window.TLB_TWEAKS. const C = window.TLB_TOKENS.color; const F = window.TLB_TOKENS.font; // ── Type helpers ──────────────────────────────────────────────── const tweak = () => window.TLB_TWEAKS || {}; const displayFamily = () => { const f = tweak().displayFont; if (f === 'cormorant') return "'Cormorant Garamond', Georgia, serif"; return F.display; }; const uiFamily = () => { const f = tweak().uiFont; if (f === 'inter') return "Inter, system-ui, sans-serif"; return F.ui; }; const density = () => (tweak().density || 'comfortable'); const ctaRadius = () => (tweak().ctaShape === 'square' ? 0 : 100); const heroRatio = () => (tweak().heroRatio || '4/5'); function Display({ children, size = 32, weight = 500, italic = false, color = C.ink, ls = -0.01, lh = 1.1, style = {}, as: Tag = 'div' }) { return ( {children} ); } function Sans({ children, size = 14, weight = 400, color = C.ink, ls = 0, lh = 1.5, upper = false, style = {}, as: Tag = 'div' }) { return ( {children} ); } function Eyebrow({ children, color = C.mid, style = {} }) { return {children}; } // ── Buttons ───────────────────────────────────────────────────── function Button({ children, variant = 'primary', size = 'md', full = false, onClick, style = {}, leading, trailing, disabled }) { const radius = ctaRadius(); const h = size === 'lg' ? 52 : size === 'sm' ? 36 : 46; const pad = size === 'lg' ? '0 32px' : size === 'sm' ? '0 16px' : '0 24px'; const fs = size === 'lg' ? 13 : size === 'sm' ? 11 : 12; const base = { height: h, padding: pad, borderRadius: radius, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8, fontFamily: uiFamily(), fontSize: fs, fontWeight: 600, letterSpacing: '0.12em', textTransform: 'uppercase', border: 'none', cursor: disabled ? 'not-allowed' : 'pointer', transition: 'all .2s ease', width: full ? '100%' : undefined, opacity: disabled ? 0.4 : 1, ...style, }; const variants = { primary: { background: C.ink, color: C.white }, secondary: { background: C.white, color: C.ink, border: `1px solid ${C.ink}` }, ghost: { background: 'transparent', color: C.ink, textDecoration: 'underline', textUnderlineOffset: 4, letterSpacing: '0.04em', textTransform: 'none', fontWeight: 500, padding: 0 }, onDark: { background: C.white, color: C.ink }, destructive: { background: 'transparent', color: '#9a1c1c', textTransform: 'none', letterSpacing: '0.02em', fontWeight: 500 }, }; return ( ); } function IconButton({ name, onClick, size = 38, dark = false, bg, style = {} }) { const resolvedBg = bg || (dark ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.88)'); return ( ); } // ── Inputs ────────────────────────────────────────────────────── function Field({ label, value, onChange, placeholder, type = 'text', dark = false, right }) { const [focused, setF] = React.useState(false); return ( ); } function Toggle({ on, onChange }) { return ( ); } function Checkbox({ on, onChange, label }) { return ( ); } function Chip({ children, active, onClick, leading }) { return ( ); } // ── Cards ─────────────────────────────────────────────────────── function VillaCard({ img, name, place, price, nights, sleeps, wide, bookmark, onBookmark, onClick }) { return (
{place && {place}} {name} {(price || sleeps) && (
{price && ${price} / night} {sleeps && Sleeps {sleeps}}
)}
); } function Hairline({ color = C.border, style = {} }) { return
; } // ── Bottom tab bar ────────────────────────────────────────────── function TabBar({ active = 'explore', onTab }) { const tabs = [ { key: 'explore', icon: 'explore', label: 'Explore' }, { key: 'search', icon: 'search', label: 'Search' }, { key: 'saved', icon: 'bookmark',label: 'Saved' }, { key: 'trips', icon: 'trips', label: 'Trips' }, { key: 'profile', icon: 'profile', label: 'Profile' }, ]; return (
{tabs.map(t => { const on = active === t.key; return ( ); })}
); } // ── Bottom sheet shell ────────────────────────────────────────── function BottomSheet({ children, style = {} }) { return (
{children}
); } // ── Progress bar ──────────────────────────────────────────────── function Progress({ value = 0 }) { return (
); } // ── Skeleton ──────────────────────────────────────────────────── function Skel({ w = '100%', h = 12, r = 2, style = {} }) { return
; } // ── Pull-quote ────────────────────────────────────────────────── function PullQuote({ children, attribution }) { return (
"{children}" {attribution && — {attribution}}
); } // ── Modal (desktop) ───────────────────────────────────────────── function Modal({ children, onClose, width = 560 }) { React.useEffect(() => { const h = (e) => e.key === 'Escape' && onClose && onClose(); window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [onClose]); return (
e.stopPropagation()} style={{ width, maxWidth: '92vw', maxHeight: '90vh', background: C.white, boxShadow: '0 40px 80px rgba(0,0,0,0.35)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> {children}
); } // ── Expose ────────────────────────────────────────────────────── Object.assign(window, { C_TLB: C, F_TLB: F, Display, Sans, Eyebrow, Button, IconButton, Modal, Field, Toggle, Checkbox, Chip, VillaCard, Hairline, PullQuote, TabBar, BottomSheet, Progress, Skel, displayFamily, uiFamily, density, ctaRadius, heroRatio, });