@json-render/react 0.0.1

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.mjs ADDED
@@ -0,0 +1,769 @@
1
+ // src/contexts/data.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useCallback,
7
+ useMemo
8
+ } from "react";
9
+ import {
10
+ getByPath,
11
+ setByPath
12
+ } from "@json-render/core";
13
+ import { jsx } from "react/jsx-runtime";
14
+ var DataContext = createContext(null);
15
+ function DataProvider({
16
+ initialData = {},
17
+ authState,
18
+ onDataChange,
19
+ children
20
+ }) {
21
+ const [data, setData] = useState(initialData);
22
+ const get = useCallback(
23
+ (path) => getByPath(data, path),
24
+ [data]
25
+ );
26
+ const set = useCallback(
27
+ (path, value2) => {
28
+ setData((prev) => {
29
+ const next = { ...prev };
30
+ setByPath(next, path, value2);
31
+ return next;
32
+ });
33
+ onDataChange?.(path, value2);
34
+ },
35
+ [onDataChange]
36
+ );
37
+ const update = useCallback(
38
+ (updates) => {
39
+ setData((prev) => {
40
+ const next = { ...prev };
41
+ for (const [path, value2] of Object.entries(updates)) {
42
+ setByPath(next, path, value2);
43
+ onDataChange?.(path, value2);
44
+ }
45
+ return next;
46
+ });
47
+ },
48
+ [onDataChange]
49
+ );
50
+ const value = useMemo(
51
+ () => ({
52
+ data,
53
+ authState,
54
+ get,
55
+ set,
56
+ update
57
+ }),
58
+ [data, authState, get, set, update]
59
+ );
60
+ return /* @__PURE__ */ jsx(DataContext.Provider, { value, children });
61
+ }
62
+ function useData() {
63
+ const ctx = useContext(DataContext);
64
+ if (!ctx) {
65
+ throw new Error("useData must be used within a DataProvider");
66
+ }
67
+ return ctx;
68
+ }
69
+ function useDataValue(path) {
70
+ const { get } = useData();
71
+ return get(path);
72
+ }
73
+ function useDataBinding(path) {
74
+ const { get, set } = useData();
75
+ const value = get(path);
76
+ const setValue = useCallback(
77
+ (newValue) => set(path, newValue),
78
+ [path, set]
79
+ );
80
+ return [value, setValue];
81
+ }
82
+
83
+ // src/contexts/visibility.tsx
84
+ import { createContext as createContext2, useContext as useContext2, useMemo as useMemo2 } from "react";
85
+ import {
86
+ evaluateVisibility
87
+ } from "@json-render/core";
88
+ import { jsx as jsx2 } from "react/jsx-runtime";
89
+ var VisibilityContext = createContext2(null);
90
+ function VisibilityProvider({ children }) {
91
+ const { data, authState } = useData();
92
+ const ctx = useMemo2(
93
+ () => ({
94
+ dataModel: data,
95
+ authState
96
+ }),
97
+ [data, authState]
98
+ );
99
+ const isVisible = useMemo2(
100
+ () => (condition) => evaluateVisibility(condition, ctx),
101
+ [ctx]
102
+ );
103
+ const value = useMemo2(
104
+ () => ({ isVisible, ctx }),
105
+ [isVisible, ctx]
106
+ );
107
+ return /* @__PURE__ */ jsx2(VisibilityContext.Provider, { value, children });
108
+ }
109
+ function useVisibility() {
110
+ const ctx = useContext2(VisibilityContext);
111
+ if (!ctx) {
112
+ throw new Error("useVisibility must be used within a VisibilityProvider");
113
+ }
114
+ return ctx;
115
+ }
116
+ function useIsVisible(condition) {
117
+ const { isVisible } = useVisibility();
118
+ return isVisible(condition);
119
+ }
120
+
121
+ // src/contexts/actions.tsx
122
+ import {
123
+ createContext as createContext3,
124
+ useContext as useContext3,
125
+ useState as useState2,
126
+ useCallback as useCallback2,
127
+ useMemo as useMemo3
128
+ } from "react";
129
+ import {
130
+ resolveAction,
131
+ executeAction
132
+ } from "@json-render/core";
133
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
134
+ var ActionContext = createContext3(null);
135
+ function ActionProvider({
136
+ handlers: initialHandlers = {},
137
+ navigate,
138
+ children
139
+ }) {
140
+ const { data, set } = useData();
141
+ const [handlers, setHandlers] = useState2(initialHandlers);
142
+ const [loadingActions, setLoadingActions] = useState2(/* @__PURE__ */ new Set());
143
+ const [pendingConfirmation, setPendingConfirmation] = useState2(null);
144
+ const registerHandler = useCallback2((name, handler) => {
145
+ setHandlers((prev) => ({ ...prev, [name]: handler }));
146
+ }, []);
147
+ const execute = useCallback2(
148
+ async (action) => {
149
+ const resolved = resolveAction(action, data);
150
+ const handler = handlers[resolved.name];
151
+ if (!handler) {
152
+ console.warn(`No handler registered for action: ${resolved.name}`);
153
+ return;
154
+ }
155
+ if (resolved.confirm) {
156
+ return new Promise((resolve, reject) => {
157
+ setPendingConfirmation({
158
+ action: resolved,
159
+ handler,
160
+ resolve: () => {
161
+ setPendingConfirmation(null);
162
+ resolve();
163
+ },
164
+ reject: () => {
165
+ setPendingConfirmation(null);
166
+ reject(new Error("Action cancelled"));
167
+ }
168
+ });
169
+ }).then(async () => {
170
+ setLoadingActions((prev) => new Set(prev).add(resolved.name));
171
+ try {
172
+ await executeAction({
173
+ action: resolved,
174
+ handler,
175
+ setData: set,
176
+ navigate,
177
+ executeAction: async (name) => {
178
+ const subAction = { name };
179
+ await execute(subAction);
180
+ }
181
+ });
182
+ } finally {
183
+ setLoadingActions((prev) => {
184
+ const next = new Set(prev);
185
+ next.delete(resolved.name);
186
+ return next;
187
+ });
188
+ }
189
+ });
190
+ }
191
+ setLoadingActions((prev) => new Set(prev).add(resolved.name));
192
+ try {
193
+ await executeAction({
194
+ action: resolved,
195
+ handler,
196
+ setData: set,
197
+ navigate,
198
+ executeAction: async (name) => {
199
+ const subAction = { name };
200
+ await execute(subAction);
201
+ }
202
+ });
203
+ } finally {
204
+ setLoadingActions((prev) => {
205
+ const next = new Set(prev);
206
+ next.delete(resolved.name);
207
+ return next;
208
+ });
209
+ }
210
+ },
211
+ [data, handlers, set, navigate]
212
+ );
213
+ const confirm = useCallback2(() => {
214
+ pendingConfirmation?.resolve();
215
+ }, [pendingConfirmation]);
216
+ const cancel = useCallback2(() => {
217
+ pendingConfirmation?.reject();
218
+ }, [pendingConfirmation]);
219
+ const value = useMemo3(
220
+ () => ({
221
+ handlers,
222
+ loadingActions,
223
+ pendingConfirmation,
224
+ execute,
225
+ confirm,
226
+ cancel,
227
+ registerHandler
228
+ }),
229
+ [handlers, loadingActions, pendingConfirmation, execute, confirm, cancel, registerHandler]
230
+ );
231
+ return /* @__PURE__ */ jsx3(ActionContext.Provider, { value, children });
232
+ }
233
+ function useActions() {
234
+ const ctx = useContext3(ActionContext);
235
+ if (!ctx) {
236
+ throw new Error("useActions must be used within an ActionProvider");
237
+ }
238
+ return ctx;
239
+ }
240
+ function useAction(action) {
241
+ const { execute, loadingActions } = useActions();
242
+ const isLoading = loadingActions.has(action.name);
243
+ const executeAction2 = useCallback2(() => execute(action), [execute, action]);
244
+ return { execute: executeAction2, isLoading };
245
+ }
246
+ function ConfirmDialog({ confirm, onConfirm, onCancel }) {
247
+ const isDanger = confirm.variant === "danger";
248
+ return /* @__PURE__ */ jsx3(
249
+ "div",
250
+ {
251
+ style: {
252
+ position: "fixed",
253
+ inset: 0,
254
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
255
+ display: "flex",
256
+ alignItems: "center",
257
+ justifyContent: "center",
258
+ zIndex: 50
259
+ },
260
+ onClick: onCancel,
261
+ children: /* @__PURE__ */ jsxs(
262
+ "div",
263
+ {
264
+ style: {
265
+ backgroundColor: "white",
266
+ borderRadius: "8px",
267
+ padding: "24px",
268
+ maxWidth: "400px",
269
+ width: "100%",
270
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)"
271
+ },
272
+ onClick: (e) => e.stopPropagation(),
273
+ children: [
274
+ /* @__PURE__ */ jsx3(
275
+ "h3",
276
+ {
277
+ style: {
278
+ margin: "0 0 8px 0",
279
+ fontSize: "18px",
280
+ fontWeight: 600
281
+ },
282
+ children: confirm.title
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsx3(
286
+ "p",
287
+ {
288
+ style: {
289
+ margin: "0 0 24px 0",
290
+ color: "#6b7280"
291
+ },
292
+ children: confirm.message
293
+ }
294
+ ),
295
+ /* @__PURE__ */ jsxs(
296
+ "div",
297
+ {
298
+ style: {
299
+ display: "flex",
300
+ gap: "12px",
301
+ justifyContent: "flex-end"
302
+ },
303
+ children: [
304
+ /* @__PURE__ */ jsx3(
305
+ "button",
306
+ {
307
+ onClick: onCancel,
308
+ style: {
309
+ padding: "8px 16px",
310
+ borderRadius: "6px",
311
+ border: "1px solid #d1d5db",
312
+ backgroundColor: "white",
313
+ cursor: "pointer"
314
+ },
315
+ children: confirm.cancelLabel ?? "Cancel"
316
+ }
317
+ ),
318
+ /* @__PURE__ */ jsx3(
319
+ "button",
320
+ {
321
+ onClick: onConfirm,
322
+ style: {
323
+ padding: "8px 16px",
324
+ borderRadius: "6px",
325
+ border: "none",
326
+ backgroundColor: isDanger ? "#dc2626" : "#3b82f6",
327
+ color: "white",
328
+ cursor: "pointer"
329
+ },
330
+ children: confirm.confirmLabel ?? "Confirm"
331
+ }
332
+ )
333
+ ]
334
+ }
335
+ )
336
+ ]
337
+ }
338
+ )
339
+ }
340
+ );
341
+ }
342
+
343
+ // src/contexts/validation.tsx
344
+ import React4, {
345
+ createContext as createContext4,
346
+ useContext as useContext4,
347
+ useState as useState3,
348
+ useCallback as useCallback3,
349
+ useMemo as useMemo4
350
+ } from "react";
351
+ import {
352
+ runValidation
353
+ } from "@json-render/core";
354
+ import { jsx as jsx4 } from "react/jsx-runtime";
355
+ var ValidationContext = createContext4(null);
356
+ function ValidationProvider({
357
+ customFunctions = {},
358
+ children
359
+ }) {
360
+ const { data, authState } = useData();
361
+ const [fieldStates, setFieldStates] = useState3({});
362
+ const [fieldConfigs, setFieldConfigs] = useState3({});
363
+ const registerField = useCallback3((path, config) => {
364
+ setFieldConfigs((prev) => ({ ...prev, [path]: config }));
365
+ }, []);
366
+ const validate = useCallback3(
367
+ (path, config) => {
368
+ const value2 = data[path.split("/").filter(Boolean).join(".")];
369
+ const result = runValidation(config, {
370
+ value: value2,
371
+ dataModel: data,
372
+ customFunctions,
373
+ authState
374
+ });
375
+ setFieldStates((prev) => ({
376
+ ...prev,
377
+ [path]: {
378
+ touched: prev[path]?.touched ?? true,
379
+ validated: true,
380
+ result
381
+ }
382
+ }));
383
+ return result;
384
+ },
385
+ [data, customFunctions, authState]
386
+ );
387
+ const touch = useCallback3((path) => {
388
+ setFieldStates((prev) => ({
389
+ ...prev,
390
+ [path]: {
391
+ ...prev[path],
392
+ touched: true,
393
+ validated: prev[path]?.validated ?? false,
394
+ result: prev[path]?.result ?? null
395
+ }
396
+ }));
397
+ }, []);
398
+ const clear = useCallback3((path) => {
399
+ setFieldStates((prev) => {
400
+ const { [path]: _, ...rest } = prev;
401
+ return rest;
402
+ });
403
+ }, []);
404
+ const validateAll = useCallback3(() => {
405
+ let allValid = true;
406
+ for (const [path, config] of Object.entries(fieldConfigs)) {
407
+ const result = validate(path, config);
408
+ if (!result.valid) {
409
+ allValid = false;
410
+ }
411
+ }
412
+ return allValid;
413
+ }, [fieldConfigs, validate]);
414
+ const value = useMemo4(
415
+ () => ({
416
+ customFunctions,
417
+ fieldStates,
418
+ validate,
419
+ touch,
420
+ clear,
421
+ validateAll,
422
+ registerField
423
+ }),
424
+ [customFunctions, fieldStates, validate, touch, clear, validateAll, registerField]
425
+ );
426
+ return /* @__PURE__ */ jsx4(ValidationContext.Provider, { value, children });
427
+ }
428
+ function useValidation() {
429
+ const ctx = useContext4(ValidationContext);
430
+ if (!ctx) {
431
+ throw new Error("useValidation must be used within a ValidationProvider");
432
+ }
433
+ return ctx;
434
+ }
435
+ function useFieldValidation(path, config) {
436
+ const {
437
+ fieldStates,
438
+ validate: validateField,
439
+ touch: touchField,
440
+ clear: clearField,
441
+ registerField
442
+ } = useValidation();
443
+ React4.useEffect(() => {
444
+ if (config) {
445
+ registerField(path, config);
446
+ }
447
+ }, [path, config, registerField]);
448
+ const state = fieldStates[path] ?? {
449
+ touched: false,
450
+ validated: false,
451
+ result: null
452
+ };
453
+ const validate = useCallback3(
454
+ () => validateField(path, config ?? { checks: [] }),
455
+ [path, config, validateField]
456
+ );
457
+ const touch = useCallback3(() => touchField(path), [path, touchField]);
458
+ const clear = useCallback3(() => clearField(path), [path, clearField]);
459
+ return {
460
+ state,
461
+ validate,
462
+ touch,
463
+ clear,
464
+ errors: state.result?.errors ?? [],
465
+ isValid: state.result?.valid ?? true
466
+ };
467
+ }
468
+
469
+ // src/renderer.tsx
470
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
471
+ function ElementRenderer({
472
+ element,
473
+ tree,
474
+ registry,
475
+ loading,
476
+ fallback
477
+ }) {
478
+ const isVisible = useIsVisible(element.visible);
479
+ const { execute } = useActions();
480
+ if (!isVisible) {
481
+ return null;
482
+ }
483
+ const Component = registry[element.type] ?? fallback;
484
+ if (!Component) {
485
+ console.warn(`No renderer for component type: ${element.type}`);
486
+ return null;
487
+ }
488
+ const children = element.children?.map((childKey) => {
489
+ const childElement = tree.elements[childKey];
490
+ if (!childElement) {
491
+ return null;
492
+ }
493
+ return /* @__PURE__ */ jsx5(
494
+ ElementRenderer,
495
+ {
496
+ element: childElement,
497
+ tree,
498
+ registry,
499
+ loading,
500
+ fallback
501
+ },
502
+ childKey
503
+ );
504
+ });
505
+ return /* @__PURE__ */ jsx5(
506
+ Component,
507
+ {
508
+ element,
509
+ onAction: execute,
510
+ loading,
511
+ children
512
+ }
513
+ );
514
+ }
515
+ function Renderer({ tree, registry, loading, fallback }) {
516
+ if (!tree || !tree.root) {
517
+ return null;
518
+ }
519
+ const rootElement = tree.elements[tree.root];
520
+ if (!rootElement) {
521
+ return null;
522
+ }
523
+ return /* @__PURE__ */ jsx5(
524
+ ElementRenderer,
525
+ {
526
+ element: rootElement,
527
+ tree,
528
+ registry,
529
+ loading,
530
+ fallback
531
+ }
532
+ );
533
+ }
534
+ function JSONUIProvider({
535
+ registry,
536
+ initialData,
537
+ authState,
538
+ actionHandlers,
539
+ navigate,
540
+ validationFunctions,
541
+ onDataChange,
542
+ children
543
+ }) {
544
+ return /* @__PURE__ */ jsx5(
545
+ DataProvider,
546
+ {
547
+ initialData,
548
+ authState,
549
+ onDataChange,
550
+ children: /* @__PURE__ */ jsx5(VisibilityProvider, { children: /* @__PURE__ */ jsx5(ActionProvider, { handlers: actionHandlers, navigate, children: /* @__PURE__ */ jsxs2(ValidationProvider, { customFunctions: validationFunctions, children: [
551
+ children,
552
+ /* @__PURE__ */ jsx5(ConfirmationDialogManager, {})
553
+ ] }) }) })
554
+ }
555
+ );
556
+ }
557
+ function ConfirmationDialogManager() {
558
+ const { pendingConfirmation, confirm, cancel } = useActions();
559
+ if (!pendingConfirmation?.action.confirm) {
560
+ return null;
561
+ }
562
+ return /* @__PURE__ */ jsx5(
563
+ ConfirmDialog,
564
+ {
565
+ confirm: pendingConfirmation.action.confirm,
566
+ onConfirm: confirm,
567
+ onCancel: cancel
568
+ }
569
+ );
570
+ }
571
+ function createRendererFromCatalog(_catalog, registry) {
572
+ return function CatalogRenderer(props) {
573
+ return /* @__PURE__ */ jsx5(Renderer, { ...props, registry });
574
+ };
575
+ }
576
+
577
+ // src/hooks.ts
578
+ import { useState as useState4, useCallback as useCallback4, useRef, useEffect } from "react";
579
+ import { setByPath as setByPath2 } from "@json-render/core";
580
+ function parsePatchLine(line) {
581
+ try {
582
+ const trimmed = line.trim();
583
+ if (!trimmed || trimmed.startsWith("//")) {
584
+ return null;
585
+ }
586
+ return JSON.parse(trimmed);
587
+ } catch {
588
+ return null;
589
+ }
590
+ }
591
+ function applyPatch(tree, patch) {
592
+ const newTree = { ...tree, elements: { ...tree.elements } };
593
+ switch (patch.op) {
594
+ case "set":
595
+ case "add":
596
+ case "replace": {
597
+ if (patch.path === "/root") {
598
+ newTree.root = patch.value;
599
+ return newTree;
600
+ }
601
+ if (patch.path.startsWith("/elements/")) {
602
+ const pathParts = patch.path.slice("/elements/".length).split("/");
603
+ const elementKey = pathParts[0];
604
+ if (!elementKey) return newTree;
605
+ if (pathParts.length === 1) {
606
+ newTree.elements[elementKey] = patch.value;
607
+ } else {
608
+ const element = newTree.elements[elementKey];
609
+ if (element) {
610
+ const propPath = "/" + pathParts.slice(1).join("/");
611
+ const newElement = { ...element };
612
+ setByPath2(newElement, propPath, patch.value);
613
+ newTree.elements[elementKey] = newElement;
614
+ }
615
+ }
616
+ }
617
+ break;
618
+ }
619
+ case "remove": {
620
+ if (patch.path.startsWith("/elements/")) {
621
+ const elementKey = patch.path.slice("/elements/".length).split("/")[0];
622
+ if (elementKey) {
623
+ const { [elementKey]: _, ...rest } = newTree.elements;
624
+ newTree.elements = rest;
625
+ }
626
+ }
627
+ break;
628
+ }
629
+ }
630
+ return newTree;
631
+ }
632
+ function useUIStream({
633
+ api,
634
+ onComplete,
635
+ onError
636
+ }) {
637
+ const [tree, setTree] = useState4(null);
638
+ const [isStreaming, setIsStreaming] = useState4(false);
639
+ const [error, setError] = useState4(null);
640
+ const abortControllerRef = useRef(null);
641
+ const clear = useCallback4(() => {
642
+ setTree(null);
643
+ setError(null);
644
+ }, []);
645
+ const send = useCallback4(
646
+ async (prompt, context) => {
647
+ abortControllerRef.current?.abort();
648
+ abortControllerRef.current = new AbortController();
649
+ setIsStreaming(true);
650
+ setError(null);
651
+ let currentTree = { root: "", elements: {} };
652
+ setTree(currentTree);
653
+ try {
654
+ const response = await fetch(api, {
655
+ method: "POST",
656
+ headers: { "Content-Type": "application/json" },
657
+ body: JSON.stringify({
658
+ prompt,
659
+ context,
660
+ currentTree
661
+ }),
662
+ signal: abortControllerRef.current.signal
663
+ });
664
+ if (!response.ok) {
665
+ throw new Error(`HTTP error: ${response.status}`);
666
+ }
667
+ const reader = response.body?.getReader();
668
+ if (!reader) {
669
+ throw new Error("No response body");
670
+ }
671
+ const decoder = new TextDecoder();
672
+ let buffer = "";
673
+ while (true) {
674
+ const { done, value } = await reader.read();
675
+ if (done) break;
676
+ buffer += decoder.decode(value, { stream: true });
677
+ const lines = buffer.split("\n");
678
+ buffer = lines.pop() ?? "";
679
+ for (const line of lines) {
680
+ const patch = parsePatchLine(line);
681
+ if (patch) {
682
+ currentTree = applyPatch(currentTree, patch);
683
+ setTree({ ...currentTree });
684
+ }
685
+ }
686
+ }
687
+ if (buffer.trim()) {
688
+ const patch = parsePatchLine(buffer);
689
+ if (patch) {
690
+ currentTree = applyPatch(currentTree, patch);
691
+ setTree({ ...currentTree });
692
+ }
693
+ }
694
+ onComplete?.(currentTree);
695
+ } catch (err) {
696
+ if (err.name === "AbortError") {
697
+ return;
698
+ }
699
+ const error2 = err instanceof Error ? err : new Error(String(err));
700
+ setError(error2);
701
+ onError?.(error2);
702
+ } finally {
703
+ setIsStreaming(false);
704
+ }
705
+ },
706
+ [api, onComplete, onError]
707
+ );
708
+ useEffect(() => {
709
+ return () => {
710
+ abortControllerRef.current?.abort();
711
+ };
712
+ }, []);
713
+ return {
714
+ tree,
715
+ isStreaming,
716
+ error,
717
+ send,
718
+ clear
719
+ };
720
+ }
721
+ function flatToTree(elements) {
722
+ const elementMap = {};
723
+ let root = "";
724
+ for (const element of elements) {
725
+ elementMap[element.key] = {
726
+ key: element.key,
727
+ type: element.type,
728
+ props: element.props,
729
+ children: [],
730
+ visible: element.visible
731
+ };
732
+ }
733
+ for (const element of elements) {
734
+ if (element.parentKey) {
735
+ const parent = elementMap[element.parentKey];
736
+ if (parent) {
737
+ if (!parent.children) {
738
+ parent.children = [];
739
+ }
740
+ parent.children.push(element.key);
741
+ }
742
+ } else {
743
+ root = element.key;
744
+ }
745
+ }
746
+ return { root, elements: elementMap };
747
+ }
748
+ export {
749
+ ActionProvider,
750
+ ConfirmDialog,
751
+ DataProvider,
752
+ JSONUIProvider,
753
+ Renderer,
754
+ ValidationProvider,
755
+ VisibilityProvider,
756
+ createRendererFromCatalog,
757
+ flatToTree,
758
+ useAction,
759
+ useActions,
760
+ useData,
761
+ useDataBinding,
762
+ useDataValue,
763
+ useFieldValidation,
764
+ useIsVisible,
765
+ useUIStream,
766
+ useValidation,
767
+ useVisibility
768
+ };
769
+ //# sourceMappingURL=index.mjs.map