catalog.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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. }),
  37. slots: ["default"],
  38. description: "Flex container for layouts",
  39. example: { direction: "vertical", gap: "md" },
  40. },
  41. Grid: {
  42. props: z.object({
  43. columns: z.number().nullable(),
  44. gap: z.enum(["sm", "md", "lg"]).nullable(),
  45. }),
  46. slots: ["default"],
  47. description: "Grid layout (1-6 columns)",
  48. example: { columns: 3, gap: "md" },
  49. },
  50. Separator: {
  51. props: z.object({
  52. orientation: z.enum(["horizontal", "vertical"]).nullable(),
  53. }),
  54. description: "Visual separator line",
  55. },
  56. Tabs: {
  57. props: z.object({
  58. tabs: z.array(
  59. z.object({
  60. label: z.string(),
  61. value: z.string(),
  62. }),
  63. ),
  64. defaultValue: z.string().nullable(),
  65. value: z.string().nullable(),
  66. }),
  67. events: ["change"],
  68. description:
  69. "Tab navigation. Use { $bindState } on value for active tab binding.",
  70. },
  71. Accordion: {
  72. props: z.object({
  73. items: z.array(
  74. z.object({
  75. title: z.string(),
  76. content: z.string(),
  77. }),
  78. ),
  79. type: z.enum(["single", "multiple"]).nullable(),
  80. }),
  81. description:
  82. "Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.",
  83. },
  84. Collapsible: {
  85. props: z.object({
  86. title: z.string(),
  87. defaultOpen: z.boolean().nullable(),
  88. }),
  89. slots: ["default"],
  90. description: "Collapsible section with trigger. Children render inside.",
  91. },
  92. Dialog: {
  93. props: z.object({
  94. title: z.string(),
  95. description: z.string().nullable(),
  96. openPath: z.string(),
  97. }),
  98. slots: ["default"],
  99. description:
  100. "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.",
  101. },
  102. Drawer: {
  103. props: z.object({
  104. title: z.string(),
  105. description: z.string().nullable(),
  106. openPath: z.string(),
  107. }),
  108. slots: ["default"],
  109. description:
  110. "Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.",
  111. },
  112. Carousel: {
  113. props: z.object({
  114. items: z.array(
  115. z.object({
  116. title: z.string().nullable(),
  117. description: z.string().nullable(),
  118. }),
  119. ),
  120. }),
  121. description: "Horizontally scrollable carousel of cards.",
  122. },
  123. // ── Data Display ────────────────────────────────────────────────────
  124. Table: {
  125. props: z.object({
  126. columns: z.array(z.string()),
  127. rows: z.array(z.array(z.string())),
  128. caption: z.string().nullable(),
  129. }),
  130. description:
  131. 'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
  132. example: {
  133. columns: ["Name", "Role"],
  134. rows: [
  135. ["Alice", "Admin"],
  136. ["Bob", "User"],
  137. ],
  138. },
  139. },
  140. Heading: {
  141. props: z.object({
  142. text: z.string(),
  143. level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
  144. }),
  145. description: "Heading text (h1-h4)",
  146. example: { text: "Welcome", level: "h1" },
  147. },
  148. Text: {
  149. props: z.object({
  150. text: z.string(),
  151. variant: z
  152. .enum(["body", "caption", "muted", "lead", "code"])
  153. .nullable(),
  154. }),
  155. description:
  156. 'Paragraph text. In repeat scopes, use { "$template": "${field1} ${field2}" } to interpolate item fields.',
  157. example: { text: "Hello, world!" },
  158. },
  159. Image: {
  160. props: z.object({
  161. alt: z.string(),
  162. width: z.number().nullable(),
  163. height: z.number().nullable(),
  164. }),
  165. description: "Placeholder image (displays alt text in a styled box)",
  166. },
  167. Icon: {
  168. props: z.object({
  169. name: z.string(),
  170. size: z.enum(["sm", "md", "lg"]).nullable(),
  171. color: z
  172. .enum(["default", "muted", "primary", "success", "warning", "danger"])
  173. .nullable(),
  174. }),
  175. description:
  176. "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. Use in horizontal Stacks with Text for icon+label patterns. Never use emoji — always use Icon.",
  177. },
  178. Avatar: {
  179. props: z.object({
  180. src: z.string().nullable(),
  181. name: z.string(),
  182. size: z.enum(["sm", "md", "lg"]).nullable(),
  183. }),
  184. description: "User avatar with fallback initials",
  185. example: { name: "Jane Doe", size: "md" },
  186. },
  187. Badge: {
  188. props: z.object({
  189. text: z.string(),
  190. variant: z.enum(["default", "success", "warning", "danger"]).nullable(),
  191. }),
  192. description: "Status badge",
  193. example: { text: "Active", variant: "success" },
  194. },
  195. Alert: {
  196. props: z.object({
  197. title: z.string(),
  198. message: z.string().nullable(),
  199. type: z.enum(["info", "success", "warning", "error"]).nullable(),
  200. }),
  201. description: "Alert banner",
  202. example: {
  203. title: "Note",
  204. message: "Your changes have been saved.",
  205. type: "success",
  206. },
  207. },
  208. Progress: {
  209. props: z.object({
  210. value: z.number(),
  211. max: z.number().nullable(),
  212. label: z.string().nullable(),
  213. }),
  214. description: "Progress bar (value 0-100)",
  215. example: { value: 65, max: 100, label: "Upload progress" },
  216. },
  217. Skeleton: {
  218. props: z.object({
  219. width: z.string().nullable(),
  220. height: z.string().nullable(),
  221. rounded: z.boolean().nullable(),
  222. }),
  223. description: "Loading placeholder skeleton",
  224. },
  225. Spinner: {
  226. props: z.object({
  227. size: z.enum(["sm", "md", "lg"]).nullable(),
  228. label: z.string().nullable(),
  229. }),
  230. description: "Loading spinner indicator",
  231. },
  232. Tooltip: {
  233. props: z.object({
  234. content: z.string(),
  235. text: z.string(),
  236. }),
  237. description: "Hover tooltip. Shows content on hover over text.",
  238. },
  239. Popover: {
  240. props: z.object({
  241. trigger: z.string(),
  242. content: z.string(),
  243. }),
  244. description: "Popover that appears on click of trigger.",
  245. },
  246. Rating: {
  247. props: z.object({
  248. value: z.number(),
  249. max: z.number().nullable(),
  250. label: z.string().nullable(),
  251. interactive: z.boolean().nullable(),
  252. }),
  253. events: ["change"],
  254. description:
  255. "Interactive star rating. Use { $bindState } on value for binding. Set interactive: false for read-only display.",
  256. example: { value: 4, max: 5, label: "Rating" },
  257. },
  258. Metric: {
  259. props: z.object({
  260. label: z.string(),
  261. value: z.string(),
  262. change: z.string().nullable(),
  263. changeType: z.enum(["positive", "negative", "neutral"]).nullable(),
  264. prefix: z.string().nullable(),
  265. suffix: z.string().nullable(),
  266. }),
  267. description:
  268. "Key metric / stat display. Shows a large value with label and optional change indicator. Use for dashboard KPIs.",
  269. example: {
  270. label: "Total Revenue",
  271. value: "125,000",
  272. prefix: "$",
  273. change: "+12.5%",
  274. changeType: "positive",
  275. },
  276. },
  277. // ── Charts ──────────────────────────────────────────────────────────
  278. BarGraph: {
  279. props: z.object({
  280. title: z.string().nullable(),
  281. data: z.array(
  282. z.object({
  283. label: z.string(),
  284. value: z.number(),
  285. }),
  286. ),
  287. }),
  288. description: "Vertical bar chart",
  289. },
  290. LineGraph: {
  291. props: z.object({
  292. title: z.string().nullable(),
  293. data: z.array(
  294. z.object({
  295. label: z.string(),
  296. value: z.number(),
  297. }),
  298. ),
  299. }),
  300. description: "Line chart with points",
  301. },
  302. // ── Form Inputs ─────────────────────────────────────────────────────
  303. Input: {
  304. props: z.object({
  305. label: z.string(),
  306. name: z.string(),
  307. type: z.enum(["text", "email", "password", "number"]).nullable(),
  308. placeholder: z.string().nullable(),
  309. value: z.string().nullable(),
  310. checks: z
  311. .array(
  312. z.object({
  313. type: z.string(),
  314. message: z.string(),
  315. args: z.record(z.string(), z.unknown()).optional(),
  316. }),
  317. )
  318. .nullable(),
  319. }),
  320. events: ["submit", "focus", "blur"],
  321. description:
  322. "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).",
  323. example: {
  324. label: "Email",
  325. name: "email",
  326. type: "email",
  327. placeholder: "you@example.com",
  328. },
  329. },
  330. Textarea: {
  331. props: z.object({
  332. label: z.string(),
  333. name: z.string(),
  334. placeholder: z.string().nullable(),
  335. rows: z.number().nullable(),
  336. value: z.string().nullable(),
  337. checks: z
  338. .array(
  339. z.object({
  340. type: z.string(),
  341. message: z.string(),
  342. args: z.record(z.string(), z.unknown()).optional(),
  343. }),
  344. )
  345. .nullable(),
  346. }),
  347. description:
  348. "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.",
  349. },
  350. Select: {
  351. props: z.object({
  352. label: z.string(),
  353. name: z.string(),
  354. options: z.array(z.string()),
  355. placeholder: z.string().nullable(),
  356. value: z.string().nullable(),
  357. checks: z
  358. .array(
  359. z.object({
  360. type: z.string(),
  361. message: z.string(),
  362. args: z.record(z.string(), z.unknown()).optional(),
  363. }),
  364. )
  365. .nullable(),
  366. }),
  367. events: ["change"],
  368. description:
  369. "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.",
  370. },
  371. Checkbox: {
  372. props: z.object({
  373. label: z.string(),
  374. name: z.string(),
  375. checked: z.boolean().nullable(),
  376. }),
  377. events: ["change"],
  378. description: "Checkbox input. Use { $bindState } on checked for binding.",
  379. },
  380. Radio: {
  381. props: z.object({
  382. label: z.string(),
  383. name: z.string(),
  384. options: z.array(z.string()),
  385. value: z.string().nullable(),
  386. }),
  387. events: ["change"],
  388. description:
  389. "Radio button group. Use { $bindState } on value for binding.",
  390. },
  391. Switch: {
  392. props: z.object({
  393. label: z.string(),
  394. name: z.string(),
  395. checked: z.boolean().nullable(),
  396. }),
  397. events: ["change"],
  398. description: "Toggle switch. Use { $bindState } on checked for binding.",
  399. },
  400. Slider: {
  401. props: z.object({
  402. label: z.string().nullable(),
  403. min: z.number().nullable(),
  404. max: z.number().nullable(),
  405. step: z.number().nullable(),
  406. value: z.number().nullable(),
  407. }),
  408. events: ["change"],
  409. description:
  410. "Range slider input. Use { $bindState } on value for binding.",
  411. },
  412. // ── Actions ─────────────────────────────────────────────────────────
  413. Button: {
  414. props: z.object({
  415. label: z.string(),
  416. variant: z
  417. .enum(["primary", "secondary", "outline", "danger"])
  418. .nullable(),
  419. disabled: z.boolean().nullable(),
  420. }),
  421. events: ["press"],
  422. description:
  423. "Clickable button. primary = solid fill, outline = bordered/transparent, secondary = muted fill. Bind on.press for handler.",
  424. example: { label: "Submit", variant: "primary" },
  425. },
  426. Link: {
  427. props: z.object({
  428. label: z.string(),
  429. href: z.string(),
  430. }),
  431. events: ["press"],
  432. description: "Anchor link. Bind on.press for click handler.",
  433. },
  434. DropdownMenu: {
  435. props: z.object({
  436. label: z.string(),
  437. items: z.array(
  438. z.object({
  439. label: z.string(),
  440. value: z.string(),
  441. }),
  442. ),
  443. }),
  444. events: ["select"],
  445. description: "Dropdown menu with trigger button and selectable items.",
  446. },
  447. Toggle: {
  448. props: z.object({
  449. label: z.string(),
  450. pressed: z.boolean().nullable(),
  451. variant: z.enum(["default", "outline"]).nullable(),
  452. }),
  453. events: ["change"],
  454. description:
  455. "Toggle button. Use { $bindState } on pressed for state binding.",
  456. },
  457. ToggleGroup: {
  458. props: z.object({
  459. items: z.array(
  460. z.object({
  461. label: z.string(),
  462. value: z.string(),
  463. }),
  464. ),
  465. type: z.enum(["single", "multiple"]).nullable(),
  466. value: z.string().nullable(),
  467. }),
  468. events: ["change"],
  469. description:
  470. "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.",
  471. },
  472. ButtonGroup: {
  473. props: z.object({
  474. buttons: z.array(
  475. z.object({
  476. label: z.string(),
  477. value: z.string(),
  478. }),
  479. ),
  480. selected: z.string().nullable(),
  481. }),
  482. events: ["change"],
  483. description:
  484. "Segmented button group. Use { $bindState } on selected for selected value.",
  485. },
  486. Pagination: {
  487. props: z.object({
  488. totalPages: z.number(),
  489. page: z.number().nullable(),
  490. }),
  491. events: ["change"],
  492. description:
  493. "Page navigation. Use { $bindState } on page for current page number.",
  494. },
  495. },
  496. actions: {
  497. setState: {
  498. params: z.object({
  499. statePath: z.string(),
  500. value: z.unknown(),
  501. }),
  502. description: "Update a value in the state model at the given statePath.",
  503. },
  504. pushState: {
  505. params: z.object({
  506. statePath: z.string(),
  507. value: z.unknown(),
  508. clearStatePath: z.string().optional(),
  509. }),
  510. description:
  511. '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.',
  512. },
  513. removeState: {
  514. params: z.object({
  515. statePath: z.string(),
  516. index: z.number(),
  517. }),
  518. description: "Remove an item from an array in state at the given index.",
  519. },
  520. buttonClick: {
  521. params: z.object({
  522. message: z.string().nullable(),
  523. }),
  524. description: "Shows a toast with the message.",
  525. },
  526. formSubmit: {
  527. params: z.object({
  528. formName: z.string().nullable(),
  529. }),
  530. description: "Shows a toast confirming form submission.",
  531. },
  532. linkClick: {
  533. params: z.object({
  534. href: z.string(),
  535. }),
  536. description: "Shows a toast with the link destination.",
  537. },
  538. },
  539. });