@djangocfg/layouts 2.1.248 → 2.1.251
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 +11 -9
- package/package.json +18 -18
- package/src/index.ts +4 -2
- package/src/layouts/AppLayout/AppLayout.tsx +70 -13
- package/src/layouts/AppLayout/index.ts +7 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +27 -42
- package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +13 -2
- package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +7 -5
- package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +130 -109
- package/src/layouts/PublicLayout/components/PublicFooter/types.ts +9 -4
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +86 -38
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +74 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +341 -67
- package/src/layouts/PublicLayout/components/index.ts +1 -0
- package/src/layouts/PublicLayout/context.tsx +4 -9
- package/src/layouts/PublicLayout/index.ts +9 -0
- package/src/layouts/types/ui.types.ts +2 -0
|
@@ -6,92 +6,367 @@
|
|
|
6
6
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
|
-
import { Menu, X } from 'lucide-react';
|
|
9
|
+
import { ChevronDown, Menu, X } from 'lucide-react';
|
|
10
10
|
import Link from 'next/link';
|
|
11
|
-
import React, { useMemo } from 'react';
|
|
11
|
+
import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
12
12
|
|
|
13
13
|
import { useAuth } from '@djangocfg/api/auth';
|
|
14
14
|
import { useAppT } from '@djangocfg/i18n';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import {
|
|
16
|
+
Button,
|
|
17
|
+
} from '@djangocfg/ui-core/components';
|
|
18
|
+
import { useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
18
19
|
// cn is reserved for future conditional styling
|
|
19
20
|
import { cn as _cn } from '@djangocfg/ui-core/lib';
|
|
21
|
+
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
20
22
|
|
|
21
23
|
import { UserMenu } from '../../_components/UserMenu';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
import { usePublicLayoutOptional } from '../context';
|
|
25
|
+
|
|
26
|
+
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
27
|
+
|
|
28
|
+
export type PublicNavbarVariant = 'floating' | 'flush';
|
|
29
|
+
export type PublicNavbarPosition = 'sticky' | 'fixed' | 'static';
|
|
30
|
+
|
|
31
|
+
export interface PublicDesktopDropdownRenderProps {
|
|
32
|
+
item: NavigationItem;
|
|
33
|
+
isOpen: boolean;
|
|
34
|
+
isActive: boolean;
|
|
35
|
+
close: () => void;
|
|
36
|
+
defaultPopover: React.ReactNode;
|
|
37
|
+
defaultItems: React.ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PublicDesktopDropdownRenderer = (props: PublicDesktopDropdownRenderProps) => React.ReactNode;
|
|
41
|
+
|
|
42
|
+
interface PublicNavigationProps {
|
|
43
|
+
/** Custom brand node (full control over logo/text/link) */
|
|
44
|
+
brand?: ReactNode;
|
|
45
|
+
/** Brand link for default brand renderer */
|
|
46
|
+
brandHref?: string;
|
|
47
|
+
logo?: string;
|
|
48
|
+
siteName?: string;
|
|
49
|
+
navigation?: NavigationItem[];
|
|
50
|
+
userMenu?: UserMenuConfig;
|
|
51
|
+
containerClassName?: string;
|
|
52
|
+
navbarVariant?: PublicNavbarVariant;
|
|
53
|
+
navbarPosition?: PublicNavbarPosition;
|
|
54
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
55
|
+
/** Max visible top-level desktop items before collapsing into "More" */
|
|
56
|
+
desktopMaxPrimaryItems?: number;
|
|
57
|
+
mobileMenuOpen?: boolean;
|
|
58
|
+
onMobileMenuToggle?: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function PublicNavigation(props: PublicNavigationProps = {}) {
|
|
62
|
+
const context = usePublicLayoutOptional();
|
|
63
|
+
const brand = props.brand;
|
|
64
|
+
const brandHref = props.brandHref ?? '/';
|
|
65
|
+
const logo = props.logo;
|
|
66
|
+
const siteName = props.siteName ?? 'App';
|
|
67
|
+
const navigation = props.navigation ?? [];
|
|
68
|
+
const userMenu = props.userMenu;
|
|
69
|
+
const containerClassName = props.containerClassName;
|
|
70
|
+
const navbarVariant = props.navbarVariant ?? 'floating';
|
|
71
|
+
const navbarPosition = props.navbarPosition ?? 'sticky';
|
|
72
|
+
const renderDesktopDropdown = props.renderDesktopDropdown;
|
|
73
|
+
const desktopMaxPrimaryItems = Math.max(1, props.desktopMaxPrimaryItems ?? 7);
|
|
74
|
+
const mobileMenuOpen = props.mobileMenuOpen ?? context?.mobileMenuOpen ?? false;
|
|
75
|
+
const toggleMobileMenu = props.onMobileMenuToggle ?? context?.toggleMobileMenu ?? (() => {});
|
|
34
76
|
const { isAuthenticated: _isAuthenticated } = useAuth();
|
|
35
|
-
const
|
|
77
|
+
const isTabletOrBelow = useIsTabletOrBelow();
|
|
78
|
+
const pathname = usePathnameWithoutLocale();
|
|
36
79
|
const t = useAppT();
|
|
80
|
+
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null);
|
|
81
|
+
const openTimerRef = useRef<number | null>(null);
|
|
82
|
+
const closeTimerRef = useRef<number | null>(null);
|
|
83
|
+
const navOuterRef = useRef<HTMLDivElement | null>(null);
|
|
37
84
|
|
|
38
85
|
const toggleMobileLabel = useMemo(() => t('layouts.navigation.toggleMobile'), [t]);
|
|
86
|
+
const desktopNavItemClass =
|
|
87
|
+
'inline-flex h-8 items-center rounded-md px-2 text-sm font-medium text-foreground/90 transition-colors hover:text-foreground hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50';
|
|
88
|
+
|
|
89
|
+
const clearOpenTimer = () => {
|
|
90
|
+
if (openTimerRef.current) {
|
|
91
|
+
window.clearTimeout(openTimerRef.current);
|
|
92
|
+
openTimerRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const clearCloseTimer = () => {
|
|
97
|
+
if (closeTimerRef.current) {
|
|
98
|
+
window.clearTimeout(closeTimerRef.current);
|
|
99
|
+
closeTimerRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const scheduleOpen = (key: string) => {
|
|
104
|
+
clearOpenTimer();
|
|
105
|
+
clearCloseTimer();
|
|
106
|
+
openTimerRef.current = window.setTimeout(() => {
|
|
107
|
+
setOpenDropdownKey(key);
|
|
108
|
+
}, 80);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const scheduleClose = (key: string) => {
|
|
112
|
+
clearOpenTimer();
|
|
113
|
+
clearCloseTimer();
|
|
114
|
+
closeTimerRef.current = window.setTimeout(() => {
|
|
115
|
+
setOpenDropdownKey((prev) => (prev === key ? null : prev));
|
|
116
|
+
}, 120);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
return () => {
|
|
121
|
+
clearOpenTimer();
|
|
122
|
+
clearCloseTimer();
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (isTabletOrBelow) {
|
|
128
|
+
setOpenDropdownKey(null);
|
|
129
|
+
clearOpenTimer();
|
|
130
|
+
clearCloseTimer();
|
|
131
|
+
}
|
|
132
|
+
}, [isTabletOrBelow]);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const updateDrawerViewportVars = () => {
|
|
136
|
+
const root = document.documentElement;
|
|
137
|
+
const navEl = navOuterRef.current;
|
|
138
|
+
if (!navEl) return;
|
|
139
|
+
|
|
140
|
+
const rect = navEl.getBoundingClientRect();
|
|
141
|
+
const top = Math.max(0, Math.round(rect.bottom + 8));
|
|
142
|
+
const viewportHeight = window.innerHeight;
|
|
143
|
+
const maxHeight = Math.max(240, viewportHeight - top - 12);
|
|
144
|
+
|
|
145
|
+
root.style.setProperty('--public-navbar-mobile-drawer-top', `${top}px`);
|
|
146
|
+
root.style.setProperty('--public-navbar-mobile-drawer-max-height', `${maxHeight}px`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
updateDrawerViewportVars();
|
|
150
|
+
const navEl = navOuterRef.current;
|
|
151
|
+
const observer = navEl ? new ResizeObserver(updateDrawerViewportVars) : null;
|
|
152
|
+
if (navEl && observer) observer.observe(navEl);
|
|
153
|
+
window.addEventListener('resize', updateDrawerViewportVars);
|
|
154
|
+
window.addEventListener('scroll', updateDrawerViewportVars, { passive: true });
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
if (navEl && observer) observer.unobserve(navEl);
|
|
158
|
+
observer?.disconnect();
|
|
159
|
+
window.removeEventListener('resize', updateDrawerViewportVars);
|
|
160
|
+
window.removeEventListener('scroll', updateDrawerViewportVars);
|
|
161
|
+
};
|
|
162
|
+
}, [navbarPosition, navbarVariant, containerClassName]);
|
|
163
|
+
|
|
164
|
+
const navOuterClassName = (() => {
|
|
165
|
+
const positionClass =
|
|
166
|
+
navbarPosition === 'fixed'
|
|
167
|
+
? 'fixed'
|
|
168
|
+
: navbarPosition === 'static'
|
|
169
|
+
? 'static'
|
|
170
|
+
: 'sticky';
|
|
171
|
+
|
|
172
|
+
const topClass = navbarVariant === 'floating' ? 'top-3' : 'top-0';
|
|
173
|
+
const insetPaddingClass = navbarVariant === 'floating' ? 'px-3 sm:px-4 lg:px-6' : '';
|
|
174
|
+
|
|
175
|
+
return `${positionClass} ${topClass} inset-x-0 z-50 ${insetPaddingClass}`.trim().replace(/\s+/g, ' ');
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
const navInnerClassName = (() => {
|
|
179
|
+
const base =
|
|
180
|
+
'mx-auto w-full border border-border/40 dark:border-border/70';
|
|
181
|
+
|
|
182
|
+
const visual =
|
|
183
|
+
navbarVariant === 'floating'
|
|
184
|
+
? 'rounded-2xl shadow-[0_10px_28px_rgba(0,0,0,0.14)] dark:shadow-[0_10px_28px_rgba(0,0,0,0.22)]'
|
|
185
|
+
: 'rounded-none border-x-0 border-t-0 shadow-none';
|
|
186
|
+
|
|
187
|
+
return `${base} ${visual} ${containerClassName || ''}`.trim().replace(/\s+/g, ' ');
|
|
188
|
+
})();
|
|
189
|
+
|
|
190
|
+
const isActivePath = (href: string) => {
|
|
191
|
+
if (href === '/') return pathname === '/';
|
|
192
|
+
return pathname === href || pathname.startsWith(`${href}/`);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const isGroupActive = (item: NavigationItem) => {
|
|
196
|
+
if (isActivePath(item.href)) return true;
|
|
197
|
+
if (!item.items) return false;
|
|
198
|
+
return item.items.some((subItem) => isActivePath(subItem.href));
|
|
199
|
+
};
|
|
39
200
|
|
|
40
|
-
const
|
|
201
|
+
const closeDropdown = () => setOpenDropdownKey(null);
|
|
202
|
+
const desktopPrimaryNavigation = navigation.slice(0, desktopMaxPrimaryItems);
|
|
203
|
+
const desktopOverflowNavigation = navigation.slice(desktopMaxPrimaryItems);
|
|
204
|
+
|
|
205
|
+
const renderDesktopNavItem = (item: NavigationItem) => {
|
|
206
|
+
if (item.items && item.items.length > 0) {
|
|
207
|
+
const dropdownKey = `${item.label}-${item.href}`;
|
|
208
|
+
const defaultItems = (
|
|
209
|
+
<>
|
|
210
|
+
{item.items.map((subItem) => (
|
|
211
|
+
<div key={`${item.label}-${subItem.href}`} className="rounded-md">
|
|
212
|
+
{subItem.external ? (
|
|
213
|
+
<a
|
|
214
|
+
href={subItem.href}
|
|
215
|
+
target="_blank"
|
|
216
|
+
rel="noopener noreferrer"
|
|
217
|
+
className="block rounded-md px-2.5 py-2 text-sm font-medium hover:bg-accent/40"
|
|
218
|
+
>
|
|
219
|
+
{subItem.label}
|
|
220
|
+
</a>
|
|
221
|
+
) : (
|
|
222
|
+
<Link
|
|
223
|
+
href={subItem.href}
|
|
224
|
+
className="block rounded-md px-2.5 py-2 text-sm font-medium hover:bg-accent/40"
|
|
225
|
+
>
|
|
226
|
+
{subItem.label}
|
|
227
|
+
</Link>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const defaultPopover = (
|
|
235
|
+
<div
|
|
236
|
+
className="absolute left-0 top-full mt-1 z-[1200] min-w-56 rounded-xl border border-border/70 bg-background/95 backdrop-blur-sm p-1 shadow-[0_10px_28px_rgba(0,0,0,0.22)]"
|
|
237
|
+
onMouseEnter={() => {
|
|
238
|
+
clearOpenTimer();
|
|
239
|
+
clearCloseTimer();
|
|
240
|
+
}}
|
|
241
|
+
onMouseLeave={() => scheduleClose(dropdownKey)}
|
|
242
|
+
>
|
|
243
|
+
{defaultItems}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const isOpen = openDropdownKey === dropdownKey;
|
|
248
|
+
const isActive = isGroupActive(item);
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
key={dropdownKey}
|
|
252
|
+
className="relative"
|
|
253
|
+
onMouseEnter={() => scheduleOpen(dropdownKey)}
|
|
254
|
+
onMouseLeave={() => scheduleClose(dropdownKey)}
|
|
255
|
+
>
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
className={`group ${desktopNavItemClass} ${isOpen || isActive ? 'bg-accent/50 text-foreground' : ''}`}
|
|
260
|
+
>
|
|
261
|
+
<span>{item.label}</span>
|
|
262
|
+
<ChevronDown
|
|
263
|
+
className={`ml-1 h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
264
|
+
/>
|
|
265
|
+
</Button>
|
|
266
|
+
|
|
267
|
+
{isOpen && (
|
|
268
|
+
renderDesktopDropdown
|
|
269
|
+
? renderDesktopDropdown({
|
|
270
|
+
item,
|
|
271
|
+
isOpen,
|
|
272
|
+
isActive,
|
|
273
|
+
close: closeDropdown,
|
|
274
|
+
defaultPopover,
|
|
275
|
+
defaultItems,
|
|
276
|
+
})
|
|
277
|
+
: defaultPopover
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<Link
|
|
285
|
+
key={item.href}
|
|
286
|
+
href={item.href}
|
|
287
|
+
className={`${desktopNavItemClass} ${isActivePath(item.href) ? 'bg-accent/50 text-foreground' : ''}`}
|
|
288
|
+
>
|
|
289
|
+
{item.label}
|
|
290
|
+
</Link>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
41
293
|
|
|
42
294
|
return (
|
|
43
|
-
<div className={
|
|
295
|
+
<div ref={navOuterRef} className={navOuterClassName}>
|
|
44
296
|
<nav
|
|
45
|
-
className={
|
|
297
|
+
className={navInnerClassName}
|
|
46
298
|
style={{ backgroundColor: 'hsl(var(--background) / 0.72)', backdropFilter: 'blur(10px)' }}
|
|
47
299
|
>
|
|
48
|
-
<div className="w-full px-
|
|
49
|
-
<div
|
|
50
|
-
className="flex items-center justify-between py-3.5"
|
|
51
|
-
onClick={isMobile ? toggleMobileMenu : undefined}
|
|
52
|
-
onKeyDown={isMobile ? (event) => {
|
|
53
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
54
|
-
event.preventDefault();
|
|
55
|
-
toggleMobileMenu();
|
|
56
|
-
}
|
|
57
|
-
} : undefined}
|
|
58
|
-
role={isMobile ? 'button' : undefined}
|
|
59
|
-
tabIndex={isMobile ? 0 : undefined}
|
|
60
|
-
aria-label={isMobile ? toggleMobileLabel : undefined}
|
|
61
|
-
>
|
|
300
|
+
<div className="w-full px-3 sm:px-4 lg:px-6">
|
|
301
|
+
<div className="flex items-center justify-between py-3.5">
|
|
62
302
|
{/* Logo */}
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
{logo && (
|
|
66
|
-
<img src={logo} alt={siteName} className="h-6 w-auto object-contain" />
|
|
67
|
-
)}
|
|
68
|
-
<span className="font-bold text-lg">{siteName}</span>
|
|
69
|
-
</div>
|
|
303
|
+
{brand ? (
|
|
304
|
+
brand
|
|
70
305
|
) : (
|
|
71
|
-
<Link href=
|
|
306
|
+
<Link href={brandHref} className="flex items-center gap-1.5">
|
|
72
307
|
{logo && (
|
|
73
|
-
<img src={logo} alt={siteName} className="h-
|
|
308
|
+
<img src={logo} alt={siteName} className="h-5.5 w-auto object-contain" />
|
|
74
309
|
)}
|
|
75
|
-
<span className="font-bold text-
|
|
310
|
+
<span className="font-bold text-[15px]">{siteName}</span>
|
|
76
311
|
</Link>
|
|
77
312
|
)}
|
|
78
313
|
|
|
79
314
|
{/* Desktop Navigation */}
|
|
80
|
-
<div className="hidden
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
315
|
+
<div className="hidden lg:flex items-center gap-3">
|
|
316
|
+
{desktopPrimaryNavigation.map(renderDesktopNavItem)}
|
|
317
|
+
{desktopOverflowNavigation.length > 0 && (
|
|
318
|
+
<div
|
|
319
|
+
className="relative"
|
|
320
|
+
onMouseEnter={() => scheduleOpen('__overflow-more')}
|
|
321
|
+
onMouseLeave={() => scheduleClose('__overflow-more')}
|
|
86
322
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
323
|
+
<Button
|
|
324
|
+
variant="ghost"
|
|
325
|
+
size="sm"
|
|
326
|
+
className={`group ${desktopNavItemClass} ${
|
|
327
|
+
openDropdownKey === '__overflow-more' || desktopOverflowNavigation.some((item) => isGroupActive(item))
|
|
328
|
+
? 'bg-accent/50 text-foreground'
|
|
329
|
+
: ''
|
|
330
|
+
}`}
|
|
331
|
+
>
|
|
332
|
+
<span>More</span>
|
|
333
|
+
<ChevronDown
|
|
334
|
+
className={`ml-1 h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${
|
|
335
|
+
openDropdownKey === '__overflow-more' ? 'rotate-180' : ''
|
|
336
|
+
}`}
|
|
337
|
+
/>
|
|
338
|
+
</Button>
|
|
339
|
+
|
|
340
|
+
{openDropdownKey === '__overflow-more' && (
|
|
341
|
+
<div
|
|
342
|
+
className="absolute right-0 top-full mt-1 z-[1200] min-w-56 rounded-xl border border-border/70 bg-background/95 backdrop-blur-sm p-1 shadow-[0_10px_28px_rgba(0,0,0,0.22)]"
|
|
343
|
+
onMouseEnter={() => {
|
|
344
|
+
clearOpenTimer();
|
|
345
|
+
clearCloseTimer();
|
|
346
|
+
}}
|
|
347
|
+
onMouseLeave={() => scheduleClose('__overflow-more')}
|
|
348
|
+
>
|
|
349
|
+
{desktopOverflowNavigation.map((item) => (
|
|
350
|
+
<div key={`overflow-${item.href}`} className="rounded-md">
|
|
351
|
+
<Link
|
|
352
|
+
href={item.href}
|
|
353
|
+
className={`block rounded-md px-2.5 py-2 text-sm font-medium hover:bg-accent/40 ${
|
|
354
|
+
isGroupActive(item) ? 'bg-accent/45 text-foreground' : ''
|
|
355
|
+
}`}
|
|
356
|
+
>
|
|
357
|
+
{item.label}
|
|
358
|
+
</Link>
|
|
359
|
+
</div>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
90
365
|
</div>
|
|
91
366
|
|
|
92
367
|
{/* User Menu / Actions */}
|
|
93
368
|
<div className="flex items-center gap-4">
|
|
94
|
-
|
|
369
|
+
<div className="hidden lg:flex">
|
|
95
370
|
<>
|
|
96
371
|
{/* User Menu */}
|
|
97
372
|
<UserMenu
|
|
@@ -100,20 +375,19 @@ export function PublicNavigation() {
|
|
|
100
375
|
authPath={userMenu?.authPath}
|
|
101
376
|
/>
|
|
102
377
|
</>
|
|
103
|
-
|
|
378
|
+
</div>
|
|
104
379
|
|
|
105
380
|
{/* Mobile Menu Button */}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)}
|
|
381
|
+
<Button
|
|
382
|
+
variant="ghost"
|
|
383
|
+
size="icon"
|
|
384
|
+
aria-label={toggleMobileLabel}
|
|
385
|
+
data-mobile-menu-trigger="true"
|
|
386
|
+
className="lg:hidden"
|
|
387
|
+
onClick={toggleMobileMenu}
|
|
388
|
+
>
|
|
389
|
+
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
390
|
+
</Button>
|
|
117
391
|
</div>
|
|
118
392
|
</div>
|
|
119
393
|
</div>
|
|
@@ -2,16 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext } from 'react';
|
|
4
4
|
|
|
5
|
-
import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
|
|
6
|
-
import type { NavigationItem, UserMenuConfig } from '../types';
|
|
7
|
-
|
|
8
5
|
export interface PublicLayoutContextValue {
|
|
9
|
-
logo?: string;
|
|
10
|
-
siteName: string;
|
|
11
|
-
navigation: NavigationItem[];
|
|
12
|
-
userMenu?: UserMenuConfig;
|
|
13
|
-
i18n?: I18nLayoutConfig;
|
|
14
|
-
containerClassName?: string;
|
|
15
6
|
mobileMenuOpen: boolean;
|
|
16
7
|
toggleMobileMenu: () => void;
|
|
17
8
|
closeMobileMenu: () => void;
|
|
@@ -37,3 +28,7 @@ export function usePublicLayout() {
|
|
|
37
28
|
return context;
|
|
38
29
|
}
|
|
39
30
|
|
|
31
|
+
export function usePublicLayoutOptional() {
|
|
32
|
+
return useContext(PublicLayoutContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
export { PublicLayout } from './PublicLayout';
|
|
6
6
|
export type { PublicLayoutProps } from './PublicLayout';
|
|
7
|
+
export { PublicNavigation, PublicMobileDrawer, PublicNavbar } from './components';
|
|
8
|
+
export type {
|
|
9
|
+
PublicNavbarVariant,
|
|
10
|
+
PublicNavbarPosition,
|
|
11
|
+
PublicDesktopDropdownRenderer,
|
|
12
|
+
PublicDesktopDropdownRenderProps,
|
|
13
|
+
} from './components/PublicNavigation';
|
|
14
|
+
export type { PublicNavbarConfig, PublicNavbarProps } from './components/PublicNavbar';
|
|
7
15
|
export {
|
|
8
16
|
PublicFooter,
|
|
9
17
|
FooterProjectInfo,
|
|
@@ -15,4 +23,5 @@ export {
|
|
|
15
23
|
export type {
|
|
16
24
|
PublicFooterProps,
|
|
17
25
|
} from './components/PublicFooter';
|
|
26
|
+
export { PublicLayoutProvider, usePublicLayout } from './context';
|
|
18
27
|
|