@djangocfg/layouts 2.1.109 → 2.1.111

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 (53) hide show
  1. package/package.json +16 -14
  2. package/src/components/errors/ErrorBoundary.tsx +12 -6
  3. package/src/components/errors/ErrorLayout.tsx +19 -9
  4. package/src/components/errors/errorConfig.ts +28 -22
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  6. package/src/layouts/AuthLayout/components/index.ts +11 -7
  7. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  8. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  13. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  14. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  16. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  17. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  18. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  19. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  20. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  23. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  24. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  25. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  26. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  27. package/src/layouts/AuthLayout/constants.ts +24 -0
  28. package/src/layouts/AuthLayout/content.ts +78 -0
  29. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  30. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  31. package/src/layouts/AuthLayout/index.ts +9 -5
  32. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  33. package/src/layouts/ProfileLayout/ProfileLayout.tsx +130 -58
  34. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  35. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
  36. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +6 -2
  37. package/src/layouts/_components/UserMenu.tsx +14 -6
  38. package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
  39. package/src/snippets/Breadcrumbs.tsx +19 -8
  40. package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
  41. package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
  42. package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
  43. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
  44. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
  45. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
  46. package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
  47. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  48. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  49. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  50. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  51. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  52. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  53. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import { Check, Copy } from 'lucide-react';
4
+ import React, { useState } from 'react';
5
+ import { QRCodeSVG } from 'qrcode.react';
6
+
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger,
11
+ } from '@djangocfg/ui-core/components';
12
+
13
+ import { AUTH } from '../../../constants';
14
+ import { AUTH_CONTENT } from '../../../content';
15
+ import { useCopyToClipboard } from '../../../hooks';
16
+ import {
17
+ AuthButton,
18
+ AuthContainer,
19
+ AuthError,
20
+ AuthHeader,
21
+ AuthLink,
22
+ AuthOTPInput,
23
+ } from '../../shared';
24
+
25
+ interface SetupQRCodeProps {
26
+ provisioningUri: string;
27
+ secret: string;
28
+ isLoading: boolean;
29
+ error?: string;
30
+ onConfirm: (code: string) => Promise<unknown>;
31
+ onSkip?: () => void;
32
+ }
33
+
34
+ /**
35
+ * SetupQRCode - QR code scanning step for 2FA setup
36
+ */
37
+ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
38
+ provisioningUri,
39
+ secret,
40
+ isLoading,
41
+ error,
42
+ onConfirm,
43
+ onSkip,
44
+ }) => {
45
+ const [confirmCode, setConfirmCode] = useState('');
46
+ const { copied, copy } = useCopyToClipboard();
47
+ const content = AUTH_CONTENT.setup.qrCode;
48
+
49
+ const handleSubmit = async (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ await onConfirm(confirmCode);
52
+ };
53
+
54
+ const handleCopySecret = () => {
55
+ copy(secret);
56
+ };
57
+
58
+ return (
59
+ <AuthContainer step="2fa-setup">
60
+ <AuthHeader title={content.title} subtitle={content.subtitle} />
61
+
62
+ <form onSubmit={handleSubmit} className="auth-form-group">
63
+ <div className="auth-qr-container">
64
+ <div className="auth-qr">
65
+ <QRCodeSVG
66
+ value={provisioningUri}
67
+ size={AUTH.QR_CODE_SIZE}
68
+ level="M"
69
+ marginSize={0}
70
+ />
71
+ </div>
72
+ </div>
73
+
74
+ <Collapsible>
75
+ <CollapsibleTrigger asChild>
76
+ <AuthLink>{content.manualEntry}</AuthLink>
77
+ </CollapsibleTrigger>
78
+ <CollapsibleContent>
79
+ <div className="auth-secret-container">
80
+ <code className="auth-secret">{secret}</code>
81
+ <AuthLink onClick={handleCopySecret}>
82
+ {copied ? (
83
+ <>
84
+ <Check className="w-4 h-4" />
85
+ {AUTH_CONTENT.setup.complete.copied}
86
+ </>
87
+ ) : (
88
+ <>
89
+ <Copy className="w-4 h-4" />
90
+ Copy
91
+ </>
92
+ )}
93
+ </AuthLink>
94
+ </div>
95
+ </CollapsibleContent>
96
+ </Collapsible>
97
+
98
+ <p className="auth-instruction">{content.confirmPrompt}</p>
99
+
100
+ <AuthOTPInput
101
+ value={confirmCode}
102
+ onChange={setConfirmCode}
103
+ disabled={isLoading}
104
+ />
105
+
106
+ <AuthError message={error} />
107
+
108
+ <AuthButton
109
+ loading={isLoading}
110
+ disabled={confirmCode.length !== AUTH.OTP_LENGTH}
111
+ >
112
+ {content.button}
113
+ </AuthButton>
114
+
115
+ {onSkip && (
116
+ <div className="auth-actions">
117
+ <AuthLink onClick={onSkip} disabled={isLoading}>
118
+ {content.skip}
119
+ </AuthLink>
120
+ </div>
121
+ )}
122
+ </form>
123
+ </AuthContainer>
124
+ );
125
+ };
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { useTwoFactorSetup } from '@djangocfg/api/auth';
6
+
7
+ import { useAuthFormContext } from '../../../context';
8
+ import { SetupComplete } from './SetupComplete';
9
+ import { SetupLoading } from './SetupLoading';
10
+ import { SetupQRCode } from './SetupQRCode';
11
+
12
+ export interface SetupStepProps {
13
+ onComplete?: () => void;
14
+ onSkip?: () => void;
15
+ }
16
+
17
+ /**
18
+ * SetupStep - Orchestrator for 2FA setup flow
19
+ *
20
+ * Delegates rendering to focused sub-components:
21
+ * - SetupLoading: Initial loading state
22
+ * - SetupQRCode: QR code scanning
23
+ * - SetupComplete: Backup codes display
24
+ */
25
+ export const SetupStep: React.FC<SetupStepProps> = ({ onComplete, onSkip }) => {
26
+ const { setStep } = useAuthFormContext();
27
+
28
+ const handleComplete = () => {
29
+ onComplete?.();
30
+ setStep('success');
31
+ };
32
+
33
+ const handleSkip = () => {
34
+ onSkip?.();
35
+ setStep('success');
36
+ };
37
+
38
+ const {
39
+ isLoading,
40
+ error,
41
+ setupData,
42
+ backupCodes,
43
+ backupCodesWarning,
44
+ setupStep,
45
+ startSetup,
46
+ confirmSetup,
47
+ } = useTwoFactorSetup({
48
+ onComplete: handleComplete,
49
+ onError: () => {},
50
+ });
51
+
52
+ // Start setup on mount
53
+ React.useEffect(() => {
54
+ if (setupStep === 'idle') {
55
+ startSetup();
56
+ }
57
+ }, [setupStep, startSetup]);
58
+
59
+ // Loading state
60
+ if (isLoading && !setupData) {
61
+ return <SetupLoading />;
62
+ }
63
+
64
+ // Complete - show backup codes
65
+ if (setupStep === 'complete' && backupCodes) {
66
+ return (
67
+ <SetupComplete
68
+ backupCodes={backupCodes}
69
+ backupCodesWarning={backupCodesWarning}
70
+ onDone={handleComplete}
71
+ />
72
+ );
73
+ }
74
+
75
+ // Setup - show QR code
76
+ if (setupData) {
77
+ return (
78
+ <SetupQRCode
79
+ provisioningUri={setupData.provisioningUri}
80
+ secret={setupData.secret}
81
+ isLoading={isLoading}
82
+ error={error}
83
+ onConfirm={confirmSetup}
84
+ onSkip={onSkip ? handleSkip : undefined}
85
+ />
86
+ );
87
+ }
88
+
89
+ // Fallback to loading
90
+ return <SetupLoading />;
91
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { Input } from '@djangocfg/ui-core/components';
6
+
7
+ import { AUTH } from '../../constants';
8
+ import { AUTH_CONTENT } from '../../content';
9
+ import { useAuthFormContext } from '../../context';
10
+ import {
11
+ AuthButton,
12
+ AuthContainer,
13
+ AuthError,
14
+ AuthHeader,
15
+ AuthLink,
16
+ AuthOTPInput,
17
+ } from '../shared';
18
+
19
+ /**
20
+ * TwoFactorStep - Apple-style 2FA verification
21
+ *
22
+ * Minimal design with:
23
+ * - TOTP code input (default)
24
+ * - Backup code input (alternate)
25
+ * - Clean toggle between modes
26
+ */
27
+ export const TwoFactorStep: React.FC = () => {
28
+ const {
29
+ twoFactorCode,
30
+ useBackupCode,
31
+ error,
32
+ is2FALoading,
33
+ twoFactorWarning,
34
+ setTwoFactorCode,
35
+ handle2FASubmit,
36
+ handleUseBackupCode,
37
+ handleUseTOTP,
38
+ } = useAuthFormContext();
39
+
40
+ const content = AUTH_CONTENT.twoFactor;
41
+ const subtitle = useBackupCode ? content.subtitle.backup : content.subtitle.totp;
42
+
43
+ return (
44
+ <AuthContainer step="2fa">
45
+ <AuthHeader title={content.title} subtitle={subtitle} />
46
+
47
+ <form onSubmit={handle2FASubmit} className="auth-form-group">
48
+ {useBackupCode ? (
49
+ <Input
50
+ type="text"
51
+ value={twoFactorCode}
52
+ onChange={(e) => setTwoFactorCode(e.target.value.toUpperCase())}
53
+ placeholder={content.placeholder.backup}
54
+ disabled={is2FALoading}
55
+ maxLength={AUTH.BACKUP_CODE_MAX_LENGTH}
56
+ autoComplete="off"
57
+ autoFocus
58
+ className="auth-input font-mono tracking-widest text-center"
59
+ />
60
+ ) : (
61
+ <AuthOTPInput
62
+ value={twoFactorCode}
63
+ onChange={setTwoFactorCode}
64
+ disabled={is2FALoading}
65
+ />
66
+ )}
67
+
68
+ {twoFactorWarning && (
69
+ <div className="auth-dev-notice">{twoFactorWarning}</div>
70
+ )}
71
+
72
+ <AuthError message={error} />
73
+
74
+ <AuthButton
75
+ loading={is2FALoading}
76
+ disabled={!useBackupCode && twoFactorCode.length !== AUTH.OTP_LENGTH}
77
+ >
78
+ {content.button}
79
+ </AuthButton>
80
+
81
+ <div className="auth-actions">
82
+ <AuthLink
83
+ onClick={useBackupCode ? handleUseTOTP : handleUseBackupCode}
84
+ disabled={is2FALoading}
85
+ >
86
+ {useBackupCode ? content.toggle.toTotp : content.toggle.toBackup}
87
+ </AuthLink>
88
+ </div>
89
+ </form>
90
+ </AuthContainer>
91
+ );
92
+ };
@@ -0,0 +1,6 @@
1
+ export { IdentifierStep } from './IdentifierStep';
2
+ export { OTPStep } from './OTPStep';
3
+ export { TwoFactorStep } from './TwoFactorStep';
4
+ export { SetupStep } from './SetupStep';
5
+
6
+ export type { SetupStepProps } from './SetupStep';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AuthLayout Constants
3
+ *
4
+ * All magic numbers extracted for clarity and maintainability.
5
+ */
6
+
7
+ export const AUTH = {
8
+ // Input lengths
9
+ OTP_LENGTH: 6,
10
+ BACKUP_CODE_MAX_LENGTH: 12,
11
+
12
+ // Timing (ms)
13
+ ANIMATION_DURATION: 400,
14
+ AUTO_SUBMIT_DELAY: 100,
15
+ COPY_FEEDBACK_DURATION: 2000,
16
+ REDIRECT_DELAY: 1500,
17
+ ANIMATION_START_DELAY: 50,
18
+
19
+ // Sizes (px)
20
+ QR_CODE_SIZE: 160,
21
+
22
+ // Routes
23
+ DEFAULT_REDIRECT: '/dashboard',
24
+ } as const;
@@ -0,0 +1,78 @@
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;
@@ -0,0 +1 @@
1
+ export { useCopyToClipboard } from './useCopyToClipboard';
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ import { AUTH } from '../constants';
6
+
7
+ interface UseCopyToClipboardResult {
8
+ copied: boolean;
9
+ copy: (text: string) => Promise<void>;
10
+ }
11
+
12
+ /**
13
+ * useCopyToClipboard - Copy text with temporary feedback state
14
+ *
15
+ * @param duration - How long to show "copied" state (default: 2000ms)
16
+ */
17
+ export const useCopyToClipboard = (
18
+ duration = AUTH.COPY_FEEDBACK_DURATION
19
+ ): UseCopyToClipboardResult => {
20
+ const [copied, setCopied] = useState(false);
21
+
22
+ const copy = useCallback(
23
+ async (text: string) => {
24
+ try {
25
+ await navigator.clipboard.writeText(text);
26
+ setCopied(true);
27
+ setTimeout(() => setCopied(false), duration);
28
+ } catch {
29
+ // Clipboard API not available
30
+ console.warn('Clipboard API not available');
31
+ }
32
+ },
33
+ [duration]
34
+ );
35
+
36
+ return { copied, copy };
37
+ };
@@ -9,16 +9,20 @@ export type { AuthLayoutProps } from './AuthLayout';
9
9
  // Context and hooks
10
10
  export { AuthFormProvider, useAuthFormContext } from './context';
11
11
 
12
- // Components
12
+ // Apple-style step components
13
13
  export {
14
- IdentifierForm,
15
- OTPForm,
16
- AuthHelp,
17
- OAuthProviders,
14
+ IdentifierStep,
15
+ OTPStep,
16
+ TwoFactorStep,
17
+ SetupStep,
18
+ type SetupStepProps,
18
19
  OAuthCallback,
19
20
  type OAuthCallbackProps,
20
21
  } from './components';
21
22
 
23
+ // Shared UI components
24
+ export * from './components/shared';
25
+
22
26
  // Types (re-exported from @djangocfg/api/auth)
23
27
  export type {
24
28
  AuthChannel,