@djangocfg/layouts 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/auth/context/AuthContext.tsx +10 -6
- package/src/layouts/AppLayout/AppLayout.tsx +82 -55
- 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/UILayout/config/components/data.config.tsx +125 -0
- package/src/layouts/UILayout/config/components/feedback.config.tsx +44 -0
- package/src/layouts/UILayout/config/components/forms.config.tsx +194 -0
- package/src/layouts/UILayout/config/components/layout.config.tsx +95 -0
- package/src/layouts/UILayout/config/components/navigation.config.tsx +50 -0
- package/src/layouts/UILayout/config/components/specialized.config.tsx +250 -0
- package/src/layouts/index.ts +3 -0
- package/src/utils/logger.ts +4 -2
|
@@ -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
|
];
|