@djangocfg/layouts 2.1.346 → 2.1.348

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.346",
3
+ "version": "2.1.348",
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.346",
88
- "@djangocfg/centrifugo": "^2.1.346",
89
- "@djangocfg/debuger": "^2.1.346",
90
- "@djangocfg/i18n": "^2.1.346",
91
- "@djangocfg/monitor": "^2.1.346",
92
- "@djangocfg/ui-core": "^2.1.346",
93
- "@djangocfg/ui-nextjs": "^2.1.346",
87
+ "@djangocfg/api": "^2.1.348",
88
+ "@djangocfg/centrifugo": "^2.1.348",
89
+ "@djangocfg/debuger": "^2.1.348",
90
+ "@djangocfg/i18n": "^2.1.348",
91
+ "@djangocfg/monitor": "^2.1.348",
92
+ "@djangocfg/ui-core": "^2.1.348",
93
+ "@djangocfg/ui-nextjs": "^2.1.348",
94
94
  "@hookform/resolvers": "^5.2.2",
95
95
  "consola": "^3.4.2",
96
96
  "lucide-react": "^0.545.0",
@@ -120,15 +120,15 @@
120
120
  "uuid": "^11.1.0"
121
121
  },
122
122
  "devDependencies": {
123
- "@djangocfg/api": "^2.1.346",
124
- "@djangocfg/centrifugo": "^2.1.346",
125
- "@djangocfg/debuger": "^2.1.346",
126
- "@djangocfg/i18n": "^2.1.346",
127
- "@djangocfg/monitor": "^2.1.346",
128
- "@djangocfg/typescript-config": "^2.1.346",
129
- "@djangocfg/ui-core": "^2.1.346",
130
- "@djangocfg/ui-nextjs": "^2.1.346",
131
- "@djangocfg/ui-tools": "^2.1.346",
123
+ "@djangocfg/api": "^2.1.348",
124
+ "@djangocfg/centrifugo": "^2.1.348",
125
+ "@djangocfg/debuger": "^2.1.348",
126
+ "@djangocfg/i18n": "^2.1.348",
127
+ "@djangocfg/monitor": "^2.1.348",
128
+ "@djangocfg/typescript-config": "^2.1.348",
129
+ "@djangocfg/ui-core": "^2.1.348",
130
+ "@djangocfg/ui-nextjs": "^2.1.348",
131
+ "@djangocfg/ui-tools": "^2.1.348",
132
132
  "@types/node": "^24.7.2",
133
133
  "@types/react": "^19.1.0",
134
134
  "@types/react-dom": "^19.1.0",
@@ -100,7 +100,7 @@ import { AuthLayout } from '@djangocfg/layouts';
100
100
  | Step | Description |
101
101
  |---|---|
102
102
  | `identifier` | Email or phone input |
103
- | `otp` | 6-digit OTP code entry |
103
+ | `otp` | 4-digit email OTP code entry |
104
104
  | `2fa` | TOTP or backup code verification |
105
105
  | `2fa-setup` | QR code + backup codes setup |
106
106
  | `success` | Full-screen success overlay → redirect |
@@ -2,6 +2,8 @@
2
2
 
3
3
  import React, { useEffect, useState } from 'react';
4
4
 
5
+ import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
6
+ import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
5
7
  import { AUTH } from '../../constants';
6
8
  import type { AuthStep } from '../../types';
7
9
 
@@ -44,12 +46,32 @@ export const AuthContainer: React.FC<AuthContainerProps> = ({
44
46
  }
45
47
  }, [isEntering]);
46
48
 
49
+ // Locale picker is mounted as the first child so it sits at the very
50
+ // top of the form column — visually paired with the form chrome (not
51
+ // floating in a viewport corner). Renders automatically when
52
+ // LayoutI18nProvider is mounted (BaseApp does this when the host app
53
+ // passes `i18n`); no prop gating needed.
54
+ const i18n = useLayoutI18nOptional();
55
+
47
56
  return (
48
57
  <div
49
58
  className={`auth-container ${className}`}
50
59
  data-entering={isEntering}
51
60
  data-step={step}
52
61
  >
62
+ {i18n ? (
63
+ <div className="auth-container__meta">
64
+ <LocaleSwitcher
65
+ variant="dropdown"
66
+ buttonVariant="ghost"
67
+ size="sm"
68
+ showFlag
69
+ showCode={false}
70
+ showTriggerLabel
71
+ className="auth-locale-trigger"
72
+ />
73
+ </div>
74
+ ) : null}
53
75
  {children}
54
76
  </div>
55
77
  );
@@ -13,12 +13,13 @@ export interface AuthOTPInputProps {
13
13
  disabled?: boolean;
14
14
  autoFocus?: boolean;
15
15
  size?: 'sm' | 'default' | 'lg';
16
+ length?: number;
16
17
  }
17
18
 
18
19
  /**
19
20
  * AuthOTPInput - Reusable OTP input with consistent styling
20
21
  *
21
- * Used in: OTPStep, TwoFactorStep, SetupStep
22
+ * Used in: OTPStep (email, 4 digits), TwoFactorStep (TOTP, 6 digits), SetupStep (TOTP, 6 digits)
22
23
  */
23
24
  export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
24
25
  value,
@@ -27,10 +28,11 @@ export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
27
28
  disabled = false,
28
29
  autoFocus = true,
29
30
  size,
31
+ length = AUTH.TOTP_LENGTH,
30
32
  }) => (
31
33
  <div className="auth-otp-container auth-otp-wrapper">
32
34
  <OTPInput
33
- length={AUTH.OTP_LENGTH}
35
+ length={length}
34
36
  validationMode="numeric"
35
37
  pasteBehavior="clean"
36
38
  value={value}
@@ -66,7 +66,7 @@ export const OTPStep: React.FC = () => {
66
66
  const handleOTPComplete = useCallback(
67
67
  async (completedValue: string) => {
68
68
  if (submitState !== 'idle' || isLoading || isAutoSubmittingFromUrl.current) return;
69
- if (completedValue.length !== AUTH.OTP_LENGTH) return;
69
+ if (completedValue.length !== AUTH.EMAIL_OTP_LENGTH) return;
70
70
 
71
71
  setSubmitState('submitting');
72
72
  const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
@@ -96,6 +96,8 @@ export const OTPStep: React.FC = () => {
96
96
  onChange={setOtp}
97
97
  onComplete={handleOTPComplete}
98
98
  disabled={isLoading}
99
+ length={AUTH.EMAIL_OTP_LENGTH}
100
+ size="lg"
99
101
  />
100
102
 
101
103
  {config.isDevelopment && (
@@ -104,7 +106,7 @@ export const OTPStep: React.FC = () => {
104
106
 
105
107
  <AuthError message={error} />
106
108
 
107
- <AuthButton loading={isLoading} disabled={otp.length < AUTH.OTP_LENGTH}>
109
+ <AuthButton loading={isLoading} disabled={otp.length < AUTH.EMAIL_OTP_LENGTH}>
108
110
  {content.button}
109
111
  </AuthButton>
110
112
 
@@ -117,7 +117,7 @@ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
117
117
 
118
118
  <AuthButton
119
119
  loading={isLoading}
120
- disabled={confirmCode.length !== AUTH.OTP_LENGTH}
120
+ disabled={confirmCode.length !== AUTH.TOTP_LENGTH}
121
121
  >
122
122
  {content.button}
123
123
  </AuthButton>
@@ -105,7 +105,7 @@ export const TwoFactorStep: React.FC = () => {
105
105
 
106
106
  <AuthButton
107
107
  loading={is2FALoading}
108
- disabled={!useBackupCode && twoFactorCode.length !== AUTH.OTP_LENGTH}
108
+ disabled={!useBackupCode && twoFactorCode.length !== AUTH.TOTP_LENGTH}
109
109
  >
110
110
  {content.button}
111
111
  </AuthButton>
@@ -1,13 +1,18 @@
1
1
  /**
2
2
  * AuthLayout Constants
3
3
  *
4
- * All magic numbers extracted for clarity and maintainability.
4
+ * Code lengths come from the shared single source of truth in
5
+ * `@djangocfg/api/auth` (AUTH_CONSTANTS). Layout-only values (timings, sizes,
6
+ * routes) live here.
5
7
  */
6
8
 
9
+ import { AUTH_CONSTANTS } from '@djangocfg/api/auth';
10
+
7
11
  export const AUTH = {
8
- // Input lengths
9
- OTP_LENGTH: 6,
10
- BACKUP_CODE_MAX_LENGTH: 12,
12
+ // Input lengths (re-exported from @djangocfg/api/auth)
13
+ EMAIL_OTP_LENGTH: AUTH_CONSTANTS.EMAIL_OTP_LENGTH,
14
+ TOTP_LENGTH: AUTH_CONSTANTS.TOTP_LENGTH,
15
+ BACKUP_CODE_MAX_LENGTH: AUTH_CONSTANTS.BACKUP_CODE_MAX_LENGTH,
11
16
 
12
17
  // Timing (ms)
13
18
  ANIMATION_DURATION: 400,
@@ -26,6 +26,25 @@
26
26
  gap: 1.25rem;
27
27
  }
28
28
 
29
+ /* Meta row at the top of the form — currently hosts the locale picker.
30
+ Right-aligned, tight, not eating much vertical space; feels like a
31
+ tab/utility chip rather than a floating control. */
32
+ .auth-container__meta {
33
+ display: flex;
34
+ justify-content: flex-end;
35
+ margin-bottom: -0.5rem;
36
+ }
37
+
38
+ .auth-locale-trigger {
39
+ height: 1.75rem;
40
+ padding-inline: 0.5rem;
41
+ font-size: 0.75rem;
42
+ color: hsl(var(--muted-foreground));
43
+ }
44
+ .auth-locale-trigger:hover {
45
+ color: hsl(var(--foreground));
46
+ }
47
+
29
48
  .auth-container[data-entering="true"] {
30
49
  animation: authSlideIn 0.45s var(--spring-smooth) both;
31
50
  }
@@ -43,6 +43,45 @@
43
43
  outline: 1px solid hsl(var(--border));
44
44
  box-shadow: 0 4px 24px hsl(0 0% 0% / 0.08);
45
45
  overflow: hidden;
46
+ /* Apple-style frosted entry — the card "condenses" out of the
47
+ background. Uses the same spring curve (0.16, 1, 0.3, 1) that
48
+ iOS modal sheets ride on. We animate filter+transform+opacity
49
+ together so it reads as one continuous gesture, not three
50
+ stacked tweens. willChange hints the compositor to promote the
51
+ element ahead of the run; reset to auto on completion (CSS
52
+ `forwards` keeps the end state implicitly). */
53
+ animation: authShellCardEntry 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
54
+ will-change: transform, filter, opacity;
55
+ }
56
+
57
+ @keyframes authShellCardEntry {
58
+ 0% {
59
+ opacity: 0;
60
+ transform: translateY(24px) scale(0.96);
61
+ filter: blur(10px);
62
+ }
63
+ 60% {
64
+ /* Mid-keyframe lets the blur clear faster than the slide so the
65
+ form contents become readable while the card is still settling
66
+ — feels responsive instead of waiting on the animation. */
67
+ filter: blur(0);
68
+ }
69
+ 100% {
70
+ opacity: 1;
71
+ transform: translateY(0) scale(1);
72
+ filter: blur(0);
73
+ }
74
+ }
75
+
76
+ /* Respect reduced-motion preference — fall back to a plain fade. */
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .auth-shell-split__card {
79
+ animation: authShellCardEntryReduced 200ms ease-out both;
80
+ }
81
+ @keyframes authShellCardEntryReduced {
82
+ from { opacity: 0; }
83
+ to { opacity: 1; }
84
+ }
46
85
  }
47
86
 
48
87
  /* Form column */