@json-render/vue 0.19.0 → 0.21.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 +40 -5
- package/dist/{chunk-WIPZLAF7.mjs → chunk-34A2QV2X.mjs} +9 -3
- package/dist/chunk-34A2QV2X.mjs.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +73 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -20
- package/dist/index.mjs.map +1 -1
- package/dist/schema.d.mts +17 -1
- package/dist/schema.d.ts +17 -1
- package/dist/schema.js +8 -2
- package/dist/schema.js.map +1 -1
- package/dist/schema.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-WIPZLAF7.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ export const catalog = defineCatalog(schema, {
|
|
|
26
26
|
title: z.string(),
|
|
27
27
|
description: z.string().nullable(),
|
|
28
28
|
}),
|
|
29
|
+
slots: ["default", "header", "footer"],
|
|
29
30
|
description: "A card container",
|
|
30
31
|
},
|
|
31
32
|
Button: {
|
|
@@ -53,7 +54,7 @@ export const catalog = defineCatalog(schema, {
|
|
|
53
54
|
|
|
54
55
|
### 2. Define Component Implementations
|
|
55
56
|
|
|
56
|
-
Components are written using Vue's `h()` render function. `
|
|
57
|
+
Components are written using Vue's `h()` render function. The `slots` context uses Vue's native slot functions, so render a region with `slots.header?.()`. For convenience and React parity, `children` contains the already-rendered result of `slots.default?.()`.
|
|
57
58
|
|
|
58
59
|
`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it entirely.
|
|
59
60
|
|
|
@@ -64,11 +65,12 @@ import { catalog } from "./catalog";
|
|
|
64
65
|
|
|
65
66
|
export const { registry } = defineRegistry(catalog, {
|
|
66
67
|
components: {
|
|
67
|
-
Card: ({ props,
|
|
68
|
+
Card: ({ props, slots }) =>
|
|
68
69
|
h("div", { class: "card" }, [
|
|
69
|
-
h("h3", null, props.title),
|
|
70
|
+
h("header", null, slots.header?.() ?? h("h3", null, props.title)),
|
|
70
71
|
props.description ? h("p", null, props.description) : null,
|
|
71
|
-
|
|
72
|
+
slots.default?.(),
|
|
73
|
+
h("footer", null, slots.footer?.()),
|
|
72
74
|
]),
|
|
73
75
|
Button: ({ props, emit }) =>
|
|
74
76
|
h("button", { onClick: () => emit("press") }, props.label),
|
|
@@ -131,6 +133,7 @@ interface UIElement {
|
|
|
131
133
|
type: string; // Component name from catalog
|
|
132
134
|
props: Record<string, unknown>; // Component props
|
|
133
135
|
children?: string[]; // Keys of child elements
|
|
136
|
+
slots?: Record<string, string[]>; // Named slots mapped to child keys
|
|
134
137
|
visible?: VisibilityCondition; // Visibility condition
|
|
135
138
|
}
|
|
136
139
|
```
|
|
@@ -163,6 +166,35 @@ Example spec:
|
|
|
163
166
|
}
|
|
164
167
|
```
|
|
165
168
|
|
|
169
|
+
### Named Slots
|
|
170
|
+
|
|
171
|
+
Use `children` for the default slot and the element's top-level `slots` object for other slot names declared by the catalog:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
Layout: ({ children, slots }) =>
|
|
175
|
+
h("div", null, [
|
|
176
|
+
h("header", null, slots.header?.()),
|
|
177
|
+
h("main", null, children),
|
|
178
|
+
h("footer", null, slots.footer?.()),
|
|
179
|
+
]),
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The corresponding spec maps element keys to each region:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"type": "Layout",
|
|
187
|
+
"props": {},
|
|
188
|
+
"children": ["main-content"],
|
|
189
|
+
"slots": {
|
|
190
|
+
"header": ["page-heading"],
|
|
191
|
+
"footer": ["page-actions"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Registry components receive Vue-native slot functions and render them with `slots.header?.()`, `slots.footer?.()`, and so on. `slots.default?.()` renders the spec's `children`; `children` is an alias for that rendered result. In the JSON spec, keep default content in `children` rather than adding a `default` entry to `slots`.
|
|
197
|
+
|
|
166
198
|
## Providers
|
|
167
199
|
|
|
168
200
|
### StateProvider
|
|
@@ -366,11 +398,12 @@ The `setState`, `pushState`, `removeState`, and `validateForm` actions are built
|
|
|
366
398
|
When using `defineRegistry`, components receive these props via their render function:
|
|
367
399
|
|
|
368
400
|
```typescript
|
|
369
|
-
import type { VNode } from "vue";
|
|
401
|
+
import type { Slots, VNode } from "vue";
|
|
370
402
|
|
|
371
403
|
interface ComponentContext<P> {
|
|
372
404
|
props: P; // Typed props from the catalog (expressions resolved)
|
|
373
405
|
children?: VNode | VNode[]; // Rendered children (for container components)
|
|
406
|
+
slots: Slots; // Vue-native slot functions
|
|
374
407
|
emit: (event: string) => void; // Emit a named event (always defined)
|
|
375
408
|
on: (event: string) => EventHandle; // Get event handle with metadata
|
|
376
409
|
loading?: boolean; // Whether the parent is loading
|
|
@@ -488,6 +521,8 @@ const spec = {
|
|
|
488
521
|
|
|
489
522
|
## Differences from `@json-render/react`
|
|
490
523
|
|
|
524
|
+
Nested lists can set `repeat.statePath` to `{ "$item": "field" }` to iterate an array on the enclosing repeat item.
|
|
525
|
+
|
|
491
526
|
| API | React | Vue | Note |
|
|
492
527
|
|-----|-------|-----|------|
|
|
493
528
|
| `useStateStore().state` | `StateModel` | `ShallowRef<StateModel>` | Vue reactivity; use `state.value` |
|
|
@@ -15,8 +15,12 @@ var schema = defineSchema(
|
|
|
15
15
|
props: s.propsOf("catalog.components"),
|
|
16
16
|
/** Child element keys (flat reference) */
|
|
17
17
|
children: s.array(s.string()),
|
|
18
|
+
/** Named slots mapped to child element keys */
|
|
19
|
+
slots: { ...s.record(s.array(s.string())), ...s.optional() },
|
|
18
20
|
/** Visibility condition */
|
|
19
|
-
visible: s.any()
|
|
21
|
+
visible: { ...s.any(), ...s.optional() },
|
|
22
|
+
/** Repeat children from a state array */
|
|
23
|
+
repeat: { ...s.any(), ...s.optional() }
|
|
20
24
|
})
|
|
21
25
|
)
|
|
22
26
|
}),
|
|
@@ -65,12 +69,14 @@ var schema = defineSchema(
|
|
|
65
69
|
// Element integrity
|
|
66
70
|
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
|
67
71
|
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
|
72
|
+
'REQUIRED FIELDS: Every element MUST include a "children" array. Leaf elements (text, badges, inputs, images) use an empty array: "children": []. Omitting "children" fails validation.',
|
|
73
|
+
'NAMED SLOTS: Use "children" for the default slot. For other slots declared by the component, use a top-level "slots" object that maps each slot name to child element keys, for example {"slots":{"header":["heading"],"footer":["actions"]}}. Never use "slots.default". Every referenced key must exist.',
|
|
68
74
|
// Field placement
|
|
69
75
|
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.',
|
|
70
76
|
'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
|
|
71
77
|
// State and data
|
|
72
78
|
"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).",
|
|
73
|
-
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
|
|
79
|
+
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. For a nested list stored on the enclosing item, use "repeat": { "statePath": { "$item": "comments" }, "key": "id" }. The $item statePath form is valid only inside another repeat. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
|
|
74
80
|
// Design quality
|
|
75
81
|
"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.",
|
|
76
82
|
"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.",
|
|
@@ -82,4 +88,4 @@ var schema = defineSchema(
|
|
|
82
88
|
export {
|
|
83
89
|
schema
|
|
84
90
|
};
|
|
85
|
-
//# sourceMappingURL=chunk-
|
|
91
|
+
//# sourceMappingURL=chunk-34A2QV2X.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/vue\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n (s) => ({\n // What the AI-generated SPEC looks like\n spec: s.object({\n /** Root element key */\n root: s.string(),\n /** Flat map of elements by key */\n elements: s.record(\n s.object({\n /** Component type from catalog */\n type: s.ref(\"catalog.components\"),\n /** Component props */\n props: s.propsOf(\"catalog.components\"),\n /** Child element keys (flat reference) */\n children: s.array(s.string()),\n /** Named slots mapped to child element keys */\n slots: { ...s.record(s.array(s.string())), ...s.optional() },\n /** Visibility condition */\n visible: { ...s.any(), ...s.optional() },\n /** Repeat children from a state array */\n repeat: { ...s.any(), ...s.optional() },\n }),\n ),\n }),\n\n // What the CATALOG must provide\n catalog: s.object({\n /** Component definitions */\n components: s.map({\n /** Zod schema for component props */\n props: s.zod(),\n /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n slots: s.array(s.string()),\n /** Description for AI generation hints */\n description: s.string(),\n /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n example: s.any(),\n }),\n /** Action definitions (optional) */\n actions: s.map({\n /** Zod schema for action params */\n params: s.zod(),\n /** Description for AI generation hints */\n description: s.string(),\n }),\n }),\n }),\n {\n builtInActions: [\n {\n name: \"setState\",\n description:\n \"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }\",\n },\n {\n name: \"pushState\",\n description:\n 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {\"$state\":\"/path\"} refs and \"$id\" for auto IDs.',\n },\n {\n name: \"removeState\",\n description:\n \"Remove an item from an array in state by index. Params: { statePath: string, index: number }\",\n },\n {\n name: \"validateForm\",\n description:\n \"Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.\",\n },\n ],\n defaultRules: [\n // Element integrity\n \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n 'REQUIRED FIELDS: Every element MUST include a \"children\" array. Leaf elements (text, badges, inputs, images) use an empty array: \"children\": []. Omitting \"children\" fails validation.',\n 'NAMED SLOTS: Use \"children\" for the default slot. For other slots declared by the component, use a top-level \"slots\" object that maps each slot name to child element keys, for example {\"slots\":{\"header\":[\"heading\"],\"footer\":[\"actions\"]}}. Never use \"slots.default\". Every referenced key must exist.',\n\n // Field placement\n 'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"},\"children\":[...]}.',\n 'CRITICAL: The \"on\" field goes on the ELEMENT object, NOT inside \"props\". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',\n\n // State and data\n \"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).\",\n 'When building repeating content backed by a state array (e.g. posts, products, items), use the \"repeat\" field on a container element. Example: { \"type\": \"<ContainerComponent>\", \"props\": {}, \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" }, \"children\": [\"post-card\"] }. For a nested list stored on the enclosing item, use \"repeat\": { \"statePath\": { \"$item\": \"comments\" }, \"key\": \"id\" }. The $item statePath form is valid only inside another repeat. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n // Design quality\n \"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.\",\n \"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.\",\n \"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.\",\n ],\n },\n);\n\n/**\n * Type for the Vue schema\n */\nexport type VueSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type VueSpec<TCatalog> = typeof schema extends {\n createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n ? S\n : never;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAStB,IAAM,SAAS;AAAA,EACpB,CAAC,OAAO;AAAA;AAAA,IAEN,MAAM,EAAE,OAAO;AAAA;AAAA,MAEb,MAAM,EAAE,OAAO;AAAA;AAAA,MAEf,UAAU,EAAE;AAAA,QACV,EAAE,OAAO;AAAA;AAAA,UAEP,MAAM,EAAE,IAAI,oBAAoB;AAAA;AAAA,UAEhC,OAAO,EAAE,QAAQ,oBAAoB;AAAA;AAAA,UAErC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA;AAAA,UAE5B,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA;AAAA,UAE3D,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA;AAAA,UAEvC,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA,QACxC,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA;AAAA,IAGD,SAAS,EAAE,OAAO;AAAA;AAAA,MAEhB,YAAY,EAAE,IAAI;AAAA;AAAA,QAEhB,OAAO,EAAE,IAAI;AAAA;AAAA,QAEb,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA;AAAA,QAEzB,aAAa,EAAE,OAAO;AAAA;AAAA,QAEtB,SAAS,EAAE,IAAI;AAAA,MACjB,CAAC;AAAA;AAAA,MAED,SAAS,EAAE,IAAI;AAAA;AAAA,QAEb,QAAQ,EAAE,IAAI;AAAA;AAAA,QAEd,aAAa,EAAE,OAAO;AAAA,MACxB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EACA;AAAA,IACE,gBAAgB;AAAA,MACd;AAAA,QACE,MAAM;AAAA,QACN,aACE;AAAA,MACJ;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aACE;AAAA,MACJ;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aACE;AAAA,MACJ;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,cAAc;AAAA;AAAA,MAEZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
|
-
import { PropType, ShallowRef, ComputedRef, VNode, Ref, Component } from 'vue';
|
|
2
|
+
import { PropType, ShallowRef, ComputedRef, VNode, Slots, Ref, Component } from 'vue';
|
|
3
3
|
import { StateStore, StateModel, VisibilityCondition, VisibilityContext, ActionHandler, ResolvedAction, ActionBinding, ActionConfirm, ValidationFunction, ValidationResult, ValidationConfig, Catalog, InferCatalogComponents, InferComponentProps, InferCatalogActions, InferActionParams, Spec, FlatElement, SchemaDefinition, ComputedFunction, DirectiveDefinition, UIElement } from '@json-render/core';
|
|
4
4
|
export { ComputedFunction, Spec, StateModel, StateStore, createStateStore } from '@json-render/core';
|
|
5
5
|
export { VueSchema, VueSpec, schema } from './schema.mjs';
|
|
@@ -384,6 +384,8 @@ interface BaseComponentProps<P = Record<string, unknown>> {
|
|
|
384
384
|
props: P;
|
|
385
385
|
/** Rendered children (from the default slot) */
|
|
386
386
|
children?: VNode | VNode[];
|
|
387
|
+
/** Vue-native slot functions, including the default slot */
|
|
388
|
+
slots: Slots;
|
|
387
389
|
/** Simple event emitter (shorthand). Fires the event and returns void. */
|
|
388
390
|
emit: (event: string) => void;
|
|
389
391
|
/** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */
|
|
@@ -849,7 +851,7 @@ type DefineRegistryOptions<C extends Catalog> = {
|
|
|
849
851
|
* });
|
|
850
852
|
* ```
|
|
851
853
|
*/
|
|
852
|
-
declare function defineRegistry<C extends Catalog>(
|
|
854
|
+
declare function defineRegistry<C extends Catalog>(catalog: C, options: DefineRegistryOptions<C>): DefineRegistryResult;
|
|
853
855
|
/**
|
|
854
856
|
* Props for renderers created with createRenderer
|
|
855
857
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
|
-
import { PropType, ShallowRef, ComputedRef, VNode, Ref, Component } from 'vue';
|
|
2
|
+
import { PropType, ShallowRef, ComputedRef, VNode, Slots, Ref, Component } from 'vue';
|
|
3
3
|
import { StateStore, StateModel, VisibilityCondition, VisibilityContext, ActionHandler, ResolvedAction, ActionBinding, ActionConfirm, ValidationFunction, ValidationResult, ValidationConfig, Catalog, InferCatalogComponents, InferComponentProps, InferCatalogActions, InferActionParams, Spec, FlatElement, SchemaDefinition, ComputedFunction, DirectiveDefinition, UIElement } from '@json-render/core';
|
|
4
4
|
export { ComputedFunction, Spec, StateModel, StateStore, createStateStore } from '@json-render/core';
|
|
5
5
|
export { VueSchema, VueSpec, schema } from './schema.js';
|
|
@@ -384,6 +384,8 @@ interface BaseComponentProps<P = Record<string, unknown>> {
|
|
|
384
384
|
props: P;
|
|
385
385
|
/** Rendered children (from the default slot) */
|
|
386
386
|
children?: VNode | VNode[];
|
|
387
|
+
/** Vue-native slot functions, including the default slot */
|
|
388
|
+
slots: Slots;
|
|
387
389
|
/** Simple event emitter (shorthand). Fires the event and returns void. */
|
|
388
390
|
emit: (event: string) => void;
|
|
389
391
|
/** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */
|
|
@@ -849,7 +851,7 @@ type DefineRegistryOptions<C extends Catalog> = {
|
|
|
849
851
|
* });
|
|
850
852
|
* ```
|
|
851
853
|
*/
|
|
852
|
-
declare function defineRegistry<C extends Catalog>(
|
|
854
|
+
declare function defineRegistry<C extends Catalog>(catalog: C, options: DefineRegistryOptions<C>): DefineRegistryResult;
|
|
853
855
|
/**
|
|
854
856
|
* Props for renderers created with createRenderer
|
|
855
857
|
*/
|
package/dist/index.js
CHANGED
|
@@ -542,9 +542,8 @@ var ActionProvider = (0, import_vue4.defineComponent)({
|
|
|
542
542
|
handler,
|
|
543
543
|
setState: set,
|
|
544
544
|
navigate: props.navigate,
|
|
545
|
-
executeAction: async (
|
|
546
|
-
|
|
547
|
-
await execute(subBinding);
|
|
545
|
+
executeAction: async (binding2) => {
|
|
546
|
+
await execute(binding2);
|
|
548
547
|
}
|
|
549
548
|
});
|
|
550
549
|
} finally {
|
|
@@ -563,9 +562,8 @@ var ActionProvider = (0, import_vue4.defineComponent)({
|
|
|
563
562
|
handler,
|
|
564
563
|
setState: set,
|
|
565
564
|
navigate: props.navigate,
|
|
566
|
-
executeAction: async (
|
|
567
|
-
|
|
568
|
-
await execute(subBinding);
|
|
565
|
+
executeAction: async (binding2) => {
|
|
566
|
+
await execute(binding2);
|
|
569
567
|
}
|
|
570
568
|
});
|
|
571
569
|
} finally {
|
|
@@ -785,8 +783,12 @@ var schema = (0, import_core5.defineSchema)(
|
|
|
785
783
|
props: s.propsOf("catalog.components"),
|
|
786
784
|
/** Child element keys (flat reference) */
|
|
787
785
|
children: s.array(s.string()),
|
|
786
|
+
/** Named slots mapped to child element keys */
|
|
787
|
+
slots: { ...s.record(s.array(s.string())), ...s.optional() },
|
|
788
788
|
/** Visibility condition */
|
|
789
|
-
visible: s.any()
|
|
789
|
+
visible: { ...s.any(), ...s.optional() },
|
|
790
|
+
/** Repeat children from a state array */
|
|
791
|
+
repeat: { ...s.any(), ...s.optional() }
|
|
790
792
|
})
|
|
791
793
|
)
|
|
792
794
|
}),
|
|
@@ -835,12 +837,14 @@ var schema = (0, import_core5.defineSchema)(
|
|
|
835
837
|
// Element integrity
|
|
836
838
|
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
|
837
839
|
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
|
840
|
+
'REQUIRED FIELDS: Every element MUST include a "children" array. Leaf elements (text, badges, inputs, images) use an empty array: "children": []. Omitting "children" fails validation.',
|
|
841
|
+
'NAMED SLOTS: Use "children" for the default slot. For other slots declared by the component, use a top-level "slots" object that maps each slot name to child element keys, for example {"slots":{"header":["heading"],"footer":["actions"]}}. Never use "slots.default". Every referenced key must exist.',
|
|
838
842
|
// Field placement
|
|
839
843
|
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.',
|
|
840
844
|
'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
|
|
841
845
|
// State and data
|
|
842
846
|
"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).",
|
|
843
|
-
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
|
|
847
|
+
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. For a nested list stored on the enclosing item, use "repeat": { "statePath": { "$item": "comments" }, "key": "id" }. The $item statePath form is valid only inside another repeat. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
|
|
844
848
|
// Design quality
|
|
845
849
|
"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.",
|
|
846
850
|
"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.",
|
|
@@ -1332,6 +1336,7 @@ function useChatUI({
|
|
|
1332
1336
|
// src/renderer.ts
|
|
1333
1337
|
var import_vue7 = require("vue");
|
|
1334
1338
|
var import_core7 = require("@json-render/core");
|
|
1339
|
+
var registryMetadata = /* @__PURE__ */ new WeakMap();
|
|
1335
1340
|
var EMPTY_FUNCTIONS = {};
|
|
1336
1341
|
var FUNCTIONS_KEY = /* @__PURE__ */ Symbol("json-render:functions");
|
|
1337
1342
|
var FunctionsProvider = (0, import_vue7.defineComponent)({
|
|
@@ -1556,18 +1561,28 @@ var ElementRenderer = (0, import_vue7.defineComponent)({
|
|
|
1556
1561
|
);
|
|
1557
1562
|
return null;
|
|
1558
1563
|
}
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1564
|
+
const metadata = registryMetadata.get(props.registry)?.[resolvedElement.type];
|
|
1565
|
+
if (resolvedElement.slots && metadata?.slots) {
|
|
1566
|
+
const availableSlots = new Set(metadata.slots);
|
|
1567
|
+
for (const slotName of Object.keys(resolvedElement.slots)) {
|
|
1568
|
+
if (slotName === "default") {
|
|
1569
|
+
console.warn(
|
|
1570
|
+
`[json-render] Component "${resolvedElement.type}" uses slots.default. Use "children" for default slot content.`
|
|
1571
|
+
);
|
|
1572
|
+
} else if (!availableSlots.has(slotName)) {
|
|
1573
|
+
console.warn(
|
|
1574
|
+
`[json-render] Unknown slot "${slotName}" on component "${resolvedElement.type}". Available slots: ${metadata.slots.join(", ")}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
const renderChildKeys = (childKeys, slotName) => childKeys.map((childKey) => {
|
|
1566
1580
|
const childElement = props.spec.elements[childKey];
|
|
1567
1581
|
if (!childElement) {
|
|
1568
1582
|
if (!props.loading) {
|
|
1583
|
+
const location = slotName ? `in slot "${slotName}" of "${resolvedElement.type}"` : `as child of "${resolvedElement.type}"`;
|
|
1569
1584
|
console.warn(
|
|
1570
|
-
`[json-render] Missing element "${childKey}" referenced
|
|
1585
|
+
`[json-render] Missing element "${childKey}" referenced ${location}. This element will not render.`
|
|
1571
1586
|
);
|
|
1572
1587
|
}
|
|
1573
1588
|
return null;
|
|
@@ -1581,7 +1596,20 @@ var ElementRenderer = (0, import_vue7.defineComponent)({
|
|
|
1581
1596
|
loading: props.loading,
|
|
1582
1597
|
fallback: props.fallback
|
|
1583
1598
|
});
|
|
1584
|
-
}).filter((
|
|
1599
|
+
}).filter((node) => node !== null);
|
|
1600
|
+
const childrenVNodes = resolvedElement.repeat ? (0, import_vue7.h)(RepeatChildren, {
|
|
1601
|
+
element: resolvedElement,
|
|
1602
|
+
spec: props.spec,
|
|
1603
|
+
registry: props.registry,
|
|
1604
|
+
loading: props.loading,
|
|
1605
|
+
fallback: props.fallback
|
|
1606
|
+
}) : resolvedElement.children ? renderChildKeys(resolvedElement.children) : void 0;
|
|
1607
|
+
const namedSlots = resolvedElement.slots ? Object.fromEntries(
|
|
1608
|
+
Object.entries(resolvedElement.slots).filter(([slotName]) => slotName !== "default").map(([slotName, childKeys]) => [
|
|
1609
|
+
slotName,
|
|
1610
|
+
() => renderChildKeys(childKeys, slotName)
|
|
1611
|
+
])
|
|
1612
|
+
) : {};
|
|
1585
1613
|
const componentVNode = (0, import_vue7.h)(
|
|
1586
1614
|
Component,
|
|
1587
1615
|
{
|
|
@@ -1591,7 +1619,7 @@ var ElementRenderer = (0, import_vue7.defineComponent)({
|
|
|
1591
1619
|
bindings: elementBindings,
|
|
1592
1620
|
loading: props.loading
|
|
1593
1621
|
},
|
|
1594
|
-
{ default: () => childrenVNodes }
|
|
1622
|
+
{ default: () => childrenVNodes, ...namedSlots }
|
|
1595
1623
|
);
|
|
1596
1624
|
const tagged = devtoolsActive.value && props.elementKey ? (0, import_vue7.h)(
|
|
1597
1625
|
"span",
|
|
@@ -1632,10 +1660,20 @@ var RepeatChildren = (0, import_vue7.defineComponent)({
|
|
|
1632
1660
|
},
|
|
1633
1661
|
setup(props) {
|
|
1634
1662
|
const { state } = useStateStore();
|
|
1663
|
+
const parentScope = useRepeatScope();
|
|
1635
1664
|
return () => {
|
|
1636
1665
|
const repeat = props.element.repeat;
|
|
1637
1666
|
if (!repeat?.statePath) return null;
|
|
1638
|
-
const statePath =
|
|
1667
|
+
const statePath = (0, import_core7.resolveRepeatStatePath)(
|
|
1668
|
+
repeat.statePath,
|
|
1669
|
+
parentScope?.basePath
|
|
1670
|
+
);
|
|
1671
|
+
if (statePath === void 0) {
|
|
1672
|
+
console.warn(
|
|
1673
|
+
"[json-render/vue] $item in repeat.statePath used outside of a repeat scope"
|
|
1674
|
+
);
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1639
1677
|
const raw = (0, import_core7.getByPath)(state.value, statePath);
|
|
1640
1678
|
const items = Array.isArray(raw) ? raw : [];
|
|
1641
1679
|
return items.map((itemValue, index) => {
|
|
@@ -1644,7 +1682,12 @@ var RepeatChildren = (0, import_vue7.defineComponent)({
|
|
|
1644
1682
|
) : String(index);
|
|
1645
1683
|
return (0, import_vue7.h)(
|
|
1646
1684
|
RepeatScopeProvider,
|
|
1647
|
-
{
|
|
1685
|
+
{
|
|
1686
|
+
key,
|
|
1687
|
+
item: itemValue,
|
|
1688
|
+
index,
|
|
1689
|
+
basePath: (0, import_core7.resolveRepeatItemStatePath)(statePath, index)
|
|
1690
|
+
},
|
|
1648
1691
|
{
|
|
1649
1692
|
default: () => props.element.children?.map((childKey) => {
|
|
1650
1693
|
const childElement = props.spec.elements[childKey];
|
|
@@ -1805,7 +1848,7 @@ var JSONUIProvider = (0, import_vue7.defineComponent)({
|
|
|
1805
1848
|
);
|
|
1806
1849
|
}
|
|
1807
1850
|
});
|
|
1808
|
-
function defineRegistry(
|
|
1851
|
+
function defineRegistry(catalog, options) {
|
|
1809
1852
|
const registry = {};
|
|
1810
1853
|
if (options.components) {
|
|
1811
1854
|
for (const [name, componentFn] of Object.entries(options.components)) {
|
|
@@ -1837,6 +1880,7 @@ function defineRegistry(_catalog, options) {
|
|
|
1837
1880
|
return () => componentFn({
|
|
1838
1881
|
props: registryProps.element.props,
|
|
1839
1882
|
children: slots.default?.(),
|
|
1883
|
+
slots,
|
|
1840
1884
|
emit: registryProps.emit,
|
|
1841
1885
|
on: registryProps.on,
|
|
1842
1886
|
bindings: registryProps.bindings,
|
|
@@ -1846,6 +1890,10 @@ function defineRegistry(_catalog, options) {
|
|
|
1846
1890
|
});
|
|
1847
1891
|
}
|
|
1848
1892
|
}
|
|
1893
|
+
const catalogComponents = catalog.data?.components;
|
|
1894
|
+
if (catalogComponents) {
|
|
1895
|
+
registryMetadata.set(registry, catalogComponents);
|
|
1896
|
+
}
|
|
1849
1897
|
const actionMap = options.actions ? Object.entries(options.actions) : [];
|
|
1850
1898
|
const handlers = (getSetState, getState) => {
|
|
1851
1899
|
const result = {};
|
|
@@ -1872,6 +1920,10 @@ function defineRegistry(_catalog, options) {
|
|
|
1872
1920
|
}
|
|
1873
1921
|
function createRenderer(catalog, components) {
|
|
1874
1922
|
const registry = components;
|
|
1923
|
+
const catalogComponents = catalog.data.components;
|
|
1924
|
+
if (catalogComponents) {
|
|
1925
|
+
registryMetadata.set(registry, catalogComponents);
|
|
1926
|
+
}
|
|
1875
1927
|
return (0, import_vue7.defineComponent)({
|
|
1876
1928
|
name: "CatalogRenderer",
|
|
1877
1929
|
props: {
|