@djangocfg/layouts 2.1.356 → 2.1.358

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 (83) hide show
  1. package/package.json +21 -19
  2. package/src/configurator/private/schema.ts +12 -0
  3. package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +35 -15
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -2
  6. package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
  7. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
  8. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
  9. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
  10. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
  11. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
  12. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
  13. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
  14. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
  16. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
  17. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
  18. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
  24. package/src/layouts/AuthLayout/context.tsx +35 -13
  25. package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
  26. package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
  27. package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
  28. package/src/layouts/AuthLayout/shells/context.tsx +16 -5
  29. package/src/layouts/PrivateLayout/PrivateLayout.tsx +45 -248
  30. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +113 -430
  31. package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +82 -105
  32. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +168 -0
  33. package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
  34. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
  35. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
  36. package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
  37. package/src/layouts/PrivateLayout/components/index.ts +4 -0
  38. package/src/layouts/PrivateLayout/context.tsx +211 -0
  39. package/src/layouts/PrivateLayout/density.ts +48 -0
  40. package/src/layouts/PrivateLayout/hooks/index.ts +14 -0
  41. package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
  42. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +110 -0
  43. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
  44. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
  45. package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
  46. package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
  47. package/src/layouts/PrivateLayout/index.ts +2 -2
  48. package/src/layouts/PrivateLayout/types.ts +193 -0
  49. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
  50. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  51. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
  52. package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +8 -8
  53. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +148 -0
  54. package/src/layouts/ProfileLayout/README.md +118 -0
  55. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
  56. package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
  57. package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
  58. package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
  59. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
  60. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
  61. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +35 -0
  62. package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
  63. package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
  64. package/src/layouts/ProfileLayout/components/index.ts +5 -2
  65. package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
  66. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +48 -0
  67. package/src/layouts/ProfileLayout/index.ts +7 -3
  68. package/src/layouts/ProfileLayout/types.ts +47 -0
  69. package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
  70. package/src/layouts/PublicLayout/components/index.ts +4 -0
  71. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
  72. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
  73. package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
  74. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
  75. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
  76. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
  77. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
  78. package/src/layouts/_components/index.ts +2 -6
  79. package/src/layouts/index.ts +9 -4
  80. package/src/layouts/ProfileLayout/ProfileLayout.tsx +0 -284
  81. package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
  82. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
  83. /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.356",
3
+ "version": "2.1.358",
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.356",
88
- "@djangocfg/centrifugo": "^2.1.356",
89
- "@djangocfg/debuger": "^2.1.356",
90
- "@djangocfg/i18n": "^2.1.356",
91
- "@djangocfg/monitor": "^2.1.356",
92
- "@djangocfg/ui-core": "^2.1.356",
93
- "@djangocfg/ui-nextjs": "^2.1.356",
87
+ "@djangocfg/api": "^2.1.358",
88
+ "@djangocfg/centrifugo": "^2.1.358",
89
+ "@djangocfg/debuger": "^2.1.358",
90
+ "@djangocfg/i18n": "^2.1.358",
91
+ "@djangocfg/monitor": "^2.1.358",
92
+ "@djangocfg/ui-core": "^2.1.358",
93
+ "@djangocfg/ui-nextjs": "^2.1.358",
94
94
  "@hookform/resolvers": "^5.2.2",
95
95
  "consola": "^3.4.2",
96
96
  "lucide-react": "^0.545.0",
@@ -105,7 +105,8 @@
105
105
  "swr": "^2.3.7",
106
106
  "tailwindcss": "^4.1.18",
107
107
  "tailwindcss-animate": "^1.0.7",
108
- "zod": "^4.3.6"
108
+ "zod": "^4.3.6",
109
+ "zustand": "^5.0.0"
109
110
  },
110
111
  "peerDependenciesMeta": {
111
112
  "@djangocfg/monitor": {
@@ -120,21 +121,22 @@
120
121
  "uuid": "^11.1.0"
121
122
  },
122
123
  "devDependencies": {
123
- "@djangocfg/api": "^2.1.356",
124
- "@djangocfg/centrifugo": "^2.1.356",
125
- "@djangocfg/debuger": "^2.1.356",
126
- "@djangocfg/i18n": "^2.1.356",
127
- "@djangocfg/monitor": "^2.1.356",
128
- "@djangocfg/typescript-config": "^2.1.356",
129
- "@djangocfg/ui-core": "^2.1.356",
130
- "@djangocfg/ui-nextjs": "^2.1.356",
131
- "@djangocfg/ui-tools": "^2.1.356",
124
+ "@djangocfg/api": "^2.1.358",
125
+ "@djangocfg/centrifugo": "^2.1.358",
126
+ "@djangocfg/debuger": "^2.1.358",
127
+ "@djangocfg/i18n": "^2.1.358",
128
+ "@djangocfg/monitor": "^2.1.358",
129
+ "@djangocfg/typescript-config": "^2.1.358",
130
+ "@djangocfg/ui-core": "^2.1.358",
131
+ "@djangocfg/ui-nextjs": "^2.1.358",
132
+ "@djangocfg/ui-tools": "^2.1.358",
132
133
  "@types/node": "^24.7.2",
133
134
  "@types/react": "^19.1.0",
134
135
  "@types/react-dom": "^19.1.0",
135
136
  "eslint": "^9.37.0",
136
137
  "next-intl": "^4.9.1",
137
- "typescript": "^5.9.3"
138
+ "typescript": "^5.9.3",
139
+ "zustand": "^5.0.4"
138
140
  },
139
141
  "publishConfig": {
140
142
  "access": "public"
@@ -37,6 +37,7 @@ export interface PrivateLayoutConfiguratorData {
37
37
  header: {
38
38
  userPlan: string;
39
39
  showSecondaryAction: boolean;
40
+ accountAction: 'menu' | 'dialog';
40
41
  };
41
42
  }
42
43
 
@@ -126,6 +127,12 @@ export const privateLayoutConfiguratorSchema: CustomJsonSchema7 = {
126
127
  title: 'Footer secondary action',
127
128
  description: 'Adds a download-style icon button inside the footer trigger with a pulsing accent dot.',
128
129
  },
130
+ accountAction: {
131
+ type: 'string',
132
+ title: 'Account action',
133
+ enum: ['menu', 'dialog'],
134
+ description: "'menu' opens a dropdown; 'dialog' opens the global ProfileDialog.",
135
+ },
129
136
  },
130
137
  },
131
138
  },
@@ -165,6 +172,10 @@ export const privateLayoutConfiguratorUiSchema: CustomJsonUiSchema7 = {
165
172
  header: {
166
173
  'ui:collapsible': true,
167
174
  showSecondaryAction: { 'ui:widget': 'switch' },
175
+ accountAction: {
176
+ 'ui:widget': 'radio',
177
+ 'ui:options': { inline: true },
178
+ },
168
179
  },
169
180
  };
170
181
 
@@ -186,5 +197,6 @@ export const defaultPrivateLayoutConfiguratorData: PrivateLayoutConfiguratorData
186
197
  header: {
187
198
  userPlan: 'Pro plan',
188
199
  showSecondaryAction: false,
200
+ accountAction: 'menu',
189
201
  },
190
202
  };
@@ -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);