@djangocfg/layouts 2.1.427 → 2.1.429
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 +29 -21
- package/package.json +15 -15
- package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +19 -0
- package/src/components/errors/ErrorsTracker/utils/curl-generator.ts +24 -10
- package/src/components/errors/README.md +63 -0
- package/src/layouts/AppLayout/BaseApp.tsx +7 -0
- package/src/layouts/AppLayout/README.md +79 -64
- package/src/layouts/AppLayout/index.ts +12 -19
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +0 -1
- package/src/layouts/PrivateLayout/README.md +30 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +1 -1
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +12 -3
- package/src/layouts/PrivateLayout/types.ts +0 -3
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +33 -30
- package/src/layouts/index.ts +0 -1
- package/src/utils/logger.ts +9 -4
- package/src/layouts/AdminLayout/AdminLayout.tsx +0 -57
- package/src/layouts/AdminLayout/index.ts +0 -7
- package/src/layouts/AppLayout/AppLayout.tsx +0 -513
|
@@ -61,12 +61,14 @@ function ApiKeyCard() {
|
|
|
61
61
|
const masked = apiKey ? isMasked(apiKey) : false;
|
|
62
62
|
const displayKey = apiKey ?? '—';
|
|
63
63
|
|
|
64
|
-
// Title-row state badge (read-only "Active" indicator).
|
|
64
|
+
// Title-row state badge (read-only "Active" indicator). A `div` wrapper —
|
|
65
|
+
// `Badge` renders a block `<div>`, which is invalid inside a `<span>`/`<p>`.
|
|
66
|
+
// SettingRow renders `description` inside a block, so this is valid.
|
|
65
67
|
const titleDescription = (
|
|
66
|
-
<
|
|
68
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
67
69
|
{apiKey && <Badge variant="secondary" className="text-xs">Active</Badge>}
|
|
68
70
|
<span>{labels.description}</span>
|
|
69
|
-
</
|
|
71
|
+
</div>
|
|
70
72
|
);
|
|
71
73
|
|
|
72
74
|
// Action buttons (arm/regenerate/test) for the title row.
|
|
@@ -87,31 +89,17 @@ function ApiKeyCard() {
|
|
|
87
89
|
</Button>
|
|
88
90
|
</div>
|
|
89
91
|
) : (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{/* Test button only when we have a fresh (full) key */}
|
|
102
|
-
{isFresh && (
|
|
103
|
-
<Button
|
|
104
|
-
variant="secondary"
|
|
105
|
-
size="sm"
|
|
106
|
-
onClick={testKey}
|
|
107
|
-
disabled={isTesting}
|
|
108
|
-
>
|
|
109
|
-
{isTesting
|
|
110
|
-
? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.testing}</>
|
|
111
|
-
: <><FlaskConical className="mr-2 h-4 w-4" />{labels.test}</>}
|
|
112
|
-
</Button>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
92
|
+
// Title row keeps a single action (Regenerate). The Test action moves next
|
|
93
|
+
// to the fresh key below, so this row stays uncluttered.
|
|
94
|
+
<Button
|
|
95
|
+
variant="outline"
|
|
96
|
+
size="sm"
|
|
97
|
+
onClick={arm}
|
|
98
|
+
disabled={!apiKey || isRegenerating}
|
|
99
|
+
>
|
|
100
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
101
|
+
{labels.regenerate}
|
|
102
|
+
</Button>
|
|
115
103
|
);
|
|
116
104
|
|
|
117
105
|
return (
|
|
@@ -157,8 +145,23 @@ function ApiKeyCard() {
|
|
|
157
145
|
</span>
|
|
158
146
|
</div>
|
|
159
147
|
|
|
160
|
-
{/* Copy only when the key is fresh (full key after regenerate) */}
|
|
161
|
-
{isFresh &&
|
|
148
|
+
{/* Copy + Test only when the key is fresh (full key after regenerate) */}
|
|
149
|
+
{isFresh && (
|
|
150
|
+
<>
|
|
151
|
+
<CopyButton value={apiKey} />
|
|
152
|
+
<Button
|
|
153
|
+
variant="secondary"
|
|
154
|
+
size="sm"
|
|
155
|
+
onClick={testKey}
|
|
156
|
+
disabled={isTesting}
|
|
157
|
+
className="shrink-0"
|
|
158
|
+
>
|
|
159
|
+
{isTesting
|
|
160
|
+
? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.testing}</>
|
|
161
|
+
: <><FlaskConical className="mr-2 h-4 w-4" />{labels.test}</>}
|
|
162
|
+
</Button>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
162
165
|
</div>
|
|
163
166
|
</SettingRow>
|
|
164
167
|
)}
|
package/src/layouts/index.ts
CHANGED
package/src/utils/logger.ts
CHANGED
|
@@ -11,15 +11,20 @@ import { createConsola } from 'consola';
|
|
|
11
11
|
* - 3: log, info
|
|
12
12
|
* - 4: debug
|
|
13
13
|
* - 5: trace, verbose
|
|
14
|
+
*
|
|
15
|
+
* Level is driven by the global runtime controller in @djangocfg/api so admins
|
|
16
|
+
* get verbose logs (even in prod) and regular users keep a clean console.
|
|
14
17
|
*/
|
|
15
|
-
import {
|
|
16
|
-
const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
|
|
17
|
-
const showLogs = isDevelopment || isStaticBuild;
|
|
18
|
+
import { getLogLevel, onLogLevelChange } from '@djangocfg/api';
|
|
18
19
|
|
|
19
20
|
export const logger = createConsola({
|
|
20
|
-
level:
|
|
21
|
+
level: getLogLevel(),
|
|
21
22
|
}).withTag('layouts');
|
|
22
23
|
|
|
24
|
+
onLogLevelChange((level) => {
|
|
25
|
+
logger.level = level;
|
|
26
|
+
});
|
|
27
|
+
|
|
23
28
|
// ─────────────────────────────────────────────────────────────────────────
|
|
24
29
|
// Module-specific loggers
|
|
25
30
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Layout
|
|
3
|
-
*
|
|
4
|
-
* Layout for admin dashboard pages
|
|
5
|
-
* Import and use directly with props - no complex configs needed!
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* import { AdminLayout } from '@djangocfg/layouts';
|
|
10
|
-
*
|
|
11
|
-
* <AdminLayout
|
|
12
|
-
* sidebar={{
|
|
13
|
-
* items: [
|
|
14
|
-
* { label: 'Overview', href: '/admin', icon: 'LayoutDashboard' },
|
|
15
|
-
* { label: 'Users', href: '/admin/users', icon: 'Users' }
|
|
16
|
-
* ]
|
|
17
|
-
* }}
|
|
18
|
-
* header={{
|
|
19
|
-
* title: 'Admin Dashboard',
|
|
20
|
-
* groups: [
|
|
21
|
-
* {
|
|
22
|
-
* title: 'Admin',
|
|
23
|
-
* items: [
|
|
24
|
-
* { label: 'Profile', href: '/profile' },
|
|
25
|
-
* { label: 'Settings', href: '/settings' }
|
|
26
|
-
* ]
|
|
27
|
-
* }
|
|
28
|
-
* ],
|
|
29
|
-
* authPath: '/auth'
|
|
30
|
-
* }}
|
|
31
|
-
* >
|
|
32
|
-
* {children}
|
|
33
|
-
* </AdminLayout>
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
'use client';
|
|
38
|
-
|
|
39
|
-
import { ReactNode } from 'react';
|
|
40
|
-
|
|
41
|
-
import { PrivateLayout } from '../PrivateLayout';
|
|
42
|
-
import type { PrivateLayoutProps } from '../PrivateLayout';
|
|
43
|
-
|
|
44
|
-
export interface AdminLayoutProps extends PrivateLayoutProps {
|
|
45
|
-
children: ReactNode;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Admin Layout Component
|
|
50
|
-
*
|
|
51
|
-
* Wrapper around PrivateLayout with admin-specific defaults
|
|
52
|
-
* Same API as PrivateLayout - just a convenience export
|
|
53
|
-
*/
|
|
54
|
-
export function AdminLayout(props: AdminLayoutProps) {
|
|
55
|
-
return <PrivateLayout {...props} />;
|
|
56
|
-
}
|
|
57
|
-
|
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AppLayout - Smart Layout Router with All Providers
|
|
3
|
-
*
|
|
4
|
-
* Automatically detects route type and applies the correct layout
|
|
5
|
-
* Includes all necessary providers: Theme, Auth, Analytics, Centrifugo, Error Tracking
|
|
6
|
-
* Simple props-based configuration - no complex configs needed!
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```tsx
|
|
10
|
-
* import { AppLayout } from '@djangocfg/layouts';
|
|
11
|
-
*
|
|
12
|
-
* <AppLayout
|
|
13
|
-
* publicLayout={{
|
|
14
|
-
* component: PublicLayout,
|
|
15
|
-
* enabledPath: ['/', '/about', '/contact']
|
|
16
|
-
* }}
|
|
17
|
-
* privateLayout={{
|
|
18
|
-
* component: DashboardLayout,
|
|
19
|
-
* enabledPath: '/dashboard'
|
|
20
|
-
* }}
|
|
21
|
-
* adminLayout={{
|
|
22
|
-
* component: AdminLayout,
|
|
23
|
-
* enabledPath: '/admin'
|
|
24
|
-
* }}
|
|
25
|
-
* // Paths that render without any layout wrapper (fullscreen pages)
|
|
26
|
-
* noLayoutPaths={['/private/terminal', '/embed']}
|
|
27
|
-
* >
|
|
28
|
-
* {children}
|
|
29
|
-
* </AppLayout>
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
'use client';
|
|
34
|
-
|
|
35
|
-
import React, { ReactNode, memo, useMemo } from 'react';
|
|
36
|
-
|
|
37
|
-
import { ClientOnly, Suspense } from '../../components/core';
|
|
38
|
-
import { usePathnameWithoutLocale } from '../../hooks';
|
|
39
|
-
import { matchesPath } from '../../utils/pathMatcher';
|
|
40
|
-
import { BaseApp } from './BaseApp';
|
|
41
|
-
import { useLayoutI18nOptional } from './LayoutI18nProvider';
|
|
42
|
-
import { ForcedThemeProvider, ThemeOverride, resolveForcedTheme } from '@djangocfg/ui-core/theme';
|
|
43
|
-
|
|
44
|
-
import type {
|
|
45
|
-
ThemeConfig,
|
|
46
|
-
AnalyticsConfig,
|
|
47
|
-
CentrifugoConfig,
|
|
48
|
-
ErrorTrackingConfig,
|
|
49
|
-
ErrorBoundaryConfig,
|
|
50
|
-
SWRConfigOptions,
|
|
51
|
-
DebugConfig,
|
|
52
|
-
I18nLayoutConfig,
|
|
53
|
-
} from '../types';
|
|
54
|
-
export type { I18nLayoutConfig } from '../types';
|
|
55
|
-
import type { AuthConfig } from '@djangocfg/api/auth';
|
|
56
|
-
import type { MonitorConfig } from '@djangocfg/monitor';
|
|
57
|
-
import type { ThemeOverrideRule } from '@djangocfg/ui-core/theme';
|
|
58
|
-
|
|
59
|
-
import type { FloatingNavbarConfig } from '../PublicLayout/navbars/FloatingNavbar';
|
|
60
|
-
import type { DefaultFooterConfig } from '../PublicLayout/footers/DefaultFooter/types';
|
|
61
|
-
|
|
62
|
-
export type LayoutMode = 'public' | 'private' | 'admin';
|
|
63
|
-
|
|
64
|
-
/** `<main>` spacing in `PublicLayout` — offset from navbar / padding before footer. */
|
|
65
|
-
export type PublicMainTopSpacing = 'auto' | 'none';
|
|
66
|
-
|
|
67
|
-
/** Bottom padding of `<main>` above the footer. `compact` = less than `auto`; `none` = no extra gap. */
|
|
68
|
-
export type PublicMainBottomSpacing = 'auto' | 'none' | 'compact';
|
|
69
|
-
|
|
70
|
-
/** Shared marketing chrome defaults for `FloatingNavbar` / `DefaultFooter`, merged into your public layout. */
|
|
71
|
-
export interface AppLayoutPublicChrome {
|
|
72
|
-
navbar?: Partial<FloatingNavbarConfig>;
|
|
73
|
-
footer?: Partial<DefaultFooterConfig>;
|
|
74
|
-
/**
|
|
75
|
-
* Passed through to `PublicLayout` as `contentTopSpacing` / `contentBottomSpacing`.
|
|
76
|
-
* Use `bottomSpacing: 'none'` when the page (or footer) should sit flush with no default gap.
|
|
77
|
-
*/
|
|
78
|
-
main?: {
|
|
79
|
-
topSpacing?: PublicMainTopSpacing;
|
|
80
|
-
bottomSpacing?: PublicMainBottomSpacing;
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Full-viewport background layer rendered behind the navbar and all page content.
|
|
84
|
-
* Pass a `fixed inset-0 -z-10 pointer-events-none` element — it covers the whole
|
|
85
|
-
* viewport (including the sticky navbar area) without affecting layout flow.
|
|
86
|
-
*
|
|
87
|
-
* Set per-page via `AppLayout publicChrome` or directly on `PublicSiteLayout`.
|
|
88
|
-
*/
|
|
89
|
-
backgroundSlot?: ReactNode;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function mergePartialNavbar(
|
|
93
|
-
base?: Partial<FloatingNavbarConfig>,
|
|
94
|
-
overlay?: Partial<FloatingNavbarConfig>
|
|
95
|
-
): Partial<FloatingNavbarConfig> | undefined {
|
|
96
|
-
if (!base && !overlay) return undefined;
|
|
97
|
-
const merged: Partial<FloatingNavbarConfig> = { ...base, ...overlay };
|
|
98
|
-
if (base?.shell || overlay?.shell) {
|
|
99
|
-
merged.shell = { ...base?.shell, ...overlay?.shell };
|
|
100
|
-
}
|
|
101
|
-
return merged;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function mergePartialFooter(
|
|
105
|
-
base?: Partial<DefaultFooterConfig>,
|
|
106
|
-
overlay?: Partial<DefaultFooterConfig>
|
|
107
|
-
): Partial<DefaultFooterConfig> | undefined {
|
|
108
|
-
if (!base && !overlay) return undefined;
|
|
109
|
-
const merged: Partial<DefaultFooterConfig> = { ...base, ...overlay };
|
|
110
|
-
if (base?.shell || overlay?.shell) merged.shell = { ...base?.shell, ...overlay?.shell };
|
|
111
|
-
if (base?.brand || overlay?.brand) merged.brand = { ...base?.brand, ...overlay?.brand };
|
|
112
|
-
if (base?.menus || overlay?.menus) merged.menus = { ...base?.menus, ...overlay?.menus };
|
|
113
|
-
if (base?.meta || overlay?.meta) merged.meta = { ...base?.meta, ...overlay?.meta };
|
|
114
|
-
if (base?.social || overlay?.social) merged.social = { ...base?.social, ...overlay?.social };
|
|
115
|
-
if (base?.controls || overlay?.controls) {
|
|
116
|
-
merged.controls = { ...base?.controls, ...overlay?.controls };
|
|
117
|
-
}
|
|
118
|
-
return merged;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Deep-merge public chrome: root `publicChrome` first, then `layouts.publicChrome` overlays.
|
|
123
|
-
*/
|
|
124
|
-
export function mergeAppLayoutPublicChrome(
|
|
125
|
-
root?: AppLayoutPublicChrome,
|
|
126
|
-
fromLayouts?: AppLayoutPublicChrome
|
|
127
|
-
): AppLayoutPublicChrome | undefined {
|
|
128
|
-
if (!root && !fromLayouts) return undefined;
|
|
129
|
-
const navbar = mergePartialNavbar(root?.navbar, fromLayouts?.navbar);
|
|
130
|
-
const footer = mergePartialFooter(root?.footer, fromLayouts?.footer);
|
|
131
|
-
const main =
|
|
132
|
-
root?.main || fromLayouts?.main
|
|
133
|
-
? { ...root?.main, ...fromLayouts?.main }
|
|
134
|
-
: undefined;
|
|
135
|
-
// backgroundSlot: layouts-level overrides root (more specific wins)
|
|
136
|
-
const backgroundSlot = fromLayouts?.backgroundSlot ?? root?.backgroundSlot;
|
|
137
|
-
if (!navbar && !footer && !main && !backgroundSlot) return undefined;
|
|
138
|
-
return { navbar, footer, ...(main ? { main } : {}), ...(backgroundSlot ? { backgroundSlot } : {}) };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Determine layout mode from pathname and enabledPath props
|
|
143
|
-
*/
|
|
144
|
-
function determineLayoutMode(
|
|
145
|
-
pathname: string,
|
|
146
|
-
adminLayout?: { component: any; enabledPath?: string | string[] },
|
|
147
|
-
privateLayout?: { component: any; enabledPath?: string | string[] },
|
|
148
|
-
publicLayout?: { component: any; enabledPath?: string | string[] }
|
|
149
|
-
): LayoutMode {
|
|
150
|
-
// Check in order: admin -> private -> public
|
|
151
|
-
if (adminLayout && matchesPath(pathname, adminLayout.enabledPath)) return 'admin';
|
|
152
|
-
if (privateLayout && matchesPath(pathname, privateLayout.enabledPath)) return 'private';
|
|
153
|
-
if (publicLayout && matchesPath(pathname, publicLayout.enabledPath)) return 'public';
|
|
154
|
-
|
|
155
|
-
// Default: if no enabledPath specified, use public as fallback
|
|
156
|
-
return 'public';
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Props passed to every layout component (`public` / `private` / `admin`).
|
|
161
|
-
* Use `publicChrome` to pass defaults for `FloatingNavbar` / `DefaultFooter` from `AppLayout`.
|
|
162
|
-
*
|
|
163
|
-
* Locale plumbing is no longer threaded as a prop — layouts read it from
|
|
164
|
-
* `useLayoutI18nOptional()` (mounted in `BaseApp`).
|
|
165
|
-
*/
|
|
166
|
-
export interface AppLayoutLayoutComponentProps {
|
|
167
|
-
children: ReactNode;
|
|
168
|
-
publicChrome?: AppLayoutPublicChrome;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Layout configuration with component and enabled paths */
|
|
172
|
-
interface LayoutConfig {
|
|
173
|
-
component: React.ComponentType<AppLayoutLayoutComponentProps>;
|
|
174
|
-
enabledPath?: string | string[];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface AppLayoutLayoutsConfig {
|
|
178
|
-
public?: LayoutConfig;
|
|
179
|
-
private?: LayoutConfig;
|
|
180
|
-
admin?: LayoutConfig;
|
|
181
|
-
noLayoutPaths?: string | string[];
|
|
182
|
-
authPath?: string;
|
|
183
|
-
/** Merged over root `publicChrome` on `AppLayout` (see `mergeAppLayoutPublicChrome`). */
|
|
184
|
-
publicChrome?: AppLayoutPublicChrome;
|
|
185
|
-
/**
|
|
186
|
-
* Per-route theme overrides. Each rule uses the same matcher as `enabledPath`
|
|
187
|
-
* (string, string[], or glob with `*` / `**`). When the pathname matches a
|
|
188
|
-
* rule, the forced theme is applied via `next-themes`; when navigation leaves
|
|
189
|
-
* the matched path, the user's previous theme choice is restored.
|
|
190
|
-
*
|
|
191
|
-
* Rules are evaluated top-to-bottom — first match wins.
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* ```ts
|
|
195
|
-
* themeOverrides: [
|
|
196
|
-
* { path: '/', theme: 'dark' }, // marketing landing always dark
|
|
197
|
-
* { path: ['/legal', '/legal/**'], theme: 'light' }, // legal always light
|
|
198
|
-
* ]
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
themeOverrides?: ThemeOverrideRule[];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export interface AppLayoutBaseAppConfig {
|
|
205
|
-
project?: string;
|
|
206
|
-
theme?: ThemeConfig;
|
|
207
|
-
auth?: AuthConfig;
|
|
208
|
-
analytics?: AnalyticsConfig;
|
|
209
|
-
centrifugo?: CentrifugoConfig;
|
|
210
|
-
errorTracking?: ErrorTrackingConfig;
|
|
211
|
-
swr?: SWRConfigOptions;
|
|
212
|
-
errorBoundary?: ErrorBoundaryConfig;
|
|
213
|
-
monitor?: MonitorConfig;
|
|
214
|
-
debug?: DebugConfig;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface AppLayoutProps {
|
|
218
|
-
children: ReactNode;
|
|
219
|
-
|
|
220
|
-
/** Compact routing/layout config */
|
|
221
|
-
layouts?: AppLayoutLayoutsConfig;
|
|
222
|
-
/** Compact providers/base-app config */
|
|
223
|
-
baseApp?: AppLayoutBaseAppConfig;
|
|
224
|
-
|
|
225
|
-
/** Project name — used as default for monitor.project and debug panel title */
|
|
226
|
-
project?: string;
|
|
227
|
-
|
|
228
|
-
/** Public layout component with enabled paths */
|
|
229
|
-
publicLayout?: LayoutConfig;
|
|
230
|
-
|
|
231
|
-
/** Private layout component with enabled paths */
|
|
232
|
-
privateLayout?: LayoutConfig;
|
|
233
|
-
|
|
234
|
-
/** Admin layout component with enabled paths */
|
|
235
|
-
adminLayout?: LayoutConfig;
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Paths that render without any layout wrapper (fullscreen pages)
|
|
239
|
-
* Providers (auth, theme, etc.) are still applied, only layout is skipped
|
|
240
|
-
* Useful for: fullscreen terminal, embed pages, print views
|
|
241
|
-
*/
|
|
242
|
-
noLayoutPaths?: string | string[];
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Auth path prefix. When set, all routes starting with this path
|
|
246
|
-
* (and their localized variants like /ru/auth, /en/auth) render without
|
|
247
|
-
* any layout wrapper — fullscreen, no navbar/sidebar.
|
|
248
|
-
* @default '/auth'
|
|
249
|
-
* @example authPath="/auth" → skips layout for /auth, /auth/*, /ru/auth, /ru/auth/*, etc.
|
|
250
|
-
*/
|
|
251
|
-
authPath?: string;
|
|
252
|
-
|
|
253
|
-
/** Theme configuration */
|
|
254
|
-
theme?: ThemeConfig;
|
|
255
|
-
|
|
256
|
-
/** Auth configuration */
|
|
257
|
-
auth?: AuthConfig;
|
|
258
|
-
|
|
259
|
-
/** Analytics configuration */
|
|
260
|
-
analytics?: AnalyticsConfig;
|
|
261
|
-
|
|
262
|
-
/** Centrifugo configuration */
|
|
263
|
-
centrifugo?: CentrifugoConfig;
|
|
264
|
-
|
|
265
|
-
/** Error tracking configuration */
|
|
266
|
-
errorTracking?: ErrorTrackingConfig;
|
|
267
|
-
|
|
268
|
-
/** SWR configuration */
|
|
269
|
-
swr?: SWRConfigOptions;
|
|
270
|
-
|
|
271
|
-
/** Error boundary configuration */
|
|
272
|
-
errorBoundary?: ErrorBoundaryConfig;
|
|
273
|
-
|
|
274
|
-
/** i18n configuration for locale switching (applies to all layouts) */
|
|
275
|
-
i18n?: I18nLayoutConfig;
|
|
276
|
-
|
|
277
|
-
/** Base layer for `publicChrome`; `layouts.publicChrome` overlays this. */
|
|
278
|
-
publicChrome?: AppLayoutPublicChrome;
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Per-route theme overrides. Shortcut for `layouts.themeOverrides`.
|
|
282
|
-
* See `AppLayoutLayoutsConfig.themeOverrides` for the full docs.
|
|
283
|
-
*/
|
|
284
|
-
themeOverrides?: ThemeOverrideRule[];
|
|
285
|
-
|
|
286
|
-
/** Monitor configuration — initialises window.monitor + auto-captures JS errors & console */
|
|
287
|
-
monitor?: MonitorConfig;
|
|
288
|
-
|
|
289
|
-
debug?: DebugConfig;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
interface AppLayoutContentProps {
|
|
293
|
-
children: ReactNode;
|
|
294
|
-
publicLayout?: LayoutConfig;
|
|
295
|
-
privateLayout?: LayoutConfig;
|
|
296
|
-
adminLayout?: LayoutConfig;
|
|
297
|
-
noLayoutPaths?: string | string[];
|
|
298
|
-
authPath?: string;
|
|
299
|
-
publicChrome?: AppLayoutPublicChrome;
|
|
300
|
-
themeOverrides?: ThemeOverrideRule[];
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* AppLayout Content - Renders layout with all providers
|
|
305
|
-
*
|
|
306
|
-
* SSR is only enabled for publicLayout.
|
|
307
|
-
* Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
|
|
308
|
-
*/
|
|
309
|
-
function AppLayoutContentRaw({
|
|
310
|
-
children,
|
|
311
|
-
publicLayout,
|
|
312
|
-
privateLayout,
|
|
313
|
-
adminLayout,
|
|
314
|
-
noLayoutPaths,
|
|
315
|
-
authPath = '/auth',
|
|
316
|
-
publicChrome,
|
|
317
|
-
themeOverrides,
|
|
318
|
-
}: AppLayoutContentProps) {
|
|
319
|
-
// Use pathname without locale prefix for route matching. The current
|
|
320
|
-
// locale comes from the LayoutI18nProvider mounted by BaseApp — passing
|
|
321
|
-
// it explicitly avoids the regex misfiring on `/ui`, `/ko`, etc.
|
|
322
|
-
const i18n = useLayoutI18nOptional();
|
|
323
|
-
const pathname = usePathnameWithoutLocale(i18n?.locale);
|
|
324
|
-
|
|
325
|
-
// Merge authPath into noLayoutPaths — auth pages are always fullscreen
|
|
326
|
-
const effectiveNoLayoutPaths = useMemo(() => {
|
|
327
|
-
const base = noLayoutPaths
|
|
328
|
-
? (Array.isArray(noLayoutPaths) ? noLayoutPaths : [noLayoutPaths])
|
|
329
|
-
: []
|
|
330
|
-
return [...base, authPath]
|
|
331
|
-
}, [noLayoutPaths, authPath])
|
|
332
|
-
|
|
333
|
-
// Check if current path should skip layout
|
|
334
|
-
const shouldSkipLayout = useMemo(
|
|
335
|
-
() => matchesPath(pathname, effectiveNoLayoutPaths),
|
|
336
|
-
[pathname, effectiveNoLayoutPaths]
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
const layoutMode = useMemo(
|
|
340
|
-
() => determineLayoutMode(
|
|
341
|
-
pathname,
|
|
342
|
-
adminLayout,
|
|
343
|
-
privateLayout,
|
|
344
|
-
publicLayout
|
|
345
|
-
),
|
|
346
|
-
[pathname, adminLayout, privateLayout, publicLayout]
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
// Prepare everything above the JSX — no inline conditionals in return().
|
|
350
|
-
const hasThemeOverrides = Boolean(themeOverrides && themeOverrides.length > 0);
|
|
351
|
-
const forcedTheme = hasThemeOverrides
|
|
352
|
-
? resolveForcedTheme(pathname, themeOverrides)
|
|
353
|
-
: null;
|
|
354
|
-
const themeOverrideElement = hasThemeOverrides
|
|
355
|
-
? <ThemeOverride pathname={pathname} rules={themeOverrides!} />
|
|
356
|
-
: null;
|
|
357
|
-
|
|
358
|
-
// Memoize layout element so it doesn't re-render on every pathname change
|
|
359
|
-
// that doesn't affect layout mode (e.g. /dashboard/a → /dashboard/b).
|
|
360
|
-
const layoutElement = useMemo(() => {
|
|
361
|
-
// Skip layout for noLayoutPaths (fullscreen pages)
|
|
362
|
-
if (shouldSkipLayout) {
|
|
363
|
-
return children;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
switch (layoutMode) {
|
|
367
|
-
case 'admin':
|
|
368
|
-
if (!adminLayout && privateLayout) {
|
|
369
|
-
return (
|
|
370
|
-
<ClientOnly>
|
|
371
|
-
<Suspense>
|
|
372
|
-
<privateLayout.component publicChrome={publicChrome}>
|
|
373
|
-
{children}
|
|
374
|
-
</privateLayout.component>
|
|
375
|
-
</Suspense>
|
|
376
|
-
</ClientOnly>
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (!adminLayout) {
|
|
380
|
-
return children;
|
|
381
|
-
}
|
|
382
|
-
return (
|
|
383
|
-
<ClientOnly>
|
|
384
|
-
<Suspense>
|
|
385
|
-
<adminLayout.component publicChrome={publicChrome}>
|
|
386
|
-
{children}
|
|
387
|
-
</adminLayout.component>
|
|
388
|
-
</Suspense>
|
|
389
|
-
</ClientOnly>
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
case 'private':
|
|
393
|
-
if (!privateLayout) {
|
|
394
|
-
if (publicLayout) {
|
|
395
|
-
return (
|
|
396
|
-
<publicLayout.component publicChrome={publicChrome}>
|
|
397
|
-
{children}
|
|
398
|
-
</publicLayout.component>
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
return children;
|
|
402
|
-
}
|
|
403
|
-
return (
|
|
404
|
-
<ClientOnly>
|
|
405
|
-
<Suspense>
|
|
406
|
-
<privateLayout.component publicChrome={publicChrome}>
|
|
407
|
-
{children}
|
|
408
|
-
</privateLayout.component>
|
|
409
|
-
</Suspense>
|
|
410
|
-
</ClientOnly>
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
case 'public':
|
|
414
|
-
default:
|
|
415
|
-
// Public layout renders with SSR (no ClientOnly wrapper)
|
|
416
|
-
if (!publicLayout) {
|
|
417
|
-
return children;
|
|
418
|
-
}
|
|
419
|
-
return (
|
|
420
|
-
<publicLayout.component publicChrome={publicChrome}>
|
|
421
|
-
{children}
|
|
422
|
-
</publicLayout.component>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
}, [shouldSkipLayout, layoutMode, publicLayout, privateLayout, adminLayout, publicChrome, children]);
|
|
426
|
-
|
|
427
|
-
// No providers here - all providers now in BaseApp
|
|
428
|
-
return (
|
|
429
|
-
<ForcedThemeProvider value={forcedTheme}>
|
|
430
|
-
{themeOverrideElement}
|
|
431
|
-
{layoutElement}
|
|
432
|
-
</ForcedThemeProvider>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Memoised layout content wrapper. Re-renders only when layout config props
|
|
438
|
-
* change (publicLayout, privateLayout, adminLayout, noLayoutPaths, etc.).
|
|
439
|
-
* The `children` prop is compared by reference — the consumer should wrap
|
|
440
|
-
* page content in React.memo or use stable element references to avoid
|
|
441
|
-
* unnecessary layout re-mounts on every parent render.
|
|
442
|
-
*/
|
|
443
|
-
const AppLayoutContent = memo(AppLayoutContentRaw, (prev, next) => {
|
|
444
|
-
return (
|
|
445
|
-
prev.children === next.children &&
|
|
446
|
-
prev.publicLayout === next.publicLayout &&
|
|
447
|
-
prev.privateLayout === next.privateLayout &&
|
|
448
|
-
prev.adminLayout === next.adminLayout &&
|
|
449
|
-
prev.noLayoutPaths === next.noLayoutPaths &&
|
|
450
|
-
prev.authPath === next.authPath &&
|
|
451
|
-
prev.publicChrome === next.publicChrome &&
|
|
452
|
-
prev.themeOverrides === next.themeOverrides
|
|
453
|
-
);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* AppLayout - Main Component with All Providers
|
|
458
|
-
*/
|
|
459
|
-
export function AppLayout(props: AppLayoutProps) {
|
|
460
|
-
const layoutsConfig = props.layouts;
|
|
461
|
-
const baseAppConfig = props.baseApp;
|
|
462
|
-
|
|
463
|
-
const publicLayout = layoutsConfig?.public ?? props.publicLayout;
|
|
464
|
-
const privateLayout = layoutsConfig?.private ?? props.privateLayout;
|
|
465
|
-
const adminLayout = layoutsConfig?.admin ?? props.adminLayout;
|
|
466
|
-
const noLayoutPaths = layoutsConfig?.noLayoutPaths ?? props.noLayoutPaths;
|
|
467
|
-
const authPath = layoutsConfig?.authPath ?? props.authPath;
|
|
468
|
-
const publicChrome = mergeAppLayoutPublicChrome(props.publicChrome, layoutsConfig?.publicChrome);
|
|
469
|
-
const themeOverrides = layoutsConfig?.themeOverrides ?? props.themeOverrides;
|
|
470
|
-
|
|
471
|
-
const {
|
|
472
|
-
i18n,
|
|
473
|
-
children,
|
|
474
|
-
} = props;
|
|
475
|
-
|
|
476
|
-
const project = baseAppConfig?.project ?? props.project;
|
|
477
|
-
const theme = baseAppConfig?.theme ?? props.theme;
|
|
478
|
-
const auth = baseAppConfig?.auth ?? props.auth;
|
|
479
|
-
const analytics = baseAppConfig?.analytics ?? props.analytics;
|
|
480
|
-
const centrifugo = baseAppConfig?.centrifugo ?? props.centrifugo;
|
|
481
|
-
const errorTracking = baseAppConfig?.errorTracking ?? props.errorTracking;
|
|
482
|
-
const errorBoundary = baseAppConfig?.errorBoundary ?? props.errorBoundary;
|
|
483
|
-
const swr = baseAppConfig?.swr ?? props.swr;
|
|
484
|
-
const monitor = baseAppConfig?.monitor ?? props.monitor;
|
|
485
|
-
const debug = baseAppConfig?.debug ?? props.debug;
|
|
486
|
-
|
|
487
|
-
return (
|
|
488
|
-
<BaseApp
|
|
489
|
-
project={project}
|
|
490
|
-
theme={theme}
|
|
491
|
-
auth={auth}
|
|
492
|
-
analytics={analytics}
|
|
493
|
-
centrifugo={centrifugo}
|
|
494
|
-
errorTracking={errorTracking}
|
|
495
|
-
errorBoundary={errorBoundary}
|
|
496
|
-
swr={swr}
|
|
497
|
-
monitor={monitor}
|
|
498
|
-
debug={debug}
|
|
499
|
-
i18n={i18n}
|
|
500
|
-
>
|
|
501
|
-
<AppLayoutContent
|
|
502
|
-
children={children}
|
|
503
|
-
publicLayout={publicLayout}
|
|
504
|
-
privateLayout={privateLayout}
|
|
505
|
-
adminLayout={adminLayout}
|
|
506
|
-
noLayoutPaths={noLayoutPaths}
|
|
507
|
-
authPath={authPath}
|
|
508
|
-
publicChrome={publicChrome}
|
|
509
|
-
themeOverrides={themeOverrides}
|
|
510
|
-
/>
|
|
511
|
-
</BaseApp>
|
|
512
|
-
);
|
|
513
|
-
}
|