@djangocfg/layouts 2.1.275 → 2.1.277
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 +52 -180
- package/package.json +18 -18
- package/src/layouts/AppLayout/AppLayout.tsx +14 -14
- package/src/layouts/PublicLayout/README.md +144 -0
- package/src/layouts/PublicLayout/{components/PublicFooter/PublicFooter.tsx → footers/DefaultFooter/DefaultFooter.tsx} +21 -15
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/DjangoCFGLogo.tsx +0 -6
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterBottom.tsx +3 -7
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterMenuSections.tsx +4 -7
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterProjectInfo.tsx +0 -4
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterSocialLinks.tsx +0 -5
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/index.ts +2 -12
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/types.ts +21 -26
- package/src/layouts/PublicLayout/footers/index.ts +1 -0
- package/src/layouts/PublicLayout/hooks/index.ts +1 -0
- package/src/layouts/PublicLayout/hooks/useResponsiveOverflow.ts +140 -0
- package/src/layouts/PublicLayout/index.ts +38 -20
- package/src/layouts/PublicLayout/navbarTypes.ts +27 -4
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingMobileDrawer.tsx +29 -0
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +127 -0
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushMobileDrawer.tsx +19 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +122 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalMobileDrawer.tsx +19 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +180 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/index.ts +3 -0
- package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +69 -0
- package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +95 -0
- package/src/layouts/PublicLayout/{components → primitives}/NavActions.tsx +26 -1
- package/src/layouts/PublicLayout/{components → primitives}/NavBrand.tsx +4 -3
- package/src/layouts/PublicLayout/{components → primitives}/NavDesktopItems.tsx +105 -61
- package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +81 -0
- package/src/layouts/PublicLayout/{components → primitives}/ThemeBrandMark.tsx +0 -8
- package/src/layouts/PublicLayout/primitives/index.ts +18 -0
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +205 -0
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +295 -0
- package/src/layouts/PublicLayout/shared/index.ts +4 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +0 -211
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +0 -99
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +0 -287
- package/src/layouts/PublicLayout/components/index.ts +0 -11
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavbarShell — internal orchestrator shared by all public navbar variants.
|
|
3
|
+
*
|
|
4
|
+
* Handles the common layer each variant has to wire up anyway:
|
|
5
|
+
* - scroll behavior (hide on scroll, transparent → opaque)
|
|
6
|
+
* - desktop dropdown hover timers
|
|
7
|
+
* - viewport CSS vars (for the mobile drawer anchoring)
|
|
8
|
+
* - active-path detection
|
|
9
|
+
* - layout rows (default / brand-left / centered / split)
|
|
10
|
+
* - context registration (`setNavbarSurface`)
|
|
11
|
+
*
|
|
12
|
+
* Variants pass in their own chrome (outer positioning, shape, background) through
|
|
13
|
+
* class-building render callbacks, then stay thin (just pick defaults, export types).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use client';
|
|
17
|
+
|
|
18
|
+
import React, {
|
|
19
|
+
type ReactNode,
|
|
20
|
+
useEffect,
|
|
21
|
+
useLayoutEffect,
|
|
22
|
+
useMemo,
|
|
23
|
+
useRef,
|
|
24
|
+
} from 'react';
|
|
25
|
+
|
|
26
|
+
import { useAppT } from '@djangocfg/i18n';
|
|
27
|
+
import { useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
28
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
29
|
+
|
|
30
|
+
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
31
|
+
import { usePublicLayoutOptional } from '../context';
|
|
32
|
+
import {
|
|
33
|
+
useDropdownMenu,
|
|
34
|
+
useNavbarScroll,
|
|
35
|
+
useNavbarViewportVars,
|
|
36
|
+
} from '../hooks';
|
|
37
|
+
import type {
|
|
38
|
+
PublicDesktopDropdownRenderer,
|
|
39
|
+
PublicNavbarHeight,
|
|
40
|
+
PublicNavbarPosition,
|
|
41
|
+
PublicNavbarVariant,
|
|
42
|
+
PublicNavLayout,
|
|
43
|
+
} from '../navbarTypes';
|
|
44
|
+
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
45
|
+
|
|
46
|
+
import { NavActions } from '../primitives/NavActions';
|
|
47
|
+
import type { NavAction } from '../primitives/NavActionItem';
|
|
48
|
+
import { NavBrand } from '../primitives/NavBrand';
|
|
49
|
+
import { NavDesktopItems } from '../primitives/NavDesktopItems';
|
|
50
|
+
|
|
51
|
+
const heightCls: Record<PublicNavbarHeight, string> = {
|
|
52
|
+
sm: 'py-2',
|
|
53
|
+
md: 'py-3.5',
|
|
54
|
+
lg: 'py-5',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface ShapeInputs {
|
|
58
|
+
/** true at scrollY >= threshold */
|
|
59
|
+
scrolled: boolean;
|
|
60
|
+
/** Matches `transparent` flag at the source. */
|
|
61
|
+
transparent: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface NavbarShellProps {
|
|
65
|
+
// ── Surface identity (reported to context) ────────────────────────────────
|
|
66
|
+
variant: PublicNavbarVariant;
|
|
67
|
+
position: PublicNavbarPosition;
|
|
68
|
+
|
|
69
|
+
// ── Content ───────────────────────────────────────────────────────────────
|
|
70
|
+
brand?: ReactNode;
|
|
71
|
+
brandHref?: string;
|
|
72
|
+
navigation?: NavigationItem[];
|
|
73
|
+
userMenu?: UserMenuConfig;
|
|
74
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
75
|
+
/**
|
|
76
|
+
* Hard cap for how many items to try to fit on desktop. Useful when a
|
|
77
|
+
* consumer wants to force a compact primary row even if more would fit.
|
|
78
|
+
* Defaults to the full list length (let the responsive measurer decide).
|
|
79
|
+
*/
|
|
80
|
+
desktopMaxPrimaryItems?: number;
|
|
81
|
+
|
|
82
|
+
// ── Layout ────────────────────────────────────────────────────────────────
|
|
83
|
+
navLayout?: PublicNavLayout;
|
|
84
|
+
navbarHeight?: PublicNavbarHeight;
|
|
85
|
+
/** Inner padding for the row container. */
|
|
86
|
+
innerPadding?: string;
|
|
87
|
+
|
|
88
|
+
// ── Scroll behavior ───────────────────────────────────────────────────────
|
|
89
|
+
hideNavOnScroll?: boolean;
|
|
90
|
+
transparent?: boolean;
|
|
91
|
+
transparentThreshold?: number;
|
|
92
|
+
|
|
93
|
+
// ── Chrome (per-variant) ──────────────────────────────────────────────────
|
|
94
|
+
/** Classes on the outer wrapper (fixed/sticky, top offset, outer x-padding). */
|
|
95
|
+
outerClassName?: string;
|
|
96
|
+
/** Classes for the `<nav>` element defining the shape (rounding, border, shadow). */
|
|
97
|
+
shapeClassName?: string;
|
|
98
|
+
/** Surface classes (bg, backdrop-blur). Driven by `shapeForState`. */
|
|
99
|
+
shapeForState?: (inputs: ShapeInputs) => string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Replaces the default actions column (UserMenu + mobile-toggle).
|
|
103
|
+
* Variant owns the mobile toggle too. Receives helpers so it can build the
|
|
104
|
+
* right-hand side however it wants.
|
|
105
|
+
*/
|
|
106
|
+
renderActions?: (ctx: NavbarActionsContext) => ReactNode;
|
|
107
|
+
|
|
108
|
+
// ── Default-actions slots (only used when `renderActions` is NOT provided) ─
|
|
109
|
+
/** Typed CTA pills (Book a demo / Get started / …) before UserMenu. */
|
|
110
|
+
actions?: NavAction[];
|
|
111
|
+
/** Arbitrary ReactNode between actions and UserMenu. */
|
|
112
|
+
actionsLeadingSlot?: ReactNode;
|
|
113
|
+
/** Arbitrary ReactNode after the mobile toggle. */
|
|
114
|
+
actionsTrailingSlot?: ReactNode;
|
|
115
|
+
|
|
116
|
+
// ── Props override (used by variants that proxy ctx differently) ──────────
|
|
117
|
+
mobileMenuOpen?: boolean;
|
|
118
|
+
onMobileMenuToggle?: () => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface NavbarActionsContext {
|
|
122
|
+
userMenu?: UserMenuConfig;
|
|
123
|
+
mobileMenuOpen: boolean;
|
|
124
|
+
toggleMobileMenu: () => void;
|
|
125
|
+
toggleMobileLabel: string;
|
|
126
|
+
navLayout: PublicNavLayout;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function NavbarShell(props: NavbarShellProps) {
|
|
130
|
+
const context = usePublicLayoutOptional();
|
|
131
|
+
|
|
132
|
+
const {
|
|
133
|
+
variant,
|
|
134
|
+
position,
|
|
135
|
+
brand,
|
|
136
|
+
brandHref = '/',
|
|
137
|
+
navigation = [],
|
|
138
|
+
userMenu,
|
|
139
|
+
renderDesktopDropdown,
|
|
140
|
+
desktopMaxPrimaryItems: desktopMaxPrimaryItemsProp,
|
|
141
|
+
navLayout = 'default',
|
|
142
|
+
navbarHeight = 'md',
|
|
143
|
+
innerPadding = 'w-full pl-6 pr-3 sm:px-4 lg:px-6',
|
|
144
|
+
hideNavOnScroll = false,
|
|
145
|
+
transparent = false,
|
|
146
|
+
transparentThreshold = 40,
|
|
147
|
+
outerClassName,
|
|
148
|
+
shapeClassName,
|
|
149
|
+
shapeForState,
|
|
150
|
+
} = props;
|
|
151
|
+
|
|
152
|
+
const desktopMaxPrimaryItems = desktopMaxPrimaryItemsProp
|
|
153
|
+
? Math.max(1, desktopMaxPrimaryItemsProp)
|
|
154
|
+
: undefined;
|
|
155
|
+
const mobileMenuOpen = props.mobileMenuOpen ?? context?.mobileMenuOpen ?? false;
|
|
156
|
+
const toggleMobileMenu = props.onMobileMenuToggle ?? context?.toggleMobileMenu ?? (() => {});
|
|
157
|
+
|
|
158
|
+
const navOuterRef = useRef<HTMLDivElement | null>(null);
|
|
159
|
+
|
|
160
|
+
const { hidden, scrolled } = useNavbarScroll({ hideNavOnScroll, transparent, transparentThreshold });
|
|
161
|
+
const dropdown = useDropdownMenu();
|
|
162
|
+
useNavbarViewportVars(navOuterRef, [position, variant, outerClassName, shapeClassName] as const);
|
|
163
|
+
|
|
164
|
+
const isTabletOrBelow = useIsTabletOrBelow();
|
|
165
|
+
const t = useAppT();
|
|
166
|
+
const pathname = usePathnameWithoutLocale();
|
|
167
|
+
|
|
168
|
+
// Close dropdowns when switching to mobile
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (isTabletOrBelow) dropdown.closeDropdown();
|
|
171
|
+
}, [isTabletOrBelow, dropdown.closeDropdown]);
|
|
172
|
+
|
|
173
|
+
// Sync navbar surface into context
|
|
174
|
+
const setNavbarSurface = context?.setNavbarSurface;
|
|
175
|
+
useLayoutEffect(() => {
|
|
176
|
+
if (!setNavbarSurface) return;
|
|
177
|
+
setNavbarSurface({ variant, position });
|
|
178
|
+
return () => setNavbarSurface(null);
|
|
179
|
+
}, [setNavbarSurface, variant, position]);
|
|
180
|
+
|
|
181
|
+
const toggleMobileLabel = useMemo(() => t('layouts.navigation.toggleMobile'), [t]);
|
|
182
|
+
|
|
183
|
+
const isActivePath = useMemo(() => (href: string) => {
|
|
184
|
+
if (href === '/') return pathname === '/';
|
|
185
|
+
return pathname === href || pathname.startsWith(`${href}/`);
|
|
186
|
+
}, [pathname]);
|
|
187
|
+
|
|
188
|
+
const isGroupActive = useMemo(() => (item: NavigationItem): boolean => {
|
|
189
|
+
if (isActivePath(item.href)) return true;
|
|
190
|
+
return item.items?.some((sub) => isActivePath(sub.href)) ?? false;
|
|
191
|
+
}, [isActivePath]);
|
|
192
|
+
|
|
193
|
+
// Filter lone link duplicating brand (keeps full list in drawer)
|
|
194
|
+
const desktopNavItems = useMemo(
|
|
195
|
+
() => navigation.filter((item) => item.items?.length || item.href !== brandHref),
|
|
196
|
+
[navigation, brandHref],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const desktopItems = desktopNavItems;
|
|
200
|
+
|
|
201
|
+
const outerCls = cn(
|
|
202
|
+
outerClassName,
|
|
203
|
+
'inset-x-0 z-50',
|
|
204
|
+
hideNavOnScroll && 'transition-transform duration-300 ease-in-out will-change-transform',
|
|
205
|
+
hideNavOnScroll && hidden && !mobileMenuOpen && '-translate-y-full',
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const surfaceCls = shapeForState?.({ scrolled, transparent });
|
|
209
|
+
|
|
210
|
+
const brandNode = (
|
|
211
|
+
<div className="min-w-0 shrink-0 flex items-center">
|
|
212
|
+
<NavBrand brand={brand} brandHref={brandHref} />
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const desktopNavNode = navLayout !== 'split' ? (
|
|
217
|
+
<NavDesktopItems
|
|
218
|
+
items={desktopItems}
|
|
219
|
+
maxVisible={desktopMaxPrimaryItems}
|
|
220
|
+
isActivePath={isActivePath}
|
|
221
|
+
isGroupActive={isGroupActive}
|
|
222
|
+
dropdown={dropdown}
|
|
223
|
+
renderDesktopDropdown={renderDesktopDropdown}
|
|
224
|
+
/>
|
|
225
|
+
) : null;
|
|
226
|
+
|
|
227
|
+
const actionsNode = props.renderActions ? (
|
|
228
|
+
props.renderActions({
|
|
229
|
+
userMenu,
|
|
230
|
+
mobileMenuOpen,
|
|
231
|
+
toggleMobileMenu,
|
|
232
|
+
toggleMobileLabel,
|
|
233
|
+
navLayout,
|
|
234
|
+
})
|
|
235
|
+
) : (
|
|
236
|
+
<NavActions
|
|
237
|
+
userMenu={userMenu}
|
|
238
|
+
mobileMenuOpen={mobileMenuOpen}
|
|
239
|
+
onMobileMenuToggle={toggleMobileMenu}
|
|
240
|
+
toggleMobileLabel={toggleMobileLabel}
|
|
241
|
+
forceShowMobileTrigger={navLayout === 'split'}
|
|
242
|
+
actions={props.actions}
|
|
243
|
+
leadingSlot={props.actionsLeadingSlot}
|
|
244
|
+
trailingSlot={props.actionsTrailingSlot}
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const h = heightCls[navbarHeight];
|
|
249
|
+
|
|
250
|
+
const row = (() => {
|
|
251
|
+
switch (navLayout) {
|
|
252
|
+
case 'brand-left':
|
|
253
|
+
return (
|
|
254
|
+
<div className={cn('flex items-center gap-4', h)}>
|
|
255
|
+
<div className="min-w-0 shrink-0 flex items-center">{brandNode}</div>
|
|
256
|
+
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center gap-1">{desktopNavNode}</div>
|
|
257
|
+
<div className="flex shrink-0 items-center gap-4">{actionsNode}</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
case 'centered':
|
|
261
|
+
return (
|
|
262
|
+
<div className={cn('flex items-center gap-4', h)}>
|
|
263
|
+
<div className="shrink-0">{brandNode}</div>
|
|
264
|
+
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center justify-center gap-1">{desktopNavNode}</div>
|
|
265
|
+
<div className="flex shrink-0 items-center">{actionsNode}</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
case 'split':
|
|
269
|
+
return (
|
|
270
|
+
<div className={cn('flex items-center justify-between', h)}>
|
|
271
|
+
{brandNode}
|
|
272
|
+
{actionsNode}
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
default:
|
|
276
|
+
return (
|
|
277
|
+
<div className={cn('flex items-center gap-4', h)}>
|
|
278
|
+
<div className="shrink-0">{brandNode}</div>
|
|
279
|
+
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center justify-center gap-1">
|
|
280
|
+
{desktopNavNode}
|
|
281
|
+
</div>
|
|
282
|
+
<div className="flex shrink-0 items-center">{actionsNode}</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div ref={navOuterRef} className={outerCls}>
|
|
290
|
+
<nav className={cn(shapeClassName, surfaceCls)}>
|
|
291
|
+
<div className={innerPadding}>{row}</div>
|
|
292
|
+
</nav>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public Layout Mobile Drawer
|
|
3
|
-
*
|
|
4
|
-
* Mobile drawer component for PublicLayout navigation
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
9
|
-
import { ArrowRight } from 'lucide-react';
|
|
10
|
-
import Link from 'next/link';
|
|
11
|
-
import React, { useMemo } from 'react';
|
|
12
|
-
|
|
13
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
14
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
15
|
-
import { Button } from '@djangocfg/ui-core/components';
|
|
16
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
|
-
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
18
|
-
|
|
19
|
-
import { UserMenu } from '../../_components/UserMenu';
|
|
20
|
-
import { publicFloatingChromeClassName } from '../publicShellShadow';
|
|
21
|
-
import { usePublicLayoutOptional } from '../context';
|
|
22
|
-
import { useMobileNavPanel } from '../hooks';
|
|
23
|
-
|
|
24
|
-
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
25
|
-
|
|
26
|
-
interface PublicMobileDrawerProps {
|
|
27
|
-
isOpen?: boolean;
|
|
28
|
-
onClose?: () => void;
|
|
29
|
-
navigation?: NavigationItem[];
|
|
30
|
-
userMenu?: UserMenuConfig;
|
|
31
|
-
containerClassName?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Tailwind rounding for the drawer panel. Defaults to `rounded-2xl`.
|
|
34
|
-
* Match `PublicNavigation` `rounding` so bar and sheet align visually.
|
|
35
|
-
*/
|
|
36
|
-
rounding?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
|
|
40
|
-
const context = usePublicLayoutOptional();
|
|
41
|
-
const mobileMenuOpen = props.isOpen ?? context?.mobileMenuOpen ?? false;
|
|
42
|
-
const closeMobileMenu = props.onClose ?? context?.closeMobileMenu ?? (() => {});
|
|
43
|
-
const navigation = props.navigation ?? [];
|
|
44
|
-
const userMenu = props.userMenu;
|
|
45
|
-
const containerClassName = props.containerClassName;
|
|
46
|
-
const rounding = props.rounding;
|
|
47
|
-
const { isAuthenticated, user } = useAuth();
|
|
48
|
-
const pathname = usePathnameWithoutLocale();
|
|
49
|
-
const t = useAppT();
|
|
50
|
-
const { mounted, visible } = useMobileNavPanel({
|
|
51
|
-
isOpen: mobileMenuOpen,
|
|
52
|
-
onClose: closeMobileMenu,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const labels = useMemo(() => ({
|
|
56
|
-
menu: t('layouts.navigation.menu'),
|
|
57
|
-
quickActions: 'Actions',
|
|
58
|
-
signIn: t('layouts.profile.login'),
|
|
59
|
-
}), [t]);
|
|
60
|
-
|
|
61
|
-
const mobileNavigation = useMemo(() => {
|
|
62
|
-
const hasHome = navigation.some((item) => item.href === '/');
|
|
63
|
-
if (hasHome) return navigation;
|
|
64
|
-
return [{ label: 'Home', href: '/' }, ...navigation];
|
|
65
|
-
}, [navigation]);
|
|
66
|
-
|
|
67
|
-
const isActivePath = (href: string) => {
|
|
68
|
-
if (href === '/') return pathname === '/';
|
|
69
|
-
return pathname === href || pathname.startsWith(`${href}/`);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
if (!mounted) return null;
|
|
73
|
-
|
|
74
|
-
const hasSessionUser = Boolean(isAuthenticated && user);
|
|
75
|
-
const showSignInFooter = !hasSessionUser;
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<>
|
|
79
|
-
{mobileMenuOpen && (
|
|
80
|
-
<button
|
|
81
|
-
type="button"
|
|
82
|
-
aria-label={t('layouts.mobile.closeMenu')}
|
|
83
|
-
className="fixed inset-0 z-[998] lg:hidden bg-black/35 transition-opacity duration-200"
|
|
84
|
-
onClick={closeMobileMenu}
|
|
85
|
-
/>
|
|
86
|
-
)}
|
|
87
|
-
{/* Outer shell must not capture taps when the panel is closed: with pointer-events-none on the
|
|
88
|
-
inner panel, events would otherwise hit this transparent fixed layer (z-1000) and block the page. */}
|
|
89
|
-
<div
|
|
90
|
-
className="pointer-events-none fixed inset-x-0 z-1000 lg:hidden px-4 pb-3 sm:px-6 sm:pb-3 lg:px-8"
|
|
91
|
-
style={{
|
|
92
|
-
top: 'var(--public-navbar-mobile-drawer-top, 5rem)',
|
|
93
|
-
bottom: 0,
|
|
94
|
-
}}
|
|
95
|
-
>
|
|
96
|
-
<div
|
|
97
|
-
className={cn(
|
|
98
|
-
'mx-auto flex h-full min-h-0 max-h-full w-full flex-col overflow-hidden bg-background/72 backdrop-blur-[10px] dark:bg-card/80 transform-gpu will-change-transform transition-[transform,opacity] duration-[220ms] ease-out',
|
|
99
|
-
publicFloatingChromeClassName,
|
|
100
|
-
rounding ?? 'rounded-2xl',
|
|
101
|
-
containerClassName,
|
|
102
|
-
visible
|
|
103
|
-
? 'pointer-events-auto opacity-100 translate-y-0 scale-100'
|
|
104
|
-
: 'pointer-events-none opacity-0 -translate-y-2 scale-[0.985]',
|
|
105
|
-
)}
|
|
106
|
-
style={{
|
|
107
|
-
maxHeight: 'min(var(--public-navbar-mobile-drawer-max-height, calc(100dvh - 5rem - 12px)), calc(100dvh - 12px))',
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
110
|
-
{/* Scrollable content */}
|
|
111
|
-
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 pb-10 space-y-5">
|
|
112
|
-
{hasSessionUser && (
|
|
113
|
-
<div className="px-2">
|
|
114
|
-
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
115
|
-
{labels.quickActions}
|
|
116
|
-
</h3>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
{hasSessionUser && (
|
|
121
|
-
<UserMenu variant="mobile" groups={userMenu?.groups} authPath={userMenu?.authPath} i18n={userMenu?.i18n} />
|
|
122
|
-
)}
|
|
123
|
-
|
|
124
|
-
{/* Navigation Items */}
|
|
125
|
-
<div className="space-y-2">
|
|
126
|
-
<div className="px-2">
|
|
127
|
-
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
128
|
-
{labels.menu}
|
|
129
|
-
</h3>
|
|
130
|
-
</div>
|
|
131
|
-
<div className="space-y-1">
|
|
132
|
-
{mobileNavigation.map((item) => {
|
|
133
|
-
const childItems = item.items ?? [];
|
|
134
|
-
const hasChildNav = childItems.length > 0;
|
|
135
|
-
const anyChildActive = hasChildNav && childItems.some((sub) => isActivePath(sub.href));
|
|
136
|
-
const parentPageActive = hasChildNav
|
|
137
|
-
? isActivePath(item.href) && !anyChildActive
|
|
138
|
-
: isActivePath(item.href);
|
|
139
|
-
const parentOnlySectionOpen = hasChildNav && anyChildActive;
|
|
140
|
-
return (
|
|
141
|
-
<div key={item.href}>
|
|
142
|
-
<Link
|
|
143
|
-
href={item.href}
|
|
144
|
-
onClick={closeMobileMenu}
|
|
145
|
-
title={item.label}
|
|
146
|
-
className={cn(
|
|
147
|
-
'block min-h-11 min-w-0 max-w-full rounded-full border-0 px-5 py-3 text-[15px] font-medium transition-colors ring-0 truncate',
|
|
148
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
|
|
149
|
-
parentPageActive
|
|
150
|
-
? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted dark:shadow-none'
|
|
151
|
-
: parentOnlySectionOpen
|
|
152
|
-
? 'font-semibold text-foreground'
|
|
153
|
-
: 'text-foreground hover:bg-accent/60 hover:text-accent-foreground',
|
|
154
|
-
)}
|
|
155
|
-
>
|
|
156
|
-
{item.label}
|
|
157
|
-
</Link>
|
|
158
|
-
{hasChildNav && (
|
|
159
|
-
<div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
|
|
160
|
-
{childItems.map((subItem) => {
|
|
161
|
-
const subActive = isActivePath(subItem.href);
|
|
162
|
-
return (
|
|
163
|
-
<Link
|
|
164
|
-
key={`${item.href}-${subItem.href}`}
|
|
165
|
-
href={subItem.href}
|
|
166
|
-
onClick={closeMobileMenu}
|
|
167
|
-
title={subItem.label}
|
|
168
|
-
className={cn(
|
|
169
|
-
'flex min-h-11 min-w-0 max-w-full items-center rounded-full border-0 px-4 py-2.5 text-sm font-medium transition-colors ring-0 truncate',
|
|
170
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
|
|
171
|
-
subActive
|
|
172
|
-
? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted/90 dark:shadow-none'
|
|
173
|
-
: 'border-0 text-muted-foreground hover:bg-accent/55 hover:text-foreground',
|
|
174
|
-
)}
|
|
175
|
-
>
|
|
176
|
-
{subItem.label}
|
|
177
|
-
</Link>
|
|
178
|
-
);
|
|
179
|
-
})}
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
})}
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
{showSignInFooter && (
|
|
190
|
-
<div className="shrink-0 border-t border-border/50 p-4">
|
|
191
|
-
<Link
|
|
192
|
-
href={userMenu?.authPath || '/auth'}
|
|
193
|
-
onClick={closeMobileMenu}
|
|
194
|
-
className="block"
|
|
195
|
-
>
|
|
196
|
-
<Button className="relative w-full justify-center rounded-full h-11 px-6 pr-12">
|
|
197
|
-
{labels.signIn}
|
|
198
|
-
<ArrowRight
|
|
199
|
-
className="pointer-events-none absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 shrink-0"
|
|
200
|
-
aria-hidden
|
|
201
|
-
/>
|
|
202
|
-
</Button>
|
|
203
|
-
</Link>
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
import { PublicMobileDrawer } from './PublicMobileDrawer';
|
|
6
|
-
import { PublicNavigation } from './PublicNavigation';
|
|
7
|
-
|
|
8
|
-
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
9
|
-
import type { PublicDesktopDropdownRenderer } from './PublicNavigation';
|
|
10
|
-
import type {
|
|
11
|
-
PublicNavbarShellConfig,
|
|
12
|
-
PublicNavbarPosition,
|
|
13
|
-
PublicNavbarVariant,
|
|
14
|
-
PublicNavLayout,
|
|
15
|
-
PublicNavbarHeight,
|
|
16
|
-
} from '../navbarTypes';
|
|
17
|
-
|
|
18
|
-
export interface PublicNavbarConfig {
|
|
19
|
-
shell?: PublicNavbarShellConfig;
|
|
20
|
-
/** Brand: custom React node, or a plain string (wrapped in `<Link href={brandHref}>`). */
|
|
21
|
-
brand?: React.ReactNode;
|
|
22
|
-
/** @default '/' */
|
|
23
|
-
brandHref?: string;
|
|
24
|
-
navigation?: NavigationItem[];
|
|
25
|
-
userMenu?: UserMenuConfig;
|
|
26
|
-
navbarVariant?: PublicNavbarVariant;
|
|
27
|
-
navbarPosition?: PublicNavbarPosition;
|
|
28
|
-
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
29
|
-
desktopMaxPrimaryItems?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Desktop nav arrangement.
|
|
32
|
-
* - `default` — brand left | nav centered | actions right
|
|
33
|
-
* - `brand-left` — brand left | nav after brand | actions pushed right
|
|
34
|
-
* - `centered` — all items centered in one row
|
|
35
|
-
* - `split` — brand left | actions right | no desktop nav (drawer only)
|
|
36
|
-
* @default 'default'
|
|
37
|
-
*/
|
|
38
|
-
navLayout?: PublicNavLayout;
|
|
39
|
-
/**
|
|
40
|
-
* Navbar vertical padding / height.
|
|
41
|
-
* - `sm` → compact
|
|
42
|
-
* - `md` → default
|
|
43
|
-
* - `lg` → tall
|
|
44
|
-
* @default 'md'
|
|
45
|
-
*/
|
|
46
|
-
navbarHeight?: PublicNavbarHeight;
|
|
47
|
-
/**
|
|
48
|
-
* Slide navbar off-screen on scroll-down; restore on scroll-up.
|
|
49
|
-
* @default false
|
|
50
|
-
*/
|
|
51
|
-
hideNavOnScroll?: boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Transparent at page top, opaque after scrolling.
|
|
54
|
-
* Pair with `navbarVariant="floating"` for best results.
|
|
55
|
-
* @default false
|
|
56
|
-
*/
|
|
57
|
-
transparent?: boolean;
|
|
58
|
-
/**
|
|
59
|
-
* scrollY threshold (px) for transparent → opaque transition.
|
|
60
|
-
* @default 40
|
|
61
|
-
*/
|
|
62
|
-
transparentThreshold?: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface PublicNavbarProps {
|
|
66
|
-
config: PublicNavbarConfig;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function PublicNavbar({ config }: PublicNavbarProps) {
|
|
70
|
-
const navigation = config.navigation ?? [];
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<>
|
|
74
|
-
<PublicNavigation
|
|
75
|
-
brand={config.brand}
|
|
76
|
-
brandHref={config.brandHref}
|
|
77
|
-
navigation={navigation}
|
|
78
|
-
userMenu={config.userMenu}
|
|
79
|
-
containerClassName={config.shell?.className}
|
|
80
|
-
navbarVariant={config.navbarVariant}
|
|
81
|
-
navbarPosition={config.navbarPosition}
|
|
82
|
-
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
83
|
-
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
84
|
-
rounding={config.shell?.rounding}
|
|
85
|
-
navLayout={config.navLayout}
|
|
86
|
-
navbarHeight={config.navbarHeight}
|
|
87
|
-
hideNavOnScroll={config.hideNavOnScroll}
|
|
88
|
-
transparent={config.transparent}
|
|
89
|
-
transparentThreshold={config.transparentThreshold}
|
|
90
|
-
/>
|
|
91
|
-
<PublicMobileDrawer
|
|
92
|
-
navigation={navigation}
|
|
93
|
-
userMenu={config.userMenu}
|
|
94
|
-
containerClassName={config.shell?.className}
|
|
95
|
-
rounding={config.shell?.rounding}
|
|
96
|
-
/>
|
|
97
|
-
</>
|
|
98
|
-
);
|
|
99
|
-
}
|