@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/dist/index.cjs +322 -211
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +143 -69
- package/dist/index.d.ts +143 -69
- package/dist/index.js +224 -114
- 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 +2 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +30 -2
- package/src/components/organisms/sidebar/sidebar.tsx +77 -26
- package/src/components/organisms/topbar/topbar.props.ts +13 -0
- package/src/components/organisms/topbar/topbar.tsx +35 -9
- package/src/components/templates/layout/layout.props.ts +8 -0
- package/src/components/templates/layout/layout.tsx +111 -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,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
|
-
/**
|
|
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
|
-
|
|
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=
|
|
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:
|
|
361
|
-
boxShadow:
|
|
362
|
-
border:
|
|
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 (!
|
|
402
|
+
if (!active) e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
366
403
|
}}
|
|
367
404
|
onMouseLeave={(e) => {
|
|
368
|
-
if (!
|
|
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
|
-
{
|
|
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:
|
|
391
|
-
opacity:
|
|
392
|
-
fontSize:
|
|
393
|
-
filter:
|
|
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:
|
|
405
|
-
fontSize: '0.8rem',
|
|
406
|
-
fontWeight:
|
|
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:
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
}
|