@djangocfg/layouts 2.1.114 → 2.1.115

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.114",
3
+ "version": "2.1.115",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,12 +74,12 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.114",
78
- "@djangocfg/centrifugo": "^2.1.114",
79
- "@djangocfg/i18n": "^2.1.114",
80
- "@djangocfg/ui-core": "^2.1.114",
81
- "@djangocfg/ui-nextjs": "^2.1.114",
82
- "@djangocfg/ui-tools": "^2.1.114",
77
+ "@djangocfg/api": "^2.1.115",
78
+ "@djangocfg/centrifugo": "^2.1.115",
79
+ "@djangocfg/i18n": "^2.1.115",
80
+ "@djangocfg/ui-core": "^2.1.115",
81
+ "@djangocfg/ui-nextjs": "^2.1.115",
82
+ "@djangocfg/ui-tools": "^2.1.115",
83
83
  "@hookform/resolvers": "^5.2.2",
84
84
  "consola": "^3.4.2",
85
85
  "lucide-react": "^0.545.0",
@@ -102,13 +102,13 @@
102
102
  "uuid": "^11.1.0"
103
103
  },
104
104
  "devDependencies": {
105
- "@djangocfg/api": "^2.1.114",
106
- "@djangocfg/i18n": "^2.1.114",
107
- "@djangocfg/centrifugo": "^2.1.114",
108
- "@djangocfg/typescript-config": "^2.1.114",
109
- "@djangocfg/ui-core": "^2.1.114",
110
- "@djangocfg/ui-nextjs": "^2.1.114",
111
- "@djangocfg/ui-tools": "^2.1.114",
105
+ "@djangocfg/api": "^2.1.115",
106
+ "@djangocfg/i18n": "^2.1.115",
107
+ "@djangocfg/centrifugo": "^2.1.115",
108
+ "@djangocfg/typescript-config": "^2.1.115",
109
+ "@djangocfg/ui-core": "^2.1.115",
110
+ "@djangocfg/ui-nextjs": "^2.1.115",
111
+ "@djangocfg/ui-tools": "^2.1.115",
112
112
  "@types/node": "^24.7.2",
113
113
  "@types/react": "^19.1.0",
114
114
  "@types/react-dom": "^19.1.0",
@@ -10,12 +10,12 @@
10
10
  import React, { useEffect, useState } from 'react';
11
11
 
12
12
  import { useCfgRouter } from '@djangocfg/api/auth';
13
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
13
14
 
14
15
  import { Suspense } from '../../components';
15
16
  import { OAuthCallback } from './components/oauth';
16
17
  import { IdentifierStep, OTPStep, SetupStep, TwoFactorStep } from './components/steps';
17
18
  import { AUTH } from './constants';
18
- import { AUTH_CONTENT } from './content';
19
19
  import { AuthFormProvider, useAuthFormContext } from './context';
20
20
 
21
21
  import './styles/auth.css';
@@ -101,8 +101,11 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
101
101
  redirectDelay = AUTH.REDIRECT_DELAY,
102
102
  }) => {
103
103
  const router = useCfgRouter();
104
+ const t = useTypedT<I18nTranslations>();
104
105
  const [isVisible, setIsVisible] = useState(false);
105
106
 
107
+ const successMessage = React.useMemo(() => t('layouts.auth.success.message'), [t]);
108
+
106
109
  useEffect(() => {
107
110
  const animTimer = setTimeout(() => setIsVisible(true), AUTH.ANIMATION_START_DELAY);
108
111
  const redirectTimer = setTimeout(() => {
@@ -150,7 +153,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
150
153
  className="auth-success-text"
151
154
  style={{ opacity: isVisible ? 1 : 0 }}
152
155
  >
153
- {AUTH_CONTENT.success.message}
156
+ {successMessage}
154
157
  </p>
155
158
  </div>
156
159
  </div>
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
+
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
4
6
 
5
7
  export interface AuthDividerProps {
6
8
  text?: string;
@@ -11,12 +13,16 @@ export interface AuthDividerProps {
11
13
  * AuthDivider - Minimal "or" divider
12
14
  */
13
15
  export const AuthDivider: React.FC<AuthDividerProps> = ({
14
- text = 'or',
16
+ text,
15
17
  className = '',
16
18
  }) => {
19
+ const t = useTypedT<I18nTranslations>();
20
+ const defaultText = useMemo(() => t('layouts.auth.divider.or'), [t]);
21
+ const dividerText = text ?? defaultText;
22
+
17
23
  return (
18
24
  <div className={`auth-divider ${className}`}>
19
- {text}
25
+ {dividerText}
20
26
  </div>
21
27
  );
22
28
  };
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
+
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
4
6
 
5
7
  export interface AuthFooterProps {
6
8
  termsUrl?: string;
@@ -18,10 +20,17 @@ export const AuthFooter: React.FC<AuthFooterProps> = ({
18
20
  supportUrl,
19
21
  className = '',
20
22
  }) => {
23
+ const t = useTypedT<I18nTranslations>();
24
+ const labels = useMemo(() => ({
25
+ terms: t('layouts.auth.footer.terms'),
26
+ privacy: t('layouts.auth.footer.privacy'),
27
+ support: t('layouts.auth.footer.support'),
28
+ }), [t]);
29
+
21
30
  const links = [
22
- termsUrl && { href: termsUrl, label: 'Terms' },
23
- privacyUrl && { href: privacyUrl, label: 'Privacy' },
24
- supportUrl && { href: supportUrl, label: 'Help' },
31
+ termsUrl && { href: termsUrl, label: labels.terms },
32
+ privacyUrl && { href: privacyUrl, label: labels.privacy },
33
+ supportUrl && { href: supportUrl, label: labels.support },
25
34
  ].filter(Boolean) as { href: string; label: string }[];
26
35
 
27
36
  if (links.length === 0) {
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { Mail, Phone } from 'lucide-react';
4
- import React from 'react';
4
+ import React, { useMemo } from 'react';
5
+
6
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
5
7
 
6
8
  import type { AuthChannel } from '../../types';
7
9
 
@@ -21,6 +23,12 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
21
23
  disabled = false,
22
24
  className = '',
23
25
  }) => {
26
+ const t = useTypedT<I18nTranslations>();
27
+ const labels = useMemo(() => ({
28
+ email: t('layouts.auth.identifier.channel.email'),
29
+ phone: t('layouts.auth.identifier.channel.phone'),
30
+ }), [t]);
31
+
24
32
  return (
25
33
  <div className={`auth-channel-toggle ${className}`}>
26
34
  <button
@@ -31,7 +39,7 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
31
39
  disabled={disabled}
32
40
  >
33
41
  <Mail />
34
- Email
42
+ {labels.email}
35
43
  </button>
36
44
  <button
37
45
  type="button"
@@ -41,7 +49,7 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
41
49
  disabled={disabled}
42
50
  >
43
51
  <Phone />
44
- Phone
52
+ {labels.phone}
45
53
  </button>
46
54
  </div>
47
55
  );
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
4
 
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
5
6
  import { Checkbox } from '@djangocfg/ui-core/components';
6
7
 
7
8
  export interface TermsCheckboxProps {
@@ -24,6 +25,14 @@ export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
24
25
  disabled = false,
25
26
  className = '',
26
27
  }) => {
28
+ const t = useTypedT<I18nTranslations>();
29
+ const labels = useMemo(() => ({
30
+ agree: t('layouts.auth.terms.agree'),
31
+ and: t('layouts.auth.terms.and'),
32
+ terms: t('layouts.auth.terms.terms'),
33
+ privacy: t('layouts.auth.terms.privacy'),
34
+ }), [t]);
35
+
27
36
  // Don't render if no links provided
28
37
  if (!termsUrl && !privacyUrl) {
29
38
  return null;
@@ -39,16 +48,16 @@ export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
39
48
  className="auth-terms-checkbox"
40
49
  />
41
50
  <label htmlFor="auth-terms">
42
- I agree to the{' '}
51
+ {labels.agree}{' '}
43
52
  {termsUrl && (
44
53
  <a href={termsUrl} target="_blank" rel="noopener noreferrer">
45
- Terms
54
+ {labels.terms}
46
55
  </a>
47
56
  )}
48
- {termsUrl && privacyUrl && ' and '}
57
+ {termsUrl && privacyUrl && ` ${labels.and} `}
49
58
  {privacyUrl && (
50
59
  <a href={privacyUrl} target="_blank" rel="noopener noreferrer">
51
- Privacy Policy
60
+ {labels.privacy}
52
61
  </a>
53
62
  )}
54
63
  </label>
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { Github } from 'lucide-react';
4
- import React, { useEffect } from 'react';
4
+ import React, { useEffect, useMemo } from 'react';
5
5
 
6
6
  import { useGithubAuth } from '@djangocfg/api/auth';
7
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
8
  import { Input, PhoneInput } from '@djangocfg/ui-core/components';
8
9
 
9
- import { AUTH_CONTENT } from '../../content';
10
10
  import { useAuthFormContext } from '../../context';
11
11
  import {
12
12
  AuthButton,
@@ -52,6 +52,24 @@ export const IdentifierStep: React.FC = () => {
52
52
  validateIdentifier,
53
53
  } = useAuthFormContext();
54
54
 
55
+ // Translations
56
+ const t = useTypedT<I18nTranslations>();
57
+ const content = useMemo(() => ({
58
+ title: t('layouts.auth.identifier.title'),
59
+ subtitle: {
60
+ email: t('layouts.auth.identifier.subtitle.email'),
61
+ phone: t('layouts.auth.identifier.subtitle.phone'),
62
+ },
63
+ placeholder: {
64
+ email: t('layouts.auth.identifier.placeholder.email'),
65
+ phone: t('layouts.auth.identifier.placeholder.phone'),
66
+ },
67
+ button: t('layouts.auth.identifier.button'),
68
+ oauth: {
69
+ github: t('layouts.auth.identifier.oauth.github'),
70
+ },
71
+ }), [t]);
72
+
55
73
  // GitHub OAuth
56
74
  const { isLoading: isGithubLoading, startGithubAuth } = useGithubAuth({
57
75
  sourceUrl,
@@ -87,7 +105,6 @@ export const IdentifierStep: React.FC = () => {
87
105
  }
88
106
  };
89
107
 
90
- const content = AUTH_CONTENT.identifier;
91
108
  const subtitle = channel === 'phone' ? content.subtitle.phone : content.subtitle.email;
92
109
  const hasTerms = Boolean(termsUrl || privacyUrl);
93
110
 
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import React, { useCallback, useState } from 'react';
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
4
6
 
5
7
  import { config } from '../../../../utils';
6
8
  import { AUTH } from '../../constants';
7
- import { AUTH_CONTENT } from '../../content';
8
9
  import { useAuthFormContext } from '../../context';
9
10
  import {
10
11
  AuthButton,
@@ -42,8 +43,20 @@ export const OTPStep: React.FC = () => {
42
43
 
43
44
  const [submitState, setSubmitState] = useState<SubmitState>('idle');
44
45
 
46
+ // Translations
47
+ const t = useTypedT<I18nTranslations>();
48
+ const content = useMemo(() => ({
49
+ title: t('layouts.auth.otp.title'),
50
+ subtitle: t('layouts.auth.otp.subtitle'),
51
+ button: t('layouts.auth.otp.button'),
52
+ resend: t('layouts.auth.otp.resend'),
53
+ changeEmail: t('layouts.auth.otp.changeEmail'),
54
+ changePhone: t('layouts.auth.otp.changePhone'),
55
+ devMode: t('layouts.auth.dev.anyCodeWorks'),
56
+ }), [t]);
57
+
45
58
  // Mask identifier for privacy
46
- const maskedIdentifier = React.useMemo(() => {
59
+ const maskedIdentifier = useMemo(() => {
47
60
  if (channel === 'phone') {
48
61
  return identifier.slice(-4).padStart(identifier.length, '*');
49
62
  }
@@ -72,7 +85,7 @@ export const OTPStep: React.FC = () => {
72
85
  [handleOTPSubmit, isLoading, isAutoSubmittingFromUrl, submitState]
73
86
  );
74
87
 
75
- const content = AUTH_CONTENT.otp;
88
+ const changeIdentifierLabel = channel === 'phone' ? content.changePhone : content.changeEmail;
76
89
 
77
90
  return (
78
91
  <AuthContainer step="otp">
@@ -91,7 +104,7 @@ export const OTPStep: React.FC = () => {
91
104
  />
92
105
 
93
106
  {config.isDevelopment && (
94
- <div className="auth-dev-notice">{AUTH_CONTENT.dev.anyCodeWorks}</div>
107
+ <div className="auth-dev-notice">{content.devMode}</div>
95
108
  )}
96
109
 
97
110
  <AuthError message={error} />
@@ -105,7 +118,7 @@ export const OTPStep: React.FC = () => {
105
118
  {content.resend}
106
119
  </AuthLink>
107
120
  <AuthLink onClick={handleBackToIdentifier} disabled={isLoading}>
108
- {content.changeIdentifier(channel)}
121
+ {changeIdentifierLabel}
109
122
  </AuthLink>
110
123
  </div>
111
124
  </form>
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { Check, Copy } from 'lucide-react';
4
- import React from 'react';
4
+ import React, { useMemo } from 'react';
5
+
6
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
5
7
 
6
- import { AUTH_CONTENT } from '../../../content';
7
8
  import { useCopyToClipboard } from '../../../hooks';
8
9
  import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
9
10
 
@@ -22,7 +23,15 @@ export const SetupComplete: React.FC<SetupCompleteProps> = ({
22
23
  onDone,
23
24
  }) => {
24
25
  const { copied, copy } = useCopyToClipboard();
25
- const content = AUTH_CONTENT.setup.complete;
26
+ const t = useTypedT<I18nTranslations>();
27
+ const content = useMemo(() => ({
28
+ title: t('layouts.auth.setup.complete.title'),
29
+ subtitle: t('layouts.auth.setup.complete.subtitle'),
30
+ instruction: t('layouts.auth.setup.complete.instruction'),
31
+ copyAll: t('layouts.auth.setup.complete.copyAll'),
32
+ copied: t('layouts.auth.setup.complete.copied'),
33
+ done: t('layouts.auth.setup.complete.done'),
34
+ }), [t]);
26
35
 
27
36
  const handleCopyAll = () => {
28
37
  copy(backupCodes.join('\n'));
@@ -1,15 +1,20 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
+
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
4
6
 
5
- import { AUTH_CONTENT } from '../../../content';
6
7
  import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
7
8
 
8
9
  /**
9
10
  * SetupLoading - Loading state for 2FA setup
10
11
  */
11
12
  export const SetupLoading: React.FC = () => {
12
- const content = AUTH_CONTENT.setup.loading;
13
+ const t = useTypedT<I18nTranslations>();
14
+ const content = useMemo(() => ({
15
+ title: t('layouts.auth.setup.loading.title'),
16
+ button: t('layouts.auth.setup.loading.button'),
17
+ }), [t]);
13
18
 
14
19
  return (
15
20
  <AuthContainer step="2fa-setup">
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { Check, Copy } from 'lucide-react';
4
- import React, { useState } from 'react';
4
+ import React, { useMemo, useState } from 'react';
5
5
  import { QRCodeSVG } from 'qrcode.react';
6
6
 
7
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
8
  import {
8
9
  Collapsible,
9
10
  CollapsibleContent,
@@ -11,7 +12,6 @@ import {
11
12
  } from '@djangocfg/ui-core/components';
12
13
 
13
14
  import { AUTH } from '../../../constants';
14
- import { AUTH_CONTENT } from '../../../content';
15
15
  import { useCopyToClipboard } from '../../../hooks';
16
16
  import {
17
17
  AuthButton,
@@ -44,7 +44,17 @@ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
44
44
  }) => {
45
45
  const [confirmCode, setConfirmCode] = useState('');
46
46
  const { copied, copy } = useCopyToClipboard();
47
- const content = AUTH_CONTENT.setup.qrCode;
47
+ const t = useTypedT<I18nTranslations>();
48
+ const content = useMemo(() => ({
49
+ title: t('layouts.auth.setup.qrCode.title'),
50
+ subtitle: t('layouts.auth.setup.qrCode.subtitle'),
51
+ manualEntry: t('layouts.auth.setup.qrCode.manualEntry'),
52
+ confirmPrompt: t('layouts.auth.setup.qrCode.confirmPrompt'),
53
+ button: t('layouts.auth.setup.qrCode.button'),
54
+ skip: t('layouts.auth.setup.qrCode.skip'),
55
+ copied: t('layouts.auth.setup.complete.copied'),
56
+ copy: 'Copy',
57
+ }), [t]);
48
58
 
49
59
  const handleSubmit = async (e: React.FormEvent) => {
50
60
  e.preventDefault();
@@ -82,12 +92,12 @@ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
82
92
  {copied ? (
83
93
  <>
84
94
  <Check className="w-4 h-4" />
85
- {AUTH_CONTENT.setup.complete.copied}
95
+ {content.copied}
86
96
  </>
87
97
  ) : (
88
98
  <>
89
99
  <Copy className="w-4 h-4" />
90
- Copy
100
+ {content.copy}
91
101
  </>
92
102
  )}
93
103
  </AuthLink>
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
4
 
5
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
5
6
  import { Input } from '@djangocfg/ui-core/components';
6
7
 
7
8
  import { AUTH } from '../../constants';
8
- import { AUTH_CONTENT } from '../../content';
9
9
  import { useAuthFormContext } from '../../context';
10
10
  import {
11
11
  AuthButton,
@@ -37,7 +37,24 @@ export const TwoFactorStep: React.FC = () => {
37
37
  handleUseTOTP,
38
38
  } = useAuthFormContext();
39
39
 
40
- const content = AUTH_CONTENT.twoFactor;
40
+ // Translations
41
+ const t = useTypedT<I18nTranslations>();
42
+ const content = useMemo(() => ({
43
+ title: t('layouts.auth.twoFactor.title'),
44
+ subtitle: {
45
+ totp: t('layouts.auth.twoFactor.subtitle.totp'),
46
+ backup: t('layouts.auth.twoFactor.subtitle.backup'),
47
+ },
48
+ placeholder: {
49
+ backup: t('layouts.auth.twoFactor.placeholder.backup'),
50
+ },
51
+ button: t('layouts.auth.twoFactor.button'),
52
+ toggle: {
53
+ toBackup: t('layouts.auth.twoFactor.toggle.toBackup'),
54
+ toTotp: t('layouts.auth.twoFactor.toggle.toTotp'),
55
+ },
56
+ }), [t]);
57
+
41
58
  const subtitle = useBackupCode ? content.subtitle.backup : content.subtitle.totp;
42
59
 
43
60
  return (
@@ -1,78 +0,0 @@
1
- /**
2
- * AuthLayout Content
3
- *
4
- * All UI strings centralized for easy updates and i18n.
5
- */
6
-
7
- export const AUTH_CONTENT = {
8
- identifier: {
9
- title: 'Sign in',
10
- subtitle: {
11
- email: 'Enter your email to continue',
12
- phone: 'Enter your phone number to continue',
13
- },
14
- placeholder: {
15
- email: 'email@example.com',
16
- phone: 'Enter your phone number',
17
- },
18
- button: 'Continue',
19
- oauth: {
20
- github: 'Continue with GitHub',
21
- },
22
- },
23
-
24
- otp: {
25
- title: 'Enter code',
26
- subtitle: 'We sent a code to',
27
- button: 'Verify',
28
- resend: 'Resend code',
29
- changeIdentifier: (channel: string) => `Change ${channel}`,
30
- },
31
-
32
- twoFactor: {
33
- title: 'Two-factor authentication',
34
- subtitle: {
35
- totp: 'Enter the 6-digit code from your authenticator',
36
- backup: 'Enter one of your backup codes',
37
- },
38
- placeholder: {
39
- backup: 'XXXXXXXX',
40
- },
41
- button: 'Verify',
42
- toggle: {
43
- toBackup: 'Use backup code',
44
- toTotp: 'Use authenticator app',
45
- },
46
- },
47
-
48
- setup: {
49
- loading: {
50
- title: 'Setting up...',
51
- button: 'Loading',
52
- },
53
- qrCode: {
54
- title: 'Set up 2FA',
55
- subtitle: 'Scan this QR code with your authenticator app',
56
- manualEntry: "Can't scan? Enter manually",
57
- confirmPrompt: 'Enter the 6-digit code to confirm',
58
- button: 'Enable 2FA',
59
- skip: 'Skip for now',
60
- },
61
- complete: {
62
- title: '2FA enabled',
63
- subtitle: 'Save these backup codes securely',
64
- instruction: 'Each code can only be used once',
65
- copyAll: 'Copy all codes',
66
- copied: 'Copied',
67
- done: 'Done',
68
- },
69
- },
70
-
71
- success: {
72
- message: 'Signed in successfully',
73
- },
74
-
75
- dev: {
76
- anyCodeWorks: 'Dev Mode: Any code works',
77
- },
78
- } as const;