@djangocfg/layouts 2.1.580 → 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.
Files changed (30) hide show
  1. package/package.json +12 -12
  2. package/src/layouts/AuthLayout/AuthLayout.tsx +25 -7
  3. package/src/layouts/AuthLayout/README.md +16 -12
  4. package/src/layouts/AuthLayout/components/index.ts +0 -2
  5. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +8 -9
  6. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +1 -1
  7. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
  8. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +10 -2
  9. package/src/layouts/AuthLayout/components/steps/index.ts +0 -3
  10. package/src/layouts/AuthLayout/constants.ts +0 -4
  11. package/src/layouts/AuthLayout/index.ts +0 -3
  12. package/src/layouts/AuthLayout/styles/auth.css +0 -110
  13. package/src/layouts/AuthLayout/types.ts +5 -2
  14. package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +2 -2
  15. package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +2 -1
  16. package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +4 -3
  17. package/src/layouts/_components/locale-switcher/iconFlag.ts +5 -0
  18. package/src/snippets/TwoFactor/TwoFactorChallengeDialog.tsx +188 -0
  19. package/src/snippets/TwoFactor/TwoFactorSetup.tsx +209 -0
  20. package/src/snippets/TwoFactor/index.ts +2 -0
  21. package/src/snippets/index.ts +1 -0
  22. package/src/testing/MockAuthFormProvider.tsx +7 -41
  23. package/src/testing/index.ts +4 -2
  24. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +0 -86
  25. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +0 -36
  26. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +0 -141
  27. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +0 -145
  28. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +0 -133
  29. package/src/layouts/AuthLayout/hooks/index.ts +0 -1
  30. 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.580",
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.580",
94
- "@djangocfg/api": "^2.1.580",
95
- "@djangocfg/devtools": "^2.1.580",
96
- "@djangocfg/i18n": "^2.1.580",
97
- "@djangocfg/ui-core": "^2.1.580",
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.580",
128
- "@djangocfg/api": "^2.1.580",
129
- "@djangocfg/devtools": "^2.1.580",
130
- "@djangocfg/i18n": "^2.1.580",
131
- "@djangocfg/typescript-config": "^2.1.580",
132
- "@djangocfg/ui-core": "^2.1.580",
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), 2FA (TOTP + backup codes)
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, TwoFactorStep } from './components/steps';
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 — hides them on otp / 2fa / etc. */
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, GitHub OAuth, and **verifying** an existing 2FA
72
- challenge (TOTP + backup codes).
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 setup is not part of the sign-in flow.** Authentication only verifies
81
- > identity — it checks the OTP and, if the account already has 2FA, prompts for
82
- > the TOTP/backup code. *Configuring* 2FA (QR + backup codes) lives in
83
- > **ProfileLayout** (`TwoFactorSection`). The sign-in flow never shows a setup
84
- > prompt.
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, setTwoFactorSessionId, setShouldPrompt2FA } = useAuthFormContext();
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, shouldPrompt) => {
89
- // Handle 2FA requirement
90
- setTwoFactorSessionId(sessionId);
91
- setShouldPrompt2FA(shouldPrompt);
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), TwoFactorStep (TOTP, 6 digits), SetupStep (TOTP, 6 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
@@ -156,7 +156,7 @@ function IdentifierStepRaw() {
156
156
  </label>
157
157
  )}
158
158
 
159
- {rememberMeConfig?.enabled !== false && (
159
+ {rememberMeConfig?.enabled && (
160
160
  <label className="auth-optin">
161
161
  <Checkbox
162
162
  checked={rememberMe}
@@ -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 onClick={handleResendOTP} disabled={isLoading || isRateLimited}>
156
- {isRateLimited ? `${content.resend} (${rateLimitLabel})` : content.resend}
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}
@@ -1,6 +1,3 @@
1
1
  export { IdentifierStep } from './IdentifierStep';
2
2
  export { OTPStep } from './OTPStep';
3
- export { TwoFactorStep } from './TwoFactorStep';
4
- export { SetupStep } from './SetupStep';
5
3
 
6
- export type { SetupStepProps } from './SetupStep';
@@ -31,10 +31,6 @@ export const AUTH = {
31
31
  HOME_URL: '/',
32
32
  ANIMATION_DURATION: 400,
33
33
  AUTO_SUBMIT_DELAY: 100,
34
- COPY_FEEDBACK_DURATION: 2000,
35
34
  REDIRECT_DELAY: 1500,
36
35
  ANIMATION_START_DELAY: 50,
37
-
38
- // Sizes (px)
39
- QR_CODE_SIZE: 160,
40
36
  } as const;
@@ -21,9 +21,6 @@ export {
21
21
  export {
22
22
  IdentifierStep,
23
23
  OTPStep,
24
- TwoFactorStep,
25
- SetupStep,
26
- type SetupStepProps,
27
24
  OAuthCallback,
28
25
  type OAuthCallbackProps,
29
26
  } from './components';
@@ -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
- /** Persistent-login checkbox on the identifier step. */
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: true. */
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 { SetupStepStandalone } from '../../../AuthLayout/components/steps/SetupStep';
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
- <SetupStepStandalone
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="h-full w-full object-cover" />
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: full-bleed flag inside a circular pill that lines up
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
- className="h-full w-full object-cover"
72
+ rounded
73
+ className={ICON_FLAG_CLASS}
73
74
  />
74
75
  ) : (
75
76
  <Globe className="h-4 w-4" aria-hidden />
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Flag size inside an icon-only trigger. Fixed on purpose: presets stretch the
3
+ * button (44px touch targets), and a flag sized from it would be cropped and oversized.
4
+ */
5
+ export const ICON_FLAG_CLASS = 'h-[18px] w-6 shrink-0';