@json-render/react-native 0.5.2 → 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/dist/index.js CHANGED
@@ -39,18 +39,17 @@ __export(index_exports, {
39
39
  ValidationProvider: () => ValidationProvider,
40
40
  VisibilityProvider: () => VisibilityProvider,
41
41
  createRenderer: () => createRenderer,
42
- createRendererFromCatalog: () => createRendererFromCatalog,
43
42
  createStandardActionHandlers: () => createStandardActionHandlers,
44
43
  defineRegistry: () => defineRegistry,
45
44
  elementTreeSchema: () => elementTreeSchema,
46
45
  flatToTree: () => flatToTree,
47
- rewriteRepeatTokens: () => rewriteRepeatTokens,
48
46
  schema: () => schema,
49
47
  standardActionDefinitions: () => standardActionDefinitions,
50
48
  standardComponentDefinitions: () => standardComponentDefinitions,
51
49
  standardComponents: () => standardComponents,
52
50
  useAction: () => useAction,
53
51
  useActions: () => useActions,
52
+ useBoundProp: () => useBoundProp,
54
53
  useFieldValidation: () => useFieldValidation,
55
54
  useIsVisible: () => useIsVisible,
56
55
  useRepeatScope: () => useRepeatScope,
@@ -70,11 +69,12 @@ var import_jsx_runtime = require("react/jsx-runtime");
70
69
  var StateContext = (0, import_react.createContext)(null);
71
70
  function StateProvider({
72
71
  initialState = {},
73
- authState,
74
72
  onStateChange,
75
73
  children
76
74
  }) {
77
75
  const [state, setStateInternal] = (0, import_react.useState)(initialState);
76
+ const stateRef = (0, import_react.useRef)(state);
77
+ stateRef.current = state;
78
78
  const initialStateJsonRef = (0, import_react.useRef)(JSON.stringify(initialState));
79
79
  (0, import_react.useEffect)(() => {
80
80
  const newJson = JSON.stringify(initialState);
@@ -85,7 +85,10 @@ function StateProvider({
85
85
  }
86
86
  }
87
87
  }, [initialState]);
88
- const get = (0, import_react.useCallback)((path) => (0, import_core.getByPath)(state, path), [state]);
88
+ const get = (0, import_react.useCallback)(
89
+ (path) => (0, import_core.getByPath)(stateRef.current, path),
90
+ []
91
+ );
89
92
  const set = (0, import_react.useCallback)(
90
93
  (path, value2) => {
91
94
  setStateInternal((prev) => {
@@ -99,26 +102,28 @@ function StateProvider({
99
102
  );
100
103
  const update = (0, import_react.useCallback)(
101
104
  (updates) => {
105
+ const entries = Object.entries(updates);
102
106
  setStateInternal((prev) => {
103
107
  const next = { ...prev };
104
- for (const [path, value2] of Object.entries(updates)) {
108
+ for (const [path, value2] of entries) {
105
109
  (0, import_core.setByPath)(next, path, value2);
106
- onStateChange?.(path, value2);
107
110
  }
108
111
  return next;
109
112
  });
113
+ for (const [path, value2] of entries) {
114
+ onStateChange?.(path, value2);
115
+ }
110
116
  },
111
117
  [onStateChange]
112
118
  );
113
119
  const value = (0, import_react.useMemo)(
114
120
  () => ({
115
121
  state,
116
- authState,
117
122
  get,
118
123
  set,
119
124
  update
120
125
  }),
121
- [state, authState, get, set, update]
126
+ [state, get, set, update]
122
127
  );
123
128
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StateContext.Provider, { value, children });
124
129
  }
@@ -149,13 +154,12 @@ var import_core2 = require("@json-render/core");
149
154
  var import_jsx_runtime2 = require("react/jsx-runtime");
150
155
  var VisibilityContext = (0, import_react2.createContext)(null);
151
156
  function VisibilityProvider({ children }) {
152
- const { state, authState } = useStateStore();
157
+ const { state } = useStateStore();
153
158
  const ctx = (0, import_react2.useMemo)(
154
159
  () => ({
155
- stateModel: state,
156
- authState
160
+ stateModel: state
157
161
  }),
158
- [state, authState]
162
+ [state]
159
163
  );
160
164
  const isVisible = (0, import_react2.useMemo)(
161
165
  () => (condition) => (0, import_core2.evaluateVisibility)(condition, ctx),
@@ -197,8 +201,8 @@ function deepResolveValue(value, get) {
197
201
  if (typeof value === "object" && !Array.isArray(value)) {
198
202
  const obj = value;
199
203
  const keys = Object.keys(obj);
200
- if (keys.length === 1 && typeof obj.path === "string") {
201
- return get(obj.path);
204
+ if (keys.length === 1 && typeof obj.$state === "string") {
205
+ return get(obj.$state);
202
206
  }
203
207
  if (keys.length === 1 && "$id" in obj) {
204
208
  return generateUniqueId();
@@ -236,34 +240,34 @@ function ActionProvider({
236
240
  async (binding) => {
237
241
  const resolved = (0, import_core3.resolveAction)(binding, state);
238
242
  if (resolved.action === "setState" && resolved.params) {
239
- const path = resolved.params.path;
243
+ const statePath = resolved.params.statePath;
240
244
  const value2 = resolved.params.value;
241
- if (path) {
242
- set(path, value2);
245
+ if (statePath) {
246
+ set(statePath, value2);
243
247
  }
244
248
  return;
245
249
  }
246
250
  if (resolved.action === "pushState" && resolved.params) {
247
- const path = resolved.params.path;
251
+ const statePath = resolved.params.statePath;
248
252
  const rawValue = resolved.params.value;
249
- if (path) {
253
+ if (statePath) {
250
254
  const resolvedValue = deepResolveValue(rawValue, get);
251
- const arr = get(path) ?? [];
252
- set(path, [...arr, resolvedValue]);
253
- const clearPath = resolved.params.clearPath;
254
- if (clearPath) {
255
- set(clearPath, "");
255
+ const arr = get(statePath) ?? [];
256
+ set(statePath, [...arr, resolvedValue]);
257
+ const clearStatePath = resolved.params.clearStatePath;
258
+ if (clearStatePath) {
259
+ set(clearStatePath, "");
256
260
  }
257
261
  }
258
262
  return;
259
263
  }
260
264
  if (resolved.action === "removeState" && resolved.params) {
261
- const path = resolved.params.path;
265
+ const statePath = resolved.params.statePath;
262
266
  const index = resolved.params.index;
263
- if (path !== void 0 && index !== void 0) {
264
- const arr = get(path) ?? [];
267
+ if (statePath !== void 0 && index !== void 0) {
268
+ const arr = get(statePath) ?? [];
265
269
  set(
266
- path,
270
+ statePath,
267
271
  arr.filter((_, i) => i !== index)
268
272
  );
269
273
  }
@@ -492,27 +496,75 @@ var import_react4 = __toESM(require("react"));
492
496
  var import_core4 = require("@json-render/core");
493
497
  var import_jsx_runtime4 = require("react/jsx-runtime");
494
498
  var ValidationContext = (0, import_react4.createContext)(null);
499
+ function dynamicArgsEqual(a, b) {
500
+ if (a === b) return true;
501
+ if (!a || !b) return false;
502
+ const keysA = Object.keys(a);
503
+ const keysB = Object.keys(b);
504
+ if (keysA.length !== keysB.length) return false;
505
+ for (const key of keysA) {
506
+ const va = a[key];
507
+ const vb = b[key];
508
+ if (va === vb) continue;
509
+ if (typeof va === "object" && va !== null && typeof vb === "object" && vb !== null) {
510
+ const sa = va.$state;
511
+ const sb = vb.$state;
512
+ if (typeof sa === "string" && sa === sb) continue;
513
+ }
514
+ return false;
515
+ }
516
+ return true;
517
+ }
518
+ function validationConfigEqual(a, b) {
519
+ if (a === b) return true;
520
+ if (a.validateOn !== b.validateOn) return false;
521
+ const ac = a.checks ?? [];
522
+ const bc = b.checks ?? [];
523
+ if (ac.length !== bc.length) return false;
524
+ for (let i = 0; i < ac.length; i++) {
525
+ const ca = ac[i];
526
+ const cb = bc[i];
527
+ if (ca.type !== cb.type) return false;
528
+ if (ca.message !== cb.message) return false;
529
+ if (!dynamicArgsEqual(ca.args, cb.args)) return false;
530
+ }
531
+ return true;
532
+ }
495
533
  function ValidationProvider({
496
534
  customFunctions = {},
497
535
  children
498
536
  }) {
499
- const { state, authState } = useStateStore();
537
+ const { state } = useStateStore();
500
538
  const [fieldStates, setFieldStates] = (0, import_react4.useState)({});
501
539
  const [fieldConfigs, setFieldConfigs] = (0, import_react4.useState)({});
502
540
  const registerField = (0, import_react4.useCallback)(
503
541
  (path, config) => {
504
- setFieldConfigs((prev) => ({ ...prev, [path]: config }));
542
+ setFieldConfigs((prev) => {
543
+ const existing = prev[path];
544
+ if (existing && validationConfigEqual(existing, config)) {
545
+ return prev;
546
+ }
547
+ return { ...prev, [path]: config };
548
+ });
505
549
  },
506
550
  []
507
551
  );
508
552
  const validate = (0, import_react4.useCallback)(
509
553
  (path, config) => {
510
- const value2 = state[path.split("/").filter(Boolean).join(".")];
554
+ const segments = path.split("/").filter(Boolean);
555
+ let value2 = state;
556
+ for (const seg of segments) {
557
+ if (value2 != null && typeof value2 === "object") {
558
+ value2 = value2[seg];
559
+ } else {
560
+ value2 = void 0;
561
+ break;
562
+ }
563
+ }
511
564
  const result = (0, import_core4.runValidation)(config, {
512
565
  value: value2,
513
566
  stateModel: state,
514
- customFunctions,
515
- authState
567
+ customFunctions
516
568
  });
517
569
  setFieldStates((prev) => ({
518
570
  ...prev,
@@ -524,7 +576,7 @@ function ValidationProvider({
524
576
  }));
525
577
  return result;
526
578
  },
527
- [state, customFunctions, authState]
579
+ [state, customFunctions]
528
580
  );
529
581
  const touch = (0, import_react4.useCallback)((path) => {
530
582
  setFieldStates((prev) => ({
@@ -621,44 +673,16 @@ var import_react5 = require("react");
621
673
  var import_jsx_runtime5 = require("react/jsx-runtime");
622
674
  var RepeatScopeContext = (0, import_react5.createContext)(null);
623
675
  function RepeatScopeProvider({
624
- basePath,
676
+ item,
625
677
  index,
678
+ basePath,
626
679
  children
627
680
  }) {
628
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RepeatScopeContext.Provider, { value: { basePath, index }, children });
681
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RepeatScopeContext.Provider, { value: { item, index, basePath }, children });
629
682
  }
630
683
  function useRepeatScope() {
631
684
  return (0, import_react5.useContext)(RepeatScopeContext);
632
685
  }
633
- function rewriteRepeatTokens(value, basePath, index) {
634
- if (typeof value === "string") {
635
- if (value === "$index") return index;
636
- if (value === "$item") return basePath;
637
- if (value.startsWith("$item/"))
638
- return basePath + value.slice("$item".length);
639
- return value;
640
- }
641
- if (Array.isArray(value)) {
642
- let changed = false;
643
- const result = value.map((item) => {
644
- const rewritten = rewriteRepeatTokens(item, basePath, index);
645
- if (rewritten !== item) changed = true;
646
- return rewritten;
647
- });
648
- return changed ? result : value;
649
- }
650
- if (typeof value === "object" && value !== null) {
651
- let changed = false;
652
- const result = {};
653
- for (const [key, val] of Object.entries(value)) {
654
- const rewritten = rewriteRepeatTokens(val, basePath, index);
655
- if (rewritten !== val) changed = true;
656
- result[key] = rewritten;
657
- }
658
- return changed ? result : value;
659
- }
660
- return value;
661
- }
662
686
 
663
687
  // src/schema.ts
664
688
  var import_core5 = require("@json-render/core");
@@ -712,177 +736,555 @@ var schema = (0, import_core5.defineSchema)(
712
736
  // Element integrity
713
737
  "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.",
714
738
  "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.",
715
- '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": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }. Inside repeated children, use "$item/field" for per-item state paths and "$index" for the current array index. Do NOT hardcode individual elements for each array item.',
739
+ '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.',
716
740
  // Visible field placement
717
- 'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"eq":[{"path":"/activeTab"},"home"]},"children":[...]}. WRONG: {"type":"<ComponentName>","props":{},"visible":{...},"children":[...]} with visible inside props.',
741
+ '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.',
718
742
  // Tab navigation pattern
719
743
  "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.",
720
- 'Each tab button should be a pressable component wrapping its icon/label children, with action "setState" and actionParams { "path": "/activeTab", "value": "tabName" }.',
721
- `Each tab's content section should have a visible condition: { "eq": [{ "path": "/activeTab" }, "tabName"] }.`,
744
+ 'Each tab button should be a pressable component wrapping its icon/label children, with action "setState" and actionParams { "statePath": "/activeTab", "value": "tabName" }.',
745
+ `Each tab's content section should have a visible condition: { "$state": "/activeTab", "eq": "tabName" }.`,
722
746
  "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.",
723
747
  // Tab active state highlighting (using dynamic props)
724
748
  "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.",
725
- ' - For the icon name: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "home", "$else": "home-outline" }',
726
- ' - For the icon color: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "#007AFF", "$else": "#8E8E93" }',
749
+ ' - For the icon name: { "$cond": { "$state": "/activeTab", "eq": "thisTabName" }, "$then": "home", "$else": "home-outline" }',
750
+ ' - For the icon color: { "$cond": { "$state": "/activeTab", "eq": "thisTabName" }, "$then": "#007AFF", "$else": "#8E8E93" }',
727
751
  " - For labels, use $cond on the color prop similarly.",
728
- ' - For the FIRST/DEFAULT tab, use { "$cond": { "or": [{ "eq": [{ "path": "/activeTab" }, "thisTabName"] }, { "not": { "path": "/activeTab" } }] }, "$then": "#007AFF", "$else": "#8E8E93" } so it appears active before any tab is tapped.',
752
+ ' - 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.',
729
753
  // Push/pop screen navigation (all screens in one spec)
730
754
  '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.',
731
- 'Each screen section uses a visible condition on /currentScreen: { "eq": [{ "path": "/currentScreen" }, "screenName"] }. The default/home screen should also be visible when /currentScreen is not set: { "or": [{ "eq": [{ "path": "/currentScreen" }, "home"] }, { "not": { "path": "/currentScreen" } }] }.',
755
+ '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.',
732
756
  "push automatically maintains a /navStack in the state model so pop always returns to the previous screen.",
733
757
  '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.',
734
758
  "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.",
735
- '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:{"eq":[{"path":"/currentScreen"},"detail"]} and contains a back button with action:"pop". ONLY use components from the AVAILABLE COMPONENTS list.'
759
+ '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.'
736
760
  ]
737
761
  }
738
762
  );
739
763
  var elementTreeSchema = schema;
740
764
 
741
765
  // src/renderer.tsx
742
- var import_react6 = __toESM(require("react"));
743
- var import_core6 = require("@json-render/core");
766
+ var import_react7 = __toESM(require("react"));
767
+ var import_core7 = require("@json-render/core");
744
768
 
745
769
  // src/components/standard.tsx
746
770
  var import_react_native2 = require("react-native");
747
- var import_jsx_runtime6 = require("react/jsx-runtime");
748
- function ContainerComponent({ element, children }) {
749
- const p = element.props;
750
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
751
- import_react_native2.View,
752
- {
753
- style: {
754
- padding: p.padding ?? void 0,
755
- paddingHorizontal: p.paddingHorizontal ?? void 0,
756
- paddingVertical: p.paddingVertical ?? void 0,
757
- margin: p.margin ?? void 0,
758
- backgroundColor: p.backgroundColor ?? void 0,
759
- borderRadius: p.borderRadius ?? void 0,
760
- flex: p.flex ?? void 0
761
- },
762
- children
763
- }
764
- );
765
- }
766
- function RowComponent({ element, children }) {
767
- const p = element.props;
768
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
769
- import_react_native2.View,
770
- {
771
- style: {
772
- flexDirection: "row",
773
- gap: p.gap ?? void 0,
774
- alignItems: p.alignItems ?? void 0,
775
- justifyContent: p.justifyContent ?? void 0,
776
- flexWrap: p.flexWrap ?? void 0,
777
- padding: p.padding ?? void 0,
778
- flex: p.flex ?? void 0
779
- },
780
- children
781
- }
771
+
772
+ // src/hooks.ts
773
+ var import_react6 = require("react");
774
+ var import_core6 = require("@json-render/core");
775
+ function useBoundProp(propValue, bindingPath) {
776
+ const { set } = useStateStore();
777
+ const setValue = (0, import_react6.useCallback)(
778
+ (value) => {
779
+ if (bindingPath) set(bindingPath, value);
780
+ },
781
+ [bindingPath, set]
782
782
  );
783
+ return [propValue, setValue];
783
784
  }
784
- function ColumnComponent({ element, children }) {
785
- const p = element.props;
786
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
787
- import_react_native2.View,
788
- {
789
- style: {
790
- flexDirection: "column",
791
- gap: p.gap ?? void 0,
792
- alignItems: p.alignItems ?? void 0,
793
- justifyContent: p.justifyContent ?? void 0,
794
- padding: p.padding ?? void 0,
795
- flex: p.flex ?? void 0
796
- },
797
- children
798
- }
799
- );
785
+ function looksLikeJson(line) {
786
+ return line.startsWith("{") || line.startsWith("[");
800
787
  }
801
- function ScrollContainerComponent({ element, children }) {
802
- const p = element.props;
803
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
804
- import_react_native2.ScrollView,
805
- {
806
- horizontal: p.horizontal ?? false,
807
- showsVerticalScrollIndicator: p.showsScrollIndicator ?? true,
808
- showsHorizontalScrollIndicator: p.showsScrollIndicator ?? true,
809
- contentContainerStyle: {
810
- padding: p.padding ?? void 0
811
- },
812
- style: {
813
- flex: p.flex ?? 1,
814
- backgroundColor: p.backgroundColor ?? void 0
815
- },
816
- children
788
+ function parsePatchLine(line) {
789
+ const trimmed = line.trim();
790
+ if (!trimmed || trimmed.startsWith("//")) {
791
+ return { patch: null, malformed: false };
792
+ }
793
+ if (!looksLikeJson(trimmed)) {
794
+ return { patch: null, malformed: false };
795
+ }
796
+ try {
797
+ return { patch: JSON.parse(trimmed), malformed: false };
798
+ } catch {
799
+ }
800
+ let attempt = trimmed;
801
+ for (let i = 0; i < 3; i++) {
802
+ const last = attempt[attempt.length - 1];
803
+ if (last === "}" || last === "]") {
804
+ attempt = attempt.slice(0, -1);
805
+ try {
806
+ const result = JSON.parse(attempt);
807
+ console.warn(
808
+ `[json-render] Recovered malformed JSONL line by removing ${i + 1} trailing '${last}'`
809
+ );
810
+ return { patch: result, malformed: false };
811
+ } catch {
812
+ }
813
+ } else {
814
+ break;
817
815
  }
818
- );
816
+ }
817
+ return { patch: null, malformed: true };
819
818
  }
820
- function SafeAreaComponent({ element, children }) {
821
- const p = element.props;
822
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
823
- import_react_native2.SafeAreaView,
824
- {
825
- style: {
826
- flex: 1,
827
- backgroundColor: p.backgroundColor ?? void 0
828
- },
829
- children
819
+ function setSpecValue(newSpec, path, value) {
820
+ if (path === "/root") {
821
+ newSpec.root = value;
822
+ return;
823
+ }
824
+ if (path === "/state") {
825
+ newSpec.state = value;
826
+ return;
827
+ }
828
+ if (path.startsWith("/state/")) {
829
+ if (!newSpec.state) newSpec.state = {};
830
+ const statePath = path.slice("/state".length);
831
+ (0, import_core6.setByPath)(newSpec.state, statePath, value);
832
+ return;
833
+ }
834
+ if (path.startsWith("/elements/")) {
835
+ const pathParts = path.slice("/elements/".length).split("/");
836
+ const elementKey = pathParts[0];
837
+ if (!elementKey) return;
838
+ if (pathParts.length === 1) {
839
+ newSpec.elements[elementKey] = value;
840
+ } else {
841
+ const element = newSpec.elements[elementKey];
842
+ if (element) {
843
+ const propPath = "/" + pathParts.slice(1).join("/");
844
+ const newElement = { ...element };
845
+ (0, import_core6.setByPath)(
846
+ newElement,
847
+ propPath,
848
+ value
849
+ );
850
+ newSpec.elements[elementKey] = newElement;
851
+ }
830
852
  }
831
- );
853
+ }
832
854
  }
833
- function SpacerComponent({ element }) {
834
- const p = element.props;
835
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
836
- import_react_native2.View,
837
- {
838
- style: {
839
- width: p.size ?? void 0,
840
- height: p.size ?? void 0,
841
- flex: p.flex ?? void 0
855
+ function removeSpecValue(newSpec, path) {
856
+ if (path === "/state") {
857
+ newSpec.state = void 0;
858
+ return;
859
+ }
860
+ if (path.startsWith("/state/") && newSpec.state) {
861
+ const statePath = path.slice("/state".length);
862
+ (0, import_core6.removeByPath)(newSpec.state, statePath);
863
+ return;
864
+ }
865
+ if (path.startsWith("/elements/")) {
866
+ const pathParts = path.slice("/elements/".length).split("/");
867
+ const elementKey = pathParts[0];
868
+ if (!elementKey) return;
869
+ if (pathParts.length === 1) {
870
+ const { [elementKey]: _, ...rest } = newSpec.elements;
871
+ newSpec.elements = rest;
872
+ } else {
873
+ const element = newSpec.elements[elementKey];
874
+ if (element) {
875
+ const propPath = "/" + pathParts.slice(1).join("/");
876
+ const newElement = { ...element };
877
+ (0, import_core6.removeByPath)(
878
+ newElement,
879
+ propPath
880
+ );
881
+ newSpec.elements[elementKey] = newElement;
842
882
  }
843
883
  }
844
- );
884
+ }
845
885
  }
846
- function PressableComponent({ children, emit }) {
847
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
848
- import_react_native2.Pressable,
849
- {
850
- onPress: () => emit?.("press"),
851
- onLongPress: () => emit?.("longPress"),
852
- style: ({ pressed }) => ({
853
- opacity: pressed ? 0.7 : 1
854
- }),
855
- children
856
- }
857
- );
886
+ function getSpecValue(spec, path) {
887
+ if (path === "/root") return spec.root;
888
+ return (0, import_core6.getByPath)(spec, path);
858
889
  }
859
- function DividerComponent({ element }) {
860
- const p = element.props;
861
- const isVertical = p.direction === "vertical";
862
- const thickness = p.thickness ?? 1;
863
- const color = p.color ?? "#e5e7eb";
864
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
865
- import_react_native2.View,
866
- {
867
- style: {
868
- width: isVertical ? thickness : "100%",
869
- height: isVertical ? "100%" : thickness,
870
- backgroundColor: color,
871
- marginVertical: isVertical ? 0 : p.margin ?? 8,
872
- marginHorizontal: isVertical ? p.margin ?? 8 : 0
873
- }
890
+ function applyPatch(spec, patch) {
891
+ const newSpec = { ...spec, elements: { ...spec.elements } };
892
+ switch (patch.op) {
893
+ case "add":
894
+ case "replace": {
895
+ setSpecValue(newSpec, patch.path, patch.value);
896
+ break;
874
897
  }
875
- );
876
- }
877
- var headingSizes = {
878
- h1: 32,
879
- h2: 24,
880
- h3: 20,
881
- h4: 16
882
- };
883
- function HeadingComponent({ element }) {
884
- const p = element.props;
885
- const level = p.level ?? "h2";
898
+ case "remove": {
899
+ removeSpecValue(newSpec, patch.path);
900
+ break;
901
+ }
902
+ case "move": {
903
+ if (!patch.from) break;
904
+ const moveValue = getSpecValue(newSpec, patch.from);
905
+ removeSpecValue(newSpec, patch.from);
906
+ setSpecValue(newSpec, patch.path, moveValue);
907
+ break;
908
+ }
909
+ case "copy": {
910
+ if (!patch.from) break;
911
+ const copyValue = getSpecValue(newSpec, patch.from);
912
+ setSpecValue(newSpec, patch.path, copyValue);
913
+ break;
914
+ }
915
+ case "test": {
916
+ break;
917
+ }
918
+ }
919
+ return newSpec;
920
+ }
921
+ function useUIStream({
922
+ api,
923
+ onComplete,
924
+ onError,
925
+ fetch: fetchFn = globalThis.fetch,
926
+ validate: enableValidation = false,
927
+ maxRetries = 5
928
+ }) {
929
+ const [spec, setSpec] = (0, import_react6.useState)(null);
930
+ const [isStreaming, setIsStreaming] = (0, import_react6.useState)(false);
931
+ const [error, setError] = (0, import_react6.useState)(null);
932
+ const [rawLines, setRawLines] = (0, import_react6.useState)([]);
933
+ const abortControllerRef = (0, import_react6.useRef)(null);
934
+ const stop = (0, import_react6.useCallback)(() => {
935
+ abortControllerRef.current?.abort();
936
+ abortControllerRef.current = null;
937
+ setIsStreaming(false);
938
+ }, []);
939
+ const clear = (0, import_react6.useCallback)(() => {
940
+ setSpec(null);
941
+ setError(null);
942
+ setRawLines([]);
943
+ }, []);
944
+ const streamRequest = (0, import_react6.useCallback)(
945
+ async (prompt, context, initialSpec, abortOnMalformed) => {
946
+ let currentSpec = initialSpec;
947
+ setSpec(currentSpec);
948
+ const malformedLines = [];
949
+ const response = await fetchFn(api, {
950
+ method: "POST",
951
+ headers: { "Content-Type": "application/json" },
952
+ body: JSON.stringify({
953
+ prompt,
954
+ context,
955
+ currentSpec
956
+ }),
957
+ signal: abortControllerRef.current.signal
958
+ });
959
+ if (!response.ok) {
960
+ let errorMessage = `HTTP error: ${response.status}`;
961
+ try {
962
+ const errorData = await response.json();
963
+ if (errorData.message) {
964
+ errorMessage = errorData.message;
965
+ } else if (errorData.error) {
966
+ errorMessage = errorData.error;
967
+ }
968
+ } catch {
969
+ }
970
+ throw new Error(errorMessage);
971
+ }
972
+ const reader = response.body?.getReader();
973
+ if (!reader) {
974
+ throw new Error("No response body");
975
+ }
976
+ const decoder = new TextDecoder();
977
+ let buffer = "";
978
+ let aborted = false;
979
+ while (true) {
980
+ const { done, value } = await reader.read();
981
+ if (done) {
982
+ break;
983
+ }
984
+ const decoded = decoder.decode(value, { stream: true });
985
+ buffer += decoded;
986
+ const lines = buffer.split("\n");
987
+ buffer = lines.pop() ?? "";
988
+ const newLines = [];
989
+ for (const line of lines) {
990
+ if (line.trim()) {
991
+ newLines.push(line);
992
+ }
993
+ const { patch, malformed } = parsePatchLine(line);
994
+ if (patch) {
995
+ currentSpec = applyPatch(currentSpec, patch);
996
+ setSpec({ ...currentSpec });
997
+ } else if (malformed) {
998
+ malformedLines.push(line.trim());
999
+ if (abortOnMalformed) {
1000
+ await reader.cancel();
1001
+ aborted = true;
1002
+ break;
1003
+ }
1004
+ }
1005
+ }
1006
+ if (aborted) break;
1007
+ if (newLines.length > 0) {
1008
+ setRawLines((prev) => [...prev, ...newLines]);
1009
+ }
1010
+ }
1011
+ if (!aborted && buffer.trim()) {
1012
+ setRawLines((prev) => [...prev, buffer]);
1013
+ const { patch, malformed } = parsePatchLine(buffer);
1014
+ if (patch) {
1015
+ currentSpec = applyPatch(currentSpec, patch);
1016
+ setSpec({ ...currentSpec });
1017
+ } else if (malformed) {
1018
+ malformedLines.push(buffer.trim());
1019
+ }
1020
+ }
1021
+ return { spec: currentSpec, completed: !aborted, malformedLines };
1022
+ },
1023
+ [api, fetchFn]
1024
+ );
1025
+ const send = (0, import_react6.useCallback)(
1026
+ async (prompt, context) => {
1027
+ abortControllerRef.current?.abort();
1028
+ abortControllerRef.current = new AbortController();
1029
+ setIsStreaming(true);
1030
+ setError(null);
1031
+ setRawLines([]);
1032
+ const previousSpec = context?.previousSpec;
1033
+ let currentSpec = previousSpec && previousSpec.root ? { ...previousSpec, elements: { ...previousSpec.elements } } : { root: "", elements: {} };
1034
+ let retriesUsed = 0;
1035
+ let currentPrompt = prompt;
1036
+ let currentContext = context;
1037
+ try {
1038
+ while (retriesUsed <= maxRetries) {
1039
+ const result = await streamRequest(
1040
+ currentPrompt,
1041
+ currentContext,
1042
+ currentSpec,
1043
+ enableValidation
1044
+ // only abort on malformed when validation is on
1045
+ );
1046
+ currentSpec = result.spec;
1047
+ if (!result.completed && result.malformedLines.length > 0) {
1048
+ if (retriesUsed >= maxRetries) {
1049
+ break;
1050
+ }
1051
+ retriesUsed++;
1052
+ currentContext = { ...context, previousSpec: currentSpec };
1053
+ currentPrompt = `The previous generation contained malformed JSON that could not be parsed. The line was:
1054
+ ${result.malformedLines[result.malformedLines.length - 1]?.slice(0, 500)}
1055
+
1056
+ The current spec state is provided. Continue generating from where you left off. Output ONLY the remaining patches needed to complete the UI.`;
1057
+ continue;
1058
+ }
1059
+ if (!enableValidation || !currentSpec.root) {
1060
+ break;
1061
+ }
1062
+ const { spec: fixedSpec, fixes } = (0, import_core6.autoFixSpec)(currentSpec);
1063
+ if (fixes.length > 0) {
1064
+ currentSpec = fixedSpec;
1065
+ setSpec({ ...currentSpec });
1066
+ }
1067
+ const validation = (0, import_core6.validateSpec)(currentSpec);
1068
+ if (validation.valid) {
1069
+ break;
1070
+ }
1071
+ const errors = validation.issues.filter(
1072
+ (i) => i.severity === "error"
1073
+ );
1074
+ if (retriesUsed >= maxRetries) {
1075
+ break;
1076
+ }
1077
+ retriesUsed++;
1078
+ const issueText = (0, import_core6.formatSpecIssues)(validation.issues);
1079
+ currentContext = { ...context, previousSpec: currentSpec };
1080
+ currentPrompt = `FIX THE FOLLOWING ERRORS in the current UI spec. Output ONLY the patches needed to fix these issues, do not recreate the entire UI.
1081
+
1082
+ ` + issueText;
1083
+ }
1084
+ onComplete?.(currentSpec);
1085
+ } catch (err) {
1086
+ if (err.name === "AbortError") {
1087
+ return;
1088
+ }
1089
+ const error2 = err instanceof Error ? err : new Error(String(err));
1090
+ setError(error2);
1091
+ onError?.(error2);
1092
+ } finally {
1093
+ setIsStreaming(false);
1094
+ }
1095
+ },
1096
+ [
1097
+ api,
1098
+ fetchFn,
1099
+ onComplete,
1100
+ onError,
1101
+ enableValidation,
1102
+ maxRetries,
1103
+ streamRequest
1104
+ ]
1105
+ );
1106
+ (0, import_react6.useEffect)(() => {
1107
+ return () => {
1108
+ abortControllerRef.current?.abort();
1109
+ };
1110
+ }, []);
1111
+ return {
1112
+ spec,
1113
+ isStreaming,
1114
+ error,
1115
+ rawLines,
1116
+ send,
1117
+ stop,
1118
+ clear
1119
+ };
1120
+ }
1121
+ function flatToTree(elements) {
1122
+ const elementMap = {};
1123
+ let root = "";
1124
+ for (const element of elements) {
1125
+ elementMap[element.key] = {
1126
+ type: element.type,
1127
+ props: element.props,
1128
+ children: [],
1129
+ visible: element.visible
1130
+ };
1131
+ }
1132
+ for (const element of elements) {
1133
+ if (element.parentKey) {
1134
+ const parent = elementMap[element.parentKey];
1135
+ if (parent) {
1136
+ if (!parent.children) {
1137
+ parent.children = [];
1138
+ }
1139
+ parent.children.push(element.key);
1140
+ }
1141
+ } else {
1142
+ root = element.key;
1143
+ }
1144
+ }
1145
+ return { root, elements: elementMap };
1146
+ }
1147
+
1148
+ // src/components/standard.tsx
1149
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1150
+ function ContainerComponent({ element, children }) {
1151
+ const p = element.props;
1152
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1153
+ import_react_native2.View,
1154
+ {
1155
+ style: {
1156
+ padding: p.padding ?? void 0,
1157
+ paddingHorizontal: p.paddingHorizontal ?? void 0,
1158
+ paddingVertical: p.paddingVertical ?? void 0,
1159
+ margin: p.margin ?? void 0,
1160
+ backgroundColor: p.backgroundColor ?? void 0,
1161
+ borderRadius: p.borderRadius ?? void 0,
1162
+ flex: p.flex ?? void 0
1163
+ },
1164
+ children
1165
+ }
1166
+ );
1167
+ }
1168
+ function RowComponent({ element, children }) {
1169
+ const p = element.props;
1170
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1171
+ import_react_native2.View,
1172
+ {
1173
+ style: {
1174
+ flexDirection: "row",
1175
+ gap: p.gap ?? void 0,
1176
+ alignItems: p.alignItems ?? void 0,
1177
+ justifyContent: p.justifyContent ?? void 0,
1178
+ flexWrap: p.flexWrap ?? void 0,
1179
+ padding: p.padding ?? void 0,
1180
+ flex: p.flex ?? void 0
1181
+ },
1182
+ children
1183
+ }
1184
+ );
1185
+ }
1186
+ function ColumnComponent({ element, children }) {
1187
+ const p = element.props;
1188
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1189
+ import_react_native2.View,
1190
+ {
1191
+ style: {
1192
+ flexDirection: "column",
1193
+ gap: p.gap ?? void 0,
1194
+ alignItems: p.alignItems ?? void 0,
1195
+ justifyContent: p.justifyContent ?? void 0,
1196
+ padding: p.padding ?? void 0,
1197
+ flex: p.flex ?? void 0
1198
+ },
1199
+ children
1200
+ }
1201
+ );
1202
+ }
1203
+ function ScrollContainerComponent({ element, children }) {
1204
+ const p = element.props;
1205
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1206
+ import_react_native2.ScrollView,
1207
+ {
1208
+ horizontal: p.horizontal ?? false,
1209
+ showsVerticalScrollIndicator: p.showsScrollIndicator ?? true,
1210
+ showsHorizontalScrollIndicator: p.showsScrollIndicator ?? true,
1211
+ contentContainerStyle: {
1212
+ padding: p.padding ?? void 0
1213
+ },
1214
+ style: {
1215
+ flex: p.flex ?? 1,
1216
+ backgroundColor: p.backgroundColor ?? void 0
1217
+ },
1218
+ children
1219
+ }
1220
+ );
1221
+ }
1222
+ function SafeAreaComponent({ element, children }) {
1223
+ const p = element.props;
1224
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1225
+ import_react_native2.SafeAreaView,
1226
+ {
1227
+ style: {
1228
+ flex: 1,
1229
+ backgroundColor: p.backgroundColor ?? void 0
1230
+ },
1231
+ children
1232
+ }
1233
+ );
1234
+ }
1235
+ function SpacerComponent({ element }) {
1236
+ const p = element.props;
1237
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1238
+ import_react_native2.View,
1239
+ {
1240
+ style: {
1241
+ width: p.size ?? void 0,
1242
+ height: p.size ?? void 0,
1243
+ flex: p.flex ?? void 0
1244
+ }
1245
+ }
1246
+ );
1247
+ }
1248
+ function PressableComponent({ children, emit }) {
1249
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1250
+ import_react_native2.Pressable,
1251
+ {
1252
+ onPress: () => emit("press"),
1253
+ onLongPress: () => emit("longPress"),
1254
+ style: ({ pressed }) => ({
1255
+ opacity: pressed ? 0.7 : 1
1256
+ }),
1257
+ children
1258
+ }
1259
+ );
1260
+ }
1261
+ function DividerComponent({ element }) {
1262
+ const p = element.props;
1263
+ const isVertical = p.direction === "vertical";
1264
+ const thickness = p.thickness ?? 1;
1265
+ const color = p.color ?? "#e5e7eb";
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1267
+ import_react_native2.View,
1268
+ {
1269
+ style: {
1270
+ width: isVertical ? thickness : "100%",
1271
+ height: isVertical ? "100%" : thickness,
1272
+ backgroundColor: color,
1273
+ marginVertical: isVertical ? 0 : p.margin ?? 8,
1274
+ marginHorizontal: isVertical ? p.margin ?? 8 : 0
1275
+ }
1276
+ }
1277
+ );
1278
+ }
1279
+ var headingSizes = {
1280
+ h1: 32,
1281
+ h2: 24,
1282
+ h3: 20,
1283
+ h4: 16
1284
+ };
1285
+ function HeadingComponent({ element }) {
1286
+ const p = element.props;
1287
+ const level = p.level ?? "h2";
886
1288
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
887
1289
  import_react_native2.Text,
888
1290
  {
@@ -1041,7 +1443,7 @@ function ChipComponent({ element, emit }) {
1041
1443
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1042
1444
  import_react_native2.Pressable,
1043
1445
  {
1044
- onPress: () => emit?.("press"),
1446
+ onPress: () => emit("press"),
1045
1447
  style: {
1046
1448
  flexDirection: "row",
1047
1449
  alignItems: "center",
@@ -1053,7 +1455,7 @@ function ChipComponent({ element, emit }) {
1053
1455
  },
1054
1456
  children: [
1055
1457
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: textColor }, children: p.label }),
1056
- hasRemove && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Pressable, { onPress: () => emit?.("remove"), style: { marginLeft: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: textColor, fontWeight: "700" }, children: "x" }) })
1458
+ hasRemove && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Pressable, { onPress: () => emit("remove"), style: { marginLeft: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: textColor, fontWeight: "700" }, children: "x" }) })
1057
1459
  ]
1058
1460
  }
1059
1461
  );
@@ -1086,7 +1488,7 @@ function ButtonComponent({ element, emit }) {
1086
1488
  import_react_native2.Pressable,
1087
1489
  {
1088
1490
  disabled,
1089
- onPress: () => emit?.("press"),
1491
+ onPress: () => emit("press"),
1090
1492
  style: ({ pressed }) => ({
1091
1493
  backgroundColor: variant.bg,
1092
1494
  paddingHorizontal: size.paddingH,
@@ -1124,12 +1526,13 @@ function ButtonComponent({ element, emit }) {
1124
1526
  }
1125
1527
  );
1126
1528
  }
1127
- function TextInputComponent({ element }) {
1529
+ function TextInputComponent({ element, bindings }) {
1128
1530
  const p = element.props;
1129
- const [boundValue, setBoundValue] = useStateBinding(
1130
- p.statePath ?? ""
1531
+ const [boundValue, setBoundValue] = useBoundProp(
1532
+ p.value,
1533
+ bindings?.value
1131
1534
  );
1132
- const displayValue = p.statePath ? boundValue ?? "" : p.value ?? "";
1535
+ const displayValue = boundValue ?? "";
1133
1536
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: p.flex != null ? { flex: p.flex } : void 0, children: [
1134
1537
  p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: inputStyles.label, children: p.label }),
1135
1538
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -1137,7 +1540,7 @@ function TextInputComponent({ element }) {
1137
1540
  {
1138
1541
  placeholder: p.placeholder ?? void 0,
1139
1542
  value: displayValue,
1140
- onChangeText: p.statePath ? setBoundValue : void 0,
1543
+ onChangeText: bindings?.value ? setBoundValue : void 0,
1141
1544
  secureTextEntry: p.secureTextEntry ?? false,
1142
1545
  keyboardType: p.keyboardType ?? "default",
1143
1546
  multiline: p.multiline ?? false,
@@ -1153,1146 +1556,713 @@ function TextInputComponent({ element }) {
1153
1556
  )
1154
1557
  ] });
1155
1558
  }
1156
- function SwitchComponent({ element }) {
1559
+ function SwitchComponent({ element, bindings }) {
1157
1560
  const p = element.props;
1158
- const [boundValue, setBoundValue] = useStateBinding(
1159
- p.statePath ?? ""
1561
+ const [checked, setChecked] = useBoundProp(
1562
+ p.checked,
1563
+ bindings?.checked
1160
1564
  );
1161
- const displayValue = p.statePath ? boundValue ?? false : p.value ?? false;
1565
+ const displayValue = checked ?? false;
1162
1566
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: { flexDirection: "row", alignItems: "center", gap: 8 }, children: [
1163
1567
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1164
1568
  import_react_native2.Switch,
1165
1569
  {
1166
1570
  value: displayValue,
1167
- onValueChange: p.statePath ? setBoundValue : void 0,
1571
+ onValueChange: bindings?.checked ? setChecked : void 0,
1168
1572
  disabled: p.disabled ?? false
1169
1573
  }
1170
1574
  ),
1171
- p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#374151" }, children: p.label })
1172
- ] });
1173
- }
1174
- function CheckboxComponent({ element }) {
1175
- const p = element.props;
1176
- const [boundValue, setBoundValue] = useStateBinding(
1177
- p.statePath ?? ""
1178
- );
1179
- const checked = p.statePath ? boundValue ?? false : p.checked ?? false;
1180
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1181
- import_react_native2.Pressable,
1182
- {
1183
- disabled: p.disabled ?? false,
1184
- onPress: () => {
1185
- if (p.statePath) {
1186
- setBoundValue(!checked);
1187
- }
1188
- },
1189
- style: {
1190
- flexDirection: "row",
1191
- alignItems: "center",
1192
- gap: 8,
1193
- opacity: p.disabled ? 0.5 : 1
1194
- },
1195
- children: [
1196
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1197
- import_react_native2.View,
1198
- {
1199
- style: {
1200
- width: 20,
1201
- height: 20,
1202
- borderRadius: 4,
1203
- borderWidth: 2,
1204
- borderColor: checked ? "#3b82f6" : "#d1d5db",
1205
- backgroundColor: checked ? "#3b82f6" : "transparent",
1206
- alignItems: "center",
1207
- justifyContent: "center"
1208
- },
1209
- children: checked && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { color: "#ffffff", fontSize: 12, fontWeight: "700" }, children: "\u2713" })
1210
- }
1211
- ),
1212
- p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#374151" }, children: p.label })
1213
- ]
1214
- }
1215
- );
1216
- }
1217
- function SliderComponent({ element }) {
1218
- const p = element.props;
1219
- const min = p.min ?? 0;
1220
- const max = p.max ?? 100;
1221
- const value = p.value ?? min;
1222
- const progress = max > min ? (value - min) / (max - min) : 0;
1223
- const trackColor = p.color ?? "#3b82f6";
1224
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { children: [
1225
- p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1226
- import_react_native2.View,
1227
- {
1228
- style: {
1229
- flexDirection: "row",
1230
- justifyContent: "space-between",
1231
- marginBottom: 4
1232
- },
1233
- children: [
1234
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#374151" }, children: p.label }),
1235
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280" }, children: value })
1236
- ]
1237
- }
1238
- ),
1239
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1240
- import_react_native2.View,
1241
- {
1242
- style: {
1243
- height: 6,
1244
- backgroundColor: "#e5e7eb",
1245
- borderRadius: 3,
1246
- overflow: "hidden"
1247
- },
1248
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1249
- import_react_native2.View,
1250
- {
1251
- style: {
1252
- width: `${Math.min(Math.max(progress * 100, 0), 100)}%`,
1253
- height: "100%",
1254
- backgroundColor: trackColor,
1255
- borderRadius: 3
1256
- }
1257
- }
1258
- )
1259
- }
1260
- )
1261
- ] });
1262
- }
1263
- function SearchBarComponent({ element, emit }) {
1264
- const p = element.props;
1265
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1266
- import_react_native2.View,
1267
- {
1268
- style: {
1269
- flexDirection: "row",
1270
- alignItems: "center",
1271
- backgroundColor: "#f3f4f6",
1272
- borderRadius: 8,
1273
- paddingHorizontal: 12
1274
- },
1275
- children: [
1276
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#9ca3af", marginRight: 8 }, children: "Search" }),
1277
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1278
- import_react_native2.TextInput,
1279
- {
1280
- placeholder: p.placeholder ?? "Search...",
1281
- value: p.value ?? void 0,
1282
- returnKeyType: "search",
1283
- onSubmitEditing: () => emit?.("submit"),
1284
- style: {
1285
- flex: 1,
1286
- paddingVertical: 10,
1287
- fontSize: 16,
1288
- color: "#111827"
1289
- }
1290
- }
1291
- )
1292
- ]
1293
- }
1294
- );
1295
- }
1296
- function SpinnerComponent({ element }) {
1297
- const p = element.props;
1298
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.ActivityIndicator, { size: p.size ?? "small", color: p.color ?? "#3b82f6" });
1299
- }
1300
- function ProgressBarComponent({ element }) {
1301
- const p = element.props;
1302
- const height = p.height ?? 6;
1303
- const progress = Math.min(Math.max(p.progress, 0), 1);
1304
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1305
- import_react_native2.View,
1306
- {
1307
- style: {
1308
- height,
1309
- backgroundColor: p.trackColor ?? "#e5e7eb",
1310
- borderRadius: height / 2,
1311
- overflow: "hidden"
1312
- },
1313
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1314
- import_react_native2.View,
1315
- {
1316
- style: {
1317
- width: `${progress * 100}%`,
1318
- height: "100%",
1319
- backgroundColor: p.color ?? "#3b82f6",
1320
- borderRadius: height / 2
1321
- }
1322
- }
1323
- )
1324
- }
1325
- );
1326
- }
1327
- function CardComponent({ element, children }) {
1328
- const p = element.props;
1329
- const elevated = p.elevated !== false;
1330
- const padding = p.padding ?? 16;
1331
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1332
- import_react_native2.View,
1333
- {
1334
- style: [
1335
- {
1336
- backgroundColor: p.backgroundColor ?? "#ffffff",
1337
- borderRadius: p.borderRadius ?? 12,
1338
- padding
1339
- },
1340
- elevated && {
1341
- shadowColor: "#000",
1342
- shadowOffset: { width: 0, height: 2 },
1343
- shadowOpacity: 0.1,
1344
- shadowRadius: 8,
1345
- elevation: 3
1346
- }
1347
- ],
1348
- children: [
1349
- p.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1350
- import_react_native2.Text,
1351
- {
1352
- style: {
1353
- fontSize: 18,
1354
- fontWeight: "600",
1355
- color: "#111827",
1356
- marginBottom: p.subtitle ? 2 : 12
1357
- },
1358
- children: p.title
1359
- }
1360
- ),
1361
- p.subtitle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1362
- import_react_native2.Text,
1363
- {
1364
- style: {
1365
- fontSize: 14,
1366
- color: "#6b7280",
1367
- marginBottom: 12
1368
- },
1369
- children: p.subtitle
1370
- }
1371
- ),
1372
- children
1373
- ]
1374
- }
1375
- );
1575
+ p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#374151" }, children: p.label })
1576
+ ] });
1376
1577
  }
1377
- function ListItemComponent({ element, emit }) {
1578
+ function CheckboxComponent({ element, bindings }) {
1378
1579
  const p = element.props;
1379
- const hasPress = !!element.on?.press;
1380
- const content = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1381
- import_react_native2.View,
1580
+ const [checked, setChecked] = useBoundProp(
1581
+ p.checked,
1582
+ bindings?.checked
1583
+ );
1584
+ const isChecked = checked ?? false;
1585
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1586
+ import_react_native2.Pressable,
1382
1587
  {
1588
+ disabled: p.disabled ?? false,
1589
+ onPress: () => {
1590
+ if (bindings?.checked) {
1591
+ setChecked(!isChecked);
1592
+ }
1593
+ },
1383
1594
  style: {
1384
1595
  flexDirection: "row",
1385
1596
  alignItems: "center",
1386
- paddingVertical: 12,
1387
- paddingHorizontal: 16
1597
+ gap: 8,
1598
+ opacity: p.disabled ? 0.5 : 1
1388
1599
  },
1389
1600
  children: [
1390
- p.leading && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1391
- import_react_native2.Text,
1601
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1602
+ import_react_native2.View,
1392
1603
  {
1393
1604
  style: {
1394
- fontSize: 16,
1395
- color: "#6b7280",
1396
- marginRight: 12,
1397
- width: 24,
1398
- textAlign: "center"
1605
+ width: 20,
1606
+ height: 20,
1607
+ borderRadius: 4,
1608
+ borderWidth: 2,
1609
+ borderColor: isChecked ? "#3b82f6" : "#d1d5db",
1610
+ backgroundColor: isChecked ? "#3b82f6" : "transparent",
1611
+ alignItems: "center",
1612
+ justifyContent: "center"
1399
1613
  },
1400
- children: p.leading
1401
- }
1402
- ),
1403
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: { flex: 1 }, children: [
1404
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#111827" }, children: p.title }),
1405
- p.subtitle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280", marginTop: 2 }, children: p.subtitle })
1406
- ] }),
1407
- p.trailing && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280", marginLeft: 8 }, children: p.trailing }),
1408
- p.showChevron && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#9ca3af", marginLeft: 8 }, children: ">" })
1409
- ]
1410
- }
1411
- );
1412
- if (hasPress) {
1413
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1414
- import_react_native2.Pressable,
1415
- {
1416
- onPress: () => emit?.("press"),
1417
- style: ({ pressed }) => ({
1418
- opacity: pressed ? 0.7 : 1,
1419
- backgroundColor: pressed ? "#f9fafb" : "transparent"
1420
- }),
1421
- children: content
1422
- }
1423
- );
1424
- }
1425
- return content;
1426
- }
1427
- function ModalComponent({ element, children }) {
1428
- const p = element.props;
1429
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1430
- import_react_native2.Modal,
1431
- {
1432
- visible: p.visible,
1433
- animationType: p.animationType ?? "slide",
1434
- transparent: true,
1435
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.View, { style: modalStyles.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: modalStyles.content, children: [
1436
- p.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: modalStyles.title, children: p.title }),
1437
- children
1438
- ] }) })
1439
- }
1440
- );
1441
- }
1442
- function createStandardActionHandlers(options) {
1443
- return {
1444
- navigate: async (params) => {
1445
- const screen = params.screen;
1446
- const navParams = params.params;
1447
- options?.navigate?.(screen, navParams ?? void 0);
1448
- },
1449
- goBack: async () => {
1450
- options?.goBack?.();
1451
- },
1452
- showAlert: async (params) => {
1453
- const title = params.title;
1454
- const message = params.message ?? void 0;
1455
- const buttons = params.buttons;
1456
- const alertButtons = buttons?.map((btn) => ({
1457
- text: btn.text,
1458
- style: btn.style
1459
- }));
1460
- import_react_native2.Alert.alert(title, message, alertButtons);
1461
- },
1462
- share: async (params) => {
1463
- const message = params.message;
1464
- const url = params.url ?? void 0;
1465
- const title = params.title ?? void 0;
1466
- await import_react_native2.Share.share({ message, url, title });
1467
- },
1468
- openURL: async (params) => {
1469
- const url = params.url;
1470
- const canOpen = await import_react_native2.Linking.canOpenURL(url);
1471
- if (canOpen) {
1472
- await import_react_native2.Linking.openURL(url);
1473
- }
1474
- },
1475
- setData: async (params) => {
1476
- const path = params.path;
1477
- const value = params.value;
1478
- options?.onSetData?.(path, value);
1479
- },
1480
- refresh: async (params) => {
1481
- const target = params.target ?? void 0;
1482
- options?.onRefresh?.(target);
1483
- }
1484
- };
1485
- }
1486
- var standardComponents = {
1487
- // Layout
1488
- Container: ContainerComponent,
1489
- Row: RowComponent,
1490
- Column: ColumnComponent,
1491
- ScrollContainer: ScrollContainerComponent,
1492
- SafeArea: SafeAreaComponent,
1493
- Pressable: PressableComponent,
1494
- Spacer: SpacerComponent,
1495
- Divider: DividerComponent,
1496
- // Content
1497
- Heading: HeadingComponent,
1498
- Paragraph: ParagraphComponent,
1499
- Label: LabelComponent,
1500
- Image: ImageComponent,
1501
- Avatar: AvatarComponent,
1502
- Badge: BadgeComponent,
1503
- Chip: ChipComponent,
1504
- // Input
1505
- Button: ButtonComponent,
1506
- TextInput: TextInputComponent,
1507
- Switch: SwitchComponent,
1508
- Checkbox: CheckboxComponent,
1509
- Slider: SliderComponent,
1510
- SearchBar: SearchBarComponent,
1511
- // Feedback
1512
- Spinner: SpinnerComponent,
1513
- ProgressBar: ProgressBarComponent,
1514
- // Composite
1515
- Card: CardComponent,
1516
- ListItem: ListItemComponent,
1517
- Modal: ModalComponent
1518
- };
1519
- var inputStyles = import_react_native2.StyleSheet.create({
1520
- label: {
1521
- fontSize: 14,
1522
- fontWeight: "500",
1523
- color: "#374151",
1524
- marginBottom: 4
1525
- },
1526
- input: {
1527
- borderWidth: 1,
1528
- borderColor: "#d1d5db",
1529
- borderRadius: 8,
1530
- paddingHorizontal: 12,
1531
- paddingVertical: 10,
1532
- fontSize: 16,
1533
- color: "#111827",
1534
- backgroundColor: "#ffffff"
1535
- }
1536
- });
1537
- var modalStyles = import_react_native2.StyleSheet.create({
1538
- overlay: {
1539
- flex: 1,
1540
- backgroundColor: "rgba(0, 0, 0, 0.5)",
1541
- justifyContent: "flex-end"
1542
- },
1543
- content: {
1544
- backgroundColor: "#ffffff",
1545
- borderTopLeftRadius: 16,
1546
- borderTopRightRadius: 16,
1547
- padding: 24,
1548
- maxHeight: "80%"
1549
- },
1550
- title: {
1551
- fontSize: 20,
1552
- fontWeight: "600",
1553
- color: "#111827",
1554
- marginBottom: 16
1555
- }
1556
- });
1557
-
1558
- // src/renderer.tsx
1559
- var import_jsx_runtime7 = require("react/jsx-runtime");
1560
- var ElementErrorBoundary = class extends import_react6.default.Component {
1561
- constructor(props) {
1562
- super(props);
1563
- this.state = { hasError: false };
1564
- }
1565
- static getDerivedStateFromError() {
1566
- return { hasError: true };
1567
- }
1568
- componentDidCatch(error, info) {
1569
- console.error(
1570
- `[json-render] Rendering error in <${this.props.elementType}>:`,
1571
- error,
1572
- info.componentStack
1573
- );
1574
- }
1575
- render() {
1576
- if (this.state.hasError) {
1577
- return null;
1578
- }
1579
- return this.props.children;
1580
- }
1581
- };
1582
- function ElementRenderer({
1583
- element,
1584
- spec,
1585
- registry,
1586
- loading,
1587
- fallback
1588
- }) {
1589
- const repeatScope = useRepeatScope();
1590
- const { ctx } = useVisibility();
1591
- const { execute } = useActions();
1592
- let effectiveElement = element;
1593
- if (repeatScope) {
1594
- const rewrittenProps = rewriteRepeatTokens(
1595
- element.props,
1596
- repeatScope.basePath,
1597
- repeatScope.index
1598
- );
1599
- const rewrittenVisible = element.visible !== void 0 ? rewriteRepeatTokens(
1600
- element.visible,
1601
- repeatScope.basePath,
1602
- repeatScope.index
1603
- ) : element.visible;
1604
- const rewrittenOn = element.on !== void 0 ? rewriteRepeatTokens(
1605
- element.on,
1606
- repeatScope.basePath,
1607
- repeatScope.index
1608
- ) : element.on;
1609
- if (rewrittenProps !== element.props || rewrittenVisible !== element.visible || rewrittenOn !== element.on) {
1610
- effectiveElement = {
1611
- ...element,
1612
- props: rewrittenProps,
1613
- visible: rewrittenVisible,
1614
- on: rewrittenOn
1615
- };
1616
- }
1617
- }
1618
- const isVisible = useIsVisible(effectiveElement.visible);
1619
- const onBindings = effectiveElement.on;
1620
- const emit = (0, import_react6.useCallback)(
1621
- (eventName) => {
1622
- const binding = onBindings?.[eventName];
1623
- if (!binding) return;
1624
- const bindings = Array.isArray(binding) ? binding : [binding];
1625
- for (const b of bindings) {
1626
- execute(b);
1627
- }
1628
- },
1629
- [onBindings, execute]
1630
- );
1631
- if (!isVisible) {
1632
- return null;
1633
- }
1634
- const resolvedProps = (0, import_core6.resolveElementProps)(
1635
- effectiveElement.props,
1636
- ctx
1637
- );
1638
- const resolvedElement = resolvedProps !== effectiveElement.props ? { ...effectiveElement, props: resolvedProps } : effectiveElement;
1639
- const Component = registry[resolvedElement.type] ?? fallback;
1640
- if (!Component) {
1641
- console.warn(`No renderer for component type: ${resolvedElement.type}`);
1642
- return null;
1643
- }
1644
- const children = resolvedElement.repeat ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1645
- RepeatChildren,
1646
- {
1647
- element: resolvedElement,
1648
- spec,
1649
- registry,
1650
- loading,
1651
- fallback
1652
- }
1653
- ) : resolvedElement.children?.map((childKey) => {
1654
- const childElement = spec.elements[childKey];
1655
- if (!childElement) {
1656
- if (!loading) {
1657
- console.warn(
1658
- `[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`
1659
- );
1660
- }
1661
- return null;
1614
+ children: isChecked && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { color: "#ffffff", fontSize: 12, fontWeight: "700" }, children: "\u2713" })
1615
+ }
1616
+ ),
1617
+ p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#374151" }, children: p.label })
1618
+ ]
1662
1619
  }
1663
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1664
- ElementRenderer,
1665
- {
1666
- element: childElement,
1667
- spec,
1668
- registry,
1669
- loading,
1670
- fallback
1671
- },
1672
- childKey
1673
- );
1674
- });
1675
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ElementErrorBoundary, { elementType: resolvedElement.type, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Component, { element: resolvedElement, emit, loading, children }) });
1620
+ );
1676
1621
  }
1677
- function RepeatChildren({
1678
- element,
1679
- spec,
1680
- registry,
1681
- loading,
1682
- fallback
1683
- }) {
1684
- const { state } = useStateStore();
1685
- const repeat = element.repeat;
1686
- const statePath = repeat.path;
1687
- const items = (0, import_core6.getByPath)(state, statePath) ?? [];
1688
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: items.map((item, index) => {
1689
- const key = repeat.key && typeof item === "object" && item !== null ? String(item[repeat.key] ?? index) : String(index);
1690
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1691
- RepeatScopeProvider,
1622
+ function SliderComponent({ element }) {
1623
+ const p = element.props;
1624
+ const min = p.min ?? 0;
1625
+ const max = p.max ?? 100;
1626
+ const value = p.value ?? min;
1627
+ const progress = max > min ? (value - min) / (max - min) : 0;
1628
+ const trackColor = p.color ?? "#3b82f6";
1629
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { children: [
1630
+ p.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1631
+ import_react_native2.View,
1692
1632
  {
1693
- basePath: `${statePath}/${index}`,
1694
- index,
1695
- children: element.children?.map((childKey) => {
1696
- const childElement = spec.elements[childKey];
1697
- if (!childElement) {
1698
- if (!loading) {
1699
- console.warn(
1700
- `[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`
1701
- );
1633
+ style: {
1634
+ flexDirection: "row",
1635
+ justifyContent: "space-between",
1636
+ marginBottom: 4
1637
+ },
1638
+ children: [
1639
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#374151" }, children: p.label }),
1640
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280" }, children: value })
1641
+ ]
1642
+ }
1643
+ ),
1644
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1645
+ import_react_native2.View,
1646
+ {
1647
+ style: {
1648
+ height: 6,
1649
+ backgroundColor: "#e5e7eb",
1650
+ borderRadius: 3,
1651
+ overflow: "hidden"
1652
+ },
1653
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1654
+ import_react_native2.View,
1655
+ {
1656
+ style: {
1657
+ width: `${Math.min(Math.max(progress * 100, 0), 100)}%`,
1658
+ height: "100%",
1659
+ backgroundColor: trackColor,
1660
+ borderRadius: 3
1702
1661
  }
1703
- return null;
1704
1662
  }
1705
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1706
- ElementRenderer,
1707
- {
1708
- element: childElement,
1709
- spec,
1710
- registry,
1711
- loading,
1712
- fallback
1713
- },
1714
- childKey
1715
- );
1716
- })
1717
- },
1718
- key
1719
- );
1720
- }) });
1663
+ )
1664
+ }
1665
+ )
1666
+ ] });
1721
1667
  }
1722
- function Renderer({
1723
- spec,
1724
- registry: customRegistry,
1725
- includeStandard = true,
1726
- loading,
1727
- fallback
1728
- }) {
1729
- const registry = (0, import_react6.useMemo)(
1730
- () => ({
1731
- ...includeStandard ? standardComponents : {},
1732
- ...customRegistry
1733
- }),
1734
- [customRegistry, includeStandard]
1735
- );
1736
- if (!spec || !spec.root) {
1737
- return null;
1738
- }
1739
- const rootElement = spec.elements[spec.root];
1740
- if (!rootElement) {
1741
- return null;
1742
- }
1743
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1744
- ElementRenderer,
1668
+ function SearchBarComponent({ element, emit }) {
1669
+ const p = element.props;
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1671
+ import_react_native2.View,
1745
1672
  {
1746
- element: rootElement,
1747
- spec,
1748
- registry,
1749
- loading,
1750
- fallback
1673
+ style: {
1674
+ flexDirection: "row",
1675
+ alignItems: "center",
1676
+ backgroundColor: "#f3f4f6",
1677
+ borderRadius: 8,
1678
+ paddingHorizontal: 12
1679
+ },
1680
+ children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#9ca3af", marginRight: 8 }, children: "Search" }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1683
+ import_react_native2.TextInput,
1684
+ {
1685
+ placeholder: p.placeholder ?? "Search...",
1686
+ value: p.value ?? void 0,
1687
+ returnKeyType: "search",
1688
+ onSubmitEditing: () => emit("submit"),
1689
+ style: {
1690
+ flex: 1,
1691
+ paddingVertical: 10,
1692
+ fontSize: 16,
1693
+ color: "#111827"
1694
+ }
1695
+ }
1696
+ )
1697
+ ]
1751
1698
  }
1752
1699
  );
1753
1700
  }
1754
- function JSONUIProvider({
1755
- registry,
1756
- initialState,
1757
- authState,
1758
- actionHandlers,
1759
- navigate,
1760
- validationFunctions,
1761
- onStateChange,
1762
- children
1763
- }) {
1764
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1765
- StateProvider,
1766
- {
1767
- initialState,
1768
- authState,
1769
- onStateChange,
1770
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VisibilityProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionProvider, { handlers: actionHandlers, navigate, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(ValidationProvider, { customFunctions: validationFunctions, children: [
1771
- children,
1772
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ConfirmationDialogManager, {})
1773
- ] }) }) })
1774
- }
1775
- );
1701
+ function SpinnerComponent({ element }) {
1702
+ const p = element.props;
1703
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.ActivityIndicator, { size: p.size ?? "small", color: p.color ?? "#3b82f6" });
1776
1704
  }
1777
- function ConfirmationDialogManager() {
1778
- const { pendingConfirmation, confirm, cancel } = useActions();
1779
- if (!pendingConfirmation?.action.confirm) {
1780
- return null;
1781
- }
1782
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1783
- ConfirmDialog,
1705
+ function ProgressBarComponent({ element }) {
1706
+ const p = element.props;
1707
+ const height = p.height ?? 6;
1708
+ const progress = Math.min(Math.max(p.progress, 0), 1);
1709
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1710
+ import_react_native2.View,
1784
1711
  {
1785
- confirm: pendingConfirmation.action.confirm,
1786
- onConfirm: confirm,
1787
- onCancel: cancel
1712
+ style: {
1713
+ height,
1714
+ backgroundColor: p.trackColor ?? "#e5e7eb",
1715
+ borderRadius: height / 2,
1716
+ overflow: "hidden"
1717
+ },
1718
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1719
+ import_react_native2.View,
1720
+ {
1721
+ style: {
1722
+ width: `${progress * 100}%`,
1723
+ height: "100%",
1724
+ backgroundColor: p.color ?? "#3b82f6",
1725
+ borderRadius: height / 2
1726
+ }
1727
+ }
1728
+ )
1788
1729
  }
1789
1730
  );
1790
1731
  }
1791
- function createRendererFromCatalog(_catalog, registry) {
1792
- return function CatalogRenderer(props) {
1793
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Renderer, { ...props, registry });
1794
- };
1795
- }
1796
- function defineRegistry(_catalog, options) {
1797
- const registry = {};
1798
- if (options.components) {
1799
- for (const [name, componentFn] of Object.entries(options.components)) {
1800
- registry[name] = ({
1801
- element,
1802
- children,
1803
- emit,
1804
- loading
1805
- }) => {
1806
- return componentFn({
1807
- props: element.props,
1808
- children,
1809
- emit,
1810
- loading
1811
- });
1812
- };
1813
- }
1814
- }
1815
- const actionMap = options.actions ? Object.entries(options.actions) : [];
1816
- const handlers = (getSetState, getState) => {
1817
- const result = {};
1818
- for (const [name, actionFn] of actionMap) {
1819
- result[name] = async (params) => {
1820
- const setState = getSetState();
1821
- const state = getState();
1822
- if (setState) {
1823
- await actionFn(params, setState, state);
1824
- }
1825
- };
1826
- }
1827
- return result;
1828
- };
1829
- const executeAction2 = async (actionName, params, setState, state = {}) => {
1830
- const entry = actionMap.find(([name]) => name === actionName);
1831
- if (entry) {
1832
- await entry[1](params, setState, state);
1833
- } else {
1834
- console.warn(`Unknown action: ${actionName}`);
1732
+ function CardComponent({ element, children }) {
1733
+ const p = element.props;
1734
+ const elevated = p.elevated !== false;
1735
+ const padding = p.padding ?? 16;
1736
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1737
+ import_react_native2.View,
1738
+ {
1739
+ style: [
1740
+ {
1741
+ backgroundColor: p.backgroundColor ?? "#ffffff",
1742
+ borderRadius: p.borderRadius ?? 12,
1743
+ padding
1744
+ },
1745
+ elevated && {
1746
+ shadowColor: "#000",
1747
+ shadowOffset: { width: 0, height: 2 },
1748
+ shadowOpacity: 0.1,
1749
+ shadowRadius: 8,
1750
+ elevation: 3
1751
+ }
1752
+ ],
1753
+ children: [
1754
+ p.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1755
+ import_react_native2.Text,
1756
+ {
1757
+ style: {
1758
+ fontSize: 18,
1759
+ fontWeight: "600",
1760
+ color: "#111827",
1761
+ marginBottom: p.subtitle ? 2 : 12
1762
+ },
1763
+ children: p.title
1764
+ }
1765
+ ),
1766
+ p.subtitle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1767
+ import_react_native2.Text,
1768
+ {
1769
+ style: {
1770
+ fontSize: 14,
1771
+ color: "#6b7280",
1772
+ marginBottom: 12
1773
+ },
1774
+ children: p.subtitle
1775
+ }
1776
+ ),
1777
+ children
1778
+ ]
1835
1779
  }
1836
- };
1837
- return { registry, handlers, executeAction: executeAction2 };
1780
+ );
1838
1781
  }
1839
- function createRenderer(catalog, components) {
1840
- const registry = components;
1841
- return function CatalogRenderer({
1842
- spec,
1843
- state,
1844
- onAction,
1845
- onStateChange,
1846
- loading,
1847
- authState,
1848
- fallback
1849
- }) {
1850
- const actionHandlers = onAction ? {
1851
- __default__: (params) => {
1852
- const actionName = params.__actionName__;
1853
- const actionParams = params.__actionParams__;
1854
- return onAction(actionName, actionParams);
1855
- }
1856
- } : void 0;
1857
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1858
- StateProvider,
1782
+ function ListItemComponent({ element, emit }) {
1783
+ const p = element.props;
1784
+ const hasPress = !!element.on?.press;
1785
+ const content = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1786
+ import_react_native2.View,
1787
+ {
1788
+ style: {
1789
+ flexDirection: "row",
1790
+ alignItems: "center",
1791
+ paddingVertical: 12,
1792
+ paddingHorizontal: 16
1793
+ },
1794
+ children: [
1795
+ p.leading && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1796
+ import_react_native2.Text,
1797
+ {
1798
+ style: {
1799
+ fontSize: 16,
1800
+ color: "#6b7280",
1801
+ marginRight: 12,
1802
+ width: 24,
1803
+ textAlign: "center"
1804
+ },
1805
+ children: p.leading
1806
+ }
1807
+ ),
1808
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: { flex: 1 }, children: [
1809
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#111827" }, children: p.title }),
1810
+ p.subtitle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280", marginTop: 2 }, children: p.subtitle })
1811
+ ] }),
1812
+ p.trailing && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 14, color: "#6b7280", marginLeft: 8 }, children: p.trailing }),
1813
+ p.showChevron && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: { fontSize: 16, color: "#9ca3af", marginLeft: 8 }, children: ">" })
1814
+ ]
1815
+ }
1816
+ );
1817
+ if (hasPress) {
1818
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1819
+ import_react_native2.Pressable,
1859
1820
  {
1860
- initialState: state,
1861
- authState,
1862
- onStateChange,
1863
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VisibilityProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionProvider, { handlers: actionHandlers, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(ValidationProvider, { children: [
1864
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1865
- Renderer,
1866
- {
1867
- spec,
1868
- registry,
1869
- loading,
1870
- fallback
1871
- }
1872
- ),
1873
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ConfirmationDialogManager, {})
1874
- ] }) }) })
1821
+ onPress: () => emit("press"),
1822
+ style: ({ pressed }) => ({
1823
+ opacity: pressed ? 0.7 : 1,
1824
+ backgroundColor: pressed ? "#f9fafb" : "transparent"
1825
+ }),
1826
+ children: content
1875
1827
  }
1876
1828
  );
1877
- };
1878
- }
1879
-
1880
- // src/hooks.ts
1881
- var import_react7 = require("react");
1882
- var import_core7 = require("@json-render/core");
1883
- function looksLikeJson(line) {
1884
- return line.startsWith("{") || line.startsWith("[");
1885
- }
1886
- function parsePatchLine(line) {
1887
- const trimmed = line.trim();
1888
- if (!trimmed || trimmed.startsWith("//")) {
1889
- return { patch: null, malformed: false };
1890
- }
1891
- if (!looksLikeJson(trimmed)) {
1892
- return { patch: null, malformed: false };
1893
- }
1894
- try {
1895
- return { patch: JSON.parse(trimmed), malformed: false };
1896
- } catch {
1897
1829
  }
1898
- let attempt = trimmed;
1899
- for (let i = 0; i < 3; i++) {
1900
- const last = attempt[attempt.length - 1];
1901
- if (last === "}" || last === "]") {
1902
- attempt = attempt.slice(0, -1);
1903
- try {
1904
- const result = JSON.parse(attempt);
1905
- console.warn(
1906
- `[json-render] Recovered malformed JSONL line by removing ${i + 1} trailing '${last}'`
1907
- );
1908
- return { patch: result, malformed: false };
1909
- } catch {
1910
- }
1911
- } else {
1912
- break;
1830
+ return content;
1831
+ }
1832
+ function ModalComponent({ element, children }) {
1833
+ const p = element.props;
1834
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1835
+ import_react_native2.Modal,
1836
+ {
1837
+ visible: p.visible,
1838
+ animationType: p.animationType ?? "slide",
1839
+ transparent: true,
1840
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.View, { style: modalStyles.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native2.View, { style: modalStyles.content, children: [
1841
+ p.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native2.Text, { style: modalStyles.title, children: p.title }),
1842
+ children
1843
+ ] }) })
1913
1844
  }
1914
- }
1915
- return { patch: null, malformed: true };
1845
+ );
1916
1846
  }
1917
- function setSpecValue(newSpec, path, value) {
1918
- if (path === "/root") {
1919
- newSpec.root = value;
1920
- return;
1921
- }
1922
- if (path === "/state") {
1923
- newSpec.state = value;
1924
- return;
1925
- }
1926
- if (path.startsWith("/state/")) {
1927
- if (!newSpec.state) newSpec.state = {};
1928
- const statePath = path.slice("/state".length);
1929
- (0, import_core7.setByPath)(newSpec.state, statePath, value);
1930
- return;
1931
- }
1932
- if (path.startsWith("/elements/")) {
1933
- const pathParts = path.slice("/elements/".length).split("/");
1934
- const elementKey = pathParts[0];
1935
- if (!elementKey) return;
1936
- if (pathParts.length === 1) {
1937
- newSpec.elements[elementKey] = value;
1938
- } else {
1939
- const element = newSpec.elements[elementKey];
1940
- if (element) {
1941
- const propPath = "/" + pathParts.slice(1).join("/");
1942
- const newElement = { ...element };
1943
- (0, import_core7.setByPath)(
1944
- newElement,
1945
- propPath,
1946
- value
1947
- );
1948
- newSpec.elements[elementKey] = newElement;
1847
+ function createStandardActionHandlers(options) {
1848
+ return {
1849
+ navigate: async (params) => {
1850
+ const screen = params.screen;
1851
+ const navParams = params.params;
1852
+ options?.navigate?.(screen, navParams ?? void 0);
1853
+ },
1854
+ goBack: async () => {
1855
+ options?.goBack?.();
1856
+ },
1857
+ showAlert: async (params) => {
1858
+ const title = params.title;
1859
+ const message = params.message ?? void 0;
1860
+ const buttons = params.buttons;
1861
+ const alertButtons = buttons?.map((btn) => ({
1862
+ text: btn.text,
1863
+ style: btn.style
1864
+ }));
1865
+ import_react_native2.Alert.alert(title, message, alertButtons);
1866
+ },
1867
+ share: async (params) => {
1868
+ const message = params.message;
1869
+ const url = params.url ?? void 0;
1870
+ const title = params.title ?? void 0;
1871
+ await import_react_native2.Share.share({ message, url, title });
1872
+ },
1873
+ openURL: async (params) => {
1874
+ const url = params.url;
1875
+ const canOpen = await import_react_native2.Linking.canOpenURL(url);
1876
+ if (canOpen) {
1877
+ await import_react_native2.Linking.openURL(url);
1949
1878
  }
1879
+ },
1880
+ setData: async (params) => {
1881
+ const statePath = params.statePath;
1882
+ const value = params.value;
1883
+ options?.onSetData?.(statePath, value);
1884
+ },
1885
+ refresh: async (params) => {
1886
+ const target = params.target ?? void 0;
1887
+ options?.onRefresh?.(target);
1950
1888
  }
1889
+ };
1890
+ }
1891
+ var standardComponents = {
1892
+ // Layout
1893
+ Container: ContainerComponent,
1894
+ Row: RowComponent,
1895
+ Column: ColumnComponent,
1896
+ ScrollContainer: ScrollContainerComponent,
1897
+ SafeArea: SafeAreaComponent,
1898
+ Pressable: PressableComponent,
1899
+ Spacer: SpacerComponent,
1900
+ Divider: DividerComponent,
1901
+ // Content
1902
+ Heading: HeadingComponent,
1903
+ Paragraph: ParagraphComponent,
1904
+ Label: LabelComponent,
1905
+ Image: ImageComponent,
1906
+ Avatar: AvatarComponent,
1907
+ Badge: BadgeComponent,
1908
+ Chip: ChipComponent,
1909
+ // Input
1910
+ Button: ButtonComponent,
1911
+ TextInput: TextInputComponent,
1912
+ Switch: SwitchComponent,
1913
+ Checkbox: CheckboxComponent,
1914
+ Slider: SliderComponent,
1915
+ SearchBar: SearchBarComponent,
1916
+ // Feedback
1917
+ Spinner: SpinnerComponent,
1918
+ ProgressBar: ProgressBarComponent,
1919
+ // Composite
1920
+ Card: CardComponent,
1921
+ ListItem: ListItemComponent,
1922
+ Modal: ModalComponent
1923
+ };
1924
+ var inputStyles = import_react_native2.StyleSheet.create({
1925
+ label: {
1926
+ fontSize: 14,
1927
+ fontWeight: "500",
1928
+ color: "#374151",
1929
+ marginBottom: 4
1930
+ },
1931
+ input: {
1932
+ borderWidth: 1,
1933
+ borderColor: "#d1d5db",
1934
+ borderRadius: 8,
1935
+ paddingHorizontal: 12,
1936
+ paddingVertical: 10,
1937
+ fontSize: 16,
1938
+ color: "#111827",
1939
+ backgroundColor: "#ffffff"
1951
1940
  }
1952
- }
1953
- function removeSpecValue(newSpec, path) {
1954
- if (path === "/state") {
1955
- newSpec.state = void 0;
1956
- return;
1941
+ });
1942
+ var modalStyles = import_react_native2.StyleSheet.create({
1943
+ overlay: {
1944
+ flex: 1,
1945
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
1946
+ justifyContent: "flex-end"
1947
+ },
1948
+ content: {
1949
+ backgroundColor: "#ffffff",
1950
+ borderTopLeftRadius: 16,
1951
+ borderTopRightRadius: 16,
1952
+ padding: 24,
1953
+ maxHeight: "80%"
1954
+ },
1955
+ title: {
1956
+ fontSize: 20,
1957
+ fontWeight: "600",
1958
+ color: "#111827",
1959
+ marginBottom: 16
1957
1960
  }
1958
- if (path.startsWith("/state/") && newSpec.state) {
1959
- const statePath = path.slice("/state".length);
1960
- (0, import_core7.removeByPath)(newSpec.state, statePath);
1961
- return;
1961
+ });
1962
+
1963
+ // src/renderer.tsx
1964
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1965
+ var ElementErrorBoundary = class extends import_react7.default.Component {
1966
+ constructor(props) {
1967
+ super(props);
1968
+ this.state = { hasError: false };
1962
1969
  }
1963
- if (path.startsWith("/elements/")) {
1964
- const pathParts = path.slice("/elements/".length).split("/");
1965
- const elementKey = pathParts[0];
1966
- if (!elementKey) return;
1967
- if (pathParts.length === 1) {
1968
- const { [elementKey]: _, ...rest } = newSpec.elements;
1969
- newSpec.elements = rest;
1970
- } else {
1971
- const element = newSpec.elements[elementKey];
1972
- if (element) {
1973
- const propPath = "/" + pathParts.slice(1).join("/");
1974
- const newElement = { ...element };
1975
- (0, import_core7.removeByPath)(
1976
- newElement,
1977
- propPath
1978
- );
1979
- newSpec.elements[elementKey] = newElement;
1980
- }
1981
- }
1970
+ static getDerivedStateFromError() {
1971
+ return { hasError: true };
1982
1972
  }
1983
- }
1984
- function getSpecValue(spec, path) {
1985
- if (path === "/root") return spec.root;
1986
- return (0, import_core7.getByPath)(spec, path);
1987
- }
1988
- function applyPatch(spec, patch) {
1989
- const newSpec = { ...spec, elements: { ...spec.elements } };
1990
- switch (patch.op) {
1991
- case "add":
1992
- case "replace": {
1993
- setSpecValue(newSpec, patch.path, patch.value);
1994
- break;
1995
- }
1996
- case "remove": {
1997
- removeSpecValue(newSpec, patch.path);
1998
- break;
1999
- }
2000
- case "move": {
2001
- if (!patch.from) break;
2002
- const moveValue = getSpecValue(newSpec, patch.from);
2003
- removeSpecValue(newSpec, patch.from);
2004
- setSpecValue(newSpec, patch.path, moveValue);
2005
- break;
2006
- }
2007
- case "copy": {
2008
- if (!patch.from) break;
2009
- const copyValue = getSpecValue(newSpec, patch.from);
2010
- setSpecValue(newSpec, patch.path, copyValue);
2011
- break;
2012
- }
2013
- case "test": {
2014
- break;
1973
+ componentDidCatch(error, info) {
1974
+ console.error(
1975
+ `[json-render] Rendering error in <${this.props.elementType}>:`,
1976
+ error,
1977
+ info.componentStack
1978
+ );
1979
+ }
1980
+ render() {
1981
+ if (this.state.hasError) {
1982
+ return null;
2015
1983
  }
1984
+ return this.props.children;
2016
1985
  }
2017
- return newSpec;
2018
- }
2019
- function useUIStream({
2020
- api,
2021
- onComplete,
2022
- onError,
2023
- fetch: fetchFn = globalThis.fetch,
2024
- validate: enableValidation = false,
2025
- maxRetries = 5
1986
+ };
1987
+ var ElementRenderer = import_react7.default.memo(function ElementRenderer2({
1988
+ element,
1989
+ spec,
1990
+ registry,
1991
+ loading,
1992
+ fallback
2026
1993
  }) {
2027
- const [spec, setSpec] = (0, import_react7.useState)(null);
2028
- const [isStreaming, setIsStreaming] = (0, import_react7.useState)(false);
2029
- const [error, setError] = (0, import_react7.useState)(null);
2030
- const [rawLines, setRawLines] = (0, import_react7.useState)([]);
2031
- const abortControllerRef = (0, import_react7.useRef)(null);
2032
- const stop = (0, import_react7.useCallback)(() => {
2033
- abortControllerRef.current?.abort();
2034
- abortControllerRef.current = null;
2035
- setIsStreaming(false);
2036
- }, []);
2037
- const clear = (0, import_react7.useCallback)(() => {
2038
- setSpec(null);
2039
- setError(null);
2040
- setRawLines([]);
2041
- }, []);
2042
- const streamRequest = (0, import_react7.useCallback)(
2043
- async (prompt, context, initialSpec, abortOnMalformed) => {
2044
- let currentSpec = initialSpec;
2045
- setSpec(currentSpec);
2046
- const malformedLines = [];
2047
- const response = await fetchFn(api, {
2048
- method: "POST",
2049
- headers: { "Content-Type": "application/json" },
2050
- body: JSON.stringify({
2051
- prompt,
2052
- context,
2053
- currentSpec
2054
- }),
2055
- signal: abortControllerRef.current.signal
2056
- });
2057
- console.log("[useUIStream] response status:", response.status);
2058
- if (!response.ok) {
2059
- let errorMessage = `HTTP error: ${response.status}`;
2060
- try {
2061
- const errorData = await response.json();
2062
- if (errorData.message) {
2063
- errorMessage = errorData.message;
2064
- } else if (errorData.error) {
2065
- errorMessage = errorData.error;
2066
- }
2067
- } catch {
2068
- }
2069
- throw new Error(errorMessage);
2070
- }
2071
- const reader = response.body?.getReader();
2072
- if (!reader) {
2073
- throw new Error("No response body");
2074
- }
2075
- const decoder = new TextDecoder();
2076
- let buffer = "";
2077
- let chunkCount = 0;
2078
- let totalBytes = 0;
2079
- let aborted = false;
2080
- while (true) {
2081
- const { done, value } = await reader.read();
2082
- if (done) {
2083
- console.log(
2084
- "[useUIStream] stream done. Total chunks:",
2085
- chunkCount,
2086
- "bytes:",
2087
- totalBytes
2088
- );
2089
- break;
2090
- }
2091
- chunkCount++;
2092
- totalBytes += value?.byteLength ?? 0;
2093
- const decoded = decoder.decode(value, { stream: true });
2094
- buffer += decoded;
2095
- if (chunkCount <= 5) {
2096
- console.log(
2097
- `[useUIStream] chunk #${chunkCount} (${value?.byteLength ?? 0}b):`,
2098
- decoded.slice(0, 200)
2099
- );
2100
- }
2101
- const lines = buffer.split("\n");
2102
- buffer = lines.pop() ?? "";
2103
- const newLines = [];
2104
- for (const line of lines) {
2105
- if (line.trim()) {
2106
- newLines.push(line);
2107
- }
2108
- const { patch, malformed } = parsePatchLine(line);
2109
- if (patch) {
2110
- console.log("[useUIStream] applied patch:", patch.op, patch.path);
2111
- currentSpec = applyPatch(currentSpec, patch);
2112
- setSpec({ ...currentSpec });
2113
- } else if (malformed) {
2114
- malformedLines.push(line.trim());
2115
- console.warn(
2116
- "[useUIStream] malformed JSON line:",
2117
- line.trim().slice(0, 200)
2118
- );
2119
- if (abortOnMalformed) {
2120
- console.warn(
2121
- "[useUIStream] aborting stream due to malformed JSON, will retry"
2122
- );
2123
- await reader.cancel();
2124
- aborted = true;
2125
- break;
2126
- }
2127
- }
2128
- }
2129
- if (aborted) break;
2130
- if (newLines.length > 0) {
2131
- setRawLines((prev) => [...prev, ...newLines]);
1994
+ const repeatScope = useRepeatScope();
1995
+ const { ctx } = useVisibility();
1996
+ const { execute } = useActions();
1997
+ const fullCtx = (0, import_react7.useMemo)(
1998
+ () => repeatScope ? {
1999
+ ...ctx,
2000
+ repeatItem: repeatScope.item,
2001
+ repeatIndex: repeatScope.index,
2002
+ repeatBasePath: repeatScope.basePath
2003
+ } : ctx,
2004
+ [ctx, repeatScope]
2005
+ );
2006
+ const isVisible = element.visible === void 0 ? true : (0, import_core7.evaluateVisibility)(element.visible, fullCtx);
2007
+ const onBindings = element.on;
2008
+ const emit = (0, import_react7.useCallback)(
2009
+ (eventName) => {
2010
+ const binding = onBindings?.[eventName];
2011
+ if (!binding) return;
2012
+ const actionBindings = Array.isArray(binding) ? binding : [binding];
2013
+ for (const b of actionBindings) {
2014
+ if (!b.params) {
2015
+ execute(b);
2016
+ continue;
2132
2017
  }
2133
- }
2134
- if (!aborted && buffer.trim()) {
2135
- console.log("[useUIStream] remaining buffer:", buffer.slice(0, 200));
2136
- setRawLines((prev) => [...prev, buffer]);
2137
- const { patch, malformed } = parsePatchLine(buffer);
2138
- if (patch) {
2139
- currentSpec = applyPatch(currentSpec, patch);
2140
- setSpec({ ...currentSpec });
2141
- } else if (malformed) {
2142
- malformedLines.push(buffer.trim());
2018
+ const resolved = {};
2019
+ for (const [key, val] of Object.entries(b.params)) {
2020
+ resolved[key] = (0, import_core7.resolveActionParam)(val, fullCtx);
2143
2021
  }
2022
+ execute({ ...b, params: resolved });
2144
2023
  }
2145
- console.log(
2146
- "[useUIStream] spec state - root:",
2147
- currentSpec.root,
2148
- "elements:",
2149
- Object.keys(currentSpec.elements).length,
2150
- "completed:",
2151
- !aborted,
2152
- "malformed:",
2153
- malformedLines.length
2154
- );
2155
- return { spec: currentSpec, completed: !aborted, malformedLines };
2156
2024
  },
2157
- [api, fetchFn]
2025
+ [onBindings, execute, fullCtx]
2158
2026
  );
2159
- const send = (0, import_react7.useCallback)(
2160
- async (prompt, context) => {
2161
- abortControllerRef.current?.abort();
2162
- abortControllerRef.current = new AbortController();
2163
- setIsStreaming(true);
2164
- setError(null);
2165
- setRawLines([]);
2166
- const previousSpec = context?.previousSpec;
2167
- let currentSpec = previousSpec && previousSpec.root ? { ...previousSpec, elements: { ...previousSpec.elements } } : { root: "", elements: {} };
2168
- let retriesUsed = 0;
2169
- let currentPrompt = prompt;
2170
- let currentContext = context;
2171
- try {
2172
- while (retriesUsed <= maxRetries) {
2173
- const result = await streamRequest(
2174
- currentPrompt,
2175
- currentContext,
2176
- currentSpec,
2177
- enableValidation
2178
- // only abort on malformed when validation is on
2179
- );
2180
- currentSpec = result.spec;
2181
- if (!result.completed && result.malformedLines.length > 0) {
2182
- if (retriesUsed >= maxRetries) {
2027
+ if (!isVisible) {
2028
+ return null;
2029
+ }
2030
+ const rawProps = element.props;
2031
+ const elementBindings = (0, import_core7.resolveBindings)(rawProps, fullCtx);
2032
+ const resolvedProps = (0, import_core7.resolveElementProps)(rawProps, fullCtx);
2033
+ const resolvedElement = resolvedProps !== element.props ? { ...element, props: resolvedProps } : element;
2034
+ const Component = registry[resolvedElement.type] ?? fallback;
2035
+ if (!Component) {
2036
+ console.warn(`No renderer for component type: ${resolvedElement.type}`);
2037
+ return null;
2038
+ }
2039
+ const children = resolvedElement.repeat ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2040
+ RepeatChildren,
2041
+ {
2042
+ element: resolvedElement,
2043
+ spec,
2044
+ registry,
2045
+ loading,
2046
+ fallback
2047
+ }
2048
+ ) : resolvedElement.children?.map((childKey) => {
2049
+ const childElement = spec.elements[childKey];
2050
+ if (!childElement) {
2051
+ if (!loading) {
2052
+ console.warn(
2053
+ `[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`
2054
+ );
2055
+ }
2056
+ return null;
2057
+ }
2058
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2059
+ ElementRenderer2,
2060
+ {
2061
+ element: childElement,
2062
+ spec,
2063
+ registry,
2064
+ loading,
2065
+ fallback
2066
+ },
2067
+ childKey
2068
+ );
2069
+ });
2070
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ElementErrorBoundary, { elementType: resolvedElement.type, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2071
+ Component,
2072
+ {
2073
+ element: resolvedElement,
2074
+ emit,
2075
+ bindings: elementBindings,
2076
+ loading,
2077
+ children
2078
+ }
2079
+ ) });
2080
+ });
2081
+ function RepeatChildren({
2082
+ element,
2083
+ spec,
2084
+ registry,
2085
+ loading,
2086
+ fallback
2087
+ }) {
2088
+ const { state } = useStateStore();
2089
+ const repeat = element.repeat;
2090
+ const statePath = repeat.statePath;
2091
+ const items = (0, import_core7.getByPath)(state, statePath) ?? [];
2092
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: items.map((itemValue, index) => {
2093
+ const key = repeat.key && typeof itemValue === "object" && itemValue !== null ? String(
2094
+ itemValue[repeat.key] ?? index
2095
+ ) : String(index);
2096
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2097
+ RepeatScopeProvider,
2098
+ {
2099
+ item: itemValue,
2100
+ index,
2101
+ basePath: `${statePath}/${index}`,
2102
+ children: element.children?.map((childKey) => {
2103
+ const childElement = spec.elements[childKey];
2104
+ if (!childElement) {
2105
+ if (!loading) {
2183
2106
  console.warn(
2184
- "[useUIStream] malformed lines but max retries exhausted"
2107
+ `[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`
2185
2108
  );
2186
- break;
2187
2109
  }
2188
- retriesUsed++;
2189
- console.warn(
2190
- `[useUIStream] mid-stream retry ${retriesUsed}/${maxRetries} due to malformed JSON`
2191
- );
2192
- currentContext = { ...context, previousSpec: currentSpec };
2193
- currentPrompt = `The previous generation contained malformed JSON that could not be parsed. The line was:
2194
- ${result.malformedLines[result.malformedLines.length - 1]?.slice(0, 500)}
2195
-
2196
- The current spec state is provided. Continue generating from where you left off. Output ONLY the remaining patches needed to complete the UI.`;
2197
- continue;
2198
- }
2199
- if (!enableValidation || !currentSpec.root) {
2200
- break;
2201
- }
2202
- const { spec: fixedSpec, fixes } = (0, import_core7.autoFixSpec)(currentSpec);
2203
- if (fixes.length > 0) {
2204
- console.log("[useUIStream] auto-fixed spec issues:", fixes);
2205
- currentSpec = fixedSpec;
2206
- setSpec({ ...currentSpec });
2207
- }
2208
- const validation = (0, import_core7.validateSpec)(currentSpec);
2209
- if (validation.valid) {
2210
- console.log("[useUIStream] spec validation passed");
2211
- break;
2212
- }
2213
- const errors = validation.issues.filter(
2214
- (i) => i.severity === "error"
2215
- );
2216
- if (retriesUsed >= maxRetries) {
2217
- console.warn(
2218
- "[useUIStream] spec validation failed, max retries exhausted:",
2219
- errors.map((e) => e.message)
2220
- );
2221
- break;
2110
+ return null;
2222
2111
  }
2223
- retriesUsed++;
2224
- const issueText = (0, import_core7.formatSpecIssues)(validation.issues);
2225
- console.warn(
2226
- `[useUIStream] post-stream retry ${retriesUsed}/${maxRetries}:`,
2227
- errors.map((e) => e.message)
2112
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2113
+ ElementRenderer,
2114
+ {
2115
+ element: childElement,
2116
+ spec,
2117
+ registry,
2118
+ loading,
2119
+ fallback
2120
+ },
2121
+ childKey
2228
2122
  );
2229
- currentContext = { ...context, previousSpec: currentSpec };
2230
- currentPrompt = `FIX THE FOLLOWING ERRORS in the current UI spec. Output ONLY the patches needed to fix these issues, do not recreate the entire UI.
2231
-
2232
- ` + issueText;
2233
- }
2234
- onComplete?.(currentSpec);
2235
- } catch (err) {
2236
- if (err.name === "AbortError") {
2237
- return;
2238
- }
2239
- const error2 = err instanceof Error ? err : new Error(String(err));
2240
- setError(error2);
2241
- onError?.(error2);
2242
- } finally {
2243
- setIsStreaming(false);
2244
- }
2245
- },
2246
- [
2247
- api,
2248
- fetchFn,
2249
- onComplete,
2250
- onError,
2251
- enableValidation,
2252
- maxRetries,
2253
- streamRequest
2254
- ]
2123
+ })
2124
+ },
2125
+ key
2126
+ );
2127
+ }) });
2128
+ }
2129
+ function Renderer({
2130
+ spec,
2131
+ registry: customRegistry,
2132
+ includeStandard = true,
2133
+ loading,
2134
+ fallback
2135
+ }) {
2136
+ const registry = (0, import_react7.useMemo)(
2137
+ () => ({
2138
+ ...includeStandard ? standardComponents : {},
2139
+ ...customRegistry
2140
+ }),
2141
+ [customRegistry, includeStandard]
2142
+ );
2143
+ if (!spec || !spec.root) {
2144
+ return null;
2145
+ }
2146
+ const rootElement = spec.elements[spec.root];
2147
+ if (!rootElement) {
2148
+ return null;
2149
+ }
2150
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2151
+ ElementRenderer,
2152
+ {
2153
+ element: rootElement,
2154
+ spec,
2155
+ registry,
2156
+ loading,
2157
+ fallback
2158
+ }
2255
2159
  );
2256
- (0, import_react7.useEffect)(() => {
2257
- return () => {
2258
- abortControllerRef.current?.abort();
2259
- };
2260
- }, []);
2261
- return {
2262
- spec,
2263
- isStreaming,
2264
- error,
2265
- rawLines,
2266
- send,
2267
- stop,
2268
- clear
2269
- };
2270
2160
  }
2271
- function flatToTree(elements) {
2272
- const elementMap = {};
2273
- let root = "";
2274
- for (const element of elements) {
2275
- elementMap[element.key] = {
2276
- type: element.type,
2277
- props: element.props,
2278
- children: [],
2279
- visible: element.visible
2280
- };
2161
+ function JSONUIProvider({
2162
+ registry,
2163
+ initialState,
2164
+ handlers,
2165
+ navigate,
2166
+ validationFunctions,
2167
+ onStateChange,
2168
+ children
2169
+ }) {
2170
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StateProvider, { initialState, onStateChange, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VisibilityProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionProvider, { handlers, navigate, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(ValidationProvider, { customFunctions: validationFunctions, children: [
2171
+ children,
2172
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ConfirmationDialogManager, {})
2173
+ ] }) }) }) });
2174
+ }
2175
+ function ConfirmationDialogManager() {
2176
+ const { pendingConfirmation, confirm, cancel } = useActions();
2177
+ if (!pendingConfirmation?.action.confirm) {
2178
+ return null;
2281
2179
  }
2282
- for (const element of elements) {
2283
- if (element.parentKey) {
2284
- const parent = elementMap[element.parentKey];
2285
- if (parent) {
2286
- if (!parent.children) {
2287
- parent.children = [];
2180
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2181
+ ConfirmDialog,
2182
+ {
2183
+ confirm: pendingConfirmation.action.confirm,
2184
+ onConfirm: confirm,
2185
+ onCancel: cancel
2186
+ }
2187
+ );
2188
+ }
2189
+ function defineRegistry(_catalog, options) {
2190
+ const registry = {};
2191
+ if (options.components) {
2192
+ for (const [name, componentFn] of Object.entries(options.components)) {
2193
+ registry[name] = ({
2194
+ element,
2195
+ children,
2196
+ emit,
2197
+ bindings,
2198
+ loading
2199
+ }) => {
2200
+ return componentFn({
2201
+ props: element.props,
2202
+ children,
2203
+ emit,
2204
+ bindings,
2205
+ loading
2206
+ });
2207
+ };
2208
+ }
2209
+ }
2210
+ const actionMap = options.actions ? Object.entries(options.actions) : [];
2211
+ const handlers = (getSetState, getState) => {
2212
+ const result = {};
2213
+ for (const [name, actionFn] of actionMap) {
2214
+ result[name] = async (params) => {
2215
+ const setState = getSetState();
2216
+ const state = getState();
2217
+ if (setState) {
2218
+ await actionFn(params, setState, state);
2288
2219
  }
2289
- parent.children.push(element.key);
2290
- }
2220
+ };
2221
+ }
2222
+ return result;
2223
+ };
2224
+ const executeAction2 = async (actionName, params, setState, state = {}) => {
2225
+ const entry = actionMap.find(([name]) => name === actionName);
2226
+ if (entry) {
2227
+ await entry[1](params, setState, state);
2291
2228
  } else {
2292
- root = element.key;
2229
+ console.warn(`Unknown action: ${actionName}`);
2293
2230
  }
2294
- }
2295
- return { root, elements: elementMap };
2231
+ };
2232
+ return { registry, handlers, executeAction: executeAction2 };
2233
+ }
2234
+ function createRenderer(catalog, components) {
2235
+ const registry = components;
2236
+ return function CatalogRenderer({
2237
+ spec,
2238
+ state,
2239
+ onAction,
2240
+ onStateChange,
2241
+ loading,
2242
+ fallback
2243
+ }) {
2244
+ const actionHandlers = onAction ? new Proxy(
2245
+ {},
2246
+ {
2247
+ get: (_target, prop) => {
2248
+ return (params) => onAction(prop, params);
2249
+ },
2250
+ has: () => true
2251
+ }
2252
+ ) : void 0;
2253
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StateProvider, { initialState: state, onStateChange, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VisibilityProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionProvider, { handlers: actionHandlers, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(ValidationProvider, { children: [
2254
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2255
+ Renderer,
2256
+ {
2257
+ spec,
2258
+ registry,
2259
+ loading,
2260
+ fallback
2261
+ }
2262
+ ),
2263
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ConfirmationDialogManager, {})
2264
+ ] }) }) }) });
2265
+ };
2296
2266
  }
2297
2267
 
2298
2268
  // src/catalog.ts
@@ -2500,7 +2470,6 @@ var standardComponentDefinitions = {
2500
2470
  props: import_zod.z.object({
2501
2471
  placeholder: import_zod.z.string().nullable(),
2502
2472
  value: import_zod.z.string().nullable(),
2503
- statePath: import_zod.z.string().nullable(),
2504
2473
  secureTextEntry: import_zod.z.boolean().nullable(),
2505
2474
  keyboardType: import_zod.z.enum(["default", "email-address", "numeric", "phone-pad", "url"]).nullable(),
2506
2475
  multiline: import_zod.z.boolean().nullable(),
@@ -2510,34 +2479,31 @@ var standardComponentDefinitions = {
2510
2479
  }),
2511
2480
  events: ["submit", "focus", "blur"],
2512
2481
  slots: [],
2513
- description: "Text input field. Use statePath to bind to the state model for two-way binding. The value typed by the user is stored at the statePath.",
2482
+ description: "Text input field. Use $bindState to bind to the state model for two-way binding. The value typed by the user is stored at the bound path.",
2514
2483
  example: {
2515
2484
  placeholder: "Enter text...",
2516
- statePath: "/inputValue",
2517
2485
  label: "Name"
2518
2486
  }
2519
2487
  },
2520
2488
  Switch: {
2521
2489
  props: import_zod.z.object({
2522
- value: import_zod.z.boolean().nullable(),
2523
- statePath: import_zod.z.string().nullable(),
2490
+ checked: import_zod.z.boolean().nullable(),
2524
2491
  label: import_zod.z.string().nullable(),
2525
2492
  disabled: import_zod.z.boolean().nullable()
2526
2493
  }),
2527
2494
  events: ["change"],
2528
2495
  slots: [],
2529
- description: "Toggle switch. Use statePath to bind to the state model."
2496
+ description: "Toggle switch. Use $bindState to bind to the state model."
2530
2497
  },
2531
2498
  Checkbox: {
2532
2499
  props: import_zod.z.object({
2533
2500
  checked: import_zod.z.boolean().nullable(),
2534
- statePath: import_zod.z.string().nullable(),
2535
2501
  label: import_zod.z.string().nullable(),
2536
2502
  disabled: import_zod.z.boolean().nullable()
2537
2503
  }),
2538
2504
  events: ["change"],
2539
2505
  slots: [],
2540
- description: "Checkbox for boolean selections. Use statePath to bind to the state model."
2506
+ description: "Checkbox for boolean selections. Use $bindState to bind to the state model."
2541
2507
  },
2542
2508
  Slider: {
2543
2509
  props: import_zod.z.object({
@@ -2545,18 +2511,16 @@ var standardComponentDefinitions = {
2545
2511
  max: import_zod.z.number().nullable(),
2546
2512
  step: import_zod.z.number().nullable(),
2547
2513
  value: import_zod.z.number().nullable(),
2548
- statePath: import_zod.z.string().nullable(),
2549
2514
  label: import_zod.z.string().nullable(),
2550
2515
  color: import_zod.z.string().nullable()
2551
2516
  }),
2552
2517
  slots: [],
2553
- description: "Range slider for numeric values. Use statePath to bind to the state model."
2518
+ description: "Range slider for numeric values. Use $bindState to bind to the state model."
2554
2519
  },
2555
2520
  SearchBar: {
2556
2521
  props: import_zod.z.object({
2557
2522
  placeholder: import_zod.z.string().nullable(),
2558
- value: import_zod.z.string().nullable(),
2559
- statePath: import_zod.z.string().nullable()
2523
+ value: import_zod.z.string().nullable()
2560
2524
  }),
2561
2525
  events: ["submit"],
2562
2526
  slots: [],
@@ -2620,11 +2584,10 @@ var standardComponentDefinitions = {
2620
2584
  props: import_zod.z.object({
2621
2585
  visible: import_zod.z.boolean(),
2622
2586
  title: import_zod.z.string().nullable(),
2623
- animationType: import_zod.z.enum(["slide", "fade", "none"]).nullable(),
2624
- statePath: import_zod.z.string().nullable()
2587
+ animationType: import_zod.z.enum(["slide", "fade", "none"]).nullable()
2625
2588
  }),
2626
2589
  slots: ["default"],
2627
- description: "Modal overlay dialog. Use statePath to bind visibility to the state model."
2590
+ description: "Modal overlay dialog. Use $bindState on visible to bind visibility to the state model."
2628
2591
  }
2629
2592
  };
2630
2593
  var standardActionDefinitions = {
@@ -2669,22 +2632,22 @@ var standardActionDefinitions = {
2669
2632
  },
2670
2633
  setState: {
2671
2634
  params: import_zod.z.object({
2672
- path: import_zod.z.string(),
2635
+ statePath: import_zod.z.string(),
2673
2636
  value: import_zod.z.unknown()
2674
2637
  }),
2675
- description: "Update a value in the state model at the given path."
2638
+ description: "Update a value in the state model at the given statePath."
2676
2639
  },
2677
2640
  pushState: {
2678
2641
  params: import_zod.z.object({
2679
- path: import_zod.z.string(),
2642
+ statePath: import_zod.z.string(),
2680
2643
  value: import_zod.z.unknown(),
2681
- clearPath: import_zod.z.string().optional()
2644
+ clearStatePath: import_zod.z.string().optional()
2682
2645
  }),
2683
- description: 'Append an item to an array in the state model. The value can contain { path: "/statePath" } references to read from current state, and "$id" to auto-generate a unique ID. Use clearPath to reset another path after pushing (e.g. clear an input field). Example: { path: "/todos", value: { id: "$id", title: { path: "/newTodoText" }, completed: false }, clearPath: "/newTodoText" }.'
2646
+ description: 'Append an item to an array in the state model. The value can contain { $state: "/statePath" } references to read from current state, and "$id" to auto-generate a unique ID. Use clearStatePath to reset another path after pushing (e.g. clear an input field). Example: { statePath: "/todos", value: { id: "$id", title: { $state: "/newTodoText" }, completed: false }, clearStatePath: "/newTodoText" }.'
2684
2647
  },
2685
2648
  removeState: {
2686
2649
  params: import_zod.z.object({
2687
- path: import_zod.z.string(),
2650
+ statePath: import_zod.z.string(),
2688
2651
  index: import_zod.z.number()
2689
2652
  }),
2690
2653
  description: "Remove an item from an array in the state model at the given index."
@@ -2707,18 +2670,17 @@ var standardActionDefinitions = {
2707
2670
  ValidationProvider,
2708
2671
  VisibilityProvider,
2709
2672
  createRenderer,
2710
- createRendererFromCatalog,
2711
2673
  createStandardActionHandlers,
2712
2674
  defineRegistry,
2713
2675
  elementTreeSchema,
2714
2676
  flatToTree,
2715
- rewriteRepeatTokens,
2716
2677
  schema,
2717
2678
  standardActionDefinitions,
2718
2679
  standardComponentDefinitions,
2719
2680
  standardComponents,
2720
2681
  useAction,
2721
2682
  useActions,
2683
+ useBoundProp,
2722
2684
  useFieldValidation,
2723
2685
  useIsVisible,
2724
2686
  useRepeatScope,