@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.
Files changed (28) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/AppLayout.tsx +29 -27
  3. package/src/layouts/AppLayout/BaseApp.tsx +36 -38
  4. package/src/layouts/PublicLayout/PublicLayout.tsx +9 -43
  5. package/src/layouts/PublicLayout/components/PublicFooter/DjangoCFGLogo.tsx +45 -0
  6. package/src/layouts/PublicLayout/components/PublicFooter/FooterBottom.tsx +114 -0
  7. package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +53 -0
  8. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +77 -0
  9. package/src/layouts/PublicLayout/components/PublicFooter/FooterSocialLinks.tsx +82 -0
  10. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +129 -0
  11. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +17 -0
  12. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +57 -0
  13. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +3 -6
  14. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +3 -6
  15. package/src/layouts/PublicLayout/index.ts +12 -1
  16. package/src/layouts/_components/UserMenu.tsx +160 -38
  17. package/src/layouts/index.ts +4 -1
  18. package/src/layouts/shared/README.md +86 -0
  19. package/src/layouts/shared/index.ts +21 -0
  20. package/src/layouts/shared/types.ts +215 -0
  21. package/src/snippets/McpChat/components/AIChatWidget.tsx +150 -53
  22. package/src/snippets/McpChat/components/AskAIButton.tsx +2 -5
  23. package/src/snippets/McpChat/components/ChatMessages.tsx +40 -10
  24. package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
  25. package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
  26. package/src/snippets/McpChat/components/MessageBubble.tsx +46 -34
  27. package/src/snippets/McpChat/context/AIChatContext.tsx +23 -6
  28. 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 '../PublicLayout';
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 '../PublicLayout';
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, NavigationItem, FooterConfig } from './PublicLayout';
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
- * Simplified user menu component that works without AppContext
5
- * Uses only useAuth() hook
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 { User, LogOut, Settings } from 'lucide-react';
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
- {profilePath && (
96
- <Link
97
- href={profilePath}
98
- className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-foreground hover:bg-accent hover:text-accent-foreground rounded-sm transition-colors"
99
- >
100
- <Settings className="h-4 w-4" />
101
- Profile
102
- </Link>
103
- )}
104
- <button
105
- onClick={() => logout()}
106
- className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-sm transition-colors w-full text-left"
107
- >
108
- <LogOut className="h-4 w-4" />
109
- Sign Out
110
- </button>
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
- <DropdownMenuGroup>
139
- {profilePath && (
140
- <DropdownMenuItem asChild>
141
- <Link href={profilePath} className="flex items-center">
142
- <Settings className="mr-2 h-4 w-4" />
143
- <span>Profile</span>
144
- </Link>
145
- </DropdownMenuItem>
146
- )}
147
- </DropdownMenuGroup>
148
- <DropdownMenuSeparator />
149
- <DropdownMenuItem
150
- onClick={() => logout()}
151
- className="text-destructive focus:text-destructive"
152
- >
153
- <LogOut className="mr-2 h-4 w-4" />
154
- <span>Sign Out</span>
155
- </DropdownMenuItem>
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
  );
@@ -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