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