@djangocfg/layouts 2.1.430 → 2.1.432

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
@@ -162,6 +162,12 @@ export default function Layout({ children }) {
162
162
 
163
163
  Wraps `Boundary` from `@djangocfg/ui-core` — same `variant`/`fallback`/`resetKeys` API, plus auto `FrontendMonitor.capture(...)`.
164
164
 
165
+ > **One top-level boundary.** `BaseApp` already mounts the app-wide crash
166
+ > boundary (UiProviders' built-in one, fed an i18n fallback) — don't wrap a
167
+ > second one around the whole app. Use `MonitorBoundary` for **per-section**
168
+ > isolation (so one panel's crash doesn't take the page), not as a duplicate
169
+ > root boundary. All boundaries share the single ui-core `Boundary` catch.
170
+
165
171
  For local UI widgets that should not report to backend, use `Boundary` from `@djangocfg/ui-core` directly.
166
172
 
167
173
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.430",
3
+ "version": "2.1.432",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,12 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.430",
93
- "@djangocfg/centrifugo": "^2.1.430",
94
- "@djangocfg/debuger": "^2.1.430",
95
- "@djangocfg/i18n": "^2.1.430",
96
- "@djangocfg/monitor": "^2.1.430",
97
- "@djangocfg/ui-core": "^2.1.430",
92
+ "@djangocfg/api": "^2.1.432",
93
+ "@djangocfg/centrifugo": "^2.1.432",
94
+ "@djangocfg/debuger": "^2.1.432",
95
+ "@djangocfg/i18n": "^2.1.432",
96
+ "@djangocfg/monitor": "^2.1.432",
97
+ "@djangocfg/ui-core": "^2.1.432",
98
98
  "@hookform/resolvers": "^5.2.2",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -125,14 +125,14 @@
125
125
  "uuid": "^11.1.0"
126
126
  },
127
127
  "devDependencies": {
128
- "@djangocfg/api": "^2.1.430",
129
- "@djangocfg/centrifugo": "^2.1.430",
130
- "@djangocfg/debuger": "^2.1.430",
131
- "@djangocfg/i18n": "^2.1.430",
132
- "@djangocfg/monitor": "^2.1.430",
133
- "@djangocfg/typescript-config": "^2.1.430",
134
- "@djangocfg/ui-core": "^2.1.430",
135
- "@djangocfg/ui-tools": "^2.1.430",
128
+ "@djangocfg/api": "^2.1.432",
129
+ "@djangocfg/centrifugo": "^2.1.432",
130
+ "@djangocfg/debuger": "^2.1.432",
131
+ "@djangocfg/i18n": "^2.1.432",
132
+ "@djangocfg/monitor": "^2.1.432",
133
+ "@djangocfg/typescript-config": "^2.1.432",
134
+ "@djangocfg/ui-core": "^2.1.432",
135
+ "@djangocfg/ui-tools": "^2.1.432",
136
136
  "@types/node": "^25.2.3",
137
137
  "@types/react": "^19.2.15",
138
138
  "@types/react-dom": "^19.2.3",
@@ -1,14 +1,18 @@
1
1
  /**
2
- * Simple ErrorBoundary Component
2
+ * ErrorBoundary — the i18n'd, support-email fallback for Next app layouts.
3
3
  *
4
- * Catches React errors and displays a fallback UI
4
+ * Thin wrapper over the ui-core `Boundary` primitive: ui-core owns the catch
5
+ * machinery (getDerivedStateFromError / componentDidCatch / reset), this layer
6
+ * only supplies a localized fullscreen fallback + an optional support email.
7
+ * One catch implementation across the stack — UiProviders' built-in boundary,
8
+ * MonitorBoundary, and this all build on the same ui-core Boundary.
5
9
  */
6
10
 
7
11
  'use client';
8
12
 
9
- import { isDev } from "@djangocfg/ui-core/lib";
10
- import React, { Component, ErrorInfo, ReactNode } from 'react';
13
+ import { useMemo, type ComponentType, type ErrorInfo, type ReactNode } from 'react';
11
14
 
15
+ import { Boundary, type BoundaryRenderProps } from '@djangocfg/ui-core/components';
12
16
  import { getT } from '@djangocfg/i18n';
13
17
 
14
18
  interface ErrorBoundaryProps {
@@ -17,90 +21,60 @@ interface ErrorBoundaryProps {
17
21
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
18
22
  }
19
23
 
20
- interface ErrorBoundaryState {
21
- hasError: boolean;
22
- error: Error | null;
23
- }
24
-
25
- export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
26
- private handlePopState: (() => void) | null = null;
27
-
28
- constructor(props: ErrorBoundaryProps) {
29
- super(props);
30
- this.state = { hasError: false, error: null };
31
- }
32
-
33
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
34
- return { hasError: true, error };
35
- }
36
-
37
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
38
- if (this.props.onError) {
39
- this.props.onError(error, errorInfo);
40
- }
41
-
42
- if (isDev) {
43
- console.error('ErrorBoundary caught an error:', error, errorInfo);
44
- }
45
- }
46
-
47
- componentDidUpdate(_prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
48
- if (this.state.hasError && !prevState.hasError) {
49
- this.handlePopState = () => {
50
- window.location.reload();
51
- };
52
- window.addEventListener('popstate', this.handlePopState);
53
- } else if (!this.state.hasError && prevState.hasError) {
54
- if (this.handlePopState) {
55
- window.removeEventListener('popstate', this.handlePopState);
56
- this.handlePopState = null;
57
- }
58
- }
59
- }
60
-
61
- componentWillUnmount() {
62
- if (this.handlePopState) {
63
- window.removeEventListener('popstate', this.handlePopState);
64
- this.handlePopState = null;
65
- }
66
- }
67
-
68
- render() {
69
- if (this.state.hasError) {
70
- const title = getT('layouts.errors.somethingWentWrong');
71
- const description = getT('layouts.errors.tryRefreshing');
72
- const refreshButton = getT('layouts.errors.refreshPage');
73
-
74
- return (
75
- <div className="flex min-h-screen items-center justify-center bg-background p-4">
76
- <div className="max-w-md w-full space-y-4 text-center">
77
- <h1 className="text-2xl font-bold text-foreground">{title}</h1>
78
- <p className="text-muted-foreground">
79
- {description}
24
+ /** Localized fullscreen fallback. `reset()` re-mounts the tree (ui-core); we
25
+ * also offer a hard reload, matching the prior behavior. */
26
+ function makeFallback(supportEmail?: string) {
27
+ return function ErrorFallback({ reset }: BoundaryRenderProps) {
28
+ const title = getT('layouts.errors.somethingWentWrong');
29
+ const description = getT('layouts.errors.tryRefreshing');
30
+ const refreshButton = getT('layouts.errors.refreshPage');
31
+ return (
32
+ <div className="flex min-h-screen items-center justify-center bg-background p-4">
33
+ <div className="max-w-md w-full space-y-4 text-center">
34
+ <h1 className="text-2xl font-bold text-foreground">{title}</h1>
35
+ <p className="text-muted-foreground">{description}</p>
36
+ {supportEmail && (
37
+ <p className="text-sm text-muted-foreground">
38
+ {getT('layouts.errors.persistsContact', { email: '' })}{' '}
39
+ <a
40
+ href={`mailto:${supportEmail}`}
41
+ className="text-primary hover:underline"
42
+ >
43
+ {supportEmail}
44
+ </a>
80
45
  </p>
81
- {this.props.supportEmail && (
82
- <p className="text-sm text-muted-foreground">
83
- {getT('layouts.errors.persistsContact', { email: '' })}{' '}
84
- <a
85
- href={`mailto:${this.props.supportEmail}`}
86
- className="text-primary hover:underline"
87
- >
88
- {this.props.supportEmail}
89
- </a>
90
- </p>
91
- )}
92
- <button
93
- onClick={() => window.location.reload()}
94
- className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
95
- >
96
- {refreshButton}
97
- </button>
98
- </div>
46
+ )}
47
+ <button
48
+ onClick={() => {
49
+ reset();
50
+ // Hard reload as a fallback if a pure reset can't recover.
51
+ window.location.reload();
52
+ }}
53
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
54
+ >
55
+ {refreshButton}
56
+ </button>
99
57
  </div>
100
- );
101
- }
102
-
103
- return this.props.children;
104
- }
58
+ </div>
59
+ );
60
+ };
105
61
  }
106
62
 
63
+ export function ErrorBoundary({ children, supportEmail, onError }: ErrorBoundaryProps) {
64
+ // Memoize so the boundary's FallbackComponent identity is stable across
65
+ // renders (no remount churn).
66
+ const Fallback = useMemo<ComponentType<BoundaryRenderProps>>(
67
+ () => makeFallback(supportEmail),
68
+ [supportEmail],
69
+ );
70
+ return (
71
+ <Boundary
72
+ variant="fullscreen"
73
+ name="layouts-error-boundary"
74
+ onError={onError}
75
+ FallbackComponent={Fallback}
76
+ >
77
+ {children}
78
+ </Boundary>
79
+ );
80
+ }
@@ -5,10 +5,14 @@ runtime error tracker, and developer-facing toasts (with a Copy-as-cURL action).
5
5
 
6
6
  ## Pieces
7
7
 
8
+ All boundaries here build on the single `Boundary` primitive in
9
+ `@djangocfg/ui-core` — no duplicate catch logic. `BaseApp` mounts ONE boundary
10
+ (UiProviders' built-in, fed an i18n fallback), so apps don't wrap their own.
11
+
8
12
  | File | What it does |
9
13
  |------|--------------|
10
- | `ErrorBoundary.tsx` | React error boundary with a friendly fallback. |
11
- | `MonitorBoundary.tsx` | Error boundary that also reports to `@djangocfg/monitor`. |
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`. |
12
16
  | `ErrorLayout.tsx` | Full-page error layout (`getErrorContent` / `ERROR_CODES`). |
13
17
  | `ErrorsTracker/` | Global runtime tracker — listens for error CustomEvents and shows toasts. |
14
18
 
@@ -57,7 +57,8 @@ import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters
57
57
  import { NextIntlLinkBridge } from './NextIntlLinkBridge';
58
58
  import { ThemeProvider } from '@djangocfg/ui-core/theme';
59
59
  import { ThemeStyleBridge } from '../../theme/ThemeStyleBridge';
60
- import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
60
+ import type { BoundaryRenderProps } from '@djangocfg/ui-core/components';
61
+ import { getT } from '@djangocfg/i18n';
61
62
  import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
62
63
  import { AnalyticsProvider } from '../../snippets/Analytics';
63
64
  import { AuthDialog } from '../../snippets/AuthDialog';
@@ -73,6 +74,51 @@ const DebugButton = React.lazy(() =>
73
74
  // For backwards compatibility, re-export as BaseAppProps
74
75
  export type BaseAppProps = BaseLayoutProps;
75
76
 
77
+ /**
78
+ * Localized crash fallback fed into UiProviders' built-in Boundary. Renders the
79
+ * i18n'd "something went wrong" screen + optional support email. `reset()`
80
+ * re-mounts (soft recover); the Reload button hard-reloads when a soft reset
81
+ * can't recover (e.g. the crash is in a provider above the boundary's subtree).
82
+ */
83
+ function AppErrorFallback({
84
+ reset,
85
+ supportEmail,
86
+ }: BoundaryRenderProps & { supportEmail?: string }) {
87
+ const title = getT('layouts.errors.somethingWentWrong');
88
+ const description = getT('layouts.errors.tryRefreshing');
89
+ const retry = getT('layouts.errors.refreshPage');
90
+ return (
91
+ <div className="flex min-h-screen items-center justify-center bg-background p-4">
92
+ <div className="max-w-md w-full space-y-4 text-center">
93
+ <h1 className="text-2xl font-bold text-foreground">{title}</h1>
94
+ <p className="text-muted-foreground">{description}</p>
95
+ {supportEmail && (
96
+ <p className="text-sm text-muted-foreground">
97
+ {getT('layouts.errors.persistsContact', { email: '' })}{' '}
98
+ <a href={`mailto:${supportEmail}`} className="text-primary hover:underline">
99
+ {supportEmail}
100
+ </a>
101
+ </p>
102
+ )}
103
+ <div className="flex items-center justify-center gap-2">
104
+ <button
105
+ onClick={reset}
106
+ className="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
107
+ >
108
+ {retry}
109
+ </button>
110
+ <button
111
+ onClick={() => window.location.reload()}
112
+ className="rounded-md border border-border px-4 py-2 text-foreground hover:bg-muted"
113
+ >
114
+ {getT('layouts.errors.refreshPage')}
115
+ </button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ );
120
+ }
121
+
76
122
  /**
77
123
  * BaseApp - Core providers wrapper for any React/Next.js app
78
124
  *
@@ -131,7 +177,21 @@ export function BaseApp({
131
177
  */}
132
178
  <NextRouterAdapter>
133
179
  <NextLinkProvider>
134
- <UiProviders>
180
+ {/* Single top-level boundary lives inside UiProviders. We feed it the
181
+ app's i18n fallback + onError instead of wrapping our own boundary on
182
+ top — one catch in the tree. errorBoundary={false} fully disables it
183
+ when the host opts out. */}
184
+ <UiProviders
185
+ errorBoundary={enableErrorBoundary ? undefined : false}
186
+ onError={errorBoundary?.onError}
187
+ errorFallback={
188
+ enableErrorBoundary
189
+ ? (props) => (
190
+ <AppErrorFallback {...props} supportEmail={errorBoundary?.supportEmail} />
191
+ )
192
+ : undefined
193
+ }
194
+ >
135
195
  <SWRConfig
136
196
  value={{
137
197
  revalidateOnFocus: swr?.revalidateOnFocus ?? true,
@@ -189,17 +249,7 @@ export function BaseApp({
189
249
  </ThemeProvider>
190
250
  );
191
251
 
192
- // Wrap with ErrorBoundary only if explicitly enabled
193
- if (enableErrorBoundary) {
194
- return (
195
- <ErrorBoundary
196
- supportEmail={errorBoundary?.supportEmail}
197
- onError={errorBoundary?.onError}
198
- >
199
- {content}
200
- </ErrorBoundary>
201
- );
202
- }
203
-
252
+ // The error boundary is now UiProviders' built-in one (fed our i18n fallback
253
+ // + onError above) — no outer wrapper needed.
204
254
  return content;
205
255
  }