@djangocfg/layouts 2.1.20 → 2.1.22
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 +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +29 -27
- package/src/layouts/AppLayout/BaseApp.tsx +36 -38
- package/src/layouts/PublicLayout/PublicLayout.tsx +9 -43
- package/src/layouts/PublicLayout/components/PublicFooter/DjangoCFGLogo.tsx +45 -0
- package/src/layouts/PublicLayout/components/PublicFooter/FooterBottom.tsx +114 -0
- package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +53 -0
- package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +77 -0
- package/src/layouts/PublicLayout/components/PublicFooter/FooterSocialLinks.tsx +82 -0
- package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +129 -0
- package/src/layouts/PublicLayout/components/PublicFooter/index.ts +17 -0
- package/src/layouts/PublicLayout/components/PublicFooter/types.ts +57 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +3 -6
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +3 -6
- package/src/layouts/PublicLayout/index.ts +12 -1
- package/src/layouts/_components/UserMenu.tsx +160 -38
- package/src/layouts/index.ts +4 -1
- package/src/layouts/shared/README.md +86 -0
- package/src/layouts/shared/index.ts +21 -0
- package/src/layouts/shared/types.ts +215 -0
- package/src/snippets/McpChat/components/AIChatWidget.tsx +150 -53
- package/src/snippets/McpChat/components/AskAIButton.tsx +2 -5
- package/src/snippets/McpChat/components/ChatMessages.tsx +40 -10
- package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
- package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
- package/src/snippets/McpChat/components/MessageBubble.tsx +46 -34
- package/src/snippets/McpChat/context/AIChatContext.tsx +23 -6
- package/src/layouts/PublicLayout/components/PublicFooter.tsx +0 -190
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer Social Links Component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Github,
|
|
10
|
+
Linkedin,
|
|
11
|
+
Twitter,
|
|
12
|
+
MessageCircle,
|
|
13
|
+
Youtube,
|
|
14
|
+
Facebook,
|
|
15
|
+
Instagram,
|
|
16
|
+
Mail,
|
|
17
|
+
MessageSquare,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import type { FooterSocialLinks } from './types';
|
|
20
|
+
|
|
21
|
+
export interface FooterSocialLinksProps {
|
|
22
|
+
socialLinks?: FooterSocialLinks;
|
|
23
|
+
className?: string;
|
|
24
|
+
iconClassName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const socialIconsMap = {
|
|
28
|
+
github: { icon: Github, title: 'GitHub' },
|
|
29
|
+
linkedin: { icon: Linkedin, title: 'LinkedIn' },
|
|
30
|
+
twitter: { icon: Twitter, title: 'Twitter' },
|
|
31
|
+
telegram: { icon: MessageCircle, title: 'Telegram' },
|
|
32
|
+
youtube: { icon: Youtube, title: 'YouTube' },
|
|
33
|
+
facebook: { icon: Facebook, title: 'Facebook' },
|
|
34
|
+
instagram: { icon: Instagram, title: 'Instagram' },
|
|
35
|
+
whatsapp: { icon: MessageSquare, title: 'WhatsApp' },
|
|
36
|
+
email: { icon: Mail, title: 'Email' },
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export function FooterSocialLinksComponent({
|
|
40
|
+
socialLinks,
|
|
41
|
+
className = 'flex space-x-4',
|
|
42
|
+
iconClassName = 'w-5 h-5',
|
|
43
|
+
}: FooterSocialLinksProps) {
|
|
44
|
+
// Prepare social links data BEFORE render
|
|
45
|
+
const socialLinksData = socialLinks
|
|
46
|
+
? Object.entries(socialLinks)
|
|
47
|
+
.filter(([_, url]) => url)
|
|
48
|
+
.map(([platform, url]) => {
|
|
49
|
+
const social = socialIconsMap[platform as keyof typeof socialIconsMap];
|
|
50
|
+
if (!social) return null;
|
|
51
|
+
return {
|
|
52
|
+
platform,
|
|
53
|
+
url: url!,
|
|
54
|
+
icon: social.icon,
|
|
55
|
+
title: social.title,
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
.filter((item): item is NonNullable<typeof item> => item !== null)
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
if (socialLinksData.length === 0) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={className}>
|
|
65
|
+
{socialLinksData.map((social) => {
|
|
66
|
+
const Icon = social.icon;
|
|
67
|
+
return (
|
|
68
|
+
<a
|
|
69
|
+
key={social.platform}
|
|
70
|
+
href={social.url}
|
|
71
|
+
target="_blank"
|
|
72
|
+
rel="noopener noreferrer"
|
|
73
|
+
className="text-muted-foreground hover:text-primary transition-colors"
|
|
74
|
+
title={social.title}
|
|
75
|
+
>
|
|
76
|
+
<Icon className={iconClassName} />
|
|
77
|
+
</a>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Layout Footer
|
|
3
|
+
*
|
|
4
|
+
* Professional, flexible footer component for PublicLayout
|
|
5
|
+
* Supports desktop/mobile responsive layouts, social links, menu sections
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
|
|
13
|
+
import { FooterProjectInfo } from './FooterProjectInfo';
|
|
14
|
+
import { FooterMenuSections } from './FooterMenuSections';
|
|
15
|
+
import { FooterBottom } from './FooterBottom';
|
|
16
|
+
import type { PublicFooterProps } from './types';
|
|
17
|
+
|
|
18
|
+
export function PublicFooter({
|
|
19
|
+
siteName,
|
|
20
|
+
description,
|
|
21
|
+
logo,
|
|
22
|
+
badge,
|
|
23
|
+
socialLinks,
|
|
24
|
+
links = [],
|
|
25
|
+
menuSections = [],
|
|
26
|
+
copyright: copyrightProp,
|
|
27
|
+
credits: creditsProp,
|
|
28
|
+
variant = 'full',
|
|
29
|
+
}: PublicFooterProps) {
|
|
30
|
+
const isMobile = useIsMobile();
|
|
31
|
+
|
|
32
|
+
// Prepare data BEFORE render
|
|
33
|
+
const currentYear = new Date().getFullYear();
|
|
34
|
+
const copyright = copyrightProp || `© ${currentYear} ${siteName}. All rights reserved.`;
|
|
35
|
+
const credits = creditsProp || {
|
|
36
|
+
text: 'Built with DjangoCFG',
|
|
37
|
+
url: 'https://djangocfg.com',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Simple variant - minimal footer
|
|
41
|
+
if (variant === 'simple') {
|
|
42
|
+
return (
|
|
43
|
+
<footer className="bg-background border-t border-border mt-auto">
|
|
44
|
+
<div className="w-full px-4 py-4">
|
|
45
|
+
<div className="text-center">
|
|
46
|
+
<div className="text-sm text-muted-foreground">{copyright}</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</footer>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Mobile Footer
|
|
54
|
+
if (isMobile) {
|
|
55
|
+
return (
|
|
56
|
+
<footer className="lg:hidden bg-background border-t border-border mt-auto">
|
|
57
|
+
<div className="w-full px-4 py-8">
|
|
58
|
+
<FooterProjectInfo
|
|
59
|
+
siteName={siteName}
|
|
60
|
+
description={description}
|
|
61
|
+
logo={logo}
|
|
62
|
+
socialLinks={socialLinks}
|
|
63
|
+
variant="mobile"
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
{/* Quick Links */}
|
|
67
|
+
{links.length > 0 && (
|
|
68
|
+
<div className="flex flex-wrap justify-center gap-3 mb-6">
|
|
69
|
+
{links.map((link) =>
|
|
70
|
+
link.external ? (
|
|
71
|
+
<a
|
|
72
|
+
key={link.path}
|
|
73
|
+
href={link.path}
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
|
77
|
+
>
|
|
78
|
+
{link.label}
|
|
79
|
+
</a>
|
|
80
|
+
) : (
|
|
81
|
+
<Link
|
|
82
|
+
key={link.path}
|
|
83
|
+
href={link.path}
|
|
84
|
+
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
|
85
|
+
>
|
|
86
|
+
{link.label}
|
|
87
|
+
</Link>
|
|
88
|
+
)
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<FooterBottom
|
|
94
|
+
copyright={copyright}
|
|
95
|
+
credits={credits}
|
|
96
|
+
variant="mobile"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</footer>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Desktop Footer
|
|
104
|
+
return (
|
|
105
|
+
<footer className="max-lg:hidden bg-background border-t border-border mt-auto">
|
|
106
|
+
<div className="w-full px-4 sm:px-6 lg:px-8 py-12">
|
|
107
|
+
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12">
|
|
108
|
+
<FooterProjectInfo
|
|
109
|
+
siteName={siteName}
|
|
110
|
+
description={description}
|
|
111
|
+
logo={logo}
|
|
112
|
+
badge={badge}
|
|
113
|
+
socialLinks={socialLinks}
|
|
114
|
+
variant="desktop"
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<FooterMenuSections menuSections={menuSections} />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<FooterBottom
|
|
121
|
+
copyright={copyright}
|
|
122
|
+
credits={credits}
|
|
123
|
+
links={links}
|
|
124
|
+
variant="desktop"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</footer>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Footer Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { PublicFooter } from './PublicFooter';
|
|
6
|
+
export { FooterProjectInfo } from './FooterProjectInfo';
|
|
7
|
+
export { FooterMenuSections } from './FooterMenuSections';
|
|
8
|
+
export { FooterBottom } from './FooterBottom';
|
|
9
|
+
export { FooterSocialLinksComponent } from './FooterSocialLinks';
|
|
10
|
+
export { DjangoCFGLogo } from './DjangoCFGLogo';
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
PublicFooterProps,
|
|
14
|
+
FooterLink,
|
|
15
|
+
FooterMenuSection,
|
|
16
|
+
FooterSocialLinks,
|
|
17
|
+
} from './types';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Footer Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export interface FooterLink {
|
|
8
|
+
label: string;
|
|
9
|
+
path: string;
|
|
10
|
+
external?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FooterMenuSection {
|
|
14
|
+
title: string;
|
|
15
|
+
items: FooterLink[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FooterSocialLinks {
|
|
19
|
+
github?: string;
|
|
20
|
+
linkedin?: string;
|
|
21
|
+
twitter?: string;
|
|
22
|
+
telegram?: string;
|
|
23
|
+
youtube?: string;
|
|
24
|
+
facebook?: string;
|
|
25
|
+
instagram?: string;
|
|
26
|
+
whatsapp?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PublicFooterProps {
|
|
31
|
+
/** Project name */
|
|
32
|
+
siteName: string;
|
|
33
|
+
/** Project description */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** Logo path or URL */
|
|
36
|
+
logo?: string;
|
|
37
|
+
/** Optional badge */
|
|
38
|
+
badge?: {
|
|
39
|
+
icon: LucideIcon;
|
|
40
|
+
text: string;
|
|
41
|
+
};
|
|
42
|
+
/** Social media links */
|
|
43
|
+
socialLinks?: FooterSocialLinks;
|
|
44
|
+
/** Quick links (bottom bar) */
|
|
45
|
+
links?: FooterLink[];
|
|
46
|
+
/** Footer menu sections (desktop grid) */
|
|
47
|
+
menuSections?: FooterMenuSection[];
|
|
48
|
+
/** Copyright text (auto-generated if not provided) */
|
|
49
|
+
copyright?: string;
|
|
50
|
+
/** Credits */
|
|
51
|
+
credits?: {
|
|
52
|
+
text: string;
|
|
53
|
+
url?: string;
|
|
54
|
+
};
|
|
55
|
+
/** Variant: full (with all sections) or simple (minimal) */
|
|
56
|
+
variant?: 'full' | 'simple';
|
|
57
|
+
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
|
|
21
21
|
import { useAuth } from '@djangocfg/api/auth';
|
|
22
22
|
import { UserMenu } from '../../_components/UserMenu';
|
|
23
|
-
import type { NavigationItem } from '
|
|
23
|
+
import type { NavigationItem, UserMenuConfig } from '../../shared/types';
|
|
24
24
|
|
|
25
25
|
interface PublicMobileDrawerProps {
|
|
26
26
|
isOpen: boolean;
|
|
@@ -28,11 +28,7 @@ interface PublicMobileDrawerProps {
|
|
|
28
28
|
logo?: string;
|
|
29
29
|
siteName: string;
|
|
30
30
|
navigation: NavigationItem[];
|
|
31
|
-
userMenu?:
|
|
32
|
-
profilePath?: string;
|
|
33
|
-
dashboardPath?: string;
|
|
34
|
-
authPath?: string;
|
|
35
|
-
};
|
|
31
|
+
userMenu?: UserMenuConfig;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
export function PublicMobileDrawer({
|
|
@@ -83,6 +79,7 @@ export function PublicMobileDrawer({
|
|
|
83
79
|
{/* User Menu */}
|
|
84
80
|
<UserMenu
|
|
85
81
|
variant="mobile"
|
|
82
|
+
groups={userMenu?.groups}
|
|
86
83
|
profilePath={userMenu?.profilePath}
|
|
87
84
|
dashboardPath={userMenu?.dashboardPath}
|
|
88
85
|
authPath={userMenu?.authPath}
|
|
@@ -15,17 +15,13 @@ import { cn } from '@djangocfg/ui-nextjs/lib';
|
|
|
15
15
|
import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
|
|
16
16
|
import { useAuth } from '@djangocfg/api/auth';
|
|
17
17
|
import { UserMenu } from '../../_components/UserMenu';
|
|
18
|
-
import type { NavigationItem } from '
|
|
18
|
+
import type { NavigationItem, UserMenuConfig } from '../../shared/types';
|
|
19
19
|
|
|
20
20
|
interface PublicNavigationProps {
|
|
21
21
|
logo?: string;
|
|
22
22
|
siteName: string;
|
|
23
23
|
navigation: NavigationItem[];
|
|
24
|
-
userMenu?:
|
|
25
|
-
profilePath?: string;
|
|
26
|
-
dashboardPath?: string;
|
|
27
|
-
authPath?: string;
|
|
28
|
-
};
|
|
24
|
+
userMenu?: UserMenuConfig;
|
|
29
25
|
onMobileMenuClick: () => void;
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -74,6 +70,7 @@ export function PublicNavigation({
|
|
|
74
70
|
{/* User Menu */}
|
|
75
71
|
<UserMenu
|
|
76
72
|
variant="desktop"
|
|
73
|
+
groups={userMenu?.groups}
|
|
77
74
|
profilePath={userMenu?.profilePath}
|
|
78
75
|
dashboardPath={userMenu?.dashboardPath}
|
|
79
76
|
authPath={userMenu?.authPath}
|
|
@@ -3,5 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { PublicLayout } from './PublicLayout';
|
|
6
|
-
export type { PublicLayoutProps
|
|
6
|
+
export type { PublicLayoutProps } from './PublicLayout';
|
|
7
|
+
export {
|
|
8
|
+
PublicFooter,
|
|
9
|
+
FooterProjectInfo,
|
|
10
|
+
FooterMenuSections,
|
|
11
|
+
FooterBottom,
|
|
12
|
+
FooterSocialLinksComponent,
|
|
13
|
+
DjangoCFGLogo,
|
|
14
|
+
} from './components/PublicFooter';
|
|
15
|
+
export type {
|
|
16
|
+
PublicFooterProps,
|
|
17
|
+
} from './components/PublicFooter';
|
|
7
18
|
|
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Menu Component for Layouts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Uses
|
|
4
|
+
* Flexible user menu component with group-based structure (like footer)
|
|
5
|
+
* Uses useAuth() hook for authentication state
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Settings, LogOut, User } from 'lucide-react';
|
|
10
|
+
*
|
|
11
|
+
* <UserMenu
|
|
12
|
+
* groups={[
|
|
13
|
+
* {
|
|
14
|
+
* title: 'Account',
|
|
15
|
+
* items: [
|
|
16
|
+
* { label: 'Profile', href: '/profile', icon: Settings },
|
|
17
|
+
* { label: 'Dashboard', href: '/dashboard', icon: User }
|
|
18
|
+
* ]
|
|
19
|
+
* },
|
|
20
|
+
* {
|
|
21
|
+
* items: [
|
|
22
|
+
* { label: 'Sign Out', onClick: () => logout(), icon: LogOut, variant: 'destructive' }
|
|
23
|
+
* ]
|
|
24
|
+
* }
|
|
25
|
+
* ]}
|
|
26
|
+
* authPath="/auth"
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
6
29
|
*/
|
|
7
30
|
|
|
8
31
|
'use client';
|
|
9
32
|
|
|
10
33
|
import React from 'react';
|
|
11
34
|
import Link from 'next/link';
|
|
12
|
-
import {
|
|
35
|
+
import { LogOut, Settings } from 'lucide-react';
|
|
13
36
|
import {
|
|
14
37
|
DropdownMenu,
|
|
15
38
|
DropdownMenuContent,
|
|
@@ -24,16 +47,24 @@ import {
|
|
|
24
47
|
Button,
|
|
25
48
|
} from '@djangocfg/ui-nextjs/components';
|
|
26
49
|
import { useAuth } from '@djangocfg/api/auth';
|
|
50
|
+
import type { UserMenuGroup } from '../shared/types';
|
|
27
51
|
|
|
28
52
|
export interface UserMenuProps {
|
|
53
|
+
/** Display variant */
|
|
29
54
|
variant?: 'desktop' | 'mobile';
|
|
55
|
+
/** Menu groups for authenticated users */
|
|
56
|
+
groups?: UserMenuGroup[];
|
|
57
|
+
/** Auth page path (for sign in button) */
|
|
58
|
+
authPath?: string;
|
|
59
|
+
/** @deprecated Use groups instead - Profile page path (backward compatibility) */
|
|
30
60
|
profilePath?: string;
|
|
61
|
+
/** @deprecated Use groups instead - Dashboard page path (backward compatibility) */
|
|
31
62
|
dashboardPath?: string;
|
|
32
|
-
authPath?: string;
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
export function UserMenu({
|
|
36
66
|
variant = 'desktop',
|
|
67
|
+
groups,
|
|
37
68
|
profilePath = '/private/profile',
|
|
38
69
|
dashboardPath = '/private',
|
|
39
70
|
authPath = '/auth',
|
|
@@ -45,6 +76,42 @@ export function UserMenu({
|
|
|
45
76
|
setMounted(true);
|
|
46
77
|
}, []);
|
|
47
78
|
|
|
79
|
+
// Prepare menu groups (new groups prop or fallback to legacy props)
|
|
80
|
+
// Must be before early return to maintain hook order
|
|
81
|
+
const menuGroups: UserMenuGroup[] = React.useMemo(() => {
|
|
82
|
+
if (groups && groups.length > 0) {
|
|
83
|
+
return groups;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback to legacy behavior for backward compatibility
|
|
87
|
+
const legacyGroups: UserMenuGroup[] = [];
|
|
88
|
+
|
|
89
|
+
if (profilePath) {
|
|
90
|
+
legacyGroups.push({
|
|
91
|
+
items: [
|
|
92
|
+
{
|
|
93
|
+
label: 'Profile',
|
|
94
|
+
href: profilePath,
|
|
95
|
+
icon: Settings,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
legacyGroups.push({
|
|
102
|
+
items: [
|
|
103
|
+
{
|
|
104
|
+
label: 'Sign Out',
|
|
105
|
+
onClick: () => logout(),
|
|
106
|
+
icon: LogOut,
|
|
107
|
+
variant: 'destructive',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return legacyGroups;
|
|
113
|
+
}, [groups, profilePath, logout]);
|
|
114
|
+
|
|
48
115
|
if (!mounted) {
|
|
49
116
|
return null;
|
|
50
117
|
}
|
|
@@ -92,22 +159,50 @@ export function UserMenu({
|
|
|
92
159
|
</div>
|
|
93
160
|
</div>
|
|
94
161
|
<div className="space-y-1">
|
|
95
|
-
{
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
{menuGroups.map((group, groupIndex) => (
|
|
163
|
+
<div key={groupIndex}>
|
|
164
|
+
{group.title && (
|
|
165
|
+
<div className="px-4 py-2">
|
|
166
|
+
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
167
|
+
{group.title}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{group.items.map((item, itemIndex) => {
|
|
172
|
+
const Icon = item.icon;
|
|
173
|
+
const isDestructive = item.variant === 'destructive';
|
|
174
|
+
const baseClasses = `flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-sm transition-colors w-full text-left ${
|
|
175
|
+
isDestructive
|
|
176
|
+
? 'text-destructive hover:bg-destructive/10'
|
|
177
|
+
: 'text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
178
|
+
}`;
|
|
179
|
+
|
|
180
|
+
if (item.onClick) {
|
|
181
|
+
return (
|
|
182
|
+
<button
|
|
183
|
+
key={itemIndex}
|
|
184
|
+
onClick={item.onClick}
|
|
185
|
+
className={baseClasses}
|
|
186
|
+
>
|
|
187
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
188
|
+
{item.label}
|
|
189
|
+
</button>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (item.href) {
|
|
194
|
+
return (
|
|
195
|
+
<Link key={itemIndex} href={item.href} className={baseClasses}>
|
|
196
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
197
|
+
{item.label}
|
|
198
|
+
</Link>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
})}
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
111
206
|
</div>
|
|
112
207
|
</div>
|
|
113
208
|
);
|
|
@@ -135,24 +230,51 @@ export function UserMenu({
|
|
|
135
230
|
</div>
|
|
136
231
|
</DropdownMenuLabel>
|
|
137
232
|
<DropdownMenuSeparator />
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
233
|
+
{menuGroups.map((group, groupIndex) => (
|
|
234
|
+
<React.Fragment key={groupIndex}>
|
|
235
|
+
{groupIndex > 0 && <DropdownMenuSeparator />}
|
|
236
|
+
<DropdownMenuGroup>
|
|
237
|
+
{group.title && (
|
|
238
|
+
<DropdownMenuLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
239
|
+
{group.title}
|
|
240
|
+
</DropdownMenuLabel>
|
|
241
|
+
)}
|
|
242
|
+
{group.items.map((item, itemIndex) => {
|
|
243
|
+
const Icon = item.icon;
|
|
244
|
+
const isDestructive = item.variant === 'destructive';
|
|
245
|
+
|
|
246
|
+
if (item.onClick) {
|
|
247
|
+
return (
|
|
248
|
+
<DropdownMenuItem
|
|
249
|
+
key={itemIndex}
|
|
250
|
+
onClick={item.onClick}
|
|
251
|
+
className={isDestructive ? 'text-destructive focus:text-destructive' : ''}
|
|
252
|
+
>
|
|
253
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
254
|
+
<span>{item.label}</span>
|
|
255
|
+
</DropdownMenuItem>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (item.href) {
|
|
260
|
+
return (
|
|
261
|
+
<DropdownMenuItem key={itemIndex} asChild>
|
|
262
|
+
<Link
|
|
263
|
+
href={item.href}
|
|
264
|
+
className={`flex items-center ${isDestructive ? 'text-destructive' : ''}`}
|
|
265
|
+
>
|
|
266
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
267
|
+
<span>{item.label}</span>
|
|
268
|
+
</Link>
|
|
269
|
+
</DropdownMenuItem>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
})}
|
|
275
|
+
</DropdownMenuGroup>
|
|
276
|
+
</React.Fragment>
|
|
277
|
+
))}
|
|
156
278
|
</DropdownMenuContent>
|
|
157
279
|
</DropdownMenu>
|
|
158
280
|
);
|
package/src/layouts/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layouts exports
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Simple, straightforward layout components
|
|
5
5
|
* Import and use directly with props - no complex configs needed!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// Shared types (universal type system)
|
|
9
|
+
export * from './shared';
|
|
10
|
+
|
|
8
11
|
// Smart layout router
|
|
9
12
|
export * from './AppLayout';
|
|
10
13
|
|