@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
|
@@ -0,0 +1,127 @@
|
|
|
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 { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
15
|
+
import type { NavAction } from '../../primitives/NavActionItem';
|
|
16
|
+
import { NavbarShell } from '../../shared';
|
|
17
|
+
import type {
|
|
18
|
+
PublicDesktopDropdownRenderer,
|
|
19
|
+
PublicNavbarHeight,
|
|
20
|
+
PublicNavbarPosition,
|
|
21
|
+
PublicNavbarShellConfig,
|
|
22
|
+
PublicNavLayout,
|
|
23
|
+
} from '../../navbarTypes';
|
|
24
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
25
|
+
|
|
26
|
+
import { FloatingMobileDrawer } from './FloatingMobileDrawer';
|
|
27
|
+
|
|
28
|
+
export interface FloatingNavbarConfig {
|
|
29
|
+
shell?: PublicNavbarShellConfig;
|
|
30
|
+
brand?: React.ReactNode;
|
|
31
|
+
/** @default '/' */
|
|
32
|
+
brandHref?: string;
|
|
33
|
+
navigation?: NavigationItem[];
|
|
34
|
+
userMenu?: UserMenuConfig;
|
|
35
|
+
/** @default 'sticky' */
|
|
36
|
+
navbarPosition?: PublicNavbarPosition;
|
|
37
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
38
|
+
desktopMaxPrimaryItems?: number;
|
|
39
|
+
/** @default 'default' */
|
|
40
|
+
navLayout?: PublicNavLayout;
|
|
41
|
+
/** @default 'md' */
|
|
42
|
+
navbarHeight?: PublicNavbarHeight;
|
|
43
|
+
/** @default false */
|
|
44
|
+
hideNavOnScroll?: boolean;
|
|
45
|
+
/** @default false */
|
|
46
|
+
transparent?: boolean;
|
|
47
|
+
/** @default 40 */
|
|
48
|
+
transparentThreshold?: number;
|
|
49
|
+
/** Typed CTA pills (Book a demo / Get started / …) before UserMenu. */
|
|
50
|
+
actions?: NavAction[];
|
|
51
|
+
/** Arbitrary ReactNode between actions and UserMenu. */
|
|
52
|
+
actionsLeadingSlot?: React.ReactNode;
|
|
53
|
+
/** Arbitrary ReactNode after the mobile toggle. */
|
|
54
|
+
actionsTrailingSlot?: React.ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* Path prefixes that should be navigated with a plain `<a>` (full page load)
|
|
57
|
+
* instead of `next/link`. Use this for routes served by a catch-all handler
|
|
58
|
+
* outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
|
|
59
|
+
* cannot return an RSC payload, so `next/link` client navigation fails.
|
|
60
|
+
* @default []
|
|
61
|
+
*/
|
|
62
|
+
externalPrefixes?: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FloatingNavbarProps {
|
|
66
|
+
config: FloatingNavbarConfig;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
70
|
+
const navigation = config.navigation ?? [];
|
|
71
|
+
const rounding = config.shell?.rounding;
|
|
72
|
+
const containerClassName = config.shell?.className;
|
|
73
|
+
const position = config.navbarPosition ?? 'sticky';
|
|
74
|
+
const externalPrefixes = config.externalPrefixes;
|
|
75
|
+
|
|
76
|
+
const outerClassName = cn(
|
|
77
|
+
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
78
|
+
'top-3 px-3 sm:px-4 lg:px-6',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const shapeClassName = cn(
|
|
82
|
+
'mx-auto w-full',
|
|
83
|
+
rounding ?? 'rounded-2xl',
|
|
84
|
+
publicFloatingChromeClassName,
|
|
85
|
+
containerClassName,
|
|
86
|
+
'!border-0 dark:!border dark:!border-border/75',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
91
|
+
<NavbarShell
|
|
92
|
+
variant="floating"
|
|
93
|
+
position={position}
|
|
94
|
+
brand={config.brand}
|
|
95
|
+
brandHref={config.brandHref}
|
|
96
|
+
navigation={navigation}
|
|
97
|
+
userMenu={config.userMenu}
|
|
98
|
+
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
99
|
+
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
100
|
+
navLayout={config.navLayout}
|
|
101
|
+
navbarHeight={config.navbarHeight}
|
|
102
|
+
hideNavOnScroll={config.hideNavOnScroll}
|
|
103
|
+
transparent={config.transparent}
|
|
104
|
+
transparentThreshold={config.transparentThreshold}
|
|
105
|
+
actions={config.actions}
|
|
106
|
+
actionsLeadingSlot={config.actionsLeadingSlot}
|
|
107
|
+
actionsTrailingSlot={config.actionsTrailingSlot}
|
|
108
|
+
outerClassName={outerClassName}
|
|
109
|
+
shapeClassName={shapeClassName}
|
|
110
|
+
shapeForState={({ scrolled, transparent }) =>
|
|
111
|
+
cn(
|
|
112
|
+
transparent && 'transition-[background-color,backdrop-filter] duration-200 ease-out',
|
|
113
|
+
!transparent || scrolled
|
|
114
|
+
? 'bg-background/72 backdrop-blur-[10px] dark:bg-card/80'
|
|
115
|
+
: 'bg-transparent backdrop-blur-0 dark:bg-transparent',
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
<FloatingMobileDrawer
|
|
120
|
+
navigation={navigation}
|
|
121
|
+
userMenu={config.userMenu}
|
|
122
|
+
containerClassName={containerClassName}
|
|
123
|
+
rounding={rounding}
|
|
124
|
+
/>
|
|
125
|
+
</ExternalPrefixesProvider>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlushNavbar — edge-to-edge bar with a subtle bottom border.
|
|
3
|
+
*
|
|
4
|
+
* Pairs with FlushMobileDrawer (no 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 { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
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 { FlushMobileDrawer } from './FlushMobileDrawer';
|
|
26
|
+
|
|
27
|
+
export interface FlushNavbarConfig {
|
|
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
|
+
* Path prefixes that should be navigated with a plain `<a>` (full page load)
|
|
56
|
+
* instead of `next/link`. Use this for routes served by a catch-all handler
|
|
57
|
+
* outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
|
|
58
|
+
* cannot return an RSC payload, so `next/link` client navigation fails.
|
|
59
|
+
* @default []
|
|
60
|
+
*/
|
|
61
|
+
externalPrefixes?: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FlushNavbarProps {
|
|
65
|
+
config: FlushNavbarConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
69
|
+
const navigation = config.navigation ?? [];
|
|
70
|
+
const containerClassName = config.shell?.className;
|
|
71
|
+
const position = config.navbarPosition ?? 'sticky';
|
|
72
|
+
const externalPrefixes = config.externalPrefixes;
|
|
73
|
+
|
|
74
|
+
const outerClassName = cn(
|
|
75
|
+
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
76
|
+
'top-0',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const shapeClassName = cn(
|
|
80
|
+
'mx-auto w-full',
|
|
81
|
+
'rounded-none border-x-0 border-t-0 border-b border-border/40 dark:border-border/70 shadow-none',
|
|
82
|
+
containerClassName,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
87
|
+
<NavbarShell
|
|
88
|
+
variant="flush"
|
|
89
|
+
position={position}
|
|
90
|
+
brand={config.brand}
|
|
91
|
+
brandHref={config.brandHref}
|
|
92
|
+
navigation={navigation}
|
|
93
|
+
userMenu={config.userMenu}
|
|
94
|
+
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
95
|
+
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
96
|
+
navLayout={config.navLayout}
|
|
97
|
+
navbarHeight={config.navbarHeight}
|
|
98
|
+
hideNavOnScroll={config.hideNavOnScroll}
|
|
99
|
+
transparent={config.transparent}
|
|
100
|
+
transparentThreshold={config.transparentThreshold}
|
|
101
|
+
actions={config.actions}
|
|
102
|
+
actionsLeadingSlot={config.actionsLeadingSlot}
|
|
103
|
+
actionsTrailingSlot={config.actionsTrailingSlot}
|
|
104
|
+
outerClassName={outerClassName}
|
|
105
|
+
shapeClassName={shapeClassName}
|
|
106
|
+
shapeForState={({ scrolled, transparent }) =>
|
|
107
|
+
cn(
|
|
108
|
+
transparent && 'transition-[background-color,backdrop-filter] duration-200 ease-out',
|
|
109
|
+
!transparent || scrolled
|
|
110
|
+
? 'bg-background/72 backdrop-blur-[10px] dark:bg-card/80'
|
|
111
|
+
: 'bg-transparent backdrop-blur-0 dark:bg-transparent',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
<FlushMobileDrawer
|
|
116
|
+
navigation={navigation}
|
|
117
|
+
userMenu={config.userMenu}
|
|
118
|
+
containerClassName={containerClassName}
|
|
119
|
+
/>
|
|
120
|
+
</ExternalPrefixesProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { MobileDrawerShell, type MobileDrawerShellProps } from '../../shared';
|
|
6
|
+
|
|
7
|
+
type MinimalMobileDrawerProps = Omit<MobileDrawerShellProps, 'panelClassName' | 'outerClassName'> & {
|
|
8
|
+
containerClassName?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function MinimalMobileDrawer({ containerClassName, ...rest }: MinimalMobileDrawerProps) {
|
|
12
|
+
return (
|
|
13
|
+
<MobileDrawerShell
|
|
14
|
+
{...rest}
|
|
15
|
+
outerClassName={containerClassName}
|
|
16
|
+
panelClassName="rounded-none shadow-none border-0"
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MinimalNavbar — borderless, transparent-by-default, eesel-style.
|
|
3
|
+
*
|
|
4
|
+
* Differences from Floating/Flush:
|
|
5
|
+
* - no shell border/shadow, no rounded chrome
|
|
6
|
+
* - default container: max-w-[1400px], px-4 lg:px-10
|
|
7
|
+
* - right-hand side: pill CTAs + UserMenu + mobile toggle (no default UserMenu-only)
|
|
8
|
+
* - transparent at top of page by default; solid + backdrop-blur after scroll
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use client';
|
|
12
|
+
|
|
13
|
+
import { Menu, X } from 'lucide-react';
|
|
14
|
+
import React, { type ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
17
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
|
+
|
|
19
|
+
import { UserMenu } from '../../../_components/UserMenu';
|
|
20
|
+
import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
21
|
+
import { NavActionItem, type NavAction } from '../../primitives/NavActionItem';
|
|
22
|
+
import { NavbarShell, type NavbarActionsContext } from '../../shared';
|
|
23
|
+
import type {
|
|
24
|
+
PublicDesktopDropdownRenderer,
|
|
25
|
+
PublicNavbarHeight,
|
|
26
|
+
PublicNavbarPosition,
|
|
27
|
+
PublicNavLayout,
|
|
28
|
+
} from '../../navbarTypes';
|
|
29
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
30
|
+
|
|
31
|
+
import { MinimalMobileDrawer } from './MinimalMobileDrawer';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use `NavAction` from `@djangocfg/layouts`. Kept as an alias so
|
|
35
|
+
* existing imports keep working.
|
|
36
|
+
*/
|
|
37
|
+
export type MinimalNavbarAction = NavAction;
|
|
38
|
+
|
|
39
|
+
export interface MinimalNavbarConfig {
|
|
40
|
+
brand?: ReactNode;
|
|
41
|
+
/** @default '/' */
|
|
42
|
+
brandHref?: string;
|
|
43
|
+
navigation?: NavigationItem[];
|
|
44
|
+
userMenu?: UserMenuConfig;
|
|
45
|
+
/** Right-hand CTA pills. Rendered before UserMenu / mobile toggle. */
|
|
46
|
+
actions?: NavAction[];
|
|
47
|
+
|
|
48
|
+
/** @default 'sticky' */
|
|
49
|
+
navbarPosition?: PublicNavbarPosition;
|
|
50
|
+
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
51
|
+
desktopMaxPrimaryItems?: number;
|
|
52
|
+
/** @default 'default' */
|
|
53
|
+
navLayout?: PublicNavLayout;
|
|
54
|
+
/** @default 'md' */
|
|
55
|
+
navbarHeight?: PublicNavbarHeight;
|
|
56
|
+
/** @default false */
|
|
57
|
+
hideNavOnScroll?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Transparent at page top, opaque after `transparentThreshold`.
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
transparent?: boolean;
|
|
63
|
+
/** @default 40 */
|
|
64
|
+
transparentThreshold?: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Outer wrapper container classes — e.g. `mx-auto max-w-[1400px] px-4 nav:px-10`.
|
|
68
|
+
* @default 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10'
|
|
69
|
+
*/
|
|
70
|
+
containerClassName?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Path prefixes that should be navigated with a plain `<a>` (full page load)
|
|
74
|
+
* instead of `next/link`. Use this for routes served by a catch-all handler
|
|
75
|
+
* outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
|
|
76
|
+
* cannot return an RSC payload, so `next/link` client navigation fails.
|
|
77
|
+
* @default []
|
|
78
|
+
*/
|
|
79
|
+
externalPrefixes?: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MinimalNavbarProps {
|
|
83
|
+
config: MinimalNavbarConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function MinimalActions({
|
|
87
|
+
ctx,
|
|
88
|
+
actions = [],
|
|
89
|
+
}: {
|
|
90
|
+
ctx: NavbarActionsContext;
|
|
91
|
+
actions?: NavAction[];
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex shrink-0 items-center gap-4">
|
|
95
|
+
{actions.length > 0 && (
|
|
96
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
97
|
+
{actions.map((a) => (
|
|
98
|
+
<NavActionItem key={`${a.label}-${a.href}`} action={a} />
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div className="hidden lg:flex">
|
|
104
|
+
<UserMenu
|
|
105
|
+
variant="desktop"
|
|
106
|
+
groups={ctx.userMenu?.groups}
|
|
107
|
+
authPath={ctx.userMenu?.authPath}
|
|
108
|
+
i18n={ctx.userMenu?.i18n}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<Button
|
|
113
|
+
variant="ghost"
|
|
114
|
+
size="icon"
|
|
115
|
+
aria-label={ctx.toggleMobileLabel}
|
|
116
|
+
data-mobile-menu-trigger="true"
|
|
117
|
+
className={cn(
|
|
118
|
+
'rounded-full text-foreground/90 hover:bg-foreground/10',
|
|
119
|
+
ctx.navLayout === 'split' ? '' : 'lg:hidden',
|
|
120
|
+
)}
|
|
121
|
+
onClick={ctx.toggleMobileMenu}
|
|
122
|
+
>
|
|
123
|
+
{ctx.mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
130
|
+
const navigation = config.navigation ?? [];
|
|
131
|
+
const position = config.navbarPosition ?? 'sticky';
|
|
132
|
+
const transparent = config.transparent ?? true;
|
|
133
|
+
const containerClassName = config.containerClassName ?? 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10';
|
|
134
|
+
const externalPrefixes = config.externalPrefixes;
|
|
135
|
+
|
|
136
|
+
const outerClassName = cn(
|
|
137
|
+
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
138
|
+
'top-0',
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// No border, no rounding, no shadow. Full-bleed.
|
|
142
|
+
const shapeClassName = 'w-full rounded-none border-0 shadow-none';
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
146
|
+
<NavbarShell
|
|
147
|
+
variant="minimal"
|
|
148
|
+
position={position}
|
|
149
|
+
brand={config.brand}
|
|
150
|
+
brandHref={config.brandHref}
|
|
151
|
+
navigation={navigation}
|
|
152
|
+
userMenu={config.userMenu}
|
|
153
|
+
renderDesktopDropdown={config.renderDesktopDropdown}
|
|
154
|
+
desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
|
|
155
|
+
navLayout={config.navLayout}
|
|
156
|
+
navbarHeight={config.navbarHeight}
|
|
157
|
+
hideNavOnScroll={config.hideNavOnScroll}
|
|
158
|
+
transparent={transparent}
|
|
159
|
+
transparentThreshold={config.transparentThreshold}
|
|
160
|
+
outerClassName={outerClassName}
|
|
161
|
+
shapeClassName={shapeClassName}
|
|
162
|
+
innerPadding={containerClassName}
|
|
163
|
+
shapeForState={({ scrolled }) =>
|
|
164
|
+
cn(
|
|
165
|
+
'transition-[background-color,backdrop-filter] duration-200 ease-out',
|
|
166
|
+
transparent && !scrolled
|
|
167
|
+
? 'bg-transparent backdrop-blur-0'
|
|
168
|
+
: 'bg-background/72 backdrop-blur-[10px] dark:bg-card/80',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
renderActions={(ctx) => <MinimalActions ctx={ctx} actions={config.actions} />}
|
|
172
|
+
/>
|
|
173
|
+
<MinimalMobileDrawer
|
|
174
|
+
navigation={navigation}
|
|
175
|
+
userMenu={config.userMenu}
|
|
176
|
+
containerClassName={containerClassName}
|
|
177
|
+
/>
|
|
178
|
+
</ExternalPrefixesProvider>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalPrefixesContext
|
|
3
|
+
*
|
|
4
|
+
* Provides a list of path prefixes that should be treated as "outside" the
|
|
5
|
+
* Next.js App Router (e.g. a catch-all served by Nextra). Consumers inside the
|
|
6
|
+
* navbar (`SmartNavLink`) check whether the `href` starts with any configured
|
|
7
|
+
* prefix; if so, they render a native `<a>` (full page load) instead of a
|
|
8
|
+
* `next/link` that would fetch an RSC payload the route cannot produce.
|
|
9
|
+
*
|
|
10
|
+
* Default: `[]` — so behaviour is unchanged for consumers who do not opt in.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
16
|
+
|
|
17
|
+
export type ExternalPrefixes = readonly string[];
|
|
18
|
+
|
|
19
|
+
const ExternalPrefixesContext = createContext<ExternalPrefixes>([]);
|
|
20
|
+
|
|
21
|
+
export interface ExternalPrefixesProviderProps {
|
|
22
|
+
/** List of path prefixes treated as external to the App Router. */
|
|
23
|
+
value?: ExternalPrefixes;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ExternalPrefixesProvider({
|
|
28
|
+
value,
|
|
29
|
+
children,
|
|
30
|
+
}: ExternalPrefixesProviderProps) {
|
|
31
|
+
const resolved = useMemo<ExternalPrefixes>(() => value ?? [], [value]);
|
|
32
|
+
return (
|
|
33
|
+
<ExternalPrefixesContext.Provider value={resolved}>
|
|
34
|
+
{children}
|
|
35
|
+
</ExternalPrefixesContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useExternalPrefixes(): ExternalPrefixes {
|
|
40
|
+
return useContext(ExternalPrefixesContext);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true if `href` is a string that starts with any of the provided
|
|
45
|
+
* external prefixes. Non-string hrefs (objects, `next/link` URL shape) always
|
|
46
|
+
* return false — those are App Router internal.
|
|
47
|
+
*
|
|
48
|
+
* Matches the prefix exactly or at a path-boundary (`/`, `?`, `#`) so that
|
|
49
|
+
* `/docs` in the prefix list matches `/docs`, `/docs/`, `/docs/foo`, `/docs?x=1`,
|
|
50
|
+
* `/docs#bar` — but NOT `/docsearch` (which should stay an App Router link).
|
|
51
|
+
*/
|
|
52
|
+
export function isExternalPrefixHref(
|
|
53
|
+
href: unknown,
|
|
54
|
+
prefixes: ExternalPrefixes,
|
|
55
|
+
): href is string {
|
|
56
|
+
if (typeof href !== 'string' || prefixes.length === 0) return false;
|
|
57
|
+
for (const prefix of prefixes) {
|
|
58
|
+
if (!prefix) continue;
|
|
59
|
+
if (href === prefix) return true;
|
|
60
|
+
if (
|
|
61
|
+
href.startsWith(`${prefix}/`) ||
|
|
62
|
+
href.startsWith(`${prefix}?`) ||
|
|
63
|
+
href.startsWith(`${prefix}#`)
|
|
64
|
+
) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavAction — shared CTA primitive used by every PublicLayout navbar.
|
|
3
|
+
*
|
|
4
|
+
* Previously lived on MinimalNavbar only. Promoted so Floating/Flush can accept
|
|
5
|
+
* the same typed CTA pills (Book a demo / Get started …) without a fork.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { type ReactNode } from 'react';
|
|
11
|
+
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { SmartNavLink } from './SmartNavLink';
|
|
15
|
+
|
|
16
|
+
export interface NavAction {
|
|
17
|
+
label: string;
|
|
18
|
+
href: string;
|
|
19
|
+
/** Open in a new tab. @default false */
|
|
20
|
+
external?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Visual style.
|
|
23
|
+
* - `link` — plain text link
|
|
24
|
+
* - `ghost` — transparent pill, fills on hover (default)
|
|
25
|
+
* - `outline` — bordered pill
|
|
26
|
+
* - `primary` — solid high-contrast pill (main CTA)
|
|
27
|
+
* @default 'ghost'
|
|
28
|
+
*/
|
|
29
|
+
variant?: 'link' | 'ghost' | 'outline' | 'primary';
|
|
30
|
+
/** Hide on screens below lg (useful for secondary CTAs). @default false */
|
|
31
|
+
hideOnSmall?: boolean;
|
|
32
|
+
/** Optional inline icon (use lucide-react or any ReactNode). */
|
|
33
|
+
icon?: ReactNode;
|
|
34
|
+
/** Custom click handler (still navigates via href unless preventDefault'd). */
|
|
35
|
+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const baseCls =
|
|
39
|
+
'inline-flex shrink-0 items-center justify-center gap-1.5 rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35';
|
|
40
|
+
|
|
41
|
+
const variantCls: Record<NonNullable<NavAction['variant']>, string> = {
|
|
42
|
+
link:
|
|
43
|
+
'px-1.5 py-1 text-foreground/85 hover:text-foreground',
|
|
44
|
+
ghost:
|
|
45
|
+
'px-4 py-1.5 min-h-9 text-foreground/90 hover:bg-accent/55 hover:text-foreground',
|
|
46
|
+
outline:
|
|
47
|
+
'px-4 py-1.5 min-h-9 border border-border/70 text-foreground/90 hover:bg-accent/40 hover:text-foreground',
|
|
48
|
+
primary:
|
|
49
|
+
'px-4 py-1.5 min-h-9 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface NavActionItemProps {
|
|
53
|
+
action: NavAction;
|
|
54
|
+
className?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function NavActionItem({ action, className }: NavActionItemProps) {
|
|
58
|
+
const variant = action.variant ?? 'ghost';
|
|
59
|
+
const cls = cn(
|
|
60
|
+
baseCls,
|
|
61
|
+
variantCls[variant],
|
|
62
|
+
action.hideOnSmall && 'hidden lg:inline-flex',
|
|
63
|
+
className,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const content = (
|
|
67
|
+
<>
|
|
68
|
+
{action.icon && (
|
|
69
|
+
<span className="inline-flex size-3.5 shrink-0 items-center justify-center" aria-hidden>
|
|
70
|
+
{action.icon}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
<span className="truncate">{action.label}</span>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (action.external) {
|
|
78
|
+
return (
|
|
79
|
+
<a
|
|
80
|
+
href={action.href}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
className={cls}
|
|
84
|
+
onClick={action.onClick}
|
|
85
|
+
>
|
|
86
|
+
{content}
|
|
87
|
+
</a>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return (
|
|
91
|
+
<SmartNavLink href={action.href} className={cls} onClick={action.onClick}>
|
|
92
|
+
{content}
|
|
93
|
+
</SmartNavLink>
|
|
94
|
+
);
|
|
95
|
+
}
|