Files
accounted/app/global-error.tsx
T
314efe8b22 fix(design): stop synthesizing bold on Hedvig display headings (#1555)
* fix(design): stop synthesizing bold on Hedvig display headings

Hedvig Letters Serif ships weight 400 only, but the h1-h3 base rule
forced font-weight 500 and DialogTitle/SheetTitle stacked font-semibold
on top, so every display heading rendered browser-synthesized bold: the
smudged heavy look on dialog titles and page headings. Drop the base
rule to 400, remove the weight utilities from the title primitives, and
sweep the 41 files that hand-set font-medium/semibold/bold on serif
headings (a pattern design.md already forbids). Headings that opt into
font-sans keep their weight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): drop empty className left by the weight sweep

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:27:25 +02:00

92 lines
3.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
// Last-resort boundary: only fires when the root layout itself throws, so it
// renders its own <html> and must stay free of app providers/components
// (NextIntlClientProvider, CompanyContext, etc. are not mounted here). It is
// also server-rendered when the root layout throws during SSR, so the lazy
// initializer must never touch window without a guard.
//
// Like app/error.tsx, transient failures here (e.g. a token race in the first
// request after login) heal on a fresh request, so auto-reload ONCE before
// showing the manual fallback. A per-path, per-tab-session flag (a monotonic
// one-shot, not a time window that could still loop on a slow failing render)
// keeps the single reload from becoming a loop on a genuinely broken root layout.
const RELOAD_FLAG_PREFIX = "accounted:global-error-reloaded:";
function reloadKey(): string {
return (
RELOAD_FLAG_PREFIX +
(typeof window !== "undefined" ? window.location.pathname : "")
);
}
// support@gnubok.se is hardcoded on purpose: this boundary renders when the root
// layout failed, so the branding service and any provider are unavailable here.
const SUPPORT_EMAIL = "support@gnubok.se";
function decideInitialPhase(): "reloading" | "fallback" {
if (typeof window === "undefined") return "reloading";
try {
if (window.sessionStorage.getItem(reloadKey())) return "fallback";
// Claim the one-shot and reload only if it persisted: writing here (not in
// the effect) keeps "mark reloaded" and "decide to reload" atomic, so a
// failed write (quota full / blocked) falls through to 'fallback' instead
// of reloading forever without ever recording it.
window.sessionStorage.setItem(reloadKey(), "1");
return "reloading";
} catch {
return "fallback";
}
}
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [phase] = useState<"reloading" | "fallback">(decideInitialPhase);
useEffect(() => {
console.error("[global] Unhandled error:", error);
// The one-shot flag is already claimed in decideInitialPhase, so reaching
// 'reloading' guarantees it persisted: reload exactly once.
if (phase === "reloading") window.location.reload();
}, [phase, error]);
return (
<html lang="sv" translate="no">
<head>
<meta name="google" content="notranslate" />
</head>
<body>
{phase === "fallback" ? (
<div className="flex min-h-dvh items-center justify-center p-8">
<div className="text-center space-y-4">
<h2 className="text-xl">Något gick fel</h2>
<p className="text-muted-foreground">
Ett oväntat fel inträffade. Försök igen eller{" "}
<a
href={`mailto:${SUPPORT_EMAIL}?subject=Oväntat fel`}
className="underline underline-offset-4 hover:text-foreground"
>
kontakta support
</a>{" "}
om problemet kvarstår.
</p>
<button
onClick={() => window.location.reload()}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
>
Försök igen
</button>
</div>
</div>
) : null}
</body>
</html>
);
}