@json-render/react-native 0.18.0 → 0.20.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
@@ -236,6 +236,8 @@ When `store` is provided, `initialState` and `onStateChange` are ignored. The st
236
236
 
237
237
  ## Hooks
238
238
 
239
+ Nested lists can set `repeat.statePath` to `{ "$item": "field" }` to iterate an array on the enclosing repeat item.
240
+
239
241
  | Hook | Purpose |
240
242
  |------|---------|
241
243
  | `useStateStore()` | Access state context (`state`, `get`, `set`, `update`) |
@@ -16,7 +16,9 @@ var schema = defineSchema(
16
16
  /** Child element keys (flat reference) */
17
17
  children: s.array(s.string()),
18
18
  /** Visibility condition */
19
- visible: s.any()
19
+ visible: { ...s.any(), ...s.optional() },
20
+ /** Repeat children from a state array */
21
+ repeat: { ...s.any(), ...s.optional() }
20
22
  })
21
23
  )
22
24
  }),
@@ -50,7 +52,8 @@ var schema = defineSchema(
50
52
  // Element integrity
51
53
  "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.",
52
54
  "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.",
53
- 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "statePath": "/todos", "key": "id" }, "children": ["todo-item"] }. 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.',
55
+ 'REQUIRED FIELDS: Every element MUST include a "children" array. Leaf elements (text, badges, inputs, images) use an empty array: "children": []. Omitting "children" fails validation.',
56
+ 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "statePath": "/todos", "key": "id" }, "children": ["todo-item"] }. 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. 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.',
54
57
  // Visible field placement
55
58
  'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/activeTab","eq":"home"},"children":[...]}. WRONG: {"type":"<ComponentName>","props":{},"visible":{...},"children":[...]} with visible inside props.',
56
59
  // Tab navigation pattern
@@ -80,4 +83,4 @@ export {
80
83
  schema,
81
84
  elementTreeSchema
82
85
  };
83
- //# sourceMappingURL=chunk-T27PQA6P.mjs.map
86
+ //# sourceMappingURL=chunk-TAJ5ASZ5.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/react-native\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 /** 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 defaultRules: [\n // Layout patterns\n \"FIXED BOTTOM BAR PATTERN: When building a screen with a fixed header and/or fixed bottom tab bar, the outermost vertical layout component must have flex:1 so it fills the screen. The scrollable content area must also have flex:1. Structure: screen wrapper > vertical layout(flex:1, gap:0) > [header, content wrapper(flex:1) > [scroll container(...)], bottom-tabs]. Both the outer layout AND the content wrapper need flex:1. ONLY use components from the AVAILABLE COMPONENTS list.\",\n \"NEVER place a bottom tab bar or fixed footer inside a scroll container. It must be a sibling AFTER the flex:1 container that holds the scroll content.\",\n\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 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the \"repeat\" field on a container element from the AVAILABLE COMPONENTS list. Example: { \"type\": \"<ContainerComponent>\", \"props\": { \"gap\": 8 }, \"repeat\": { \"statePath\": \"/todos\", \"key\": \"id\" }, \"children\": [\"todo-item\"] }. 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. 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 // Visible field placement\n 'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/activeTab\",\"eq\":\"home\"},\"children\":[...]}. WRONG: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{...},\"children\":[...]} with visible inside props.',\n\n // Tab navigation pattern\n \"TAB NAVIGATION PATTERN: When building a UI with multiple tabs, use a pressable/tappable component + setState action + visible conditions to make tabs functional. ONLY use components from the AVAILABLE COMPONENTS list.\",\n 'Each tab button should be a pressable component wrapping its icon/label children, with action \"setState\" and actionParams { \"statePath\": \"/activeTab\", \"value\": \"tabName\" }.',\n 'Each tab\\'s content section should have a visible condition: { \"$state\": \"/activeTab\", \"eq\": \"tabName\" }.',\n \"The first tab's content should NOT have a visible condition (so it shows by default when no tab is selected yet). All other tabs MUST have a visible condition.\",\n\n // Tab active state highlighting (using dynamic props)\n \"TAB ACTIVE STYLING: Use $cond dynamic props on icon elements inside each tab button so a single icon changes appearance based on the active tab.\",\n ' - For the icon name: { \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }, \"$then\": \"home\", \"$else\": \"home-outline\" }',\n ' - For the icon color: { \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }, \"$then\": \"#007AFF\", \"$else\": \"#8E8E93\" }',\n \" - For labels, use $cond on the color prop similarly.\",\n ' - For the FIRST/DEFAULT tab, use { \"$cond\": [{ \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }], \"$then\": \"#007AFF\", \"$else\": \"#8E8E93\" } so it appears active before any tab is tapped. When no tab is selected yet, include a default tab with no visible condition.',\n\n // Push/pop screen navigation (all screens in one spec)\n 'SCREEN NAVIGATION: Use a pressable component with action \"push\" and actionParams { \"screen\": \"screenName\" } to navigate to a new screen. Use action \"pop\" to go back. All screens must be defined in the SAME spec. ONLY use components from the AVAILABLE COMPONENTS list.',\n 'Each screen section uses a visible condition on /currentScreen: { \"$state\": \"/currentScreen\", \"eq\": \"screenName\" }. The default/home screen should show by default (no visible condition) and all other screens should have the appropriate visible condition.',\n \"push automatically maintains a /navStack in the state model so pop always returns to the previous screen.\",\n 'Include a back button on pushed screens using action \"pop\". Example: pressable(action:\"pop\") > row layout > back icon + back label. ONLY use components from the AVAILABLE COMPONENTS list.',\n \"Use push/pop for drill-down flows: tapping a list item to see details, opening a profile, etc. Use setState + visible conditions for tab switching within a screen.\",\n 'Example: A list screen with items that push to detail: a pressable component with action:\"push\" and actionParams:{screen:\"detail\"} wrapping each list item. The detail screen section has visible:{\"$state\":\"/currentScreen\",\"eq\":\"detail\"} and contains a back button with action:\"pop\". ONLY use components from the AVAILABLE COMPONENTS list.',\n ],\n },\n);\n\n/**\n * Type for the React Native schema\n */\nexport type ReactNativeSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type ReactNativeSpec<TCatalog> = typeof schema extends {\n createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n ? S\n : never;\n\n// Backward compatibility aliases\n/** @deprecated Use `schema` instead */\nexport const elementTreeSchema = schema;\n/** @deprecated Use `ReactNativeSchema` instead */\nexport type ElementTreeSchema = ReactNativeSchema;\n/** @deprecated Use `ReactNativeSpec` instead */\nexport type ElementTreeSpec<T> = ReactNativeSpec<T>;\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,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,cAAc;AAAA;AAAA,MAEZ;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAGA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAkBO,IAAM,oBAAoB;","names":[]}
package/dist/index.js CHANGED
@@ -364,9 +364,8 @@ function ActionProvider({
364
364
  handler,
365
365
  setState: set,
366
366
  navigate,
367
- executeAction: async (name) => {
368
- const subBinding = { action: name };
369
- await execute(subBinding);
367
+ executeAction: async (binding2) => {
368
+ await execute(binding2);
370
369
  }
371
370
  });
372
371
  } finally {
@@ -385,9 +384,8 @@ function ActionProvider({
385
384
  handler,
386
385
  setState: set,
387
386
  navigate,
388
- executeAction: async (name) => {
389
- const subBinding = { action: name };
390
- await execute(subBinding);
387
+ executeAction: async (binding2) => {
388
+ await execute(binding2);
391
389
  }
392
390
  });
393
391
  } finally {
@@ -739,7 +737,9 @@ var schema = (0, import_core5.defineSchema)(
739
737
  /** Child element keys (flat reference) */
740
738
  children: s.array(s.string()),
741
739
  /** Visibility condition */
742
- visible: s.any()
740
+ visible: { ...s.any(), ...s.optional() },
741
+ /** Repeat children from a state array */
742
+ repeat: { ...s.any(), ...s.optional() }
743
743
  })
744
744
  )
745
745
  }),
@@ -773,7 +773,8 @@ var schema = (0, import_core5.defineSchema)(
773
773
  // Element integrity
774
774
  "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.",
775
775
  "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.",
776
- 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "statePath": "/todos", "key": "id" }, "children": ["todo-item"] }. 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.',
776
+ 'REQUIRED FIELDS: Every element MUST include a "children" array. Leaf elements (text, badges, inputs, images) use an empty array: "children": []. Omitting "children" fails validation.',
777
+ 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "statePath": "/todos", "key": "id" }, "children": ["todo-item"] }. 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. 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.',
777
778
  // Visible field placement
778
779
  'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/activeTab","eq":"home"},"children":[...]}. WRONG: {"type":"<ComponentName>","props":{},"visible":{...},"children":[...]} with visible inside props.',
779
780
  // Tab navigation pattern
@@ -1099,8 +1100,10 @@ The current spec state is provided. Continue generating from where you left off.
1099
1100
  if (!enableValidation || !currentSpec.root) {
1100
1101
  break;
1101
1102
  }
1102
- const { spec: fixedSpec, fixes } = (0, import_core6.autoFixSpec)(currentSpec);
1103
- if (fixes.length > 0) {
1103
+ const { spec: fixedSpec, fixDetails } = (0, import_core6.autoFixSpec)(currentSpec, {
1104
+ lossy: retriesUsed >= maxRetries
1105
+ });
1106
+ if (fixDetails.length > 0) {
1104
1107
  currentSpec = fixedSpec;
1105
1108
  setSpec({ ...currentSpec });
1106
1109
  }
@@ -1121,6 +1124,19 @@ The current spec state is provided. Continue generating from where you left off.
1121
1124
 
1122
1125
  ` + issueText;
1123
1126
  }
1127
+ if (enableValidation && retriesUsed >= maxRetries && currentSpec.root) {
1128
+ const finalValidation = (0, import_core6.validateSpec)(currentSpec);
1129
+ if (!finalValidation.valid) {
1130
+ const issueText = (0, import_core6.formatSpecIssues)(finalValidation.issues);
1131
+ const validationError = new Error(
1132
+ `Spec validation failed after ${maxRetries} retries:
1133
+ ${issueText}`
1134
+ );
1135
+ setError(validationError);
1136
+ onError?.(validationError);
1137
+ return;
1138
+ }
1139
+ }
1124
1140
  onComplete?.(currentSpec);
1125
1141
  } catch (err) {
1126
1142
  if (err.name === "AbortError") {
@@ -2131,8 +2147,18 @@ function RepeatChildren({
2131
2147
  fallback
2132
2148
  }) {
2133
2149
  const { state } = useStateStore();
2150
+ const parentScope = useRepeatScope();
2134
2151
  const repeat = element.repeat;
2135
- const statePath = repeat.statePath;
2152
+ const statePath = (0, import_core7.resolveRepeatStatePath)(
2153
+ repeat.statePath,
2154
+ parentScope?.basePath
2155
+ );
2156
+ if (statePath === void 0) {
2157
+ console.warn(
2158
+ "[json-render/react-native] $item in repeat.statePath used outside of a repeat scope"
2159
+ );
2160
+ return null;
2161
+ }
2136
2162
  const items = (0, import_core7.getByPath)(state, statePath) ?? [];
2137
2163
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: items.map((itemValue, index) => {
2138
2164
  const key = repeat.key && typeof itemValue === "object" && itemValue !== null ? String(
@@ -2143,7 +2169,7 @@ function RepeatChildren({
2143
2169
  {
2144
2170
  item: itemValue,
2145
2171
  index,
2146
- basePath: `${statePath}/${index}`,
2172
+ basePath: (0, import_core7.resolveRepeatItemStatePath)(statePath, index),
2147
2173
  children: element.children?.map((childKey) => {
2148
2174
  const childElement = spec.elements[childKey];
2149
2175
  if (!childElement) {