@djangocfg/layouts 1.2.6 → 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.6",
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.6",
57
- "@djangocfg/og-image": "^1.2.6",
58
- "@djangocfg/ui": "^1.2.6",
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.6",
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",
@@ -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,8 +97,9 @@ 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
 
@@ -122,10 +135,11 @@ function LayoutRouter({
122
135
  }
123
136
 
124
137
  // Determine layout mode for non-private routes
125
- const getLayoutMode = (): 'public' | 'auth' => {
138
+ const getLayoutMode = (): 'public' | 'auth' | 'admin' => {
126
139
  if (forceLayout === 'auth') return 'auth';
127
140
  if (forceLayout === 'public') return 'public';
128
141
  if (isAuthRoute) return 'auth';
142
+ if (isAdminMode) return 'admin';
129
143
  return 'public';
130
144
  };
131
145
 
@@ -133,6 +147,9 @@ function LayoutRouter({
133
147
 
134
148
  // Render appropriate layout
135
149
  switch (layoutMode) {
150
+ case 'admin':
151
+ return <CfgLayout>{children}</CfgLayout>;
152
+ break;
136
153
  // Public routes: render immediately (SSR enabled)
137
154
  case 'public':
138
155
  return <PublicLayout>{children}</PublicLayout>;
@@ -192,6 +209,9 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
192
209
  const supportEmail = config.errors?.supportEmail;
193
210
  const onError = config.errors?.onError;
194
211
 
212
+ // Determine if admin mode is enabled (Django CFG iframe integration)
213
+ const isAdminMode = forceLayout === 'admin';
214
+
195
215
  const content = (
196
216
  <>
197
217
  {/* Global Font Styles */}
@@ -202,29 +222,33 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
202
222
  )}
203
223
 
204
224
  <CoreProviders config={config}>
205
- <AppContextProvider config={config} showPackageVersions={showPackageVersions}>
206
- {/* SEO Meta Tags */}
207
- <Seo
208
- pageConfig={{
209
- title: config.app.name,
210
- description: config.app.description,
211
- 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={{
212
232
  title: config.app.name,
213
- subtitle: config.app.description,
214
- },
215
- }}
216
- icons={config.app.icons}
217
- siteUrl={config.app.siteUrl}
218
- />
219
-
220
- {/* Loading Progress Bar */}
221
- <PageProgress />
222
-
223
- {/* Smart Layout Router */}
224
- <LayoutRouter disableLayout={disableLayout} forceLayout={forceLayout} config={config}>
225
- {children}
226
- </LayoutRouter>
227
- </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>
228
252
  </CoreProviders>
229
253
  </>
230
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-27T16:32:06.128Z
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.6"
24
+ "version": "1.2.7"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.6"
28
+ "version": "1.2.7"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.6"
32
+ "version": "1.2.7"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.6"
36
+ "version": "1.2.7"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.6"
40
+ "version": "1.2.7"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.6"
44
+ "version": "1.2.7"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.6"
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
+ }
@@ -0,0 +1,389 @@
1
+ # CfgLayout - Django CFG Layout with iframe Integration
2
+
3
+ Universal layout component for Django CFG applications that handles iframe embedding, theme synchronization, and authentication communication.
4
+
5
+ **Note:** CfgLayout is now integrated into AppLayout. For most use cases, use `AppLayout` with `isCfgAdmin={true}` instead of using CfgLayout directly.
6
+
7
+ ## Features
8
+
9
+ - 🖼️ **iframe Embedding Detection** - Automatically detects when running inside an iframe
10
+ - 🎨 **Theme Synchronization** - Syncs theme from Django Unfold parent window
11
+ - 🔐 **Auth Token Passing** - Receives JWT tokens from parent window via postMessage
12
+ - 📤 **Auth Status Reporting** - Sends authentication status to parent window
13
+ - 📏 **Auto Resize** - Automatically adjusts iframe height based on content
14
+ - 🔒 **Secure Communication** - Origin validation for postMessage security
15
+ - ⚡ **Lightweight** - Minimal overhead, works with any layout system
16
+
17
+ ## Installation
18
+
19
+ Already included in `@djangocfg/layouts` package.
20
+
21
+ ## Recommended Usage (via AppLayout)
22
+
23
+ ### Simple - Enable Django CFG Admin Mode
24
+
25
+ ```tsx
26
+ // apps/admin/src/pages/_app.tsx
27
+ import { AppLayout } from '@djangocfg/layouts';
28
+
29
+ function MyApp({ Component, pageProps }) {
30
+ return (
31
+ <AppProviders>
32
+ <AppLayout config={appLayoutConfig} isCfgAdmin={true}>
33
+ <Component {...pageProps} />
34
+ </AppLayout>
35
+ </AppProviders>
36
+ );
37
+ }
38
+ ```
39
+
40
+ **That's it!** Setting `isCfgAdmin={true}` automatically:
41
+ - Wraps your app with CfgLayout
42
+ - Sets up iframe communication
43
+ - Handles auth token passing
44
+ - Syncs theme from parent window
45
+
46
+ ## Direct Usage (Advanced)
47
+
48
+ If you need more control, you can use CfgLayout directly:
49
+
50
+ ### 1. Wrap your app with CfgLayout (Zero Config!)
51
+
52
+ ```tsx
53
+ // apps/admin/src/pages/_app.tsx
54
+ import { CfgLayout, AppLayout } from '@djangocfg/layouts';
55
+
56
+ function MyApp({ Component, pageProps }) {
57
+ return (
58
+ <CfgLayout>
59
+ <AppLayout config={appLayoutConfig}>
60
+ <Component {...pageProps} />
61
+ </AppLayout>
62
+ </CfgLayout>
63
+ );
64
+ }
65
+ ```
66
+
67
+ **That's it!** Auth tokens are automatically set in the API client when received from parent window.
68
+
69
+ ### 2. With custom auth handler (optional)
70
+
71
+ ```tsx
72
+ // Only if you need additional logic when tokens are received
73
+ <CfgLayout
74
+ config={{
75
+ onAuthTokenReceived: (authToken, refreshToken) => {
76
+ // Tokens are already set in API client automatically
77
+ console.log('Tokens received!');
78
+ // Your custom logic here
79
+ }
80
+ }}
81
+ >
82
+ <AppLayout config={appLayoutConfig}>
83
+ <Component {...pageProps} />
84
+ </AppLayout>
85
+ </CfgLayout>
86
+ ```
87
+
88
+ ## Hook Usage
89
+
90
+ ### Use useCfgApp hook to access embedding state
91
+
92
+ ```tsx
93
+ import { useCfgApp } from '@djangocfg/layouts';
94
+
95
+ function MyComponent() {
96
+ const { isEmbedded, disableLayout, parentTheme } = useCfgApp();
97
+
98
+ return (
99
+ <div>
100
+ {isEmbedded && <p>Running in iframe mode</p>}
101
+ <p>Current theme from parent: {parentTheme}</p>
102
+ </div>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### Use with AppLayout's disableLayout prop
108
+
109
+ ```tsx
110
+ import { AppLayout, useCfgApp } from '@djangocfg/layouts';
111
+
112
+ function MyApp({ Component, pageProps }) {
113
+ const { disableLayout } = useCfgApp();
114
+
115
+ return (
116
+ <AppLayout
117
+ config={config}
118
+ disableLayout={disableLayout}
119
+ isCfgAdmin={true}
120
+ >
121
+ <Component {...pageProps} />
122
+ </AppLayout>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ## API Reference
128
+
129
+ ### AppLayout with isCfgAdmin
130
+
131
+ **Recommended approach:**
132
+
133
+ ```typescript
134
+ <AppLayout
135
+ config={appLayoutConfig}
136
+ isCfgAdmin={true} // ← Enables Django CFG admin mode
137
+ fontFamily={manrope.style.fontFamily}
138
+ >
139
+ {children}
140
+ </AppLayout>
141
+ ```
142
+
143
+ ### CfgLayout Props
144
+
145
+ | Prop | Type | Default | Description |
146
+ |------|------|---------|-------------|
147
+ | `children` | `ReactNode` | Required | Your app content |
148
+ | `config` | `CfgLayoutConfig` | `{}` | Configuration object |
149
+ | `enableParentSync` | `boolean` | `true` | Enable automatic theme/auth sync |
150
+
151
+ ### CfgLayoutConfig
152
+
153
+ **All options are optional!** CfgLayout works with zero configuration.
154
+
155
+ ```typescript
156
+ interface CfgLayoutConfig {
157
+ // Optional: Called when auth tokens are received from parent
158
+ // Note: Tokens are ALWAYS automatically set in API client
159
+ // Use this only if you need additional custom logic
160
+ onAuthTokenReceived?: (authToken, refreshToken?) => void;
161
+
162
+ // Enable/disable theme sync (default: true)
163
+ enableThemeSync?: boolean;
164
+
165
+ // Enable/disable auth status sync (default: true)
166
+ enableAuthStatusSync?: boolean;
167
+ }
168
+ ```
169
+
170
+ ### useCfgApp Hook
171
+
172
+ ```typescript
173
+ const {
174
+ isEmbedded, // true if running in iframe
175
+ isStandalone, // true if running as PWA
176
+ disableLayout, // true if layout should be disabled
177
+ referrer, // parent URL if embedded
178
+ isMounted, // true after client-side mount
179
+ parentTheme, // 'light' | 'dark' | null
180
+ parentThemeMode, // 'auto' | 'light' | 'dark' | null
181
+ } = useCfgApp({
182
+ onAuthTokenReceived: (authToken, refreshToken) => {
183
+ // Handle tokens (optional - tokens are auto-set in API client)
184
+ }
185
+ });
186
+ ```
187
+
188
+ **Note:** `useApp` is also exported as an alias for backward compatibility.
189
+
190
+ ## postMessage Communication Protocol
191
+
192
+ ### Parent → iframe
193
+
194
+ | Message Type | Data | Description |
195
+ |-------------|------|-------------|
196
+ | `parent-theme` | `{ theme: 'dark'\|'light', themeMode: 'auto'\|'dark'\|'light' }` | Theme from Django Unfold |
197
+ | `parent-auth` | `{ authToken, refreshToken }` | JWT authentication tokens |
198
+
199
+ ### iframe → Parent
200
+
201
+ | Message Type | Data | Description |
202
+ |-------------|------|-------------|
203
+ | `iframe-ready` | `{ url, referrer }` | iframe loaded successfully |
204
+ | `iframe-auth-status` | `{ isAuthenticated, isLoading, hasUser }` | Auth status update |
205
+ | `iframe-resize` | `{ height }` | Content height changed |
206
+ | `iframe-navigation` | `{ path, route }` | User navigated to new page |
207
+
208
+ ## Advanced Usage
209
+
210
+ ### Manual Theme Sync
211
+
212
+ ```tsx
213
+ import { useCfgApp } from '@djangocfg/layouts';
214
+ import { useThemeContext } from '@djangocfg/ui';
215
+
216
+ function MyThemeSync() {
217
+ const { isEmbedded, parentTheme } = useCfgApp();
218
+ const { setTheme } = useThemeContext();
219
+
220
+ useEffect(() => {
221
+ if (isEmbedded && parentTheme) {
222
+ setTheme(parentTheme);
223
+ }
224
+ }, [isEmbedded, parentTheme, setTheme]);
225
+
226
+ return null;
227
+ }
228
+ ```
229
+
230
+ ### Disable Auto Sync
231
+
232
+ ```tsx
233
+ <CfgLayout enableParentSync={false}>
234
+ {/* Handle sync manually */}
235
+ </CfgLayout>
236
+ ```
237
+
238
+ ### URL Override
239
+
240
+ Force iframe mode via URL:
241
+ ```
242
+ https://example.com/admin/?embed=true
243
+ ```
244
+
245
+ Disable iframe mode:
246
+ ```
247
+ https://example.com/admin/?embed=false
248
+ ```
249
+
250
+ ## Integration with Django
251
+
252
+ See Django template at: `src/django_cfg/templates/admin/index.html`
253
+
254
+ The Django template uses an iframe to load the Next.js app:
255
+ ```html
256
+ <iframe
257
+ id="nextjs-dashboard-iframe"
258
+ src="{% nextjs_admin_url 'private' %}"
259
+ title="Next.js Dashboard"
260
+ ></iframe>
261
+ ```
262
+
263
+ ## Components
264
+
265
+ ### ParentSync
266
+
267
+ Handles bidirectional communication with parent window. Automatically included when using `isCfgAdmin={true}`.
268
+
269
+ ```tsx
270
+ import { ParentSync } from '@djangocfg/layouts';
271
+
272
+ // Usually placed inside AppLayout (automatic with isCfgAdmin={true})
273
+ <AuthProvider>
274
+ <ThemeProvider>
275
+ <ParentSync />
276
+ {children}
277
+ </ThemeProvider>
278
+ </AuthProvider>
279
+ ```
280
+
281
+ ## Security Considerations
282
+
283
+ - All postMessage communications validate origins
284
+ - Tokens are only accepted from expected parent origins
285
+ - iframe uses sandbox attribute with specific permissions
286
+ - localStorage is used for token persistence (XSS protection needed)
287
+
288
+ ## Migration from Old API
289
+
290
+ **Old approach:**
291
+ ```tsx
292
+ import { AppLayout, CfgLayout, useCfgApp } from '@djangocfg/layouts';
293
+
294
+ function AppLayoutWrapper() {
295
+ const { disableLayout, isEmbedded } = useCfgApp();
296
+ return (
297
+ <AppLayout disableLayout={disableLayout}>
298
+ {children}
299
+ </AppLayout>
300
+ );
301
+ }
302
+
303
+ export default function App() {
304
+ return (
305
+ <CfgLayout>
306
+ <AppLayoutWrapper>
307
+ {children}
308
+ </AppLayoutWrapper>
309
+ </CfgLayout>
310
+ );
311
+ }
312
+ ```
313
+
314
+ **New approach:**
315
+ ```tsx
316
+ import { AppLayout } from '@djangocfg/layouts';
317
+
318
+ export default function App({ Component, pageProps }) {
319
+ return (
320
+ <AppLayout config={appLayoutConfig} isCfgAdmin={true}>
321
+ <Component {...pageProps} />
322
+ </AppLayout>
323
+ );
324
+ }
325
+ ```
326
+
327
+ ## Troubleshooting
328
+
329
+ ### Theme not syncing
330
+ - Check parent window is sending `parent-theme` messages
331
+ - Verify ThemeProvider is in component tree
332
+ - Enable debug logging in `useApp.ts`
333
+
334
+ ### Auth tokens not working
335
+ - Tokens are automatically set in API client
336
+ - Check parent window sends `parent-auth` message
337
+ - Inspect localStorage for tokens: `auth_token`, `refresh_token`
338
+
339
+ ### Layout not hiding in iframe
340
+ - Use `isCfgAdmin={true}` on AppLayout (recommended)
341
+ - Or manually pass `disableLayout` from `useCfgApp()` hook
342
+ - Try URL override: `?embed=true`
343
+
344
+ ### Static export build fails
345
+ - Make sure ParentSync is wrapped with SSR protection (already handled)
346
+ - Check that `useAuth()` is only called on client-side
347
+ - Verify `'use client'` directive is present in components
348
+
349
+ ## Examples
350
+
351
+ **Simple Django CFG Admin:**
352
+ ```tsx
353
+ <AppLayout config={config} isCfgAdmin={true}>
354
+ <Component {...pageProps} />
355
+ </AppLayout>
356
+ ```
357
+
358
+ **With custom font:**
359
+ ```tsx
360
+ <AppLayout
361
+ config={config}
362
+ isCfgAdmin={true}
363
+ fontFamily={manrope.style.fontFamily}
364
+ >
365
+ <Component {...pageProps} />
366
+ </AppLayout>
367
+ ```
368
+
369
+ **With custom auth logic:**
370
+ ```tsx
371
+ <CfgLayout
372
+ config={{
373
+ onAuthTokenReceived: (token, refresh) => {
374
+ console.log('Tokens received from parent');
375
+ // Additional custom logic
376
+ }
377
+ }}
378
+ >
379
+ <AppLayout config={config}>
380
+ <Component {...pageProps} />
381
+ </AppLayout>
382
+ </CfgLayout>
383
+ ```
384
+
385
+ ## Related Files
386
+
387
+ - Django Template: `src/django_cfg/templates/admin/index.html`
388
+ - Django Views: `src/django_cfg/apps/frontend/views.py`
389
+ - Example App: `src/@frontend/apps/admin/src/pages/_app.tsx`
@@ -0,0 +1,149 @@
1
+ // ============================================================================
2
+ // Parent Sync Component
3
+ // ============================================================================
4
+ // Handles all synchronization between iframe and parent window:
5
+ // - Theme sync (parent → iframe)
6
+ // - Auth status sync (iframe → parent)
7
+ // Must be used inside AuthProvider and ThemeProvider contexts
8
+
9
+ 'use client';
10
+
11
+ import { useEffect, useState } from 'react';
12
+ import { useAuth } from '../../../../../auth';
13
+ import { useThemeContext } from '@djangocfg/ui';
14
+ import { useCfgApp } from '../hooks/useApp';
15
+
16
+ /**
17
+ * ParentSync Component
18
+ *
19
+ * Handles bidirectional communication with parent window when embedded:
20
+ * 1. Receives theme updates from parent (Django Unfold) and applies to ThemeProvider
21
+ * 2. Sends auth status to parent when authentication state changes
22
+ *
23
+ * Usage:
24
+ * - Place inside AppLayout (after AuthProvider and ThemeProvider)
25
+ * - Automatically handles all iframe ↔ parent communication
26
+ *
27
+ * Note: Safe for static export - only runs on client-side after hydration
28
+ */
29
+ export function ParentSync() {
30
+ const [isClient, setIsClient] = useState(false);
31
+
32
+ // Wait for client-side hydration to avoid SSR/static export errors
33
+ useEffect(() => {
34
+ setIsClient(true);
35
+ }, []);
36
+
37
+ // Early return during SSR/static export
38
+ if (!isClient) {
39
+ return null;
40
+ }
41
+
42
+ return <ParentSyncClient />;
43
+ }
44
+
45
+ /**
46
+ * Client-only component that uses auth context
47
+ * Separated to avoid SSR issues during static export
48
+ */
49
+ function ParentSyncClient() {
50
+ const auth = useAuth();
51
+ const { setTheme } = useThemeContext();
52
+ const { isEmbedded, isMounted, parentTheme } = useCfgApp();
53
+
54
+ // 1. Sync theme from parent → iframe
55
+ useEffect(() => {
56
+ if (isEmbedded && parentTheme) {
57
+ // console.log('[ParentSync] 🎨 Syncing theme from parent:', parentTheme);
58
+ setTheme(parentTheme);
59
+ }
60
+ }, [isEmbedded, parentTheme, setTheme]);
61
+
62
+ // 2. Send auth status from iframe → parent
63
+ useEffect(() => {
64
+ // Only send if embedded and mounted
65
+ if (!isEmbedded || !isMounted) {
66
+ // console.log('[ParentSync] Skipping auth sync - not embedded or not mounted:', {
67
+ // isEmbedded,
68
+ // isMounted
69
+ // });
70
+ return;
71
+ }
72
+
73
+ const authData = {
74
+ isAuthenticated: auth.isAuthenticated,
75
+ isLoading: auth.isLoading,
76
+ hasUser: !!auth.user
77
+ };
78
+
79
+ // console.log('[ParentSync] 📤 Sending auth status to parent:', authData);
80
+
81
+ try {
82
+ window.parent.postMessage({
83
+ type: 'iframe-auth-status',
84
+ data: authData
85
+ }, '*');
86
+ // console.log('[ParentSync] ✅ Auth status sent successfully');
87
+ } catch (e) {
88
+ console.error('[ParentSync] ❌ Failed to send auth status:', e);
89
+ }
90
+ }, [auth.isAuthenticated, auth.isLoading, auth.user, isEmbedded, isMounted]);
91
+
92
+ // 3. Send iframe height changes to parent for auto-resize
93
+ useEffect(() => {
94
+ // Only send if embedded and mounted
95
+ if (!isEmbedded || !isMounted) {
96
+ return;
97
+ }
98
+
99
+ // Function to send height to parent
100
+ const sendHeight = () => {
101
+ const height = document.documentElement.scrollHeight;
102
+ // console.log('[ParentSync] 📏 Sending height to parent:', height);
103
+
104
+ try {
105
+ window.parent.postMessage({
106
+ type: 'iframe-resize',
107
+ data: { height }
108
+ }, '*');
109
+ } catch (e) {
110
+ console.error('[ParentSync] ❌ Failed to send height:', e);
111
+ }
112
+ };
113
+
114
+ // Send initial height
115
+ sendHeight();
116
+
117
+ // Watch for content height changes with ResizeObserver
118
+ const resizeObserver = new ResizeObserver(() => {
119
+ sendHeight();
120
+ });
121
+
122
+ // Observe body element for size changes
123
+ resizeObserver.observe(document.body);
124
+
125
+ // Also observe on route changes (for Next.js)
126
+ const handleRouteChange = () => {
127
+ // Delay to allow content to render
128
+ setTimeout(sendHeight, 100);
129
+ };
130
+
131
+ // Listen for Next.js router events if available
132
+ if (typeof window !== 'undefined') {
133
+ window.addEventListener('popstate', handleRouteChange);
134
+ }
135
+
136
+ return () => {
137
+ resizeObserver.disconnect();
138
+ if (typeof window !== 'undefined') {
139
+ window.removeEventListener('popstate', handleRouteChange);
140
+ }
141
+ };
142
+ }, [isEmbedded, isMounted]);
143
+
144
+ // This component doesn't render anything
145
+ return null;
146
+ }
147
+
148
+ // Export with old name for backward compatibility
149
+ export { ParentSync as AuthStatusSync };
@@ -0,0 +1 @@
1
+ export { ParentSync, AuthStatusSync } from './ParentSync';
@@ -0,0 +1,6 @@
1
+ export { useCfgApp } from './useApp';
2
+ export type { UseCfgAppReturn, UseCfgAppOptions } from './useApp';
3
+
4
+ // Backward compatibility alias
5
+ export { useCfgApp as useApp } from './useApp';
6
+ export type { UseCfgAppReturn as UseAppReturn, UseCfgAppOptions as UseAppOptions } from './useApp';
@@ -0,0 +1,282 @@
1
+ // ============================================================================
2
+ // CfgApp Hook - Application State and Embedding Detection
3
+ // ============================================================================
4
+
5
+ 'use client';
6
+
7
+ import { useState, useEffect } from 'react';
8
+ import { useRouter } from 'next/router';
9
+
10
+ export interface UseCfgAppReturn {
11
+ /**
12
+ * Whether the app is embedded in an iframe
13
+ */
14
+ isEmbedded: boolean;
15
+
16
+ /**
17
+ * Whether the app is running in standalone mode (PWA)
18
+ */
19
+ isStandalone: boolean;
20
+
21
+ /**
22
+ * Whether to disable the layout (useful for iframes)
23
+ */
24
+ disableLayout: boolean;
25
+
26
+ /**
27
+ * The referrer URL if embedded
28
+ */
29
+ referrer: string | null;
30
+
31
+ /**
32
+ * Whether the app has been mounted on the client
33
+ */
34
+ isMounted: boolean;
35
+
36
+ /**
37
+ * The theme received from parent (if embedded)
38
+ */
39
+ parentTheme: 'light' | 'dark' | null;
40
+
41
+ /**
42
+ * The theme mode from parent ('dark', 'light', 'auto')
43
+ */
44
+ parentThemeMode: string | null;
45
+
46
+ /**
47
+ * Handler for setting auth token (used by parent window)
48
+ */
49
+ setToken?: (authToken: string, refreshToken?: string) => void;
50
+ }
51
+
52
+ export interface UseCfgAppOptions {
53
+ /**
54
+ * Handler for setting auth token when received from parent
55
+ */
56
+ onAuthTokenReceived?: (authToken: string, refreshToken?: string) => void;
57
+ }
58
+
59
+ /**
60
+ * Custom hook for detecting app embedding and providing app-level state
61
+ *
62
+ * Features:
63
+ * - Detects iframe embedding
64
+ * - Detects URL-based embedding (`?embed=true`)
65
+ * - Detects standalone PWA mode
66
+ * - Provides referrer information
67
+ * - Handles postMessage communication with parent window
68
+ * - Syncs theme from parent window
69
+ * - Receives auth tokens from parent window
70
+ * - SSR-safe (returns default values on server)
71
+ *
72
+ * Usage:
73
+ * ```tsx
74
+ * const { isEmbedded, disableLayout, referrer, parentTheme } = useCfgApp({
75
+ * onAuthTokenReceived: (authToken, refreshToken) => {
76
+ * api.setToken(authToken, refreshToken);
77
+ * }
78
+ * });
79
+ *
80
+ * return (
81
+ * <AppLayout disableLayout={disableLayout}>
82
+ * {isEmbedded && <div>Running in embedded mode from {referrer}</div>}
83
+ * </AppLayout>
84
+ * );
85
+ * ```
86
+ */
87
+ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
88
+ const router = useRouter();
89
+ const [isMounted, setIsMounted] = useState(false);
90
+ const [isEmbedded, setIsEmbedded] = useState(false);
91
+ const [isStandalone, setIsStandalone] = useState(false);
92
+ const [referrer, setReferrer] = useState<string | null>(null);
93
+ const [parentTheme, setParentTheme] = useState<'light' | 'dark' | null>(null);
94
+ const [parentThemeMode, setParentThemeMode] = useState<string | null>(null);
95
+
96
+ useEffect(() => {
97
+ // Debug logging (uncomment for debugging)
98
+ // console.log('[useCfgApp] Hook initializing...');
99
+ setIsMounted(true);
100
+
101
+ // Check if running in iframe
102
+ const inIframe = window.self !== window.top;
103
+ // console.log('[useCfgApp] Iframe detection:', { inIframe });
104
+ setIsEmbedded(inIframe);
105
+
106
+ // Check if running as standalone PWA
107
+ const standalone = window.matchMedia('(display-mode: standalone)').matches;
108
+ setIsStandalone(standalone);
109
+
110
+ // Get referrer if embedded
111
+ if (inIframe && document.referrer) {
112
+ // console.log('[useCfgApp] Referrer:', document.referrer);
113
+ setReferrer(document.referrer);
114
+ }
115
+
116
+ // Setup resize observer and interval for iframe height updates
117
+ let resizeObserver: ResizeObserver | null = null;
118
+ let checkHeightInterval: NodeJS.Timeout | null = null;
119
+
120
+ // Notify parent window that iframe is ready
121
+ if (inIframe) {
122
+ try {
123
+ // console.log('[useCfgApp] Sending iframe-ready message to parent');
124
+ window.parent.postMessage({
125
+ type: 'iframe-ready',
126
+ data: {
127
+ url: window.location.href,
128
+ referrer: document.referrer
129
+ }
130
+ }, '*'); // Use '*' or specific origin for security
131
+ // console.log('[useCfgApp] iframe-ready message sent');
132
+ } catch (e) {
133
+ console.error('[useCfgApp] Failed to notify parent:', e);
134
+ }
135
+
136
+ // Send current content height (both increases and decreases)
137
+ const sendHeight = () => {
138
+ try {
139
+ // Use body.scrollHeight to avoid iframe height feedback loop
140
+ // document.documentElement.scrollHeight includes iframe's own height!
141
+ const bodyScrollHeight = document.body.scrollHeight;
142
+ const bodyOffsetHeight = document.body.offsetHeight;
143
+ const height = Math.max(bodyScrollHeight, bodyOffsetHeight);
144
+
145
+ window.parent.postMessage({
146
+ type: 'iframe-resize',
147
+ data: { height }
148
+ }, '*');
149
+ } catch (e) {
150
+ console.error('[useCfgApp] Failed to send height:', e);
151
+ }
152
+ };
153
+
154
+ // Send height immediately after mount
155
+ setTimeout(sendHeight, 100);
156
+
157
+ // Watch for content size changes using ResizeObserver
158
+ resizeObserver = new ResizeObserver(() => {
159
+ sendHeight();
160
+ });
161
+
162
+ // Observe document body for size changes
163
+ resizeObserver.observe(document.body);
164
+
165
+ // Also observe on route changes and after data loads
166
+ checkHeightInterval = setInterval(sendHeight, 1000);
167
+ }
168
+
169
+ // Listen for messages from parent window
170
+ const handleMessage = (event: MessageEvent) => {
171
+ // console.log('[useCfgApp] Message received:', {
172
+ // origin: event.origin,
173
+ // type: event.data?.type,
174
+ // data: event.data?.data
175
+ // });
176
+
177
+ // Verify origin for security (optional - adjust for your needs)
178
+ // if (event.origin !== window.location.origin) return;
179
+
180
+ const { type, data } = event.data || {};
181
+
182
+ switch (type) {
183
+ case 'parent-auth':
184
+ // console.log('[useCfgApp] Processing parent-auth message');
185
+ // Receive authentication tokens from parent
186
+ if (data?.authToken && options?.onAuthTokenReceived) {
187
+ try {
188
+ // console.log('[useCfgApp] Calling onAuthTokenReceived');
189
+ options.onAuthTokenReceived(data.authToken, data.refreshToken);
190
+ // console.log('[useCfgApp] Auth tokens received from parent');
191
+ } catch (e) {
192
+ console.error('[useCfgApp] Failed to process auth tokens:', e);
193
+ }
194
+ }
195
+ break;
196
+
197
+ case 'parent-theme':
198
+ // console.log('[useCfgApp] Processing parent-theme message');
199
+ // Receive theme from parent
200
+ if (data?.theme) {
201
+ try {
202
+ // console.log('[useCfgApp] Setting theme:', {
203
+ // theme: data.theme,
204
+ // themeMode: data.themeMode
205
+ // });
206
+ setParentTheme(data.theme);
207
+ if (data.themeMode) {
208
+ setParentThemeMode(data.themeMode);
209
+ }
210
+ // console.log('[useCfgApp] Theme received from parent:', data.theme, 'mode:', data.themeMode);
211
+ } catch (e) {
212
+ console.error('[useCfgApp] Failed to process theme:', e);
213
+ }
214
+ }
215
+ break;
216
+
217
+ case 'parent-resize':
218
+ // Handle parent window resize (optional)
219
+ // console.log('[useCfgApp] Parent resized:', data);
220
+ break;
221
+
222
+ default:
223
+ // if (type) {
224
+ // console.log('[useCfgApp] Unknown message type:', type);
225
+ // }
226
+ break;
227
+ }
228
+ };
229
+
230
+ // console.log('[useCfgApp] Adding message event listener');
231
+ window.addEventListener('message', handleMessage);
232
+
233
+ return () => {
234
+ // console.log('[useCfgApp] Cleaning up message event listener');
235
+ window.removeEventListener('message', handleMessage);
236
+
237
+ // Cleanup resize observer and interval
238
+ if (resizeObserver) {
239
+ resizeObserver.disconnect();
240
+ }
241
+ if (checkHeightInterval) {
242
+ clearInterval(checkHeightInterval);
243
+ }
244
+ };
245
+ }, [options]);
246
+
247
+ // Notify parent about route changes
248
+ useEffect(() => {
249
+ if (!isEmbedded || !isMounted) return;
250
+
251
+ try {
252
+ window.parent.postMessage({
253
+ type: 'iframe-navigation',
254
+ data: {
255
+ path: router.asPath,
256
+ route: router.pathname
257
+ }
258
+ }, '*');
259
+ } catch (e) {
260
+ console.error('[iframe] Failed to notify parent about navigation:', e);
261
+ }
262
+ }, [router.asPath, router.pathname, isEmbedded, isMounted]);
263
+
264
+ // Check URL parameter for embed mode
265
+ const embedParam = router.query.embed === 'true' || router.query.embed === '1';
266
+
267
+ // Determine if layout should be disabled
268
+ // Disable layout if:
269
+ // 1. Running in iframe AND no explicit embed=false param
270
+ // 2. URL has embed=true param
271
+ const shouldDisableLayout = (isEmbedded && router.query.embed !== 'false') || embedParam;
272
+
273
+ return {
274
+ isEmbedded,
275
+ isStandalone,
276
+ disableLayout: shouldDisableLayout,
277
+ referrer,
278
+ isMounted,
279
+ parentTheme,
280
+ parentThemeMode,
281
+ };
282
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CfgLayout - Django CFG Layout with iframe Integration
3
+ *
4
+ * Universal layout component for Django CFG applications
5
+ * Handles iframe embedding, theme sync, and auth communication
6
+ */
7
+
8
+ // Main component
9
+ export { CfgLayout } from './CfgLayout';
10
+ export type { CfgLayoutProps } from './CfgLayout';
11
+
12
+ // Hooks
13
+ export { useCfgApp, useApp } from './hooks';
14
+ export type { UseCfgAppReturn, UseCfgAppOptions, UseAppReturn, UseAppOptions } from './hooks';
15
+
16
+ // Components
17
+ export { ParentSync, AuthStatusSync } from './components';
18
+
19
+ // Types
20
+ export type { CfgLayoutConfig } from './types';
@@ -0,0 +1,45 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /**
4
+ * Configuration for CfgLayout
5
+ * All options are optional - CfgLayout works out of the box with zero config
6
+ */
7
+ export interface CfgLayoutConfig {
8
+ /**
9
+ * Optional handler called when auth tokens are received from parent window
10
+ *
11
+ * Note: Tokens are ALWAYS automatically set in API client via `api.setToken()`.
12
+ * Use this callback only if you need additional custom logic.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <CfgLayout config={{
17
+ * onAuthTokenReceived: (authToken, refreshToken) => {
18
+ * console.log('Tokens received and set in API client');
19
+ * // Additional custom logic here
20
+ * }
21
+ * }}>
22
+ * ```
23
+ */
24
+ onAuthTokenReceived?: (authToken: string, refreshToken?: string) => void;
25
+
26
+ /**
27
+ * Whether to automatically sync theme from parent window
28
+ * @default true
29
+ */
30
+ enableThemeSync?: boolean;
31
+
32
+ /**
33
+ * Whether to automatically send auth status to parent window
34
+ * @default true
35
+ */
36
+ enableAuthStatusSync?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Props for CfgLayout component
41
+ */
42
+ export interface CfgLayoutProps {
43
+ children: ReactNode;
44
+ config?: CfgLayoutConfig;
45
+ }
@@ -11,3 +11,6 @@ export * from './ErrorLayout';
11
11
 
12
12
  // UILayout - Config-driven UI documentation layout
13
13
  export * from './UILayout';
14
+
15
+ // Note: CfgLayout is now part of AppLayout exports
16
+ // Import it via: import { CfgLayout, useCfgApp } from '@djangocfg/layouts';