@djangocfg/layouts 1.2.58 → 1.4.1
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/index.ts +3 -0
- package/src/layouts/AppLayout/AppLayout.tsx +56 -55
- package/src/layouts/AppLayout/components/UpdateNotifier/UpdateNotifier.tsx +170 -0
- package/src/layouts/AppLayout/components/UpdateNotifier/index.ts +2 -0
- package/src/layouts/AppLayout/components/index.ts +2 -2
- package/src/layouts/AppLayout/context/AppContext.tsx +4 -4
- package/src/layouts/AppLayout/index.ts +2 -1
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +1 -8
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +1 -8
- package/src/layouts/AppLayout/types/index.ts +1 -0
- package/src/layouts/AppLayout/types/page.ts +80 -0
- package/src/layouts/AppLayout/types/routes.ts +0 -5
- package/src/types/index.ts +2 -1
- package/src/types/pageConfig.ts +5 -8
- package/src/layouts/AppLayout/components/PackageVersions/PackageVersions.tsx +0 -101
- package/src/layouts/AppLayout/components/PackageVersions/index.ts +0 -7
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +0 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.
|
|
67
|
-
"@djangocfg/og-image": "^1.
|
|
68
|
-
"@djangocfg/ui": "^1.
|
|
66
|
+
"@djangocfg/api": "^1.4.1",
|
|
67
|
+
"@djangocfg/og-image": "^1.4.1",
|
|
68
|
+
"@djangocfg/ui": "^1.4.1",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.4.1",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
package/src/index.ts
CHANGED
|
@@ -29,17 +29,17 @@ import { useRouter } from 'next/router';
|
|
|
29
29
|
import dynamic from 'next/dynamic';
|
|
30
30
|
import { AppContextProvider } from './context';
|
|
31
31
|
import { CoreProviders } from './providers';
|
|
32
|
-
import { Seo, PageProgress, ErrorBoundary } from './components';
|
|
32
|
+
import { Seo, PageProgress, ErrorBoundary, UpdateNotifier } from './components';
|
|
33
33
|
import { PublicLayout } from './layouts/PublicLayout';
|
|
34
34
|
import { PrivateLayout } from './layouts/PrivateLayout';
|
|
35
35
|
import { AuthLayout } from './layouts/AuthLayout';
|
|
36
36
|
import { PagePreloader } from './layouts/AdminLayout/components';
|
|
37
37
|
import { determineLayoutMode, getRedirectUrl } from './utils';
|
|
38
38
|
import { useAuth } from '../../auth';
|
|
39
|
-
import type { AppLayoutConfig } from './types';
|
|
39
|
+
import type { AppLayoutConfig, PageWithLayout, LayoutMode } from './types';
|
|
40
40
|
import type { ValidationErrorConfig, CORSErrorConfig, NetworkErrorConfig } from '../../validation';
|
|
41
|
-
import type { PageWithConfig } from '../../types/pageConfig';
|
|
42
41
|
import { determinePageConfig } from '../../types/pageConfig';
|
|
42
|
+
import packageJson from '../../../package.json';
|
|
43
43
|
|
|
44
44
|
// Dynamic import for AdminLayout to prevent SSR hydration issues
|
|
45
45
|
const AdminLayout = dynamic(
|
|
@@ -51,31 +51,16 @@ export interface AppLayoutProps {
|
|
|
51
51
|
children: ReactNode;
|
|
52
52
|
config: AppLayoutConfig;
|
|
53
53
|
/**
|
|
54
|
-
* Next.js page component (for reading pageConfig)
|
|
54
|
+
* Next.js page component (for reading pageConfig and layout preferences)
|
|
55
|
+
* Pass Component from _app.tsx to enable smart layout detection
|
|
55
56
|
* @example component={Component}
|
|
56
57
|
*/
|
|
57
|
-
component?:
|
|
58
|
+
component?: PageWithLayout | any;
|
|
58
59
|
/**
|
|
59
60
|
* Next.js page props (for reading dynamic pageConfig from SSR)
|
|
60
61
|
* @example pageProps={pageProps}
|
|
61
62
|
*/
|
|
62
63
|
pageProps?: Record<string, any>;
|
|
63
|
-
/**
|
|
64
|
-
* Disable layout rendering (Navigation, Sidebar, Footer)
|
|
65
|
-
* Only providers and SEO remain active
|
|
66
|
-
* Useful for custom layouts like landing pages
|
|
67
|
-
*/
|
|
68
|
-
disableLayout?: boolean;
|
|
69
|
-
/**
|
|
70
|
-
* Force a specific layout regardless of route
|
|
71
|
-
* Overrides automatic layout detection
|
|
72
|
-
*
|
|
73
|
-
* @example forceLayout="public" - always use PublicLayout
|
|
74
|
-
* @example forceLayout="private" - always use PrivateLayout
|
|
75
|
-
* @example forceLayout="auth" - always use AuthLayout
|
|
76
|
-
* @example forceLayout="admin" - Django CFG admin mode with iframe integration
|
|
77
|
-
*/
|
|
78
|
-
forceLayout?: 'public' | 'private' | 'auth' | 'admin';
|
|
79
64
|
/**
|
|
80
65
|
* Font family to apply globally
|
|
81
66
|
* Accepts Next.js font object or CSS font-family string
|
|
@@ -84,11 +69,11 @@ export interface AppLayoutProps {
|
|
|
84
69
|
*/
|
|
85
70
|
fontFamily?: string;
|
|
86
71
|
/**
|
|
87
|
-
* Show
|
|
88
|
-
* @default
|
|
89
|
-
* @example
|
|
72
|
+
* Show update notifier (checks npm for new versions)
|
|
73
|
+
* @default true
|
|
74
|
+
* @example showUpdateNotifier={false}
|
|
90
75
|
*/
|
|
91
|
-
|
|
76
|
+
showUpdateNotifier?: boolean;
|
|
92
77
|
/**
|
|
93
78
|
* Validation error tracking configuration
|
|
94
79
|
* @default { enabled: true, showToast: true, maxErrors: 50 }
|
|
@@ -109,18 +94,18 @@ export interface AppLayoutProps {
|
|
|
109
94
|
/**
|
|
110
95
|
* Layout Router Component
|
|
111
96
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
97
|
+
* Smart layout detection with priority:
|
|
98
|
+
* 1. component.getLayout (custom layout function)
|
|
99
|
+
* 2. component.layoutMode ('none' | 'public' | 'private' | 'auth' | 'admin')
|
|
100
|
+
* 3. Automatic route-based detection
|
|
114
101
|
*/
|
|
115
102
|
function LayoutRouter({
|
|
116
103
|
children,
|
|
117
|
-
|
|
118
|
-
forceLayout,
|
|
104
|
+
component,
|
|
119
105
|
config
|
|
120
106
|
}: {
|
|
121
107
|
children: ReactNode;
|
|
122
|
-
|
|
123
|
-
forceLayout?: 'public' | 'private' | 'auth' | 'admin';
|
|
108
|
+
component?: PageWithLayout | any;
|
|
124
109
|
config: AppLayoutConfig;
|
|
125
110
|
}) {
|
|
126
111
|
const router = useRouter();
|
|
@@ -132,8 +117,18 @@ function LayoutRouter({
|
|
|
132
117
|
setIsMounted(true);
|
|
133
118
|
}, []);
|
|
134
119
|
|
|
135
|
-
//
|
|
136
|
-
|
|
120
|
+
// Priority 1: Check if page has custom getLayout function
|
|
121
|
+
const hasCustomLayout = component && typeof component.getLayout === 'function';
|
|
122
|
+
if (hasCustomLayout) {
|
|
123
|
+
// Use custom layout - render children directly (getLayout applied in _app.tsx)
|
|
124
|
+
return <>{children}</>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Priority 2: Check component.layoutMode
|
|
128
|
+
const componentLayoutMode = component?.layoutMode;
|
|
129
|
+
|
|
130
|
+
// If layoutMode is 'none', render children directly
|
|
131
|
+
if (componentLayoutMode === 'none') {
|
|
137
132
|
return <>{children}</>;
|
|
138
133
|
}
|
|
139
134
|
|
|
@@ -144,7 +139,7 @@ function LayoutRouter({
|
|
|
144
139
|
|
|
145
140
|
// Admin routes: Always show loading during SSR and initial client render
|
|
146
141
|
// This prevents hydration mismatch when isAuthenticated differs between server/client
|
|
147
|
-
if (isAdminRoute && !
|
|
142
|
+
if ((isAdminRoute && !componentLayoutMode) || componentLayoutMode === 'admin') {
|
|
148
143
|
// In embedded mode (iframe), render AdminLayout immediately to receive postMessage
|
|
149
144
|
const isEmbedded = typeof window !== 'undefined' && window !== window.parent;
|
|
150
145
|
|
|
@@ -185,7 +180,7 @@ function LayoutRouter({
|
|
|
185
180
|
|
|
186
181
|
// Private routes: Always show loading during SSR and initial client render
|
|
187
182
|
// This prevents hydration mismatch when isAuthenticated differs between server/client
|
|
188
|
-
if (isPrivateRoute && !
|
|
183
|
+
if ((isPrivateRoute && !componentLayoutMode) || componentLayoutMode === 'private') {
|
|
189
184
|
if (!isMounted || isLoading) {
|
|
190
185
|
return <PagePreloader />;
|
|
191
186
|
}
|
|
@@ -206,11 +201,12 @@ function LayoutRouter({
|
|
|
206
201
|
return <PrivateLayout>{children}</PrivateLayout>;
|
|
207
202
|
}
|
|
208
203
|
|
|
209
|
-
// Determine layout mode for non-private routes
|
|
204
|
+
// Determine layout mode for non-private/admin routes
|
|
210
205
|
const getLayoutMode = (): 'public' | 'auth' | 'admin' => {
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
if (
|
|
206
|
+
// Priority: componentLayoutMode > auto-detect
|
|
207
|
+
if (componentLayoutMode === 'auth') return 'auth';
|
|
208
|
+
if (componentLayoutMode === 'public') return 'public';
|
|
209
|
+
if (componentLayoutMode === 'admin') return 'admin';
|
|
214
210
|
if (isAuthRoute) return 'auth';
|
|
215
211
|
return 'public';
|
|
216
212
|
};
|
|
@@ -250,33 +246,35 @@ function LayoutRouter({
|
|
|
250
246
|
/**
|
|
251
247
|
* AppLayout - Main Component
|
|
252
248
|
*
|
|
253
|
-
* Single entry point for all layout logic
|
|
249
|
+
* Single entry point for all layout logic with smart layout detection
|
|
254
250
|
* Wrap your app once in _app.tsx
|
|
255
251
|
*
|
|
256
252
|
* @example
|
|
257
253
|
* ```tsx
|
|
258
|
-
* //
|
|
259
|
-
* <AppLayout config={appLayoutConfig}>
|
|
260
|
-
* <Component {...pageProps} />
|
|
254
|
+
* // Smart auto-detection (recommended)
|
|
255
|
+
* <AppLayout config={appLayoutConfig} component={Component} pageProps={pageProps}>
|
|
256
|
+
* {Component.getLayout ? Component.getLayout(<Component {...pageProps} />) : <Component {...pageProps} />}
|
|
261
257
|
* </AppLayout>
|
|
262
258
|
*
|
|
263
259
|
* // With custom font
|
|
264
|
-
* <AppLayout config={appLayoutConfig} fontFamily={
|
|
260
|
+
* <AppLayout config={appLayoutConfig} component={Component} fontFamily={inter.style.fontFamily}>
|
|
265
261
|
* <Component {...pageProps} />
|
|
266
262
|
* </AppLayout>
|
|
267
263
|
*
|
|
268
|
-
* //
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* </AppLayout>
|
|
264
|
+
* // Page with custom layout (in page file)
|
|
265
|
+
* const Page: PageWithLayout = () => <div>Content</div>;
|
|
266
|
+
* Page.getLayout = (page) => <CustomLayout>{page}</CustomLayout>;
|
|
272
267
|
*
|
|
273
|
-
* //
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
268
|
+
* // Page with forced layout mode (in page file)
|
|
269
|
+
* const DashboardPage: PageWithLayout = () => <div>Dashboard</div>;
|
|
270
|
+
* DashboardPage.layoutMode = 'private';
|
|
271
|
+
*
|
|
272
|
+
* // Page without layout (in page file)
|
|
273
|
+
* const LandingPage: PageWithLayout = () => <FullPageDesign />;
|
|
274
|
+
* LandingPage.layoutMode = 'none';
|
|
277
275
|
* ```
|
|
278
276
|
*/
|
|
279
|
-
export function AppLayout({ children, config, component, pageProps,
|
|
277
|
+
export function AppLayout({ children, config, component, pageProps, fontFamily, showUpdateNotifier, validation, cors, network }: AppLayoutProps) {
|
|
280
278
|
const router = useRouter();
|
|
281
279
|
|
|
282
280
|
// Check if ErrorBoundary is enabled (default: true)
|
|
@@ -302,7 +300,7 @@ export function AppLayout({ children, config, component, pageProps, disableLayou
|
|
|
302
300
|
};
|
|
303
301
|
|
|
304
302
|
const appContent = (
|
|
305
|
-
<AppContextProvider config={config}
|
|
303
|
+
<AppContextProvider config={config} showUpdateNotifier={showUpdateNotifier}>
|
|
306
304
|
{/* SEO Meta Tags */}
|
|
307
305
|
<Seo
|
|
308
306
|
pageConfig={finalPageConfig}
|
|
@@ -310,11 +308,14 @@ export function AppLayout({ children, config, component, pageProps, disableLayou
|
|
|
310
308
|
siteUrl={config.app.siteUrl}
|
|
311
309
|
/>
|
|
312
310
|
|
|
311
|
+
{/* Update Notifier */}
|
|
312
|
+
<UpdateNotifier enabled={showUpdateNotifier} currentVersion={packageJson.version} />
|
|
313
|
+
|
|
313
314
|
{/* Loading Progress Bar */}
|
|
314
315
|
<PageProgress />
|
|
315
316
|
|
|
316
317
|
{/* Smart Layout Router */}
|
|
317
|
-
<LayoutRouter
|
|
318
|
+
<LayoutRouter component={component} config={config}>
|
|
318
319
|
{children}
|
|
319
320
|
</LayoutRouter>
|
|
320
321
|
</AppContextProvider>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Notifier Component
|
|
3
|
+
*
|
|
4
|
+
* Checks npm registry for @djangocfg package updates
|
|
5
|
+
* Shows toast notification when new version is available
|
|
6
|
+
* Uses localStorage to cache check and avoid spam
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import React, { useEffect, useState } from 'react';
|
|
12
|
+
import { toast } from '@djangocfg/ui/hooks';
|
|
13
|
+
|
|
14
|
+
const PACKAGE_NAME = '@djangocfg/layouts';
|
|
15
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
16
|
+
const CACHE_KEY = 'djangocfg_update_check';
|
|
17
|
+
|
|
18
|
+
interface UpdateCheckCache {
|
|
19
|
+
lastCheck: number;
|
|
20
|
+
latestVersion: string;
|
|
21
|
+
dismissed: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UpdateNotifierProps {
|
|
25
|
+
/**
|
|
26
|
+
* Enable update notifications
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Current package version (auto-injected from package.json)
|
|
32
|
+
*/
|
|
33
|
+
currentVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare semver versions
|
|
38
|
+
* Returns true if newVersion > currentVersion
|
|
39
|
+
*/
|
|
40
|
+
function isNewerVersion(current: string, latest: string): boolean {
|
|
41
|
+
const parseCurrent = current.split('.').map(Number);
|
|
42
|
+
const parseLatest = latest.split('.').map(Number);
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
if (parseLatest[i] > parseCurrent[i]) return true;
|
|
46
|
+
if (parseLatest[i] < parseCurrent[i]) return false;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch latest version from npm registry
|
|
53
|
+
*/
|
|
54
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: { 'Accept': 'application/json' },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) return null;
|
|
62
|
+
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
return data.version || null;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn('[UpdateNotifier] Failed to check for updates:', error);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get cached update check data
|
|
73
|
+
*/
|
|
74
|
+
function getCache(): UpdateCheckCache | null {
|
|
75
|
+
if (typeof window === 'undefined') return null;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
79
|
+
if (!cached) return null;
|
|
80
|
+
|
|
81
|
+
const data: UpdateCheckCache = JSON.parse(cached);
|
|
82
|
+
return data;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Save update check data to cache
|
|
90
|
+
*/
|
|
91
|
+
function setCache(data: UpdateCheckCache): void {
|
|
92
|
+
if (typeof window === 'undefined') return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('[UpdateNotifier] Failed to cache update check:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function UpdateNotifier({ enabled = false, currentVersion }: UpdateNotifierProps) {
|
|
102
|
+
const [checked, setChecked] = useState(false);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!enabled || checked || typeof window === 'undefined') return;
|
|
106
|
+
if (!currentVersion) return;
|
|
107
|
+
|
|
108
|
+
const checkForUpdates = async () => {
|
|
109
|
+
// Check cache first
|
|
110
|
+
const cache = getCache();
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
|
|
113
|
+
// If we checked recently, skip
|
|
114
|
+
if (cache && (now - cache.lastCheck) < CHECK_INTERVAL_MS) {
|
|
115
|
+
// Show notification if there's an update and it wasn't dismissed
|
|
116
|
+
if (cache.latestVersion && !cache.dismissed && isNewerVersion(currentVersion, cache.latestVersion)) {
|
|
117
|
+
showUpdateNotification(currentVersion, cache.latestVersion);
|
|
118
|
+
}
|
|
119
|
+
setChecked(true);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fetch latest version from npm
|
|
124
|
+
const latestVersion = await fetchLatestVersion();
|
|
125
|
+
|
|
126
|
+
if (!latestVersion) {
|
|
127
|
+
setChecked(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Update cache
|
|
132
|
+
setCache({
|
|
133
|
+
lastCheck: now,
|
|
134
|
+
latestVersion,
|
|
135
|
+
dismissed: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Show notification if newer version available
|
|
139
|
+
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
140
|
+
showUpdateNotification(currentVersion, latestVersion);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setChecked(true);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Check after a short delay to not block initial render
|
|
147
|
+
const timer = setTimeout(checkForUpdates, 2000);
|
|
148
|
+
|
|
149
|
+
return () => clearTimeout(timer);
|
|
150
|
+
}, [enabled, checked, currentVersion]);
|
|
151
|
+
|
|
152
|
+
return null; // This component doesn't render anything
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Show update notification toast
|
|
157
|
+
*/
|
|
158
|
+
function showUpdateNotification(currentVersion: string, latestVersion: string) {
|
|
159
|
+
toast({
|
|
160
|
+
title: `📦 Update Available`,
|
|
161
|
+
description: `New version ${latestVersion} of @djangocfg packages is available. You're using ${currentVersion}. Run: pnpm update @djangocfg/layouts@latest`,
|
|
162
|
+
duration: 10000,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Mark as dismissed in cache after showing
|
|
166
|
+
const cache = getCache();
|
|
167
|
+
if (cache) {
|
|
168
|
+
setCache({ ...cache, dismissed: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export { default as Seo } from './Seo';
|
|
6
6
|
export { default as PageProgress } from './PageProgress';
|
|
7
7
|
export { ErrorBoundary } from './ErrorBoundary';
|
|
8
|
-
export {
|
|
8
|
+
export { UpdateNotifier } from './UpdateNotifier';
|
|
9
9
|
export { UserMenu } from './UserMenu';
|
|
10
|
-
export type {
|
|
10
|
+
export type { UpdateNotifierProps } from './UpdateNotifier';
|
|
11
11
|
export type { UserMenuProps } from './UserMenu';
|
|
@@ -43,7 +43,7 @@ interface AppContextValue {
|
|
|
43
43
|
toggleSidebar: () => void;
|
|
44
44
|
|
|
45
45
|
// Features
|
|
46
|
-
|
|
46
|
+
showUpdateNotifier?: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -59,7 +59,7 @@ const AppContext = createContext<AppContextValue | null>(null);
|
|
|
59
59
|
export interface AppContextProviderProps {
|
|
60
60
|
children: ReactNode;
|
|
61
61
|
config: AppLayoutConfig;
|
|
62
|
-
|
|
62
|
+
showUpdateNotifier?: boolean;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -68,7 +68,7 @@ export interface AppContextProviderProps {
|
|
|
68
68
|
* Provides unified application context to all child components
|
|
69
69
|
* Manages layout state and exposes configuration
|
|
70
70
|
*/
|
|
71
|
-
export function AppContextProvider({ children, config,
|
|
71
|
+
export function AppContextProvider({ children, config, showUpdateNotifier }: AppContextProviderProps) {
|
|
72
72
|
const router = useRouter();
|
|
73
73
|
|
|
74
74
|
// UI state
|
|
@@ -117,7 +117,7 @@ export function AppContextProvider({ children, config, showPackageVersions }: Ap
|
|
|
117
117
|
collapseSidebar,
|
|
118
118
|
expandSidebar,
|
|
119
119
|
toggleSidebar,
|
|
120
|
-
|
|
120
|
+
showUpdateNotifier,
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AppLayout - Unified Application Layout System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Smart layout system with automatic detection
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main component
|
|
@@ -19,6 +19,7 @@ export type {
|
|
|
19
19
|
NavigationSection,
|
|
20
20
|
DashboardMenuItem,
|
|
21
21
|
DashboardMenuGroup,
|
|
22
|
+
PageWithLayout,
|
|
22
23
|
} from './types';
|
|
23
24
|
|
|
24
25
|
// Context and hooks
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
} from '@djangocfg/ui/components';
|
|
30
30
|
import { useAppContext } from '../../../context';
|
|
31
31
|
import { useNavigation } from '../../../hooks';
|
|
32
|
-
import { PackageVersions } from '../../../components';
|
|
33
32
|
|
|
34
33
|
export interface DashboardSidebarProps {
|
|
35
34
|
isAdmin?: boolean;
|
|
@@ -49,7 +48,7 @@ export interface DashboardSidebarProps {
|
|
|
49
48
|
* All data from context!
|
|
50
49
|
*/
|
|
51
50
|
export function DashboardSidebar({ isAdmin = false }: DashboardSidebarProps) {
|
|
52
|
-
const { config
|
|
51
|
+
const { config } = useAppContext();
|
|
53
52
|
const { currentPath } = useNavigation();
|
|
54
53
|
const { state, isMobile } = useSidebar();
|
|
55
54
|
|
|
@@ -177,12 +176,6 @@ export function DashboardSidebar({ isAdmin = false }: DashboardSidebarProps) {
|
|
|
177
176
|
</SidebarGroup>
|
|
178
177
|
))}
|
|
179
178
|
</SidebarContent>
|
|
180
|
-
|
|
181
|
-
{showPackageVersions && (
|
|
182
|
-
<SidebarFooter>
|
|
183
|
-
<PackageVersions variant="sidebar" />
|
|
184
|
-
</SidebarFooter>
|
|
185
|
-
)}
|
|
186
179
|
</Sidebar>
|
|
187
180
|
);
|
|
188
181
|
}
|
|
@@ -11,7 +11,6 @@ import React from 'react';
|
|
|
11
11
|
import Link from 'next/link';
|
|
12
12
|
import { useIsMobile } from '@djangocfg/ui/hooks';
|
|
13
13
|
import { useAppContext } from '../../../context';
|
|
14
|
-
import { PackageVersions } from '../../../components';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Footer Component
|
|
@@ -28,7 +27,7 @@ import { PackageVersions } from '../../../components';
|
|
|
28
27
|
* All data from context!
|
|
29
28
|
*/
|
|
30
29
|
export function Footer() {
|
|
31
|
-
const { config
|
|
30
|
+
const { config } = useAppContext();
|
|
32
31
|
const isMobile = useIsMobile();
|
|
33
32
|
|
|
34
33
|
const { app, publicLayout } = config;
|
|
@@ -60,9 +59,6 @@ export function Footer() {
|
|
|
60
59
|
|
|
61
60
|
{/* Quick Links */}
|
|
62
61
|
<div className="flex flex-wrap justify-center gap-4 mb-6 items-center">
|
|
63
|
-
{showPackageVersions && (
|
|
64
|
-
<PackageVersions variant="footer-minimal" />
|
|
65
|
-
)}
|
|
66
62
|
{footer.links.docs && (
|
|
67
63
|
<a
|
|
68
64
|
href={footer.links.docs}
|
|
@@ -193,9 +189,6 @@ export function Footer() {
|
|
|
193
189
|
</a>
|
|
194
190
|
</div>
|
|
195
191
|
<div className="flex flex-wrap items-center gap-4">
|
|
196
|
-
{showPackageVersions && (
|
|
197
|
-
<PackageVersions variant="footer-minimal" />
|
|
198
|
-
)}
|
|
199
192
|
{footer.links.docs && (
|
|
200
193
|
<a
|
|
201
194
|
href={footer.links.docs}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Universal types for Next.js pages with layout and SEO support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
8
|
+
import type { NextPage } from 'next';
|
|
9
|
+
import type { PageConfig } from '../../../types/pageConfig';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Layout mode types
|
|
13
|
+
*
|
|
14
|
+
* - 'public': Public layout (landing, docs, etc.)
|
|
15
|
+
* - 'private': Private layout (dashboard, authenticated pages)
|
|
16
|
+
* - 'auth': Auth layout (login, register, OTP)
|
|
17
|
+
* - 'admin': Admin layout (Django CFG iframe integration)
|
|
18
|
+
* - 'none': No layout (fully custom page)
|
|
19
|
+
*/
|
|
20
|
+
export type LayoutMode = 'public' | 'private' | 'auth' | 'admin' | 'none';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Universal Page Type - combines layout and SEO configuration
|
|
24
|
+
*
|
|
25
|
+
* Extends NextPage with:
|
|
26
|
+
* - Layout control (getLayout, layoutMode)
|
|
27
|
+
* - SEO metadata (pageConfig)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // Custom layout using getLayout
|
|
32
|
+
* const Page: PageWithLayout = () => <div>Content</div>;
|
|
33
|
+
* Page.getLayout = (page) => <CustomLayout>{page}</CustomLayout>;
|
|
34
|
+
*
|
|
35
|
+
* // Force specific layout mode
|
|
36
|
+
* const DashboardPage: PageWithLayout = () => <div>Dashboard</div>;
|
|
37
|
+
* DashboardPage.layoutMode = 'private';
|
|
38
|
+
*
|
|
39
|
+
* // With SEO config
|
|
40
|
+
* const HomePage: PageWithLayout = () => <div>Home</div>;
|
|
41
|
+
* HomePage.pageConfig = {
|
|
42
|
+
* title: 'Home Page',
|
|
43
|
+
* description: 'Welcome to our site',
|
|
44
|
+
* ogImage: { title: 'Home', subtitle: 'Welcome' }
|
|
45
|
+
* };
|
|
46
|
+
*
|
|
47
|
+
* // Disable all layouts
|
|
48
|
+
* const LandingPage: PageWithLayout = () => <FullPageDesign />;
|
|
49
|
+
* LandingPage.layoutMode = 'none';
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export type PageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
|
53
|
+
/**
|
|
54
|
+
* Custom layout function
|
|
55
|
+
* When provided, AppLayout will use this instead of automatic layout detection
|
|
56
|
+
*
|
|
57
|
+
* @param page - The page component wrapped in React element
|
|
58
|
+
* @returns Layout-wrapped page
|
|
59
|
+
*/
|
|
60
|
+
getLayout?: (page: ReactElement) => ReactNode;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Force specific layout mode
|
|
64
|
+
* Overrides automatic route detection
|
|
65
|
+
*
|
|
66
|
+
* - undefined: Auto-detect based on route
|
|
67
|
+
* - 'public': Always use PublicLayout
|
|
68
|
+
* - 'private': Always use PrivateLayout
|
|
69
|
+
* - 'auth': Always use AuthLayout
|
|
70
|
+
* - 'admin': Always use AdminLayout
|
|
71
|
+
* - 'none': No layout (page renders directly)
|
|
72
|
+
*/
|
|
73
|
+
layoutMode?: LayoutMode;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Page SEO and metadata configuration
|
|
77
|
+
* Used for <head> tags, OpenGraph, Twitter cards, etc.
|
|
78
|
+
*/
|
|
79
|
+
pageConfig?: PageConfig;
|
|
80
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export type { PageConfig
|
|
1
|
+
export type { PageConfig } from "./pageConfig";
|
|
2
|
+
export { determinePageConfig } from "./pageConfig";
|
package/src/types/pageConfig.ts
CHANGED
|
@@ -54,16 +54,13 @@ export interface PageConfig {
|
|
|
54
54
|
twitter?: TwitterConfig;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Type for a Page component that includes page configuration
|
|
58
|
-
export type PageWithConfig<T = {}> = FC<T> & {
|
|
59
|
-
pageConfig?: PageConfig;
|
|
60
|
-
[key: string]: any;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
57
|
// --- Helper Function ---
|
|
58
|
+
/**
|
|
59
|
+
* Determine final page config by merging static and dynamic configs
|
|
60
|
+
*/
|
|
64
61
|
export const determinePageConfig = (
|
|
65
|
-
Component:
|
|
66
|
-
pageProps: Record<string, any>,
|
|
62
|
+
Component: any,
|
|
63
|
+
pageProps: Record<string, any>,
|
|
67
64
|
defaultTitle?: string,
|
|
68
65
|
defaultDescription?: string,
|
|
69
66
|
): PageConfig => {
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package Versions Display
|
|
3
|
-
*
|
|
4
|
-
* Shows all @djangocfg packages versions in a popover
|
|
5
|
-
* Works in both sidebar (PrivateLayout) and footer (PublicLayout)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use client';
|
|
9
|
-
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { Info, Package } from 'lucide-react';
|
|
12
|
-
import {
|
|
13
|
-
Popover,
|
|
14
|
-
PopoverContent,
|
|
15
|
-
PopoverTrigger,
|
|
16
|
-
} from '@djangocfg/ui/components';
|
|
17
|
-
import { Button } from '@djangocfg/ui/components';
|
|
18
|
-
import { getPackageVersions } from './packageVersions.config';
|
|
19
|
-
|
|
20
|
-
export interface PackageVersionsProps {
|
|
21
|
-
/**
|
|
22
|
-
* Display variant
|
|
23
|
-
* - 'sidebar': Adapts to sidebar collapsed state (PrivateLayout)
|
|
24
|
-
* - 'footer': Simple button for footer (PublicLayout)
|
|
25
|
-
* - 'footer-minimal': Only icon, no text (for compact footer)
|
|
26
|
-
*/
|
|
27
|
-
variant?: 'sidebar' | 'footer' | 'footer-minimal';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function PackageVersions({ variant = 'footer' }: PackageVersionsProps) {
|
|
31
|
-
// Try to use sidebar state if available (only in PrivateLayout)
|
|
32
|
-
let isCollapsed = false;
|
|
33
|
-
if (variant === 'sidebar') {
|
|
34
|
-
try {
|
|
35
|
-
// Dynamic import to avoid errors in PublicLayout
|
|
36
|
-
const { useSidebar } = require('@djangocfg/ui/components');
|
|
37
|
-
const { state } = useSidebar();
|
|
38
|
-
isCollapsed = state === 'collapsed';
|
|
39
|
-
} catch (e) {
|
|
40
|
-
// Sidebar not available, use default
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const isSidebarVariant = variant === 'sidebar';
|
|
45
|
-
const isMinimalVariant = variant === 'footer-minimal';
|
|
46
|
-
const popoverAlign = isSidebarVariant ? 'start' : 'center';
|
|
47
|
-
const popoverSide = isSidebarVariant ? 'right' : 'top';
|
|
48
|
-
|
|
49
|
-
// Determine if we should show text
|
|
50
|
-
const showText = !isCollapsed && !isMinimalVariant;
|
|
51
|
-
|
|
52
|
-
// Get package versions dynamically
|
|
53
|
-
const packages = getPackageVersions();
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<Popover>
|
|
57
|
-
<PopoverTrigger asChild>
|
|
58
|
-
<Button
|
|
59
|
-
variant="ghost"
|
|
60
|
-
size={isCollapsed || isMinimalVariant ? "icon" : "sm"}
|
|
61
|
-
className={isSidebarVariant
|
|
62
|
-
? "w-full justify-start text-xs text-muted-foreground hover:text-foreground"
|
|
63
|
-
: isMinimalVariant
|
|
64
|
-
? "h-auto w-auto p-1 text-muted-foreground hover:text-primary transition-colors"
|
|
65
|
-
: "text-xs text-muted-foreground hover:text-foreground"
|
|
66
|
-
}
|
|
67
|
-
title={isMinimalVariant ? "Package Versions" : undefined}
|
|
68
|
-
>
|
|
69
|
-
<Info className={isMinimalVariant ? "h-3 w-3" : "h-3.5 w-3.5"} />
|
|
70
|
-
{showText && <span className="ml-2">Package Versions</span>}
|
|
71
|
-
</Button>
|
|
72
|
-
</PopoverTrigger>
|
|
73
|
-
<PopoverContent align={popoverAlign} side={popoverSide} className="w-80">
|
|
74
|
-
<div className="space-y-3">
|
|
75
|
-
<div className="flex items-center gap-2">
|
|
76
|
-
<Package className="h-4 w-4 text-primary" />
|
|
77
|
-
<h4 className="font-semibold text-sm">Package Versions</h4>
|
|
78
|
-
</div>
|
|
79
|
-
<div className="space-y-1.5">
|
|
80
|
-
{packages.map((pkg) => (
|
|
81
|
-
<a
|
|
82
|
-
key={pkg.name}
|
|
83
|
-
href={`https://www.npmjs.com/package/${pkg.name}`}
|
|
84
|
-
target="_blank"
|
|
85
|
-
rel="noopener noreferrer"
|
|
86
|
-
className="flex items-center justify-between py-1.5 px-2 rounded-sm hover:bg-accent/50 transition-colors cursor-pointer group"
|
|
87
|
-
>
|
|
88
|
-
<span className="text-xs font-mono text-muted-foreground group-hover:text-foreground transition-colors">
|
|
89
|
-
{pkg.name}
|
|
90
|
-
</span>
|
|
91
|
-
<span className="text-xs font-semibold bg-primary/10 text-primary px-2 py-0.5 rounded group-hover:bg-primary/20 transition-colors">
|
|
92
|
-
v{pkg.version}
|
|
93
|
-
</span>
|
|
94
|
-
</a>
|
|
95
|
-
))}
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
</PopoverContent>
|
|
99
|
-
</Popover>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package Versions Configuration
|
|
3
|
-
*
|
|
4
|
-
* NOTE: This file is auto-generated by packages/scripts/sync-package-versions.js
|
|
5
|
-
* Do not edit manually! Run 'make build' or 'pnpm sync-versions' to update.
|
|
6
|
-
*
|
|
7
|
-
* Versions are synced from actual package.json files in the monorepo.
|
|
8
|
-
* This ensures compatibility with both monorepo (dev) and npm (production).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface PackageInfo {
|
|
12
|
-
name: string;
|
|
13
|
-
version: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Package versions registry
|
|
18
|
-
* Auto-synced from package.json files
|
|
19
|
-
* Last updated: 2025-11-23T06:18:54.167Z
|
|
20
|
-
*/
|
|
21
|
-
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
|
-
{
|
|
23
|
-
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.58"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.58"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.58"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.58"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.58"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.58"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.58"
|
|
49
|
-
}
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get all package versions
|
|
54
|
-
*/
|
|
55
|
-
export function getPackageVersions(): PackageInfo[] {
|
|
56
|
-
return PACKAGE_VERSIONS;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Get single package version by name
|
|
61
|
-
*/
|
|
62
|
-
export function getPackageVersion(packageName: string): string | undefined {
|
|
63
|
-
const packages = getPackageVersions();
|
|
64
|
-
return packages.find((pkg) => pkg.name === packageName)?.version;
|
|
65
|
-
}
|