@djangocfg/layouts 2.1.249 → 2.1.252
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/README.md +11 -9
- package/package.json +18 -18
- package/src/index.ts +4 -2
- package/src/layouts/AppLayout/AppLayout.tsx +70 -13
- package/src/layouts/AppLayout/index.ts +7 -1
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +22 -66
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +28 -11
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +159 -116
- package/src/layouts/PrivateLayout/components/index.ts +0 -2
- package/src/layouts/PublicLayout/PublicLayout.tsx +27 -42
- package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +13 -2
- package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +7 -5
- package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +130 -109
- package/src/layouts/PublicLayout/components/PublicFooter/types.ts +9 -4
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +32 -8
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +74 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +274 -113
- package/src/layouts/PublicLayout/components/index.ts +1 -0
- package/src/layouts/PublicLayout/context.tsx +0 -9
- package/src/layouts/PublicLayout/index.ts +8 -1
- package/src/layouts/_components/PrivateSidebarAccount.tsx +168 -0
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +0 -72
|
@@ -1,155 +1,198 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Private
|
|
3
|
-
*
|
|
4
|
-
* Sidebar navigation component for PrivateLayout
|
|
2
|
+
* Private sidebar: header (brand only when expanded; icon mode shows expand trigger only),
|
|
3
|
+
* nav groups, account footer. Nav: muted inactive rows, pill active; density scales with item count.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
'use client';
|
|
8
7
|
|
|
9
8
|
import Link from 'next/link';
|
|
10
|
-
import { usePathname } from 'next/navigation';
|
|
9
|
+
import { usePathname as useNextPathname } from 'next/navigation';
|
|
11
10
|
import React from 'react';
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Sidebar,
|
|
14
|
+
SidebarContent,
|
|
15
|
+
SidebarFooter,
|
|
16
|
+
SidebarGroup,
|
|
17
|
+
SidebarGroupContent,
|
|
18
|
+
SidebarGroupLabel,
|
|
19
|
+
SidebarHeader,
|
|
20
|
+
SidebarMenu,
|
|
21
|
+
SidebarMenuBadge,
|
|
22
|
+
SidebarMenuButton,
|
|
23
|
+
SidebarMenuItem,
|
|
24
|
+
SidebarTrigger,
|
|
25
|
+
useSidebar,
|
|
16
26
|
} from '@djangocfg/ui-nextjs/components';
|
|
17
27
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
28
|
|
|
29
|
+
import { PrivateSidebarAccount } from '../../_components/PrivateSidebarAccount';
|
|
19
30
|
import { LucideIcon } from '../../../components';
|
|
20
31
|
|
|
21
|
-
import type {
|
|
32
|
+
import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
|
|
33
|
+
import type { HeaderConfig, SidebarItem, SidebarConfig } from '../PrivateLayout';
|
|
34
|
+
|
|
35
|
+
type NavDensity = 'comfortable' | 'default' | 'compact';
|
|
36
|
+
|
|
37
|
+
function navDensityFromCount(n: number): NavDensity {
|
|
38
|
+
if (n <= 6) return 'comfortable';
|
|
39
|
+
if (n <= 14) return 'default';
|
|
40
|
+
return 'compact';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Nav rows: inactive muted. Light active = neutral pill (readable on white); dark = dark chip.
|
|
45
|
+
* Avoids `sidebar-accent` in light — project tokens often map to near-black and wash out labels.
|
|
46
|
+
*/
|
|
47
|
+
const navItemClass = cn(
|
|
48
|
+
'border-0 font-normal shadow-none transition-colors',
|
|
49
|
+
'text-muted-foreground',
|
|
50
|
+
'data-[active=true]:font-medium',
|
|
51
|
+
'data-[active=true]:bg-zinc-200/90 data-[active=true]:text-zinc-900',
|
|
52
|
+
'dark:data-[active=true]:bg-[#1a1a1a] dark:data-[active=true]:text-zinc-50',
|
|
53
|
+
'hover:bg-zinc-100/90 hover:text-foreground dark:hover:bg-white/[0.06]',
|
|
54
|
+
'data-[active=true]:hover:bg-zinc-200 dark:data-[active=true]:hover:bg-[#1a1a1a]',
|
|
55
|
+
'[&>svg]:shrink-0 [&>svg]:text-muted-foreground [&>svg]:opacity-85',
|
|
56
|
+
'data-[active=true]:[&>svg]:text-zinc-800 data-[active=true]:[&>svg]:opacity-100',
|
|
57
|
+
'dark:data-[active=true]:[&>svg]:text-zinc-50',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const DENSITY = {
|
|
61
|
+
comfortable: {
|
|
62
|
+
menu: 'gap-1.5',
|
|
63
|
+
group: 'gap-3',
|
|
64
|
+
label:
|
|
65
|
+
'h-7 uppercase text-[10px] font-light leading-none tracking-[0.14em] text-muted-foreground/40',
|
|
66
|
+
buttonSize: 'lg' as const,
|
|
67
|
+
iconClass: 'h-5 w-5',
|
|
68
|
+
extraButton: 'rounded-lg !px-3',
|
|
69
|
+
},
|
|
70
|
+
default: {
|
|
71
|
+
menu: 'gap-1',
|
|
72
|
+
group: 'gap-2',
|
|
73
|
+
label:
|
|
74
|
+
'uppercase text-[9px] font-light leading-none tracking-[0.12em] text-muted-foreground/50',
|
|
75
|
+
buttonSize: 'default' as const,
|
|
76
|
+
iconClass: 'h-4 w-4',
|
|
77
|
+
extraButton: 'rounded-lg',
|
|
78
|
+
},
|
|
79
|
+
compact: {
|
|
80
|
+
menu: 'gap-0.5',
|
|
81
|
+
group: 'gap-1',
|
|
82
|
+
label:
|
|
83
|
+
'h-6 uppercase text-[8px] font-light leading-none tracking-[0.1em] text-muted-foreground/40',
|
|
84
|
+
buttonSize: 'sm' as const,
|
|
85
|
+
iconClass: 'h-3.5 w-3.5',
|
|
86
|
+
extraButton: 'rounded-md !px-2',
|
|
87
|
+
},
|
|
88
|
+
} as const;
|
|
22
89
|
|
|
23
90
|
interface PrivateSidebarProps {
|
|
24
91
|
sidebar: SidebarConfig;
|
|
92
|
+
header?: HeaderConfig;
|
|
93
|
+
i18n?: I18nLayoutConfig;
|
|
94
|
+
pathname?: string;
|
|
25
95
|
}
|
|
26
96
|
|
|
27
|
-
export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
97
|
+
export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }: PrivateSidebarProps) {
|
|
98
|
+
const pathnameFromNext = useNextPathname();
|
|
99
|
+
const pathname = pathnameProp ?? pathnameFromNext;
|
|
100
|
+
const { state, isMobile, setOpenMobile } = useSidebar();
|
|
30
101
|
const homeHref = sidebar.homeHref || '/';
|
|
31
102
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (isMobile) setOpenMobile(false);
|
|
105
|
+
}, [pathname, isMobile, setOpenMobile]);
|
|
106
|
+
const brandTitle = header?.title?.trim() || 'Dashboard';
|
|
107
|
+
|
|
108
|
+
const allItems = React.useMemo(
|
|
109
|
+
() => sidebar.groups.flatMap((g) => g.items),
|
|
110
|
+
[sidebar.groups],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const density = React.useMemo(() => navDensityFromCount(allItems.length), [allItems.length]);
|
|
114
|
+
const d = DENSITY[density];
|
|
36
115
|
|
|
37
116
|
const isActive = (href: string) => {
|
|
38
|
-
const matches = pathname === href || pathname.startsWith(href
|
|
117
|
+
const matches = pathname === href || pathname.startsWith(`${href}/`);
|
|
39
118
|
if (!matches) return false;
|
|
40
|
-
|
|
41
|
-
// Check if there's a more specific (longer) path that also matches
|
|
42
119
|
return !allItems.some(
|
|
43
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
(pathname ===
|
|
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>
|
|
120
|
+
(other) =>
|
|
121
|
+
other.href !== href &&
|
|
122
|
+
other.href.startsWith(`${href}/`) &&
|
|
123
|
+
(pathname === other.href || pathname.startsWith(`${other.href}/`)),
|
|
75
124
|
);
|
|
76
125
|
};
|
|
77
126
|
|
|
78
|
-
|
|
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
|
-
};
|
|
127
|
+
const expanded = state === 'expanded';
|
|
96
128
|
|
|
97
129
|
return (
|
|
98
130
|
<Sidebar collapsible="icon">
|
|
99
|
-
<SidebarHeader>
|
|
100
|
-
|
|
101
|
-
className="flex items-center gap-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
transition: 'padding 200ms ease-in-out',
|
|
109
|
-
}
|
|
110
|
-
: {
|
|
111
|
-
padding: '0.5rem',
|
|
112
|
-
transition: 'padding 200ms ease-in-out',
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
>
|
|
116
|
-
<Link href={homeHref}>
|
|
117
|
-
<div className="flex items-center gap-3">
|
|
118
|
-
<div
|
|
119
|
-
className={cn(
|
|
120
|
-
'bg-primary rounded-sm flex items-center justify-center flex-shrink-0',
|
|
121
|
-
isMobile ? 'h-10 w-10' : 'h-8 w-8'
|
|
122
|
-
)}
|
|
123
|
-
>
|
|
124
|
-
<span className="text-primary-foreground font-bold text-sm">
|
|
131
|
+
<SidebarHeader className="px-2 py-1.5">
|
|
132
|
+
{expanded ? (
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<Link
|
|
135
|
+
href={homeHref}
|
|
136
|
+
className="flex min-w-0 flex-1 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
137
|
+
>
|
|
138
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary">
|
|
139
|
+
<span className="text-[11px] font-bold leading-none tracking-tight text-primary-foreground">
|
|
125
140
|
D
|
|
126
141
|
</span>
|
|
127
142
|
</div>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</div>
|
|
140
|
-
</Link>
|
|
141
|
-
</div>
|
|
143
|
+
<span className="truncate text-sm font-semibold tracking-tight text-foreground">
|
|
144
|
+
{brandTitle}
|
|
145
|
+
</span>
|
|
146
|
+
</Link>
|
|
147
|
+
<SidebarTrigger className="shrink-0" aria-label="Collapse sidebar" />
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<div className="flex justify-center py-0.5">
|
|
151
|
+
<SidebarTrigger aria-label="Expand sidebar" />
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
142
154
|
</SidebarHeader>
|
|
143
155
|
|
|
144
|
-
<SidebarContent
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
<SidebarContent className={cn('gap-2', d.group)}>
|
|
157
|
+
{sidebar.groups.map((group) => {
|
|
158
|
+
if (group.dynamic && group.items.length === 0) return null;
|
|
159
|
+
return (
|
|
160
|
+
<SidebarGroup key={group.label} className="gap-0">
|
|
161
|
+
<SidebarGroupLabel className={cn('px-2', d.label)}>{group.label}</SidebarGroupLabel>
|
|
162
|
+
<SidebarGroupContent>
|
|
163
|
+
<SidebarMenu className={d.menu}>
|
|
164
|
+
{group.items.map((item: SidebarItem) => (
|
|
165
|
+
<SidebarMenuItem key={item.href}>
|
|
166
|
+
<SidebarMenuButton
|
|
167
|
+
asChild
|
|
168
|
+
isActive={isActive(item.href)}
|
|
169
|
+
size={d.buttonSize}
|
|
170
|
+
className={cn(navItemClass, d.extraButton)}
|
|
171
|
+
>
|
|
172
|
+
<Link href={item.href}>
|
|
173
|
+
{item.icon && (
|
|
174
|
+
<LucideIcon
|
|
175
|
+
icon={typeof item.icon === 'string' ? item.icon : item.icon}
|
|
176
|
+
className={d.iconClass}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
<span>{item.label}</span>
|
|
180
|
+
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
|
|
181
|
+
</Link>
|
|
182
|
+
</SidebarMenuButton>
|
|
183
|
+
</SidebarMenuItem>
|
|
184
|
+
))}
|
|
185
|
+
</SidebarMenu>
|
|
186
|
+
</SidebarGroupContent>
|
|
187
|
+
</SidebarGroup>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</SidebarContent>
|
|
191
|
+
|
|
192
|
+
<SidebarFooter className="p-2">
|
|
193
|
+
{sidebar.footer && <div className="mb-2">{sidebar.footer}</div>}
|
|
194
|
+
<PrivateSidebarAccount header={header} i18n={i18n} />
|
|
195
|
+
</SidebarFooter>
|
|
152
196
|
</Sidebar>
|
|
153
197
|
);
|
|
154
198
|
}
|
|
155
|
-
|
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```tsx
|
|
14
|
-
* import { PublicLayout } from '@djangocfg/layouts';
|
|
14
|
+
* import { PublicLayout, PublicNavbar } from '@djangocfg/layouts';
|
|
15
15
|
*
|
|
16
16
|
* <PublicLayout
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* navbar={
|
|
18
|
+
* <PublicNavbar
|
|
19
|
+
* logo="/logo.svg"
|
|
20
|
+
* siteName="My App"
|
|
21
|
+
* navigation={[
|
|
22
|
+
* { label: 'Home', href: '/' },
|
|
23
|
+
* { label: 'Docs', href: '/docs' }
|
|
24
|
+
* ]}
|
|
25
|
+
* />
|
|
26
|
+
* }
|
|
23
27
|
* >
|
|
24
28
|
* {children}
|
|
25
29
|
* </PublicLayout>
|
|
@@ -31,36 +35,25 @@
|
|
|
31
35
|
import { usePathname } from 'next/navigation';
|
|
32
36
|
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
|
33
37
|
|
|
34
|
-
import { PublicMobileDrawer, PublicNavigation } from './components';
|
|
35
38
|
import { PublicLayoutProvider } from './context';
|
|
36
39
|
|
|
37
|
-
import type { NavigationItem, UserMenuConfig } from '../types';
|
|
38
|
-
import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
|
|
39
|
-
|
|
40
40
|
export interface PublicLayoutProps {
|
|
41
41
|
children: ReactNode;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
i18n?: I18nLayoutConfig;
|
|
52
|
-
/** Custom className for navbar container (e.g. "max-w-7xl mx-auto") */
|
|
53
|
-
navbarContainerClassName?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Slots (advanced).
|
|
45
|
+
*
|
|
46
|
+
* Slots-only API: this layout does not render any default navbar/drawer/footer.
|
|
47
|
+
* Pass `navbar`, `mobileDrawer`, `footer` explicitly (or keep them empty).
|
|
48
|
+
*/
|
|
49
|
+
navbar?: ReactNode;
|
|
50
|
+
footer?: ReactNode;
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
export function PublicLayout({
|
|
57
54
|
children,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
navigation = [],
|
|
61
|
-
userMenu,
|
|
62
|
-
i18n,
|
|
63
|
-
navbarContainerClassName,
|
|
55
|
+
navbar,
|
|
56
|
+
footer,
|
|
64
57
|
}: PublicLayoutProps) {
|
|
65
58
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
66
59
|
const pathname = usePathname();
|
|
@@ -71,30 +64,22 @@ export function PublicLayout({
|
|
|
71
64
|
}, [pathname]);
|
|
72
65
|
|
|
73
66
|
const contextValue = useMemo(() => ({
|
|
74
|
-
logo,
|
|
75
|
-
siteName,
|
|
76
|
-
navigation,
|
|
77
|
-
userMenu,
|
|
78
|
-
i18n,
|
|
79
|
-
containerClassName: navbarContainerClassName,
|
|
80
67
|
mobileMenuOpen,
|
|
81
68
|
toggleMobileMenu: () => setMobileMenuOpen((prev) => !prev),
|
|
82
69
|
closeMobileMenu: () => setMobileMenuOpen(false),
|
|
83
|
-
}), [
|
|
70
|
+
}), [
|
|
71
|
+
mobileMenuOpen,
|
|
72
|
+
]);
|
|
84
73
|
|
|
85
74
|
return (
|
|
86
75
|
<PublicLayoutProvider value={contextValue}>
|
|
87
76
|
<div className="min-h-screen flex flex-col">
|
|
88
|
-
{
|
|
89
|
-
<PublicNavigation />
|
|
90
|
-
|
|
91
|
-
{/* Mobile Drawer */}
|
|
92
|
-
<PublicMobileDrawer />
|
|
77
|
+
{navbar ?? null}
|
|
93
78
|
|
|
94
79
|
{/* Main Content */}
|
|
95
80
|
<main className="flex-1">{children}</main>
|
|
96
81
|
|
|
97
|
-
{
|
|
82
|
+
{footer ?? null}
|
|
98
83
|
</div>
|
|
99
84
|
</PublicLayoutProvider>
|
|
100
85
|
);
|
|
@@ -11,13 +11,24 @@ import type { FooterMenuSection } from './types';
|
|
|
11
11
|
|
|
12
12
|
export interface FooterMenuSectionsProps {
|
|
13
13
|
menuSections: FooterMenuSection[];
|
|
14
|
+
minColumnWidth?: number;
|
|
15
|
+
maxColumns?: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export function FooterMenuSections({
|
|
18
|
+
export function FooterMenuSections({
|
|
19
|
+
menuSections,
|
|
20
|
+
minColumnWidth = 180,
|
|
21
|
+
maxColumns = 5,
|
|
22
|
+
}: FooterMenuSectionsProps) {
|
|
17
23
|
if (menuSections.length === 0) return null;
|
|
18
24
|
|
|
25
|
+
const effectiveColumns = Math.max(1, Math.min(maxColumns, menuSections.length));
|
|
26
|
+
|
|
19
27
|
return (
|
|
20
|
-
<div
|
|
28
|
+
<div
|
|
29
|
+
className="w-full grid gap-8 lg:gap-x-12"
|
|
30
|
+
style={{ gridTemplateColumns: `repeat(${effectiveColumns}, minmax(${minColumnWidth}px, 1fr))` }}
|
|
31
|
+
>
|
|
21
32
|
{menuSections.map((section) => (
|
|
22
33
|
<div key={section.title} className="min-w-0">
|
|
23
34
|
<h3 className="text-xs font-medium text-muted-foreground mb-3">
|
|
@@ -13,7 +13,7 @@ import type { LucideIcon } from 'lucide-react';
|
|
|
13
13
|
import type { FooterSocialLinks } from './types';
|
|
14
14
|
|
|
15
15
|
export interface FooterProjectInfoProps {
|
|
16
|
-
siteName
|
|
16
|
+
siteName?: string;
|
|
17
17
|
description?: string;
|
|
18
18
|
logo?: string;
|
|
19
19
|
badge?: {
|
|
@@ -41,16 +41,18 @@ export function FooterProjectInfo({
|
|
|
41
41
|
<div className={isMobile ? 'w-6 h-6 flex items-center justify-center' : 'w-8 h-8 flex items-center justify-center'}>
|
|
42
42
|
<img
|
|
43
43
|
src={logo}
|
|
44
|
-
alt={`${siteName} Logo`}
|
|
44
|
+
alt={`${siteName || 'Project'} Logo`}
|
|
45
45
|
className="w-full h-full object-contain"
|
|
46
46
|
/>
|
|
47
47
|
</div>
|
|
48
48
|
) : (
|
|
49
49
|
<DjangoCFGLogo size={isMobile ? 24 : 32} className="text-foreground" />
|
|
50
50
|
)}
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
51
|
+
{siteName && (
|
|
52
|
+
<span className={isMobile ? 'text-lg font-bold text-foreground' : 'text-lg font-semibold text-foreground'}>
|
|
53
|
+
{siteName}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
54
56
|
</div>
|
|
55
57
|
|
|
56
58
|
{description && (
|