@axzydev/axzy_ui_system 1.5.3 → 1.6.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/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.5.3",
7
+ "version": "1.6.0",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -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>
@@ -1304,7 +1304,7 @@ export const LayoutShowcase = () => {
1304
1304
  { name: "children", type: "React.ReactNode", required: true, description: "Contenido principal." },
1305
1305
  { name: "className", type: "string", description: "Clases del wrapper externo." },
1306
1306
  { name: "contentClassName", type: "string", description: "Clases del contenedor de contenido." },
1307
- { name: "sidebar.header", type: "React.ReactNode", description: "Slot opcional sobre la navegación (ej. wordmark o filtro). Oculto al colapsar." },
1307
+ { name: "sidebar.header", type: "React.ReactNode", description: "Slot opcional sobre la navegación (ej. wordmark o filtro). Oculto al colapsar, pero reserva su altura." },
1308
1308
  { name: "sidebar.notification", type: "ITSidebarNotification", description: "Fila de notificación (icono + label + ITBadget) sobre la navegación: { count, icon?, label?, onClick?, badgeProps? }. Visible también al colapsar." },
1309
1309
  { name: "sidebar.navigationItems[].badge", type: "number | string", description: "Contador que se renderiza con ITBadget en el ítem." },
1310
1310
  { name: "sidebar.navigationItems[].badgeProps", type: "ITBadgetProps", description: "Estilo del badge del ítem (color, size, variant, className para padding)." },
@@ -1312,9 +1312,10 @@ export const LayoutShowcase = () => {
1312
1312
  notes: [
1313
1313
  "`subitems` acepta entradas planas o grupos con `label`+`items`; el título del grupo no es foco ni un tercer nivel y cada grupo se nombra por `aria-labelledby`. En el riel colapsado el flyout aplana los grupos (sin títulos).",
1314
1314
  "El sidebar hereda aria-current/aria-expanded y acepta un slot header opcional.",
1315
- "Los badges por ítem (`badge` + `badgeProps`) y la fila `notification` se dibujan con el átomo `ITBadget`; el padding se ajusta con `badgeProps.className`.",
1315
+ "Los badges por ítem (`badge` + `badgeProps`) y la fila `notification` se dibujan con el átomo `ITBadget`; el padding se ajusta con `badgeProps.className`. `badge` es un contador: en el riel colapsado solo caben ≤3 caracteres y un texto largo se degrada a un punto.",
1316
1316
  "Con `sidebar.isCollapsed` omitido el contenido reserva 88px (hover expande sobre el contenido). Si lo controlas a `false`, reserva 280px y no tapa el contenido.",
1317
1317
  "El riel colapsado se expande automáticamente al pasar el cursor por encima; no hay botón de colapso.",
1318
+ "Riel y panel comparten columna de iconos y alto de fila: los iconos no se mueven al expandir/colapsar. El slot `header` reserva su altura (invisible) en el riel por el mismo motivo.",
1318
1319
  ],
1319
1320
  }}
1320
1321
  />