@djangocfg/layouts 2.0.9 → 2.0.10
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": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"check": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@djangocfg/api": "^1.4.
|
|
96
|
-
"@djangocfg/centrifugo": "^1.4.
|
|
97
|
-
"@djangocfg/ui": "^1.4.
|
|
95
|
+
"@djangocfg/api": "^1.4.40",
|
|
96
|
+
"@djangocfg/centrifugo": "^1.4.40",
|
|
97
|
+
"@djangocfg/ui": "^1.4.40",
|
|
98
98
|
"@hookform/resolvers": "^5.2.0",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"uuid": "^11.1.0"
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
117
|
+
"@djangocfg/typescript-config": "^1.4.40",
|
|
118
118
|
"@types/node": "^24.7.2",
|
|
119
119
|
"@types/react": "19.2.2",
|
|
120
120
|
"@types/react-dom": "19.2.1",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientOnly Component
|
|
3
|
+
*
|
|
4
|
+
* Renders children only on the client side to prevent SSR hydration mismatch.
|
|
5
|
+
* Shows a fallback (loading state) during SSR and initial client mount.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { ClientOnly } from '@djangocfg/layouts/components';
|
|
10
|
+
*
|
|
11
|
+
* // With default loading fallback
|
|
12
|
+
* <ClientOnly>
|
|
13
|
+
* <ComponentThatUsesClientOnlyAPIs />
|
|
14
|
+
* </ClientOnly>
|
|
15
|
+
*
|
|
16
|
+
* // With custom fallback
|
|
17
|
+
* <ClientOnly fallback={<MyCustomLoader />}>
|
|
18
|
+
* <ComponentThatUsesClientOnlyAPIs />
|
|
19
|
+
* </ClientOnly>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import { useState, useEffect, ReactNode } from 'react';
|
|
26
|
+
import { Preloader } from '@djangocfg/ui/components';
|
|
27
|
+
|
|
28
|
+
export interface ClientOnlyProps {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Fallback to show during SSR and initial mount
|
|
32
|
+
* @default Preloader with "Loading..." text
|
|
33
|
+
*/
|
|
34
|
+
fallback?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default fallback - fullscreen preloader
|
|
39
|
+
*/
|
|
40
|
+
const defaultFallback = (
|
|
41
|
+
<Preloader
|
|
42
|
+
variant="fullscreen"
|
|
43
|
+
text="Loading..."
|
|
44
|
+
size="lg"
|
|
45
|
+
backdrop={true}
|
|
46
|
+
backdropOpacity={80}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ClientOnly - Prevents SSR hydration mismatch
|
|
52
|
+
*
|
|
53
|
+
* Use this to wrap components that:
|
|
54
|
+
* - Access browser-only APIs (window, localStorage, etc.)
|
|
55
|
+
* - Have different initial state on server vs client
|
|
56
|
+
* - Use authentication state that differs between SSR and CSR
|
|
57
|
+
*/
|
|
58
|
+
export function ClientOnly({
|
|
59
|
+
children,
|
|
60
|
+
fallback = defaultFallback,
|
|
61
|
+
}: ClientOnlyProps) {
|
|
62
|
+
const [mounted, setMounted] = useState(false);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setMounted(true);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
if (!mounted) {
|
|
69
|
+
return <>{fallback}</>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <>{children}</>;
|
|
73
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Core components exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export { ClientOnly } from './ClientOnly';
|
|
6
|
+
export type { ClientOnlyProps } from './ClientOnly';
|
|
5
7
|
export { JsonLd } from './JsonLd';
|
|
6
8
|
export { LucideIcon } from './LucideIcon';
|
|
7
9
|
export type { LucideIconProps } from './LucideIcon';
|
|
@@ -73,13 +73,12 @@ function getErrorIcon(code?: string | number): React.ReactNode {
|
|
|
73
73
|
viewBox="0 0 24 24"
|
|
74
74
|
aria-hidden="true"
|
|
75
75
|
>
|
|
76
|
-
{/* Server Error Icon */}
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/>
|
|
76
|
+
{/* Server Error Icon - Server with X */}
|
|
77
|
+
<rect x="2" y="3" width="20" height="7" rx="1" strokeWidth={1.5} />
|
|
78
|
+
<rect x="2" y="14" width="20" height="7" rx="1" strokeWidth={1.5} />
|
|
79
|
+
<circle cx="6" cy="6.5" r="1" fill="currentColor" />
|
|
80
|
+
<circle cx="6" cy="17.5" r="1" fill="currentColor" />
|
|
81
|
+
<path strokeLinecap="round" strokeWidth={1.5} d="M22 2L2 22" />
|
|
83
82
|
</svg>
|
|
84
83
|
);
|
|
85
84
|
case '403':
|
|
@@ -47,7 +47,7 @@ import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig
|
|
|
47
47
|
import { AnalyticsProvider } from '../../snippets/Analytics';
|
|
48
48
|
import { PageProgress } from '../../components/core/PageProgress';
|
|
49
49
|
import { UpdateNotifier } from '../../components/UpdateNotifier';
|
|
50
|
-
import { Suspense } from '../../components/core';
|
|
50
|
+
import { Suspense, ClientOnly } from '../../components/core';
|
|
51
51
|
|
|
52
52
|
export type LayoutMode = 'public' | 'private' | 'admin';
|
|
53
53
|
|
|
@@ -148,22 +148,22 @@ export interface AppLayoutProps {
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* AppLayout Content - Renders layout with all providers
|
|
151
|
+
*
|
|
152
|
+
* SSR is only enabled for publicLayout.
|
|
153
|
+
* Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
|
|
151
154
|
*/
|
|
152
155
|
function AppLayoutContent({
|
|
153
156
|
children,
|
|
154
157
|
publicLayout,
|
|
155
158
|
privateLayout,
|
|
156
159
|
adminLayout,
|
|
157
|
-
theme,
|
|
158
|
-
auth,
|
|
159
160
|
analytics,
|
|
160
161
|
centrifugo,
|
|
161
|
-
errorTracking,
|
|
162
162
|
errorBoundary,
|
|
163
163
|
updateNotifier,
|
|
164
164
|
}: AppLayoutProps) {
|
|
165
165
|
const pathname = usePathname();
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
const layoutMode = useMemo(
|
|
168
168
|
() => determineLayoutMode(
|
|
169
169
|
pathname,
|
|
@@ -173,46 +173,51 @@ function AppLayoutContent({
|
|
|
173
173
|
),
|
|
174
174
|
[pathname, adminLayout, privateLayout, publicLayout]
|
|
175
175
|
);
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
const enableErrorBoundary = errorBoundary?.enabled !== false;
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
// Render appropriate layout based on mode
|
|
180
180
|
const renderLayout = () => {
|
|
181
181
|
switch (layoutMode) {
|
|
182
182
|
case 'admin':
|
|
183
183
|
if (!adminLayout && privateLayout) {
|
|
184
|
-
// Fallback to private layout if no admin layout provided
|
|
185
184
|
return (
|
|
186
|
-
<
|
|
187
|
-
<
|
|
188
|
-
|
|
185
|
+
<ClientOnly>
|
|
186
|
+
<Suspense>
|
|
187
|
+
<privateLayout.component>{children}</privateLayout.component>
|
|
188
|
+
</Suspense>
|
|
189
|
+
</ClientOnly>
|
|
189
190
|
);
|
|
190
191
|
}
|
|
191
192
|
if (!adminLayout) {
|
|
192
193
|
return children;
|
|
193
194
|
}
|
|
194
195
|
return (
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
196
|
+
<ClientOnly>
|
|
197
|
+
<Suspense>
|
|
198
|
+
<adminLayout.component>{children}</adminLayout.component>
|
|
199
|
+
</Suspense>
|
|
200
|
+
</ClientOnly>
|
|
198
201
|
);
|
|
199
|
-
|
|
202
|
+
|
|
200
203
|
case 'private':
|
|
201
204
|
if (!privateLayout) {
|
|
202
|
-
// Fallback to public if no private layout provided
|
|
203
205
|
if (publicLayout) {
|
|
204
206
|
return <publicLayout.component>{children}</publicLayout.component>;
|
|
205
207
|
}
|
|
206
208
|
return children;
|
|
207
209
|
}
|
|
208
210
|
return (
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
211
|
+
<ClientOnly>
|
|
212
|
+
<Suspense>
|
|
213
|
+
<privateLayout.component>{children}</privateLayout.component>
|
|
214
|
+
</Suspense>
|
|
215
|
+
</ClientOnly>
|
|
212
216
|
);
|
|
213
|
-
|
|
217
|
+
|
|
214
218
|
case 'public':
|
|
215
219
|
default:
|
|
220
|
+
// Public layout renders with SSR (no ClientOnly wrapper)
|
|
216
221
|
if (!publicLayout) {
|
|
217
222
|
return children;
|
|
218
223
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Private Layout
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Layout for authenticated user pages (dashboard, profile, etc.)
|
|
5
5
|
* Import and use directly with props - no complex configs needed!
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Responsive sidebar with mobile burger menu
|
|
9
9
|
* - Keyboard shortcut (Ctrl/Cmd + B) to toggle sidebar
|
|
10
10
|
* - Header with sidebar trigger and user menu
|
|
11
11
|
* - Configurable content padding
|
|
12
|
-
*
|
|
12
|
+
* - NO SSR - renders only on client to avoid hydration mismatch
|
|
13
|
+
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```tsx
|
|
15
16
|
* import { PrivateLayout } from '@djangocfg/layouts';
|
|
16
|
-
*
|
|
17
|
+
*
|
|
17
18
|
* <PrivateLayout
|
|
18
19
|
* sidebar={{
|
|
19
20
|
* items: [
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
* >
|
|
29
30
|
* {children}
|
|
30
31
|
* </PrivateLayout>
|
|
31
|
-
*
|
|
32
|
+
*
|
|
32
33
|
* Note: User data (name, email, avatar) is automatically loaded from useAuth() context
|
|
33
34
|
* Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar
|
|
34
35
|
* ```
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
'use client';
|
|
38
39
|
|
|
39
40
|
import React, { ReactNode } from 'react';
|
|
40
|
-
import { SidebarProvider, SidebarInset, Preloader,
|
|
41
|
+
import { SidebarProvider, SidebarInset, Preloader, ButtonLink } from '@djangocfg/ui/components';
|
|
41
42
|
import { useAuth } from '../../auth';
|
|
42
43
|
import { PrivateSidebar, PrivateHeader, PrivateContent } from './components';
|
|
43
44
|
import type { LucideIcon as LucideIconType } from 'lucide-react';
|
|
@@ -76,20 +77,10 @@ export function PrivateLayout({
|
|
|
76
77
|
header,
|
|
77
78
|
contentPadding = 'default',
|
|
78
79
|
}: PrivateLayoutProps) {
|
|
79
|
-
const { isAuthenticated, isLoading
|
|
80
|
-
|
|
81
|
-
// Debug logging in development
|
|
82
|
-
if (process.env.NODE_ENV === 'development') {
|
|
83
|
-
console.log('[PrivateLayout] Render state:', {
|
|
84
|
-
isLoading,
|
|
85
|
-
isAuthenticated,
|
|
86
|
-
hasUser: !!user,
|
|
87
|
-
hasSidebar: !!sidebar,
|
|
88
|
-
hasHeader: !!header,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
80
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
91
81
|
|
|
92
82
|
// Show loading state while auth is being checked
|
|
83
|
+
// Note: SSR hydration is handled by ClientOnly wrapper in AppLayout
|
|
93
84
|
if (isLoading) {
|
|
94
85
|
return (
|
|
95
86
|
<Preloader
|
|
@@ -104,9 +95,6 @@ export function PrivateLayout({
|
|
|
104
95
|
|
|
105
96
|
// Don't render if user is not authenticated
|
|
106
97
|
if (!isAuthenticated) {
|
|
107
|
-
if (process.env.NODE_ENV === 'development') {
|
|
108
|
-
console.warn('[PrivateLayout] User not authenticated, returning null');
|
|
109
|
-
}
|
|
110
98
|
return (
|
|
111
99
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
|
112
100
|
<h3 className="text-2xl font-bold">
|