| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682 |
- 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/<unique-word>/<width>/<height> (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/<unique-word>/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.",
- },
- },
- });
|