catalog.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  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: "Paragraph text",
  156. example: { text: "Hello, world!" },
  157. },
  158. Image: {
  159. props: z.object({
  160. alt: z.string(),
  161. width: z.number().nullable(),
  162. height: z.number().nullable(),
  163. }),
  164. description: "Placeholder image (displays alt text in a styled box)",
  165. },
  166. Avatar: {
  167. props: z.object({
  168. src: z.string().nullable(),
  169. name: z.string(),
  170. size: z.enum(["sm", "md", "lg"]).nullable(),
  171. }),
  172. description: "User avatar with fallback initials",
  173. example: { name: "Jane Doe", size: "md" },
  174. },
  175. Badge: {
  176. props: z.object({
  177. text: z.string(),
  178. variant: z.enum(["default", "success", "warning", "danger"]).nullable(),
  179. }),
  180. description: "Status badge",
  181. example: { text: "Active", variant: "success" },
  182. },
  183. Alert: {
  184. props: z.object({
  185. title: z.string(),
  186. message: z.string().nullable(),
  187. type: z.enum(["info", "success", "warning", "error"]).nullable(),
  188. }),
  189. description: "Alert banner",
  190. example: {
  191. title: "Note",
  192. message: "Your changes have been saved.",
  193. type: "success",
  194. },
  195. },
  196. Progress: {
  197. props: z.object({
  198. value: z.number(),
  199. max: z.number().nullable(),
  200. label: z.string().nullable(),
  201. }),
  202. description: "Progress bar (value 0-100)",
  203. example: { value: 65, max: 100, label: "Upload progress" },
  204. },
  205. Skeleton: {
  206. props: z.object({
  207. width: z.string().nullable(),
  208. height: z.string().nullable(),
  209. rounded: z.boolean().nullable(),
  210. }),
  211. description: "Loading placeholder skeleton",
  212. },
  213. Spinner: {
  214. props: z.object({
  215. size: z.enum(["sm", "md", "lg"]).nullable(),
  216. label: z.string().nullable(),
  217. }),
  218. description: "Loading spinner indicator",
  219. },
  220. Tooltip: {
  221. props: z.object({
  222. content: z.string(),
  223. text: z.string(),
  224. }),
  225. description: "Hover tooltip. Shows content on hover over text.",
  226. },
  227. Popover: {
  228. props: z.object({
  229. trigger: z.string(),
  230. content: z.string(),
  231. }),
  232. description: "Popover that appears on click of trigger.",
  233. },
  234. Rating: {
  235. props: z.object({
  236. value: z.number(),
  237. max: z.number().nullable(),
  238. label: z.string().nullable(),
  239. }),
  240. description: "Star rating display",
  241. },
  242. // ── Charts ──────────────────────────────────────────────────────────
  243. BarGraph: {
  244. props: z.object({
  245. title: z.string().nullable(),
  246. data: z.array(
  247. z.object({
  248. label: z.string(),
  249. value: z.number(),
  250. }),
  251. ),
  252. }),
  253. description: "Vertical bar chart",
  254. },
  255. LineGraph: {
  256. props: z.object({
  257. title: z.string().nullable(),
  258. data: z.array(
  259. z.object({
  260. label: z.string(),
  261. value: z.number(),
  262. }),
  263. ),
  264. }),
  265. description: "Line chart with points",
  266. },
  267. // ── Form Inputs ─────────────────────────────────────────────────────
  268. Input: {
  269. props: z.object({
  270. label: z.string(),
  271. name: z.string(),
  272. type: z.enum(["text", "email", "password", "number"]).nullable(),
  273. placeholder: z.string().nullable(),
  274. value: z.string().nullable(),
  275. checks: z
  276. .array(
  277. z.object({
  278. type: z.string(),
  279. message: z.string(),
  280. args: z.record(z.string(), z.unknown()).optional(),
  281. }),
  282. )
  283. .nullable(),
  284. }),
  285. events: ["submit", "focus", "blur"],
  286. description:
  287. "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).",
  288. example: {
  289. label: "Email",
  290. name: "email",
  291. type: "email",
  292. placeholder: "you@example.com",
  293. },
  294. },
  295. Textarea: {
  296. props: z.object({
  297. label: z.string(),
  298. name: z.string(),
  299. placeholder: z.string().nullable(),
  300. rows: z.number().nullable(),
  301. value: z.string().nullable(),
  302. checks: z
  303. .array(
  304. z.object({
  305. type: z.string(),
  306. message: z.string(),
  307. args: z.record(z.string(), z.unknown()).optional(),
  308. }),
  309. )
  310. .nullable(),
  311. }),
  312. description:
  313. "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.",
  314. },
  315. Select: {
  316. props: z.object({
  317. label: z.string(),
  318. name: z.string(),
  319. options: z.array(z.string()),
  320. placeholder: z.string().nullable(),
  321. value: z.string().nullable(),
  322. checks: z
  323. .array(
  324. z.object({
  325. type: z.string(),
  326. message: z.string(),
  327. args: z.record(z.string(), z.unknown()).optional(),
  328. }),
  329. )
  330. .nullable(),
  331. }),
  332. events: ["change"],
  333. description:
  334. "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.",
  335. },
  336. Checkbox: {
  337. props: z.object({
  338. label: z.string(),
  339. name: z.string(),
  340. checked: z.boolean().nullable(),
  341. }),
  342. events: ["change"],
  343. description: "Checkbox input. Use { $bindState } on checked for binding.",
  344. },
  345. Radio: {
  346. props: z.object({
  347. label: z.string(),
  348. name: z.string(),
  349. options: z.array(z.string()),
  350. value: z.string().nullable(),
  351. }),
  352. events: ["change"],
  353. description:
  354. "Radio button group. Use { $bindState } on value for binding.",
  355. },
  356. Switch: {
  357. props: z.object({
  358. label: z.string(),
  359. name: z.string(),
  360. checked: z.boolean().nullable(),
  361. }),
  362. events: ["change"],
  363. description: "Toggle switch. Use { $bindState } on checked for binding.",
  364. },
  365. Slider: {
  366. props: z.object({
  367. label: z.string().nullable(),
  368. min: z.number().nullable(),
  369. max: z.number().nullable(),
  370. step: z.number().nullable(),
  371. value: z.number().nullable(),
  372. }),
  373. events: ["change"],
  374. description:
  375. "Range slider input. Use { $bindState } on value for binding.",
  376. },
  377. // ── Actions ─────────────────────────────────────────────────────────
  378. Button: {
  379. props: z.object({
  380. label: z.string(),
  381. variant: z.enum(["primary", "secondary", "danger"]).nullable(),
  382. disabled: z.boolean().nullable(),
  383. }),
  384. events: ["press"],
  385. description: "Clickable button. Bind on.press for handler.",
  386. example: { label: "Submit", variant: "primary" },
  387. },
  388. Link: {
  389. props: z.object({
  390. label: z.string(),
  391. href: z.string(),
  392. }),
  393. events: ["press"],
  394. description: "Anchor link. Bind on.press for click handler.",
  395. },
  396. DropdownMenu: {
  397. props: z.object({
  398. label: z.string(),
  399. items: z.array(
  400. z.object({
  401. label: z.string(),
  402. value: z.string(),
  403. }),
  404. ),
  405. }),
  406. events: ["select"],
  407. description: "Dropdown menu with trigger button and selectable items.",
  408. },
  409. Toggle: {
  410. props: z.object({
  411. label: z.string(),
  412. pressed: z.boolean().nullable(),
  413. variant: z.enum(["default", "outline"]).nullable(),
  414. }),
  415. events: ["change"],
  416. description:
  417. "Toggle button. Use { $bindState } on pressed for state binding.",
  418. },
  419. ToggleGroup: {
  420. props: z.object({
  421. items: z.array(
  422. z.object({
  423. label: z.string(),
  424. value: z.string(),
  425. }),
  426. ),
  427. type: z.enum(["single", "multiple"]).nullable(),
  428. value: z.string().nullable(),
  429. }),
  430. events: ["change"],
  431. description:
  432. "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.",
  433. },
  434. ButtonGroup: {
  435. props: z.object({
  436. buttons: z.array(
  437. z.object({
  438. label: z.string(),
  439. value: z.string(),
  440. }),
  441. ),
  442. selected: z.string().nullable(),
  443. }),
  444. events: ["change"],
  445. description:
  446. "Segmented button group. Use { $bindState } on selected for selected value.",
  447. },
  448. Pagination: {
  449. props: z.object({
  450. totalPages: z.number(),
  451. page: z.number().nullable(),
  452. }),
  453. events: ["change"],
  454. description:
  455. "Page navigation. Use { $bindState } on page for current page number.",
  456. },
  457. },
  458. actions: {
  459. setState: {
  460. params: z.object({
  461. statePath: z.string(),
  462. value: z.unknown(),
  463. }),
  464. description: "Update a value in the state model at the given statePath.",
  465. },
  466. pushState: {
  467. params: z.object({
  468. statePath: z.string(),
  469. value: z.unknown(),
  470. clearStatePath: z.string().optional(),
  471. }),
  472. description:
  473. '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.',
  474. },
  475. removeState: {
  476. params: z.object({
  477. statePath: z.string(),
  478. index: z.number(),
  479. }),
  480. description: "Remove an item from an array in state at the given index.",
  481. },
  482. buttonClick: {
  483. params: z.object({
  484. message: z.string().nullable(),
  485. }),
  486. description: "Shows a toast with the message.",
  487. },
  488. formSubmit: {
  489. params: z.object({
  490. formName: z.string().nullable(),
  491. }),
  492. description: "Shows a toast confirming form submission.",
  493. },
  494. linkClick: {
  495. params: z.object({
  496. href: z.string(),
  497. }),
  498. description: "Shows a toast with the link destination.",
  499. },
  500. },
  501. });