@axzydev/axzy_ui_system 1.4.3 → 1.4.4

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.4.3",
7
+ "version": "1.4.4",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -22,6 +22,7 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
22
22
  | `header` | `ReactNode` | — | Optional content rendered above the navigation list (e.g. a wordmark). Hidden while the sidebar is collapsed, so it only appears when the hover-expanded rail is shown. |
23
23
  | `notification` | `ITSidebarNotification` | — | Optional notification entry (icon + label + `ITBadget` counter) pinned above the navigation. Unlike `header`, it stays visible while collapsed, showing only the icon and counter. |
24
24
  | `brand` | `ITSidebarBrand` | — | Brand (`logo` + `text`) pinned in a 64px row at the very top. The logo stays visible while collapsed; the text does not. `ITLayout` fills it from `topBar.logo` / `topBar.logoText` when `sidebarFullHeight` is set. |
25
+ | `expandOnHover` | `boolean` | `true` | `true`: the collapsed rail expands over the content on hover. `false`: it stays an icon-only rail (item name in a tooltip) and only opens through `onToggleCollapse` — the top bar menu button in `ITLayout`. Clicking a collapsed parent item opens the sidebar with that group expanded. |
25
26
 
26
27
  ### ITNavigationItem
27
28
 
@@ -103,8 +103,19 @@ export interface ITSidebarProps {
103
103
  navigationItems: ITNavigationItem[];
104
104
  /** Whether the sidebar is collapsed to icon-only mode. */
105
105
  isCollapsed?: boolean;
106
- /** Callback when the user toggles collapse via the toggle button. */
106
+ /**
107
+ * Callback that flips `isCollapsed`. `ITLayout` calls it from the top bar's
108
+ * menu button when `expandOnHover` is `false`; the sidebar itself calls it
109
+ * when a collapsed parent item is clicked, so its submenu can be shown.
110
+ */
107
111
  onToggleCollapse?: () => void;
112
+ /**
113
+ * How the collapsed rail opens. `true`: it expands over the content while
114
+ * the pointer is on it (no button). `false`: it stays an icon-only rail —
115
+ * the item name shows in a tooltip on hover — and it only opens through
116
+ * `onToggleCollapse` (the top bar's menu button in `ITLayout`). @default true
117
+ */
118
+ expandOnHover?: boolean;
108
119
  /** Force sidebar visible on mobile breakpoints. */
109
120
  visibleOnMobile?: boolean;
110
121
  /** Callback when a top-level navigation item is clicked. Receives the item. */
@@ -68,6 +68,8 @@ export default function ITSidebar({
68
68
  header,
69
69
  notification,
70
70
  brand,
71
+ onToggleCollapse,
72
+ expandOnHover = true,
71
73
  }: ITSidebarProps) {
72
74
  // Flat appearance: plain surface, no glow/accent bar, narrower panel.
73
75
  const flat = useITFlatAppearance();
@@ -132,6 +134,13 @@ export default function ITSidebar({
132
134
  }
133
135
  }, [navigationItems]);
134
136
 
137
+ const isSidebarCollapsed = visibleOnMobile ? false : isCollapsed && !(expandOnHover && isHovering);
138
+ const sidebarWidth = isSidebarCollapsed
139
+ ? flat ? "w-[72px]" : "w-[88px]"
140
+ : flat ? "w-[248px]" : "w-[280px]";
141
+ // Icon-only rail that never expands on hover: names show in a tooltip.
142
+ const fixedRail = isSidebarCollapsed && !expandOnHover;
143
+
135
144
  const toggleExpanded = (itemId: string) => {
136
145
  const newExpanded = new Set(expandedItems);
137
146
  if (newExpanded.has(itemId)) newExpanded.delete(itemId);
@@ -141,15 +150,20 @@ export default function ITSidebar({
141
150
 
142
151
  const handleItemClick = (item: ITNavigationItem) => {
143
152
  if (item.subitems && item.subitems.length > 0) {
144
- toggleExpanded(item.id);
153
+ if (isSidebarCollapsed && !expandOnHover) {
154
+ // Icon-only rail: there is no room for the submenu, so open the
155
+ // sidebar with this group expanded.
156
+ setExpandedItems((prev) => new Set(prev).add(item.id));
157
+ onToggleCollapse?.();
158
+ } else {
159
+ toggleExpanded(item.id);
160
+ }
145
161
  } else {
146
162
  if (item.action) item.action();
147
163
  if (onItemClick) onItemClick(item);
148
164
  }
149
165
  };
150
166
 
151
- const isSidebarCollapsed = visibleOnMobile ? false : (!isHovering && isCollapsed);
152
- const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : flat ? "w-[248px]" : "w-[280px]";
153
167
 
154
168
  const renderSubItem = (subitem: ITNavigationSubItem) => (
155
169
  <li key={subitem.id} className="relative">
@@ -163,7 +177,7 @@ export default function ITSidebar({
163
177
  style={{
164
178
  color: subitem.isActive ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-label-color, var(--color-secondary-600))",
165
179
  backgroundColor: subitem.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
166
- fontSize: '0.78rem',
180
+ fontSize: flat ? '0.84rem' : '0.78rem',
167
181
  fontWeight: subitem.isActive ? 600 : 500,
168
182
  letterSpacing: '0.01em',
169
183
  marginLeft: subitemConnector === '|' ? '-1px' : '0',
@@ -181,7 +195,7 @@ export default function ITSidebar({
181
195
  }
182
196
  }}
183
197
  >
184
- {subitem.isActive && subitemConnector === '|' && (
198
+ {subitem.isActive && subitemConnector === '|' && !flat && (
185
199
  <div
186
200
  className="absolute left-0 top-1/3 bottom-1/3 w-[2.5px] rounded-r-full transition-all"
187
201
  style={{
@@ -357,12 +371,15 @@ export default function ITSidebar({
357
371
  )}
358
372
 
359
373
  {/* Navigation Items */}
360
- <nav aria-label="Componentes de la librería" className="flex-1 py-4 overflow-y-auto overflow-x-hidden custom-scrollbar px-4">
374
+ <nav aria-label="Componentes de la librería" className={`flex-1 py-4 custom-scrollbar ${fixedRail ? "overflow-visible" : "overflow-y-auto overflow-x-hidden"} ${flat && isSidebarCollapsed ? "px-3" : "px-4"}`}>
361
375
  <ul className="space-y-1">
362
376
  {navigationItems.map((item) => {
363
377
  const hasSubmenu = !!item.subitems && item.subitems.length > 0;
364
378
  const submenuId = `it-sidebar-submenu-${item.id}`;
365
379
  const { className: badgeClassName, ...badgeRest } = item.badgeProps ?? {};
380
+ // Flat + expanded: a parent whose subitem is active stays plain; the
381
+ // subitem carries the highlight. Collapsed, the parent icon shows it.
382
+ const active = !!item.isActive && !(flat && hasSubmenu && !isSidebarCollapsed);
366
383
  return (
367
384
  <li key={item.id} className="relative group/navitem">
368
385
  <div
@@ -374,18 +391,18 @@ export default function ITSidebar({
374
391
  className={`flex items-center cursor-pointer
375
392
  transition-all duration-300 ease-[cubic-bezier(0.2,0,0,1)]
376
393
  rounded-xl relative overflow-visible
377
- ${isSidebarCollapsed ? "justify-center p-2 mb-1" : "justify-between px-3 py-2 mb-0.5"}
394
+ ${isSidebarCollapsed ? (flat ? "justify-center h-10 mb-1" : "justify-center p-2 mb-1") : "justify-between px-3 py-2 mb-0.5"}
378
395
  `}
379
396
  style={{
380
- backgroundColor: item.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
381
- boxShadow: item.isActive && !flat ? 'var(--shadow-xs)' : 'none',
382
- border: item.isActive && !flat ? "1px solid var(--it-sidebar-border, var(--color-secondary-200))" : '1px solid transparent'
397
+ backgroundColor: active ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
398
+ boxShadow: active && !flat ? 'var(--shadow-xs)' : 'none',
399
+ border: active && !flat ? "1px solid var(--it-sidebar-border, var(--color-secondary-200))" : '1px solid transparent'
383
400
  }}
384
401
  onMouseEnter={(e) => {
385
- if (!item.isActive) e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
402
+ if (!active) e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
386
403
  }}
387
404
  onMouseLeave={(e) => {
388
- if (!item.isActive) e.currentTarget.style.backgroundColor = 'transparent';
405
+ if (!active) e.currentTarget.style.backgroundColor = 'transparent';
389
406
  }}
390
407
  onClick={() => handleItemClick(item)}
391
408
  onKeyDown={(e) => {
@@ -395,7 +412,7 @@ export default function ITSidebar({
395
412
  }
396
413
  }}
397
414
  >
398
- {item.isActive && !isSidebarCollapsed && !flat && (
415
+ {active && !isSidebarCollapsed && !flat && (
399
416
  <div
400
417
  className="absolute left-0 top-1/4 bottom-1/4 w-[3px] rounded-r-full transition-all"
401
418
  style={{ backgroundColor: "var(--it-sidebar-active-icon, var(--color-primary-500))", boxShadow: "0 0 10px var(--it-sidebar-active-icon, var(--color-primary-500))" }}
@@ -407,10 +424,10 @@ export default function ITSidebar({
407
424
  <div
408
425
  className={`transition-all duration-300 flex-shrink-0 flex items-center justify-center`}
409
426
  style={{
410
- color: item.isActive ? "var(--it-sidebar-active-icon, var(--color-primary-500))" : "var(--it-sidebar-icon-color, #9ca3af)",
411
- opacity: item.isActive ? 1 : 0.8,
412
- fontSize: flat ? '1rem' : item.isActive ? '1.12rem' : '1.05rem',
413
- filter: item.isActive && !flat ? 'drop-shadow(0 0 8px rgba(255,255,255,0.2))' : 'none'
427
+ color: active ? "var(--it-sidebar-active-icon, var(--color-primary-500))" : "var(--it-sidebar-icon-color, #9ca3af)",
428
+ opacity: active ? 1 : 0.8,
429
+ fontSize: flat ? '1rem' : active ? '1.12rem' : '1.05rem',
430
+ filter: active && !flat ? 'drop-shadow(0 0 8px rgba(255,255,255,0.2))' : 'none'
414
431
  }}
415
432
  >
416
433
  {item.icon}
@@ -421,9 +438,9 @@ export default function ITSidebar({
421
438
  <ITText as="span"
422
439
  className={`transition-all duration-300 truncate tracking-wide`}
423
440
  style={{
424
- color: item.isActive ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
441
+ color: active ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
425
442
  fontSize: flat ? '0.875rem' : '0.8rem',
426
- fontWeight: item.isActive ? '600' : '500'
443
+ fontWeight: active ? '600' : '500'
427
444
  }}
428
445
  >
429
446
  {item.label}
@@ -433,7 +450,7 @@ export default function ITSidebar({
433
450
 
434
451
  {!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
435
452
  <div className={`flex-shrink-0 transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "rotate-180" : ""}`}
436
- style={{ color: item.isActive ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-icon-color, var(--color-secondary-500))", opacity: 0.7 }}>
453
+ style={{ color: active ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-icon-color, var(--color-secondary-500))", opacity: 0.7 }}>
437
454
  <FaChevronDown className="w-3 h-3" />
438
455
  </div>
439
456
  )}
@@ -457,7 +474,21 @@ export default function ITSidebar({
457
474
  </div>
458
475
 
459
476
  {/* Glassmorphism Collapsed Tooltip / Submenu */}
460
- {isSidebarCollapsed && (
477
+ {isSidebarCollapsed && flat && (
478
+ <div
479
+ role="tooltip"
480
+ className="absolute left-full top-1/2 -translate-y-1/2 ml-3 px-2.5 py-1.5 rounded-lg whitespace-nowrap opacity-0 invisible group-hover/navitem:opacity-100 group-hover/navitem:visible transition-opacity duration-150 pointer-events-none z-[70]"
481
+ style={{
482
+ backgroundColor: "var(--it-sidebar-tooltip-bg, var(--color-secondary-900))",
483
+ color: "var(--it-sidebar-tooltip-color, #ffffff)",
484
+ fontSize: "0.78rem",
485
+ fontWeight: 500,
486
+ }}
487
+ >
488
+ {item.label}
489
+ </div>
490
+ )}
491
+ {isSidebarCollapsed && !flat && (
461
492
  <div
462
493
  className="absolute left-full top-0 ml-4 rounded-2xl opacity-0 invisible group-hover/navitem:opacity-100 group-hover/navitem:visible transition-all duration-300 pointer-events-none z-[70] min-w-[220px] overflow-hidden -translate-x-2 group-hover/navitem:translate-x-0 shadow-[0_10px_40px_-10px_rgba(0,0,0,0.15)]"
463
494
  style={{
@@ -503,7 +534,7 @@ export default function ITSidebar({
503
534
  {!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
504
535
  <div id={submenuId} role="group" aria-label={item.label} className={`overflow-hidden transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "max-h-[1000px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
505
536
  <ul
506
- className="ml-4 flex flex-col gap-0 py-0.5"
537
+ className={`${flat && subitemConnector === '|' ? "ml-[21px] pl-2" : "ml-4"} flex flex-col gap-0 py-0.5`}
507
538
  style={{
508
539
  borderLeft: subitemConnector === '|'
509
540
  ? "1px solid var(--it-sidebar-border, var(--color-secondary-200))"
@@ -49,6 +49,12 @@ export interface ITTopBarProps {
49
49
  showMobileMenuButton?: boolean;
50
50
  /** Callback fired when the mobile menu toggle button is clicked. */
51
51
  onToggleMobileMenu?: () => void;
52
+ /**
53
+ * Keeps the menu toggle button visible from the `lg` breakpoint up as well
54
+ * (it is mobile-only by default). `ITLayout` turns it on when the sidebar
55
+ * collapses by button instead of hover (`sidebar.expandOnHover === false`). @default false
56
+ */
57
+ showDesktopMenuButton?: boolean;
52
58
  /**
53
59
  * Hides the logo and `logoText` from the `lg` breakpoint up and left-aligns
54
60
  * `centerContent`. Use it when the brand already lives somewhere else (e.g.
@@ -69,6 +69,7 @@ export default function ITTopBar({
69
69
  navItems,
70
70
  onNavItemClick,
71
71
  hideBrandOnDesktop = false,
72
+ showDesktopMenuButton = false,
72
73
  }: ITTopBarProps) {
73
74
  // Flat appearance: plain bar (no shadow/blur), initials avatar, name before it.
74
75
  const flat = useITFlatAppearance();
@@ -100,7 +101,9 @@ export default function ITTopBar({
100
101
  {/* Mobile Sidebar Toggle */}
101
102
  {showMobileMenuButton && (
102
103
  <button
103
- className="lg:hidden p-2.5 rounded-xl transition-colors duration-200"
104
+ type="button"
105
+ aria-label="Menú"
106
+ className={`${showDesktopMenuButton ? "" : "lg:hidden"} p-2.5 -ml-2.5 rounded-xl transition-colors duration-200`}
104
107
  style={{
105
108
  color: "var(--it-topbar-icon, #64748b)",
106
109
  }}
@@ -56,8 +56,17 @@ export default function ITLayout({
56
56
  const handleToggleCollapse = isControlled ? (sidebar.onToggleCollapse ?? (() => {})) : () => setInternalCollapsed(v => !v);
57
57
 
58
58
  const layoutTokens = theme.layout;
59
- // Must match ITSidebar's own at-rest width (88px rail / 280px, 248px when flat).
60
- const reservedWidth = desktopCollapsed ? "w-[88px]" : flat ? "w-[248px]" : "w-[280px]";
59
+ // Must match ITSidebar's own at-rest width (88px rail / 280px; 72px / 248px when flat).
60
+ const reservedWidth = desktopCollapsed
61
+ ? flat ? "w-[72px]" : "w-[88px]"
62
+ : flat ? "w-[248px]" : "w-[280px]";
63
+ // The sidebar collapses by button instead of hover: the top bar's menu
64
+ // button toggles the rail on desktop and still opens the drawer on mobile.
65
+ const toggleByButton = sidebar.expandOnHover === false;
66
+ const handleMenuButton = () => {
67
+ if (toggleByButton && window.matchMedia("(min-width: 1024px)").matches) handleToggleCollapse();
68
+ else setMobileSidebarOpen(v => !v);
69
+ };
61
70
  // With a full-height sidebar the brand moves from the top bar to the sidebar.
62
71
  const brand = sidebarFullHeight
63
72
  ? (sidebar.brand ?? { logo: topBar.logo, text: topBar.logoText })
@@ -68,7 +77,8 @@ export default function ITLayout({
68
77
  {...topBar}
69
78
  hideBrandOnDesktop={sidebarFullHeight || topBar.hideBrandOnDesktop}
70
79
  showMobileMenuButton
71
- onToggleMobileMenu={() => setMobileSidebarOpen(v => !v)}
80
+ showDesktopMenuButton={toggleByButton || topBar.showDesktopMenuButton}
81
+ onToggleMobileMenu={handleMenuButton}
72
82
  />
73
83
  );
74
84