// Shared UI primitives — buttons, cards, badges, sparkline, avatar, etc. // All driven by CSS variables defined in theme.jsx. const cx = (...xs) => xs.filter(Boolean).join(' '); // ── Avatar ─────────────────────────────────────────────────────────── const Avatar = ({ person, size = 28, ring = false }) => { const p = typeof person === 'string' ? PERSON_BY_ID[person] : person; if (!p) return null; const palette = ['#C55A3C','#5B6F8F','#4A7C59','#A88E54','#7A4A6C','#3F6E78','#9A4A2E','#5C5C7E','#6B8E5A']; const idx = (p.id.charCodeAt(p.id.length-1) || 0) % palette.length; return (
{p.initials}
); }; const AvatarStack = ({ ids, max = 4, size = 24 }) => { const shown = ids.slice(0, max); const more = ids.length - shown.length; return (
{shown.map((id, i) => (
))} {more > 0 && (
+{more}
)}
); }; // ── Card ───────────────────────────────────────────────────────────── const Card = ({ children, style, pad = true, hoverable = false, onClick, accent = false }) => (
{ e.currentTarget.style.borderColor = 'var(--apm-line-2)'; e.currentTarget.style.boxShadow = 'var(--apm-shadow-md)'; } : undefined} onMouseLeave={hoverable ? e => { e.currentTarget.style.borderColor = 'var(--apm-line)'; e.currentTarget.style.boxShadow = 'var(--apm-shadow-sm)'; } : undefined}> {children}
); // ── Button ─────────────────────────────────────────────────────────── const Button = ({ children, variant = 'ghost', size = 'md', onClick, icon, style, disabled, type = 'button' }) => { const variants = { primary: { bg: 'var(--apm-ink)', fg: 'var(--apm-bg-2)', border: 'var(--apm-ink)' }, accent: { bg: 'var(--apm-accent)', fg: '#fff', border: 'var(--apm-accent)' }, outline: { bg: 'transparent', fg: 'var(--apm-ink)', border: 'var(--apm-line-2)' }, ghost: { bg: 'transparent', fg: 'var(--apm-ink-2)',border: 'transparent' }, soft: { bg: 'var(--apm-bg-sunken)', fg: 'var(--apm-ink)', border: 'var(--apm-bg-sunken)' }, danger: { bg: 'var(--apm-risk)', fg: '#fff', border: 'var(--apm-risk)' }, }[variant]; const sizes = { sm: { h: 28, px: 10, fs: 12.5 }, md: { h: 'var(--apm-input-h)', px: 14, fs: 13.5 }, lg: { h: 44, px: 18, fs: 14.5 }, }[size]; return ( ); }; // ── Input ──────────────────────────────────────────────────────────── const Input = ({ value, onChange, placeholder, type = 'text', style, prefix, ...rest }) => (
{prefix && {prefix}} onChange?.(e.target.value)} placeholder={placeholder} style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', color: 'var(--apm-ink)', fontFamily: 'var(--apm-font-body)', fontSize: 'var(--apm-text-base)', }} {...rest} />
); const Textarea = ({ value, onChange, placeholder, rows = 3, style }) => (