// Shared primitives: buttons, badges, dividers, kbd, etc. All formal / hairline.
const Btn = ({ children, onClick, variant = 'ghost', size = 'md', icon, iconRight, style, active, title, ...rest }) => {
const sizes = {
sm: { h: 22, px: 8, fs: 11.5 },
md: { h: 28, px: 10, fs: 12.5 },
lg: { h: 34, px: 14, fs: 13 },
}[size];
const variants = {
ghost: {
background: active ? 'var(--bg-panel-2)' : 'transparent',
color: 'var(--ink)',
border: '1px solid transparent',
},
line: {
background: 'var(--bg-elevated)',
color: 'var(--ink)',
border: '1px solid var(--line-2)',
},
solid: {
background: 'var(--ink)',
color: 'var(--bg-panel)',
border: '1px solid var(--ink)',
},
gold: {
background: 'var(--gold)',
color: '#FBF8F1',
border: '1px solid var(--gold)',
},
danger: {
background: 'transparent',
color: 'var(--status-err)',
border: '1px solid var(--line-2)',
},
}[variant];
return (
);
};
const IconBtn = ({ icon, onClick, active, title, size = 28, style }) => (
);
const Kbd = ({ children }) => (
{children}
);
const Badge = ({ children, tone = 'neutral', style }) => {
const tones = {
neutral: { bg: 'transparent', fg: 'var(--ink-muted)', bd: 'var(--line-2)' },
ok: { bg: 'transparent', fg: 'var(--status-ok)', bd: 'var(--status-ok)' },
warn: { bg: 'transparent', fg: 'var(--status-warn)', bd: 'var(--status-warn)' },
err: { bg: 'transparent', fg: 'var(--status-err)', bd: 'var(--status-err)' },
info: { bg: 'transparent', fg: 'var(--status-info)', bd: 'var(--status-info)' },
gold: { bg: 'var(--gold-soft)', fg: 'var(--gold)', bd: 'var(--gold)' },
solid: { bg: 'var(--ink)', fg: 'var(--bg-panel)', bd: 'var(--ink)' },
}[tone];
return (
{children}
);
};
const Divider = ({ vertical, style }) => (
);
const Row = ({ children, gap = 8, align = 'center', justify = 'flex-start', style, ...rest }) => (
{children}
);
const Col = ({ children, gap = 8, align = 'stretch', style, ...rest }) => (
{children}
);
const Label = ({ children, style }) => (
{children}
);
// Formal user avatar — square with initials
const Avatar = ({ name = '?', tone = 'ink', size = 22, style }) => {
const initials = name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase();
const bg = tone === 'gold' ? 'var(--gold)' : tone === 'ink' ? 'var(--ink)' : 'var(--ink-3)';
return (
{initials}
);
};
// Number stepper — for panel inputs
const NumInput = ({ value, onChange, unit, width = 64, step = 1, min, max, style }) => {
const [local, setLocal] = React.useState(value);
React.useEffect(() => setLocal(value), [value]);
return (
setLocal(e.target.value)}
onBlur={() => {
const n = parseFloat(local);
if (!isNaN(n)) { onChange && onChange(n); }
else setLocal(value);
}}
style={{
width: '100%',
height: '100%',
padding: '0 6px',
background: 'transparent',
border: 'none',
outline: 'none',
fontFamily: 'inherit',
fontSize: 11.5,
color: 'var(--ink)',
}}
/>
{unit && (
{unit}
)}
);
};
// Formal group with sidebar label
const Field = ({ label, children, style }) => (
);
// Colored swatch tile
const Swatch = ({ color, label, size = 20, active, onClick, style }) => (
);
// Section header inside panels
const PanelSection = ({ title, right, children, defaultOpen = true, style }) => {
const [open, setOpen] = React.useState(defaultOpen);
return (
setOpen(!open)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 12px',
cursor: 'pointer',
userSelect: 'none',
}}
>
e.stopPropagation()}>{right}
{open &&
{children}
}
);
};
Object.assign(window, {
Btn, IconBtn, Kbd, Badge, Divider, Row, Col, Label, Avatar, NumInput, Field, Swatch, PanelSection
});