@djangocfg/layouts 2.1.280 → 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.
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. 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. |
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.280",
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.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",
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.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",
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. */
@@ -76,6 +76,8 @@ Three variants. All share the same core props (below); only the chrome differs.
76
76
  | `transparentThreshold` | `number` | `40` | Px past which the nav becomes opaque. |
77
77
  | `desktopMaxPrimaryItems` | `number` | auto | Hard cap for primary items before overflow. |
78
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`). |
79
81
 
80
82
  ### Variant-only
81
83
 
@@ -92,6 +94,48 @@ Three variants. All share the same core props (below); only the chrome differs.
92
94
  - `centered` — all three groups centered in one row.
93
95
  - `split` — brand left, actions right, **no desktop nav** (drawer only).
94
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
+
95
139
  ## `NavAction`
96
140
 
97
141
  Typed pill used by every navbar's `actions`.
@@ -155,7 +199,7 @@ Three variants: `full` (default) with brand column + menus + controls; `compact`
155
199
 
156
200
  ## Primitives
157
201
 
158
- `NavBrand`, `NavActions`, `NavActionItem`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `LinkComponentProvider`, `useLinkComponent`, `PublicLayoutProvider`, `usePublicLayout`.
202
+ `NavBrand`, `NavActions`, `NavActionItem`, `NavControls`, `NavDesktopItems`, `ThemeBrandMark`, `ThemeBrandMarkImg`, `LinkComponentProvider`, `useLinkComponent`, `PublicLayoutProvider`, `usePublicLayout`.
159
203
 
160
204
  ## Context
161
205
 
@@ -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,6 +33,7 @@ export {
33
33
  NavBrand,
34
34
  NavActions,
35
35
  NavActionItem,
36
+ NavControls,
36
37
  NavDesktopItems,
37
38
  ThemeBrandMark,
38
39
  ThemeBrandMarkImg,
@@ -41,6 +42,7 @@ export {
41
42
  } from './primitives';
42
43
  export type {
43
44
  NavAction,
45
+ NavControlsProps,
44
46
  ThemeBrandMarkProps,
45
47
  ThemeBrandMarkImgProps,
46
48
  LinkComponent,
@@ -20,7 +20,7 @@ import type {
20
20
  PublicNavbarShellConfig,
21
21
  PublicNavLayout,
22
22
  } from '../../navbarTypes';
23
- import type { NavigationItem, UserMenuConfig } from '../../../types';
23
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
24
24
 
25
25
  import { FloatingMobileDrawer } from './FloatingMobileDrawer';
26
26
 
@@ -51,6 +51,19 @@ export interface FloatingNavbarConfig {
51
51
  actionsLeadingSlot?: React.ReactNode;
52
52
  /** Arbitrary ReactNode after the mobile toggle. */
53
53
  actionsTrailingSlot?: React.ReactNode;
54
+ /**
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`.
58
+ */
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;
54
67
  }
55
68
 
56
69
  export interface FloatingNavbarProps {
@@ -95,6 +108,8 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
95
108
  actions={config.actions}
96
109
  actionsLeadingSlot={config.actionsLeadingSlot}
97
110
  actionsTrailingSlot={config.actionsTrailingSlot}
111
+ controls={config.controls}
112
+ i18n={config.i18n}
98
113
  outerClassName={outerClassName}
99
114
  shapeClassName={shapeClassName}
100
115
  shapeForState={({ scrolled, transparent }) =>
@@ -111,6 +126,8 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
111
126
  userMenu={config.userMenu}
112
127
  containerClassName={containerClassName}
113
128
  rounding={rounding}
129
+ controls={config.controls}
130
+ i18n={config.i18n}
114
131
  />
115
132
  </>
116
133
  );
@@ -19,7 +19,7 @@ import type {
19
19
  PublicNavbarShellConfig,
20
20
  PublicNavLayout,
21
21
  } from '../../navbarTypes';
22
- import type { NavigationItem, UserMenuConfig } from '../../../types';
22
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
23
23
 
24
24
  import { FlushMobileDrawer } from './FlushMobileDrawer';
25
25
 
@@ -50,6 +50,19 @@ export interface FlushNavbarConfig {
50
50
  actionsLeadingSlot?: React.ReactNode;
51
51
  /** Arbitrary ReactNode after the mobile toggle. */
52
52
  actionsTrailingSlot?: React.ReactNode;
53
+ /**
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`.
57
+ */
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;
53
66
  }
54
67
 
55
68
  export interface FlushNavbarProps {
@@ -91,6 +104,8 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
91
104
  actions={config.actions}
92
105
  actionsLeadingSlot={config.actionsLeadingSlot}
93
106
  actionsTrailingSlot={config.actionsTrailingSlot}
107
+ controls={config.controls}
108
+ i18n={config.i18n}
94
109
  outerClassName={outerClassName}
95
110
  shapeClassName={shapeClassName}
96
111
  shapeForState={({ scrolled, transparent }) =>
@@ -106,6 +121,8 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
106
121
  navigation={navigation}
107
122
  userMenu={config.userMenu}
108
123
  containerClassName={containerClassName}
124
+ controls={config.controls}
125
+ i18n={config.i18n}
109
126
  />
110
127
  </>
111
128
  );
@@ -25,7 +25,7 @@ import type {
25
25
  PublicNavbarPosition,
26
26
  PublicNavLayout,
27
27
  } from '../../navbarTypes';
28
- import type { NavigationItem, UserMenuConfig } from '../../../types';
28
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
29
29
 
30
30
  import { MinimalMobileDrawer } from './MinimalMobileDrawer';
31
31
 
@@ -67,6 +67,19 @@ export interface MinimalNavbarConfig {
67
67
  * @default 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10'
68
68
  */
69
69
  containerClassName?: string;
70
+ /**
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`.
74
+ */
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;
70
83
  }
71
84
 
72
85
  export interface MinimalNavbarProps {
@@ -90,6 +103,10 @@ function MinimalActions({
90
103
  </div>
91
104
  )}
92
105
 
106
+ {ctx.controls && (
107
+ <div className="hidden lg:flex shrink-0 items-center">{ctx.controls}</div>
108
+ )}
109
+
93
110
  <div className="hidden lg:flex">
94
111
  <UserMenu
95
112
  variant="desktop"
@@ -146,6 +163,8 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
146
163
  hideNavOnScroll={config.hideNavOnScroll}
147
164
  transparent={transparent}
148
165
  transparentThreshold={config.transparentThreshold}
166
+ controls={config.controls}
167
+ i18n={config.i18n}
149
168
  outerClassName={outerClassName}
150
169
  shapeClassName={shapeClassName}
151
170
  innerPadding={containerClassName}
@@ -163,6 +182,8 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
163
182
  navigation={navigation}
164
183
  userMenu={config.userMenu}
165
184
  containerClassName={containerClassName}
185
+ controls={config.controls}
186
+ i18n={config.i18n}
166
187
  />
167
188
  </>
168
189
  );
@@ -23,6 +23,11 @@ interface NavActionsProps {
23
23
  leadingSlot?: ReactNode;
24
24
  /** Arbitrary slot rendered after the mobile toggle (desktop + mobile). */
25
25
  trailingSlot?: ReactNode;
26
+ /**
27
+ * Theme / locale controls rendered right before UserMenu (desktop only).
28
+ * Built by `NavbarShell` from `config.controls` + `config.i18n`.
29
+ */
30
+ controlsSlot?: ReactNode;
26
31
  }
27
32
 
28
33
  export function NavActions({
@@ -34,6 +39,7 @@ export function NavActions({
34
39
  actions,
35
40
  leadingSlot,
36
41
  trailingSlot,
42
+ controlsSlot,
37
43
  }: NavActionsProps) {
38
44
  const hasActions = actions && actions.length > 0;
39
45
 
@@ -49,6 +55,8 @@ export function NavActions({
49
55
 
50
56
  {leadingSlot && <div className="hidden lg:flex shrink-0 items-center">{leadingSlot}</div>}
51
57
 
58
+ {controlsSlot && <div className="hidden lg:flex shrink-0 items-center">{controlsSlot}</div>}
59
+
52
60
  <div className="hidden lg:flex">
53
61
  <UserMenu
54
62
  variant="desktop"
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { Laptop, Moon, Sun } from 'lucide-react';
4
+ import React, { useEffect, useState } from 'react';
5
+
6
+ import { Button } from '@djangocfg/ui-core/components';
7
+ import { cn } from '@djangocfg/ui-core/lib';
8
+ import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
9
+
10
+ import type { I18nLayoutConfig } from '../../types';
11
+ import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
12
+
13
+ export interface NavControlsProps {
14
+ /** Optional i18n config. Required to render the locale switcher. */
15
+ i18n?: I18nLayoutConfig;
16
+ /** Show the theme (light / system / dark) pill. @default false */
17
+ showThemeSwitcher?: boolean;
18
+ /** Show the locale dropdown. Requires `i18n`. @default false */
19
+ showLocaleSwitcher?: boolean;
20
+ /** Visual size. `compact` matches navbar row, `default` matches footer. @default 'compact' */
21
+ size?: 'compact' | 'default';
22
+ /** Extra classes for the outer container. */
23
+ className?: string;
24
+ }
25
+
26
+ function ThemeModeControl({ size }: { size: 'compact' | 'default' }) {
27
+ const { theme, setTheme } = useThemeContext();
28
+ const [mounted, setMounted] = useState(false);
29
+
30
+ useEffect(() => {
31
+ setMounted(true);
32
+ }, []);
33
+
34
+ const currentTheme = mounted ? (theme || 'system') : 'system';
35
+ const isActive = (value: 'system' | 'light' | 'dark') => currentTheme === value;
36
+
37
+ const btnSize = size === 'compact' ? 'h-7 w-7' : 'h-8 w-8';
38
+ const iconSize = size === 'compact' ? 'h-3.5 w-3.5' : 'h-4 w-4';
39
+ const baseItemClass = `${btnSize} rounded-full p-0 text-muted-foreground hover:text-foreground`;
40
+ const activeItemClass = 'bg-background/80 text-foreground shadow-sm';
41
+
42
+ return (
43
+ <div className="inline-flex items-center gap-0.5 rounded-full border border-border/60 bg-muted/30 p-0.5">
44
+ <Button
45
+ type="button"
46
+ variant="ghost"
47
+ size="icon"
48
+ className={cn(baseItemClass, isActive('system') && activeItemClass)}
49
+ onClick={() => setTheme('system')}
50
+ aria-label="Use system theme"
51
+ >
52
+ <Laptop className={iconSize} />
53
+ </Button>
54
+ <Button
55
+ type="button"
56
+ variant="ghost"
57
+ size="icon"
58
+ className={cn(baseItemClass, isActive('light') && activeItemClass)}
59
+ onClick={() => setTheme('light')}
60
+ aria-label="Use light theme"
61
+ >
62
+ <Sun className={iconSize} />
63
+ </Button>
64
+ <Button
65
+ type="button"
66
+ variant="ghost"
67
+ size="icon"
68
+ className={cn(baseItemClass, isActive('dark') && activeItemClass)}
69
+ onClick={() => setTheme('dark')}
70
+ aria-label="Use dark theme"
71
+ >
72
+ <Moon className={iconSize} />
73
+ </Button>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Navbar-sized theme + locale controls. Mirrors `DefaultFooter` controls but
80
+ * tuned for a navbar row. Rendered by `NavActions` when `controls` are passed
81
+ * to a navbar config.
82
+ */
83
+ export function NavControls({
84
+ i18n,
85
+ showThemeSwitcher = false,
86
+ showLocaleSwitcher = false,
87
+ size = 'compact',
88
+ className,
89
+ }: NavControlsProps) {
90
+ const renderLocale = showLocaleSwitcher && Boolean(i18n);
91
+ if (!showThemeSwitcher && !renderLocale) return null;
92
+
93
+ const localeBtnClass =
94
+ size === 'compact'
95
+ ? 'h-8 rounded-full border-border/60 bg-muted/30 px-2.5 text-xs hover:bg-muted/40'
96
+ : 'h-9 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40';
97
+
98
+ return (
99
+ <div className={cn('inline-flex items-center gap-1.5', className)}>
100
+ {showThemeSwitcher && <ThemeModeControl size={size} />}
101
+ {renderLocale && i18n && (
102
+ <LocaleSwitcher
103
+ locale={i18n.locale}
104
+ locales={i18n.locales}
105
+ onChange={i18n.onLocaleChange}
106
+ variant="outline"
107
+ size={size === 'compact' ? 'sm' : 'default'}
108
+ showTriggerLabel={false}
109
+ className={localeBtnClass}
110
+ />
111
+ )}
112
+ </div>
113
+ );
114
+ }
@@ -2,6 +2,8 @@ export { NavBrand } from './NavBrand';
2
2
  export { NavActions } from './NavActions';
3
3
  export { NavActionItem } from './NavActionItem';
4
4
  export type { NavAction } from './NavActionItem';
5
+ export { NavControls } from './NavControls';
6
+ export type { NavControlsProps } from './NavControls';
5
7
  export { NavDesktopItems } from './NavDesktopItems';
6
8
  export { ThemeBrandMark, ThemeBrandMarkImg } from './ThemeBrandMark';
7
9
  export type { ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './ThemeBrandMark';
@@ -20,8 +20,9 @@ import { UserMenu } from '../../_components/UserMenu';
20
20
  import { usePublicLayoutOptional } from '../context';
21
21
  import { useMobileNavPanel } from '../hooks';
22
22
  import { useLinkComponent } from '../primitives/LinkComponentContext';
23
+ import { NavControls } from '../primitives/NavControls';
23
24
 
24
- import type { NavigationItem, UserMenuConfig } from '../../types';
25
+ import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../types';
25
26
 
26
27
  export interface MobileDrawerShellProps {
27
28
  isOpen?: boolean;
@@ -32,6 +33,13 @@ export interface MobileDrawerShellProps {
32
33
  outerClassName?: string;
33
34
  /** Panel surface (bg, border, rounding, shadow). */
34
35
  panelClassName?: string;
36
+ /** Optional theme/locale controls shown as a row inside the drawer footer. */
37
+ controls?: {
38
+ showThemeSwitcher?: boolean;
39
+ showLocaleSwitcher?: boolean;
40
+ };
41
+ /** i18n config — required for the locale switcher row. */
42
+ i18n?: I18nLayoutConfig;
35
43
  }
36
44
 
37
45
  export function MobileDrawerShell(props: MobileDrawerShellProps) {
@@ -71,6 +79,10 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
71
79
 
72
80
  const hasSessionUser = Boolean(isAuthenticated && user);
73
81
  const showSignInFooter = !hasSessionUser;
82
+ const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
83
+ const showLocaleSwitcher =
84
+ props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
85
+ const showControlsRow = showThemeSwitcher || showLocaleSwitcher;
74
86
 
75
87
  return (
76
88
  <>
@@ -181,6 +193,17 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
181
193
  </div>
182
194
  </div>
183
195
 
196
+ {showControlsRow && (
197
+ <div className="shrink-0 border-t border-border/50 px-4 py-3 flex items-center justify-center gap-2">
198
+ <NavControls
199
+ i18n={props.i18n}
200
+ showThemeSwitcher={showThemeSwitcher}
201
+ showLocaleSwitcher={showLocaleSwitcher}
202
+ size="default"
203
+ />
204
+ </div>
205
+ )}
206
+
184
207
  {showSignInFooter && (
185
208
  <div className="shrink-0 border-t border-border/50 p-4">
186
209
  <Link
@@ -46,8 +46,11 @@ import type { NavigationItem, UserMenuConfig } from '../../types';
46
46
  import { NavActions } from '../primitives/NavActions';
47
47
  import type { NavAction } from '../primitives/NavActionItem';
48
48
  import { NavBrand } from '../primitives/NavBrand';
49
+ import { NavControls } from '../primitives/NavControls';
49
50
  import { NavDesktopItems } from '../primitives/NavDesktopItems';
50
51
 
52
+ import type { I18nLayoutConfig } from '../../types';
53
+
51
54
  const heightCls: Record<PublicNavbarHeight, string> = {
52
55
  sm: 'py-2',
53
56
  md: 'py-3.5',
@@ -113,6 +116,17 @@ export interface NavbarShellProps {
113
116
  /** Arbitrary ReactNode after the mobile toggle. */
114
117
  actionsTrailingSlot?: ReactNode;
115
118
 
119
+ // ── Theme + locale controls (rendered next to UserMenu on desktop) ────────
120
+ /** i18n config — enables the locale switcher. Same type as `DefaultFooter`. */
121
+ i18n?: I18nLayoutConfig;
122
+ /** Toggle individual controls. Locale switcher also requires `i18n`. */
123
+ controls?: {
124
+ /** Light / system / dark pill. @default false */
125
+ showThemeSwitcher?: boolean;
126
+ /** Locale dropdown. Requires `i18n`. @default false */
127
+ showLocaleSwitcher?: boolean;
128
+ };
129
+
116
130
  // ── Props override (used by variants that proxy ctx differently) ──────────
117
131
  mobileMenuOpen?: boolean;
118
132
  onMobileMenuToggle?: () => void;
@@ -124,6 +138,11 @@ export interface NavbarActionsContext {
124
138
  toggleMobileMenu: () => void;
125
139
  toggleMobileLabel: string;
126
140
  navLayout: PublicNavLayout;
141
+ /**
142
+ * Pre-built theme/locale controls node. `null` when both toggles are off.
143
+ * Variants with a custom `renderActions` can choose where to place it.
144
+ */
145
+ controls: ReactNode | null;
127
146
  }
128
147
 
129
148
  export function NavbarShell(props: NavbarShellProps) {
@@ -224,6 +243,18 @@ export function NavbarShell(props: NavbarShellProps) {
224
243
  />
225
244
  ) : null;
226
245
 
246
+ const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
247
+ const showLocaleSwitcher =
248
+ props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
249
+ const hasControls = showThemeSwitcher || showLocaleSwitcher;
250
+ const controlsNode = hasControls ? (
251
+ <NavControls
252
+ i18n={props.i18n}
253
+ showThemeSwitcher={showThemeSwitcher}
254
+ showLocaleSwitcher={showLocaleSwitcher}
255
+ />
256
+ ) : null;
257
+
227
258
  const actionsNode = props.renderActions ? (
228
259
  props.renderActions({
229
260
  userMenu,
@@ -231,6 +262,7 @@ export function NavbarShell(props: NavbarShellProps) {
231
262
  toggleMobileMenu,
232
263
  toggleMobileLabel,
233
264
  navLayout,
265
+ controls: controlsNode,
234
266
  })
235
267
  ) : (
236
268
  <NavActions
@@ -242,6 +274,7 @@ export function NavbarShell(props: NavbarShellProps) {
242
274
  actions={props.actions}
243
275
  leadingSlot={props.actionsLeadingSlot}
244
276
  trailingSlot={props.actionsTrailingSlot}
277
+ controlsSlot={controlsNode}
245
278
  />
246
279
  );
247
280
 
@@ -27,7 +27,7 @@ import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
27
27
  import { useLogout } from '../../hooks';
28
28
  import { LocaleSwitcher } from './LocaleSwitcher';
29
29
 
30
- import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
30
+ import type { I18nLayoutConfig } from '../types';
31
31
  import type { HeaderConfig } from '../PrivateLayout/PrivateLayout';
32
32
 
33
33
  /** Radix portals (dropdown, select, popover) render outside the account node — ignore those clicks for “outside”. */
@@ -52,4 +52,4 @@ export type {
52
52
  // Layout Types
53
53
  // ============================================================================
54
54
 
55
- export type { BaseLayoutProps, DebugConfig } from './layout.types';
55
+ export type { BaseLayoutProps, DebugConfig, I18nLayoutConfig } from './layout.types';
@@ -70,3 +70,16 @@ export interface DebugConfig extends DebugButtonProps {
70
70
  enabled?: boolean;
71
71
  }
72
72
 
73
+ /**
74
+ * i18n configuration consumed by layouts, navbars, footer controls, and the
75
+ * UserMenu locale row. Same shape as `@djangocfg/nextjs`'s `useLocaleSwitcher`.
76
+ */
77
+ export interface I18nLayoutConfig {
78
+ /** Current locale (e.g. `"en"`, `"ru"`, `"pt-BR"`). */
79
+ locale: string;
80
+ /** Available locales. */
81
+ locales: string[];
82
+ /** Called when the user picks a new locale. */
83
+ onLocaleChange: (locale: string) => void;
84
+ }
85
+