@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.58",
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.2.58",
67
- "@djangocfg/og-image": "^1.2.58",
68
- "@djangocfg/ui": "^1.2.58",
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.2.58",
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
@@ -10,6 +10,9 @@ export * from './auth';
10
10
  // Layout components
11
11
  export * from './layouts';
12
12
 
13
+ // Types
14
+ export * from './types';
15
+
13
16
  // Snippets - Reusable UI components
14
17
  export * from './snippets';
15
18
 
@@ -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?: PageWithConfig | any;
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 package versions button in sidebar footer
88
- * @default false
89
- * @example showPackageVersions={true}
72
+ * Show update notifier (checks npm for new versions)
73
+ * @default true
74
+ * @example showUpdateNotifier={false}
90
75
  */
91
- showPackageVersions?: boolean;
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
- * Determines which layout to use based on route
113
- * Uses AppContext - no props passed down!
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
- disableLayout,
118
- forceLayout,
104
+ component,
119
105
  config
120
106
  }: {
121
107
  children: ReactNode;
122
- disableLayout?: boolean;
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
- // If layout is disabled, render children directly (providers still active!)
136
- if (disableLayout) {
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 && !forceLayout) {
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 && !forceLayout) {
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
- if (forceLayout === 'auth') return 'auth';
212
- if (forceLayout === 'public') return 'public';
213
- if (forceLayout === 'admin') return 'admin';
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
- * // With layout (default - auto-detect)
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={manrope.style.fontFamily}>
260
+ * <AppLayout config={appLayoutConfig} component={Component} fontFamily={inter.style.fontFamily}>
265
261
  * <Component {...pageProps} />
266
262
  * </AppLayout>
267
263
  *
268
- * // Without layout (providers still active)
269
- * <AppLayout config={appLayoutConfig} disableLayout>
270
- * <CustomLandingPage />
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
- * // Force public layout for all pages
274
- * <AppLayout config={appLayoutConfig} forceLayout="public">
275
- * <Component {...pageProps} />
276
- * </AppLayout>
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, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validation, cors, network }: AppLayoutProps) {
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} showPackageVersions={showPackageVersions}>
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 disableLayout={disableLayout} forceLayout={forceLayout} config={config}>
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
+ }
@@ -0,0 +1,2 @@
1
+ export { UpdateNotifier } from './UpdateNotifier';
2
+ export type { UpdateNotifierProps } from './UpdateNotifier';
@@ -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 { PackageVersions, getPackageVersions, getPackageVersion } from './PackageVersions';
8
+ export { UpdateNotifier } from './UpdateNotifier';
9
9
  export { UserMenu } from './UserMenu';
10
- export type { PackageInfo } from './PackageVersions';
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
- showPackageVersions?: boolean;
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
- showPackageVersions?: boolean;
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, showPackageVersions }: AppContextProviderProps) {
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
- showPackageVersions,
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
- * Single self-sufficient component for all layout needs
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, showPackageVersions } = useAppContext();
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, showPackageVersions } = useAppContext();
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}
@@ -8,3 +8,4 @@ export * from './config';
8
8
  export * from './layout';
9
9
  export * from './navigation';
10
10
  export * from './routes';
11
+ export * from './page';
@@ -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
+ };
@@ -41,8 +41,3 @@ export interface RouteDetectors {
41
41
  /** Get page title for route */
42
42
  getPageTitle: (path: string) => string;
43
43
  }
44
-
45
- /**
46
- * Layout mode based on route
47
- */
48
- export type LayoutMode = 'public' | 'private' | 'admin' | 'auth';
@@ -1 +1,2 @@
1
- export type { PageConfig, PageWithConfig } from "./pageConfig";
1
+ export type { PageConfig } from "./pageConfig";
2
+ export { determinePageConfig } from "./pageConfig";
@@ -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: PageWithConfig,
66
- pageProps: Record<string, any>, // Use a general type for pageProps
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,7 +0,0 @@
1
- /**
2
- * Package Versions Module
3
- */
4
-
5
- export { PackageVersions } from './PackageVersions';
6
- export { getPackageVersions, getPackageVersion } from './packageVersions.config';
7
- export type { PackageInfo } from './packageVersions.config';
@@ -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
- }