@djangocfg/layouts 2.1.304 → 2.1.308
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 +14 -11
- package/src/layouts/AppLayout/BaseApp.tsx +3 -1
- package/src/layouts/AppLayout/LayoutI18nProvider.tsx +59 -0
- package/src/layouts/AppLayout/index.ts +7 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -5
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +2 -4
- package/src/layouts/PublicLayout/README.md +13 -13
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +6 -11
- package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +15 -6
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +1 -5
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +1 -5
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -6
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +5 -9
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +2 -6
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +1 -7
- package/src/layouts/_components/LocaleSwitcher.tsx +40 -178
- package/src/layouts/_components/PrivateSidebarAccount.tsx +6 -8
- package/src/layouts/_components/UserMenu.tsx +15 -19
- package/src/layouts/_components/index.ts +23 -2
- package/src/layouts/_components/locale-switcher/LocaleCard.tsx +91 -0
- package/src/layouts/_components/locale-switcher/LocaleGrid.tsx +128 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +100 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherDialog.tsx +168 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +101 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +103 -0
- package/src/layouts/_components/locale-switcher/index.ts +27 -0
- package/src/layouts/_components/locale-switcher/localeMeta.ts +109 -0
- package/src/layouts/_components/locale-switcher/types.ts +48 -0
- package/src/layouts/types/layout.types.ts +37 -1
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. All anchors render through `<Link>` from `@djangocfg/ui-core/components` — wrap with `LinkProvider` higher in the tree to inject a locale-aware Link (e.g. `next-intl`). All three navbars accept `controls`
|
|
90
|
+
| **`PublicLayout`** | Marketing / docs. Slots for navbar + footer. All anchors render through `<Link>` from `@djangocfg/ui-core/components` — wrap with `LinkProvider` higher in the tree to inject a locale-aware Link (e.g. `next-intl`). All three navbars accept a `controls` block to show theme / locale switchers next to `UserMenu`; locale data flows from `LayoutI18nProvider` (mounted by `AppLayout` / `BaseApp` when you pass `i18n`). The locale switcher renders SVG country flags and matches the `UserMenu` avatar size at `size="icon"`. **[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. Defaults to the `boxed` visual (inset rounded card on a sidebar-coloured canvas); pass `visual={{ variant: 'full-bleed' }}` for the legacy edge-to-edge layout. |
|
|
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.308",
|
|
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.308",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.308",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.308",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.308",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.308",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.308",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.308",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.308",
|
|
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.308",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.308",
|
|
115
|
+
"@djangocfg/debuger": "^2.1.308",
|
|
116
|
+
"@djangocfg/i18n": "^2.1.308",
|
|
117
|
+
"@djangocfg/monitor": "^2.1.308",
|
|
118
|
+
"@djangocfg/typescript-config": "^2.1.308",
|
|
119
|
+
"@djangocfg/ui-core": "^2.1.308",
|
|
120
|
+
"@djangocfg/ui-nextjs": "^2.1.308",
|
|
121
|
+
"@djangocfg/ui-tools": "^2.1.308",
|
|
122
122
|
"@types/node": "^24.7.2",
|
|
123
123
|
"@types/react": "^19.1.0",
|
|
124
124
|
"@types/react-dom": "^19.1.0",
|
|
@@ -38,6 +38,7 @@ import { ClientOnly, Suspense } from '../../components/core';
|
|
|
38
38
|
import { usePathnameWithoutLocale } from '../../hooks';
|
|
39
39
|
import { matchesPath } from '../../utils/pathMatcher';
|
|
40
40
|
import { BaseApp } from './BaseApp';
|
|
41
|
+
import { useLayoutI18nOptional } from './LayoutI18nProvider';
|
|
41
42
|
import { ForcedThemeProvider, ThemeOverride, resolveForcedTheme } from '@djangocfg/ui-nextjs/theme';
|
|
42
43
|
|
|
43
44
|
import type {
|
|
@@ -159,10 +160,12 @@ function determineLayoutMode(
|
|
|
159
160
|
/**
|
|
160
161
|
* Props passed to every layout component (`public` / `private` / `admin`).
|
|
161
162
|
* Use `publicChrome` to pass defaults for `FloatingNavbar` / `DefaultFooter` from `AppLayout`.
|
|
163
|
+
*
|
|
164
|
+
* Locale plumbing is no longer threaded as a prop — layouts read it from
|
|
165
|
+
* `useLayoutI18nOptional()` (mounted in `BaseApp`).
|
|
162
166
|
*/
|
|
163
167
|
export interface AppLayoutLayoutComponentProps {
|
|
164
168
|
children: ReactNode;
|
|
165
|
-
i18n?: I18nLayoutConfig;
|
|
166
169
|
publicChrome?: AppLayoutPublicChrome;
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -298,7 +301,6 @@ interface AppLayoutContentProps {
|
|
|
298
301
|
adminLayout?: LayoutConfig;
|
|
299
302
|
noLayoutPaths?: string | string[];
|
|
300
303
|
authPath?: string;
|
|
301
|
-
i18n?: I18nLayoutConfig;
|
|
302
304
|
publicChrome?: AppLayoutPublicChrome;
|
|
303
305
|
themeOverrides?: ThemeOverrideRule[];
|
|
304
306
|
}
|
|
@@ -316,12 +318,13 @@ function AppLayoutContent({
|
|
|
316
318
|
adminLayout,
|
|
317
319
|
noLayoutPaths,
|
|
318
320
|
authPath = '/auth',
|
|
319
|
-
i18n,
|
|
320
321
|
publicChrome,
|
|
321
322
|
themeOverrides,
|
|
322
323
|
}: AppLayoutContentProps) {
|
|
323
|
-
// Use pathname without locale prefix for route matching.
|
|
324
|
-
//
|
|
324
|
+
// Use pathname without locale prefix for route matching. The current
|
|
325
|
+
// locale comes from the LayoutI18nProvider mounted by BaseApp — passing
|
|
326
|
+
// it explicitly avoids the regex misfiring on `/ui`, `/ko`, etc.
|
|
327
|
+
const i18n = useLayoutI18nOptional();
|
|
325
328
|
const pathname = usePathnameWithoutLocale(i18n?.locale);
|
|
326
329
|
|
|
327
330
|
// Merge authPath into noLayoutPaths — auth pages are always fullscreen
|
|
@@ -361,7 +364,7 @@ function AppLayoutContent({
|
|
|
361
364
|
return (
|
|
362
365
|
<ClientOnly>
|
|
363
366
|
<Suspense>
|
|
364
|
-
<privateLayout.component
|
|
367
|
+
<privateLayout.component publicChrome={publicChrome}>
|
|
365
368
|
{children}
|
|
366
369
|
</privateLayout.component>
|
|
367
370
|
</Suspense>
|
|
@@ -374,7 +377,7 @@ function AppLayoutContent({
|
|
|
374
377
|
return (
|
|
375
378
|
<ClientOnly>
|
|
376
379
|
<Suspense>
|
|
377
|
-
<adminLayout.component
|
|
380
|
+
<adminLayout.component publicChrome={publicChrome}>
|
|
378
381
|
{children}
|
|
379
382
|
</adminLayout.component>
|
|
380
383
|
</Suspense>
|
|
@@ -385,7 +388,7 @@ function AppLayoutContent({
|
|
|
385
388
|
if (!privateLayout) {
|
|
386
389
|
if (publicLayout) {
|
|
387
390
|
return (
|
|
388
|
-
<publicLayout.component
|
|
391
|
+
<publicLayout.component publicChrome={publicChrome}>
|
|
389
392
|
{children}
|
|
390
393
|
</publicLayout.component>
|
|
391
394
|
);
|
|
@@ -395,7 +398,7 @@ function AppLayoutContent({
|
|
|
395
398
|
return (
|
|
396
399
|
<ClientOnly>
|
|
397
400
|
<Suspense>
|
|
398
|
-
<privateLayout.component
|
|
401
|
+
<privateLayout.component publicChrome={publicChrome}>
|
|
399
402
|
{children}
|
|
400
403
|
</privateLayout.component>
|
|
401
404
|
</Suspense>
|
|
@@ -409,7 +412,7 @@ function AppLayoutContent({
|
|
|
409
412
|
return children;
|
|
410
413
|
}
|
|
411
414
|
return (
|
|
412
|
-
<publicLayout.component
|
|
415
|
+
<publicLayout.component publicChrome={publicChrome}>
|
|
413
416
|
{children}
|
|
414
417
|
</publicLayout.component>
|
|
415
418
|
);
|
|
@@ -480,6 +483,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|
|
480
483
|
pwaInstall={pwaInstall}
|
|
481
484
|
monitor={monitor}
|
|
482
485
|
debug={debug}
|
|
486
|
+
i18n={i18n}
|
|
483
487
|
>
|
|
484
488
|
<AppLayoutContent
|
|
485
489
|
children={children}
|
|
@@ -488,7 +492,6 @@ export function AppLayout(props: AppLayoutProps) {
|
|
|
488
492
|
adminLayout={adminLayout}
|
|
489
493
|
noLayoutPaths={noLayoutPaths}
|
|
490
494
|
authPath={authPath}
|
|
491
|
-
i18n={i18n}
|
|
492
495
|
publicChrome={publicChrome}
|
|
493
496
|
themeOverrides={themeOverrides}
|
|
494
497
|
/>
|
|
@@ -58,6 +58,7 @@ import { AuthDialog } from '../../snippets/AuthDialog';
|
|
|
58
58
|
import { A2HSHint, PWAPageResumeManager, PwaProvider } from '@djangocfg/ui-nextjs/pwa';
|
|
59
59
|
|
|
60
60
|
import type { BaseLayoutProps } from '../types/layout.types';
|
|
61
|
+
import { LayoutI18nProvider } from './LayoutI18nProvider';
|
|
61
62
|
|
|
62
63
|
// Lazy load DebugButton from @djangocfg/debuger
|
|
63
64
|
const DebugButton = dynamic(
|
|
@@ -89,6 +90,7 @@ export function BaseApp({
|
|
|
89
90
|
pwaInstall,
|
|
90
91
|
monitor,
|
|
91
92
|
debug,
|
|
93
|
+
i18n,
|
|
92
94
|
}: BaseAppProps) {
|
|
93
95
|
// ErrorBoundary is enabled by default
|
|
94
96
|
const enableErrorBoundary = errorBoundary?.enabled !== false;
|
|
@@ -156,7 +158,7 @@ export function BaseApp({
|
|
|
156
158
|
onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
|
|
157
159
|
>
|
|
158
160
|
<MonitorProvider {...monitorConfig} />
|
|
159
|
-
{children}
|
|
161
|
+
<LayoutI18nProvider value={i18n}>{children}</LayoutI18nProvider>
|
|
160
162
|
<NextTopLoader
|
|
161
163
|
color="hsl(var(--primary))"
|
|
162
164
|
height={3}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutI18nProvider — single source of truth for locale plumbing inside layouts.
|
|
3
|
+
*
|
|
4
|
+
* Mounted by `BaseApp` when `i18n` is passed. Every locale-aware component
|
|
5
|
+
* inside layouts (footer, navbar controls, mobile drawer, private sidebar
|
|
6
|
+
* account, UserMenu, LocaleSwitcher) reads from here instead of receiving
|
|
7
|
+
* `i18n` as a prop.
|
|
8
|
+
*
|
|
9
|
+
* Components that may render outside the provider (e.g. raw shells used in
|
|
10
|
+
* playground previews) should call `useLayoutI18nOptional()` and gracefully
|
|
11
|
+
* hide locale-related UI when it returns `null`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
|
|
18
|
+
import type { I18nLayoutConfig } from '../types/layout.types';
|
|
19
|
+
|
|
20
|
+
const LayoutI18nContext = React.createContext<I18nLayoutConfig | null>(null);
|
|
21
|
+
|
|
22
|
+
export interface LayoutI18nProviderProps {
|
|
23
|
+
value: I18nLayoutConfig | null | undefined;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Provider — mount once at the app root (handled automatically by `BaseApp`).
|
|
29
|
+
* Passing `null`/`undefined` is fine: consumers fall back to "no locale switcher".
|
|
30
|
+
*/
|
|
31
|
+
export function LayoutI18nProvider({ value, children }: LayoutI18nProviderProps) {
|
|
32
|
+
// `null` keeps the context discriminator simple — `useLayoutI18nOptional`
|
|
33
|
+
// returns `null` whether the provider is missing or `value` is empty.
|
|
34
|
+
const stable = value ?? null;
|
|
35
|
+
return <LayoutI18nContext.Provider value={stable}>{children}</LayoutI18nContext.Provider>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Throws when used outside the provider. Use for components that strictly
|
|
40
|
+
* require locale plumbing (the locale picker dialog, for instance).
|
|
41
|
+
*/
|
|
42
|
+
export function useLayoutI18n(): I18nLayoutConfig {
|
|
43
|
+
const ctx = React.useContext(LayoutI18nContext);
|
|
44
|
+
if (!ctx) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'useLayoutI18n must be used inside <LayoutI18nProvider> — pass `i18n` to <BaseApp> / <AppLayout>.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional accessor — returns `null` outside the provider or when no `i18n`
|
|
54
|
+
* was configured. Use for opt-in mounts (footer locale switcher, navbar
|
|
55
|
+
* locale dropdown) that should silently disappear when locale support is off.
|
|
56
|
+
*/
|
|
57
|
+
export function useLayoutI18nOptional(): I18nLayoutConfig | null {
|
|
58
|
+
return React.useContext(LayoutI18nContext);
|
|
59
|
+
}
|
|
@@ -20,6 +20,13 @@ export { mergeAppLayoutPublicChrome } from './AppLayout';
|
|
|
20
20
|
export { BaseApp } from './BaseApp';
|
|
21
21
|
export type { BaseAppProps } from './BaseApp';
|
|
22
22
|
|
|
23
|
+
export {
|
|
24
|
+
LayoutI18nProvider,
|
|
25
|
+
useLayoutI18n,
|
|
26
|
+
useLayoutI18nOptional,
|
|
27
|
+
} from './LayoutI18nProvider';
|
|
28
|
+
export type { LayoutI18nProviderProps } from './LayoutI18nProvider';
|
|
29
|
+
|
|
23
30
|
// Re-export theme-override primitives so consumers can import both the
|
|
24
31
|
// AppLayout config surface and the rule/theme types from one place.
|
|
25
32
|
export {
|
|
@@ -16,7 +16,7 @@ import { Preloader } from '@djangocfg/ui-core/components';
|
|
|
16
16
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-nextjs/components';
|
|
17
17
|
|
|
18
18
|
import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
|
|
19
|
-
import type {
|
|
19
|
+
import type { LayoutVisualConfig } from '../types';
|
|
20
20
|
import { UserMenuConfig } from '../types';
|
|
21
21
|
import { PrivateContent, PrivateSidebar } from './components';
|
|
22
22
|
|
|
@@ -101,8 +101,6 @@ export interface PrivateLayoutProps {
|
|
|
101
101
|
pathname?: string;
|
|
102
102
|
/** Content padding */
|
|
103
103
|
contentPadding?: 'none' | 'default';
|
|
104
|
-
/** i18n configuration for locale switching */
|
|
105
|
-
i18n?: I18nLayoutConfig;
|
|
106
104
|
/**
|
|
107
105
|
* Visual style of the shell. Defaults to `'boxed'` (inset rounded card on a
|
|
108
106
|
* sidebar-coloured canvas). Pass `{ variant: 'full-bleed' }` for the legacy
|
|
@@ -124,7 +122,6 @@ export function PrivateLayout({
|
|
|
124
122
|
header,
|
|
125
123
|
pathname,
|
|
126
124
|
contentPadding = 'default',
|
|
127
|
-
i18n,
|
|
128
125
|
visual,
|
|
129
126
|
requireAuth = true,
|
|
130
127
|
}: PrivateLayoutProps) {
|
|
@@ -167,7 +164,6 @@ export function PrivateLayout({
|
|
|
167
164
|
<PrivateSidebar
|
|
168
165
|
sidebar={sidebar}
|
|
169
166
|
header={header}
|
|
170
|
-
i18n={i18n}
|
|
171
167
|
pathname={pathname}
|
|
172
168
|
variant={sidebarVariant}
|
|
173
169
|
/>
|
|
@@ -29,7 +29,6 @@ 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 '../../types';
|
|
33
32
|
import type { HeaderConfig, SidebarItem, SidebarConfig } from '../PrivateLayout';
|
|
34
33
|
|
|
35
34
|
/** Few items → roomier rows; many items → tighter. Same breakpoints for demo, CarAPIS, etc. */
|
|
@@ -99,7 +98,6 @@ const RAIL_NAV = DENSITY.default;
|
|
|
99
98
|
interface PrivateSidebarProps {
|
|
100
99
|
sidebar: SidebarConfig;
|
|
101
100
|
header?: HeaderConfig;
|
|
102
|
-
i18n?: I18nLayoutConfig;
|
|
103
101
|
pathname?: string;
|
|
104
102
|
/**
|
|
105
103
|
* shadcn-sidebar `variant`. Used to trigger the inset/boxed visual:
|
|
@@ -109,7 +107,7 @@ interface PrivateSidebarProps {
|
|
|
109
107
|
variant?: 'sidebar' | 'inset';
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
export function PrivateSidebar({ sidebar, header,
|
|
110
|
+
export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, variant = 'sidebar' }: PrivateSidebarProps) {
|
|
113
111
|
const pathnameFromNext = useNextPathname();
|
|
114
112
|
const pathname = pathnameProp ?? pathnameFromNext;
|
|
115
113
|
const { state, isMobile, setOpen, setOpenMobile } = useSidebar();
|
|
@@ -324,7 +322,7 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp,
|
|
|
324
322
|
|
|
325
323
|
<SidebarFooter className="p-2">
|
|
326
324
|
{footerExtra}
|
|
327
|
-
<PrivateSidebarAccount header={header}
|
|
325
|
+
<PrivateSidebarAccount header={header} />
|
|
328
326
|
</SidebarFooter>
|
|
329
327
|
</Sidebar>
|
|
330
328
|
);
|
|
@@ -7,7 +7,7 @@ import { PublicLayout, FloatingNavbar, DefaultFooter } from '@djangocfg/layouts'
|
|
|
7
7
|
|
|
8
8
|
<PublicLayout
|
|
9
9
|
navbar={<FloatingNavbar config={{ brand, navigation, userMenu, actions }} />}
|
|
10
|
-
footer={<DefaultFooter config={{ variant: 'full', menus: { sections }
|
|
10
|
+
footer={<DefaultFooter config={{ variant: 'full', menus: { sections } }} />}
|
|
11
11
|
>
|
|
12
12
|
{children}
|
|
13
13
|
</PublicLayout>
|
|
@@ -74,8 +74,7 @@ Three variants. All share the same core props (below); only the chrome differs.
|
|
|
74
74
|
| `transparentThreshold` | `number` | `40` | Px past which the nav becomes opaque. |
|
|
75
75
|
| `desktopMaxPrimaryItems` | `number` | auto | Hard cap for primary items before overflow. |
|
|
76
76
|
| `renderDesktopDropdown` | `(ctx) => ReactNode` | — | Replace default popover per-item. |
|
|
77
|
-
| `controls` | `{ showThemeSwitcher?; showLocaleSwitcher? }` | — | Compact theme + locale pills next to UserMenu. See below. |
|
|
78
|
-
| `i18n` | `{ locale, locales, onLocaleChange }` | — | Required for the locale switcher (same shape as `DefaultFooter.i18n`). |
|
|
77
|
+
| `controls` | `{ showThemeSwitcher?; showLocaleSwitcher? }` | — | Compact theme + locale pills next to UserMenu. Locale data flows from `LayoutI18nProvider` (mounted by `BaseApp`); the switcher hides itself when no provider is present. See below. |
|
|
79
78
|
|
|
80
79
|
### Variant-only
|
|
81
80
|
|
|
@@ -94,10 +93,9 @@ Three variants. All share the same core props (below); only the chrome differs.
|
|
|
94
93
|
|
|
95
94
|
## Theme + locale controls (navbar)
|
|
96
95
|
|
|
97
|
-
All three navbars accept an optional `controls` block
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
drawer footer.
|
|
96
|
+
All three navbars accept an optional `controls` block. When enabled, a compact
|
|
97
|
+
`NavControls` pill is rendered on desktop right before `UserMenu`, and an
|
|
98
|
+
equivalent row appears in the mobile drawer footer.
|
|
101
99
|
|
|
102
100
|
```tsx
|
|
103
101
|
<FloatingNavbar
|
|
@@ -106,7 +104,6 @@ drawer footer.
|
|
|
106
104
|
navigation,
|
|
107
105
|
userMenu: { authPath: '/auth' },
|
|
108
106
|
controls: { showThemeSwitcher: true, showLocaleSwitcher: true },
|
|
109
|
-
i18n: { locale, locales, onLocaleChange: changeLocale },
|
|
110
107
|
}}
|
|
111
108
|
/>
|
|
112
109
|
```
|
|
@@ -114,7 +111,12 @@ drawer footer.
|
|
|
114
111
|
| Control | Required | Notes |
|
|
115
112
|
|---|---|---|
|
|
116
113
|
| `controls.showThemeSwitcher` | — | Light / system / dark pill (uses `useThemeContext`). |
|
|
117
|
-
| `controls.showLocaleSwitcher` | `
|
|
114
|
+
| `controls.showLocaleSwitcher` | `LayoutI18nProvider` | Locale switcher (icon-size pill matching `UserMenu` avatar). Hidden when no provider is mounted. |
|
|
115
|
+
|
|
116
|
+
Locale data (`locale`, `locales`, `onLocaleChange`) is read from
|
|
117
|
+
`LayoutI18nProvider`, which `BaseApp` (and `AppLayout`) mounts automatically
|
|
118
|
+
when you pass an `i18n` config. The switcher renders SVG country flags via
|
|
119
|
+
`country-flag-icons` — no emoji.
|
|
118
120
|
|
|
119
121
|
Same shape works for `FlushNavbar` and `MinimalNavbar`. If you build a custom
|
|
120
122
|
navbar with `NavbarShell`, the controls node is pre-built and delivered to
|
|
@@ -129,7 +131,6 @@ import { NavControls } from '@djangocfg/layouts';
|
|
|
129
131
|
<NavControls
|
|
130
132
|
showThemeSwitcher
|
|
131
133
|
showLocaleSwitcher
|
|
132
|
-
i18n={{ locale, locales, onLocaleChange }}
|
|
133
134
|
size="compact" // 'compact' (navbar) | 'default' (footer)
|
|
134
135
|
/>
|
|
135
136
|
```
|
|
@@ -160,7 +161,7 @@ actions={[
|
|
|
160
161
|
Three variants: `full` (default) with brand column + menus + controls; `compact` (one row + bottom); `simple` (copyright only).
|
|
161
162
|
|
|
162
163
|
```tsx
|
|
163
|
-
<DefaultFooter config={{ variant: 'full', menus: { sections },
|
|
164
|
+
<DefaultFooter config={{ variant: 'full', menus: { sections }, slots }} />
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
| Prop (`config.*`) | Type | Default | Role |
|
|
@@ -178,9 +179,8 @@ Three variants: `full` (default) with brand column + menus + controls; `compact`
|
|
|
178
179
|
| `social` | `FooterSocialLinks` | — | Social icon row under brand. |
|
|
179
180
|
| `meta.copyright` | `string` | `© ${year}. All rights reserved.` | |
|
|
180
181
|
| `meta.credits` | `{ text: string; url?: string }` | — | |
|
|
181
|
-
| `i18n` | `{ locale, locales, onLocaleChange }` | — | Required for the locale switcher. |
|
|
182
182
|
| `controls.showThemeSwitcher` | `boolean` | `true` | Full variant only. |
|
|
183
|
-
| `controls.showLocaleSwitcher` | `boolean` | `true`
|
|
183
|
+
| `controls.showLocaleSwitcher` | `boolean` | `true` | Full variant only. Reads locale from `LayoutI18nProvider`; hidden when no provider is mounted. |
|
|
184
184
|
| `slots.aboveMenus` | `ReactNode` | — | Above the brand/menus grid (full variant). |
|
|
185
185
|
| `slots.belowMenus` | `ReactNode` | — | Between menus and bottom row. |
|
|
186
186
|
| `slots.bottomStart` | `ReactNode` | — | Next to copyright. |
|
|
@@ -15,6 +15,7 @@ import { useForcedTheme, useThemeContext } from '@djangocfg/ui-nextjs/theme';
|
|
|
15
15
|
import { Link } from '@djangocfg/ui-core/components';
|
|
16
16
|
|
|
17
17
|
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
18
|
+
import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
|
|
18
19
|
import { FooterMenuSections } from './FooterMenuSections';
|
|
19
20
|
import { FooterProjectInfo } from './FooterProjectInfo';
|
|
20
21
|
|
|
@@ -117,7 +118,7 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
117
118
|
const socialLinks = config.social;
|
|
118
119
|
const copyrightProp = config.meta?.copyright;
|
|
119
120
|
const creditsProp = config.meta?.credits;
|
|
120
|
-
const i18n =
|
|
121
|
+
const i18n = useLayoutI18nOptional();
|
|
121
122
|
const showThemeSwitcher = config.controls?.showThemeSwitcher !== false;
|
|
122
123
|
const showLocaleSwitcher =
|
|
123
124
|
config.controls?.showLocaleSwitcher !== false && Boolean(i18n);
|
|
@@ -260,12 +261,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
260
261
|
{showFooterControlsRow && (
|
|
261
262
|
<div className="mt-5 flex items-center justify-center gap-2 border-t border-border/60 pt-4">
|
|
262
263
|
{showThemeSwitcher && <ThemeModeControl />}
|
|
263
|
-
{showLocaleSwitcher &&
|
|
264
|
+
{showLocaleSwitcher && (
|
|
264
265
|
<LocaleSwitcher
|
|
265
|
-
|
|
266
|
-
locales={i18n.locales}
|
|
267
|
-
onChange={i18n.onLocaleChange}
|
|
268
|
-
variant="outline"
|
|
266
|
+
buttonVariant="outline"
|
|
269
267
|
size="default"
|
|
270
268
|
className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
|
|
271
269
|
/>
|
|
@@ -348,12 +346,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
|
|
|
348
346
|
<div className="flex items-center justify-center gap-2 lg:justify-self-end">
|
|
349
347
|
{slots?.bottomEnd}
|
|
350
348
|
{showThemeSwitcher && <ThemeModeControl />}
|
|
351
|
-
{showLocaleSwitcher &&
|
|
349
|
+
{showLocaleSwitcher && (
|
|
352
350
|
<LocaleSwitcher
|
|
353
|
-
|
|
354
|
-
locales={i18n.locales}
|
|
355
|
-
onChange={i18n.onLocaleChange}
|
|
356
|
-
variant="outline"
|
|
351
|
+
buttonVariant="outline"
|
|
357
352
|
size="default"
|
|
358
353
|
className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
|
|
359
354
|
/>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { LucideIcon } from 'lucide-react';
|
|
6
6
|
import type { ReactNode } from 'react';
|
|
7
7
|
|
|
8
|
-
import type { FooterLink, FooterMenuSection, FooterSocialLinks
|
|
8
|
+
import type { FooterLink, FooterMenuSection, FooterSocialLinks } from '../../../types';
|
|
9
9
|
|
|
10
10
|
export type { FooterLink, FooterMenuSection, FooterSocialLinks };
|
|
11
11
|
|
|
@@ -23,6 +23,15 @@ export interface DefaultFooterConfig {
|
|
|
23
23
|
icon: LucideIcon;
|
|
24
24
|
text: string;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Logo node displayed in the locale-switcher dialog header.
|
|
28
|
+
* Falls back to a generic Globe glyph when omitted.
|
|
29
|
+
*/
|
|
30
|
+
logo?: ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* Project name displayed beside the logo in the locale-switcher dialog.
|
|
33
|
+
*/
|
|
34
|
+
name?: string;
|
|
26
35
|
};
|
|
27
36
|
menus?: {
|
|
28
37
|
sections?: FooterMenuSection[];
|
|
@@ -38,16 +47,16 @@ export interface DefaultFooterConfig {
|
|
|
38
47
|
};
|
|
39
48
|
};
|
|
40
49
|
social?: FooterSocialLinks;
|
|
41
|
-
i18n?: I18nLayoutConfig;
|
|
42
50
|
/**
|
|
43
|
-
* Full (`variant: 'full'`) footer only — bottom row with theme and/or locale
|
|
44
|
-
*
|
|
45
|
-
*
|
|
51
|
+
* Full (`variant: 'full'`) footer only — bottom row with theme and/or locale
|
|
52
|
+
* controls. The locale switcher reads its plumbing from `LayoutI18nProvider`
|
|
53
|
+
* (mounted by `BaseApp`); when the provider is empty, the switcher hides
|
|
54
|
+
* itself regardless of this flag.
|
|
46
55
|
*/
|
|
47
56
|
controls?: {
|
|
48
57
|
/** Light / system / dark toggle. @default true */
|
|
49
58
|
showThemeSwitcher?: boolean;
|
|
50
|
-
/**
|
|
59
|
+
/** @default true */
|
|
51
60
|
showLocaleSwitcher?: boolean;
|
|
52
61
|
};
|
|
53
62
|
/**
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
PublicNavbarShellConfig,
|
|
22
22
|
PublicNavLayout,
|
|
23
23
|
} from '../../navbarTypes';
|
|
24
|
-
import type {
|
|
24
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
25
25
|
|
|
26
26
|
import { FloatingMobileDrawer } from './FloatingMobileDrawer';
|
|
27
27
|
|
|
@@ -65,8 +65,6 @@ export interface FloatingNavbarConfig {
|
|
|
65
65
|
/** Locale dropdown. Requires `i18n`. @default false */
|
|
66
66
|
showLocaleSwitcher?: boolean;
|
|
67
67
|
};
|
|
68
|
-
/** i18n config (current locale + locales + onLocaleChange). */
|
|
69
|
-
i18n?: I18nLayoutConfig;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
export interface FloatingNavbarProps {
|
|
@@ -113,7 +111,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
113
111
|
actionsLeadingSlot={config.actionsLeadingSlot}
|
|
114
112
|
actionsTrailingSlot={config.actionsTrailingSlot}
|
|
115
113
|
controls={config.controls}
|
|
116
|
-
i18n={config.i18n}
|
|
117
114
|
outerClassName={outerClassName}
|
|
118
115
|
shapeClassName={shapeClassName}
|
|
119
116
|
shapeForState={({ scrolled, transparent }) =>
|
|
@@ -131,7 +128,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
|
|
|
131
128
|
containerClassName={containerClassName}
|
|
132
129
|
rounding={rounding}
|
|
133
130
|
controls={config.controls}
|
|
134
|
-
i18n={config.i18n}
|
|
135
131
|
/>
|
|
136
132
|
</>
|
|
137
133
|
);
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
PublicNavbarShellConfig,
|
|
21
21
|
PublicNavLayout,
|
|
22
22
|
} from '../../navbarTypes';
|
|
23
|
-
import type {
|
|
23
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
24
24
|
|
|
25
25
|
import { FlushMobileDrawer } from './FlushMobileDrawer';
|
|
26
26
|
|
|
@@ -64,8 +64,6 @@ export interface FlushNavbarConfig {
|
|
|
64
64
|
/** Locale dropdown. Requires `i18n`. @default false */
|
|
65
65
|
showLocaleSwitcher?: boolean;
|
|
66
66
|
};
|
|
67
|
-
/** i18n config (current locale + locales + onLocaleChange). */
|
|
68
|
-
i18n?: I18nLayoutConfig;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
export interface FlushNavbarProps {
|
|
@@ -109,7 +107,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
109
107
|
actionsLeadingSlot={config.actionsLeadingSlot}
|
|
110
108
|
actionsTrailingSlot={config.actionsTrailingSlot}
|
|
111
109
|
controls={config.controls}
|
|
112
|
-
i18n={config.i18n}
|
|
113
110
|
outerClassName={outerClassName}
|
|
114
111
|
shapeClassName={shapeClassName}
|
|
115
112
|
shapeForState={({ scrolled, transparent }) =>
|
|
@@ -126,7 +123,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
|
|
|
126
123
|
userMenu={config.userMenu}
|
|
127
124
|
containerClassName={containerClassName}
|
|
128
125
|
controls={config.controls}
|
|
129
|
-
i18n={config.i18n}
|
|
130
126
|
/>
|
|
131
127
|
</>
|
|
132
128
|
);
|
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
PublicNavbarPosition,
|
|
27
27
|
PublicNavLayout,
|
|
28
28
|
} from '../../navbarTypes';
|
|
29
|
-
import type {
|
|
29
|
+
import type { NavigationItem, UserMenuConfig } from '../../../types';
|
|
30
30
|
|
|
31
31
|
import { MinimalMobileDrawer } from './MinimalMobileDrawer';
|
|
32
32
|
|
|
@@ -81,8 +81,6 @@ export interface MinimalNavbarConfig {
|
|
|
81
81
|
/** Locale dropdown. Requires `i18n`. @default false */
|
|
82
82
|
showLocaleSwitcher?: boolean;
|
|
83
83
|
};
|
|
84
|
-
/** i18n config (current locale + locales + onLocaleChange). */
|
|
85
|
-
i18n?: I18nLayoutConfig;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
export interface MinimalNavbarProps {
|
|
@@ -115,7 +113,6 @@ function MinimalActions({
|
|
|
115
113
|
variant="desktop"
|
|
116
114
|
groups={ctx.userMenu?.groups}
|
|
117
115
|
authPath={ctx.userMenu?.authPath}
|
|
118
|
-
i18n={ctx.userMenu?.i18n}
|
|
119
116
|
/>
|
|
120
117
|
</div>
|
|
121
118
|
|
|
@@ -168,7 +165,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
168
165
|
transparent={transparent}
|
|
169
166
|
transparentThreshold={config.transparentThreshold}
|
|
170
167
|
controls={config.controls}
|
|
171
|
-
i18n={config.i18n}
|
|
172
168
|
outerClassName={outerClassName}
|
|
173
169
|
shapeClassName={shapeClassName}
|
|
174
170
|
innerPadding={containerClassName}
|
|
@@ -187,7 +183,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
|
|
|
187
183
|
userMenu={config.userMenu}
|
|
188
184
|
containerClassName={containerClassName}
|
|
189
185
|
controls={config.controls}
|
|
190
|
-
i18n={config.i18n}
|
|
191
186
|
/>
|
|
192
187
|
</>
|
|
193
188
|
);
|