diff --git a/demo/demo.tsx b/demo/demo.tsx index f02d476..197d6c5 100644 --- a/demo/demo.tsx +++ b/demo/demo.tsx @@ -5,6 +5,7 @@ import {createBrowserRouter} from "react-router-dom"; import {Kernel} from "../src/Application/Kernel"; import {NavTest} from "./NavTest"; import {Avocado} from "@phosphor-icons/react"; +import {ApplicationError} from "../src/Application/ApplicationError"; // Router initialization. const router = createBrowserRouter([ @@ -17,6 +18,7 @@ const router = createBrowserRouter([ element: , } ], + errorElement: , } ]) diff --git a/src/Application/Application.tsx b/src/Application/Application.tsx index 3676892..4e69835 100644 --- a/src/Application/Application.tsx +++ b/src/Application/Application.tsx @@ -1,13 +1,18 @@ import React from "react"; +import {ApplicationError, ApplicationErrorBoundary} from "./ApplicationError"; /** * Main Kernel UI application. */ -export function Application({children}: React.PropsWithChildren<{}>) +export function Application({errorElement, children}: React.PropsWithChildren<{ + errorElement?: React.ReactNode; +}>) { return ( -
- {children} -
+ }> +
+ {children} +
+
); } diff --git a/src/Application/ApplicationError.tsx b/src/Application/ApplicationError.tsx new file mode 100644 index 0000000..2943f75 --- /dev/null +++ b/src/Application/ApplicationError.tsx @@ -0,0 +1,94 @@ +import React, {useContext, useState} from "react"; +import {ArrowsClockwise, Bug, BugDroid} from "@phosphor-icons/react"; +import {useRouteError} from "react-router-dom"; + +/** + * Application error context. + */ +const ApplicationErrorContext = React.createContext(undefined); + +/** + * Get current error from context or router. + */ +export function useApplicationError(): Error +{ + // Get error from context or router. + const error = useContext(ApplicationErrorContext); + const routeError = useRouteError() as Error; + return error ?? routeError; +} + +/** + * Application error component. + */ +export function ApplicationError({children}: { + children?: (error: Error) => React.ReactElement; +}) +{ + // Get error from context. + const error = useApplicationError(); + + // Show details state. + const [showDetails, setShowDetails] = useState(false); + + return ( +
+ {children ? children(error) : ( + <> + +

Error

+ +
+ +

{error.name}

+ +

An unexpected error happened and the application was forced to quit.

+ +
{error.message}
+ + + +
+ + + { // Show details if required. + showDetails && ( +
+									{error.stack}
+								
+ ) + } +
+ + )} +
+ ); +} + +/** + * Error boundary component for the application. + */ +export class ApplicationErrorBoundary extends React.Component, { + error: Error; +}> +{ + static getDerivedStateFromError(error: Error) + { + return { error: error }; + } + + render() + { + if (this.state?.error) + // An error happened, showing the application error content. + return ( + + {this.props.errorElement} + + ); + + return this.props.children; + } +} diff --git a/src/styles/_components.less b/src/styles/_components.less index 645159c..acadead 100644 --- a/src/styles/_components.less +++ b/src/styles/_components.less @@ -2,6 +2,7 @@ @import "components/_button"; @import "components/_card"; @import "components/_dates"; +@import "components/_errors"; @import "components/_floating"; @import "components/_form"; @import "components/_headings"; diff --git a/src/styles/components/_errors.less b/src/styles/components/_errors.less new file mode 100644 index 0000000..94fffb6 --- /dev/null +++ b/src/styles/components/_errors.less @@ -0,0 +1,60 @@ +main.error +{ + font-size: 1.2em; + + > svg + { + display: block; + margin: 1em auto; + color: var(--red); + } + + hr + { + margin: 1em auto; + + width: 20em; + max-width: 50%; + height: 0.25em; + border-radius: 0.1em; + + + border: none; + background: var(--red); + } + + h1, h2, h3, h4, h5, h6 + { + margin: auto; + text-align: center; + } + + p, pre + { + margin: 1em auto; + width: 40em; + max-width: 95%; + text-align: center; + } + + button + { + display: block; + margin: 1em auto; + padding: 0.66em 1em; + } + + .details + { + font-size: 0.8em; + + button + {} + + pre + { + width: auto; + text-align: left; + } + } +}