@axzydev/axzy_ui_system 1.4.2 → 1.4.3

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.3",
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,7 @@ 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. |
24
25
 
25
26
  ### ITNavigationItem
26
27
 
@@ -86,6 +86,17 @@ 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. */
@@ -115,5 +126,11 @@ export interface ITSidebarProps {
115
126
  * while collapsed, showing just the icon and the counter.
116
127
  */
117
128
  notification?: ITSidebarNotification;
129
+ /**
130
+ * Brand (logo + wordmark) pinned at the very top of the sidebar, in a 64px
131
+ * row that lines up with the top bar. Unlike {@link ITSidebarProps.header}
132
+ * the logo stays visible while collapsed. `ITLayout` fills it from
133
+ * `topBar.logo` / `topBar.logoText` when `sidebarFullHeight` is set.
134
+ */
135
+ brand?: ITSidebarBrand;
118
136
  }
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,10 @@ export default function ITSidebar({
66
67
  subitemConnector = 'dot',
67
68
  header,
68
69
  notification,
70
+ brand,
69
71
  }: ITSidebarProps) {
72
+ // Flat appearance: plain surface, no glow/accent bar, narrower panel.
73
+ const flat = useITFlatAppearance();
70
74
  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
71
75
  const [isHovering, setIsHovering] = useState(false);
72
76
  const sidebarRef = useRef<HTMLDivElement>(null);
@@ -145,7 +149,7 @@ export default function ITSidebar({
145
149
  };
146
150
 
147
151
  const isSidebarCollapsed = visibleOnMobile ? false : (!isHovering && isCollapsed);
148
- const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : "w-[280px]";
152
+ const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : flat ? "w-[248px]" : "w-[280px]";
149
153
 
150
154
  const renderSubItem = (subitem: ITNavigationSubItem) => (
151
155
  <li key={subitem.id} className="relative">
@@ -318,16 +322,32 @@ export default function ITSidebar({
318
322
  ${sidebarWidth}
319
323
  ${className}
320
324
  ${!visibleOnMobile ? "hidden lg:flex" : "flex"}
321
- shadow-[4px_0_32px_rgba(15,23,42,0.04)]
325
+ ${flat ? "" : "shadow-[4px_0_32px_rgba(15,23,42,0.04)]"}
322
326
  `}
323
327
  style={{
324
328
  zIndex: 50,
325
329
  backgroundColor: "var(--it-sidebar-bg, rgba(255, 255, 255, 0.90))",
326
330
  borderRight: "1px solid var(--it-sidebar-border, var(--color-secondary-200))",
327
- WebkitBackdropFilter: 'blur(12px)',
328
- backdropFilter: 'blur(12px)',
331
+ ...(flat ? {} : { WebkitBackdropFilter: 'blur(12px)', backdropFilter: 'blur(12px)' }),
329
332
  }}
330
333
  >
334
+ {brand && (brand.logo || brand.text) && (
335
+ <div
336
+ className={`flex items-center flex-shrink-0 h-[64px] ${isSidebarCollapsed ? "justify-center px-2" : "gap-2.5 px-7"}`}
337
+ >
338
+ {brand.logo && <div className="flex-shrink-0 flex items-center">{brand.logo}</div>}
339
+ {brand.text && !isSidebarCollapsed && (
340
+ <ITText
341
+ as="span"
342
+ className="text-[1.05rem] font-bold tracking-tight truncate"
343
+ style={{ color: "var(--it-sidebar-brand-color, var(--it-sidebar-label-color, var(--color-secondary-900)))" }}
344
+ >
345
+ {brand.text}
346
+ </ITText>
347
+ )}
348
+ </div>
349
+ )}
350
+
331
351
  {header && !isSidebarCollapsed && (
332
352
  <div className="px-4 pt-4 pb-1 flex-shrink-0">{header}</div>
333
353
  )}
@@ -358,8 +378,8 @@ export default function ITSidebar({
358
378
  `}
359
379
  style={{
360
380
  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'
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'
363
383
  }}
364
384
  onMouseEnter={(e) => {
365
385
  if (!item.isActive) e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
@@ -375,7 +395,7 @@ export default function ITSidebar({
375
395
  }
376
396
  }}
377
397
  >
378
- {item.isActive && !isSidebarCollapsed && (
398
+ {item.isActive && !isSidebarCollapsed && !flat && (
379
399
  <div
380
400
  className="absolute left-0 top-1/4 bottom-1/4 w-[3px] rounded-r-full transition-all"
381
401
  style={{ backgroundColor: "var(--it-sidebar-active-icon, var(--color-primary-500))", boxShadow: "0 0 10px var(--it-sidebar-active-icon, var(--color-primary-500))" }}
@@ -389,8 +409,8 @@ export default function ITSidebar({
389
409
  style={{
390
410
  color: item.isActive ? "var(--it-sidebar-active-icon, var(--color-primary-500))" : "var(--it-sidebar-icon-color, #9ca3af)",
391
411
  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'
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'
394
414
  }}
395
415
  >
396
416
  {item.icon}
@@ -402,7 +422,7 @@ export default function ITSidebar({
402
422
  className={`transition-all duration-300 truncate tracking-wide`}
403
423
  style={{
404
424
  color: item.isActive ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
405
- fontSize: '0.8rem',
425
+ fontSize: flat ? '0.875rem' : '0.8rem',
406
426
  fontWeight: item.isActive ? '600' : '500'
407
427
  }}
408
428
  >
@@ -49,4 +49,11 @@ 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
+ * Hides the logo and `logoText` from the `lg` breakpoint up and left-aligns
54
+ * `centerContent`. Use it when the brand already lives somewhere else (e.g.
55
+ * at the top of a full-height sidebar); `ITLayout` sets it for you when
56
+ * `sidebarFullHeight` is on. Below `lg` the logo is still shown. @default false
57
+ */
58
+ hideBrandOnDesktop?: boolean;
52
59
  }
@@ -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,16 @@ export default function ITTopBar({
67
68
  onToggleMobileMenu,
68
69
  navItems,
69
70
  onNavItemClick,
71
+ hideBrandOnDesktop = false,
70
72
  }: ITTopBarProps) {
73
+ // Flat appearance: plain bar (no shadow/blur), initials avatar, name before it.
74
+ const flat = useITFlatAppearance();
75
+ const initials = (userMenu?.userName ?? "")
76
+ .trim()
77
+ .split(/\s+/)
78
+ .slice(0, 2)
79
+ .map((word) => word.charAt(0).toUpperCase())
80
+ .join("");
71
81
  const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
72
82
  const userMenuRef = useRef<HTMLDivElement>(null);
73
83
 
@@ -75,14 +85,14 @@ export default function ITTopBar({
75
85
 
76
86
  return (
77
87
  <header
78
- className="sticky top-0 z-40 backdrop-blur-md transition-all duration-300"
88
+ className={`sticky top-0 z-40 transition-all duration-300 ${flat ? "" : "backdrop-blur-md"}`}
79
89
  style={{
80
90
  backgroundColor: "var(--it-topbar-bg, rgba(255, 255, 255, 0.9))",
81
91
  borderBottom: "1px solid var(--it-topbar-border, rgba(15, 23, 42, 0.06))",
82
- boxShadow: "var(--shadow-xs)",
92
+ boxShadow: flat ? "none" : "var(--shadow-xs)",
83
93
  }}
84
94
  >
85
- <div className="flex items-center justify-between h-[72px] px-6 lg:px-8">
95
+ <div className={`flex items-center justify-between px-6 lg:px-8 ${flat ? "h-[64px]" : "h-[72px]"}`}>
86
96
 
87
97
  {/* LEFT AREA: Logo & Mobile Toggle */}
88
98
  <div className="flex items-center gap-5">
@@ -103,7 +113,7 @@ export default function ITTopBar({
103
113
  )}
104
114
 
105
115
  {/* Logo */}
106
- <div className="flex items-center gap-3">
116
+ <div className={`items-center gap-3 ${hideBrandOnDesktop ? "flex lg:hidden" : "flex"}`}>
107
117
  {logo && (
108
118
  <div className="flex-shrink-0 drop-shadow-sm">
109
119
  {logo}
@@ -153,7 +163,7 @@ export default function ITTopBar({
153
163
  `justify-between` has nothing left to distribute: the logo/nav and
154
164
  the user menu stay flush with the row edges. */}
155
165
  {centerContent && (
156
- <div className="hidden lg:flex flex-1 min-w-0 items-center justify-center px-4">
166
+ <div className={`hidden lg:flex flex-1 min-w-0 items-center ${hideBrandOnDesktop ? "justify-start pr-4" : "justify-center px-4"}`}>
157
167
  <div className="w-full max-w-[420px]">{centerContent}</div>
158
168
  </div>
159
169
  )}
@@ -169,7 +179,7 @@ export default function ITTopBar({
169
179
  <div className="relative">
170
180
  <button
171
181
  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"
182
+ 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
183
  style={{
174
184
  backgroundColor: isUserMenuOpen ? "var(--it-topbar-user-hover, #f1f5f9)" : "transparent",
175
185
  }}
@@ -189,17 +199,30 @@ export default function ITTopBar({
189
199
  src={userMenu.userImage}
190
200
  alt="Current user"
191
201
  />
202
+ ) : flat && initials ? (
203
+ <div
204
+ aria-hidden="true"
205
+ className="w-9 h-9 rounded-full flex items-center justify-center text-[0.8rem] font-semibold"
206
+ style={{
207
+ backgroundColor: "color-mix(in srgb, var(--color-primary, #3b82f6) 14%, transparent)",
208
+ color: "var(--color-primary, #2563eb)",
209
+ }}
210
+ >
211
+ {initials}
212
+ </div>
192
213
  ) : (
193
214
  <div className="w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center ring-2 ring-white shadow-sm">
194
215
  <FaUserCircle className="w-6 h-6" style={{ color: "var(--it-topbar-icon, #94a3b8)" }} />
195
216
  </div>
196
217
  )}
197
218
  {/* 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>
219
+ {!flat && (
220
+ <div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
221
+ )}
199
222
  </div>
200
223
 
201
224
  {/* Name Details */}
202
- <div className="hidden sm:flex flex-col text-left py-0.5">
225
+ <div className={`hidden sm:flex flex-col py-0.5 ${flat ? "text-right" : "text-left"}`}>
203
226
  <ITText as="span"
204
227
  className="font-semibold text-[0.85rem] leading-tight"
205
228
  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
  }
@@ -4,6 +4,7 @@ import ITTopBar from "@/components/organisms/topbar/topbar";
4
4
  import ITSidebar from "@/components/organisms/sidebar/sidebar";
5
5
  import { ITLayoutProps } from "./layout.props";
6
6
  import { theme } from "@/theme/theme";
7
+ import { useITFlatAppearance } from "@/theme/theme-context";
7
8
 
8
9
  /**
9
10
  * Main application shell with sidebar, topbar, and content area.
@@ -27,6 +28,16 @@ import { theme } from "@/theme/theme";
27
28
  * >
28
29
  * <SettingsPage />
29
30
  * </ITLayout>
31
+ *
32
+ * @example
33
+ * // Full-height sidebar with the brand on top; the top bar covers only the content.
34
+ * <ITLayout
35
+ * sidebarFullHeight
36
+ * topBar={{ logo: <Logo />, logoText: "Acme", centerContent: <SearchBox /> }}
37
+ * sidebar={{ navigationItems, isCollapsed: false }}
38
+ * >
39
+ * <Dashboard />
40
+ * </ITLayout>
30
41
  */
31
42
  export default function ITLayout({
32
43
  topBar,
@@ -34,77 +45,113 @@ export default function ITLayout({
34
45
  children,
35
46
  className = "",
36
47
  contentClassName = "",
48
+ sidebarFullHeight = false,
37
49
  }: ITLayoutProps) {
38
50
  const [internalCollapsed, setInternalCollapsed] = useState(true);
39
51
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
52
+ const flat = useITFlatAppearance();
40
53
 
41
54
  const isControlled = sidebar.isCollapsed !== undefined;
42
55
  const desktopCollapsed = isControlled ? sidebar.isCollapsed : internalCollapsed;
43
56
  const handleToggleCollapse = isControlled ? (sidebar.onToggleCollapse ?? (() => {})) : () => setInternalCollapsed(v => !v);
44
57
 
45
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]";
61
+ // With a full-height sidebar the brand moves from the top bar to the sidebar.
62
+ const brand = sidebarFullHeight
63
+ ? (sidebar.brand ?? { logo: topBar.logo, text: topBar.logoText })
64
+ : sidebar.brand;
46
65
 
47
- return (
48
- <div className={`flex flex-col h-screen overflow-hidden w-full ${className}`}>
49
- <ITTopBar
50
- {...topBar}
51
- showMobileMenuButton
52
- onToggleMobileMenu={() => setMobileSidebarOpen(v => !v)}
53
- />
66
+ const topBarNode = (
67
+ <ITTopBar
68
+ {...topBar}
69
+ hideBrandOnDesktop={sidebarFullHeight || topBar.hideBrandOnDesktop}
70
+ showMobileMenuButton
71
+ onToggleMobileMenu={() => setMobileSidebarOpen(v => !v)}
72
+ />
73
+ );
54
74
 
75
+ const desktopSidebar = (
76
+ // Full-height: the sidebar sits beside the top bar, so it needs to win the
77
+ // stacking order when the collapsed rail expands over the content on hover.
78
+ <div className={`hidden lg:block relative h-full ${sidebarFullHeight ? "z-50" : "z-40"}`}>
79
+ {/* Reserves the sidebar's at-rest width. When the sidebar is collapsed
80
+ (default), this is the 88px rail and hover expands over the content.
81
+ When controlled-expanded (`sidebar.isCollapsed === false`), it
82
+ reserves the full width so the sidebar never overlays content. */}
83
+ <div className={`${reservedWidth} h-full flex-shrink-0`} />
84
+ <div className="absolute top-0 left-0 h-full">
85
+ <ITSidebar
86
+ {...sidebar}
87
+ brand={brand}
88
+ isCollapsed={desktopCollapsed}
89
+ onToggleCollapse={handleToggleCollapse}
90
+ visibleOnMobile={false}
91
+ className={`h-full ${flat ? "" : "drop-shadow-2xl"} transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] flex-shrink-0`}
92
+ />
93
+ </div>
94
+ </div>
95
+ );
96
+
97
+ const mobileSidebar = mobileSidebarOpen && (
98
+ <div
99
+ className="lg:hidden fixed inset-0 z-[40] transition-opacity duration-300 backdrop-blur-sm bg-black/40"
100
+ onClick={() => setMobileSidebarOpen(false)}
101
+ >
55
102
  <div
56
- className="flex flex-1 overflow-hidden relative"
103
+ className="h-full w-fit flex transform transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)]"
104
+ onClick={(e) => e.stopPropagation()}
105
+ >
106
+ <ITSidebar
107
+ {...sidebar}
108
+ brand={brand}
109
+ isCollapsed={false}
110
+ visibleOnMobile={true}
111
+ className="h-full shadow-2xl relative z-[60]"
112
+ onToggleCollapse={() => setMobileSidebarOpen(false)}
113
+ onItemClick={() => setMobileSidebarOpen(false)}
114
+ onSubItemClick={() => setMobileSidebarOpen(false)}
115
+ />
116
+ </div>
117
+ </div>
118
+ );
119
+
120
+ const main = (
121
+ <main className="flex-1 overflow-y-auto w-full custom-scrollbar relative z-0">
122
+ <div className={clsx("mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6 h-full", contentClassName)}>
123
+ {children}
124
+ </div>
125
+ </main>
126
+ );
127
+
128
+ if (sidebarFullHeight) {
129
+ return (
130
+ <div
131
+ className={`flex h-screen overflow-hidden w-full relative ${className}`}
57
132
  style={{ backgroundColor: layoutTokens.backgroundColor }}
58
133
  >
59
- {/* DESKTOP SIDEBAR */}
60
- <div className="hidden lg:block relative z-40 h-full">
61
- {/* Reserves the sidebar's at-rest width. When the sidebar is collapsed
62
- (default), this is the 88px rail and hover expands over the content.
63
- When controlled-expanded (`sidebar.isCollapsed === false`), it
64
- reserves the full 280px so the sidebar never overlays content. */}
65
- <div
66
- className={`${desktopCollapsed ? "w-[88px]" : "w-[280px]"} h-full flex-shrink-0`}
67
- />
68
- <div className="absolute top-0 left-0 h-full">
69
- <ITSidebar
70
- {...sidebar}
71
- isCollapsed={desktopCollapsed}
72
- onToggleCollapse={handleToggleCollapse}
73
- visibleOnMobile={false}
74
- className="h-full drop-shadow-2xl transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] flex-shrink-0"
75
- />
76
- </div>
134
+ {desktopSidebar}
135
+ <div className="flex flex-col flex-1 min-w-0 overflow-hidden">
136
+ {topBarNode}
137
+ {main}
77
138
  </div>
139
+ {mobileSidebar}
140
+ </div>
141
+ );
142
+ }
78
143
 
79
- {/* MOBILE SIDEBAR PANE */}
80
- {mobileSidebarOpen && (
81
- <div
82
- className="lg:hidden fixed inset-0 z-[40] transition-opacity duration-300 backdrop-blur-sm bg-black/40"
83
- onClick={() => setMobileSidebarOpen(false)}
84
- >
85
- <div
86
- className="h-full w-fit flex transform transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)]"
87
- onClick={(e) => e.stopPropagation()}
88
- >
89
- <ITSidebar
90
- {...sidebar}
91
- isCollapsed={false}
92
- visibleOnMobile={true}
93
- className="h-full shadow-2xl relative z-[60]"
94
- onToggleCollapse={() => setMobileSidebarOpen(false)}
95
- onItemClick={() => setMobileSidebarOpen(false)}
96
- onSubItemClick={() => setMobileSidebarOpen(false)}
97
- />
98
- </div>
99
- </div>
100
- )}
144
+ return (
145
+ <div className={`flex flex-col h-screen overflow-hidden w-full ${className}`}>
146
+ {topBarNode}
101
147
 
102
- {/* MAIN CONTENT AREA */}
103
- <main className="flex-1 overflow-y-auto w-full custom-scrollbar relative z-0">
104
- <div className={clsx("mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6 h-full", contentClassName)}>
105
- {children}
106
- </div>
107
- </main>
148
+ <div
149
+ className="flex flex-1 overflow-hidden relative"
150
+ style={{ backgroundColor: layoutTokens.backgroundColor }}
151
+ >
152
+ {desktopSidebar}
153
+ {mobileSidebar}
154
+ {main}
108
155
  </div>
109
156
  </div>
110
157
  );
@@ -17,6 +17,7 @@ Root theme context that injects CSS custom properties into the entire component
17
17
  | `density` | `number` | `1` | Global density/compactness factor: values below `1` shrink base sizes (fonts, paddings, gaps), above `1` grow them. Applies by scaling the root font-size so every `rem`-based token compacts together while the layout stays fluid. Clamped to `0.5`–`1.5`. |
18
18
  | `radius` | `number` | — | Global border radius in **px** applied to the whole rounding scale. `2` ⇒ 2px corners (near-square), `8` ⇒ 8px, `0` ⇒ square. No override when omitted (Tailwind defaults). |
19
19
  | `shadow` | `number` | `2` | Global shadow strength level: `0` ⇒ none, `1` ⇒ subtle, `2` ⇒ default current look, `3` ⇒ pronounced. No override when omitted (keeps defaults). |
20
+ | `appearance` | `"soft" \| "flat"` | `"soft"` | Visual language. `"flat"` switches `ITCard`, `ITSidebar`, `ITTopBar` and `ITPageHeader` to flat surfaces (hairline borders, no shadows, no header icon tile) and sets `data-it-appearance` on `<html>`. Read it in your own components with `useITFlatAppearance()`. |
20
21
 
21
22
  ### ITThemePalette Shape
22
23
 
@@ -1,3 +1,7 @@
1
+ import type { ITThemeAppearance } from "@/theme/theme.types";
2
+
3
+ export type { ITThemeAppearance };
4
+
1
5
  export interface ITThemePalette {
2
6
  /** Main brand accent color (hex). Drives primary buttons, active nav items, focus rings, links, and the `--color-primary` CSS variable. @default "#06b6d4" */
3
7
  primary: string;
@@ -54,4 +58,6 @@ export interface ITThemeProviderProps {
54
58
  radius?: number;
55
59
  /** Global shadow strength level. `0` ⇒ no shadows, `1` ⇒ subtle, `2` ⇒ default current look, `3` ⇒ pronounced. Shadows are injected as `--shadow-*`/`.shadow-*` overrides so cards, dropdowns and dialogs follow the level. @default 2 (keeps current default when omitted) */
56
60
  shadow?: number;
61
+ /** Visual language of the whole system. `"soft"` keeps the current look (floating cards, tinted shadows, glass sidebar/top bar). `"flat"` switches the app shell and surfaces to flat ones: hairline borders instead of shadows on `ITCard`, a plain `ITSidebar` (no glow, no accent bar, narrower), a plain `ITTopBar` (no shadow, initials avatar) and a compact `ITPageHeader` without the icon tile. It also sets `data-it-appearance` on `<html>` so app CSS can follow it. Combine it with `shadow` and `radius` to finish the look. @default "soft" */
62
+ appearance?: ITThemeAppearance;
57
63
  }
@@ -73,7 +73,7 @@ import { DEFAULT_PALETTE, PRESETS } from "./themePresets";
73
73
  // Contexto y hooks (useITTheme / useITThemeSafe) viven en @/theme/theme-context.
74
74
  // Re-exportados desde aquí para mantener compatibilidad de la API pública.
75
75
  // eslint-disable-next-line react-refresh/only-export-components
76
- export { useITTheme, useITThemeSafe } from "@/theme/theme-context";
76
+ export { useITTheme, useITThemeSafe, useITFlatAppearance } from "@/theme/theme-context";
77
77
 
78
78
  const getNestedValue = (obj: any, path: string) => {
79
79
  return path.split(".").reduce((acc, part) => acc && acc[part], obj);
@@ -143,6 +143,7 @@ export default function ITThemeProvider({
143
143
  density = 1,
144
144
  radius,
145
145
  shadow,
146
+ appearance = "soft",
146
147
  }: ITThemeProviderProps) {
147
148
  const [palette, setPaletteState] = useState<ITThemePalette>(() => {
148
149
  const basePalette = {
@@ -276,6 +277,14 @@ export default function ITThemeProvider({
276
277
  };
277
278
  }, [density]);
278
279
 
280
+ // Lenguaje visual: se publica en <html> para que el CSS de la app pueda
281
+ // seguirlo; los componentes lo leen del contexto (useITFlatAppearance).
282
+ useLayoutEffect(() => {
283
+ const root = document.documentElement;
284
+ root.setAttribute("data-it-appearance", appearance);
285
+ return () => root.removeAttribute("data-it-appearance");
286
+ }, [appearance]);
287
+
279
288
  // Overrides globales de radius y sombras.
280
289
  // Radius: en Tailwind v4 cada `rounded-*` resuelve a var(--radius-*), así que
281
290
  // setear los vars en :root (en px) cuadra/redondea todo el sistema sin tocar
@@ -1611,6 +1620,7 @@ export default function ITThemeProvider({
1611
1620
  resolvedTheme,
1612
1621
  darkModeMode,
1613
1622
  setDarkModeMode,
1623
+ appearance,
1614
1624
  }}
1615
1625
  >
1616
1626
  {children}