@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/dist/index.cjs +273 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +125 -68
- package/dist/index.d.ts +125 -68
- package/dist/index.js +175 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/molecules/card/card.tsx +22 -9
- package/src/components/organisms/page-header/page-header.tsx +14 -3
- package/src/components/organisms/sidebar/sidebar.doc.mdx +1 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +18 -1
- package/src/components/organisms/sidebar/sidebar.tsx +30 -10
- package/src/components/organisms/topbar/topbar.props.ts +7 -0
- package/src/components/organisms/topbar/topbar.tsx +31 -8
- package/src/components/templates/layout/layout.props.ts +8 -0
- package/src/components/templates/layout/layout.tsx +101 -54
- package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
- package/src/components/theme-provider/themeProvider.props.ts +6 -0
- package/src/components/theme-provider/themeProvider.tsx +11 -1
- package/src/index.ts +6 -6
- package/src/theme/theme-context.ts +11 -1
- package/src/theme/theme.types.ts +7 -0
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
{
|
|
60
|
-
<div className="
|
|
61
|
-
{
|
|
62
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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}
|