@axzydev/axzy_ui_system 1.4.1 → 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/README.md +2 -2
- package/dist/index.cjs +422 -259
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +169 -70
- package/dist/index.d.ts +169 -70
- package/dist/index.js +324 -162
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +3 -0
- 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 +48 -2
- package/src/components/organisms/sidebar/sidebar.props.ts +58 -3
- package/src/components/organisms/sidebar/sidebar.stories.tsx +59 -1
- package/src/components/organisms/sidebar/sidebar.tsx +141 -25
- 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 -5
- package/src/sandbox/LandingShowcase.tsx +1 -1
- package/src/sandbox/navigation.ts +1 -0
- package/src/showcases/HelpShowcase.tsx +497 -0
- package/src/showcases/StructureShowcases.tsx +58 -5
- package/src/theme/theme-context.ts +11 -1
- package/src/theme/theme.types.ts +7 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import "./index.css";
|
|
|
14
14
|
// Import Showcases
|
|
15
15
|
import { HomeShowcase } from "./showcases/HomeShowcase";
|
|
16
16
|
import { GettingStartedShowcase } from "./showcases/GettingStartedShowcase";
|
|
17
|
+
import { HelpShowcase } from "./showcases/HelpShowcase";
|
|
17
18
|
import { SizesShowcase } from "./showcases/SizesShowcase";
|
|
18
19
|
import {
|
|
19
20
|
CardShowcase,
|
|
@@ -233,6 +234,8 @@ function App() {
|
|
|
233
234
|
switch (itemId) {
|
|
234
235
|
case "getting-started":
|
|
235
236
|
return <GettingStartedShowcase />;
|
|
237
|
+
case "help":
|
|
238
|
+
return <HelpShowcase />;
|
|
236
239
|
// Structure
|
|
237
240
|
case "layout":
|
|
238
241
|
return <LayoutShowcase />;
|
|
@@ -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>
|
|
@@ -20,6 +20,8 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
|
|
|
20
20
|
| `subitemConnector` | `"dot" \| "\|" \| "none"` | `"dot"` | Visual connector style for sub-items. |
|
|
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
|
+
| `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. |
|
|
23
25
|
|
|
24
26
|
### ITNavigationItem
|
|
25
27
|
|
|
@@ -31,7 +33,18 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
|
|
|
31
33
|
| `action` | `() => void` | Click handler for top-level items without submenus. |
|
|
32
34
|
| `isActive` | `boolean` | Whether this item is active/highlighted. |
|
|
33
35
|
| `subitems` | `ITNavigationSubItemEntry[]` | Array of entries: plain sub-items or titled groups (`ITNavigationSubItemGroup`). |
|
|
34
|
-
| `badge` | `string` |
|
|
36
|
+
| `badge` | `number \| string` | Notification content rendered with `ITBadget` (e.g. an unread count). |
|
|
37
|
+
| `badgeProps` | `ITBadgetProps` | Styling forwarded to the item badge (`color`, `size`, `variant`, `className` for padding). |
|
|
38
|
+
|
|
39
|
+
### ITSidebarNotification
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | Description |
|
|
42
|
+
|------|------|---------|-------------|
|
|
43
|
+
| `count` | `number` | — | Count displayed inside the notification `ITBadget`. |
|
|
44
|
+
| `icon` | `ReactNode` | — | Icon rendered left of the label (e.g. a bell). Omit for a count-only pill. |
|
|
45
|
+
| `label` | `string` | `"Notificaciones"` | Accessible label and tooltip for the notification button. |
|
|
46
|
+
| `onClick` | `() => void` | — | Click handler for the notification button. |
|
|
47
|
+
| `badgeProps` | `ITBadgetProps` | `{ color: "danger", size: "sm" }` | Styling forwarded to the internal `ITBadget`; use `className` for padding. |
|
|
35
48
|
|
|
36
49
|
### ITNavigationSubItem
|
|
37
50
|
|
|
@@ -105,13 +118,46 @@ const items = [
|
|
|
105
118
|
|
|
106
119
|
Discriminate entries with `"items" in entry`. The heading style (uppercase, `0.68rem`, weight 700, `0.08em` tracking, `var(--it-sidebar-icon-color, var(--color-secondary-400))`) is applied by the component and cannot be overridden per group.
|
|
107
120
|
|
|
121
|
+
## Notifications and badges
|
|
122
|
+
|
|
123
|
+
Item badges and the notification row both render the `ITBadget` atom. Use `badge` for a per-item counter and `notification` for a top-level entry (e.g. a bell):
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { ITSidebar } from '@axzydev/axzy_ui_system';
|
|
127
|
+
import { FaBell } from 'react-icons/fa';
|
|
128
|
+
|
|
129
|
+
const items = [
|
|
130
|
+
{ id: 'dashboard', label: 'Dashboard', isActive: true, action: () => {} },
|
|
131
|
+
{ id: 'inbox', label: 'Inbox', badge: 12, action: () => {} },
|
|
132
|
+
{
|
|
133
|
+
id: 'alerts',
|
|
134
|
+
label: 'Alerts',
|
|
135
|
+
badge: 3,
|
|
136
|
+
badgeProps: { color: 'warning', variant: 'outlined' },
|
|
137
|
+
action: () => {},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
<ITSidebar
|
|
142
|
+
navigationItems={items}
|
|
143
|
+
notification={{
|
|
144
|
+
count: 5,
|
|
145
|
+
icon: <FaBell />,
|
|
146
|
+
label: 'Notifications',
|
|
147
|
+
onClick: () => {},
|
|
148
|
+
// Tweak the ITBadget, incl. padding via className.
|
|
149
|
+
badgeProps: { color: 'danger', className: 'px-2' },
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
108
154
|
## Notes
|
|
109
155
|
|
|
110
156
|
- There is no collapse toggle button: the collapsed 88px rail expands and collapses automatically on pointer hover (300 ms leave delay).
|
|
111
157
|
- Collapsed mode shrinks to icon-only; a glassmorphism panel shows the item's sub-items on hover. For an item whose `subitems` contain groups, that panel **flattens** every group into one plain list and renders **no** group headings.
|
|
112
158
|
- Submenus expand/collapse with smooth transitions when a parent item is clicked; the expanded submenu is animated with a `max-h` transition (cap `1000px`).
|
|
113
159
|
- Active item highlighting uses a colored left-border accent and subtle background tint.
|
|
114
|
-
- Badge support renders a notification-style counter on the item.
|
|
160
|
+
- Badge support renders a notification-style counter on the item using the `ITBadget` atom; customize it with `badgeProps` (`color`, `size`, `variant`, `className`). The sidebar also accepts a `notification` entry (icon + label + `ITBadget` counter) pinned above the navigation, which stays visible while collapsed as icon + counter.
|
|
115
161
|
- `subitemConnector` styles the visual link between parent and sub-items: `dot` (default), `|` (vertical bar), or `none`. Grouping does not change it: the `|` guide is a single `border-left` on the outer submenu `<ul>` only.
|
|
116
162
|
- An active sub-item inside a group still auto-expands its parent, because auto-expand flattens groups the same way the collapsed flyout does.
|
|
117
163
|
- `visibleOnMobile` forces the sidebar visible at every breakpoint and disables the collapsed rail, so it stays fully expanded.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ITBadgetProps } from "@/components/atoms/badget/badget.props";
|
|
2
|
+
|
|
1
3
|
/** A sub-navigation item nested under a parent menu. */
|
|
2
4
|
export interface ITNavigationSubItem {
|
|
3
5
|
/** Unique identifier for the sub-item. */
|
|
@@ -50,8 +52,49 @@ export interface ITNavigationItem {
|
|
|
50
52
|
* The field `items` is reserved for {@link ITNavigationSubItemGroup}.
|
|
51
53
|
*/
|
|
52
54
|
subitems?: ITNavigationSubItemEntry[];
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Notification badge content rendered with the {@link ITBadget} atom
|
|
57
|
+
* (e.g. an unread count). Omit it to render no badge.
|
|
58
|
+
*/
|
|
59
|
+
badge?: number | string;
|
|
60
|
+
/**
|
|
61
|
+
* Styling forwarded to the `ITBadget` rendered for {@link ITNavigationItem.badge}.
|
|
62
|
+
* `color`, `size`, `variant` and `className` are supported; use `className`
|
|
63
|
+
* to tweak the badge padding/positioning.
|
|
64
|
+
*/
|
|
65
|
+
badgeProps?: ITBadgetProps;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Notification row pinned above the navigation in {@link ITSidebarProps}. It
|
|
70
|
+
* renders an optional icon, a label and an `ITBadget` counter — e.g. a bell with
|
|
71
|
+
* a number. When the rail is collapsed only the icon + counter are shown.
|
|
72
|
+
*/
|
|
73
|
+
export interface ITSidebarNotification {
|
|
74
|
+
/** Count displayed inside the notification `ITBadget`. */
|
|
75
|
+
count: number;
|
|
76
|
+
/** Icon rendered left of the label (e.g. a bell). Omit for a count-only pill. */
|
|
77
|
+
icon?: React.ReactNode;
|
|
78
|
+
/** Accessible label and tooltip for the notification button. @default "Notificaciones" */
|
|
79
|
+
label?: string;
|
|
80
|
+
/** Click handler for the notification button. */
|
|
81
|
+
onClick?: () => void;
|
|
82
|
+
/**
|
|
83
|
+
* Styling forwarded to the internal `ITBadget`. Defaults to `color: "danger"`
|
|
84
|
+
* and `size: "sm"`; override `color`/`variant`/`size` or use `className` for padding.
|
|
85
|
+
*/
|
|
86
|
+
badgeProps?: ITBadgetProps;
|
|
87
|
+
}
|
|
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;
|
|
55
98
|
}
|
|
56
99
|
|
|
57
100
|
/** Props for the ITSidebar vertical navigation component. */
|
|
@@ -77,5 +120,17 @@ export interface ITSidebarProps {
|
|
|
77
120
|
* Hidden while the sidebar is collapsed, so it only shows on hover.
|
|
78
121
|
*/
|
|
79
122
|
header?: React.ReactNode;
|
|
123
|
+
/**
|
|
124
|
+
* Optional notification entry pinned above the navigation (icon + label +
|
|
125
|
+
* `ITBadget` counter). Unlike {@link ITSidebarProps.header} it stays visible
|
|
126
|
+
* while collapsed, showing just the icon and the counter.
|
|
127
|
+
*/
|
|
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;
|
|
80
136
|
}
|
|
81
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import ITSidebar from '@/components/organisms/sidebar/sidebar';
|
|
3
|
-
import { FaHome, FaUsers, FaCog, FaChartBar, FaShieldAlt, FaShoppingCart } from 'react-icons/fa';
|
|
3
|
+
import { FaHome, FaUsers, FaCog, FaChartBar, FaShieldAlt, FaShoppingCart, FaRegBell } from 'react-icons/fa';
|
|
4
4
|
import { expect, fn, userEvent, waitFor } from 'storybook/test';
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof ITSidebar> = {
|
|
@@ -174,3 +174,61 @@ export const Grouped: Story = {
|
|
|
174
174
|
await expect(groupedSubItemActionSpy).toHaveBeenCalled();
|
|
175
175
|
},
|
|
176
176
|
};
|
|
177
|
+
|
|
178
|
+
export const WithNotificationBadges: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
navigationItems: [
|
|
181
|
+
{ id: 'dashboard', label: 'Dashboard', icon: <FaHome />, isActive: true },
|
|
182
|
+
{ id: 'inbox', label: 'Bandeja', icon: <FaUsers />, badge: 12 },
|
|
183
|
+
{
|
|
184
|
+
id: 'alerts',
|
|
185
|
+
label: 'Alertas',
|
|
186
|
+
icon: <FaShieldAlt />,
|
|
187
|
+
badge: 3,
|
|
188
|
+
badgeProps: { color: 'warning', variant: 'outlined' },
|
|
189
|
+
},
|
|
190
|
+
{ id: 'settings', label: 'Configuración', icon: <FaCog />, action: fn() },
|
|
191
|
+
],
|
|
192
|
+
notification: {
|
|
193
|
+
count: 5,
|
|
194
|
+
icon: <FaRegBell />,
|
|
195
|
+
label: 'Notificaciones',
|
|
196
|
+
onClick: fn(),
|
|
197
|
+
},
|
|
198
|
+
isCollapsed: false,
|
|
199
|
+
visibleOnMobile: true,
|
|
200
|
+
},
|
|
201
|
+
render: (args) => (
|
|
202
|
+
<div className="h-screen bg-gray-50 flex">
|
|
203
|
+
<ITSidebar {...args} />
|
|
204
|
+
<div className="flex-1 p-8 text-zinc-500 font-medium">
|
|
205
|
+
Cada ítem puede llevar un ITBadget con número (`badge` + `badgeProps`) y el sidebar admite una fila de notificación con contador (`notification`).
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const CollapsedNotifications: Story = {
|
|
212
|
+
args: {
|
|
213
|
+
navigationItems: [
|
|
214
|
+
{ id: 'dashboard', label: 'Dashboard', icon: <FaHome />, isActive: true },
|
|
215
|
+
{ id: 'inbox', label: 'Bandeja', icon: <FaUsers />, badge: 12 },
|
|
216
|
+
{ id: 'settings', label: 'Configuración', icon: <FaCog /> },
|
|
217
|
+
],
|
|
218
|
+
notification: {
|
|
219
|
+
count: 9,
|
|
220
|
+
icon: <FaRegBell />,
|
|
221
|
+
label: 'Notificaciones',
|
|
222
|
+
},
|
|
223
|
+
isCollapsed: true,
|
|
224
|
+
visibleOnMobile: true,
|
|
225
|
+
},
|
|
226
|
+
render: (args) => (
|
|
227
|
+
<div className="h-screen bg-gray-50 flex">
|
|
228
|
+
<ITSidebar {...args} />
|
|
229
|
+
<div className="flex-1 p-8 text-zinc-500 font-medium">
|
|
230
|
+
Al colapsar, la fila de notificación queda como icono + contador, y los badges de cada ítem siguen visibles como píldoras con número.
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
),
|
|
234
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
2
3
|
import { FaChevronDown } from "react-icons/fa";
|
|
3
4
|
import {
|
|
4
5
|
ITNavigationItem,
|
|
@@ -7,7 +8,9 @@ import {
|
|
|
7
8
|
ITNavigationSubItemGroup,
|
|
8
9
|
ITSidebarProps,
|
|
9
10
|
} from "./sidebar.props";
|
|
11
|
+
import ITBadget from "@/components/atoms/badget/badget";
|
|
10
12
|
import ITText from "@/components/atoms/text/text";
|
|
13
|
+
import { useITFlatAppearance } from "@/theme/theme-context";
|
|
11
14
|
|
|
12
15
|
/** Narrows a submenu entry to a titled group. */
|
|
13
16
|
function isSubItemGroup(
|
|
@@ -42,7 +45,14 @@ function flattenSubItems(
|
|
|
42
45
|
* { id: "billing", label: "Billing", isActive: true },
|
|
43
46
|
* ],
|
|
44
47
|
* },
|
|
48
|
+
* { id: "inbox", label: "Inbox", icon: <FaEnvelope />, badge: 12 },
|
|
45
49
|
* ]}
|
|
50
|
+
* notification={{
|
|
51
|
+
* count: 5,
|
|
52
|
+
* icon: <FaBell />,
|
|
53
|
+
* label: "Notificaciones",
|
|
54
|
+
* onClick: () => navigate("/notifications"),
|
|
55
|
+
* }}
|
|
46
56
|
* isCollapsed
|
|
47
57
|
* />
|
|
48
58
|
* ```
|
|
@@ -56,7 +66,11 @@ export default function ITSidebar({
|
|
|
56
66
|
onSubItemClick,
|
|
57
67
|
subitemConnector = 'dot',
|
|
58
68
|
header,
|
|
69
|
+
notification,
|
|
70
|
+
brand,
|
|
59
71
|
}: ITSidebarProps) {
|
|
72
|
+
// Flat appearance: plain surface, no glow/accent bar, narrower panel.
|
|
73
|
+
const flat = useITFlatAppearance();
|
|
60
74
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
|
61
75
|
const [isHovering, setIsHovering] = useState(false);
|
|
62
76
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
@@ -135,7 +149,7 @@ export default function ITSidebar({
|
|
|
135
149
|
};
|
|
136
150
|
|
|
137
151
|
const isSidebarCollapsed = visibleOnMobile ? false : (!isHovering && isCollapsed);
|
|
138
|
-
const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : "w-[280px]";
|
|
152
|
+
const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : flat ? "w-[248px]" : "w-[280px]";
|
|
139
153
|
|
|
140
154
|
const renderSubItem = (subitem: ITNavigationSubItem) => (
|
|
141
155
|
<li key={subitem.id} className="relative">
|
|
@@ -217,6 +231,88 @@ export default function ITSidebar({
|
|
|
217
231
|
);
|
|
218
232
|
};
|
|
219
233
|
|
|
234
|
+
const renderNotification = () => {
|
|
235
|
+
if (!notification) return null;
|
|
236
|
+
|
|
237
|
+
const {
|
|
238
|
+
count,
|
|
239
|
+
icon,
|
|
240
|
+
label = "Notificaciones",
|
|
241
|
+
onClick,
|
|
242
|
+
badgeProps,
|
|
243
|
+
} = notification;
|
|
244
|
+
const { className: badgeClassName, ...badgeRest } = badgeProps ?? {};
|
|
245
|
+
|
|
246
|
+
const badge = (
|
|
247
|
+
<ITBadget
|
|
248
|
+
color="danger"
|
|
249
|
+
size="sm"
|
|
250
|
+
{...badgeRest}
|
|
251
|
+
className={clsx("shadow-md", badgeClassName)}
|
|
252
|
+
>
|
|
253
|
+
{String(count)}
|
|
254
|
+
</ITBadget>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (isSidebarCollapsed) {
|
|
258
|
+
return (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
aria-label={label}
|
|
262
|
+
title={label}
|
|
263
|
+
onClick={onClick}
|
|
264
|
+
className="relative flex items-center justify-center w-full p-2 mb-1 rounded-xl transition-all duration-300"
|
|
265
|
+
style={{ color: "var(--it-sidebar-icon-color, var(--color-secondary-500))" }}
|
|
266
|
+
onMouseEnter={(e) => {
|
|
267
|
+
e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
268
|
+
}}
|
|
269
|
+
onMouseLeave={(e) => {
|
|
270
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
{icon && (
|
|
274
|
+
<span className="flex items-center justify-center text-[1.05rem]">{icon}</span>
|
|
275
|
+
)}
|
|
276
|
+
<span className="absolute top-0.5 right-0.5 pointer-events-none">{badge}</span>
|
|
277
|
+
</button>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
aria-label={label}
|
|
285
|
+
title={label}
|
|
286
|
+
onClick={onClick}
|
|
287
|
+
className="flex items-center gap-2.5 w-full text-left px-3 py-2 mb-1 rounded-xl transition-all duration-300"
|
|
288
|
+
style={{ color: "var(--it-sidebar-label-color, var(--color-secondary-600))" }}
|
|
289
|
+
onMouseEnter={(e) => {
|
|
290
|
+
e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
291
|
+
}}
|
|
292
|
+
onMouseLeave={(e) => {
|
|
293
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{icon && (
|
|
297
|
+
<span
|
|
298
|
+
className="flex-shrink-0 flex items-center justify-center text-[1.05rem]"
|
|
299
|
+
style={{ color: "var(--it-sidebar-icon-color, #9ca3af)" }}
|
|
300
|
+
>
|
|
301
|
+
{icon}
|
|
302
|
+
</span>
|
|
303
|
+
)}
|
|
304
|
+
<ITText
|
|
305
|
+
as="span"
|
|
306
|
+
className="truncate tracking-wide"
|
|
307
|
+
style={{ fontSize: "0.8rem", fontWeight: 500 }}
|
|
308
|
+
>
|
|
309
|
+
{label}
|
|
310
|
+
</ITText>
|
|
311
|
+
<span className="ml-auto flex-shrink-0">{badge}</span>
|
|
312
|
+
</button>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
220
316
|
return (
|
|
221
317
|
<aside
|
|
222
318
|
ref={sidebarRef}
|
|
@@ -226,26 +322,47 @@ export default function ITSidebar({
|
|
|
226
322
|
${sidebarWidth}
|
|
227
323
|
${className}
|
|
228
324
|
${!visibleOnMobile ? "hidden lg:flex" : "flex"}
|
|
229
|
-
shadow-[4px_0_32px_rgba(15,23,42,0.04)]
|
|
325
|
+
${flat ? "" : "shadow-[4px_0_32px_rgba(15,23,42,0.04)]"}
|
|
230
326
|
`}
|
|
231
327
|
style={{
|
|
232
328
|
zIndex: 50,
|
|
233
329
|
backgroundColor: "var(--it-sidebar-bg, rgba(255, 255, 255, 0.90))",
|
|
234
330
|
borderRight: "1px solid var(--it-sidebar-border, var(--color-secondary-200))",
|
|
235
|
-
WebkitBackdropFilter: 'blur(12px)',
|
|
236
|
-
backdropFilter: 'blur(12px)',
|
|
331
|
+
...(flat ? {} : { WebkitBackdropFilter: 'blur(12px)', backdropFilter: 'blur(12px)' }),
|
|
237
332
|
}}
|
|
238
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
|
+
|
|
239
351
|
{header && !isSidebarCollapsed && (
|
|
240
352
|
<div className="px-4 pt-4 pb-1 flex-shrink-0">{header}</div>
|
|
241
353
|
)}
|
|
242
354
|
|
|
355
|
+
{notification && (
|
|
356
|
+
<div className="px-4 pt-3 flex-shrink-0">{renderNotification()}</div>
|
|
357
|
+
)}
|
|
358
|
+
|
|
243
359
|
{/* Navigation Items */}
|
|
244
360
|
<nav aria-label="Componentes de la librería" className="flex-1 py-4 overflow-y-auto overflow-x-hidden custom-scrollbar px-4">
|
|
245
361
|
<ul className="space-y-1">
|
|
246
362
|
{navigationItems.map((item) => {
|
|
247
363
|
const hasSubmenu = !!item.subitems && item.subitems.length > 0;
|
|
248
364
|
const submenuId = `it-sidebar-submenu-${item.id}`;
|
|
365
|
+
const { className: badgeClassName, ...badgeRest } = item.badgeProps ?? {};
|
|
249
366
|
return (
|
|
250
367
|
<li key={item.id} className="relative group/navitem">
|
|
251
368
|
<div
|
|
@@ -261,8 +378,8 @@ export default function ITSidebar({
|
|
|
261
378
|
`}
|
|
262
379
|
style={{
|
|
263
380
|
backgroundColor: item.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
|
|
264
|
-
boxShadow: item.isActive ? 'var(--shadow-xs)' : 'none',
|
|
265
|
-
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'
|
|
266
383
|
}}
|
|
267
384
|
onMouseEnter={(e) => {
|
|
268
385
|
if (!item.isActive) e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
|
|
@@ -278,7 +395,7 @@ export default function ITSidebar({
|
|
|
278
395
|
}
|
|
279
396
|
}}
|
|
280
397
|
>
|
|
281
|
-
{item.isActive && !isSidebarCollapsed && (
|
|
398
|
+
{item.isActive && !isSidebarCollapsed && !flat && (
|
|
282
399
|
<div
|
|
283
400
|
className="absolute left-0 top-1/4 bottom-1/4 w-[3px] rounded-r-full transition-all"
|
|
284
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))" }}
|
|
@@ -292,8 +409,8 @@ export default function ITSidebar({
|
|
|
292
409
|
style={{
|
|
293
410
|
color: item.isActive ? "var(--it-sidebar-active-icon, var(--color-primary-500))" : "var(--it-sidebar-icon-color, #9ca3af)",
|
|
294
411
|
opacity: item.isActive ? 1 : 0.8,
|
|
295
|
-
fontSize: item.isActive ? '1.12rem' : '1.05rem',
|
|
296
|
-
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'
|
|
297
414
|
}}
|
|
298
415
|
>
|
|
299
416
|
{item.icon}
|
|
@@ -305,7 +422,7 @@ export default function ITSidebar({
|
|
|
305
422
|
className={`transition-all duration-300 truncate tracking-wide`}
|
|
306
423
|
style={{
|
|
307
424
|
color: item.isActive ? "var(--it-sidebar-active-color, #ffffff)" : "var(--it-sidebar-label-color, var(--color-secondary-300))",
|
|
308
|
-
fontSize: '0.8rem',
|
|
425
|
+
fontSize: flat ? '0.875rem' : '0.8rem',
|
|
309
426
|
fontWeight: item.isActive ? '600' : '500'
|
|
310
427
|
}}
|
|
311
428
|
>
|
|
@@ -321,22 +438,21 @@ export default function ITSidebar({
|
|
|
321
438
|
</div>
|
|
322
439
|
)}
|
|
323
440
|
|
|
324
|
-
{item.badge && (
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}}
|
|
441
|
+
{item.badge != null && item.badge !== "" && (
|
|
442
|
+
<ITBadget
|
|
443
|
+
color="primary"
|
|
444
|
+
size="sm"
|
|
445
|
+
{...badgeRest}
|
|
446
|
+
className={clsx(
|
|
447
|
+
"absolute shadow-md",
|
|
448
|
+
isSidebarCollapsed
|
|
449
|
+
? "top-0.5 right-0.5"
|
|
450
|
+
: "right-3 top-1/2 -translate-y-1/2",
|
|
451
|
+
badgeClassName,
|
|
452
|
+
)}
|
|
337
453
|
>
|
|
338
|
-
{
|
|
339
|
-
</
|
|
454
|
+
{String(item.badge)}
|
|
455
|
+
</ITBadget>
|
|
340
456
|
)}
|
|
341
457
|
</div>
|
|
342
458
|
|
|
@@ -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
|
}
|