@djangocfg/layouts 2.0.9 → 2.0.11

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.11",
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.41",
96
+ "@djangocfg/centrifugo": "^1.4.41",
97
+ "@djangocfg/ui": "^1.4.41",
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.41",
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':
@@ -12,7 +12,4 @@ export * from './errors';
12
12
  export * from './RedirectPage';
13
13
 
14
14
  // ErrorsTracker
15
- export * from './errors/ErrorsTracker';
16
-
17
- // UpdateNotifier
18
- export * from './UpdateNotifier';
15
+ export * from './errors/ErrorsTracker';
@@ -46,8 +46,7 @@ import { AuthProvider, type AuthConfig } from '../../auth/context';
46
46
  import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
47
47
  import { AnalyticsProvider } from '../../snippets/Analytics';
48
48
  import { PageProgress } from '../../components/core/PageProgress';
49
- import { UpdateNotifier } from '../../components/UpdateNotifier';
50
- import { Suspense } from '../../components/core';
49
+ import { Suspense, ClientOnly } from '../../components/core';
51
50
 
52
51
  export type LayoutMode = 'public' | 'private' | 'admin';
53
52
 
@@ -139,31 +138,25 @@ export interface AppLayoutProps {
139
138
  supportEmail?: string;
140
139
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
141
140
  };
142
-
143
- /** Update notifier configuration */
144
- updateNotifier?: {
145
- enabled?: boolean;
146
- };
147
141
  }
148
142
 
149
143
  /**
150
144
  * AppLayout Content - Renders layout with all providers
145
+ *
146
+ * SSR is only enabled for publicLayout.
147
+ * Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
151
148
  */
152
149
  function AppLayoutContent({
153
150
  children,
154
151
  publicLayout,
155
152
  privateLayout,
156
153
  adminLayout,
157
- theme,
158
- auth,
159
154
  analytics,
160
155
  centrifugo,
161
- errorTracking,
162
156
  errorBoundary,
163
- updateNotifier,
164
157
  }: AppLayoutProps) {
165
158
  const pathname = usePathname();
166
-
159
+
167
160
  const layoutMode = useMemo(
168
161
  () => determineLayoutMode(
169
162
  pathname,
@@ -173,46 +166,51 @@ function AppLayoutContent({
173
166
  ),
174
167
  [pathname, adminLayout, privateLayout, publicLayout]
175
168
  );
176
-
169
+
177
170
  const enableErrorBoundary = errorBoundary?.enabled !== false;
178
-
171
+
179
172
  // Render appropriate layout based on mode
180
173
  const renderLayout = () => {
181
174
  switch (layoutMode) {
182
175
  case 'admin':
183
176
  if (!adminLayout && privateLayout) {
184
- // Fallback to private layout if no admin layout provided
185
177
  return (
186
- <Suspense>
187
- <privateLayout.component>{children}</privateLayout.component>
188
- </Suspense>
178
+ <ClientOnly>
179
+ <Suspense>
180
+ <privateLayout.component>{children}</privateLayout.component>
181
+ </Suspense>
182
+ </ClientOnly>
189
183
  );
190
184
  }
191
185
  if (!adminLayout) {
192
186
  return children;
193
187
  }
194
188
  return (
195
- <Suspense>
196
- <adminLayout.component>{children}</adminLayout.component>
197
- </Suspense>
189
+ <ClientOnly>
190
+ <Suspense>
191
+ <adminLayout.component>{children}</adminLayout.component>
192
+ </Suspense>
193
+ </ClientOnly>
198
194
  );
199
-
195
+
200
196
  case 'private':
201
197
  if (!privateLayout) {
202
- // Fallback to public if no private layout provided
203
198
  if (publicLayout) {
204
199
  return <publicLayout.component>{children}</publicLayout.component>;
205
200
  }
206
201
  return children;
207
202
  }
208
203
  return (
209
- <Suspense>
210
- <privateLayout.component>{children}</privateLayout.component>
211
- </Suspense>
204
+ <ClientOnly>
205
+ <Suspense>
206
+ <privateLayout.component>{children}</privateLayout.component>
207
+ </Suspense>
208
+ </ClientOnly>
212
209
  );
213
-
210
+
214
211
  case 'public':
215
212
  default:
213
+ // Public layout renders with SSR (no ClientOnly wrapper)
216
214
  if (!publicLayout) {
217
215
  return children;
218
216
  }
@@ -252,9 +250,6 @@ function AppLayoutContent({
252
250
  <>
253
251
  {content}
254
252
  <PageProgress />
255
- <UpdateNotifier
256
- enabled={updateNotifier?.enabled !== false}
257
- />
258
253
  <Toaster />
259
254
  </>
260
255
  );
@@ -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">
@@ -1,178 +0,0 @@
1
- /**
2
- * Update Notifier Component
3
- *
4
- * Checks npm registry for @djangocfg package updates
5
- * Shows toast notification when new version is available
6
- * Uses localStorage to cache check and avoid spam
7
- */
8
-
9
- 'use client';
10
-
11
- import React, { useEffect, useState, useRef } from 'react';
12
- import consola from 'consola';
13
- import { toast } from '@djangocfg/ui/hooks';
14
- import { useLocalStorage } from '@djangocfg/ui/hooks';
15
-
16
- const PACKAGE_NAME = '@djangocfg/layouts';
17
- const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
18
- const CACHE_KEY = 'djangocfg_update_check';
19
-
20
- interface UpdateCheckCache {
21
- lastCheck: number;
22
- latestVersion: string;
23
- dismissed: boolean;
24
- }
25
-
26
- const defaultCache: UpdateCheckCache = {
27
- lastCheck: 0,
28
- latestVersion: '',
29
- dismissed: false,
30
- };
31
-
32
- export interface UpdateNotifierProps {
33
- /**
34
- * Enable update notifications
35
- * @default false
36
- */
37
- enabled?: boolean;
38
- }
39
-
40
- /**
41
- * Get current package version from package.json
42
- * Uses require in runtime to avoid TypeScript rootDir issues
43
- */
44
- function getCurrentVersion(): string | null {
45
- try {
46
- // Use require in runtime (works in both dev and build)
47
- // eslint-disable-next-line @typescript-eslint/no-require-imports
48
- const packageJson = require('../../../package.json');
49
- return packageJson.version || null;
50
- } catch (error) {
51
- consola.warn('[UpdateNotifier] Failed to load package.json:', error);
52
- return null;
53
- }
54
- }
55
-
56
- /**
57
- * Compare semver versions
58
- * Returns true if newVersion > currentVersion
59
- */
60
- function isNewerVersion(current: string, latest: string): boolean {
61
- const parseCurrent = current.split('.').map(Number);
62
- const parseLatest = latest.split('.').map(Number);
63
-
64
- for (let i = 0; i < 3; i++) {
65
- if (parseLatest[i] > parseCurrent[i]) return true;
66
- if (parseLatest[i] < parseCurrent[i]) return false;
67
- }
68
- return false;
69
- }
70
-
71
- /**
72
- * Fetch latest version from npm registry
73
- */
74
- async function fetchLatestVersion(): Promise<string | null> {
75
- try {
76
- const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
77
- method: 'GET',
78
- headers: { 'Accept': 'application/json' },
79
- });
80
-
81
- if (!response.ok) return null;
82
-
83
- const data = await response.json();
84
- return data.version || null;
85
- } catch (error) {
86
- consola.warn('[UpdateNotifier] Failed to check for updates:', error);
87
- return null;
88
- }
89
- }
90
-
91
- export function UpdateNotifier({ enabled = false }: UpdateNotifierProps) {
92
- const [checked, setChecked] = useState(false);
93
- const [cache, setCache] = useLocalStorage<UpdateCheckCache | null>(CACHE_KEY, null);
94
- const cacheRef = useRef(cache);
95
-
96
- // Keep cacheRef in sync with cache
97
- useEffect(() => {
98
- cacheRef.current = cache;
99
- }, [cache]);
100
-
101
- useEffect(() => {
102
- if (!enabled || checked || typeof window === 'undefined') return;
103
-
104
- const checkForUpdates = async () => {
105
- // Get current version from package.json
106
- const currentVersion = getCurrentVersion();
107
- if (!currentVersion) {
108
- setChecked(true);
109
- return;
110
- }
111
-
112
- // Check cache first (use ref to get latest value)
113
- const now = Date.now();
114
- const cachedData = cacheRef.current || defaultCache;
115
-
116
- // If we checked recently, skip
117
- if (cachedData && cachedData.lastCheck > 0 && (now - cachedData.lastCheck) < CHECK_INTERVAL_MS) {
118
- // Show notification if there's an update and it wasn't dismissed
119
- if (cachedData.latestVersion && !cachedData.dismissed && isNewerVersion(currentVersion, cachedData.latestVersion)) {
120
- showUpdateNotification(currentVersion, cachedData.latestVersion, setCache);
121
- }
122
- setChecked(true);
123
- return;
124
- }
125
-
126
- // Fetch latest version from npm
127
- const latestVersion = await fetchLatestVersion();
128
-
129
- if (!latestVersion) {
130
- setChecked(true);
131
- return;
132
- }
133
-
134
- // Update cache
135
- setCache({
136
- lastCheck: now,
137
- latestVersion,
138
- dismissed: false,
139
- });
140
-
141
- // Show notification if newer version available
142
- if (isNewerVersion(currentVersion, latestVersion)) {
143
- showUpdateNotification(currentVersion, latestVersion, setCache);
144
- }
145
-
146
- setChecked(true);
147
- };
148
-
149
- // Check after a short delay to not block initial render
150
- const timer = setTimeout(checkForUpdates, 2000);
151
-
152
- return () => clearTimeout(timer);
153
- // eslint-disable-next-line react-hooks/exhaustive-deps
154
- }, [enabled, checked]);
155
-
156
- return null; // This component doesn't render anything
157
- }
158
-
159
- /**
160
- * Show update notification toast
161
- */
162
- function showUpdateNotification(
163
- currentVersion: string,
164
- latestVersion: string,
165
- setCache: (value: UpdateCheckCache | null | ((val: UpdateCheckCache | null) => UpdateCheckCache | null)) => void
166
- ) {
167
- toast({
168
- title: `📦 Update Available`,
169
- description: `New version ${latestVersion} of @djangocfg packages is available. You're using ${currentVersion}. Run: pnpm update @djangocfg/layouts@latest`,
170
- duration: 10000,
171
- });
172
-
173
- // Mark as dismissed in cache after showing
174
- setCache((prev) => {
175
- if (!prev) return null;
176
- return { ...prev, dismissed: true };
177
- });
178
- }
@@ -1,2 +0,0 @@
1
- export { UpdateNotifier } from './UpdateNotifier';
2
- export type { UpdateNotifierProps } from './UpdateNotifier';