@djangocfg/layouts 2.1.152 → 2.1.154

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": "2.1.152",
3
+ "version": "2.1.154",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,12 +74,12 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.152",
78
- "@djangocfg/centrifugo": "^2.1.152",
79
- "@djangocfg/i18n": "^2.1.152",
80
- "@djangocfg/ui-core": "^2.1.152",
81
- "@djangocfg/ui-nextjs": "^2.1.152",
82
- "@djangocfg/ui-tools": "^2.1.152",
77
+ "@djangocfg/api": "^2.1.154",
78
+ "@djangocfg/centrifugo": "^2.1.154",
79
+ "@djangocfg/i18n": "^2.1.154",
80
+ "@djangocfg/ui-core": "^2.1.154",
81
+ "@djangocfg/ui-nextjs": "^2.1.154",
82
+ "@djangocfg/ui-tools": "^2.1.154",
83
83
  "@hookform/resolvers": "^5.2.2",
84
84
  "consola": "^3.4.2",
85
85
  "lucide-react": "^0.545.0",
@@ -102,13 +102,13 @@
102
102
  "uuid": "^11.1.0"
103
103
  },
104
104
  "devDependencies": {
105
- "@djangocfg/api": "^2.1.152",
106
- "@djangocfg/i18n": "^2.1.152",
107
- "@djangocfg/centrifugo": "^2.1.152",
108
- "@djangocfg/typescript-config": "^2.1.152",
109
- "@djangocfg/ui-core": "^2.1.152",
110
- "@djangocfg/ui-nextjs": "^2.1.152",
111
- "@djangocfg/ui-tools": "^2.1.152",
105
+ "@djangocfg/api": "^2.1.154",
106
+ "@djangocfg/i18n": "^2.1.154",
107
+ "@djangocfg/centrifugo": "^2.1.154",
108
+ "@djangocfg/typescript-config": "^2.1.154",
109
+ "@djangocfg/ui-core": "^2.1.154",
110
+ "@djangocfg/ui-nextjs": "^2.1.154",
111
+ "@djangocfg/ui-tools": "^2.1.154",
112
112
  "@types/node": "^24.7.2",
113
113
  "@types/react": "^19.1.0",
114
114
  "@types/react-dom": "^19.1.0",
@@ -64,7 +64,7 @@ function buildValidationDescription(
64
64
  */
65
65
  function buildCORSDescription(
66
66
  detail: CORSErrorDetail,
67
- config: Required<CORSErrorConfig>
67
+ _config: Required<CORSErrorConfig>
68
68
  ): React.ReactNode {
69
69
  const domain = extractDomain(detail.url);
70
70
 
@@ -44,7 +44,6 @@ import {
44
44
  import type {
45
45
  ErrorDetail,
46
46
  StoredError,
47
- ErrorTrackingConfig,
48
47
  ValidationErrorConfig,
49
48
  CORSErrorConfig,
50
49
  NetworkErrorConfig,
@@ -198,7 +197,7 @@ export function ErrorTrackingProvider({
198
197
  /**
199
198
  * Clear errors by type
200
199
  */
201
- const clearErrorsByType = useCallback((type: 'validation' | 'cors' | 'network') => {
200
+ const clearErrorsByType = useCallback((type: 'validation' | 'cors' | 'network' | 'centrifugo') => {
202
201
  setErrors((prev) => prev.filter((error) => error.type !== type));
203
202
  }, []);
204
203
 
@@ -13,7 +13,8 @@ export interface ErrorContent {
13
13
  description: string;
14
14
  }
15
15
 
16
- type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ type TranslationFn = (key: any, params?: Record<string, string | number>) => string;
17
18
 
18
19
  /**
19
20
  * Get standardized error content based on status code
@@ -36,6 +36,7 @@ import React, { ReactNode, useMemo } from 'react';
36
36
 
37
37
  import { ClientOnly, Suspense } from '../../components/core';
38
38
  import { usePathnameWithoutLocale } from '../../hooks';
39
+ import { matchesPath } from '../../utils/pathMatcher';
39
40
  import { BaseApp } from './BaseApp';
40
41
 
41
42
  import type {
@@ -52,20 +53,6 @@ import type { AuthConfig } from '@djangocfg/api/auth';
52
53
 
53
54
  export type LayoutMode = 'public' | 'private' | 'admin';
54
55
 
55
- /**
56
- * Check if pathname matches enabledPath
57
- */
58
- function matchesPath(pathname: string, enabledPath?: string | string[]): boolean {
59
- if (!enabledPath) return false;
60
-
61
- if (typeof enabledPath === 'string') {
62
- return pathname === enabledPath || pathname.startsWith(enabledPath + '/');
63
- }
64
-
65
- // Array of paths
66
- return enabledPath.some(path => pathname === path || pathname.startsWith(path + '/'));
67
- }
68
-
69
56
  /**
70
57
  * Determine layout mode from pathname and enabledPath props
71
58
  */
@@ -66,7 +66,7 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
66
66
 
67
67
  const {
68
68
  handleGithubCallback,
69
- isLoading,
69
+ isLoading: _isLoading,
70
70
  error: githubError,
71
71
  } = useGithubAuth({
72
72
  onSuccess: (user, isNewUser) => {
@@ -10,7 +10,7 @@ import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
10
10
 
11
11
  interface SetupCompleteProps {
12
12
  backupCodes: string[];
13
- backupCodesWarning?: string;
13
+ backupCodesWarning?: string | null;
14
14
  onDone: () => void;
15
15
  }
16
16
 
@@ -26,7 +26,7 @@ interface SetupQRCodeProps {
26
26
  provisioningUri: string;
27
27
  secret: string;
28
28
  isLoading: boolean;
29
- error?: string;
29
+ error?: string | null;
30
30
  onConfirm: (code: string) => Promise<unknown>;
31
31
  onSkip?: () => void;
32
32
  }
@@ -26,7 +26,7 @@ interface PrivateHeaderProps {
26
26
  }
27
27
 
28
28
  export function PrivateHeader({ header, i18n }: PrivateHeaderProps) {
29
- const { user, logout } = useAuth();
29
+ const { user } = useAuth();
30
30
 
31
31
  return (
32
32
  <header
@@ -18,7 +18,7 @@ import { cn } from '@djangocfg/ui-core/lib';
18
18
 
19
19
  import { LucideIcon } from '../../../components';
20
20
 
21
- import type { SidebarItem, SidebarConfig, SidebarGroupConfig } from '../PrivateLayout';
21
+ import type { SidebarItem, SidebarConfig } from '../PrivateLayout';
22
22
 
23
23
  interface PrivateSidebarProps {
24
24
  sidebar: SidebarConfig;
@@ -45,7 +45,8 @@ interface ProfileLayoutProps {
45
45
  enableDeleteAccount?: boolean;
46
46
  }
47
47
 
48
- type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
48
+ // Reserved for future use
49
+ // type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
49
50
 
50
51
  // ─────────────────────────────────────────────────────────────────────────────
51
52
  // Editable Field Component
@@ -201,7 +202,7 @@ const ProfileContent = ({
201
202
  enable2FA = false,
202
203
  enableDeleteAccount = true,
203
204
  }: ProfileLayoutProps) => {
204
- const { user, isLoading, logout, uploadAvatar, updateProfile } = useAuth();
205
+ const { user, isLoading, uploadAvatar, updateProfile } = useAuth();
205
206
  const [isUploading, setIsUploading] = useState(false);
206
207
  const t = useTypedT<I18nTranslations>();
207
208
 
@@ -13,7 +13,7 @@ import React, { useMemo } from 'react';
13
13
  import { useAuth } from '@djangocfg/api/auth';
14
14
  import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
15
15
  import {
16
- Button, Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle
16
+ Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle
17
17
  } from '@djangocfg/ui-core/components';
18
18
  import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
19
19
 
@@ -38,7 +38,7 @@ export function PublicMobileDrawer({
38
38
  navigation,
39
39
  userMenu,
40
40
  }: PublicMobileDrawerProps) {
41
- const { isAuthenticated } = useAuth();
41
+ const { isAuthenticated: _isAuthenticated } = useAuth();
42
42
  const t = useTypedT<I18nTranslations>();
43
43
 
44
44
  const labels = useMemo(() => ({
@@ -13,6 +13,7 @@ import React, { useMemo } from 'react';
13
13
  import { useAuth } from '@djangocfg/api/auth';
14
14
  import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
15
15
  import { Button } from '@djangocfg/ui-core/components';
16
+ // useIsMobile is used for conditional rendering
16
17
  import { useIsMobile } from '@djangocfg/ui-core/hooks';
17
18
  import { cn } from '@djangocfg/ui-core/lib';
18
19
  import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
@@ -41,7 +42,7 @@ export function PublicNavigation({
41
42
  onMobileMenuClick,
42
43
  i18n,
43
44
  }: PublicNavigationProps) {
44
- const { isAuthenticated } = useAuth();
45
+ const { isAuthenticated: _isAuthenticated } = useAuth();
45
46
  const isMobile = useIsMobile();
46
47
  const t = useTypedT<I18nTranslations>();
47
48
 
@@ -19,7 +19,7 @@ const isProduction = process.env.NODE_ENV === 'production';
19
19
 
20
20
  // Tracking state
21
21
  let isInitialized = false;
22
- let currentTrackingId: string | undefined;
22
+ let _currentTrackingId: string | undefined;
23
23
 
24
24
  /**
25
25
  * Analytics utility object for standalone usage (outside React components)
@@ -40,7 +40,7 @@ export const Analytics = {
40
40
  if (!isProduction || !trackingId || isInitialized) return;
41
41
  ReactGA.initialize(trackingId);
42
42
  isInitialized = true;
43
- currentTrackingId = trackingId;
43
+ _currentTrackingId = trackingId;
44
44
  },
45
45
 
46
46
  /**
@@ -33,7 +33,7 @@ function getBrowserCategory(browser: {
33
33
  return 'unknown';
34
34
  }
35
35
 
36
- type TranslationFn = (key: string) => string;
36
+ type TranslationFn = (key: any) => string;
37
37
 
38
38
  function getBrowserSteps(category: BrowserCategory, t: TranslationFn): InstallStep[] {
39
39
  switch (category) {
@@ -4,4 +4,5 @@
4
4
 
5
5
  export * from './config';
6
6
  export * from './logger';
7
+ export * from './pathMatcher';
7
8
 
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Path Matcher Utility
3
+ *
4
+ * Functions for matching URL paths against patterns with glob support.
5
+ */
6
+
7
+ /**
8
+ * Match pathname against glob pattern
9
+ *
10
+ * @param pathname - The URL pathname to match (e.g., '/projects/123/edit')
11
+ * @param pattern - The pattern to match against (e.g., '/projects/*/edit')
12
+ * @returns true if pathname matches the pattern
13
+ *
14
+ * Glob patterns:
15
+ * - '*' matches any single path segment
16
+ * - '**' matches any number of path segments (zero or more)
17
+ *
18
+ * @example
19
+ * matchGlobPattern('/projects/123/edit', '/projects/*/edit') // true
20
+ * matchGlobPattern('/projects/abc/edit', '/projects/*/edit') // true
21
+ * matchGlobPattern('/admin/users/1/settings', '/admin/**') // true
22
+ * matchGlobPattern('/projects/edit', '/projects/*/edit') // false
23
+ */
24
+ export function matchGlobPattern(pathname: string, pattern: string): boolean {
25
+ // Normalize paths (remove trailing slashes)
26
+ const normalizedPath = pathname.replace(/\/+$/, '');
27
+ const normalizedPattern = pattern.replace(/\/+$/, '');
28
+
29
+ // Split into segments
30
+ const pathParts = normalizedPath.split('/').filter(Boolean);
31
+ const patternParts = normalizedPattern.split('/').filter(Boolean);
32
+
33
+ let pathIdx = 0;
34
+ let patternIdx = 0;
35
+
36
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
37
+ const patternPart = patternParts[patternIdx];
38
+
39
+ if (patternPart === '**') {
40
+ // '**' matches zero or more segments
41
+ // If it's the last pattern part, it matches everything remaining
42
+ if (patternIdx === patternParts.length - 1) {
43
+ return true;
44
+ }
45
+ // Otherwise, try to match the next pattern part against remaining path parts
46
+ const nextPatternPart = patternParts[patternIdx + 1];
47
+ while (pathIdx < pathParts.length) {
48
+ if (pathParts[pathIdx] === nextPatternPart || nextPatternPart === '*') {
49
+ break;
50
+ }
51
+ pathIdx++;
52
+ }
53
+ patternIdx++;
54
+ } else if (patternPart === '*') {
55
+ // '*' matches exactly one segment
56
+ pathIdx++;
57
+ patternIdx++;
58
+ } else {
59
+ // Literal match
60
+ if (pathParts[pathIdx] !== patternPart) {
61
+ return false;
62
+ }
63
+ pathIdx++;
64
+ patternIdx++;
65
+ }
66
+ }
67
+
68
+ // Check if we've consumed both arrays
69
+ // Pattern can have trailing '**' which matches empty
70
+ if (patternIdx < patternParts.length) {
71
+ // Remaining pattern parts must all be '**'
72
+ for (let i = patternIdx; i < patternParts.length; i++) {
73
+ if (patternParts[i] !== '**') {
74
+ return false;
75
+ }
76
+ }
77
+ }
78
+
79
+ return pathIdx === pathParts.length;
80
+ }
81
+
82
+ /**
83
+ * Check if pathname matches any of the enabled paths
84
+ *
85
+ * @param pathname - The URL pathname to check
86
+ * @param enabledPath - Single path, array of paths, or undefined
87
+ * @returns true if pathname matches any enabled path
88
+ *
89
+ * Supports:
90
+ * - Exact match: '/dashboard' matches '/dashboard'
91
+ * - Prefix match: '/dashboard' matches '/dashboard/settings'
92
+ * - Glob patterns: '/projects/*\/edit' matches '/projects/123/edit'
93
+ *
94
+ * @example
95
+ * matchesPath('/dashboard', '/dashboard') // true
96
+ * matchesPath('/dashboard/settings', '/dashboard') // true
97
+ * matchesPath('/projects/123/edit', '/projects/*\/edit') // true
98
+ * matchesPath('/home', ['/dashboard', '/admin']) // false
99
+ */
100
+ export function matchesPath(pathname: string, enabledPath?: string | string[]): boolean {
101
+ if (!enabledPath) return false;
102
+
103
+ const matchSinglePath = (path: string): boolean => {
104
+ // If pattern contains glob characters, use pattern matching
105
+ if (path.includes('*')) {
106
+ return matchGlobPattern(pathname, path);
107
+ }
108
+ // Otherwise, exact or prefix match
109
+ return pathname === path || pathname.startsWith(path + '/');
110
+ };
111
+
112
+ if (typeof enabledPath === 'string') {
113
+ return matchSinglePath(enabledPath);
114
+ }
115
+
116
+ // Array of paths
117
+ return enabledPath.some(matchSinglePath);
118
+ }