@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.
- 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} +21 -15
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/DjangoCFGLogo.tsx +0 -6
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterBottom.tsx +3 -7
- package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterMenuSections.tsx +4 -7
- 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 +38 -20
- 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 +127 -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 +122 -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 +180 -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/ExternalPrefixesContext.tsx +69 -0
- package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +95 -0
- package/src/layouts/PublicLayout/{components → primitives}/NavActions.tsx +26 -1
- package/src/layouts/PublicLayout/{components → primitives}/NavBrand.tsx +4 -3
- package/src/layouts/PublicLayout/{components → primitives}/NavDesktopItems.tsx +105 -61
- package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +81 -0
- package/src/layouts/PublicLayout/{components → primitives}/ThemeBrandMark.tsx +0 -8
- package/src/layouts/PublicLayout/primitives/index.ts +18 -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
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public
|
|
2
|
+
* Default Public Footer
|
|
3
3
|
*
|
|
4
|
-
* Professional, flexible footer
|
|
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 {
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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:
|
|
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
|
-
<
|
|
305
|
+
<SmartNavLink key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
|
|
301
306
|
{link.label}
|
|
302
|
-
</
|
|
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
|
package/src/layouts/PublicLayout/{components/PublicFooter → footers/DefaultFooter}/FooterBottom.tsx
RENAMED
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
42
|
+
</SmartNavLink>
|
|
46
43
|
</li>
|
|
47
44
|
))}
|
|
48
45
|
</ul>
|
|
@@ -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,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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
+
}
|