@djangocfg/layouts 2.1.248 → 2.1.249
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/package.json +18 -18
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +59 -35
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +140 -27
- package/src/layouts/PublicLayout/context.tsx +4 -0
- package/src/layouts/PublicLayout/index.ts +2 -0
- package/src/layouts/types/ui.types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.249",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/i18n": "^2.1.
|
|
80
|
-
"@djangocfg/monitor": "^2.1.
|
|
81
|
-
"@djangocfg/debuger": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
83
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
84
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.249",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.249",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.249",
|
|
80
|
+
"@djangocfg/monitor": "^2.1.249",
|
|
81
|
+
"@djangocfg/debuger": "^2.1.249",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.249",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.249",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.249",
|
|
85
85
|
"@hookform/resolvers": "^5.2.2",
|
|
86
86
|
"consola": "^3.4.2",
|
|
87
87
|
"lucide-react": "^0.545.0",
|
|
@@ -109,15 +109,15 @@
|
|
|
109
109
|
"uuid": "^11.1.0"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
|
-
"@djangocfg/api": "^2.1.
|
|
113
|
-
"@djangocfg/i18n": "^2.1.
|
|
114
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
115
|
-
"@djangocfg/monitor": "^2.1.
|
|
116
|
-
"@djangocfg/debuger": "^2.1.
|
|
117
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
118
|
-
"@djangocfg/ui-core": "^2.1.
|
|
119
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
120
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
112
|
+
"@djangocfg/api": "^2.1.249",
|
|
113
|
+
"@djangocfg/i18n": "^2.1.249",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.249",
|
|
115
|
+
"@djangocfg/monitor": "^2.1.249",
|
|
116
|
+
"@djangocfg/debuger": "^2.1.249",
|
|
117
|
+
"@djangocfg/typescript-config": "^2.1.249",
|
|
118
|
+
"@djangocfg/ui-core": "^2.1.249",
|
|
119
|
+
"@djangocfg/ui-nextjs": "^2.1.249",
|
|
120
|
+
"@djangocfg/ui-tools": "^2.1.249",
|
|
121
121
|
"@types/node": "^24.7.2",
|
|
122
122
|
"@types/react": "^19.1.0",
|
|
123
123
|
"@types/react-dom": "^19.1.0",
|
|
@@ -15,17 +15,26 @@ import { useAppT } from '@djangocfg/i18n';
|
|
|
15
15
|
import { Button } from '@djangocfg/ui-core/components';
|
|
16
16
|
|
|
17
17
|
import { UserMenu } from '../../_components/UserMenu';
|
|
18
|
-
import {
|
|
18
|
+
import { usePublicLayoutOptional } from '../context';
|
|
19
19
|
import { useFloatingPanel } from '../hooks';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
22
|
+
|
|
23
|
+
interface PublicMobileDrawerProps {
|
|
24
|
+
isOpen?: boolean;
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
navigation?: NavigationItem[];
|
|
27
|
+
userMenu?: UserMenuConfig;
|
|
28
|
+
containerClassName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
|
|
32
|
+
const context = usePublicLayoutOptional();
|
|
33
|
+
const mobileMenuOpen = props.isOpen ?? context?.mobileMenuOpen ?? false;
|
|
34
|
+
const closeMobileMenu = props.onClose ?? context?.closeMobileMenu ?? (() => {});
|
|
35
|
+
const navigation = props.navigation ?? context?.navigation ?? [];
|
|
36
|
+
const userMenu = props.userMenu ?? context?.userMenu;
|
|
37
|
+
const containerClassName = props.containerClassName ?? context?.containerClassName;
|
|
29
38
|
const { isAuthenticated } = useAuth();
|
|
30
39
|
const t = useAppT();
|
|
31
40
|
const { isRendered, isActive, onTransitionEnd } = useFloatingPanel({
|
|
@@ -54,15 +63,15 @@ export function PublicMobileDrawer() {
|
|
|
54
63
|
<div className="fixed inset-x-0 top-20 z-1000 lg:hidden px-4 sm:px-6 lg:px-8">
|
|
55
64
|
<div
|
|
56
65
|
onTransitionEnd={onTransitionEnd}
|
|
57
|
-
className={`mx-auto w-full rounded-2xl border border-border/60 bg-background/95 backdrop-blur-xl shadow-2xl overflow-hidden transform-gpu will-change-transform transition-[transform,opacity] duration-[220ms] ease-out ${containerClassName || ''} ${
|
|
66
|
+
className={`mx-auto w-full max-h-[80vh] rounded-2xl border border-border/60 bg-background/95 backdrop-blur-xl shadow-2xl overflow-hidden flex flex-col transform-gpu will-change-transform transition-[transform,opacity] duration-[220ms] ease-out ${containerClassName || ''} ${
|
|
58
67
|
isActive
|
|
59
68
|
? 'opacity-100 translate-y-0 scale-100'
|
|
60
69
|
: 'opacity-0 -translate-y-2 scale-[0.985] pointer-events-none'
|
|
61
70
|
}`}
|
|
62
71
|
style={{ backgroundColor: 'hsl(var(--background) / 0.72)', backdropFilter: 'blur(10px)' }}
|
|
63
72
|
>
|
|
64
|
-
{/*
|
|
65
|
-
<div className="
|
|
73
|
+
{/* Scrollable content */}
|
|
74
|
+
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-5">
|
|
66
75
|
{isAuthenticated && (
|
|
67
76
|
<div className="px-2">
|
|
68
77
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
@@ -84,33 +93,48 @@ export function PublicMobileDrawer() {
|
|
|
84
93
|
</div>
|
|
85
94
|
<div className="space-y-1">
|
|
86
95
|
{navigation.map((item) => (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
<div key={item.href}>
|
|
97
|
+
<Link
|
|
98
|
+
href={item.href}
|
|
99
|
+
onClick={closeMobileMenu}
|
|
100
|
+
className="block px-3 py-2.5 rounded-lg text-[15px] font-medium transition-colors text-foreground hover:bg-accent/70 hover:text-accent-foreground"
|
|
101
|
+
>
|
|
102
|
+
{item.label}
|
|
103
|
+
</Link>
|
|
104
|
+
{item.items && item.items.length > 0 && (
|
|
105
|
+
<div className="ml-3 mt-1 space-y-1 border-l border-border/40 pl-3">
|
|
106
|
+
{item.items.map((subItem) => (
|
|
107
|
+
<Link
|
|
108
|
+
key={`${item.href}-${subItem.href}`}
|
|
109
|
+
href={subItem.href}
|
|
110
|
+
onClick={closeMobileMenu}
|
|
111
|
+
className="block px-2 py-2 rounded-md text-sm text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
|
|
112
|
+
>
|
|
113
|
+
{subItem.label}
|
|
114
|
+
</Link>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
95
119
|
))}
|
|
96
120
|
</div>
|
|
97
121
|
</div>
|
|
98
|
-
|
|
99
|
-
{!isAuthenticated && (
|
|
100
|
-
<div className="pt-4 border-t border-border/50">
|
|
101
|
-
<Link
|
|
102
|
-
href={userMenu?.authPath || '/auth'}
|
|
103
|
-
onClick={closeMobileMenu}
|
|
104
|
-
className="block"
|
|
105
|
-
>
|
|
106
|
-
<Button className="w-full justify-between rounded-lg h-11">
|
|
107
|
-
<span>{labels.signIn}</span>
|
|
108
|
-
<ArrowRight className="h-4 w-4" />
|
|
109
|
-
</Button>
|
|
110
|
-
</Link>
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
122
|
</div>
|
|
123
|
+
|
|
124
|
+
{!isAuthenticated && (
|
|
125
|
+
<div className="shrink-0 border-t border-border/50 p-4">
|
|
126
|
+
<Link
|
|
127
|
+
href={userMenu?.authPath || '/auth'}
|
|
128
|
+
onClick={closeMobileMenu}
|
|
129
|
+
className="block"
|
|
130
|
+
>
|
|
131
|
+
<Button className="w-full justify-between rounded-lg h-11">
|
|
132
|
+
<span>{labels.signIn}</span>
|
|
133
|
+
<ArrowRight className="h-4 w-4" />
|
|
134
|
+
</Button>
|
|
135
|
+
</Link>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
114
138
|
</div>
|
|
115
139
|
</div>
|
|
116
140
|
</>
|
|
@@ -6,36 +6,91 @@
|
|
|
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, { 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 {
|
|
15
|
+
import {
|
|
16
|
+
Button,
|
|
17
|
+
} from '@djangocfg/ui-core/components';
|
|
16
18
|
// useIsMobile is used for conditional rendering
|
|
17
19
|
import { useIsMobile } from '@djangocfg/ui-core/hooks';
|
|
18
20
|
// cn is reserved for future conditional styling
|
|
19
21
|
import { cn as _cn } from '@djangocfg/ui-core/lib';
|
|
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
|
+
interface PublicNavigationProps {
|
|
29
|
+
logo?: string;
|
|
30
|
+
siteName?: string;
|
|
31
|
+
navigation?: NavigationItem[];
|
|
32
|
+
userMenu?: UserMenuConfig;
|
|
33
|
+
containerClassName?: string;
|
|
34
|
+
mobileMenuOpen?: boolean;
|
|
35
|
+
onMobileMenuToggle?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function PublicNavigation(props: PublicNavigationProps = {}) {
|
|
39
|
+
const context = usePublicLayoutOptional();
|
|
40
|
+
const logo = props.logo ?? context?.logo;
|
|
41
|
+
const siteName = props.siteName ?? context?.siteName ?? 'App';
|
|
42
|
+
const navigation = props.navigation ?? context?.navigation ?? [];
|
|
43
|
+
const userMenu = props.userMenu ?? context?.userMenu;
|
|
44
|
+
const containerClassName = props.containerClassName ?? context?.containerClassName;
|
|
45
|
+
const mobileMenuOpen = props.mobileMenuOpen ?? context?.mobileMenuOpen ?? false;
|
|
46
|
+
const toggleMobileMenu = props.onMobileMenuToggle ?? context?.toggleMobileMenu ?? (() => {});
|
|
34
47
|
const { isAuthenticated: _isAuthenticated } = useAuth();
|
|
35
48
|
const isMobile = useIsMobile();
|
|
36
49
|
const t = useAppT();
|
|
50
|
+
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null);
|
|
51
|
+
const openTimerRef = useRef<number | null>(null);
|
|
52
|
+
const closeTimerRef = useRef<number | null>(null);
|
|
37
53
|
|
|
38
54
|
const toggleMobileLabel = useMemo(() => t('layouts.navigation.toggleMobile'), [t]);
|
|
55
|
+
const desktopNavItemClass =
|
|
56
|
+
'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';
|
|
57
|
+
|
|
58
|
+
const clearOpenTimer = () => {
|
|
59
|
+
if (openTimerRef.current) {
|
|
60
|
+
window.clearTimeout(openTimerRef.current);
|
|
61
|
+
openTimerRef.current = null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const clearCloseTimer = () => {
|
|
66
|
+
if (closeTimerRef.current) {
|
|
67
|
+
window.clearTimeout(closeTimerRef.current);
|
|
68
|
+
closeTimerRef.current = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const scheduleOpen = (key: string) => {
|
|
73
|
+
clearOpenTimer();
|
|
74
|
+
clearCloseTimer();
|
|
75
|
+
openTimerRef.current = window.setTimeout(() => {
|
|
76
|
+
setOpenDropdownKey(key);
|
|
77
|
+
}, 80);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const scheduleClose = (key: string) => {
|
|
81
|
+
clearOpenTimer();
|
|
82
|
+
clearCloseTimer();
|
|
83
|
+
closeTimerRef.current = window.setTimeout(() => {
|
|
84
|
+
setOpenDropdownKey((prev) => (prev === key ? null : prev));
|
|
85
|
+
}, 120);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return () => {
|
|
90
|
+
clearOpenTimer();
|
|
91
|
+
clearCloseTimer();
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
39
94
|
|
|
40
95
|
const navClass = 'sticky top-3 z-50 px-4 sm:px-6 lg:px-8';
|
|
41
96
|
|
|
@@ -65,28 +120,86 @@ export function PublicNavigation() {
|
|
|
65
120
|
{logo && (
|
|
66
121
|
<img src={logo} alt={siteName} className="h-6 w-auto object-contain" />
|
|
67
122
|
)}
|
|
68
|
-
<span className="font-bold text-
|
|
123
|
+
<span className="font-bold text-base">{siteName}</span>
|
|
69
124
|
</div>
|
|
70
125
|
) : (
|
|
71
126
|
<Link href="/" className="flex items-center gap-2">
|
|
72
127
|
{logo && (
|
|
73
128
|
<img src={logo} alt={siteName} className="h-6 w-auto object-contain" />
|
|
74
129
|
)}
|
|
75
|
-
<span className="font-bold text-
|
|
130
|
+
<span className="font-bold text-base">{siteName}</span>
|
|
76
131
|
</Link>
|
|
77
132
|
)}
|
|
78
133
|
|
|
79
134
|
{/* Desktop Navigation */}
|
|
80
|
-
<div className="hidden md:flex items-center gap-
|
|
81
|
-
{navigation.map((item) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
<div className="hidden md:flex items-center gap-3">
|
|
136
|
+
{navigation.map((item) => {
|
|
137
|
+
if (item.items && item.items.length > 0) {
|
|
138
|
+
const dropdownKey = `${item.label}-${item.href}`;
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={dropdownKey}
|
|
142
|
+
className="relative"
|
|
143
|
+
onMouseEnter={() => scheduleOpen(dropdownKey)}
|
|
144
|
+
onMouseLeave={() => scheduleClose(dropdownKey)}
|
|
145
|
+
>
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="sm"
|
|
149
|
+
className={`group ${desktopNavItemClass} ${openDropdownKey === dropdownKey ? 'bg-accent/50 text-foreground' : ''}`}
|
|
150
|
+
>
|
|
151
|
+
<span>{item.label}</span>
|
|
152
|
+
<ChevronDown
|
|
153
|
+
className={`ml-1 h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${openDropdownKey === dropdownKey ? 'rotate-180' : ''}`}
|
|
154
|
+
/>
|
|
155
|
+
</Button>
|
|
156
|
+
|
|
157
|
+
{openDropdownKey === dropdownKey && (
|
|
158
|
+
<div
|
|
159
|
+
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)]"
|
|
160
|
+
onMouseEnter={() => {
|
|
161
|
+
clearOpenTimer();
|
|
162
|
+
clearCloseTimer();
|
|
163
|
+
}}
|
|
164
|
+
onMouseLeave={() => scheduleClose(dropdownKey)}
|
|
165
|
+
>
|
|
166
|
+
{item.items.map((subItem) => (
|
|
167
|
+
<div key={`${item.label}-${subItem.href}`} className="rounded-md">
|
|
168
|
+
{subItem.external ? (
|
|
169
|
+
<a
|
|
170
|
+
href={subItem.href}
|
|
171
|
+
target="_blank"
|
|
172
|
+
rel="noopener noreferrer"
|
|
173
|
+
className="block rounded-md px-2.5 py-2 text-sm font-medium hover:bg-accent/40"
|
|
174
|
+
>
|
|
175
|
+
{subItem.label}
|
|
176
|
+
</a>
|
|
177
|
+
) : (
|
|
178
|
+
<Link
|
|
179
|
+
href={subItem.href}
|
|
180
|
+
className="block rounded-md px-2.5 py-2 text-sm font-medium hover:bg-accent/40"
|
|
181
|
+
>
|
|
182
|
+
{subItem.label}
|
|
183
|
+
</Link>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Link
|
|
195
|
+
key={item.href}
|
|
196
|
+
href={item.href}
|
|
197
|
+
className={desktopNavItemClass}
|
|
198
|
+
>
|
|
199
|
+
{item.label}
|
|
200
|
+
</Link>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
90
203
|
</div>
|
|
91
204
|
|
|
92
205
|
{/* User Menu / Actions */}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export { PublicLayout } from './PublicLayout';
|
|
6
6
|
export type { PublicLayoutProps } from './PublicLayout';
|
|
7
|
+
export { PublicNavigation, PublicMobileDrawer } from './components';
|
|
7
8
|
export {
|
|
8
9
|
PublicFooter,
|
|
9
10
|
FooterProjectInfo,
|
|
@@ -15,4 +16,5 @@ export {
|
|
|
15
16
|
export type {
|
|
16
17
|
PublicFooterProps,
|
|
17
18
|
} from './components/PublicFooter';
|
|
19
|
+
export { PublicLayoutProvider, usePublicLayout } from './context';
|
|
18
20
|
|