@djangocfg/layouts 2.0.8 → 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.8",
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.38",
96
- "@djangocfg/centrifugo": "^1.4.38",
97
- "@djangocfg/ui": "^1.4.38",
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.38",
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",
@@ -8,7 +8,7 @@ import React, {
8
8
 
9
9
  import { api, Enums } from '@djangocfg/api';
10
10
  import { useAccountsContext, AccountsProvider } from './AccountsContext';
11
- import { useLocalStorage, useQueryParams, useRouter } from '@djangocfg/ui/hooks';
11
+ import { useLocalStorage, useQueryParams, useCfgRouter } from '@djangocfg/ui/hooks';
12
12
  import { getCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
13
13
 
14
14
  import { authLogger } from '../../utils/logger';
@@ -50,7 +50,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
50
50
  });
51
51
 
52
52
  const [initialized, setInitialized] = useState(false);
53
- const router = useRouter();
53
+ const router = useCfgRouter();
54
54
  const pathname = usePathname();
55
55
  const queryParams = useQueryParams();
56
56
 
@@ -362,13 +362,14 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
362
362
  }
363
363
 
364
364
  // Handle redirect logic here
365
+ // Use hardPush for full page reload - ensures all React contexts reinitialize
365
366
  const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
366
367
 
367
368
  if (redirectUrl && redirectUrl !== defaultCallback) {
368
369
  clearRedirectUrl();
369
- router.push(redirectUrl);
370
+ router.hardPush(redirectUrl);
370
371
  } else {
371
- router.push(defaultCallback);
372
+ router.hardPush(defaultCallback);
372
373
  }
373
374
 
374
375
  return {
@@ -460,7 +461,11 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
460
461
  accounts.logout(); // Clear tokens and profile
461
462
  setInitialized(true);
462
463
  setIsLoading(false);
463
- pushToDefaultAuthCallbackUrl();
464
+
465
+ // Use hardReplace for full page reload + replace history
466
+ // This ensures contexts reinitialize AND back button won't return to protected page
467
+ const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
468
+ router.hardReplace(authCallbackUrl);
464
469
  };
465
470
 
466
471
  // Use config.onConfirm if provided, otherwise use a simple confirm
@@ -482,7 +487,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
482
487
  performLogout();
483
488
  }
484
489
  }
485
- }, [accounts, pushToDefaultAuthCallbackUrl]);
490
+ }, [accounts, config?.routes?.defaultAuthCallback, router]);
486
491
 
487
492
  // Redirect URL methods
488
493
  const getSavedRedirectUrl = useCallback((): string | null => {
@@ -3,7 +3,7 @@
3
3
  import { useEffect } from 'react';
4
4
 
5
5
  import { useAuth } from '../context';
6
- import { useRouter } from '@djangocfg/ui/hooks';
6
+ import { useCfgRouter } from '@djangocfg/ui/hooks';
7
7
 
8
8
  interface UseAuthGuardOptions {
9
9
  redirectTo?: string;
@@ -13,7 +13,7 @@ interface UseAuthGuardOptions {
13
13
  export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
14
14
  const { redirectTo = '/auth', requireAuth = true } = options;
15
15
  const { isAuthenticated, isLoading } = useAuth();
16
- const router = useRouter();
16
+ const router = useCfgRouter();
17
17
 
18
18
  useEffect(() => {
19
19
  if (!isLoading && requireAuth && !isAuthenticated) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect } from 'react';
4
4
  import { usePathname } from 'next/navigation';
5
- import { useQueryParams, useRouter } from '@djangocfg/ui/hooks';
5
+ import { useQueryParams, useCfgRouter } from '@djangocfg/ui/hooks';
6
6
  import { authLogger } from '../../utils/logger';
7
7
 
8
8
  export interface UseAutoAuthOptions {
@@ -18,7 +18,7 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
18
18
  const { onOTPDetected, cleanupUrl = true } = options;
19
19
  const queryParams = useQueryParams();
20
20
  const pathname = usePathname();
21
- const router = useRouter();
21
+ const router = useCfgRouter();
22
22
 
23
23
  const isReady = !!pathname && !!queryParams.get('otp');
24
24
  const hasOTP = !!(queryParams.get('otp'));
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
4
 
6
5
  import { api } from '@djangocfg/api';
6
+ import { useCfgRouter } from '@djangocfg/ui/hooks';
7
7
  import { authLogger } from '../../utils/logger';
8
8
  import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../snippets/Analytics';
9
9
 
@@ -42,7 +42,7 @@ export interface UseGithubAuthReturn {
42
42
  */
43
43
  export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
44
44
  const { sourceUrl, onSuccess, onError, redirectUrl } = options;
45
- const router = useRouter();
45
+ const router = useCfgRouter();
46
46
 
47
47
  const [isLoading, setIsLoading] = useState(false);
48
48
  const [error, setError] = useState<string | null>(null);
@@ -155,8 +155,9 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
155
155
  onSuccess?.(response.user, response.is_new_user || false);
156
156
 
157
157
  // Redirect to dashboard or specified URL
158
+ // Use hardPush for full page reload - ensures all React contexts reinitialize
158
159
  const finalRedirectUrl = redirectUrl || '/dashboard';
159
- router.push(finalRedirectUrl);
160
+ router.hardPush(finalRedirectUrl);
160
161
 
161
162
  } catch (err) {
162
163
  const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect } from 'react';
4
4
  import { useAuth } from '../../auth';
5
- import { useRouter } from '@djangocfg/ui/hooks';
5
+ import { useCfgRouter } from '@djangocfg/ui/hooks';
6
6
  import { Preloader } from '@djangocfg/ui/components';
7
7
 
8
8
  export interface RedirectPageProps {
@@ -49,7 +49,7 @@ export function RedirectPage({
49
49
  loadingText = 'Loading...',
50
50
  }: RedirectPageProps) {
51
51
  const { isAuthenticated } = useAuth();
52
- const router = useRouter();
52
+ const router = useCfgRouter();
53
53
 
54
54
  useEffect(() => {
55
55
  if (!isAuthenticated) {
@@ -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">
@@ -10,7 +10,7 @@ import {
10
10
  DialogHeader,
11
11
  DialogTitle,
12
12
  } from '@djangocfg/ui/components';
13
- import { useEventListener, useRouter } from '@djangocfg/ui/hooks';
13
+ import { useEventListener, useCfgRouter } from '@djangocfg/ui/hooks';
14
14
 
15
15
  // Re-export events for backwards compatibility
16
16
  export const DIALOG_EVENTS = {
@@ -31,7 +31,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
31
31
  }) => {
32
32
  const [open, setOpen] = useState(false);
33
33
  const [message, setMessage] = useState<string>('Please sign in to continue');
34
- const router = useRouter();
34
+ const router = useCfgRouter();
35
35
 
36
36
  // Listen for open auth dialog event
37
37
  useEventListener(DIALOG_EVENTS.OPEN_AUTH_DIALOG, (payload: any) => {
@@ -66,20 +66,7 @@ export interface AIChatWidgetProps extends ChatWidgetConfig {
66
66
  * Internal AI chat widget that uses context
67
67
  */
68
68
  const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className }) => {
69
- const {
70
- messages,
71
- isLoading,
72
- isMinimized,
73
- config,
74
- displayMode,
75
- isMobile,
76
- sendMessage,
77
- openChat,
78
- closeChat,
79
- toggleMinimize,
80
- setDisplayMode,
81
- stopStreaming,
82
- } = useAIChatContext();
69
+ const { config, displayMode, openChat } = useAIChatContext();
83
70
 
84
71
  // Use layout hook for consistent positioning
85
72
  const { getFabStyles, getFloatingStyles } = useChatLayout();
@@ -173,17 +160,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
173
160
  if (displayMode === 'sidebar') {
174
161
  return (
175
162
  <Portal>
176
- <ChatSidebar
177
- messages={messages}
178
- isLoading={isLoading}
179
- onSendMessage={sendMessage}
180
- onClose={closeChat}
181
- onModeChange={setDisplayMode}
182
- onStopStreaming={stopStreaming}
183
- title={config.title}
184
- placeholder={config.placeholder}
185
- greeting={config.greeting}
186
- />
163
+ <ChatSidebar />
187
164
  </Portal>
188
165
  );
189
166
  }
@@ -192,20 +169,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
192
169
  return (
193
170
  <Portal>
194
171
  <div style={floatingStyles} className={className || ''}>
195
- <ChatPanel
196
- messages={messages}
197
- isLoading={isLoading}
198
- onSendMessage={sendMessage}
199
- onClose={closeChat}
200
- onMinimize={toggleMinimize}
201
- onModeChange={setDisplayMode}
202
- onStopStreaming={stopStreaming}
203
- isMinimized={isMinimized}
204
- isMobile={isMobile}
205
- title={config.title}
206
- placeholder={config.placeholder}
207
- greeting={config.greeting}
208
- />
172
+ <ChatPanel />
209
173
  </div>
210
174
  </Portal>
211
175
  );
@@ -93,8 +93,8 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
93
93
  const padding = largeGreetingIcon ? 'py-12' : 'py-8';
94
94
 
95
95
  return (
96
- <ScrollArea ref={scrollAreaRef} className="h-full">
97
- <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4`}>
96
+ <ScrollArea ref={scrollAreaRef} className="h-full w-full">
97
+ <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4 max-w-full overflow-x-hidden`}>
98
98
  {/* Greeting */}
99
99
  {messages.length === 0 && greeting && (
100
100
  <div className={`text-center ${padding}`}>