fbd6df33b4
Adds System Preferences, Dark and Light themes, gated by the `hasThemeSwitcher` feature flag (off by default — dark stays the default theme for everyone). Old theme is now "Classic"and set as default. "System preferences" theme has both Light and Dark modes and uses your laptop settings to use a correct one. It has less color accents (specifically less colored text), and they are the same for both modes, only grayscale values change between them. And Light/Dark themes can be used separately. New Contrast setting is available for System Preferences, Dark and Light themes - it changes the contrast for the whole app. All new visual Settings live in Account.
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { HomeIcon } from "@heroicons/react/20/solid";
|
|
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
|
|
import { friendlyErrorDisplay } from "~/utils/httpErrors";
|
|
import { permissionDeniedMessage } from "~/utils/permissionDenied";
|
|
import { LinkButton } from "./primitives/Buttons";
|
|
import { Header1 } from "./primitives/Headers";
|
|
import { Paragraph } from "./primitives/Paragraph";
|
|
import { PermissionDenied } from "./PermissionDenied";
|
|
import { TriggerRotatingLogo } from "./TriggerRotatingLogo";
|
|
import { type ReactNode } from "react";
|
|
|
|
type ErrorDisplayOptions = {
|
|
button?: {
|
|
title: string;
|
|
to: string;
|
|
};
|
|
};
|
|
|
|
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
|
|
const error = useRouteError();
|
|
|
|
// A failed `authorization` check (or `throwPermissionDenied`) throws a 403
|
|
// that bubbles to the nearest route ErrorBoundary. Every layout boundary
|
|
// renders through here, so handling it once means a gated route only has to
|
|
// declare `authorization` to get the permission panel: no per-route boundary.
|
|
const permission = isRouteErrorResponse(error) ? permissionDeniedMessage(error.data) : null;
|
|
if (permission) {
|
|
return (
|
|
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
|
<div className="w-full max-w-md">
|
|
<PermissionDenied message={permission} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{isRouteErrorResponse(error) ? (
|
|
<ErrorDisplay
|
|
title={friendlyErrorDisplay(error.status, error.statusText).title}
|
|
message={
|
|
error.data.message ?? friendlyErrorDisplay(error.status, error.statusText).message
|
|
}
|
|
{...options}
|
|
/>
|
|
) : error instanceof Error ? (
|
|
<ErrorDisplay title={error.name} message={error.message} {...options} />
|
|
) : (
|
|
<ErrorDisplay title="Oops" message={JSON.stringify(error)} {...options} />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
type DisplayOptionsProps = {
|
|
title: string;
|
|
message?: ReactNode;
|
|
} & ErrorDisplayOptions;
|
|
|
|
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
|
return (
|
|
// The backdrop stays dark in every theme (the rotating-logo animation is
|
|
// dark artwork), so the text pins to the dark-theme colors on light too.
|
|
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto bg-[#16181C]">
|
|
<div className="z-10 mt-[30vh] flex shrink-0 flex-col items-center gap-8">
|
|
<Header1 className="light:text-charcoal-200">{title}</Header1>
|
|
{message && <Paragraph className="light:text-charcoal-400">{message}</Paragraph>}
|
|
<LinkButton
|
|
to={button ? button.to : "/"}
|
|
shortcut={{ key: "enter" }}
|
|
variant="primary/medium"
|
|
LeadingIcon={HomeIcon}
|
|
>
|
|
{button ? button.title : "Go to homepage"}
|
|
</LinkButton>
|
|
</div>
|
|
<TriggerRotatingLogo />
|
|
</div>
|
|
);
|
|
}
|