@djangocfg/layouts 2.1.108 → 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.
Files changed (42) hide show
  1. package/README.md +16 -9
  2. package/package.json +15 -15
  3. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  4. package/src/layouts/AuthLayout/components/index.ts +11 -7
  5. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  6. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  7. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  8. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  13. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  14. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  15. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  16. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  17. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  18. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  24. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  25. package/src/layouts/AuthLayout/constants.ts +24 -0
  26. package/src/layouts/AuthLayout/content.ts +78 -0
  27. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  28. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  29. package/src/layouts/AuthLayout/index.ts +9 -5
  30. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  31. package/src/layouts/PrivateLayout/PrivateLayout.tsx +13 -1
  32. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +59 -46
  33. package/src/layouts/PrivateLayout/index.ts +1 -1
  34. package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
  35. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  36. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  37. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  38. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  39. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  40. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  41. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  42. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -18,7 +18,7 @@ import { cn } from '@djangocfg/ui-core/lib';
18
18
 
19
19
  import { LucideIcon } from '../../../components';
20
20
 
21
- import type { SidebarItem, SidebarConfig } from '../PrivateLayout';
21
+ import type { SidebarItem, SidebarConfig, SidebarGroupConfig } from '../PrivateLayout';
22
22
 
23
23
  interface PrivateSidebarProps {
24
24
  sidebar: SidebarConfig;
@@ -29,18 +29,71 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
29
29
  const { state, isMobile } = useSidebar();
30
30
  const homeHref = sidebar.homeHref || '/';
31
31
 
32
+ // Get all items for active detection
33
+ const allItems = React.useMemo(() => {
34
+ return sidebar.groups.flatMap((g) => g.items);
35
+ }, [sidebar.groups]);
36
+
32
37
  const isActive = (href: string) => {
33
38
  const matches = pathname === href || pathname.startsWith(href + '/');
34
39
  if (!matches) return false;
35
40
 
36
41
  // Check if there's a more specific (longer) path that also matches
37
- return !sidebar.items.some(otherItem =>
38
- otherItem.href !== href &&
39
- otherItem.href.startsWith(href + '/') &&
40
- (pathname === otherItem.href || pathname.startsWith(otherItem.href + '/'))
42
+ return !allItems.some(
43
+ (otherItem) =>
44
+ otherItem.href !== href &&
45
+ otherItem.href.startsWith(href + '/') &&
46
+ (pathname === otherItem.href ||
47
+ pathname.startsWith(otherItem.href + '/'))
48
+ );
49
+ };
50
+
51
+ // Render a single menu item
52
+ const renderMenuItem = (item: SidebarItem) => {
53
+ const active = isActive(item.href);
54
+
55
+ return (
56
+ <SidebarMenuItem key={item.href}>
57
+ <SidebarMenuButton
58
+ asChild
59
+ isActive={active}
60
+ tooltip={item.label}
61
+ size={isMobile ? 'lg' : 'default'}
62
+ >
63
+ <Link href={item.href}>
64
+ {item.icon && (
65
+ <LucideIcon
66
+ icon={typeof item.icon === 'string' ? item.icon : item.icon}
67
+ className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
68
+ />
69
+ )}
70
+ <span className={isMobile ? 'text-base' : ''}>{item.label}</span>
71
+ {item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
72
+ </Link>
73
+ </SidebarMenuButton>
74
+ </SidebarMenuItem>
41
75
  );
42
76
  };
43
77
 
78
+ // Render groups
79
+ const renderContent = () => {
80
+ return sidebar.groups.map((group) => {
81
+ // Skip dynamic groups with no items
82
+ if (group.dynamic && group.items.length === 0) {
83
+ return null;
84
+ }
85
+
86
+ return (
87
+ <SidebarGroup key={group.label}>
88
+ <SidebarGroupLabel className="font-medium text-[10px]">{group.label}</SidebarGroupLabel>
89
+ <SidebarGroupContent>
90
+ <SidebarMenu>{group.items.map(renderMenuItem)}</SidebarMenu>
91
+ </SidebarGroupContent>
92
+ </SidebarGroup>
93
+ );
94
+ });
95
+ };
96
+
44
97
  return (
45
98
  <Sidebar collapsible="icon">
46
99
  <SidebarHeader>
@@ -88,47 +141,7 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
88
141
  </div>
89
142
  </SidebarHeader>
90
143
 
91
- <SidebarContent>
92
- <SidebarGroup>
93
- <SidebarGroupContent>
94
- <SidebarMenu>
95
- {sidebar.items.map((item) => {
96
- const active = isActive(item.href);
97
-
98
- return (
99
- <SidebarMenuItem key={item.href}>
100
- <SidebarMenuButton
101
- asChild
102
- isActive={active}
103
- tooltip={item.label}
104
- size={isMobile ? 'lg' : 'default'}
105
- >
106
- <Link href={item.href}>
107
- {item.icon && (
108
- <LucideIcon
109
- icon={
110
- typeof item.icon === 'string'
111
- ? item.icon
112
- : item.icon
113
- }
114
- className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
115
- />
116
- )}
117
- <span className={isMobile ? 'text-base' : ''}>
118
- {item.label}
119
- </span>
120
- {item.badge && (
121
- <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
122
- )}
123
- </Link>
124
- </SidebarMenuButton>
125
- </SidebarMenuItem>
126
- );
127
- })}
128
- </SidebarMenu>
129
- </SidebarGroupContent>
130
- </SidebarGroup>
131
- </SidebarContent>
144
+ <SidebarContent>{renderContent()}</SidebarContent>
132
145
  </Sidebar>
133
146
  );
134
147
  }
@@ -3,5 +3,5 @@
3
3
  */
4
4
 
5
5
  export { PrivateLayout } from './PrivateLayout';
6
- export type { PrivateLayoutProps, SidebarItem, SidebarConfig, HeaderConfig } from './PrivateLayout';
6
+ export type { PrivateLayoutProps, SidebarItem, SidebarGroupConfig, SidebarConfig, HeaderConfig } from './PrivateLayout';
7
7
 
@@ -28,7 +28,7 @@ import {
28
28
  } from '@djangocfg/ui-core/components';
29
29
  import { zodResolver } from '@hookform/resolvers/zod';
30
30
 
31
- import { TwoFactorSetup } from '../AuthLayout/components/TwoFactorSetup';
31
+ import { SetupStep } from '../AuthLayout/components/steps/SetupStep';
32
32
  import { profileLogger } from '../../utils/logger';
33
33
 
34
34
  // ─────────────────────────────────────────────────────────────────────────────
@@ -314,7 +314,7 @@ const ProfileContent = ({
314
314
  if (show2FASetup) {
315
315
  return (
316
316
  <div className="container mx-auto px-4 py-8 max-w-lg">
317
- <TwoFactorSetup
317
+ <SetupStep
318
318
  onComplete={() => {
319
319
  setShow2FASetup(false);
320
320
  fetch2FAStatus();
@@ -22,7 +22,7 @@ import {
22
22
  OTPInput,
23
23
  } from '@djangocfg/ui-core/components';
24
24
 
25
- import { TwoFactorSetup } from '../../AuthLayout/components/TwoFactorSetup';
25
+ import { SetupStep } from '../../AuthLayout/components/steps/SetupStep';
26
26
 
27
27
  type ViewState = 'status' | 'setup' | 'disable';
28
28
 
@@ -102,7 +102,7 @@ export const TwoFactorSection: React.FC = () => {
102
102
  </CardDescription>
103
103
  </CardHeader>
104
104
  <CardContent>
105
- <TwoFactorSetup
105
+ <SetupStep
106
106
  onComplete={handleSetupComplete}
107
107
  onSkip={handleSetupSkip}
108
108
  />
@@ -1,114 +0,0 @@
1
- import { HelpCircle, Mail, MessageCircle } from 'lucide-react';
2
- import React from 'react';
3
-
4
- import { Button } from '@djangocfg/ui-core/components';
5
-
6
- import { useAuthFormContext } from '../context';
7
-
8
- import type { AuthHelpProps } from '../types';
9
-
10
- export const AuthHelp: React.FC<AuthHelpProps> = ({
11
- className = '',
12
- variant = 'default',
13
- }) => {
14
- const { supportUrl, channel } = useAuthFormContext();
15
-
16
- const getChannelIcon = () => {
17
- return channel === 'phone' ? (
18
- <MessageCircle className="w-4 h-4 text-muted-foreground" />
19
- ) : (
20
- <Mail className="w-4 h-4 text-muted-foreground" />
21
- );
22
- };
23
-
24
- const getHelpText = () => {
25
- return channel === 'phone' ? 'Check WhatsApp/SMS' : 'Check spam folder';
26
- };
27
-
28
- const getDetailedHelp = () => {
29
- if (channel === 'phone') {
30
- return {
31
- title: "Didn't receive the code?",
32
- tips: [
33
- '• Check your WhatsApp messages',
34
- '• Look for SMS messages',
35
- '• Ensure you have signal/internet',
36
- '• Wait a few minutes for delivery',
37
- ],
38
- };
39
- } else {
40
- return {
41
- title: "Didn't receive the email?",
42
- tips: [
43
- '• Check your spam or junk folder',
44
- '• Make sure you entered the correct email address',
45
- '• Wait a few minutes for the email to arrive',
46
- ],
47
- };
48
- }
49
- };
50
-
51
- if (variant === 'compact') {
52
- return (
53
- <div
54
- className={`flex items-center justify-between p-3 bg-muted/30 rounded-sm border border-border ${className}`}
55
- >
56
- <div className="flex items-center gap-2">
57
- {getChannelIcon()}
58
- <span className="text-sm text-muted-foreground">{getHelpText()}</span>
59
- </div>
60
- {supportUrl && (
61
- <Button
62
- asChild
63
- variant="ghost"
64
- size="sm"
65
- className="text-xs"
66
- >
67
- <a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
68
- <HelpCircle className="w-3 h-3" />
69
- Need help?
70
- </a>
71
- </Button>
72
- )}
73
- </div>
74
- );
75
- }
76
-
77
- const helpData = getDetailedHelp();
78
-
79
- return (
80
- <div
81
- className={`flex flex-col gap-3 p-3 bg-muted/30 rounded-sm border border-border ${className}`}
82
- >
83
- <div className="flex items-start gap-3">
84
- {getChannelIcon()}
85
- <div className="flex flex-col gap-1">
86
- <h4 className="text-sm font-medium text-foreground">{helpData.title}</h4>
87
- <div className="flex flex-col gap-0.5 text-xs text-muted-foreground">
88
- {helpData.tips.map((tip, index) => (
89
- <p key={index}>{tip}</p>
90
- ))}
91
- </div>
92
- </div>
93
- </div>
94
-
95
- {supportUrl && (
96
- <div className="flex items-center justify-between pt-2 border-t border-border">
97
- <span className="text-xs text-muted-foreground">Still having trouble?</span>
98
- <Button
99
- asChild
100
- variant="ghost"
101
- size="sm"
102
- className="text-xs h-7 px-2"
103
- >
104
- <a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
105
- <HelpCircle className="w-3 h-3" />
106
- Get Help
107
- </a>
108
- </Button>
109
- </div>
110
- )}
111
- </div>
112
- );
113
- };
114
-
@@ -1,101 +0,0 @@
1
- /**
2
- * Auth Success Component
3
- *
4
- * Full-screen success layout shown after successful authentication.
5
- * Displays a centered logo with a subtle animation, then redirects.
6
- */
7
-
8
- 'use client';
9
-
10
- import React, { useEffect, useState } from 'react';
11
-
12
- import { useCfgRouter } from '@djangocfg/api/auth';
13
-
14
- import { useAuthFormContext } from '../context';
15
-
16
- export interface AuthSuccessProps {
17
- className?: string;
18
- /** Delay before redirect in ms (default: 1500) */
19
- redirectDelay?: number;
20
- }
21
-
22
- export const AuthSuccess: React.FC<AuthSuccessProps> = ({ className, redirectDelay = 1500 }) => {
23
- const { logoUrl, redirectUrl } = useAuthFormContext();
24
- const router = useCfgRouter();
25
- const [isVisible, setIsVisible] = useState(false);
26
-
27
- useEffect(() => {
28
- // Trigger animation after mount
29
- const animTimer = setTimeout(() => setIsVisible(true), 50);
30
-
31
- // Redirect after delay
32
- const redirectTimer = setTimeout(() => {
33
- const finalUrl = redirectUrl || '/dashboard';
34
- router.hardPush(finalUrl);
35
- }, redirectDelay);
36
-
37
- return () => {
38
- clearTimeout(animTimer);
39
- clearTimeout(redirectTimer);
40
- };
41
- }, [redirectUrl, redirectDelay, router]);
42
-
43
- if (!logoUrl) {
44
- // Fallback: simple checkmark if no logo provided
45
- return (
46
- <div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
47
- <div
48
- className={`transition-all duration-700 ease-out ${
49
- isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
50
- }`}
51
- >
52
- <div className="w-24 h-24 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
53
- <svg
54
- className="w-12 h-12 text-green-600 dark:text-green-400"
55
- fill="none"
56
- stroke="currentColor"
57
- viewBox="0 0 24 24"
58
- >
59
- <path
60
- strokeLinecap="round"
61
- strokeLinejoin="round"
62
- strokeWidth={2}
63
- d="M5 13l4 4L19 7"
64
- />
65
- </svg>
66
- </div>
67
- </div>
68
- </div>
69
- );
70
- }
71
-
72
- return (
73
- <div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
74
- <div
75
- className={`transition-all duration-700 ease-out ${
76
- isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-90'
77
- }`}
78
- >
79
- {/* Logo container with max size and animation */}
80
- <div className="relative">
81
- {/* Subtle glow effect */}
82
- <div
83
- className={`absolute inset-0 blur-3xl transition-opacity duration-1000 ${
84
- isVisible ? 'opacity-20' : 'opacity-0'
85
- }`}
86
- style={{
87
- background: 'radial-gradient(circle, currentColor 0%, transparent 70%)',
88
- }}
89
- />
90
-
91
- {/* Logo image */}
92
- <img
93
- src={logoUrl}
94
- alt="Success"
95
- className="relative w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 object-contain"
96
- />
97
- </div>
98
- </div>
99
- </div>
100
- );
101
- };