@djangocfg/layouts 1.2.5 → 1.2.7

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.5",
3
+ "version": "1.2.7",
4
4
  "description": "Layout system and components for Unrealon applications",
5
5
  "author": {
6
6
  "name": "DjangoCFG",
@@ -53,9 +53,9 @@
53
53
  "check": "tsc --noEmit"
54
54
  },
55
55
  "peerDependencies": {
56
- "@djangocfg/api": "^1.2.5",
57
- "@djangocfg/og-image": "^1.2.5",
58
- "@djangocfg/ui": "^1.2.5",
56
+ "@djangocfg/api": "^1.2.7",
57
+ "@djangocfg/og-image": "^1.2.7",
58
+ "@djangocfg/ui": "^1.2.7",
59
59
  "@hookform/resolvers": "^5.2.0",
60
60
  "consola": "^3.4.2",
61
61
  "lucide-react": "^0.468.0",
@@ -76,7 +76,7 @@
76
76
  "vidstack": "0.6.15"
77
77
  },
78
78
  "devDependencies": {
79
- "@djangocfg/typescript-config": "^1.2.5",
79
+ "@djangocfg/typescript-config": "^1.2.7",
80
80
  "@types/node": "^24.7.2",
81
81
  "@types/react": "19.2.2",
82
82
  "@types/react-dom": "19.2.1",
@@ -101,14 +101,16 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
101
101
 
102
102
  // Refresh profile from AccountsContext
103
103
  const refreshedProfile = await accounts.refreshProfile();
104
-
104
+
105
105
  if (refreshedProfile) {
106
- setInitialized(true);
107
106
  authLogger.info('Profile loaded successfully:', refreshedProfile.id);
108
107
  } else {
109
- authLogger.warn('Profile refresh returned undefined');
110
- clearAuthState('loadCurrentProfile:noProfile');
108
+ authLogger.warn('Profile refresh returned undefined - but keeping tokens');
111
109
  }
110
+
111
+ // Always mark as initialized if we have valid tokens
112
+ // Don't clear tokens just because profile fetch failed
113
+ setInitialized(true);
112
114
  } catch (error) {
113
115
  authLogger.error('Failed to load profile:', error);
114
116
  // Use global error handler first, fallback to clearing state
@@ -166,7 +168,8 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
166
168
  useEffect(() => {
167
169
  if (!initialized) return;
168
170
 
169
- const isAuthenticated = !!userRef.current && api.isAuthenticated();
171
+ // Consider authenticated if we have valid tokens, even without profile
172
+ const isAuthenticated = api.isAuthenticated();
170
173
  const authRoute = config?.routes?.auth || defaultRoutes.auth;
171
174
  const isAuthPage = router.pathname === authRoute;
172
175
 
@@ -408,7 +411,8 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
408
411
  () => ({
409
412
  user,
410
413
  isLoading,
411
- isAuthenticated: !!user && api.isAuthenticated(),
414
+ // Consider authenticated if we have valid tokens, even without user profile
415
+ isAuthenticated: api.isAuthenticated(),
412
416
  loadCurrentProfile,
413
417
  checkAuthAndRedirect,
414
418
  getToken: () => api.getToken(),
@@ -6,12 +6,19 @@
6
6
  * - Applies correct layout automatically
7
7
  * - Manages all state through context
8
8
  * - Zero prop drilling
9
+ * - Optional Django CFG admin mode with iframe integration
9
10
  *
10
11
  * Usage in _app.tsx:
11
12
  * ```tsx
13
+ * // Standard usage
12
14
  * <AppLayout config={appLayoutConfig}>
13
15
  * <Component {...pageProps} />
14
16
  * </AppLayout>
17
+ *
18
+ * // Django CFG admin mode (with iframe integration)
19
+ * <AppLayout config={appLayoutConfig} isCfgAdmin={true}>
20
+ * <Component {...pageProps} />
21
+ * </AppLayout>
15
22
  * ```
16
23
  */
17
24
 
@@ -25,6 +32,7 @@ import { Seo, PageProgress, ErrorBoundary } from './components';
25
32
  import { PublicLayout } from './layouts/PublicLayout';
26
33
  import { PrivateLayout } from './layouts/PrivateLayout';
27
34
  import { AuthLayout } from './layouts/AuthLayout';
35
+ import { CfgLayout } from './layouts/CfgLayout';
28
36
  import { determineLayoutMode, getRedirectUrl } from './utils';
29
37
  import { useAuth } from '../../auth';
30
38
  import type { AppLayoutConfig } from './types';
@@ -41,9 +49,13 @@ export interface AppLayoutProps {
41
49
  /**
42
50
  * Force a specific layout regardless of route
43
51
  * Overrides automatic layout detection
52
+ *
44
53
  * @example forceLayout="public" - always use PublicLayout
54
+ * @example forceLayout="private" - always use PrivateLayout
55
+ * @example forceLayout="auth" - always use AuthLayout
56
+ * @example forceLayout="admin" - Django CFG admin mode with iframe integration
45
57
  */
46
- forceLayout?: 'public' | 'private' | 'auth';
58
+ forceLayout?: 'public' | 'private' | 'auth' | 'admin';
47
59
  /**
48
60
  * Font family to apply globally
49
61
  * Accepts Next.js font object or CSS font-family string
@@ -73,7 +85,7 @@ function LayoutRouter({
73
85
  }: {
74
86
  children: ReactNode;
75
87
  disableLayout?: boolean;
76
- forceLayout?: 'public' | 'private' | 'auth';
88
+ forceLayout?: 'public' | 'private' | 'auth' | 'admin';
77
89
  config: AppLayoutConfig;
78
90
  }) {
79
91
  const router = useRouter();
@@ -85,29 +97,49 @@ function LayoutRouter({
85
97
  setIsMounted(true);
86
98
  }, []);
87
99
 
100
+ const isAdminMode = forceLayout === 'admin';
88
101
  // If layout is disabled, render children directly (providers still active!)
89
- if (disableLayout) {
102
+ if (disableLayout || isAdminMode) {
90
103
  return <>{children}</>;
91
104
  }
92
105
 
93
- // Determine layout mode based on route (synchronous - works with SSR)
94
- const getLayoutMode = (): 'public' | 'private' | 'auth' => {
95
- // If forceLayout is specified, use it
96
- if (forceLayout) return forceLayout;
106
+ // Check route type (synchronous - works with SSR)
107
+ const isAuthRoute = config.routes.detectors.isAuthRoute(router.pathname);
108
+ const isPrivateRoute = config.routes.detectors.isPrivateRoute(router.pathname);
97
109
 
98
- const isAuthRoute = config.routes.detectors.isAuthRoute(router.pathname);
99
- const isPrivateRoute = config.routes.detectors.isPrivateRoute(router.pathname);
100
- // const isPublicRoute = config.routes.detectors.isPublicRoute(router.pathname);
110
+ // Private routes: Always show loading during SSR and initial client render
111
+ // This prevents hydration mismatch when isAuthenticated differs between server/client
112
+ if (isPrivateRoute && !forceLayout) {
113
+ if (!isMounted || isLoading) {
114
+ return (
115
+ <div className="min-h-screen flex items-center justify-center">
116
+ <div className="text-muted-foreground">Loading...</div>
117
+ </div>
118
+ );
119
+ }
101
120
 
102
- if (isAuthRoute) return 'auth';
121
+ // After mount: check authentication
122
+ if (!isAuthenticated) {
123
+ // Redirect to auth (handled in PrivateLayout)
124
+ return (
125
+ <AuthLayout
126
+ termsUrl={config.auth?.termsUrl}
127
+ privacyUrl={config.auth?.privacyUrl}
128
+ supportUrl={config.auth?.supportUrl}
129
+ enablePhoneAuth={config.auth?.enablePhoneAuth}
130
+ />
131
+ );
132
+ }
103
133
 
104
- if (isPrivateRoute) {
105
- if (isAuthenticated) {
106
- return 'private';
107
- };
108
- return 'auth';
109
- };
134
+ return <PrivateLayout>{children}</PrivateLayout>;
135
+ }
110
136
 
137
+ // Determine layout mode for non-private routes
138
+ const getLayoutMode = (): 'public' | 'auth' | 'admin' => {
139
+ if (forceLayout === 'auth') return 'auth';
140
+ if (forceLayout === 'public') return 'public';
141
+ if (isAuthRoute) return 'auth';
142
+ if (isAdminMode) return 'admin';
111
143
  return 'public';
112
144
  };
113
145
 
@@ -115,15 +147,15 @@ function LayoutRouter({
115
147
 
116
148
  // Render appropriate layout
117
149
  switch (layoutMode) {
150
+ case 'admin':
151
+ return <CfgLayout>{children}</CfgLayout>;
152
+ break;
118
153
  // Public routes: render immediately (SSR enabled)
119
154
  case 'public':
120
155
  return <PublicLayout>{children}</PublicLayout>;
121
156
 
122
157
  // Auth routes: render inside AuthLayout
123
158
  case 'auth':
124
- // Check if we're on a private route that requires auth
125
- const isPrivateRoute = config.routes.detectors.isPrivateRoute(router.pathname);
126
-
127
159
  return (
128
160
  <AuthLayout
129
161
  termsUrl={config.auth?.termsUrl}
@@ -131,22 +163,10 @@ function LayoutRouter({
131
163
  supportUrl={config.auth?.supportUrl}
132
164
  enablePhoneAuth={config.auth?.enablePhoneAuth}
133
165
  >
134
- {/* Don't render children if redirected from private route */}
135
- {!isPrivateRoute && children}
166
+ {children}
136
167
  </AuthLayout>
137
168
  );
138
169
 
139
- // Private routes: wait for client-side hydration and auth check
140
- case 'private':
141
- if (!isMounted || isLoading) {
142
- return (
143
- <div className="min-h-screen flex items-center justify-center">
144
- <div className="text-muted-foreground">Loading...</div>
145
- </div>
146
- );
147
- }
148
- return <PrivateLayout>{children}</PrivateLayout>;
149
-
150
170
  default:
151
171
  return <PublicLayout>{children}</PublicLayout>;
152
172
  }
@@ -189,6 +209,9 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
189
209
  const supportEmail = config.errors?.supportEmail;
190
210
  const onError = config.errors?.onError;
191
211
 
212
+ // Determine if admin mode is enabled (Django CFG iframe integration)
213
+ const isAdminMode = forceLayout === 'admin';
214
+
192
215
  const content = (
193
216
  <>
194
217
  {/* Global Font Styles */}
@@ -199,29 +222,33 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
199
222
  )}
200
223
 
201
224
  <CoreProviders config={config}>
202
- <AppContextProvider config={config} showPackageVersions={showPackageVersions}>
203
- {/* SEO Meta Tags */}
204
- <Seo
205
- pageConfig={{
206
- title: config.app.name,
207
- description: config.app.description,
208
- ogImage: {
225
+ {/* CfgLayout must be inside CoreProviders to access AuthProvider */}
226
+ {/* Only enable ParentSync when in admin mode */}
227
+ <CfgLayout enableParentSync={isAdminMode}>
228
+ <AppContextProvider config={config} showPackageVersions={showPackageVersions}>
229
+ {/* SEO Meta Tags */}
230
+ <Seo
231
+ pageConfig={{
209
232
  title: config.app.name,
210
- subtitle: config.app.description,
211
- },
212
- }}
213
- icons={config.app.icons}
214
- siteUrl={config.app.siteUrl}
215
- />
216
-
217
- {/* Loading Progress Bar */}
218
- <PageProgress />
219
-
220
- {/* Smart Layout Router */}
221
- <LayoutRouter disableLayout={disableLayout} forceLayout={forceLayout} config={config}>
222
- {children}
223
- </LayoutRouter>
224
- </AppContextProvider>
233
+ description: config.app.description,
234
+ ogImage: {
235
+ title: config.app.name,
236
+ subtitle: config.app.description,
237
+ },
238
+ }}
239
+ icons={config.app.icons}
240
+ siteUrl={config.app.siteUrl}
241
+ />
242
+
243
+ {/* Loading Progress Bar */}
244
+ <PageProgress />
245
+
246
+ {/* Smart Layout Router */}
247
+ <LayoutRouter disableLayout={disableLayout} forceLayout={forceLayout} config={config}>
248
+ {children}
249
+ </LayoutRouter>
250
+ </AppContextProvider>
251
+ </CfgLayout>
225
252
  </CoreProviders>
226
253
  </>
227
254
  );
@@ -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-10-25T06:54:45.462Z
19
+ * Last updated: 2025-10-28T04:15:24.707Z
20
20
  */
21
21
  const PACKAGE_VERSIONS: PackageInfo[] = [
22
22
  {
23
23
  "name": "@djangocfg/ui",
24
- "version": "1.2.5"
24
+ "version": "1.2.7"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.5"
28
+ "version": "1.2.7"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.5"
32
+ "version": "1.2.7"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.5"
36
+ "version": "1.2.7"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.5"
40
+ "version": "1.2.7"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.5"
44
+ "version": "1.2.7"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.5"
48
+ "version": "1.2.7"
49
49
  }
50
50
  ];
51
51
 
@@ -29,3 +29,15 @@ export { useLayoutMode, useNavigation } from './hooks';
29
29
  export { PublicLayout } from './layouts/PublicLayout';
30
30
  export { PrivateLayout } from './layouts/PrivateLayout';
31
31
  export { AuthLayout } from './layouts/AuthLayout';
32
+
33
+ // CfgLayout - Django CFG iframe integration
34
+ export { CfgLayout } from './layouts/CfgLayout';
35
+ export { useCfgApp, useApp } from './layouts/CfgLayout';
36
+ export { ParentSync, AuthStatusSync } from './layouts/CfgLayout';
37
+ export type {
38
+ CfgLayoutConfig,
39
+ UseCfgAppReturn,
40
+ UseCfgAppOptions,
41
+ UseAppReturn,
42
+ UseAppOptions
43
+ } from './layouts/CfgLayout';
@@ -0,0 +1,104 @@
1
+ // ============================================================================
2
+ // CfgLayout - Django CFG Layout with iframe Integration
3
+ // ============================================================================
4
+ // Universal layout component that handles:
5
+ // - iframe embedding detection
6
+ // - Parent ↔ iframe communication (postMessage)
7
+ // - Theme synchronization from Django Unfold
8
+ // - Auth token passing from parent window (automatically sets in API client)
9
+ // - Auth status reporting to parent window
10
+ // - Automatic layout disable in iframe mode
11
+ //
12
+ // This is a lightweight wrapper that can be used with any layout system
13
+ // (AppLayout, custom layouts, etc.)
14
+
15
+ 'use client';
16
+
17
+ import React, { ReactNode } from 'react';
18
+ import { ParentSync } from './components';
19
+ import { useCfgApp } from './hooks';
20
+ import type { CfgLayoutConfig } from './types';
21
+ import { api } from '@djangocfg/api';
22
+
23
+ export interface CfgLayoutProps {
24
+ children: ReactNode;
25
+ config?: CfgLayoutConfig;
26
+ /**
27
+ * Whether to render ParentSync component
28
+ * Set to false if you want to handle sync manually
29
+ * @default true
30
+ */
31
+ enableParentSync?: boolean;
32
+ }
33
+
34
+ /**
35
+ * CfgLayout - Universal Layout Component for Django CFG
36
+ *
37
+ * Provides iframe integration features:
38
+ * - Auto-detects iframe embedding
39
+ * - Syncs theme from parent window (Django Unfold)
40
+ * - Receives auth tokens from parent window and automatically sets them in API client
41
+ * - Sends auth status to parent window
42
+ * - Provides useApp hook data via context
43
+ *
44
+ * Usage:
45
+ * ```tsx
46
+ * // Wrap your app in _app.tsx - no config needed!
47
+ * <CfgLayout>
48
+ * <AppLayout config={appLayoutConfig}>
49
+ * <Component {...pageProps} />
50
+ * </AppLayout>
51
+ * </CfgLayout>
52
+ * ```
53
+ *
54
+ * Or with custom auth handler:
55
+ * ```tsx
56
+ * <CfgLayout config={{
57
+ * onAuthTokenReceived: (authToken, refreshToken) => {
58
+ * // Custom logic before/after setting tokens
59
+ * console.log('Tokens received');
60
+ * }
61
+ * }}>
62
+ * <AppLayout config={appLayoutConfig}>
63
+ * <Component {...pageProps} />
64
+ * </AppLayout>
65
+ * </CfgLayout>
66
+ * ```
67
+ *
68
+ * Use useCfgApp hook directly:
69
+ * ```tsx
70
+ * import { useCfgApp } from '@djangocfg/layouts/CfgLayout';
71
+ *
72
+ * function MyComponent() {
73
+ * const { isEmbedded, disableLayout, parentTheme } = useCfgApp();
74
+ * // ...
75
+ * }
76
+ * ```
77
+ */
78
+ export function CfgLayout({
79
+ children,
80
+ config,
81
+ enableParentSync = true
82
+ }: CfgLayoutProps) {
83
+ // useCfgApp hook is called here to initialize iframe communication
84
+ // Automatically sets tokens in API client when received from parent
85
+ useCfgApp({
86
+ onAuthTokenReceived: (authToken, refreshToken) => {
87
+ // Always set tokens in API client
88
+ api.setToken(authToken, refreshToken);
89
+
90
+ // Call custom handler if provided
91
+ if (config?.onAuthTokenReceived) {
92
+ config.onAuthTokenReceived(authToken, refreshToken);
93
+ }
94
+ }
95
+ });
96
+
97
+ return (
98
+ <>
99
+ {/* ParentSync handles theme sync and auth status reporting */}
100
+ {enableParentSync && <ParentSync />}
101
+ {children}
102
+ </>
103
+ );
104
+ }