// 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 }) => (