@json-render/core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@ export const schema = defineSchema((s) => ({
27
27
  root: s.object({
28
28
  type: s.ref("catalog.components"),
29
29
  props: s.propsOf("catalog.components"),
30
- children: s.array(s.self()),
30
+ children: s.array(s.string()), // Element keys (flat spec format)
31
31
  }),
32
32
  }),
33
33
  catalog: s.object({
@@ -119,9 +119,9 @@ const finalSpec = compiler.getResult();
119
119
  SpecStream format uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations (each line is a patch):
120
120
 
121
121
  ```jsonl
122
- {"op":"add","path":"/root/type","value":"Card"}
123
- {"op":"add","path":"/root/props","value":{"title":"Hello"}}
124
- {"op":"add","path":"/root/children/0","value":{"type":"Button","props":{"label":"Click"}}}
122
+ {"op":"add","path":"/root","value":"card-1"}
123
+ {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":["btn-1"]}}
124
+ {"op":"add","path":"/elements/btn-1","value":{"type":"Button","props":{"label":"Click"},"children":[]}}
125
125
  ```
126
126
 
127
127
  All six RFC 6902 operations are supported: `add`, `remove`, `replace`, `move`, `copy`, `test`.
@@ -192,7 +192,7 @@ const spec = compileSpecStream<MySpec>(jsonlString);
192
192
 
193
193
  | Export | Purpose |
194
194
  |--------|---------|
195
- | `validateSpec(spec, catalog?)` | Validate spec structure and return issues |
195
+ | `validateSpec(spec, options?)` | Validate spec structure and return issues |
196
196
  | `autoFixSpec(spec)` | Auto-fix common spec issues (returns corrected copy) |
197
197
  | `formatSpecIssues(issues)` | Format validation issues as readable strings |
198
198
 
@@ -211,17 +211,33 @@ const spec = compileSpecStream<MySpec>(jsonlString);
211
211
 
212
212
  Any prop value can be a dynamic expression that resolves based on data state at render time. Expressions are resolved by the renderer before props reach components.
213
213
 
214
- ### Data Binding (`$path`)
214
+ ### Data Binding (`$state`)
215
215
 
216
216
  Read a value directly from the state model:
217
217
 
218
218
  ```json
219
219
  {
220
- "color": { "$path": "/theme/primary" },
221
- "label": { "$path": "/user/name" }
220
+ "color": { "$state": "/theme/primary" },
221
+ "label": { "$state": "/user/name" }
222
222
  }
223
223
  ```
224
224
 
225
+ ### Two-Way Binding (`$bindState` / `$bindItem`)
226
+
227
+ Use `{ "$bindState": "/path" }` on the natural value prop for form components that need read/write access. The component reads from and writes to the state path:
228
+
229
+ ```json
230
+ {
231
+ "type": "Input",
232
+ "props": {
233
+ "value": { "$bindState": "/form/email" },
234
+ "placeholder": "Email"
235
+ }
236
+ }
237
+ ```
238
+
239
+ Inside a repeat scope, use `{ "$bindItem": "completed" }` to bind to a field on the current item:
240
+
225
241
  ### Conditional (`$cond` / `$then` / `$else`)
226
242
 
227
243
  Evaluate a condition (same syntax as visibility conditions) and pick a value:
@@ -229,12 +245,12 @@ Evaluate a condition (same syntax as visibility conditions) and pick a value:
229
245
  ```json
230
246
  {
231
247
  "color": {
232
- "$cond": { "eq": [{ "path": "/activeTab" }, "home"] },
248
+ "$cond": { "$state": "/activeTab", "eq": "home" },
233
249
  "$then": "#007AFF",
234
250
  "$else": "#8E8E93"
235
251
  },
236
252
  "name": {
237
- "$cond": { "eq": [{ "path": "/activeTab" }, "home"] },
253
+ "$cond": { "$state": "/activeTab", "eq": "home" },
238
254
  "$then": "home",
239
255
  "$else": "home-outline"
240
256
  }
@@ -246,13 +262,33 @@ Evaluate a condition (same syntax as visibility conditions) and pick a value:
246
262
  ```json
247
263
  {
248
264
  "label": {
249
- "$cond": { "path": "/user/isAdmin" },
250
- "$then": { "$path": "/admin/greeting" },
265
+ "$cond": { "$state": "/user/isAdmin" },
266
+ "$then": { "$state": "/admin/greeting" },
251
267
  "$else": "Welcome"
252
268
  }
253
269
  }
254
270
  ```
255
271
 
272
+ ### Repeat Item (`$item`)
273
+
274
+ Inside children of a repeated element, read a field from the current array item:
275
+
276
+ ```json
277
+ { "$item": "title" }
278
+ ```
279
+
280
+ Use `""` to get the entire item object. `$item` takes a path string because items are typically objects with nested fields to navigate.
281
+
282
+ ### Repeat Index (`$index`)
283
+
284
+ Get the current array index inside a repeat:
285
+
286
+ ```json
287
+ { "$index": true }
288
+ ```
289
+
290
+ `$index` uses `true` as a sentinel flag because the index is a scalar value with no sub-path to navigate (unlike `$item` which needs a path).
291
+
256
292
  ### API
257
293
 
258
294
  ```typescript
@@ -260,7 +296,7 @@ import { resolvePropValue, resolveElementProps } from "@json-render/core";
260
296
 
261
297
  // Resolve a single value
262
298
  const color = resolvePropValue(
263
- { $cond: { eq: [{ path: "/active" }, "yes"] }, $then: "blue", $else: "gray" },
299
+ { $cond: { $state: "/active", eq: "yes" }, $then: "blue", $else: "gray" },
264
300
  { stateModel: myState }
265
301
  );
266
302
 
@@ -268,6 +304,45 @@ const color = resolvePropValue(
268
304
  const resolved = resolveElementProps(element.props, { stateModel: myState });
269
305
  ```
270
306
 
307
+ ## Visibility Conditions
308
+
309
+ Visibility conditions control when elements are shown. `VisibilityContext` is `{ stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }`.
310
+
311
+ ### Syntax
312
+
313
+ ```typescript
314
+ { "$state": "/path" } // truthiness
315
+ { "$state": "/path", "not": true } // falsy
316
+ { "$state": "/path", "eq": value } // equality
317
+ { "$state": "/path", "neq": value } // inequality
318
+ { "$state": "/path", "gt": number } // greater than
319
+ { "$item": "field" } // repeat item field
320
+ { "$index": true, "gt": 0 } // repeat index
321
+ [ condition, condition ] // implicit AND
322
+ { "$and": [ condition, condition ] } // explicit AND
323
+ { "$or": [ condition, condition ] } // OR
324
+ true / false // always / never
325
+ ```
326
+
327
+ ### TypeScript Helpers
328
+
329
+ ```typescript
330
+ import { visibility } from "@json-render/core";
331
+
332
+ visibility.always // true
333
+ visibility.never // false
334
+ visibility.when("/path") // { $state: "/path" }
335
+ visibility.unless("/path") // { $state: "/path", not: true }
336
+ visibility.eq("/path", val) // { $state: "/path", eq: val }
337
+ visibility.neq("/path", val) // { $state: "/path", neq: val }
338
+ visibility.gt("/path", n) // { $state: "/path", gt: n }
339
+ visibility.gte("/path", n) // { $state: "/path", gte: n }
340
+ visibility.lt("/path", n) // { $state: "/path", lt: n }
341
+ visibility.lte("/path", n) // { $state: "/path", lte: n }
342
+ visibility.and(cond1, cond2) // { $and: [cond1, cond2] }
343
+ visibility.or(cond1, cond2) // { $or: [cond1, cond2] }
344
+ ```
345
+
271
346
  ## User Prompt Builder
272
347
 
273
348
  Build structured user prompts for AI generation, with support for refinement and state context:
@@ -299,7 +374,7 @@ Validate spec structure and auto-fix common issues:
299
374
  import { validateSpec, autoFixSpec, formatSpecIssues } from "@json-render/core";
300
375
 
301
376
  // Validate a spec
302
- const { valid, issues } = validateSpec(spec, catalog);
377
+ const { valid, issues } = validateSpec(spec);
303
378
 
304
379
  // Format issues for display
305
380
  console.log(formatSpecIssues(issues));
@@ -313,8 +388,8 @@ const fixed = autoFixSpec(spec);
313
388
  json-render supports completely different spec formats for different renderers:
314
389
 
315
390
  ```typescript
316
- // React: Element tree
317
- { root: { type: "Card", props: {...}, children: [...] } }
391
+ // React: Flat element map
392
+ { root: "card-1", elements: { "card-1": { type: "Card", props: {...}, children: [...] } } }
318
393
 
319
394
  // Remotion: Timeline
320
395
  { composition: {...}, tracks: [...], clips: [...] }