// ============================================================
// vibn-ai-templates/components.jsx
// ------------------------------------------------------------
// The core component set. Every visual property is wired to a
// CSS variable from tokens.css — flipping `class="theme-glass"`
// (or any other theme class) reskins the whole library.
//
// Components export to `window` for use in script-tag HTML
// projects. In a real codebase, swap the bottom-of-file
// assignment for `export { … }`.
//
// Components included:
// Button, IconButton, Field, Input, Textarea, Select,
// Checkbox, Radio, Switch, Card, Badge, Tag, Avatar,
// AvatarStack, Tabs, Table, Modal, Banner, Divider,
// FieldGroup, KBD, Spinner.
// ============================================================
// ─── Helpers ─────────────────────────────────────────────────
const cx = (...names) => names.filter(Boolean).join(" ");
const noop = () => {};
// ─── Button ──────────────────────────────────────────────────
// variant: primary (default), secondary, ghost, destructive
// size: sm | md (default) | lg
// leadingIcon / trailingIcon:
// loading: disables and shows a spinner
const Button = ({
children, variant = "primary", size = "md", full = false,
leadingIcon, trailingIcon, loading, disabled, onClick = noop, style, type = "button",
...rest
}) => {
const sizing = {
sm: { padY: 6, padX: 12, font: "var(--text-sm)", iconSize: 13 },
md: { padY: 9, padX: 16, font: "var(--text-md)", iconSize: 15 },
lg: { padY: 12, padX: 22, font: "var(--text-lg)", iconSize: 16 },
}[size];
const variants = {
primary: {
background: "var(--button-bg)",
color: "var(--button-fg)",
border: "1px solid var(--button-border)",
},
secondary: {
background: "var(--button-secondary-bg)",
color: "var(--button-secondary-fg)",
border: "1px solid var(--button-secondary-border)",
},
ghost: {
background: "transparent",
color: "var(--button-ghost-fg)",
border: "1px solid transparent",
},
destructive: {
background: "var(--danger)",
color: "#ffffff",
border: "1px solid var(--danger)",
},
}[variant];
return (
);
};
// ─── IconButton ──────────────────────────────────────────────
const IconButton = ({ icon, name, size = "md", variant = "ghost", onClick = noop, label, style }) => {
const dims = { sm: 28, md: 32, lg: 38 }[size];
const iconSize = { sm: 14, md: 16, lg: 18 }[size];
const variants = {
ghost: { background: "transparent", color: "var(--text-2)", border: "1px solid transparent" },
secondary: { background: "var(--button-secondary-bg)", color: "var(--button-secondary-fg)",
border: "1px solid var(--button-secondary-border)" },
}[variant];
return (
);
};
// ─── Spinner ─────────────────────────────────────────────────
const Spinner = ({ size = 14, stroke = 2 }) => (
);
// ─── Field (wraps a labelled input with hint / error) ────────
const Field = ({ label, hint, error, optional, htmlFor, children, style }) => (
{label && (
)}
{children}
{(hint || error) && (
{error || hint}
)}
);
// ─── Input ───────────────────────────────────────────────────
// Bare input (use inside ). leadingIcon / trailingIcon
// add an inner ornament. invalid red-rings the border.
const Input = ({
value, placeholder, type = "text", leadingIcon, trailingIcon,
invalid, disabled, autofocus, onChange = noop, id, style, ...rest
}) => (
{leadingIcon && {leadingIcon}}
onChange(e.target.value, e)}
style={{
flex: 1, minWidth: 0, border: "none", outline: "none", background: "transparent",
fontFamily: "inherit", fontSize: "inherit", color: "inherit",
padding: 0,
}}
{...rest}
/>
{trailingIcon && {trailingIcon}}
);
// ─── Textarea ────────────────────────────────────────────────
const Textarea = ({ value, placeholder, rows = 4, onChange = noop, invalid, id, style, ...rest }) => (