@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useCallback, type KeyboardEvent, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/** Resultado de una operación de edición sobre el textarea. */
|
|
4
|
+
interface EditResult {
|
|
5
|
+
/** Texto completo resultante. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** Inicio de la selección final. */
|
|
8
|
+
selectionStart: number;
|
|
9
|
+
/** Fin de la selección final. */
|
|
10
|
+
selectionEnd: number;
|
|
11
|
+
/** Si el manejador consumió el evento (hay que hacer `preventDefault`). */
|
|
12
|
+
handled: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ajustes del hook de edición.
|
|
17
|
+
*
|
|
18
|
+
* No recibe el `value` por props a propósito: lee el documento del propio
|
|
19
|
+
* `<textarea>` en cada pulsación. Entre dos teclas rápidas React todavía puede
|
|
20
|
+
* no haber re-renderizado, y calcular la edición sobre el valor de props pierde
|
|
21
|
+
* o duplica caracteres. `applyEdit` se encarga de notificar y de reponer el
|
|
22
|
+
* cursor tras el commit.
|
|
23
|
+
*/
|
|
24
|
+
export interface UseJsonEditorKeyboardOptions {
|
|
25
|
+
/** Referencia al textarea real. */
|
|
26
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
27
|
+
/** Espacios por nivel de indentación. */
|
|
28
|
+
indentSize: number;
|
|
29
|
+
/** `true` cuando el editor es de solo lectura (no se intercepta nada). */
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
/** Aplica una edición al textarea conservando el deshacer nativo. */
|
|
32
|
+
applyEdit: (result: EditResult) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Caracteres que abren bloque y su pareja de cierre. */
|
|
36
|
+
const PAIRS: Record<string, string> = { "{": "}", "[": "]", '"': '"' };
|
|
37
|
+
|
|
38
|
+
/** Caracteres de cierre que se "saltan" al teclear si ya están delante. */
|
|
39
|
+
const CLOSERS = new Set(["}", "]", '"']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Comportamiento de editor de código sobre un `<textarea>` nativo: indentación
|
|
43
|
+
* con Tab, continuación de indentación al pulsar Enter, cierre automático de
|
|
44
|
+
* llaves/corchetes/comillas y borrado por parejas.
|
|
45
|
+
*
|
|
46
|
+
* Se implementa como hook para que `ITJSONEditor` siga siendo un componente de
|
|
47
|
+
* presentación y el textarea conserve el deshacer del navegador.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
52
|
+
* const handleKeyDown = useJsonEditorKeyboard({ textareaRef, indentSize: 2, applyEdit });
|
|
53
|
+
* <textarea ref={textareaRef} onKeyDown={handleKeyDown} />
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function useJsonEditorKeyboard({
|
|
57
|
+
textareaRef,
|
|
58
|
+
indentSize,
|
|
59
|
+
readOnly = false,
|
|
60
|
+
applyEdit,
|
|
61
|
+
}: UseJsonEditorKeyboardOptions) {
|
|
62
|
+
return useCallback(
|
|
63
|
+
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
64
|
+
const textarea = textareaRef.current;
|
|
65
|
+
if (!textarea || readOnly) return;
|
|
66
|
+
|
|
67
|
+
const current = textarea.value;
|
|
68
|
+
const { selectionStart, selectionEnd } = textarea;
|
|
69
|
+
const indent = " ".repeat(indentSize);
|
|
70
|
+
const before = current.slice(0, selectionStart);
|
|
71
|
+
const after = current.slice(selectionEnd);
|
|
72
|
+
|
|
73
|
+
/** Sustituye la selección por `text` y deja el cursor al final. */
|
|
74
|
+
const replace = (text: string, caretOffset = text.length): EditResult => {
|
|
75
|
+
const next = current.slice(0, selectionStart) + text + after;
|
|
76
|
+
const caret = selectionStart + caretOffset;
|
|
77
|
+
return { value: next, selectionStart: caret, selectionEnd: caret, handled: true };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/* ------------------------------------------------------------ Tab */
|
|
81
|
+
if (event.key === "Tab") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const lineStart = current.lastIndexOf("\n", selectionStart - 1) + 1;
|
|
84
|
+
const lineEndRaw = current.indexOf("\n", selectionEnd);
|
|
85
|
+
const lineEnd = lineEndRaw === -1 ? current.length : lineEndRaw;
|
|
86
|
+
const multiline = current.slice(lineStart, lineEnd).includes("\n");
|
|
87
|
+
|
|
88
|
+
if (!multiline && !event.shiftKey && selectionStart === selectionEnd) {
|
|
89
|
+
// Sangrado simple en el cursor.
|
|
90
|
+
applyEdit(replace(indent));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sangrado/desangrado por bloques: afecta a todas las líneas tocadas.
|
|
95
|
+
const block = current.slice(lineStart, lineEnd);
|
|
96
|
+
const lines = block.split("\n");
|
|
97
|
+
/** Cuánto se ha desplazado cada línea (positivo al indentar). */
|
|
98
|
+
const shifts: number[] = [];
|
|
99
|
+
const shifted = lines
|
|
100
|
+
.map((line) => {
|
|
101
|
+
if (event.shiftKey) {
|
|
102
|
+
if (line.startsWith(indent)) {
|
|
103
|
+
shifts.push(-indent.length);
|
|
104
|
+
return line.slice(indent.length);
|
|
105
|
+
}
|
|
106
|
+
const match = line.match(/^[ \t]/);
|
|
107
|
+
shifts.push(match ? -1 : 0);
|
|
108
|
+
return match ? line.slice(1) : line;
|
|
109
|
+
}
|
|
110
|
+
shifts.push(indent.length);
|
|
111
|
+
return indent + line;
|
|
112
|
+
})
|
|
113
|
+
.join("\n");
|
|
114
|
+
|
|
115
|
+
const next = current.slice(0, lineStart) + shifted + current.slice(lineEnd);
|
|
116
|
+
/**
|
|
117
|
+
* Lleva una posición del bloque original a su nueva posición: suma los
|
|
118
|
+
* desplazamientos de las líneas anteriores y recorta el de la propia
|
|
119
|
+
* línea para no dejar el cursor antes de su inicio.
|
|
120
|
+
*/
|
|
121
|
+
const remap = (position: number): number => {
|
|
122
|
+
const relative = Math.max(0, position - lineStart);
|
|
123
|
+
let offset = 0;
|
|
124
|
+
let lineIndex = lines.length - 1;
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const lineLength = lines[i].length + (i < lines.length - 1 ? 1 : 0);
|
|
127
|
+
if (relative <= offset + lineLength) {
|
|
128
|
+
lineIndex = i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
offset += lineLength;
|
|
132
|
+
}
|
|
133
|
+
const accumulated = shifts.slice(0, lineIndex).reduce((total, n) => total + n, 0);
|
|
134
|
+
const inLine = relative - offset;
|
|
135
|
+
const shift = shifts[lineIndex] ?? 0;
|
|
136
|
+
return lineStart + accumulated + Math.max(0, inLine + Math.min(shift, inLine));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const startNext = remap(selectionStart);
|
|
140
|
+
const endNext = Math.max(startNext, remap(selectionEnd));
|
|
141
|
+
applyEdit({
|
|
142
|
+
value: next,
|
|
143
|
+
selectionStart: Math.min(startNext, next.length),
|
|
144
|
+
selectionEnd: Math.min(endNext, next.length),
|
|
145
|
+
handled: true,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ---------------------------------------------------------- Enter */
|
|
151
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
const currentLine = before.slice(before.lastIndexOf("\n") + 1);
|
|
154
|
+
const leading = currentLine.match(/^[ \t]*/)?.[0] ?? "";
|
|
155
|
+
/** `true` cuando el cursor está al final de su línea (nada detrás salvo el cierre). */
|
|
156
|
+
const atLineEnd = /^[ \t]*[}\]]?[ \t]*$/.test(after);
|
|
157
|
+
const nextChar = after[0] ?? "";
|
|
158
|
+
const prevChar = before[before.length - 1] ?? "";
|
|
159
|
+
const opensBlock = prevChar === "{" || prevChar === "[";
|
|
160
|
+
const closesBlock = nextChar === "}" || nextChar === "]";
|
|
161
|
+
|
|
162
|
+
// Entre llaves vacías: se abre un bloque y se deja el cursor centrado.
|
|
163
|
+
if (opensBlock && closesBlock) {
|
|
164
|
+
const inner = `\n${leading}${indent}\n${leading}`;
|
|
165
|
+
applyEdit(replace(inner, 1 + leading.length + indent.length));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (opensBlock) {
|
|
169
|
+
applyEdit(replace(`\n${leading}${indent}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!atLineEnd) {
|
|
173
|
+
// A mitad de línea solo se parte la línea: no se toca el resto, pero la
|
|
174
|
+
// línea nueva sí hereda la sangría del bloque en el que se está.
|
|
175
|
+
applyEdit(replace(`\n${leading}${closesBlock ? indent : ""}`, 1 + leading.length + (closesBlock ? indent.length : 0)));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (closesBlock) {
|
|
179
|
+
applyEdit(replace(`\n${leading}`, 1 + leading.length));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
applyEdit(replace(`\n${leading}`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* --------------------------------------------- Cierre automático */
|
|
187
|
+
if (PAIRS[event.key]) {
|
|
188
|
+
const closing = PAIRS[event.key];
|
|
189
|
+
const selected = current.slice(selectionStart, selectionEnd);
|
|
190
|
+
// Teclear la comilla de cierre sobre una ya existente solo avanza el
|
|
191
|
+
// cursor. La comprobación es contra el MISMO carácter: delante de un `}`
|
|
192
|
+
// o un `]` lo que toca es abrir una cadena, no saltar nada.
|
|
193
|
+
if (event.key === '"' && nextCharOf(after) === '"' && !selected) {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
applyEdit({ value: current, selectionStart: selectionEnd + 1, selectionEnd: selectionEnd + 1, handled: true });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
applyEdit(replace(`${event.key}${selected}${closing}`, 1));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ------------------------------------------------ Salto de cierre */
|
|
204
|
+
if (CLOSERS.has(event.key) && selectionStart === selectionEnd && after[0] === event.key) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
applyEdit({ value: current, selectionStart: selectionStart + 1, selectionEnd: selectionStart + 1, handled: true });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ----------------------------------------------- Borrado de pareja */
|
|
211
|
+
if (event.key === "Backspace" && selectionStart === selectionEnd && selectionStart > 0) {
|
|
212
|
+
const prev = before[before.length - 1];
|
|
213
|
+
const next = after[0];
|
|
214
|
+
const isPair =
|
|
215
|
+
(prev === "{" && next === "}") || (prev === "[" && next === "]") || (prev === '"' && next === '"');
|
|
216
|
+
if (isPair) {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
const nextValue = current.slice(0, selectionStart - 1) + current.slice(selectionStart + 1);
|
|
219
|
+
applyEdit({
|
|
220
|
+
value: nextValue,
|
|
221
|
+
selectionStart: selectionStart - 1,
|
|
222
|
+
selectionEnd: selectionStart - 1,
|
|
223
|
+
handled: true,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ------------------------------------------------------ Atajos */
|
|
230
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "a") {
|
|
231
|
+
// Seleccionar todo dentro del editor (no toda la página).
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
textarea.select();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "d") {
|
|
237
|
+
// Duplicar la línea actual.
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
240
|
+
const lineEndRaw = current.indexOf("\n", selectionStart);
|
|
241
|
+
const lineEnd = lineEndRaw === -1 ? current.length : lineEndRaw;
|
|
242
|
+
const line = current.slice(lineStart, lineEnd);
|
|
243
|
+
const next = current.slice(0, lineEnd) + "\n" + line + current.slice(lineEnd);
|
|
244
|
+
const caret = selectionStart + line.length + 1;
|
|
245
|
+
applyEdit({ value: next, selectionStart: caret, selectionEnd: caret, handled: true });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
[applyEdit, indentSize, readOnly, textareaRef]
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Carácter vigente en una cadena, o `""`. */
|
|
254
|
+
function nextCharOf(text: string): string {
|
|
255
|
+
return text[0] ?? "";
|
|
256
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type JsonComponentType =
|
|
2
|
+
| "button"
|
|
3
|
+
| "text"
|
|
4
|
+
| "card"
|
|
5
|
+
| "flex"
|
|
6
|
+
| "stack"
|
|
7
|
+
| "grid"
|
|
8
|
+
| "alert"
|
|
9
|
+
| "div";
|
|
10
|
+
|
|
11
|
+
export interface JsonProps {
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JsonComponentNode {
|
|
16
|
+
/** Component type to render */
|
|
17
|
+
type: JsonComponentType;
|
|
18
|
+
/** Props to pass to the component */
|
|
19
|
+
props?: JsonProps;
|
|
20
|
+
/** Children nodes */
|
|
21
|
+
children?: JsonComponentNode[] | React.ReactNode;
|
|
22
|
+
/** Text content (shorthand for text children) */
|
|
23
|
+
text?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ITJsonRendererProps {
|
|
27
|
+
/** JSON structure to render */
|
|
28
|
+
json: JsonComponentNode | JsonComponentNode[];
|
|
29
|
+
/** Additional CSS class names */
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ITJsonRendererProps, JsonComponentNode } from "./json-renderer.props";
|
|
3
|
+
import ITButton from "@/components/atoms/button/button";
|
|
4
|
+
import ITText from "@/components/atoms/text/text";
|
|
5
|
+
import ITCard from "@/components/molecules/card/card";
|
|
6
|
+
import ITFlex from "@/components/atoms/flex/flex";
|
|
7
|
+
import ITStack from "@/components/atoms/stack/stack";
|
|
8
|
+
import ITGrid from "@/components/atoms/grid/grid";
|
|
9
|
+
import ITAlert from "@/components/molecules/alert/alert";
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render UI from JSON schema using AXZY UI System components.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <ITJsonRenderer json={{ type: "button", props: { label: "Click me" } }} />
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <ITJsonRenderer json={[
|
|
20
|
+
* { type: "alert", props: { variant: "info", title: "Info" }, children: "Hello" },
|
|
21
|
+
* { type: "card", props: { title: "Card" }, children: { type: "text", text: "Content" } }
|
|
22
|
+
* ]} />
|
|
23
|
+
*/
|
|
24
|
+
export default function ITJsonRenderer({
|
|
25
|
+
json,
|
|
26
|
+
className,
|
|
27
|
+
}: ITJsonRendererProps) {
|
|
28
|
+
const renderNode = (node: JsonComponentNode | React.ReactNode): React.ReactNode => {
|
|
29
|
+
if (node == null || typeof node === "boolean") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (React.isValidElement(node)) {
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
return node.map((child, idx) => (
|
|
43
|
+
<React.Fragment key={idx}>{renderNode(child as any)}</React.Fragment>
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// node is JsonComponentNode
|
|
48
|
+
const { type, props = {}, children, text } = node as JsonComponentNode;
|
|
49
|
+
|
|
50
|
+
const childContent = children !== undefined ? renderNode(children as any) : text;
|
|
51
|
+
|
|
52
|
+
switch (type) {
|
|
53
|
+
case "button":
|
|
54
|
+
return <ITButton {...props}>{childContent}</ITButton>;
|
|
55
|
+
case "text":
|
|
56
|
+
return <ITText {...props}>{childContent}</ITText>;
|
|
57
|
+
case "card":
|
|
58
|
+
return <ITCard {...props}>{childContent}</ITCard>;
|
|
59
|
+
case "flex":
|
|
60
|
+
return <ITFlex {...props}>{childContent}</ITFlex>;
|
|
61
|
+
case "stack":
|
|
62
|
+
return <ITStack {...props}>{childContent}</ITStack>;
|
|
63
|
+
case "grid":
|
|
64
|
+
// ITGrid can be container or item; by default treat as container if has children layout
|
|
65
|
+
return <ITGrid container {...props}>{childContent}</ITGrid>;
|
|
66
|
+
case "alert":
|
|
67
|
+
return <ITAlert {...props}>{childContent}</ITAlert>;
|
|
68
|
+
case "div":
|
|
69
|
+
default:
|
|
70
|
+
return <div {...props}>{childContent}</div>;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(json)) {
|
|
75
|
+
return <div className={clsx(className)}>{json.map((n, idx) => <React.Fragment key={idx}>{renderNode(n)}</React.Fragment>)}</div>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <div className={clsx(className)}>{renderNode(json)}</div>;
|
|
79
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface ITDialogProps {
|
|
|
11
11
|
title?: string;
|
|
12
12
|
/** When true and `title` is provided, renders an ITFormHeader instead of the default title bar. @default false */
|
|
13
13
|
useFormHeader?: boolean;
|
|
14
|
+
/** When false, the card has no padding: the title row and body are flush to the edges, so the content can draw its own sections. @default true */
|
|
15
|
+
padded?: boolean;
|
|
14
16
|
/** Makes the dialog fill the entire viewport. @default false */
|
|
15
17
|
fullScreen?: boolean;
|
|
16
18
|
}
|
|
@@ -42,6 +42,7 @@ export default function ITDialog({
|
|
|
42
42
|
title,
|
|
43
43
|
useFormHeader = false,
|
|
44
44
|
fullScreen = false,
|
|
45
|
+
padded = true,
|
|
45
46
|
}: ITDialogProps) {
|
|
46
47
|
const { t } = useITTranslation();
|
|
47
48
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
@@ -79,7 +80,7 @@ export default function ITDialog({
|
|
|
79
80
|
className={`overflow-hidden relative ${
|
|
80
81
|
fullScreen
|
|
81
82
|
? "w-screen h-screen max-w-none rounded-none m-0 flex flex-col"
|
|
82
|
-
: `${className || ""} ${useFormHeader ? "p-0" : "p-6"}`
|
|
83
|
+
: `${className || ""} ${!padded || useFormHeader ? "p-0" : "p-6"}`
|
|
83
84
|
}`}
|
|
84
85
|
style={{
|
|
85
86
|
backgroundColor: theme.card.backgroundColor,
|
|
@@ -106,8 +107,8 @@ export default function ITDialog({
|
|
|
106
107
|
>
|
|
107
108
|
<FaRegTimesCircle />
|
|
108
109
|
</button>
|
|
109
|
-
{title && <ITText as="h2" className="text-xl font-semibold mb-4">{title}</ITText>}
|
|
110
|
-
<div>{children}</div>
|
|
110
|
+
{title && (padded ? <ITText as="h2" className="text-xl font-semibold mb-4">{title}</ITText> : <div className="flex items-center justify-between gap-3 px-6 py-4 border-b border-slate-200"><ITText as="h2" className="text-lg font-medium">{title}</ITText></div>)}
|
|
111
|
+
<div className={padded ? undefined : "min-h-0 flex-1 flex flex-col"}>{children}</div>
|
|
111
112
|
</>
|
|
112
113
|
)}
|
|
113
114
|
</div>
|
package/src/dev.css
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
--default-font-family: var(--font-sans);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* El portafolio de demostración vive en `public/demo/*.json` y sus clases
|
|
35
|
+
viajan en el JSON. Tailwind solo genera las clases que ve en el código, así
|
|
36
|
+
que hay que apuntarlo al archivo del esquema: el demo es dev-only y estas
|
|
37
|
+
utilidades no entran en `dist/index.css`. */
|
|
38
|
+
@source "../public/demo/*.json";
|
|
39
|
+
|
|
34
40
|
/* ──────────────────────────────────────────────
|
|
35
41
|
SANDBOX-ONLY ANIMATIONS (dev.css)
|
|
36
42
|
Never shipped: only src/main.tsx imports this file and `pnpm bundle`
|
|
@@ -112,7 +118,30 @@ html {
|
|
|
112
118
|
@media (prefers-reduced-motion: reduce) {
|
|
113
119
|
.it-landing-rise,
|
|
114
120
|
.it-landing-float,
|
|
115
|
-
.it-reveal
|
|
121
|
+
.it-reveal,
|
|
122
|
+
.it-logo-latido {
|
|
116
123
|
animation: none;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
126
|
+
|
|
127
|
+
/* ──────────────────────────────────────────────
|
|
128
|
+
MARCA AXZYDEV (dev.css)
|
|
129
|
+
El loader del kit es una animación Lottie; en el sitio se usa la versión
|
|
130
|
+
estática del ícono con un latido suave. Solo dev: nunca llega a dist.
|
|
131
|
+
────────────────────────────────────────────── */
|
|
132
|
+
@keyframes it-logo-latido {
|
|
133
|
+
0%,
|
|
134
|
+
100% {
|
|
135
|
+
opacity: 1;
|
|
136
|
+
transform: scale(1);
|
|
137
|
+
}
|
|
138
|
+
50% {
|
|
139
|
+
opacity: 0.72;
|
|
140
|
+
transform: scale(0.94);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.it-logo-latido {
|
|
145
|
+
animation: it-logo-latido 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
146
|
+
transform-origin: center;
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import ITJsonRenderer from "@/components/molecules/json-renderer/json-renderer";
|
|
3
|
+
import { JsonComponentNode } from "@/components/molecules/json-renderer/json-renderer.props";
|
|
4
|
+
|
|
5
|
+
export function useJsonRender(json: JsonComponentNode | JsonComponentNode[]) {
|
|
6
|
+
return useMemo(() => <ITJsonRenderer json={json} />, [json]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default useJsonRender;
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -158,6 +158,60 @@ const en: ITMessages = {
|
|
|
158
158
|
"themeProvider.presets.oceanicTeal": "Oceanic Teal 🌊",
|
|
159
159
|
"themeProvider.presets.goldenHour": "Golden Hour ☀️",
|
|
160
160
|
"themeProvider.presets.deepForest": "Deep Forest 🌲",
|
|
161
|
+
|
|
162
|
+
// -------------------------------------------------------- ITJSONEditor
|
|
163
|
+
"jsonEditor.ariaLabel": "JSON editor",
|
|
164
|
+
"jsonEditor.placeholder": '{\n "type": "button",\n "props": { "label": "Confirm" }\n}',
|
|
165
|
+
"jsonEditor.valid": "Valid JSON",
|
|
166
|
+
"jsonEditor.invalid": "Invalid JSON",
|
|
167
|
+
"jsonEditor.invalidAt": "Error on line {line}, column {column}",
|
|
168
|
+
"jsonEditor.format": "Format",
|
|
169
|
+
"jsonEditor.formatHint": "Reindents the document (Shift+Alt+F)",
|
|
170
|
+
"jsonEditor.minify": "Minify",
|
|
171
|
+
"jsonEditor.sortAsc": "A-Z",
|
|
172
|
+
"jsonEditor.sortDesc": "Z-A",
|
|
173
|
+
"jsonEditor.escape": "Escape",
|
|
174
|
+
"jsonEditor.unescape": "Unescape",
|
|
175
|
+
"jsonEditor.copy": "Copy",
|
|
176
|
+
"jsonEditor.copied": "Copied",
|
|
177
|
+
"jsonEditor.copyFailed": "Could not copy to the clipboard",
|
|
178
|
+
"jsonEditor.importFile": "Open",
|
|
179
|
+
"jsonEditor.download": "Download",
|
|
180
|
+
"jsonEditor.fileLoaded": "File loaded",
|
|
181
|
+
"jsonEditor.fileError": "The file could not be read",
|
|
182
|
+
"jsonEditor.applied": "Action applied",
|
|
183
|
+
"jsonEditor.actionFailed": "The action could not be applied",
|
|
184
|
+
"jsonEditor.viewMode": "View mode",
|
|
185
|
+
"jsonEditor.modeText": "Text",
|
|
186
|
+
"jsonEditor.modeTree": "Tree",
|
|
187
|
+
"jsonEditor.viewSettings": "View",
|
|
188
|
+
"jsonEditor.wrap": "Wrap",
|
|
189
|
+
"jsonEditor.lineNumbers": "Numbers",
|
|
190
|
+
"jsonEditor.folding": "Folding",
|
|
191
|
+
"jsonEditor.indent": "Indent",
|
|
192
|
+
"jsonEditor.indentNone": "No indent",
|
|
193
|
+
"jsonEditor.collapseAll": "Collapse all",
|
|
194
|
+
"jsonEditor.expandAll": "Expand all",
|
|
195
|
+
"jsonEditor.collapseBlock": "Collapse the block on line {line}",
|
|
196
|
+
"jsonEditor.expandBlock": "Expand the block on line {line}",
|
|
197
|
+
"jsonEditor.foldMore": "… {count} folded lines",
|
|
198
|
+
"jsonEditor.expandNode": "Expand node",
|
|
199
|
+
"jsonEditor.collapseNode": "Collapse node",
|
|
200
|
+
"jsonEditor.jumpToLine": "Go to line {line}",
|
|
201
|
+
"jsonEditor.childCount": "{count} children",
|
|
202
|
+
"jsonEditor.treeNeedsValid": "Fix the JSON to see the tree",
|
|
203
|
+
"jsonEditor.treeTruncated": "Tree truncated: too many nodes, use the text view",
|
|
204
|
+
"jsonEditor.statLines": "{count} lines",
|
|
205
|
+
"jsonEditor.statNodes": "{count} nodes",
|
|
206
|
+
"jsonEditor.statDepth": "depth {count}",
|
|
207
|
+
"jsonEditor.statRoot": "root: {kind}",
|
|
208
|
+
"jsonEditor.statSize": "size: {size}",
|
|
209
|
+
"jsonEditor.kindObject": "object",
|
|
210
|
+
"jsonEditor.kindArray": "array",
|
|
211
|
+
"jsonEditor.kindString": "string",
|
|
212
|
+
"jsonEditor.kindNumber": "number",
|
|
213
|
+
"jsonEditor.kindBoolean": "boolean",
|
|
214
|
+
"jsonEditor.kindNull": "null",
|
|
161
215
|
};
|
|
162
216
|
|
|
163
217
|
export default en;
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -160,6 +160,60 @@ const es: ITMessages = {
|
|
|
160
160
|
"themeProvider.presets.oceanicTeal": "Oceanic Teal 🌊",
|
|
161
161
|
"themeProvider.presets.goldenHour": "Golden Hour ☀️",
|
|
162
162
|
"themeProvider.presets.deepForest": "Deep Forest 🌲",
|
|
163
|
+
|
|
164
|
+
// -------------------------------------------------------- ITJSONEditor
|
|
165
|
+
"jsonEditor.ariaLabel": "Editor de JSON",
|
|
166
|
+
"jsonEditor.placeholder": '{\n "type": "button",\n "props": { "label": "Aceptar" }\n}',
|
|
167
|
+
"jsonEditor.valid": "JSON válido",
|
|
168
|
+
"jsonEditor.invalid": "JSON no válido",
|
|
169
|
+
"jsonEditor.invalidAt": "Error en línea {line}, columna {column}",
|
|
170
|
+
"jsonEditor.format": "Formatear",
|
|
171
|
+
"jsonEditor.formatHint": "Reindenta el documento (Mayús+Alt+F)",
|
|
172
|
+
"jsonEditor.minify": "Minificar",
|
|
173
|
+
"jsonEditor.sortAsc": "A-Z",
|
|
174
|
+
"jsonEditor.sortDesc": "Z-A",
|
|
175
|
+
"jsonEditor.escape": "Escapar",
|
|
176
|
+
"jsonEditor.unescape": "Desescapar",
|
|
177
|
+
"jsonEditor.copy": "Copiar",
|
|
178
|
+
"jsonEditor.copied": "Copiado",
|
|
179
|
+
"jsonEditor.copyFailed": "No se pudo copiar al portapapeles",
|
|
180
|
+
"jsonEditor.importFile": "Abrir",
|
|
181
|
+
"jsonEditor.download": "Descargar",
|
|
182
|
+
"jsonEditor.fileLoaded": "Archivo cargado",
|
|
183
|
+
"jsonEditor.fileError": "No se pudo leer el archivo",
|
|
184
|
+
"jsonEditor.applied": "Acción aplicada",
|
|
185
|
+
"jsonEditor.actionFailed": "La acción no se pudo aplicar",
|
|
186
|
+
"jsonEditor.viewMode": "Modo de vista",
|
|
187
|
+
"jsonEditor.modeText": "Texto",
|
|
188
|
+
"jsonEditor.modeTree": "Árbol",
|
|
189
|
+
"jsonEditor.viewSettings": "Vista",
|
|
190
|
+
"jsonEditor.wrap": "Ajuste",
|
|
191
|
+
"jsonEditor.lineNumbers": "Números",
|
|
192
|
+
"jsonEditor.folding": "Plegado",
|
|
193
|
+
"jsonEditor.indent": "Indentación",
|
|
194
|
+
"jsonEditor.indentNone": "Sin sangrar",
|
|
195
|
+
"jsonEditor.collapseAll": "Plegar todo",
|
|
196
|
+
"jsonEditor.expandAll": "Desplegar todo",
|
|
197
|
+
"jsonEditor.collapseBlock": "Plegar el bloque de la línea {line}",
|
|
198
|
+
"jsonEditor.expandBlock": "Desplegar el bloque de la línea {line}",
|
|
199
|
+
"jsonEditor.foldMore": "… {count} líneas plegadas",
|
|
200
|
+
"jsonEditor.expandNode": "Desplegar nodo",
|
|
201
|
+
"jsonEditor.collapseNode": "Plegar nodo",
|
|
202
|
+
"jsonEditor.jumpToLine": "Ir a la línea {line}",
|
|
203
|
+
"jsonEditor.childCount": "{count} hijos",
|
|
204
|
+
"jsonEditor.treeNeedsValid": "Corrige el JSON para ver el árbol",
|
|
205
|
+
"jsonEditor.treeTruncated": "Árbol recortado: demasiados nodos, usa la vista de texto",
|
|
206
|
+
"jsonEditor.statLines": "{count} líneas",
|
|
207
|
+
"jsonEditor.statNodes": "{count} nodos",
|
|
208
|
+
"jsonEditor.statDepth": "prof. {count}",
|
|
209
|
+
"jsonEditor.statRoot": "raíz: {kind}",
|
|
210
|
+
"jsonEditor.statSize": "tamaño: {size}",
|
|
211
|
+
"jsonEditor.kindObject": "objeto",
|
|
212
|
+
"jsonEditor.kindArray": "array",
|
|
213
|
+
"jsonEditor.kindString": "texto",
|
|
214
|
+
"jsonEditor.kindNumber": "número",
|
|
215
|
+
"jsonEditor.kindBoolean": "booleano",
|
|
216
|
+
"jsonEditor.kindNull": "nulo",
|
|
163
217
|
};
|
|
164
218
|
|
|
165
219
|
export default es;
|