@djangocfg/layouts 1.2.1 → 1.2.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Layout system and components for Unrealon applications",
5
5
  "author": {
6
6
  "name": "DjangoCFG",
@@ -53,9 +53,9 @@
53
53
  "check": "tsc --noEmit"
54
54
  },
55
55
  "peerDependencies": {
56
- "@djangocfg/api": "^1.2.1",
57
- "@djangocfg/og-image": "^1.2.1",
58
- "@djangocfg/ui": "^1.2.1",
56
+ "@djangocfg/api": "^1.2.2",
57
+ "@djangocfg/og-image": "^1.2.2",
58
+ "@djangocfg/ui": "^1.2.2",
59
59
  "@hookform/resolvers": "^5.2.0",
60
60
  "consola": "^3.4.2",
61
61
  "lucide-react": "^0.468.0",
@@ -76,7 +76,7 @@
76
76
  "vidstack": "0.6.15"
77
77
  },
78
78
  "devDependencies": {
79
- "@djangocfg/typescript-config": "^1.2.1",
79
+ "@djangocfg/typescript-config": "^1.2.2",
80
80
  "@types/node": "^24.7.2",
81
81
  "@types/react": "19.2.2",
82
82
  "@types/react-dom": "19.2.1",
@@ -51,6 +51,12 @@ export interface AppLayoutProps {
51
51
  * @example fontFamily="Inter, sans-serif"
52
52
  */
53
53
  fontFamily?: string;
54
+ /**
55
+ * Show package versions button in sidebar footer
56
+ * @default false
57
+ * @example showPackageVersions={true}
58
+ */
59
+ showPackageVersions?: boolean;
54
60
  }
55
61
 
56
62
  /**
@@ -115,6 +121,9 @@ function LayoutRouter({
115
121
 
116
122
  // Auth routes: render inside AuthLayout
117
123
  case 'auth':
124
+ // Check if we're on a private route that requires auth
125
+ const isPrivateRoute = config.routes.detectors.isPrivateRoute(router.pathname);
126
+
118
127
  return (
119
128
  <AuthLayout
120
129
  termsUrl={config.auth?.termsUrl}
@@ -122,7 +131,8 @@ function LayoutRouter({
122
131
  supportUrl={config.auth?.supportUrl}
123
132
  enablePhoneAuth={config.auth?.enablePhoneAuth}
124
133
  >
125
- {children}
134
+ {/* Don't render children if redirected from private route */}
135
+ {!isPrivateRoute && children}
126
136
  </AuthLayout>
127
137
  );
128
138
 
@@ -136,6 +146,9 @@ function LayoutRouter({
136
146
  );
137
147
  }
138
148
  return <PrivateLayout>{children}</PrivateLayout>;
149
+
150
+ default:
151
+ return <PublicLayout>{children}</PublicLayout>;
139
152
  }
140
153
  }
141
154
 
@@ -168,7 +181,7 @@ function LayoutRouter({
168
181
  * </AppLayout>
169
182
  * ```
170
183
  */
171
- export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily }: AppLayoutProps) {
184
+ export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
172
185
  const router = useRouter();
173
186
 
174
187
  // Check if ErrorBoundary is enabled (default: true)
@@ -186,7 +199,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
186
199
  )}
187
200
 
188
201
  <CoreProviders config={config}>
189
- <AppContextProvider config={config}>
202
+ <AppContextProvider config={config} showPackageVersions={showPackageVersions}>
190
203
  {/* SEO Meta Tags */}
191
204
  <Seo
192
205
  pageConfig={{
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Package Versions Display
3
+ *
4
+ * Shows all @djangocfg packages versions in a popover
5
+ * Works in both sidebar (PrivateLayout) and footer (PublicLayout)
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import { Info, Package } from 'lucide-react';
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from '@djangocfg/ui/components';
17
+ import { Button } from '@djangocfg/ui/components';
18
+ import { getPackageVersions } from './packageVersions.config';
19
+
20
+ export interface PackageVersionsProps {
21
+ /**
22
+ * Display variant
23
+ * - 'sidebar': Adapts to sidebar collapsed state (PrivateLayout)
24
+ * - 'footer': Simple button for footer (PublicLayout)
25
+ * - 'footer-minimal': Only icon, no text (for compact footer)
26
+ */
27
+ variant?: 'sidebar' | 'footer' | 'footer-minimal';
28
+ }
29
+
30
+ export function PackageVersions({ variant = 'footer' }: PackageVersionsProps) {
31
+ // Try to use sidebar state if available (only in PrivateLayout)
32
+ let isCollapsed = false;
33
+ if (variant === 'sidebar') {
34
+ try {
35
+ // Dynamic import to avoid errors in PublicLayout
36
+ const { useSidebar } = require('@djangocfg/ui/components');
37
+ const { state } = useSidebar();
38
+ isCollapsed = state === 'collapsed';
39
+ } catch (e) {
40
+ // Sidebar not available, use default
41
+ }
42
+ }
43
+
44
+ const isSidebarVariant = variant === 'sidebar';
45
+ const isMinimalVariant = variant === 'footer-minimal';
46
+ const popoverAlign = isSidebarVariant ? 'start' : 'center';
47
+ const popoverSide = isSidebarVariant ? 'right' : 'top';
48
+
49
+ // Determine if we should show text
50
+ const showText = !isCollapsed && !isMinimalVariant;
51
+
52
+ // Get package versions dynamically
53
+ const packages = getPackageVersions();
54
+
55
+ return (
56
+ <Popover>
57
+ <PopoverTrigger asChild>
58
+ <Button
59
+ variant="ghost"
60
+ size={isCollapsed || isMinimalVariant ? "icon" : "sm"}
61
+ className={isSidebarVariant
62
+ ? "w-full justify-start text-xs text-muted-foreground hover:text-foreground"
63
+ : isMinimalVariant
64
+ ? "h-auto w-auto p-1 text-muted-foreground hover:text-primary transition-colors"
65
+ : "text-xs text-muted-foreground hover:text-foreground"
66
+ }
67
+ title={isMinimalVariant ? "Package Versions" : undefined}
68
+ >
69
+ <Info className={isMinimalVariant ? "h-3 w-3" : "h-3.5 w-3.5"} />
70
+ {showText && <span className="ml-2">Package Versions</span>}
71
+ </Button>
72
+ </PopoverTrigger>
73
+ <PopoverContent align={popoverAlign} side={popoverSide} className="w-80">
74
+ <div className="space-y-3">
75
+ <div className="flex items-center gap-2">
76
+ <Package className="h-4 w-4 text-primary" />
77
+ <h4 className="font-semibold text-sm">Package Versions</h4>
78
+ </div>
79
+ <div className="space-y-1.5">
80
+ {packages.map((pkg) => (
81
+ <a
82
+ key={pkg.name}
83
+ href={`https://www.npmjs.com/package/${pkg.name}`}
84
+ target="_blank"
85
+ rel="noopener noreferrer"
86
+ className="flex items-center justify-between py-1.5 px-2 rounded-sm hover:bg-accent/50 transition-colors cursor-pointer group"
87
+ >
88
+ <span className="text-xs font-mono text-muted-foreground group-hover:text-foreground transition-colors">
89
+ {pkg.name}
90
+ </span>
91
+ <span className="text-xs font-semibold bg-primary/10 text-primary px-2 py-0.5 rounded group-hover:bg-primary/20 transition-colors">
92
+ v{pkg.version}
93
+ </span>
94
+ </a>
95
+ ))}
96
+ </div>
97
+ </div>
98
+ </PopoverContent>
99
+ </Popover>
100
+ );
101
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Package Versions Module
3
+ */
4
+
5
+ export { PackageVersions } from './PackageVersions';
6
+ export { getPackageVersions, getPackageVersion } from './packageVersions.config';
7
+ export type { PackageInfo } from './packageVersions.config';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Package Versions Configuration
3
+ *
4
+ * NOTE: This file is auto-generated by packages/scripts/sync-package-versions.js
5
+ * Do not edit manually! Run 'make build' or 'pnpm sync-versions' to update.
6
+ *
7
+ * Versions are synced from actual package.json files in the monorepo.
8
+ * This ensures compatibility with both monorepo (dev) and npm (production).
9
+ */
10
+
11
+ export interface PackageInfo {
12
+ name: string;
13
+ version: string;
14
+ }
15
+
16
+ /**
17
+ * Package versions registry
18
+ * Auto-synced from package.json files
19
+ * Last updated: 2025-10-23T06:57:54.947Z
20
+ */
21
+ const PACKAGE_VERSIONS: PackageInfo[] = [
22
+ {
23
+ "name": "@djangocfg/ui",
24
+ "version": "1.2.2"
25
+ },
26
+ {
27
+ "name": "@djangocfg/api",
28
+ "version": "1.2.2"
29
+ },
30
+ {
31
+ "name": "@djangocfg/layouts",
32
+ "version": "1.2.2"
33
+ },
34
+ {
35
+ "name": "@djangocfg/markdown",
36
+ "version": "1.2.2"
37
+ },
38
+ {
39
+ "name": "@djangocfg/og-image",
40
+ "version": "1.2.2"
41
+ },
42
+ {
43
+ "name": "@djangocfg/eslint-config",
44
+ "version": "1.2.2"
45
+ },
46
+ {
47
+ "name": "@djangocfg/typescript-config",
48
+ "version": "1.2.2"
49
+ }
50
+ ];
51
+
52
+ /**
53
+ * Get all package versions
54
+ */
55
+ export function getPackageVersions(): PackageInfo[] {
56
+ return PACKAGE_VERSIONS;
57
+ }
58
+
59
+ /**
60
+ * Get single package version by name
61
+ */
62
+ export function getPackageVersion(packageName: string): string | undefined {
63
+ const packages = getPackageVersions();
64
+ return packages.find((pkg) => pkg.name === packageName)?.version;
65
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Universal User Menu Component
3
+ *
4
+ * Single unified component for both Desktop and Mobile user menus
5
+ * Uses AppContext and Auth context directly - no prop drilling!
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import { useRouter } from 'next/router';
12
+ import { User, ChevronDown, Crown, LogOut, Settings } from 'lucide-react';
13
+ import { Button, ButtonLink, Card, CardContent } from '@djangocfg/ui/components';
14
+ import { ThemeToggle } from '@djangocfg/ui/theme';
15
+ import { useAppContext } from '../context';
16
+ import { useAuth } from '../../../auth';
17
+
18
+ export interface UserMenuProps {
19
+ variant: 'desktop' | 'mobile';
20
+ /** Mobile only: callback when navigation happens */
21
+ onNavigate?: () => void;
22
+ }
23
+
24
+ interface MenuItem {
25
+ id: string;
26
+ type: 'link' | 'button';
27
+ icon: React.ReactNode;
28
+ label: string;
29
+ href?: string;
30
+ onClick?: () => void;
31
+ variant?: 'default' | 'ghost' | 'destructive';
32
+ className?: string;
33
+ }
34
+
35
+ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
36
+ const router = useRouter();
37
+ const { config, userMenuOpen, toggleUserMenu, closeUserMenu } = useAppContext();
38
+ const { user, isAuthenticated, logout } = useAuth();
39
+
40
+ const { publicLayout, routes } = config;
41
+
42
+ // Desktop: determine if user is on dashboard
43
+ const isDashboard = variant === 'desktop' && publicLayout.userMenu.dashboardPath
44
+ ? router.pathname.includes(publicLayout.userMenu.dashboardPath)
45
+ : false;
46
+
47
+ // === DATA PREPARATION (before rendering) ===
48
+
49
+ // Handle logout
50
+ const handleLogout = React.useCallback(() => {
51
+ logout();
52
+ if (variant === 'desktop') {
53
+ closeUserMenu();
54
+ } else if (onNavigate) {
55
+ onNavigate();
56
+ }
57
+ }, [logout, variant, closeUserMenu, onNavigate]);
58
+
59
+ // Prepare menu items
60
+ const menuItems = React.useMemo<MenuItem[]>(() => {
61
+ const items: MenuItem[] = [];
62
+
63
+ // Profile
64
+ items.push({
65
+ id: 'profile',
66
+ type: 'link',
67
+ icon: <Settings className="size-4" />,
68
+ label: 'Profile',
69
+ href: publicLayout.userMenu.profilePath,
70
+ variant: 'ghost',
71
+ });
72
+
73
+ // Logout
74
+ items.push({
75
+ id: 'logout',
76
+ type: 'button',
77
+ icon: <LogOut className="size-4" />,
78
+ label: 'Sign out',
79
+ onClick: handleLogout,
80
+ variant: 'ghost',
81
+ className: 'text-destructive hover:text-destructive hover:bg-destructive/10',
82
+ });
83
+
84
+ return items;
85
+ }, [publicLayout.userMenu.profilePath, handleLogout]);
86
+
87
+ // === UNIFIED MENU ITEM RENDERER ===
88
+
89
+ /**
90
+ * Renders a single menu item (link or button)
91
+ * Used for both desktop dropdown and mobile icons
92
+ */
93
+ const renderMenuItem = React.useCallback((
94
+ item: MenuItem,
95
+ mode: 'desktop' | 'mobile-icon',
96
+ onClick?: () => void
97
+ ) => {
98
+ if (mode === 'mobile-icon') {
99
+ // Mobile: render as icon-only button
100
+ const iconElement = item.icon as React.ReactElement;
101
+ const resizedIcon = React.isValidElement(iconElement)
102
+ ? React.cloneElement(iconElement, { className: 'h-5 w-5' } as any)
103
+ : iconElement;
104
+
105
+ if (item.type === 'link' && item.href) {
106
+ return (
107
+ <ButtonLink
108
+ key={item.id}
109
+ href={item.href}
110
+ variant={item.variant as any}
111
+ size="icon"
112
+ className="h-9 w-9"
113
+ onClick={onClick}
114
+ aria-label={item.label}
115
+ >
116
+ {resizedIcon}
117
+ </ButtonLink>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <Button
123
+ key={item.id}
124
+ onClick={item.onClick}
125
+ variant={item.variant as any}
126
+ size="icon"
127
+ className={`h-9 w-9 ${item.className || ''}`}
128
+ aria-label={item.label}
129
+ >
130
+ {resizedIcon}
131
+ </Button>
132
+ );
133
+ }
134
+
135
+ // Desktop: render with text and icon
136
+ const baseClassName = `w-full justify-start gap-2 ${item.className || ''}`;
137
+
138
+ if (item.type === 'link' && item.href) {
139
+ return (
140
+ <ButtonLink
141
+ key={item.id}
142
+ href={item.href}
143
+ variant={item.variant as any}
144
+ size="sm"
145
+ className={baseClassName}
146
+ onClick={onClick}
147
+ >
148
+ {item.icon}
149
+ {item.label}
150
+ </ButtonLink>
151
+ );
152
+ }
153
+
154
+ return (
155
+ <Button
156
+ key={item.id}
157
+ onClick={item.onClick}
158
+ variant={item.variant as any}
159
+ size="sm"
160
+ className={baseClassName}
161
+ >
162
+ {item.icon}
163
+ {item.label}
164
+ </Button>
165
+ );
166
+ }, []);
167
+
168
+ // === RENDERING ===
169
+
170
+ // Desktop variant
171
+ if (variant === 'desktop') {
172
+ return (
173
+ <div className="flex items-center gap-3">
174
+ {isAuthenticated ? (
175
+ <div className="flex items-center gap-3">
176
+ {/* Dashboard button (only if not on dashboard) */}
177
+ {publicLayout.userMenu.dashboardPath && !isDashboard && (
178
+ <ButtonLink
179
+ href={publicLayout.userMenu.dashboardPath}
180
+ variant="default"
181
+ size="sm"
182
+ className="gap-2"
183
+ >
184
+ <Crown className="size-4" />
185
+ Dashboard
186
+ </ButtonLink>
187
+ )}
188
+
189
+ {/* User Dropdown */}
190
+ <div className="relative">
191
+ <button
192
+ className="flex items-center gap-2 px-3 py-2 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50"
193
+ onClick={toggleUserMenu}
194
+ aria-haspopup="true"
195
+ aria-expanded={userMenuOpen}
196
+ >
197
+ <User className="size-4" />
198
+ <span className="max-w-[120px] truncate">{user?.email}</span>
199
+ <ChevronDown
200
+ className={`size-4 transition-transform ${
201
+ userMenuOpen ? 'rotate-180' : ''
202
+ }`}
203
+ />
204
+ </button>
205
+
206
+ {userMenuOpen && (
207
+ <>
208
+ {/* Backdrop */}
209
+ <div
210
+ className="fixed inset-0 z-[9995]"
211
+ onClick={closeUserMenu}
212
+ aria-hidden="true"
213
+ />
214
+ {/* Dropdown */}
215
+ <div
216
+ className="absolute top-full right-0 mt-2 w-48 rounded-sm shadow-sm backdrop-blur-xl z-[9996] bg-popover border border-border"
217
+ role="menu"
218
+ aria-label="User menu"
219
+ >
220
+ <div className="p-2">
221
+ {/* User info */}
222
+ <div className="px-3 py-2 text-sm mb-2 border-b border-border">
223
+ <div className="text-muted-foreground">Signed in as:</div>
224
+ <div className="font-medium truncate text-popover-foreground mt-1">
225
+ {user?.email}
226
+ </div>
227
+ </div>
228
+
229
+ {/* Menu items - unified rendering */}
230
+ {menuItems.map((item) => renderMenuItem(item, 'desktop', closeUserMenu))}
231
+ </div>
232
+ </div>
233
+ </>
234
+ )}
235
+ </div>
236
+ </div>
237
+ ) : (
238
+ /* Guest - Sign in button */
239
+ <ButtonLink href={routes.auth} variant="default" size="sm" className="h-9 gap-1.5">
240
+ <User className="w-4 h-4" />
241
+ Sign In
242
+ </ButtonLink>
243
+ )}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ // Mobile variant
249
+ if (isAuthenticated) {
250
+ return (
251
+ <Card className="border-primary/20 shadow-lg !bg-accent/50">
252
+ <CardContent className="p-4">
253
+ {/* User Info Header */}
254
+ <div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
255
+ <div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
256
+ {user?.avatar ? (
257
+ <img
258
+ src={user.avatar}
259
+ alt={user?.email || 'User'}
260
+ className="w-10 h-10 rounded-full object-cover"
261
+ />
262
+ ) : (
263
+ <User className="w-5 h-5 text-primary-foreground" />
264
+ )}
265
+ {/* Active indicator */}
266
+ <div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full bg-green-500 border-2 border-background" />
267
+ </div>
268
+ <div className="flex-1 min-w-0">
269
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
270
+ Signed in as
271
+ </p>
272
+ <p className="text-sm font-semibold truncate text-foreground">
273
+ {user?.email}
274
+ </p>
275
+ </div>
276
+ </div>
277
+
278
+ {/* Action Buttons */}
279
+ <div className="space-y-3">
280
+ {/* Dashboard link */}
281
+ {publicLayout.userMenu.dashboardPath && (
282
+ <ButtonLink
283
+ href={publicLayout.userMenu.dashboardPath}
284
+ variant="default"
285
+ size="sm"
286
+ className="w-full h-9 gap-2"
287
+ onClick={onNavigate}
288
+ >
289
+ <Crown className="size-4" />
290
+ Dashboard
291
+ </ButtonLink>
292
+ )}
293
+
294
+ {/* Quick Actions - Icons only - unified rendering */}
295
+ <div className="flex items-center justify-center gap-2 pt-3 mt-1 border-t border-border/30">
296
+ {menuItems.map((item) => renderMenuItem(item, 'mobile-icon', onNavigate))}
297
+
298
+ {/* Theme Toggle */}
299
+ <ThemeToggle />
300
+ </div>
301
+ </div>
302
+ </CardContent>
303
+ </Card>
304
+ );
305
+ }
306
+
307
+ // Mobile Guest Card
308
+ return (
309
+ <Card className="border-border !bg-accent/50">
310
+ <CardContent className="p-4">
311
+ <div className="text-center space-y-4">
312
+ <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto bg-muted">
313
+ <User className="w-6 h-6 text-muted-foreground" />
314
+ </div>
315
+ <div>
316
+ <p className="text-sm font-medium mb-1 text-foreground">Welcome!</p>
317
+ <p className="text-xs text-muted-foreground">
318
+ Sign in to access your dashboard
319
+ </p>
320
+ </div>
321
+ <ButtonLink
322
+ href={routes.auth}
323
+ variant="default"
324
+ size="default"
325
+ className="w-full gap-2"
326
+ onClick={onNavigate}
327
+ >
328
+ <User className="w-5 h-5" />
329
+ Sign In
330
+ </ButtonLink>
331
+
332
+ {/* Theme toggle */}
333
+ <div className="flex justify-center pt-2 border-t border-border/30">
334
+ <ThemeToggle />
335
+ </div>
336
+ </div>
337
+ </CardContent>
338
+ </Card>
339
+ );
340
+ }
@@ -5,3 +5,7 @@
5
5
  export { default as Seo } from './Seo';
6
6
  export { default as PageProgress } from './PageProgress';
7
7
  export { ErrorBoundary } from './ErrorBoundary';
8
+ export { PackageVersions, getPackageVersions, getPackageVersion } from './PackageVersions';
9
+ export { UserMenu } from './UserMenu';
10
+ export type { PackageInfo } from './PackageVersions';
11
+ export type { UserMenuProps } from './UserMenu';
@@ -24,11 +24,11 @@ interface AppContextValue {
24
24
  currentPath: string;
25
25
  layoutMode: LayoutMode;
26
26
 
27
- // Mobile menu state
28
- mobileMenuOpen: boolean;
29
- openMobileMenu: () => void;
30
- closeMobileMenu: () => void;
31
- toggleMobileMenu: () => void;
27
+ // Mobile drawer state
28
+ mobileDrawerOpen: boolean;
29
+ openMobileDrawer: () => void;
30
+ closeMobileDrawer: () => void;
31
+ toggleMobileDrawer: () => void;
32
32
 
33
33
  // User menu state (desktop dropdown)
34
34
  userMenuOpen: boolean;
@@ -41,6 +41,9 @@ interface AppContextValue {
41
41
  collapseSidebar: () => void;
42
42
  expandSidebar: () => void;
43
43
  toggleSidebar: () => void;
44
+
45
+ // Features
46
+ showPackageVersions?: boolean;
44
47
  }
45
48
 
46
49
  // ═══════════════════════════════════════════════════════════════════════════
@@ -56,6 +59,7 @@ const AppContext = createContext<AppContextValue | null>(null);
56
59
  export interface AppContextProviderProps {
57
60
  children: ReactNode;
58
61
  config: AppLayoutConfig;
62
+ showPackageVersions?: boolean;
59
63
  }
60
64
 
61
65
  /**
@@ -64,11 +68,11 @@ export interface AppContextProviderProps {
64
68
  * Provides unified application context to all child components
65
69
  * Manages layout state and exposes configuration
66
70
  */
67
- export function AppContextProvider({ children, config }: AppContextProviderProps) {
71
+ export function AppContextProvider({ children, config, showPackageVersions }: AppContextProviderProps) {
68
72
  const router = useRouter();
69
73
 
70
74
  // UI state
71
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
75
+ const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
72
76
  const [userMenuOpen, setUserMenuOpen] = useState(false);
73
77
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
74
78
 
@@ -81,10 +85,10 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
81
85
  return 'public';
82
86
  }, [router.pathname, config.routes.detectors]);
83
87
 
84
- // Mobile menu handlers
85
- const openMobileMenu = () => setMobileMenuOpen(true);
86
- const closeMobileMenu = () => setMobileMenuOpen(false);
87
- const toggleMobileMenu = () => setMobileMenuOpen(prev => !prev);
88
+ // Mobile drawer handlers
89
+ const openMobileDrawer = () => setMobileDrawerOpen(true);
90
+ const closeMobileDrawer = () => setMobileDrawerOpen(false);
91
+ const toggleMobileDrawer = () => setMobileDrawerOpen(prev => !prev);
88
92
 
89
93
  // User menu handlers
90
94
  const openUserMenu = () => setUserMenuOpen(true);
@@ -101,10 +105,10 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
101
105
  routes: config.routes.detectors,
102
106
  currentPath: router.pathname,
103
107
  layoutMode,
104
- mobileMenuOpen,
105
- openMobileMenu,
106
- closeMobileMenu,
107
- toggleMobileMenu,
108
+ mobileDrawerOpen,
109
+ openMobileDrawer,
110
+ closeMobileDrawer,
111
+ toggleMobileDrawer,
108
112
  userMenuOpen,
109
113
  openUserMenu,
110
114
  closeUserMenu,
@@ -113,6 +117,7 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
113
117
  collapseSidebar,
114
118
  expandSidebar,
115
119
  toggleSidebar,
120
+ showPackageVersions,
116
121
  };
117
122
 
118
123
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
@@ -129,7 +134,7 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
129
134
  *
130
135
  * @example
131
136
  * ```tsx
132
- * const { config, layoutMode, toggleMobileMenu } = useAppContext();
137
+ * const { config, layoutMode, toggleMobileDrawer } = useAppContext();
133
138
  * ```
134
139
  */
135
140
  export function useAppContext(): AppContextValue {
@@ -152,8 +152,8 @@ export function DashboardHeader() {
152
152
 
153
153
  <DropdownMenuSeparator />
154
154
 
155
- <DropdownMenuItem onClick={logout}>
156
- <LogOut className="mr-2 h-4 w-4" />
155
+ <DropdownMenuItem onClick={logout} className="gap-2">
156
+ <LogOut className="h-4 w-4" />
157
157
  Logout
158
158
  </DropdownMenuItem>
159
159
  </DropdownMenuContent>
@@ -29,6 +29,7 @@ import {
29
29
  } from '@djangocfg/ui/components';
30
30
  import { useAppContext } from '../../../context';
31
31
  import { useNavigation } from '../../../hooks';
32
+ import { PackageVersions } from '../../../components';
32
33
 
33
34
  /**
34
35
  * Dashboard Sidebar Component
@@ -44,7 +45,7 @@ import { useNavigation } from '../../../hooks';
44
45
  * All data from context!
45
46
  */
46
47
  export function DashboardSidebar() {
47
- const { config } = useAppContext();
48
+ const { config, showPackageVersions } = useAppContext();
48
49
  const { currentPath } = useNavigation();
49
50
  const { state, isMobile } = useSidebar();
50
51
 
@@ -157,8 +158,11 @@ export function DashboardSidebar() {
157
158
  ))}
158
159
  </SidebarContent>
159
160
 
160
- {/* TODO: implement footer content if needed */}
161
- {/* <SidebarFooter>Footer content here</SidebarFooter> */}
161
+ {showPackageVersions && (
162
+ <SidebarFooter>
163
+ <PackageVersions variant="sidebar" />
164
+ </SidebarFooter>
165
+ )}
162
166
  </Sidebar>
163
167
  );
164
168
  }
@@ -11,6 +11,7 @@ import React from 'react';
11
11
  import Link from 'next/link';
12
12
  import { useIsMobile } from '@djangocfg/ui/hooks';
13
13
  import { useAppContext } from '../../../context';
14
+ import { PackageVersions } from '../../../components';
14
15
 
15
16
  /**
16
17
  * Footer Component
@@ -27,7 +28,7 @@ import { useAppContext } from '../../../context';
27
28
  * All data from context!
28
29
  */
29
30
  export function Footer() {
30
- const { config } = useAppContext();
31
+ const { config, showPackageVersions } = useAppContext();
31
32
  const isMobile = useIsMobile();
32
33
 
33
34
  const { app, publicLayout } = config;
@@ -58,7 +59,10 @@ export function Footer() {
58
59
  </div>
59
60
 
60
61
  {/* Quick Links */}
61
- <div className="flex flex-wrap justify-center gap-4 mb-6">
62
+ <div className="flex flex-wrap justify-center gap-4 mb-6 items-center">
63
+ {showPackageVersions && (
64
+ <PackageVersions variant="footer-minimal" />
65
+ )}
62
66
  {footer.links.docs && (
63
67
  <a
64
68
  href={footer.links.docs}
@@ -209,6 +213,9 @@ export function Footer() {
209
213
  </a>
210
214
  </div>
211
215
  <div className="flex flex-wrap items-center gap-4">
216
+ {showPackageVersions && (
217
+ <PackageVersions variant="footer-minimal" />
218
+ )}
212
219
  {footer.links.docs && (
213
220
  <a
214
221
  href={footer.links.docs}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Mobile Menu Drawer
2
+ * Mobile Drawer
3
3
  *
4
- * Full-screen slide-in menu for mobile devices
4
+ * Full-screen slide-in drawer for mobile navigation
5
5
  * Refactored from _old/MainLayout - uses context only!
6
6
  */
7
7
 
@@ -12,29 +12,25 @@ import { createPortal } from 'react-dom';
12
12
  import Link from 'next/link';
13
13
  import { X } from 'lucide-react';
14
14
  import { useAppContext } from '../../../context';
15
- import { useAuth } from '../../../../../auth';
16
15
  import { useNavigation } from '../../../hooks';
17
- import { MobileMenuUserCard } from './MobileMenuUserCard';
16
+ import { UserMenu } from '../../../components';
18
17
 
19
18
  /**
20
- * Mobile Menu Component
19
+ * Mobile Drawer Component
21
20
  *
22
21
  * Features:
23
22
  * - Slide-in drawer from right
24
- * - User card with info (authenticated)
25
- * - Welcome card with sign in (guest)
23
+ * - UserMenu component (authenticated/guest)
26
24
  * - Navigation sections
27
- * - Theme toggle
28
25
  * - Backdrop overlay
29
26
  *
30
27
  * All data from context!
31
28
  */
32
- export function MobileMenu() {
33
- const { config, mobileMenuOpen, closeMobileMenu } = useAppContext();
34
- const { user, isAuthenticated, logout } = useAuth();
29
+ export function MobileDrawer() {
30
+ const { config, mobileDrawerOpen, closeMobileDrawer } = useAppContext();
35
31
  const { isActive } = useNavigation();
36
32
 
37
- const { app, publicLayout, routes } = config;
33
+ const { app, publicLayout } = config;
38
34
 
39
35
  // Track if we should render (stays true during close animation)
40
36
  const [shouldRender, setShouldRender] = React.useState(false);
@@ -44,7 +40,7 @@ export function MobileMenu() {
44
40
 
45
41
  // Handle opening
46
42
  React.useEffect(() => {
47
- if (mobileMenuOpen) {
43
+ if (mobileDrawerOpen) {
48
44
  setShouldRender(true);
49
45
  // Trigger animation after render
50
46
  requestAnimationFrame(() => {
@@ -61,19 +57,14 @@ export function MobileMenu() {
61
57
  }, 300);
62
58
  return () => clearTimeout(timer);
63
59
  }
64
- }, [mobileMenuOpen]);
65
-
66
- const handleLogout = () => {
67
- logout();
68
- closeMobileMenu();
69
- };
60
+ }, [mobileDrawerOpen]);
70
61
 
71
62
  const handleClose = () => {
72
- closeMobileMenu();
63
+ closeMobileDrawer();
73
64
  };
74
65
 
75
66
  const handleNavigate = () => {
76
- closeMobileMenu();
67
+ closeMobileDrawer();
77
68
  };
78
69
 
79
70
  // Prepare menu sections before render
@@ -137,15 +128,7 @@ export function MobileMenu() {
137
128
  {/* Scrollable Content */}
138
129
  <div className="flex-1 overflow-y-auto p-4 space-y-6">
139
130
  {/* User Menu Card */}
140
- <MobileMenuUserCard
141
- isAuthenticated={isAuthenticated}
142
- user={user}
143
- dashboardPath={publicLayout.userMenu.dashboardPath}
144
- profilePath={publicLayout.userMenu.profilePath}
145
- authPath={routes.auth}
146
- onLogout={handleLogout}
147
- onNavigate={handleNavigate}
148
- />
131
+ <UserMenu variant="mobile" onNavigate={handleNavigate} />
149
132
 
150
133
  {/* Navigation Sections */}
151
134
  <div className="space-y-6">
@@ -20,10 +20,9 @@ import { ThemeToggle } from '@djangocfg/ui/theme';
20
20
  import { cn } from '@djangocfg/ui/lib';
21
21
  import { useIsMobile } from '@djangocfg/ui/hooks';
22
22
  import { useAppContext } from '../../../context';
23
- import { useAuth } from '../../../../../auth';
24
23
  import { useNavigation } from '../../../hooks';
25
- import { DesktopUserMenu } from './DesktopUserMenu';
26
- import { MobileMenu } from './MobileMenu';
24
+ import { UserMenu } from '../../../components';
25
+ import { MobileDrawer } from './MobileDrawer';
27
26
 
28
27
  /**
29
28
  * Navigation Component
@@ -38,8 +37,7 @@ import { MobileMenu } from './MobileMenu';
38
37
  * All data from context - zero prop drilling!
39
38
  */
40
39
  export function Navigation() {
41
- const { config, toggleMobileMenu } = useAppContext();
42
- const { user, isAuthenticated, logout } = useAuth();
40
+ const { config, toggleMobileDrawer } = useAppContext();
43
41
  const { isActive } = useNavigation();
44
42
  const isMobile = useIsMobile();
45
43
  const [openDropdown, setOpenDropdown] = React.useState<string | null>(null);
@@ -147,14 +145,14 @@ export function Navigation() {
147
145
  {!isMobile && (
148
146
  <div className="flex items-center gap-2">
149
147
  <ThemeToggle />
150
- <DesktopUserMenu />
148
+ <UserMenu variant="desktop" />
151
149
  </div>
152
150
  )}
153
151
 
154
152
  {/* Mobile Menu Button - Only visible on mobile */}
155
153
  {isMobile && (
156
154
  <button
157
- onClick={toggleMobileMenu}
155
+ onClick={toggleMobileDrawer}
158
156
  className="p-3 rounded-sm border shadow-sm transition-all duration-200 bg-card/50 hover:bg-card border-border/50 hover:border-primary/50 text-foreground hover:text-primary"
159
157
  aria-label="Toggle mobile menu"
160
158
  >
@@ -164,8 +162,8 @@ export function Navigation() {
164
162
  </div>
165
163
  </div>
166
164
 
167
- {/* Mobile Menu Drawer */}
168
- <MobileMenu />
165
+ {/* Mobile Drawer */}
166
+ <MobileDrawer />
169
167
  </nav>
170
168
  );
171
169
  }
@@ -22,13 +22,15 @@ interface ProfileLayoutProps {
22
22
  title?: string;
23
23
  description?: string;
24
24
  showMemberSince?: boolean;
25
+ showLastLogin?: boolean;
25
26
  }
26
27
 
27
28
  const ProfileContent = ({
28
29
  onUnauthenticated,
29
30
  title = 'Profile Settings',
30
31
  description = 'Manage your account information and preferences',
31
- showMemberSince = true
32
+ showMemberSince = true,
33
+ showLastLogin = true
32
34
  }: ProfileLayoutProps) => {
33
35
  const { user, isLoading } = useAuth();
34
36
 
@@ -83,11 +85,18 @@ const ProfileContent = ({
83
85
  <CardTitle className="text-xl">
84
86
  {user?.display_username || user?.email}
85
87
  </CardTitle>
86
- {showMemberSince && user?.date_joined && (
87
- <CardDescription className="text-muted-foreground">
88
- Member since {formatDate(user.date_joined)}
89
- </CardDescription>
90
- )}
88
+ <div className="space-y-1">
89
+ {showMemberSince && user?.date_joined && (
90
+ <CardDescription className="text-muted-foreground">
91
+ Member since {formatDate(user.date_joined)}
92
+ </CardDescription>
93
+ )}
94
+ {showLastLogin && user?.last_login && (
95
+ <CardDescription className="text-muted-foreground text-sm">
96
+ Last login: {formatDate(user.last_login)}
97
+ </CardDescription>
98
+ )}
99
+ </div>
91
100
  </CardHeader>
92
101
 
93
102
  <CardContent className="space-y-6">
@@ -1,136 +0,0 @@
1
- /**
2
- * Desktop User Menu
3
- *
4
- * User dropdown menu for desktop navigation
5
- * Refactored from _old/MainLayout - uses context only!
6
- */
7
-
8
- 'use client';
9
-
10
- import React from 'react';
11
- import { useRouter } from 'next/router';
12
- import { ChevronDown, LayoutDashboard, LogOut, User } from 'lucide-react';
13
- import { ButtonLink } from '@djangocfg/ui/components';
14
- import { useAppContext } from '../../../context';
15
- import { useAuth } from '../../../../../auth';
16
-
17
- /**
18
- * Desktop User Menu Component
19
- *
20
- * Features:
21
- * - Sign in button for guests
22
- * - Dashboard link for authenticated users (if not on dashboard)
23
- * - User dropdown with email and profile link
24
- * - Logout button
25
- *
26
- * All data from context!
27
- */
28
- export function DesktopUserMenu() {
29
- const router = useRouter();
30
- const { config, userMenuOpen, toggleUserMenu, closeUserMenu } = useAppContext();
31
- const { user, isAuthenticated, logout } = useAuth();
32
-
33
- const { routes, publicLayout } = config;
34
-
35
- const handleLogout = () => {
36
- logout();
37
- closeUserMenu();
38
- };
39
-
40
- const isDashboard = publicLayout.userMenu.dashboardPath
41
- ? router.pathname.includes(publicLayout.userMenu.dashboardPath)
42
- : false;
43
-
44
- return (
45
- <div className="flex items-center gap-3">
46
- {/* Authenticated user */}
47
- {isAuthenticated ? (
48
- <div className="flex items-center gap-3">
49
- {/* Dashboard button (only if not on dashboard) */}
50
- {publicLayout.userMenu.dashboardPath && !isDashboard && (
51
- <ButtonLink
52
- href={publicLayout.userMenu.dashboardPath}
53
- variant="default"
54
- size="sm"
55
- >
56
- <LayoutDashboard className="size-4 mr-2" />
57
- Dashboard
58
- </ButtonLink>
59
- )}
60
-
61
- {/* User Dropdown */}
62
- <div className="relative">
63
- <button
64
- className="flex items-center gap-2 px-3 py-2 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50"
65
- onClick={toggleUserMenu}
66
- aria-haspopup="true"
67
- aria-expanded={userMenuOpen}
68
- >
69
- <User className="size-4" />
70
- <span className="max-w-[120px] truncate">{user?.email}</span>
71
- <ChevronDown
72
- className={`size-4 transition-transform ${
73
- userMenuOpen ? 'rotate-180' : ''
74
- }`}
75
- />
76
- </button>
77
-
78
- {userMenuOpen && (
79
- <>
80
- {/* Backdrop */}
81
- <div
82
- className="fixed inset-0 z-[9995]"
83
- onClick={closeUserMenu}
84
- aria-hidden="true"
85
- />
86
- {/* Dropdown */}
87
- <div
88
- className="absolute top-full right-0 mt-2 w-48 rounded-sm shadow-sm backdrop-blur-xl z-[9996] bg-popover border border-border"
89
- role="menu"
90
- aria-label="User menu"
91
- >
92
- <div className="p-2">
93
- {/* User info */}
94
- <div className="px-3 py-2 text-sm mb-2 border-b border-border">
95
- <div className="text-muted-foreground">Signed in as:</div>
96
- <div className="font-medium truncate text-popover-foreground mt-1">
97
- {user?.email}
98
- </div>
99
- </div>
100
-
101
- {/* Profile link */}
102
- <ButtonLink
103
- href={publicLayout.userMenu.profilePath}
104
- variant="ghost"
105
- size="sm"
106
- className="w-full justify-start"
107
- onClick={closeUserMenu}
108
- >
109
- <User className="size-4 mr-2" />
110
- Profile
111
- </ButtonLink>
112
-
113
- {/* Logout button */}
114
- <button
115
- onClick={handleLogout}
116
- className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-sm transition-colors text-destructive hover:bg-destructive/[0.1]"
117
- >
118
- <LogOut className="size-4" />
119
- <span>Sign out</span>
120
- </button>
121
- </div>
122
- </div>
123
- </>
124
- )}
125
- </div>
126
- </div>
127
- ) : (
128
- /* Guest - Sign in button */
129
- <ButtonLink href={routes.auth} variant="default" size="sm" className="h-9 gap-1.5">
130
- <User className="w-4 h-4" />
131
- Sign In
132
- </ButtonLink>
133
- )}
134
- </div>
135
- );
136
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * Mobile Menu User Card Component
3
- *
4
- * Displays user information and action buttons in mobile menu
5
- * - Authenticated: shows user info with dashboard, profile, and logout
6
- * - Guest: shows welcome message with sign in button
7
- */
8
-
9
- 'use client';
10
-
11
- import React from 'react';
12
- import { Crown, LogOut, Settings, User } from 'lucide-react';
13
- import { Button, ButtonLink, Card, CardContent } from '@djangocfg/ui/components';
14
- import { ThemeToggle } from '@djangocfg/ui/theme';
15
-
16
- interface MobileMenuUserCardProps {
17
- isAuthenticated: boolean;
18
- user?: {
19
- email?: string;
20
- avatar?: string;
21
- } | null;
22
- dashboardPath?: string;
23
- profilePath: string;
24
- authPath: string;
25
- onLogout: () => void;
26
- onNavigate: () => void;
27
- }
28
-
29
- export function MobileMenuUserCard({
30
- isAuthenticated,
31
- user,
32
- dashboardPath,
33
- profilePath,
34
- authPath,
35
- onLogout,
36
- onNavigate,
37
- }: MobileMenuUserCardProps) {
38
- if (isAuthenticated) {
39
- return (
40
- <Card className="border-primary/20 shadow-lg !bg-accent/50">
41
- <CardContent className="p-4">
42
- {/* User Info Header */}
43
- <div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
44
- <div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
45
- {user?.avatar ? (
46
- <img
47
- src={user.avatar}
48
- alt={user?.email || 'User'}
49
- className="w-10 h-10 rounded-full object-cover"
50
- />
51
- ) : (
52
- <User className="w-5 h-5 text-primary-foreground" />
53
- )}
54
- {/* Active indicator */}
55
- <div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full bg-green-500 border-2 border-background" />
56
- </div>
57
- <div className="flex-1 min-w-0">
58
- <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
59
- Signed in as
60
- </p>
61
- <p className="text-sm font-semibold truncate text-foreground">
62
- {user?.email}
63
- </p>
64
- </div>
65
- </div>
66
-
67
- {/* Action Buttons */}
68
- <div className="space-y-3">
69
- {/* Dashboard link */}
70
- {dashboardPath && (
71
- <ButtonLink
72
- href={dashboardPath}
73
- variant="default"
74
- size="sm"
75
- className="w-full h-9"
76
- onClick={onNavigate}
77
- >
78
- <Crown className="w-4 h-4 mr-2" />
79
- Dashboard
80
- </ButtonLink>
81
- )}
82
-
83
- {/* Quick Actions - Icons only */}
84
- <div className="flex items-center justify-center gap-2 pt-3 mt-1 border-t border-border/30">
85
- {/* Profile Settings */}
86
- <ButtonLink
87
- href={profilePath}
88
- variant="ghost"
89
- size="icon"
90
- className="h-9 w-9"
91
- onClick={onNavigate}
92
- aria-label="Profile Settings"
93
- >
94
- <Settings className="h-5 w-5" />
95
- </ButtonLink>
96
-
97
- {/* Theme Toggle */}
98
- <ThemeToggle />
99
-
100
- {/* Sign Out */}
101
- <Button
102
- onClick={onLogout}
103
- variant="ghost"
104
- size="icon"
105
- className="h-9 w-9 text-destructive hover:bg-destructive/10 hover:text-destructive"
106
- aria-label="Sign Out"
107
- >
108
- <LogOut className="h-5 w-5" />
109
- </Button>
110
- </div>
111
- </div>
112
- </CardContent>
113
- </Card>
114
- );
115
- }
116
-
117
- // Guest Card
118
- return (
119
- <Card className="border-border !bg-accent/50">
120
- <CardContent className="p-4">
121
- <div className="text-center space-y-4">
122
- <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto bg-muted">
123
- <User className="w-6 h-6 text-muted-foreground" />
124
- </div>
125
- <div>
126
- <p className="text-sm font-medium mb-1 text-foreground">Welcome!</p>
127
- <p className="text-xs text-muted-foreground">
128
- Sign in to access your dashboard
129
- </p>
130
- </div>
131
- <ButtonLink
132
- href={authPath}
133
- variant="default"
134
- size="default"
135
- className="w-full"
136
- onClick={onNavigate}
137
- >
138
- <User className="w-5 h-5 mr-2" />
139
- Sign In
140
- </ButtonLink>
141
-
142
- {/* Theme toggle */}
143
- <div className="flex justify-center pt-2 border-t border-border/30">
144
- <ThemeToggle />
145
- </div>
146
- </div>
147
- </CardContent>
148
- </Card>
149
- );
150
- }