@djangocfg/layouts 2.1.266 → 2.1.267
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 +73 -3
- package/package.json +18 -18
- package/src/hooks/index.ts +1 -1
- package/src/hooks/usePathnameWithoutLocale.ts +35 -19
- package/src/layouts/AppLayout/AppLayout.tsx +15 -4
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +24 -13
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +22 -106
- package/src/layouts/ProfileLayout/context.tsx +2 -10
- package/src/layouts/PublicLayout/PublicLayout.tsx +18 -0
- package/src/layouts/PublicLayout/components/NavActions.tsx +50 -0
- package/src/layouts/PublicLayout/components/NavBrand.tsx +26 -0
- package/src/layouts/PublicLayout/components/NavDesktopItems.tsx +207 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +44 -6
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +199 -396
- package/src/layouts/PublicLayout/hooks/index.ts +5 -1
- package/src/layouts/PublicLayout/hooks/useDropdownMenu.ts +58 -0
- package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +61 -0
- package/src/layouts/PublicLayout/hooks/useNavbarViewportVars.ts +46 -0
- package/src/layouts/PublicLayout/index.ts +4 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +17 -0
- package/src/utils/pathMatcher.ts +6 -3
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
8
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
|
+
|
|
10
|
+
import type { NavigationItem } from '../../types';
|
|
11
|
+
import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
|
|
12
|
+
import type { PublicDesktopDropdownRenderer } from './PublicNavigation';
|
|
13
|
+
|
|
14
|
+
interface NavDesktopItemsProps {
|
|
15
|
+
primaryItems: NavigationItem[];
|
|
16
|
+
overflowItems: NavigationItem[];
|
|
17
|
+
isActivePath: (href: string) => boolean;
|
|
18
|
+
isGroupActive: (item: NavigationItem) => boolean;
|
|
19
|
+
dropdown: UseDropdownMenuReturn;
|
|
20
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const navItemCls = cn(
|
|
24
|
+
'inline-flex min-h-9 items-center justify-center gap-1 rounded-full border-0 px-4 py-1.5 text-sm font-medium',
|
|
25
|
+
'ring-0 focus-visible:ring-0',
|
|
26
|
+
'text-foreground/90 transition-colors',
|
|
27
|
+
'hover:bg-accent/55 hover:text-foreground',
|
|
28
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const navItemActiveCls =
|
|
32
|
+
'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted dark:shadow-none';
|
|
33
|
+
|
|
34
|
+
const labelCls = 'min-w-0 max-w-[11rem] truncate sm:max-w-[13rem]';
|
|
35
|
+
|
|
36
|
+
function subMenuLinkCls(active: boolean) {
|
|
37
|
+
return cn(
|
|
38
|
+
'flex min-h-9 min-w-0 max-w-[min(17rem,calc(100vw-5rem))] items-center rounded-full border-0 px-4 py-2 text-sm font-medium transition-colors',
|
|
39
|
+
'hover:bg-accent/55',
|
|
40
|
+
active
|
|
41
|
+
? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted/90 dark:shadow-none'
|
|
42
|
+
: 'border-0 text-foreground/90',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const popoverCls =
|
|
47
|
+
'absolute left-0 top-full mt-1 z-[1200] min-w-[14.5rem] rounded-xl border border-border/70 bg-background/95 backdrop-blur-sm p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_6px_18px_rgba(0,0,0,0.035)] dark:shadow-[0_6px_20px_rgba(0,0,0,0.12)]';
|
|
48
|
+
|
|
49
|
+
export function NavDesktopItems({
|
|
50
|
+
primaryItems,
|
|
51
|
+
overflowItems,
|
|
52
|
+
isActivePath,
|
|
53
|
+
isGroupActive,
|
|
54
|
+
dropdown,
|
|
55
|
+
renderDesktopDropdown,
|
|
56
|
+
}: NavDesktopItemsProps) {
|
|
57
|
+
const { openDropdownKey, scheduleOpen, scheduleClose, closeDropdown } = dropdown;
|
|
58
|
+
|
|
59
|
+
const renderItem = (item: NavigationItem) => {
|
|
60
|
+
if (item.items && item.items.length > 0) {
|
|
61
|
+
const key = `${item.label}-${item.href}`;
|
|
62
|
+
const isOpen = openDropdownKey === key;
|
|
63
|
+
const isActive = isGroupActive(item);
|
|
64
|
+
|
|
65
|
+
const defaultItems = (
|
|
66
|
+
<>
|
|
67
|
+
{item.items.map((sub) => {
|
|
68
|
+
const subActive = isActivePath(sub.href);
|
|
69
|
+
return (
|
|
70
|
+
<div key={`${item.label}-${sub.href}`} className="rounded-full">
|
|
71
|
+
{sub.external ? (
|
|
72
|
+
<a
|
|
73
|
+
href={sub.href}
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
className={subMenuLinkCls(subActive)}
|
|
77
|
+
>
|
|
78
|
+
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
79
|
+
</a>
|
|
80
|
+
) : (
|
|
81
|
+
<Link href={sub.href} className={subMenuLinkCls(subActive)}>
|
|
82
|
+
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
83
|
+
</Link>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const defaultPopover = (
|
|
92
|
+
<div
|
|
93
|
+
className={popoverCls}
|
|
94
|
+
onMouseEnter={() => { scheduleOpen(key); }}
|
|
95
|
+
onMouseLeave={() => scheduleClose(key)}
|
|
96
|
+
>
|
|
97
|
+
{defaultItems}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
key={key}
|
|
104
|
+
className="relative"
|
|
105
|
+
onMouseEnter={() => scheduleOpen(key)}
|
|
106
|
+
onMouseLeave={() => scheduleClose(key)}
|
|
107
|
+
>
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="sm"
|
|
111
|
+
className={cn(
|
|
112
|
+
'group h-auto min-h-9 max-w-[15rem] gap-1 shadow-none [&_svg]:size-3.5 [&_svg]:shrink-0',
|
|
113
|
+
navItemCls,
|
|
114
|
+
(isOpen || isActive) && navItemActiveCls,
|
|
115
|
+
isOpen && 'border-0 dark:border dark:border-border',
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<span className={labelCls} title={item.label}>{item.label}</span>
|
|
119
|
+
<span className="inline-flex size-3.5 shrink-0 items-center justify-center" aria-hidden>
|
|
120
|
+
<ChevronDown
|
|
121
|
+
className={cn(
|
|
122
|
+
'size-3.5 origin-center text-muted-foreground transition-transform duration-200 ease-out will-change-transform',
|
|
123
|
+
isOpen && 'rotate-180',
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
</span>
|
|
127
|
+
</Button>
|
|
128
|
+
|
|
129
|
+
{isOpen && (
|
|
130
|
+
renderDesktopDropdown
|
|
131
|
+
? renderDesktopDropdown({ item, isOpen, isActive, close: closeDropdown, defaultPopover, defaultItems })
|
|
132
|
+
: defaultPopover
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const active = isActivePath(item.href);
|
|
139
|
+
return (
|
|
140
|
+
<Link
|
|
141
|
+
key={item.href}
|
|
142
|
+
href={item.href}
|
|
143
|
+
className={cn(navItemCls, active && navItemActiveCls)}
|
|
144
|
+
>
|
|
145
|
+
<span className={labelCls} title={item.label}>{item.label}</span>
|
|
146
|
+
</Link>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const hasOverflow = overflowItems.length > 0;
|
|
151
|
+
const isMoreOpen = openDropdownKey === '__overflow-more';
|
|
152
|
+
const moreActive = isMoreOpen || overflowItems.some((i) => isGroupActive(i));
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
{primaryItems.map(renderItem)}
|
|
157
|
+
|
|
158
|
+
{hasOverflow && (
|
|
159
|
+
<div
|
|
160
|
+
className="relative"
|
|
161
|
+
onMouseEnter={() => scheduleOpen('__overflow-more')}
|
|
162
|
+
onMouseLeave={() => scheduleClose('__overflow-more')}
|
|
163
|
+
>
|
|
164
|
+
<Button
|
|
165
|
+
variant="ghost"
|
|
166
|
+
size="sm"
|
|
167
|
+
className={cn(
|
|
168
|
+
'group h-auto min-h-9 max-w-[15rem] gap-1 shadow-none [&_svg]:size-3.5 [&_svg]:shrink-0',
|
|
169
|
+
navItemCls,
|
|
170
|
+
moreActive && navItemActiveCls,
|
|
171
|
+
isMoreOpen && 'border-0 dark:border dark:border-border',
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<span className={labelCls}>More</span>
|
|
175
|
+
<span className="inline-flex size-3.5 shrink-0 items-center justify-center" aria-hidden>
|
|
176
|
+
<ChevronDown
|
|
177
|
+
className={cn(
|
|
178
|
+
'size-3.5 origin-center text-muted-foreground transition-transform duration-200 ease-out will-change-transform',
|
|
179
|
+
isMoreOpen && 'rotate-180',
|
|
180
|
+
)}
|
|
181
|
+
/>
|
|
182
|
+
</span>
|
|
183
|
+
</Button>
|
|
184
|
+
|
|
185
|
+
{isMoreOpen && (
|
|
186
|
+
<div
|
|
187
|
+
className={cn(popoverCls, 'left-auto right-0')}
|
|
188
|
+
onMouseEnter={() => { scheduleOpen('__overflow-more'); }}
|
|
189
|
+
onMouseLeave={() => scheduleClose('__overflow-more')}
|
|
190
|
+
>
|
|
191
|
+
{overflowItems.map((item) => {
|
|
192
|
+
const active = isGroupActive(item);
|
|
193
|
+
return (
|
|
194
|
+
<div key={`overflow-${item.href}`} className="rounded-full">
|
|
195
|
+
<Link href={item.href} className={subMenuLinkCls(active)}>
|
|
196
|
+
<span className="min-w-0 truncate" title={item.label}>{item.label}</span>
|
|
197
|
+
</Link>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -108,7 +108,7 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
|
|
|
108
108
|
}}
|
|
109
109
|
>
|
|
110
110
|
{/* Scrollable content */}
|
|
111
|
-
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-5">
|
|
111
|
+
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 pb-10 space-y-5">
|
|
112
112
|
{hasSessionUser && (
|
|
113
113
|
<div className="px-2">
|
|
114
114
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
PublicNavbarShellConfig,
|
|
12
12
|
PublicNavbarPosition,
|
|
13
13
|
PublicNavbarVariant,
|
|
14
|
+
PublicNavLayout,
|
|
15
|
+
PublicNavbarHeight,
|
|
14
16
|
} from '../navbarTypes';
|
|
15
17
|
|
|
16
18
|
export interface PublicNavbarConfig {
|
|
@@ -25,6 +27,39 @@ export interface PublicNavbarConfig {
|
|
|
25
27
|
navbarPosition?: PublicNavbarPosition;
|
|
26
28
|
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
27
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;
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
export interface PublicNavbarProps {
|
|
@@ -32,8 +67,6 @@ export interface PublicNavbarProps {
|
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
export function PublicNavbar({ config }: PublicNavbarProps) {
|
|
35
|
-
const rounding = config.shell?.rounding;
|
|
36
|
-
const containerClassName = config.shell?.className;
|
|
37
70
|
const navigation = config.navigation ?? [];
|
|
38
71
|
|
|
39
72
|
return (
|
|
@@ -43,18 +76,23 @@ export function PublicNavbar({ config }: PublicNavbarProps) {
|
|
|
43
76
|
brandHref={config.brandHref}
|
|
44
77
|
navigation={navigation}
|
|
45
78
|
userMenu={config.userMenu}
|
|
46
|
-
containerClassName={
|
|
79
|
+
containerClassName={config.shell?.className}
|
|
47
80
|
navbarVariant={config.navbarVariant}
|
|
48
81
|
navbarPosition={config.navbarPosition}
|
|
49
82
|
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
50
83
|
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
51
|
-
rounding={rounding}
|
|
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}
|
|
52
90
|
/>
|
|
53
91
|
<PublicMobileDrawer
|
|
54
92
|
navigation={navigation}
|
|
55
93
|
userMenu={config.userMenu}
|
|
56
|
-
containerClassName={
|
|
57
|
-
rounding={rounding}
|
|
94
|
+
containerClassName={config.shell?.className}
|
|
95
|
+
rounding={config.shell?.rounding}
|
|
58
96
|
/>
|
|
59
97
|
</>
|
|
60
98
|
);
|