import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/react/schema"; import { z } from "zod"; /** * Web playground component catalog * * This defines the components available for AI generation in the playground. * Components and actions are implemented in lib/registry.tsx via defineRegistry. * * Keep schemas simple — one format per prop, no unions. * Fewer components = less confusion for the AI. */ export const playgroundCatalog = defineCatalog(schema, { components: { // ── Layout ────────────────────────────────────────────────────────── Card: { props: z.object({ title: z.string().nullable(), description: z.string().nullable(), maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(), centered: z.boolean().nullable(), }), slots: ["default"], description: "Container card for content sections. Use for forms/content boxes, NOT for page headers.", example: { title: "Overview", description: "Your account summary" }, }, Stack: { props: z.object({ direction: z.enum(["horizontal", "vertical"]).nullable(), gap: z.enum(["none", "sm", "md", "lg"]).nullable(), align: z.enum(["start", "center", "end", "stretch"]).nullable(), justify: z .enum(["start", "center", "end", "between", "around"]) .nullable(), wrap: z.boolean().nullable(), }), slots: ["default"], description: "Flex container for layouts. Horizontal stacks wrap by default; set wrap:false to keep children on a single row that scrolls horizontally — use this for Kanban boards and column layouts where columns must sit side by side.", example: { direction: "vertical", gap: "md" }, }, Grid: { props: z.object({ columns: z.number().nullable(), gap: z.enum(["sm", "md", "lg"]).nullable(), }), slots: ["default"], description: "Grid layout (1-7 columns). Use columns:7 for weekly/monthly calendar grids (one column per weekday).", example: { columns: 3, gap: "md" }, }, Separator: { props: z.object({ orientation: z.enum(["horizontal", "vertical"]).nullable(), }), description: "Visual separator line", }, Tabs: { props: z.object({ tabs: z.array( z.object({ label: z.string(), value: z.string(), }), ), defaultValue: z.string().nullable(), value: z.string().nullable(), }), events: ["change"], description: "Tab navigation with panels. Provide one child element per tab, in the SAME order as the tabs array — children map positionally (children[0] is the panel for tabs[0], etc.). The matching child is shown when its tab is active. Use { $bindState } on value for active-tab binding.", }, Accordion: { props: z.object({ items: z.array( z.object({ title: z.string(), content: z.string(), }), ), type: z.enum(["single", "multiple"]).nullable(), }), description: "Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.", }, Timeline: { props: z.object({ items: z.array( z.object({ title: z.string(), description: z.string().nullable(), date: z.string().nullable(), status: z.enum(["completed", "current", "upcoming"]).nullable(), }), ), }), description: "Vertical timeline showing ordered events, steps, or historical milestones. Items as [{title, description, date, status}]. status: 'completed' (green dot), 'current' (blue dot), 'upcoming' (gray dot).", example: { items: [ { title: "Project Kickoff", description: "Team aligned on goals", date: "Jan 2026", status: "completed", }, ], }, }, Collapsible: { props: z.object({ title: z.string(), defaultOpen: z.boolean().nullable(), }), slots: ["default"], description: "Collapsible section with trigger. Children render inside.", }, Dialog: { props: z.object({ title: z.string(), description: z.string().nullable(), openPath: z.string(), }), slots: ["default"], description: "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.", }, Drawer: { props: z.object({ title: z.string(), description: z.string().nullable(), openPath: z.string(), }), slots: ["default"], description: "Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.", }, Carousel: { props: z.object({ items: z.array( z.object({ title: z.string().nullable(), description: z.string().nullable(), }), ), }), description: "Horizontally scrollable carousel of cards.", }, // ── Data Display ──────────────────────────────────────────────────── Table: { props: z.object({ columns: z.array(z.string()), rows: z.array(z.array(z.string())), caption: z.string().nullable(), }), description: 'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].', example: { columns: ["Name", "Role"], rows: [ ["Alice", "Admin"], ["Bob", "User"], ], }, }, Heading: { props: z.object({ text: z.string(), level: z.enum(["h1", "h2", "h3", "h4"]).nullable(), }), description: "Heading text (h1-h4)", example: { text: "Welcome", level: "h1" }, }, Text: { props: z.object({ text: z.string(), variant: z .enum(["body", "caption", "muted", "lead", "code"]) .nullable(), }), description: 'Paragraph text. In repeat scopes, use { "$template": "${field1} ${field2}" } to interpolate item fields.', example: { text: "Hello, world!" }, }, Image: { props: z.object({ alt: z.string(), src: z.string().nullable(), width: z.number().nullable(), height: z.number().nullable(), }), description: "Image. If 'src' (a URL) is set, renders the real image (object-cover); otherwise a placeholder box with the alt text. For realistic demo photos use Lorem Picsum: https://picsum.photos/seed/// (e.g. https://picsum.photos/seed/mountains/600/400) — vary the seed per image for different photos.", }, Map: { props: z.object({ query: z.string(), zoom: z.number().nullable(), height: z.number().nullable(), }), description: "Embedded interactive map centered on a location. 'query' is an address or place name (e.g. '350 5th Ave, New York, NY' or 'Eiffel Tower, Paris'). Optional zoom (1-20, default 14) and height in px (default 320). Use for contact pages, store locators, event venues.", example: { query: "350 5th Ave, New York, NY 10118", zoom: 15, height: 320, }, }, Pressable: { props: z.object({}), events: ["press"], slots: ["default"], description: "Clickable container that wraps any content (children) and emits a press event — bind on.press to a setState action. Use for clickable cards, list rows, and image thumbnails (e.g. a photo-gallery lightbox: a Pressable around a thumbnail Image that opens a Dialog).", }, Lightbox: { props: z.object({ images: z.array( z.object({ src: z.string(), caption: z.string().nullable(), }), ), columns: z.number().nullable(), }), description: "Self-contained photo gallery with a built-in fullscreen lightbox. Provide images as [{src, caption}] using real photo URLs (https://picsum.photos/seed//600/600, different seed per image). Renders a thumbnail grid (columns 2-6, default 3); clicking a thumbnail opens a fullscreen overlay with the large photo, caption, prev/next arrows, counter, and close (Esc / click-outside / ✕). Manages its own open state internally — no setState, openPath, or Dialog needed. This is the CORRECT way to build an image gallery with a lightbox.", example: { columns: 3, images: [ { src: "https://picsum.photos/seed/alps/600/600", caption: "The Alps", }, ], }, }, Modal: { props: z.object({ triggerLabel: z.string(), triggerVariant: z .enum(["primary", "secondary", "outline", "danger"]) .nullable(), title: z.string().nullable(), description: z.string().nullable(), size: z.enum(["sm", "md", "lg"]).nullable(), }), slots: ["default"], description: "Self-contained modal. The Modal element ITSELF renders the clickable trigger button (text = triggerLabel, styled by triggerVariant) and IS that button in the layout — place it exactly where the trigger should appear. Clicking it opens a centered overlay containing this element's children, with an optional title/description header and close (Esc / click-outside / ✕). It manages its own open state. CRITICAL: do NOT add a separate Button to open it, and do NOT attach any setState / openPath / on.press action — the Modal needs none of that. One Modal element = one trigger button + its popup. Use this (NOT Dialog + setState) for every click-to-open modal: confirmations, detail popups, modal forms. Put the modal body as this element's children.", example: { triggerLabel: "Open details", title: "Details", size: "md", }, }, Icon: { props: z.object({ name: z.string(), size: z.enum(["sm", "md", "lg", "xl", "2xl", "3xl"]).nullable(), color: z .enum(["default", "muted", "primary", "success", "warning", "danger"]) .nullable(), }), description: "Lucide icon by name. PascalCase: MapPin, Mail, Globe, Calendar, Star, Heart, Check, X, ArrowRight, Phone, Building, Clock, Shield, Zap, Users, Eye, Download, Upload, Search, Filter, Settings, Bell, ChevronRight, ExternalLink, Info, AlertTriangle, CheckCircle, XCircle. Sizes sm/md/lg are inline (16-24px); xl/2xl/3xl (32/48/64px) are for hero illustrations, empty states, and 404 pages. Use in horizontal Stacks with Text for icon+label patterns. Never use emoji — always use Icon.", }, Avatar: { props: z.object({ src: z.string().nullable(), name: z.string(), size: z.enum(["sm", "md", "lg"]).nullable(), }), description: "User avatar with fallback initials", example: { name: "Jane Doe", size: "md" }, }, Badge: { props: z.object({ text: z.string(), variant: z.enum(["default", "success", "warning", "danger"]).nullable(), }), description: "Status badge", example: { text: "Active", variant: "success" }, }, Alert: { props: z.object({ title: z.string(), message: z.string().nullable(), type: z.enum(["info", "success", "warning", "error"]).nullable(), }), description: "Alert banner", example: { title: "Note", message: "Your changes have been saved.", type: "success", }, }, Progress: { props: z.object({ value: z.number(), max: z.number().nullable(), label: z.string().nullable(), }), description: "Progress bar (value 0-100)", example: { value: 65, max: 100, label: "Upload progress" }, }, Skeleton: { props: z.object({ width: z.string().nullable(), height: z.string().nullable(), rounded: z.boolean().nullable(), }), description: "Loading placeholder skeleton", }, Spinner: { props: z.object({ size: z.enum(["sm", "md", "lg"]).nullable(), label: z.string().nullable(), }), description: "Loading spinner indicator", }, Tooltip: { props: z.object({ content: z.string(), text: z.string(), }), description: "Hover tooltip. Shows content on hover over text.", }, Popover: { props: z.object({ trigger: z.string(), content: z.string(), }), description: "Popover that appears on click of trigger.", }, Rating: { props: z.object({ value: z.number(), max: z.number().nullable(), label: z.string().nullable(), interactive: z.boolean().nullable(), }), events: ["change"], description: "Interactive star rating. Use { $bindState } on value for binding. Set interactive: false for read-only display.", example: { value: 4, max: 5, label: "Rating" }, }, Metric: { props: z.object({ label: z.string(), value: z.string(), change: z.string().nullable(), changeType: z.enum(["positive", "negative", "neutral"]).nullable(), prefix: z.string().nullable(), suffix: z.string().nullable(), }), description: "Key metric / stat display. Shows a large value with label and optional change indicator. Use for dashboard KPIs.", example: { label: "Total Revenue", value: "125,000", prefix: "$", change: "+12.5%", changeType: "positive", }, }, // ── Charts ────────────────────────────────────────────────────────── BarGraph: { props: z.object({ title: z.string().nullable(), data: z.array( z.object({ label: z.string(), value: z.number(), }), ), }), description: "Vertical bar chart", }, LineGraph: { props: z.object({ title: z.string().nullable(), data: z.array( z.object({ label: z.string(), value: z.number(), }), ), }), description: "Line chart with points", }, // ── Form Inputs ───────────────────────────────────────────────────── Input: { props: z.object({ label: z.string(), name: z.string(), type: z.enum(["text", "email", "password", "number"]).nullable(), placeholder: z.string().nullable(), value: z.string().nullable(), checks: z .array( z.object({ type: z.string(), message: z.string(), args: z.record(z.string(), z.unknown()).optional(), }), ) .nullable(), }), events: ["submit", "focus", "blur"], description: "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).", example: { label: "Email", name: "email", type: "email", placeholder: "you@example.com", }, }, Textarea: { props: z.object({ label: z.string(), name: z.string(), placeholder: z.string().nullable(), rows: z.number().nullable(), value: z.string().nullable(), checks: z .array( z.object({ type: z.string(), message: z.string(), args: z.record(z.string(), z.unknown()).optional(), }), ) .nullable(), }), description: "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.", }, Select: { props: z.object({ label: z.string(), name: z.string(), options: z.array(z.string()), placeholder: z.string().nullable(), value: z.string().nullable(), checks: z .array( z.object({ type: z.string(), message: z.string(), args: z.record(z.string(), z.unknown()).optional(), }), ) .nullable(), }), events: ["change"], description: "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.", }, Checkbox: { props: z.object({ label: z.string(), name: z.string(), checked: z.boolean().nullable(), }), events: ["change"], description: "Checkbox input. Use { $bindState } on checked for binding.", }, Radio: { props: z.object({ label: z.string(), name: z.string(), options: z.array(z.string()), value: z.string().nullable(), }), events: ["change"], description: "Radio button group. Use { $bindState } on value for binding.", }, Switch: { props: z.object({ label: z.string(), name: z.string(), checked: z.boolean().nullable(), }), events: ["change"], description: "Toggle switch. Use { $bindState } on checked for binding.", }, Slider: { props: z.object({ label: z.string().nullable(), min: z.number().nullable(), max: z.number().nullable(), step: z.number().nullable(), value: z.number().nullable(), }), events: ["change"], description: "Range slider input. Use { $bindState } on value for binding.", }, // ── Actions ───────────────────────────────────────────────────────── Button: { props: z.object({ label: z.string(), variant: z .enum(["primary", "secondary", "outline", "danger"]) .nullable(), disabled: z.boolean().nullable(), }), events: ["press"], description: "Clickable button. primary = solid fill, outline = bordered/transparent, secondary = muted fill. Bind on.press for handler.", example: { label: "Submit", variant: "primary" }, }, Link: { props: z.object({ label: z.string(), href: z.string(), }), events: ["press"], description: "Anchor link. Bind on.press for click handler.", }, DropdownMenu: { props: z.object({ label: z.string(), items: z.array( z.object({ label: z.string(), value: z.string(), }), ), }), events: ["select"], description: "Dropdown menu with trigger button and selectable items.", }, Toggle: { props: z.object({ label: z.string(), pressed: z.boolean().nullable(), variant: z.enum(["default", "outline"]).nullable(), }), events: ["change"], description: "Toggle button. Use { $bindState } on pressed for state binding.", }, ToggleGroup: { props: z.object({ items: z.array( z.object({ label: z.string(), value: z.string(), }), ), type: z.enum(["single", "multiple"]).nullable(), value: z.string().nullable(), }), events: ["change"], description: "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.", }, ButtonGroup: { props: z.object({ buttons: z.array( z.object({ label: z.string(), value: z.string(), }), ), selected: z.string().nullable(), }), events: ["change"], description: "Segmented button group. Use { $bindState } on selected for selected value.", }, Pagination: { props: z.object({ totalPages: z.number(), page: z.number().nullable(), }), events: ["change"], description: "Page navigation. Use { $bindState } on page for current page number.", }, }, actions: { setState: { params: z.object({ statePath: z.string(), value: z.unknown(), }), description: "Update a value in the state model at the given statePath.", }, pushState: { params: z.object({ statePath: z.string(), value: z.unknown(), clearStatePath: z.string().optional(), }), description: 'Append an item to an array in state. Value can contain {"$state":"/statePath"} refs and "$id" for auto IDs. clearStatePath resets another path after pushing.', }, removeState: { params: z.object({ statePath: z.string(), index: z.number(), }), description: "Remove an item from an array in state at the given index.", }, buttonClick: { params: z.object({ message: z.string().nullable(), }), description: "Shows a toast with the message.", }, formSubmit: { params: z.object({ formName: z.string().nullable(), }), description: "Shows a toast confirming form submission.", }, linkClick: { params: z.object({ href: z.string(), }), description: "Shows a toast with the link destination.", }, }, });