import * as React from 'react'; import { Alert, AlertDescription, AlertTitle } from '@/components/alert'; import { useMessages } from '~/i18n'; /** * A wall around one rendered preview. * * This exists because of the rule in `playground.tsx`: with kit components in * the site's own chrome, the showcase is running the very code it documents. A * component you are halfway through breaking throws during render, and React * unmounts the whole tree from the root — so the page you were using to see the * break is the page that disappears. * * Every place a component is rendered as *subject matter* is wrapped: the * playground instance, each matrix cell, each composition, each overview card. * The chrome itself is not — if the sidebar's Button is broken there is no * meaningful page left to degrade to, and a boundary there would only hide it. * * A class component because there is still no hook form of `componentDidCatch`. */ interface Props { children: React.ReactNode; /** What broke — the component name, or the cell's axis values. */ label?: string; /** `cell` is the compact form a matrix square has room for. */ variant?: 'block' | 'cell'; /** * Clears a caught error when it changes. * * Without it a component that only throws at `size="xl"` would stay broken * after the playground was moved back to `md` — the boundary latches, and * remounting it with a `key` instead would throw away the live component's * own state on every keystroke. */ resetKey?: string; } interface Fallback { title: string; titleFor: (label: string) => string; cell: string; } interface State { error: Error | null; } class Boundary extends React.Component { state: State = { error: null }; static getDerivedStateFromError(error: Error): State { return { error }; } componentDidCatch(error: Error, info: React.ErrorInfo) { /* Logged rather than swallowed: the boundary's job is to keep the page usable, not to hide the stack from whoever is debugging. */ console.error(`[showcase] ${this.props.label ?? 'preview'} threw`, error, info.componentStack); } componentDidUpdate(prev: Props) { if (this.state.error && prev.resetKey !== this.props.resetKey) this.setState({ error: null }); } render() { const { error } = this.state; if (!error) return this.props.children; const { messages, label, variant } = this.props; if (variant === 'cell') { return ( {messages.cell} ); } return ( {label ? messages.titleFor(label) : messages.title} {error.message} ); } } /** * The hook wrapper. `useMessages` cannot be read inside the class, and passing * the catalogue down as a prop keeps the boundary itself free of context. */ export function ErrorBoundary({ children, label, variant = 'block', resetKey }: Props) { const m = useMessages(); return ( {children} ); }