$npx -y skills add vercel-labs/json-render --skill coreCore package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.
| 1 | # @json-render/core |
| 2 | |
| 3 | Core package for schema definition, catalog creation, and spec streaming. |
| 4 | |
| 5 | ## Key Concepts |
| 6 | |
| 7 | - **Schema**: Defines the structure of specs and catalogs (use `defineSchema`) |
| 8 | - **Catalog**: Maps component/action names to their definitions (use `defineCatalog`) |
| 9 | - **Spec**: JSON output from AI that conforms to the schema |
| 10 | - **SpecStream**: JSONL streaming format for progressive spec building |
| 11 | |
| 12 | ## Defining a Schema |
| 13 | |
| 14 | ```typescript |
| 15 | import { defineSchema } from "@json-render/core"; |
| 16 | |
| 17 | export const schema = defineSchema((s) => ({ |
| 18 | spec: s.object({ |
| 19 | // Define spec structure |
| 20 | }), |
| 21 | catalog: s.object({ |
| 22 | components: s.map({ |
| 23 | props: s.zod(), |
| 24 | description: s.string(), |
| 25 | }), |
| 26 | }), |
| 27 | }), { |
| 28 | promptTemplate: myPromptTemplate, // Optional custom AI prompt |
| 29 | }); |
| 30 | ``` |
| 31 | |
| 32 | ## Creating a Catalog |
| 33 | |
| 34 | ```typescript |
| 35 | import { defineCatalog } from "@json-render/core"; |
| 36 | import { schema } from "./schema"; |
| 37 | import { z } from "zod"; |
| 38 | |
| 39 | export const catalog = defineCatalog(schema, { |
| 40 | components: { |
| 41 | Button: { |
| 42 | props: z.object({ |
| 43 | label: z.string(), |
| 44 | variant: z.enum(["primary", "secondary"]).nullable(), |
| 45 | }), |
| 46 | description: "Clickable button component", |
| 47 | }, |
| 48 | }, |
| 49 | }); |
| 50 | ``` |
| 51 | |
| 52 | ## Generating AI Prompts |
| 53 | |
| 54 | ```typescript |
| 55 | const systemPrompt = catalog.prompt(); // Uses schema's promptTemplate |
| 56 | const systemPrompt = catalog.prompt({ customRules: ["Rule 1", "Rule 2"] }); |
| 57 | ``` |
| 58 | |
| 59 | ## SpecStream Utilities |
| 60 | |
| 61 | For streaming AI responses (JSONL patches): |
| 62 | |
| 63 | ```typescript |
| 64 | import { createSpecStreamCompiler } from "@json-render/core"; |
| 65 | |
| 66 | const compiler = createSpecStreamCompiler<MySpec>(); |
| 67 | |
| 68 | // Process streaming chunks |
| 69 | const { result, newPatches } = compiler.push(chunk); |
| 70 | |
| 71 | // Get final result |
| 72 | const finalSpec = compiler.getResult(); |
| 73 | ``` |
| 74 | |
| 75 | ## Dynamic Prop Expressions |
| 76 | |
| 77 | Any prop value can be a dynamic expression resolved at render time: |
| 78 | |
| 79 | - **`{ "$state": "/state/key" }`** - reads a value from the state model (one-way read) |
| 80 | - **`{ "$bindState": "/path" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components. |
| 81 | - **`{ "$bindItem": "field" }`** - two-way binding to a repeat item field. Use inside repeat scopes. |
| 82 | - **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a visibility condition and picks a branch |
| 83 | - **`{ "$template": "Hello, ${/user/name}!" }`** - interpolates `${/path}` references with state values |
| 84 | - **`{ "$computed": "fnName", "args": { "key": <expression> } }`** - calls a registered function with resolved args |
| 85 | |
| 86 | `$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive). |
| 87 | |
| 88 | Components do not use a `statePath` prop for two-way binding. Instead, use `{ "$bindState": "/path" }` on the natural value prop (e.g. `value`, `checked`, `pressed`). |
| 89 | |
| 90 | ```json |
| 91 | { |
| 92 | "color": { |
| 93 | "$cond": { "$state": "/activeTab", "eq": "home" }, |
| 94 | "$then": "#007AFF", |
| 95 | "$else": "#8E8E93" |
| 96 | }, |
| 97 | "label": { "$template": "Welcome, ${/user/name}!" }, |
| 98 | "fullName": { |
| 99 | "$computed": "fullName", |
| 100 | "args": { |
| 101 | "first": { "$state": "/form/firstName" }, |
| 102 | "last": { "$state": "/form/lastName" } |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | ```typescript |
| 109 | import { resolvePropValue, resolveElementProps } from "@json-render/core"; |
| 110 | |
| 111 | const resolved = resolveElementProps(element.props, { stateModel: myState }); |
| 112 | ``` |
| 113 | |
| 114 | ## State Watchers |
| 115 | |
| 116 | Elements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change: |
| 117 | |
| 118 | ```json |
| 119 | { |
| 120 | "type": "Select", |
| 121 | "props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] }, |
| 122 | "watch": { |
| 123 | "/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } } |
| 124 | }, |
| 125 | "children": [] |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | Watchers only fire on value changes, not on initial render. |
| 130 | |
| 131 | ## Validation |
| 132 | |
| 133 | Built-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`. |
| 134 | |
| 135 | Cross-field validation uses `$state` expressions in args: |
| 136 | |
| 137 | ```typescript |
| 138 | import { check } from "@json-render/core"; |
| 139 | |
| 140 | check.required("Field is required"); |
| 141 | check.matches("/form/password", "Passwords must match"); |
| 142 | check.lessThan("/form/endDate", "Must be before end date"); |
| 143 | check.greaterThan("/form/startDate", "Must be after start date"); |
| 144 | check.requiredIf("/form/enableNotifications", "Required when enabled"); |
| 145 | ``` |
| 146 | |
| 147 | ## User Prompt Builder |
| 148 | |
| 149 | Build structured user prompts with optional spec refinement and state context: |
| 150 | |
| 151 | ```typescript |
| 152 | import { buildUserPrompt } from "@json-render/core"; |
| 153 | |
| 154 | // Fresh generation |
| 155 | buildUserPrompt({ prompt: "create a todo app" }); |
| 156 | |
| 157 | // Refineme |