@djangocfg/layouts 2.1.109 → 2.1.110
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 +14 -14
- package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
- package/src/layouts/AuthLayout/components/index.ts +11 -7
- package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
- package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
- package/src/layouts/AuthLayout/constants.ts +24 -0
- package/src/layouts/AuthLayout/content.ts +78 -0
- package/src/layouts/AuthLayout/hooks/index.ts +1 -0
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
- package/src/layouts/AuthLayout/index.ts +9 -5
- package/src/layouts/AuthLayout/styles/auth.css +578 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
- package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
- package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
- package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
- package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
- package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
- package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
- package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.110",
|
|
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.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/ui-core": "^2.1.
|
|
80
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
81
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
82
|
-
"@hookform/resolvers": "^5.2.
|
|
77
|
+
"@djangocfg/api": "^2.1.110",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.110",
|
|
79
|
+
"@djangocfg/ui-core": "^2.1.110",
|
|
80
|
+
"@djangocfg/ui-nextjs": "^2.1.110",
|
|
81
|
+
"@djangocfg/ui-tools": "^2.1.110",
|
|
82
|
+
"@hookform/resolvers": "^5.2.2",
|
|
83
83
|
"consola": "^3.4.2",
|
|
84
84
|
"lucide-react": "^0.545.0",
|
|
85
85
|
"moment": "^2.30.1",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"swr": "^2.3.7",
|
|
93
93
|
"tailwindcss": "^4.1.14",
|
|
94
94
|
"tailwindcss-animate": "^1.0.7",
|
|
95
|
-
"zod": "^4.
|
|
95
|
+
"zod": "^4.3.4"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
98
|
"nextjs-toploader": "^3.9.17",
|
|
@@ -101,12 +101,12 @@
|
|
|
101
101
|
"uuid": "^11.1.0"
|
|
102
102
|
},
|
|
103
103
|
"devDependencies": {
|
|
104
|
-
"@djangocfg/api": "^2.1.
|
|
105
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
106
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
107
|
-
"@djangocfg/ui-core": "^2.1.
|
|
108
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
109
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
104
|
+
"@djangocfg/api": "^2.1.110",
|
|
105
|
+
"@djangocfg/centrifugo": "^2.1.110",
|
|
106
|
+
"@djangocfg/typescript-config": "^2.1.110",
|
|
107
|
+
"@djangocfg/ui-core": "^2.1.110",
|
|
108
|
+
"@djangocfg/ui-nextjs": "^2.1.110",
|
|
109
|
+
"@djangocfg/ui-tools": "^2.1.110",
|
|
110
110
|
"@types/node": "^24.7.2",
|
|
111
111
|
"@types/react": "^19.1.0",
|
|
112
112
|
"@types/react-dom": "^19.1.0",
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth Layout
|
|
2
|
+
* Auth Layout - Apple Style
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Supports
|
|
4
|
+
* Minimal, clean authentication layout with smooth animations.
|
|
5
|
+
* Supports: email/phone OTP, OAuth (GitHub), 2FA (TOTP + backup codes)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import React from 'react';
|
|
10
|
+
import React, { useEffect, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
11
13
|
|
|
12
14
|
import { Suspense } from '../../components';
|
|
13
|
-
import {
|
|
15
|
+
import { OAuthCallback } from './components/oauth';
|
|
16
|
+
import { IdentifierStep, OTPStep, SetupStep, TwoFactorStep } from './components/steps';
|
|
17
|
+
import { AUTH } from './constants';
|
|
18
|
+
import { AUTH_CONTENT } from './content';
|
|
14
19
|
import { AuthFormProvider, useAuthFormContext } from './context';
|
|
15
20
|
|
|
21
|
+
import './styles/auth.css';
|
|
22
|
+
|
|
16
23
|
import type { AuthLayoutProps } from './types';
|
|
17
24
|
|
|
18
25
|
export type { AuthLayoutProps };
|
|
19
26
|
|
|
20
27
|
export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
21
|
-
const { enableGithubAuth, redirectUrl =
|
|
28
|
+
const { enableGithubAuth, redirectUrl = AUTH.DEFAULT_REDIRECT, onOAuthSuccess, onError, className } = props;
|
|
22
29
|
|
|
23
30
|
return (
|
|
24
31
|
<Suspense>
|
|
@@ -26,9 +33,7 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
26
33
|
{/* Full-screen success overlay */}
|
|
27
34
|
<AuthSuccessOverlay />
|
|
28
35
|
|
|
29
|
-
<div
|
|
30
|
-
className={`min-h-screen flex flex-col items-center justify-center bg-background py-6 px-4 sm:py-12 sm:px-6 lg:px-8 ${className || ''}`}
|
|
31
|
-
>
|
|
36
|
+
<div className={`auth-layout ${className || ''}`}>
|
|
32
37
|
{/* Handle OAuth callback when GitHub auth is enabled */}
|
|
33
38
|
{enableGithubAuth && (
|
|
34
39
|
<Suspense fallback={null}>
|
|
@@ -40,10 +45,8 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
|
|
|
40
45
|
</Suspense>
|
|
41
46
|
)}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<AuthContent />
|
|
46
|
-
</div>
|
|
48
|
+
{props.children}
|
|
49
|
+
<AuthContent />
|
|
47
50
|
</div>
|
|
48
51
|
</AuthFormProvider>
|
|
49
52
|
</Suspense>
|
|
@@ -55,14 +58,14 @@ const AuthContent: React.FC = () => {
|
|
|
55
58
|
|
|
56
59
|
switch (step) {
|
|
57
60
|
case 'identifier':
|
|
58
|
-
return <
|
|
61
|
+
return <IdentifierStep />;
|
|
59
62
|
case 'otp':
|
|
60
|
-
return <
|
|
63
|
+
return <OTPStep />;
|
|
61
64
|
case '2fa':
|
|
62
|
-
return <
|
|
65
|
+
return <TwoFactorStep />;
|
|
63
66
|
case '2fa-setup':
|
|
64
67
|
return (
|
|
65
|
-
<
|
|
68
|
+
<SetupStep
|
|
66
69
|
onComplete={() => setStep('success')}
|
|
67
70
|
onSkip={() => setStep('success')}
|
|
68
71
|
/>
|
|
@@ -71,16 +74,85 @@ const AuthContent: React.FC = () => {
|
|
|
71
74
|
// Success is rendered as full-screen overlay, return null here
|
|
72
75
|
return null;
|
|
73
76
|
default:
|
|
74
|
-
return <
|
|
77
|
+
return <IdentifierStep />;
|
|
75
78
|
}
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
const AuthSuccessOverlay: React.FC = () => {
|
|
79
|
-
const { step } = useAuthFormContext();
|
|
82
|
+
const { step, logoUrl, redirectUrl } = useAuthFormContext();
|
|
80
83
|
|
|
81
84
|
if (step !== 'success') {
|
|
82
85
|
return null;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
return <AuthSuccess />;
|
|
88
|
+
return <AuthSuccess logoUrl={logoUrl} redirectUrl={redirectUrl} />;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// AuthSuccess component - Apple-style success screen
|
|
92
|
+
interface AuthSuccessInlineProps {
|
|
93
|
+
logoUrl?: string;
|
|
94
|
+
redirectUrl?: string;
|
|
95
|
+
redirectDelay?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
|
|
99
|
+
logoUrl,
|
|
100
|
+
redirectUrl,
|
|
101
|
+
redirectDelay = AUTH.REDIRECT_DELAY,
|
|
102
|
+
}) => {
|
|
103
|
+
const router = useCfgRouter();
|
|
104
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const animTimer = setTimeout(() => setIsVisible(true), AUTH.ANIMATION_START_DELAY);
|
|
108
|
+
const redirectTimer = setTimeout(() => {
|
|
109
|
+
const finalUrl = redirectUrl || AUTH.DEFAULT_REDIRECT;
|
|
110
|
+
router.hardPush(finalUrl);
|
|
111
|
+
}, redirectDelay);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
clearTimeout(animTimer);
|
|
115
|
+
clearTimeout(redirectTimer);
|
|
116
|
+
};
|
|
117
|
+
}, [redirectUrl, redirectDelay, router]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="auth-success-overlay">
|
|
121
|
+
<div className="auth-success-content">
|
|
122
|
+
{logoUrl ? (
|
|
123
|
+
<img
|
|
124
|
+
src={logoUrl}
|
|
125
|
+
alt=""
|
|
126
|
+
className="auth-success-logo"
|
|
127
|
+
style={{ opacity: isVisible ? 1 : 0 }}
|
|
128
|
+
/>
|
|
129
|
+
) : (
|
|
130
|
+
<div
|
|
131
|
+
className="auth-success-check"
|
|
132
|
+
style={{ opacity: isVisible ? 1 : 0 }}
|
|
133
|
+
>
|
|
134
|
+
<svg
|
|
135
|
+
fill="none"
|
|
136
|
+
stroke="currentColor"
|
|
137
|
+
viewBox="0 0 24 24"
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
>
|
|
140
|
+
<path
|
|
141
|
+
strokeLinecap="round"
|
|
142
|
+
strokeLinejoin="round"
|
|
143
|
+
strokeWidth={2.5}
|
|
144
|
+
d="M5 13l4 4L19 7"
|
|
145
|
+
/>
|
|
146
|
+
</svg>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
<p
|
|
150
|
+
className="auth-success-text"
|
|
151
|
+
style={{ opacity: isVisible ? 1 : 0 }}
|
|
152
|
+
>
|
|
153
|
+
{AUTH_CONTENT.success.message}
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
86
158
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// Apple-style step components
|
|
2
|
+
export { IdentifierStep } from './steps/IdentifierStep';
|
|
3
|
+
export { OTPStep } from './steps/OTPStep';
|
|
4
|
+
export { TwoFactorStep } from './steps/TwoFactorStep';
|
|
5
|
+
export { SetupStep, type SetupStepProps } from './steps/SetupStep';
|
|
6
|
+
|
|
7
|
+
// Shared UI components
|
|
8
|
+
export * from './shared';
|
|
9
|
+
|
|
10
|
+
// OAuth
|
|
11
|
+
export { OAuthCallback, type OAuthCallbackProps } from './oauth';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
variant?: 'primary' | 'secondary';
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AuthButton - Apple-style button with loading state
|
|
13
|
+
*/
|
|
14
|
+
export const AuthButton: React.FC<AuthButtonProps> = ({
|
|
15
|
+
variant = 'primary',
|
|
16
|
+
loading = false,
|
|
17
|
+
disabled,
|
|
18
|
+
children,
|
|
19
|
+
className = '',
|
|
20
|
+
...props
|
|
21
|
+
}) => {
|
|
22
|
+
const variantClass = variant === 'primary' ? 'auth-button-primary' : 'auth-button-secondary';
|
|
23
|
+
const loadingClass = loading ? 'auth-button-loading' : '';
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="submit"
|
|
28
|
+
className={`auth-button ${variantClass} ${loadingClass} ${className}`}
|
|
29
|
+
disabled={disabled || loading}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { AUTH } from '../../constants';
|
|
6
|
+
import type { AuthStep } from '../../types';
|
|
7
|
+
|
|
8
|
+
export interface AuthContainerProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
step: AuthStep;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AuthContainer - Apple-style minimal container with step animations
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Full viewport centering
|
|
19
|
+
* - Max-width constraint (400px)
|
|
20
|
+
* - Animate-in on step change
|
|
21
|
+
* - No visible card/border
|
|
22
|
+
*/
|
|
23
|
+
export const AuthContainer: React.FC<AuthContainerProps> = ({
|
|
24
|
+
children,
|
|
25
|
+
step,
|
|
26
|
+
className = '',
|
|
27
|
+
}) => {
|
|
28
|
+
const [isEntering, setIsEntering] = useState(true);
|
|
29
|
+
const [currentStep, setCurrentStep] = useState(step);
|
|
30
|
+
|
|
31
|
+
// Trigger animation on step change
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (step !== currentStep) {
|
|
34
|
+
setIsEntering(true);
|
|
35
|
+
setCurrentStep(step);
|
|
36
|
+
}
|
|
37
|
+
}, [step, currentStep]);
|
|
38
|
+
|
|
39
|
+
// Reset entering state after animation
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isEntering) {
|
|
42
|
+
const timer = setTimeout(() => setIsEntering(false), AUTH.ANIMATION_DURATION);
|
|
43
|
+
return () => clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}, [isEntering]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={`auth-container ${className}`}
|
|
50
|
+
data-entering={isEntering}
|
|
51
|
+
data-step={step}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthDividerProps {
|
|
6
|
+
text?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AuthDivider - Minimal "or" divider
|
|
12
|
+
*/
|
|
13
|
+
export const AuthDivider: React.FC<AuthDividerProps> = ({
|
|
14
|
+
text = 'or',
|
|
15
|
+
className = '',
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className={`auth-divider ${className}`}>
|
|
19
|
+
{text}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthErrorProps {
|
|
6
|
+
message?: string | null;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AuthError - Subtle error display with shake animation
|
|
12
|
+
*/
|
|
13
|
+
export const AuthError: React.FC<AuthErrorProps> = ({
|
|
14
|
+
message,
|
|
15
|
+
className = '',
|
|
16
|
+
}) => {
|
|
17
|
+
if (!message) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={`auth-error ${className}`} role="alert">
|
|
23
|
+
{message}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthFooterProps {
|
|
6
|
+
termsUrl?: string;
|
|
7
|
+
privacyUrl?: string;
|
|
8
|
+
supportUrl?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AuthFooter - Minimal link row for terms, privacy, and support
|
|
14
|
+
*/
|
|
15
|
+
export const AuthFooter: React.FC<AuthFooterProps> = ({
|
|
16
|
+
termsUrl,
|
|
17
|
+
privacyUrl,
|
|
18
|
+
supportUrl,
|
|
19
|
+
className = '',
|
|
20
|
+
}) => {
|
|
21
|
+
const links = [
|
|
22
|
+
termsUrl && { href: termsUrl, label: 'Terms' },
|
|
23
|
+
privacyUrl && { href: privacyUrl, label: 'Privacy' },
|
|
24
|
+
supportUrl && { href: supportUrl, label: 'Help' },
|
|
25
|
+
].filter(Boolean) as { href: string; label: string }[];
|
|
26
|
+
|
|
27
|
+
if (links.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={`auth-footer ${className}`}>
|
|
33
|
+
{links.map((link, index) => (
|
|
34
|
+
<React.Fragment key={link.href}>
|
|
35
|
+
{index > 0 && <span className="auth-footer-dot">·</span>}
|
|
36
|
+
<a
|
|
37
|
+
href={link.href}
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
>
|
|
41
|
+
{link.label}
|
|
42
|
+
</a>
|
|
43
|
+
</React.Fragment>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthHeaderProps {
|
|
6
|
+
logo?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
identifier?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* AuthHeader - Apple-style header with logo, title, and subtitle
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Optional logo with scale animation
|
|
18
|
+
* - Large, bold title
|
|
19
|
+
* - Muted subtitle
|
|
20
|
+
* - Optional highlighted identifier
|
|
21
|
+
*/
|
|
22
|
+
export const AuthHeader: React.FC<AuthHeaderProps> = ({
|
|
23
|
+
logo,
|
|
24
|
+
title,
|
|
25
|
+
subtitle,
|
|
26
|
+
identifier,
|
|
27
|
+
className = '',
|
|
28
|
+
}) => {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`auth-header ${className}`}>
|
|
31
|
+
{logo && (
|
|
32
|
+
<img
|
|
33
|
+
src={logo}
|
|
34
|
+
alt=""
|
|
35
|
+
className="auth-logo"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
<h1 className="auth-title">{title}</h1>
|
|
40
|
+
{subtitle && (
|
|
41
|
+
<p className="auth-subtitle">
|
|
42
|
+
{subtitle}
|
|
43
|
+
{identifier && (
|
|
44
|
+
<>
|
|
45
|
+
<br />
|
|
46
|
+
<span className="auth-identifier">{identifier}</span>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
href?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AuthLink - Text link styled button/anchor
|
|
12
|
+
*/
|
|
13
|
+
export const AuthLink: React.FC<AuthLinkProps> = ({
|
|
14
|
+
children,
|
|
15
|
+
href,
|
|
16
|
+
className = '',
|
|
17
|
+
...props
|
|
18
|
+
}) => {
|
|
19
|
+
if (href) {
|
|
20
|
+
return (
|
|
21
|
+
<a
|
|
22
|
+
href={href}
|
|
23
|
+
className={`auth-link ${className}`}
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
className={`auth-link ${className}`}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { OTPInput } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
import { AUTH } from '../../constants';
|
|
8
|
+
|
|
9
|
+
export interface AuthOTPInputProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
onComplete?: (value: string) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
autoFocus?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AuthOTPInput - Reusable OTP input with consistent styling
|
|
19
|
+
*
|
|
20
|
+
* Used in: OTPStep, TwoFactorStep, SetupStep
|
|
21
|
+
*/
|
|
22
|
+
export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
onComplete,
|
|
26
|
+
disabled = false,
|
|
27
|
+
autoFocus = true,
|
|
28
|
+
}) => (
|
|
29
|
+
<div className="auth-otp-container auth-otp-wrapper">
|
|
30
|
+
<OTPInput
|
|
31
|
+
length={AUTH.OTP_LENGTH}
|
|
32
|
+
validationMode="numeric"
|
|
33
|
+
pasteBehavior="clean"
|
|
34
|
+
value={value}
|
|
35
|
+
onChange={onChange}
|
|
36
|
+
onComplete={onComplete}
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
autoFocus={autoFocus}
|
|
39
|
+
size="lg"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Mail, Phone } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import type { AuthChannel } from '../../types';
|
|
7
|
+
|
|
8
|
+
export interface ChannelToggleProps {
|
|
9
|
+
channel: AuthChannel;
|
|
10
|
+
onChange: (channel: AuthChannel) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ChannelToggle - Apple-style segmented control for email/phone
|
|
17
|
+
*/
|
|
18
|
+
export const ChannelToggle: React.FC<ChannelToggleProps> = ({
|
|
19
|
+
channel,
|
|
20
|
+
onChange,
|
|
21
|
+
disabled = false,
|
|
22
|
+
className = '',
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<div className={`auth-channel-toggle ${className}`}>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
className="auth-channel-option"
|
|
29
|
+
data-active={channel === 'email'}
|
|
30
|
+
onClick={() => onChange('email')}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
>
|
|
33
|
+
<Mail />
|
|
34
|
+
Email
|
|
35
|
+
</button>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className="auth-channel-option"
|
|
39
|
+
data-active={channel === 'phone'}
|
|
40
|
+
onClick={() => onChange('phone')}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
>
|
|
43
|
+
<Phone />
|
|
44
|
+
Phone
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { Checkbox } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
export interface TermsCheckboxProps {
|
|
8
|
+
checked: boolean;
|
|
9
|
+
onChange: (checked: boolean) => void;
|
|
10
|
+
termsUrl?: string;
|
|
11
|
+
privacyUrl?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TermsCheckbox - Compact terms acceptance checkbox
|
|
18
|
+
*/
|
|
19
|
+
export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
|
|
20
|
+
checked,
|
|
21
|
+
onChange,
|
|
22
|
+
termsUrl,
|
|
23
|
+
privacyUrl,
|
|
24
|
+
disabled = false,
|
|
25
|
+
className = '',
|
|
26
|
+
}) => {
|
|
27
|
+
// Don't render if no links provided
|
|
28
|
+
if (!termsUrl && !privacyUrl) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`auth-terms ${className}`}>
|
|
34
|
+
<Checkbox
|
|
35
|
+
id="auth-terms"
|
|
36
|
+
checked={checked}
|
|
37
|
+
onCheckedChange={onChange}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
className="auth-terms-checkbox"
|
|
40
|
+
/>
|
|
41
|
+
<label htmlFor="auth-terms">
|
|
42
|
+
I agree to the{' '}
|
|
43
|
+
{termsUrl && (
|
|
44
|
+
<a href={termsUrl} target="_blank" rel="noopener noreferrer">
|
|
45
|
+
Terms
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
{termsUrl && privacyUrl && ' and '}
|
|
49
|
+
{privacyUrl && (
|
|
50
|
+
<a href={privacyUrl} target="_blank" rel="noopener noreferrer">
|
|
51
|
+
Privacy Policy
|
|
52
|
+
</a>
|
|
53
|
+
)}
|
|
54
|
+
</label>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|