@djangocfg/layouts 2.1.448 → 2.1.449

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.448",
3
+ "version": "2.1.449",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,12 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.448",
93
- "@djangocfg/centrifugo": "^2.1.448",
94
- "@djangocfg/debuger": "^2.1.448",
95
- "@djangocfg/i18n": "^2.1.448",
96
- "@djangocfg/monitor": "^2.1.448",
97
- "@djangocfg/ui-core": "^2.1.448",
92
+ "@djangocfg/api": "^2.1.449",
93
+ "@djangocfg/centrifugo": "^2.1.449",
94
+ "@djangocfg/debuger": "^2.1.449",
95
+ "@djangocfg/i18n": "^2.1.449",
96
+ "@djangocfg/monitor": "^2.1.449",
97
+ "@djangocfg/ui-core": "^2.1.449",
98
98
  "@hookform/resolvers": "^5.2.2",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -125,14 +125,14 @@
125
125
  "uuid": "^11.1.0"
126
126
  },
127
127
  "devDependencies": {
128
- "@djangocfg/api": "^2.1.448",
129
- "@djangocfg/centrifugo": "^2.1.448",
130
- "@djangocfg/debuger": "^2.1.448",
131
- "@djangocfg/i18n": "^2.1.448",
132
- "@djangocfg/monitor": "^2.1.448",
133
- "@djangocfg/typescript-config": "^2.1.448",
134
- "@djangocfg/ui-core": "^2.1.448",
135
- "@djangocfg/ui-tools": "^2.1.448",
128
+ "@djangocfg/api": "^2.1.449",
129
+ "@djangocfg/centrifugo": "^2.1.449",
130
+ "@djangocfg/debuger": "^2.1.449",
131
+ "@djangocfg/i18n": "^2.1.449",
132
+ "@djangocfg/monitor": "^2.1.449",
133
+ "@djangocfg/typescript-config": "^2.1.449",
134
+ "@djangocfg/ui-core": "^2.1.449",
135
+ "@djangocfg/ui-tools": "^2.1.449",
136
136
  "@types/node": "^25.2.3",
137
137
  "@types/react": "^19.2.15",
138
138
  "@types/react-dom": "^19.2.3",
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import React, { memo } from 'react';
4
+ import { Mail } from 'lucide-react';
5
+
6
+ /**
7
+ * WebmailIcon — brand glyph for a webmail provider.
8
+ *
9
+ * lucide-react dropped brand icons, so the recognisable providers get a small
10
+ * inline SVG (brand-coloured, currentColor-agnostic). Everything else falls
11
+ * back to a generic `Mail` glyph that inherits `currentColor`.
12
+ *
13
+ * `provider` is the slug from the backend WebmailLink (gmail, outlook, …).
14
+ */
15
+ export interface WebmailIconProps {
16
+ provider: string;
17
+ className?: string;
18
+ size?: number;
19
+ }
20
+
21
+ const SIZE = 20;
22
+
23
+ function GmailGlyph({ size }: { size: number }) {
24
+ return (
25
+ <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
26
+ <path fill="#4285F4" d="M2 6.5A1.5 1.5 0 0 1 3.5 5H4l8 6 8-6h.5A1.5 1.5 0 0 1 22 6.5V18a1 1 0 0 1-1 1h-2V9.2l-7 5.2-7-5.2V19H3a1 1 0 0 1-1-1V6.5Z" />
27
+ <path fill="#EA4335" d="M2 6.5A1.5 1.5 0 0 1 3.5 5H4l8 6-2 1.5L2 6.5Z" />
28
+ <path fill="#FBBC04" d="M22 6.5A1.5 1.5 0 0 0 20.5 5H20l-8 6 2 1.5 8-6Z" />
29
+ <path fill="#34A853" d="M3 19h2V9.2L3 7.7V19Zm18 0h-2V9.2l2-1.5V19Z" />
30
+ </svg>
31
+ );
32
+ }
33
+
34
+ function OutlookGlyph({ size }: { size: number }) {
35
+ return (
36
+ <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
37
+ <rect x="10" y="5" width="12" height="14" rx="1" fill="#0A2767" />
38
+ <rect x="10" y="5" width="12" height="7" fill="#0364B8" />
39
+ <path fill="#28A8EA" d="M22 8.5 13 13l9 4.5V8.5Z" />
40
+ <rect x="2" y="6" width="11" height="12" rx="2" fill="#0078D4" />
41
+ <path fill="#fff" d="M7.5 8.6c-2 0-3.2 1.5-3.2 3.5s1.2 3.4 3.1 3.4 3.2-1.4 3.2-3.5-1.2-3.4-3.1-3.4Zm0 1.5c1 0 1.5.9 1.5 1.9s-.5 2-1.5 2-1.5-1-1.5-2 .5-1.9 1.5-1.9Z" />
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ function YandexGlyph({ size }: { size: number }) {
47
+ return (
48
+ <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
49
+ <circle cx="12" cy="12" r="10" fill="#FC3F1D" />
50
+ <path fill="#fff" d="M13 6h-1.8c-2 0-3.3 1.3-3.3 3.2 0 1.5.7 2.4 2 3.3L7.6 18h1.9l2.2-4.9h.6V18H14V6Zm-.7 5.7c-1.1 0-1.6-.6-1.6-2 0-1.5.6-2.1 1.6-2.1h.7v4.1h-.7Z" />
51
+ </svg>
52
+ );
53
+ }
54
+
55
+ function MailRuGlyph({ size }: { size: number }) {
56
+ return (
57
+ <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
58
+ <circle cx="12" cy="12" r="10" fill="#005FF9" />
59
+ <path fill="#fff" d="M12 7.2a4.8 4.8 0 1 0 3 8.6l-1-1.2a3.2 3.2 0 1 1 1.1-2.6h1.6a4.8 4.8 0 0 0-4.7-4.8Zm0 3.2a1.6 1.6 0 1 0 0 3.2 1.6 1.6 0 0 0 0-3.2Z" />
60
+ <path fill="#FFA930" d="M16.8 12v.9a1.4 1.4 0 0 0 2.6.7A5 5 0 1 0 12 17v-1.6a3.4 3.4 0 1 1 3.2-4.6h1.6Z" />
61
+ </svg>
62
+ );
63
+ }
64
+
65
+ const GLYPHS: Record<string, React.FC<{ size: number }>> = {
66
+ gmail: GmailGlyph,
67
+ outlook: OutlookGlyph,
68
+ yandex: YandexGlyph,
69
+ mail_ru: MailRuGlyph,
70
+ };
71
+
72
+ function WebmailIconRaw({ provider, className, size = SIZE }: WebmailIconProps) {
73
+ const Glyph = GLYPHS[provider];
74
+ if (Glyph) {
75
+ return (
76
+ <span className={className} style={{ display: 'inline-flex', lineHeight: 0 }}>
77
+ <Glyph size={size} />
78
+ </span>
79
+ );
80
+ }
81
+ return <Mail className={className} width={size} height={size} aria-hidden="true" />;
82
+ }
83
+
84
+ export const WebmailIcon = memo(WebmailIconRaw);
@@ -6,6 +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 { WebmailIcon } from './WebmailIcon';
9
10
  export { AuthConsent } from './AuthConsent';
10
11
 
11
12
  export type { AuthContainerProps } from './AuthContainer';
@@ -14,6 +14,7 @@ import {
14
14
  AuthHeader,
15
15
  AuthLink,
16
16
  AuthOTPInput,
17
+ WebmailIcon,
17
18
  } from '../shared';
18
19
 
19
20
  type SubmitState = 'idle' | 'submitting';
@@ -38,6 +39,7 @@ function OTPStepRaw() {
38
39
  error,
39
40
  isRateLimited,
40
41
  rateLimitLabel,
42
+ webmail,
41
43
  setOtp,
42
44
  handleOTPSubmit,
43
45
  handleResendOTP,
@@ -58,6 +60,17 @@ function OTPStepRaw() {
58
60
  devMode: t('layouts.auth.dev.anyCodeWorks'),
59
61
  }), [t]);
60
62
 
63
+ // "Open Gmail / Outlook / …" — jumps straight to a search for our email.
64
+ const openMailboxLabel = useMemo(
65
+ () => (webmail ? t('layouts.auth.otp.openMailbox', { provider: webmail.provider_name }) : ''),
66
+ [t, webmail]
67
+ );
68
+
69
+ const handleOpenMailbox = useCallback(() => {
70
+ if (!webmail) return;
71
+ window.open(webmail.url, '_blank', 'noopener,noreferrer');
72
+ }, [webmail]);
73
+
61
74
  // Mask identifier for privacy
62
75
  const maskedIdentifier = useMemo(() => {
63
76
  const [local, domain] = identifier.split('@');
@@ -107,6 +120,18 @@ function OTPStepRaw() {
107
120
  <div className="auth-dev-notice">{content.devMode}</div>
108
121
  )}
109
122
 
123
+ {webmail && (
124
+ <AuthButton
125
+ type="button"
126
+ variant="secondary"
127
+ onClick={handleOpenMailbox}
128
+ className="auth-webmail-button"
129
+ >
130
+ <WebmailIcon provider={webmail.provider} />
131
+ {openMailboxLabel}
132
+ </AuthButton>
133
+ )}
134
+
110
135
  <AuthError message={error} />
111
136
 
112
137
  <AuthButton loading={isLoading} disabled={otp.length < AUTH.EMAIL_OTP_LENGTH}>
@@ -5,6 +5,7 @@ import React, { useMemo, useRef, useState } from 'react';
5
5
  import type {
6
6
  AuthFormReturn,
7
7
  AuthStep,
8
+ WebmailLink,
8
9
  } from '@djangocfg/api/auth';
9
10
 
10
11
  import { AuthFormContext } from '../layouts/AuthLayout/context';
@@ -29,6 +30,7 @@ export interface MockAuthFormProviderProps extends Partial<AuthLayoutConfig> {
29
30
  twoFactorCode?: string;
30
31
  useBackupCode?: boolean;
31
32
  rateLimitSeconds?: number;
33
+ webmail?: WebmailLink | null;
32
34
  is2FALoading?: boolean;
33
35
  twoFactorWarning?: string | null;
34
36
  twoFactorAttemptsRemaining?: number | null;
@@ -68,6 +70,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
68
70
  twoFactorCode: initialTwoFactorCode = '',
69
71
  useBackupCode: initialUseBackupCode = false,
70
72
  rateLimitSeconds: initialRateLimitSeconds = 0,
73
+ webmail: initialWebmail = null,
71
74
  is2FALoading = false,
72
75
  twoFactorWarning = null,
73
76
  twoFactorAttemptsRemaining = null,
@@ -83,6 +86,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
83
86
  const [twoFactorCode, setTwoFactorCode] = useState(initialTwoFactorCode);
84
87
  const [useBackupCode, setUseBackupCode] = useState(initialUseBackupCode);
85
88
  const [rateLimitSeconds, setRateLimitSeconds] = useState(initialRateLimitSeconds);
89
+ const [webmail, setWebmail] = useState<WebmailLink | null>(initialWebmail);
86
90
 
87
91
  const isAutoSubmittingFromUrl = useRef(false);
88
92
 
@@ -106,6 +110,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
106
110
  rateLimitSeconds,
107
111
  isRateLimited: rateLimitSeconds > 0,
108
112
  rateLimitLabel: formatRateLimit(rateLimitSeconds),
113
+ webmail,
109
114
 
110
115
  // State handlers
111
116
  setIdentifier,
@@ -120,6 +125,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
120
125
  setTwoFactorCode,
121
126
  setUseBackupCode,
122
127
  startRateLimitCountdown: (seconds: number) => setRateLimitSeconds(seconds),
128
+ setWebmail,
123
129
 
124
130
  // Submit handlers — all no-op (preventDefault only) so the visible
125
131
  // step never changes from under the storyteller. To preview a
@@ -166,6 +172,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
166
172
  twoFactorCode,
167
173
  useBackupCode,
168
174
  rateLimitSeconds,
175
+ webmail,
169
176
  is2FALoading,
170
177
  twoFactorWarning,
171
178
  twoFactorAttemptsRemaining,