@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.
- package/package.json +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +126 -28
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +99 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +28 -11
- package/src/layouts/AppLayout/components/index.ts +1 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +1 -1
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +5 -5
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +2 -2
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +2 -2
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +1 -1
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +1 -1
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +53 -36
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +64 -52
- package/src/layouts/AppLayout/types/config.ts +22 -0
- package/src/layouts/ErrorLayout/ErrorLayout.tsx +169 -0
- package/src/layouts/ErrorLayout/errorConfig.tsx +152 -0
- package/src/layouts/ErrorLayout/index.ts +8 -0
- package/src/layouts/UILayout/README.md +267 -0
- package/src/layouts/UILayout/REFACTORING.md +331 -0
- package/src/layouts/UILayout/UIGuideApp.tsx +18 -0
- package/src/layouts/UILayout/UIGuideLanding.tsx +198 -0
- package/src/layouts/UILayout/UIGuideView.tsx +61 -0
- package/src/layouts/UILayout/UILayout.tsx +122 -0
- package/src/layouts/UILayout/components/AutoComponentDemo.tsx +77 -0
- package/src/layouts/UILayout/components/CategoryRenderer.tsx +45 -0
- package/src/layouts/UILayout/components/Header.tsx +114 -0
- package/src/layouts/UILayout/components/MobileOverlay.tsx +33 -0
- package/src/layouts/UILayout/components/Sidebar.tsx +195 -0
- package/src/layouts/UILayout/components/TailwindGuideRenderer.tsx +138 -0
- package/src/layouts/UILayout/config/ai-export.config.ts +80 -0
- package/src/layouts/UILayout/config/categories.config.tsx +114 -0
- package/src/layouts/UILayout/config/components/blocks.config.tsx +233 -0
- package/src/layouts/UILayout/config/components/data.config.tsx +308 -0
- package/src/layouts/UILayout/config/components/feedback.config.tsx +246 -0
- package/src/layouts/UILayout/config/components/forms.config.tsx +171 -0
- package/src/layouts/UILayout/config/components/hooks.config.tsx +131 -0
- package/src/layouts/UILayout/config/components/index.ts +69 -0
- package/src/layouts/UILayout/config/components/layout.config.tsx +133 -0
- package/src/layouts/UILayout/config/components/navigation.config.tsx +244 -0
- package/src/layouts/UILayout/config/components/overlay.config.tsx +561 -0
- package/src/layouts/UILayout/config/components/specialized.config.tsx +125 -0
- package/src/layouts/UILayout/config/components/types.ts +14 -0
- package/src/layouts/UILayout/config/index.ts +42 -0
- package/src/layouts/UILayout/config/tailwind.config.ts +77 -0
- package/src/layouts/UILayout/constants.ts +23 -0
- package/src/layouts/UILayout/context/ShowcaseContext.tsx +53 -0
- package/src/layouts/UILayout/context/index.ts +1 -0
- package/src/layouts/UILayout/index.ts +64 -0
- package/src/layouts/UILayout/types.ts +13 -0
- 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
|
|
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
|
|
57
|
-
"@djangocfg/og-image": "^1.0
|
|
58
|
-
"@djangocfg/ui": "^1.0
|
|
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
|
|
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({
|
|
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
|
|
105
|
+
// Auth routes: render inside PublicLayout with Navigation/Footer
|
|
69
106
|
case 'auth':
|
|
70
|
-
return
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
51
|
+
// Jump to 100% and then hide after showing completion
|
|
47
52
|
setProgress(100);
|
|
48
53
|
setTimeout(() => {
|
|
49
54
|
setLoading(false);
|
|
50
|
-
|
|
51
|
-
|
|
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 (!
|
|
91
|
+
if (!mounted) {
|
|
85
92
|
return null;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
return (
|
|
89
96
|
<div
|
|
90
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
};
|
|
@@ -12,7 +12,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
12
12
|
supportUrl,
|
|
13
13
|
termsUrl,
|
|
14
14
|
privacyUrl,
|
|
15
|
-
enablePhoneAuth =
|
|
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
|
|
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={`
|
|
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
|
|
83
|
+
<div className="flex items-start gap-3">
|
|
84
84
|
{getChannelIcon()}
|
|
85
|
-
<div className="
|
|
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
|
|
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={`
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
209
|
+
<div style={{ height: '15vh' }}></div>
|
|
193
210
|
</div>
|
|
194
211
|
</div>
|
|
195
212
|
</div>
|