// js/Shared.jsx — icons, buttons, eyebrows, nav, footer const Icon = ({ name, size = 20, stroke = 1.75, ...rest }) => { const paths = { arrow: <>, star: , check: , copy: <>, github: , zap: , terminal:<>, book: <>, }; const filled = name === 'star' || name === 'zap' || name === 'github'; return ( {paths[name]} ); }; const Button = ({ variant = 'primary', size = 'md', children, icon, iconLeft, href, ...rest }) => { const base = { fontFamily: 'var(--font-sans)', fontWeight: 600, borderRadius: 10, border: '1px solid transparent', cursor: 'pointer', transition: 'all 200ms cubic-bezier(0.22,1,0.36,1)', display: 'inline-flex', alignItems: 'center', gap: 8, lineHeight: 1, textDecoration: 'none', whiteSpace: 'nowrap', }; const sizes = { sm: { padding: '7px 12px', fontSize: 13, borderRadius: 8 }, md: { padding: '10px 18px', fontSize: 14 }, lg: { padding: '15px 24px', fontSize: 16 }, }; const variants = { primary: { background: 'var(--accent)', color: '#fff', boxShadow: '0 2px 0 rgba(26,23,20,0.12)' }, secondary: { background: '#fff', color: 'var(--fg1)', borderColor: 'var(--border)' }, ghost: { background: 'transparent', color: 'var(--fg1)' }, }; const El = href ? 'a' : 'button'; return {iconLeft}{children}{icon}; }; const Eyebrow = ({ children, style }) => (
{children}
); // Callout — reusable card used through the field guide to punctuate prose // with a claim, insight, warning, or action prompt. Cream card with orange // eyebrow, serif body, small mascot top-right. // Variants: eyebrow label + mascot pose; tint="warm" for a soft-orange bg. const Callout = ({ eyebrow = 'NOTE', mascot = 'work-pose', tint = 'paper', children, style = {} }) => { // Per-pose config — src + native aspect-preserving dimensions. The // geometric mark is 1536x1024 (3:2 landscape), so it gets a wider box // and is scaled up a bit since user asked for more presence. const mascotConf = { 'work-pose': { src: '/assets/mascot-work-pose.svg', w: 104, h: 104 }, 'geometric': { src: '/assets/mascot-mark-geometric.png', w: 168, h: 112 }, 'pose-2': { src: '/assets/mascot-pose-2.svg', w: 104, h: 104 }, 'pose-3': { src: '/assets/mascot-pose-3.svg', w: 104, h: 104 }, }; const conf = mascotConf[mascot] || mascotConf['work-pose']; const bg = tint === 'warm' ? 'var(--beava-orange-wash)' : 'var(--beava-paper)'; return (
{eyebrow}
{children}
); }; // Banner — dismissable top bar for announcements (cloud waitlist, launch, etc). // Dismiss state lives in localStorage with a 30-day TTL, then re-shows. Pass // an `id` to version dismissals — bump the id when copy changes to re-trigger. const Banner = ({ id = 'cloud-waitlist-v1', emoji = '☁️', children, href = '/cloud' }) => { const [dismissed, setDismissed] = React.useState(true); // default hidden to avoid flash React.useEffect(() => { try { const raw = localStorage.getItem('banner:' + id); if (!raw) { setDismissed(false); return; } const parsed = JSON.parse(raw); const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; if (Date.now() - parsed.ts > THIRTY_DAYS) setDismissed(false); } catch (_) { setDismissed(false); } }, [id]); const dismiss = (e) => { e.preventDefault(); e.stopPropagation(); try { localStorage.setItem('banner:' + id, JSON.stringify({ ts: Date.now() })); } catch (_) {} setDismissed(true); }; if (dismissed) return null; return ( {emoji} {children} ); }; // Nav — shared across pages. active=current route id. const Nav = ({ active = 'home' }) => { const [scrolled, setScrolled] = React.useState(false); React.useEffect(() => { const on = () => setScrolled(window.scrollY > 20); window.addEventListener('scroll', on); on(); return () => window.removeEventListener('scroll', on); }, []); const navStyle = { position: 'sticky', top: 0, zIndex: 50, height: 64, background: scrolled ? 'rgba(253,250,244,0.90)' : 'transparent', backdropFilter: scrolled ? 'blur(10px)' : 'none', WebkitBackdropFilter: scrolled ? 'blur(10px)' : 'none', borderBottom: '1px solid ' + (scrolled ? 'var(--border)' : 'transparent'), display: 'flex', alignItems: 'center', padding: '0 28px', transition: 'all 200ms', }; const link = (id) => ({ padding: '6px 10px', borderRadius: 8, fontSize: 14, color: active === id ? 'var(--accent)' : 'var(--fg2)', textDecoration: 'none', fontWeight: 500, fontFamily: 'var(--font-sans)', }); return ( ); }; // Footer — shared const Footer = () => { const col = { fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--fg3)', marginBottom: 14 }; const lnk = { fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--fg2)', textDecoration: 'none', display: 'block', padding: '3px 0' }; return ( ); }; // Copy-to-clipboard button for code blocks const CopyBtn = ({ text }) => { const [copied, setCopied] = React.useState(false); const onClick = () => { navigator.clipboard?.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1400); }; return ( ); }; // DfPanel — renders a beava stream/table as a pandas-style dataframe. // Fully bordered cells + row index column + accent-orange header row. // Warm palette. Home page defines a local copy that shadows this one; // anywhere else (Chapter 1, recipes), import from here. const DfPanel = ({ title, columns, rows, ellipsis = false, minHeight }) => { const cellBorder = '1px solid var(--border-strong)'; const headerCell = { padding: '8px 10px', textAlign: 'center', fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 11, color: 'var(--beava-cream)', letterSpacing: '0.02em', background: 'var(--accent)', border: '1px solid var(--accent)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }; const indexCell = { padding: '7px 10px', textAlign: 'center', fontFamily: 'var(--font-mono)', fontWeight: 600, fontSize: 12, color: 'var(--fg3)', background: 'var(--beava-cream-deep)', border: cellBorder, width: 36, }; const dataCell = (i) => ({ padding: '7px 10px', textAlign: i === 0 ? 'left' : 'right', fontFamily: 'var(--font-mono)', fontSize: 12.5, color: i === 0 ? 'var(--accent)' : 'var(--fg1)', fontWeight: i === 0 ? 600 : 400, background: 'var(--bg-card)', border: cellBorder, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }); return (
{title}
{columns.map((c) => ())} {rows.length === 0 && ( {columns.map((_, i) => ( ))} )} {rows.map((r, rowIdx) => ( {r.cells.map((c, i) => ())} ))} {ellipsis && ( {columns.map((_, i) => ( ))} )}
{c}
{i === 0 ? 'empty — send an event' : ''}
{rowIdx}{c}
......
); }; Object.assign(window, { Icon, Button, Eyebrow, Callout, Banner, Nav, Footer, CopyBtn, DfPanel });