@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,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
+ }
@@ -21,6 +21,17 @@ import {
21
21
  Toggle,
22
22
  ToggleGroup,
23
23
  ToggleGroupItem,
24
+ Calendar,
25
+ Carousel,
26
+ CarouselContent,
27
+ CarouselItem,
28
+ CarouselNext,
29
+ CarouselPrevious,
30
+ ChartContainer,
31
+ ChartTooltip,
32
+ ChartTooltipContent,
33
+ ChartLegend,
34
+ ChartLegendContent,
24
35
  } from '@djangocfg/ui';
25
36
  import type { ComponentConfig } from './types';
26
37
 
@@ -305,4 +316,118 @@ export const DATA_COMPONENTS: ComponentConfig[] = [
305
316
  </div>
306
317
  ),
307
318
  },
319
+ {
320
+ name: 'Calendar',
321
+ category: 'data',
322
+ description: 'Date picker calendar component',
323
+ importPath: `import { Calendar } from '@djangocfg/ui';`,
324
+ example: `<Calendar
325
+ mode="single"
326
+ selected={date}
327
+ onSelect={setDate}
328
+ className="rounded-md border"
329
+ />`,
330
+ preview: (
331
+ <Calendar
332
+ mode="single"
333
+ className="rounded-md border"
334
+ />
335
+ ),
336
+ },
337
+ {
338
+ name: 'Carousel',
339
+ category: 'data',
340
+ description: 'Image and content carousel with navigation',
341
+ importPath: `import {
342
+ Carousel,
343
+ CarouselContent,
344
+ CarouselItem,
345
+ CarouselNext,
346
+ CarouselPrevious,
347
+ } from '@djangocfg/ui';`,
348
+ example: `<Carousel className="w-full max-w-xs">
349
+ <CarouselContent>
350
+ <CarouselItem>
351
+ <div className="p-6 border rounded-md">
352
+ <span className="text-4xl font-semibold">1</span>
353
+ </div>
354
+ </CarouselItem>
355
+ <CarouselItem>
356
+ <div className="p-6 border rounded-md">
357
+ <span className="text-4xl font-semibold">2</span>
358
+ </div>
359
+ </CarouselItem>
360
+ <CarouselItem>
361
+ <div className="p-6 border rounded-md">
362
+ <span className="text-4xl font-semibold">3</span>
363
+ </div>
364
+ </CarouselItem>
365
+ </CarouselContent>
366
+ <CarouselPrevious />
367
+ <CarouselNext />
368
+ </Carousel>`,
369
+ preview: (
370
+ <Carousel className="w-full max-w-xs">
371
+ <CarouselContent>
372
+ {Array.from({ length: 5 }).map((_, index) => (
373
+ <CarouselItem key={index}>
374
+ <div className="p-6 border rounded-md bg-muted/50">
375
+ <div className="flex aspect-square items-center justify-center">
376
+ <span className="text-4xl font-semibold">{index + 1}</span>
377
+ </div>
378
+ </div>
379
+ </CarouselItem>
380
+ ))}
381
+ </CarouselContent>
382
+ <CarouselPrevious />
383
+ <CarouselNext />
384
+ </Carousel>
385
+ ),
386
+ },
387
+ {
388
+ name: 'Chart',
389
+ category: 'data',
390
+ description: 'Data visualization charts powered by Recharts',
391
+ importPath: `import { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent } from '@djangocfg/ui';`,
392
+ example: `import { Bar, BarChart, XAxis, YAxis } from 'recharts';
393
+
394
+ const chartConfig = {
395
+ sales: { label: "Sales", color: "hsl(var(--chart-1))" },
396
+ profit: { label: "Profit", color: "hsl(var(--chart-2))" },
397
+ };
398
+
399
+ const chartData = [
400
+ { month: "Jan", sales: 400, profit: 240 },
401
+ { month: "Feb", sales: 300, profit: 180 },
402
+ { month: "Mar", sales: 500, profit: 300 },
403
+ ];
404
+
405
+ <ChartContainer config={chartConfig} className="min-h-[200px] w-full">
406
+ <BarChart data={chartData}>
407
+ <XAxis dataKey="month" />
408
+ <YAxis />
409
+ <ChartTooltip content={<ChartTooltipContent />} />
410
+ <ChartLegend content={<ChartLegendContent />} />
411
+ <Bar dataKey="sales" fill="var(--color-sales)" />
412
+ <Bar dataKey="profit" fill="var(--color-profit)" />
413
+ </BarChart>
414
+ </ChartContainer>`,
415
+ preview: (
416
+ <div className="p-6 border rounded-md bg-muted/50">
417
+ <p className="text-sm text-muted-foreground mb-4">
418
+ Chart components built on Recharts with:
419
+ </p>
420
+ <ul className="space-y-2 text-sm text-muted-foreground">
421
+ <li>• Bar, Line, Area, Pie charts</li>
422
+ <li>• Responsive container</li>
423
+ <li>• Theme-aware colors</li>
424
+ <li>• Tooltips and legends</li>
425
+ <li>• Customizable styling</li>
426
+ </ul>
427
+ <p className="text-xs text-muted-foreground mt-4">
428
+ See <strong>Recharts documentation</strong> for all chart types
429
+ </p>
430
+ </div>
431
+ ),
432
+ },
308
433
  ];
@@ -14,6 +14,7 @@ import {
14
14
  AvatarImage,
15
15
  Button,
16
16
  useToast,
17
+ Toaster,
17
18
  } from '@djangocfg/ui';
18
19
  import type { ComponentConfig } from './types';
19
20
 
@@ -243,4 +244,47 @@ export const FEEDBACK_COMPONENTS: ComponentConfig[] = [
243
244
  </div>
244
245
  ),
245
246
  },
247
+ {
248
+ name: 'Toaster',
249
+ category: 'feedback',
250
+ description: 'Global toast notification container (works with Toast component)',
251
+ importPath: `import { Toaster, useToast } from '@djangocfg/ui';`,
252
+ example: `// Add Toaster once in your app layout
253
+ <Toaster />
254
+
255
+ // Then use the useToast hook anywhere
256
+ function MyComponent() {
257
+ const { toast } = useToast();
258
+
259
+ return (
260
+ <Button
261
+ onClick={() => {
262
+ toast({
263
+ title: "Scheduled: Catch up",
264
+ description: "Friday, February 10, 2023 at 5:57 PM",
265
+ });
266
+ }}
267
+ >
268
+ Show Toast
269
+ </Button>
270
+ );
271
+ }`,
272
+ preview: (
273
+ <div className="p-6 border rounded-md bg-muted/50">
274
+ <p className="text-sm text-muted-foreground mb-4">
275
+ Toaster is the global container for Toast notifications:
276
+ </p>
277
+ <ul className="space-y-2 text-sm text-muted-foreground">
278
+ <li>• Add once to your app layout</li>
279
+ <li>• Use with useToast hook</li>
280
+ <li>• Manages toast queue and positioning</li>
281
+ <li>• Accessible and keyboard navigable</li>
282
+ <li>• Works with Toast component</li>
283
+ </ul>
284
+ <p className="text-xs text-muted-foreground mt-4">
285
+ ℹ️ Different from Sonner - this is the built-in Toast system
286
+ </p>
287
+ </div>
288
+ ),
289
+ },
246
290
  ];