@axzydev/axzy_ui_system 1.7.3 → 1.7.4

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.
Files changed (61) hide show
  1. package/dist/index.cjs +1621 -594
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +366 -12
  5. package/dist/index.d.ts +366 -12
  6. package/dist/index.js +1331 -313
  7. package/dist/index.js.map +1 -1
  8. package/dist/layered.css +1 -1
  9. package/package.json +4 -2
  10. package/src/App.tsx +6 -8
  11. package/src/components/atoms/flex/flex.props.ts +3 -1
  12. package/src/components/atoms/flex/flex.tsx +11 -3
  13. package/src/components/atoms/textarea/textarea.props.ts +4 -0
  14. package/src/components/atoms/textarea/textarea.tsx +13 -3
  15. package/src/components/molecules/card/card.props.ts +2 -0
  16. package/src/components/molecules/card/card.tsx +15 -2
  17. package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +4 -0
  18. package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +57 -41
  19. package/src/components/organisms/dropfile/dropfile.props.ts +8 -2
  20. package/src/components/organisms/dropfile/dropfile.tsx +17 -0
  21. package/src/i18n/locales/en.ts +7 -0
  22. package/src/i18n/locales/es.ts +7 -0
  23. package/src/index.ts +31 -3
  24. package/src/json-renderer/actions/effects.test.tsx +115 -0
  25. package/src/json-renderer/actions/effects.ts +291 -0
  26. package/src/json-renderer/actions/effectsChain.test.ts +117 -0
  27. package/src/json-renderer/binding/binding.test.tsx +168 -0
  28. package/src/json-renderer/binding/resolveValue.ts +193 -0
  29. package/src/json-renderer/estado-acciones.test.tsx +166 -0
  30. package/src/json-renderer/form/FormNode.tsx +92 -0
  31. package/src/json-renderer/form/form.test.tsx +148 -0
  32. package/src/json-renderer/form/formLength.test.tsx +99 -0
  33. package/src/json-renderer/form/rules.ts +162 -0
  34. package/src/json-renderer/index.ts +21 -0
  35. package/src/json-renderer/kernel/SchemaRenderer.tsx +83 -7
  36. package/src/json-renderer/kernel/capabilities.ts +30 -0
  37. package/src/json-renderer/kernel/renderNode.tsx +256 -3
  38. package/src/json-renderer/kernel/schemaRenderer.props.ts +29 -3
  39. package/src/json-renderer/kernel/slots.test.tsx +104 -0
  40. package/src/json-renderer/kernel/slots.ts +34 -0
  41. package/src/json-renderer/kernel/types.ts +4 -0
  42. package/src/json-renderer/routing/match.ts +82 -0
  43. package/src/json-renderer/routing/navigation.ts +71 -0
  44. package/src/json-renderer/routing/routes.ts +179 -0
  45. package/src/json-renderer/routing/routing.test.ts +147 -0
  46. package/src/json-renderer/seguridad.test.tsx +299 -0
  47. package/src/json-renderer/shared/errors.ts +1 -0
  48. package/src/json-renderer/stream/tolerantParse.ts +5 -0
  49. package/src/json-renderer/stream/useSchemaStream.test.tsx +143 -0
  50. package/src/json-renderer/stream/useSchemaStream.ts +227 -0
  51. package/src/json-renderer/validation/jsonSchema.test.ts +105 -0
  52. package/src/json-renderer/validation/jsonSchema.ts +317 -0
  53. package/src/json-renderer/validation/validate.ts +17 -0
  54. package/src/sandbox/i18n.ts +38 -0
  55. package/src/sandbox/useHashRoute.ts +2 -1
  56. package/src/showcases/DemoJsonDashboard.tsx +218 -103
  57. package/src/showcases/DemoJsonDocs.tsx +11 -11
  58. package/src/showcases/JsonScreenShowcase.tsx +43 -53
  59. package/src/showcases/demoApp/demoJson.test.tsx +241 -0
  60. package/src/showcases/demoApp/routes.test.ts +63 -0
  61. package/src/showcases/demoApp/schemas.test.ts +6 -17
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "packageManager": "pnpm@10.34.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
- "version": "1.7.3",
7
+ "version": "1.7.4",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -47,7 +47,8 @@
47
47
  "audit:sandbox": "node scripts/audit-sandbox.mjs",
48
48
  "docs:llm": "pnpm ext:catalog && node scripts/build-llm-docs.mjs && node scripts/build-ai-bundle.mjs",
49
49
  "docs:ai": "node scripts/build-ai-bundle.mjs",
50
- "docs:json-renderer": "node scripts/build-json-renderer-docs.mjs"
50
+ "docs:json-renderer": "node scripts/build-json-renderer-docs.mjs",
51
+ "schema:check": "node scripts/json-renderer/check.mjs"
51
52
  },
52
53
  "dependencies": {
53
54
  "clsx": "^2.1.1",
@@ -72,6 +73,7 @@
72
73
  "@types/react-dom": "19.0.3",
73
74
  "@types/vscode": "1.85.0",
74
75
  "@vitejs/plugin-react-swc": "3.8.0",
76
+ "ajv": "8.18.0",
75
77
  "esbuild": "^0.25.0",
76
78
  "eslint": "9.19.0",
77
79
  "eslint-plugin-react-hooks": "5.0.0",
package/src/App.tsx CHANGED
@@ -70,7 +70,7 @@ import JsonRendererShowcase from "./showcases/JsonRendererShowcase";
70
70
  import JsonEditorShowcase from "./showcases/JsonEditorShowcase";
71
71
  import JsonScreenShowcase from "./showcases/JsonScreenShowcase";
72
72
  import DemoJsonPortfolio from "./showcases/DemoJsonPortfolio";
73
- import DemoJsonDashboard, { DEMO_APP_PAGES, type DemoAppPage } from "./showcases/DemoJsonDashboard";
73
+ import DemoJsonDashboard from "./showcases/DemoJsonDashboard";
74
74
 
75
75
  // Sandbox routing / landing (dev-only, never published)
76
76
  import {
@@ -377,18 +377,16 @@ function AppShell() {
377
377
  ) : route.view === "demo" ? (
378
378
  route.group === "app" ? (
379
379
  <DemoJsonDashboard
380
- page={(DEMO_APP_PAGES.includes((route.item ?? "inicio") as DemoAppPage)
381
- ? route.item
382
- : "inicio") as DemoAppPage}
383
- onNavigate={(pagina) => {
384
- window.location.hash = `#demo/app/${pagina}`;
380
+ path={`/${route.item || "inicio"}`}
381
+ query={route.query}
382
+ onNavigate={(href) => {
383
+ window.location.hash = `#demo/app${href}`;
385
384
  }}
386
385
  onExit={() => {
387
386
  window.location.hash = "#demo";
388
387
  }}
389
- filterEstado={route.query.estado ?? "Todos"}
390
388
  onFilterChange={(estado) => {
391
- const base = `#demo/app/${route.item ?? "inicio"}`;
389
+ const base = `#demo/app/${route.item || "inicio"}`;
392
390
  window.location.hash = estado === "Todos" ? base : `${base}?estado=${encodeURIComponent(estado)}`;
393
391
  }}
394
392
  />
@@ -1,4 +1,4 @@
1
- import { ReactNode, CSSProperties, ElementType } from "react";
1
+ import { ReactNode, CSSProperties, ElementType, UIEvent } from "react";
2
2
 
3
3
  export type FlexDirection = "row" | "column" | "row-reverse" | "column-reverse";
4
4
  export type FlexAlign = "start" | "end" | "center" | "stretch" | "baseline";
@@ -32,4 +32,6 @@ export interface ITFlexProps {
32
32
  as?: ElementType;
33
33
  /** Click handler */
34
34
  onClick?: (e: React.MouseEvent) => void;
35
+ /** Scroll handler (useful for scrollable chat/log panels) */
36
+ onScroll?: (e: UIEvent<HTMLElement>) => void;
35
37
  }
@@ -1,4 +1,5 @@
1
1
  import clsx from "clsx";
2
+ import { forwardRef } from "react";
2
3
  import {
3
4
  ITFlexProps,
4
5
  FlexDirection,
@@ -52,7 +53,7 @@ const wrapMap: Record<FlexWrap, string> = {
52
53
  * <main>Content</main>
53
54
  * </ITFlex>
54
55
  */
55
- export default function ITFlex({
56
+ const ITFlex = forwardRef<HTMLDivElement, ITFlexProps>(function ITFlex({
56
57
  children,
57
58
  direction = "row",
58
59
  align,
@@ -66,7 +67,8 @@ export default function ITFlex({
66
67
  style,
67
68
  as: Component = "div",
68
69
  onClick,
69
- }: ITFlexProps) {
70
+ onScroll,
71
+ }, ref) {
70
72
  const resolvedStyle: React.CSSProperties = {
71
73
  gap: gap > 0 ? `${gap * 0.25}rem` : undefined,
72
74
  flexGrow: typeof grow === "boolean" ? (grow ? 1 : 0) : grow,
@@ -77,6 +79,7 @@ export default function ITFlex({
77
79
 
78
80
  return (
79
81
  <Component
82
+ ref={ref}
80
83
  className={clsx(
81
84
  "flex",
82
85
  directionMap[direction],
@@ -87,8 +90,13 @@ export default function ITFlex({
87
90
  )}
88
91
  style={resolvedStyle}
89
92
  onClick={onClick}
93
+ onScroll={onScroll}
90
94
  >
91
95
  {children}
92
96
  </Component>
93
97
  );
94
- }
98
+ });
99
+
100
+ ITFlex.displayName = "ITFlex";
101
+
102
+ export default ITFlex;
@@ -21,4 +21,8 @@ export interface ITTextareaProps {
21
21
  maxLength?: number;
22
22
  /** Resize behavior of the textarea: "none" | "vertical" | "horizontal" | "both". */
23
23
  resize?: "none" | "vertical" | "horizontal" | "both";
24
+ /** Keydown handler (e.g. Enter to send in a chat composer). */
25
+ onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
26
+ /** Autofocus the textarea. */
27
+ autoFocus?: boolean;
24
28
  }
@@ -1,4 +1,5 @@
1
1
  import clsx from "clsx";
2
+ import { forwardRef } from "react";
2
3
  import { ITTextareaProps } from "./textarea.props";
3
4
  import { inputLabel, inputError } from "@/utils/styles";
4
5
  import ITText from "@/components/atoms/text/text";
@@ -26,7 +27,7 @@ const resizeMap = {
26
27
  * error={bio.length > 500 ? "Max 500 characters" : undefined}
27
28
  * />
28
29
  */
29
- export default function ITTextarea({
30
+ const ITTextarea = forwardRef<HTMLTextAreaElement, ITTextareaProps>(function ITTextarea({
30
31
  value,
31
32
  onChange,
32
33
  label,
@@ -38,7 +39,9 @@ export default function ITTextarea({
38
39
  name,
39
40
  maxLength,
40
41
  resize = "vertical",
41
- }: ITTextareaProps) {
42
+ onKeyDown,
43
+ autoFocus = false,
44
+ }, ref) {
42
45
  return (
43
46
  <div className={clsx("flex flex-col gap-1.5", className)}>
44
47
  {label && (
@@ -47,6 +50,7 @@ export default function ITTextarea({
47
50
  </ITText>
48
51
  )}
49
52
  <textarea
53
+ ref={ref}
50
54
  id={name}
51
55
  name={name}
52
56
  value={value}
@@ -55,6 +59,8 @@ export default function ITTextarea({
55
59
  rows={rows}
56
60
  disabled={disabled}
57
61
  maxLength={maxLength}
62
+ onKeyDown={onKeyDown}
63
+ autoFocus={autoFocus}
58
64
  className={clsx(
59
65
  "w-full border border-solid transition-all duration-200 rounded-lg px-3 py-2 text-sm outline-none",
60
66
  "focus:ring-2",
@@ -68,4 +74,8 @@ export default function ITTextarea({
68
74
  {error && <ITText as="span" className={inputError}>{error}</ITText>}
69
75
  </div>
70
76
  );
71
- }
77
+ });
78
+
79
+ ITTextarea.displayName = "ITTextarea";
80
+
81
+ export default ITTextarea;
@@ -3,6 +3,8 @@ export interface ITCardProps {
3
3
  onClick?: () => void;
4
4
  /** Card title displayed in the header area. */
5
5
  title?: string;
6
+ /** Optional actions rendered at the right of the title row (e.g. a play button). */
7
+ headerActions?: React.ReactNode;
6
8
  /** Image source URL displayed at the top of the card. */
7
9
  image?: string;
8
10
  /** Alt text for the card image. @default "Card Image" */
@@ -20,6 +20,7 @@ import { useITTranslation } from "@/i18n";
20
20
  */
21
21
  export default function ITCard({
22
22
  title,
23
+ headerActions,
23
24
  image,
24
25
  alt,
25
26
  children,
@@ -97,14 +98,26 @@ export default function ITCard({
97
98
  )}
98
99
 
99
100
  <div className={clsx(contentClassName)} style={bodyStyle}>
100
- {title && (
101
+ {headerActions ? (
102
+ <div className="mb-2 flex items-center justify-between gap-3">
103
+ {title && (
104
+ <ITText
105
+ as="h3"
106
+ className={clsx(flat ? "text-base font-semibold" : "text-xl font-semibold", titleClassName)}
107
+ >
108
+ {title}
109
+ </ITText>
110
+ )}
111
+ {headerActions}
112
+ </div>
113
+ ) : title ? (
101
114
  <ITText
102
115
  as="h3"
103
116
  className={clsx(flat ? "text-base font-semibold mb-3" : "text-xl font-semibold mb-2", titleClassName)}
104
117
  >
105
118
  {title}
106
119
  </ITText>
107
- )}
120
+ ) : null}
108
121
  <ITText as="div" className="text-secondary-600">{children}</ITText>
109
122
  </div>
110
123
  {actions && (
@@ -19,6 +19,10 @@ export interface ITLineBarChartProps {
19
19
  lineColor?: string;
20
20
  /** Color de las barras. */
21
21
  barColor?: string;
22
+ /** Muestra la serie de línea. @default true */
23
+ showLine?: boolean;
24
+ /** Muestra la serie de barras. @default true */
25
+ showBars?: boolean;
22
26
  height?: number;
23
27
  /** Formatea los valores del tooltip; por defecto número con separadores. */
24
28
  formatValue?: (value: number) => string;
@@ -16,6 +16,8 @@ export function ITLineBarChart({
16
16
  barLabel,
17
17
  lineColor = "#10b981",
18
18
  barColor = "#3b82f6",
19
+ showLine = true,
20
+ showBars = true,
19
21
  height = 240,
20
22
  formatValue,
21
23
  formatAxis,
@@ -40,8 +42,12 @@ export function ITLineBarChart({
40
42
  const plotHeight = height - padding.top - padding.bottom;
41
43
 
42
44
  const max = useMemo(
43
- () => Math.max(...data.flatMap((d) => [d.line, d.bar]), 1),
44
- [data]
45
+ () =>
46
+ Math.max(
47
+ ...data.flatMap((d) => [showLine ? d.line : 0, showBars ? d.bar : 0]),
48
+ 1
49
+ ),
50
+ [data, showLine, showBars]
45
51
  );
46
52
  const gridValues = [0, 0.25, 0.5, 0.75, 1].map((f) => max * f);
47
53
  const step = data.length > 0 ? plotWidth / data.length : plotWidth;
@@ -103,21 +109,22 @@ export function ITLineBarChart({
103
109
  </g>
104
110
  ))}
105
111
 
106
- {data.map((d, i) => (
107
- <rect
108
- key={`bar-${d.label}`}
109
- x={x(i) - barWidth / 2}
110
- y={y(d.bar)}
111
- width={barWidth}
112
- height={Math.max(padding.top + plotHeight - y(d.bar), 1)}
113
- rx={3}
114
- fill={barColor}
115
- opacity={hover === null || hover === i ? 0.85 : 0.35}
116
- />
117
- ))}
112
+ {showBars &&
113
+ data.map((d, i) => (
114
+ <rect
115
+ key={`bar-${d.label}`}
116
+ x={x(i) - barWidth / 2}
117
+ y={y(d.bar)}
118
+ width={barWidth}
119
+ height={Math.max(padding.top + plotHeight - y(d.bar), 1)}
120
+ rx={3}
121
+ fill={barColor}
122
+ opacity={hover === null || hover === i ? 0.85 : 0.35}
123
+ />
124
+ ))}
118
125
 
119
- {areaPath && <path d={areaPath} fill={`url(#area-${gradientId})`} />}
120
- {data.length > 0 && (
126
+ {showLine && areaPath && <path d={areaPath} fill={`url(#area-${gradientId})`} />}
127
+ {showLine && data.length > 0 && (
121
128
  <polyline
122
129
  points={linePoints}
123
130
  fill="none"
@@ -127,17 +134,18 @@ export function ITLineBarChart({
127
134
  strokeLinejoin="round"
128
135
  />
129
136
  )}
130
- {data.map((d, i) => (
131
- <circle
132
- key={`dot-${d.label}`}
133
- cx={x(i)}
134
- cy={y(d.line)}
135
- r={hover === i ? 4.5 : 3}
136
- fill="#ffffff"
137
- stroke={lineColor}
138
- strokeWidth={2}
139
- />
140
- ))}
137
+ {showLine &&
138
+ data.map((d, i) => (
139
+ <circle
140
+ key={`dot-${d.label}`}
141
+ cx={x(i)}
142
+ cy={y(d.line)}
143
+ r={hover === i ? 4.5 : 3}
144
+ fill="#ffffff"
145
+ stroke={lineColor}
146
+ strokeWidth={2}
147
+ />
148
+ ))}
141
149
 
142
150
  {data.map((d, i) => (
143
151
  <g key={`hit-${d.label}`} onMouseEnter={() => setHover(i)}>
@@ -157,24 +165,32 @@ export function ITLineBarChart({
157
165
  role="status"
158
166
  >
159
167
  <p className="text-[10px] font-semibold" style={{ color: "#e2e8f0" }}>{active.label}</p>
160
- <p className="text-[11px] tabular-nums" style={{ color: lineColor }}>
161
- {resolvedLineLabel}: {resolvedFormatValue(active.line)}
162
- </p>
163
- <p className="text-[11px] tabular-nums" style={{ color: "#93c5fd" }}>
164
- {resolvedBarLabel}: {resolvedFormatValue(active.bar)}
165
- </p>
168
+ {showLine && (
169
+ <p className="text-[11px] tabular-nums" style={{ color: lineColor }}>
170
+ {resolvedLineLabel}: {resolvedFormatValue(active.line)}
171
+ </p>
172
+ )}
173
+ {showBars && (
174
+ <p className="text-[11px] tabular-nums" style={{ color: "#93c5fd" }}>
175
+ {resolvedBarLabel}: {resolvedFormatValue(active.bar)}
176
+ </p>
177
+ )}
166
178
  </div>
167
179
  )}
168
180
 
169
181
  <ul className="flex flex-wrap items-center gap-4 text-[11px]" style={{ color: "#475569" }}>
170
- <li>
171
- <span className="h-2.5 w-2.5 rounded-sm" style={{ background: lineColor }} aria-hidden />
172
- {resolvedLineLabel}
173
- </li>
174
- <li>
175
- <span className="h-2.5 w-2.5 rounded-sm" style={{ background: barColor }} aria-hidden />
176
- {resolvedBarLabel}
177
- </li>
182
+ {showLine && (
183
+ <li>
184
+ <span className="h-2.5 w-2.5 rounded-sm" style={{ background: lineColor }} aria-hidden />
185
+ {resolvedLineLabel}
186
+ </li>
187
+ )}
188
+ {showBars && (
189
+ <li>
190
+ <span className="h-2.5 w-2.5 rounded-sm" style={{ background: barColor }} aria-hidden />
191
+ {resolvedBarLabel}
192
+ </li>
193
+ )}
178
194
  </ul>
179
195
  </div>
180
196
  );
@@ -28,8 +28,14 @@ export interface ITDropfileProps {
28
28
  onFileSelect: (file: File | null) => void;
29
29
  /** Called when the user cancels the current selection. */
30
30
  onCancel?: () => void;
31
- /** Called when the user confirms and submits the file. */
32
- onSubmit?: (file: File) => void;
31
+ /** Called when the user confirms and submits the file. With `autoUpload`, it runs right after selection and may return a promise that drives the status. */
32
+ onSubmit?: (file: File) => void | Promise<void>;
33
+ /**
34
+ * Sube en cuanto se elige el archivo, sin paso de confirmar. Usa `onSubmit` como la subida:
35
+ * el estado pasa a subiendo y luego a subido, o a error si la promesa falla.
36
+ * @default false
37
+ */
38
+ autoUpload?: boolean;
33
39
  /** List of accepted MIME types. */
34
40
  acceptedFileTypes?: FileTypeEnum[];
35
41
  /** Additional classes for the preview/content area. */
@@ -43,6 +43,7 @@ const ITDropfile: React.FC<ITDropfileProps> = ({
43
43
  onFileSelect,
44
44
  onCancel,
45
45
  onSubmit,
46
+ autoUpload = false,
46
47
  contentClassName,
47
48
  containerClassName,
48
49
  acceptedFileTypes = [FileTypeEnum.PDF, FileTypeEnum.XLS, FileTypeEnum.XLSX, FileTypeEnum.JPG, FileTypeEnum.PNG, FileTypeEnum.JPEG],
@@ -207,6 +208,20 @@ const ITDropfile: React.FC<ITDropfileProps> = ({
207
208
  );
208
209
  };
209
210
 
211
+ /** Subida automática: corre `onSubmit` al elegir el archivo y sigue su estado. */
212
+ const runAutoUpload = async (file: File) => {
213
+ setIsConfirmed(true);
214
+ setUploadStatus(UploadStatus.UPLOADING);
215
+ try {
216
+ onFileSelect(file);
217
+ await onSubmit?.(file);
218
+ setUploadStatus(UploadStatus.UPLOADED);
219
+ } catch (error) {
220
+ setUploadStatus(UploadStatus.ERROR);
221
+ console.error("Error al subir archivo:", error);
222
+ }
223
+ };
224
+
210
225
  const onDrop = (acceptedFiles: File[]) => {
211
226
  const file = acceptedFiles[0];
212
227
  if (file) {
@@ -224,6 +239,8 @@ const ITDropfile: React.FC<ITDropfileProps> = ({
224
239
  const previewUrl = URL.createObjectURL(file);
225
240
  setImagePreview(previewUrl);
226
241
  }
242
+
243
+ if (autoUpload) void runAutoUpload(file);
227
244
  } else {
228
245
  alert(t("dropfile.fileTypeNotAllowed"));
229
246
  setSelectedFile(null);
@@ -212,6 +212,13 @@ const en: ITMessages = {
212
212
  "jsonEditor.kindNumber": "number",
213
213
  "jsonEditor.kindBoolean": "boolean",
214
214
  "jsonEditor.kindNull": "null",
215
+
216
+ // ---------------------------------------------------------------- jsonRenderer
217
+ "jsonRenderer.form.required": "Required field.",
218
+ "jsonRenderer.form.email": "Enter a valid email address.",
219
+ "jsonRenderer.form.minLength": "Enter at least {min} characters.",
220
+ "jsonRenderer.form.maxLength": "Enter at most {max} characters.",
221
+ "jsonRenderer.form.submit": "Submit",
215
222
  };
216
223
 
217
224
  export default en;
@@ -214,6 +214,13 @@ const es: ITMessages = {
214
214
  "jsonEditor.kindNumber": "número",
215
215
  "jsonEditor.kindBoolean": "booleano",
216
216
  "jsonEditor.kindNull": "nulo",
217
+
218
+ // ---------------------------------------------------------------- jsonRenderer
219
+ "jsonRenderer.form.required": "Campo obligatorio.",
220
+ "jsonRenderer.form.email": "Escribe un correo válido.",
221
+ "jsonRenderer.form.minLength": "Escribe al menos {min} caracteres.",
222
+ "jsonRenderer.form.maxLength": "Escribe como máximo {max} caracteres.",
223
+ "jsonRenderer.form.submit": "Enviar",
217
224
  };
218
225
 
219
226
  export default es;
package/src/index.ts CHANGED
@@ -84,6 +84,7 @@ import {
84
84
  GLOBALLY_FORBIDDEN_PROPS,
85
85
  resolveNode,
86
86
  parsePartialSchema,
87
+ useSchemaStream,
87
88
  isNodePending,
88
89
  JsonEngineError,
89
90
  issue,
@@ -102,8 +103,30 @@ import {
102
103
  DEFAULT_MIGRATIONS,
103
104
  buildVocabularyContract,
104
105
  formatVocabularyForPrompt,
106
+ createBindingResolver,
107
+ validateRoutes,
108
+ flattenRoutes,
109
+ joinRoutePath,
110
+ matchRoute,
111
+ routeHref,
112
+ buildNavigationTree,
113
+ navContainsRoute,
105
114
  } from "./json-renderer";
106
115
  import type {
116
+ DataHandler,
117
+ DataLoader,
118
+ BindingScope,
119
+ BindingResolver,
120
+ SlotComponent,
121
+ SlotRegistry,
122
+ EffectRegistry,
123
+ ToastVariant,
124
+ RoutesSchema,
125
+ RouteSchema,
126
+ RouteNavSchema,
127
+ FlatRoute,
128
+ RouteMatch,
129
+ NavNode,
107
130
  ITJsonRenderEngineProps,
108
131
  ScreenSchema,
109
132
  NodeSchema,
@@ -122,6 +145,7 @@ import type {
122
145
  UnresolvedNode,
123
146
  PartialParseResult,
124
147
  StreamState,
148
+ StreamSource,
125
149
  JsonEngineIssue,
126
150
  JsonEngineCode,
127
151
  JsonEngineSeverity,
@@ -247,11 +271,13 @@ export {
247
271
  ITSchemaRenderer, NodeBoundary, renderNode, auditTree, countPendingNodes,
248
272
  axzyCatalog, axzyCatalogTypes, AXZY_CATALOG_ENTRIES, AXZY_EXCLUDED_TYPES,
249
273
  createCatalog, getCatalogEntry, isExcludedType, getExclusionReason, GLOBALLY_FORBIDDEN_PROPS,
250
- resolveNode, parsePartialSchema, isNodePending,
274
+ resolveNode, parsePartialSchema, isNodePending, useSchemaStream,
251
275
  JsonEngineError, issue, formatPath, parsePath, readPath, hasPath, pushPath, propPath,
252
276
  DEFAULT_LIMITS, resolveLimits, createLimitTracker,
253
277
  validateSchema, CURRENT_SCHEMA_VERSION, migrateSchema, DEFAULT_MIGRATIONS,
254
278
  buildVocabularyContract, formatVocabularyForPrompt,
279
+ createBindingResolver,
280
+ validateRoutes, flattenRoutes, joinRoutePath, matchRoute, routeHref, buildNavigationTree, navContainsRoute,
255
281
  };
256
282
 
257
283
  export type {
@@ -286,9 +312,11 @@ export {
286
312
  ITWysiwygProps,
287
313
  ITJsonRendererProps, JsonComponentNode, JsonComponentType,
288
314
  ITJSONEditorProps,
289
- ITJsonRenderEngineProps, ScreenSchema, NodeSchema, SchemaValue, SchemaReference, WhenSchema,
315
+ ITJsonRenderEngineProps, DataHandler, DataLoader, BindingScope, BindingResolver, SlotComponent, SlotRegistry, EffectRegistry, ToastVariant,
316
+ RoutesSchema, RouteSchema, RouteNavSchema, FlatRoute, RouteMatch, NavNode,
317
+ ScreenSchema, NodeSchema, SchemaValue, SchemaReference, WhenSchema,
290
318
  ActionSchema, EffectSchema, DataSourceSchema, NodeStatus, Catalog, CatalogEntry, PropSpec, PropKind,
291
- ResolvedNode, UnresolvedNode, PartialParseResult, StreamState, JsonEngineIssue, JsonEngineCode,
319
+ ResolvedNode, UnresolvedNode, PartialParseResult, StreamState, StreamSource, JsonEngineIssue, JsonEngineCode,
292
320
  JsonEngineSeverity, SchemaPath, PathSegment, EngineLimits, LimitTracker,
293
321
  ValidationReport, ValidationVerdict, ValidateOptions, SchemaMigration, MigrationRegistry, MigrationResult,
294
322
  VocabularyContract, ComponentContract, PropContract,
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen as testingScreen } from "@testing-library/react";
4
+ import { ITSchemaRenderer } from "../kernel/SchemaRenderer";
5
+ import type { JsonEngineIssue } from "../shared/errors";
6
+ import type { NodeSchema, ScreenSchema } from "../kernel/types";
7
+ import { wrapAction, type EffectContext, type EffectRegistry } from "./effects";
8
+
9
+ /** Contexto de pruebas: traducción literal y avisos acumulados. */
10
+ function contexto(effects?: EffectRegistry) {
11
+ const avisos: JsonEngineIssue[] = [];
12
+ const context: EffectContext = {
13
+ effects,
14
+ path: ["content", 0],
15
+ resolveText: (value) => (typeof value === "string" ? value : ""),
16
+ report: (aviso) => avisos.push(aviso),
17
+ };
18
+ return { context, avisos };
19
+ }
20
+
21
+ describe("wrapAction", () => {
22
+ it("sin then ni onError devuelve el handler tal cual", () => {
23
+ const handler = () => "ok";
24
+ const { context } = contexto({});
25
+ expect(wrapAction(handler, { action: "x" }, context)).toBe(handler);
26
+ expect(wrapAction(handler, undefined, context)).toBe(handler);
27
+ });
28
+
29
+ it("then se ejecuta después de un handler que termina bien", () => {
30
+ const orden: string[] = [];
31
+ const toast = vi.fn(() => orden.push("toast"));
32
+ const handler = vi.fn(() => orden.push("handler"));
33
+ const { context } = contexto({ toast });
34
+
35
+ const envuelto = wrapAction(handler, { action: "x", then: [{ toast: { variant: "success", message: "Hecho" } }] }, context);
36
+ envuelto("args");
37
+
38
+ expect(orden).toEqual(["handler", "toast"]);
39
+ expect(toast).toHaveBeenCalledWith({ variant: "success", message: "Hecho" });
40
+ });
41
+
42
+ it("los efectos sueltos de la acción cuentan como un then más", () => {
43
+ const navigate = vi.fn();
44
+ const { context } = contexto({ navigate });
45
+ wrapAction(() => undefined, { action: "x", navigate: "/inicio" }, context)();
46
+ expect(navigate).toHaveBeenCalledWith("/inicio");
47
+ });
48
+
49
+ it("onError se ejecuta, y then no, cuando el handler lanza", () => {
50
+ const toast = vi.fn();
51
+ const { context } = contexto({ toast });
52
+ const envuelto = wrapAction(
53
+ () => {
54
+ throw new Error("fallo");
55
+ },
56
+ { action: "x", then: [{ toast: { message: "Bien" } }], onError: { toast: { variant: "error", message: "Mal" } } },
57
+ context
58
+ );
59
+ envuelto();
60
+ expect(toast).toHaveBeenCalledTimes(1);
61
+ expect(toast).toHaveBeenCalledWith({ variant: "error", message: "Mal" });
62
+ });
63
+
64
+ it("con un handler asíncrono espera la promesa: then si resuelve, onError si rechaza", async () => {
65
+ const toast = vi.fn();
66
+ const { context } = contexto({ toast });
67
+ const acciones = { action: "x", then: [{ toast: { message: "Bien" } }], onError: { toast: { variant: "error", message: "Mal" } } };
68
+
69
+ await wrapAction(() => Promise.resolve("ok"), acciones, context)();
70
+ expect(toast).toHaveBeenLastCalledWith({ variant: "info", message: "Bien" });
71
+
72
+ await (wrapAction(() => Promise.reject(new Error("x")), acciones, context)() as Promise<unknown>).catch(() => undefined);
73
+ await new Promise((resolve) => setTimeout(resolve, 0));
74
+ expect(toast).toHaveBeenLastCalledWith({ variant: "error", message: "Mal" });
75
+ });
76
+
77
+ it("un efecto que el host no implementa avisa con JR040 al pintar, y no rompe al ejecutar", () => {
78
+ const { context, avisos } = contexto({});
79
+ const envuelto = wrapAction(() => undefined, { action: "x", then: [{ toast: { message: "Hola" } }] }, context);
80
+ expect(avisos[0]).toMatchObject({ code: "JR040", severity: "warning" });
81
+ expect(() => envuelto()).not.toThrow();
82
+ });
83
+ });
84
+
85
+ describe("efectos en pantalla", () => {
86
+ const pantalla = (action: Record<string, unknown>): ScreenSchema =>
87
+ ({
88
+ version: 1,
89
+ id: "efectos",
90
+ content: { type: "ITButton", props: { label: "Enviar", onClick: action } } as NodeSchema,
91
+ }) as ScreenSchema;
92
+
93
+ it("al pulsar, el host recibe la acción y después se ejecuta el toast del JSON", () => {
94
+ const orden: string[] = [];
95
+ const host = vi.fn(() => orden.push("host"));
96
+ const toast = vi.fn(() => orden.push("toast"));
97
+
98
+ render(
99
+ <ITSchemaRenderer
100
+ schema={pantalla({ action: "guardar", then: [{ toast: { variant: "success", message: { i18n: "k" } } }] })}
101
+ resolveAction={(_node, _prop, action) => {
102
+ const nombre = (action as { action?: string }).action;
103
+ return nombre === "guardar" ? host : undefined;
104
+ }}
105
+ resolveValue={(value) => (value && typeof value === "object" && "i18n" in value ? "Guardado" : value)}
106
+ effects={{ toast }}
107
+ />
108
+ );
109
+
110
+ fireEvent.click(testingScreen.getByRole("button", { name: "Enviar" }));
111
+
112
+ expect(orden).toEqual(["host", "toast"]);
113
+ expect(toast).toHaveBeenCalledWith({ variant: "success", message: "Guardado" });
114
+ });
115
+ });