@djangocfg/layouts 2.1.307 → 2.1.309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,7 +87,7 @@ Wraps `BaseApp` and picks **admin → private → public** layout by path (`matc
87
87
 
88
88
  | Component | Use |
89
89
  |---|---|
90
- | **`PublicLayout`** | Marketing / docs. Slots for navbar + footer. 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` + `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. |
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. |
@@ -139,11 +139,16 @@ Wraps `BaseApp` and picks **admin → private → public** layout by path (`matc
139
139
 
140
140
  ```tsx
141
141
  import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
142
+ import { routing } from '@djangocfg/nextjs/i18n/routing';
142
143
 
143
144
  const { locale, locales, changeLocale } = useLocaleSwitcher();
144
- <AppLayout i18n={{ locale, locales, onLocaleChange: changeLocale }}>{children}</AppLayout>
145
+ <AppLayout i18n={{ locale, locales, onLocaleChange: changeLocale, routing }}>
146
+ {children}
147
+ </AppLayout>
145
148
  ```
146
149
 
150
+ `i18n.routing` is the object returned by `defineRouting()` (`next-intl/routing`). When passed, `BaseApp` builds a locale-aware `Link` adapter from it and mounts a `LinkProvider`, so every `<Link>` rendered inside layouts (navbar, footer, drawer, …) keeps the active locale prefix on click. Drop it for default-locale-only apps — raw `next/link` works fine.
151
+
147
152
  ---
148
153
 
149
154
  ## Monitor & debug
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.307",
3
+ "version": "2.1.309",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,19 +74,20 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.307",
78
- "@djangocfg/centrifugo": "^2.1.307",
79
- "@djangocfg/debuger": "^2.1.307",
80
- "@djangocfg/i18n": "^2.1.307",
81
- "@djangocfg/monitor": "^2.1.307",
82
- "@djangocfg/ui-core": "^2.1.307",
83
- "@djangocfg/ui-nextjs": "^2.1.307",
84
- "@djangocfg/ui-tools": "^2.1.307",
77
+ "@djangocfg/api": "^2.1.309",
78
+ "@djangocfg/centrifugo": "^2.1.309",
79
+ "@djangocfg/debuger": "^2.1.309",
80
+ "@djangocfg/i18n": "^2.1.309",
81
+ "@djangocfg/monitor": "^2.1.309",
82
+ "@djangocfg/ui-core": "^2.1.309",
83
+ "@djangocfg/ui-nextjs": "^2.1.309",
84
+ "@djangocfg/ui-tools": "^2.1.309",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
88
88
  "moment": "^2.30.1",
89
89
  "next": "^16.2.2",
90
+ "next-intl": "^4.9.1",
90
91
  "p-retry": "^7.0.0",
91
92
  "react": "^19.1.0",
92
93
  "react-dom": "^19.1.0",
@@ -110,19 +111,20 @@
110
111
  "uuid": "^11.1.0"
111
112
  },
112
113
  "devDependencies": {
113
- "@djangocfg/api": "^2.1.307",
114
- "@djangocfg/centrifugo": "^2.1.307",
115
- "@djangocfg/debuger": "^2.1.307",
116
- "@djangocfg/i18n": "^2.1.307",
117
- "@djangocfg/monitor": "^2.1.307",
118
- "@djangocfg/typescript-config": "^2.1.307",
119
- "@djangocfg/ui-core": "^2.1.307",
120
- "@djangocfg/ui-nextjs": "^2.1.307",
121
- "@djangocfg/ui-tools": "^2.1.307",
114
+ "@djangocfg/api": "^2.1.309",
115
+ "@djangocfg/centrifugo": "^2.1.309",
116
+ "@djangocfg/debuger": "^2.1.309",
117
+ "@djangocfg/i18n": "^2.1.309",
118
+ "@djangocfg/monitor": "^2.1.309",
119
+ "@djangocfg/typescript-config": "^2.1.309",
120
+ "@djangocfg/ui-core": "^2.1.309",
121
+ "@djangocfg/ui-nextjs": "^2.1.309",
122
+ "@djangocfg/ui-tools": "^2.1.309",
122
123
  "@types/node": "^24.7.2",
123
124
  "@types/react": "^19.1.0",
124
125
  "@types/react-dom": "^19.1.0",
125
126
  "eslint": "^9.37.0",
127
+ "next-intl": "^4.9.1",
126
128
  "typescript": "^5.9.3"
127
129
  },
128
130
  "publishConfig": {
@@ -49,6 +49,7 @@ import { CentrifugoProvider } from '@djangocfg/centrifugo';
49
49
  import { Toaster, TooltipProvider } from '@djangocfg/ui-core/components';
50
50
  import { DialogProvider } from '@djangocfg/ui-core/lib/dialog-service';
51
51
  import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters/nextjs';
52
+ import { NextIntlLinkBridge } from './NextIntlLinkBridge';
52
53
  import { ThemeProvider } from '@djangocfg/ui-nextjs/theme';
53
54
  import { ThemeStyleBridge } from '../../theme/ThemeStyleBridge';
54
55
  import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
@@ -158,7 +159,13 @@ export function BaseApp({
158
159
  onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
159
160
  >
160
161
  <MonitorProvider {...monitorConfig} />
161
- <LayoutI18nProvider value={i18n}>{children}</LayoutI18nProvider>
162
+ <LayoutI18nProvider value={i18n}>
163
+ {i18n?.routing ? (
164
+ <NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
165
+ ) : (
166
+ children
167
+ )}
168
+ </LayoutI18nProvider>
162
169
  <NextTopLoader
163
170
  color="hsl(var(--primary))"
164
171
  height={3}
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * NextIntlLinkBridge
5
+ *
6
+ * Bridges next-intl's locale-aware `Link` (built from a `routing` object via
7
+ * `createNavigation()`) into our framework-agnostic `LinkProvider`. Mounted
8
+ * by `BaseApp` only when the consumer passes `i18n.routing`.
9
+ *
10
+ * Why this lives in @djangocfg/layouts (not @djangocfg/ui-core):
11
+ * ui-core stays free of next-intl. Layouts already depend on next + nextjs
12
+ * plumbing, so taking a peer on next-intl here is honest about scope and
13
+ * keeps consumer apps from having to wire `createNavigation` themselves.
14
+ */
15
+
16
+ import { forwardRef, useMemo, type ReactNode } from 'react';
17
+ import { createNavigation } from 'next-intl/navigation';
18
+
19
+ import {
20
+ LinkProvider,
21
+ type LinkComponent,
22
+ type LinkComponentProps,
23
+ } from '@djangocfg/ui-core/components';
24
+
25
+ import type { NextIntlRouting } from '../types/layout.types';
26
+
27
+ export interface NextIntlLinkBridgeProps {
28
+ /** The `routing` object returned by `defineRouting()` from `next-intl/routing`. */
29
+ routing: NextIntlRouting;
30
+ children: ReactNode;
31
+ }
32
+
33
+ export function NextIntlLinkBridge({ routing, children }: NextIntlLinkBridgeProps) {
34
+ const adapter = useMemo<LinkComponent>(() => {
35
+ // The `createNavigation` overload signature splits between localized and
36
+ // shared routing variants and TS can't narrow either way through our
37
+ // opaque `NextIntlRouting` alias — cast to satisfy the localized branch
38
+ // and rely on next-intl to handle both at runtime.
39
+ const { Link: IntlLink } = createNavigation(
40
+ routing as unknown as Parameters<typeof createNavigation>[0],
41
+ );
42
+ return forwardRef<HTMLAnchorElement, LinkComponentProps>(
43
+ function NextIntlLinkAdapter(
44
+ { href, replace, scroll, prefetch, children: kids, ...rest },
45
+ ref,
46
+ ) {
47
+ // next-intl's Link accepts the same props we expose. The cast is safe
48
+ // because LinkComponentProps is a structural subset.
49
+ const Link = IntlLink as unknown as LinkComponent;
50
+ return (
51
+ <Link
52
+ href={href}
53
+ replace={replace}
54
+ scroll={scroll}
55
+ prefetch={prefetch}
56
+ ref={ref}
57
+ {...rest}
58
+ >
59
+ {kids}
60
+ </Link>
61
+ );
62
+ },
63
+ );
64
+ }, [routing]);
65
+
66
+ return <LinkProvider value={adapter}>{children}</LinkProvider>;
67
+ }
@@ -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 }, i18n }} />}
10
+ footer={<DefaultFooter config={{ variant: 'full', menus: { sections } }} />}
11
11
  >
12
12
  {children}
13
13
  </PublicLayout>
@@ -29,20 +29,11 @@ All internal anchors (navbar, footer, mobile drawer) use `<Link>` from
29
29
  `@djangocfg/ui-core/components`. In Next.js apps it renders through `next/link`
30
30
  automatically (via `NextLinkProvider`, which `BaseApp` mounts for you).
31
31
 
32
- For a locale-aware Link (e.g. `next-intl`'s `createNavigation().Link`), swap
33
- the underlying component by mounting your own `LinkProvider` from
34
- `@djangocfg/ui-core/components` higher in the tree:
35
-
36
- ```tsx
37
- import { LinkProvider } from '@djangocfg/ui-core/components';
38
- import { Link as I18nLink } from '@/i18n/navigation';
39
-
40
- <LinkProvider value={I18nLink}>
41
- <PublicLayout navbar={<FloatingNavbar config={…} />} footer={…}>
42
- {children}
43
- </PublicLayout>
44
- </LinkProvider>
45
- ```
32
+ For multi-locale apps (`next-intl` with `localePrefix: 'always' | 'as-needed'`)
33
+ pass `i18n.routing` to `AppLayout` / `BaseApp`. The bridge inside `BaseApp`
34
+ calls `createNavigation(routing)` and swaps the default `next/link` adapter
35
+ for the locale-aware one — every navbar / footer / drawer link keeps the
36
+ active locale prefix on click. See `@djangocfg/layouts` README → **i18n**.
46
37
 
47
38
  ## Navbar variants
48
39
 
@@ -74,8 +65,7 @@ Three variants. All share the same core props (below); only the chrome differs.
74
65
  | `transparentThreshold` | `number` | `40` | Px past which the nav becomes opaque. |
75
66
  | `desktopMaxPrimaryItems` | `number` | auto | Hard cap for primary items before overflow. |
76
67
  | `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`). |
68
+ | `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
69
 
80
70
  ### Variant-only
81
71
 
@@ -94,10 +84,9 @@ Three variants. All share the same core props (below); only the chrome differs.
94
84
 
95
85
  ## Theme + locale controls (navbar)
96
86
 
97
- All three navbars accept an optional `controls` block and `i18n` config that
98
- mirrors `DefaultFooter`. When enabled, a compact `NavControls` pill is rendered
99
- on desktop right before `UserMenu`, and an equivalent row appears in the mobile
100
- drawer footer.
87
+ All three navbars accept an optional `controls` block. When enabled, a compact
88
+ `NavControls` pill is rendered on desktop right before `UserMenu`, and an
89
+ equivalent row appears in the mobile drawer footer.
101
90
 
102
91
  ```tsx
103
92
  <FloatingNavbar
@@ -106,7 +95,6 @@ drawer footer.
106
95
  navigation,
107
96
  userMenu: { authPath: '/auth' },
108
97
  controls: { showThemeSwitcher: true, showLocaleSwitcher: true },
109
- i18n: { locale, locales, onLocaleChange: changeLocale },
110
98
  }}
111
99
  />
112
100
  ```
@@ -114,7 +102,12 @@ drawer footer.
114
102
  | Control | Required | Notes |
115
103
  |---|---|---|
116
104
  | `controls.showThemeSwitcher` | — | Light / system / dark pill (uses `useThemeContext`). |
117
- | `controls.showLocaleSwitcher` | `i18n` | Globe dropdown. Silently hidden when `i18n` is omitted. |
105
+ | `controls.showLocaleSwitcher` | `LayoutI18nProvider` | Locale switcher (icon-size pill matching `UserMenu` avatar). Hidden when no provider is mounted. |
106
+
107
+ Locale data (`locale`, `locales`, `onLocaleChange`) is read from
108
+ `LayoutI18nProvider`, which `BaseApp` (and `AppLayout`) mounts automatically
109
+ when you pass an `i18n` config. The switcher renders SVG country flags via
110
+ `country-flag-icons` — no emoji.
118
111
 
119
112
  Same shape works for `FlushNavbar` and `MinimalNavbar`. If you build a custom
120
113
  navbar with `NavbarShell`, the controls node is pre-built and delivered to
@@ -129,7 +122,6 @@ import { NavControls } from '@djangocfg/layouts';
129
122
  <NavControls
130
123
  showThemeSwitcher
131
124
  showLocaleSwitcher
132
- i18n={{ locale, locales, onLocaleChange }}
133
125
  size="compact" // 'compact' (navbar) | 'default' (footer)
134
126
  />
135
127
  ```
@@ -160,7 +152,7 @@ actions={[
160
152
  Three variants: `full` (default) with brand column + menus + controls; `compact` (one row + bottom); `simple` (copyright only).
161
153
 
162
154
  ```tsx
163
- <DefaultFooter config={{ variant: 'full', menus: { sections }, i18n, slots }} />
155
+ <DefaultFooter config={{ variant: 'full', menus: { sections }, slots }} />
164
156
  ```
165
157
 
166
158
  | Prop (`config.*`) | Type | Default | Role |
@@ -178,9 +170,8 @@ Three variants: `full` (default) with brand column + menus + controls; `compact`
178
170
  | `social` | `FooterSocialLinks` | — | Social icon row under brand. |
179
171
  | `meta.copyright` | `string` | `© ${year}. All rights reserved.` | |
180
172
  | `meta.credits` | `{ text: string; url?: string }` | — | |
181
- | `i18n` | `{ locale, locales, onLocaleChange }` | — | Required for the locale switcher. |
182
173
  | `controls.showThemeSwitcher` | `boolean` | `true` | Full variant only. |
183
- | `controls.showLocaleSwitcher` | `boolean` | `true` (if `i18n`) | Full variant only. |
174
+ | `controls.showLocaleSwitcher` | `boolean` | `true` | Full variant only. Reads locale from `LayoutI18nProvider`; hidden when no provider is mounted. |
184
175
  | `slots.aboveMenus` | `ReactNode` | — | Above the brand/menus grid (full variant). |
185
176
  | `slots.belowMenus` | `ReactNode` | — | Between menus and bottom row. |
186
177
  | `slots.bottomStart` | `ReactNode` | — | Next to copyright. |
@@ -44,22 +44,38 @@ export function LocaleSwitcherDropdown({
44
44
  }: LocaleSwitcherDropdownProps) {
45
45
  const currentMeta = getLocaleMeta(locale, labels);
46
46
  const currentLabel = showCode ? locale.toUpperCase() : currentMeta.native;
47
+ const isIconOnly = size === 'icon';
47
48
 
48
49
  return (
49
50
  <DropdownMenu>
50
51
  <DropdownMenuTrigger asChild>
51
- <Button variant={variant} size={size} className={className}>
52
- {showFlag ? (
53
- <LanguageFlag
54
- code={locale}
55
- rounded
56
- className={cn('h-3 w-4 shrink-0', showTriggerLabel && 'mr-1.5')}
57
- />
58
- ) : showIcon ? (
59
- <Globe className={cn('h-4 w-4 shrink-0', showTriggerLabel && 'mr-1')} />
60
- ) : null}
61
- {showTriggerLabel && <span>{currentLabel}</span>}
62
- </Button>
52
+ {isIconOnly ? (
53
+ <Button
54
+ variant={variant}
55
+ size={size}
56
+ aria-label={currentLabel}
57
+ className={cn('overflow-hidden rounded-full p-0', className)}
58
+ >
59
+ {showFlag ? (
60
+ <LanguageFlag code={locale} className="h-full w-full object-cover" />
61
+ ) : (
62
+ <Globe className="h-4 w-4" aria-hidden />
63
+ )}
64
+ </Button>
65
+ ) : (
66
+ <Button variant={variant} size={size} className={className}>
67
+ {showFlag ? (
68
+ <LanguageFlag
69
+ code={locale}
70
+ rounded
71
+ className={cn('h-3 w-4 shrink-0', showTriggerLabel && 'mr-1.5')}
72
+ />
73
+ ) : showIcon ? (
74
+ <Globe className={cn('h-4 w-4 shrink-0', showTriggerLabel && 'mr-1')} />
75
+ ) : null}
76
+ {showTriggerLabel && <span>{currentLabel}</span>}
77
+ </Button>
78
+ )}
63
79
  </DropdownMenuTrigger>
64
80
  <DropdownMenuContent align="end">
65
81
  {locales.map((code) => {
@@ -48,6 +48,35 @@ export const LocaleSwitcherTrigger = React.forwardRef<
48
48
  ) {
49
49
  const meta = getLocaleMeta(locale, labels);
50
50
  const labelText = showCode ? locale.toUpperCase() : meta.native;
51
+ const isIconOnly = size === 'icon';
52
+
53
+ // Icon-only trigger: full-bleed flag inside a circular pill that lines up
54
+ // with neighbouring avatars / icon buttons. No padding, no inner gap.
55
+ if (isIconOnly) {
56
+ return (
57
+ <Button
58
+ ref={ref}
59
+ type="button"
60
+ variant={variant}
61
+ size={size}
62
+ onClick={onClick}
63
+ aria-label={ariaLabel ?? labelText}
64
+ className={cn(
65
+ 'overflow-hidden rounded-full p-0',
66
+ className,
67
+ )}
68
+ >
69
+ {showFlag ? (
70
+ <LanguageFlag
71
+ code={locale}
72
+ className="h-full w-full object-cover"
73
+ />
74
+ ) : (
75
+ <Globe className="h-4 w-4" aria-hidden />
76
+ )}
77
+ </Button>
78
+ );
79
+ }
51
80
 
52
81
  return (
53
82
  <Button
@@ -13,6 +13,15 @@ import type { AnalyticsConfig } from '../../snippets/Analytics/types';
13
13
  import type { PwaInstallConfig } from '@djangocfg/ui-nextjs/pwa';
14
14
  import type { ErrorBoundaryConfig, ErrorTrackingConfig } from '../../components/errors/types';
15
15
 
16
+ /**
17
+ * Opaque alias for the `routing` object returned by `defineRouting()` from
18
+ * `next-intl/routing`. Typed as `unknown` so consumer apps can pass shared
19
+ * (no `pathnames`) or localized routing without fighting next-intl's
20
+ * conditional generics. The bridge inside `BaseApp` casts it back when it
21
+ * calls `createNavigation(routing)`.
22
+ */
23
+ export type NextIntlRouting = object & { __nextIntlRouting?: never };
24
+
16
25
  // Import local provider configs
17
26
  import type { ThemeConfig, SWRConfigOptions, CentrifugoConfig } from './providers.types';
18
27
  import type { MonitorConfig } from '@djangocfg/monitor';
@@ -117,6 +126,14 @@ export interface I18nLayoutConfig {
117
126
  brand?: I18nBrandConfig;
118
127
  /** Custom UI strings for the locale-switcher dialog. */
119
128
  dialogLabels?: I18nDialogLabels;
129
+ /**
130
+ * Optional `routing` object from `next-intl`'s `defineRouting()`. When
131
+ * passed, `BaseApp` builds a locale-aware Link adapter from it and mounts
132
+ * a `LinkProvider` so every `<Link>` rendered inside layouts keeps the
133
+ * active locale prefix on click. Without it, non-default-locale users get
134
+ * sent back to the default locale on every navbar / footer click.
135
+ */
136
+ routing?: NextIntlRouting;
120
137
  }
121
138
 
122
139
  // ============================================================================