@djangocfg/layouts 2.1.257 → 2.1.259

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 (29) hide show
  1. package/README.md +101 -203
  2. package/package.json +18 -18
  3. package/src/index.ts +4 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +97 -8
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -0
  6. package/src/layouts/AppLayout/index.ts +6 -0
  7. package/src/layouts/PrivateLayout/PrivateLayout.tsx +3 -1
  8. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +4 -1
  9. package/src/layouts/PublicLayout/PublicLayout.tsx +31 -8
  10. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
  11. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
  12. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
  13. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
  14. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +69 -30
  15. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +24 -34
  16. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +162 -94
  17. package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
  18. package/src/layouts/PublicLayout/components/index.ts +2 -0
  19. package/src/layouts/PublicLayout/index.ts +5 -0
  20. package/src/layouts/PublicLayout/navbarTypes.ts +8 -0
  21. package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
  22. package/src/layouts/_components/UserMenu.tsx +2 -2
  23. package/src/layouts/types/index.ts +9 -1
  24. package/src/layouts/types/providers.types.ts +10 -0
  25. package/src/theme/ThemeStyleBridge.tsx +41 -0
  26. package/src/theme/buildThemeStyleSheet.ts +71 -0
  27. package/src/theme/index.ts +16 -0
  28. package/src/theme/themeStyle.types.ts +89 -0
  29. package/src/theme/themeStylePresets.ts +202 -0
package/README.md CHANGED
@@ -6,19 +6,16 @@
6
6
 
7
7
  # @djangocfg/layouts
8
8
 
9
- Layout components and providers for Next.js apps.
9
+ Layouts, app shell, and providers for **Next.js App Router**. Part of **[DjangoCFG](https://djangocfg.com)**.
10
10
 
11
- **Part of [DjangoCFG](https://djangocfg.com)** — modern Django framework for production-ready SaaS applications.
12
-
13
- ## Install
11
+ ## Install & CSS
14
12
 
15
13
  ```bash
16
14
  pnpm add @djangocfg/layouts
17
15
  ```
18
16
 
19
- Add to `globals.css` (before `@import "tailwindcss"`):
20
-
21
17
  ```css
18
+ /* Before @import "tailwindcss" */
22
19
  @import "@djangocfg/ui-nextjs/styles";
23
20
  @import "@djangocfg/layouts/styles";
24
21
  @import "@djangocfg/ui-tools/styles";
@@ -26,254 +23,155 @@ Add to `globals.css` (before `@import "tailwindcss"`):
26
23
  @import "tailwindcss";
27
24
  ```
28
25
 
29
- ## BaseApp
30
-
31
- Core providers wrapper. Use directly when you don't need route-based layout switching.
26
+ Peers: `@djangocfg/ui-core`, `@djangocfg/ui-nextjs`, React 19, Next.js 16+, Tailwind CSS 4.
32
27
 
33
- ```tsx
34
- import { BaseApp } from '@djangocfg/layouts';
35
-
36
- export default function RootLayout({ children }) {
37
- return (
38
- <html lang="en" suppressHydrationWarning>
39
- <body>
40
- <BaseApp
41
- project="my-app"
42
- theme={{ defaultTheme: 'dark', storageKey: 'my-theme' }}
43
- auth={{ apiUrl: process.env.NEXT_PUBLIC_API_URL }}
44
- analytics={{ googleTrackingId: 'G-XXXXXXXXXX' }}
45
- >
46
- {children}
47
- </BaseApp>
48
- </body>
49
- </html>
50
- );
51
- }
52
- ```
28
+ ---
53
29
 
54
- **Props:**
55
-
56
- | Prop | Type | Default | Description |
57
- |------|------|---------|-------------|
58
- | `project` | `string` | — | App name — auto-passed to `MonitorProvider` and debug panel |
59
- | `theme` | `ThemeConfig` | — | Theme config (defaultTheme, storageKey) |
60
- | `auth` | `AuthConfig` | — | Auth provider config |
61
- | `analytics` | `AnalyticsConfig` | — | Google Analytics config |
62
- | `centrifugo` | `CentrifugoConfig` | — | WebSocket real-time config |
63
- | `errorTracking` | `ErrorTrackingConfig` | — | Validation/CORS/network error capture |
64
- | `errorBoundary` | `ErrorBoundaryConfig` | enabled | React error boundary |
65
- | `swr` | `SWRConfigOptions` | — | SWR data fetching config |
66
- | `pwaInstall` | `PwaInstallConfig` | — | PWA install prompt (from `@djangocfg/ui-nextjs/pwa`) |
67
- | `monitor` | `MonitorConfig` | — | Override monitor config (project/environment come from `project` prop by default) |
68
- | `debug` | `DebugConfig` | enabled | Debug panel config — see below |
69
-
70
- **Included automatically:**
71
- - ThemeProvider, TooltipProvider, SWRConfig, DialogProvider
72
- - AuthProvider + AuthDialog (via zustand `dialogStore`, triggered by `window.dialog.auth()`)
73
- - AnalyticsProvider, CentrifugoProvider
74
- - PwaProvider (from `@djangocfg/ui-nextjs/pwa`)
75
- - ErrorTrackingProvider, ErrorBoundary
76
- - MonitorProvider (auto-enabled via `project` prop)
77
- - DebugButton from `@djangocfg/debuger`
78
- - NextTopLoader, Toaster
79
-
80
- ## AppLayout
81
-
82
- Smart layout router — selects layout based on current route. Wraps `BaseApp`, accepts all its props.
83
-
84
- ```tsx
85
- import { AppLayout } from '@djangocfg/layouts';
86
- import { PublicLayout } from './_layouts/PublicLayout';
87
- import { PrivateLayout } from './_layouts/PrivateLayout';
88
- import { AdminLayout } from './_layouts/AdminLayout';
89
-
90
- export default function RootLayout({ children }) {
91
- return (
92
- <html lang="en" suppressHydrationWarning>
93
- <body>
94
- <AppLayout
95
- layouts={{
96
- public: { component: PublicLayout, enabledPath: ['/', '/legal', '/contact'] },
97
- private: { component: PrivateLayout, enabledPath: ['/dashboard', '/profile'] },
98
- admin: { component: AdminLayout, enabledPath: '/admin' },
99
- noLayoutPaths: ['/private/terminal', '/embed'],
100
- }}
101
- baseApp={{
102
- project: 'my-app',
103
- theme: { defaultTheme: 'system' },
104
- auth: { apiUrl: process.env.NEXT_PUBLIC_API_URL },
105
- }}
106
- >
107
- {children}
108
- </AppLayout>
109
- </body>
110
- </html>
111
- );
112
- }
113
- ```
114
-
115
- **Layout priority:** Admin → Private → Public → Fallback
116
-
117
- **`noLayoutPaths`** — render without layout wrapper (providers still active). Useful for fullscreen pages.
30
+ ## BaseApp
118
31
 
119
- ### i18n
32
+ Root provider (theme, auth, analytics, SWR, toasts, …). Use when you **do not** need route-based layout switching.
120
33
 
121
34
  ```tsx
122
- import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
123
-
124
- const { locale, locales, changeLocale } = useLocaleSwitcher();
35
+ import { BaseApp } from '@djangocfg/layouts';
125
36
 
126
- <AppLayout
127
- i18n={{ locale, locales, onLocaleChange: changeLocale }}
37
+ <BaseApp
38
+ project="my-app"
39
+ theme={{ defaultTheme: 'system', storageKey: 'my-theme' }}
40
+ auth={{ apiUrl: process.env.NEXT_PUBLIC_API_URL }}
128
41
  >
129
42
  {children}
130
- </AppLayout>
43
+ </BaseApp>
131
44
  ```
132
45
 
133
- ## Built-in Layouts
46
+ | Prop | Role |
47
+ |------|------|
48
+ | `project` | Name for monitor / debug |
49
+ | `theme` | `defaultTheme`, `storageKey`, optional **`style`** (see below) |
50
+ | `auth`, `analytics`, `centrifugo`, `errorTracking`, `errorBoundary`, `swr`, `pwaInstall`, `monitor`, `debug` | Optional integrations |
134
51
 
135
- ```tsx
136
- import { PublicLayout, PrivateLayout, AuthLayout, AdminLayout, ProfileLayout } from '@djangocfg/layouts';
137
- ```
52
+ ### Theme: `theme.style` + `ThemeStyleBridge`
138
53
 
139
- | Layout | Description |
140
- |--------|-------------|
141
- | `PublicLayout` | Public pages with nav (home, docs, contact, legal) |
142
- | `PrivateLayout` | Authenticated pages with sidebar |
143
- | `AuthLayout` | OTP + OAuth + 2FA authentication flow |
144
- | `AdminLayout` | Admin panel |
145
- | `ProfileLayout` | User profile with 2FA management |
54
+ `BaseApp` passes theme to **next-themes** and mounts **`ThemeStyleBridge`**, which injects `<style id="djangocfg-baseapp-theme-style">` with **`--*`** variables (same naming as `@djangocfg/ui-core` `theme/light.css` & `dark.css`).
146
55
 
147
- ### AuthLayout
56
+ | Piece | Meaning |
57
+ |--------|---------|
58
+ | **`preset`** | One of **`THEME_STYLE_PRESET_ORDER`** — curated bundles in **`THEME_STYLE_PRESETS`** |
59
+ | **`vars.light` / `vars.dark`** | Partial **`ThemeCssVarMap`** — HSL triplets (e.g. `192 90% 35%`); **`radius`** accepts any CSS length (`0.75rem`, …) |
148
60
 
149
- ```tsx
150
- <AuthLayout
151
- sourceUrl="https://example.com"
152
- redirectUrl="/dashboard"
153
- logoUrl="/logo.svg"
154
- enableGithubAuth={true}
155
- termsUrl="/terms"
156
- privacyUrl="/privacy"
157
- />
158
- ```
159
-
160
- ### ProfileLayout
61
+ Merge order: **imported globals → preset → `vars`** (strongest).
161
62
 
162
- ```tsx
163
- <ProfileLayout
164
- enable2FA={true}
165
- showMemberSince={true}
166
- />
167
- ```
63
+ **Presets (summary)**
168
64
 
169
- ## Monitor & Debug
65
+ | ID | Role |
66
+ |----|------|
67
+ | `default` | No extra rules (globals only) |
68
+ | `django-cfg` | Brand cyan primary + sidebar/chart alignment |
69
+ | `ios` | System blue, hairline borders, **0.75rem** radius (Apple-like) |
70
+ | `soft` | **1rem** radius, softer borders / surfaces |
71
+ | `dense` | **0.25rem** radius, stronger borders (data / admin) |
72
+ | `high-contrast` | Stronger borders & text contrast |
170
73
 
171
- Both auto-enabled via `project` propno extra config needed.
172
-
173
- ```tsx
174
- // window.monitor available in DevTools:
175
- window.monitor.error('Something broke', { context: 'checkout' })
176
- window.monitor.warn('Slow response', { ms: 2500 })
177
- window.monitor.flush() // send buffer immediately
178
- window.monitor.status() // show current state
179
- ```
74
+ **Typing:** keys are grouped in source as **`ThemeCssVarColorKey`**, **`ThemeCssVarChromeKey`** (includes **`radius`**), **`ThemeCssVarSidebarKey`**, **`ThemeCssVarChartKey`** — together **`ThemeCssVarKey`**. Same semantics as the **Theme Configurator** playground’s `ThemeData.colors` / `radius` / `sidebar` / `charts`, mapped to kebab-case **`--`** names. Playground-only buckets (**shadows**, **typography**, **spacing**, …) are **not** injected here export **full CSS** from the configurator when you need them.
180
75
 
181
- Override monitor defaults:
76
+ **Exports:** `ThemeStyleConfig`, `ThemeCssVarKey`, `ThemeCssVarMap`, `ThemeStylePresetId`, `THEME_STYLE_PRESETS`, `THEME_STYLE_PRESET_ORDER`, `buildThemeStyleSheet`, `ThemeStyleBridge`.
182
77
 
183
78
  ```tsx
184
79
  <BaseApp
185
- project="my-app"
186
- monitor={{ baseUrl: 'https://api.example.com', captureConsole: false }}
80
+ theme={{
81
+ defaultTheme: 'system',
82
+ storageKey: 'my-app-theme',
83
+ style: {
84
+ preset: 'django-cfg',
85
+ vars: { light: { ring: '200 85% 40%' }, dark: { 'muted-foreground': '0 0% 70%' } },
86
+ },
87
+ }}
187
88
  >
89
+ {children}
90
+ </BaseApp>
188
91
  ```
189
92
 
190
- ### Debug panel
93
+ ---
191
94
 
192
- `Cmd+D` or `?debug=1` in URL to open.
193
-
194
- ```tsx
195
- // enabled by default (omit or pass empty object)
196
- <BaseApp project="my-app">
197
-
198
- // disable
199
- <BaseApp debug={{ enabled: false }}>
95
+ ## AppLayout
200
96
 
201
- // custom tabs (e.g. Zustand store viewer)
202
- import type { CustomDebugTab } from '@djangocfg/debuger';
97
+ Wraps **BaseApp** and picks **admin → private → public** layout by path (`matchesPath` / `enabledPath`). **`noLayoutPaths`** skips the shell (fullscreen / embeds). **`publicChrome`** merges navbar/footer/main spacing defaults (see types **`AppLayoutPublicChrome`**, **`mergeAppLayoutPublicChrome`**).
203
98
 
204
- const myTabs: CustomDebugTab[] = [
205
- { id: 'store', label: 'Stores', icon: Database, panel: StoreTab },
206
- ];
99
+ ```tsx
100
+ import { AppLayout } from '@djangocfg/layouts';
207
101
 
208
- <AppLayout debug={{ panel: { tabs: myTabs } }}>
102
+ <AppLayout
103
+ layouts={{
104
+ public: { component: PublicLayout, enabledPath: ['/', '/legal', '/contact'] },
105
+ private: { component: PrivateLayout, enabledPath: ['/dashboard'] },
106
+ admin: { component: AdminLayout, enabledPath: '/admin' },
107
+ noLayoutPaths: ['/embed'],
108
+ }}
109
+ baseApp={{ project: 'my-app', theme: { defaultTheme: 'system' }, auth: { apiUrl: '…' } }}
110
+ i18n={{ locale, locales, onLocaleChange }}
111
+ >
112
+ {children}
113
+ </AppLayout>
209
114
  ```
210
115
 
211
- ## Error Tracking
116
+ ---
212
117
 
213
- ```tsx
214
- import { useErrorEmitter, emitRuntimeError } from '@djangocfg/layouts';
118
+ ## Layouts (import from `@djangocfg/layouts`)
215
119
 
216
- // In components
217
- const { emitError } = useErrorEmitter('MyComponent');
218
- emitError('Something failed', error);
120
+ | Component | Use |
121
+ |-----------|-----|
122
+ | **PublicLayout** | Marketing / docs — slots: **`navbar`**, **`footer`**, **`contentTopSpacing`**, **`contentBottomSpacing`** |
123
+ | **PublicNavbar** / **PublicFooter** | **`PublicNavbarConfig`** / **`PublicFooterConfig`** — **`shell.rounding`**, **`navbarVariant`**, **`navbarPosition`**, etc. |
124
+ | **PrivateLayout** | App shell — sidebar + header |
125
+ | **AuthLayout** | Sign-in flows |
126
+ | **AdminLayout** | Admin console |
127
+ | **ProfileLayout** | Profile + 2FA |
219
128
 
220
- // Outside React
221
- emitRuntimeError('MyUtil', 'Operation failed', error);
222
- ```
129
+ **Brand:** `ThemeBrandMark` / **`ThemeBrandMarkImg`** for logo slots.
223
130
 
224
- ## Error Pages
131
+ ---
132
+
133
+ ## i18n on AppLayout
225
134
 
226
135
  ```tsx
227
- import { ErrorLayout } from '@djangocfg/layouts/components/errors';
136
+ import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
228
137
 
229
- // app/not-found.tsx
230
- export default function NotFound() {
231
- return <ErrorLayout code={404} supportEmail="support@example.com" />;
232
- }
138
+ const { locale, locales, changeLocale } = useLocaleSwitcher();
139
+ <AppLayout i18n={{ locale, locales, onLocaleChange: changeLocale }}>{children}</AppLayout>
233
140
  ```
234
141
 
142
+ ---
235
143
 
236
- ## Redirect
144
+ ## Monitor & debug
237
145
 
238
- ```tsx
239
- import { RedirectPage } from '@djangocfg/layouts/components/RedirectPage';
146
+ `project` on **BaseApp** enables **`window.monitor`**. **Debug:** `Cmd+D` or `?debug=1`.
240
147
 
241
- export default function Page() {
242
- return <RedirectPage authenticatedPath="/dashboard" unauthenticatedPath="/auth" />;
243
- }
244
- ```
148
+ ---
245
149
 
246
- ## Legal Pages
150
+ ## Utilities
247
151
 
248
- ```tsx
249
- import { PrivacyPage, TermsPage, CookiesPage, SecurityPage } from '@djangocfg/layouts/pages/legal';
152
+ | API | Role |
153
+ |-----|------|
154
+ | **useErrorEmitter**, **emitRuntimeError** | Error tracking hooks |
155
+ | **ErrorLayout** (`/components/errors`) | 404 / error pages |
156
+ | **RedirectPage** | Auth redirect helper |
157
+ | **PrivacyPage**, **TermsPage**, … (`/pages/legal`) | Legal templates |
250
158
 
251
- export default PrivacyPage;
252
- ```
159
+ ---
253
160
 
254
- ## Exports
161
+ ## Package exports
255
162
 
256
- | Path | Content |
257
- |------|---------|
258
- | `@djangocfg/layouts` | All exports |
163
+ | Path | Contents |
164
+ |------|----------|
165
+ | `@djangocfg/layouts` | Barrel |
259
166
  | `@djangocfg/layouts/layouts` | Layout components |
260
- | `@djangocfg/layouts/snippets` | Reusable components |
261
- | `@djangocfg/layouts/components` | Utility components |
167
+ | `@djangocfg/layouts/components` | Misc |
262
168
  | `@djangocfg/layouts/pages/legal` | Legal pages |
263
- | `@djangocfg/layouts/utils` | OG image utils |
264
- | `@djangocfg/layouts/styles` | CSS |
265
- | `@djangocfg/layouts/styles/dashboard` | Dashboard CSS |
169
+ | `@djangocfg/layouts/styles` | Base CSS |
266
170
 
267
- ## Extension Packages
171
+ ## Extension packages
268
172
 
269
- | Package | Description |
270
- |---------|-------------|
271
- | `@djangocfg/ext-newsletter` | Newsletter subscription |
272
- | `@djangocfg/ext-knowbase` | Knowledge base + AI chat |
273
- | `@djangocfg/ext-leads` | Lead capture forms |
274
- | `@djangocfg/ext-payments` | Payments & subscriptions |
275
- | `@djangocfg/ext-support` | Support tickets |
173
+ `@djangocfg/ext-newsletter`, `ext-knowbase`, `ext-leads`, `ext-payments`, `ext-support`, …
276
174
 
277
175
  ## License
278
176
 
279
- MIT — [DjangoCFG](https://djangocfg.com) · [GitHub](https://github.com/markolofsen/django-cfg)
177
+ MIT — [DjangoCFG](https://djangocfg.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.257",
3
+ "version": "2.1.259",
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.257",
78
- "@djangocfg/centrifugo": "^2.1.257",
79
- "@djangocfg/i18n": "^2.1.257",
80
- "@djangocfg/monitor": "^2.1.257",
81
- "@djangocfg/debuger": "^2.1.257",
82
- "@djangocfg/ui-core": "^2.1.257",
83
- "@djangocfg/ui-nextjs": "^2.1.257",
84
- "@djangocfg/ui-tools": "^2.1.257",
77
+ "@djangocfg/api": "^2.1.259",
78
+ "@djangocfg/centrifugo": "^2.1.259",
79
+ "@djangocfg/i18n": "^2.1.259",
80
+ "@djangocfg/monitor": "^2.1.259",
81
+ "@djangocfg/debuger": "^2.1.259",
82
+ "@djangocfg/ui-core": "^2.1.259",
83
+ "@djangocfg/ui-nextjs": "^2.1.259",
84
+ "@djangocfg/ui-tools": "^2.1.259",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
@@ -109,15 +109,15 @@
109
109
  "uuid": "^11.1.0"
110
110
  },
111
111
  "devDependencies": {
112
- "@djangocfg/api": "^2.1.257",
113
- "@djangocfg/i18n": "^2.1.257",
114
- "@djangocfg/centrifugo": "^2.1.257",
115
- "@djangocfg/monitor": "^2.1.257",
116
- "@djangocfg/debuger": "^2.1.257",
117
- "@djangocfg/typescript-config": "^2.1.257",
118
- "@djangocfg/ui-core": "^2.1.257",
119
- "@djangocfg/ui-nextjs": "^2.1.257",
120
- "@djangocfg/ui-tools": "^2.1.257",
112
+ "@djangocfg/api": "^2.1.259",
113
+ "@djangocfg/i18n": "^2.1.259",
114
+ "@djangocfg/centrifugo": "^2.1.259",
115
+ "@djangocfg/monitor": "^2.1.259",
116
+ "@djangocfg/debuger": "^2.1.259",
117
+ "@djangocfg/typescript-config": "^2.1.259",
118
+ "@djangocfg/ui-core": "^2.1.259",
119
+ "@djangocfg/ui-nextjs": "^2.1.259",
120
+ "@djangocfg/ui-tools": "^2.1.259",
121
121
  "@types/node": "^24.7.2",
122
122
  "@types/react": "^19.1.0",
123
123
  "@types/react-dom": "^19.1.0",
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * // Public page
12
12
  * <PublicLayout
13
- * navbar={<PublicNavbar config={{ logo: "/logo.svg", siteName: "My App", navigation: navItems }} />}
13
+ * navbar={<PublicNavbar config={{ brand: "My App", navigation: navItems, shell: { className: 'mx-auto max-w-7xl' } }} />}
14
14
  * >
15
15
  * {children}
16
16
  * </PublicLayout>
@@ -32,6 +32,9 @@
32
32
  // Layout components
33
33
  export * from './layouts';
34
34
 
35
+ // Typed theme style bridge + presets (BaseApp `theme.style`)
36
+ export * from './theme';
37
+
35
38
  // Re-export useRouter from nextjs-toploader for progress bar support
36
39
  // Use this instead of 'next/navigation' useRouter for router.push() to trigger progress
37
40
  export { useRouter } from 'nextjs-toploader/app';
@@ -52,8 +52,78 @@ import type {
52
52
  import type { AuthConfig } from '@djangocfg/api/auth';
53
53
  import type { MonitorConfig } from '@djangocfg/monitor';
54
54
 
55
+ import type { PublicNavbarConfig } from '../PublicLayout/components/PublicNavbar';
56
+ import type { PublicFooterConfig } from '../PublicLayout/components/PublicFooter/types';
57
+
55
58
  export type LayoutMode = 'public' | 'private' | 'admin';
56
59
 
60
+ /** `<main>` spacing in `PublicLayout` — offset from navbar / padding before footer. */
61
+ export type PublicMainTopSpacing = 'auto' | 'none';
62
+
63
+ /** Bottom padding of `<main>` above the footer. `compact` = less than `auto`; `none` = no extra gap. */
64
+ export type PublicMainBottomSpacing = 'auto' | 'none' | 'compact';
65
+
66
+ /** Shared marketing chrome defaults for `PublicNavbar` / `PublicFooter`, merged into your public layout. */
67
+ export interface AppLayoutPublicChrome {
68
+ navbar?: Partial<PublicNavbarConfig>;
69
+ footer?: Partial<PublicFooterConfig>;
70
+ /**
71
+ * Passed through to `PublicLayout` as `contentTopSpacing` / `contentBottomSpacing`.
72
+ * Use `bottomSpacing: 'none'` when the page (or footer) should sit flush with no default gap.
73
+ */
74
+ main?: {
75
+ topSpacing?: PublicMainTopSpacing;
76
+ bottomSpacing?: PublicMainBottomSpacing;
77
+ };
78
+ }
79
+
80
+ function mergePartialNavbar(
81
+ base?: Partial<PublicNavbarConfig>,
82
+ overlay?: Partial<PublicNavbarConfig>
83
+ ): Partial<PublicNavbarConfig> | undefined {
84
+ if (!base && !overlay) return undefined;
85
+ const merged: Partial<PublicNavbarConfig> = { ...base, ...overlay };
86
+ if (base?.shell || overlay?.shell) {
87
+ merged.shell = { ...base?.shell, ...overlay?.shell };
88
+ }
89
+ return merged;
90
+ }
91
+
92
+ function mergePartialFooter(
93
+ base?: Partial<PublicFooterConfig>,
94
+ overlay?: Partial<PublicFooterConfig>
95
+ ): Partial<PublicFooterConfig> | undefined {
96
+ if (!base && !overlay) return undefined;
97
+ const merged: Partial<PublicFooterConfig> = { ...base, ...overlay };
98
+ if (base?.shell || overlay?.shell) merged.shell = { ...base?.shell, ...overlay?.shell };
99
+ if (base?.brand || overlay?.brand) merged.brand = { ...base?.brand, ...overlay?.brand };
100
+ if (base?.menus || overlay?.menus) merged.menus = { ...base?.menus, ...overlay?.menus };
101
+ if (base?.meta || overlay?.meta) merged.meta = { ...base?.meta, ...overlay?.meta };
102
+ if (base?.social || overlay?.social) merged.social = { ...base?.social, ...overlay?.social };
103
+ if (base?.controls || overlay?.controls) {
104
+ merged.controls = { ...base?.controls, ...overlay?.controls };
105
+ }
106
+ return merged;
107
+ }
108
+
109
+ /**
110
+ * Deep-merge public chrome: root `publicChrome` first, then `layouts.publicChrome` overlays.
111
+ */
112
+ export function mergeAppLayoutPublicChrome(
113
+ root?: AppLayoutPublicChrome,
114
+ fromLayouts?: AppLayoutPublicChrome
115
+ ): AppLayoutPublicChrome | undefined {
116
+ if (!root && !fromLayouts) return undefined;
117
+ const navbar = mergePartialNavbar(root?.navbar, fromLayouts?.navbar);
118
+ const footer = mergePartialFooter(root?.footer, fromLayouts?.footer);
119
+ const main =
120
+ root?.main || fromLayouts?.main
121
+ ? { ...root?.main, ...fromLayouts?.main }
122
+ : undefined;
123
+ if (!navbar && !footer && !main) return undefined;
124
+ return { navbar, footer, ...(main ? { main } : {}) };
125
+ }
126
+
57
127
  /**
58
128
  * Determine layout mode from pathname and enabledPath props
59
129
  */
@@ -67,7 +137,7 @@ function determineLayoutMode(
67
137
  if (adminLayout && matchesPath(pathname, adminLayout.enabledPath)) return 'admin';
68
138
  if (privateLayout && matchesPath(pathname, privateLayout.enabledPath)) return 'private';
69
139
  if (publicLayout && matchesPath(pathname, publicLayout.enabledPath)) return 'public';
70
-
140
+
71
141
  // Default: if no enabledPath specified, use public as fallback
72
142
  return 'public';
73
143
  }
@@ -82,9 +152,19 @@ export interface I18nLayoutConfig {
82
152
  onLocaleChange: (locale: string) => void;
83
153
  }
84
154
 
155
+ /**
156
+ * Props passed to every layout component (`public` / `private` / `admin`).
157
+ * Use `publicChrome` to pass defaults for `PublicNavbar` / `PublicFooter` from `AppLayout`.
158
+ */
159
+ export interface AppLayoutLayoutComponentProps {
160
+ children: ReactNode;
161
+ i18n?: I18nLayoutConfig;
162
+ publicChrome?: AppLayoutPublicChrome;
163
+ }
164
+
85
165
  /** Layout configuration with component and enabled paths */
86
166
  interface LayoutConfig {
87
- component: React.ComponentType<{ children: ReactNode; i18n?: I18nLayoutConfig }>;
167
+ component: React.ComponentType<AppLayoutLayoutComponentProps>;
88
168
  enabledPath?: string | string[];
89
169
  }
90
170
 
@@ -94,6 +174,8 @@ export interface AppLayoutLayoutsConfig {
94
174
  admin?: LayoutConfig;
95
175
  noLayoutPaths?: string | string[];
96
176
  authPath?: string;
177
+ /** Merged over root `publicChrome` on `AppLayout` (see `mergeAppLayoutPublicChrome`). */
178
+ publicChrome?: AppLayoutPublicChrome;
97
179
  }
98
180
 
99
181
  export interface AppLayoutBaseAppConfig {
@@ -173,6 +255,9 @@ export interface AppLayoutProps {
173
255
  /** i18n configuration for locale switching (applies to all layouts) */
174
256
  i18n?: I18nLayoutConfig;
175
257
 
258
+ /** Base layer for `publicChrome`; `layouts.publicChrome` overlays this. */
259
+ publicChrome?: AppLayoutPublicChrome;
260
+
176
261
  /** Monitor configuration — initialises window.monitor + auto-captures JS errors & console */
177
262
  monitor?: MonitorConfig;
178
263
 
@@ -187,6 +272,7 @@ interface AppLayoutContentProps {
187
272
  noLayoutPaths?: string | string[];
188
273
  authPath?: string;
189
274
  i18n?: I18nLayoutConfig;
275
+ publicChrome?: AppLayoutPublicChrome;
190
276
  }
191
277
 
192
278
  /**
@@ -203,6 +289,7 @@ function AppLayoutContent({
203
289
  noLayoutPaths,
204
290
  authPath = '/auth',
205
291
  i18n,
292
+ publicChrome,
206
293
  }: AppLayoutContentProps) {
207
294
  // Use pathname without locale prefix for route matching
208
295
  const pathname = usePathnameWithoutLocale();
@@ -244,7 +331,7 @@ function AppLayoutContent({
244
331
  return (
245
332
  <ClientOnly>
246
333
  <Suspense>
247
- <privateLayout.component i18n={i18n}>
334
+ <privateLayout.component i18n={i18n} publicChrome={publicChrome}>
248
335
  {children}
249
336
  </privateLayout.component>
250
337
  </Suspense>
@@ -257,7 +344,7 @@ function AppLayoutContent({
257
344
  return (
258
345
  <ClientOnly>
259
346
  <Suspense>
260
- <adminLayout.component i18n={i18n}>
347
+ <adminLayout.component i18n={i18n} publicChrome={publicChrome}>
261
348
  {children}
262
349
  </adminLayout.component>
263
350
  </Suspense>
@@ -268,7 +355,7 @@ function AppLayoutContent({
268
355
  if (!privateLayout) {
269
356
  if (publicLayout) {
270
357
  return (
271
- <publicLayout.component i18n={i18n}>
358
+ <publicLayout.component i18n={i18n} publicChrome={publicChrome}>
272
359
  {children}
273
360
  </publicLayout.component>
274
361
  );
@@ -278,7 +365,7 @@ function AppLayoutContent({
278
365
  return (
279
366
  <ClientOnly>
280
367
  <Suspense>
281
- <privateLayout.component i18n={i18n}>
368
+ <privateLayout.component i18n={i18n} publicChrome={publicChrome}>
282
369
  {children}
283
370
  </privateLayout.component>
284
371
  </Suspense>
@@ -292,13 +379,13 @@ function AppLayoutContent({
292
379
  return children;
293
380
  }
294
381
  return (
295
- <publicLayout.component i18n={i18n}>
382
+ <publicLayout.component i18n={i18n} publicChrome={publicChrome}>
296
383
  {children}
297
384
  </publicLayout.component>
298
385
  );
299
386
  }
300
387
  };
301
-
388
+
302
389
  // No providers here - all providers now in BaseApp
303
390
  return renderLayout();
304
391
  }
@@ -315,6 +402,7 @@ export function AppLayout(props: AppLayoutProps) {
315
402
  const adminLayout = layoutsConfig?.admin ?? props.adminLayout;
316
403
  const noLayoutPaths = layoutsConfig?.noLayoutPaths ?? props.noLayoutPaths;
317
404
  const authPath = layoutsConfig?.authPath ?? props.authPath;
405
+ const publicChrome = mergeAppLayoutPublicChrome(props.publicChrome, layoutsConfig?.publicChrome);
318
406
 
319
407
  const {
320
408
  i18n,
@@ -355,6 +443,7 @@ export function AppLayout(props: AppLayoutProps) {
355
443
  noLayoutPaths={noLayoutPaths}
356
444
  authPath={authPath}
357
445
  i18n={i18n}
446
+ publicChrome={publicChrome}
358
447
  />
359
448
  </BaseApp>
360
449
  );
@@ -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 { ThemeProvider } from '@djangocfg/ui-nextjs/theme';
52
+ import { ThemeStyleBridge } from '../../theme/ThemeStyleBridge';
52
53
  import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
53
54
  import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
54
55
  import { AnalyticsProvider } from '../../snippets/Analytics';
@@ -115,6 +116,7 @@ export function BaseApp({
115
116
  defaultTheme={theme?.defaultTheme || 'system'}
116
117
  storageKey={theme?.storageKey}
117
118
  >
119
+ <ThemeStyleBridge style={theme?.style} />
118
120
  <DialogProvider>
119
121
  <TooltipProvider>
120
122
  <SWRConfig