@djangocfg/layouts 2.1.422 → 2.1.424

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 (29) hide show
  1. package/package.json +17 -17
  2. package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +6 -4
  3. package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +12 -1
  4. package/src/components/errors/ErrorsTracker/utils/formatters.ts +19 -5
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +8 -11
  6. package/src/layouts/AuthLayout/README.md +50 -18
  7. package/src/layouts/AuthLayout/components/shared/AuthConsent.tsx +46 -0
  8. package/src/layouts/AuthLayout/components/shared/index.ts +2 -2
  9. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -25
  10. package/src/layouts/AuthLayout/context.tsx +0 -4
  11. package/src/layouts/AuthLayout/shells/AuthShell.tsx +10 -1
  12. package/src/layouts/AuthLayout/shells/FullSplitShell.tsx +73 -0
  13. package/src/layouts/AuthLayout/shells/types.ts +14 -4
  14. package/src/layouts/AuthLayout/styles/auth.css +62 -40
  15. package/src/layouts/AuthLayout/styles/fullsplit-shell.css +163 -0
  16. package/src/layouts/AuthLayout/styles/split-shell.css +74 -71
  17. package/src/layouts/AuthLayout/types.ts +6 -6
  18. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +26 -2
  19. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  20. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +39 -7
  21. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +1 -2
  22. package/src/layouts/ProfileLayout/README.md +34 -2
  23. package/src/layouts/ProfileLayout/components/EditableField.tsx +4 -0
  24. package/src/layouts/ProfileLayout/hooks/index.ts +1 -1
  25. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +14 -6
  26. package/src/layouts/ProfileLayout/index.ts +2 -1
  27. package/src/layouts/ProfileLayout/types.ts +3 -2
  28. package/src/testing/MockAuthFormProvider.tsx +0 -3
  29. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +0 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.422",
3
+ "version": "2.1.424",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,13 +89,13 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.422",
93
- "@djangocfg/centrifugo": "^2.1.422",
94
- "@djangocfg/debuger": "^2.1.422",
95
- "@djangocfg/i18n": "^2.1.422",
96
- "@djangocfg/monitor": "^2.1.422",
97
- "@djangocfg/ui-core": "^2.1.422",
98
- "@djangocfg/ui-nextjs": "^2.1.422",
92
+ "@djangocfg/api": "^2.1.424",
93
+ "@djangocfg/centrifugo": "^2.1.424",
94
+ "@djangocfg/debuger": "^2.1.424",
95
+ "@djangocfg/i18n": "^2.1.424",
96
+ "@djangocfg/monitor": "^2.1.424",
97
+ "@djangocfg/ui-core": "^2.1.424",
98
+ "@djangocfg/ui-nextjs": "^2.1.424",
99
99
  "@hookform/resolvers": "^5.2.2",
100
100
  "consola": "^3.4.2",
101
101
  "lucide-react": "^0.545.0",
@@ -126,15 +126,15 @@
126
126
  "uuid": "^11.1.0"
127
127
  },
128
128
  "devDependencies": {
129
- "@djangocfg/api": "^2.1.422",
130
- "@djangocfg/centrifugo": "^2.1.422",
131
- "@djangocfg/debuger": "^2.1.422",
132
- "@djangocfg/i18n": "^2.1.422",
133
- "@djangocfg/monitor": "^2.1.422",
134
- "@djangocfg/typescript-config": "^2.1.422",
135
- "@djangocfg/ui-core": "^2.1.422",
136
- "@djangocfg/ui-nextjs": "^2.1.422",
137
- "@djangocfg/ui-tools": "^2.1.422",
129
+ "@djangocfg/api": "^2.1.424",
130
+ "@djangocfg/centrifugo": "^2.1.424",
131
+ "@djangocfg/debuger": "^2.1.424",
132
+ "@djangocfg/i18n": "^2.1.424",
133
+ "@djangocfg/monitor": "^2.1.424",
134
+ "@djangocfg/typescript-config": "^2.1.424",
135
+ "@djangocfg/ui-core": "^2.1.424",
136
+ "@djangocfg/ui-nextjs": "^2.1.424",
137
+ "@djangocfg/ui-tools": "^2.1.424",
138
138
  "@types/node": "^25.2.3",
139
139
  "@types/react": "^19.2.15",
140
140
  "@types/react-dom": "^19.2.3",
@@ -8,7 +8,7 @@
8
8
 
9
9
  import React from 'react';
10
10
 
11
- import { extractDomain, formatErrorTitle, formatZodIssues } from '../utils/formatters';
11
+ import { extractDomain, formatErrorTitle, formatZodIssues, safeZodIssues } from '../utils/formatters';
12
12
  import { ErrorButtons } from './ErrorButtons';
13
13
 
14
14
  import type {
@@ -39,9 +39,11 @@ function buildValidationDescription(
39
39
 
40
40
  // Add error count
41
41
  if (config.showErrorCount) {
42
- const count = detail.error.issues.length;
43
- const plural = count === 1 ? 'error' : 'errors';
44
- descriptionParts.push(`${count} ${plural}`);
42
+ const count = safeZodIssues(detail.error).length;
43
+ if (count > 0) {
44
+ const plural = count === 1 ? 'error' : 'errors';
45
+ descriptionParts.push(`${count} ${plural}`);
46
+ }
45
47
  }
46
48
 
47
49
  // Add formatted error messages
@@ -302,9 +302,20 @@ export function ErrorTrackingProvider({
302
302
  if (validationConfig.enabled) {
303
303
  const handler = (event: Event) => {
304
304
  if (!(event instanceof CustomEvent)) return;
305
+ const raw = event.detail ?? {};
306
+ // The generated client dispatches `{ operation, method, path, issues,
307
+ // data, timestamp }` — i.e. `issues`/`data`, not a `ZodError` in
308
+ // `error`. Normalise into the ValidationErrorDetail shape so the toast
309
+ // can render issue count/messages (and never read undefined.issues).
310
+ const error =
311
+ raw.error && Array.isArray(raw.error.issues)
312
+ ? raw.error
313
+ : { issues: Array.isArray(raw.issues) ? raw.issues : [] };
305
314
  const detail: ValidationErrorDetail = {
306
- ...event.detail,
315
+ ...raw,
307
316
  type: 'validation' as const,
317
+ error: error as ValidationErrorDetail['error'],
318
+ response: raw.response ?? raw.data,
308
319
  };
309
320
  handleError(detail, validationConfig);
310
321
  };
@@ -9,18 +9,31 @@ import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail, Centri
9
9
  import type { MonitorEvent } from '@djangocfg/monitor';
10
10
  import { EventType, EventLevel } from '@djangocfg/monitor';
11
11
 
12
+ /**
13
+ * Safely read Zod issues. The error toast must never crash itself, so we
14
+ * tolerate a missing/malformed `error` (e.g. a non-ZodError slipping through)
15
+ * instead of throwing `Cannot read properties of undefined (reading 'issues')`.
16
+ */
17
+ export function safeZodIssues(error: ZodError | null | undefined): ZodError['issues'] {
18
+ const issues = (error as ZodError | undefined)?.issues;
19
+ return Array.isArray(issues) ? issues : [];
20
+ }
21
+
12
22
  /**
13
23
  * Format Zod error issues for display
14
24
  */
15
25
  export function formatZodIssues(error: ZodError, maxIssues: number = 3): string {
16
- const issues = error.issues.slice(0, maxIssues);
26
+ const allIssues = safeZodIssues(error);
27
+ if (allIssues.length === 0) return '';
28
+
29
+ const issues = allIssues.slice(0, maxIssues);
17
30
  const formatted = issues.map((issue) => {
18
31
  const path = issue.path.join('.') || 'root';
19
32
  return `${path}: ${issue.message}`;
20
33
  });
21
34
 
22
- if (error.issues.length > maxIssues) {
23
- formatted.push(`... and ${error.issues.length - maxIssues} more`);
35
+ if (allIssues.length > maxIssues) {
36
+ formatted.push(`... and ${allIssues.length - maxIssues} more`);
24
37
  }
25
38
 
26
39
  return formatted.join(', ');
@@ -30,6 +43,7 @@ export function formatZodIssues(error: ZodError, maxIssues: number = 3): string
30
43
  * Format validation error for clipboard
31
44
  */
32
45
  export function formatValidationErrorForClipboard(detail: ValidationErrorDetail): string {
46
+ const issues = safeZodIssues(detail.error);
33
47
  const errorData = {
34
48
  type: 'validation',
35
49
  timestamp: detail.timestamp.toISOString(),
@@ -38,7 +52,7 @@ export function formatValidationErrorForClipboard(detail: ValidationErrorDetail)
38
52
  method: detail.method,
39
53
  path: detail.path,
40
54
  },
41
- validation_errors: detail.error.issues.map((issue) => ({
55
+ validation_errors: issues.map((issue) => ({
42
56
  path: issue.path.join('.') || 'root',
43
57
  message: issue.message,
44
58
  code: issue.code,
@@ -48,7 +62,7 @@ export function formatValidationErrorForClipboard(detail: ValidationErrorDetail)
48
62
  ...(('maximum' in issue) && { maximum: issue.maximum }),
49
63
  })),
50
64
  response: detail.response,
51
- total_errors: detail.error.issues.length,
65
+ total_errors: issues.length,
52
66
  };
53
67
 
54
68
  return JSON.stringify(errorData, null, 2);
@@ -17,7 +17,7 @@ import { useAppT } from '@djangocfg/i18n';
17
17
 
18
18
  import { Suspense } from '../../components';
19
19
  import { OAuthCallback } from './components/oauth';
20
- import { IdentifierStep, OTPStep, SetupStep, TwoFactorStep } from './components/steps';
20
+ import { IdentifierStep, OTPStep, TwoFactorStep } from './components/steps';
21
21
  import { AUTH } from './constants';
22
22
  import { AuthFormProvider, useAuthFormContext } from './context';
23
23
  import { AuthShell } from './shells';
@@ -25,6 +25,7 @@ import { AuthShell } from './shells';
25
25
  import './styles/auth.css';
26
26
  import './styles/centered-shell.css';
27
27
  import './styles/split-shell.css';
28
+ import './styles/fullsplit-shell.css';
28
29
 
29
30
  import type { AuthLayoutProps } from './types';
30
31
 
@@ -43,7 +44,8 @@ export const useAuthLayoutContext = (): AuthLayoutContextValue => useContext(Aut
43
44
 
44
45
  export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
45
46
  const {
46
- variant = 'centered',
47
+ variant = 'fullsplit',
48
+ mediaSide = 'left',
47
49
  background,
48
50
  sidebar,
49
51
  enableGithubAuth,
@@ -78,7 +80,7 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
78
80
  {/* Full-screen success overlay */}
79
81
  <AuthSuccessOverlay />
80
82
 
81
- <AuthShell variant={variant} background={background} sidebar={sidebar} className={className}>
83
+ <AuthShell variant={variant} mediaSide={mediaSide} background={background} sidebar={sidebar} className={className}>
82
84
  {/* Handle OAuth callback when GitHub auth is enabled */}
83
85
  {oauthCallback}
84
86
 
@@ -99,7 +101,7 @@ const AuthHeaderSlot: React.FC<{ children?: React.ReactNode }> = memo(({ childre
99
101
  });
100
102
 
101
103
  const AuthContent: React.FC = memo(() => {
102
- const { step, setStep } = useAuthFormContext();
104
+ const { step } = useAuthFormContext();
103
105
 
104
106
  switch (step) {
105
107
  case 'identifier':
@@ -107,14 +109,9 @@ const AuthContent: React.FC = memo(() => {
107
109
  case 'otp':
108
110
  return <OTPStep />;
109
111
  case '2fa':
112
+ // Sign-in verifies an *existing* 2FA challenge only. Setting up 2FA is
113
+ // owned by ProfileLayout, so the flow never renders the setup step.
110
114
  return <TwoFactorStep />;
111
- case '2fa-setup':
112
- return (
113
- <SetupStep
114
- onComplete={() => setStep('success')}
115
- onSkip={() => setStep('success')}
116
- />
117
- );
118
115
  case 'success':
119
116
  // Success is rendered as full-screen overlay, return null here
120
117
  return null;
@@ -1,14 +1,39 @@
1
1
  # AuthLayout
2
2
 
3
- Shell-based authentication layout with two visual variants:
4
- - `centered` (default) — Apple HIG-style, frameless, centered, glow background
5
- - `split` Two-column on desktop (form + sidebar), single-column on mobile
6
-
7
- Supports: email OTP, phone OTP, GitHub OAuth, 2FA (TOTP + backup codes).
3
+ Shell-based authentication layout with three visual variants:
4
+ - `fullsplit` **(default)****full-bleed 50/50** (the large-SaaS look —
5
+ Vercel / Linear / IBM). Desktop: edge-to-edge `background` image on one side
6
+ (left by default, see `mediaSide`) with the `sidebar` (quote/brand) overlaid
7
+ and a soft bottom-up shadow scrim; form on a solid panel on the other side.
8
+ The photo fades in once loaded and gets a slow Ken Burns zoom. Mobile: the
9
+ image half drops away, leaving a plain centered form.
10
+ - `centered` — Apple HIG-style, frameless, centered, glow background.
11
+ - `split` — on desktop a two-column **frosted-glass** card (form + sidebar)
12
+ floating over the `background` image; on mobile it collapses to a plain,
13
+ frameless centered form (no image, no glass, no sidebar).
14
+
15
+ The logo (when `logoUrl` is set) always renders above the form across all
16
+ variants; the `sidebar` slot should carry the quote/illustration, not a second
17
+ brand mark.
18
+
19
+ Supports: email OTP, phone OTP, GitHub OAuth, and **verifying** an existing 2FA
20
+ challenge (TOTP + backup codes).
21
+
22
+ **Consent, not a checkbox.** There is no opt-in terms checkbox — continuing *is*
23
+ the consent. When `termsUrl`/`privacyUrl` are set, a passive line ("By continuing
24
+ you agree to the Terms and Privacy Policy") renders under the submit button with
25
+ links that open in a new tab. The copy is intentionally hardcoded English (legal
26
+ document names stay in English across locales) — see `shared/AuthConsent`.
27
+
28
+ > **2FA setup is not part of the sign-in flow.** Authentication only verifies
29
+ > identity — it checks the OTP and, if the account already has 2FA, prompts for
30
+ > the TOTP/backup code. *Configuring* 2FA (QR + backup codes) lives in
31
+ > **ProfileLayout** (`TwoFactorSection`). The sign-in flow never shows a setup
32
+ > prompt.
8
33
 
9
34
  ## Usage
10
35
 
11
- ### Centered variant (default)
36
+ ### Centered variant
12
37
 
13
38
  ```tsx
14
39
  import { AuthLayout } from '@djangocfg/layouts';
@@ -60,17 +85,17 @@ import { AuthLayout } from '@djangocfg/layouts';
60
85
 
61
86
  | Prop | Type | Default | Description |
62
87
  |---|---|---|---|
63
- | `variant` | `'centered' \| 'split'` | `'centered'` | Shell layout variant |
88
+ | `variant` | `'centered' \| 'split' \| 'fullsplit'` | `'fullsplit'` | Shell layout variant |
89
+ | `mediaSide` | `'left' \| 'right'` | `'left'` | Which side the image half sits on (`fullsplit` only) |
64
90
  | `background` | `AuthBackgroundConfig` | — | Background image/gradient/overlay/blur |
65
91
  | `sidebar` | `ReactNode` | — | Right column content (split variant only) |
66
92
  | `sourceUrl` | `string` | — | App URL for analytics/tracking |
67
93
  | `redirectUrl` | `string` | `/dashboard` | Where to redirect after auth |
68
94
  | `enableGithubAuth` | `boolean` | `false` | Show GitHub OAuth button |
69
95
  | `enablePhoneAuth` | `boolean` | `false` | Allow phone number input |
70
- | `enable2FASetup` | `boolean` | `true` | Prompt 2FA setup after login |
71
- | `logoUrl` | `string` | — | Logo shown on success screen |
72
- | `termsUrl` | `string` | — | Terms of service link |
73
- | `privacyUrl` | `string` | — | Privacy policy link |
96
+ | `logoUrl` | `string` | | Logo shown above the form (centered only) and on the success screen |
97
+ | `termsUrl` | `string` | — | Terms link rendered in the passive consent line (opens in a new tab) |
98
+ | `privacyUrl` | `string` | — | Privacy link rendered in the passive consent line (opens in a new tab) |
74
99
  | `supportUrl` | `string` | — | Support page link |
75
100
  | `className` | `string` | — | Extra class on root element |
76
101
  | `children` | `ReactNode` | — | Custom header (identifier step only) |
@@ -97,16 +122,21 @@ import { AuthLayout } from '@djangocfg/layouts';
97
122
 
98
123
  ## Auth Steps
99
124
 
125
+ The sign-in flow walks these steps:
126
+
100
127
  | Step | Description |
101
128
  |---|---|
102
129
  | `identifier` | Email or phone input |
103
130
  | `otp` | 4-digit email OTP code entry |
104
- | `2fa` | TOTP or backup code verification |
105
- | `2fa-setup` | QR code + backup codes setup |
131
+ | `2fa` | TOTP or backup code verification (only if the account already has 2FA) |
106
132
  | `success` | Full-screen success overlay → redirect |
107
133
 
108
134
  `children` (custom header) are only rendered on the `identifier` step — hidden on all others.
109
135
 
136
+ > The `AuthStep` union also includes `'2fa-setup'`, but the sign-in flow never
137
+ > transitions to it. That value backs the standalone setup UI (`SetupStep` /
138
+ > `SetupStepStandalone`) which **ProfileLayout** reuses for configuring 2FA.
139
+
110
140
  ## Architecture
111
141
 
112
142
  AuthLayout is built on a **shell pattern** (inspired by PublicLayout's navbar/footer slots):
@@ -116,15 +146,17 @@ AuthLayout/
116
146
  ├── shells/
117
147
  │ ├── AuthShell.tsx — Orchestrator: loads bg image, dispatches variant
118
148
  │ ├── CenteredShell.tsx — Apple-style frameless layout
119
- │ ├── SplitShell.tsx — Two-column card layout
149
+ │ ├── SplitShell.tsx — Floating frosted-glass card layout
150
+ │ ├── FullSplitShell.tsx — Full-bleed 50/50 layout
120
151
  │ ├── context.tsx — Shell context (variant, hasSidebar)
121
152
  │ └── types.ts — AuthShellVariant, AuthBackgroundConfig
122
153
  ├── components/steps/ — Auth step components (IdentifierStep, OTPStep, ...)
123
- ├── components/shared/ — Reusable UI primitives (AuthButton, AuthHeader, ...)
154
+ ├── components/shared/ — Reusable UI primitives (AuthButton, AuthConsent, ...)
124
155
  ├── styles/
125
- │ ├── auth.css — Shared form/button/input styles
126
- │ ├── centered-shell.css — Centered variant layout
127
- └── split-shell.css — Split variant layout
156
+ │ ├── auth.css — Shared form/button/input styles
157
+ │ ├── centered-shell.css — Centered variant layout
158
+ ├── split-shell.css — Split variant layout
159
+ │ └── fullsplit-shell.css — Full-bleed split variant layout
128
160
  ```
129
161
 
130
162
  Adding a new variant:
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import React, { memo } from 'react';
4
+
5
+ export interface AuthConsentProps {
6
+ termsUrl?: string;
7
+ privacyUrl?: string;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * AuthConsent — passive consent line shown under the submit button.
13
+ *
14
+ * Replaces the old opt-in checkbox: continuing *is* the consent, so this only
15
+ * informs and links out. Shared by both the Centered and Split forms.
16
+ *
17
+ * The legal copy is intentionally hardcoded English and NOT translated:
18
+ * "Terms"/"Privacy Policy" are proper names of legal documents that stay in
19
+ * English across locales, and the one-line "By continuing…" boilerplate isn't
20
+ * worth a 17-locale i18n key. Links always open in a new tab.
21
+ *
22
+ * Renders nothing when neither legal URL is provided (no empty line).
23
+ *
24
+ * Memoised: re-renders only when the URL props or className change.
25
+ */
26
+ function AuthConsentRaw({ termsUrl, privacyUrl, className = '' }: AuthConsentProps) {
27
+ if (!termsUrl && !privacyUrl) return null;
28
+
29
+ const link = (href: string, label: string) => (
30
+ <a href={href} target="_blank" rel="noopener noreferrer" className="auth-consent-link">
31
+ {label}
32
+ </a>
33
+ );
34
+
35
+ return (
36
+ <p className={`auth-consent ${className}`}>
37
+ By continuing you agree to the{' '}
38
+ {termsUrl && link(termsUrl, 'Terms')}
39
+ {termsUrl && privacyUrl && ' and '}
40
+ {privacyUrl && link(privacyUrl, 'Privacy Policy')}
41
+ .
42
+ </p>
43
+ );
44
+ }
45
+
46
+ export const AuthConsent = memo(AuthConsentRaw);
@@ -6,7 +6,7 @@ export { AuthError } from './AuthError';
6
6
  export { AuthButton } from './AuthButton';
7
7
  export { AuthLink } from './AuthLink';
8
8
  export { AuthOTPInput } from './AuthOTPInput';
9
- export { TermsCheckbox } from './TermsCheckbox';
9
+ export { AuthConsent } from './AuthConsent';
10
10
 
11
11
  export type { AuthContainerProps } from './AuthContainer';
12
12
  export type { AuthHeaderProps } from './AuthHeader';
@@ -16,4 +16,4 @@ export type { AuthErrorProps } from './AuthError';
16
16
  export type { AuthButtonProps } from './AuthButton';
17
17
  export type { AuthLinkProps } from './AuthLink';
18
18
  export type { AuthOTPInputProps } from './AuthOTPInput';
19
- export type { TermsCheckboxProps } from './TermsCheckbox';
19
+ export type { AuthConsentProps } from './AuthConsent';
@@ -11,21 +11,20 @@ import { useAuthLayoutContext } from '../../AuthLayout';
11
11
  import { useAuthFormContext } from '../../context';
12
12
  import {
13
13
  AuthButton,
14
+ AuthConsent,
14
15
  AuthContainer,
15
16
  AuthDivider,
16
17
  AuthError,
17
- AuthFooter,
18
18
  AuthHeader,
19
- TermsCheckbox,
20
19
  } from '../shared';
21
20
 
22
21
  /**
23
22
  * IdentifierStep - Apple-style email input step.
24
23
  *
25
24
  * Clean, minimal design with:
26
- * - Optional logo
25
+ * - Optional logo (hidden in the split variant — sidebar carries the brand)
27
26
  * - Single email input field
28
- * - Terms checkbox
27
+ * - Passive consent line (continuing = consent; no opt-in checkbox)
29
28
  * - OAuth options
30
29
  *
31
30
  * Memoised: this component has no props; it reads everything from
@@ -40,18 +39,15 @@ function IdentifierStepRaw() {
40
39
  const {
41
40
  identifier,
42
41
  isLoading,
43
- acceptedTerms,
44
42
  error,
45
43
  isRateLimited,
46
44
  rateLimitLabel,
47
45
  logoUrl,
48
46
  termsUrl,
49
47
  privacyUrl,
50
- supportUrl,
51
48
  enableGithubAuth,
52
49
  sourceUrl,
53
50
  setIdentifier,
54
- setAcceptedTerms,
55
51
  setError,
56
52
  handleIdentifierSubmit,
57
53
  } = useAuthFormContext();
@@ -78,8 +74,6 @@ function IdentifierStepRaw() {
78
74
  onError: setError,
79
75
  });
80
76
 
81
- const hasTerms = Boolean(termsUrl || privacyUrl);
82
-
83
77
  return (
84
78
  <AuthContainer step="identifier">
85
79
  {!hideHeader && <AuthHeader logo={logoUrl} title={content.title} subtitle={content.subtitle.email} />}
@@ -87,32 +81,31 @@ function IdentifierStepRaw() {
87
81
  <form onSubmit={handleIdentifierSubmit} className="auth-form-group">
88
82
  <Input
89
83
  type="email"
84
+ name="email"
90
85
  value={identifier}
91
86
  onChange={(e) => setIdentifier(e.target.value)}
92
87
  placeholder={content.placeholder.email}
93
88
  disabled={isLoading}
94
89
  required
95
90
  autoFocus
96
- autoComplete="off"
91
+ autoComplete="email"
92
+ inputMode="email"
93
+ autoCapitalize="off"
94
+ spellCheck={false}
97
95
  className="auth-input"
98
96
  />
99
97
 
100
- <TermsCheckbox
101
- checked={acceptedTerms}
102
- onChange={setAcceptedTerms}
103
- termsUrl={termsUrl}
104
- privacyUrl={privacyUrl}
105
- disabled={isLoading}
106
- />
107
-
108
98
  <AuthError message={error} />
109
99
 
110
100
  <AuthButton
111
101
  loading={isLoading}
112
- disabled={!identifier || (hasTerms && !acceptedTerms) || isRateLimited}
102
+ disabled={!identifier || isRateLimited}
113
103
  >
114
104
  {isRateLimited ? `${content.button} (${rateLimitLabel})` : content.button}
115
105
  </AuthButton>
106
+
107
+ {/* Continuing implies consent — passive line, no opt-in checkbox. */}
108
+ <AuthConsent termsUrl={termsUrl} privacyUrl={privacyUrl} />
116
109
  </form>
117
110
 
118
111
  {enableGithubAuth && (
@@ -129,12 +122,6 @@ function IdentifierStepRaw() {
129
122
  </AuthButton>
130
123
  </>
131
124
  )}
132
-
133
- <AuthFooter
134
- termsUrl={termsUrl}
135
- privacyUrl={privacyUrl}
136
- supportUrl={supportUrl}
137
- />
138
125
  </AuthContainer>
139
126
  );
140
127
  }
@@ -25,7 +25,6 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
25
25
  termsUrl,
26
26
  privacyUrl,
27
27
  enableGithubAuth = false,
28
- enable2FASetup = true,
29
28
  logoUrl,
30
29
  redirectUrl,
31
30
  onIdentifierSuccess,
@@ -45,7 +44,6 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
45
44
  sourceUrl,
46
45
  redirectUrl,
47
46
  requireTermsAcceptance,
48
- enable2FASetup,
49
47
  });
50
48
 
51
49
  const value: AuthFormContextType = useMemo(
@@ -57,7 +55,6 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
57
55
  termsUrl,
58
56
  privacyUrl,
59
57
  enableGithubAuth,
60
- enable2FASetup,
61
58
  logoUrl,
62
59
  redirectUrl,
63
60
  }),
@@ -69,7 +66,6 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
69
66
  termsUrl,
70
67
  privacyUrl,
71
68
  enableGithubAuth,
72
- enable2FASetup,
73
69
  logoUrl,
74
70
  redirectUrl,
75
71
  ]
@@ -6,6 +6,7 @@ import { useImageLoader } from '@djangocfg/ui-core/hooks';
6
6
 
7
7
  import { AuthShellProvider } from './context';
8
8
  import { CenteredShell } from './CenteredShell';
9
+ import { FullSplitShell } from './FullSplitShell';
9
10
  import { SplitShell } from './SplitShell';
10
11
  import type { AuthShellProps } from './types';
11
12
 
@@ -27,13 +28,18 @@ import type { AuthShellProps } from './types';
27
28
  function AuthShellRaw({
28
29
  variant,
29
30
  children,
31
+ mediaSide = 'left',
30
32
  background,
31
33
  sidebar,
32
34
  className,
33
35
  }: AuthShellProps) {
34
36
  const { isLoaded: bgLoaded, hasError: bgError } = useImageLoader(background?.imageUrl);
35
37
 
36
- const hasBgImage = Boolean(background?.imageUrl) && bgLoaded && !bgError;
38
+ // Build the image style as soon as a URL is provided (the image is
39
+ // pre-warmed by useImageLoader, so painting it doesn't flash). `bgLoaded`
40
+ // is passed through separately so the shell can fade the layer in once the
41
+ // download actually completes — a quiet preloader, no spinner.
42
+ const hasBgImage = Boolean(background?.imageUrl) && !bgError;
37
43
 
38
44
  const bgStyle = useMemo(() => {
39
45
  if (hasBgImage && background?.imageUrl) {
@@ -71,12 +77,15 @@ function AuthShellRaw({
71
77
  bgStyle,
72
78
  overlayStyle,
73
79
  blurValue,
80
+ bgLoaded,
74
81
  };
75
82
 
76
83
  return (
77
84
  <AuthShellProvider value={shellContext}>
78
85
  {variant === 'split' ? (
79
86
  <SplitShell {...commonShellProps} sidebar={sidebar} />
87
+ ) : variant === 'fullsplit' ? (
88
+ <FullSplitShell {...commonShellProps} mediaSide={mediaSide} sidebar={sidebar} />
80
89
  ) : (
81
90
  <CenteredShell {...commonShellProps} />
82
91
  )}
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import React, { memo } from 'react';
4
+
5
+ import type { ShellRenderProps } from './types';
6
+
7
+ interface FullSplitShellProps extends ShellRenderProps {
8
+ sidebar?: React.ReactNode;
9
+ }
10
+
11
+ /**
12
+ * FullSplitShell — full-bleed 50/50 auth layout (the large-SaaS look).
13
+ *
14
+ * Desktop (>= lg): edge-to-edge. Left half is the background image with an
15
+ * overlay and the `sidebar` content (brand + quote) laid over it; right half
16
+ * is a solid `--background` panel with the centered form.
17
+ * Mobile (< lg): the image half is dropped entirely — just the plain centered
18
+ * form on `--background`, like an ordinary sign-in screen.
19
+ *
20
+ * No floating card, no glass: the two halves meet edge-to-edge, which reads as
21
+ * a considered product layout rather than a panel hovering in space.
22
+ *
23
+ * Memoised: re-renders only when bgStyle, overlayStyle, blurValue, sidebar or
24
+ * children references change.
25
+ */
26
+ function FullSplitShellRaw({
27
+ children,
28
+ className,
29
+ bgStyle,
30
+ overlayStyle,
31
+ blurValue,
32
+ mediaSide = 'left',
33
+ bgLoaded = false,
34
+ sidebar,
35
+ }: FullSplitShellProps) {
36
+ return (
37
+ <div
38
+ className={`auth-shell-fullsplit ${className || ''}`}
39
+ data-media-side={mediaSide}
40
+ >
41
+ {/* Media half — `data-loaded` drives the image fade-in. */}
42
+ <div className="auth-shell-fullsplit__media">
43
+ {bgStyle && (
44
+ <div
45
+ className="auth-shell-fullsplit__bg"
46
+ style={bgStyle}
47
+ data-loaded={bgLoaded}
48
+ />
49
+ )}
50
+ {overlayStyle && (
51
+ <div
52
+ className="auth-shell-fullsplit__overlay"
53
+ style={{
54
+ ...overlayStyle,
55
+ backdropFilter: blurValue ? `blur(${blurValue})` : undefined,
56
+ WebkitBackdropFilter: blurValue ? `blur(${blurValue})` : undefined,
57
+ }}
58
+ />
59
+ )}
60
+ {sidebar && (
61
+ <div className="auth-shell-fullsplit__sidebar">{sidebar}</div>
62
+ )}
63
+ </div>
64
+
65
+ {/* Right: solid panel with the centered form */}
66
+ <div className="auth-shell-fullsplit__form">
67
+ <div className="auth-shell-fullsplit__form-inner">{children}</div>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export const FullSplitShell = memo(FullSplitShellRaw);