@djangocfg/layouts 2.1.256 → 2.1.259
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 +3 -1
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +15 -4
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +3 -3
- package/src/layouts/PublicLayout/PublicLayout.tsx +82 -17
- 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 +84 -40
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +22 -35
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +184 -98
- package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
- package/src/layouts/PublicLayout/components/index.ts +2 -0
- package/src/layouts/PublicLayout/context.tsx +5 -0
- package/src/layouts/PublicLayout/hooks/index.ts +1 -1
- package/src/layouts/PublicLayout/hooks/useMobileNavPanel.ts +55 -0
- package/src/layouts/PublicLayout/index.ts +8 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +20 -0
- package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
- package/src/layouts/_components/PrivateSidebarAccount.tsx +16 -3
- package/src/layouts/_components/UserMenu.tsx +133 -30
- package/src/layouts/types/index.ts +10 -1
- package/src/layouts/types/providers.types.ts +10 -0
- package/src/layouts/types/ui.types.ts +9 -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
- package/src/layouts/PublicLayout/hooks/useFloatingPanel.ts +0 -61
|
@@ -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
|
|
|
@@ -92,6 +92,8 @@ export interface PrivateLayoutProps {
|
|
|
92
92
|
contentPadding?: 'none' | 'default';
|
|
93
93
|
/** i18n configuration for locale switching */
|
|
94
94
|
i18n?: I18nLayoutConfig;
|
|
95
|
+
/** Reserved for `AppLayout` passthrough (`publicChrome`); unused in this layout. */
|
|
96
|
+
publicChrome?: AppLayoutPublicChrome;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
export function PrivateLayout({
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React, { ReactNode } from 'react';
|
|
9
9
|
|
|
10
|
-
import { SidebarTrigger } from '@djangocfg/ui-nextjs/components';
|
|
10
|
+
import { SidebarTrigger, useSidebar } from '@djangocfg/ui-nextjs/components';
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
12
|
|
|
13
13
|
interface PrivateContentProps {
|
|
@@ -22,14 +22,25 @@ export function PrivateContent({
|
|
|
22
22
|
padding = 'default',
|
|
23
23
|
hasSidebar = true,
|
|
24
24
|
}: PrivateContentProps) {
|
|
25
|
-
|
|
25
|
+
const { isMobile, openMobile } = useSidebar();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Space for fixed FAB + safe area so content does not sit under the button.
|
|
29
|
+
* Tighter when the mobile drawer is closed (FAB only). When the drawer is open, a bit more
|
|
30
|
+
* room avoids the first line sitting under the overlay edge.
|
|
31
|
+
*/
|
|
26
32
|
const mobileFabClearance =
|
|
27
33
|
hasSidebar &&
|
|
28
|
-
|
|
34
|
+
(isMobile && !openMobile
|
|
35
|
+
? 'max-md:pt-[max(3.75rem,calc(3rem+env(safe-area-inset-top,0px)))]'
|
|
36
|
+
: 'max-md:pt-[max(4rem,calc(3.25rem+env(safe-area-inset-top,0px)))]');
|
|
29
37
|
|
|
30
38
|
const scrollAreaClass = cn(
|
|
31
39
|
'min-h-0 flex-1 overflow-y-auto',
|
|
32
|
-
padding === 'default' &&
|
|
40
|
+
padding === 'default' && [
|
|
41
|
+
'pt-4 px-4 sm:pt-6 sm:px-6 lg:pt-8 lg:px-8',
|
|
42
|
+
'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))]',
|
|
43
|
+
],
|
|
33
44
|
mobileFabClearance,
|
|
34
45
|
);
|
|
35
46
|
|
|
@@ -48,9 +48,9 @@ function navDensityFromCount(n: number): NavDensity {
|
|
|
48
48
|
* Nav rows use semantic sidebar tokens so light/dark follows ui-core theme vars.
|
|
49
49
|
*/
|
|
50
50
|
const navItemClass = cn(
|
|
51
|
-
'border-0 font-
|
|
52
|
-
'text-sidebar-foreground/
|
|
53
|
-
'data-[active=true]:font-
|
|
51
|
+
'border-0 font-medium shadow-none transition-colors',
|
|
52
|
+
'text-sidebar-foreground/80',
|
|
53
|
+
'data-[active=true]:font-semibold data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
|
54
54
|
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
55
55
|
'data-[active=true]:hover:bg-sidebar-accent',
|
|
56
56
|
'[&>svg]:shrink-0 [&>svg]:text-sidebar-foreground/70 [&>svg]:opacity-85',
|
|
@@ -16,26 +16,31 @@
|
|
|
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';
|
|
34
36
|
|
|
35
37
|
import { usePathname } from 'next/navigation';
|
|
36
|
-
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
|
38
|
+
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
|
37
39
|
|
|
38
|
-
import {
|
|
40
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
41
|
+
|
|
42
|
+
import { PublicLayoutProvider, usePublicLayoutOptional } from './context';
|
|
43
|
+
import type { PublicNavbarSurface } from './navbarTypes';
|
|
39
44
|
|
|
40
45
|
export interface PublicLayoutProps {
|
|
41
46
|
children: ReactNode;
|
|
@@ -48,36 +53,96 @@ export interface PublicLayoutProps {
|
|
|
48
53
|
*/
|
|
49
54
|
navbar?: ReactNode;
|
|
50
55
|
footer?: ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* When `auto` (default), `<main>` gets a small top offset from `PublicNavigation` surface
|
|
58
|
+
* (`floating` vs `flush`). Set `none` if the page controls spacing itself.
|
|
59
|
+
*/
|
|
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';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function PublicMain({
|
|
70
|
+
children,
|
|
71
|
+
contentTopSpacing,
|
|
72
|
+
contentBottomSpacing,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
contentTopSpacing: 'auto' | 'none';
|
|
76
|
+
contentBottomSpacing: 'auto' | 'none' | 'compact';
|
|
77
|
+
}) {
|
|
78
|
+
const ctx = usePublicLayoutOptional();
|
|
79
|
+
const variant = ctx?.navbarSurface?.variant;
|
|
80
|
+
|
|
81
|
+
const topClass =
|
|
82
|
+
contentTopSpacing === 'none'
|
|
83
|
+
? undefined
|
|
84
|
+
: !variant
|
|
85
|
+
? 'pt-4 sm:pt-5'
|
|
86
|
+
: variant === 'floating'
|
|
87
|
+
? 'pt-2 sm:pt-3 lg:pt-4'
|
|
88
|
+
: 'pt-1 sm:pt-2 lg:pt-3';
|
|
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
|
+
|
|
97
|
+
return (
|
|
98
|
+
<main className={cn('flex-1', topClass, bottomClass)}>
|
|
99
|
+
{children}
|
|
100
|
+
</main>
|
|
101
|
+
);
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
export function PublicLayout({
|
|
54
105
|
children,
|
|
55
106
|
navbar,
|
|
56
107
|
footer,
|
|
108
|
+
contentTopSpacing = 'auto',
|
|
109
|
+
contentBottomSpacing = 'auto',
|
|
57
110
|
}: PublicLayoutProps) {
|
|
58
111
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
112
|
+
const [navbarSurface, setNavbarSurfaceState] = useState<PublicNavbarSurface | null>(null);
|
|
59
113
|
const pathname = usePathname();
|
|
60
114
|
|
|
115
|
+
const setNavbarSurface = useCallback((surface: PublicNavbarSurface | null) => {
|
|
116
|
+
setNavbarSurfaceState(surface);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
61
119
|
// Close mobile menu on route change
|
|
62
120
|
useEffect(() => {
|
|
63
121
|
setMobileMenuOpen(false);
|
|
64
122
|
}, [pathname]);
|
|
65
123
|
|
|
66
|
-
const contextValue = useMemo(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
const contextValue = useMemo(
|
|
125
|
+
() => ({
|
|
126
|
+
mobileMenuOpen,
|
|
127
|
+
toggleMobileMenu: () => setMobileMenuOpen((prev) => !prev),
|
|
128
|
+
closeMobileMenu: () => setMobileMenuOpen(false),
|
|
129
|
+
navbarSurface,
|
|
130
|
+
setNavbarSurface,
|
|
131
|
+
}),
|
|
132
|
+
[mobileMenuOpen, navbarSurface, setNavbarSurface],
|
|
133
|
+
);
|
|
73
134
|
|
|
74
135
|
return (
|
|
75
136
|
<PublicLayoutProvider value={contextValue}>
|
|
76
137
|
<div className="min-h-screen flex flex-col">
|
|
77
138
|
{navbar ?? null}
|
|
78
139
|
|
|
79
|
-
|
|
80
|
-
|
|
140
|
+
<PublicMain
|
|
141
|
+
contentTopSpacing={contentTopSpacing}
|
|
142
|
+
contentBottomSpacing={contentBottomSpacing}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
</PublicMain>
|
|
81
146
|
|
|
82
147
|
{footer ?? null}
|
|
83
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,
|
|
@@ -29,39 +29,49 @@ export interface FooterSocialLinks {
|
|
|
29
29
|
email?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
export interface PublicFooterConfig {
|
|
33
|
+
variant?: 'full' | 'compact' | 'simple';
|
|
34
|
+
shell?: {
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
brand?: {
|
|
38
|
+
/** Custom brand column (`ReactNode`) */
|
|
39
|
+
slot?: ReactNode;
|
|
40
|
+
description?: string;
|
|
41
|
+
showColumn?: boolean;
|
|
42
|
+
badge?: {
|
|
43
|
+
icon: LucideIcon;
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
menus?: {
|
|
48
|
+
sections?: FooterMenuSection[];
|
|
49
|
+
columnMinWidth?: number;
|
|
50
|
+
maxColumns?: number;
|
|
41
51
|
};
|
|
42
|
-
/** Social media links */
|
|
43
|
-
socialLinks?: FooterSocialLinks;
|
|
44
|
-
/** Quick links (bottom bar) */
|
|
45
52
|
links?: FooterLink[];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Soft max column count for menu grid */
|
|
53
|
-
menuMaxColumns?: number;
|
|
54
|
-
/** Copyright text (auto-generated if not provided) */
|
|
55
|
-
copyright?: string;
|
|
56
|
-
/** Credits */
|
|
57
|
-
credits?: {
|
|
58
|
-
text: string;
|
|
59
|
-
url?: string;
|
|
53
|
+
meta?: {
|
|
54
|
+
copyright?: string;
|
|
55
|
+
credits?: {
|
|
56
|
+
text: string;
|
|
57
|
+
url?: string;
|
|
58
|
+
};
|
|
60
59
|
};
|
|
61
|
-
|
|
62
|
-
variant?: 'full' | 'compact' | 'simple';
|
|
63
|
-
/** Custom className for content container (e.g. "max-w-4xl" or "container") */
|
|
64
|
-
containerClassName?: string;
|
|
65
|
-
/** i18n configuration for language switcher */
|
|
60
|
+
social?: FooterSocialLinks;
|
|
66
61
|
i18n?: I18nLayoutConfig;
|
|
62
|
+
/**
|
|
63
|
+
* Full (`variant: 'full'`) footer only — bottom row with theme and/or locale controls.
|
|
64
|
+
* - Locale: still requires `i18n`; omit `i18n` to hide the language switcher, or set
|
|
65
|
+
* `showLocaleSwitcher: false` when you pass `i18n` but want the switcher elsewhere.
|
|
66
|
+
*/
|
|
67
|
+
controls?: {
|
|
68
|
+
/** Light / system / dark toggle. @default true */
|
|
69
|
+
showThemeSwitcher?: boolean;
|
|
70
|
+
/** Requires `i18n`. @default true */
|
|
71
|
+
showLocaleSwitcher?: boolean;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PublicFooterProps {
|
|
76
|
+
config: PublicFooterConfig;
|
|
67
77
|
}
|