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