@djangocfg/layouts 2.1.275 → 2.1.277

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 (42) hide show
  1. package/README.md +52 -180
  2. package/package.json +18 -18
  3. package/src/layouts/AppLayout/AppLayout.tsx +14 -14
  4. package/src/layouts/PublicLayout/README.md +144 -0
  5. package/src/layouts/PublicLayout/{components/PublicFooter/PublicFooter.tsx → footers/DefaultFooter/DefaultFooter.tsx} +21 -15
  6. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/DjangoCFGLogo.tsx +0 -6
  7. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterBottom.tsx +3 -7
  8. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterMenuSections.tsx +4 -7
  9. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterProjectInfo.tsx +0 -4
  10. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterSocialLinks.tsx +0 -5
  11. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/index.ts +2 -12
  12. package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/types.ts +21 -26
  13. package/src/layouts/PublicLayout/footers/index.ts +1 -0
  14. package/src/layouts/PublicLayout/hooks/index.ts +1 -0
  15. package/src/layouts/PublicLayout/hooks/useResponsiveOverflow.ts +140 -0
  16. package/src/layouts/PublicLayout/index.ts +38 -20
  17. package/src/layouts/PublicLayout/navbarTypes.ts +27 -4
  18. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingMobileDrawer.tsx +29 -0
  19. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +127 -0
  20. package/src/layouts/PublicLayout/navbars/FloatingNavbar/index.ts +3 -0
  21. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushMobileDrawer.tsx +19 -0
  22. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +122 -0
  23. package/src/layouts/PublicLayout/navbars/FlushNavbar/index.ts +3 -0
  24. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalMobileDrawer.tsx +19 -0
  25. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +180 -0
  26. package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +3 -0
  27. package/src/layouts/PublicLayout/navbars/index.ts +3 -0
  28. package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +69 -0
  29. package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +95 -0
  30. package/src/layouts/PublicLayout/{components → primitives}/NavActions.tsx +26 -1
  31. package/src/layouts/PublicLayout/{components → primitives}/NavBrand.tsx +4 -3
  32. package/src/layouts/PublicLayout/{components → primitives}/NavDesktopItems.tsx +105 -61
  33. package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +81 -0
  34. package/src/layouts/PublicLayout/{components → primitives}/ThemeBrandMark.tsx +0 -8
  35. package/src/layouts/PublicLayout/primitives/index.ts +18 -0
  36. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +205 -0
  37. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +295 -0
  38. package/src/layouts/PublicLayout/shared/index.ts +4 -0
  39. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +0 -211
  40. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +0 -99
  41. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +0 -287
  42. package/src/layouts/PublicLayout/components/index.ts +0 -11
@@ -1,13 +1,11 @@
1
1
  /**
2
- * Public Layout Footer
2
+ * Default Public Footer
3
3
  *
4
- * Professional, flexible footer component for PublicLayout
5
- * Supports desktop/mobile responsive layouts, social links, menu sections
4
+ * Professional, flexible footer with full / compact / simple variants.
6
5
  */
7
6
 
8
7
  'use client';
9
8
 
10
- import Link from 'next/link';
11
9
  import React, { useEffect, useState } from 'react';
12
10
  import { Laptop, Moon, Sun } from 'lucide-react';
13
11
 
@@ -15,10 +13,11 @@ import { Button } from '@djangocfg/ui-core/components';
15
13
  import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
16
14
 
17
15
  import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
16
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
18
17
  import { FooterMenuSections } from './FooterMenuSections';
19
18
  import { FooterProjectInfo } from './FooterProjectInfo';
20
19
 
21
- import type { PublicFooterProps } from './types';
20
+ import type { DefaultFooterProps } from './types';
22
21
 
23
22
  function ThemeModeControl() {
24
23
  const { theme, setTheme } = useThemeContext();
@@ -69,7 +68,7 @@ function ThemeModeControl() {
69
68
  );
70
69
  }
71
70
 
72
- export function PublicFooter({ config }: PublicFooterProps) {
71
+ export function DefaultFooter({ config }: DefaultFooterProps) {
73
72
  const variant = config.variant ?? 'full';
74
73
  const shellClass = config.shell?.className;
75
74
  const brandSlot = config.brand?.slot;
@@ -88,6 +87,7 @@ export function PublicFooter({ config }: PublicFooterProps) {
88
87
  const showLocaleSwitcher =
89
88
  config.controls?.showLocaleSwitcher !== false && Boolean(i18n);
90
89
  const showFooterControlsRow = showThemeSwitcher || showLocaleSwitcher;
90
+ const slots = config.slots;
91
91
 
92
92
  const currentYear = new Date().getFullYear();
93
93
  const copyright = copyrightProp || `© ${currentYear}. All rights reserved.`;
@@ -132,13 +132,13 @@ export function PublicFooter({ config }: PublicFooterProps) {
132
132
  {link.label}
133
133
  </a>
134
134
  ) : (
135
- <Link
135
+ <SmartNavLink
136
136
  key={link.path}
137
137
  href={link.path}
138
138
  className="text-sm text-muted-foreground hover:text-foreground transition-colors"
139
139
  >
140
140
  {link.label}
141
- </Link>
141
+ </SmartNavLink>
142
142
  )
143
143
  )}
144
144
  </div>
@@ -195,13 +195,13 @@ export function PublicFooter({ config }: PublicFooterProps) {
195
195
  {link.label}
196
196
  </a>
197
197
  ) : (
198
- <Link
198
+ <SmartNavLink
199
199
  key={link.path}
200
200
  href={link.path}
201
201
  className="text-xs text-muted-foreground hover:text-primary transition-colors"
202
202
  >
203
203
  {link.label}
204
- </Link>
204
+ </SmartNavLink>
205
205
  )
206
206
  )}
207
207
  </div>
@@ -241,6 +241,8 @@ export function PublicFooter({ config }: PublicFooterProps) {
241
241
  </footer>
242
242
  <footer className={`max-lg:hidden ${footerSurfaceClass} ${footerSafeBottom}`}>
243
243
  <div className={`mx-auto px-6 sm:px-8 lg:px-10 py-14 ${shellClass || 'w-full'}`}>
244
+ {slots?.aboveMenus && <div className="mb-10">{slots.aboveMenus}</div>}
245
+
244
246
  <div className="grid grid-cols-12 gap-10 lg:gap-14">
245
247
  {showBrandColumn && (
246
248
  <div className="col-span-12 lg:col-span-4">
@@ -264,6 +266,8 @@ export function PublicFooter({ config }: PublicFooterProps) {
264
266
  </div>
265
267
  </div>
266
268
 
269
+ {slots?.belowMenus && <div className="mt-10">{slots.belowMenus}</div>}
270
+
267
271
  <div
268
272
  className={
269
273
  showFooterControlsRow
@@ -271,8 +275,9 @@ export function PublicFooter({ config }: PublicFooterProps) {
271
275
  : 'mt-12 flex flex-col gap-4 border-t border-border/60 pt-5 lg:flex-row lg:items-center lg:justify-between'
272
276
  }
273
277
  >
274
- <div className="text-center text-xs text-muted-foreground lg:text-left lg:justify-self-start">
275
- {copyright}
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-start lg:text-left">
279
+ <span className="whitespace-nowrap">{copyright}</span>
280
+ {slots?.bottomStart}
276
281
  </div>
277
282
 
278
283
  <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,15 +302,16 @@ export function PublicFooter({ config }: PublicFooterProps) {
297
302
  {link.label}
298
303
  </a>
299
304
  ) : (
300
- <Link key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
305
+ <SmartNavLink key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
301
306
  {link.label}
302
- </Link>
307
+ </SmartNavLink>
303
308
  )
304
309
  )}
305
310
  </div>
306
311
 
307
- {showFooterControlsRow && (
312
+ {(showFooterControlsRow || slots?.bottomEnd) && (
308
313
  <div className="flex items-center justify-center gap-2 lg:justify-self-end">
314
+ {slots?.bottomEnd}
309
315
  {showThemeSwitcher && <ThemeModeControl />}
310
316
  {showLocaleSwitcher && i18n && (
311
317
  <LocaleSwitcher
@@ -1,9 +1,3 @@
1
- /**
2
- * DjangoCFG Logo Component
3
- *
4
- * Default SVG logo for DjangoCFG
5
- */
6
-
7
1
  'use client';
8
2
 
9
3
  import React from 'react';
@@ -1,12 +1,8 @@
1
- /**
2
- * Footer Bottom Section Component
3
- */
4
-
5
1
  'use client';
6
2
 
7
- import Link from 'next/link';
8
3
  import React from 'react';
9
4
 
5
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
10
6
  import { DjangoCFGLogo } from './DjangoCFGLogo';
11
7
 
12
8
  import type { FooterLink } from './types';
@@ -99,13 +95,13 @@ export function FooterBottom({
99
95
  {link.label}
100
96
  </a>
101
97
  ) : (
102
- <Link
98
+ <SmartNavLink
103
99
  key={link.path}
104
100
  href={link.path}
105
101
  className="text-xs text-muted-foreground hover:text-primary transition-colors"
106
102
  >
107
103
  {link.label}
108
- </Link>
104
+ </SmartNavLink>
109
105
  )
110
106
  )}
111
107
  </div>
@@ -1,12 +1,9 @@
1
- /**
2
- * Footer Menu Sections Component
3
- */
4
-
5
1
  'use client';
6
2
 
7
- import Link from 'next/link';
8
3
  import React from 'react';
9
4
 
5
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
6
+
10
7
  import type { FooterMenuSection } from './types';
11
8
 
12
9
  export interface FooterMenuSectionsProps {
@@ -37,12 +34,12 @@ export function FooterMenuSections({
37
34
  <ul className="space-y-2">
38
35
  {section.items.map((item) => (
39
36
  <li key={item.path}>
40
- <Link
37
+ <SmartNavLink
41
38
  href={item.path}
42
39
  className="text-sm text-foreground/90 hover:text-foreground transition-colors"
43
40
  >
44
41
  {item.label}
45
- </Link>
42
+ </SmartNavLink>
46
43
  </li>
47
44
  ))}
48
45
  </ul>
@@ -1,7 +1,3 @@
1
- /**
2
- * Footer Project Info Component
3
- */
4
-
5
1
  'use client';
6
2
 
7
3
  import React from 'react';
@@ -1,7 +1,3 @@
1
- /**
2
- * Footer Social Links Component
3
- */
4
-
5
1
  'use client';
6
2
 
7
3
  import {
@@ -34,7 +30,6 @@ export function FooterSocialLinksComponent({
34
30
  className = 'flex space-x-4',
35
31
  iconClassName = 'w-5 h-5',
36
32
  }: FooterSocialLinksProps) {
37
- // Prepare social links data BEFORE render
38
33
  const socialLinksData = socialLinks
39
34
  ? Object.entries(socialLinks)
40
35
  .filter(([_, url]) => url)
@@ -1,8 +1,4 @@
1
- /**
2
- * Public Footer Exports
3
- */
4
-
5
- export { PublicFooter } from './PublicFooter';
1
+ export { DefaultFooter } from './DefaultFooter';
6
2
  export { FooterProjectInfo } from './FooterProjectInfo';
7
3
  export type { FooterProjectInfoProps } from './FooterProjectInfo';
8
4
  export { FooterMenuSections } from './FooterMenuSections';
@@ -10,10 +6,4 @@ export { FooterBottom } from './FooterBottom';
10
6
  export { FooterSocialLinksComponent } from './FooterSocialLinks';
11
7
  export { DjangoCFGLogo } from './DjangoCFGLogo';
12
8
 
13
- export type {
14
- PublicFooterProps,
15
- PublicFooterConfig,
16
- FooterLink,
17
- FooterMenuSection,
18
- FooterSocialLinks,
19
- } from './types';
9
+ export type { DefaultFooterProps, DefaultFooterConfig } from './types';
@@ -1,35 +1,16 @@
1
1
  /**
2
- * Public Footer Types
2
+ * Default Footer types.
3
3
  */
4
4
 
5
5
  import type { LucideIcon } from 'lucide-react';
6
6
  import type { ReactNode } from 'react';
7
+
7
8
  import type { I18nLayoutConfig } from '../../../AppLayout/AppLayout';
9
+ import type { FooterLink, FooterMenuSection, FooterSocialLinks } from '../../../types';
8
10
 
9
- export interface FooterLink {
10
- label: string;
11
- path: string;
12
- external?: boolean;
13
- }
11
+ export type { FooterLink, FooterMenuSection, FooterSocialLinks };
14
12
 
15
- export interface FooterMenuSection {
16
- title: string;
17
- items: FooterLink[];
18
- }
19
-
20
- export interface FooterSocialLinks {
21
- github?: string;
22
- linkedin?: string;
23
- twitter?: string;
24
- telegram?: string;
25
- youtube?: string;
26
- facebook?: string;
27
- instagram?: string;
28
- whatsapp?: string;
29
- email?: string;
30
- }
31
-
32
- export interface PublicFooterConfig {
13
+ export interface DefaultFooterConfig {
33
14
  variant?: 'full' | 'compact' | 'simple';
34
15
  shell?: {
35
16
  className?: string;
@@ -70,8 +51,22 @@ export interface PublicFooterConfig {
70
51
  /** Requires `i18n`. @default true */
71
52
  showLocaleSwitcher?: boolean;
72
53
  };
54
+ /**
55
+ * Arbitrary ReactNode slots for custom content (newsletter, status badges,
56
+ * extra CTAs …). Only rendered in the `full` variant.
57
+ */
58
+ slots?: {
59
+ /** Rendered above the brand/menus grid. Full width. */
60
+ aboveMenus?: ReactNode;
61
+ /** Rendered between the menus grid and the bottom row. Full width. */
62
+ belowMenus?: ReactNode;
63
+ /** Rendered in the bottom row, next to the copyright. */
64
+ bottomStart?: ReactNode;
65
+ /** Rendered in the bottom row, next to controls. */
66
+ bottomEnd?: ReactNode;
67
+ };
73
68
  }
74
69
 
75
- export interface PublicFooterProps {
76
- config: PublicFooterConfig;
70
+ export interface DefaultFooterProps {
71
+ config: DefaultFooterConfig;
77
72
  }
@@ -0,0 +1 @@
1
+ export * from './DefaultFooter';
@@ -4,3 +4,4 @@ export type { UseDropdownMenuReturn } from './useDropdownMenu';
4
4
  export { useNavbarScroll } from './useNavbarScroll';
5
5
  export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './useNavbarScroll';
6
6
  export { useNavbarViewportVars } from './useNavbarViewportVars';
7
+ export { useResponsiveOverflow } from './useResponsiveOverflow';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * useResponsiveOverflow — figure out how many nav items fit in the available
3
+ * space, pushing the rest into a "More" dropdown.
4
+ *
5
+ * Strategy (SSR-safe, no flash):
6
+ * 1. On the server and first client render we return `count === total`; the
7
+ * caller renders every item inside an *invisible* measurement layer plus
8
+ * the live row (see caller). No layout is committed until the observer
9
+ * runs.
10
+ * 2. After mount, a ResizeObserver on the container + per-item width reads
11
+ * recompute `visibleCount` whenever the nav's available width changes.
12
+ * 3. We reserve space for a "More" chip when anything is overflowing, so the
13
+ * chip itself never pushes the last visible item out (which would cause
14
+ * thrash: n → n-1 → n → …).
15
+ */
16
+
17
+ 'use client';
18
+
19
+ import { useCallback, useEffect, useRef, useState, type RefCallback } from 'react';
20
+
21
+ interface Options {
22
+ total: number;
23
+ /** Approx width reserved for the "More" chip when overflow exists. */
24
+ moreWidth?: number;
25
+ /** Gap between items (must match the flex gap in px). */
26
+ gap?: number;
27
+ /** Min items that must always be visible (clamps lower bound to 1). */
28
+ minVisible?: number;
29
+ }
30
+
31
+ interface Result {
32
+ /** How many items fit. */
33
+ visibleCount: number;
34
+ /** Attach to the container that constrains width. */
35
+ containerRef: RefCallback<HTMLElement | null>;
36
+ /** Attach to each item in the measurement layer (call for every index 0..total-1). */
37
+ itemRef: (index: number) => RefCallback<HTMLElement | null>;
38
+ /** True once a measurement has happened — caller can hide the measure layer then. */
39
+ measured: boolean;
40
+ }
41
+
42
+ export function useResponsiveOverflow({
43
+ total,
44
+ moreWidth = 72,
45
+ gap = 4,
46
+ minVisible = 1,
47
+ }: Options): Result {
48
+ const [visibleCount, setVisibleCount] = useState<number>(total);
49
+ const [measured, setMeasured] = useState(false);
50
+
51
+ const containerElRef = useRef<HTMLElement | null>(null);
52
+ const itemElsRef = useRef<Array<HTMLElement | null>>([]);
53
+ const roRef = useRef<ResizeObserver | null>(null);
54
+
55
+ const recompute = useCallback(() => {
56
+ const container = containerElRef.current;
57
+ if (!container) return;
58
+ const available = container.clientWidth;
59
+ if (available <= 0) return;
60
+
61
+ const widths: number[] = [];
62
+ for (let i = 0; i < total; i++) {
63
+ const el = itemElsRef.current[i];
64
+ widths.push(el ? Math.ceil(el.getBoundingClientRect().width) : 0);
65
+ }
66
+
67
+ // Try to fit all first.
68
+ let usedAll = 0;
69
+ for (let i = 0; i < total; i++) {
70
+ usedAll += widths[i] + (i > 0 ? gap : 0);
71
+ }
72
+ if (usedAll <= available) {
73
+ setVisibleCount(total);
74
+ setMeasured(true);
75
+ return;
76
+ }
77
+
78
+ // Some overflow: reserve space for the "More" chip.
79
+ const budget = available - moreWidth - gap;
80
+ let used = 0;
81
+ let fit = 0;
82
+ for (let i = 0; i < total; i++) {
83
+ const next = used + widths[i] + (i > 0 ? gap : 0);
84
+ if (next > budget) break;
85
+ used = next;
86
+ fit = i + 1;
87
+ }
88
+
89
+ const clamped = Math.max(minVisible, Math.min(total, fit));
90
+ setVisibleCount(clamped);
91
+ setMeasured(true);
92
+ }, [total, moreWidth, gap, minVisible]);
93
+
94
+ // Recompute when `total` changes (items added/removed).
95
+ useEffect(() => {
96
+ itemElsRef.current.length = total;
97
+ recompute();
98
+ }, [total, recompute]);
99
+
100
+ const containerRef = useCallback<RefCallback<HTMLElement | null>>(
101
+ (node) => {
102
+ // Tear down previous observer.
103
+ if (roRef.current) {
104
+ roRef.current.disconnect();
105
+ roRef.current = null;
106
+ }
107
+ containerElRef.current = node;
108
+ if (!node) return;
109
+ if (typeof ResizeObserver === 'undefined') {
110
+ recompute();
111
+ return;
112
+ }
113
+ const ro = new ResizeObserver(() => recompute());
114
+ ro.observe(node);
115
+ roRef.current = ro;
116
+ // Initial measurement — rAF to let children mount.
117
+ requestAnimationFrame(recompute);
118
+ },
119
+ [recompute],
120
+ );
121
+
122
+ const itemRef = useCallback(
123
+ (index: number): RefCallback<HTMLElement | null> =>
124
+ (node) => {
125
+ itemElsRef.current[index] = node;
126
+ },
127
+ [],
128
+ );
129
+
130
+ useEffect(() => {
131
+ return () => {
132
+ if (roRef.current) {
133
+ roRef.current.disconnect();
134
+ roRef.current = null;
135
+ }
136
+ };
137
+ }, []);
138
+
139
+ return { visibleCount, containerRef, itemRef, measured };
140
+ }
@@ -1,10 +1,18 @@
1
1
  /**
2
2
  * Public Layout exports
3
+ *
4
+ * Each navbar and footer is a self-contained variant — pick one and pass it
5
+ * into `PublicLayout` via the `navbar` / `footer` slots.
3
6
  */
4
7
 
8
+ // Layout container
5
9
  export { PublicLayout } from './PublicLayout';
6
10
  export type { PublicLayoutProps } from './PublicLayout';
7
- export { PublicNavigation, PublicMobileDrawer, PublicNavbar } from './components';
11
+
12
+ // Context
13
+ export { PublicLayoutProvider, usePublicLayout } from './context';
14
+
15
+ // Shared types
8
16
  export type {
9
17
  PublicNavbarSurface,
10
18
  PublicNavbarVariant,
@@ -12,28 +20,38 @@ export type {
12
20
  PublicNavbarShellConfig,
13
21
  PublicNavLayout,
14
22
  PublicNavbarHeight,
23
+ PublicDesktopDropdownRenderer,
24
+ PublicDesktopDropdownRenderProps,
15
25
  } from './navbarTypes';
26
+
27
+ // Hook types (consumers occasionally want them)
16
28
  export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './hooks/useNavbarScroll';
17
29
  export type { UseDropdownMenuReturn } from './hooks/useDropdownMenu';
18
- export type {
19
- PublicDesktopDropdownRenderer,
20
- PublicDesktopDropdownRenderProps,
21
- } from './components/PublicNavigation';
22
- export type { PublicNavbarConfig, PublicNavbarProps } from './components/PublicNavbar';
30
+
31
+ // Primitives (for users who want to build a custom navbar)
23
32
  export {
24
- PublicFooter,
25
- FooterProjectInfo,
26
- FooterMenuSections,
27
- FooterBottom,
28
- FooterSocialLinksComponent,
29
- DjangoCFGLogo,
30
- } from './components/PublicFooter';
31
- export { ThemeBrandMark, ThemeBrandMarkImg } from './components/ThemeBrandMark';
32
- export type { ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './components/ThemeBrandMark';
33
+ NavBrand,
34
+ NavActions,
35
+ NavActionItem,
36
+ NavDesktopItems,
37
+ ThemeBrandMark,
38
+ ThemeBrandMarkImg,
39
+ ExternalPrefixesProvider,
40
+ useExternalPrefixes,
41
+ isExternalPrefixHref,
42
+ SmartNavLink,
43
+ } from './primitives';
33
44
  export type {
34
- PublicFooterProps,
35
- PublicFooterConfig,
36
- FooterProjectInfoProps,
37
- } from './components/PublicFooter';
38
- export { PublicLayoutProvider, usePublicLayout } from './context';
45
+ NavAction,
46
+ ThemeBrandMarkProps,
47
+ ThemeBrandMarkImgProps,
48
+ ExternalPrefixes,
49
+ ExternalPrefixesProviderProps,
50
+ SmartNavLinkProps,
51
+ } from './primitives';
52
+
53
+ // Navbar variants
54
+ export * from './navbars';
39
55
 
56
+ // Footer variants
57
+ export * from './footers';
@@ -1,8 +1,18 @@
1
1
  /**
2
- * Shared navbar surface types — used by PublicNavigation (registration) and PublicLayout (main offset).
2
+ * Shared navbar types — consumed by all navbar variants and PublicLayout main offset.
3
3
  */
4
4
 
5
- export type PublicNavbarVariant = 'floating' | 'flush';
5
+ import type { ReactNode } from 'react';
6
+
7
+ import type { NavigationItem } from '../types';
8
+
9
+ /**
10
+ * Navbar chrome variant used for main-content top spacing.
11
+ * - `floating` — rounded, inset shell (extra top offset for main).
12
+ * - `flush` — edge-to-edge bar with bottom border.
13
+ * - `minimal` — no shell at all (eesel-style): transparent, borderless.
14
+ */
15
+ export type PublicNavbarVariant = 'floating' | 'flush' | 'minimal';
6
16
 
7
17
  export type PublicNavbarPosition = 'sticky' | 'fixed' | 'static';
8
18
 
@@ -18,7 +28,7 @@ export type PublicNavLayout = 'default' | 'brand-left' | 'centered' | 'split';
18
28
  /**
19
29
  * Navbar vertical padding / height.
20
30
  * - `sm` → py-2
21
- * - `md` → py-3.5 (default, matches current)
31
+ * - `md` → py-3.5 (default)
22
32
  * - `lg` → py-5
23
33
  */
24
34
  export type PublicNavbarHeight = 'sm' | 'md' | 'lg';
@@ -28,10 +38,23 @@ export interface PublicNavbarSurface {
28
38
  position: PublicNavbarPosition;
29
39
  }
30
40
 
31
- /** Floating bar + mobile drawer shell (rounding, width / centering). */
41
+ /** Outer shell (rounding, centering). Used by `floating` / `flush` navbars. */
32
42
  export interface PublicNavbarShellConfig {
33
43
  /** Tailwind rounding class (e.g. `rounded-3xl`). */
34
44
  rounding?: string;
35
45
  /** Strip + drawer wrapper (e.g. `mx-auto max-w-7xl`). */
36
46
  className?: string;
37
47
  }
48
+
49
+ // ─── Desktop dropdown renderer ───────────────────────────────────────────────
50
+
51
+ export interface PublicDesktopDropdownRenderProps {
52
+ item: NavigationItem;
53
+ isOpen: boolean;
54
+ isActive: boolean;
55
+ close: () => void;
56
+ defaultPopover: ReactNode;
57
+ defaultItems: ReactNode;
58
+ }
59
+
60
+ export type PublicDesktopDropdownRenderer = (props: PublicDesktopDropdownRenderProps) => ReactNode;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import { publicFloatingChromeClassName } from '../../publicShellShadow';
8
+ import { MobileDrawerShell, type MobileDrawerShellProps } from '../../shared';
9
+
10
+ type FloatingMobileDrawerProps = Omit<MobileDrawerShellProps, 'panelClassName' | 'outerClassName'> & {
11
+ /** Tailwind rounding for the drawer panel. @default 'rounded-2xl' */
12
+ rounding?: string;
13
+ /** Outer wrapper className (e.g. `mx-auto max-w-7xl`). */
14
+ containerClassName?: string;
15
+ };
16
+
17
+ export function FloatingMobileDrawer({
18
+ rounding,
19
+ containerClassName,
20
+ ...rest
21
+ }: FloatingMobileDrawerProps) {
22
+ return (
23
+ <MobileDrawerShell
24
+ {...rest}
25
+ panelClassName={cn(publicFloatingChromeClassName, rounding ?? 'rounded-2xl')}
26
+ outerClassName={containerClassName}
27
+ />
28
+ );
29
+ }