@djangocfg/layouts 2.1.280 → 2.1.282
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 +18 -18
- package/src/layouts/AppLayout/AppLayout.tsx +2 -10
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +2 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +1 -1
- package/src/layouts/PublicLayout/README.md +45 -1
- package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +1 -2
- package/src/layouts/PublicLayout/index.ts +2 -0
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +18 -1
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +18 -1
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +22 -1
- package/src/layouts/PublicLayout/primitives/NavActions.tsx +8 -0
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +114 -0
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +6 -9
- package/src/layouts/PublicLayout/primitives/index.ts +2 -0
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +30 -2
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +40 -1
- package/src/layouts/_components/PrivateSidebarAccount.tsx +1 -1
- package/src/layouts/types/index.ts +1 -1
- package/src/layouts/types/layout.types.ts +13 -0
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.
|
|
3
|
+
"version": "2.1.282",
|
|
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.
|
|
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.282",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.282",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.282",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.282",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.282",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.282",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.282",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.282",
|
|
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.
|
|
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.282",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.282",
|
|
115
|
+
"@djangocfg/debuger": "^2.1.282",
|
|
116
|
+
"@djangocfg/i18n": "^2.1.282",
|
|
117
|
+
"@djangocfg/monitor": "^2.1.282",
|
|
118
|
+
"@djangocfg/typescript-config": "^2.1.282",
|
|
119
|
+
"@djangocfg/ui-core": "^2.1.282",
|
|
120
|
+
"@djangocfg/ui-nextjs": "^2.1.282",
|
|
121
|
+
"@djangocfg/ui-tools": "^2.1.282",
|
|
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
|
|
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 '../../
|
|
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 '../../../
|
|
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
|
+
}
|
|
@@ -66,9 +66,11 @@ export function NavDesktopItems({
|
|
|
66
66
|
gap: 4,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
const effectiveCount =
|
|
69
|
+
const effectiveCount = measured
|
|
70
|
+
? Math.min(visibleCount, maxVisible ?? visibleCount)
|
|
71
|
+
: (maxVisible ?? items.length);
|
|
70
72
|
const primaryItems = items.slice(0, effectiveCount);
|
|
71
|
-
const overflowItems = items.slice(effectiveCount);
|
|
73
|
+
const overflowItems = measured ? items.slice(effectiveCount) : [];
|
|
72
74
|
|
|
73
75
|
const renderItem = (item: NavigationItem) => {
|
|
74
76
|
if (item.items && item.items.length > 0) {
|
|
@@ -191,13 +193,8 @@ export function NavDesktopItems({
|
|
|
191
193
|
))}
|
|
192
194
|
</div>
|
|
193
195
|
|
|
194
|
-
{/* Live row —
|
|
195
|
-
<div
|
|
196
|
-
className={cn(
|
|
197
|
-
'flex min-w-0 items-center gap-1',
|
|
198
|
-
!measured && 'invisible',
|
|
199
|
-
)}
|
|
200
|
-
>
|
|
196
|
+
{/* Live row — renders all items on SSR, overflow splits after measurement. */}
|
|
197
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
201
198
|
{primaryItems.map(renderItem)}
|
|
202
199
|
|
|
203
200
|
{hasOverflow && (
|
|
@@ -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';
|
|
@@ -13,6 +13,7 @@ import React, { useMemo } from 'react';
|
|
|
13
13
|
import { useAuth } from '@djangocfg/api/auth';
|
|
14
14
|
import { useAppT } from '@djangocfg/i18n';
|
|
15
15
|
import { Button } from '@djangocfg/ui-core/components';
|
|
16
|
+
import { useBodyScrollLock, useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
16
17
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
18
|
|
|
18
19
|
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
@@ -20,8 +21,9 @@ import { UserMenu } from '../../_components/UserMenu';
|
|
|
20
21
|
import { usePublicLayoutOptional } from '../context';
|
|
21
22
|
import { useMobileNavPanel } from '../hooks';
|
|
22
23
|
import { useLinkComponent } from '../primitives/LinkComponentContext';
|
|
24
|
+
import { NavControls } from '../primitives/NavControls';
|
|
23
25
|
|
|
24
|
-
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
26
|
+
import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../types';
|
|
25
27
|
|
|
26
28
|
export interface MobileDrawerShellProps {
|
|
27
29
|
isOpen?: boolean;
|
|
@@ -32,6 +34,13 @@ export interface MobileDrawerShellProps {
|
|
|
32
34
|
outerClassName?: string;
|
|
33
35
|
/** Panel surface (bg, border, rounding, shadow). */
|
|
34
36
|
panelClassName?: string;
|
|
37
|
+
/** Optional theme/locale controls shown as a row inside the drawer footer. */
|
|
38
|
+
controls?: {
|
|
39
|
+
showThemeSwitcher?: boolean;
|
|
40
|
+
showLocaleSwitcher?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** i18n config — required for the locale switcher row. */
|
|
43
|
+
i18n?: I18nLayoutConfig;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
@@ -49,6 +58,8 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
49
58
|
isOpen: mobileMenuOpen,
|
|
50
59
|
onClose: closeMobileMenu,
|
|
51
60
|
});
|
|
61
|
+
const isTabletOrBelow = useIsTabletOrBelow();
|
|
62
|
+
useBodyScrollLock(mobileMenuOpen && isTabletOrBelow);
|
|
52
63
|
|
|
53
64
|
const labels = useMemo(() => ({
|
|
54
65
|
menu: t('layouts.navigation.menu'),
|
|
@@ -71,6 +82,10 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
71
82
|
|
|
72
83
|
const hasSessionUser = Boolean(isAuthenticated && user);
|
|
73
84
|
const showSignInFooter = !hasSessionUser;
|
|
85
|
+
const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
|
|
86
|
+
const showLocaleSwitcher =
|
|
87
|
+
props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
|
|
88
|
+
const showControlsRow = showThemeSwitcher || showLocaleSwitcher;
|
|
74
89
|
|
|
75
90
|
return (
|
|
76
91
|
<>
|
|
@@ -84,7 +99,9 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
84
99
|
)}
|
|
85
100
|
<div
|
|
86
101
|
className={cn(
|
|
87
|
-
|
|
102
|
+
// Horizontal padding mirrors the navbar outer wrapper so the drawer
|
|
103
|
+
// panel aligns edge-to-edge with the navbar shell.
|
|
104
|
+
'pointer-events-none fixed inset-x-0 z-1000 lg:hidden px-3 pb-3 sm:px-4 sm:pb-3 lg:px-6',
|
|
88
105
|
)}
|
|
89
106
|
style={{
|
|
90
107
|
top: 'var(--public-navbar-mobile-drawer-top, 5rem)',
|
|
@@ -181,6 +198,17 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
181
198
|
</div>
|
|
182
199
|
</div>
|
|
183
200
|
|
|
201
|
+
{showControlsRow && (
|
|
202
|
+
<div className="shrink-0 border-t border-border/50 px-4 py-3 flex items-center justify-center gap-2">
|
|
203
|
+
<NavControls
|
|
204
|
+
i18n={props.i18n}
|
|
205
|
+
showThemeSwitcher={showThemeSwitcher}
|
|
206
|
+
showLocaleSwitcher={showLocaleSwitcher}
|
|
207
|
+
size="default"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
184
212
|
{showSignInFooter && (
|
|
185
213
|
<div className="shrink-0 border-t border-border/50 p-4">
|
|
186
214
|
<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) {
|
|
@@ -200,7 +219,10 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
200
219
|
|
|
201
220
|
const outerCls = cn(
|
|
202
221
|
outerClassName,
|
|
203
|
-
'inset-x-0
|
|
222
|
+
'inset-x-0',
|
|
223
|
+
// Stay above the mobile drawer backdrop (z-[998]) and drawer (z-[1000])
|
|
224
|
+
// so the brand + close button remain visible and clickable.
|
|
225
|
+
mobileMenuOpen ? 'z-[1001]' : 'z-50',
|
|
204
226
|
hideNavOnScroll && 'transition-transform duration-300 ease-in-out will-change-transform',
|
|
205
227
|
hideNavOnScroll && hidden && !mobileMenuOpen && '-translate-y-full',
|
|
206
228
|
);
|
|
@@ -224,6 +246,18 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
224
246
|
/>
|
|
225
247
|
) : null;
|
|
226
248
|
|
|
249
|
+
const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
|
|
250
|
+
const showLocaleSwitcher =
|
|
251
|
+
props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
|
|
252
|
+
const hasControls = showThemeSwitcher || showLocaleSwitcher;
|
|
253
|
+
const controlsNode = hasControls ? (
|
|
254
|
+
<NavControls
|
|
255
|
+
i18n={props.i18n}
|
|
256
|
+
showThemeSwitcher={showThemeSwitcher}
|
|
257
|
+
showLocaleSwitcher={showLocaleSwitcher}
|
|
258
|
+
/>
|
|
259
|
+
) : null;
|
|
260
|
+
|
|
227
261
|
const actionsNode = props.renderActions ? (
|
|
228
262
|
props.renderActions({
|
|
229
263
|
userMenu,
|
|
@@ -231,6 +265,7 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
231
265
|
toggleMobileMenu,
|
|
232
266
|
toggleMobileLabel,
|
|
233
267
|
navLayout,
|
|
268
|
+
controls: controlsNode,
|
|
234
269
|
})
|
|
235
270
|
) : (
|
|
236
271
|
<NavActions
|
|
@@ -242,6 +277,7 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
242
277
|
actions={props.actions}
|
|
243
278
|
leadingSlot={props.actionsLeadingSlot}
|
|
244
279
|
trailingSlot={props.actionsTrailingSlot}
|
|
280
|
+
controlsSlot={controlsNode}
|
|
245
281
|
/>
|
|
246
282
|
);
|
|
247
283
|
|
|
@@ -254,6 +290,7 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
254
290
|
<div className={cn('flex items-center gap-4', h)}>
|
|
255
291
|
<div className="min-w-0 shrink-0 flex items-center">{brandNode}</div>
|
|
256
292
|
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center gap-1">{desktopNavNode}</div>
|
|
293
|
+
<div className="flex-1 lg:hidden" />
|
|
257
294
|
<div className="flex shrink-0 items-center gap-4">{actionsNode}</div>
|
|
258
295
|
</div>
|
|
259
296
|
);
|
|
@@ -262,6 +299,7 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
262
299
|
<div className={cn('flex items-center gap-4', h)}>
|
|
263
300
|
<div className="shrink-0">{brandNode}</div>
|
|
264
301
|
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center justify-center gap-1">{desktopNavNode}</div>
|
|
302
|
+
<div className="flex-1 lg:hidden" />
|
|
265
303
|
<div className="flex shrink-0 items-center">{actionsNode}</div>
|
|
266
304
|
</div>
|
|
267
305
|
);
|
|
@@ -279,6 +317,7 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
279
317
|
<div className="hidden isolate lg:flex min-w-0 flex-1 items-center justify-center gap-1">
|
|
280
318
|
{desktopNavNode}
|
|
281
319
|
</div>
|
|
320
|
+
<div className="flex-1 lg:hidden" />
|
|
282
321
|
<div className="flex shrink-0 items-center">{actionsNode}</div>
|
|
283
322
|
</div>
|
|
284
323
|
);
|
|
@@ -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 '../
|
|
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
|
+
|