@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.9",
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.39",
96
- "@djangocfg/centrifugo": "^1.4.39",
97
- "@djangocfg/ui": "^1.4.39",
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.39",
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
- <path
78
- strokeLinecap="round"
79
- strokeLinejoin="round"
80
- strokeWidth={1.5}
81
- d="M18.364 18.364A9 9 0 005.636 5.636m12.728 0l-6.849 6.849m0 0l-6.849-6.849m6.849 6.849V21m0 0h7.5M12 21v-7.5M7.5 21H3m7.5 0h7.5M3 18V9a4.5 4.5 0 014.5-4.5h9A4.5 4.5 0 0121 9v9a4.5 4.5 0 01-4.5 4.5h-9A4.5 4.5 0 013 18z"
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
- <Suspense>
187
- <privateLayout.component>{children}</privateLayout.component>
188
- </Suspense>
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
- <Suspense>
196
- <adminLayout.component>{children}</adminLayout.component>
197
- </Suspense>
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
- <Suspense>
210
- <privateLayout.component>{children}</privateLayout.component>
211
- </Suspense>
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, Button, ButtonLink } from '@djangocfg/ui/components';
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, user } = useAuth();
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">