@djangocfg/layouts 2.1.274 → 2.1.276
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 +52 -180
- package/package.json +18 -18
- package/src/layouts/AppLayout/AppLayout.tsx +14 -14
- package/src/layouts/PublicLayout/README.md +144 -0
- package/src/layouts/PublicLayout/{components/PublicFooter/PublicFooter.tsx → footers/DefaultFooter/DefaultFooter.tsx} +14 -8
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/DjangoCFGLogo.tsx +0 -6
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterBottom.tsx +0 -4
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterMenuSections.tsx +0 -4
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterProjectInfo.tsx +0 -4
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterSocialLinks.tsx +0 -5
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/index.ts +2 -12
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/types.ts +21 -26
- package/src/layouts/PublicLayout/footers/index.ts +1 -0
- package/src/layouts/PublicLayout/hooks/index.ts +1 -0
- package/src/layouts/PublicLayout/hooks/useResponsiveOverflow.ts +140 -0
- package/src/layouts/PublicLayout/index.ts +22 -22
- package/src/layouts/PublicLayout/navbarTypes.ts +27 -4
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingMobileDrawer.tsx +29 -0
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +117 -0
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushMobileDrawer.tsx +19 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +112 -0
- package/src/layouts/PublicLayout/navbars/FlushNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalMobileDrawer.tsx +19 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +169 -0
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +3 -0
- package/src/layouts/PublicLayout/navbars/index.ts +3 -0
- package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +94 -0
- package/src/layouts/PublicLayout/{components → primitives}/NavActions.tsx +26 -1
- package/src/layouts/PublicLayout/{components → primitives}/NavDesktopItems.tsx +100 -56
- package/src/layouts/PublicLayout/{components → primitives}/ThemeBrandMark.tsx +0 -8
- package/src/layouts/PublicLayout/primitives/index.ts +7 -0
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +205 -0
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +295 -0
- package/src/layouts/PublicLayout/shared/index.ts +4 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +0 -211
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +0 -99
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +0 -287
- package/src/layouts/PublicLayout/components/index.ts +0 -11
- /package/src/layouts/PublicLayout/{components → primitives}/NavBrand.tsx +0 -0
|
@@ -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
|
-
*
|
|
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
|
|
10
|
-
label: string;
|
|
11
|
-
path: string;
|
|
12
|
-
external?: boolean;
|
|
13
|
-
}
|
|
11
|
+
export type { FooterLink, FooterMenuSection, FooterSocialLinks };
|
|
14
12
|
|
|
15
|
-
export interface
|
|
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
|
|
76
|
-
config:
|
|
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
|
-
|
|
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,20 @@ 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';
|
|
23
|
-
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
|
-
export type {
|
|
34
|
-
PublicFooterProps,
|
|
35
|
-
PublicFooterConfig,
|
|
36
|
-
FooterProjectInfoProps,
|
|
37
|
-
} from './components/PublicFooter';
|
|
38
|
-
export { PublicLayoutProvider, usePublicLayout } from './context';
|
|
39
30
|
|
|
31
|
+
// Primitives (for users who want to build a custom navbar)
|
|
32
|
+
export { NavBrand, NavActions, NavActionItem, NavDesktopItems, ThemeBrandMark, ThemeBrandMarkImg } from './primitives';
|
|
33
|
+
export type { NavAction, ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './primitives';
|
|
34
|
+
|
|
35
|
+
// Navbar variants
|
|
36
|
+
export * from './navbars';
|
|
37
|
+
|
|
38
|
+
// Footer variants
|
|
39
|
+
export * from './footers';
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared navbar
|
|
2
|
+
* Shared navbar types — consumed by all navbar variants and PublicLayout main offset.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FloatingNavbar — rounded, inset shell with soft shadow.
|
|
3
|
+
*
|
|
4
|
+
* Pairs with FloatingMobileDrawer (matching rounding + shadow).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
|
|
11
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
+
|
|
13
|
+
import { publicFloatingChromeClassName } from '../../publicShellShadow';
|
|
14
|
+
import type { NavAction } from '../../primitives/NavActionItem';
|
|
15
|
+
import { NavbarShell } from '../../shared';
|
|
16
|
+
import type {
|
|
17
|
+
PublicDesktopDropdownRenderer,
|
|
18
|
+
PublicNavbarHeight,
|
|
19
|
+
PublicNavbarPosition,
|
|
20
|
+
PublicNavbarShellConfig,
|
|
21
|
+
PublicNavLayout,
|
|
22
|
+
} from '../../navbarTypes';
|
|
23
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
24
|
+
|
|
25
|
+
import { FloatingMobileDrawer } from './FloatingMobileDrawer';
|
|
26
|
+
|
|
27
|
+
export interface FloatingNavbarConfig {
|
|
28
|
+
shell?: PublicNavbarShellConfig;
|
|
29
|
+
brand?: React.ReactNode;
|
|
30
|
+
/** @default '/' */
|
|
31
|
+
brandHref?: string;
|
|
32
|
+
navigation?: NavigationItem[];
|
|
33
|
+
userMenu?: UserMenuConfig;
|
|
34
|
+
/** @default 'sticky' */
|
|
35
|
+
navbarPosition?: PublicNavbarPosition;
|
|
36
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
37
|
+
desktopMaxPrimaryItems?: number;
|
|
38
|
+
/** @default 'default' */
|
|
39
|
+
navLayout?: PublicNavLayout;
|
|
40
|
+
/** @default 'md' */
|
|
41
|
+
navbarHeight?: PublicNavbarHeight;
|
|
42
|
+
/** @default false */
|
|
43
|
+
hideNavOnScroll?: boolean;
|
|
44
|
+
/** @default false */
|
|
45
|
+
transparent?: boolean;
|
|
46
|
+
/** @default 40 */
|
|
47
|
+
transparentThreshold?: number;
|
|
48
|
+
/** Typed CTA pills (Book a demo / Get started / …) before UserMenu. */
|
|
49
|
+
actions?: NavAction[];
|
|
50
|
+
/** Arbitrary ReactNode between actions and UserMenu. */
|
|
51
|
+
actionsLeadingSlot?: React.ReactNode;
|
|
52
|
+
/** Arbitrary ReactNode after the mobile toggle. */
|
|
53
|
+
actionsTrailingSlot?: React.ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FloatingNavbarProps {
|
|
57
|
+
config: FloatingNavbarConfig;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
61
|
+
const navigation = config.navigation ?? [];
|
|
62
|
+
const rounding = config.shell?.rounding;
|
|
63
|
+
const containerClassName = config.shell?.className;
|
|
64
|
+
const position = config.navbarPosition ?? 'sticky';
|
|
65
|
+
|
|
66
|
+
const outerClassName = cn(
|
|
67
|
+
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
68
|
+
'top-3 px-3 sm:px-4 lg:px-6',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const shapeClassName = cn(
|
|
72
|
+
'mx-auto w-full',
|
|
73
|
+
rounding ?? 'rounded-2xl',
|
|
74
|
+
publicFloatingChromeClassName,
|
|
75
|
+
containerClassName,
|
|
76
|
+
'!border-0 dark:!border dark:!border-border/75',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<NavbarShell
|
|
82
|
+
variant="floating"
|
|
83
|
+
position={position}
|
|
84
|
+
brand={config.brand}
|
|
85
|
+
brandHref={config.brandHref}
|
|
86
|
+
navigation={navigation}
|
|
87
|
+
userMenu={config.userMenu}
|
|
88
|
+
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
89
|
+
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
90
|
+
navLayout={config.navLayout}
|
|
91
|
+
navbarHeight={config.navbarHeight}
|
|
92
|
+
hideNavOnScroll={config.hideNavOnScroll}
|
|
93
|
+
transparent={config.transparent}
|
|
94
|
+
transparentThreshold={config.transparentThreshold}
|
|
95
|
+
actions={config.actions}
|
|
96
|
+
actionsLeadingSlot={config.actionsLeadingSlot}
|
|
97
|
+
actionsTrailingSlot={config.actionsTrailingSlot}
|
|
98
|
+
outerClassName={outerClassName}
|
|
99
|
+
shapeClassName={shapeClassName}
|
|
100
|
+
shapeForState={({ scrolled, transparent }) =>
|
|
101
|
+
cn(
|
|
102
|
+
transparent && 'transition-[background-color,backdrop-filter] duration-200 ease-out',
|
|
103
|
+
!transparent || scrolled
|
|
104
|
+
? 'bg-background/72 backdrop-blur-[10px] dark:bg-card/80'
|
|
105
|
+
: 'bg-transparent backdrop-blur-0 dark:bg-transparent',
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
<FloatingMobileDrawer
|
|
110
|
+
navigation={navigation}
|
|
111
|
+
userMenu={config.userMenu}
|
|
112
|
+
containerClassName={containerClassName}
|
|
113
|
+
rounding={rounding}
|
|
114
|
+
/>
|
|
115
|
+
</>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { MobileDrawerShell, type MobileDrawerShellProps } from '../../shared';
|
|
6
|
+
|
|
7
|
+
type FlushMobileDrawerProps = Omit<MobileDrawerShellProps, 'panelClassName' | 'outerClassName'> & {
|
|
8
|
+
containerClassName?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function FlushMobileDrawer({ containerClassName, ...rest }: FlushMobileDrawerProps) {
|
|
12
|
+
return (
|
|
13
|
+
<MobileDrawerShell
|
|
14
|
+
{...rest}
|
|
15
|
+
outerClassName={containerClassName}
|
|
16
|
+
panelClassName="border border-border/40 dark:border-border/70 rounded-xl"
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|