@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.
Files changed (35) hide show
  1. package/dist/index.cjs +6071 -4316
  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 +145 -1
  6. package/dist/index.d.ts +145 -1
  7. package/dist/index.js +4493 -2723
  8. package/dist/index.js.map +1 -1
  9. package/dist/layered.css +1 -1
  10. package/package.json +1 -1
  11. package/src/App.tsx +10 -0
  12. package/src/components/molecules/json-editor/json-editor-tree.tsx +235 -0
  13. package/src/components/molecules/json-editor/json-editor.index.ts +2 -0
  14. package/src/components/molecules/json-editor/json-editor.props.ts +82 -0
  15. package/src/components/molecules/json-editor/json-editor.tsx +972 -0
  16. package/src/components/molecules/json-editor/json-editor.utils.ts +717 -0
  17. package/src/components/molecules/json-editor/useJsonEditorKeyboard.ts +256 -0
  18. package/src/components/molecules/json-renderer/json-renderer.index.ts +2 -0
  19. package/src/components/molecules/json-renderer/json-renderer.props.ts +31 -0
  20. package/src/components/molecules/json-renderer/json-renderer.tsx +79 -0
  21. package/src/components/organisms/sidebar/sidebar.tsx +51 -14
  22. package/src/dev.css +24 -1
  23. package/src/hooks/useJsonRender.tsx +9 -0
  24. package/src/i18n/locales/en.ts +54 -0
  25. package/src/i18n/locales/es.ts +54 -0
  26. package/src/index.css +414 -0
  27. package/src/index.ts +10 -2
  28. package/src/sandbox/AxzyLogo.tsx +139 -0
  29. package/src/sandbox/i18n.ts +2 -0
  30. package/src/sandbox/navigation.ts +2 -0
  31. package/src/showcases/FeedbackShowcases.tsx +16 -5
  32. package/src/showcases/HomeShowcase.tsx +16 -9
  33. package/src/showcases/JsonEditorShowcase.tsx +150 -0
  34. package/src/showcases/JsonRendererShowcase.tsx +70 -0
  35. 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,2 @@
1
+ export { default as ITJsonRenderer } from "./json-renderer";
2
+ export type { ITJsonRendererProps, JsonComponentNode, JsonComponentType } from "./json-renderer.props";
@@ -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 sidebarWidth = isSidebarCollapsed
141
- ? flat ? "w-[72px]" : "w-[88px]"
142
- : flat ? "w-[248px]" : "w-[280px]";
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="relative flex items-center justify-center w-full p-2 mb-1 rounded-xl transition-all duration-300"
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="flex items-center gap-2.5 w-full text-left px-3 py-2 mb-1 rounded-xl transition-all duration-300"
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 && !isSidebarCollapsed && (
368
- <div className="px-4 pt-4 pb-1 flex-shrink-0">{header}</div>
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"} ${flat && isSidebarCollapsed ? "px-3" : "px-4"}`}>
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
- ${isSidebarCollapsed ? (flat ? "justify-center h-10 mb-1" : "justify-center p-2 mb-1") : "justify-between px-3 py-2 mb-0.5"}
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={`flex items-center ${!isSidebarCollapsed ? "gap-2.5" : "justify-center"} relative z-10 w-full`}>
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;
@@ -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;