@djangocfg/layouts 2.1.277 → 2.1.280
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 +1 -1
- package/package.json +19 -19
- package/src/layouts/PublicLayout/README.md +25 -1
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +8 -7
- package/src/layouts/PublicLayout/footers/DefaultFooter/FooterBottom.tsx +4 -3
- package/src/layouts/PublicLayout/footers/DefaultFooter/FooterMenuSections.tsx +4 -3
- package/src/layouts/PublicLayout/index.ts +5 -7
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +2 -12
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +2 -12
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +2 -13
- package/src/layouts/PublicLayout/primitives/LinkComponentContext.tsx +50 -0
- package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +4 -3
- package/src/layouts/PublicLayout/primitives/NavBrand.tsx +5 -3
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +8 -7
- package/src/layouts/PublicLayout/primitives/index.ts +7 -9
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +8 -7
- package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +0 -69
- package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +0 -81
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ Wraps `BaseApp` and picks **admin → private → public** layout by path (`matc
|
|
|
87
87
|
|
|
88
88
|
| Component | Use |
|
|
89
89
|
|---|---|
|
|
90
|
-
| **`PublicLayout`** | Marketing / docs. Slots for navbar + footer. **[See PublicLayout README](./src/layouts/PublicLayout/README.md)** for full props, navbar variants (`FloatingNavbar` / `FlushNavbar` / `MinimalNavbar`), `DefaultFooter`, `NavAction`, and hooks. |
|
|
90
|
+
| **`PublicLayout`** | Marketing / docs. Slots for navbar + footer. Supports `LinkComponentProvider` to inject an i18n-aware `Link` (e.g. `next-intl`) so every anchor picks up the locale prefix. **[See PublicLayout README](./src/layouts/PublicLayout/README.md)** for full props, navbar variants (`FloatingNavbar` / `FlushNavbar` / `MinimalNavbar`), `DefaultFooter`, `NavAction`, and hooks. |
|
|
91
91
|
| **`PrivateLayout`** | App shell — sidebar + header. |
|
|
92
92
|
| **`AuthLayout`** | Sign-in flows. |
|
|
93
93
|
| **`AdminLayout`** | Admin console. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.280",
|
|
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.280",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.280",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.280",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.280",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.280",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.280",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.280",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.280",
|
|
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.280",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.280",
|
|
115
|
+
"@djangocfg/debuger": "^2.1.280",
|
|
116
|
+
"@djangocfg/i18n": "^2.1.280",
|
|
117
|
+
"@djangocfg/monitor": "^2.1.280",
|
|
118
|
+
"@djangocfg/typescript-config": "^2.1.280",
|
|
119
|
+
"@djangocfg/ui-core": "^2.1.280",
|
|
120
|
+
"@djangocfg/ui-nextjs": "^2.1.280",
|
|
121
|
+
"@djangocfg/ui-tools": "^2.1.280",
|
|
122
122
|
"@types/node": "^24.7.2",
|
|
123
123
|
"@types/react": "^19.1.0",
|
|
124
124
|
"@types/react-dom": "^19.1.0",
|
|
@@ -23,6 +23,30 @@ import { PublicLayout, FloatingNavbar, DefaultFooter } from '@djangocfg/layouts'
|
|
|
23
23
|
| `contentTopSpacing` | `'auto' \| 'none'` | `'auto'` | Auto pads `<main>` based on the navbar variant. |
|
|
24
24
|
| `contentBottomSpacing` | `'auto' \| 'none' \| 'compact'` | `'auto'` | Bottom breathing room before footer. |
|
|
25
25
|
|
|
26
|
+
## `LinkComponentProvider` — inject a custom Link
|
|
27
|
+
|
|
28
|
+
All internal anchors (navbar, footer, mobile drawer) render through a single
|
|
29
|
+
injectable `Link` component. Default is plain `next/link`. Wrap the layout in
|
|
30
|
+
`LinkComponentProvider` to swap it — for example to pipe in a locale-aware
|
|
31
|
+
`Link` from `next-intl`:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { LinkComponentProvider, PublicLayout, FloatingNavbar } from '@djangocfg/layouts';
|
|
35
|
+
import { Link } from '@/i18n/navigation'; // next-intl createNavigation().Link
|
|
36
|
+
|
|
37
|
+
<LinkComponentProvider value={Link}>
|
|
38
|
+
<PublicLayout navbar={<FloatingNavbar config={…} />} footer={…}>
|
|
39
|
+
{children}
|
|
40
|
+
</PublicLayout>
|
|
41
|
+
</LinkComponentProvider>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Why: with `next-intl`'s `localePrefix: 'as-needed'`, plain `next/link` does
|
|
45
|
+
not prepend the active locale, so every navbar item drops the `/ru/` prefix
|
|
46
|
+
on click. Injecting the i18n-aware `Link` fixes that once for the whole tree.
|
|
47
|
+
|
|
48
|
+
Monolingual apps don't need the provider — the default `next/link` is fine.
|
|
49
|
+
|
|
26
50
|
## Navbar variants
|
|
27
51
|
|
|
28
52
|
Three variants. All share the same core props (below); only the chrome differs.
|
|
@@ -131,7 +155,7 @@ Three variants: `full` (default) with brand column + menus + controls; `compact`
|
|
|
131
155
|
|
|
132
156
|
## Primitives
|
|
133
157
|
|
|
134
|
-
`NavBrand`, `NavActions`, `NavActionItem`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `PublicLayoutProvider`, `usePublicLayout`.
|
|
158
|
+
`NavBrand`, `NavActions`, `NavActionItem`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `LinkComponentProvider`, `useLinkComponent`, `PublicLayoutProvider`, `usePublicLayout`.
|
|
135
159
|
|
|
136
160
|
## Context
|
|
137
161
|
|
|
@@ -13,7 +13,7 @@ import { Button } from '@djangocfg/ui-core/components';
|
|
|
13
13
|
import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
|
|
14
14
|
|
|
15
15
|
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
16
|
-
import {
|
|
16
|
+
import { useLinkComponent } from '../../primitives/LinkComponentContext';
|
|
17
17
|
import { FooterMenuSections } from './FooterMenuSections';
|
|
18
18
|
import { FooterProjectInfo } from './FooterProjectInfo';
|
|
19
19
|
|
|
@@ -69,6 +69,7 @@ function ThemeModeControl() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
72
|
+
const Link = useLinkComponent();
|
|
72
73
|
const variant = config.variant ?? 'full';
|
|
73
74
|
const shellClass = config.shell?.className;
|
|
74
75
|
const brandSlot = config.brand?.slot;
|
|
@@ -132,13 +133,13 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
132
133
|
{link.label}
|
|
133
134
|
</a>
|
|
134
135
|
) : (
|
|
135
|
-
<
|
|
136
|
+
<Link
|
|
136
137
|
key={link.path}
|
|
137
138
|
href={link.path}
|
|
138
139
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
139
140
|
>
|
|
140
141
|
{link.label}
|
|
141
|
-
</
|
|
142
|
+
</Link>
|
|
142
143
|
)
|
|
143
144
|
)}
|
|
144
145
|
</div>
|
|
@@ -195,13 +196,13 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
195
196
|
{link.label}
|
|
196
197
|
</a>
|
|
197
198
|
) : (
|
|
198
|
-
<
|
|
199
|
+
<Link
|
|
199
200
|
key={link.path}
|
|
200
201
|
href={link.path}
|
|
201
202
|
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
|
202
203
|
>
|
|
203
204
|
{link.label}
|
|
204
|
-
</
|
|
205
|
+
</Link>
|
|
205
206
|
)
|
|
206
207
|
)}
|
|
207
208
|
</div>
|
|
@@ -302,9 +303,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
302
303
|
{link.label}
|
|
303
304
|
</a>
|
|
304
305
|
) : (
|
|
305
|
-
<
|
|
306
|
+
<Link key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
|
|
306
307
|
{link.label}
|
|
307
|
-
</
|
|
308
|
+
</Link>
|
|
308
309
|
)
|
|
309
310
|
)}
|
|
310
311
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useLinkComponent } from '../../primitives/LinkComponentContext';
|
|
6
6
|
import { DjangoCFGLogo } from './DjangoCFGLogo';
|
|
7
7
|
|
|
8
8
|
import type { FooterLink } from './types';
|
|
@@ -23,6 +23,7 @@ export function FooterBottom({
|
|
|
23
23
|
links = [],
|
|
24
24
|
variant = 'desktop',
|
|
25
25
|
}: FooterBottomProps) {
|
|
26
|
+
const Link = useLinkComponent();
|
|
26
27
|
const isMobile = variant === 'mobile';
|
|
27
28
|
|
|
28
29
|
if (isMobile) {
|
|
@@ -95,13 +96,13 @@ export function FooterBottom({
|
|
|
95
96
|
{link.label}
|
|
96
97
|
</a>
|
|
97
98
|
) : (
|
|
98
|
-
<
|
|
99
|
+
<Link
|
|
99
100
|
key={link.path}
|
|
100
101
|
href={link.path}
|
|
101
102
|
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
|
102
103
|
>
|
|
103
104
|
{link.label}
|
|
104
|
-
</
|
|
105
|
+
</Link>
|
|
105
106
|
)
|
|
106
107
|
)}
|
|
107
108
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useLinkComponent } from '../../primitives/LinkComponentContext';
|
|
6
6
|
|
|
7
7
|
import type { FooterMenuSection } from './types';
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@ export function FooterMenuSections({
|
|
|
17
17
|
minColumnWidth = 180,
|
|
18
18
|
maxColumns = 5,
|
|
19
19
|
}: FooterMenuSectionsProps) {
|
|
20
|
+
const Link = useLinkComponent();
|
|
20
21
|
if (menuSections.length === 0) return null;
|
|
21
22
|
|
|
22
23
|
const effectiveColumns = Math.max(1, Math.min(maxColumns, menuSections.length));
|
|
@@ -34,12 +35,12 @@ export function FooterMenuSections({
|
|
|
34
35
|
<ul className="space-y-2">
|
|
35
36
|
{section.items.map((item) => (
|
|
36
37
|
<li key={item.path}>
|
|
37
|
-
<
|
|
38
|
+
<Link
|
|
38
39
|
href={item.path}
|
|
39
40
|
className="text-sm text-foreground/90 hover:text-foreground transition-colors"
|
|
40
41
|
>
|
|
41
42
|
{item.label}
|
|
42
|
-
</
|
|
43
|
+
</Link>
|
|
43
44
|
</li>
|
|
44
45
|
))}
|
|
45
46
|
</ul>
|
|
@@ -36,18 +36,16 @@ export {
|
|
|
36
36
|
NavDesktopItems,
|
|
37
37
|
ThemeBrandMark,
|
|
38
38
|
ThemeBrandMarkImg,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
isExternalPrefixHref,
|
|
42
|
-
SmartNavLink,
|
|
39
|
+
LinkComponentProvider,
|
|
40
|
+
useLinkComponent,
|
|
43
41
|
} from './primitives';
|
|
44
42
|
export type {
|
|
45
43
|
NavAction,
|
|
46
44
|
ThemeBrandMarkProps,
|
|
47
45
|
ThemeBrandMarkImgProps,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
LinkComponent,
|
|
47
|
+
LinkComponentProps,
|
|
48
|
+
LinkComponentProviderProps,
|
|
51
49
|
} from './primitives';
|
|
52
50
|
|
|
53
51
|
// Navbar variants
|
|
@@ -11,7 +11,6 @@ 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';
|
|
15
14
|
import type { NavAction } from '../../primitives/NavActionItem';
|
|
16
15
|
import { NavbarShell } from '../../shared';
|
|
17
16
|
import type {
|
|
@@ -52,14 +51,6 @@ export interface FloatingNavbarConfig {
|
|
|
52
51
|
actionsLeadingSlot?: React.ReactNode;
|
|
53
52
|
/** Arbitrary ReactNode after the mobile toggle. */
|
|
54
53
|
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
54
|
}
|
|
64
55
|
|
|
65
56
|
export interface FloatingNavbarProps {
|
|
@@ -71,7 +62,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
71
62
|
const rounding = config.shell?.rounding;
|
|
72
63
|
const containerClassName = config.shell?.className;
|
|
73
64
|
const position = config.navbarPosition ?? 'sticky';
|
|
74
|
-
const externalPrefixes = config.externalPrefixes;
|
|
75
65
|
|
|
76
66
|
const outerClassName = cn(
|
|
77
67
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -87,7 +77,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
87
77
|
);
|
|
88
78
|
|
|
89
79
|
return (
|
|
90
|
-
|
|
80
|
+
<>
|
|
91
81
|
<NavbarShell
|
|
92
82
|
variant="floating"
|
|
93
83
|
position={position}
|
|
@@ -122,6 +112,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
122
112
|
containerClassName={containerClassName}
|
|
123
113
|
rounding={rounding}
|
|
124
114
|
/>
|
|
125
|
-
|
|
115
|
+
</>
|
|
126
116
|
);
|
|
127
117
|
}
|
|
@@ -10,7 +10,6 @@ import React from 'react';
|
|
|
10
10
|
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
12
|
|
|
13
|
-
import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
|
|
14
13
|
import type { NavAction } from '../../primitives/NavActionItem';
|
|
15
14
|
import { NavbarShell } from '../../shared';
|
|
16
15
|
import type {
|
|
@@ -51,14 +50,6 @@ export interface FlushNavbarConfig {
|
|
|
51
50
|
actionsLeadingSlot?: React.ReactNode;
|
|
52
51
|
/** Arbitrary ReactNode after the mobile toggle. */
|
|
53
52
|
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
53
|
}
|
|
63
54
|
|
|
64
55
|
export interface FlushNavbarProps {
|
|
@@ -69,7 +60,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
69
60
|
const navigation = config.navigation ?? [];
|
|
70
61
|
const containerClassName = config.shell?.className;
|
|
71
62
|
const position = config.navbarPosition ?? 'sticky';
|
|
72
|
-
const externalPrefixes = config.externalPrefixes;
|
|
73
63
|
|
|
74
64
|
const outerClassName = cn(
|
|
75
65
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -83,7 +73,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
83
73
|
);
|
|
84
74
|
|
|
85
75
|
return (
|
|
86
|
-
|
|
76
|
+
<>
|
|
87
77
|
<NavbarShell
|
|
88
78
|
variant="flush"
|
|
89
79
|
position={position}
|
|
@@ -117,6 +107,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
117
107
|
userMenu={config.userMenu}
|
|
118
108
|
containerClassName={containerClassName}
|
|
119
109
|
/>
|
|
120
|
-
|
|
110
|
+
</>
|
|
121
111
|
);
|
|
122
112
|
}
|
|
@@ -17,7 +17,6 @@ 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';
|
|
21
20
|
import { NavActionItem, type NavAction } from '../../primitives/NavActionItem';
|
|
22
21
|
import { NavbarShell, type NavbarActionsContext } from '../../shared';
|
|
23
22
|
import type {
|
|
@@ -68,15 +67,6 @@ export interface MinimalNavbarConfig {
|
|
|
68
67
|
* @default 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10'
|
|
69
68
|
*/
|
|
70
69
|
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
70
|
}
|
|
81
71
|
|
|
82
72
|
export interface MinimalNavbarProps {
|
|
@@ -131,7 +121,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
131
121
|
const position = config.navbarPosition ?? 'sticky';
|
|
132
122
|
const transparent = config.transparent ?? true;
|
|
133
123
|
const containerClassName = config.containerClassName ?? 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10';
|
|
134
|
-
const externalPrefixes = config.externalPrefixes;
|
|
135
124
|
|
|
136
125
|
const outerClassName = cn(
|
|
137
126
|
position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
|
|
@@ -142,7 +131,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
142
131
|
const shapeClassName = 'w-full rounded-none border-0 shadow-none';
|
|
143
132
|
|
|
144
133
|
return (
|
|
145
|
-
|
|
134
|
+
<>
|
|
146
135
|
<NavbarShell
|
|
147
136
|
variant="minimal"
|
|
148
137
|
position={position}
|
|
@@ -175,6 +164,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
175
164
|
userMenu={config.userMenu}
|
|
176
165
|
containerClassName={containerClassName}
|
|
177
166
|
/>
|
|
178
|
-
|
|
167
|
+
</>
|
|
179
168
|
);
|
|
180
169
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkComponentContext
|
|
3
|
+
*
|
|
4
|
+
* Dependency-injects the Link component used by navbars, footers, and mobile
|
|
5
|
+
* drawers. Apps wire in their own i18n-aware Link (e.g. next-intl's
|
|
6
|
+
* `createNavigation().Link`) so internal navigation picks up the locale
|
|
7
|
+
* prefix. Default falls back to plain `next/link`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use client';
|
|
11
|
+
|
|
12
|
+
import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
|
|
13
|
+
import React, {
|
|
14
|
+
createContext,
|
|
15
|
+
useContext,
|
|
16
|
+
type AnchorHTMLAttributes,
|
|
17
|
+
type ComponentType,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from 'react';
|
|
20
|
+
|
|
21
|
+
type AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
|
|
22
|
+
|
|
23
|
+
export type LinkComponentProps = Omit<NextLinkProps, 'passHref' | 'legacyBehavior'> &
|
|
24
|
+
AnchorProps & {
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LinkComponent = ComponentType<LinkComponentProps>;
|
|
29
|
+
|
|
30
|
+
const LinkComponentContext = createContext<LinkComponent>(NextLink as LinkComponent);
|
|
31
|
+
|
|
32
|
+
export interface LinkComponentProviderProps {
|
|
33
|
+
value: LinkComponent;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function LinkComponentProvider({
|
|
38
|
+
value,
|
|
39
|
+
children,
|
|
40
|
+
}: LinkComponentProviderProps) {
|
|
41
|
+
return (
|
|
42
|
+
<LinkComponentContext.Provider value={value}>
|
|
43
|
+
{children}
|
|
44
|
+
</LinkComponentContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useLinkComponent(): LinkComponent {
|
|
49
|
+
return useContext(LinkComponentContext);
|
|
50
|
+
}
|
|
@@ -11,7 +11,7 @@ import React, { type ReactNode } from 'react';
|
|
|
11
11
|
|
|
12
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { useLinkComponent } from './LinkComponentContext';
|
|
15
15
|
|
|
16
16
|
export interface NavAction {
|
|
17
17
|
label: string;
|
|
@@ -55,6 +55,7 @@ interface NavActionItemProps {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function NavActionItem({ action, className }: NavActionItemProps) {
|
|
58
|
+
const Link = useLinkComponent();
|
|
58
59
|
const variant = action.variant ?? 'ghost';
|
|
59
60
|
const cls = cn(
|
|
60
61
|
baseCls,
|
|
@@ -88,8 +89,8 @@ export function NavActionItem({ action, className }: NavActionItemProps) {
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
return (
|
|
91
|
-
<
|
|
92
|
+
<Link href={action.href} className={cls} onClick={action.onClick}>
|
|
92
93
|
{content}
|
|
93
|
-
</
|
|
94
|
+
</Link>
|
|
94
95
|
);
|
|
95
96
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { type ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useLinkComponent } from './LinkComponentContext';
|
|
6
6
|
|
|
7
7
|
interface NavBrandProps {
|
|
8
8
|
brand?: ReactNode;
|
|
@@ -10,16 +10,18 @@ interface NavBrandProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function NavBrand({ brand, brandHref = '/' }: NavBrandProps) {
|
|
13
|
+
const Link = useLinkComponent();
|
|
14
|
+
|
|
13
15
|
if (brand == null || brand === '' || brand === false) return null;
|
|
14
16
|
|
|
15
17
|
if (typeof brand === 'string') {
|
|
16
18
|
return (
|
|
17
|
-
<
|
|
19
|
+
<Link
|
|
18
20
|
href={brandHref}
|
|
19
21
|
className="font-bold text-[15px] text-foreground hover:opacity-90 transition-opacity"
|
|
20
22
|
>
|
|
21
23
|
{brand}
|
|
22
|
-
</
|
|
24
|
+
</Link>
|
|
23
25
|
);
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -10,7 +10,7 @@ import type { NavigationItem } from '../../types';
|
|
|
10
10
|
import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
|
|
11
11
|
import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
|
|
12
12
|
import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
|
|
13
|
-
import {
|
|
13
|
+
import { useLinkComponent } from './LinkComponentContext';
|
|
14
14
|
|
|
15
15
|
interface NavDesktopItemsProps {
|
|
16
16
|
/** Full ordered item list; overflow is computed responsively. */
|
|
@@ -57,6 +57,7 @@ export function NavDesktopItems({
|
|
|
57
57
|
dropdown,
|
|
58
58
|
renderDesktopDropdown,
|
|
59
59
|
}: NavDesktopItemsProps) {
|
|
60
|
+
const Link = useLinkComponent();
|
|
60
61
|
const { openDropdownKey, scheduleOpen, scheduleClose, closeDropdown } = dropdown;
|
|
61
62
|
|
|
62
63
|
const { visibleCount, containerRef, itemRef, measured } = useResponsiveOverflow({
|
|
@@ -91,9 +92,9 @@ export function NavDesktopItems({
|
|
|
91
92
|
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
92
93
|
</a>
|
|
93
94
|
) : (
|
|
94
|
-
<
|
|
95
|
+
<Link href={sub.href} className={subMenuLinkCls(subActive)}>
|
|
95
96
|
<span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
|
|
96
|
-
</
|
|
97
|
+
</Link>
|
|
97
98
|
)}
|
|
98
99
|
</div>
|
|
99
100
|
);
|
|
@@ -149,13 +150,13 @@ export function NavDesktopItems({
|
|
|
149
150
|
|
|
150
151
|
const active = isActivePath(item.href);
|
|
151
152
|
return (
|
|
152
|
-
<
|
|
153
|
+
<Link
|
|
153
154
|
key={item.href}
|
|
154
155
|
href={item.href}
|
|
155
156
|
className={cn(navItemCls, active && navItemActiveCls)}
|
|
156
157
|
>
|
|
157
158
|
<span className={labelCls} title={item.label}>{item.label}</span>
|
|
158
|
-
</
|
|
159
|
+
</Link>
|
|
159
160
|
);
|
|
160
161
|
};
|
|
161
162
|
|
|
@@ -235,9 +236,9 @@ export function NavDesktopItems({
|
|
|
235
236
|
const active = isGroupActive(item);
|
|
236
237
|
return (
|
|
237
238
|
<div key={`overflow-${item.href}`} className="rounded-full">
|
|
238
|
-
<
|
|
239
|
+
<Link href={item.href} className={subMenuLinkCls(active)}>
|
|
239
240
|
<span className="min-w-0 truncate" title={item.label}>{item.label}</span>
|
|
240
|
-
</
|
|
241
|
+
</Link>
|
|
241
242
|
</div>
|
|
242
243
|
);
|
|
243
244
|
})}
|
|
@@ -6,13 +6,11 @@ export { NavDesktopItems } from './NavDesktopItems';
|
|
|
6
6
|
export { ThemeBrandMark, ThemeBrandMarkImg } from './ThemeBrandMark';
|
|
7
7
|
export type { ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './ThemeBrandMark';
|
|
8
8
|
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './ExternalPrefixesContext';
|
|
9
|
+
LinkComponentProvider,
|
|
10
|
+
useLinkComponent,
|
|
11
|
+
} from './LinkComponentContext';
|
|
13
12
|
export type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export type { SmartNavLinkProps } from './SmartNavLink';
|
|
13
|
+
LinkComponent,
|
|
14
|
+
LinkComponentProps,
|
|
15
|
+
LinkComponentProviderProps,
|
|
16
|
+
} from './LinkComponentContext';
|
|
@@ -19,7 +19,7 @@ import { usePathnameWithoutLocale } from '../../../hooks';
|
|
|
19
19
|
import { UserMenu } from '../../_components/UserMenu';
|
|
20
20
|
import { usePublicLayoutOptional } from '../context';
|
|
21
21
|
import { useMobileNavPanel } from '../hooks';
|
|
22
|
-
import {
|
|
22
|
+
import { useLinkComponent } from '../primitives/LinkComponentContext';
|
|
23
23
|
|
|
24
24
|
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
25
25
|
|
|
@@ -35,6 +35,7 @@ export interface MobileDrawerShellProps {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
38
|
+
const Link = useLinkComponent();
|
|
38
39
|
const context = usePublicLayoutOptional();
|
|
39
40
|
const mobileMenuOpen = props.isOpen ?? context?.mobileMenuOpen ?? false;
|
|
40
41
|
const closeMobileMenu = props.onClose ?? context?.closeMobileMenu ?? (() => {});
|
|
@@ -133,7 +134,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
133
134
|
const parentOnlySectionOpen = hasChildNav && anyChildActive;
|
|
134
135
|
return (
|
|
135
136
|
<div key={item.href}>
|
|
136
|
-
<
|
|
137
|
+
<Link
|
|
137
138
|
href={item.href}
|
|
138
139
|
onClick={closeMobileMenu}
|
|
139
140
|
title={item.label}
|
|
@@ -148,13 +149,13 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
148
149
|
)}
|
|
149
150
|
>
|
|
150
151
|
{item.label}
|
|
151
|
-
</
|
|
152
|
+
</Link>
|
|
152
153
|
{hasChildNav && (
|
|
153
154
|
<div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
|
|
154
155
|
{childItems.map((subItem) => {
|
|
155
156
|
const subActive = isActivePath(subItem.href);
|
|
156
157
|
return (
|
|
157
|
-
<
|
|
158
|
+
<Link
|
|
158
159
|
key={`${item.href}-${subItem.href}`}
|
|
159
160
|
href={subItem.href}
|
|
160
161
|
onClick={closeMobileMenu}
|
|
@@ -168,7 +169,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
168
169
|
)}
|
|
169
170
|
>
|
|
170
171
|
{subItem.label}
|
|
171
|
-
</
|
|
172
|
+
</Link>
|
|
172
173
|
);
|
|
173
174
|
})}
|
|
174
175
|
</div>
|
|
@@ -182,7 +183,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
182
183
|
|
|
183
184
|
{showSignInFooter && (
|
|
184
185
|
<div className="shrink-0 border-t border-border/50 p-4">
|
|
185
|
-
<
|
|
186
|
+
<Link
|
|
186
187
|
href={userMenu?.authPath || '/auth'}
|
|
187
188
|
onClick={closeMobileMenu}
|
|
188
189
|
className="block"
|
|
@@ -194,7 +195,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
194
195
|
aria-hidden
|
|
195
196
|
/>
|
|
196
197
|
</Button>
|
|
197
|
-
</
|
|
198
|
+
</Link>
|
|
198
199
|
</div>
|
|
199
200
|
)}
|
|
200
201
|
</div>
|
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
);
|