@djangocfg/layouts 2.1.279 → 2.1.281

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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/package.json +18 -18
  3. package/src/layouts/AppLayout/AppLayout.tsx +2 -10
  4. package/src/layouts/PrivateLayout/PrivateLayout.tsx +2 -1
  5. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +1 -1
  6. package/src/layouts/PublicLayout/README.md +69 -1
  7. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +8 -7
  8. package/src/layouts/PublicLayout/footers/DefaultFooter/FooterBottom.tsx +4 -3
  9. package/src/layouts/PublicLayout/footers/DefaultFooter/FooterMenuSections.tsx +4 -3
  10. package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +1 -2
  11. package/src/layouts/PublicLayout/index.ts +7 -7
  12. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +18 -11
  13. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +18 -11
  14. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +22 -12
  15. package/src/layouts/PublicLayout/primitives/LinkComponentContext.tsx +50 -0
  16. package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +4 -3
  17. package/src/layouts/PublicLayout/primitives/NavActions.tsx +8 -0
  18. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +5 -3
  19. package/src/layouts/PublicLayout/primitives/NavControls.tsx +114 -0
  20. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +8 -7
  21. package/src/layouts/PublicLayout/primitives/index.ts +9 -9
  22. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +32 -8
  23. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +33 -0
  24. package/src/layouts/_components/PrivateSidebarAccount.tsx +1 -1
  25. package/src/layouts/types/index.ts +1 -1
  26. package/src/layouts/types/layout.types.ts +13 -0
  27. package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +0 -69
  28. 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. All three navbars accept `controls` + `i18n` to show theme / locale switchers next to UserMenu (same shape as `DefaultFooter.controls`). **[See PublicLayout README](./src/layouts/PublicLayout/README.md)** for full props, navbar variants (`FloatingNavbar` / `FlushNavbar` / `MinimalNavbar`), `DefaultFooter`, `NavAction`, `NavControls`, 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.279",
3
+ "version": "2.1.281",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,14 +74,14 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
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",
77
+ "@djangocfg/api": "^2.1.281",
78
+ "@djangocfg/centrifugo": "^2.1.281",
79
+ "@djangocfg/debuger": "^2.1.281",
80
+ "@djangocfg/i18n": "^2.1.281",
81
+ "@djangocfg/monitor": "^2.1.281",
82
+ "@djangocfg/ui-core": "^2.1.281",
83
+ "@djangocfg/ui-nextjs": "^2.1.281",
84
+ "@djangocfg/ui-tools": "^2.1.281",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
@@ -110,15 +110,15 @@
110
110
  "uuid": "^11.1.0"
111
111
  },
112
112
  "devDependencies": {
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",
113
+ "@djangocfg/api": "^2.1.281",
114
+ "@djangocfg/centrifugo": "^2.1.281",
115
+ "@djangocfg/debuger": "^2.1.281",
116
+ "@djangocfg/i18n": "^2.1.281",
117
+ "@djangocfg/monitor": "^2.1.281",
118
+ "@djangocfg/typescript-config": "^2.1.281",
119
+ "@djangocfg/ui-core": "^2.1.281",
120
+ "@djangocfg/ui-nextjs": "^2.1.281",
121
+ "@djangocfg/ui-tools": "^2.1.281",
122
122
  "@types/node": "^24.7.2",
123
123
  "@types/react": "^19.1.0",
124
124
  "@types/react-dom": "^19.1.0",
@@ -48,7 +48,9 @@ import type {
48
48
  SWRConfigOptions,
49
49
  PwaInstallConfig,
50
50
  DebugConfig,
51
+ I18nLayoutConfig,
51
52
  } from '../types';
53
+ export type { I18nLayoutConfig } from '../types';
52
54
  import type { AuthConfig } from '@djangocfg/api/auth';
53
55
  import type { MonitorConfig } from '@djangocfg/monitor';
54
56
 
@@ -152,16 +154,6 @@ function determineLayoutMode(
152
154
  return 'public';
153
155
  }
154
156
 
155
- /** i18n configuration for locale switching */
156
- export interface I18nLayoutConfig {
157
- /** Current locale */
158
- locale: string;
159
- /** Available locales */
160
- locales: string[];
161
- /** Callback when locale changes */
162
- onLocaleChange: (locale: string) => void;
163
- }
164
-
165
157
  /**
166
158
  * Props passed to every layout component (`public` / `private` / `admin`).
167
159
  * Use `publicChrome` to pass defaults for `FloatingNavbar` / `DefaultFooter` from `AppLayout`.
@@ -15,7 +15,8 @@ import { useAuth } from '@djangocfg/api/auth';
15
15
  import { Preloader } from '@djangocfg/ui-core/components';
16
16
  import { SidebarInset, SidebarProvider } from '@djangocfg/ui-nextjs/components';
17
17
 
18
- import type { AppLayoutPublicChrome, I18nLayoutConfig } from '../AppLayout/AppLayout';
18
+ import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
19
+ import type { I18nLayoutConfig } from '../types';
19
20
  import { UserMenuConfig } from '../types';
20
21
  import { PrivateContent, PrivateSidebar } from './components';
21
22
 
@@ -29,7 +29,7 @@ import { cn } from '@djangocfg/ui-core/lib';
29
29
  import { PrivateSidebarAccount } from '../../_components/PrivateSidebarAccount';
30
30
  import { LucideIcon } from '../../../components';
31
31
 
32
- import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
32
+ import type { I18nLayoutConfig } from '../../types';
33
33
  import type { HeaderConfig, SidebarItem, SidebarConfig } from '../PrivateLayout';
34
34
 
35
35
  /** Few items → roomier rows; many items → tighter. Same breakpoints for demo, CarAPIS, etc. */
@@ -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.
@@ -52,6 +76,8 @@ Three variants. All share the same core props (below); only the chrome differs.
52
76
  | `transparentThreshold` | `number` | `40` | Px past which the nav becomes opaque. |
53
77
  | `desktopMaxPrimaryItems` | `number` | auto | Hard cap for primary items before overflow. |
54
78
  | `renderDesktopDropdown` | `(ctx) => ReactNode` | — | Replace default popover per-item. |
79
+ | `controls` | `{ showThemeSwitcher?; showLocaleSwitcher? }` | — | Compact theme + locale pills next to UserMenu. See below. |
80
+ | `i18n` | `{ locale, locales, onLocaleChange }` | — | Required for the locale switcher (same shape as `DefaultFooter.i18n`). |
55
81
 
56
82
  ### Variant-only
57
83
 
@@ -68,6 +94,48 @@ Three variants. All share the same core props (below); only the chrome differs.
68
94
  - `centered` — all three groups centered in one row.
69
95
  - `split` — brand left, actions right, **no desktop nav** (drawer only).
70
96
 
97
+ ## Theme + locale controls (navbar)
98
+
99
+ All three navbars accept an optional `controls` block and `i18n` config that
100
+ mirrors `DefaultFooter`. When enabled, a compact `NavControls` pill is rendered
101
+ on desktop right before `UserMenu`, and an equivalent row appears in the mobile
102
+ drawer footer.
103
+
104
+ ```tsx
105
+ <FloatingNavbar
106
+ config={{
107
+ brand: <BrandLogo />,
108
+ navigation,
109
+ userMenu: { authPath: '/auth' },
110
+ controls: { showThemeSwitcher: true, showLocaleSwitcher: true },
111
+ i18n: { locale, locales, onLocaleChange: changeLocale },
112
+ }}
113
+ />
114
+ ```
115
+
116
+ | Control | Required | Notes |
117
+ |---|---|---|
118
+ | `controls.showThemeSwitcher` | — | Light / system / dark pill (uses `useThemeContext`). |
119
+ | `controls.showLocaleSwitcher` | `i18n` | Globe dropdown. Silently hidden when `i18n` is omitted. |
120
+
121
+ Same shape works for `FlushNavbar` and `MinimalNavbar`. If you build a custom
122
+ navbar with `NavbarShell`, the controls node is pre-built and delivered to
123
+ `renderActions(ctx)` as `ctx.controls`.
124
+
125
+ Need to render the pills somewhere custom (e.g. your own navbar)? Import
126
+ `NavControls` directly:
127
+
128
+ ```tsx
129
+ import { NavControls } from '@djangocfg/layouts';
130
+
131
+ <NavControls
132
+ showThemeSwitcher
133
+ showLocaleSwitcher
134
+ i18n={{ locale, locales, onLocaleChange }}
135
+ size="compact" // 'compact' (navbar) | 'default' (footer)
136
+ />
137
+ ```
138
+
71
139
  ## `NavAction`
72
140
 
73
141
  Typed pill used by every navbar's `actions`.
@@ -131,7 +199,7 @@ Three variants: `full` (default) with brand column + menus + controls; `compact`
131
199
 
132
200
  ## Primitives
133
201
 
134
- `NavBrand`, `NavActions`, `NavActionItem`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `PublicLayoutProvider`, `usePublicLayout`.
202
+ `NavBrand`, `NavActions`, `NavActionItem`, `NavControls`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `LinkComponentProvider`, `useLinkComponent`, `PublicLayoutProvider`, `usePublicLayout`.
135
203
 
136
204
  ## Context
137
205
 
@@ -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 { SmartNavLink } from '../../primitives/SmartNavLink';
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
- <SmartNavLink
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
- </SmartNavLink>
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
- <SmartNavLink
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
- </SmartNavLink>
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
- <SmartNavLink key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
306
+ <Link key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
306
307
  {link.label}
307
- </SmartNavLink>
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 { SmartNavLink } from '../../primitives/SmartNavLink';
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
- <SmartNavLink
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
- </SmartNavLink>
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 { SmartNavLink } from '../../primitives/SmartNavLink';
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
- <SmartNavLink
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
- </SmartNavLink>
43
+ </Link>
43
44
  </li>
44
45
  ))}
45
46
  </ul>
@@ -5,8 +5,7 @@
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
+ import type { FooterLink, FooterMenuSection, FooterSocialLinks, I18nLayoutConfig } from '../../../types';
10
9
 
11
10
  export type { FooterLink, FooterMenuSection, FooterSocialLinks };
12
11
 
@@ -33,21 +33,21 @@ export {
33
33
  NavBrand,
34
34
  NavActions,
35
35
  NavActionItem,
36
+ NavControls,
36
37
  NavDesktopItems,
37
38
  ThemeBrandMark,
38
39
  ThemeBrandMarkImg,
39
- ExternalPrefixesProvider,
40
- useExternalPrefixes,
41
- isExternalPrefixHref,
42
- SmartNavLink,
40
+ LinkComponentProvider,
41
+ useLinkComponent,
43
42
  } from './primitives';
44
43
  export type {
45
44
  NavAction,
45
+ NavControlsProps,
46
46
  ThemeBrandMarkProps,
47
47
  ThemeBrandMarkImgProps,
48
- ExternalPrefixes,
49
- ExternalPrefixesProviderProps,
50
- SmartNavLinkProps,
48
+ LinkComponent,
49
+ LinkComponentProps,
50
+ LinkComponentProviderProps,
51
51
  } from './primitives';
52
52
 
53
53
  // 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 {
@@ -21,7 +20,7 @@ import type {
21
20
  PublicNavbarShellConfig,
22
21
  PublicNavLayout,
23
22
  } from '../../navbarTypes';
24
- import type { NavigationItem, UserMenuConfig } from '../../../types';
23
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
25
24
 
26
25
  import { FloatingMobileDrawer } from './FloatingMobileDrawer';
27
26
 
@@ -53,13 +52,18 @@ export interface FloatingNavbarConfig {
53
52
  /** Arbitrary ReactNode after the mobile toggle. */
54
53
  actionsTrailingSlot?: React.ReactNode;
55
54
  /**
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 []
55
+ * Optional theme + locale controls rendered next to UserMenu on desktop
56
+ * and as a footer row in the mobile drawer. Locale switcher requires
57
+ * `i18n`. Mirrors `DefaultFooter.controls`.
61
58
  */
62
- externalPrefixes?: readonly string[];
59
+ controls?: {
60
+ /** Light / system / dark pill. @default false */
61
+ showThemeSwitcher?: boolean;
62
+ /** Locale dropdown. Requires `i18n`. @default false */
63
+ showLocaleSwitcher?: boolean;
64
+ };
65
+ /** i18n config (current locale + locales + onLocaleChange). */
66
+ i18n?: I18nLayoutConfig;
63
67
  }
64
68
 
65
69
  export interface FloatingNavbarProps {
@@ -71,7 +75,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
71
75
  const rounding = config.shell?.rounding;
72
76
  const containerClassName = config.shell?.className;
73
77
  const position = config.navbarPosition ?? 'sticky';
74
- const externalPrefixes = config.externalPrefixes;
75
78
 
76
79
  const outerClassName = cn(
77
80
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -87,7 +90,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
87
90
  );
88
91
 
89
92
  return (
90
- <ExternalPrefixesProvider value={externalPrefixes}>
93
+ <>
91
94
  <NavbarShell
92
95
  variant="floating"
93
96
  position={position}
@@ -105,6 +108,8 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
105
108
  actions={config.actions}
106
109
  actionsLeadingSlot={config.actionsLeadingSlot}
107
110
  actionsTrailingSlot={config.actionsTrailingSlot}
111
+ controls={config.controls}
112
+ i18n={config.i18n}
108
113
  outerClassName={outerClassName}
109
114
  shapeClassName={shapeClassName}
110
115
  shapeForState={({ scrolled, transparent }) =>
@@ -121,7 +126,9 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
121
126
  userMenu={config.userMenu}
122
127
  containerClassName={containerClassName}
123
128
  rounding={rounding}
129
+ controls={config.controls}
130
+ i18n={config.i18n}
124
131
  />
125
- </ExternalPrefixesProvider>
132
+ </>
126
133
  );
127
134
  }
@@ -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 {
@@ -20,7 +19,7 @@ import type {
20
19
  PublicNavbarShellConfig,
21
20
  PublicNavLayout,
22
21
  } from '../../navbarTypes';
23
- import type { NavigationItem, UserMenuConfig } from '../../../types';
22
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
24
23
 
25
24
  import { FlushMobileDrawer } from './FlushMobileDrawer';
26
25
 
@@ -52,13 +51,18 @@ export interface FlushNavbarConfig {
52
51
  /** Arbitrary ReactNode after the mobile toggle. */
53
52
  actionsTrailingSlot?: React.ReactNode;
54
53
  /**
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 []
54
+ * Optional theme + locale controls rendered next to UserMenu on desktop
55
+ * and as a footer row in the mobile drawer. Locale switcher requires
56
+ * `i18n`. Mirrors `DefaultFooter.controls`.
60
57
  */
61
- externalPrefixes?: readonly string[];
58
+ controls?: {
59
+ /** Light / system / dark pill. @default false */
60
+ showThemeSwitcher?: boolean;
61
+ /** Locale dropdown. Requires `i18n`. @default false */
62
+ showLocaleSwitcher?: boolean;
63
+ };
64
+ /** i18n config (current locale + locales + onLocaleChange). */
65
+ i18n?: I18nLayoutConfig;
62
66
  }
63
67
 
64
68
  export interface FlushNavbarProps {
@@ -69,7 +73,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
69
73
  const navigation = config.navigation ?? [];
70
74
  const containerClassName = config.shell?.className;
71
75
  const position = config.navbarPosition ?? 'sticky';
72
- const externalPrefixes = config.externalPrefixes;
73
76
 
74
77
  const outerClassName = cn(
75
78
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -83,7 +86,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
83
86
  );
84
87
 
85
88
  return (
86
- <ExternalPrefixesProvider value={externalPrefixes}>
89
+ <>
87
90
  <NavbarShell
88
91
  variant="flush"
89
92
  position={position}
@@ -101,6 +104,8 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
101
104
  actions={config.actions}
102
105
  actionsLeadingSlot={config.actionsLeadingSlot}
103
106
  actionsTrailingSlot={config.actionsTrailingSlot}
107
+ controls={config.controls}
108
+ i18n={config.i18n}
104
109
  outerClassName={outerClassName}
105
110
  shapeClassName={shapeClassName}
106
111
  shapeForState={({ scrolled, transparent }) =>
@@ -116,7 +121,9 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
116
121
  navigation={navigation}
117
122
  userMenu={config.userMenu}
118
123
  containerClassName={containerClassName}
124
+ controls={config.controls}
125
+ i18n={config.i18n}
119
126
  />
120
- </ExternalPrefixesProvider>
127
+ </>
121
128
  );
122
129
  }
@@ -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 {
@@ -26,7 +25,7 @@ import type {
26
25
  PublicNavbarPosition,
27
26
  PublicNavLayout,
28
27
  } from '../../navbarTypes';
29
- import type { NavigationItem, UserMenuConfig } from '../../../types';
28
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
30
29
 
31
30
  import { MinimalMobileDrawer } from './MinimalMobileDrawer';
32
31
 
@@ -68,15 +67,19 @@ 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
70
  /**
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 []
71
+ * Optional theme + locale controls rendered next to UserMenu on desktop
72
+ * and as a footer row in the mobile drawer. Locale switcher requires
73
+ * `i18n`. Mirrors `DefaultFooter.controls`.
78
74
  */
79
- externalPrefixes?: readonly string[];
75
+ controls?: {
76
+ /** Light / system / dark pill. @default false */
77
+ showThemeSwitcher?: boolean;
78
+ /** Locale dropdown. Requires `i18n`. @default false */
79
+ showLocaleSwitcher?: boolean;
80
+ };
81
+ /** i18n config (current locale + locales + onLocaleChange). */
82
+ i18n?: I18nLayoutConfig;
80
83
  }
81
84
 
82
85
  export interface MinimalNavbarProps {
@@ -100,6 +103,10 @@ function MinimalActions({
100
103
  </div>
101
104
  )}
102
105
 
106
+ {ctx.controls && (
107
+ <div className="hidden lg:flex shrink-0 items-center">{ctx.controls}</div>
108
+ )}
109
+
103
110
  <div className="hidden lg:flex">
104
111
  <UserMenu
105
112
  variant="desktop"
@@ -131,7 +138,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
131
138
  const position = config.navbarPosition ?? 'sticky';
132
139
  const transparent = config.transparent ?? true;
133
140
  const containerClassName = config.containerClassName ?? 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10';
134
- const externalPrefixes = config.externalPrefixes;
135
141
 
136
142
  const outerClassName = cn(
137
143
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -142,7 +148,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
142
148
  const shapeClassName = 'w-full rounded-none border-0 shadow-none';
143
149
 
144
150
  return (
145
- <ExternalPrefixesProvider value={externalPrefixes}>
151
+ <>
146
152
  <NavbarShell
147
153
  variant="minimal"
148
154
  position={position}
@@ -157,6 +163,8 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
157
163
  hideNavOnScroll={config.hideNavOnScroll}
158
164
  transparent={transparent}
159
165
  transparentThreshold={config.transparentThreshold}
166
+ controls={config.controls}
167
+ i18n={config.i18n}
160
168
  outerClassName={outerClassName}
161
169
  shapeClassName={shapeClassName}
162
170
  innerPadding={containerClassName}
@@ -174,7 +182,9 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
174
182
  navigation={navigation}
175
183
  userMenu={config.userMenu}
176
184
  containerClassName={containerClassName}
185
+ controls={config.controls}
186
+ i18n={config.i18n}
177
187
  />
178
- </ExternalPrefixesProvider>
188
+ </>
179
189
  );
180
190
  }