The table header carried an arbitrary-value copy of fade-below; a contract refuses a hand gradient of a surface colour. 100vh is the viewport with a tablet browser's toolbar retracted, so h-screen / min-h-screen / static vh hid the last rows under it; a contract holds dvh (the Plex player stage changes with S3f, where it is played on the tablet).
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
import i18n from "../i18n";
|
|
import { notify } from "../lib/notifications";
|
|
import { errorAttrs, log } from "../lib/log";
|
|
import { reloadingForNewBuild } from "../lib/staleBuild";
|
|
import { Button } from "./ui/Button";
|
|
|
|
interface Props {
|
|
children: ReactNode;
|
|
}
|
|
interface State {
|
|
crashed: boolean;
|
|
}
|
|
|
|
// Catches render-time crashes anywhere in the tree, logs a fatal notification (kept
|
|
// in the center across the reload) and offers a recovery action.
|
|
export default class ErrorBoundary extends Component<Props, State> {
|
|
state: State = { crashed: false };
|
|
|
|
static getDerivedStateFromError(): State {
|
|
return { crashed: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
// The tab is already reloading onto a newer build (lib/staleBuild): this is the chunk it could not
|
|
// load, not a crash — no fatal notice, which would outlive the reload.
|
|
if (reloadingForNewBuild()) return;
|
|
log.fatal("ui.crash", "the app crashed while rendering", {
|
|
...errorAttrs(error),
|
|
...(info.componentStack ? { component_stack: info.componentStack } : {}),
|
|
});
|
|
notify({
|
|
level: "fatal",
|
|
title: i18n.t("errors.boundary.notifTitle"),
|
|
message: error.message || i18n.t("errors.boundary.notifMessage"),
|
|
requiresInteraction: true,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (this.state.crashed) {
|
|
if (reloadingForNewBuild()) return null;
|
|
return (
|
|
<div className="min-h-dvh grid place-items-center text-muted p-6 text-center">
|
|
<div>
|
|
<div className="text-lg font-semibold text-fg mb-1">
|
|
{i18n.t("errors.boundary.title")}
|
|
</div>
|
|
<div className="text-sm mb-3">{i18n.t("errors.boundary.subtitle")}</div>
|
|
<Button variant="link" onClick={() => location.reload()} className="text-sm">
|
|
{i18n.t("errors.boundary.reload")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|