@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.
@@ -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';