@zauru-sdk/components 6.15.1 → 6.16.1
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/CHANGELOG.md +22 -0
- package/dist/esm/NavBar/NavBar.js +198 -42
- package/package.json +2 -2
- package/src/NavBar/NavBar.tsx +626 -238
- package/stories/NavBar.stories.tsx +92 -0
package/src/NavBar/NavBar.tsx
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useId,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type Dispatch,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type SetStateAction,
|
|
10
|
+
} from "react";
|
|
2
11
|
import {
|
|
3
12
|
DropDownArrowSvgIcon,
|
|
13
|
+
ExitSvg,
|
|
4
14
|
LogoutDropDownSvgIcon,
|
|
5
15
|
MenuAlt4Svg,
|
|
6
16
|
OpcionButtonSvgIcon,
|
|
@@ -8,73 +18,69 @@ import {
|
|
|
8
18
|
import { COLORS } from "./NavBar.utils.js";
|
|
9
19
|
import type {
|
|
10
20
|
ColorInterface,
|
|
11
|
-
EntityProps,
|
|
12
21
|
NavBarItem,
|
|
13
22
|
NavBarProps,
|
|
14
23
|
} from "./NavBar.types.js";
|
|
15
|
-
import { Link,
|
|
24
|
+
import { Link, useLocation, useNavigate } from "@remix-run/react";
|
|
16
25
|
import { useAppSelector } from "@zauru-sdk/redux";
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
color,
|
|
20
|
-
options,
|
|
21
|
-
name,
|
|
22
|
-
ariaLabel,
|
|
23
|
-
}: EntityProps & { ariaLabel?: string }) => {
|
|
24
|
-
const [showOptionsMenu, setShowOptionsMenu] = useState(true);
|
|
27
|
+
type NavLayout = "desktop" | "mobile";
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ariaLabel ?? (typeof name === "string" ? name : "Menú")
|
|
34
|
-
}
|
|
35
|
-
aria-haspopup="menu"
|
|
36
|
-
aria-expanded={!showOptionsMenu}
|
|
37
|
-
onClick={() => setShowOptionsMenu(!showOptionsMenu)}
|
|
38
|
-
className={`relative flex cursor-pointer items-center p-2 text-xs text-white ${color.bg700} active:${color.bg900} border border-transparent rounded-full uppercase focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2`}
|
|
39
|
-
>
|
|
40
|
-
{name ?? <OpcionButtonSvgIcon />}
|
|
41
|
-
<DropDownArrowSvgIcon />
|
|
42
|
-
</button>
|
|
43
|
-
<div
|
|
44
|
-
className="absolute right-0 z-20 w-56 py-2 mt-2 overflow-hidden bg-white rounded-md shadow-xl dark:bg-gray-800"
|
|
45
|
-
hidden={showOptionsMenu}
|
|
46
|
-
onMouseLeave={() => setShowOptionsMenu(true)}
|
|
47
|
-
>
|
|
48
|
-
{options.map((option, index) => (
|
|
49
|
-
<React.Fragment key={index}>{option}</React.Fragment>
|
|
50
|
-
))}
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
29
|
+
type UserMenuEntry = {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
to?: string;
|
|
33
|
+
onClick?: () => void;
|
|
34
|
+
icon?: ReactNode;
|
|
35
|
+
danger?: boolean;
|
|
56
36
|
};
|
|
57
37
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
selectedColor
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
38
|
+
const itemColor = (
|
|
39
|
+
item: Pick<NavBarItem, "selectedColor">,
|
|
40
|
+
fallback: ColorInterface
|
|
41
|
+
): ColorInterface =>
|
|
42
|
+
item.selectedColor ? COLORS[item.selectedColor] : fallback;
|
|
43
|
+
|
|
44
|
+
const visibleChildrenOf = (item: NavBarItem): NavBarItem[] =>
|
|
45
|
+
(item.childrens ?? []).filter((child) => child.hide !== true);
|
|
46
|
+
|
|
47
|
+
const pathMatches = (pathname: string, link?: string) => {
|
|
48
|
+
if (!link) return false;
|
|
49
|
+
return pathname === link || pathname.startsWith(`${link}/`);
|
|
50
|
+
};
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
: COLORS["slate"];
|
|
52
|
+
const focusRing =
|
|
53
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-transparent";
|
|
72
54
|
|
|
73
|
-
|
|
55
|
+
const desktopTriggerClass = (color: ColorInterface, isActive: boolean) =>
|
|
56
|
+
`inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-4 text-xs font-medium uppercase leading-none text-white transition-opacity duration-150 motion-reduce:transition-none ${
|
|
57
|
+
isActive ? color.bg900 : color.bg700
|
|
58
|
+
} hover:opacity-90 ${focusRing}`;
|
|
59
|
+
|
|
60
|
+
const mobileRowClass = (isActive: boolean) =>
|
|
61
|
+
`flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-lg px-3 text-left text-sm font-medium text-white transition-colors duration-150 motion-reduce:transition-none hover:bg-white/10 ${focusRing} ${
|
|
62
|
+
isActive ? "bg-white/15" : ""
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
const dropdownItemClass = (danger?: boolean) =>
|
|
66
|
+
`flex min-h-11 w-full cursor-pointer items-center gap-2 px-4 text-sm transition-colors duration-150 motion-reduce:transition-none ${
|
|
67
|
+
danger
|
|
68
|
+
? "text-red-700 hover:bg-red-50"
|
|
69
|
+
: "text-gray-700 hover:bg-gray-100"
|
|
70
|
+
}`;
|
|
71
|
+
|
|
72
|
+
const NotificationBadge = ({ value }: { value: string | number }) => (
|
|
73
|
+
<span className="absolute -top-1.5 -right-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white">
|
|
74
|
+
{value}
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const useNotificationBadge = (item: NavBarItem) => {
|
|
74
79
|
const relevantState = useAppSelector((state) =>
|
|
75
|
-
reduxNotificationBadge
|
|
80
|
+
item.reduxNotificationBadge
|
|
81
|
+
? item.reduxNotificationBadge(state)
|
|
82
|
+
: undefined
|
|
76
83
|
);
|
|
77
|
-
|
|
78
84
|
const [notificationBadge, setNotificationBadge] = useState<
|
|
79
85
|
undefined | string | number
|
|
80
86
|
>();
|
|
@@ -83,55 +89,343 @@ const NavItem = ({
|
|
|
83
89
|
setNotificationBadge(relevantState);
|
|
84
90
|
}, [relevantState]);
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
return notificationBadge;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const Chevron = ({ open }: { open: boolean }) => (
|
|
96
|
+
<span
|
|
97
|
+
className={`inline-flex shrink-0 transition-transform duration-150 motion-reduce:transition-none ${
|
|
98
|
+
open ? "rotate-180" : ""
|
|
99
|
+
}`}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
>
|
|
102
|
+
<DropDownArrowSvgIcon />
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const NavLinkItem = ({
|
|
107
|
+
item,
|
|
108
|
+
color,
|
|
109
|
+
layout,
|
|
110
|
+
onNavigate,
|
|
111
|
+
}: {
|
|
112
|
+
item: NavBarItem;
|
|
113
|
+
color: ColorInterface;
|
|
114
|
+
layout: NavLayout;
|
|
115
|
+
onNavigate?: () => void;
|
|
116
|
+
}) => {
|
|
117
|
+
const location = useLocation();
|
|
118
|
+
const isActive = pathMatches(location.pathname, item.link);
|
|
119
|
+
const notificationBadge = useNotificationBadge(item);
|
|
120
|
+
const palette = itemColor(item, color);
|
|
121
|
+
|
|
122
|
+
if (layout === "mobile") {
|
|
123
|
+
return (
|
|
124
|
+
<li>
|
|
125
|
+
<Link
|
|
126
|
+
to={item.link}
|
|
127
|
+
aria-current={isActive ? "page" : undefined}
|
|
128
|
+
onClick={onNavigate}
|
|
129
|
+
className={`${mobileRowClass(isActive)} relative`}
|
|
130
|
+
>
|
|
131
|
+
{item.icon}
|
|
132
|
+
<span className="flex-1">{item.name}</span>
|
|
133
|
+
{notificationBadge !== undefined && (
|
|
134
|
+
<NotificationBadge value={notificationBadge} />
|
|
135
|
+
)}
|
|
136
|
+
</Link>
|
|
137
|
+
</li>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<li className="relative">
|
|
143
|
+
<Link
|
|
144
|
+
to={item.link}
|
|
145
|
+
aria-current={isActive ? "page" : undefined}
|
|
146
|
+
className={`${desktopTriggerClass(palette, isActive)} relative`}
|
|
147
|
+
>
|
|
148
|
+
{item.icon}
|
|
149
|
+
<span>{item.name}</span>
|
|
150
|
+
{notificationBadge !== undefined && (
|
|
151
|
+
<NotificationBadge value={notificationBadge} />
|
|
152
|
+
)}
|
|
153
|
+
</Link>
|
|
154
|
+
</li>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const NavItemGroup = ({
|
|
159
|
+
item,
|
|
160
|
+
color,
|
|
161
|
+
layout,
|
|
162
|
+
open,
|
|
163
|
+
onToggle,
|
|
164
|
+
onNavigate,
|
|
165
|
+
}: {
|
|
166
|
+
item: NavBarItem;
|
|
167
|
+
color: ColorInterface;
|
|
168
|
+
layout: NavLayout;
|
|
169
|
+
open: boolean;
|
|
170
|
+
onToggle: () => void;
|
|
171
|
+
onNavigate?: () => void;
|
|
172
|
+
}) => {
|
|
173
|
+
const location = useLocation();
|
|
174
|
+
const children = visibleChildrenOf(item);
|
|
175
|
+
const childActive = children.some((child) =>
|
|
176
|
+
pathMatches(location.pathname, child.link)
|
|
177
|
+
);
|
|
178
|
+
const palette = itemColor(item, color);
|
|
179
|
+
const menuId = useId();
|
|
180
|
+
|
|
181
|
+
if (children.length === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (layout === "mobile") {
|
|
88
186
|
return (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
187
|
+
<li>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
aria-expanded={open}
|
|
191
|
+
aria-controls={menuId}
|
|
192
|
+
onClick={onToggle}
|
|
193
|
+
className={mobileRowClass(childActive)}
|
|
194
|
+
>
|
|
195
|
+
{item.icon}
|
|
196
|
+
<span className="flex-1">{item.name}</span>
|
|
197
|
+
<Chevron open={open} />
|
|
198
|
+
</button>
|
|
199
|
+
<ul
|
|
200
|
+
id={menuId}
|
|
201
|
+
hidden={!open}
|
|
202
|
+
className="mt-1 ml-3 space-y-1 border-l border-white/20 pl-2"
|
|
203
|
+
>
|
|
204
|
+
{children.map((child) => {
|
|
205
|
+
const isActive = pathMatches(location.pathname, child.link);
|
|
206
|
+
return (
|
|
207
|
+
<li key={child.link}>
|
|
208
|
+
<Link
|
|
209
|
+
to={child.link}
|
|
210
|
+
aria-current={isActive ? "page" : undefined}
|
|
211
|
+
onClick={onNavigate}
|
|
212
|
+
className={mobileRowClass(isActive)}
|
|
213
|
+
>
|
|
214
|
+
{child.icon}
|
|
215
|
+
<span>{child.name}</span>
|
|
216
|
+
</Link>
|
|
217
|
+
</li>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</ul>
|
|
221
|
+
</li>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<li className="relative">
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
aria-haspopup="menu"
|
|
230
|
+
aria-expanded={open}
|
|
231
|
+
aria-controls={menuId}
|
|
232
|
+
onClick={onToggle}
|
|
233
|
+
className={desktopTriggerClass(palette, childActive)}
|
|
234
|
+
>
|
|
235
|
+
{item.icon}
|
|
236
|
+
<span>{item.name}</span>
|
|
237
|
+
<Chevron open={open} />
|
|
238
|
+
</button>
|
|
239
|
+
<div
|
|
240
|
+
id={menuId}
|
|
241
|
+
role="menu"
|
|
242
|
+
hidden={!open}
|
|
243
|
+
className="absolute left-0 z-30 mt-2 min-w-[14rem] overflow-hidden rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5"
|
|
244
|
+
>
|
|
245
|
+
{children.map((child) => (
|
|
93
246
|
<Link
|
|
94
|
-
key={
|
|
95
|
-
|
|
96
|
-
|
|
247
|
+
key={child.link}
|
|
248
|
+
role="menuitem"
|
|
249
|
+
to={child.link}
|
|
250
|
+
onClick={onNavigate}
|
|
251
|
+
className={dropdownItemClass()}
|
|
97
252
|
>
|
|
98
|
-
{
|
|
253
|
+
{child.icon}
|
|
254
|
+
<span>{child.name}</span>
|
|
99
255
|
</Link>
|
|
100
256
|
))}
|
|
101
|
-
|
|
257
|
+
</div>
|
|
258
|
+
</li>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const UserMenu = ({
|
|
263
|
+
color,
|
|
264
|
+
layout,
|
|
265
|
+
open,
|
|
266
|
+
onToggle,
|
|
267
|
+
onNavigate,
|
|
268
|
+
entries,
|
|
269
|
+
}: {
|
|
270
|
+
color: ColorInterface;
|
|
271
|
+
layout: NavLayout;
|
|
272
|
+
open: boolean;
|
|
273
|
+
onToggle: () => void;
|
|
274
|
+
onNavigate?: () => void;
|
|
275
|
+
entries: UserMenuEntry[];
|
|
276
|
+
}) => {
|
|
277
|
+
const menuId = useId();
|
|
278
|
+
|
|
279
|
+
if (entries.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (layout === "mobile") {
|
|
284
|
+
return (
|
|
285
|
+
<div className="mt-4 border-t border-white/20 pt-4">
|
|
286
|
+
<p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-white/70">
|
|
287
|
+
Cuenta
|
|
288
|
+
</p>
|
|
289
|
+
<ul className="space-y-1">
|
|
290
|
+
{entries.map((entry) => (
|
|
291
|
+
<li key={entry.key}>
|
|
292
|
+
{entry.to ? (
|
|
293
|
+
<Link
|
|
294
|
+
to={entry.to}
|
|
295
|
+
onClick={onNavigate}
|
|
296
|
+
className={`${mobileRowClass(false)} [&_svg]:!text-current ${
|
|
297
|
+
entry.danger ? "text-red-100 hover:bg-red-500/20" : ""
|
|
298
|
+
}`}
|
|
299
|
+
>
|
|
300
|
+
{entry.icon}
|
|
301
|
+
<span>{entry.label}</span>
|
|
302
|
+
</Link>
|
|
303
|
+
) : (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={() => {
|
|
307
|
+
entry.onClick?.();
|
|
308
|
+
onNavigate?.();
|
|
309
|
+
}}
|
|
310
|
+
className={`${mobileRowClass(false)} [&_svg]:!text-current`}
|
|
311
|
+
>
|
|
312
|
+
{entry.icon}
|
|
313
|
+
<span>{entry.label}</span>
|
|
314
|
+
</button>
|
|
315
|
+
)}
|
|
316
|
+
</li>
|
|
317
|
+
))}
|
|
318
|
+
</ul>
|
|
319
|
+
</div>
|
|
102
320
|
);
|
|
103
321
|
}
|
|
104
322
|
|
|
105
|
-
// Si NO tiene elementos hijos, se renderiza como un ítem simple:
|
|
106
323
|
return (
|
|
107
|
-
<
|
|
324
|
+
<div className="relative ml-auto">
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
aria-label="Menú de usuario"
|
|
328
|
+
aria-haspopup="menu"
|
|
329
|
+
aria-expanded={open}
|
|
330
|
+
aria-controls={menuId}
|
|
331
|
+
onClick={onToggle}
|
|
332
|
+
className={`${desktopTriggerClass(color, open)} p-0 px-3`}
|
|
333
|
+
>
|
|
334
|
+
<OpcionButtonSvgIcon />
|
|
335
|
+
<Chevron open={open} />
|
|
336
|
+
</button>
|
|
108
337
|
<div
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150`}
|
|
338
|
+
id={menuId}
|
|
339
|
+
role="menu"
|
|
340
|
+
hidden={!open}
|
|
341
|
+
className="absolute right-0 z-30 mt-2 w-56 overflow-hidden rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5"
|
|
114
342
|
>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
343
|
+
{entries.map((entry) =>
|
|
344
|
+
entry.to ? (
|
|
345
|
+
<Link
|
|
346
|
+
key={entry.key}
|
|
347
|
+
role="menuitem"
|
|
348
|
+
to={entry.to}
|
|
349
|
+
onClick={onNavigate}
|
|
350
|
+
className={dropdownItemClass(entry.danger)}
|
|
351
|
+
>
|
|
352
|
+
{entry.icon}
|
|
353
|
+
<span>{entry.label}</span>
|
|
354
|
+
</Link>
|
|
355
|
+
) : (
|
|
356
|
+
<button
|
|
357
|
+
key={entry.key}
|
|
358
|
+
type="button"
|
|
359
|
+
role="menuitem"
|
|
360
|
+
onClick={() => {
|
|
361
|
+
entry.onClick?.();
|
|
362
|
+
onNavigate?.();
|
|
363
|
+
}}
|
|
364
|
+
className={dropdownItemClass(entry.danger)}
|
|
365
|
+
>
|
|
366
|
+
{entry.icon}
|
|
367
|
+
<span>{entry.label}</span>
|
|
368
|
+
</button>
|
|
369
|
+
)
|
|
370
|
+
)}
|
|
130
371
|
</div>
|
|
131
|
-
</
|
|
372
|
+
</div>
|
|
132
373
|
);
|
|
133
374
|
};
|
|
134
375
|
|
|
376
|
+
const NavItems = ({
|
|
377
|
+
items,
|
|
378
|
+
color,
|
|
379
|
+
layout,
|
|
380
|
+
openMenuId,
|
|
381
|
+
setOpenMenuId,
|
|
382
|
+
onNavigate,
|
|
383
|
+
}: {
|
|
384
|
+
items: NavBarItem[];
|
|
385
|
+
color: ColorInterface;
|
|
386
|
+
layout: NavLayout;
|
|
387
|
+
openMenuId: string | null;
|
|
388
|
+
setOpenMenuId: Dispatch<SetStateAction<string | null>>;
|
|
389
|
+
onNavigate?: () => void;
|
|
390
|
+
}) => (
|
|
391
|
+
<ul
|
|
392
|
+
className={
|
|
393
|
+
layout === "mobile"
|
|
394
|
+
? "flex flex-col gap-1"
|
|
395
|
+
: "flex flex-wrap items-center gap-1"
|
|
396
|
+
}
|
|
397
|
+
>
|
|
398
|
+
{items.map((item, index) => {
|
|
399
|
+
const children = visibleChildrenOf(item);
|
|
400
|
+
const menuKey = `${item.name}-${index}`;
|
|
401
|
+
if (children.length > 0) {
|
|
402
|
+
return (
|
|
403
|
+
<NavItemGroup
|
|
404
|
+
key={menuKey}
|
|
405
|
+
item={item}
|
|
406
|
+
color={color}
|
|
407
|
+
layout={layout}
|
|
408
|
+
open={openMenuId === menuKey}
|
|
409
|
+
onToggle={() =>
|
|
410
|
+
setOpenMenuId((current) => (current === menuKey ? null : menuKey))
|
|
411
|
+
}
|
|
412
|
+
onNavigate={onNavigate}
|
|
413
|
+
/>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return (
|
|
417
|
+
<NavLinkItem
|
|
418
|
+
key={menuKey}
|
|
419
|
+
item={item}
|
|
420
|
+
color={color}
|
|
421
|
+
layout={layout}
|
|
422
|
+
onNavigate={onNavigate}
|
|
423
|
+
/>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</ul>
|
|
427
|
+
);
|
|
428
|
+
|
|
135
429
|
export const NavBar = ({
|
|
136
430
|
title,
|
|
137
431
|
loggedIn,
|
|
@@ -143,10 +437,25 @@ export const NavBar = ({
|
|
|
143
437
|
showDarkModeToggle = false,
|
|
144
438
|
}: NavBarProps) => {
|
|
145
439
|
const color: ColorInterface = COLORS[selectedColor];
|
|
146
|
-
const [
|
|
440
|
+
const [navOpen, setNavOpen] = useState(false);
|
|
441
|
+
const [desktopMenuId, setDesktopMenuId] = useState<string | null>(null);
|
|
442
|
+
const [mobileAccordionId, setMobileAccordionId] = useState<string | null>(
|
|
443
|
+
null
|
|
444
|
+
);
|
|
147
445
|
const [currentVersion, setCurrentVersion] = useState(version);
|
|
148
446
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
149
447
|
const navigate = useNavigate();
|
|
448
|
+
const location = useLocation();
|
|
449
|
+
const desktopMenuRef = useRef<HTMLDivElement>(null);
|
|
450
|
+
const mobileDrawerRef = useRef<HTMLDivElement>(null);
|
|
451
|
+
const wasNavOpen = useRef(false);
|
|
452
|
+
const mobileNavId = useId();
|
|
453
|
+
|
|
454
|
+
const visibleItems = useMemo(
|
|
455
|
+
() =>
|
|
456
|
+
items.filter((item) => item.loggedIn === loggedIn && item.hide !== true),
|
|
457
|
+
[items, loggedIn]
|
|
458
|
+
);
|
|
150
459
|
|
|
151
460
|
useEffect(() => {
|
|
152
461
|
document.documentElement.classList.toggle("dark", isDarkMode);
|
|
@@ -158,180 +467,259 @@ export const NavBar = ({
|
|
|
158
467
|
}
|
|
159
468
|
}, [version, currentVersion]);
|
|
160
469
|
|
|
161
|
-
|
|
162
|
-
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
setNavOpen(false);
|
|
472
|
+
setDesktopMenuId(null);
|
|
473
|
+
setMobileAccordionId(null);
|
|
474
|
+
}, [location.pathname]);
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
if (!navOpen) return;
|
|
478
|
+
const previousOverflow = document.body.style.overflow;
|
|
479
|
+
document.body.style.overflow = "hidden";
|
|
480
|
+
return () => {
|
|
481
|
+
document.body.style.overflow = previousOverflow;
|
|
482
|
+
};
|
|
483
|
+
}, [navOpen]);
|
|
484
|
+
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
const drawer = mobileDrawerRef.current;
|
|
487
|
+
if (!drawer) return;
|
|
488
|
+
if (navOpen) {
|
|
489
|
+
drawer.removeAttribute("inert");
|
|
490
|
+
} else {
|
|
491
|
+
drawer.setAttribute("inert", "");
|
|
492
|
+
}
|
|
493
|
+
}, [navOpen]);
|
|
494
|
+
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
497
|
+
if (event.key !== "Escape") return;
|
|
498
|
+
setNavOpen(false);
|
|
499
|
+
setDesktopMenuId(null);
|
|
500
|
+
setMobileAccordionId(null);
|
|
501
|
+
};
|
|
502
|
+
document.addEventListener("keydown", onKeyDown);
|
|
503
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
504
|
+
}, []);
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
if (!desktopMenuId) return;
|
|
508
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
509
|
+
const target = event.target as Node;
|
|
510
|
+
if (mobileDrawerRef.current?.contains(target)) return;
|
|
511
|
+
if (desktopMenuRef.current?.contains(target)) return;
|
|
512
|
+
setDesktopMenuId(null);
|
|
513
|
+
};
|
|
514
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
515
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
516
|
+
}, [desktopMenuId]);
|
|
517
|
+
|
|
518
|
+
const closeMenus = () => {
|
|
519
|
+
setNavOpen(false);
|
|
520
|
+
setDesktopMenuId(null);
|
|
521
|
+
setMobileAccordionId(null);
|
|
163
522
|
};
|
|
164
523
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<NavItem
|
|
170
|
-
key={index}
|
|
171
|
-
name={item.name}
|
|
172
|
-
link={item.link}
|
|
173
|
-
icon={item.icon}
|
|
174
|
-
selectedColor={item.selectedColor}
|
|
175
|
-
color={color}
|
|
176
|
-
loggedIn={item.loggedIn}
|
|
177
|
-
reduxNotificationBadge={item.reduxNotificationBadge}
|
|
178
|
-
childrens={item.childrens?.map((x) => {
|
|
179
|
-
return { ...x } as any;
|
|
180
|
-
})}
|
|
181
|
-
/>
|
|
182
|
-
);
|
|
183
|
-
})}
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
524
|
+
const onLinkNavigate = () => {
|
|
525
|
+
window.setTimeout(closeMenus, 0);
|
|
526
|
+
};
|
|
186
527
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
[
|
|
190
|
-
|
|
528
|
+
const userEntries = useMemo<UserMenuEntry[]>(() => {
|
|
529
|
+
if (!loggedIn) return [];
|
|
530
|
+
const entries: UserMenuEntry[] = [];
|
|
531
|
+
if (settingsOption) {
|
|
532
|
+
entries.push({
|
|
533
|
+
key: "configuracion",
|
|
534
|
+
label: "Configuración",
|
|
535
|
+
to: "/configuracion",
|
|
536
|
+
icon: (
|
|
537
|
+
<svg
|
|
538
|
+
className="h-5 w-5 shrink-0"
|
|
539
|
+
width="24"
|
|
540
|
+
height="24"
|
|
541
|
+
fill="none"
|
|
542
|
+
stroke="currentColor"
|
|
543
|
+
strokeWidth="1.5"
|
|
544
|
+
viewBox="0 0 24 24"
|
|
545
|
+
strokeLinecap="round"
|
|
546
|
+
strokeLinejoin="round"
|
|
547
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
548
|
+
aria-hidden="true"
|
|
549
|
+
>
|
|
550
|
+
<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" />
|
|
551
|
+
<path d="M19.4 13a7.7 7.7 0 0 0 .06-2l2.1-1.64-2-3.46-2.55 1a7.7 7.7 0 0 0-1.73-1L14.7 2H9.3l-.58 2.9a7.7 7.7 0 0 0-1.73 1l-2.55-1-2 3.46L4.54 11a7.7 7.7 0 0 0 0 2l-2.1 1.64 2 3.46 2.55-1a7.7 7.7 0 0 0 1.73 1L9.3 22h5.4l.58-2.9a7.7 7.7 0 0 0 1.73-1l2.55 1 2-3.46Z" />
|
|
552
|
+
</svg>
|
|
553
|
+
),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (reloadCatalogsOption) {
|
|
557
|
+
entries.push({
|
|
558
|
+
key: "recargar-catalogos",
|
|
559
|
+
label: "Recargar catálogos",
|
|
560
|
+
to: "/reload-catalogs",
|
|
561
|
+
icon: (
|
|
562
|
+
<svg
|
|
563
|
+
className="h-5 w-5 shrink-0"
|
|
564
|
+
width="24"
|
|
565
|
+
height="24"
|
|
566
|
+
fill="none"
|
|
567
|
+
stroke="currentColor"
|
|
568
|
+
strokeWidth="1.5"
|
|
569
|
+
viewBox="0 0 24 24"
|
|
570
|
+
strokeLinecap="round"
|
|
571
|
+
strokeLinejoin="round"
|
|
572
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
573
|
+
aria-hidden="true"
|
|
574
|
+
>
|
|
575
|
+
<path d="M20.5 8c-1.392-3.179-4.823-5-8.522-5C7.299 3 3.453 6.552 3 11.1" />
|
|
576
|
+
<path d="M16.489 8.4h3.97A.54.54 0 0 0 21 7.86V3.9M3.5 16c1.392 3.179 4.823 5 8.522 5 4.679 0 8.525-3.552 8.978-8.1" />
|
|
577
|
+
<path d="M7.511 15.6h-3.97a.54.54 0 0 0-.541.54v3.96" />
|
|
578
|
+
</svg>
|
|
579
|
+
),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (showDarkModeToggle) {
|
|
583
|
+
entries.push({
|
|
584
|
+
key: "dark-mode-toggle",
|
|
585
|
+
label: isDarkMode ? "Modo claro" : "Modo oscuro",
|
|
586
|
+
onClick: () => setIsDarkMode((value) => !value),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
entries.push({
|
|
590
|
+
key: "cerrar-sesion",
|
|
591
|
+
label: "Cerrar sesión",
|
|
592
|
+
to: "/logout",
|
|
593
|
+
icon: <LogoutDropDownSvgIcon />,
|
|
594
|
+
danger: true,
|
|
595
|
+
});
|
|
596
|
+
return entries;
|
|
597
|
+
}, [
|
|
598
|
+
loggedIn,
|
|
599
|
+
settingsOption,
|
|
600
|
+
reloadCatalogsOption,
|
|
601
|
+
showDarkModeToggle,
|
|
602
|
+
isDarkMode,
|
|
603
|
+
]);
|
|
191
604
|
|
|
192
|
-
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
const justOpened = navOpen && !wasNavOpen.current;
|
|
607
|
+
wasNavOpen.current = navOpen;
|
|
608
|
+
if (!justOpened) return;
|
|
609
|
+
const activeGroup = visibleItems.find((item) =>
|
|
610
|
+
visibleChildrenOf(item).some((child) =>
|
|
611
|
+
pathMatches(location.pathname, child.link)
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
if (!activeGroup) return;
|
|
615
|
+
const index = visibleItems.indexOf(activeGroup);
|
|
616
|
+
setMobileAccordionId(`${activeGroup.name}-${index}`);
|
|
617
|
+
}, [navOpen, visibleItems, location.pathname]);
|
|
618
|
+
|
|
619
|
+
const renderCluster = (layout: NavLayout) => {
|
|
620
|
+
const isMobile = layout === "mobile";
|
|
193
621
|
return (
|
|
194
622
|
<>
|
|
195
|
-
<
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
>
|
|
214
|
-
<div className="mx-auto pt-2">
|
|
215
|
-
<svg
|
|
216
|
-
className="h-5 w-5 mr-1"
|
|
217
|
-
width="24"
|
|
218
|
-
height="24"
|
|
219
|
-
fill="none"
|
|
220
|
-
stroke="currentColor"
|
|
221
|
-
strokeWidth="1.5"
|
|
222
|
-
viewBox="0 0 24 24"
|
|
223
|
-
strokeLinecap="round"
|
|
224
|
-
strokeLinejoin="round"
|
|
225
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
226
|
-
aria-hidden="true"
|
|
227
|
-
>
|
|
228
|
-
<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" />
|
|
229
|
-
<path d="M19.4 13a7.7 7.7 0 0 0 .06-2l2.1-1.64-2-3.46-2.55 1a7.7 7.7 0 0 0-1.73-1L14.7 2H9.3l-.58 2.9a7.7 7.7 0 0 0-1.73 1l-2.55-1-2 3.46L4.54 11a7.7 7.7 0 0 0 0 2l-2.1 1.64 2 3.46 2.55-1a7.7 7.7 0 0 0 1.73 1L9.3 22h5.4l.58-2.9a7.7 7.7 0 0 0 1.73-1l2.55 1 2-3.46Z" />
|
|
230
|
-
</svg>
|
|
231
|
-
<span>Configuración</span>
|
|
232
|
-
</div>
|
|
233
|
-
</Link>
|
|
234
|
-
),
|
|
235
|
-
reloadCatalogsOption && (
|
|
236
|
-
<Link
|
|
237
|
-
key="recargar-catalogos"
|
|
238
|
-
className={`block px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-red-100 dark:hover:bg-gray-700 dark:hover:text-white`}
|
|
239
|
-
to="/reload-catalogs"
|
|
240
|
-
>
|
|
241
|
-
<div className="mx-auto pt-2">
|
|
242
|
-
<svg
|
|
243
|
-
className={`h-5 w-5 mr-1`}
|
|
244
|
-
width="24"
|
|
245
|
-
height="24"
|
|
246
|
-
fill="none"
|
|
247
|
-
stroke="currentColor"
|
|
248
|
-
strokeWidth="1.5"
|
|
249
|
-
viewBox="0 0 24 24"
|
|
250
|
-
strokeLinecap="round"
|
|
251
|
-
strokeLinejoin="round"
|
|
252
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
253
|
-
>
|
|
254
|
-
<path d="M20.5 8c-1.392-3.179-4.823-5-8.522-5C7.299 3 3.453 6.552 3 11.1" />
|
|
255
|
-
<path d="M16.489 8.4h3.97A.54.54 0 0 0 21 7.86V3.9M3.5 16c1.392 3.179 4.823 5 8.522 5 4.679 0 8.525-3.552 8.978-8.1" />
|
|
256
|
-
<path d="M7.511 15.6h-3.97a.54.54 0 0 0-.541.54v3.96" />
|
|
257
|
-
</svg>
|
|
258
|
-
<span>Recargar catálogos</span>
|
|
259
|
-
</div>
|
|
260
|
-
</Link>
|
|
261
|
-
),
|
|
262
|
-
showDarkModeToggle && (
|
|
263
|
-
<button
|
|
264
|
-
key="dark-mode-toggle"
|
|
265
|
-
type="button"
|
|
266
|
-
className="block w-full cursor-pointer text-left px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
267
|
-
onClick={() => setIsDarkMode((value) => !value)}
|
|
268
|
-
>
|
|
269
|
-
<div className="mx-auto pt-2">
|
|
270
|
-
<span>{isDarkMode ? "Modo claro" : "Modo oscuro"}</span>
|
|
271
|
-
</div>
|
|
272
|
-
</button>
|
|
273
|
-
),
|
|
274
|
-
<Link
|
|
275
|
-
key="cerrar-sesion"
|
|
276
|
-
className={`block px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-red-100 dark:hover:bg-gray-700 dark:hover:text-white`}
|
|
277
|
-
to="/logout"
|
|
278
|
-
>
|
|
279
|
-
<div className="mx-auto pt-2">
|
|
280
|
-
{<LogoutDropDownSvgIcon />}
|
|
281
|
-
<span>Cerrar sesión</span>
|
|
282
|
-
</div>
|
|
283
|
-
</Link>,
|
|
284
|
-
]}
|
|
285
|
-
/>
|
|
286
|
-
)}
|
|
287
|
-
</ul>
|
|
623
|
+
<NavItems
|
|
624
|
+
items={visibleItems}
|
|
625
|
+
color={color}
|
|
626
|
+
layout={layout}
|
|
627
|
+
openMenuId={isMobile ? mobileAccordionId : desktopMenuId}
|
|
628
|
+
setOpenMenuId={isMobile ? setMobileAccordionId : setDesktopMenuId}
|
|
629
|
+
onNavigate={onLinkNavigate}
|
|
630
|
+
/>
|
|
631
|
+
<UserMenu
|
|
632
|
+
color={color}
|
|
633
|
+
layout={layout}
|
|
634
|
+
open={desktopMenuId === "user"}
|
|
635
|
+
onToggle={() =>
|
|
636
|
+
setDesktopMenuId((current) => (current === "user" ? null : "user"))
|
|
637
|
+
}
|
|
638
|
+
onNavigate={onLinkNavigate}
|
|
639
|
+
entries={userEntries}
|
|
640
|
+
/>
|
|
288
641
|
</>
|
|
289
642
|
);
|
|
290
|
-
}
|
|
643
|
+
};
|
|
291
644
|
|
|
292
645
|
return (
|
|
293
|
-
<nav className={`py-
|
|
294
|
-
<div className="flex items-center justify-between
|
|
295
|
-
<div className="flex
|
|
646
|
+
<nav className={`py-2 ${color.bg600}`} aria-label="Principal">
|
|
647
|
+
<div className="flex items-center justify-between gap-3 px-4 sm:px-5">
|
|
648
|
+
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 lg:w-auto lg:flex-none">
|
|
296
649
|
<Link
|
|
297
|
-
className=
|
|
298
|
-
to=
|
|
650
|
+
className={`inline-flex min-h-11 min-w-0 cursor-pointer items-center gap-2 truncate text-sm font-bold uppercase text-white ${focusRing}`}
|
|
651
|
+
to="/home"
|
|
299
652
|
>
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
</div>
|
|
303
|
-
{title}
|
|
653
|
+
<img className="h-7 w-auto shrink-0" src="/logo.png" alt="" />
|
|
654
|
+
<span className="truncate">{title}</span>
|
|
304
655
|
</Link>
|
|
305
656
|
{version !== currentVersion && (
|
|
306
657
|
<button
|
|
307
|
-
|
|
308
|
-
|
|
658
|
+
type="button"
|
|
659
|
+
className={`shrink-0 cursor-pointer rounded-full px-3 py-2 text-xs text-white ${color.bg700} hover:opacity-90 ${focusRing}`}
|
|
660
|
+
onClick={() => navigate(0)}
|
|
309
661
|
>
|
|
310
|
-
|
|
662
|
+
Actualizar versión
|
|
311
663
|
</button>
|
|
312
664
|
)}
|
|
313
665
|
<button
|
|
314
|
-
className={`rounded lg:hidden focus:outline-none focus:ring focus:${color.ring600} focus:ring-opacity-50`}
|
|
315
|
-
aria-label="Toggle mobile menu"
|
|
316
666
|
type="button"
|
|
317
|
-
|
|
667
|
+
className={`inline-flex min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-md text-white lg:hidden ${focusRing} ${
|
|
668
|
+
navOpen ? "pointer-events-none invisible" : ""
|
|
669
|
+
}`}
|
|
670
|
+
aria-label="Abrir menú"
|
|
671
|
+
aria-expanded={navOpen}
|
|
672
|
+
aria-controls={mobileNavId}
|
|
673
|
+
aria-hidden={navOpen}
|
|
674
|
+
tabIndex={navOpen ? -1 : undefined}
|
|
675
|
+
onClick={() => setNavOpen(true)}
|
|
318
676
|
>
|
|
319
|
-
<MenuAlt4Svg open={
|
|
677
|
+
<MenuAlt4Svg open={false} />
|
|
320
678
|
</button>
|
|
321
679
|
</div>
|
|
322
|
-
|
|
680
|
+
|
|
681
|
+
{navOpen && (
|
|
682
|
+
<button
|
|
683
|
+
type="button"
|
|
684
|
+
aria-label="Cerrar menú"
|
|
685
|
+
className="fixed inset-0 z-40 cursor-pointer bg-black/40 lg:hidden"
|
|
686
|
+
onClick={closeMenus}
|
|
687
|
+
/>
|
|
688
|
+
)}
|
|
689
|
+
|
|
323
690
|
<div
|
|
324
|
-
|
|
691
|
+
ref={mobileDrawerRef}
|
|
692
|
+
id={mobileNavId}
|
|
693
|
+
aria-hidden={!navOpen}
|
|
694
|
+
className={`fixed inset-y-0 left-0 z-50 flex w-[min(20rem,86vw)] flex-col ${
|
|
325
695
|
color.bg700
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
}
|
|
696
|
+
} shadow-lg transition-transform duration-200 ease-out motion-reduce:transition-none lg:hidden ${
|
|
697
|
+
navOpen ? "translate-x-0" : "pointer-events-none -translate-x-full"
|
|
698
|
+
}`}
|
|
329
699
|
>
|
|
330
|
-
<div className="
|
|
700
|
+
<div className="flex items-center justify-between gap-2 border-b border-white/15 px-4 py-3">
|
|
701
|
+
<p className="truncate text-sm font-bold uppercase text-white">
|
|
702
|
+
{title}
|
|
703
|
+
</p>
|
|
704
|
+
<button
|
|
705
|
+
type="button"
|
|
706
|
+
className={`inline-flex min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-md text-white ${focusRing}`}
|
|
707
|
+
aria-label="Cerrar menú"
|
|
708
|
+
onClick={closeMenus}
|
|
709
|
+
>
|
|
710
|
+
<ExitSvg />
|
|
711
|
+
</button>
|
|
712
|
+
</div>
|
|
713
|
+
<div className="flex-1 overflow-y-auto overscroll-contain p-3">
|
|
714
|
+
{renderCluster("mobile")}
|
|
715
|
+
</div>
|
|
331
716
|
</div>
|
|
332
|
-
|
|
333
|
-
<div
|
|
334
|
-
{
|
|
717
|
+
|
|
718
|
+
<div
|
|
719
|
+
ref={desktopMenuRef}
|
|
720
|
+
className="hidden min-w-0 flex-1 items-center gap-2 lg:flex"
|
|
721
|
+
>
|
|
722
|
+
{renderCluster("desktop")}
|
|
335
723
|
</div>
|
|
336
724
|
</div>
|
|
337
725
|
</nav>
|