@axzydev/axzy_ui_system 1.4.2 → 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.2",
7
+ "version": "1.4.4",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -2,6 +2,7 @@ import clsx from "clsx";
2
2
  import { ITCardProps } from "./card.props";
3
3
  import { useState } from "react";
4
4
  import ITText from "@/components/atoms/text/text";
5
+ import { useITFlatAppearance } from "@/theme/theme-context";
5
6
 
6
7
  /**
7
8
  * Versatile card container with optional image, title, body content, and action footer. Supports interactive hover states.
@@ -30,21 +31,33 @@ export default function ITCard({
30
31
  onClick,
31
32
  }: ITCardProps) {
32
33
  const [isHovered, setIsHovered] = useState(false);
34
+ const flat = useITFlatAppearance();
33
35
 
34
36
  const containerStyle: React.CSSProperties = {
35
37
  backgroundColor: "var(--it-card-bg, #ffffff)",
36
- borderColor: "var(--it-card-border, rgba(15, 23, 42, 0.06))",
38
+ borderColor: flat
39
+ ? "var(--it-card-flat-border, var(--color-secondary-200))"
40
+ : "var(--it-card-border, rgba(15, 23, 42, 0.06))",
37
41
  borderWidth: "1px",
38
- borderRadius: "var(--it-card-radius, var(--radius-2xl))",
42
+ borderStyle: "solid",
43
+ borderRadius: flat
44
+ ? "var(--it-card-radius, var(--radius-lg))"
45
+ : "var(--it-card-radius, var(--radius-2xl))",
39
46
  // Soft depth: every card floats a little at rest; interactive cards
40
47
  // gain visual weight on hover instead of appearing flat until clicked.
41
- boxShadow: onClick
42
- ? isHovered
43
- ? "var(--shadow-lg)"
44
- : "var(--shadow-sm)"
45
- : "var(--shadow-sm)",
48
+ // Flat appearance: the hairline border carries the surface, so a card
49
+ // only gets a shadow as hover feedback when it is clickable.
50
+ boxShadow: flat
51
+ ? onClick && isHovered
52
+ ? "var(--shadow-sm)"
53
+ : "none"
54
+ : onClick
55
+ ? isHovered
56
+ ? "var(--shadow-lg)"
57
+ : "var(--shadow-sm)"
58
+ : "var(--shadow-sm)",
46
59
  transition: onClick ? "box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out" : "none",
47
- transform: onClick && isHovered ? "translateY(-2px)" : "translateY(0)",
60
+ transform: onClick && isHovered && !flat ? "translateY(-2px)" : "translateY(0)",
48
61
  cursor: onClick ? "pointer" : "default",
49
62
  };
50
63
 
@@ -72,7 +85,7 @@ export default function ITCard({
72
85
  {title && (
73
86
  <ITText
74
87
  as="h3"
75
- className={clsx("text-xl font-semibold mb-2", titleClassName)}
88
+ className={clsx(flat ? "text-base font-semibold mb-3" : "text-xl font-semibold mb-2", titleClassName)}
76
89
  >
77
90
  {title}
78
91
  </ITText>
@@ -2,6 +2,7 @@ import { FaChevronLeft } from "react-icons/fa";
2
2
  import { ITPageHeaderProps } from "./page-header.props";
3
3
  import ITBreadcrumbs from "@/components/molecules/breadcrumbs/breadcrumbs";
4
4
  import ITText from "@/components/atoms/text/text";
5
+ import { useITFlatAppearance } from "@/theme/theme-context";
5
6
 
6
7
  /**
7
8
  * Page title bar with breadcrumbs, description, back button, and action buttons.
@@ -29,6 +30,8 @@ export default function ITPageHeader({
29
30
  className,
30
31
  }: ITPageHeaderProps) {
31
32
  const showTopRow = breadcrumbs?.length || backAction;
33
+ // Flat appearance: compact title, no decorative icon tile.
34
+ const flat = useITFlatAppearance();
32
35
 
33
36
  return (
34
37
  <div className={`${className} space-y-3`}>
@@ -60,7 +63,7 @@ export default function ITPageHeader({
60
63
  {/* Fila principal: icono + título + descripción + (acciones si no hay top row) */}
61
64
  <div className="flex items-start justify-between gap-6">
62
65
  <div className="flex items-start gap-4 min-w-0">
63
- {icon && (
66
+ {icon && !flat && (
64
67
  <div
65
68
  className="flex items-center justify-center w-12 h-12 rounded-full border border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300 flex-shrink-0 transition-all duration-200 hover:border-slate-300 dark:hover:border-slate-500 hover:shadow-sm"
66
69
  style={iconColor ? { color: iconColor } : undefined}
@@ -71,14 +74,22 @@ export default function ITPageHeader({
71
74
  <div className="min-w-0">
72
75
  <ITText
73
76
  as="h1"
74
- className="text-3xl font-bold text-slate-800 dark:text-white tracking-tight leading-tight"
77
+ className={
78
+ flat
79
+ ? "text-[1.625rem] font-bold text-slate-900 dark:text-white tracking-tight leading-tight"
80
+ : "text-3xl font-bold text-slate-800 dark:text-white tracking-tight leading-tight"
81
+ }
75
82
  >
76
83
  {title}
77
84
  </ITText>
78
85
  {description && (
79
86
  <ITText
80
87
  as="p"
81
- className="text-sm font-light text-slate-500 dark:text-slate-400 mt-1 leading-relaxed"
88
+ className={
89
+ flat
90
+ ? "text-sm font-normal text-slate-500 dark:text-slate-400 mt-0.5 leading-relaxed"
91
+ : "text-sm font-light text-slate-500 dark:text-slate-400 mt-1 leading-relaxed"
92
+ }
82
93
  >
83
94
  {description}
84
95
  </ITText>
@@ -21,6 +21,8 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
21
21
  | `className` | `string` | — | Additional CSS classes on the sidebar `<aside>`. |
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
+ | `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. |
24
26
 
25
27
  ### ITNavigationItem
26
28
 
@@ -86,14 +86,36 @@ export interface ITSidebarNotification {
86
86
  badgeProps?: ITBadgetProps;
87
87
  }
88
88
 
89
+ /**
90
+ * Brand block pinned at the top of {@link ITSidebarProps}. The logo stays
91
+ * visible while the rail is collapsed; the text only shows when expanded.
92
+ */
93
+ export interface ITSidebarBrand {
94
+ /** Logo element (e.g. an `<img>` or an SVG mark). Always visible. */
95
+ logo?: React.ReactNode;
96
+ /** Wordmark rendered next to the logo. Hidden while the sidebar is collapsed. */
97
+ text?: string;
98
+ }
99
+
89
100
  /** Props for the ITSidebar vertical navigation component. */
90
101
  export interface ITSidebarProps {
91
102
  /** Navigation structure: top-level items with optional sub-items. */
92
103
  navigationItems: ITNavigationItem[];
93
104
  /** Whether the sidebar is collapsed to icon-only mode. */
94
105
  isCollapsed?: boolean;
95
- /** 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
+ */
96
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;
97
119
  /** Force sidebar visible on mobile breakpoints. */
98
120
  visibleOnMobile?: boolean;
99
121
  /** Callback when a top-level navigation item is clicked. Receives the item. */
@@ -115,5 +137,11 @@ export interface ITSidebarProps {
115
137
  * while collapsed, showing just the icon and the counter.
116
138
  */
117
139
  notification?: ITSidebarNotification;
140
+ /**
141
+ * Brand (logo + wordmark) pinned at the very top of the sidebar, in a 64px
142
+ * row that lines up with the top bar. Unlike {@link ITSidebarProps.header}
143
+ * the logo stays visible while collapsed. `ITLayout` fills it from
144
+ * `topBar.logo` / `topBar.logoText` when `sidebarFullHeight` is set.
145
+ */
146
+ brand?: ITSidebarBrand;
118
147
  }
119
-
@@ -10,6 +10,7 @@ import {
10
10
  } from "./sidebar.props";
11
11
  import ITBadget from "@/components/atoms/badget/badget";
12
12
  import ITText from "@/components/atoms/text/text";
13
+ import { useITFlatAppearance } from "@/theme/theme-context";
13
14
 
14
15
  /** Narrows a submenu entry to a titled group. */
15
16
  function isSubItemGroup(
@@ -66,7 +67,12 @@ export default function ITSidebar({
66
67
  subitemConnector = 'dot',
67
68
  header,
68
69
  notification,
70
+ brand,
71
+ onToggleCollapse,
72
+ expandOnHover = true,
69
73
  }: ITSidebarProps) {
74
+ // Flat appearance: plain surface, no glow/accent bar, narrower panel.
75
+ const flat = useITFlatAppearance();
70
76
  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
71
77
  const [isHovering, setIsHovering] = useState(false);
72
78
  const sidebarRef = useRef<HTMLDivElement>(null);
@@ -128,6 +134,13 @@ export default function ITSidebar({
128
134
  }
129
135
  }, [navigationItems]);
130
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
+
131
144
  const toggleExpanded = (itemId: string) => {
132
145
  const newExpanded = new Set(expandedItems);
133
146
  if (newExpanded.has(itemId)) newExpanded.delete(itemId);
@@ -137,15 +150,20 @@ export default function ITSidebar({
137
150
 
138
151
  const handleItemClick = (item: ITNavigationItem) => {
139
152
  if (item.subitems && item.subitems.length > 0) {
140
- 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
+ }
141
161
  } else {
142
162
  if (item.action) item.action();
143
163
  if (onItemClick) onItemClick(item);
144
164
  }
145
165
  };
146
166
 
147
- const isSidebarCollapsed = visibleOnMobile ? false : (!isHovering && isCollapsed);
148
- const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : "w-[280px]";
149
167
 
150
168
  const renderSubItem = (subitem: ITNavigationSubItem) => (
151
169
  <li key={subitem.id} className="relative">
@@ -159,7 +177,7 @@ export default function ITSidebar({
159
177
  style={{
160
178
  color: subitem.isActive ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-label-color, var(--color-secondary-600))",
161
179
  backgroundColor: subitem.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
162
- fontSize: '0.78rem',
180
+ fontSize: flat ? '0.84rem' : '0.78rem',
163
181
  fontWeight: subitem.isActive ? 600 : 500,
164
182
  letterSpacing: '0.01em',
165
183
  marginLeft: subitemConnector === '|' ? '-1px' : '0',
@@ -177,7 +195,7 @@ export default function ITSidebar({
177
195
  }
178
196
  }}
179
197
  >
180
- {subitem.isActive && subitemConnector === '|' && (
198
+ {subitem.isActive && subitemConnector === '|' && !flat && (
181
199
  <div
182
200
  className="absolute left-0 top-1/3 bottom-1/3 w-[2.5px] rounded-r-full transition-all"
183
201
  style={{
@@ -318,16 +336,32 @@ export default function ITSidebar({
318
336
  ${sidebarWidth}
319
337
  ${className}
320
338
  ${!visibleOnMobile ? "hidden lg:flex" : "flex"}
321
- shadow-[4px_0_32px_rgba(15,23,42,0.04)]
339
+ ${flat ? "" : "shadow-[4px_0_32px_rgba(15,23,42,0.04)]"}
322
340
  `}
323
341
  style={{
324
342
  zIndex: 50,
325
343
  backgroundColor: "var(--it-sidebar-bg, rgba(255, 255, 255, 0.90))",
326
344
  borderRight: "1px solid var(--it-sidebar-border, var(--color-secondary-200))",
327
- WebkitBackdropFilter: 'blur(12px)',
328
- backdropFilter: 'blur(12px)',
345
+ ...(flat ? {} : { WebkitBackdropFilter: 'blur(12px)', backdropFilter: 'blur(12px)' }),
329
346
  }}
330
347
  >
348
+ {brand && (brand.logo || brand.text) && (
349
+ <div
350
+ className={`flex items-center flex-shrink-0 h-[64px] ${isSidebarCollapsed ? "justify-center px-2" : "gap-2.5 px-7"}`}
351
+ >
352
+ {brand.logo && <div className="flex-shrink-0 flex items-center">{brand.logo}</div>}
353
+ {brand.text && !isSidebarCollapsed && (
354
+ <ITText
355
+ as="span"
356
+ className="text-[1.05rem] font-bold tracking-tight truncate"
357
+ style={{ color: "var(--it-sidebar-brand-color, var(--it-sidebar-label-color, var(--color-secondary-900)))" }}
358
+ >
359
+ {brand.text}
360
+ </ITText>
361
+ )}
362
+ </div>
363
+ )}
364
+
331
365
  {header && !isSidebarCollapsed && (
332
366
  <div className="px-4 pt-4 pb-1 flex-shrink-0">{header}</div>
333
367
  )}
@@ -337,12 +371,15 @@ export default function ITSidebar({
337
371
  )}
338
372
 
339
373
  {/* Navigation Items */}
340
- <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"}`}>
341
375
  <ul className="space-y-1">
342
376
  {navigationItems.map((item) => {
343
377
  const hasSubmenu = !!item.subitems && item.subitems.length > 0;
344
378
  const submenuId = `it-sidebar-submenu-${item.id}`;
345
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);
346
383
  return (
347
384
  <li key={item.id} className="relative group/navitem">
348
385
  <div
@@ -354,18 +391,18 @@ export default function ITSidebar({
354
391
  className={`flex items-center cursor-pointer
355
392
  transition-all duration-300 ease-[cubic-bezier(0.2,0,0,1)]
356
393
  rounded-xl relative overflow-visible
357
- ${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"}
358
395
  `}
359
396
  style={{
360
- backgroundColor: item.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
361
- boxShadow: item.isActive ? 'var(--shadow-xs)' : 'none',
362
- border: item.isActive ? "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'
363
400
  }}
364
401
  onMouseEnter={(e) => {
365
- 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))";
366
403
  }}
367
404
  onMouseLeave={(e) => {
368
- if (!item.isActive) e.currentTarget.style.backgroundColor = 'transparent';
405
+ if (!active) e.currentTarget.style.backgroundColor = 'transparent';
369
406
  }}
370
407
  onClick={() => handleItemClick(item)}
371
408
  onKeyDown={(e) => {
@@ -375,7 +412,7 @@ export default function ITSidebar({
375
412
  }
376
413
  }}
377
414
  >
378
- {item.isActive && !isSidebarCollapsed && (
415
+ {active && !isSidebarCollapsed && !flat && (
379
416
  <div
380
417
  className="absolute left-0 top-1/4 bottom-1/4 w-[3px] rounded-r-full transition-all"
381
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))" }}
@@ -387,10 +424,10 @@ export default function ITSidebar({
387
424
  <div
388
425
  className={`transition-all duration-300 flex-shrink-0 flex items-center justify-center`}
389
426
  style={{
390
- color: item.isActive ? "var(--it-sidebar-active-icon, var(--color-primary-500))" : "var(--it-sidebar-icon-color, #9ca3af)",
391
- opacity: item.isActive ? 1 : 0.8,
392
- fontSize: item.isActive ? '1.12rem' : '1.05rem',
393
- filter: item.isActive ? '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'
394
431
  }}
395
432
  >
396
433
  {item.icon}
@@ -401,9 +438,9 @@ export default function ITSidebar({
401
438
  <ITText as="span"
402
439
  className={`transition-all duration-300 truncate tracking-wide`}
403
440
  style={{
404
- color: item.isActive ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
405
- fontSize: '0.8rem',
406
- fontWeight: item.isActive ? '600' : '500'
441
+ color: active ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
442
+ fontSize: flat ? '0.875rem' : '0.8rem',
443
+ fontWeight: active ? '600' : '500'
407
444
  }}
408
445
  >
409
446
  {item.label}
@@ -413,7 +450,7 @@ export default function ITSidebar({
413
450
 
414
451
  {!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
415
452
  <div className={`flex-shrink-0 transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "rotate-180" : ""}`}
416
- 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 }}>
417
454
  <FaChevronDown className="w-3 h-3" />
418
455
  </div>
419
456
  )}
@@ -437,7 +474,21 @@ export default function ITSidebar({
437
474
  </div>
438
475
 
439
476
  {/* Glassmorphism Collapsed Tooltip / Submenu */}
440
- {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 && (
441
492
  <div
442
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)]"
443
494
  style={{
@@ -483,7 +534,7 @@ export default function ITSidebar({
483
534
  {!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
484
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"}`}>
485
536
  <ul
486
- 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`}
487
538
  style={{
488
539
  borderLeft: subitemConnector === '|'
489
540
  ? "1px solid var(--it-sidebar-border, var(--color-secondary-200))"
@@ -49,4 +49,17 @@ 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;
58
+ /**
59
+ * Hides the logo and `logoText` from the `lg` breakpoint up and left-aligns
60
+ * `centerContent`. Use it when the brand already lives somewhere else (e.g.
61
+ * at the top of a full-height sidebar); `ITLayout` sets it for you when
62
+ * `sidebarFullHeight` is on. Below `lg` the logo is still shown. @default false
63
+ */
64
+ hideBrandOnDesktop?: boolean;
52
65
  }
@@ -3,6 +3,7 @@ import useClickOutside from "@/hooks/useClickOutside";
3
3
  import { useRef, useState } from "react";
4
4
  import { ITTopBarProps } from "./topbar.props";
5
5
  import ITText from "@/components/atoms/text/text";
6
+ import { useITFlatAppearance } from "@/theme/theme-context";
6
7
 
7
8
  /**
8
9
  * ITTopBar — sticky top navigation bar with logo, nav items, and user avatar dropdown.
@@ -67,7 +68,17 @@ export default function ITTopBar({
67
68
  onToggleMobileMenu,
68
69
  navItems,
69
70
  onNavItemClick,
71
+ hideBrandOnDesktop = false,
72
+ showDesktopMenuButton = false,
70
73
  }: ITTopBarProps) {
74
+ // Flat appearance: plain bar (no shadow/blur), initials avatar, name before it.
75
+ const flat = useITFlatAppearance();
76
+ const initials = (userMenu?.userName ?? "")
77
+ .trim()
78
+ .split(/\s+/)
79
+ .slice(0, 2)
80
+ .map((word) => word.charAt(0).toUpperCase())
81
+ .join("");
71
82
  const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
72
83
  const userMenuRef = useRef<HTMLDivElement>(null);
73
84
 
@@ -75,14 +86,14 @@ export default function ITTopBar({
75
86
 
76
87
  return (
77
88
  <header
78
- className="sticky top-0 z-40 backdrop-blur-md transition-all duration-300"
89
+ className={`sticky top-0 z-40 transition-all duration-300 ${flat ? "" : "backdrop-blur-md"}`}
79
90
  style={{
80
91
  backgroundColor: "var(--it-topbar-bg, rgba(255, 255, 255, 0.9))",
81
92
  borderBottom: "1px solid var(--it-topbar-border, rgba(15, 23, 42, 0.06))",
82
- boxShadow: "var(--shadow-xs)",
93
+ boxShadow: flat ? "none" : "var(--shadow-xs)",
83
94
  }}
84
95
  >
85
- <div className="flex items-center justify-between h-[72px] px-6 lg:px-8">
96
+ <div className={`flex items-center justify-between px-6 lg:px-8 ${flat ? "h-[64px]" : "h-[72px]"}`}>
86
97
 
87
98
  {/* LEFT AREA: Logo & Mobile Toggle */}
88
99
  <div className="flex items-center gap-5">
@@ -90,7 +101,9 @@ export default function ITTopBar({
90
101
  {/* Mobile Sidebar Toggle */}
91
102
  {showMobileMenuButton && (
92
103
  <button
93
- 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`}
94
107
  style={{
95
108
  color: "var(--it-topbar-icon, #64748b)",
96
109
  }}
@@ -103,7 +116,7 @@ export default function ITTopBar({
103
116
  )}
104
117
 
105
118
  {/* Logo */}
106
- <div className="flex items-center gap-3">
119
+ <div className={`items-center gap-3 ${hideBrandOnDesktop ? "flex lg:hidden" : "flex"}`}>
107
120
  {logo && (
108
121
  <div className="flex-shrink-0 drop-shadow-sm">
109
122
  {logo}
@@ -153,7 +166,7 @@ export default function ITTopBar({
153
166
  `justify-between` has nothing left to distribute: the logo/nav and
154
167
  the user menu stay flush with the row edges. */}
155
168
  {centerContent && (
156
- <div className="hidden lg:flex flex-1 min-w-0 items-center justify-center px-4">
169
+ <div className={`hidden lg:flex flex-1 min-w-0 items-center ${hideBrandOnDesktop ? "justify-start pr-4" : "justify-center px-4"}`}>
157
170
  <div className="w-full max-w-[420px]">{centerContent}</div>
158
171
  </div>
159
172
  )}
@@ -169,7 +182,7 @@ export default function ITTopBar({
169
182
  <div className="relative">
170
183
  <button
171
184
  type="button"
172
- className="flex items-center gap-3 rounded-full pl-2 pr-4 py-1.5 transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)] border border-transparent hover:border-secondary-200"
185
+ className={`flex items-center gap-3 rounded-full py-1.5 transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)] border border-transparent hover:border-secondary-200 ${flat ? "flex-row-reverse pl-4 pr-2" : "pl-2 pr-4"}`}
173
186
  style={{
174
187
  backgroundColor: isUserMenuOpen ? "var(--it-topbar-user-hover, #f1f5f9)" : "transparent",
175
188
  }}
@@ -189,17 +202,30 @@ export default function ITTopBar({
189
202
  src={userMenu.userImage}
190
203
  alt="Current user"
191
204
  />
205
+ ) : flat && initials ? (
206
+ <div
207
+ aria-hidden="true"
208
+ className="w-9 h-9 rounded-full flex items-center justify-center text-[0.8rem] font-semibold"
209
+ style={{
210
+ backgroundColor: "color-mix(in srgb, var(--color-primary, #3b82f6) 14%, transparent)",
211
+ color: "var(--color-primary, #2563eb)",
212
+ }}
213
+ >
214
+ {initials}
215
+ </div>
192
216
  ) : (
193
217
  <div className="w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center ring-2 ring-white shadow-sm">
194
218
  <FaUserCircle className="w-6 h-6" style={{ color: "var(--it-topbar-icon, #94a3b8)" }} />
195
219
  </div>
196
220
  )}
197
221
  {/* Active dot indicator */}
198
- <div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
222
+ {!flat && (
223
+ <div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
224
+ )}
199
225
  </div>
200
226
 
201
227
  {/* Name Details */}
202
- <div className="hidden sm:flex flex-col text-left py-0.5">
228
+ <div className={`hidden sm:flex flex-col py-0.5 ${flat ? "text-right" : "text-left"}`}>
203
229
  <ITText as="span"
204
230
  className="font-semibold text-[0.85rem] leading-tight"
205
231
  style={{ color: "var(--it-topbar-user-text, #0f172a)" }}
@@ -12,4 +12,12 @@ export interface ITLayoutProps {
12
12
  className?: string;
13
13
  /** Additional CSS classes for the content container */
14
14
  contentClassName?: string;
15
+ /**
16
+ * App-shell arrangement. `false` (default): the top bar spans the full width
17
+ * and the sidebar sits below it. `true`: the sidebar spans the full viewport
18
+ * height and shows the brand (`topBar.logo` + `topBar.logoText`) at its top,
19
+ * while the top bar covers only the content column and drops the logo from
20
+ * the `lg` breakpoint up. Mobile is unchanged. @default false
21
+ */
22
+ sidebarFullHeight?: boolean;
15
23
  }