@djangocfg/layouts 2.1.456 → 2.1.459

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
@@ -136,7 +136,7 @@ When `routing` is set, `BaseApp` mounts a locale-aware `<Link>` adapter so every
136
136
 
137
137
  ### `MonitorBoundary`
138
138
 
139
- Error boundary that automatically reports caught errors to `@djangocfg/monitor`. Use at page/layout level so unexpected render errors land in your backend dashboard.
139
+ Error boundary that automatically reports caught errors to `@djangocfg/devtools`. Use at page/layout level so unexpected render errors land in your backend dashboard.
140
140
 
141
141
  ```tsx
142
142
  import { MonitorBoundary } from '@djangocfg/layouts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.456",
3
+ "version": "2.1.459",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,12 +89,10 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.456",
93
- "@djangocfg/centrifugo": "^2.1.456",
94
- "@djangocfg/debuger": "^2.1.456",
95
- "@djangocfg/i18n": "^2.1.456",
96
- "@djangocfg/monitor": "^2.1.456",
97
- "@djangocfg/ui-core": "^2.1.456",
92
+ "@djangocfg/api": "^2.1.459",
93
+ "@djangocfg/centrifugo": "^2.1.459",
94
+ "@djangocfg/i18n": "^2.1.459",
95
+ "@djangocfg/ui-core": "^2.1.459",
98
96
  "@hookform/resolvers": "^5.2.2",
99
97
  "consola": "^3.4.2",
100
98
  "lucide-react": "^0.545.0",
@@ -110,10 +108,11 @@
110
108
  "tailwindcss": "^4.1.18",
111
109
  "tailwindcss-animate": "^1.0.7",
112
110
  "zod": "^4.3.6",
113
- "zustand": "^5.0.0"
111
+ "zustand": "^5.0.0",
112
+ "@djangocfg/devtools": "^2.1.459"
114
113
  },
115
114
  "peerDependenciesMeta": {
116
- "@djangocfg/monitor": {
115
+ "@djangocfg/devtools": {
117
116
  "optional": true
118
117
  }
119
118
  },
@@ -125,21 +124,20 @@
125
124
  "uuid": "^11.1.0"
126
125
  },
127
126
  "devDependencies": {
128
- "@djangocfg/api": "^2.1.456",
129
- "@djangocfg/centrifugo": "^2.1.456",
130
- "@djangocfg/debuger": "^2.1.456",
131
- "@djangocfg/i18n": "^2.1.456",
132
- "@djangocfg/monitor": "^2.1.456",
133
- "@djangocfg/typescript-config": "^2.1.456",
134
- "@djangocfg/ui-core": "^2.1.456",
135
- "@djangocfg/ui-tools": "^2.1.456",
127
+ "@djangocfg/api": "^2.1.459",
128
+ "@djangocfg/centrifugo": "^2.1.459",
129
+ "@djangocfg/i18n": "^2.1.459",
130
+ "@djangocfg/typescript-config": "^2.1.459",
131
+ "@djangocfg/ui-core": "^2.1.459",
132
+ "@djangocfg/ui-tools": "^2.1.459",
136
133
  "@types/node": "^25.2.3",
137
134
  "@types/react": "^19.2.15",
138
135
  "@types/react-dom": "^19.2.3",
139
136
  "eslint": "^9.37.0",
140
137
  "next-intl": "^4.9.1",
141
138
  "typescript": "^5.9.3",
142
- "zustand": "^5.0.9"
139
+ "zustand": "^5.0.9",
140
+ "@djangocfg/devtools": "^2.1.459"
143
141
  },
144
142
  "publishConfig": {
145
143
  "access": "public"
@@ -1,5 +1,4 @@
1
1
  'use client';
2
- import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
3
2
 
4
3
  import { useEffect } from 'react';
5
4
 
@@ -47,19 +46,20 @@ export interface RedirectPageProps {
47
46
  */
48
47
  export function RedirectPage({
49
48
  authenticatedPath = '/private',
50
- unauthenticatedPath = DEFAULT_AUTH_PATH,
49
+ unauthenticatedPath,
51
50
  loadingText = 'Loading...',
52
51
  }: RedirectPageProps) {
53
- const { isAuthenticated } = useAuth();
52
+ const { isAuthenticated, routes } = useAuth();
54
53
  const router = useCfgRouter();
54
+ const resolvedUnauthPath = unauthenticatedPath ?? routes.auth;
55
55
 
56
56
  useEffect(() => {
57
57
  if (!isAuthenticated) {
58
- router.hardPush(unauthenticatedPath);
58
+ router.hardPush(resolvedUnauthPath);
59
59
  } else {
60
60
  router.hardPush(authenticatedPath);
61
61
  }
62
- }, [isAuthenticated, router, authenticatedPath, unauthenticatedPath]);
62
+ }, [isAuthenticated, router, authenticatedPath, resolvedUnauthPath]);
63
63
 
64
64
  return (
65
65
  <Preloader
@@ -50,7 +50,7 @@ export {
50
50
  formatRuntimeErrorForClipboard,
51
51
  formatErrorTitle,
52
52
  extractDomain,
53
- errorDetailToMonitorEvent,
53
+ errorDetailToDevtoolsEvent,
54
54
  } from './utils/formatters';
55
55
 
56
56
  export {
@@ -31,7 +31,7 @@ import React, {
31
31
  } from 'react';
32
32
 
33
33
  import { toast } from '@djangocfg/ui-core/hooks';
34
- import { useDebugMode, isProduction } from '@djangocfg/monitor/client';
34
+ import { useDebugMode, isProduction } from '@djangocfg/devtools';
35
35
 
36
36
  import { createErrorToast } from '../components/ErrorToast';
37
37
  import { generateShortErrorId, getProductionToast } from '../utils/formatters';
@@ -171,7 +171,7 @@ export interface ErrorTrackingProviderProps {
171
171
  centrifugo?: Partial<CentrifugoErrorConfig>;
172
172
  runtime?: Partial<RuntimeErrorConfig>;
173
173
  onError?: (error: ErrorDetail) => boolean | void;
174
- /** Called with the raw ErrorDetail for every tracked error. Wire to FrontendMonitor.capture() (via toMonitorEvent) to forward errors to the backend. */
174
+ /** Called with the raw ErrorDetail for every tracked error. Wire to devtools.capture() (via errorDetailToDevtoolsEvent) to forward errors to the backend. */
175
175
  onMonitorCapture?: (detail: ErrorDetail) => void;
176
176
  }
177
177
 
@@ -6,8 +6,8 @@
6
6
 
7
7
  import type { ZodError } from 'zod';
8
8
  import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail, CentrifugoErrorDetail, RuntimeErrorDetail, ErrorDetail } from '../types';
9
- import type { MonitorEvent } from '@djangocfg/monitor';
10
- import { EventType, EventLevel } from '@djangocfg/monitor';
9
+ import type { DevtoolsEvent } from '@djangocfg/devtools';
10
+ import { EventType, EventLevel } from '@djangocfg/devtools';
11
11
 
12
12
  /**
13
13
  * Safely read Zod issues. The error toast must never crash itself, so we
@@ -148,10 +148,10 @@ export function formatRuntimeErrorForClipboard(detail: RuntimeErrorDetail): stri
148
148
  }
149
149
 
150
150
  /**
151
- * Convert ErrorDetail to MonitorEvent for forwarding to @djangocfg/monitor backend.
151
+ * Convert ErrorDetail to DevtoolsEvent for forwarding to the backend ingest.
152
152
  * Use via ErrorTrackingProvider.onMonitorCapture.
153
153
  */
154
- export function errorDetailToMonitorEvent(detail: ErrorDetail): MonitorEvent {
154
+ export function errorDetailToDevtoolsEvent(detail: ErrorDetail): DevtoolsEvent {
155
155
  const url = typeof window !== 'undefined' ? window.location.href : ''
156
156
  switch (detail.type) {
157
157
  case 'validation':
@@ -5,12 +5,12 @@ import type {
5
5
  BoundaryProps,
6
6
  BoundaryResetDetails,
7
7
  } from '@djangocfg/ui-core/components';
8
- import { EventLevel, EventType, FrontendMonitor } from '@djangocfg/monitor/client';
8
+ import { EventLevel, EventType, devtools } from '@djangocfg/devtools';
9
9
  import { useCallback } from 'react';
10
10
  import type { ErrorInfo, ReactNode } from 'react';
11
11
 
12
12
  export interface MonitorBoundaryProps extends Omit<BoundaryProps, 'onError' | 'onReset'> {
13
- /** Forwarded to FrontendMonitor as `extra.source` so you can filter events by boundary. */
13
+ /** Forwarded to devtools as `extra.source` so you can filter events by boundary. */
14
14
  name?: string;
15
15
  /** Additional render-error handler called alongside the monitor capture. */
16
16
  onError?: (error: Error, info: ErrorInfo) => void;
@@ -27,7 +27,7 @@ function currentUrl(): string | undefined {
27
27
 
28
28
  function reportError(name: string | undefined, error: Error, info: ErrorInfo): void {
29
29
  try {
30
- FrontendMonitor.capture({
30
+ devtools.capture({
31
31
  event_type: EventType.ERROR,
32
32
  level: EventLevel.ERROR,
33
33
  message: error.message || 'Unhandled React render error',
@@ -45,7 +45,7 @@ function reportError(name: string | undefined, error: Error, info: ErrorInfo): v
45
45
 
46
46
  function reportReset(name: string | undefined, details: BoundaryResetDetails): void {
47
47
  try {
48
- FrontendMonitor.capture({
48
+ devtools.capture({
49
49
  event_type: EventType.ERROR,
50
50
  level: EventLevel.INFO,
51
51
  message: `Boundary recovered (${details.reason})`,
@@ -61,7 +61,7 @@ function reportReset(name: string | undefined, details: BoundaryResetDetails): v
61
61
  }
62
62
 
63
63
  /**
64
- * `Boundary` + automatic reporting to `@djangocfg/monitor`.
64
+ * `Boundary` + automatic reporting to `@djangocfg/devtools`.
65
65
  *
66
66
  * Use at page/layout level so unexpected render errors are captured in your backend.
67
67
  * For widgets you don't want to report (chat, embeds), use `Boundary` from ui-core directly.
@@ -12,7 +12,7 @@ All boundaries here build on the single `Boundary` primitive in
12
12
  | File | What it does |
13
13
  |------|--------------|
14
14
  | `ErrorBoundary.tsx` | Thin wrapper over ui-core `Boundary` — i18n fallback + support email (for direct use; `BaseApp` no longer wraps it). |
15
- | `MonitorBoundary.tsx` | ui-core `Boundary` that also reports to `@djangocfg/monitor`. |
15
+ | `MonitorBoundary.tsx` | ui-core `Boundary` that also reports to `@djangocfg/devtools`. |
16
16
  | `ErrorLayout.tsx` | Full-page error layout (`getErrorContent` / `ERROR_CODES`). |
17
17
  | `ErrorsTracker/` | Global runtime tracker — listens for error CustomEvents and shows toasts. |
18
18
 
@@ -63,5 +63,5 @@ internal proof wording (gated by `isDev`).
63
63
 
64
64
  - Env/feature flags come from the single source of truth: `isDev` from
65
65
  `@djangocfg/ui-core/lib`, `dpopEnabled` from `@djangocfg/api`.
66
- - Errors also flow to `@djangocfg/monitor` `@djangocfg/debuger` panel for
66
+ - Errors also flow to `@djangocfg/devtools` (backend ingest + debug panel) for
67
67
  inspection regardless of toast visibility.
@@ -47,8 +47,8 @@ import { nodeEnv } from "@djangocfg/ui-core/lib";
47
47
  import NextTopLoader from 'nextjs-toploader';
48
48
  import { SWRConfig } from 'swr';
49
49
 
50
- import { MonitorProvider, FrontendMonitor } from '@djangocfg/monitor/client';
51
- import { errorDetailToMonitorEvent } from '../../components/errors/ErrorsTracker';
50
+ import { Devtools, devtools } from '@djangocfg/devtools';
51
+ import { errorDetailToDevtoolsEvent } from '../../components/errors/ErrorsTracker';
52
52
  import { CfgCentrifugo } from '@djangocfg/api';
53
53
  import { AuthProvider } from '@djangocfg/api/auth';
54
54
  import { CentrifugoProvider } from '@djangocfg/centrifugo';
@@ -67,9 +67,6 @@ import type { BaseLayoutProps } from '../types/layout.types';
67
67
  import { LayoutI18nProvider } from './LayoutI18nProvider';
68
68
 
69
69
  import React from 'react';
70
- const DebugButton = React.lazy(() =>
71
- import('@djangocfg/debuger').then((m) => ({ default: m.DebugButton }))
72
- );
73
70
 
74
71
  // For backwards compatibility, re-export as BaseAppProps
75
72
  export type BaseAppProps = BaseLayoutProps;
@@ -136,8 +133,7 @@ export function BaseApp({
136
133
  errorTracking,
137
134
  errorBoundary,
138
135
  swr,
139
- monitor,
140
- debug,
136
+ devtools: devtoolsConfig,
141
137
  i18n,
142
138
  }: BaseAppProps) {
143
139
  // auth=false disables AuthProvider entirely (public apps with no login)
@@ -151,15 +147,12 @@ export function BaseApp({
151
147
  const centrifugoUrl = centrifugo?.url || process.env.NEXT_PUBLIC_CENTRIFUGO_URL;
152
148
  const centrifugoEnabled = centrifugo?.enabled !== false;
153
149
 
154
- // Debug panel always mounted, DebugButton handles visibility itself
155
- const { enabled: debugEnabled, ...debugProps } = debug ?? {};
156
-
157
-
158
- // Monitor — project prop as default, override with monitor config
159
- const monitorConfig = {
150
+ // Devtoolscapture + ingest + debug panel in one component.
151
+ // Project prop is the default; the devtools prop overrides field by field.
152
+ const devtoolsProps = {
160
153
  project,
161
154
  environment: nodeEnv,
162
- ...monitor,
155
+ ...devtoolsConfig,
163
156
  };
164
157
 
165
158
  const content = (
@@ -215,9 +208,9 @@ export function BaseApp({
215
208
  cors={errorTracking?.cors}
216
209
  network={errorTracking?.network}
217
210
  onError={errorTracking?.onError}
218
- onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
211
+ onMonitorCapture={(d) => devtools.capture(errorDetailToDevtoolsEvent(d))}
219
212
  >
220
- <MonitorProvider {...monitorConfig} />
213
+ <Devtools {...devtoolsProps} />
221
214
  <LayoutI18nProvider value={i18n}>
222
215
  {i18n?.routing ? (
223
216
  <NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
@@ -234,10 +227,6 @@ export function BaseApp({
234
227
 
235
228
  {/* Auth Dialog - only when auth is enabled */}
236
229
  {authEnabled && <AuthDialog authPath={authConfig?.routes?.auth} />}
237
-
238
- {/* Debug Panel — auto in dev, ?debug=1 in prod, disable with debug={{ enabled: false }} */}
239
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
240
- <DebugButton enabled={debugEnabled} {...(debugProps as any)} />
241
230
  </ErrorTrackingProvider>
242
231
  </CentrifugoProvider>
243
232
  </AnalyticsProvider>
@@ -12,7 +12,7 @@
12
12
 
13
13
  import React, { createContext, memo, useContext, useMemo } from 'react';
14
14
 
15
- import { useCfgRouter } from '@djangocfg/api/auth';
15
+ import { consumeSavedRedirect, useAuth, useCfgRouter } from '@djangocfg/api/auth';
16
16
  import { useAppT } from '@djangocfg/i18n';
17
17
 
18
18
  import { Suspense } from '../../components';
@@ -49,7 +49,9 @@ export const AuthLayout: React.FC<AuthLayoutProps> = (props) => {
49
49
  background,
50
50
  sidebar,
51
51
  enableGithubAuth,
52
- redirectUrl = AUTH.DEFAULT_REDIRECT,
52
+ // No default here — an unset prop falls through to the saved back-url /
53
+ // resolved routes.defaultCallback chain at navigation time (AuthSuccess).
54
+ redirectUrl,
53
55
  onOAuthSuccess,
54
56
  onError,
55
57
  className,
@@ -143,6 +145,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
143
145
  redirectDelay = AUTH.REDIRECT_DELAY,
144
146
  }) => {
145
147
  const router = useCfgRouter();
148
+ const { routes } = useAuth();
146
149
  const t = useAppT();
147
150
  const [isVisible, setIsVisible] = React.useState(false);
148
151
 
@@ -151,7 +154,9 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
151
154
  React.useEffect(() => {
152
155
  const animTimer = setTimeout(() => setIsVisible(true), AUTH.ANIMATION_START_DELAY);
153
156
  const redirectTimer = setTimeout(() => {
154
- const finalUrl = redirectUrl || AUTH.DEFAULT_REDIRECT;
157
+ // Priority: saved back-url (where the guard bounced the user from,
158
+ // single-use) > the app's static redirectUrl prop > resolved default.
159
+ const finalUrl = consumeSavedRedirect() || redirectUrl || routes.defaultCallback;
155
160
  router.hardPush(finalUrl);
156
161
  }, redirectDelay);
157
162
 
@@ -159,7 +164,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = memo(({
159
164
  clearTimeout(animTimer);
160
165
  clearTimeout(redirectTimer);
161
166
  };
162
- }, [redirectUrl, redirectDelay, router]);
167
+ }, [redirectUrl, redirectDelay, router, routes.defaultCallback]);
163
168
 
164
169
  return (
165
170
  <div className="auth-success-overlay">
@@ -1,11 +1,10 @@
1
1
  'use client';
2
- import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
3
2
 
4
3
  import { AlertCircle, Loader2 } from 'lucide-react';
5
4
  import { useSearchParams } from 'next/navigation';
6
5
  import React, { memo, useEffect, useState } from 'react';
7
6
 
8
- import { useGithubAuth } from '@djangocfg/api/auth';
7
+ import { useAuth, useGithubAuth } from '@djangocfg/api/auth';
9
8
  import {
10
9
  Card, CardContent, CardDescription, CardHeader, CardTitle
11
10
  } from '@djangocfg/ui-core/components';
@@ -59,6 +58,7 @@ function OAuthCallbackRaw({
59
58
  redirectUrl,
60
59
  }: OAuthCallbackProps) {
61
60
  const searchParams = useSearchParams();
61
+ const { routes } = useAuth();
62
62
  const { setStep, setTwoFactorSessionId, setShouldPrompt2FA } = useAuthFormContext();
63
63
  const [status, setStatus] = useState<CallbackStatus | null>(null);
64
64
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -166,7 +166,7 @@ function OAuthCallbackRaw({
166
166
  {status === 'error' && (
167
167
  <CardContent className="text-center">
168
168
  <a
169
- href={DEFAULT_AUTH_PATH}
169
+ href={routes.auth}
170
170
  className="text-primary hover:underline text-sm"
171
171
  >
172
172
  Try again
@@ -2,8 +2,9 @@
2
2
  * AuthLayout Constants
3
3
  *
4
4
  * Code lengths come from the shared single source of truth in
5
- * `@djangocfg/api/auth` (AUTH_CONSTANTS). Layout-only values (timings, sizes,
6
- * routes) live here.
5
+ * `@djangocfg/api/auth` (AUTH_CONSTANTS). Layout-only values (timings, sizes)
6
+ * live here. Routes deliberately do NOT — they come resolved from
7
+ * `useAuth().routes` so an app's AuthConfig override is honored everywhere.
7
8
  */
8
9
 
9
10
  import { AUTH_CONSTANTS } from '@djangocfg/api/auth';
@@ -23,7 +24,4 @@ export const AUTH = {
23
24
 
24
25
  // Sizes (px)
25
26
  QR_CODE_SIZE: 160,
26
-
27
- // Routes
28
- DEFAULT_REDIRECT: '/dashboard',
29
27
  } as const;
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  'use client';
8
- import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
9
8
 
10
9
  import { useEffect, useState } from 'react';
11
10
  import { useRouter } from 'next/navigation';
@@ -19,6 +18,7 @@ import {
19
18
 
20
19
  interface UseAuthGuardOptions {
21
20
  requireAuth?: boolean;
21
+ /** Explicit override; defaults to the app's resolved `useAuth().routes.auth`. */
22
22
  authPath?: string;
23
23
  /**
24
24
  * Safety net: if auth is still "loading" this many ms after mount, stop
@@ -40,10 +40,11 @@ interface UseAuthGuardResult {
40
40
 
41
41
  export function useAuthGuard({
42
42
  requireAuth = true,
43
- authPath = DEFAULT_AUTH_PATH,
43
+ authPath,
44
44
  loadingTimeoutMs = 8000,
45
45
  }: UseAuthGuardOptions): UseAuthGuardResult {
46
- const { isAuthenticated, isLoading: authLoading, saveRedirectUrl } = useAuth();
46
+ const { isAuthenticated, isLoading: authLoading, saveRedirectUrl, routes } = useAuth();
47
+ const resolvedAuthPath = authPath ?? routes.auth;
47
48
  const router = useRouter();
48
49
  const [isRedirecting, setIsRedirecting] = useState(false);
49
50
 
@@ -84,10 +85,10 @@ export function useAuthGuard({
84
85
  const currentUrl = window.location.pathname + window.location.search;
85
86
  saveRedirectUrl(currentUrl);
86
87
  setIsRedirecting(true);
87
- router.push(authPath);
88
+ router.push(resolvedAuthPath);
88
89
  }
89
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
- }, [mounted, requireAuth, isAuthenticated, effectiveAuthLoading, isRedirecting, router, saveRedirectUrl, authPath]);
91
+ }, [mounted, requireAuth, isAuthenticated, effectiveAuthLoading, isRedirecting, router, saveRedirectUrl, resolvedAuthPath]);
91
92
 
92
93
  const isLoading = resolveGuardIsLoading(guardState);
93
94
  const loadingText = isRedirecting ? 'Redirecting to login...' : 'Authenticating...';
@@ -29,7 +29,6 @@
29
29
  */
30
30
 
31
31
  'use client';
32
- import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
33
32
 
34
33
  import { ArrowRight, Languages, LogOut } from 'lucide-react';
35
34
  import { Link } from '@djangocfg/ui-core/components';
@@ -70,11 +69,12 @@ export interface UserMenuProps {
70
69
  export function UserMenu({
71
70
  variant = 'desktop',
72
71
  groups,
73
- authPath = DEFAULT_AUTH_PATH,
72
+ authPath,
74
73
  disableAuth = false,
75
74
  i18n,
76
75
  }: UserMenuProps) {
77
- const { user, isAuthenticated } = useAuth();
76
+ const { user, isAuthenticated, routes } = useAuth();
77
+ const resolvedAuthPath = authPath ?? routes.auth;
78
78
  const handleLogout = useLogout();
79
79
  const [mounted, setMounted] = React.useState(false);
80
80
  const t = useAppT();
@@ -129,7 +129,7 @@ export function UserMenu({
129
129
  return (
130
130
  <div className="pt-4 border-t border-border/50">
131
131
  <Link
132
- href={authPath}
132
+ href={resolvedAuthPath}
133
133
  className="group flex items-center justify-between rounded-full px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
134
134
  >
135
135
  <span>{labels.signIn}</span>
@@ -146,7 +146,7 @@ export function UserMenu({
146
146
  );
147
147
  }
148
148
  return (
149
- <Link href={authPath}>
149
+ <Link href={resolvedAuthPath}>
150
150
  <Button variant="default" size="sm" className="rounded-full px-5">
151
151
  {labels.signIn}
152
152
  </Button>
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  'use client';
9
- import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
10
9
 
11
10
  import { ArrowRight } from 'lucide-react';
12
11
  import React, { useMemo } from 'react';
@@ -48,7 +47,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
48
47
  const navigation = props.navigation ?? [];
49
48
  const userMenu = props.userMenu;
50
49
 
51
- const { isAuthenticated, user } = useAuth();
50
+ const { isAuthenticated, user, routes } = useAuth();
52
51
  const pathname = usePathnameWithoutLocale();
53
52
  const t = useAppT();
54
53
  const { mounted, visible } = useMobileNavPanel({
@@ -212,7 +211,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
212
211
  {showSignInFooter && (
213
212
  <div className="shrink-0 border-t border-border/50 p-4">
214
213
  <Link
215
- href={userMenu?.authPath || DEFAULT_AUTH_PATH}
214
+ href={userMenu?.authPath || routes.auth}
216
215
  onClick={closeMobileMenu}
217
216
  className="block"
218
217
  >
@@ -9,12 +9,6 @@ import {
9
9
  AlertDescription,
10
10
  Badge,
11
11
  Button,
12
- Dialog,
13
- DialogContent,
14
- DialogDescription,
15
- DialogFooter,
16
- DialogHeader,
17
- DialogTitle,
18
12
  OTPInput,
19
13
  Preloader,
20
14
  SettingRow,
@@ -49,69 +43,62 @@ function StatusBadge({ enabled }: { enabled: boolean }) {
49
43
  }
50
44
 
51
45
  // ─────────────────────────────────────────────────────────────────────────────
52
- // Disable dialog
46
+ // Disable confirmation — inline, not a dialog. Settings already lives in a
47
+ // dialog; stacking a second modal on top fights its focus trap and dims the
48
+ // context the user needs. Destructive confirmation expands in place instead
49
+ // (progressive disclosure), so the device list stays visible while confirming.
53
50
  // ─────────────────────────────────────────────────────────────────────────────
54
51
 
55
- function DisableDialog({
56
- open,
52
+ function DisableConfirm({
57
53
  isLoading,
58
54
  error,
59
55
  onConfirm,
60
56
  onCancel,
61
57
  }: {
62
- open: boolean;
63
58
  isLoading: boolean;
64
59
  error: string | null;
65
60
  onConfirm: (code: string) => void;
66
61
  onCancel: () => void;
67
62
  }) {
63
+ // Mounted fresh each time the confirmation opens — state starts clean.
68
64
  const [code, setCode] = useState('');
69
65
 
70
- // Reset on open
71
- useEffect(() => {
72
- if (open) setCode('');
73
- }, [open]);
74
-
75
66
  return (
76
- <Dialog open={open} onOpenChange={(v) => !v && onCancel()}>
77
- <DialogContent className="sm:max-w-sm">
78
- <DialogHeader>
79
- <DialogTitle className="flex items-center gap-2">
80
- <ShieldOff className="w-5 h-5 text-destructive" />
81
- Disable Two-Factor Authentication
82
- </DialogTitle>
83
- <DialogDescription>
67
+ <div className="mt-3 space-y-4 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
68
+ <div className="flex gap-2.5">
69
+ <ShieldOff className="mt-0.5 h-4 w-4 flex-shrink-0 text-destructive" />
70
+ <div className="space-y-1">
71
+ <p className="text-sm font-medium">Disable two-factor authentication?</p>
72
+ <p className="text-[13px] leading-relaxed text-muted-foreground">
84
73
  Enter the 6-digit code from your authenticator app to confirm.
85
74
  This will make your account less secure.
86
- </DialogDescription>
87
- </DialogHeader>
88
-
89
- <div className="py-2 space-y-4">
90
- {error && (
91
- <Alert variant="destructive">
92
- <AlertDescription>{error}</AlertDescription>
93
- </Alert>
94
- )}
95
- <div className="flex justify-center">
96
- <OTPInput
97
- length={6}
98
- validationMode="numeric"
99
- pasteBehavior="clean"
100
- value={code}
101
- onChange={setCode}
102
- disabled={isLoading}
103
- autoFocus
104
- size="lg"
105
- />
106
- </div>
75
+ </p>
107
76
  </div>
77
+ </div>
78
+
79
+ {error && (
80
+ <Alert variant="destructive">
81
+ <AlertDescription>{error}</AlertDescription>
82
+ </Alert>
83
+ )}
108
84
 
109
- <DialogFooter>
110
- <Button variant="outline" onClick={onCancel} disabled={isLoading}>
85
+ <div className="flex flex-wrap items-center justify-between gap-3">
86
+ <OTPInput
87
+ length={6}
88
+ validationMode="numeric"
89
+ pasteBehavior="clean"
90
+ value={code}
91
+ onChange={setCode}
92
+ disabled={isLoading}
93
+ autoFocus
94
+ />
95
+ <div className="flex gap-2">
96
+ <Button variant="outline" size="sm" onClick={onCancel} disabled={isLoading}>
111
97
  Cancel
112
98
  </Button>
113
99
  <Button
114
100
  variant="destructive"
101
+ size="sm"
115
102
  onClick={() => onConfirm(code)}
116
103
  disabled={isLoading || code.length !== 6}
117
104
  >
@@ -121,9 +108,9 @@ function DisableDialog({
121
108
  'Disable 2FA'
122
109
  )}
123
110
  </Button>
124
- </DialogFooter>
125
- </DialogContent>
126
- </Dialog>
111
+ </div>
112
+ </div>
113
+ </div>
127
114
  );
128
115
  }
129
116
 
@@ -224,7 +211,7 @@ export const TwoFactorSection: React.FC = () => {
224
211
  variant="outline"
225
212
  size="sm"
226
213
  onClick={() => { clearError(); setShowDisable(true); }}
227
- disabled={isLoading}
214
+ disabled={isLoading || showDisable}
228
215
  className="text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30"
229
216
  >
230
217
  Disable
@@ -245,6 +232,16 @@ export const TwoFactorSection: React.FC = () => {
245
232
  action={statusAction}
246
233
  />
247
234
 
235
+ {/* Inline destructive confirmation (no nested dialog) */}
236
+ {showDisable && (
237
+ <DisableConfirm
238
+ isLoading={isLoading}
239
+ error={error}
240
+ onConfirm={handleDisableConfirm}
241
+ onCancel={handleDisableCancel}
242
+ />
243
+ )}
244
+
248
245
  {/* Fetch error */}
249
246
  {error && !showDisable && (
250
247
  <div className="py-3">
@@ -279,14 +276,6 @@ export const TwoFactorSection: React.FC = () => {
279
276
  </SettingsBlock>
280
277
  )}
281
278
  </div>
282
-
283
- <DisableDialog
284
- open={showDisable}
285
- isLoading={isLoading}
286
- error={error}
287
- onConfirm={handleDisableConfirm}
288
- onCancel={handleDisableCancel}
289
- />
290
279
  </>
291
280
  );
292
281
  };
@@ -25,13 +25,16 @@ interface BuildBuiltinArgs {
25
25
  * are left as plain strings here (the inner section bodies pull localized copy
26
26
  * from ProfileProvider); only the nav-level label/title live at this layer.
27
27
  *
28
- * Defaults: account on, security off (needs API support), apiKeys on.
28
+ * Defaults: everything on. Security (2FA management) is a built-in the user
29
+ * must be able to find without the app opting in — `/cfg/totp/*` ships with
30
+ * every django-cfg backend. Apps that truly lack it opt out explicitly via
31
+ * `builtins.security: false`.
29
32
  */
30
33
  export function buildBuiltinSections({
31
34
  builtins,
32
35
  enableDeleteAccount,
33
36
  }: BuildBuiltinArgs): SettingsSection[] {
34
- const { account = true, security = false, apiKeys = true } = builtins;
37
+ const { account = true, security = true, apiKeys = true } = builtins;
35
38
  const sections: SettingsSection[] = [];
36
39
 
37
40
  if (account) {
@@ -53,7 +53,7 @@ export type {
53
53
 
54
54
  export type {
55
55
  BaseLayoutProps,
56
- DebugConfig,
56
+ DevtoolsConfig,
57
57
  I18nLayoutConfig,
58
58
  LayoutVisualVariant,
59
59
  LayoutVisualConfig,
@@ -22,7 +22,7 @@ export type NextIntlRouting = object & { __nextIntlRouting?: never };
22
22
 
23
23
  // Import local provider configs
24
24
  import type { ThemeConfig, SWRConfigOptions, CentrifugoConfig } from './providers.types';
25
- import type { MonitorConfig } from '@djangocfg/monitor';
25
+ import type { DevtoolsProps } from '@djangocfg/devtools';
26
26
 
27
27
  // ============================================================================
28
28
  // Base Layout Props
@@ -37,7 +37,7 @@ import type { MonitorConfig } from '@djangocfg/monitor';
37
37
  export interface BaseLayoutProps {
38
38
  children: ReactNode;
39
39
 
40
- /** Project name — used as default for monitor.project and debug panel title */
40
+ /** Project name — used as default for devtools.project */
41
41
  project?: string;
42
42
 
43
43
  /** Theme configuration */
@@ -61,11 +61,13 @@ export interface BaseLayoutProps {
61
61
  /** Error boundary configuration (from components/errors, enabled by default) */
62
62
  errorBoundary?: ErrorBoundaryConfig;
63
63
 
64
- /** Monitor configuration — initialises window.monitor + auto-captures JS errors & console */
65
- monitor?: MonitorConfig;
66
-
67
- /** Debug panel configuration */
68
- debug?: DebugConfig;
64
+ /**
65
+ * Devtools configuration (`@djangocfg/devtools`): error/console capture with
66
+ * backend ingest + the floating debug panel. Capture is on by default;
67
+ * `devtools={{ button: false }}` hides the panel button, `ingest: false`
68
+ * keeps events local.
69
+ */
70
+ devtools?: DevtoolsConfig;
69
71
 
70
72
  /**
71
73
  * Locale-switching plumbing. Mounted into a `LayoutI18nProvider` inside
@@ -75,14 +77,8 @@ export interface BaseLayoutProps {
75
77
  i18n?: I18nLayoutConfig;
76
78
  }
77
79
 
78
- /** Debug panel config enabled in development by default, pass enabled: true/false to override */
79
- export interface DebugConfig {
80
- /** Set false to disable, true to force-enable. Default: process.env.NODE_ENV === 'development' */
81
- enabled?: boolean;
82
- className?: string;
83
- /** Props forwarded to DebugPanel */
84
- panel?: Record<string, unknown>;
85
- }
80
+ /** Devtools config for BaseApp the `<Devtools>` props minus children. */
81
+ export type DevtoolsConfig = Omit<DevtoolsProps, 'children'>;
86
82
 
87
83
  /**
88
84
  * Brand block surfaced inside the locale-switcher dialog. Shared via
@@ -3,7 +3,7 @@
3
3
  import { LogIn } from 'lucide-react';
4
4
  import React, { useMemo } from 'react';
5
5
 
6
- import { DEFAULT_AUTH_PATH, useAuth, useCfgRouter } from '@djangocfg/api/auth';
6
+ import { useAuth, useCfgRouter } from '@djangocfg/api/auth';
7
7
  import { useAppT } from '@djangocfg/i18n';
8
8
  import {
9
9
  Button, Dialog, DialogContent, DialogHeader, DialogTitle
@@ -17,7 +17,7 @@ interface AuthDialogProps {
17
17
 
18
18
  export const AuthDialog: React.FC<AuthDialogProps> = ({
19
19
  onAuthRequired,
20
- authPath = DEFAULT_AUTH_PATH
20
+ authPath
21
21
  }) => {
22
22
  const open = useDialogStore((s) => s.authOpen);
23
23
  const authOptions = useDialogStore((s) => s.authOptions);
@@ -32,7 +32,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
32
32
 
33
33
  const message = authOptions?.message || labels.pleaseSignIn;
34
34
  const router = useCfgRouter();
35
- const { saveRedirectUrl } = useAuth();
35
+ const { saveRedirectUrl, routes } = useAuth();
36
36
 
37
37
  const handleClose = () => {
38
38
  resolveAuth(false);
@@ -49,7 +49,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
49
49
  if (onAuthRequired) {
50
50
  onAuthRequired();
51
51
  } else {
52
- router.push(authPath);
52
+ router.push(authPath ?? routes.auth);
53
53
  }
54
54
 
55
55
  resolveAuth(true);
@@ -8,6 +8,6 @@ export * from './Breadcrumbs';
8
8
  export * from './AuthDialog';
9
9
  export * from './Analytics';
10
10
 
11
- // MonitorProvider lives in @djangocfg/monitor/client — re-exported here for convenience
12
- export { MonitorProvider } from '@djangocfg/monitor/client';
13
- export type { MonitorProviderProps } from '@djangocfg/monitor/client';
11
+ // Devtools lives in @djangocfg/devtools — re-exported here for convenience
12
+ export { Devtools } from '@djangocfg/devtools';
13
+ export type { DevtoolsProps } from '@djangocfg/devtools';
@@ -62,6 +62,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
62
62
  isLoading,
63
63
  isAuthenticated: isAuthenticated ?? Boolean(user),
64
64
  isAdminUser,
65
+ routes: { auth: '/auth', defaultCallback: '/', defaultAuthCallback: '/auth' },
65
66
 
66
67
  loadCurrentProfile: noopAsync,
67
68
  checkAuthAndRedirect: noopAsync,