@djangocfg/layouts 2.1.276 → 2.1.279
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/package.json +19 -19
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +7 -7
- package/src/layouts/PublicLayout/footers/DefaultFooter/FooterBottom.tsx +3 -3
- package/src/layouts/PublicLayout/footers/DefaultFooter/FooterMenuSections.tsx +4 -3
- package/src/layouts/PublicLayout/index.ts +20 -2
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +12 -2
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +12 -2
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +13 -2
- package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +69 -0
- package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +4 -3
- package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -3
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +7 -7
- package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +81 -0
- package/src/layouts/PublicLayout/primitives/index.ts +11 -0
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.279",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,19 +74,19 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/debuger": "^2.1.
|
|
80
|
-
"@djangocfg/i18n": "^2.1.
|
|
81
|
-
"@djangocfg/monitor": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
83
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
84
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.279",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.279",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.279",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.279",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.279",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.279",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.279",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.279",
|
|
85
85
|
"@hookform/resolvers": "^5.2.2",
|
|
86
86
|
"consola": "^3.4.2",
|
|
87
87
|
"lucide-react": "^0.545.0",
|
|
88
88
|
"moment": "^2.30.1",
|
|
89
|
-
"next": "^16.
|
|
89
|
+
"next": "^16.2.2",
|
|
90
90
|
"p-retry": "^7.0.0",
|
|
91
91
|
"react": "^19.1.0",
|
|
92
92
|
"react-dom": "^19.1.0",
|
|
@@ -110,15 +110,15 @@
|
|
|
110
110
|
"uuid": "^11.1.0"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@djangocfg/api": "^2.1.
|
|
114
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
115
|
-
"@djangocfg/debuger": "^2.1.
|
|
116
|
-
"@djangocfg/i18n": "^2.1.
|
|
117
|
-
"@djangocfg/monitor": "^2.1.
|
|
118
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
119
|
-
"@djangocfg/ui-core": "^2.1.
|
|
120
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
121
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
113
|
+
"@djangocfg/api": "^2.1.279",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.279",
|
|
115
|
+
"@djangocfg/debuger": "^2.1.279",
|
|
116
|
+
"@djangocfg/i18n": "^2.1.279",
|
|
117
|
+
"@djangocfg/monitor": "^2.1.279",
|
|
118
|
+
"@djangocfg/typescript-config": "^2.1.279",
|
|
119
|
+
"@djangocfg/ui-core": "^2.1.279",
|
|
120
|
+
"@djangocfg/ui-nextjs": "^2.1.279",
|
|
121
|
+
"@djangocfg/ui-tools": "^2.1.279",
|
|
122
122
|
"@types/node": "^24.7.2",
|
|
123
123
|
"@types/react": "^19.1.0",
|
|
124
124
|
"@types/react-dom": "^19.1.0",
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
|
-
import Link from 'next/link';
|
|
10
9
|
import React, { useEffect, useState } from 'react';
|
|
11
10
|
import { Laptop, Moon, Sun } from 'lucide-react';
|
|
12
11
|
|
|
@@ -14,6 +13,7 @@ import { Button } from '@djangocfg/ui-core/components';
|
|
|
14
13
|
import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
|
|
15
14
|
|
|
16
15
|
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
16
|
+
import { SmartNavLink } from '../../primitives/SmartNavLink';
|
|
17
17
|
import { FooterMenuSections } from './FooterMenuSections';
|
|
18
18
|
import { FooterProjectInfo } from './FooterProjectInfo';
|
|
19
19
|
|
|
@@ -132,13 +132,13 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
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 DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
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>
|
|
@@ -302,9 +302,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
302
302
|
{link.label}
|
|
303
303
|
</a>
|
|
304
304
|
) : (
|
|
305
|
-
<
|
|
305
|
+
<SmartNavLink key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
|
|
306
306
|
{link.label}
|
|
307
|
-
</
|
|
307
|
+
</SmartNavLink>
|
|
308
308
|
)
|
|
309
309
|
)}
|
|
310
310
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import Link from 'next/link';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
|
|
5
|
+
import { SmartNavLink } from '../../primitives/SmartNavLink';
|
|
6
6
|
import { DjangoCFGLogo } from './DjangoCFGLogo';
|
|
7
7
|
|
|
8
8
|
import type { FooterLink } from './types';
|
|
@@ -95,13 +95,13 @@ export function FooterBottom({
|
|
|
95
95
|
{link.label}
|
|
96
96
|
</a>
|
|
97
97
|
) : (
|
|
98
|
-
<
|
|
98
|
+
<SmartNavLink
|
|
99
99
|
key={link.path}
|
|
100
100
|
href={link.path}
|
|
101
101
|
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
|
102
102
|
>
|
|
103
103
|
{link.label}
|
|
104
|
-
</
|
|
104
|
+
</SmartNavLink>
|
|
105
105
|
)
|
|
106
106
|
)}
|
|
107
107
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import Link from 'next/link';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
|
|
5
|
+
import { SmartNavLink } from '../../primitives/SmartNavLink';
|
|
6
|
+
|
|
6
7
|
import type { FooterMenuSection } from './types';
|
|
7
8
|
|
|
8
9
|
export interface FooterMenuSectionsProps {
|
|
@@ -33,12 +34,12 @@ export function FooterMenuSections({
|
|
|
33
34
|
<ul className="space-y-2">
|
|
34
35
|
{section.items.map((item) => (
|
|
35
36
|
<li key={item.path}>
|
|
36
|
-
<
|
|
37
|
+
<SmartNavLink
|
|
37
38
|
href={item.path}
|
|
38
39
|
className="text-sm text-foreground/90 hover:text-foreground transition-colors"
|
|
39
40
|
>
|
|
40
41
|
{item.label}
|
|
41
|
-
</
|
|
42
|
+
</SmartNavLink>
|
|
42
43
|
</li>
|
|
43
44
|
))}
|
|
44
45
|
</ul>
|
|
@@ -29,8 +29,26 @@ export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './hooks/useN
|
|
|
29
29
|
export type { UseDropdownMenuReturn } from './hooks/useDropdownMenu';
|
|
30
30
|
|
|
31
31
|
// Primitives (for users who want to build a custom navbar)
|
|
32
|
-
export {
|
|
33
|
-
|
|
32
|
+
export {
|
|
33
|
+
NavBrand,
|
|
34
|
+
NavActions,
|
|
35
|
+
NavActionItem,
|
|
36
|
+
NavDesktopItems,
|
|
37
|
+
ThemeBrandMark,
|
|
38
|
+
ThemeBrandMarkImg,
|
|
39
|
+
ExternalPrefixesProvider,
|
|
40
|
+
useExternalPrefixes,
|
|
41
|
+
isExternalPrefixHref,
|
|
42
|
+
SmartNavLink,
|
|
43
|
+
} from './primitives';
|
|
44
|
+
export type {
|
|
45
|
+
NavAction,
|
|
46
|
+
ThemeBrandMarkProps,
|
|
47
|
+
ThemeBrandMarkImgProps,
|
|
48
|
+
ExternalPrefixes,
|
|
49
|
+
ExternalPrefixesProviderProps,
|
|
50
|
+
SmartNavLinkProps,
|
|
51
|
+
} from './primitives';
|
|
34
52
|
|
|
35
53
|
// Navbar variants
|
|
36
54
|
export * from './navbars';
|
|
@@ -11,6 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
12
|
|
|
13
13
|
import { publicFloatingChromeClassName } from '../../publicShellShadow';
|
|
14
|
+
import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
14
15
|
import type { NavAction } from '../../primitives/NavActionItem';
|
|
15
16
|
import { NavbarShell } from '../../shared';
|
|
16
17
|
import type {
|
|
@@ -51,6 +52,14 @@ export interface FloatingNavbarConfig {
|
|
|
51
52
|
actionsLeadingSlot?: React.ReactNode;
|
|
52
53
|
/** Arbitrary ReactNode after the mobile toggle. */
|
|
53
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[];
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
export interface FloatingNavbarProps {
|
|
@@ -62,6 +71,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
62
71
|
const rounding = config.shell?.rounding;
|
|
63
72
|
const containerClassName = config.shell?.className;
|
|
64
73
|
const position = config.navbarPosition ?? 'sticky';
|
|
74
|
+
const externalPrefixes = config.externalPrefixes;
|
|
65
75
|
|
|
66
76
|
const outerClassName = cn(
|
|
67
77
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -77,7 +87,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
77
87
|
);
|
|
78
88
|
|
|
79
89
|
return (
|
|
80
|
-
|
|
90
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
81
91
|
<NavbarShell
|
|
82
92
|
variant="floating"
|
|
83
93
|
position={position}
|
|
@@ -112,6 +122,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
112
122
|
containerClassName={containerClassName}
|
|
113
123
|
rounding={rounding}
|
|
114
124
|
/>
|
|
115
|
-
|
|
125
|
+
</ExternalPrefixesProvider>
|
|
116
126
|
);
|
|
117
127
|
}
|
|
@@ -10,6 +10,7 @@ import React from 'react';
|
|
|
10
10
|
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
12
|
|
|
13
|
+
import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
13
14
|
import type { NavAction } from '../../primitives/NavActionItem';
|
|
14
15
|
import { NavbarShell } from '../../shared';
|
|
15
16
|
import type {
|
|
@@ -50,6 +51,14 @@ export interface FlushNavbarConfig {
|
|
|
50
51
|
actionsLeadingSlot?: React.ReactNode;
|
|
51
52
|
/** Arbitrary ReactNode after the mobile toggle. */
|
|
52
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[];
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export interface FlushNavbarProps {
|
|
@@ -60,6 +69,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
60
69
|
const navigation = config.navigation ?? [];
|
|
61
70
|
const containerClassName = config.shell?.className;
|
|
62
71
|
const position = config.navbarPosition ?? 'sticky';
|
|
72
|
+
const externalPrefixes = config.externalPrefixes;
|
|
63
73
|
|
|
64
74
|
const outerClassName = cn(
|
|
65
75
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -73,7 +83,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
73
83
|
);
|
|
74
84
|
|
|
75
85
|
return (
|
|
76
|
-
|
|
86
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
77
87
|
<NavbarShell
|
|
78
88
|
variant="flush"
|
|
79
89
|
position={position}
|
|
@@ -107,6 +117,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
107
117
|
userMenu={config.userMenu}
|
|
108
118
|
containerClassName={containerClassName}
|
|
109
119
|
/>
|
|
110
|
-
|
|
120
|
+
</ExternalPrefixesProvider>
|
|
111
121
|
);
|
|
112
122
|
}
|
|
@@ -17,6 +17,7 @@ import { Button } from '@djangocfg/ui-core/components';
|
|
|
17
17
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
18
|
|
|
19
19
|
import { UserMenu } from '../../../_components/UserMenu';
|
|
20
|
+
import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
20
21
|
import { NavActionItem, type NavAction } from '../../primitives/NavActionItem';
|
|
21
22
|
import { NavbarShell, type NavbarActionsContext } from '../../shared';
|
|
22
23
|
import type {
|
|
@@ -67,6 +68,15 @@ export interface MinimalNavbarConfig {
|
|
|
67
68
|
* @default 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10'
|
|
68
69
|
*/
|
|
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[];
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface MinimalNavbarProps {
|
|
@@ -121,6 +131,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
121
131
|
const position = config.navbarPosition ?? 'sticky';
|
|
122
132
|
const transparent = config.transparent ?? true;
|
|
123
133
|
const containerClassName = config.containerClassName ?? 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10';
|
|
134
|
+
const externalPrefixes = config.externalPrefixes;
|
|
124
135
|
|
|
125
136
|
const outerClassName = cn(
|
|
126
137
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -131,7 +142,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
131
142
|
const shapeClassName = 'w-full rounded-none border-0 shadow-none';
|
|
132
143
|
|
|
133
144
|
return (
|
|
134
|
-
|
|
145
|
+
<ExternalPrefixesProvider value={externalPrefixes}>
|
|
135
146
|
<NavbarShell
|
|
136
147
|
variant="minimal"
|
|
137
148
|
position={position}
|
|
@@ -164,6 +175,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
164
175
|
userMenu={config.userMenu}
|
|
165
176
|
containerClassName={containerClassName}
|
|
166
177
|
/>
|
|
167
|
-
|
|
178
|
+
</ExternalPrefixesProvider>
|
|
168
179
|
);
|
|
169
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
|
+
}
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import Link from 'next/link';
|
|
11
10
|
import React, { type ReactNode } from 'react';
|
|
12
11
|
|
|
13
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
13
|
|
|
14
|
+
import { SmartNavLink } from './SmartNavLink';
|
|
15
|
+
|
|
15
16
|
export interface NavAction {
|
|
16
17
|
label: string;
|
|
17
18
|
href: string;
|
|
@@ -87,8 +88,8 @@ export function NavActionItem({ action, className }: NavActionItemProps) {
|
|
|
87
88
|
);
|
|
88
89
|
}
|
|
89
90
|
return (
|
|
90
|
-
<
|
|
91
|
+
<SmartNavLink href={action.href} className={cls} onClick={action.onClick}>
|
|
91
92
|
{content}
|
|
92
|
-
</
|
|
93
|
+
</SmartNavLink>
|
|
93
94
|
);
|
|
94
95
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import Link from 'next/link';
|
|
4
3
|
import React, { type ReactNode } from 'react';
|
|
5
4
|
|
|
5
|
+
import { SmartNavLink } from './SmartNavLink';
|
|
6
|
+
|
|
6
7
|
interface NavBrandProps {
|
|
7
8
|
brand?: ReactNode;
|
|
8
9
|
brandHref?: string;
|
|
@@ -13,12 +14,12 @@ export function NavBrand({ brand, brandHref = '/' }: NavBrandProps) {
|
|
|
13
14
|
|
|
14
15
|
if (typeof brand === 'string') {
|
|
15
16
|
return (
|
|
16
|
-
<
|
|
17
|
+
<SmartNavLink
|
|
17
18
|
href={brandHref}
|
|
18
19
|
className="font-bold text-[15px] text-foreground hover:opacity-90 transition-opacity"
|
|
19
20
|
>
|
|
20
21
|
{brand}
|
|
21
|
-
</
|
|
22
|
+
</SmartNavLink>
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ChevronDown } from 'lucide-react';
|
|
4
|
-
import Link from 'next/link';
|
|
5
4
|
import React from 'react';
|
|
6
5
|
|
|
7
6
|
import { Button } from '@djangocfg/ui-core/components';
|
|
@@ -11,6 +10,7 @@ import type { NavigationItem } from '../../types';
|
|
|
11
10
|
import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
|
|
12
11
|
import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
|
|
13
12
|
import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
|
|
13
|
+
import { SmartNavLink } from './SmartNavLink';
|
|
14
14
|
|
|
15
15
|
interface NavDesktopItemsProps {
|
|
16
16
|
/** Full ordered item list; overflow is computed responsively. */
|
|
@@ -91,9 +91,9 @@ export function NavDesktopItems({
|
|
|
91
91
|
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
92
92
|
</a>
|
|
93
93
|
) : (
|
|
94
|
-
<
|
|
94
|
+
<SmartNavLink href={sub.href} className={subMenuLinkCls(subActive)}>
|
|
95
95
|
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
96
|
-
</
|
|
96
|
+
</SmartNavLink>
|
|
97
97
|
)}
|
|
98
98
|
</div>
|
|
99
99
|
);
|
|
@@ -149,13 +149,13 @@ export function NavDesktopItems({
|
|
|
149
149
|
|
|
150
150
|
const active = isActivePath(item.href);
|
|
151
151
|
return (
|
|
152
|
-
<
|
|
152
|
+
<SmartNavLink
|
|
153
153
|
key={item.href}
|
|
154
154
|
href={item.href}
|
|
155
155
|
className={cn(navItemCls, active && navItemActiveCls)}
|
|
156
156
|
>
|
|
157
157
|
<span className={labelCls} title={item.label}>{item.label}</span>
|
|
158
|
-
</
|
|
158
|
+
</SmartNavLink>
|
|
159
159
|
);
|
|
160
160
|
};
|
|
161
161
|
|
|
@@ -235,9 +235,9 @@ export function NavDesktopItems({
|
|
|
235
235
|
const active = isGroupActive(item);
|
|
236
236
|
return (
|
|
237
237
|
<div key={`overflow-${item.href}`} className="rounded-full">
|
|
238
|
-
<
|
|
238
|
+
<SmartNavLink href={item.href} className={subMenuLinkCls(active)}>
|
|
239
239
|
<span className="min-w-0 truncate" title={item.label}>{item.label}</span>
|
|
240
|
-
</
|
|
240
|
+
</SmartNavLink>
|
|
241
241
|
</div>
|
|
242
242
|
);
|
|
243
243
|
})}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartNavLink
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for `next/link` used inside PublicLayout navbars/footers.
|
|
5
|
+
*
|
|
6
|
+
* If the `href` is a string that starts with one of the prefixes configured
|
|
7
|
+
* via `ExternalPrefixesProvider`, renders a plain `<a>` (full page navigation).
|
|
8
|
+
* This is required for routes owned by a catch-all handler outside App Router
|
|
9
|
+
* (e.g. Nextra `/docs/*`), where `next/link` client navigation asks for an
|
|
10
|
+
* RSC payload the route cannot produce — resulting in a hard error banner.
|
|
11
|
+
*
|
|
12
|
+
* Otherwise falls back to `next/link` so existing behaviour is preserved.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use client';
|
|
16
|
+
|
|
17
|
+
import Link, { type LinkProps } from 'next/link';
|
|
18
|
+
import React, { forwardRef, type AnchorHTMLAttributes, type ReactNode } from 'react';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
isExternalPrefixHref,
|
|
22
|
+
useExternalPrefixes,
|
|
23
|
+
type ExternalPrefixes,
|
|
24
|
+
} from './ExternalPrefixesContext';
|
|
25
|
+
|
|
26
|
+
type AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
|
|
27
|
+
|
|
28
|
+
export interface SmartNavLinkProps
|
|
29
|
+
extends Omit<LinkProps, 'href' | 'passHref' | 'legacyBehavior'>,
|
|
30
|
+
AnchorProps {
|
|
31
|
+
href: LinkProps['href'];
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
/** Optional override — falls back to context value. */
|
|
34
|
+
externalPrefixes?: ExternalPrefixes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SmartNavLink = forwardRef<HTMLAnchorElement, SmartNavLinkProps>(
|
|
38
|
+
function SmartNavLink(props, ref) {
|
|
39
|
+
const {
|
|
40
|
+
href,
|
|
41
|
+
children,
|
|
42
|
+
externalPrefixes,
|
|
43
|
+
// LinkProps-specific fields we should NOT forward to a plain <a>.
|
|
44
|
+
prefetch,
|
|
45
|
+
replace,
|
|
46
|
+
scroll,
|
|
47
|
+
shallow,
|
|
48
|
+
locale,
|
|
49
|
+
// Rest are anchor-compatible attributes.
|
|
50
|
+
...anchorProps
|
|
51
|
+
} = props;
|
|
52
|
+
|
|
53
|
+
const contextPrefixes = useExternalPrefixes();
|
|
54
|
+
const prefixes = externalPrefixes ?? contextPrefixes;
|
|
55
|
+
|
|
56
|
+
const shouldUseAnchor = isExternalPrefixHref(href, prefixes);
|
|
57
|
+
|
|
58
|
+
if (shouldUseAnchor) {
|
|
59
|
+
return (
|
|
60
|
+
<a ref={ref} href={href} {...anchorProps}>
|
|
61
|
+
{children}
|
|
62
|
+
</a>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Link
|
|
68
|
+
ref={ref}
|
|
69
|
+
href={href}
|
|
70
|
+
prefetch={prefetch}
|
|
71
|
+
replace={replace}
|
|
72
|
+
scroll={scroll}
|
|
73
|
+
shallow={shallow}
|
|
74
|
+
locale={locale}
|
|
75
|
+
{...anchorProps}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</Link>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
@@ -5,3 +5,14 @@ export type { NavAction } from './NavActionItem';
|
|
|
5
5
|
export { NavDesktopItems } from './NavDesktopItems';
|
|
6
6
|
export { ThemeBrandMark, ThemeBrandMarkImg } from './ThemeBrandMark';
|
|
7
7
|
export type { ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './ThemeBrandMark';
|
|
8
|
+
export {
|
|
9
|
+
ExternalPrefixesProvider,
|
|
10
|
+
useExternalPrefixes,
|
|
11
|
+
isExternalPrefixHref,
|
|
12
|
+
} from './ExternalPrefixesContext';
|
|
13
|
+
export type {
|
|
14
|
+
ExternalPrefixes,
|
|
15
|
+
ExternalPrefixesProviderProps,
|
|
16
|
+
} from './ExternalPrefixesContext';
|
|
17
|
+
export { SmartNavLink } from './SmartNavLink';
|
|
18
|
+
export type { SmartNavLinkProps } from './SmartNavLink';
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import { ArrowRight } from 'lucide-react';
|
|
11
|
-
import Link from 'next/link';
|
|
12
11
|
import React, { useMemo } from 'react';
|
|
13
12
|
|
|
14
13
|
import { useAuth } from '@djangocfg/api/auth';
|
|
@@ -20,6 +19,7 @@ import { usePathnameWithoutLocale } from '../../../hooks';
|
|
|
20
19
|
import { UserMenu } from '../../_components/UserMenu';
|
|
21
20
|
import { usePublicLayoutOptional } from '../context';
|
|
22
21
|
import { useMobileNavPanel } from '../hooks';
|
|
22
|
+
import { SmartNavLink } from '../primitives/SmartNavLink';
|
|
23
23
|
|
|
24
24
|
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
25
25
|
|
|
@@ -133,7 +133,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
133
133
|
const parentOnlySectionOpen = hasChildNav && anyChildActive;
|
|
134
134
|
return (
|
|
135
135
|
<div key={item.href}>
|
|
136
|
-
<
|
|
136
|
+
<SmartNavLink
|
|
137
137
|
href={item.href}
|
|
138
138
|
onClick={closeMobileMenu}
|
|
139
139
|
title={item.label}
|
|
@@ -148,13 +148,13 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
148
148
|
)}
|
|
149
149
|
>
|
|
150
150
|
{item.label}
|
|
151
|
-
</
|
|
151
|
+
</SmartNavLink>
|
|
152
152
|
{hasChildNav && (
|
|
153
153
|
<div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
|
|
154
154
|
{childItems.map((subItem) => {
|
|
155
155
|
const subActive = isActivePath(subItem.href);
|
|
156
156
|
return (
|
|
157
|
-
<
|
|
157
|
+
<SmartNavLink
|
|
158
158
|
key={`${item.href}-${subItem.href}`}
|
|
159
159
|
href={subItem.href}
|
|
160
160
|
onClick={closeMobileMenu}
|
|
@@ -168,7 +168,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
168
168
|
)}
|
|
169
169
|
>
|
|
170
170
|
{subItem.label}
|
|
171
|
-
</
|
|
171
|
+
</SmartNavLink>
|
|
172
172
|
);
|
|
173
173
|
})}
|
|
174
174
|
</div>
|
|
@@ -182,7 +182,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
182
182
|
|
|
183
183
|
{showSignInFooter && (
|
|
184
184
|
<div className="shrink-0 border-t border-border/50 p-4">
|
|
185
|
-
<
|
|
185
|
+
<SmartNavLink
|
|
186
186
|
href={userMenu?.authPath || '/auth'}
|
|
187
187
|
onClick={closeMobileMenu}
|
|
188
188
|
className="block"
|
|
@@ -194,7 +194,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
194
194
|
aria-hidden
|
|
195
195
|
/>
|
|
196
196
|
</Button>
|
|
197
|
-
</
|
|
197
|
+
</SmartNavLink>
|
|
198
198
|
</div>
|
|
199
199
|
)}
|
|
200
200
|
</div>
|