@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.
Files changed (71) hide show
  1. package/dist/index.cjs +7842 -4676
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +883 -3
  6. package/dist/index.d.ts +883 -3
  7. package/dist/index.js +9118 -5971
  8. package/dist/index.js.map +1 -1
  9. package/dist/layered.css +1 -1
  10. package/package.json +14 -7
  11. package/src/App.tsx +70 -2
  12. package/src/components/atoms/image/image.props.ts +2 -0
  13. package/src/components/atoms/image/image.tsx +3 -2
  14. package/src/components/molecules/json-editor/json-editor-tree.tsx +235 -0
  15. package/src/components/molecules/json-editor/json-editor.index.ts +2 -0
  16. package/src/components/molecules/json-editor/json-editor.props.ts +82 -0
  17. package/src/components/molecules/json-editor/json-editor.tsx +972 -0
  18. package/src/components/molecules/json-editor/json-editor.utils.ts +717 -0
  19. package/src/components/molecules/json-editor/useJsonEditorKeyboard.ts +256 -0
  20. package/src/components/molecules/json-renderer/json-renderer.index.ts +2 -0
  21. package/src/components/molecules/json-renderer/json-renderer.props.ts +31 -0
  22. package/src/components/molecules/json-renderer/json-renderer.tsx +79 -0
  23. package/src/components/organisms/dialog/dialog.props.ts +2 -0
  24. package/src/components/organisms/dialog/dialog.tsx +4 -3
  25. package/src/dev.css +30 -1
  26. package/src/hooks/useJsonRender.tsx +9 -0
  27. package/src/i18n/locales/en.ts +54 -0
  28. package/src/i18n/locales/es.ts +54 -0
  29. package/src/index.css +414 -0
  30. package/src/index.ts +99 -2
  31. package/src/json-renderer/catalog/axzyCatalog.ts +362 -0
  32. package/src/json-renderer/catalog/catalog.props.ts +128 -0
  33. package/src/json-renderer/catalog/resolve.ts +225 -0
  34. package/src/json-renderer/index.ts +70 -0
  35. package/src/json-renderer/kernel/NodeBoundary.tsx +53 -0
  36. package/src/json-renderer/kernel/SchemaRenderer.tsx +197 -0
  37. package/src/json-renderer/kernel/kernel.test.tsx +341 -0
  38. package/src/json-renderer/kernel/partial.ts +92 -0
  39. package/src/json-renderer/kernel/renderNode.tsx +113 -0
  40. package/src/json-renderer/kernel/schemaRenderer.props.ts +42 -0
  41. package/src/json-renderer/kernel/types.ts +159 -0
  42. package/src/json-renderer/shared/errors.ts +92 -0
  43. package/src/json-renderer/shared/limits.ts +87 -0
  44. package/src/json-renderer/shared/paths.ts +66 -0
  45. package/src/json-renderer/shared/shared.test.ts +108 -0
  46. package/src/json-renderer/stream/tolerantParse.test.ts +152 -0
  47. package/src/json-renderer/stream/tolerantParse.ts +449 -0
  48. package/src/json-renderer/test/setup.ts +38 -0
  49. package/src/json-renderer/validation/migrate.ts +83 -0
  50. package/src/json-renderer/validation/validate.ts +161 -0
  51. package/src/json-renderer/validation/validation.test.ts +165 -0
  52. package/src/json-renderer/vocabulary/buildContract.ts +158 -0
  53. package/src/sandbox/AxzyLogo.tsx +139 -0
  54. package/src/sandbox/axzyConfig.test.ts +40 -0
  55. package/src/sandbox/axzyConfig.ts +77 -0
  56. package/src/sandbox/i18n.ts +220 -0
  57. package/src/sandbox/navigation.ts +3 -0
  58. package/src/sandbox/useHashRoute.ts +39 -9
  59. package/src/showcases/DemoJsonDashboard.tsx +489 -0
  60. package/src/showcases/DemoJsonDocs.tsx +339 -0
  61. package/src/showcases/DemoJsonPortfolio.tsx +257 -0
  62. package/src/showcases/FeedbackShowcases.tsx +16 -5
  63. package/src/showcases/HomeShowcase.tsx +16 -9
  64. package/src/showcases/JsonEditorShowcase.tsx +150 -0
  65. package/src/showcases/JsonRendererShowcase.tsx +70 -0
  66. package/src/showcases/JsonScreenShowcase.tsx +341 -0
  67. package/src/showcases/StructureShowcases.tsx +6 -1
  68. package/src/showcases/demoApp/demoData.ts +181 -0
  69. package/src/showcases/demoApp/nodeTree.test.ts +39 -0
  70. package/src/showcases/demoApp/nodeTree.ts +35 -0
  71. 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.6.0",
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
- "@types/vscode": "1.85.0",
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 showFab={false} density={0.98} radius={10} shadow={2}>
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 object-cover"
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 object-cover"
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,2 @@
1
+ export { default as ITJSONEditor } from "./json-editor";
2
+ export type { ITJSONEditorProps } from "./json-editor.props";
@@ -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
+ }