@djangocfg/layouts 2.1.355 → 2.1.357

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.
Files changed (75) hide show
  1. package/package.json +17 -17
  2. package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
  3. package/src/layouts/AppLayout/AppLayout.tsx +35 -15
  4. package/src/layouts/AppLayout/BaseApp.tsx +2 -2
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
  6. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
  7. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
  8. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
  9. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
  10. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
  11. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
  12. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
  13. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
  14. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
  15. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
  16. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
  17. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
  18. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
  22. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
  23. package/src/layouts/AuthLayout/context.tsx +35 -13
  24. package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
  25. package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
  26. package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
  27. package/src/layouts/AuthLayout/shells/context.tsx +16 -5
  28. package/src/layouts/PrivateLayout/PrivateLayout.tsx +32 -247
  29. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +115 -426
  30. package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +40 -19
  31. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +165 -0
  32. package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
  33. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
  34. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
  35. package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
  36. package/src/layouts/PrivateLayout/components/index.ts +4 -0
  37. package/src/layouts/PrivateLayout/context.tsx +211 -0
  38. package/src/layouts/PrivateLayout/density.ts +48 -0
  39. package/src/layouts/PrivateLayout/hooks/index.ts +13 -0
  40. package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
  41. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +103 -0
  42. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
  43. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
  44. package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
  45. package/src/layouts/PrivateLayout/index.ts +2 -2
  46. package/src/layouts/PrivateLayout/types.ts +187 -0
  47. package/src/layouts/ProfileLayout/ProfileLayout.tsx +44 -183
  48. package/src/layouts/ProfileLayout/README.md +58 -0
  49. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
  50. package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
  51. package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
  52. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
  53. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +29 -0
  54. package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
  55. package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
  56. package/src/layouts/ProfileLayout/components/index.ts +4 -2
  57. package/src/layouts/ProfileLayout/context.tsx +4 -6
  58. package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
  59. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +43 -0
  60. package/src/layouts/ProfileLayout/index.ts +6 -3
  61. package/src/layouts/ProfileLayout/types.ts +37 -0
  62. package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
  63. package/src/layouts/PublicLayout/components/index.ts +4 -0
  64. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
  65. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
  66. package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
  67. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
  68. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
  69. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
  70. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
  71. package/src/layouts/_components/index.ts +2 -7
  72. package/src/layouts/index.ts +9 -4
  73. package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
  74. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
  75. /package/src/layouts/{_components → PublicLayout/components}/UserAvatar.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.355",
3
+ "version": "2.1.357",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -84,13 +84,13 @@
84
84
  "check": "tsc --noEmit"
85
85
  },
86
86
  "peerDependencies": {
87
- "@djangocfg/api": "^2.1.355",
88
- "@djangocfg/centrifugo": "^2.1.355",
89
- "@djangocfg/debuger": "^2.1.355",
90
- "@djangocfg/i18n": "^2.1.355",
91
- "@djangocfg/monitor": "^2.1.355",
92
- "@djangocfg/ui-core": "^2.1.355",
93
- "@djangocfg/ui-nextjs": "^2.1.355",
87
+ "@djangocfg/api": "^2.1.357",
88
+ "@djangocfg/centrifugo": "^2.1.357",
89
+ "@djangocfg/debuger": "^2.1.357",
90
+ "@djangocfg/i18n": "^2.1.357",
91
+ "@djangocfg/monitor": "^2.1.357",
92
+ "@djangocfg/ui-core": "^2.1.357",
93
+ "@djangocfg/ui-nextjs": "^2.1.357",
94
94
  "@hookform/resolvers": "^5.2.2",
95
95
  "consola": "^3.4.2",
96
96
  "lucide-react": "^0.545.0",
@@ -120,15 +120,15 @@
120
120
  "uuid": "^11.1.0"
121
121
  },
122
122
  "devDependencies": {
123
- "@djangocfg/api": "^2.1.355",
124
- "@djangocfg/centrifugo": "^2.1.355",
125
- "@djangocfg/debuger": "^2.1.355",
126
- "@djangocfg/i18n": "^2.1.355",
127
- "@djangocfg/monitor": "^2.1.355",
128
- "@djangocfg/typescript-config": "^2.1.355",
129
- "@djangocfg/ui-core": "^2.1.355",
130
- "@djangocfg/ui-nextjs": "^2.1.355",
131
- "@djangocfg/ui-tools": "^2.1.355",
123
+ "@djangocfg/api": "^2.1.357",
124
+ "@djangocfg/centrifugo": "^2.1.357",
125
+ "@djangocfg/debuger": "^2.1.357",
126
+ "@djangocfg/i18n": "^2.1.357",
127
+ "@djangocfg/monitor": "^2.1.357",
128
+ "@djangocfg/typescript-config": "^2.1.357",
129
+ "@djangocfg/ui-core": "^2.1.357",
130
+ "@djangocfg/ui-nextjs": "^2.1.357",
131
+ "@djangocfg/ui-tools": "^2.1.357",
132
132
  "@types/node": "^24.7.2",
133
133
  "@types/react": "^19.1.0",
134
134
  "@types/react-dom": "^19.1.0",
@@ -38,7 +38,8 @@
38
38
 
39
39
  import { ReactNode } from 'react';
40
40
 
41
- import { PrivateLayout, PrivateLayoutProps} from '../PrivateLayout/PrivateLayout';
41
+ import { PrivateLayout } from '../PrivateLayout';
42
+ import type { PrivateLayoutProps } from '../PrivateLayout';
42
43
 
43
44
  export interface AdminLayoutProps extends PrivateLayoutProps {
44
45
  children: ReactNode;
@@ -32,7 +32,7 @@
32
32
 
33
33
  'use client';
34
34
 
35
- import React, { ReactNode, useMemo } from 'react';
35
+ import React, { ReactNode, memo, useMemo } from 'react';
36
36
 
37
37
  import { ClientOnly, Suspense } from '../../components/core';
38
38
  import { usePathnameWithoutLocale } from '../../hooks';
@@ -311,7 +311,7 @@ interface AppLayoutContentProps {
311
311
  * SSR is only enabled for publicLayout.
312
312
  * Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
313
313
  */
314
- function AppLayoutContent({
314
+ function AppLayoutContentRaw({
315
315
  children,
316
316
  publicLayout,
317
317
  privateLayout,
@@ -351,8 +351,18 @@ function AppLayoutContent({
351
351
  [pathname, adminLayout, privateLayout, publicLayout]
352
352
  );
353
353
 
354
- // Render appropriate layout based on mode
355
- const renderLayout = () => {
354
+ // Prepare everything above the JSX — no inline conditionals in return().
355
+ const hasThemeOverrides = Boolean(themeOverrides && themeOverrides.length > 0);
356
+ const forcedTheme = hasThemeOverrides
357
+ ? resolveForcedTheme(pathname, themeOverrides)
358
+ : null;
359
+ const themeOverrideElement = hasThemeOverrides
360
+ ? <ThemeOverride pathname={pathname} rules={themeOverrides!} />
361
+ : null;
362
+
363
+ // Memoize layout element so it doesn't re-render on every pathname change
364
+ // that doesn't affect layout mode (e.g. /dashboard/a → /dashboard/b).
365
+ const layoutElement = useMemo(() => {
356
366
  // Skip layout for noLayoutPaths (fullscreen pages)
357
367
  if (shouldSkipLayout) {
358
368
  return children;
@@ -417,17 +427,7 @@ function AppLayoutContent({
417
427
  </publicLayout.component>
418
428
  );
419
429
  }
420
- };
421
-
422
- // Prepare everything above the JSX — no inline conditionals in return().
423
- const hasThemeOverrides = Boolean(themeOverrides && themeOverrides.length > 0);
424
- const forcedTheme = hasThemeOverrides
425
- ? resolveForcedTheme(pathname, themeOverrides)
426
- : null;
427
- const themeOverrideElement = hasThemeOverrides
428
- ? <ThemeOverride pathname={pathname} rules={themeOverrides!} />
429
- : null;
430
- const layoutElement = renderLayout();
430
+ }, [shouldSkipLayout, layoutMode, publicLayout, privateLayout, adminLayout, publicChrome, children]);
431
431
 
432
432
  // No providers here - all providers now in BaseApp
433
433
  return (
@@ -438,6 +438,26 @@ function AppLayoutContent({
438
438
  );
439
439
  }
440
440
 
441
+ /**
442
+ * Memoised layout content wrapper. Re-renders only when layout config props
443
+ * change (publicLayout, privateLayout, adminLayout, noLayoutPaths, etc.).
444
+ * The `children` prop is compared by reference — the consumer should wrap
445
+ * page content in React.memo or use stable element references to avoid
446
+ * unnecessary layout re-mounts on every parent render.
447
+ */
448
+ const AppLayoutContent = memo(AppLayoutContentRaw, (prev, next) => {
449
+ return (
450
+ prev.children === next.children &&
451
+ prev.publicLayout === next.publicLayout &&
452
+ prev.privateLayout === next.privateLayout &&
453
+ prev.adminLayout === next.adminLayout &&
454
+ prev.noLayoutPaths === next.noLayoutPaths &&
455
+ prev.authPath === next.authPath &&
456
+ prev.publicChrome === next.publicChrome &&
457
+ prev.themeOverrides === next.themeOverrides
458
+ );
459
+ });
460
+
441
461
  /**
442
462
  * AppLayout - Main Component with All Providers
443
463
  */
@@ -43,7 +43,7 @@ import { SWRConfig } from 'swr';
43
43
 
44
44
  import { MonitorProvider, FrontendMonitor } from '@djangocfg/monitor/client';
45
45
  import { errorDetailToMonitorEvent } from '../../components/errors/ErrorsTracker';
46
- import { CentrifugoAuth } from '@djangocfg/api';
46
+ import { CfgCentrifugo } from '@djangocfg/api';
47
47
  import { AuthProvider } from '@djangocfg/api/auth';
48
48
  import { CentrifugoProvider } from '@djangocfg/centrifugo';
49
49
  import { Toaster, TooltipProvider } from '@djangocfg/ui-core/components';
@@ -146,7 +146,7 @@ export function BaseApp({
146
146
  autoConnect={centrifugoEnabled && centrifugo?.autoConnect}
147
147
  url={centrifugoUrl}
148
148
  onTokenRefresh={async () => {
149
- const result = await CentrifugoAuth.cfgCentrifugoAuthTokenRetrieve({ throwOnError: true });
149
+ const result = await CfgCentrifugo.cfgCentrifugoAuthTokenRetrieve({ throwOnError: true });
150
150
  return result.data.token;
151
151
  }}
152
152
  >
@@ -10,7 +10,7 @@
10
10
 
11
11
  'use client';
12
12
 
13
- import React, { createContext, useContext } from 'react';
13
+ import React, { createContext, memo, useContext, useMemo } from 'react';
14
14
 
15
15
  import { useCfgRouter } from '@djangocfg/api/auth';
16
16
  import { useAppT } from '@djangocfg/i18n';
@@ -56,24 +56,31 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
56
56
 
57
57
  const hideHeader = Boolean(children);
58
58
 
59
+ const layoutContextValue = useMemo(() => ({ hideHeader }), [hideHeader]);
60
+
61
+ const oauthCallback = useMemo(() => {
62
+ if (!enableGithubAuth) return null;
63
+ return (
64
+ <Suspense fallback={null}>
65
+ <OAuthCallback
66
+ redirectUrl={redirectUrl}
67
+ onSuccess={onOAuthSuccess ? (user, isNewUser) => onOAuthSuccess(user, isNewUser, 'github') : undefined}
68
+ onError={onError}
69
+ />
70
+ </Suspense>
71
+ );
72
+ }, [enableGithubAuth, redirectUrl, onOAuthSuccess, onError]);
73
+
59
74
  return (
60
75
  <Suspense>
61
76
  <AuthFormProvider {...props}>
62
- <AuthLayoutContext.Provider value={{ hideHeader }}>
77
+ <AuthLayoutContext.Provider value={layoutContextValue}>
63
78
  {/* Full-screen success overlay */}
64
79
  <AuthSuccessOverlay />
65
80
 
66
81
  <AuthShell variant={variant} background={background} sidebar={sidebar} className={className}>
67
82
  {/* Handle OAuth callback when GitHub auth is enabled */}
68
- {enableGithubAuth && (
69
- <Suspense fallback={null}>
70
- <OAuthCallback
71
- redirectUrl={redirectUrl}
72
- onSuccess={onOAuthSuccess ? (user, isNewUser) => onOAuthSuccess(user, isNewUser, 'github') : undefined}
73
- onError={onError}
74
- />
75
- </Suspense>
76
- )}
83
+ {oauthCallback}
77
84
 
78
85
  <AuthHeaderSlot>{children}</AuthHeaderSlot>
79
86
  <AuthContent />
@@ -85,13 +92,13 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
85
92
  };
86
93
 
87
94
  /** Renders custom children only on the identifier step — hides them on otp / 2fa / etc. */
88
- const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
95
+ const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = memo(({ children }) => {
89
96
  const { step } = useAuthFormContext();
90
97
  if (!children || step !== 'identifier') return null;
91
98
  return <>{children}</>;
92
- };
99
+ });
93
100
 
94
- const AuthContent: React.FC = () => {
101
+ const AuthContent: React.FC = memo(() => {
95
102
  const { step, setStep } = useAuthFormContext();
96
103
 
97
104
  switch (step) {
@@ -114,9 +121,9 @@ const AuthContent: React.FC = () => {
114
121
  default:
115
122
  return <IdentifierStep />;
116
123
  }
117
- };
124
+ });
118
125
 
119
- const AuthSuccessOverlay: React.FC = () => {
126
+ const AuthSuccessOverlay: React.FC = memo(() => {
120
127
  const { step, logoUrl, redirectUrl } = useAuthFormContext();
121
128
 
122
129
  if (step !== 'success') {
@@ -124,7 +131,7 @@ const AuthSuccessOverlay: React.FC = () => {
124
131
  }
125
132
 
126
133
  return <AuthSuccess logoUrl={logoUrl} redirectUrl={redirectUrl} />;
127
- };
134
+ });
128
135
 
129
136
  // AuthSuccess component - Apple-style success screen
130
137
  interface AuthSuccessInlineProps {
@@ -133,7 +140,7 @@ interface AuthSuccessInlineProps {
133
140
  redirectDelay?: number;
134
141
  }
135
142
 
136
- const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
143
+ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
137
144
  logoUrl,
138
145
  redirectUrl,
139
146
  redirectDelay = AUTH.REDIRECT_DELAY,
@@ -196,4 +203,4 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
196
203
  </div>
197
204
  </div>
198
205
  );
199
- };
206
+ });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { AlertCircle, Loader2 } from 'lucide-react';
4
4
  import { useSearchParams } from 'next/navigation';
5
- import React, { useEffect, useState } from 'react';
5
+ import React, { memo, useEffect, useState } from 'react';
6
6
 
7
7
  import { useGithubAuth } from '@djangocfg/api/auth';
8
8
  import {
@@ -47,12 +47,16 @@ type CallbackStatus = 'processing' | 'error';
47
47
  * );
48
48
  * }
49
49
  * ```
50
+ *
51
+ * Memoised: re-renders only when `onSuccess`, `onError`, or `redirectUrl`
52
+ * change. `onSuccess` and `onError` are compared by reference — callers
53
+ * should stabilise them with useCallback.
50
54
  */
51
- export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
55
+ function OAuthCallbackRaw({
52
56
  onSuccess,
53
57
  onError,
54
58
  redirectUrl,
55
- }) => {
59
+ }: OAuthCallbackProps) {
56
60
  const searchParams = useSearchParams();
57
61
  const { setStep, setTwoFactorSessionId, setShouldPrompt2FA } = useAuthFormContext();
58
62
  const [status, setStatus] = useState<CallbackStatus | null>(null);
@@ -171,4 +175,6 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
171
175
  </Card>
172
176
  </div>
173
177
  );
174
- };
178
+ }
179
+
180
+ export const OAuthCallback = memo(OAuthCallbackRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { memo } from 'react';
4
4
 
5
5
  export interface AuthButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
6
  variant?: 'primary' | 'secondary';
@@ -9,16 +9,20 @@ export interface AuthButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
9
9
  }
10
10
 
11
11
  /**
12
- * AuthButton - Apple-style button with loading state
12
+ * AuthButton - Apple-style button with loading state.
13
+ *
14
+ * Memoised: re-renders only when variant, loading, disabled, className or
15
+ * children change. Event handlers passed via ...props are compared by
16
+ * reference — the caller should stabilise them with useCallback.
13
17
  */
14
- export const AuthButton: React.FC<AuthButtonProps> = ({
18
+ function AuthButtonRaw({
15
19
  variant = 'primary',
16
20
  loading = false,
17
21
  disabled,
18
22
  children,
19
23
  className = '',
20
24
  ...props
21
- }) => {
25
+ }: AuthButtonProps) {
22
26
  const variantClass = variant === 'primary' ? 'auth-button-primary' : 'auth-button-secondary';
23
27
  const loadingClass = loading ? 'auth-button-loading' : '';
24
28
 
@@ -32,4 +36,6 @@ export const AuthButton: React.FC<AuthButtonProps> = ({
32
36
  {children}
33
37
  </button>
34
38
  );
35
- };
39
+ }
40
+
41
+ export const AuthButton = memo(AuthButtonRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useEffect, useState } from 'react';
3
+ import React, { memo, useEffect, useState } from 'react';
4
4
 
5
5
  import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
6
6
  import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
@@ -14,19 +14,17 @@ export interface AuthContainerProps {
14
14
  }
15
15
 
16
16
  /**
17
- * AuthContainer - Apple-style minimal container with step animations
17
+ * AuthContainer - Apple-style minimal container with step animations.
18
18
  *
19
- * Features:
20
- * - Full viewport centering
21
- * - Max-width constraint (400px)
22
- * - Animate-in on step change
23
- * - No visible card/border
19
+ * Memoised: re-renders only when `step`, `className` or `children`
20
+ * reference change. Internal animation state is isolated and does
21
+ * not propagate upward.
24
22
  */
25
- export const AuthContainer: React.FC<AuthContainerProps> = ({
23
+ function AuthContainerRaw({
26
24
  children,
27
25
  step,
28
26
  className = '',
29
- }) => {
27
+ }: AuthContainerProps) {
30
28
  const [isEntering, setIsEntering] = useState(true);
31
29
  const [currentStep, setCurrentStep] = useState(step);
32
30
 
@@ -75,4 +73,6 @@ export const AuthContainer: React.FC<AuthContainerProps> = ({
75
73
  {children}
76
74
  </div>
77
75
  );
78
- };
76
+ }
77
+
78
+ export const AuthContainer = memo(AuthContainerRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useMemo } from 'react';
3
+ import React, { memo, useMemo } from 'react';
4
4
 
5
5
  import { useAppT } from '@djangocfg/i18n';
6
6
 
@@ -10,12 +10,16 @@ export interface AuthDividerProps {
10
10
  }
11
11
 
12
12
  /**
13
- * AuthDivider - Minimal "or" divider
13
+ * AuthDivider - Minimal "or" divider.
14
+ *
15
+ * Memoised: re-renders only when `text` or `className` change.
16
+ * The default translation is memoised internally so it does not
17
+ * break the comparison on every render.
14
18
  */
15
- export const AuthDivider: React.FC<AuthDividerProps> = ({
19
+ function AuthDividerRaw({
16
20
  text,
17
21
  className = '',
18
- }) => {
22
+ }: AuthDividerProps) {
19
23
  const t = useAppT();
20
24
  const defaultText = useMemo(() => t('layouts.auth.divider.or'), [t]);
21
25
  const dividerText = text ?? defaultText;
@@ -25,4 +29,6 @@ export const AuthDivider: React.FC<AuthDividerProps> = ({
25
29
  {dividerText}
26
30
  </div>
27
31
  );
28
- };
32
+ }
33
+
34
+ export const AuthDivider = memo(AuthDividerRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { memo } from 'react';
4
4
 
5
5
  export interface AuthErrorProps {
6
6
  message?: string | null;
@@ -8,12 +8,15 @@ export interface AuthErrorProps {
8
8
  }
9
9
 
10
10
  /**
11
- * AuthError - Subtle error display with shake animation
11
+ * AuthError - Subtle error display with shake animation.
12
+ *
13
+ * Memoised: re-renders only when `message` or `className` change.
14
+ * Returns null when message is empty — the null branch is stable.
12
15
  */
13
- export const AuthError: React.FC<AuthErrorProps> = ({
16
+ function AuthErrorRaw({
14
17
  message,
15
18
  className = '',
16
- }) => {
19
+ }: AuthErrorProps) {
17
20
  if (!message) {
18
21
  return null;
19
22
  }
@@ -31,4 +34,6 @@ export const AuthError: React.FC<AuthErrorProps> = ({
31
34
  <span>{message}</span>
32
35
  </div>
33
36
  );
34
- };
37
+ }
38
+
39
+ export const AuthError = memo(AuthErrorRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useMemo } from 'react';
3
+ import React, { memo, useMemo } from 'react';
4
4
 
5
5
  import { useAppT } from '@djangocfg/i18n';
6
6
 
@@ -12,14 +12,18 @@ export interface AuthFooterProps {
12
12
  }
13
13
 
14
14
  /**
15
- * AuthFooter - Minimal link row for terms, privacy, and support
15
+ * AuthFooter - Minimal link row for terms, privacy, and support.
16
+ *
17
+ * Memoised: re-renders only when URL props or className change.
18
+ * Translation labels are derived inside the component via useMemo
19
+ * so they do not break memoisation.
16
20
  */
17
- export const AuthFooter: React.FC<AuthFooterProps> = ({
21
+ function AuthFooterRaw({
18
22
  termsUrl,
19
23
  privacyUrl,
20
24
  supportUrl,
21
25
  className = '',
22
- }) => {
26
+ }: AuthFooterProps) {
23
27
  const t = useAppT();
24
28
  const labels = useMemo(() => ({
25
29
  terms: t('layouts.auth.footer.terms'),
@@ -53,4 +57,6 @@ export const AuthFooter: React.FC<AuthFooterProps> = ({
53
57
  ))}
54
58
  </div>
55
59
  );
56
- };
60
+ }
61
+
62
+ export const AuthFooter = memo(AuthFooterRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { memo } from 'react';
4
4
 
5
5
  export interface AuthHeaderProps {
6
6
  logo?: string;
@@ -11,21 +11,19 @@ export interface AuthHeaderProps {
11
11
  }
12
12
 
13
13
  /**
14
- * AuthHeader - Apple-style header with logo, title, and subtitle
14
+ * AuthHeader - Apple-style header with logo, title, and subtitle.
15
15
  *
16
- * Features:
17
- * - Optional logo with scale animation
18
- * - Large, bold title
19
- * - Muted subtitle
20
- * - Optional highlighted identifier
16
+ * Memoised: re-renders only when logo, title, subtitle, identifier or
17
+ * className change. All props are primitives (strings), so the default
18
+ * shallow comparison is sufficient.
21
19
  */
22
- export const AuthHeader: React.FC<AuthHeaderProps> = ({
20
+ function AuthHeaderRaw({
23
21
  logo,
24
22
  title,
25
23
  subtitle,
26
24
  identifier,
27
25
  className = '',
28
- }) => {
26
+ }: AuthHeaderProps) {
29
27
  return (
30
28
  <div className={`auth-header ${className}`}>
31
29
  {logo && (
@@ -51,4 +49,6 @@ export const AuthHeader: React.FC<AuthHeaderProps> = ({
51
49
  )}
52
50
  </div>
53
51
  );
54
- };
52
+ }
53
+
54
+ export const AuthHeader = memo(AuthHeaderRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { memo } from 'react';
4
4
 
5
5
  export interface AuthLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
6
  children: React.ReactNode;
@@ -8,14 +8,18 @@ export interface AuthLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElem
8
8
  }
9
9
 
10
10
  /**
11
- * AuthLink - Text link styled button/anchor
11
+ * AuthLink - Text link styled button/anchor.
12
+ *
13
+ * Memoised: re-renders only when `href`, `className`, `children` or
14
+ * event handlers change. Callers should stabilise `onClick` with
15
+ * useCallback to avoid unnecessary re-renders.
12
16
  */
13
- export const AuthLink: React.FC<AuthLinkProps> = ({
17
+ function AuthLinkRaw({
14
18
  children,
15
19
  href,
16
20
  className = '',
17
21
  ...props
18
- }) => {
22
+ }: AuthLinkProps) {
19
23
  if (href) {
20
24
  return (
21
25
  <a
@@ -38,4 +42,6 @@ export const AuthLink: React.FC<AuthLinkProps> = ({
38
42
  {children}
39
43
  </button>
40
44
  );
41
- };
45
+ }
46
+
47
+ export const AuthLink = memo(AuthLinkRaw);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { memo } from 'react';
4
4
 
5
5
  import { OTPInput } from '@djangocfg/ui-core/components';
6
6
 
@@ -17,11 +17,15 @@ export interface AuthOTPInputProps {
17
17
  }
18
18
 
19
19
  /**
20
- * AuthOTPInput - Reusable OTP input with consistent styling
20
+ * AuthOTPInput - Reusable OTP input with consistent styling.
21
21
  *
22
- * Used in: OTPStep (email, 4 digits), TwoFactorStep (TOTP, 6 digits), SetupStep (TOTP, 6 digits)
22
+ * Used in: OTPStep (email, 4 digits), TwoFactorStep (TOTP, 6 digits), SetupStep (TOTP, 6 digits).
23
+ *
24
+ * Memoised: re-renders only when value, disabled, autoFocus, size or length
25
+ * change. `onChange` and `onComplete` are compared by reference — callers
26
+ * should stabilise them with useCallback.
23
27
  */
24
- export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
28
+ function AuthOTPInputRaw({
25
29
  value,
26
30
  onChange,
27
31
  onComplete,
@@ -29,19 +33,23 @@ export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
29
33
  autoFocus = true,
30
34
  size,
31
35
  length = AUTH.TOTP_LENGTH,
32
- }) => (
33
- <div className="auth-otp-container auth-otp-wrapper">
34
- <OTPInput
35
- length={length}
36
- validationMode="numeric"
37
- pasteBehavior="clean"
38
- value={value}
39
- onChange={onChange}
40
- onComplete={onComplete}
41
- disabled={disabled}
42
- autoFocus={autoFocus}
43
- size={size}
44
- fluid
45
- />
46
- </div>
47
- );
36
+ }: AuthOTPInputProps) {
37
+ return (
38
+ <div className="auth-otp-container auth-otp-wrapper">
39
+ <OTPInput
40
+ length={length}
41
+ validationMode="numeric"
42
+ pasteBehavior="clean"
43
+ value={value}
44
+ onChange={onChange}
45
+ onComplete={onComplete}
46
+ disabled={disabled}
47
+ autoFocus={autoFocus}
48
+ size={size}
49
+ fluid
50
+ />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export const AuthOTPInput = memo(AuthOTPInputRaw);