@djangocfg/layouts 1.2.32 → 1.2.34

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": "1.2.32",
3
+ "version": "1.2.34",
4
4
  "description": "Layout system and components for Unrealon applications",
5
5
  "author": {
6
6
  "name": "DjangoCFG",
@@ -63,9 +63,9 @@
63
63
  "check": "tsc --noEmit"
64
64
  },
65
65
  "peerDependencies": {
66
- "@djangocfg/api": "^1.2.32",
67
- "@djangocfg/og-image": "^1.2.32",
68
- "@djangocfg/ui": "^1.2.32",
66
+ "@djangocfg/api": "^1.2.34",
67
+ "@djangocfg/og-image": "^1.2.34",
68
+ "@djangocfg/ui": "^1.2.34",
69
69
  "@hookform/resolvers": "^5.2.0",
70
70
  "consola": "^3.4.2",
71
71
  "lucide-react": "^0.468.0",
@@ -86,7 +86,7 @@
86
86
  "vidstack": "0.6.15"
87
87
  },
88
88
  "devDependencies": {
89
- "@djangocfg/typescript-config": "^1.2.32",
89
+ "@djangocfg/typescript-config": "^1.2.34",
90
90
  "@types/node": "^24.7.2",
91
91
  "@types/react": "19.2.2",
92
92
  "@types/react-dom": "19.2.1",
@@ -156,6 +156,10 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
156
156
  const initializeAuth = async () => {
157
157
  authLogger.info('Initializing auth...');
158
158
 
159
+ // Check if running in iframe (AdminLayout will handle auth via postMessage)
160
+ const isInIframe = typeof window !== 'undefined' && window.self !== window.top;
161
+ authLogger.info('Is in iframe:', isInIframe);
162
+
159
163
  // Debug token state
160
164
  const token = api.getToken();
161
165
  const refreshToken = api.getRefreshToken();
@@ -183,11 +187,21 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
183
187
  return;
184
188
  }
185
189
 
190
+ // In iframe mode WITHOUT tokens yet - wait for AdminLayout to receive them via postMessage
191
+ // Don't initialize yet - AdminLayout.handleAuthToken will call loadCurrentProfile
192
+ if (isInIframe && !hasTokens) {
193
+ authLogger.info('Running in iframe without tokens - waiting for parent to send via postMessage');
194
+ authLogger.info('AdminLayout will handle auth initialization, skipping AuthContext init');
195
+ setInitialized(true); // Mark as initialized to prevent re-initialization
196
+ setIsLoading(false);
197
+ return;
198
+ }
199
+
186
200
  if (hasTokens) {
187
201
  setIsLoading(true);
188
202
  try {
189
203
  authLogger.info('No cached profile found, loading from API...');
190
- await loadCurrentProfile();
204
+ await loadCurrentProfile('AuthContext.initializeAuth');
191
205
  } catch (error) {
192
206
  authLogger.error('Failed to load profile during initialization:', error);
193
207
  // If profile loading fails, clear auth state
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @djangocfg/layouts
3
- *
3
+ *
4
4
  * Reusable layout components and authentication system
5
5
  */
6
6
 
@@ -13,3 +13,6 @@ export * from './layouts';
13
13
  // Snippets - Reusable UI components
14
14
  export * from './snippets';
15
15
 
16
+ // Validation error tracking
17
+ export * from './validation';
18
+
@@ -36,6 +36,7 @@ import { AdminLayout } from './layouts/AdminLayout';
36
36
  import { determineLayoutMode, getRedirectUrl } from './utils';
37
37
  import { useAuth } from '../../auth';
38
38
  import type { AppLayoutConfig } from './types';
39
+ import type { ValidationErrorConfig } from '../../validation';
39
40
 
40
41
  export interface AppLayoutProps {
41
42
  children: ReactNode;
@@ -69,6 +70,12 @@ export interface AppLayoutProps {
69
70
  * @example showPackageVersions={true}
70
71
  */
71
72
  showPackageVersions?: boolean;
73
+ /**
74
+ * Configuration for validation error tracking
75
+ * @default { enableToast: true, maxErrors: 50 }
76
+ * @example validationConfig={{ enableToast: false }}
77
+ */
78
+ validationConfig?: Partial<ValidationErrorConfig>;
72
79
  }
73
80
 
74
81
  /**
@@ -249,7 +256,7 @@ function LayoutRouter({
249
256
  * </AppLayout>
250
257
  * ```
251
258
  */
252
- export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
259
+ export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validationConfig }: AppLayoutProps) {
253
260
  const router = useRouter();
254
261
 
255
262
  // Check if ErrorBoundary is enabled (default: true)
@@ -292,7 +299,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
292
299
  }} />
293
300
  )}
294
301
 
295
- <CoreProviders config={config}>
302
+ <CoreProviders config={config} validationConfig={validationConfig}>
296
303
  {appContent}
297
304
  </CoreProviders>
298
305
  </>
@@ -16,36 +16,36 @@ export interface PackageInfo {
16
16
  /**
17
17
  * Package versions registry
18
18
  * Auto-synced from package.json files
19
- * Last updated: 2025-11-10T07:27:36.920Z
19
+ * Last updated: 2025-11-11T06:22:05.284Z
20
20
  */
21
21
  const PACKAGE_VERSIONS: PackageInfo[] = [
22
22
  {
23
23
  "name": "@djangocfg/ui",
24
- "version": "1.2.32"
24
+ "version": "1.2.34"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.32"
28
+ "version": "1.2.34"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.32"
32
+ "version": "1.2.34"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.32"
36
+ "version": "1.2.34"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.32"
40
+ "version": "1.2.34"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.32"
44
+ "version": "1.2.34"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.32"
48
+ "version": "1.2.34"
49
49
  }
50
50
  ];
51
51
 
@@ -118,39 +118,65 @@ function AdminLayoutClient({
118
118
  const { isAdminUser, user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
119
119
  // console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
120
120
 
121
- // Use ref instead of state to prevent re-renders
121
+ // Use refs to prevent re-renders and maintain state across renders
122
122
  const profileLoadedRef = React.useRef(false);
123
+ const tokensReceivedRef = React.useRef(false);
124
+ const loadCurrentProfileRef = React.useRef(loadCurrentProfile);
123
125
 
124
- // Memoize the callback to prevent useCfgApp from re-subscribing to events
126
+ // Update ref when loadCurrentProfile changes (but don't trigger re-render)
127
+ React.useEffect(() => {
128
+ loadCurrentProfileRef.current = loadCurrentProfile;
129
+ }, [loadCurrentProfile]);
130
+
131
+ // Create a STABLE callback that NEVER changes (no dependencies)
132
+ // Use refs to access latest values without recreating the callback
125
133
  const handleAuthToken = React.useCallback(async (authToken: string, refreshToken?: string) => {
126
- // console.log('[AdminLayout] handleAuthToken called');
127
- // console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
134
+ console.log('[AdminLayout] handleAuthToken called, tokensReceived:', tokensReceivedRef.current, 'profileLoaded:', profileLoadedRef.current);
135
+
136
+ // Prevent duplicate token processing
137
+ if (tokensReceivedRef.current) {
138
+ console.log('[AdminLayout] Tokens already received and processed, ignoring duplicate');
139
+ return;
140
+ }
141
+
142
+ // Mark tokens as received IMMEDIATELY to prevent race conditions
143
+ tokensReceivedRef.current = true;
144
+
145
+ console.log('[AdminLayout] First time receiving tokens, processing...');
146
+ console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
128
147
 
129
148
  // Always set tokens in API client
130
149
  api.setToken(authToken, refreshToken);
131
- // console.log('[AdminLayout] Tokens set in API client');
150
+ console.log('[AdminLayout] Tokens set in API client');
132
151
 
133
152
  // Load user profile after setting tokens - ONLY ONCE per session
134
153
  if (!profileLoadedRef.current) {
135
- // console.log('[AdminLayout] Loading user profile (first time)...');
136
- await loadCurrentProfile('AdminLayout.onAuthTokenReceived');
137
- profileLoadedRef.current = true;
138
- // console.log('[AdminLayout] User profile loaded and marked as loaded');
154
+ console.log('[AdminLayout] Loading user profile (first time)...');
155
+ try {
156
+ await loadCurrentProfileRef.current('AdminLayout.onAuthTokenReceived');
157
+ profileLoadedRef.current = true;
158
+ console.log('[AdminLayout] User profile loaded successfully');
159
+ } catch (error) {
160
+ console.error('[AdminLayout] Failed to load profile:', error);
161
+ // Reset flags on error so user can retry
162
+ tokensReceivedRef.current = false;
163
+ profileLoadedRef.current = false;
164
+ }
139
165
  } else {
140
- // console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
166
+ console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
141
167
  }
142
168
 
143
169
  // Call custom handler if provided
144
170
  if (config?.onAuthTokenReceived) {
145
- // console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
171
+ console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
146
172
  config.onAuthTokenReceived(authToken, refreshToken);
147
173
  }
148
- }, [loadCurrentProfile, config?.onAuthTokenReceived]);
174
+ }, []); // ← EMPTY DEPS - callback NEVER changes
149
175
 
150
176
  // useCfgApp hook is called here to initialize iframe communication
151
177
  // Automatically sets tokens in API client when received from parent
152
178
  const { isEmbedded } = useCfgApp({
153
- onAuthTokenReceived: handleAuthToken // Stable reference
179
+ onAuthTokenReceived: handleAuthToken // Now truly stable - never recreated
154
180
  });
155
181
 
156
182
  // console.log('[AdminLayout] isEmbedded:', isEmbedded);
@@ -4,6 +4,7 @@
4
4
 
5
5
  'use client';
6
6
 
7
+ import React from 'react';
7
8
  import { useState, useEffect } from 'react';
8
9
  import { useRouter } from 'next/router';
9
10
  import { authLogger } from '../../../../../utils/logger';
@@ -94,6 +95,14 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
94
95
  const [parentTheme, setParentTheme] = useState<'light' | 'dark' | null>(null);
95
96
  const [parentThemeMode, setParentThemeMode] = useState<string | null>(null);
96
97
 
98
+ // Store callback in ref OUTSIDE useEffect so it's always available
99
+ const callbackRef = React.useRef(options?.onAuthTokenReceived);
100
+
101
+ // Update callback ref when options change (but don't re-register listener)
102
+ React.useEffect(() => {
103
+ callbackRef.current = options?.onAuthTokenReceived;
104
+ }, [options?.onAuthTokenReceived]);
105
+
97
106
  useEffect(() => {
98
107
  setIsMounted(true);
99
108
 
@@ -112,6 +121,8 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
112
121
 
113
122
  // Debounce timeout for parent-auth messages
114
123
  let authTokenTimeout: NodeJS.Timeout | null = null;
124
+ // Track if we've already processed auth tokens
125
+ let authTokenProcessed = false;
115
126
 
116
127
  // Listen for messages from parent window
117
128
  const handleMessage = (event: MessageEvent) => {
@@ -127,7 +138,13 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
127
138
 
128
139
  switch (type) {
129
140
  case 'parent-auth':
130
- // console.log('[useCfgApp] parent-auth message received');
141
+ console.log('[useCfgApp] parent-auth message received, authTokenProcessed:', authTokenProcessed);
142
+
143
+ // Prevent processing if already handled
144
+ if (authTokenProcessed) {
145
+ console.log('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
146
+ return;
147
+ }
131
148
 
132
149
  // Cancel previous timeout to debounce rapid auth messages
133
150
  if (authTokenTimeout) {
@@ -136,18 +153,30 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
136
153
 
137
154
  // Debounce auth token processing to prevent rapid calls (300ms)
138
155
  authTokenTimeout = setTimeout(() => {
156
+ // Double-check still not processed (race condition protection)
157
+ if (authTokenProcessed) {
158
+ console.log('[useCfgApp] Auth tokens already processed during debounce, skipping');
159
+ return;
160
+ }
161
+
139
162
  // Receive authentication tokens from parent
140
- if (data?.authToken && options?.onAuthTokenReceived) {
141
- // console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
142
- // console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
163
+ if (data?.authToken && callbackRef.current) {
164
+ console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
165
+ console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
166
+
167
+ // Mark as processed BEFORE calling callback
168
+ authTokenProcessed = true;
169
+
143
170
  try {
144
- options.onAuthTokenReceived(data.authToken, data.refreshToken);
145
- // authLogger.debug('onAuthTokenReceived callback completed successfully');
171
+ callbackRef.current(data.authToken, data.refreshToken);
172
+ console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
146
173
  } catch (e) {
147
174
  authLogger.error('Failed to process auth tokens:', e);
175
+ // Reset on error to allow retry
176
+ authTokenProcessed = false;
148
177
  }
149
178
  } else {
150
- authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!options?.onAuthTokenReceived });
179
+ authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!callbackRef.current });
151
180
  }
152
181
  }, 300); // 300ms debounce
153
182
  break;
@@ -202,7 +231,7 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
202
231
  clearTimeout(authTokenTimeout);
203
232
  }
204
233
  };
205
- }, [options]);
234
+ }, []); // ← EMPTY DEPS - register listener ONCE
206
235
 
207
236
  // Notify parent about route changes
208
237
  useEffect(() => {
@@ -9,11 +9,18 @@
9
9
  import React, { ReactNode } from 'react';
10
10
  import { ThemeProvider, Toaster } from '@djangocfg/ui';
11
11
  import { AuthProvider } from '../../../auth';
12
+ import { ValidationErrorProvider } from '../../../validation';
12
13
  import type { AppLayoutConfig } from '../types';
14
+ import type { ValidationErrorConfig } from '../../../validation';
13
15
 
14
16
  export interface CoreProvidersProps {
15
17
  children: ReactNode;
16
18
  config: AppLayoutConfig;
19
+ /**
20
+ * Configuration for validation error tracking
21
+ * @default { enableToast: true, maxErrors: 50 }
22
+ */
23
+ validationConfig?: Partial<ValidationErrorConfig>;
17
24
  }
18
25
 
19
26
  /**
@@ -22,9 +29,10 @@ export interface CoreProvidersProps {
22
29
  * Provides:
23
30
  * - ThemeProvider (dark/light mode)
24
31
  * - AuthProvider (authentication)
32
+ * - ValidationErrorProvider (Zod validation error tracking)
25
33
  * - Toaster (notifications)
26
34
  */
27
- export function CoreProviders({ children, config }: CoreProvidersProps) {
35
+ export function CoreProviders({ children, config, validationConfig }: CoreProvidersProps) {
28
36
  return (
29
37
  <ThemeProvider>
30
38
  <AuthProvider
@@ -37,7 +45,9 @@ export function CoreProviders({ children, config }: CoreProvidersProps) {
37
45
  },
38
46
  }}
39
47
  >
40
- {children}
48
+ <ValidationErrorProvider config={validationConfig}>
49
+ {children}
50
+ </ValidationErrorProvider>
41
51
  </AuthProvider>
42
52
 
43
53
  {/* Global toast notifications */}
@@ -7,6 +7,7 @@
7
7
 
8
8
  import React from 'react';
9
9
  import { CopyAIButton } from './CopyAIButton';
10
+ import { TestValidationButton } from './TestValidationButton';
10
11
 
11
12
  export interface HeaderProps {
12
13
  /** Page title */
@@ -42,10 +43,16 @@ export function Header({
42
43
  </div>
43
44
  </div>
44
45
 
45
- {/* Right: Copy for AI Button */}
46
- {onCopyForAI && (
47
- <CopyAIButton onCopyForAI={onCopyForAI} />
48
- )}
46
+ {/* Right: Developer Tools */}
47
+ <div className="flex items-center gap-2">
48
+ {/* Test Validation Error Button (dev only) */}
49
+ <TestValidationButton size="sm" />
50
+
51
+ {/* Copy for AI Button */}
52
+ {onCopyForAI && (
53
+ <CopyAIButton onCopyForAI={onCopyForAI} />
54
+ )}
55
+ </div>
49
56
  </div>
50
57
  </div>
51
58
  </header>
@@ -7,6 +7,7 @@
7
7
 
8
8
  import React from 'react';
9
9
  import { CopyAIButton } from './CopyAIButton';
10
+ import { TestValidationButton } from './TestValidationButton';
10
11
 
11
12
  interface HeaderDesktopProps {
12
13
  /** Page title */
@@ -31,13 +32,19 @@ export function HeaderDesktop({
31
32
  <h1 className="text-lg font-semibold">{title}</h1>
32
33
  </div>
33
34
 
34
- {/* Copy for AI Button */}
35
- <CopyAIButton
36
- onCopyForAI={onCopyForAI}
37
- size="sm"
38
- showLabel={true}
39
- className="gap-2"
40
- />
35
+ {/* Developer Tools */}
36
+ <div className="flex items-center gap-2">
37
+ {/* Test Validation Error Button (dev only) */}
38
+ <TestValidationButton size="sm" />
39
+
40
+ {/* Copy for AI Button */}
41
+ <CopyAIButton
42
+ onCopyForAI={onCopyForAI}
43
+ size="sm"
44
+ showLabel={true}
45
+ className="gap-2"
46
+ />
47
+ </div>
41
48
  </div>
42
49
  </div>
43
50
  );