@djangocfg/layouts 1.2.58 → 1.4.0

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.0",
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.0",
67
+ "@djangocfg/og-image": "^1.4.0",
68
+ "@djangocfg/ui": "^1.4.0",
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.0",
90
90
  "@types/node": "^24.7.2",
91
91
  "@types/react": "19.2.2",
92
92
  "@types/react-dom": "19.2.1",
@@ -29,7 +29,7 @@ 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';
@@ -40,6 +40,7 @@ import type { AppLayoutConfig } from './types';
40
40
  import type { ValidationErrorConfig, CORSErrorConfig, NetworkErrorConfig } from '../../validation';
41
41
  import type { PageWithConfig } from '../../types/pageConfig';
42
42
  import { determinePageConfig } from '../../types/pageConfig';
43
+ import packageJson from '../../../package.json';
43
44
 
44
45
  // Dynamic import for AdminLayout to prevent SSR hydration issues
45
46
  const AdminLayout = dynamic(
@@ -84,11 +85,11 @@ export interface AppLayoutProps {
84
85
  */
85
86
  fontFamily?: string;
86
87
  /**
87
- * Show package versions button in sidebar footer
88
- * @default false
89
- * @example showPackageVersions={true}
88
+ * Show update notifier (checks npm for new versions)
89
+ * @default true
90
+ * @example showUpdateNotifier={false}
90
91
  */
91
- showPackageVersions?: boolean;
92
+ showUpdateNotifier?: boolean;
92
93
  /**
93
94
  * Validation error tracking configuration
94
95
  * @default { enabled: true, showToast: true, maxErrors: 50 }
@@ -276,7 +277,7 @@ function LayoutRouter({
276
277
  * </AppLayout>
277
278
  * ```
278
279
  */
279
- export function AppLayout({ children, config, component, pageProps, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validation, cors, network }: AppLayoutProps) {
280
+ export function AppLayout({ children, config, component, pageProps, disableLayout = false, forceLayout, fontFamily, showUpdateNotifier, validation, cors, network }: AppLayoutProps) {
280
281
  const router = useRouter();
281
282
 
282
283
  // Check if ErrorBoundary is enabled (default: true)
@@ -302,7 +303,7 @@ export function AppLayout({ children, config, component, pageProps, disableLayou
302
303
  };
303
304
 
304
305
  const appContent = (
305
- <AppContextProvider config={config} showPackageVersions={showPackageVersions}>
306
+ <AppContextProvider config={config} showUpdateNotifier={showUpdateNotifier}>
306
307
  {/* SEO Meta Tags */}
307
308
  <Seo
308
309
  pageConfig={finalPageConfig}
@@ -310,6 +311,9 @@ export function AppLayout({ children, config, component, pageProps, disableLayou
310
311
  siteUrl={config.app.siteUrl}
311
312
  />
312
313
 
314
+ {/* Update Notifier */}
315
+ <UpdateNotifier enabled={showUpdateNotifier} currentVersion={packageJson.version} />
316
+
313
317
  {/* Loading Progress Bar */}
314
318
  <PageProgress />
315
319
 
@@ -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>;
@@ -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}
@@ -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
- }