@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
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
|
+
"version": "1.7.1",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": [
|
|
10
10
|
"*.css"
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"build:app": "tsc -b && vite build -c vite.app.config.ts",
|
|
34
34
|
"build:css": "node scripts/build-css.mjs",
|
|
35
35
|
"lint": "eslint . && node scripts/check-atomic-deps.mjs && node scripts/check-css-isolation.mjs",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
36
38
|
"check:atomic": "node scripts/check-atomic-deps.mjs",
|
|
37
39
|
"check:css": "node scripts/check-css-isolation.mjs",
|
|
38
40
|
"preview": "vite preview",
|
|
@@ -44,7 +46,8 @@
|
|
|
44
46
|
"ext:package": "node vscode-extension/esbuild.mjs && cd vscode-extension && npx --yes @vscode/vsce package --no-dependencies",
|
|
45
47
|
"audit:sandbox": "node scripts/audit-sandbox.mjs",
|
|
46
48
|
"docs:llm": "pnpm ext:catalog && node scripts/build-llm-docs.mjs && node scripts/build-ai-bundle.mjs",
|
|
47
|
-
"docs:ai": "node scripts/build-ai-bundle.mjs"
|
|
49
|
+
"docs:ai": "node scripts/build-ai-bundle.mjs",
|
|
50
|
+
"docs:json-renderer": "node scripts/build-json-renderer-docs.mjs"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
50
53
|
"clsx": "^2.1.1",
|
|
@@ -61,22 +64,26 @@
|
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@eslint/js": "9.19.0",
|
|
67
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
68
|
+
"@testing-library/dom": "^10.4.2",
|
|
69
|
+
"@testing-library/react": "^16.3.3",
|
|
64
70
|
"@types/node": "^22.13.1",
|
|
65
71
|
"@types/react": "19.0.8",
|
|
66
72
|
"@types/react-dom": "19.0.3",
|
|
73
|
+
"@types/vscode": "1.85.0",
|
|
67
74
|
"@vitejs/plugin-react-swc": "3.8.0",
|
|
75
|
+
"esbuild": "^0.25.0",
|
|
68
76
|
"eslint": "9.19.0",
|
|
69
77
|
"eslint-plugin-react-hooks": "5.0.0",
|
|
70
78
|
"eslint-plugin-react-refresh": "0.4.18",
|
|
71
79
|
"globals": "15.14.0",
|
|
80
|
+
"jsdom": "^26.1.0",
|
|
81
|
+
"tailwindcss": "^4.0.0",
|
|
82
|
+
"tsup": "^8.3.6",
|
|
72
83
|
"typescript": "~5.7.2",
|
|
73
84
|
"typescript-eslint": "8.22.0",
|
|
74
85
|
"vite": "6.1.0",
|
|
75
|
-
"@tailwindcss/vite": "^4.0.0",
|
|
76
|
-
"tailwindcss": "^4.0.0",
|
|
77
|
-
"tsup": "^8.3.6",
|
|
78
86
|
"vite-plugin-dts": "^4.5.0",
|
|
79
|
-
"
|
|
80
|
-
"esbuild": "^0.25.0"
|
|
87
|
+
"vitest": "^3.2.7"
|
|
81
88
|
}
|
|
82
89
|
}
|
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { ITLayout, ITThemeProvider, ITI18nProvider, useITTranslation, useDebouncedSearch } from "./index";
|
|
3
|
+
import { DEFAULT_AXZY_CONFIG, mergeAxzyConfig, type AxzyConfig } from "./sandbox/axzyConfig";
|
|
3
4
|
import type { ITNavigationItem } from "./index";
|
|
4
5
|
|
|
5
6
|
// Import Showcases
|
|
@@ -65,6 +66,11 @@ import {
|
|
|
65
66
|
LoaderShowcase,
|
|
66
67
|
ThemeProviderShowcase,
|
|
67
68
|
} from "./showcases/FeedbackShowcases";
|
|
69
|
+
import JsonRendererShowcase from "./showcases/JsonRendererShowcase";
|
|
70
|
+
import JsonEditorShowcase from "./showcases/JsonEditorShowcase";
|
|
71
|
+
import JsonScreenShowcase from "./showcases/JsonScreenShowcase";
|
|
72
|
+
import DemoJsonPortfolio from "./showcases/DemoJsonPortfolio";
|
|
73
|
+
import DemoJsonDashboard, { DEMO_APP_PAGES, type DemoAppPage } from "./showcases/DemoJsonDashboard";
|
|
68
74
|
|
|
69
75
|
// Sandbox routing / landing (dev-only, never published)
|
|
70
76
|
import {
|
|
@@ -75,6 +81,7 @@ import { useHashRoute } from "./sandbox/useHashRoute";
|
|
|
75
81
|
import TopbarSearch from "./sandbox/TopbarSearch";
|
|
76
82
|
import { LandingShowcase } from "./sandbox/LandingShowcase";
|
|
77
83
|
import NotFoundShowcase from "./sandbox/NotFoundShowcase";
|
|
84
|
+
import { AxzyLogo } from "./sandbox/AxzyLogo";
|
|
78
85
|
import { sandboxMessages, translateLabel } from "./sandbox/i18n";
|
|
79
86
|
import { SandboxLocaleProvider, useSandboxLocale } from "./sandbox/locale";
|
|
80
87
|
import { SelectorIdioma, SelectorTema } from "./sandbox/controls";
|
|
@@ -87,7 +94,15 @@ import { GROUP_ICONS } from "./sandbox/icons";
|
|
|
87
94
|
*/
|
|
88
95
|
function AppShell() {
|
|
89
96
|
const { t } = useITTranslation();
|
|
90
|
-
const { route, goToItem, goToLanding, goToPortfolio } = useHashRoute();
|
|
97
|
+
const { route, goToItem, goToLanding, goToPortfolio, goToDemo } = useHashRoute();
|
|
98
|
+
// Configuración global del sandbox: `public/axzy.json`.
|
|
99
|
+
const [configGlobal, setConfigGlobal] = useState<AxzyConfig>(DEFAULT_AXZY_CONFIG);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
fetch("/axzy.json")
|
|
102
|
+
.then((r) => r.json())
|
|
103
|
+
.then((json: unknown) => setConfigGlobal(mergeAxzyConfig(json)))
|
|
104
|
+
.catch(() => setConfigGlobal(DEFAULT_AXZY_CONFIG));
|
|
105
|
+
}, []);
|
|
91
106
|
const [subitemConnector] = useState<"dot" | "|" | "none">("dot");
|
|
92
107
|
|
|
93
108
|
// Debounced term drives filtering; `searchValue` is the live input value.
|
|
@@ -172,6 +187,9 @@ function AppShell() {
|
|
|
172
187
|
};
|
|
173
188
|
|
|
174
189
|
const topBarProps = {
|
|
190
|
+
// La marca axzydev sustituye al nombre suelto: el wordmark ya dice a quién
|
|
191
|
+
// pertenece el catálogo y el texto se mantiene como respaldo accesible.
|
|
192
|
+
logo: <AxzyLogo variante="icono" tamano={22} />,
|
|
175
193
|
logoText: "AXZY UI System",
|
|
176
194
|
centerContent: (
|
|
177
195
|
<TopbarSearch
|
|
@@ -202,10 +220,16 @@ function AppShell() {
|
|
|
202
220
|
label: t("sandbox.nav.portfolio"),
|
|
203
221
|
action: () => goToPortfolio(),
|
|
204
222
|
},
|
|
223
|
+
{
|
|
224
|
+
id: "demo",
|
|
225
|
+
label: t("sandbox.nav.demo"),
|
|
226
|
+
action: () => goToDemo(),
|
|
227
|
+
},
|
|
205
228
|
],
|
|
206
229
|
onNavItemClick: (id: string) => {
|
|
207
230
|
if (id === "ui-system") goToLanding();
|
|
208
231
|
else if (id === "portfolio") goToPortfolio();
|
|
232
|
+
else if (id === "demo") goToDemo();
|
|
209
233
|
},
|
|
210
234
|
// Sin `userMenu`: el sandbox no tiene sesión. Antes mostraba un usuario
|
|
211
235
|
// inventado ("Alex Dev / alex@axzy.dev") en un portafolio real.
|
|
@@ -309,6 +333,12 @@ function AppShell() {
|
|
|
309
333
|
return <LoaderShowcase />;
|
|
310
334
|
case "themeprovider":
|
|
311
335
|
return <ThemeProviderShowcase />;
|
|
336
|
+
case "jsonrenderer":
|
|
337
|
+
return <JsonRendererShowcase />;
|
|
338
|
+
case "jsoneditor":
|
|
339
|
+
return <JsonEditorShowcase />;
|
|
340
|
+
case "jsonscreen":
|
|
341
|
+
return <JsonScreenShowcase />;
|
|
312
342
|
default:
|
|
313
343
|
return (
|
|
314
344
|
<NotFoundShowcase
|
|
@@ -321,7 +351,14 @@ function AppShell() {
|
|
|
321
351
|
};
|
|
322
352
|
|
|
323
353
|
return (
|
|
324
|
-
<ITThemeProvider
|
|
354
|
+
<ITThemeProvider
|
|
355
|
+
showFab={configGlobal.showFab}
|
|
356
|
+
density={configGlobal.density}
|
|
357
|
+
radius={configGlobal.radius}
|
|
358
|
+
shadow={configGlobal.shadow}
|
|
359
|
+
appearance={configGlobal.appearance}
|
|
360
|
+
theme={configGlobal.theme}
|
|
361
|
+
>
|
|
325
362
|
{route.view === "producto" ? (
|
|
326
363
|
<div className="min-h-[100dvh] bg-[#fafafa] dark:bg-[#0b0d10]">
|
|
327
364
|
{route.valid ? (
|
|
@@ -337,6 +374,37 @@ function AppShell() {
|
|
|
337
374
|
/>
|
|
338
375
|
)}
|
|
339
376
|
</div>
|
|
377
|
+
) : route.view === "demo" ? (
|
|
378
|
+
route.group === "app" ? (
|
|
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}`;
|
|
385
|
+
}}
|
|
386
|
+
onExit={() => {
|
|
387
|
+
window.location.hash = "#demo";
|
|
388
|
+
}}
|
|
389
|
+
filterEstado={route.query.estado ?? "Todos"}
|
|
390
|
+
onFilterChange={(estado) => {
|
|
391
|
+
const base = `#demo/app/${route.item ?? "inicio"}`;
|
|
392
|
+
window.location.hash = estado === "Todos" ? base : `${base}?estado=${encodeURIComponent(estado)}`;
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
) : (
|
|
396
|
+
<div className="min-h-[100dvh] bg-[#fafafa] dark:bg-[#0b0d10]">
|
|
397
|
+
<DemoJsonPortfolio
|
|
398
|
+
onBack={() => goToLanding()}
|
|
399
|
+
onOpenProject={(id) => {
|
|
400
|
+
window.location.hash = `#proyecto/${id}`;
|
|
401
|
+
}}
|
|
402
|
+
onOpenPanel={() => {
|
|
403
|
+
window.location.hash = "#demo/app/inicio";
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
340
408
|
) : route.view === "home" ? (
|
|
341
409
|
<div className="min-h-[100dvh] bg-[#fafafa] dark:bg-[#0b0d10]">
|
|
342
410
|
<HomeShowcase />
|
|
@@ -7,6 +7,8 @@ export interface ITImageProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
/** Fallback image URL shown when the primary `src` fails to load */
|
|
9
9
|
fallback?: string;
|
|
10
|
+
/** How the image fills its box: "cover" crops to fill (default), "contain" shows the whole image */
|
|
11
|
+
fit?: "cover" | "contain";
|
|
10
12
|
/** Click handler */
|
|
11
13
|
onClick?: () => void;
|
|
12
14
|
}
|
|
@@ -26,6 +26,7 @@ const ITImage = ({
|
|
|
26
26
|
alt,
|
|
27
27
|
className = "",
|
|
28
28
|
fallback = "",
|
|
29
|
+
fit = "cover",
|
|
29
30
|
}: ITImageProps) => {
|
|
30
31
|
const [imageError, setImageError] = useState(false);
|
|
31
32
|
const { t } = useITTranslation();
|
|
@@ -42,14 +43,14 @@ const ITImage = ({
|
|
|
42
43
|
<img
|
|
43
44
|
src={fallback}
|
|
44
45
|
alt={t("image.fallbackAlt")}
|
|
45
|
-
className="w-full h-full
|
|
46
|
+
className="w-full h-full" style={{ objectFit: fit }}
|
|
46
47
|
/>
|
|
47
48
|
) : (
|
|
48
49
|
<img
|
|
49
50
|
src={src}
|
|
50
51
|
alt={alt}
|
|
51
52
|
onError={handleImageError}
|
|
52
|
-
className="w-full h-full
|
|
53
|
+
className="w-full h-full" style={{ objectFit: fit }}
|
|
53
54
|
/>
|
|
54
55
|
)}
|
|
55
56
|
</div>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { FaChevronDown, FaChevronRight, FaCrosshairs } from "react-icons/fa";
|
|
4
|
+
import { useITTranslation } from "@/i18n";
|
|
5
|
+
import { pathKey, previewValue, valueKind, type ITCodeAnalysis } from "./json-editor.utils";
|
|
6
|
+
|
|
7
|
+
/** Un nodo del árbol ya resuelto, listo para pintar. */
|
|
8
|
+
interface TreeNode {
|
|
9
|
+
/** Clave o índice con el que se muestra (vacío en la raíz). */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Ruta absoluta dentro del documento, como clave estable. */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Ruta absoluta troceada (evita re-parsear la clave: las claves pueden llevar puntos). */
|
|
14
|
+
path: (string | number)[];
|
|
15
|
+
/** Profundidad de anidación (la raíz es 0). */
|
|
16
|
+
level: number;
|
|
17
|
+
/** Valor del nodo. */
|
|
18
|
+
value: unknown;
|
|
19
|
+
/** Tipo del valor. */
|
|
20
|
+
kind: ReturnType<typeof valueKind>;
|
|
21
|
+
/** Número de hijos directos. */
|
|
22
|
+
childCount: number;
|
|
23
|
+
/** Hijos, solo en objetos y arrays. */
|
|
24
|
+
children: TreeNode[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Props de `ITCodeTreeView`. */
|
|
28
|
+
export interface ITCodeTreeViewProps {
|
|
29
|
+
/** Valor ya parseado que hay que representar. */
|
|
30
|
+
value: unknown;
|
|
31
|
+
/** Análisis del texto, para poder saltar a la línea de cada nodo. */
|
|
32
|
+
analysis: ITCodeAnalysis;
|
|
33
|
+
/** Muestra la línea de cada nodo y permite saltar a ella. @default false */
|
|
34
|
+
showLines?: boolean;
|
|
35
|
+
/** Se dispara al pulsar el botón de salto de un nodo. */
|
|
36
|
+
onJumpToLine?: (line: number) => void;
|
|
37
|
+
/** Clases del contenedor. */
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Número máximo de filas pintadas; evita colgar la vista con JSON enormes. */
|
|
42
|
+
const MAX_ROWS = 1500;
|
|
43
|
+
/** Niveles que vienen desplegados al abrir el árbol. */
|
|
44
|
+
const AUTO_EXPANDED_LEVELS = 2;
|
|
45
|
+
|
|
46
|
+
/** Construye el árbol de nodos a partir de un valor ya parseado. */
|
|
47
|
+
function buildTree(
|
|
48
|
+
value: unknown,
|
|
49
|
+
label: string,
|
|
50
|
+
path: (string | number)[],
|
|
51
|
+
level: number
|
|
52
|
+
): TreeNode {
|
|
53
|
+
const kind = valueKind(value);
|
|
54
|
+
const children: TreeNode[] = [];
|
|
55
|
+
if (kind === "object") {
|
|
56
|
+
for (const [childKey, nested] of Object.entries(value as Record<string, unknown>)) {
|
|
57
|
+
children.push(buildTree(nested, childKey, [...path, childKey], level + 1));
|
|
58
|
+
}
|
|
59
|
+
} else if (kind === "array") {
|
|
60
|
+
(value as unknown[]).forEach((nested, index) => {
|
|
61
|
+
children.push(buildTree(nested, String(index), [...path, index], level + 1));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
label,
|
|
66
|
+
key: pathKey(path),
|
|
67
|
+
path,
|
|
68
|
+
level,
|
|
69
|
+
value,
|
|
70
|
+
kind,
|
|
71
|
+
childCount: children.length,
|
|
72
|
+
children,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `ITCodeTreeView` — árbol navegable de un JSON ya parseado.
|
|
78
|
+
*
|
|
79
|
+
* Pieza visual interna de `ITJSONEditor`: no valida ni parsea, solo pinta lo que
|
|
80
|
+
* recibe. Se exporta aparte por si hace falta reutilizarla en paneles o en
|
|
81
|
+
* documentación.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <ITCodeTreeView value={{ a: 1 }} analysis={analysis} showLines onJumpToLine={setLine} />
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export default function ITCodeTreeView({
|
|
89
|
+
value,
|
|
90
|
+
analysis,
|
|
91
|
+
showLines = false,
|
|
92
|
+
onJumpToLine,
|
|
93
|
+
className,
|
|
94
|
+
}: ITCodeTreeViewProps) {
|
|
95
|
+
const { t } = useITTranslation();
|
|
96
|
+
const root = useMemo(() => buildTree(value, "", [], 0), [value]);
|
|
97
|
+
/** Nodos plegados por el usuario; el resto sigue la heurística de niveles. */
|
|
98
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
99
|
+
|
|
100
|
+
/** Filas visibles ya aplanadas, con el corte por `MAX_ROWS` aplicado. */
|
|
101
|
+
const rows = useMemo(() => {
|
|
102
|
+
const result: TreeNode[] = [];
|
|
103
|
+
const isOpen = (node: TreeNode) => {
|
|
104
|
+
const explicit = collapsed[node.key];
|
|
105
|
+
if (explicit !== undefined) return !explicit;
|
|
106
|
+
return node.level < AUTO_EXPANDED_LEVELS;
|
|
107
|
+
};
|
|
108
|
+
const walk = (node: TreeNode) => {
|
|
109
|
+
if (result.length >= MAX_ROWS) return;
|
|
110
|
+
result.push(node);
|
|
111
|
+
if (node.childCount && isOpen(node)) node.children.forEach(walk);
|
|
112
|
+
};
|
|
113
|
+
if (root.kind === "object" || root.kind === "array") root.children.forEach(walk);
|
|
114
|
+
else result.push(root);
|
|
115
|
+
return result;
|
|
116
|
+
}, [root, collapsed]);
|
|
117
|
+
|
|
118
|
+
const truncated = rows.length >= MAX_ROWS;
|
|
119
|
+
|
|
120
|
+
const toggle = (key: string, level: number) =>
|
|
121
|
+
setCollapsed((previous) => ({
|
|
122
|
+
...previous,
|
|
123
|
+
// Sin decisión previa, la fila estaba abierta si su nivel se auto-despliega.
|
|
124
|
+
[key]: previous[key] === undefined ? level < AUTO_EXPANDED_LEVELS : !previous[key],
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div tabIndex={0} className={clsx("it-json-editor-tree overflow-auto", className)}>
|
|
129
|
+
{rows.map((node) => {
|
|
130
|
+
const line = analysis.nodeLines.get(pathKey(node.path))?.line;
|
|
131
|
+
const isRoot = node.key === "";
|
|
132
|
+
const isCollapsed =
|
|
133
|
+
collapsed[node.key] ?? (node.childCount > 0 && node.level >= AUTO_EXPANDED_LEVELS);
|
|
134
|
+
const hasChildren = node.childCount > 0;
|
|
135
|
+
const isPrimitive = node.kind !== "object" && node.kind !== "array";
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
key={node.key || "root"}
|
|
140
|
+
className="it-json-editor-tree-row"
|
|
141
|
+
style={{ paddingLeft: node.level * 14 }}
|
|
142
|
+
>
|
|
143
|
+
{hasChildren ? (
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => toggle(node.key, node.level)}
|
|
147
|
+
aria-expanded={!isCollapsed}
|
|
148
|
+
aria-label={isCollapsed ? t("jsonEditor.expandNode") : t("jsonEditor.collapseNode")}
|
|
149
|
+
className="it-json-editor-tree-toggle"
|
|
150
|
+
>
|
|
151
|
+
{isCollapsed ? <FaChevronRight size={8} /> : <FaChevronDown size={8} />}
|
|
152
|
+
</button>
|
|
153
|
+
) : (
|
|
154
|
+
<span className="it-json-editor-tree-spacer" aria-hidden="true" />
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{!isRoot && (
|
|
158
|
+
<>
|
|
159
|
+
<span className="it-json-editor-tk-key">{node.label}</span>
|
|
160
|
+
<span className="it-json-editor-tk-punct">:</span>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{hasChildren ? (
|
|
165
|
+
<>
|
|
166
|
+
<span className="it-json-editor-tk-punct">{node.kind === "array" ? "[" : "{"}</span>
|
|
167
|
+
{isCollapsed && (
|
|
168
|
+
<span className="it-json-editor-tk-punct">
|
|
169
|
+
{node.kind === "array" ? "]" : "}"}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="it-json-editor-tree-badge">
|
|
173
|
+
{t("jsonEditor.childCount", { count: node.childCount })}
|
|
174
|
+
</span>
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<span
|
|
178
|
+
className={clsx(
|
|
179
|
+
"it-json-editor-tk-value",
|
|
180
|
+
node.kind === "string" && "it-json-editor-tk-string",
|
|
181
|
+
node.kind === "number" && "it-json-editor-tk-number",
|
|
182
|
+
node.kind === "boolean" && "it-json-editor-tk-keyword",
|
|
183
|
+
node.kind === "null" && "it-json-editor-tk-null"
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{previewValue(node.value)}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{isPrimitive && isRoot && (
|
|
191
|
+
<span className="it-json-editor-tree-badge">{kindLabel(node.kind, t)}</span>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{showLines && line !== undefined && (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={() => onJumpToLine?.(line)}
|
|
198
|
+
title={t("jsonEditor.jumpToLine", { line })}
|
|
199
|
+
aria-label={t("jsonEditor.jumpToLine", { line })}
|
|
200
|
+
className="it-json-editor-tree-jump"
|
|
201
|
+
>
|
|
202
|
+
<FaCrosshairs size={8} />
|
|
203
|
+
<span>{line}</span>
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
|
|
210
|
+
{truncated && (
|
|
211
|
+
<p className="it-json-editor-tree-badge m-2 inline-block px-2 py-1">
|
|
212
|
+
{t("jsonEditor.treeTruncated")}
|
|
213
|
+
</p>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Etiqueta corta y traducible del tipo de un valor. */
|
|
220
|
+
function kindLabel(kind: TreeNode["kind"], t: (key: string) => string): string {
|
|
221
|
+
switch (kind) {
|
|
222
|
+
case "object":
|
|
223
|
+
return t("jsonEditor.kindObject");
|
|
224
|
+
case "array":
|
|
225
|
+
return t("jsonEditor.kindArray");
|
|
226
|
+
case "string":
|
|
227
|
+
return t("jsonEditor.kindString");
|
|
228
|
+
case "number":
|
|
229
|
+
return t("jsonEditor.kindNumber");
|
|
230
|
+
case "boolean":
|
|
231
|
+
return t("jsonEditor.kindBoolean");
|
|
232
|
+
default:
|
|
233
|
+
return t("jsonEditor.kindNull");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modo de presentación del documento.
|
|
5
|
+
*
|
|
6
|
+
* - `"text"`: editor de código con resaltado, gutter y plegado.
|
|
7
|
+
* - `"tree"`: árbol navegable de solo lectura sobre el mismo JSON.
|
|
8
|
+
*/
|
|
9
|
+
export type ITJSONEditorMode = "text" | "tree";
|
|
10
|
+
|
|
11
|
+
/** Paleta del lienzo de código. */
|
|
12
|
+
export type ITJSONEditorTheme = "ink" | "light";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props de `ITJSONEditor`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <ITJSONEditor value={json} onChange={setJson} height={420} mode="text" />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface ITJSONEditorProps {
|
|
23
|
+
/** Documento JSON como cadena. Si es `undefined`, el editor arranca vacío. @default "" */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** Se dispara con el texto completo cada vez que cambia (teclado, pegado o barra de acciones). */
|
|
26
|
+
onChange?: (value: string) => void;
|
|
27
|
+
/** Se dispara tras cada validación con el valor parseado y su estado. */
|
|
28
|
+
onParse?: (parsed: unknown, isValid: boolean, error?: string) => void;
|
|
29
|
+
/** Texto de ayuda que se muestra cuando el documento está vacío. */
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
/** Bloquea la edición: desaparecen las acciones que modifican el texto. @default false */
|
|
32
|
+
readOnly?: boolean;
|
|
33
|
+
/** Modo inicial: `"text"` (código) o `"tree"` (árbol navegable). @default "text" */
|
|
34
|
+
mode?: ITJSONEditorMode;
|
|
35
|
+
/** Permite alternar entre texto y árbol desde la barra. @default true */
|
|
36
|
+
showModeSwitch?: boolean;
|
|
37
|
+
/** Muestra la barra superior de acciones. @default true */
|
|
38
|
+
showToolbar?: boolean;
|
|
39
|
+
/** Muestra el indicador de válido/ inválido con línea y columna del error. @default true */
|
|
40
|
+
showValidation?: boolean;
|
|
41
|
+
/** Estadísticas del pie: líneas, nodos, profundidad y tamaño. @default true */
|
|
42
|
+
showStatusBar?: boolean;
|
|
43
|
+
/** Muestra los números de línea del gutter. @default true */
|
|
44
|
+
showLineNumbers?: boolean;
|
|
45
|
+
/** Muestra el gutter de plegado de bloques `{…}` / `[…]`. @default true */
|
|
46
|
+
showFoldGutter?: boolean;
|
|
47
|
+
/** Habilita las acciones de la barra (formatear, minificar, ordenar, escapar). @default true */
|
|
48
|
+
showTransformActions?: boolean;
|
|
49
|
+
/** Habilita el botón de copiar el documento completo. @default true */
|
|
50
|
+
showCopyButton?: boolean;
|
|
51
|
+
/** Habilita la carga de un `.json` desde disco. @default true */
|
|
52
|
+
showImportButton?: boolean;
|
|
53
|
+
/** Habilita la descarga del documento como `.json`. @default true */
|
|
54
|
+
showDownloadButton?: boolean;
|
|
55
|
+
/** Muestra la fila de ajustes de vista (ajuste de línea, números, plegado, indentación). @default true */
|
|
56
|
+
showViewSettings?: boolean;
|
|
57
|
+
/** Espacios por nivel de indentación, entre 0 y 8. @default 2 */
|
|
58
|
+
indentSize?: number;
|
|
59
|
+
/** Ajusta las líneas largas al ancho del editor. Desactivado mantiene el scroll horizontal y el gutter sincronizado. @default false */
|
|
60
|
+
wrapLines?: boolean;
|
|
61
|
+
/** Ofrece números de línea y plegado también en la vista de árbol. @default false */
|
|
62
|
+
treeShowLines?: boolean;
|
|
63
|
+
/** Paleta del lienzo: `"ink"` (tinta oscura, por defecto) o `"light"`. @default "ink" */
|
|
64
|
+
canvasTheme?: ITJSONEditorTheme;
|
|
65
|
+
/** Altura del área de edición: número en píxeles o cualquier medida CSS. @default 340 */
|
|
66
|
+
height?: number | string;
|
|
67
|
+
/** Ancho mínimo del editor. Útil dentro de layouts que comprimen. */
|
|
68
|
+
minWidth?: number | string;
|
|
69
|
+
/** Acciones extra que se añaden al final de la barra, antes de copiar/descargar. */
|
|
70
|
+
actions?: ReactNode;
|
|
71
|
+
/** Clases para el contenedor raíz. */
|
|
72
|
+
className?: string;
|
|
73
|
+
/** Identificador HTML del contenedor raíz, para anclarlo desde fuera o en pruebas. */
|
|
74
|
+
id?: string;
|
|
75
|
+
/** Clases para el `<textarea>`, por si hay que afinar la tipografía. */
|
|
76
|
+
textareaClassName?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Línea (1-based) que hay que marcar como errónea cuando el fallo lo detecta
|
|
79
|
+
* la aplicación y no el propio editor.
|
|
80
|
+
*/
|
|
81
|
+
errorLine?: number;
|
|
82
|
+
}
|