// 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 (
);
};
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 (
);
};
// 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) => ({c} | ))}
{rows.length === 0 && (
| — |
{columns.map((_, i) => (
{i === 0 ? 'empty — send an event' : ''}
|
))}
)}
{rows.map((r, rowIdx) => (
| {rowIdx} |
{r.cells.map((c, i) => ({c} | ))}
))}
{ellipsis && (
| ... |
{columns.map((_, i) => (
... |
))}
)}
);
};
Object.assign(window, { Icon, Button, Eyebrow, Callout, Banner, Nav, Footer, CopyBtn, DfPanel });