@djangocfg/layouts 2.1.279 → 2.1.281

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/package.json +18 -18
  3. package/src/layouts/AppLayout/AppLayout.tsx +2 -10
  4. package/src/layouts/PrivateLayout/PrivateLayout.tsx +2 -1
  5. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +1 -1
  6. package/src/layouts/PublicLayout/README.md +69 -1
  7. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +8 -7
  8. package/src/layouts/PublicLayout/footers/DefaultFooter/FooterBottom.tsx +4 -3
  9. package/src/layouts/PublicLayout/footers/DefaultFooter/FooterMenuSections.tsx +4 -3
  10. package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +1 -2
  11. package/src/layouts/PublicLayout/index.ts +7 -7
  12. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +18 -11
  13. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +18 -11
  14. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +22 -12
  15. package/src/layouts/PublicLayout/primitives/LinkComponentContext.tsx +50 -0
  16. package/src/layouts/PublicLayout/primitives/NavActionItem.tsx +4 -3
  17. package/src/layouts/PublicLayout/primitives/NavActions.tsx +8 -0
  18. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +5 -3
  19. package/src/layouts/PublicLayout/primitives/NavControls.tsx +114 -0
  20. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +8 -7
  21. package/src/layouts/PublicLayout/primitives/index.ts +9 -9
  22. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +32 -8
  23. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +33 -0
  24. package/src/layouts/_components/PrivateSidebarAccount.tsx +1 -1
  25. package/src/layouts/types/index.ts +1 -1
  26. package/src/layouts/types/layout.types.ts +13 -0
  27. package/src/layouts/PublicLayout/primitives/ExternalPrefixesContext.tsx +0 -69
  28. package/src/layouts/PublicLayout/primitives/SmartNavLink.tsx +0 -81
@@ -0,0 +1,50 @@
1
+ /**
2
+ * LinkComponentContext
3
+ *
4
+ * Dependency-injects the Link component used by navbars, footers, and mobile
5
+ * drawers. Apps wire in their own i18n-aware Link (e.g. next-intl's
6
+ * `createNavigation().Link`) so internal navigation picks up the locale
7
+ * prefix. Default falls back to plain `next/link`.
8
+ */
9
+
10
+ 'use client';
11
+
12
+ import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
13
+ import React, {
14
+ createContext,
15
+ useContext,
16
+ type AnchorHTMLAttributes,
17
+ type ComponentType,
18
+ type ReactNode,
19
+ } from 'react';
20
+
21
+ type AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
22
+
23
+ export type LinkComponentProps = Omit<NextLinkProps, 'passHref' | 'legacyBehavior'> &
24
+ AnchorProps & {
25
+ children?: ReactNode;
26
+ };
27
+
28
+ export type LinkComponent = ComponentType<LinkComponentProps>;
29
+
30
+ const LinkComponentContext = createContext<LinkComponent>(NextLink as LinkComponent);
31
+
32
+ export interface LinkComponentProviderProps {
33
+ value: LinkComponent;
34
+ children: ReactNode;
35
+ }
36
+
37
+ export function LinkComponentProvider({
38
+ value,
39
+ children,
40
+ }: LinkComponentProviderProps) {
41
+ return (
42
+ <LinkComponentContext.Provider value={value}>
43
+ {children}
44
+ </LinkComponentContext.Provider>
45
+ );
46
+ }
47
+
48
+ export function useLinkComponent(): LinkComponent {
49
+ return useContext(LinkComponentContext);
50
+ }
@@ -11,7 +11,7 @@ import React, { type ReactNode } from 'react';
11
11
 
12
12
  import { cn } from '@djangocfg/ui-core/lib';
13
13
 
14
- import { SmartNavLink } from './SmartNavLink';
14
+ import { useLinkComponent } from './LinkComponentContext';
15
15
 
16
16
  export interface NavAction {
17
17
  label: string;
@@ -55,6 +55,7 @@ interface NavActionItemProps {
55
55
  }
56
56
 
57
57
  export function NavActionItem({ action, className }: NavActionItemProps) {
58
+ const Link = useLinkComponent();
58
59
  const variant = action.variant ?? 'ghost';
59
60
  const cls = cn(
60
61
  baseCls,
@@ -88,8 +89,8 @@ export function NavActionItem({ action, className }: NavActionItemProps) {
88
89
  );
89
90
  }
90
91
  return (
91
- <SmartNavLink href={action.href} className={cls} onClick={action.onClick}>
92
+ <Link href={action.href} className={cls} onClick={action.onClick}>
92
93
  {content}
93
- </SmartNavLink>
94
+ </Link>
94
95
  );
95
96
  }
@@ -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"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { type ReactNode } from 'react';
4
4
 
5
- import { SmartNavLink } from './SmartNavLink';
5
+ import { useLinkComponent } from './LinkComponentContext';
6
6
 
7
7
  interface NavBrandProps {
8
8
  brand?: ReactNode;
@@ -10,16 +10,18 @@ interface NavBrandProps {
10
10
  }
11
11
 
12
12
  export function NavBrand({ brand, brandHref = '/' }: NavBrandProps) {
13
+ const Link = useLinkComponent();
14
+
13
15
  if (brand == null || brand === '' || brand === false) return null;
14
16
 
15
17
  if (typeof brand === 'string') {
16
18
  return (
17
- <SmartNavLink
19
+ <Link
18
20
  href={brandHref}
19
21
  className="font-bold text-[15px] text-foreground hover:opacity-90 transition-opacity"
20
22
  >
21
23
  {brand}
22
- </SmartNavLink>
24
+ </Link>
23
25
  );
24
26
  }
25
27
 
@@ -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
+ }
@@ -10,7 +10,7 @@ import type { NavigationItem } from '../../types';
10
10
  import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
11
11
  import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
12
12
  import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
13
- import { SmartNavLink } from './SmartNavLink';
13
+ import { useLinkComponent } from './LinkComponentContext';
14
14
 
15
15
  interface NavDesktopItemsProps {
16
16
  /** Full ordered item list; overflow is computed responsively. */
@@ -57,6 +57,7 @@ export function NavDesktopItems({
57
57
  dropdown,
58
58
  renderDesktopDropdown,
59
59
  }: NavDesktopItemsProps) {
60
+ const Link = useLinkComponent();
60
61
  const { openDropdownKey, scheduleOpen, scheduleClose, closeDropdown } = dropdown;
61
62
 
62
63
  const { visibleCount, containerRef, itemRef, measured } = useResponsiveOverflow({
@@ -91,9 +92,9 @@ export function NavDesktopItems({
91
92
  <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
92
93
  </a>
93
94
  ) : (
94
- <SmartNavLink href={sub.href} className={subMenuLinkCls(subActive)}>
95
+ <Link href={sub.href} className={subMenuLinkCls(subActive)}>
95
96
  <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
96
- </SmartNavLink>
97
+ </Link>
97
98
  )}
98
99
  </div>
99
100
  );
@@ -149,13 +150,13 @@ export function NavDesktopItems({
149
150
 
150
151
  const active = isActivePath(item.href);
151
152
  return (
152
- <SmartNavLink
153
+ <Link
153
154
  key={item.href}
154
155
  href={item.href}
155
156
  className={cn(navItemCls, active && navItemActiveCls)}
156
157
  >
157
158
  <span className={labelCls} title={item.label}>{item.label}</span>
158
- </SmartNavLink>
159
+ </Link>
159
160
  );
160
161
  };
161
162
 
@@ -235,9 +236,9 @@ export function NavDesktopItems({
235
236
  const active = isGroupActive(item);
236
237
  return (
237
238
  <div key={`overflow-${item.href}`} className="rounded-full">
238
- <SmartNavLink href={item.href} className={subMenuLinkCls(active)}>
239
+ <Link href={item.href} className={subMenuLinkCls(active)}>
239
240
  <span className="min-w-0 truncate" title={item.label}>{item.label}</span>
240
- </SmartNavLink>
241
+ </Link>
241
242
  </div>
242
243
  );
243
244
  })}
@@ -2,17 +2,17 @@ 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';
8
10
  export {
9
- ExternalPrefixesProvider,
10
- useExternalPrefixes,
11
- isExternalPrefixHref,
12
- } from './ExternalPrefixesContext';
11
+ LinkComponentProvider,
12
+ useLinkComponent,
13
+ } from './LinkComponentContext';
13
14
  export type {
14
- ExternalPrefixes,
15
- ExternalPrefixesProviderProps,
16
- } from './ExternalPrefixesContext';
17
- export { SmartNavLink } from './SmartNavLink';
18
- export type { SmartNavLinkProps } from './SmartNavLink';
15
+ LinkComponent,
16
+ LinkComponentProps,
17
+ LinkComponentProviderProps,
18
+ } from './LinkComponentContext';
@@ -19,9 +19,10 @@ import { usePathnameWithoutLocale } from '../../../hooks';
19
19
  import { UserMenu } from '../../_components/UserMenu';
20
20
  import { usePublicLayoutOptional } from '../context';
21
21
  import { useMobileNavPanel } from '../hooks';
22
- import { SmartNavLink } from '../primitives/SmartNavLink';
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,9 +33,17 @@ 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) {
46
+ const Link = useLinkComponent();
38
47
  const context = usePublicLayoutOptional();
39
48
  const mobileMenuOpen = props.isOpen ?? context?.mobileMenuOpen ?? false;
40
49
  const closeMobileMenu = props.onClose ?? context?.closeMobileMenu ?? (() => {});
@@ -70,6 +79,10 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
70
79
 
71
80
  const hasSessionUser = Boolean(isAuthenticated && user);
72
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;
73
86
 
74
87
  return (
75
88
  <>
@@ -133,7 +146,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
133
146
  const parentOnlySectionOpen = hasChildNav && anyChildActive;
134
147
  return (
135
148
  <div key={item.href}>
136
- <SmartNavLink
149
+ <Link
137
150
  href={item.href}
138
151
  onClick={closeMobileMenu}
139
152
  title={item.label}
@@ -148,13 +161,13 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
148
161
  )}
149
162
  >
150
163
  {item.label}
151
- </SmartNavLink>
164
+ </Link>
152
165
  {hasChildNav && (
153
166
  <div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
154
167
  {childItems.map((subItem) => {
155
168
  const subActive = isActivePath(subItem.href);
156
169
  return (
157
- <SmartNavLink
170
+ <Link
158
171
  key={`${item.href}-${subItem.href}`}
159
172
  href={subItem.href}
160
173
  onClick={closeMobileMenu}
@@ -168,7 +181,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
168
181
  )}
169
182
  >
170
183
  {subItem.label}
171
- </SmartNavLink>
184
+ </Link>
172
185
  );
173
186
  })}
174
187
  </div>
@@ -180,9 +193,20 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
180
193
  </div>
181
194
  </div>
182
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
+
183
207
  {showSignInFooter && (
184
208
  <div className="shrink-0 border-t border-border/50 p-4">
185
- <SmartNavLink
209
+ <Link
186
210
  href={userMenu?.authPath || '/auth'}
187
211
  onClick={closeMobileMenu}
188
212
  className="block"
@@ -194,7 +218,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
194
218
  aria-hidden
195
219
  />
196
220
  </Button>
197
- </SmartNavLink>
221
+ </Link>
198
222
  </div>
199
223
  )}
200
224
  </div>
@@ -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
+
@@ -1,69 +0,0 @@
1
- /**
2
- * ExternalPrefixesContext
3
- *
4
- * Provides a list of path prefixes that should be treated as "outside" the
5
- * Next.js App Router (e.g. a catch-all served by Nextra). Consumers inside the
6
- * navbar (`SmartNavLink`) check whether the `href` starts with any configured
7
- * prefix; if so, they render a native `<a>` (full page load) instead of a
8
- * `next/link` that would fetch an RSC payload the route cannot produce.
9
- *
10
- * Default: `[]` — so behaviour is unchanged for consumers who do not opt in.
11
- */
12
-
13
- 'use client';
14
-
15
- import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
16
-
17
- export type ExternalPrefixes = readonly string[];
18
-
19
- const ExternalPrefixesContext = createContext<ExternalPrefixes>([]);
20
-
21
- export interface ExternalPrefixesProviderProps {
22
- /** List of path prefixes treated as external to the App Router. */
23
- value?: ExternalPrefixes;
24
- children: ReactNode;
25
- }
26
-
27
- export function ExternalPrefixesProvider({
28
- value,
29
- children,
30
- }: ExternalPrefixesProviderProps) {
31
- const resolved = useMemo<ExternalPrefixes>(() => value ?? [], [value]);
32
- return (
33
- <ExternalPrefixesContext.Provider value={resolved}>
34
- {children}
35
- </ExternalPrefixesContext.Provider>
36
- );
37
- }
38
-
39
- export function useExternalPrefixes(): ExternalPrefixes {
40
- return useContext(ExternalPrefixesContext);
41
- }
42
-
43
- /**
44
- * Returns true if `href` is a string that starts with any of the provided
45
- * external prefixes. Non-string hrefs (objects, `next/link` URL shape) always
46
- * return false — those are App Router internal.
47
- *
48
- * Matches the prefix exactly or at a path-boundary (`/`, `?`, `#`) so that
49
- * `/docs` in the prefix list matches `/docs`, `/docs/`, `/docs/foo`, `/docs?x=1`,
50
- * `/docs#bar` — but NOT `/docsearch` (which should stay an App Router link).
51
- */
52
- export function isExternalPrefixHref(
53
- href: unknown,
54
- prefixes: ExternalPrefixes,
55
- ): href is string {
56
- if (typeof href !== 'string' || prefixes.length === 0) return false;
57
- for (const prefix of prefixes) {
58
- if (!prefix) continue;
59
- if (href === prefix) return true;
60
- if (
61
- href.startsWith(`${prefix}/`) ||
62
- href.startsWith(`${prefix}?`) ||
63
- href.startsWith(`${prefix}#`)
64
- ) {
65
- return true;
66
- }
67
- }
68
- return false;
69
- }