import { Component, useState, type ComponentType, type ErrorInfo, type ReactNode, } from 'react'; export type ErrorBoundaryLayer = 'app' | 'route' | 'viewer' | 'panel' | 'control'; export interface ErrorBoundaryFallbackProps { error: Error; errorInfo?: ErrorInfo; eventId: string; layer: ErrorBoundaryLayer; resetErrorBoundary: () => void; } export interface ErrorBoundaryProps { children: ReactNode; layer?: ErrorBoundaryLayer; fallback?: ComponentType | ReactNode; onError?: (error: Error, errorInfo: ErrorInfo, eventId: string) => void; onReset?: (eventId: string | undefined) => void; } interface ErrorBoundaryState { error: Error | null; errorInfo?: ErrorInfo; eventId?: string; } const initialState: ErrorBoundaryState = { error: null, }; export class ErrorBoundary extends Component { state: ErrorBoundaryState = initialState; static getDerivedStateFromError(error: unknown): Partial { return { error: normaliseError(error), }; } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { const eventId = createEventId(this.props.layer ?? 'app'); const normalisedError = normaliseError(error); this.setState({ error: normalisedError, errorInfo, eventId, }); this.props.onError?.(normalisedError, errorInfo, eventId); } resetErrorBoundary = (): void => { this.props.onReset?.(this.state.eventId); this.setState(initialState); }; render(): ReactNode { const { children, fallback, layer = 'app' } = this.props; const { error, errorInfo, eventId } = this.state; if (!error) { return children; } const fallbackProps: ErrorBoundaryFallbackProps = { error, errorInfo, eventId: eventId ?? createEventId(layer), layer, resetErrorBoundary: this.resetErrorBoundary, }; if (typeof fallback === 'function') { const FallbackComponent = fallback as ComponentType; return ; } if (fallback !== undefined) { return fallback; } return ; } } export function AppErrorBoundary(props: Omit): JSX.Element { return ; } export function RouteErrorBoundary(props: Omit): JSX.Element { return ; } export function ViewerErrorBoundary(props: Omit): JSX.Element { return ; } export function PanelErrorBoundary(props: Omit): JSX.Element { return ; } export function ControlErrorBoundary(props: Omit): JSX.Element { return ; } export function useErrorBoundaryRethrow(): (error: unknown) => void { const [, setError] = useState(null); return (error: unknown) => { setError(() => { throw normaliseError(error); }); }; } export function DefaultErrorFallback({ error, errorInfo, eventId, layer, resetErrorBoundary, }: ErrorBoundaryFallbackProps): JSX.Element { const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle'); const copy = ERROR_COPY[layer] ?? ERROR_COPY.app; const diagnostics = formatErrorDiagnostics(error, errorInfo, eventId, layer); const copyDiagnostics = async () => { if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) { setCopyState('failed'); return; } try { await navigator.clipboard.writeText(diagnostics); setCopyState('copied'); } catch { setCopyState('failed'); } }; const reload = () => { if (typeof window !== 'undefined') { window.location.reload(); } }; return (

{copy.eyebrow}

{copy.title}

{copy.message}

Event ID: {eventId}

{error.message}

Diagnostic details
          {diagnostics}
        
); } export function formatErrorDiagnostics( error: Error, errorInfo: ErrorInfo | undefined, eventId: string, layer: ErrorBoundaryLayer, ): string { return [ `Mechanica runtime error`, `Event ID: ${eventId}`, `Layer: ${layer}`, `Message: ${error.message}`, '', 'Stack:', error.stack ?? '(no stack available)', '', 'React component stack:', errorInfo?.componentStack ?? '(no component stack available)', ].join('\n'); } function normaliseError(error: unknown): Error { if (error instanceof Error) { return error; } return new Error(typeof error === 'string' ? error : 'Unknown runtime error'); } function createEventId(layer: ErrorBoundaryLayer): string { return `${layer}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; } const ERROR_COPY: Record< ErrorBoundaryLayer, { eyebrow: string; title: string; message: string } > = { app: { eyebrow: 'Application fault', title: 'Mechanica hit an unexpected problem.', message: 'The application shell failed safely instead of leaving a blank screen. Try again, reload, or copy diagnostics for maintainers.', }, route: { eyebrow: 'Route fault', title: 'This page could not be rendered.', message: 'Navigation and saved links are still available. Retry the route or reload if the issue persists.', }, viewer: { eyebrow: 'Viewer fault', title: 'The 3D viewer stopped rendering safely.', message: 'The catalogue and learning panels remain protected. Retry the viewer after reducing display quality or disabling extensions.', }, panel: { eyebrow: 'Panel fault', title: 'This engineering panel could not be displayed.', message: 'The rest of the machine explorer is still available. Retry the panel or reload the application.', }, control: { eyebrow: 'Control fault', title: 'A control surface failed safely.', message: 'Playback and view controls are isolated so one broken widget cannot take down the entire scene.', }, };