@axzydev/axzy_ui_system 1.6.0 → 1.7.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.cjs +7842 -4676
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +883 -3
- package/dist/index.d.ts +883 -3
- package/dist/index.js +9118 -5971
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +14 -7
- package/src/App.tsx +70 -2
- package/src/components/atoms/image/image.props.ts +2 -0
- package/src/components/atoms/image/image.tsx +3 -2
- package/src/components/molecules/json-editor/json-editor-tree.tsx +235 -0
- package/src/components/molecules/json-editor/json-editor.index.ts +2 -0
- package/src/components/molecules/json-editor/json-editor.props.ts +82 -0
- package/src/components/molecules/json-editor/json-editor.tsx +972 -0
- package/src/components/molecules/json-editor/json-editor.utils.ts +717 -0
- package/src/components/molecules/json-editor/useJsonEditorKeyboard.ts +256 -0
- package/src/components/molecules/json-renderer/json-renderer.index.ts +2 -0
- package/src/components/molecules/json-renderer/json-renderer.props.ts +31 -0
- package/src/components/molecules/json-renderer/json-renderer.tsx +79 -0
- package/src/components/organisms/dialog/dialog.props.ts +2 -0
- package/src/components/organisms/dialog/dialog.tsx +4 -3
- package/src/dev.css +30 -1
- package/src/hooks/useJsonRender.tsx +9 -0
- package/src/i18n/locales/en.ts +54 -0
- package/src/i18n/locales/es.ts +54 -0
- package/src/index.css +414 -0
- package/src/index.ts +99 -2
- package/src/json-renderer/catalog/axzyCatalog.ts +362 -0
- package/src/json-renderer/catalog/catalog.props.ts +128 -0
- package/src/json-renderer/catalog/resolve.ts +225 -0
- package/src/json-renderer/index.ts +70 -0
- package/src/json-renderer/kernel/NodeBoundary.tsx +53 -0
- package/src/json-renderer/kernel/SchemaRenderer.tsx +197 -0
- package/src/json-renderer/kernel/kernel.test.tsx +341 -0
- package/src/json-renderer/kernel/partial.ts +92 -0
- package/src/json-renderer/kernel/renderNode.tsx +113 -0
- package/src/json-renderer/kernel/schemaRenderer.props.ts +42 -0
- package/src/json-renderer/kernel/types.ts +159 -0
- package/src/json-renderer/shared/errors.ts +92 -0
- package/src/json-renderer/shared/limits.ts +87 -0
- package/src/json-renderer/shared/paths.ts +66 -0
- package/src/json-renderer/shared/shared.test.ts +108 -0
- package/src/json-renderer/stream/tolerantParse.test.ts +152 -0
- package/src/json-renderer/stream/tolerantParse.ts +449 -0
- package/src/json-renderer/test/setup.ts +38 -0
- package/src/json-renderer/validation/migrate.ts +83 -0
- package/src/json-renderer/validation/validate.ts +161 -0
- package/src/json-renderer/validation/validation.test.ts +165 -0
- package/src/json-renderer/vocabulary/buildContract.ts +158 -0
- package/src/sandbox/AxzyLogo.tsx +139 -0
- package/src/sandbox/axzyConfig.test.ts +40 -0
- package/src/sandbox/axzyConfig.ts +77 -0
- package/src/sandbox/i18n.ts +220 -0
- package/src/sandbox/navigation.ts +3 -0
- package/src/sandbox/useHashRoute.ts +39 -9
- package/src/showcases/DemoJsonDashboard.tsx +489 -0
- package/src/showcases/DemoJsonDocs.tsx +339 -0
- package/src/showcases/DemoJsonPortfolio.tsx +257 -0
- package/src/showcases/FeedbackShowcases.tsx +16 -5
- package/src/showcases/HomeShowcase.tsx +16 -9
- package/src/showcases/JsonEditorShowcase.tsx +150 -0
- package/src/showcases/JsonRendererShowcase.tsx +70 -0
- package/src/showcases/JsonScreenShowcase.tsx +341 -0
- package/src/showcases/StructureShowcases.tsx +6 -1
- package/src/showcases/demoApp/demoData.ts +181 -0
- package/src/showcases/demoApp/nodeTree.test.ts +39 -0
- package/src/showcases/demoApp/nodeTree.ts +35 -0
- package/src/showcases/demoApp/schemas.test.ts +54 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
VolverArriba,
|
|
28
28
|
} from "./portfolioUi";
|
|
29
29
|
import { useITTranslation } from "@/i18n";
|
|
30
|
+
import { AxzyLogo } from "@/sandbox/AxzyLogo";
|
|
30
31
|
|
|
31
32
|
const CV_URL = "/Martin_Amaro_CV_ES_2026.pdf";
|
|
32
33
|
const GITHUB_URL = "https://github.com/maag070208";
|
|
@@ -125,9 +126,7 @@ const PortfolioNav = () => {
|
|
|
125
126
|
<nav aria-label={t("showcase.home.sectionNav")} className={HOJA}>
|
|
126
127
|
<div className="flex h-14 items-center gap-4">
|
|
127
128
|
<a href="#sobre-mi" className="flex items-center gap-2.5">
|
|
128
|
-
<
|
|
129
|
-
AA
|
|
130
|
-
</span>
|
|
129
|
+
<AxzyLogo variante="icono" tamano={24} />
|
|
131
130
|
<ITText
|
|
132
131
|
as="span"
|
|
133
132
|
className="hidden font-display !text-[13px] !font-bold -tracking-[0.01em] text-slate-900 dark:text-white sm:block"
|
|
@@ -441,6 +440,10 @@ export const HomeShowcase = () => {
|
|
|
441
440
|
</a>
|
|
442
441
|
</ITFlex>
|
|
443
442
|
|
|
443
|
+
<ITFlex align="center" className="it-reveal !mt-8">
|
|
444
|
+
<AxzyLogo variante="wordmark" tamano={300} />
|
|
445
|
+
</ITFlex>
|
|
446
|
+
|
|
444
447
|
<div className="mt-10 grid gap-10 lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-14">
|
|
445
448
|
<div className="min-w-0">
|
|
446
449
|
<ITText
|
|
@@ -783,12 +786,16 @@ export const HomeShowcase = () => {
|
|
|
783
786
|
<div
|
|
784
787
|
className={`${HOJA} flex flex-col items-start justify-between gap-3 py-7 sm:flex-row sm:items-center`}
|
|
785
788
|
>
|
|
786
|
-
<
|
|
787
|
-
|
|
788
|
-
className="
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
789
|
+
<div className="flex items-center gap-4">
|
|
790
|
+
<AxzyLogo variante="wordmark" tamano={190} />
|
|
791
|
+
<span aria-hidden="true" className="h-8 w-px bg-slate-200 dark:bg-slate-800" />
|
|
792
|
+
<ITText
|
|
793
|
+
as="p"
|
|
794
|
+
className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
795
|
+
>
|
|
796
|
+
{`Asael Amaro · ${t("showcase.home.role")}`}
|
|
797
|
+
</ITText>
|
|
798
|
+
</div>
|
|
792
799
|
<div className="flex flex-wrap items-center gap-x-5 gap-y-2">
|
|
793
800
|
<ITText
|
|
794
801
|
as="span"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { ITCard, ITJSONEditor, ITFlex } from "../index";
|
|
3
|
+
import { ShowcaseLayout } from "./ShowcaseLayout";
|
|
4
|
+
|
|
5
|
+
/** Documento de arranque de la demo. */
|
|
6
|
+
const initial = `[
|
|
7
|
+
{
|
|
8
|
+
"type": "card",
|
|
9
|
+
"props": { "title": "Demo JSON Editor" },
|
|
10
|
+
"children": [
|
|
11
|
+
{ "type": "text", "text": "Edita este JSON y mira el preview" },
|
|
12
|
+
{ "type": "button", "props": { "label": "Aceptar" } }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]`;
|
|
16
|
+
|
|
17
|
+
export const JsonEditorShowcase: React.FC = () => {
|
|
18
|
+
const [value, setValue] = useState(initial);
|
|
19
|
+
|
|
20
|
+
/** Un documento más grande para que luzcan el plegado y el árbol. */
|
|
21
|
+
const bigDocument = useMemo(
|
|
22
|
+
() =>
|
|
23
|
+
JSON.stringify(
|
|
24
|
+
{
|
|
25
|
+
app: "axzy-ui-system",
|
|
26
|
+
version: "1.6.0",
|
|
27
|
+
layers: {
|
|
28
|
+
atoms: ["button", "input", "text", "flex", "grid", "stack"],
|
|
29
|
+
molecules: ["card", "alert", "table", "json-editor"],
|
|
30
|
+
organisms: ["data-table", "dialog", "sidebar", "navbar"],
|
|
31
|
+
},
|
|
32
|
+
flags: { darkMode: true, i18n: true, a11y: true, treeView: true },
|
|
33
|
+
limits: { maxDepth: 12, maxNodes: 5000, timeout: null },
|
|
34
|
+
},
|
|
35
|
+
null,
|
|
36
|
+
2
|
|
37
|
+
),
|
|
38
|
+
[]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const code = `<ITJSONEditor
|
|
42
|
+
value={json}
|
|
43
|
+
onChange={setJson}
|
|
44
|
+
onParse={handleParse}
|
|
45
|
+
mode="text" // "text" | "tree"
|
|
46
|
+
indentSize={2} // 0 | 2 | 4 | 8
|
|
47
|
+
height={420}
|
|
48
|
+
showToolbar
|
|
49
|
+
showViewSettings
|
|
50
|
+
showValidation
|
|
51
|
+
showStatusBar
|
|
52
|
+
showFoldGutter
|
|
53
|
+
showLineNumbers
|
|
54
|
+
/>`;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ShowcaseLayout
|
|
58
|
+
title="ITJSONEditor"
|
|
59
|
+
description="Editor JSON con resaltado, gutter plegable, errores por línea/columna y árbol navegable. Sin dependencias nuevas."
|
|
60
|
+
demo={
|
|
61
|
+
<ITFlex direction="column" gap={4} className="w-full">
|
|
62
|
+
<ITJSONEditor id="json-editor-demo" value={value} onChange={setValue} height={420} indentSize={2} />
|
|
63
|
+
</ITFlex>
|
|
64
|
+
}
|
|
65
|
+
gallery={
|
|
66
|
+
<ITFlex direction="column" gap={4} className="w-full">
|
|
67
|
+
<ITCard title="Vista de árbol (mode="tree")">
|
|
68
|
+
<ITJSONEditor
|
|
69
|
+
value={bigDocument}
|
|
70
|
+
readOnly
|
|
71
|
+
mode="tree"
|
|
72
|
+
treeShowLines
|
|
73
|
+
height={320}
|
|
74
|
+
showToolbar={false}
|
|
75
|
+
showViewSettings={false}
|
|
76
|
+
/>
|
|
77
|
+
</ITCard>
|
|
78
|
+
<ITCard title="Documento grande con plegado">
|
|
79
|
+
<ITJSONEditor value={bigDocument} onChange={() => undefined} height={320} />
|
|
80
|
+
</ITCard>
|
|
81
|
+
<ITCard title="Lienzo claro y ajuste de línea (canvasTheme="light", wrapLines)">
|
|
82
|
+
<ITJSONEditor
|
|
83
|
+
value={bigDocument}
|
|
84
|
+
readOnly
|
|
85
|
+
canvasTheme="light"
|
|
86
|
+
wrapLines
|
|
87
|
+
height={260}
|
|
88
|
+
showTransformActions={false}
|
|
89
|
+
showImportButton={false}
|
|
90
|
+
/>
|
|
91
|
+
</ITCard>
|
|
92
|
+
<ITCard title="Estado de error: línea y columna señaladas">
|
|
93
|
+
<ITJSONEditor
|
|
94
|
+
value={'{\n "a": 1,\n "b": ,\n "c": 3\n}'}
|
|
95
|
+
height={220}
|
|
96
|
+
showStatusBar
|
|
97
|
+
/>
|
|
98
|
+
</ITCard>
|
|
99
|
+
</ITFlex>
|
|
100
|
+
}
|
|
101
|
+
code={code}
|
|
102
|
+
doc={{
|
|
103
|
+
summary: "Editor JSON para prototipar con ITJsonRenderer",
|
|
104
|
+
description:
|
|
105
|
+
"El resaltado se pinta en una capa bajo un textarea nativo, así que conserva el deshacer del navegador y no añade dependencias. Valida con JSON.parse y traduce el error a línea y columna.",
|
|
106
|
+
examples: [
|
|
107
|
+
'<ITJSONEditor value={json} onChange={setJson} />',
|
|
108
|
+
'<ITJSONEditor value={json} mode="tree" treeShowLines />',
|
|
109
|
+
'<ITJSONEditor value={json} indentSize={4} wrapLines canvasTheme="light" />',
|
|
110
|
+
'<ITJSONEditor value={json} readOnly showToolbar={false} height="100%" />',
|
|
111
|
+
],
|
|
112
|
+
props: [
|
|
113
|
+
{ name: "value", type: "string", default: '""', description: "Documento JSON como cadena." },
|
|
114
|
+
{ name: "onChange", type: "(v: string) => void", default: "—", description: "Se dispara con el texto completo en cada cambio." },
|
|
115
|
+
{ name: "onParse", type: "(parsed, isValid, error?) => void", default: "—", description: "Valor parseado y estado tras cada validación." },
|
|
116
|
+
{ name: "mode", type: '"text" | "tree"', default: '"text"', description: "Modo inicial del lienzo." },
|
|
117
|
+
{ name: "showModeSwitch", type: "boolean", default: "true", description: "Conmutador texto/árbol en la barra." },
|
|
118
|
+
{ name: "showToolbar", type: "boolean", default: "true", description: "Barra superior de acciones." },
|
|
119
|
+
{ name: "showViewSettings", type: "boolean", default: "true", description: "Fila de ajustes de vista." },
|
|
120
|
+
{ name: "showValidation", type: "boolean", default: "true", description: "Indicador válido/ inválido con L:C." },
|
|
121
|
+
{ name: "showStatusBar", type: "boolean", default: "true", description: "Pie con líneas, nodos, profundidad y tamaño." },
|
|
122
|
+
{ name: "showLineNumbers", type: "boolean", default: "true", description: "Números de línea en el gutter." },
|
|
123
|
+
{ name: "showFoldGutter", type: "boolean", default: "true", description: "Gutter de plegado de bloques." },
|
|
124
|
+
{ name: "showTransformActions", type: "boolean", default: "true", description: "Formatear, minificar, ordenar, escapar." },
|
|
125
|
+
{ name: "showCopyButton", type: "boolean", default: "true", description: "Botón de copiar el documento." },
|
|
126
|
+
{ name: "showImportButton", type: "boolean", default: "true", description: "Carga de un archivo .json." },
|
|
127
|
+
{ name: "showDownloadButton", type: "boolean", default: "true", description: "Descarga del documento." },
|
|
128
|
+
{ name: "indentSize", type: "number", default: "2", description: "Espacios por nivel (0, 2, 4 u 8)." },
|
|
129
|
+
{ name: "wrapLines", type: "boolean", default: "false", description: "Ajuste de línea dentro del ancho." },
|
|
130
|
+
{ name: "treeShowLines", type: "boolean", default: "false", description: "Muestra la línea de cada nodo y permite saltar." },
|
|
131
|
+
{ name: "canvasTheme", type: '"ink" | "light"', default: '"ink"', description: "Paleta del lienzo de código." },
|
|
132
|
+
{ name: "height", type: "number | string", default: "340", description: "Alto del área de edición." },
|
|
133
|
+
{ name: "readOnly", type: "boolean", default: "false", description: "Solo lectura, sin acciones de edición." },
|
|
134
|
+
{ name: "actions", type: "ReactNode", default: "—", description: "Acciones propias al final de la barra." },
|
|
135
|
+
{ name: "className", type: "string", default: "—", description: "Clases del contenedor raíz." },
|
|
136
|
+
{ name: "textareaClassName", type: "string", default: "—", description: "Clases extra del textarea." },
|
|
137
|
+
{ name: "errorLine", type: "number", default: "—", description: "Línea a marcar como errónea desde fuera." },
|
|
138
|
+
],
|
|
139
|
+
notes: [
|
|
140
|
+
"Atajos: Tab indenta el bloque, Mayús+Tab lo desangra, Mayús+Alt+F formatea, +M minifica y +S ordena.",
|
|
141
|
+
"Las llaves, corchetes y comillas se cierran solos y se borran por parejas.",
|
|
142
|
+
"Con wrapLines activo el resaltado de línea vigente se desactiva: la posición dependería de las vueltas de cada línea.",
|
|
143
|
+
"Es totalmente independiente de ITJsonRenderer: los une solo el JSON que tú les pases.",
|
|
144
|
+
],
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default JsonEditorShowcase;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { ITCard, ITJsonRenderer } from "../index";
|
|
3
|
+
import { ShowcaseLayout } from "./ShowcaseLayout";
|
|
4
|
+
|
|
5
|
+
const exampleWithChildren = [
|
|
6
|
+
{
|
|
7
|
+
type: "alert" as const,
|
|
8
|
+
props: { variant: "info" as const, title: "Información" },
|
|
9
|
+
text: "Esto se renderiza desde JSON",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
type: "card" as const,
|
|
13
|
+
props: { title: "Tarjeta desde JSON" },
|
|
14
|
+
children: [
|
|
15
|
+
{ type: "text" as const, text: "Contenido dentro de la tarjeta" },
|
|
16
|
+
{
|
|
17
|
+
type: "flex" as const,
|
|
18
|
+
props: { gap: 2, justify: "end" as const },
|
|
19
|
+
children: [
|
|
20
|
+
{ type: "button" as const, props: { label: "Cancelar", color: "gray" as const, variant: "outlined" as const } },
|
|
21
|
+
{ type: "button" as const, props: { label: "Aceptar", color: "primary" as const } },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const JsonRendererShowcase: React.FC = () => {
|
|
29
|
+
const [jsonText, setJsonText] = useState(JSON.stringify(exampleWithChildren, null, 2));
|
|
30
|
+
|
|
31
|
+
let parsed: any = exampleWithChildren;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(jsonText);
|
|
34
|
+
} catch {
|
|
35
|
+
parsed = exampleWithChildren;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ShowcaseLayout
|
|
40
|
+
title="ITJsonRenderer"
|
|
41
|
+
description="Renderiza componentes del UI System a partir de un JSON."
|
|
42
|
+
doc={{
|
|
43
|
+
summary: "PoC: Builder JSON para AXZY UI System",
|
|
44
|
+
description:
|
|
45
|
+
"Permite construir interfaces pasándole un árbol JSON con type, props, children/text.",
|
|
46
|
+
examples: [
|
|
47
|
+
'<ITJsonRenderer json={{ type: "button", props: { label: "Click" } }} />',
|
|
48
|
+
],
|
|
49
|
+
props: [],
|
|
50
|
+
}}
|
|
51
|
+
demo={
|
|
52
|
+
<ITCard title="Preview">
|
|
53
|
+
<ITJsonRenderer json={parsed} />
|
|
54
|
+
</ITCard>
|
|
55
|
+
}
|
|
56
|
+
code={jsonText}
|
|
57
|
+
controls={
|
|
58
|
+
<ITCard title="JSON Editor (editable)">
|
|
59
|
+
<textarea
|
|
60
|
+
value={jsonText}
|
|
61
|
+
onChange={(e) => setJsonText(e.target.value)}
|
|
62
|
+
className="w-full h-64 font-mono text-sm border rounded p-3"
|
|
63
|
+
/>
|
|
64
|
+
</ITCard>
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default JsonRendererShowcase;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { FaBolt, FaBug, FaCheck, FaTrashAlt } from "react-icons/fa";
|
|
3
|
+
import {
|
|
4
|
+
ITAlert,
|
|
5
|
+
ITBadget,
|
|
6
|
+
ITButton,
|
|
7
|
+
ITCard,
|
|
8
|
+
ITFlex,
|
|
9
|
+
ITJSONEditor,
|
|
10
|
+
ITSchemaRenderer,
|
|
11
|
+
ITText,
|
|
12
|
+
type JsonEngineIssue,
|
|
13
|
+
type ScreenSchema,
|
|
14
|
+
parsePartialSchema,
|
|
15
|
+
axzyCatalogTypes,
|
|
16
|
+
} from "../index";
|
|
17
|
+
import { ShowcaseLayout } from "./ShowcaseLayout";
|
|
18
|
+
|
|
19
|
+
/** Esquema de ejemplo: usa el catálogo real y muestra permisos y estado. */
|
|
20
|
+
const EJEMPLO: ScreenSchema = {
|
|
21
|
+
version: 1,
|
|
22
|
+
id: "laboratorio",
|
|
23
|
+
title: "Pantalla de ejemplo",
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "ITFlex",
|
|
27
|
+
props: { align: "center", justify: "between", wrap: "wrap", gap: 3 },
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
type: "ITFlex",
|
|
31
|
+
props: { align: "center", gap: 2 },
|
|
32
|
+
children: [
|
|
33
|
+
{ type: "ITBadget", props: { label: "Motor", color: "primary" } },
|
|
34
|
+
{ type: "ITText", props: { muted: true }, text: "Esquema interpretado en vivo" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "ITButton",
|
|
39
|
+
props: { label: "Acción declarada", color: "primary", size: "sm", onClick: { action: "saludar" } },
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "ITGrid",
|
|
45
|
+
props: { container: true, spacing: 3 },
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
type: "ITGrid",
|
|
49
|
+
props: { item: true, md: 6 },
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
type: "ITCard",
|
|
53
|
+
props: { title: "Listo para pintar" },
|
|
54
|
+
children: [
|
|
55
|
+
{ type: "ITText", text: "Este árbol viene de un JSON, no de TSX." },
|
|
56
|
+
{ type: "ITProgress", props: { value: 64, max: 100, size: "sm" } },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "ITGrid",
|
|
63
|
+
props: { item: true, md: 6 },
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
type: "ITAlert",
|
|
67
|
+
props: { variant: "info", title: "Casos cubiertos" },
|
|
68
|
+
children: [{ type: "ITText", text: "Si algo no está en el catálogo, se avisa y no se pinta." }],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Texto inicial del editor: el esquema anterior, formateado. */
|
|
78
|
+
const TEXTO_INICIAL = JSON.stringify(EJEMPLO, null, 2);
|
|
79
|
+
|
|
80
|
+
/** Velocidad del stream simulado (caracteres por tick). */
|
|
81
|
+
const CHUNK = 6;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Laboratorio del motor de pantallas declarativas.
|
|
85
|
+
*
|
|
86
|
+
* Izquierda: el esquema en `ITJSONEditor`. Derecha: lo que pinta el motor. El
|
|
87
|
+
* botón «emitir como stream» escribe el esquema carácter a carácter para ver el
|
|
88
|
+
* render progresivo y comprobar que lo pendiente no se pinta a medias.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Ruta del sandbox: #ui-system/feedback/jsonscreen
|
|
92
|
+
*/
|
|
93
|
+
export const JsonScreenShowcase: React.FC = () => {
|
|
94
|
+
const [texto, setTexto] = useState(TEXTO_INICIAL);
|
|
95
|
+
const [streaming, setStreaming] = useState(false);
|
|
96
|
+
const [emitido, setEmitido] = useState(TEXTO_INICIAL.length);
|
|
97
|
+
const [avisos, setAvisos] = useState<JsonEngineIssue[]>([]);
|
|
98
|
+
const timer = useRef<number | null>(null);
|
|
99
|
+
|
|
100
|
+
/** Estado del esquema: mientras se emite, se parsea en modo tolerante. */
|
|
101
|
+
const estado = useMemo(() => {
|
|
102
|
+
if (!streaming) {
|
|
103
|
+
try {
|
|
104
|
+
const completo = JSON.parse(texto) as ScreenSchema;
|
|
105
|
+
return { tree: completo, status: "complete" as const, pending: 0, roto: false };
|
|
106
|
+
} catch {
|
|
107
|
+
return { tree: null, status: "complete" as const, pending: 0, roto: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const parcial = parsePartialSchema(texto);
|
|
111
|
+
return {
|
|
112
|
+
tree: parcial.tree,
|
|
113
|
+
status: parcial.status,
|
|
114
|
+
pending: parcial.pendingNodes,
|
|
115
|
+
roto: parcial.broken,
|
|
116
|
+
};
|
|
117
|
+
}, [texto, streaming]);
|
|
118
|
+
|
|
119
|
+
/** Detiene el stream y limpia el temporizador. */
|
|
120
|
+
const detener = useCallback(() => {
|
|
121
|
+
if (timer.current !== null) {
|
|
122
|
+
window.clearInterval(timer.current);
|
|
123
|
+
timer.current = null;
|
|
124
|
+
}
|
|
125
|
+
setStreaming(false);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
useEffect(() => () => detener(), [detener]);
|
|
129
|
+
|
|
130
|
+
/** Emite el esquema carácter a carácter, como haría un modelo. */
|
|
131
|
+
const emitirStream = useCallback(() => {
|
|
132
|
+
detener();
|
|
133
|
+
setAvisos([]);
|
|
134
|
+
setEmitido(0);
|
|
135
|
+
setTexto("");
|
|
136
|
+
setStreaming(true);
|
|
137
|
+
|
|
138
|
+
const total = TEXTO_INICIAL;
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
|
|
141
|
+
timer.current = window.setInterval(() => {
|
|
142
|
+
cursor = Math.min(total.length, cursor + CHUNK);
|
|
143
|
+
setTexto(total.slice(0, cursor));
|
|
144
|
+
setEmitido(cursor);
|
|
145
|
+
if (cursor >= total.length) {
|
|
146
|
+
window.clearInterval(timer.current as number);
|
|
147
|
+
timer.current = null;
|
|
148
|
+
setStreaming(false);
|
|
149
|
+
}
|
|
150
|
+
}, 24);
|
|
151
|
+
}, [detener]);
|
|
152
|
+
|
|
153
|
+
/** Registro de acciones del host: el esquema solo puede pedir estos nombres. */
|
|
154
|
+
const resolveAction = useCallback(
|
|
155
|
+
() => () => {
|
|
156
|
+
window.alert("Acción declarada ejecutada por el host.");
|
|
157
|
+
},
|
|
158
|
+
[]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const registrarAviso = useCallback((issue: JsonEngineIssue) => {
|
|
162
|
+
setAvisos((previos) =>
|
|
163
|
+
previos.some((item) => item.code === issue.code && item.path === issue.path)
|
|
164
|
+
? previos
|
|
165
|
+
: [...previos, issue].slice(-8)
|
|
166
|
+
);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const codigo = `<ITSchemaRenderer
|
|
170
|
+
schema={parsePartialSchema(texto).tree}
|
|
171
|
+
onIssue={(issue) => registrar(issue)}
|
|
172
|
+
resolveAction={() => actions[accion]}
|
|
173
|
+
pendingFallback={<ITSkeleton />}
|
|
174
|
+
/>`;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<ShowcaseLayout
|
|
178
|
+
title="Motor de pantallas"
|
|
179
|
+
description="ITSchemaRenderer interpreta un esquema JSON contra un catálogo de componentes con lista blanca. Pinta mientras el esquema llega por trozos, avisa con código y ruta, y aísla los nodos que fallan."
|
|
180
|
+
demo={
|
|
181
|
+
<div className="grid w-full gap-4 xl:grid-cols-[minmax(0,380px)_minmax(0,1fr)]">
|
|
182
|
+
<div className="min-w-0">
|
|
183
|
+
<ITCard title="Esquema (JSON)">
|
|
184
|
+
<ITJSONEditor value={texto} onChange={setTexto} height={420} readOnly={streaming} />
|
|
185
|
+
</ITCard>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="min-w-0">
|
|
188
|
+
<ITCard title="Pantalla resultante">
|
|
189
|
+
<div className="min-h-[420px]">
|
|
190
|
+
{estado.roto && !estado.tree ? (
|
|
191
|
+
<ITAlert variant="error" title="El esquema no es JSON válido">
|
|
192
|
+
Corrige el texto de la izquierda para volver a pintar.
|
|
193
|
+
</ITAlert>
|
|
194
|
+
) : (
|
|
195
|
+
<ITSchemaRenderer
|
|
196
|
+
schema={estado.tree}
|
|
197
|
+
onIssue={registrarAviso}
|
|
198
|
+
resolveAction={resolveAction}
|
|
199
|
+
pendingFallback={<ITText muted>·</ITText>}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<ITFlex align="center" gap={2} wrap="wrap" className="mt-4 border-t border-slate-200 pt-3 dark:border-slate-800">
|
|
205
|
+
<ITButton
|
|
206
|
+
label={streaming ? "Emitiendo…" : "Emitir como stream"}
|
|
207
|
+
icon={<FaBolt size={10} />}
|
|
208
|
+
size="sm"
|
|
209
|
+
color="primary"
|
|
210
|
+
onClick={emitirStream}
|
|
211
|
+
disabled={streaming}
|
|
212
|
+
/>
|
|
213
|
+
<ITButton
|
|
214
|
+
label="Documentación del motor"
|
|
215
|
+
icon={<FaBolt size={10} />}
|
|
216
|
+
size="sm"
|
|
217
|
+
variant="text"
|
|
218
|
+
color="gray"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
window.location.hash = "#demo/app/docs";
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
<ITButton
|
|
224
|
+
label="Ver panel (sistema completo)"
|
|
225
|
+
icon={<FaBolt size={10} />}
|
|
226
|
+
size="sm"
|
|
227
|
+
color="primary"
|
|
228
|
+
onClick={() => {
|
|
229
|
+
window.location.hash = "#demo/app/inicio";
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
<ITButton
|
|
233
|
+
label="Ver portafolio de demo"
|
|
234
|
+
icon={<FaBolt size={10} />}
|
|
235
|
+
size="sm"
|
|
236
|
+
variant="outlined"
|
|
237
|
+
color="gray"
|
|
238
|
+
onClick={() => {
|
|
239
|
+
window.location.hash = "#demo";
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
<ITButton
|
|
243
|
+
label="Reiniciar"
|
|
244
|
+
icon={<FaTrashAlt size={10} />}
|
|
245
|
+
size="sm"
|
|
246
|
+
variant="outlined"
|
|
247
|
+
color="gray"
|
|
248
|
+
onClick={() => {
|
|
249
|
+
detener();
|
|
250
|
+
setTexto(TEXTO_INICIAL);
|
|
251
|
+
setEmitido(TEXTO_INICIAL.length);
|
|
252
|
+
setAvisos([]);
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
<ITText
|
|
256
|
+
as="span"
|
|
257
|
+
className="font-mono !text-[10px] uppercase !tracking-[0.12em]"
|
|
258
|
+
muted
|
|
259
|
+
>
|
|
260
|
+
{streaming
|
|
261
|
+
? `stream ${emitido}/${TEXTO_INICIAL.length} · ${estado.pending} nodos pendientes`
|
|
262
|
+
: `esquema completo · ${axzyCatalogTypes.length} tipos en el catálogo`}
|
|
263
|
+
</ITText>
|
|
264
|
+
</ITFlex>
|
|
265
|
+
|
|
266
|
+
{avisos.length > 0 && (
|
|
267
|
+
<div className="mt-3 space-y-1.5">
|
|
268
|
+
<ITFlex align="center" gap={1.5}>
|
|
269
|
+
<FaBug size={10} className="text-amber-500" />
|
|
270
|
+
<ITText
|
|
271
|
+
as="span"
|
|
272
|
+
className="font-mono !text-[10px] uppercase !tracking-[0.12em]"
|
|
273
|
+
muted
|
|
274
|
+
>
|
|
275
|
+
Avisos del motor
|
|
276
|
+
</ITText>
|
|
277
|
+
</ITFlex>
|
|
278
|
+
{avisos.map((aviso) => (
|
|
279
|
+
<ITFlex
|
|
280
|
+
key={`${aviso.code}-${aviso.path}`}
|
|
281
|
+
align="start"
|
|
282
|
+
gap={2}
|
|
283
|
+
className="rounded-[6px] border border-slate-200 px-2 py-1.5 dark:border-slate-800"
|
|
284
|
+
>
|
|
285
|
+
<ITBadget
|
|
286
|
+
label={aviso.code}
|
|
287
|
+
color={aviso.severity === "error" ? "danger" : "warning"}
|
|
288
|
+
size="sm"
|
|
289
|
+
/>
|
|
290
|
+
<div className="min-w-0">
|
|
291
|
+
<p className="text-[11px] leading-snug text-slate-700 dark:text-slate-300">{aviso.message}</p>
|
|
292
|
+
<p className="font-mono text-[10px] text-slate-500 dark:text-slate-400">{aviso.path || "raíz"}</p>
|
|
293
|
+
</div>
|
|
294
|
+
</ITFlex>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{!streaming && avisos.length === 0 && (
|
|
300
|
+
<ITFlex align="center" gap={1.5} className="mt-3">
|
|
301
|
+
<FaCheck size={10} className="text-emerald-500" />
|
|
302
|
+
<ITText as="span" className="font-mono !text-[10px] uppercase !tracking-[0.12em]" muted>
|
|
303
|
+
Sin avisos: el esquema cabe entero en el catálogo
|
|
304
|
+
</ITText>
|
|
305
|
+
</ITFlex>
|
|
306
|
+
)}
|
|
307
|
+
</ITCard>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
}
|
|
311
|
+
code={codigo}
|
|
312
|
+
doc={{
|
|
313
|
+
summary: "Ejecuta un esquema JSON contra el catálogo de componentes, con lista blanca y avisos.",
|
|
314
|
+
description:
|
|
315
|
+
"El esquema se trata como entrada no confiable: un tipo fuera del catálogo no rompe la pantalla, una prop prohibida se ignora con aviso y un componente que lanza queda aislado. Durante el stream, los nodos pendientes no se pintan a medias.",
|
|
316
|
+
examples: [
|
|
317
|
+
'<ITSchemaRenderer schema={{ version: 1, id: "demo", content: { type: "ITText", text: "Hola" } }} />',
|
|
318
|
+
'<ITSchemaRenderer schema={parsePartialSchema(tokens).tree} pendingFallback={<ITSkeleton />} />',
|
|
319
|
+
'<ITSchemaRenderer schema={schema} onIssue={(issue) => console.warn(issue.code, issue.path)} />',
|
|
320
|
+
],
|
|
321
|
+
props: [
|
|
322
|
+
{ name: "schema", type: "ScreenSchema | null", required: true, description: "Esquema de pantalla; admite árbol parcial." },
|
|
323
|
+
{ name: "catalog", type: "Catalog", default: "axzyCatalog", description: "Componentes y props permitidos." },
|
|
324
|
+
{ name: "limits", type: "Partial<EngineLimits>", default: "—", description: "Presupuesto de nodos, profundidad y tiempos." },
|
|
325
|
+
{ name: "resolveValue", type: "(value, path) => unknown", default: "—", description: "Resuelve referencias ($state, $param…)." },
|
|
326
|
+
{ name: "resolveAction", type: "(node, prop, action, path) => fn", default: "—", description: "Traduce acciones declaradas a handlers del host." },
|
|
327
|
+
{ name: "pendingFallback", type: "ReactNode", default: "—", description: "Qué pintar mientras un nodo está pendiente." },
|
|
328
|
+
{ name: "onIssue", type: "(issue) => void", default: "—", description: "Avisos con código y ruta." },
|
|
329
|
+
{ name: "className", type: "string", default: "—", description: "Clases del contenedor raíz." },
|
|
330
|
+
],
|
|
331
|
+
notes: [
|
|
332
|
+
`El catálogo de esta fase expone ${axzyCatalogTypes.length} tipos con lista blanca de props.`,
|
|
333
|
+
"Códigos de aviso: JR010 tipo fuera del catálogo · JR011 prop ignorada · JR012 nodo aislado · JR013 presupuesto excedido.",
|
|
334
|
+
"Pulsa «Emitir como stream» para ver el render progresivo: lo pendiente no se pinta a medias.",
|
|
335
|
+
],
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export default JsonScreenShowcase;
|
|
@@ -37,6 +37,7 @@ import type { ITSidebarProps } from "../index";
|
|
|
37
37
|
import { ShowcaseLayout, CodeViewer } from "./ShowcaseLayout";
|
|
38
38
|
import { EjemploContenedor } from "@/sandbox/ui";
|
|
39
39
|
import { useITTranslation } from "@/i18n";
|
|
40
|
+
import { AxzyLogo } from "@/sandbox/AxzyLogo";
|
|
40
41
|
|
|
41
42
|
// ─────────────────────────────────────────
|
|
42
43
|
// 1. ITCard Showcase
|
|
@@ -972,7 +973,7 @@ export const LayoutShowcase = () => {
|
|
|
972
973
|
const [navActiveId, setNavActiveId] = useState<string | null>("1");
|
|
973
974
|
|
|
974
975
|
const topBar = {
|
|
975
|
-
logo: <
|
|
976
|
+
logo: <AxzyLogo variante="icono" tamano={36} etiqueta="" />,
|
|
976
977
|
logoText: "AXZY Console",
|
|
977
978
|
centerContent: (
|
|
978
979
|
<input
|
|
@@ -1155,6 +1156,7 @@ export const LayoutShowcase = () => {
|
|
|
1155
1156
|
numero="02"
|
|
1156
1157
|
titulo={t("showcase.structure.galleryNavbarStandalone")}
|
|
1157
1158
|
codigo={`<ITNavbar
|
|
1159
|
+
logo={<AxzyLogo variante="icono" tamano={28} etiqueta="" />}
|
|
1158
1160
|
logoText="AXZY"
|
|
1159
1161
|
navigationItems={[
|
|
1160
1162
|
{ id: "1", label: "Inicio", icon: <FaHome />, isActive: true },
|
|
@@ -1169,6 +1171,7 @@ export const LayoutShowcase = () => {
|
|
|
1169
1171
|
>
|
|
1170
1172
|
<div className="h-[380px] overflow-hidden [&>div]:!h-full">
|
|
1171
1173
|
<ITNavbar
|
|
1174
|
+
logo={<AxzyLogo variante="icono" tamano={28} etiqueta="" />}
|
|
1172
1175
|
logoText="AXZY"
|
|
1173
1176
|
navigationItems={navbarItems}
|
|
1174
1177
|
userMenu={{
|
|
@@ -1197,6 +1200,7 @@ export const LayoutShowcase = () => {
|
|
|
1197
1200
|
numero="03"
|
|
1198
1201
|
titulo={t("showcase.structure.galleryNavbarSubmenus")}
|
|
1199
1202
|
codigo={`<ITNavbar
|
|
1203
|
+
logo={<AxzyLogo variante="icono" tamano={28} etiqueta="" />}
|
|
1200
1204
|
logoText="Admin"
|
|
1201
1205
|
navigationItems={[
|
|
1202
1206
|
{ id: "u", label: "Usuarios", icon: <FaUsers /> },
|
|
@@ -1215,6 +1219,7 @@ export const LayoutShowcase = () => {
|
|
|
1215
1219
|
>
|
|
1216
1220
|
<div className="h-[380px] overflow-hidden [&>div]:!h-full">
|
|
1217
1221
|
<ITNavbar
|
|
1222
|
+
logo={<AxzyLogo variante="icono" tamano={28} etiqueta="" />}
|
|
1218
1223
|
logoText="Admin"
|
|
1219
1224
|
navigationItems={[
|
|
1220
1225
|
{ id: "u", label: t("showcase.structure.navUsers"), icon: <FaUsers /> },
|