Hoists hooks above early return (render-time Navigate); ports finance's ConfirmDialog/PageHeader/loading/states + APC error-copy mapper; removes window.confirm, dead appbar search, and seeded fake notifications; wires toasts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
import {
|
|
Links,
|
|
Meta,
|
|
Outlet,
|
|
Scripts,
|
|
ScrollRestoration,
|
|
isRouteErrorResponse,
|
|
} from "react-router"
|
|
import { AlertTriangle, Home, RotateCcw } from "lucide-react"
|
|
|
|
import type { Route } from "./+types/root"
|
|
import "./app.css"
|
|
|
|
import { humanErrorMessage, humanErrorTitle } from "~/lib/errors"
|
|
import { Button, buttonVariants } from "~/components/ui/button"
|
|
|
|
import { ToastProvider } from "@crema/notification-ui"
|
|
import { CommandBusProvider } from "@crema/action-bus"
|
|
// CREMA:PROVIDERS-IMPORTS
|
|
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en" suppressHydrationWarning>
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<Meta />
|
|
<Links />
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `(function(){try{var t=localStorage.getItem('crema-theme');if(!t)t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';if(t==='dark')document.documentElement.classList.add('dark');var f=localStorage.getItem('crema-font-scale');if(f&&/^(sm|md|lg|xl)$/.test(f))document.documentElement.dataset.fontScale=f;var b=localStorage.getItem('crema-bg');if(b&&/^(pearl|linen|mist|dawn|seafoam|aurora|sunset|meadow|midnight|blush|noir)$/.test(b)){document.addEventListener('DOMContentLoaded',function(){document.body.dataset.bg=b;});}var s=localStorage.getItem('crema-surface');if(s&&/^(snow|stone|sage|slate)$/.test(s)){document.addEventListener('DOMContentLoaded',function(){document.body.dataset.surface=s;});}}catch(e){}})();`,
|
|
}}
|
|
/>
|
|
</head>
|
|
<body suppressHydrationWarning>
|
|
{children}
|
|
<ScrollRestoration />
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
/* CREMA:PROVIDERS-WRAP-OPEN */
|
|
<ToastProvider>
|
|
<CommandBusProvider>
|
|
<Outlet />
|
|
</CommandBusProvider>
|
|
</ToastProvider>
|
|
/* CREMA:PROVIDERS-WRAP-CLOSE */
|
|
)
|
|
}
|
|
|
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
const title = humanErrorTitle(error)
|
|
const message = humanErrorMessage(error)
|
|
const is404 = isRouteErrorResponse(error) && error.status === 404
|
|
// Stack traces are a developer affordance — never leak them to users in
|
|
// production, and even in dev keep them tucked below the human copy.
|
|
const stack =
|
|
import.meta.env.DEV && error instanceof Error ? error.stack : undefined
|
|
|
|
return (
|
|
<main className="flex min-h-svh items-center justify-center bg-background p-6">
|
|
<div className="w-full max-w-md rounded-2xl border bg-card p-8 text-center shadow-e1">
|
|
<div className="mx-auto mb-5 flex size-12 items-center justify-center rounded-2xl bg-destructive/10 text-destructive">
|
|
<AlertTriangle className="size-6" />
|
|
</div>
|
|
<h1 className="text-title font-semibold tracking-tight text-card-foreground">
|
|
{is404 ? "Page not found" : title}
|
|
</h1>
|
|
<p className="mx-auto mt-2 max-w-sm text-[15px] leading-relaxed text-muted-foreground">
|
|
{message}
|
|
</p>
|
|
<div className="mt-6 flex flex-wrap items-center justify-center gap-2">
|
|
<a
|
|
href="/"
|
|
data-action="error-go-home"
|
|
className={buttonVariants({ variant: "default" })}
|
|
>
|
|
<Home className="size-4" /> Go home
|
|
</a>
|
|
<Button
|
|
data-action="error-retry"
|
|
variant="outline"
|
|
onClick={() => window.location.reload()}
|
|
>
|
|
<RotateCcw className="size-4" /> Try again
|
|
</Button>
|
|
</div>
|
|
{stack ? (
|
|
<pre className="mt-6 max-h-64 overflow-auto rounded-lg border bg-muted/40 p-4 text-left font-mono text-xs leading-relaxed text-muted-foreground">
|
|
<code>{stack}</code>
|
|
</pre>
|
|
) : null}
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|