@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.
Files changed (30) hide show
  1. package/README.md +101 -203
  2. package/package.json +18 -18
  3. package/src/index.ts +4 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +97 -8
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -0
  6. package/src/layouts/AppLayout/index.ts +6 -0
  7. package/src/layouts/PrivateLayout/PrivateLayout.tsx +5 -1
  8. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +12 -1
  9. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +45 -14
  10. package/src/layouts/PublicLayout/PublicLayout.tsx +31 -8
  11. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
  12. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
  13. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
  14. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
  15. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +69 -30
  16. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +24 -34
  17. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +162 -94
  18. package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
  19. package/src/layouts/PublicLayout/components/index.ts +2 -0
  20. package/src/layouts/PublicLayout/index.ts +5 -0
  21. package/src/layouts/PublicLayout/navbarTypes.ts +8 -0
  22. package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
  23. package/src/layouts/_components/UserMenu.tsx +2 -2
  24. package/src/layouts/types/index.ts +9 -1
  25. package/src/layouts/types/providers.types.ts +10 -0
  26. package/src/theme/ThemeStyleBridge.tsx +41 -0
  27. package/src/theme/buildThemeStyleSheet.ts +71 -0
  28. package/src/theme/index.ts +16 -0
  29. package/src/theme/themeStyle.types.ts +89 -0
  30. 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' && 'p-4 sm:p-6 lg:p-8',
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
- <Link
207
- href={homeHref}
208
- 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"
209
- >
210
- <div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
211
- <span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
212
- </Link>
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
- <Link
227
- href={homeHref}
228
- className="flex min-w-0 flex-1 items-center gap-3 rounded-md py-1 outline-none ring-sidebar-ring focus-visible:ring-2"
229
- >
230
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">{brandMark}</div>
231
- <span className="truncate text-sm font-semibold tracking-tight text-sidebar-foreground">{brandTitle}</span>
232
- </Link>
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
- * logo="/logo.svg"
20
- * siteName="My App"
21
- * navigation={[
22
- * { label: 'Home', href: '/' },
23
- * { label: 'Docs', href: '/docs' }
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 contentTopSpacing={contentTopSpacing}>{children}</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
- siteName?: string;
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
- siteName,
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
- <div className={isMobile ? 'flex items-center justify-center gap-2' : 'flex items-center gap-2'}>
40
- {logo ? (
41
- <div className={isMobile ? 'w-6 h-6 flex items-center justify-center' : 'w-8 h-8 flex items-center justify-center'}>
42
- <img
43
- src={logo}
44
- alt={`${siteName || 'Project'} Logo`}
45
- className="w-full h-full object-contain"
46
- />
47
- </div>
48
- ) : (
49
- <DjangoCFGLogo size={isMobile ? 24 : 32} className="text-foreground" />
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 px-3 py-1 rounded-full bg-gradient-to-r from-primary/80 to-secondary/60 border border-primary/30 shadow-brand text-xs font-semibold text-primary-foreground">
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
- projectInfo,
74
- description,
75
- badge,
76
- socialLinks,
77
- links = [],
78
- menuSections = [],
79
- showProjectInfo = true,
80
- menuColumnMinWidth = 180,
81
- menuMaxColumns = 5,
82
- copyright: copyrightProp,
83
- credits: creditsProp,
84
- variant = 'full',
85
- containerClassName,
86
- i18n,
87
- }: PublicFooterProps) {
88
- // Prepare data BEFORE render
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
- // Simple variant - minimal footer
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="border-t border-border/60 mt-auto"
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="border-t border-border/60 mt-auto"
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
- {/* Logo + Name */}
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="lg:hidden border-t border-border/60 mt-auto"
187
- style={{
188
- background: 'linear-gradient(to bottom, hsl(var(--accent) / 0.26), hsl(var(--background) / 0.97) 42%, hsl(var(--accent) / 0.34))',
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
- <div className="mt-5 pt-4 border-t border-border/60 flex items-center justify-center gap-2">
246
- <ThemeModeControl />
247
- {i18n && (
248
- <LocaleSwitcher
249
- locale={i18n.locale}
250
- locales={i18n.locales}
251
- onChange={i18n.onLocaleChange}
252
- variant="outline"
253
- size="default"
254
- className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
255
- />
256
- )}
257
- </div>
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="max-lg:hidden border-t border-border/60 mt-auto"
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
- {showProjectInfo && (
245
+ {showBrandColumn && (
270
246
  <div className="col-span-12 lg:col-span-4">
271
- {projectInfo ?? (
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={showProjectInfo ? 'col-span-12 lg:col-span-8 lg:pl-8' : 'col-span-12'}>
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 className="mt-12 pt-5 border-t border-border/60 grid grid-cols-1 lg:grid-cols-[1fr_auto_1fr] items-center gap-4 lg:gap-6">
292
- <div className="text-xs text-muted-foreground text-center lg:text-left lg:justify-self-start">
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 flex-wrap">
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
- <div className="flex items-center justify-center lg:justify-self-end gap-2">
326
- <ThemeModeControl />
327
- {i18n && (
328
- <LocaleSwitcher
329
- locale={i18n.locale}
330
- locales={i18n.locales}
331
- onChange={i18n.onLocaleChange}
332
- variant="outline"
333
- size="default"
334
- className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
335
- />
336
- )}
337
- </div>
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,