|
|
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
|
|
|
|
interface Props { children: ReactNode }
|
|
|
interface State { error: Error | undefined }
|
|
|
|
|
|
/**
|
|
|
* Top-level render guard. Without this, any throw during render white-screens the
|
|
|
* whole console (the whole point of finding #1). We catch it, log it for the
|
|
|
* console, and show a friendly recover-with-reload fallback instead of a blank page.
|
|
|
*
|
|
|
* Deliberately self-contained (no @sims/ui import): the boundary must keep working
|
|
|
* even when the failure is in a shared component it would otherwise render.
|
|
|
*/
|
|
|
export class ErrorBoundary extends Component<Props, State> {
|
|
|
state: State = { error: undefined }
|
|
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
|
return { error }
|
|
|
}
|
|
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
|
// Surface it in the browser console for whoever's debugging; no telemetry sink yet.
|
|
|
console.error('Console crashed while rendering:', error, info.componentStack)
|
|
|
}
|
|
|
|
|
|
private readonly reset = (): void => this.setState({ error: undefined })
|
|
|
|
|
|
render(): ReactNode {
|
|
|
const { error } = this.state
|
|
|
if (error === undefined) return this.props.children
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
role="alert"
|
|
|
style={{
|
|
|
minHeight: '100dvh', display: 'flex', flexDirection: 'column',
|
|
|
alignItems: 'center', justifyContent: 'center', gap: 16, padding: 24,
|
|
|
textAlign: 'center', color: 'var(--fg, #1f2937)',
|
|
|
background: 'var(--bg, #f8fafc)', fontFamily: 'inherit',
|
|
|
}}
|
|
|
>
|
|
|
<div style={{ fontSize: 40, lineHeight: 1 }} aria-hidden>⚠️</div>
|
|
|
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}>Something went wrong</h1>
|
|
|
<p style={{ margin: 0, maxWidth: 460, fontSize: 14, opacity: 0.75 }}>
|
|
|
The console hit an unexpected error and couldn’t finish rendering this screen.
|
|
|
Your data is safe on the server — reloading usually clears it.
|
|
|
</p>
|
|
|
<pre
|
|
|
style={{
|
|
|
margin: 0, maxWidth: 460, maxHeight: 120, overflow: 'auto',
|
|
|
fontSize: 12, opacity: 0.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
|
}}
|
|
|
>{error.message}</pre>
|
|
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
|
<button
|
|
|
type="button" onClick={this.reset}
|
|
|
style={{
|
|
|
padding: '8px 16px', borderRadius: 8, cursor: 'pointer', fontSize: 14,
|
|
|
border: '1px solid var(--line, #d1d5db)', background: 'transparent', color: 'inherit',
|
|
|
}}
|
|
|
>
|
|
|
Try again
|
|
|
</button>
|
|
|
<button
|
|
|
type="button" onClick={() => window.location.reload()}
|
|
|
style={{
|
|
|
padding: '8px 16px', borderRadius: 8, cursor: 'pointer', fontSize: 14,
|
|
|
border: 'none', background: 'var(--accent, #2563eb)', color: '#fff', fontWeight: 500,
|
|
|
}}
|
|
|
>
|
|
|
Reload console
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
)
|
|
|
}
|
|
|
}
|