@djangocfg/layouts 2.1.257 → 2.1.260
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 +101 -203
- package/package.json +18 -18
- package/src/index.ts +4 -1
- package/src/layouts/AppLayout/AppLayout.tsx +97 -8
- package/src/layouts/AppLayout/BaseApp.tsx +2 -0
- package/src/layouts/AppLayout/index.ts +6 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +5 -1
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +12 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +45 -14
- package/src/layouts/PublicLayout/PublicLayout.tsx +31 -8
- package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
- package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
- package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
- package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +69 -30
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +24 -34
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +162 -94
- package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
- package/src/layouts/PublicLayout/components/index.ts +2 -0
- package/src/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +8 -0
- package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
- package/src/layouts/_components/UserMenu.tsx +2 -2
- package/src/layouts/types/index.ts +9 -1
- package/src/layouts/types/providers.types.ts +10 -0
- package/src/theme/ThemeStyleBridge.tsx +41 -0
- package/src/theme/buildThemeStyleSheet.ts +71 -0
- package/src/theme/index.ts +16 -0
- package/src/theme/themeStyle.types.ts +89 -0
- package/src/theme/themeStylePresets.ts +202 -0
|
@@ -7,10 +7,16 @@ export type {
|
|
|
7
7
|
AppLayoutProps,
|
|
8
8
|
AppLayoutLayoutsConfig,
|
|
9
9
|
AppLayoutBaseAppConfig,
|
|
10
|
+
AppLayoutLayoutComponentProps,
|
|
11
|
+
AppLayoutPublicChrome,
|
|
10
12
|
LayoutMode,
|
|
11
13
|
I18nLayoutConfig,
|
|
14
|
+
PublicMainTopSpacing,
|
|
15
|
+
PublicMainBottomSpacing,
|
|
12
16
|
} from './AppLayout';
|
|
13
17
|
|
|
18
|
+
export { mergeAppLayoutPublicChrome } from './AppLayout';
|
|
19
|
+
|
|
14
20
|
export { BaseApp } from './BaseApp';
|
|
15
21
|
export type { BaseAppProps } from './BaseApp';
|
|
16
22
|
|
|
@@ -15,7 +15,7 @@ import { useAuth } from '@djangocfg/api/auth';
|
|
|
15
15
|
import { Preloader } from '@djangocfg/ui-core/components';
|
|
16
16
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-nextjs/components';
|
|
17
17
|
|
|
18
|
-
import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
|
|
18
|
+
import type { AppLayoutPublicChrome, I18nLayoutConfig } from '../AppLayout/AppLayout';
|
|
19
19
|
import { UserMenuConfig } from '../types';
|
|
20
20
|
import { PrivateContent, PrivateSidebar } from './components';
|
|
21
21
|
|
|
@@ -59,6 +59,8 @@ export interface SidebarConfig {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface HeaderConfig {
|
|
62
|
+
/** Custom header brand node (same idea as PublicNavbar `brand`). */
|
|
63
|
+
brand?: ReactNode;
|
|
62
64
|
/** Shown next to the logo when the sidebar is expanded */
|
|
63
65
|
title?: string;
|
|
64
66
|
/**
|
|
@@ -92,6 +94,8 @@ export interface PrivateLayoutProps {
|
|
|
92
94
|
contentPadding?: 'none' | 'default';
|
|
93
95
|
/** i18n configuration for locale switching */
|
|
94
96
|
i18n?: I18nLayoutConfig;
|
|
97
|
+
/** Reserved for `AppLayout` passthrough (`publicChrome`); unused in this layout. */
|
|
98
|
+
publicChrome?: AppLayoutPublicChrome;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
export function PrivateLayout({
|
|
@@ -35,9 +35,20 @@ export function PrivateContent({
|
|
|
35
35
|
? 'max-md:pt-[max(3.75rem,calc(3rem+env(safe-area-inset-top,0px)))]'
|
|
36
36
|
: 'max-md:pt-[max(4rem,calc(3.25rem+env(safe-area-inset-top,0px)))]');
|
|
37
37
|
|
|
38
|
+
const contentTopPaddingClass =
|
|
39
|
+
padding === 'default'
|
|
40
|
+
? hasSidebar
|
|
41
|
+
? 'md:pt-6 lg:pt-8'
|
|
42
|
+
: 'pt-4 sm:pt-6 lg:pt-8'
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
38
45
|
const scrollAreaClass = cn(
|
|
39
46
|
'min-h-0 flex-1 overflow-y-auto',
|
|
40
|
-
padding === 'default' &&
|
|
47
|
+
padding === 'default' && [
|
|
48
|
+
'px-4 sm:px-6 lg:px-8',
|
|
49
|
+
'pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))] lg:pb-[calc(2rem+env(safe-area-inset-bottom,0px))]',
|
|
50
|
+
],
|
|
51
|
+
contentTopPaddingClass,
|
|
41
52
|
mobileFabClearance,
|
|
42
53
|
);
|
|
43
54
|
|
|
@@ -114,6 +114,7 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
114
114
|
}, [pathname, isMobile, setOpenMobile]);
|
|
115
115
|
const brandTitle = header?.title?.trim() || 'Dashboard';
|
|
116
116
|
const brandMonogram = (header?.brandLetter?.trim().charAt(0) || brandTitle.charAt(0) || 'D').toUpperCase();
|
|
117
|
+
const customBrand = header?.brand;
|
|
117
118
|
|
|
118
119
|
const allItems = React.useMemo(
|
|
119
120
|
() => sidebar.groups.flatMap((g) => g.items),
|
|
@@ -203,13 +204,28 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
203
204
|
|
|
204
205
|
const expandedHeader = (
|
|
205
206
|
<div className={headerRowClass}>
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
<div className="min-w-0 flex-1">
|
|
208
|
+
{customBrand != null && customBrand !== false
|
|
209
|
+
? typeof customBrand === 'string'
|
|
210
|
+
? (
|
|
211
|
+
<Link
|
|
212
|
+
href={homeHref}
|
|
213
|
+
className="flex min-w-0 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
214
|
+
>
|
|
215
|
+
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{customBrand}</span>
|
|
216
|
+
</Link>
|
|
217
|
+
)
|
|
218
|
+
: customBrand
|
|
219
|
+
: (
|
|
220
|
+
<Link
|
|
221
|
+
href={homeHref}
|
|
222
|
+
className="flex min-w-0 items-center gap-2 rounded-md py-0.5 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
223
|
+
>
|
|
224
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
225
|
+
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
226
|
+
</Link>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
213
229
|
{!isMobile && <SidebarTrigger className="shrink-0" aria-label="Collapse sidebar" />}
|
|
214
230
|
</div>
|
|
215
231
|
);
|
|
@@ -223,13 +239,28 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp }
|
|
|
223
239
|
/** Mobile drawer: menu open/close only from the main column trigger — no duplicate toggle in the sheet. */
|
|
224
240
|
const mobileHeader = (
|
|
225
241
|
<div className="flex items-center gap-3">
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
<div className="min-w-0 flex-1">
|
|
243
|
+
{customBrand != null && customBrand !== false
|
|
244
|
+
? typeof customBrand === 'string'
|
|
245
|
+
? (
|
|
246
|
+
<Link
|
|
247
|
+
href={homeHref}
|
|
248
|
+
className="flex min-w-0 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
249
|
+
>
|
|
250
|
+
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{customBrand}</span>
|
|
251
|
+
</Link>
|
|
252
|
+
)
|
|
253
|
+
: customBrand
|
|
254
|
+
: (
|
|
255
|
+
<Link
|
|
256
|
+
href={homeHref}
|
|
257
|
+
className="flex min-w-0 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
|
|
258
|
+
>
|
|
259
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
|
|
260
|
+
<span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
|
|
261
|
+
</Link>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
233
264
|
</div>
|
|
234
265
|
);
|
|
235
266
|
|
|
@@ -16,18 +16,20 @@
|
|
|
16
16
|
* <PublicLayout
|
|
17
17
|
* navbar={
|
|
18
18
|
* <PublicNavbar
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* config={{
|
|
20
|
+
* brand: <YourNavbarBrand />,
|
|
21
|
+
* navigation: [
|
|
22
|
+
* { label: 'Home', href: '/' },
|
|
23
|
+
* { label: 'Docs', href: '/docs' },
|
|
24
|
+
* ],
|
|
25
|
+
* }}
|
|
25
26
|
* />
|
|
26
27
|
* }
|
|
27
28
|
* >
|
|
28
29
|
* {children}
|
|
29
30
|
* </PublicLayout>
|
|
30
31
|
* ```
|
|
32
|
+
* Pass `brand` on `PublicNavbar` (`ReactNode` or string).
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
'use client';
|
|
@@ -56,14 +58,22 @@ export interface PublicLayoutProps {
|
|
|
56
58
|
* (`floating` vs `flush`). Set `none` if the page controls spacing itself.
|
|
57
59
|
*/
|
|
58
60
|
contentTopSpacing?: 'auto' | 'none';
|
|
61
|
+
/**
|
|
62
|
+
* When `auto` (default), `<main>` gets bottom padding before the footer so the last block of
|
|
63
|
+
* page content is not flush against the footer edge. Set `none` if the page/footer slot handles it.
|
|
64
|
+
* `compact` uses a smaller gap than `auto`.
|
|
65
|
+
*/
|
|
66
|
+
contentBottomSpacing?: 'auto' | 'none' | 'compact';
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
function PublicMain({
|
|
62
70
|
children,
|
|
63
71
|
contentTopSpacing,
|
|
72
|
+
contentBottomSpacing,
|
|
64
73
|
}: {
|
|
65
74
|
children: ReactNode;
|
|
66
75
|
contentTopSpacing: 'auto' | 'none';
|
|
76
|
+
contentBottomSpacing: 'auto' | 'none' | 'compact';
|
|
67
77
|
}) {
|
|
68
78
|
const ctx = usePublicLayoutOptional();
|
|
69
79
|
const variant = ctx?.navbarSurface?.variant;
|
|
@@ -77,8 +87,15 @@ function PublicMain({
|
|
|
77
87
|
? 'pt-2 sm:pt-3 lg:pt-4'
|
|
78
88
|
: 'pt-1 sm:pt-2 lg:pt-3';
|
|
79
89
|
|
|
90
|
+
const bottomClass =
|
|
91
|
+
contentBottomSpacing === 'none'
|
|
92
|
+
? undefined
|
|
93
|
+
: contentBottomSpacing === 'compact'
|
|
94
|
+
? 'pb-4 sm:pb-6 lg:pb-8'
|
|
95
|
+
: 'pb-8 sm:pb-10 lg:pb-12';
|
|
96
|
+
|
|
80
97
|
return (
|
|
81
|
-
<main className={cn('flex-1', topClass)}>
|
|
98
|
+
<main className={cn('flex-1', topClass, bottomClass)}>
|
|
82
99
|
{children}
|
|
83
100
|
</main>
|
|
84
101
|
);
|
|
@@ -89,6 +106,7 @@ export function PublicLayout({
|
|
|
89
106
|
navbar,
|
|
90
107
|
footer,
|
|
91
108
|
contentTopSpacing = 'auto',
|
|
109
|
+
contentBottomSpacing = 'auto',
|
|
92
110
|
}: PublicLayoutProps) {
|
|
93
111
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
94
112
|
const [navbarSurface, setNavbarSurfaceState] = useState<PublicNavbarSurface | null>(null);
|
|
@@ -119,7 +137,12 @@ export function PublicLayout({
|
|
|
119
137
|
<div className="min-h-screen flex flex-col">
|
|
120
138
|
{navbar ?? null}
|
|
121
139
|
|
|
122
|
-
<PublicMain
|
|
140
|
+
<PublicMain
|
|
141
|
+
contentTopSpacing={contentTopSpacing}
|
|
142
|
+
contentBottomSpacing={contentBottomSpacing}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
</PublicMain>
|
|
123
146
|
|
|
124
147
|
{footer ?? null}
|
|
125
148
|
</div>
|
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
|
|
9
|
-
import { DjangoCFGLogo } from './DjangoCFGLogo';
|
|
10
9
|
import { FooterSocialLinksComponent } from './FooterSocialLinks';
|
|
11
10
|
|
|
12
11
|
import type { LucideIcon } from 'lucide-react';
|
|
13
12
|
import type { FooterSocialLinks } from './types';
|
|
14
13
|
|
|
15
14
|
export interface FooterProjectInfoProps {
|
|
16
|
-
|
|
15
|
+
/** Brand row: custom node, or a plain string (styled title). */
|
|
16
|
+
brand?: React.ReactNode;
|
|
17
17
|
description?: string;
|
|
18
|
-
logo?: string;
|
|
19
18
|
badge?: {
|
|
20
19
|
icon: LucideIcon;
|
|
21
20
|
text: string;
|
|
@@ -25,35 +24,29 @@ export interface FooterProjectInfoProps {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function FooterProjectInfo({
|
|
28
|
-
|
|
27
|
+
brand,
|
|
29
28
|
description,
|
|
30
|
-
logo,
|
|
31
29
|
badge,
|
|
32
30
|
socialLinks,
|
|
33
31
|
variant = 'desktop',
|
|
34
32
|
}: FooterProjectInfoProps) {
|
|
35
33
|
const isMobile = variant === 'mobile';
|
|
36
34
|
|
|
35
|
+
const showBrand = brand != null && brand !== '' && brand !== false;
|
|
36
|
+
|
|
37
37
|
return (
|
|
38
38
|
<div className={isMobile ? 'text-center space-y-4 mb-6' : 'space-y-4 lg:flex-shrink-0 lg:w-80'}>
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)}
|
|
51
|
-
{siteName && (
|
|
52
|
-
<span className={isMobile ? 'text-lg font-bold text-foreground' : 'text-lg font-semibold text-foreground'}>
|
|
53
|
-
{siteName}
|
|
54
|
-
</span>
|
|
55
|
-
)}
|
|
56
|
-
</div>
|
|
39
|
+
{showBrand && (
|
|
40
|
+
<div className={isMobile ? 'flex items-center justify-center gap-2' : 'flex items-center gap-2'}>
|
|
41
|
+
{typeof brand === 'string' ? (
|
|
42
|
+
<span className={isMobile ? 'text-lg font-bold text-foreground' : 'text-lg font-semibold text-foreground'}>
|
|
43
|
+
{brand}
|
|
44
|
+
</span>
|
|
45
|
+
) : (
|
|
46
|
+
brand
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
57
50
|
|
|
58
51
|
{description && (
|
|
59
52
|
<p className={isMobile ? 'text-muted-foreground text-sm leading-relaxed max-w-md mx-auto' : 'text-muted-foreground text-xs leading-relaxed max-w-xs'}>
|
|
@@ -63,7 +56,7 @@ export function FooterProjectInfo({
|
|
|
63
56
|
|
|
64
57
|
{badge && !isMobile && (
|
|
65
58
|
<div className="pt-2">
|
|
66
|
-
<span className="inline-flex items-center gap-2
|
|
59
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-primary/25 bg-primary/15 px-3 py-1 text-xs font-semibold text-primary">
|
|
67
60
|
<badge.icon className="w-4 h-4" />
|
|
68
61
|
{badge.text}
|
|
69
62
|
</span>
|
|
@@ -69,38 +69,40 @@ function ThemeModeControl() {
|
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function PublicFooter({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
menuColumnMinWidth = 180
|
|
81
|
-
menuMaxColumns = 5
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
i18n
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
export function PublicFooter({ config }: PublicFooterProps) {
|
|
73
|
+
const variant = config.variant ?? 'full';
|
|
74
|
+
const shellClass = config.shell?.className;
|
|
75
|
+
const brandSlot = config.brand?.slot;
|
|
76
|
+
const description = config.brand?.description;
|
|
77
|
+
const badge = config.brand?.badge;
|
|
78
|
+
const showBrandColumn = config.brand?.showColumn ?? true;
|
|
79
|
+
const menuSections = config.menus?.sections ?? [];
|
|
80
|
+
const menuColumnMinWidth = config.menus?.columnMinWidth ?? 180;
|
|
81
|
+
const menuMaxColumns = config.menus?.maxColumns ?? 5;
|
|
82
|
+
const links = config.links ?? [];
|
|
83
|
+
const socialLinks = config.social;
|
|
84
|
+
const copyrightProp = config.meta?.copyright;
|
|
85
|
+
const creditsProp = config.meta?.credits;
|
|
86
|
+
const i18n = config.i18n;
|
|
87
|
+
const showThemeSwitcher = config.controls?.showThemeSwitcher !== false;
|
|
88
|
+
const showLocaleSwitcher =
|
|
89
|
+
config.controls?.showLocaleSwitcher !== false && Boolean(i18n);
|
|
90
|
+
const showFooterControlsRow = showThemeSwitcher || showLocaleSwitcher;
|
|
91
|
+
|
|
89
92
|
const currentYear = new Date().getFullYear();
|
|
90
93
|
const copyright = copyrightProp || `© ${currentYear}. All rights reserved.`;
|
|
91
94
|
const credits = creditsProp;
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
/** Extra space below footer on devices with a home indicator / gesture bar. */
|
|
97
|
+
const footerSafeBottom = 'pb-[env(safe-area-inset-bottom,0px)]';
|
|
98
|
+
|
|
99
|
+
const footerSurfaceClass =
|
|
100
|
+
'border-t border-border/60 bg-muted/35 mt-auto';
|
|
101
|
+
|
|
94
102
|
if (variant === 'simple') {
|
|
95
103
|
return (
|
|
96
|
-
<footer
|
|
97
|
-
className=
|
|
98
|
-
style={{
|
|
99
|
-
background: 'linear-gradient(to bottom, hsl(var(--accent) / 0.26), hsl(var(--background) / 0.97) 42%, hsl(var(--accent) / 0.34))',
|
|
100
|
-
boxShadow: 'inset 0 1px 0 hsl(var(--border) / 0.75), inset 0 14px 26px hsl(var(--accent) / 0.18)',
|
|
101
|
-
}}
|
|
102
|
-
>
|
|
103
|
-
<div className={`mx-auto px-4 py-4 ${containerClassName || 'w-full'}`}>
|
|
104
|
+
<footer className={`${footerSurfaceClass} ${footerSafeBottom}`}>
|
|
105
|
+
<div className={`mx-auto px-4 py-4 ${shellClass || 'w-full'}`}>
|
|
104
106
|
<div className="text-center">
|
|
105
107
|
<div className="text-sm text-muted-foreground">{copyright}</div>
|
|
106
108
|
</div>
|
|
@@ -109,27 +111,13 @@ export function PublicFooter({
|
|
|
109
111
|
);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
// Compact variant - single line with logo and links
|
|
113
114
|
if (variant === 'compact') {
|
|
114
115
|
return (
|
|
115
|
-
<footer
|
|
116
|
-
className=
|
|
117
|
-
style={{
|
|
118
|
-
background: 'linear-gradient(to bottom, hsl(var(--accent) / 0.26), hsl(var(--background) / 0.97) 42%, hsl(var(--accent) / 0.34))',
|
|
119
|
-
boxShadow: 'inset 0 1px 0 hsl(var(--border) / 0.75), inset 0 14px 26px hsl(var(--accent) / 0.18)',
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
<div className={`mx-auto px-4 sm:px-6 lg:px-8 py-8 ${containerClassName || 'w-full'}`}>
|
|
123
|
-
{/* Main row: logo left, links right */}
|
|
116
|
+
<footer className={`${footerSurfaceClass} ${footerSafeBottom}`}>
|
|
117
|
+
<div className={`mx-auto px-4 sm:px-6 lg:px-8 py-8 ${shellClass || 'w-full'}`}>
|
|
124
118
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
|
|
125
|
-
{
|
|
126
|
-
{projectInfo ?? (
|
|
127
|
-
<div className="flex items-center gap-3">
|
|
128
|
-
<span className="text-lg font-semibold text-foreground">Project</span>
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
119
|
+
{brandSlot ? <div className="flex items-center gap-3">{brandSlot}</div> : null}
|
|
131
120
|
|
|
132
|
-
{/* Links */}
|
|
133
121
|
{links.length > 0 && (
|
|
134
122
|
<div className="flex flex-wrap items-center justify-center gap-6">
|
|
135
123
|
{links.map((link) =>
|
|
@@ -157,7 +145,6 @@ export function PublicFooter({
|
|
|
157
145
|
)}
|
|
158
146
|
</div>
|
|
159
147
|
|
|
160
|
-
{/* Bottom row: copyright + credits */}
|
|
161
148
|
<div className="mt-6 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground">
|
|
162
149
|
<span>{copyright}</span>
|
|
163
150
|
{credits && (
|
|
@@ -182,16 +169,10 @@ export function PublicFooter({
|
|
|
182
169
|
|
|
183
170
|
return (
|
|
184
171
|
<>
|
|
185
|
-
<footer
|
|
186
|
-
className=
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
boxShadow: 'inset 0 1px 0 hsl(var(--border) / 0.75), inset 0 14px 26px hsl(var(--accent) / 0.18)',
|
|
190
|
-
}}
|
|
191
|
-
>
|
|
192
|
-
<div className={`mx-auto px-4 py-8 ${containerClassName || 'w-full'}`}>
|
|
193
|
-
{showProjectInfo && (
|
|
194
|
-
projectInfo ?? (
|
|
172
|
+
<footer className={`lg:hidden ${footerSurfaceClass} ${footerSafeBottom}`}>
|
|
173
|
+
<div className={`mx-auto px-4 py-8 ${shellClass || 'w-full'}`}>
|
|
174
|
+
{showBrandColumn && (
|
|
175
|
+
brandSlot ?? (
|
|
195
176
|
<FooterProjectInfo
|
|
196
177
|
description={description}
|
|
197
178
|
socialLinks={socialLinks}
|
|
@@ -200,7 +181,6 @@ export function PublicFooter({
|
|
|
200
181
|
)
|
|
201
182
|
)}
|
|
202
183
|
|
|
203
|
-
{/* Quick Links */}
|
|
204
184
|
{links.length > 0 && (
|
|
205
185
|
<div className="flex flex-wrap justify-center gap-3 mb-6">
|
|
206
186
|
{links.map((link) =>
|
|
@@ -242,33 +222,29 @@ export function PublicFooter({
|
|
|
242
222
|
)}
|
|
243
223
|
</div>
|
|
244
224
|
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
225
|
+
{showFooterControlsRow && (
|
|
226
|
+
<div className="mt-5 flex items-center justify-center gap-2 border-t border-border/60 pt-4">
|
|
227
|
+
{showThemeSwitcher && <ThemeModeControl />}
|
|
228
|
+
{showLocaleSwitcher && i18n && (
|
|
229
|
+
<LocaleSwitcher
|
|
230
|
+
locale={i18n.locale}
|
|
231
|
+
locales={i18n.locales}
|
|
232
|
+
onChange={i18n.onLocaleChange}
|
|
233
|
+
variant="outline"
|
|
234
|
+
size="default"
|
|
235
|
+
className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
258
240
|
</div>
|
|
259
241
|
</footer>
|
|
260
|
-
<footer
|
|
261
|
-
className=
|
|
262
|
-
style={{
|
|
263
|
-
background: 'linear-gradient(to bottom, hsl(var(--accent) / 0.26), hsl(var(--background) / 0.97) 42%, hsl(var(--accent) / 0.34))',
|
|
264
|
-
boxShadow: 'inset 0 1px 0 hsl(var(--border) / 0.75), inset 0 14px 26px hsl(var(--accent) / 0.18)',
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
267
|
-
<div className={`mx-auto px-6 sm:px-8 lg:px-10 py-14 ${containerClassName || 'w-full'}`}>
|
|
242
|
+
<footer className={`max-lg:hidden ${footerSurfaceClass} ${footerSafeBottom}`}>
|
|
243
|
+
<div className={`mx-auto px-6 sm:px-8 lg:px-10 py-14 ${shellClass || 'w-full'}`}>
|
|
268
244
|
<div className="grid grid-cols-12 gap-10 lg:gap-14">
|
|
269
|
-
{
|
|
245
|
+
{showBrandColumn && (
|
|
270
246
|
<div className="col-span-12 lg:col-span-4">
|
|
271
|
-
{
|
|
247
|
+
{brandSlot ?? (
|
|
272
248
|
<FooterProjectInfo
|
|
273
249
|
description={description}
|
|
274
250
|
badge={badge}
|
|
@@ -279,7 +255,7 @@ export function PublicFooter({
|
|
|
279
255
|
</div>
|
|
280
256
|
)}
|
|
281
257
|
|
|
282
|
-
<div className={
|
|
258
|
+
<div className={showBrandColumn ? 'col-span-12 lg:col-span-8 lg:pl-8' : 'col-span-12'}>
|
|
283
259
|
<FooterMenuSections
|
|
284
260
|
menuSections={menuSections}
|
|
285
261
|
minColumnWidth={menuColumnMinWidth}
|
|
@@ -288,12 +264,18 @@ export function PublicFooter({
|
|
|
288
264
|
</div>
|
|
289
265
|
</div>
|
|
290
266
|
|
|
291
|
-
<div
|
|
292
|
-
|
|
267
|
+
<div
|
|
268
|
+
className={
|
|
269
|
+
showFooterControlsRow
|
|
270
|
+
? 'mt-12 grid grid-cols-1 items-center gap-4 border-t border-border/60 pt-5 lg:grid-cols-[1fr_auto_1fr] lg:gap-6'
|
|
271
|
+
: 'mt-12 flex flex-col gap-4 border-t border-border/60 pt-5 lg:flex-row lg:items-center lg:justify-between'
|
|
272
|
+
}
|
|
273
|
+
>
|
|
274
|
+
<div className="text-center text-xs text-muted-foreground lg:text-left lg:justify-self-start">
|
|
293
275
|
{copyright}
|
|
294
276
|
</div>
|
|
295
277
|
|
|
296
|
-
<div className="min-w-0 flex items-center justify-center gap-3 text-xs text-muted-foreground
|
|
278
|
+
<div className="flex min-w-0 flex-wrap items-center justify-center gap-3 text-center text-xs text-muted-foreground lg:justify-self-center">
|
|
297
279
|
{credits && (
|
|
298
280
|
credits.url ? (
|
|
299
281
|
<a href={credits.url} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors whitespace-nowrap">
|
|
@@ -322,19 +304,21 @@ export function PublicFooter({
|
|
|
322
304
|
)}
|
|
323
305
|
</div>
|
|
324
306
|
|
|
325
|
-
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
307
|
+
{showFooterControlsRow && (
|
|
308
|
+
<div className="flex items-center justify-center gap-2 lg:justify-self-end">
|
|
309
|
+
{showThemeSwitcher && <ThemeModeControl />}
|
|
310
|
+
{showLocaleSwitcher && i18n && (
|
|
311
|
+
<LocaleSwitcher
|
|
312
|
+
locale={i18n.locale}
|
|
313
|
+
locales={i18n.locales}
|
|
314
|
+
onChange={i18n.onLocaleChange}
|
|
315
|
+
variant="outline"
|
|
316
|
+
size="default"
|
|
317
|
+
className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
338
322
|
</div>
|
|
339
323
|
</div>
|
|
340
324
|
</footer>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export { PublicFooter } from './PublicFooter';
|
|
6
6
|
export { FooterProjectInfo } from './FooterProjectInfo';
|
|
7
|
+
export type { FooterProjectInfoProps } from './FooterProjectInfo';
|
|
7
8
|
export { FooterMenuSections } from './FooterMenuSections';
|
|
8
9
|
export { FooterBottom } from './FooterBottom';
|
|
9
10
|
export { FooterSocialLinksComponent } from './FooterSocialLinks';
|
|
@@ -11,6 +12,7 @@ export { DjangoCFGLogo } from './DjangoCFGLogo';
|
|
|
11
12
|
|
|
12
13
|
export type {
|
|
13
14
|
PublicFooterProps,
|
|
15
|
+
PublicFooterConfig,
|
|
14
16
|
FooterLink,
|
|
15
17
|
FooterMenuSection,
|
|
16
18
|
FooterSocialLinks,
|