import { defineRegistry, useBoundProp } from "@json-render/react"; import { catalog } from "./catalog"; // --------------------------------------------------------------------------- // Shared tokens // --------------------------------------------------------------------------- const gapMap = { xs: 4, sm: 8, md: 12, lg: 20 } as const; const cardToneBg = { default: "var(--surface-raised)", accent: "color-mix(in oklab, var(--accent) 8%, var(--surface-raised))", success: "color-mix(in oklab, var(--success) 8%, var(--surface-raised))", warn: "color-mix(in oklab, var(--warn) 10%, var(--surface-raised))", muted: "var(--surface-muted)", } as const; const cardToneBorder = { default: "var(--border)", accent: "color-mix(in oklab, var(--accent) 45%, var(--border))", success: "color-mix(in oklab, var(--success) 45%, var(--border))", warn: "color-mix(in oklab, var(--warn) 45%, var(--border))", muted: "var(--border)", } as const; const badgeTones = { default: { bg: "var(--surface-muted)", fg: "var(--text)" }, accent: { bg: "color-mix(in oklab, var(--accent) 15%, transparent)", fg: "var(--accent)", }, success: { bg: "color-mix(in oklab, var(--success) 15%, transparent)", fg: "var(--success)", }, warn: { bg: "color-mix(in oklab, var(--warn) 15%, transparent)", fg: "var(--warn)", }, muted: { bg: "var(--surface-muted)", fg: "var(--text-muted)" }, } as const; // --------------------------------------------------------------------------- // Registry // --------------------------------------------------------------------------- export const { registry } = defineRegistry(catalog, { components: { Card: ({ props, children }) => { const tone = props.tone ?? "default"; return (
{(props.title || props.subtitle) && (
{props.title && (
{props.title}
)} {props.subtitle && (
{props.subtitle}
)}
)} {children}
); }, Heading: ({ props }) => { const level = props.level ?? "2"; const sizes = { "1": 22, "2": 16, "3": 13 } as const; const Tag = `h${level}` as unknown as "h1"; return ( {props.text} ); }, Text: ({ props }) => { const weight = props.weight === "bold" ? 700 : props.weight === "medium" ? 500 : 400; return (

{props.text}

); }, Stack: ({ props, children }) => (
{children}
), Row: ({ props, children }) => { const alignMap = { start: "flex-start", center: "center", end: "flex-end", between: "space-between", } as const; return (
{children}
); }, Grid: ({ props, children }) => (
{children}
), Metric: ({ props }) => { const trendColor = props.trend === "up" ? "var(--success)" : props.trend === "down" ? "var(--warn)" : "var(--text-muted)"; const trendGlyph = props.trend === "up" ? "↑" : props.trend === "down" ? "↓" : "→"; return (
{props.label}
{props.value}
{props.delta && (
{trendGlyph} {props.delta}
)}
); }, Badge: ({ props }) => { const tone = badgeTones[props.tone ?? "default"]; return ( {props.label} ); }, Divider: () => (
), Button: ({ props, emit }) => { const variant = props.variant ?? "primary"; const size = props.size ?? "md"; const base: React.CSSProperties = { padding: size === "sm" ? "4px 10px" : "6px 12px", borderRadius: 8, fontSize: size === "sm" ? 12 : 13, fontWeight: 500, cursor: props.disabled ? "not-allowed" : "pointer", border: "1px solid transparent", transition: "background 0.15s", opacity: props.disabled ? 0.5 : 1, }; const variants: Record = { primary: { background: "var(--accent)", color: "var(--accent-fg)", }, secondary: { background: "var(--surface-muted)", color: "var(--text)", borderColor: "var(--border)", }, ghost: { background: "transparent", color: "var(--text)", }, }; return ( ); }, TextInput: ({ props, bindings }) => { const [value, setValue] = useBoundProp( (props.value ?? "") as string, bindings?.value, ); return ( setValue(e.target.value)} style={{ padding: "6px 10px", borderRadius: 8, border: "1px solid var(--border)", background: "var(--surface)", color: "var(--text)", fontSize: 13, outline: "none", width: "100%", }} /> ); }, Checkbox: ({ props, bindings }) => { const [checked, setChecked] = useBoundProp( props.checked ?? false, bindings?.checked, ); return ( ); }, ProgressBar: ({ props }) => { const max = props.max ?? 100; const value = Math.max(0, Math.min(max, props.value)); const pct = (value / max) * 100; const color = props.tone === "success" ? "var(--success)" : props.tone === "warn" ? "var(--warn)" : props.tone === "accent" ? "var(--accent)" : "var(--text-muted)"; return (
); }, Callout: ({ props }) => { const tone = props.tone ?? "info"; const toneBg = { info: "color-mix(in oklab, var(--accent) 10%, transparent)", success: "color-mix(in oklab, var(--success) 10%, transparent)", warn: "color-mix(in oklab, var(--warn) 12%, transparent)", tip: "color-mix(in oklab, var(--accent) 8%, transparent)", } as const; const toneBorder = { info: "color-mix(in oklab, var(--accent) 30%, var(--border))", success: "color-mix(in oklab, var(--success) 30%, var(--border))", warn: "color-mix(in oklab, var(--warn) 35%, var(--border))", tip: "color-mix(in oklab, var(--accent) 25%, var(--border))", } as const; return (
{props.title && (
{props.title}
)}
{props.text}
); }, // List is an unstyled flex-column container on purpose: when repeated // children are interactive (Row with Checkbox + Button), a framed // container looks like "a card in a card". ListItem below paints its // own subtle top border between rows to keep a clear separator. List: ({ children }) => (
{children}
), ListItem: ({ props }) => (
{props.title}
{props.description && (
{props.description}
)}
{props.meta && (
{props.meta}
)}
), Avatar: ({ props }) => { const tones = { default: { bg: "var(--surface-muted)", fg: "var(--text)" }, accent: { bg: "color-mix(in oklab, var(--accent) 20%, transparent)", fg: "var(--accent)", }, success: { bg: "color-mix(in oklab, var(--success) 20%, transparent)", fg: "var(--success)", }, warn: { bg: "color-mix(in oklab, var(--warn) 20%, transparent)", fg: "var(--warn)", }, } as const; const t = tones[props.tone ?? "default"]; return ( {props.initials.slice(0, 2).toUpperCase()} ); }, }, });