@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 +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +50 -26
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/index.ts +12 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/CfgLayout.tsx +104 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/README.md +389 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/components/ParentSync.tsx +149 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/components/index.ts +1 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/hooks/useApp.ts +282 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/index.ts +20 -0
- package/src/layouts/AppLayout/layouts/CfgLayout/types/index.ts +45 -0
- package/src/layouts/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
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.
|
|
57
|
-
"@djangocfg/og-image": "^1.2.
|
|
58
|
-
"@djangocfg/ui": "^1.2.
|
|
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.
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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-
|
|
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.
|
|
24
|
+
"version": "1.2.7"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.7"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.7"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.7"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.7"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.7"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
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
|
+
}
|
package/src/layouts/index.ts
CHANGED