catalog.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. import { defineCatalog } from "@json-render/core";
  2. import { schema } from "@json-render/react/schema";
  3. import { z } from "zod";
  4. /**
  5. * Web playground component catalog
  6. *
  7. * This defines the components available for AI generation in the playground.
  8. * Components and actions are implemented in lib/registry.tsx via defineRegistry.
  9. *
  10. * Keep schemas simple — one format per prop, no unions.
  11. * Fewer components = less confusion for the AI.
  12. */
  13. export const playgroundCatalog = defineCatalog(schema, {
  14. components: {
  15. // ── Layout ──────────────────────────────────────────────────────────
  16. Card: {
  17. props: z.object({
  18. title: z.string().nullable(),
  19. description: z.string().nullable(),
  20. maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(),
  21. centered: z.boolean().nullable(),
  22. }),
  23. slots: ["default"],
  24. description:
  25. "Container card for content sections. Use for forms/content boxes, NOT for page headers.",
  26. example: { title: "Overview", description: "Your account summary" },
  27. },
  28. Stack: {
  29. props: z.object({
  30. direction: z.enum(["horizontal", "vertical"]).nullable(),
  31. gap: z.enum(["none", "sm", "md", "lg"]).nullable(),
  32. align: z.enum(["start", "center", "end", "stretch"]).nullable(),
  33. justify: z
  34. .enum(["start", "center", "end", "between", "around"])
  35. .nullable(),
  36. wrap: z.boolean().nullable(),
  37. }),
  38. slots: ["default"],
  39. description:
  40. "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.",
  41. example: { direction: "vertical", gap: "md" },
  42. },
  43. Grid: {
  44. props: z.object({
  45. columns: z.number().nullable(),
  46. gap: z.enum(["sm", "md", "lg"]).nullable(),
  47. }),
  48. slots: ["default"],
  49. description:
  50. "Grid layout (1-7 columns). Use columns:7 for weekly/monthly calendar grids (one column per weekday).",
  51. example: { columns: 3, gap: "md" },
  52. },
  53. Separator: {
  54. props: z.object({
  55. orientation: z.enum(["horizontal", "vertical"]).nullable(),
  56. }),
  57. description: "Visual separator line",
  58. },
  59. Tabs: {
  60. props: z.object({
  61. tabs: z.array(
  62. z.object({
  63. label: z.string(),
  64. value: z.string(),
  65. }),
  66. ),
  67. defaultValue: z.string().nullable(),
  68. value: z.string().nullable(),
  69. }),
  70. events: ["change"],
  71. description:
  72. "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.",
  73. },
  74. Accordion: {
  75. props: z.object({
  76. items: z.array(
  77. z.object({
  78. title: z.string(),
  79. content: z.string(),
  80. }),
  81. ),
  82. type: z.enum(["single", "multiple"]).nullable(),
  83. }),
  84. description:
  85. "Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.",
  86. },
  87. Timeline: {
  88. props: z.object({
  89. items: z.array(
  90. z.object({
  91. title: z.string(),
  92. description: z.string().nullable(),
  93. date: z.string().nullable(),
  94. status: z.enum(["completed", "current", "upcoming"]).nullable(),
  95. }),
  96. ),
  97. }),
  98. description:
  99. "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).",
  100. example: {
  101. items: [
  102. {
  103. title: "Project Kickoff",
  104. description: "Team aligned on goals",
  105. date: "Jan 2026",
  106. status: "completed",
  107. },
  108. ],
  109. },
  110. },
  111. Collapsible: {
  112. props: z.object({
  113. title: z.string(),
  114. defaultOpen: z.boolean().nullable(),
  115. }),
  116. slots: ["default"],
  117. description: "Collapsible section with trigger. Children render inside.",
  118. },
  119. Dialog: {
  120. props: z.object({
  121. title: z.string(),
  122. description: z.string().nullable(),
  123. openPath: z.string(),
  124. }),
  125. slots: ["default"],
  126. description:
  127. "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.",
  128. },
  129. Drawer: {
  130. props: z.object({
  131. title: z.string(),
  132. description: z.string().nullable(),
  133. openPath: z.string(),
  134. }),
  135. slots: ["default"],
  136. description:
  137. "Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.",
  138. },
  139. Carousel: {
  140. props: z.object({
  141. items: z.array(
  142. z.object({
  143. title: z.string().nullable(),
  144. description: z.string().nullable(),
  145. }),
  146. ),
  147. }),
  148. description: "Horizontally scrollable carousel of cards.",
  149. },
  150. // ── Data Display ────────────────────────────────────────────────────
  151. Table: {
  152. props: z.object({
  153. columns: z.array(z.string()),
  154. rows: z.array(z.array(z.string())),
  155. caption: z.string().nullable(),
  156. }),
  157. description:
  158. 'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
  159. example: {
  160. columns: ["Name", "Role"],
  161. rows: [
  162. ["Alice", "Admin"],
  163. ["Bob", "User"],
  164. ],
  165. },
  166. },
  167. Heading: {
  168. props: z.object({
  169. text: z.string(),
  170. level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
  171. }),
  172. description: "Heading text (h1-h4)",
  173. example: { text: "Welcome", level: "h1" },
  174. },
  175. Text: {
  176. props: z.object({
  177. text: z.string(),
  178. variant: z
  179. .enum(["body", "caption", "muted", "lead", "code"])
  180. .nullable(),
  181. }),
  182. description:
  183. 'Paragraph text. In repeat scopes, use { "$template": "${field1} ${field2}" } to interpolate item fields.',
  184. example: { text: "Hello, world!" },
  185. },
  186. Image: {
  187. props: z.object({
  188. alt: z.string(),
  189. src: z.string().nullable(),
  190. width: z.number().nullable(),
  191. height: z.number().nullable(),
  192. }),
  193. description:
  194. "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.",
  195. },
  196. Map: {
  197. props: z.object({
  198. query: z.string(),
  199. zoom: z.number().nullable(),
  200. height: z.number().nullable(),
  201. }),
  202. description:
  203. "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.",
  204. example: {
  205. query: "350 5th Ave, New York, NY 10118",
  206. zoom: 15,
  207. height: 320,
  208. },
  209. },
  210. Pressable: {
  211. props: z.object({}),
  212. events: ["press"],
  213. slots: ["default"],
  214. description:
  215. "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).",
  216. },
  217. Lightbox: {
  218. props: z.object({
  219. images: z.array(
  220. z.object({
  221. src: z.string(),
  222. caption: z.string().nullable(),
  223. }),
  224. ),
  225. columns: z.number().nullable(),
  226. }),
  227. description:
  228. "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.",
  229. example: {
  230. columns: 3,
  231. images: [
  232. {
  233. src: "https://picsum.photos/seed/alps/600/600",
  234. caption: "The Alps",
  235. },
  236. ],
  237. },
  238. },
  239. Modal: {
  240. props: z.object({
  241. triggerLabel: z.string(),
  242. triggerVariant: z
  243. .enum(["primary", "secondary", "outline", "danger"])
  244. .nullable(),
  245. title: z.string().nullable(),
  246. description: z.string().nullable(),
  247. size: z.enum(["sm", "md", "lg"]).nullable(),
  248. }),
  249. slots: ["default"],
  250. description:
  251. "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.",
  252. example: {
  253. triggerLabel: "Open details",
  254. title: "Details",
  255. size: "md",
  256. },
  257. },
  258. Icon: {
  259. props: z.object({
  260. name: z.string(),
  261. size: z.enum(["sm", "md", "lg", "xl", "2xl", "3xl"]).nullable(),
  262. color: z
  263. .enum(["default", "muted", "primary", "success", "warning", "danger"])
  264. .nullable(),
  265. }),
  266. description:
  267. "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.",
  268. },
  269. Avatar: {
  270. props: z.object({
  271. src: z.string().nullable(),
  272. name: z.string(),
  273. size: z.enum(["sm", "md", "lg"]).nullable(),
  274. }),
  275. description: "User avatar with fallback initials",
  276. example: { name: "Jane Doe", size: "md" },
  277. },
  278. Badge: {
  279. props: z.object({
  280. text: z.string(),
  281. variant: z.enum(["default", "success", "warning", "danger"]).nullable(),
  282. }),
  283. description: "Status badge",
  284. example: { text: "Active", variant: "success" },
  285. },
  286. Alert: {
  287. props: z.object({
  288. title: z.string(),
  289. message: z.string().nullable(),
  290. type: z.enum(["info", "success", "warning", "error"]).nullable(),
  291. }),
  292. description: "Alert banner",
  293. example: {
  294. title: "Note",
  295. message: "Your changes have been saved.",
  296. type: "success",
  297. },
  298. },
  299. Progress: {
  300. props: z.object({
  301. value: z.number(),
  302. max: z.number().nullable(),
  303. label: z.string().nullable(),
  304. }),
  305. description: "Progress bar (value 0-100)",
  306. example: { value: 65, max: 100, label: "Upload progress" },
  307. },
  308. Skeleton: {
  309. props: z.object({
  310. width: z.string().nullable(),
  311. height: z.string().nullable(),
  312. rounded: z.boolean().nullable(),
  313. }),
  314. description: "Loading placeholder skeleton",
  315. },
  316. Spinner: {
  317. props: z.object({
  318. size: z.enum(["sm", "md", "lg"]).nullable(),
  319. label: z.string().nullable(),
  320. }),
  321. description: "Loading spinner indicator",
  322. },
  323. Tooltip: {
  324. props: z.object({
  325. content: z.string(),
  326. text: z.string(),
  327. }),
  328. description: "Hover tooltip. Shows content on hover over text.",
  329. },
  330. Popover: {
  331. props: z.object({
  332. trigger: z.string(),
  333. content: z.string(),
  334. }),
  335. description: "Popover that appears on click of trigger.",
  336. },
  337. Rating: {
  338. props: z.object({
  339. value: z.number(),
  340. max: z.number().nullable(),
  341. label: z.string().nullable(),
  342. interactive: z.boolean().nullable(),
  343. }),
  344. events: ["change"],
  345. description:
  346. "Interactive star rating. Use { $bindState } on value for binding. Set interactive: false for read-only display.",
  347. example: { value: 4, max: 5, label: "Rating" },
  348. },
  349. Metric: {
  350. props: z.object({
  351. label: z.string(),
  352. value: z.string(),
  353. change: z.string().nullable(),
  354. changeType: z.enum(["positive", "negative", "neutral"]).nullable(),
  355. prefix: z.string().nullable(),
  356. suffix: z.string().nullable(),
  357. }),
  358. description:
  359. "Key metric / stat display. Shows a large value with label and optional change indicator. Use for dashboard KPIs.",
  360. example: {
  361. label: "Total Revenue",
  362. value: "125,000",
  363. prefix: "$",
  364. change: "+12.5%",
  365. changeType: "positive",
  366. },
  367. },
  368. // ── Charts ──────────────────────────────────────────────────────────
  369. BarGraph: {
  370. props: z.object({
  371. title: z.string().nullable(),
  372. data: z.array(
  373. z.object({
  374. label: z.string(),
  375. value: z.number(),
  376. }),
  377. ),
  378. }),
  379. description: "Vertical bar chart",
  380. },
  381. LineGraph: {
  382. props: z.object({
  383. title: z.string().nullable(),
  384. data: z.array(
  385. z.object({
  386. label: z.string(),
  387. value: z.number(),
  388. }),
  389. ),
  390. }),
  391. description: "Line chart with points",
  392. },
  393. // ── Form Inputs ─────────────────────────────────────────────────────
  394. Input: {
  395. props: z.object({
  396. label: z.string(),
  397. name: z.string(),
  398. type: z.enum(["text", "email", "password", "number"]).nullable(),
  399. placeholder: z.string().nullable(),
  400. value: z.string().nullable(),
  401. checks: z
  402. .array(
  403. z.object({
  404. type: z.string(),
  405. message: z.string(),
  406. args: z.record(z.string(), z.unknown()).optional(),
  407. }),
  408. )
  409. .nullable(),
  410. }),
  411. events: ["submit", "focus", "blur"],
  412. description:
  413. "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).",
  414. example: {
  415. label: "Email",
  416. name: "email",
  417. type: "email",
  418. placeholder: "you@example.com",
  419. },
  420. },
  421. Textarea: {
  422. props: z.object({
  423. label: z.string(),
  424. name: z.string(),
  425. placeholder: z.string().nullable(),
  426. rows: z.number().nullable(),
  427. value: z.string().nullable(),
  428. checks: z
  429. .array(
  430. z.object({
  431. type: z.string(),
  432. message: z.string(),
  433. args: z.record(z.string(), z.unknown()).optional(),
  434. }),
  435. )
  436. .nullable(),
  437. }),
  438. description:
  439. "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.",
  440. },
  441. Select: {
  442. props: z.object({
  443. label: z.string(),
  444. name: z.string(),
  445. options: z.array(z.string()),
  446. placeholder: z.string().nullable(),
  447. value: z.string().nullable(),
  448. checks: z
  449. .array(
  450. z.object({
  451. type: z.string(),
  452. message: z.string(),
  453. args: z.record(z.string(), z.unknown()).optional(),
  454. }),
  455. )
  456. .nullable(),
  457. }),
  458. events: ["change"],
  459. description:
  460. "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.",
  461. },
  462. Checkbox: {
  463. props: z.object({
  464. label: z.string(),
  465. name: z.string(),
  466. checked: z.boolean().nullable(),
  467. }),
  468. events: ["change"],
  469. description: "Checkbox input. Use { $bindState } on checked for binding.",
  470. },
  471. Radio: {
  472. props: z.object({
  473. label: z.string(),
  474. name: z.string(),
  475. options: z.array(z.string()),
  476. value: z.string().nullable(),
  477. }),
  478. events: ["change"],
  479. description:
  480. "Radio button group. Use { $bindState } on value for binding.",
  481. },
  482. Switch: {
  483. props: z.object({
  484. label: z.string(),
  485. name: z.string(),
  486. checked: z.boolean().nullable(),
  487. }),
  488. events: ["change"],
  489. description: "Toggle switch. Use { $bindState } on checked for binding.",
  490. },
  491. Slider: {
  492. props: z.object({
  493. label: z.string().nullable(),
  494. min: z.number().nullable(),
  495. max: z.number().nullable(),
  496. step: z.number().nullable(),
  497. value: z.number().nullable(),
  498. }),
  499. events: ["change"],
  500. description:
  501. "Range slider input. Use { $bindState } on value for binding.",
  502. },
  503. // ── Actions ─────────────────────────────────────────────────────────
  504. Button: {
  505. props: z.object({
  506. label: z.string(),
  507. variant: z
  508. .enum(["primary", "secondary", "outline", "danger"])
  509. .nullable(),
  510. disabled: z.boolean().nullable(),
  511. }),
  512. events: ["press"],
  513. description:
  514. "Clickable button. primary = solid fill, outline = bordered/transparent, secondary = muted fill. Bind on.press for handler.",
  515. example: { label: "Submit", variant: "primary" },
  516. },
  517. Link: {
  518. props: z.object({
  519. label: z.string(),
  520. href: z.string(),
  521. }),
  522. events: ["press"],
  523. description: "Anchor link. Bind on.press for click handler.",
  524. },
  525. DropdownMenu: {
  526. props: z.object({
  527. label: z.string(),
  528. items: z.array(
  529. z.object({
  530. label: z.string(),
  531. value: z.string(),
  532. }),
  533. ),
  534. }),
  535. events: ["select"],
  536. description: "Dropdown menu with trigger button and selectable items.",
  537. },
  538. Toggle: {
  539. props: z.object({
  540. label: z.string(),
  541. pressed: z.boolean().nullable(),
  542. variant: z.enum(["default", "outline"]).nullable(),
  543. }),
  544. events: ["change"],
  545. description:
  546. "Toggle button. Use { $bindState } on pressed for state binding.",
  547. },
  548. ToggleGroup: {
  549. props: z.object({
  550. items: z.array(
  551. z.object({
  552. label: z.string(),
  553. value: z.string(),
  554. }),
  555. ),
  556. type: z.enum(["single", "multiple"]).nullable(),
  557. value: z.string().nullable(),
  558. }),
  559. events: ["change"],
  560. description:
  561. "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.",
  562. },
  563. ButtonGroup: {
  564. props: z.object({
  565. buttons: z.array(
  566. z.object({
  567. label: z.string(),
  568. value: z.string(),
  569. }),
  570. ),
  571. selected: z.string().nullable(),
  572. }),
  573. events: ["change"],
  574. description:
  575. "Segmented button group. Use { $bindState } on selected for selected value.",
  576. },
  577. Pagination: {
  578. props: z.object({
  579. totalPages: z.number(),
  580. page: z.number().nullable(),
  581. }),
  582. events: ["change"],
  583. description:
  584. "Page navigation. Use { $bindState } on page for current page number.",
  585. },
  586. },
  587. actions: {
  588. setState: {
  589. params: z.object({
  590. statePath: z.string(),
  591. value: z.unknown(),
  592. }),
  593. description: "Update a value in the state model at the given statePath.",
  594. },
  595. pushState: {
  596. params: z.object({
  597. statePath: z.string(),
  598. value: z.unknown(),
  599. clearStatePath: z.string().optional(),
  600. }),
  601. description:
  602. '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.',
  603. },
  604. removeState: {
  605. params: z.object({
  606. statePath: z.string(),
  607. index: z.number(),
  608. }),
  609. description: "Remove an item from an array in state at the given index.",
  610. },
  611. buttonClick: {
  612. params: z.object({
  613. message: z.string().nullable(),
  614. }),
  615. description: "Shows a toast with the message.",
  616. },
  617. formSubmit: {
  618. params: z.object({
  619. formName: z.string().nullable(),
  620. }),
  621. description: "Shows a toast confirming form submission.",
  622. },
  623. linkClick: {
  624. params: z.object({
  625. href: z.string(),
  626. }),
  627. description: "Shows a toast with the link destination.",
  628. },
  629. },
  630. });