@djangocfg/layouts 2.1.426 → 2.1.428
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 -17
- 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 +36 -52
- package/src/layouts/AppLayout/README.md +79 -64
- package/src/layouts/AppLayout/index.ts +12 -19
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -4
- package/src/layouts/PrivateLayout/README.md +30 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +6 -2
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +12 -3
- package/src/layouts/PrivateLayout/types.ts +8 -3
- package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
- package/src/layouts/SettingsLayout/README.md +258 -0
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
- package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +192 -0
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
- package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
- package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
- package/src/layouts/SettingsLayout/components/index.ts +6 -0
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
- package/src/layouts/SettingsLayout/context/index.ts +2 -0
- package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
- package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
- package/src/layouts/SettingsLayout/index.ts +67 -0
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
- package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
- package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
- package/src/layouts/SettingsLayout/sections/index.ts +8 -0
- package/src/layouts/SettingsLayout/store.ts +47 -0
- package/src/layouts/SettingsLayout/types.ts +107 -0
- package/src/layouts/index.ts +1 -2
- package/src/layouts/types/index.ts +0 -1
- package/src/layouts/types/layout.types.ts +0 -4
- 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 -520
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
- package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
- package/src/layouts/ProfileLayout/README.md +0 -150
- package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
- package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
- package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
- package/src/layouts/ProfileLayout/components/index.ts +0 -11
- package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
- package/src/layouts/ProfileLayout/index.ts +0 -8
- package/src/layouts/ProfileLayout/types.ts +0 -48
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/index.ts +0 -0
package/README.md
CHANGED
|
@@ -13,38 +13,45 @@ pnpm add @djangocfg/layouts
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```css
|
|
16
|
-
/*
|
|
17
|
-
@import "@djangocfg/ui-
|
|
16
|
+
/* Golden path — bundles Tailwind + tokens + base + utilities, layer-safe */
|
|
17
|
+
@import "@djangocfg/ui-core/styles/full";
|
|
18
18
|
@import "@djangocfg/layouts/styles";
|
|
19
|
-
@import "tailwindcss";
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
Peers: `@djangocfg/ui-core`,
|
|
21
|
+
Peers: `@djangocfg/ui-core`, React 19, Next.js 16+, Tailwind CSS 4.
|
|
23
22
|
|
|
24
23
|
---
|
|
25
24
|
|
|
26
25
|
## Quick start
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
Two responsibilities, kept separate:
|
|
28
|
+
|
|
29
|
+
1. **Providers** — mount `BaseApp` ONCE at the app root (theme, auth, i18n, SWR,
|
|
30
|
+
monitor, toasts). It's framework-agnostic (works in Wails/Vite too).
|
|
31
|
+
2. **Per-section shell** — each Next.js **route-group `layout.tsx`** imports the
|
|
32
|
+
matching shell (`PrivateLayout` / `PublicLayout`) and passes its own config.
|
|
33
|
+
No runtime layout-router, no `enabledPath` matching.
|
|
29
34
|
|
|
30
35
|
```tsx
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
noLayoutPaths: ['/embed'],
|
|
37
|
-
}}
|
|
38
|
-
baseApp={{ project: 'my-app', theme: { defaultTheme: 'system' }, auth: { apiUrl: '…' } }}
|
|
39
|
-
i18n={{ locale, locales, onLocaleChange: changeLocale, routing }}
|
|
40
|
-
>
|
|
36
|
+
// app/[locale]/layout.tsx — providers, once
|
|
37
|
+
import { BaseApp } from '@djangocfg/layouts';
|
|
38
|
+
<BaseApp project="my-app" theme={{ defaultTheme: 'system' }}
|
|
39
|
+
auth={{ apiUrl: '…', routes: { auth: '/auth' } }}
|
|
40
|
+
i18n={{ locale, locales, onLocaleChange: changeLocale, routing }}>
|
|
41
41
|
{children}
|
|
42
|
-
</
|
|
42
|
+
</BaseApp>
|
|
43
|
+
|
|
44
|
+
// app/[locale]/(pages)/private/layout.tsx — the private shell
|
|
45
|
+
import { PrivateLayout } from '@djangocfg/layouts';
|
|
46
|
+
<PrivateLayout sidebar={sidebar} header={header}>{children}</PrivateLayout>
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
See the [**BaseApp README**](./src/layouts/AppLayout/README.md) for the full
|
|
50
|
+
provider surface and the Next.js vs Wails wiring, and the demo app
|
|
51
|
+
(`apps/demo/app/_layouts`) for a complete reference.
|
|
46
52
|
|
|
47
|
-
> **Pass `i18n`** when using `next-intl` or any locale-prefixed routing
|
|
53
|
+
> **Pass `i18n`** when using `next-intl` or any locale-prefixed routing, so the
|
|
54
|
+
> link bridge and locale switcher resolve correctly.
|
|
48
55
|
|
|
49
56
|
---
|
|
50
57
|
|
|
@@ -55,8 +62,9 @@ Use `BaseApp` directly when you don't need route-based layout switching — see
|
|
|
55
62
|
| **`PublicLayout`** | Marketing / docs. Slots for navbar (`Floating`/`Flush`/`Minimal`) + footer + locale + auth controls. | [README](./src/layouts/PublicLayout/README.md) |
|
|
56
63
|
| **`PrivateLayout`** | Authenticated app shell — sidebar (collapsible icon rail, accordion groups, rail/featured/CTA slots) + popover account footer. | [README](./src/layouts/PrivateLayout/README.md) |
|
|
57
64
|
| **`AuthLayout`** | Sign-in / sign-up flows. Shell-based: `centered` (default) or `split` (two-column desktop). | [README](./src/layouts/AuthLayout/README.md) |
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
|
|
66
|
+
> **Admin** is not a separate layout — use `PrivateLayout` with an admin `sidebar`
|
|
67
|
+
> config in your `app/.../admin/layout.tsx`.
|
|
60
68
|
|
|
61
69
|
### `AuthLayout` shells
|
|
62
70
|
|
|
@@ -115,7 +123,7 @@ import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
|
|
|
115
123
|
import { routing } from '@djangocfg/nextjs/i18n/routing';
|
|
116
124
|
|
|
117
125
|
const { locale, locales, changeLocale } = useLocaleSwitcher();
|
|
118
|
-
<
|
|
126
|
+
<BaseApp i18n={{ locale, locales, onLocaleChange: changeLocale, routing }}>...</BaseApp>
|
|
119
127
|
```
|
|
120
128
|
|
|
121
129
|
When `routing` is set, `BaseApp` mounts a locale-aware `<Link>` adapter so every layout-rendered link keeps the active locale prefix. Drop it for default-locale-only apps.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.428",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -89,13 +89,12 @@
|
|
|
89
89
|
"check": "tsc --noEmit"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"@djangocfg/api": "^2.1.
|
|
93
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
94
|
-
"@djangocfg/debuger": "^2.1.
|
|
95
|
-
"@djangocfg/i18n": "^2.1.
|
|
96
|
-
"@djangocfg/monitor": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
98
|
-
"@djangocfg/ui-nextjs": "^2.1.426",
|
|
92
|
+
"@djangocfg/api": "^2.1.428",
|
|
93
|
+
"@djangocfg/centrifugo": "^2.1.428",
|
|
94
|
+
"@djangocfg/debuger": "^2.1.428",
|
|
95
|
+
"@djangocfg/i18n": "^2.1.428",
|
|
96
|
+
"@djangocfg/monitor": "^2.1.428",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.428",
|
|
99
98
|
"@hookform/resolvers": "^5.2.2",
|
|
100
99
|
"consola": "^3.4.2",
|
|
101
100
|
"lucide-react": "^0.545.0",
|
|
@@ -126,15 +125,14 @@
|
|
|
126
125
|
"uuid": "^11.1.0"
|
|
127
126
|
},
|
|
128
127
|
"devDependencies": {
|
|
129
|
-
"@djangocfg/api": "^2.1.
|
|
130
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
131
|
-
"@djangocfg/debuger": "^2.1.
|
|
132
|
-
"@djangocfg/i18n": "^2.1.
|
|
133
|
-
"@djangocfg/monitor": "^2.1.
|
|
134
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
135
|
-
"@djangocfg/ui-core": "^2.1.
|
|
136
|
-
"@djangocfg/ui-
|
|
137
|
-
"@djangocfg/ui-tools": "^2.1.426",
|
|
128
|
+
"@djangocfg/api": "^2.1.428",
|
|
129
|
+
"@djangocfg/centrifugo": "^2.1.428",
|
|
130
|
+
"@djangocfg/debuger": "^2.1.428",
|
|
131
|
+
"@djangocfg/i18n": "^2.1.428",
|
|
132
|
+
"@djangocfg/monitor": "^2.1.428",
|
|
133
|
+
"@djangocfg/typescript-config": "^2.1.428",
|
|
134
|
+
"@djangocfg/ui-core": "^2.1.428",
|
|
135
|
+
"@djangocfg/ui-tools": "^2.1.428",
|
|
138
136
|
"@types/node": "^25.2.3",
|
|
139
137
|
"@types/react": "^19.2.15",
|
|
140
138
|
"@types/react-dom": "^19.2.3",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
|
|
11
|
+
import { isDev } from '@djangocfg/ui-core/lib';
|
|
12
|
+
|
|
11
13
|
import { extractDomain, formatErrorTitle, formatZodIssues, safeZodIssues } from '../utils/formatters';
|
|
12
14
|
import { ErrorButtons } from './ErrorButtons';
|
|
13
15
|
|
|
@@ -106,6 +108,17 @@ function buildNetworkDescription(
|
|
|
106
108
|
parts.push(`Status: ${detail.statusCode}`);
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
// DPoP (RFC 9449) 401s are easy to mistake for a plain auth failure. In dev,
|
|
112
|
+
// add a clear hint pointing at the cause. Detected from the backend's error
|
|
113
|
+
// message (codes dpop_proof_required / dpop_proof_invalid). Dev-only so end
|
|
114
|
+
// users never see internal proof wording.
|
|
115
|
+
const looksDpop =
|
|
116
|
+
detail.statusCode === 401 && /dpop/i.test(detail.error || '');
|
|
117
|
+
const dpopHint = isDev && looksDpop
|
|
118
|
+
? 'DPoP: this token is sender-constrained — the request needs a valid DPoP proof '
|
|
119
|
+
+ 'signed by the in-browser key. A copied/replayed token will 401.'
|
|
120
|
+
: null;
|
|
121
|
+
|
|
109
122
|
return (
|
|
110
123
|
<div className="flex flex-col gap-2 text-sm">
|
|
111
124
|
{parts.length > 0 && (
|
|
@@ -116,6 +129,12 @@ function buildNetworkDescription(
|
|
|
116
129
|
|
|
117
130
|
<div className="opacity-90">{detail.error}</div>
|
|
118
131
|
|
|
132
|
+
{dpopHint && (
|
|
133
|
+
<div className="rounded-md bg-amber-500/10 px-2 py-1 text-xs text-amber-600 dark:text-amber-400">
|
|
134
|
+
{dpopHint}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
119
138
|
<ErrorButtons detail={detail} />
|
|
120
139
|
</div>
|
|
121
140
|
);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Generates cURL commands from API request details with authentication token
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { auth, dpopEnabled } from '@djangocfg/api';
|
|
9
10
|
import consola from 'consola';
|
|
10
11
|
|
|
11
12
|
export interface CurlOptions {
|
|
@@ -19,18 +20,19 @@ export interface CurlOptions {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* Get
|
|
23
|
+
* Get the current access token via the shared auth store.
|
|
24
|
+
*
|
|
25
|
+
* Must go through `auth.getToken()` — it reads the live token from whichever
|
|
26
|
+
* backend the app configured (localStorage *or* cookie) under the canonical
|
|
27
|
+
* `cfg.access_token` key. Reading raw localStorage keys here (the old
|
|
28
|
+
* `access_token`/`token`/`auth_token` guesses) never matched and always
|
|
29
|
+
* returned null, so the copied cURL had no Authorization header.
|
|
23
30
|
*/
|
|
24
31
|
export function getAuthToken(): string | null {
|
|
25
32
|
if (typeof window === 'undefined') return null;
|
|
26
33
|
|
|
27
34
|
try {
|
|
28
|
-
|
|
29
|
-
const token = localStorage.getItem('access_token') ||
|
|
30
|
-
localStorage.getItem('token') ||
|
|
31
|
-
localStorage.getItem('auth_token');
|
|
32
|
-
|
|
33
|
-
return token;
|
|
35
|
+
return auth.getToken();
|
|
34
36
|
} catch (error) {
|
|
35
37
|
consola.error('Failed to get auth token:', error);
|
|
36
38
|
return null;
|
|
@@ -84,8 +86,20 @@ export function generateCurl(options: CurlOptions): string {
|
|
|
84
86
|
...headers,
|
|
85
87
|
};
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const dpop = dpopEnabled;
|
|
90
|
+
let preamble = '';
|
|
91
|
+
|
|
92
|
+
if (dpop) {
|
|
93
|
+
// DPoP on → the Bearer token is non-replayable from a terminal. Offer the
|
|
94
|
+
// API-key path instead (the supported way to script the API), with a
|
|
95
|
+
// placeholder the user fills from Settings → API keys.
|
|
96
|
+
allHeaders['X-API-Key'] = '<your-api-key>';
|
|
97
|
+
preamble =
|
|
98
|
+
'# This endpoint is DPoP-protected: a copied Bearer token returns 401\n' +
|
|
99
|
+
'# (it is bound to a non-extractable in-browser key, proof expires ~60s).\n' +
|
|
100
|
+
'# For scripts, use an API key — get one at: Settings → API keys.\n';
|
|
101
|
+
} else if (token) {
|
|
102
|
+
// No DPoP → a Bearer token works in a terminal as-is.
|
|
89
103
|
allHeaders['Authorization'] = `Bearer ${token}`;
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -102,7 +116,7 @@ export function generateCurl(options: CurlOptions): string {
|
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
// Join with line continuation
|
|
105
|
-
return curlParts.join(' \\\n ');
|
|
119
|
+
return preamble + curlParts.join(' \\\n ');
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
/**
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Errors — boundaries + runtime error tracking
|
|
2
|
+
|
|
3
|
+
In-app error surfacing for `@djangocfg/layouts`: React error boundaries, a global
|
|
4
|
+
runtime error tracker, and developer-facing toasts (with a Copy-as-cURL action).
|
|
5
|
+
|
|
6
|
+
## Pieces
|
|
7
|
+
|
|
8
|
+
| File | What it does |
|
|
9
|
+
|------|--------------|
|
|
10
|
+
| `ErrorBoundary.tsx` | React error boundary with a friendly fallback. |
|
|
11
|
+
| `MonitorBoundary.tsx` | Error boundary that also reports to `@djangocfg/monitor`. |
|
|
12
|
+
| `ErrorLayout.tsx` | Full-page error layout (`getErrorContent` / `ERROR_CODES`). |
|
|
13
|
+
| `ErrorsTracker/` | Global runtime tracker — listens for error CustomEvents and shows toasts. |
|
|
14
|
+
|
|
15
|
+
## ErrorsTracker
|
|
16
|
+
|
|
17
|
+
`ErrorTrackingProvider` listens on `window` for typed error events dispatched by
|
|
18
|
+
the generated API client and runtime, then renders a Sonner toast per error.
|
|
19
|
+
|
|
20
|
+
Tracked error types: `validation` (zod response mismatch), `network` (failed/4xx/5xx
|
|
21
|
+
requests, incl. a CORS heuristic), `centrifugo` (WS), and `runtime` (uncaught JS).
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { ErrorTrackingProvider } from '@djangocfg/layouts'; // (or the module path)
|
|
25
|
+
|
|
26
|
+
<ErrorTrackingProvider
|
|
27
|
+
validation={{ enabled: true }}
|
|
28
|
+
network={{ enabled: true, showStatusCode: true }}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</ErrorTrackingProvider>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Toast actions
|
|
35
|
+
|
|
36
|
+
Each toast has **Copy error** (JSON to clipboard) and, for requests, **Copy cURL**.
|
|
37
|
+
|
|
38
|
+
### Copy-as-cURL + DPoP
|
|
39
|
+
|
|
40
|
+
`utils/curl-generator.ts` builds a runnable cURL from the failed request:
|
|
41
|
+
|
|
42
|
+
- **DPoP off** — includes `Authorization: Bearer <token>` (read live via
|
|
43
|
+
`auth.getToken()`), so the command works as-is in a terminal.
|
|
44
|
+
- **DPoP on** (`dpopEnabled` from `@djangocfg/api`) — a copied Bearer token would
|
|
45
|
+
just `401` (it is bound to a non-extractable in-browser key and needs a fresh
|
|
46
|
+
proof that can't leave the browser). So the cURL instead emits an
|
|
47
|
+
`X-API-Key: <your-api-key>` placeholder plus a comment pointing to
|
|
48
|
+
**Settings → API keys** — the supported way to script the API (Stripe/GitHub
|
|
49
|
+
model). See `@djangocfg/nextjs/@docs/DPOP.md`.
|
|
50
|
+
|
|
51
|
+
### DPoP error hint
|
|
52
|
+
|
|
53
|
+
A `401` whose body mentions DPoP (`dpop_proof_required` / `dpop_proof_invalid`)
|
|
54
|
+
gets an extra **dev-only** hint in the toast explaining it's a sender-constraint
|
|
55
|
+
failure, so it isn't mistaken for a plain auth error. End users never see the
|
|
56
|
+
internal proof wording (gated by `isDev`).
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- Env/feature flags come from the single source of truth: `isDev` from
|
|
61
|
+
`@djangocfg/ui-core/lib`, `dpopEnabled` from `@djangocfg/api`.
|
|
62
|
+
- Errors also flow to `@djangocfg/monitor` → `@djangocfg/debuger` panel for
|
|
63
|
+
inspection regardless of toast visibility.
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BaseApp - Core Providers Wrapper
|
|
3
3
|
*
|
|
4
|
+
* The canonical provider stack for ANY React host — it is framework-agnostic and
|
|
5
|
+
* does NOT depend on Next.js routing. Use it directly as the single top-level
|
|
6
|
+
* wrapper in environments that have no Next.js app router: **Wails desktop**,
|
|
7
|
+
* Electron, Vite/SPA, plain React. (In Next.js apps it's mounted once in the
|
|
8
|
+
* root `[locale]/layout.tsx`; per-route shells are chosen by native route-group
|
|
9
|
+
* `layout.tsx` files, not by this component.)
|
|
10
|
+
*
|
|
4
11
|
* Provides essential app-wide providers:
|
|
5
12
|
* - ThemeProvider (light/dark/system theme)
|
|
6
13
|
* - TooltipProvider (tooltip positioning)
|
|
@@ -54,7 +61,6 @@ import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
|
|
|
54
61
|
import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
|
|
55
62
|
import { AnalyticsProvider } from '../../snippets/Analytics';
|
|
56
63
|
import { AuthDialog } from '../../snippets/AuthDialog';
|
|
57
|
-
import { A2HSHint, PWAPageResumeManager, PwaProvider } from '@djangocfg/ui-nextjs/pwa';
|
|
58
64
|
|
|
59
65
|
import type { BaseLayoutProps } from '../types/layout.types';
|
|
60
66
|
import { LayoutI18nProvider } from './LayoutI18nProvider';
|
|
@@ -71,8 +77,7 @@ export type BaseAppProps = BaseLayoutProps;
|
|
|
71
77
|
* BaseApp - Core providers wrapper for any React/Next.js app
|
|
72
78
|
*
|
|
73
79
|
* Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, AnalyticsProvider,
|
|
74
|
-
* CentrifugoProvider,
|
|
75
|
-
* ErrorBoundary (optional)
|
|
80
|
+
* CentrifugoProvider, ErrorTrackingProvider, ErrorBoundary (optional)
|
|
76
81
|
* Also renders global Toaster and PageProgress components
|
|
77
82
|
*/
|
|
78
83
|
export function BaseApp({
|
|
@@ -85,7 +90,6 @@ export function BaseApp({
|
|
|
85
90
|
errorTracking,
|
|
86
91
|
errorBoundary,
|
|
87
92
|
swr,
|
|
88
|
-
pwaInstall,
|
|
89
93
|
monitor,
|
|
90
94
|
debug,
|
|
91
95
|
i18n,
|
|
@@ -97,10 +101,6 @@ export function BaseApp({
|
|
|
97
101
|
// ErrorBoundary is enabled by default
|
|
98
102
|
const enableErrorBoundary = errorBoundary?.enabled !== false;
|
|
99
103
|
|
|
100
|
-
// Check if PWA Install is enabled
|
|
101
|
-
const pwaInstallEnabled = pwaInstall?.enabled === true;
|
|
102
|
-
const showInstallHint = pwaInstallEnabled && pwaInstall?.showInstallHint !== false;
|
|
103
|
-
|
|
104
104
|
// Centrifugo configuration
|
|
105
105
|
const centrifugoUrl = centrifugo?.url || process.env.NEXT_PUBLIC_CENTRIFUGO_URL;
|
|
106
106
|
const centrifugoEnabled = centrifugo?.enabled !== false;
|
|
@@ -150,51 +150,35 @@ export function BaseApp({
|
|
|
150
150
|
return result.data.token;
|
|
151
151
|
}}
|
|
152
152
|
>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{i18n
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
children
|
|
167
|
-
)}
|
|
168
|
-
</LayoutI18nProvider>
|
|
169
|
-
<NextTopLoader
|
|
170
|
-
color="var(--primary)"
|
|
171
|
-
height={3}
|
|
172
|
-
showSpinner={false}
|
|
173
|
-
shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
|
|
174
|
-
/>
|
|
175
|
-
|
|
176
|
-
{/* PWA Install Hint */}
|
|
177
|
-
{showInstallHint && (
|
|
178
|
-
<A2HSHint
|
|
179
|
-
resetAfterDays={pwaInstall?.resetAfterDays}
|
|
180
|
-
delayMs={pwaInstall?.delayMs}
|
|
181
|
-
logo={pwaInstall?.logo}
|
|
182
|
-
/>
|
|
183
|
-
)}
|
|
184
|
-
|
|
185
|
-
{/* PWA Page Resume Manager */}
|
|
186
|
-
{pwaInstallEnabled && pwaInstall?.resumeLastPage && (
|
|
187
|
-
<PWAPageResumeManager enabled={true} />
|
|
153
|
+
<ErrorTrackingProvider
|
|
154
|
+
validation={errorTracking?.validation}
|
|
155
|
+
cors={errorTracking?.cors}
|
|
156
|
+
network={errorTracking?.network}
|
|
157
|
+
onError={errorTracking?.onError}
|
|
158
|
+
onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
|
|
159
|
+
>
|
|
160
|
+
<MonitorProvider {...monitorConfig} />
|
|
161
|
+
<LayoutI18nProvider value={i18n}>
|
|
162
|
+
{i18n?.routing ? (
|
|
163
|
+
<NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
|
|
164
|
+
) : (
|
|
165
|
+
children
|
|
188
166
|
)}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
167
|
+
</LayoutI18nProvider>
|
|
168
|
+
<NextTopLoader
|
|
169
|
+
color="var(--primary)"
|
|
170
|
+
height={3}
|
|
171
|
+
showSpinner={false}
|
|
172
|
+
shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
{/* Auth Dialog - only when auth is enabled */}
|
|
176
|
+
{authEnabled && <AuthDialog authPath={authConfig?.routes?.auth} />}
|
|
177
|
+
|
|
178
|
+
{/* Debug Panel — auto in dev, ?debug=1 in prod, disable with debug={{ enabled: false }} */}
|
|
179
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
180
|
+
<DebugButton enabled={debugEnabled} {...(debugProps as any)} />
|
|
181
|
+
</ErrorTrackingProvider>
|
|
198
182
|
</CentrifugoProvider>
|
|
199
183
|
</AnalyticsProvider>
|
|
200
184
|
</AuthProvider>
|
|
@@ -1,88 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# BaseApp — provider stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`BaseApp` is the **single, framework-agnostic provider wrapper** for any React
|
|
4
|
+
host. It mounts theme, auth, i18n, SWR, error tracking, monitor, the debug
|
|
5
|
+
panel, and the router adapter — everything an app needs *above* the page tree.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
There is **no `AppLayout` layout-router** anymore. Picking which shell
|
|
8
|
+
(public / private / admin) wraps a route is the job of **native Next.js
|
|
9
|
+
route-group `layout.tsx` files**, not a runtime path-matcher. This is simpler,
|
|
10
|
+
RSC-friendly, and supports per-segment `loading.tsx` / `error.tsx`.
|
|
11
|
+
|
|
12
|
+
## Where it goes
|
|
13
|
+
|
|
14
|
+
### Next.js — mount once at the app root
|
|
6
15
|
|
|
7
16
|
```tsx
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</AppLayout>
|
|
17
|
+
// app/[locale]/layout.tsx (or app/layout.tsx)
|
|
18
|
+
import { BaseApp } from '@djangocfg/layouts';
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({ children }) {
|
|
21
|
+
return (
|
|
22
|
+
<html><body>
|
|
23
|
+
<BaseApp
|
|
24
|
+
project="my-app"
|
|
25
|
+
theme={{ defaultTheme: 'system', storageKey: 'myapp-theme' }}
|
|
26
|
+
auth={{ apiUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
27
|
+
routes: { auth: '/auth', defaultCallback: '/private' } }}
|
|
28
|
+
// analytics / centrifugo / monitor / errorTracking / debug — all optional
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</BaseApp>
|
|
32
|
+
</body></html>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
Then each section owns its shell via a route-group layout:
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
```tsx
|
|
40
|
+
// app/[locale]/(pages)/private/layout.tsx
|
|
41
|
+
import { PrivateLayout } from '@djangocfg/layouts';
|
|
42
|
+
export default function PrivateRouteLayout({ children }) {
|
|
43
|
+
return <PrivateLayout sidebar={sidebar} header={header}>{children}</PrivateLayout>;
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
// app/[locale]/(marketing)/layout.tsx
|
|
47
|
+
import { PublicLayout } from '@djangocfg/layouts';
|
|
48
|
+
export default function PublicRouteLayout({ children }) {
|
|
49
|
+
return <PublicLayout navbar={<MyNavbar/>} footer={<MyFooter/>}>{children}</PublicLayout>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
`/auth/*` and other fullscreen pages just live outside those groups — no
|
|
54
|
+
`noLayoutPaths` config needed; they simply aren't wrapped.
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
> **Admin** is not a separate layout — it's `PrivateLayout` with a different
|
|
57
|
+
> `sidebar` config in `app/.../admin/layout.tsx`.
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
### Non-Next.js (Wails / Electron / Vite / plain React)
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{ path: '/', theme: 'dark' },
|
|
51
|
-
{ path: '/legal/**', theme: 'light' },
|
|
52
|
-
]
|
|
53
|
-
```
|
|
61
|
+
`BaseApp` does not depend on Next routing — use it directly as the top-level
|
|
62
|
+
wrapper. This is exactly how the Wails desktop app mounts the framework.
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
```tsx
|
|
65
|
+
import { BaseApp } from '@djangocfg/layouts';
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
export function App() {
|
|
68
|
+
return <BaseApp project="desktop" theme={{ defaultTheme: 'dark' }}>{routes}</BaseApp>;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
58
71
|
|
|
59
|
-
##
|
|
72
|
+
## Config surface
|
|
60
73
|
|
|
61
74
|
```ts
|
|
62
|
-
interface
|
|
63
|
-
layouts?: {
|
|
64
|
-
public?: { component; enabledPath? };
|
|
65
|
-
private?: { component; enabledPath? };
|
|
66
|
-
admin?: { component; enabledPath? };
|
|
67
|
-
noLayoutPaths?: string | string[];
|
|
68
|
-
authPath?: string; // default '/auth' — always fullscreen
|
|
69
|
-
publicChrome?: AppLayoutPublicChrome; // navbar/footer defaults
|
|
70
|
-
themeOverrides?: ThemeOverrideRule[];
|
|
71
|
-
};
|
|
72
|
-
baseApp?: {
|
|
73
|
-
project?; theme?; auth?; analytics?; centrifugo?;
|
|
74
|
-
errorTracking?; errorBoundary?; swr?; pwaInstall?;
|
|
75
|
-
monitor?; debug?;
|
|
76
|
-
};
|
|
77
|
-
i18n?: I18nLayoutConfig;
|
|
75
|
+
interface BaseAppProps {
|
|
78
76
|
children: ReactNode;
|
|
77
|
+
project?: string; // monitor.project + debug panel title default
|
|
78
|
+
theme?: ThemeConfig; // defaultTheme, storageKey, style preset
|
|
79
|
+
auth?: AuthConfig | false; // false = no AuthProvider (fully public app)
|
|
80
|
+
analytics?: AnalyticsConfig; // Google Analytics
|
|
81
|
+
centrifugo?: CentrifugoConfig;
|
|
82
|
+
errorTracking?: ErrorTrackingConfig;
|
|
83
|
+
errorBoundary?: ErrorBoundaryConfig; // enabled by default
|
|
84
|
+
swr?: SWRConfigOptions;
|
|
85
|
+
monitor?: MonitorConfig;
|
|
86
|
+
debug?: DebugConfig; // debug panel (auto in dev / ?debug=1 in prod)
|
|
87
|
+
i18n?: I18nLayoutConfig; // locale + routing for the link bridge
|
|
79
88
|
}
|
|
80
89
|
```
|
|
81
90
|
|
|
82
|
-
|
|
91
|
+
## Per-route forced theme (optional)
|
|
92
|
+
|
|
93
|
+
To force a theme on a subtree (e.g. an always-dark marketing landing) without
|
|
94
|
+
locking the whole app, wrap that route-group's layout with `<ThemeOverride>` /
|
|
95
|
+
`<ForceTheme>` (re-exported here from `@djangocfg/ui-core/theme`). Because shell
|
|
96
|
+
selection is now per route-group, the override lives right where the route does.
|
|
83
97
|
|
|
84
98
|
## Related
|
|
85
99
|
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
|
|
100
|
+
- `PrivateLayout` / `PublicLayout` — the per-section shells (see their READMEs).
|
|
101
|
+
- `ErrorLayout` (`@djangocfg/layouts/components`) — universal error page for
|
|
102
|
+
`error.tsx` / `not-found.tsx`.
|
|
103
|
+
- `ForceTheme` / `ThemeOverride` (`@djangocfg/ui-core/theme`) — theme primitives.
|