@djangocfg/layouts 1.0.6 → 1.1.0

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.
Files changed (50) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/AppLayout.tsx +126 -28
  3. package/src/layouts/AppLayout/components/ErrorBoundary.tsx +99 -0
  4. package/src/layouts/AppLayout/components/PageProgress.tsx +28 -11
  5. package/src/layouts/AppLayout/components/index.ts +1 -0
  6. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +1 -1
  7. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +5 -5
  8. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +2 -2
  9. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +2 -2
  10. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +1 -1
  11. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +1 -1
  12. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +53 -36
  13. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +64 -52
  14. package/src/layouts/AppLayout/types/config.ts +22 -0
  15. package/src/layouts/ErrorLayout/ErrorLayout.tsx +169 -0
  16. package/src/layouts/ErrorLayout/errorConfig.tsx +152 -0
  17. package/src/layouts/ErrorLayout/index.ts +8 -0
  18. package/src/layouts/UILayout/README.md +267 -0
  19. package/src/layouts/UILayout/REFACTORING.md +331 -0
  20. package/src/layouts/UILayout/UIGuideApp.tsx +18 -0
  21. package/src/layouts/UILayout/UIGuideLanding.tsx +198 -0
  22. package/src/layouts/UILayout/UIGuideView.tsx +61 -0
  23. package/src/layouts/UILayout/UILayout.tsx +122 -0
  24. package/src/layouts/UILayout/components/AutoComponentDemo.tsx +77 -0
  25. package/src/layouts/UILayout/components/CategoryRenderer.tsx +45 -0
  26. package/src/layouts/UILayout/components/Header.tsx +114 -0
  27. package/src/layouts/UILayout/components/MobileOverlay.tsx +33 -0
  28. package/src/layouts/UILayout/components/Sidebar.tsx +195 -0
  29. package/src/layouts/UILayout/components/TailwindGuideRenderer.tsx +138 -0
  30. package/src/layouts/UILayout/config/ai-export.config.ts +80 -0
  31. package/src/layouts/UILayout/config/categories.config.tsx +114 -0
  32. package/src/layouts/UILayout/config/components/blocks.config.tsx +233 -0
  33. package/src/layouts/UILayout/config/components/data.config.tsx +308 -0
  34. package/src/layouts/UILayout/config/components/feedback.config.tsx +246 -0
  35. package/src/layouts/UILayout/config/components/forms.config.tsx +171 -0
  36. package/src/layouts/UILayout/config/components/hooks.config.tsx +131 -0
  37. package/src/layouts/UILayout/config/components/index.ts +69 -0
  38. package/src/layouts/UILayout/config/components/layout.config.tsx +133 -0
  39. package/src/layouts/UILayout/config/components/navigation.config.tsx +244 -0
  40. package/src/layouts/UILayout/config/components/overlay.config.tsx +561 -0
  41. package/src/layouts/UILayout/config/components/specialized.config.tsx +125 -0
  42. package/src/layouts/UILayout/config/components/types.ts +14 -0
  43. package/src/layouts/UILayout/config/index.ts +42 -0
  44. package/src/layouts/UILayout/config/tailwind.config.ts +77 -0
  45. package/src/layouts/UILayout/constants.ts +23 -0
  46. package/src/layouts/UILayout/context/ShowcaseContext.tsx +53 -0
  47. package/src/layouts/UILayout/context/index.ts +1 -0
  48. package/src/layouts/UILayout/index.ts +64 -0
  49. package/src/layouts/UILayout/types.ts +13 -0
  50. package/src/layouts/index.ts +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
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.0.6",
57
- "@djangocfg/og-image": "^1.0.6",
58
- "@djangocfg/ui": "^1.0.6",
56
+ "@djangocfg/api": "^1.1.0",
57
+ "@djangocfg/og-image": "^1.1.0",
58
+ "@djangocfg/ui": "^1.1.0",
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.0.6",
79
+ "@djangocfg/typescript-config": "^1.1.0",
80
80
  "@types/node": "^24.7.2",
81
81
  "@types/react": "19.2.2",
82
82
  "@types/react-dom": "19.2.1",
@@ -21,7 +21,7 @@ import React, { ReactNode, useEffect, useState } from 'react';
21
21
  import { useRouter } from 'next/router';
22
22
  import { AppContextProvider } from './context';
23
23
  import { CoreProviders } from './providers';
24
- import { Seo, PageProgress } from './components';
24
+ import { Seo, PageProgress, ErrorBoundary } from './components';
25
25
  import { PublicLayout } from './layouts/PublicLayout';
26
26
  import { PrivateLayout } from './layouts/PrivateLayout';
27
27
  import { AuthLayout } from './layouts/AuthLayout';
@@ -32,6 +32,25 @@ import type { AppLayoutConfig } from './types';
32
32
  export interface AppLayoutProps {
33
33
  children: ReactNode;
34
34
  config: AppLayoutConfig;
35
+ /**
36
+ * Disable layout rendering (Navigation, Sidebar, Footer)
37
+ * Only providers and SEO remain active
38
+ * Useful for custom layouts like landing pages
39
+ */
40
+ disableLayout?: boolean;
41
+ /**
42
+ * Force a specific layout regardless of route
43
+ * Overrides automatic layout detection
44
+ * @example forceLayout="public" - always use PublicLayout
45
+ */
46
+ forceLayout?: 'public' | 'private' | 'auth';
47
+ /**
48
+ * Font family to apply globally
49
+ * Accepts Next.js font object or CSS font-family string
50
+ * @example fontFamily={manrope.style.fontFamily}
51
+ * @example fontFamily="Inter, sans-serif"
52
+ */
53
+ fontFamily?: string;
35
54
  }
36
55
 
37
56
  /**
@@ -40,7 +59,17 @@ export interface AppLayoutProps {
40
59
  * Determines which layout to use based on route
41
60
  * Uses AppContext - no props passed down!
42
61
  */
43
- function LayoutRouter({ children }: { children: ReactNode }) {
62
+ function LayoutRouter({
63
+ children,
64
+ disableLayout,
65
+ forceLayout,
66
+ config
67
+ }: {
68
+ children: ReactNode;
69
+ disableLayout?: boolean;
70
+ forceLayout?: 'public' | 'private' | 'auth';
71
+ config: AppLayoutConfig;
72
+ }) {
44
73
  const router = useRouter();
45
74
  const { isAuthenticated, isLoading } = useAuth();
46
75
  const [isMounted, setIsMounted] = useState(false);
@@ -50,8 +79,16 @@ function LayoutRouter({ children }: { children: ReactNode }) {
50
79
  setIsMounted(true);
51
80
  }, []);
52
81
 
82
+ // If layout is disabled, render children directly (providers still active!)
83
+ if (disableLayout) {
84
+ return <>{children}</>;
85
+ }
86
+
53
87
  // Determine layout mode based on route (synchronous - works with SSR)
54
88
  const getLayoutMode = (): 'public' | 'private' | 'auth' => {
89
+ // If forceLayout is specified, use it
90
+ if (forceLayout) return forceLayout;
91
+
55
92
  if (router.pathname.startsWith('/auth')) return 'auth';
56
93
  if (router.pathname.startsWith('/private')) return 'private';
57
94
  return 'public';
@@ -65,9 +102,20 @@ function LayoutRouter({ children }: { children: ReactNode }) {
65
102
  case 'public':
66
103
  return <PublicLayout>{children}</PublicLayout>;
67
104
 
68
- // Auth routes: render immediately (SSR enabled)
105
+ // Auth routes: render inside PublicLayout with Navigation/Footer
69
106
  case 'auth':
70
- return <AuthLayout>{children}</AuthLayout>;
107
+ return (
108
+ <PublicLayout>
109
+ <AuthLayout
110
+ termsUrl={config.auth?.termsUrl}
111
+ privacyUrl={config.auth?.privacyUrl}
112
+ supportUrl={config.auth?.supportUrl}
113
+ enablePhoneAuth={config.auth?.enablePhoneAuth}
114
+ >
115
+ {children}
116
+ </AuthLayout>
117
+ </PublicLayout>
118
+ );
71
119
 
72
120
  // Private routes: wait for client-side hydration and auth check
73
121
  case 'private':
@@ -87,31 +135,81 @@ function LayoutRouter({ children }: { children: ReactNode }) {
87
135
  *
88
136
  * Single entry point for all layout logic
89
137
  * Wrap your app once in _app.tsx
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * // With layout (default - auto-detect)
142
+ * <AppLayout config={appLayoutConfig}>
143
+ * <Component {...pageProps} />
144
+ * </AppLayout>
145
+ *
146
+ * // With custom font
147
+ * <AppLayout config={appLayoutConfig} fontFamily={manrope.style.fontFamily}>
148
+ * <Component {...pageProps} />
149
+ * </AppLayout>
150
+ *
151
+ * // Without layout (providers still active)
152
+ * <AppLayout config={appLayoutConfig} disableLayout>
153
+ * <CustomLandingPage />
154
+ * </AppLayout>
155
+ *
156
+ * // Force public layout for all pages
157
+ * <AppLayout config={appLayoutConfig} forceLayout="public">
158
+ * <Component {...pageProps} />
159
+ * </AppLayout>
160
+ * ```
90
161
  */
91
- export function AppLayout({ children, config }: AppLayoutProps) {
92
- return (
93
- <CoreProviders config={config}>
94
- <AppContextProvider config={config}>
95
- {/* SEO Meta Tags */}
96
- <Seo
97
- pageConfig={{
98
- title: config.app.name,
99
- description: config.app.description,
100
- ogImage: {
162
+ export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily }: AppLayoutProps) {
163
+ // Check if ErrorBoundary is enabled (default: true)
164
+ const enableErrorBoundary = config.errors?.enableErrorBoundary !== false;
165
+ const supportEmail = config.errors?.supportEmail;
166
+ const onError = config.errors?.onError;
167
+
168
+ const content = (
169
+ <>
170
+ {/* Global Font Styles */}
171
+ {fontFamily && (
172
+ <style dangerouslySetInnerHTML={{
173
+ __html: `html { font-family: ${fontFamily}; }`
174
+ }} />
175
+ )}
176
+
177
+ <CoreProviders config={config}>
178
+ <AppContextProvider config={config}>
179
+ {/* SEO Meta Tags */}
180
+ <Seo
181
+ pageConfig={{
101
182
  title: config.app.name,
102
- subtitle: config.app.description,
103
- },
104
- }}
105
- icons={config.app.icons}
106
- siteUrl={config.app.siteUrl}
107
- />
108
-
109
- {/* Loading Progress Bar */}
110
- <PageProgress />
111
-
112
- {/* Smart Layout Router */}
113
- <LayoutRouter>{children}</LayoutRouter>
114
- </AppContextProvider>
115
- </CoreProviders>
183
+ description: config.app.description,
184
+ ogImage: {
185
+ title: config.app.name,
186
+ subtitle: config.app.description,
187
+ },
188
+ }}
189
+ icons={config.app.icons}
190
+ siteUrl={config.app.siteUrl}
191
+ />
192
+
193
+ {/* Loading Progress Bar */}
194
+ <PageProgress />
195
+
196
+ {/* Smart Layout Router */}
197
+ <LayoutRouter disableLayout={disableLayout} forceLayout={forceLayout} config={config}>
198
+ {children}
199
+ </LayoutRouter>
200
+ </AppContextProvider>
201
+ </CoreProviders>
202
+ </>
116
203
  );
204
+
205
+ // Wrap with ErrorBoundary if enabled
206
+ if (enableErrorBoundary) {
207
+ return (
208
+ <ErrorBoundary supportEmail={supportEmail} onError={onError}>
209
+ {content}
210
+ </ErrorBoundary>
211
+ );
212
+ }
213
+
214
+ return content;
117
215
  }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * ErrorBoundary - Automatic Error Handling
3
+ *
4
+ * Built-in React Error Boundary for AppLayout
5
+ * Catches all runtime errors and displays ErrorLayout
6
+ * No manual setup required - works automatically!
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import React, { Component, ReactNode } from 'react';
12
+ import { ErrorLayout } from '../../ErrorLayout';
13
+ import { Bug } from 'lucide-react';
14
+
15
+ interface ErrorBoundaryProps {
16
+ children: ReactNode;
17
+ /** Callback when error occurs */
18
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
19
+ /** Custom fallback UI */
20
+ fallback?: (error: Error, reset: () => void) => ReactNode;
21
+ /** Support email for error pages */
22
+ supportEmail?: string;
23
+ }
24
+
25
+ interface ErrorBoundaryState {
26
+ hasError: boolean;
27
+ error?: Error;
28
+ errorInfo?: React.ErrorInfo;
29
+ }
30
+
31
+ /**
32
+ * ErrorBoundary Component
33
+ *
34
+ * Automatically wraps all AppLayout children
35
+ * Catches React errors and shows ErrorLayout
36
+ */
37
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
38
+ constructor(props: ErrorBoundaryProps) {
39
+ super(props);
40
+ this.state = { hasError: false };
41
+ }
42
+
43
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
44
+ return { hasError: true, error };
45
+ }
46
+
47
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
48
+ // Log error
49
+ console.error('ErrorBoundary caught error:', error);
50
+ console.error('Error info:', errorInfo);
51
+
52
+ // Call optional callback
53
+ this.props.onError?.(error, errorInfo);
54
+
55
+ // Store error info in state
56
+ this.setState({ errorInfo });
57
+ }
58
+
59
+ resetError = () => {
60
+ this.setState({ hasError: false, error: undefined, errorInfo: undefined });
61
+ };
62
+
63
+ render() {
64
+ if (this.state.hasError && this.state.error) {
65
+ // Use custom fallback if provided
66
+ if (this.props.fallback) {
67
+ return this.props.fallback(this.state.error, this.resetError);
68
+ }
69
+
70
+ // Default error UI using ErrorLayout
71
+ return (
72
+ <ErrorLayout
73
+ title="Application Error"
74
+ description="Something went wrong while rendering the page. Try refreshing or going back."
75
+ illustration={<Bug className="w-24 h-24 text-destructive/50" strokeWidth={1.5} />}
76
+ supportEmail={this.props.supportEmail}
77
+ actions={
78
+ <div className="flex gap-4">
79
+ <button
80
+ onClick={() => window.location.reload()}
81
+ className="px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
82
+ >
83
+ Refresh Page
84
+ </button>
85
+ <button
86
+ onClick={() => window.history.back()}
87
+ className="px-6 py-3 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/90 transition-colors font-medium"
88
+ >
89
+ Go Back
90
+ </button>
91
+ </div>
92
+ }
93
+ />
94
+ );
95
+ }
96
+
97
+ return this.props.children;
98
+ }
99
+ }
@@ -5,8 +5,13 @@ const PageProgress = () => {
5
5
  const router = useRouter();
6
6
  const [loading, setLoading] = useState(false);
7
7
  const [progress, setProgress] = useState(0);
8
+ const [mounted, setMounted] = useState(false);
8
9
  const progressTimer = useRef<NodeJS.Timeout | null>(null);
9
10
 
11
+ useEffect(() => {
12
+ setMounted(true);
13
+ }, []);
14
+
10
15
  // Simulate realistic progress
11
16
  const startFakeProgress = () => {
12
17
  // Clear any existing timer
@@ -43,12 +48,14 @@ const PageProgress = () => {
43
48
  progressTimer.current = null;
44
49
  }
45
50
 
46
- // Jump to 100% and then hide after animation duration
51
+ // Jump to 100% and then hide after showing completion
47
52
  setProgress(100);
48
53
  setTimeout(() => {
49
54
  setLoading(false);
50
- setProgress(0);
51
- }, 300);
55
+ setTimeout(() => {
56
+ setProgress(0);
57
+ }, 300); // Wait for fade out animation
58
+ }, 500); // Show 100% for half a second
52
59
  };
53
60
 
54
61
  useEffect(() => {
@@ -81,22 +88,32 @@ const PageProgress = () => {
81
88
  };
82
89
  }, [router.events]);
83
90
 
84
- if (!loading && progress === 0) {
91
+ if (!mounted) {
85
92
  return null;
86
93
  }
87
94
 
88
95
  return (
89
96
  <div
90
- className={`fixed top-0 left-0 w-full h-1 z-50 transition-opacity duration-300 ${
97
+ data-page-progress="root"
98
+ data-loading={loading}
99
+ data-progress={progress}
100
+ className={`fixed top-0 left-0 w-full transition-opacity duration-300 ${
91
101
  loading ? 'opacity-100' : 'opacity-0'
92
102
  }`}
103
+ style={{
104
+ zIndex: 99999,
105
+ height: '3px',
106
+ }}
93
107
  >
94
- <div className="w-full h-full bg-gray-200">
95
- <div
96
- className="h-full bg-blue-600 transition-all duration-200 ease-linear"
97
- style={{ width: `${progress}%` }}
98
- />
99
- </div>
108
+ <div
109
+ className="h-full transition-all duration-200 ease-linear"
110
+ style={{
111
+ width: `${progress}%`,
112
+ background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 50%, #3b82f6 100%)',
113
+ boxShadow: '0 0 10px rgba(59, 130, 246, 0.6), 0 0 20px rgba(59, 130, 246, 0.4), 0 0 30px rgba(59, 130, 246, 0.2)',
114
+ filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.8))',
115
+ }}
116
+ />
100
117
  </div>
101
118
  );
102
119
  };
@@ -4,3 +4,4 @@
4
4
 
5
5
  export { default as Seo } from './Seo';
6
6
  export { default as PageProgress } from './PageProgress';
7
+ export { ErrorBoundary } from './ErrorBoundary';
@@ -12,7 +12,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
12
12
  supportUrl,
13
13
  termsUrl,
14
14
  privacyUrl,
15
- enablePhoneAuth = true, // Default to true for backward compatibility
15
+ enablePhoneAuth = false, // Default to true for backward compatibility
16
16
  onIdentifierSuccess,
17
17
  onOTPSuccess,
18
18
  onError,
@@ -53,7 +53,7 @@ export const AuthHelp: React.FC<AuthHelpProps> = ({
53
53
  <div
54
54
  className={`flex items-center justify-between p-3 bg-muted/30 rounded-sm border border-border ${className}`}
55
55
  >
56
- <div className="flex items-center space-x-2">
56
+ <div className="flex items-center gap-2">
57
57
  {getChannelIcon()}
58
58
  <span className="text-sm text-muted-foreground">{getHelpText()}</span>
59
59
  </div>
@@ -78,13 +78,13 @@ export const AuthHelp: React.FC<AuthHelpProps> = ({
78
78
 
79
79
  return (
80
80
  <div
81
- className={`space-y-3 p-3 bg-muted/30 rounded-sm border border-border ${className}`}
81
+ className={`flex flex-col gap-3 p-3 bg-muted/30 rounded-sm border border-border ${className}`}
82
82
  >
83
- <div className="flex items-start space-x-2">
83
+ <div className="flex items-start gap-3">
84
84
  {getChannelIcon()}
85
- <div className="space-y-1">
85
+ <div className="flex flex-col gap-1">
86
86
  <h4 className="text-sm font-medium text-foreground">{helpData.title}</h4>
87
- <div className="text-xs text-muted-foreground space-y-0.5">
87
+ <div className="flex flex-col gap-0.5 text-xs text-muted-foreground">
88
88
  {helpData.tips.map((tip, index) => (
89
89
  <p key={index}>{tip}</p>
90
90
  ))}
@@ -10,9 +10,9 @@ export const AuthLayout: React.FC<AuthProps> = (props) => {
10
10
  return (
11
11
  <AuthProvider {...props}>
12
12
  <div
13
- className={`min-h-screen flex flex-col items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8 ${props.className || ''}`}
13
+ className={`flex flex-col items-center justify-center bg-background py-6 px-4 sm:py-12 sm:px-6 lg:px-8 ${props.className || ''}`}
14
14
  >
15
- <div className="max-w-md w-full space-y-8 flex-1 flex flex-col justify-center">
15
+ <div className="w-full sm:max-w-md space-y-8">
16
16
  {props.children}
17
17
 
18
18
  <AuthContent />
@@ -179,7 +179,7 @@ export const IdentifierForm: React.FC = () => {
179
179
  </TabsContent>
180
180
 
181
181
  {/* Terms and Conditions */}
182
- <div className="flex items-start space-x-3">
182
+ <div className="flex items-start gap-3">
183
183
  <Checkbox
184
184
  id="terms"
185
185
  checked={acceptedTerms}
@@ -262,7 +262,7 @@ export const IdentifierForm: React.FC = () => {
262
262
  </div>
263
263
 
264
264
  {/* Terms and Conditions */}
265
- <div className="flex items-start space-x-3">
265
+ <div className="flex items-start gap-3">
266
266
  <Checkbox
267
267
  id="terms-email"
268
268
  checked={acceptedTerms}
@@ -65,7 +65,7 @@ export function DashboardHeader() {
65
65
  };
66
66
 
67
67
  return (
68
- <header className="sticky top-0 py-2 z-50 h-16 flex items-center justify-between px-4 shrink-0 bg-background border-b border-border">
68
+ <header className="sticky top-0 py-2 z-10 h-16 flex items-center justify-between px-4 shrink-0 bg-background border-b border-border">
69
69
  {/* Left side */}
70
70
  <div className="flex items-center gap-4">
71
71
  <SidebarTrigger className="-ml-1" />
@@ -148,7 +148,7 @@ export function Footer() {
148
148
  </div>
149
149
 
150
150
  {/* Right Column - Footer Menu Sections */}
151
- <div className="grid grid-cols-2 gap-8">
151
+ <div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 flex-1">
152
152
  {footer.menuSections.map((section) => {
153
153
  // Single item section - render as direct link
154
154
  if (section.items.length === 1) {
@@ -76,6 +76,17 @@ export function MobileMenu() {
76
76
  closeMobileMenu();
77
77
  };
78
78
 
79
+ // Prepare menu sections before render
80
+ const singleItemSections = React.useMemo(
81
+ () => publicLayout.navigation.menuSections.filter(s => s.items.length === 1),
82
+ [publicLayout.navigation.menuSections]
83
+ );
84
+
85
+ const multipleItemsSections = React.useMemo(
86
+ () => publicLayout.navigation.menuSections.filter(s => s.items.length > 1),
87
+ [publicLayout.navigation.menuSections]
88
+ );
89
+
79
90
  if (!shouldRender) return null;
80
91
 
81
92
  // Portal to body to avoid z-index and positioning issues
@@ -138,37 +149,18 @@ export function MobileMenu() {
138
149
 
139
150
  {/* Navigation Sections */}
140
151
  <div className="space-y-6">
141
- {publicLayout.navigation.menuSections.map((section) => {
142
- // Single item section
143
- if (section.items.length === 1) {
144
- const item = section.items[0];
145
- if (!item) return null;
146
-
147
- return (
148
- <div key={section.title} className="space-y-2">
149
- <Link
150
- href={item.path}
151
- className={`block px-4 py-3 rounded-sm text-base font-medium transition-colors ${
152
- isActive(item.path)
153
- ? 'text-primary border border-primary/20 bg-primary/[0.1]'
154
- : 'text-muted-foreground hover:text-primary hover:bg-accent/50'
155
- }`}
156
- onClick={handleNavigate}
157
- >
158
- {item.label}
159
- </Link>
160
- </div>
161
- );
162
- }
163
-
164
- // Multiple items section
165
- return (
166
- <div key={section.title} className="space-y-3">
167
- <h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
168
- {section.title}
169
- </h3>
170
- <div className="space-y-1">
171
- {section.items.map((item) => (
152
+ {/* Group all single-item sections into "Menu" */}
153
+ {singleItemSections.length > 0 && (
154
+ <div className="space-y-3">
155
+ <h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
156
+ Menu
157
+ </h3>
158
+ <div className="space-y-1">
159
+ {singleItemSections.map((section) => {
160
+ const item = section.items[0];
161
+ if (!item) return null;
162
+
163
+ return (
172
164
  <Link
173
165
  key={item.path}
174
166
  href={item.path}
@@ -181,15 +173,40 @@ export function MobileMenu() {
181
173
  >
182
174
  {item.label}
183
175
  </Link>
184
- ))}
185
- </div>
176
+ );
177
+ })}
178
+ </div>
179
+ </div>
180
+ )}
181
+
182
+ {/* Render multiple-items sections normally */}
183
+ {multipleItemsSections.map((section) => (
184
+ <div key={section.title} className="space-y-3">
185
+ <h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
186
+ {section.title}
187
+ </h3>
188
+ <div className="space-y-1">
189
+ {section.items.map((item) => (
190
+ <Link
191
+ key={item.path}
192
+ href={item.path}
193
+ className={`block px-4 py-3 rounded-sm text-base font-medium transition-colors ${
194
+ isActive(item.path)
195
+ ? 'bg-accent text-accent-foreground'
196
+ : 'text-foreground hover:bg-accent hover:text-accent-foreground'
197
+ }`}
198
+ onClick={handleNavigate}
199
+ >
200
+ {item.label}
201
+ </Link>
202
+ ))}
186
203
  </div>
187
- );
188
- })}
204
+ </div>
205
+ ))}
189
206
  </div>
190
207
 
191
208
  {/* Bottom spacer */}
192
- <div className="h-20"></div>
209
+ <div style={{ height: '15vh' }}></div>
193
210
  </div>
194
211
  </div>
195
212
  </div>