@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.
- package/package.json +21 -19
- package/src/configurator/private/schema.ts +12 -0
- package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
- package/src/layouts/AppLayout/AppLayout.tsx +35 -15
- package/src/layouts/AppLayout/BaseApp.tsx +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
- package/src/layouts/AuthLayout/context.tsx +35 -13
- package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
- package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/context.tsx +16 -5
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +45 -248
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +113 -430
- package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +82 -105
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +168 -0
- package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
- package/src/layouts/PrivateLayout/components/index.ts +4 -0
- package/src/layouts/PrivateLayout/context.tsx +211 -0
- package/src/layouts/PrivateLayout/density.ts +48 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +14 -0
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +110 -0
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
- package/src/layouts/PrivateLayout/index.ts +2 -2
- package/src/layouts/PrivateLayout/types.ts +193 -0
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
- package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +8 -8
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +148 -0
- package/src/layouts/ProfileLayout/README.md +118 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
- package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +35 -0
- package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
- package/src/layouts/ProfileLayout/components/index.ts +5 -2
- package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +48 -0
- package/src/layouts/ProfileLayout/index.ts +7 -3
- package/src/layouts/ProfileLayout/types.ts +47 -0
- package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
- package/src/layouts/PublicLayout/components/index.ts +4 -0
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
- package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
- package/src/layouts/_components/index.ts +2 -6
- package/src/layouts/index.ts +9 -4
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +0 -284
- package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
- /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.
|
|
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.
|
|
88
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
89
|
-
"@djangocfg/debuger": "^2.1.
|
|
90
|
-
"@djangocfg/i18n": "^2.1.
|
|
91
|
-
"@djangocfg/monitor": "^2.1.
|
|
92
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
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.
|
|
124
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
125
|
-
"@djangocfg/debuger": "^2.1.
|
|
126
|
-
"@djangocfg/i18n": "^2.1.
|
|
127
|
-
"@djangocfg/monitor": "^2.1.
|
|
128
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
129
|
-
"@djangocfg/ui-core": "^2.1.
|
|
130
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
131
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
355
|
-
const
|
|
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 {
|
|
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
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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);
|