@djangocfg/layouts 2.1.579 → 2.1.581
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 +12 -12
- package/src/layouts/AuthLayout/AuthLayout.tsx +25 -7
- package/src/layouts/AuthLayout/README.md +16 -12
- package/src/layouts/AuthLayout/components/index.ts +0 -2
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +8 -9
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +1 -1
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +10 -2
- package/src/layouts/AuthLayout/components/steps/index.ts +0 -3
- package/src/layouts/AuthLayout/constants.ts +0 -4
- package/src/layouts/AuthLayout/index.ts +0 -3
- package/src/layouts/AuthLayout/styles/auth.css +0 -110
- package/src/layouts/AuthLayout/types.ts +5 -2
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +2 -2
- package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +2 -1
- package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +4 -3
- package/src/layouts/_components/locale-switcher/iconFlag.ts +5 -0
- package/src/snippets/TwoFactor/TwoFactorChallengeDialog.tsx +188 -0
- package/src/snippets/TwoFactor/TwoFactorSetup.tsx +209 -0
- package/src/snippets/TwoFactor/index.ts +2 -0
- package/src/snippets/index.ts +1 -0
- package/src/testing/MockAuthFormProvider.tsx +7 -41
- package/src/testing/index.ts +4 -2
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +0 -86
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +0 -36
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +0 -141
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +0 -145
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +0 -133
- package/src/layouts/AuthLayout/hooks/index.ts +0 -1
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +0 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.581",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"check": "tsc --noEmit"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@djangocfg/analytics": "^2.1.
|
|
94
|
-
"@djangocfg/api": "^2.1.
|
|
95
|
-
"@djangocfg/devtools": "^2.1.
|
|
96
|
-
"@djangocfg/i18n": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
+
"@djangocfg/analytics": "^2.1.581",
|
|
94
|
+
"@djangocfg/api": "^2.1.581",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.581",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.581",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.581",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -124,12 +124,12 @@
|
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/analytics": "^2.1.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/devtools": "^2.1.
|
|
130
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
132
|
-
"@djangocfg/ui-core": "^2.1.
|
|
127
|
+
"@djangocfg/analytics": "^2.1.581",
|
|
128
|
+
"@djangocfg/api": "^2.1.581",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.581",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.581",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.581",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.581",
|
|
133
133
|
"@storybook/react-vite": "^10.5.0",
|
|
134
134
|
"@types/node": "^25.9.5",
|
|
135
135
|
"@types/react": "19.2.15",
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - `centered` — Apple-style frameless, centered, glow background
|
|
8
8
|
* - `split` — Two-column frosted card on desktop, single-column on mobile
|
|
9
9
|
*
|
|
10
|
-
* Supports: email/phone OTP, OAuth (GitHub)
|
|
10
|
+
* Supports: email/phone OTP, OAuth (GitHub). A 2FA challenge opens as a dialog
|
|
11
|
+
* over the page (`TwoFactorChallengeDialog`), not as a step of this layout.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
'use client';
|
|
@@ -19,9 +20,10 @@ import { useAnalytics } from '@djangocfg/analytics';
|
|
|
19
20
|
import { useAppT } from '@djangocfg/i18n';
|
|
20
21
|
|
|
21
22
|
import { Suspense } from '../../components';
|
|
23
|
+
import { TwoFactorChallengeDialog } from '../../snippets/TwoFactor';
|
|
22
24
|
import { AuthBrandPanel } from './components/shared';
|
|
23
25
|
import { OAuthCallback } from './components/oauth';
|
|
24
|
-
import { IdentifierStep, OTPStep
|
|
26
|
+
import { IdentifierStep, OTPStep } from './components/steps';
|
|
25
27
|
import { AUTH } from './constants';
|
|
26
28
|
import { AuthFormProvider, useAuthFormContext } from './context';
|
|
27
29
|
import { AuthShell } from './shells';
|
|
@@ -80,6 +82,7 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
80
82
|
// resolved routes.defaultCallback chain at navigation time (AuthSuccess).
|
|
81
83
|
redirectUrl,
|
|
82
84
|
onOAuthSuccess,
|
|
85
|
+
onOTPSuccess,
|
|
83
86
|
onError,
|
|
84
87
|
className,
|
|
85
88
|
children,
|
|
@@ -130,6 +133,7 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
130
133
|
{/* Full-screen success overlay */}
|
|
131
134
|
<AuthSuccessOverlay />
|
|
132
135
|
<AuthFunnelTracker />
|
|
136
|
+
<AuthTwoFactorChallenge onVerified={onOTPSuccess} />
|
|
133
137
|
|
|
134
138
|
<AuthShell variant={variant} mediaSide={mediaSide} background={background} sidebar={resolvedSidebar} className={className}>
|
|
135
139
|
{/* Handle OAuth callback when GitHub auth is enabled */}
|
|
@@ -165,13 +169,31 @@ const AuthFunnelTracker: React.FC = memo(function AuthFunnelTracker() {
|
|
|
165
169
|
return null;
|
|
166
170
|
});
|
|
167
171
|
|
|
168
|
-
/** Renders custom children only on the identifier step
|
|
172
|
+
/** Renders custom children only on the identifier step. */
|
|
169
173
|
const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = memo(function AuthHeaderSlot({ children }) {
|
|
170
174
|
const { step } = useAuthFormContext();
|
|
171
175
|
if (!children || step !== 'identifier') return null;
|
|
172
176
|
return <>{children}</>;
|
|
173
177
|
});
|
|
174
178
|
|
|
179
|
+
/** Wires the challenge dialog to the form: success shows the success screen, cancel restarts sign-in. */
|
|
180
|
+
const AuthTwoFactorChallenge: React.FC<{ onVerified?: () => void }> = memo(function AuthTwoFactorChallenge({ onVerified }) {
|
|
181
|
+
const { setStep, setOtp, handleBackToIdentifier } = useAuthFormContext();
|
|
182
|
+
|
|
183
|
+
const handleVerified = React.useCallback(() => {
|
|
184
|
+
setStep('success');
|
|
185
|
+
onVerified?.();
|
|
186
|
+
}, [setStep, onVerified]);
|
|
187
|
+
|
|
188
|
+
// The email code is spent by now, so going back to it would only fail.
|
|
189
|
+
const handleCancel = React.useCallback(() => {
|
|
190
|
+
setOtp('');
|
|
191
|
+
handleBackToIdentifier();
|
|
192
|
+
}, [setOtp, handleBackToIdentifier]);
|
|
193
|
+
|
|
194
|
+
return <TwoFactorChallengeDialog onVerified={handleVerified} onCancel={handleCancel} />;
|
|
195
|
+
});
|
|
196
|
+
|
|
175
197
|
const AuthContent: React.FC = memo(function AuthContent() {
|
|
176
198
|
const { step } = useAuthFormContext();
|
|
177
199
|
|
|
@@ -180,10 +202,6 @@ const AuthContent: React.FC = memo(function AuthContent() {
|
|
|
180
202
|
return <IdentifierStep />;
|
|
181
203
|
case 'otp':
|
|
182
204
|
return <OTPStep />;
|
|
183
|
-
case '2fa':
|
|
184
|
-
// Sign-in verifies an *existing* 2FA challenge only. Setting up 2FA is
|
|
185
|
-
// owned by ProfileLayout, so the flow never renders the setup step.
|
|
186
|
-
return <TwoFactorStep />;
|
|
187
205
|
case 'success':
|
|
188
206
|
// Success is rendered as full-screen overlay, return null here
|
|
189
207
|
return null;
|
|
@@ -68,8 +68,8 @@ asset swap.
|
|
|
68
68
|
<AuthLayout logo={<BrandMark className="text-foreground" />} … />
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Supports: email OTP, phone OTP
|
|
72
|
-
|
|
71
|
+
Supports: email OTP, phone OTP and GitHub OAuth. A 2FA challenge is not part of
|
|
72
|
+
this layout — see below.
|
|
73
73
|
|
|
74
74
|
**Consent, not a checkbox.** There is no opt-in terms checkbox — continuing *is*
|
|
75
75
|
the consent. When `termsUrl`/`privacyUrl` are set, a passive line ("By continuing
|
|
@@ -77,11 +77,20 @@ you agree to the Terms and Privacy Policy") renders under the submit button with
|
|
|
77
77
|
links that open in a new tab. The copy is intentionally hardcoded English (legal
|
|
78
78
|
document names stay in English across locales) — see `shared/AuthConsent`.
|
|
79
79
|
|
|
80
|
-
> **2FA
|
|
81
|
-
>
|
|
82
|
-
>
|
|
83
|
-
>
|
|
84
|
-
>
|
|
80
|
+
> **2FA is not a step of the sign-in flow.** When a sign-in step (OTP, password,
|
|
81
|
+
> OAuth callback) receives `requires_2fa`, it calls `beginTwoFactorChallenge` from
|
|
82
|
+
> `@djangocfg/api/auth` and nothing more. `TwoFactorChallengeDialog`
|
|
83
|
+
> (`snippets/TwoFactorDialog`, mounted by `AuthLayout`) opens over the page and
|
|
84
|
+
> owns code entry, the backup-code fallback and lockout hints. The form holds no
|
|
85
|
+
> 2FA state. *Configuring* 2FA (QR + backup codes) is `TwoFactorSetup`, used by
|
|
86
|
+
> **SettingsLayout** (`TwoFactorSection`); both live in `snippets/TwoFactor`.
|
|
87
|
+
|
|
88
|
+
**Fast code screen.** Submitting the email opens the code screen immediately and
|
|
89
|
+
rolls back to the identifier step if the request fails. The resend link counts
|
|
90
|
+
down from the server's `resend_in` (30s fallback).
|
|
91
|
+
|
|
92
|
+
**Remembered by default.** Sign-ins last 30 days; `rememberMe={{ enabled: true }}`
|
|
93
|
+
adds a checkbox for apps that want the choice.
|
|
85
94
|
|
|
86
95
|
## Usage
|
|
87
96
|
|
|
@@ -180,15 +189,10 @@ The sign-in flow walks these steps:
|
|
|
180
189
|
|---|---|
|
|
181
190
|
| `identifier` | Email or phone input |
|
|
182
191
|
| `otp` | 4-digit email OTP code entry |
|
|
183
|
-
| `2fa` | TOTP or backup code verification (only if the account already has 2FA) |
|
|
184
192
|
| `success` | Full-screen confirmation → redirect (see below) |
|
|
185
193
|
|
|
186
194
|
`children` (custom header) are only rendered on the `identifier` step — hidden on all others.
|
|
187
195
|
|
|
188
|
-
> The `AuthStep` union also includes `'2fa-setup'`, but the sign-in flow never
|
|
189
|
-
> transitions to it. That value backs the standalone setup UI (`SetupStep` /
|
|
190
|
-
> `SetupStepStandalone`) which **ProfileLayout** reuses for configuring 2FA.
|
|
191
|
-
|
|
192
196
|
## The success screen
|
|
193
197
|
|
|
194
198
|
A checkmark, the outcome, and what the wait is for:
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Apple-style step components
|
|
2
2
|
export { IdentifierStep } from './steps/IdentifierStep';
|
|
3
3
|
export { OTPStep } from './steps/OTPStep';
|
|
4
|
-
export { TwoFactorStep } from './steps/TwoFactorStep';
|
|
5
|
-
export { SetupStep, type SetupStepProps } from './steps/SetupStep';
|
|
6
4
|
|
|
7
5
|
// Shared UI components
|
|
8
6
|
export * from './shared';
|
|
@@ -4,7 +4,7 @@ import { AlertCircle, Loader2 } from 'lucide-react';
|
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import React, { memo, useEffect, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { useAuth, useGithubAuth } from '@djangocfg/api/auth';
|
|
7
|
+
import { beginTwoFactorChallenge, useAuth, useGithubAuth } from '@djangocfg/api/auth';
|
|
8
8
|
import {
|
|
9
9
|
Card, CardContent, CardDescription, CardHeader, CardTitle
|
|
10
10
|
} from '@djangocfg/ui-core/components';
|
|
@@ -18,7 +18,7 @@ export interface OAuthCallbackProps {
|
|
|
18
18
|
redirectUrl?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
type CallbackStatus = 'processing' | 'error';
|
|
21
|
+
type CallbackStatus = 'processing' | 'error' | 'challenge';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* OAuth Callback Handler Component
|
|
@@ -60,7 +60,7 @@ function OAuthCallbackRaw({
|
|
|
60
60
|
}: OAuthCallbackProps) {
|
|
61
61
|
const searchParams = useSearchParams();
|
|
62
62
|
const { routes } = useAuth();
|
|
63
|
-
const { setStep
|
|
63
|
+
const { setStep } = useAuthFormContext();
|
|
64
64
|
const [status, setStatus] = useState<CallbackStatus | null>(null);
|
|
65
65
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
66
66
|
|
|
@@ -85,11 +85,10 @@ function OAuthCallbackRaw({
|
|
|
85
85
|
setErrorMessage(err);
|
|
86
86
|
onError?.(err);
|
|
87
87
|
},
|
|
88
|
-
onRequires2FA: (sessionId
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setStep('2fa');
|
|
88
|
+
onRequires2FA: (sessionId) => {
|
|
89
|
+
// The dialog needs the page; this overlay would cover it.
|
|
90
|
+
setStatus('challenge');
|
|
91
|
+
beginTwoFactorChallenge({ sessionId });
|
|
93
92
|
},
|
|
94
93
|
redirectUrl,
|
|
95
94
|
skipRedirect: true, // We handle navigation via success screen
|
|
@@ -129,7 +128,7 @@ function OAuthCallbackRaw({
|
|
|
129
128
|
}, [provider, code, state, error]);
|
|
130
129
|
|
|
131
130
|
// Don't render if not an OAuth callback
|
|
132
|
-
if (!provider || (!code && !error)) {
|
|
131
|
+
if (!provider || (!code && !error) || status === 'challenge') {
|
|
133
132
|
return null;
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -19,7 +19,7 @@ export interface AuthOTPInputProps {
|
|
|
19
19
|
/**
|
|
20
20
|
* AuthOTPInput - Reusable OTP input with consistent styling.
|
|
21
21
|
*
|
|
22
|
-
* Used in: OTPStep (email, 4 digits)
|
|
22
|
+
* Used in: OTPStep (email, 4 digits).
|
|
23
23
|
*
|
|
24
24
|
* Memoised: re-renders only when value, disabled, autoFocus, size or length
|
|
25
25
|
* change. `onChange` and `onComplete` are compared by reference — callers
|
|
@@ -39,6 +39,8 @@ function OTPStepRaw() {
|
|
|
39
39
|
error,
|
|
40
40
|
isRateLimited,
|
|
41
41
|
rateLimitLabel,
|
|
42
|
+
resendSeconds,
|
|
43
|
+
resendLabel,
|
|
42
44
|
webmail,
|
|
43
45
|
setOtp,
|
|
44
46
|
handleOTPSubmit,
|
|
@@ -78,6 +80,9 @@ function OTPStepRaw() {
|
|
|
78
80
|
return `${local[0]}${'*'.repeat(Math.min(local.length - 1, 5))}@${domain}`;
|
|
79
81
|
}, [identifier]);
|
|
80
82
|
|
|
83
|
+
const waitLabel = isRateLimited ? rateLimitLabel : resendLabel;
|
|
84
|
+
const resendLabelText = waitLabel ? `${content.resend} (${waitLabel})` : content.resend;
|
|
85
|
+
|
|
81
86
|
const autoSubmitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
87
|
|
|
83
88
|
useEffect(
|
|
@@ -152,8 +157,11 @@ function OTPStepRaw() {
|
|
|
152
157
|
</AuthButton>
|
|
153
158
|
|
|
154
159
|
<div className="auth-actions">
|
|
155
|
-
<AuthLink
|
|
156
|
-
{
|
|
160
|
+
<AuthLink
|
|
161
|
+
onClick={handleResendOTP}
|
|
162
|
+
disabled={isLoading || isRateLimited || resendSeconds > 0}
|
|
163
|
+
>
|
|
164
|
+
{resendLabelText}
|
|
157
165
|
</AuthLink>
|
|
158
166
|
<AuthLink onClick={handleBackToIdentifier} disabled={isLoading}>
|
|
159
167
|
{content.changeEmail}
|
|
@@ -561,70 +561,6 @@ a.auth-logo:focus-visible {
|
|
|
561
561
|
width: 100%;
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
/* ===== QR CODE ===== */
|
|
565
|
-
|
|
566
|
-
.auth-qr-container {
|
|
567
|
-
display: flex;
|
|
568
|
-
justify-content: center;
|
|
569
|
-
padding: 0.75rem 0;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.auth-qr {
|
|
573
|
-
padding: 0.875rem;
|
|
574
|
-
background: white;
|
|
575
|
-
border-radius: var(--auth-radius);
|
|
576
|
-
box-shadow: 0 2px 12px hsl(0 0% 0% / 0.1);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
.auth-collapsible-centered {
|
|
580
|
-
display: flex;
|
|
581
|
-
flex-direction: column;
|
|
582
|
-
align-items: center;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/* ===== SECRET CODE ===== */
|
|
586
|
-
|
|
587
|
-
.auth-secret-container {
|
|
588
|
-
display: flex;
|
|
589
|
-
flex-direction: column;
|
|
590
|
-
align-items: center;
|
|
591
|
-
gap: 0.5rem;
|
|
592
|
-
padding: 0.75rem;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
.auth-secret {
|
|
596
|
-
padding: 0.5rem 0.875rem;
|
|
597
|
-
font-family: ui-monospace, 'SF Mono', monospace;
|
|
598
|
-
font-size: 0.8125rem;
|
|
599
|
-
letter-spacing: 0.08em;
|
|
600
|
-
background: var(--muted);
|
|
601
|
-
border-radius: var(--auth-radius-xs);
|
|
602
|
-
color: var(--foreground);
|
|
603
|
-
word-break: break-all;
|
|
604
|
-
text-align: center;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/* ===== BACKUP CODES ===== */
|
|
608
|
-
|
|
609
|
-
.auth-backup-codes {
|
|
610
|
-
display: grid;
|
|
611
|
-
grid-template-columns: repeat(2, 1fr);
|
|
612
|
-
gap: 0.375rem;
|
|
613
|
-
padding: 0.875rem;
|
|
614
|
-
background: color-mix(in oklab, var(--muted) 40%, transparent);
|
|
615
|
-
border: 1px dashed var(--border);
|
|
616
|
-
border-radius: var(--auth-radius-sm);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
.auth-backup-code {
|
|
620
|
-
font-family: ui-monospace, 'SF Mono', monospace;
|
|
621
|
-
font-size: 0.8125rem;
|
|
622
|
-
letter-spacing: 0.05em;
|
|
623
|
-
text-align: center;
|
|
624
|
-
padding: 0.375rem;
|
|
625
|
-
color: var(--foreground);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
564
|
/* ===== SUCCESS OVERLAY ===== */
|
|
629
565
|
|
|
630
566
|
.auth-success-overlay {
|
|
@@ -716,44 +652,6 @@ a.auth-logo:focus-visible {
|
|
|
716
652
|
color: var(--muted-foreground);
|
|
717
653
|
}
|
|
718
654
|
|
|
719
|
-
/* ===== 2FA BOX ===== */
|
|
720
|
-
|
|
721
|
-
.auth-2fa-box {
|
|
722
|
-
padding: 1.5rem 1.25rem 1.25rem;
|
|
723
|
-
background: var(--card, color-mix(in oklab, var(--muted) 25%, transparent));
|
|
724
|
-
border: 1px solid var(--border);
|
|
725
|
-
border-radius: var(--auth-radius);
|
|
726
|
-
box-shadow:
|
|
727
|
-
0 2px 12px hsl(0 0% 0% / 0.08),
|
|
728
|
-
0 0 0 1px color-mix(in oklab, var(--border) 40%, transparent) inset;
|
|
729
|
-
display: flex;
|
|
730
|
-
flex-direction: column;
|
|
731
|
-
gap: 1.125rem;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
.auth-2fa-shield {
|
|
735
|
-
display: flex;
|
|
736
|
-
align-items: center;
|
|
737
|
-
justify-content: center;
|
|
738
|
-
width: 2.75rem;
|
|
739
|
-
height: 2.75rem;
|
|
740
|
-
margin: 0 auto;
|
|
741
|
-
border-radius: 50%;
|
|
742
|
-
background: color-mix(in oklab, var(--primary) 10%, transparent);
|
|
743
|
-
color: var(--primary);
|
|
744
|
-
animation: authLogoIn 0.5s var(--spring-bounce) both;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
.auth-2fa-shield svg {
|
|
748
|
-
width: 1.375rem;
|
|
749
|
-
height: 1.375rem;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
.auth-2fa-box .auth-actions {
|
|
753
|
-
flex-wrap: wrap;
|
|
754
|
-
row-gap: 0.625rem;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
655
|
/* ===== DEV NOTICE ===== */
|
|
758
656
|
|
|
759
657
|
/* Uses the shared warning surface tokens (defined for both themes) instead of
|
|
@@ -771,14 +669,6 @@ a.auth-logo:focus-visible {
|
|
|
771
669
|
|
|
772
670
|
/* ===== INSTRUCTION ===== */
|
|
773
671
|
|
|
774
|
-
.auth-instruction {
|
|
775
|
-
font-size: 0.875rem;
|
|
776
|
-
line-height: 1.5;
|
|
777
|
-
color: var(--muted-foreground);
|
|
778
|
-
text-align: center;
|
|
779
|
-
margin: 0;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
672
|
/* ===== RESPONSIVE ===== */
|
|
783
673
|
|
|
784
674
|
@media (max-width: 480px) {
|
|
@@ -50,9 +50,12 @@ export interface AuthMarketingConsentConfig {
|
|
|
50
50
|
defaultChecked?: boolean;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Persistent-login checkbox on the identifier step. Sign-ins are remembered for
|
|
55
|
+
* 30 days by default; the checkbox only exists for apps that want the choice.
|
|
56
|
+
*/
|
|
54
57
|
export interface AuthRememberMeConfig {
|
|
55
|
-
/** Render the checkbox. Default:
|
|
58
|
+
/** Render the checkbox, checked by default. Default: false (always remembered). */
|
|
56
59
|
enabled?: boolean;
|
|
57
60
|
/** Checkbox label. Default: "Keep me signed in for 30 days". */
|
|
58
61
|
label?: React.ReactNode;
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '@djangocfg/ui-core/components';
|
|
17
17
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import { TwoFactorSetup } from '../../../../snippets/TwoFactor';
|
|
20
20
|
|
|
21
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
22
|
// Types
|
|
@@ -180,7 +180,7 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
180
180
|
</Button>
|
|
181
181
|
</div>
|
|
182
182
|
<div className="flex justify-center py-2">
|
|
183
|
-
<
|
|
183
|
+
<TwoFactorSetup
|
|
184
184
|
onComplete={handleSetupDone}
|
|
185
185
|
onSkip={() => setView('status')}
|
|
186
186
|
/>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '@djangocfg/ui-core/components';
|
|
14
14
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
15
15
|
|
|
16
|
+
import { ICON_FLAG_CLASS } from './iconFlag';
|
|
16
17
|
import { getLocaleMeta } from './localeMeta';
|
|
17
18
|
import type { LocaleSwitcherSharedProps } from './types';
|
|
18
19
|
|
|
@@ -57,7 +58,7 @@ export function LocaleSwitcherDropdown({
|
|
|
57
58
|
className={cn('overflow-hidden rounded-full p-0', className)}
|
|
58
59
|
>
|
|
59
60
|
{showFlag ? (
|
|
60
|
-
<LanguageFlag code={locale} className=
|
|
61
|
+
<LanguageFlag code={locale} rounded className={ICON_FLAG_CLASS} />
|
|
61
62
|
) : (
|
|
62
63
|
<Globe className="h-4 w-4" aria-hidden />
|
|
63
64
|
)}
|
|
@@ -6,6 +6,7 @@ import { Globe } from 'lucide-react';
|
|
|
6
6
|
import { Button, LanguageFlag } from '@djangocfg/ui-core/components';
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
|
|
9
|
+
import { ICON_FLAG_CLASS } from './iconFlag';
|
|
9
10
|
import { getLocaleMeta } from './localeMeta';
|
|
10
11
|
|
|
11
12
|
export interface LocaleSwitcherTriggerProps {
|
|
@@ -50,8 +51,7 @@ export const LocaleSwitcherTrigger = React.forwardRef<
|
|
|
50
51
|
const labelText = showCode ? locale.toUpperCase() : meta.native;
|
|
51
52
|
const isIconOnly = size === 'icon';
|
|
52
53
|
|
|
53
|
-
// Icon-only trigger:
|
|
54
|
-
// with neighbouring avatars / icon buttons. No padding, no inner gap.
|
|
54
|
+
// Icon-only trigger: a fixed-size flag centred in the button.
|
|
55
55
|
if (isIconOnly) {
|
|
56
56
|
return (
|
|
57
57
|
<Button
|
|
@@ -69,7 +69,8 @@ export const LocaleSwitcherTrigger = React.forwardRef<
|
|
|
69
69
|
{showFlag ? (
|
|
70
70
|
<LanguageFlag
|
|
71
71
|
code={locale}
|
|
72
|
-
|
|
72
|
+
rounded
|
|
73
|
+
className={ICON_FLAG_CLASS}
|
|
73
74
|
/>
|
|
74
75
|
) : (
|
|
75
76
|
<Globe className="h-4 w-4" aria-hidden />
|