@axzydev/axzy_ui_system 1.5.3 → 1.7.0
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 +6071 -4316
- 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 +145 -1
- package/dist/index.d.ts +145 -1
- package/dist/index.js +4493 -2723
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +1 -1
- package/src/App.tsx +10 -0
- 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/sidebar/sidebar.tsx +51 -14
- package/src/dev.css +24 -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 +10 -2
- package/src/sandbox/AxzyLogo.tsx +139 -0
- package/src/sandbox/i18n.ts +2 -0
- package/src/sandbox/navigation.ts +2 -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/StructureShowcases.tsx +9 -3
|
@@ -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
|
+
}
|
|
@@ -29,6 +29,28 @@ function flattenSubItems(
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Caracteres que caben en el badge del riel (`max-w-[2.1rem]`). */
|
|
33
|
+
const RAIL_BADGE_MAX_CHARS = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Contenido del badge cuando el sidebar está colapsado.
|
|
37
|
+
*
|
|
38
|
+
* En el riel solo cabe un valor corto (contadores tipo `12` o `99+`). Un texto
|
|
39
|
+
* largo deformaría el riel y taparía el icono, así que se degrada a un punto que
|
|
40
|
+
* conserva el aviso de color; el nombre completo sigue disponible en el tooltip.
|
|
41
|
+
*/
|
|
42
|
+
function RailBadge({ value }: { value: number | string }) {
|
|
43
|
+
const text = String(value);
|
|
44
|
+
if (text.length <= RAIL_BADGE_MAX_CHARS) return <>{text}</>;
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
className="block w-1.5 h-1.5 rounded-full"
|
|
49
|
+
style={{ backgroundColor: "currentColor" }}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
/**
|
|
33
55
|
* Vertical navigation sidebar with submenu expand/collapse, hover tooltips in collapsed mode,
|
|
34
56
|
* and glassmorphism styling. The collapsed rail expands automatically on pointer hover — there
|
|
@@ -137,9 +159,9 @@ export default function ITSidebar({
|
|
|
137
159
|
}, [navigationItems]);
|
|
138
160
|
|
|
139
161
|
const isSidebarCollapsed = visibleOnMobile ? false : isCollapsed && !(expandOnHover && isHovering);
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
162
|
+
const collapsedWidth = flat ? "w-[72px]" : "w-[88px]";
|
|
163
|
+
const expandedWidth = flat ? "w-[248px]" : "w-[280px]";
|
|
164
|
+
const sidebarWidth = isSidebarCollapsed ? collapsedWidth : expandedWidth;
|
|
143
165
|
// Icon-only rail that never expands on hover: names show in a tooltip.
|
|
144
166
|
const fixedRail = isSidebarCollapsed && !expandOnHover;
|
|
145
167
|
|
|
@@ -266,7 +288,7 @@ export default function ITSidebar({
|
|
|
266
288
|
{...badgeRest}
|
|
267
289
|
className={clsx("shadow-md", badgeClassName)}
|
|
268
290
|
>
|
|
269
|
-
{String(count)}
|
|
291
|
+
{isSidebarCollapsed ? <RailBadge value={count} /> : String(count)}
|
|
270
292
|
</ITBadget>
|
|
271
293
|
);
|
|
272
294
|
|
|
@@ -277,7 +299,7 @@ export default function ITSidebar({
|
|
|
277
299
|
aria-label={label}
|
|
278
300
|
title={label}
|
|
279
301
|
onClick={onClick}
|
|
280
|
-
className=
|
|
302
|
+
className={`relative flex items-center w-full h-10 mb-1 rounded-xl transition-all duration-300 ${flat ? "px-3" : "px-5"}`}
|
|
281
303
|
style={{ color: "var(--it-sidebar-icon-color, var(--color-secondary-500))" }}
|
|
282
304
|
onMouseEnter={(e) => {
|
|
283
305
|
e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
@@ -289,7 +311,7 @@ export default function ITSidebar({
|
|
|
289
311
|
{icon && (
|
|
290
312
|
<span className="flex items-center justify-center text-[1.05rem]">{icon}</span>
|
|
291
313
|
)}
|
|
292
|
-
<span className="absolute top-0.5 right-0.5 pointer-events-none">{badge}</span>
|
|
314
|
+
<span className="absolute top-0.5 right-0.5 max-w-[2.1rem] overflow-hidden pointer-events-none">{badge}</span>
|
|
293
315
|
</button>
|
|
294
316
|
);
|
|
295
317
|
}
|
|
@@ -300,7 +322,7 @@ export default function ITSidebar({
|
|
|
300
322
|
aria-label={label}
|
|
301
323
|
title={label}
|
|
302
324
|
onClick={onClick}
|
|
303
|
-
className=
|
|
325
|
+
className={`flex items-center gap-2.5 w-full text-left h-10 mb-1 rounded-xl transition-all duration-300 ${flat ? "px-3" : "px-5"}`}
|
|
304
326
|
style={{ color: "var(--it-sidebar-label-color, var(--color-secondary-600))" }}
|
|
305
327
|
onMouseEnter={(e) => {
|
|
306
328
|
e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
@@ -364,8 +386,19 @@ export default function ITSidebar({
|
|
|
364
386
|
</div>
|
|
365
387
|
)}
|
|
366
388
|
|
|
367
|
-
{header
|
|
368
|
-
|
|
389
|
+
{/* El header reserva su altura también en el riel para que la navegación no
|
|
390
|
+
salte en vertical al expandir. Se maqueta con el ancho expandido (oculto
|
|
391
|
+
y recortado) para que el texto no se reparta en más líneas en el riel. */}
|
|
392
|
+
{header && (
|
|
393
|
+
<div className={`px-4 pt-4 pb-1 flex-shrink-0 ${isSidebarCollapsed ? "overflow-hidden" : ""}`}>
|
|
394
|
+
{isSidebarCollapsed ? (
|
|
395
|
+
<div className={`${expandedWidth} invisible`} aria-hidden="true">
|
|
396
|
+
{header}
|
|
397
|
+
</div>
|
|
398
|
+
) : (
|
|
399
|
+
header
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
369
402
|
)}
|
|
370
403
|
|
|
371
404
|
{notification && (
|
|
@@ -373,7 +406,7 @@ export default function ITSidebar({
|
|
|
373
406
|
)}
|
|
374
407
|
|
|
375
408
|
{/* Navigation Items */}
|
|
376
|
-
<nav aria-label={t("sidebar.navAria")} className={`flex-1 py-4 custom-scrollbar ${fixedRail ? "overflow-visible" : "overflow-y-auto overflow-x-hidden"}
|
|
409
|
+
<nav aria-label={t("sidebar.navAria")} className={`flex-1 py-4 px-4 custom-scrollbar ${fixedRail ? "overflow-visible" : "overflow-y-auto overflow-x-hidden"}`}>
|
|
377
410
|
<ul className="space-y-1">
|
|
378
411
|
{navigationItems.map((item) => {
|
|
379
412
|
const hasSubmenu = !!item.subitems && item.subitems.length > 0;
|
|
@@ -396,7 +429,9 @@ export default function ITSidebar({
|
|
|
396
429
|
className={`flex items-center cursor-pointer
|
|
397
430
|
transition-all duration-300 ease-[cubic-bezier(0.2,0,0,1)]
|
|
398
431
|
rounded-xl relative overflow-visible
|
|
399
|
-
|
|
432
|
+
h-10
|
|
433
|
+
${flat ? "px-3" : "px-5"}
|
|
434
|
+
${isSidebarCollapsed ? "justify-start" : "justify-between"}
|
|
400
435
|
`}
|
|
401
436
|
style={{
|
|
402
437
|
backgroundColor: active ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
|
|
@@ -424,7 +459,7 @@ export default function ITSidebar({
|
|
|
424
459
|
/>
|
|
425
460
|
)}
|
|
426
461
|
|
|
427
|
-
<div className=
|
|
462
|
+
<div className="flex items-center gap-2.5 relative z-10 w-full">
|
|
428
463
|
{item.icon && (
|
|
429
464
|
<div
|
|
430
465
|
className={`transition-all duration-300 flex-shrink-0 flex items-center justify-center`}
|
|
@@ -467,13 +502,15 @@ export default function ITSidebar({
|
|
|
467
502
|
{...badgeRest}
|
|
468
503
|
className={clsx(
|
|
469
504
|
"absolute shadow-md",
|
|
505
|
+
// En el riel no hay ancho para una etiqueta larga: el badge se
|
|
506
|
+
// ancla a la esquina y se recorta, así nunca desborda ni tapa el icono.
|
|
470
507
|
isSidebarCollapsed
|
|
471
|
-
? "top-0.5 right-0.5"
|
|
508
|
+
? "top-0.5 right-0.5 max-w-[2.1rem] overflow-hidden"
|
|
472
509
|
: "right-3 top-1/2 -translate-y-1/2",
|
|
473
510
|
badgeClassName,
|
|
474
511
|
)}
|
|
475
512
|
>
|
|
476
|
-
{String(item.badge)}
|
|
513
|
+
{isSidebarCollapsed ? <RailBadge value={item.badge} /> : String(item.badge)}
|
|
477
514
|
</ITBadget>
|
|
478
515
|
)}
|
|
479
516
|
</div>
|
package/src/dev.css
CHANGED
|
@@ -112,7 +112,30 @@ html {
|
|
|
112
112
|
@media (prefers-reduced-motion: reduce) {
|
|
113
113
|
.it-landing-rise,
|
|
114
114
|
.it-landing-float,
|
|
115
|
-
.it-reveal
|
|
115
|
+
.it-reveal,
|
|
116
|
+
.it-logo-latido {
|
|
116
117
|
animation: none;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
/* ──────────────────────────────────────────────
|
|
122
|
+
MARCA AXZYDEV (dev.css)
|
|
123
|
+
El loader del kit es una animación Lottie; en el sitio se usa la versión
|
|
124
|
+
estática del ícono con un latido suave. Solo dev: nunca llega a dist.
|
|
125
|
+
────────────────────────────────────────────── */
|
|
126
|
+
@keyframes it-logo-latido {
|
|
127
|
+
0%,
|
|
128
|
+
100% {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
transform: scale(1);
|
|
131
|
+
}
|
|
132
|
+
50% {
|
|
133
|
+
opacity: 0.72;
|
|
134
|
+
transform: scale(0.94);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.it-logo-latido {
|
|
139
|
+
animation: it-logo-latido 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
140
|
+
transform-origin: center;
|
|
141
|
+
}
|
|
@@ -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;
|