diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fb9d007..031f4e9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,15 @@ import type { Metadata } from "next"; +import { Inter } from "next/font/google"; + +import { ThemeScript } from "@/components/theme/theme-script"; import "./globals.css"; +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", + display: "swap", +}); + export const metadata: Metadata = { title: "Client Board", description: "Project tracking for client work", @@ -10,7 +19,12 @@ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + // suppressHydrationWarning: ThemeScript mutates the class on before + // React hydrates, so server and client markup differ here by design. + + + + {children} ); diff --git a/src/components/theme/theme-script.tsx b/src/components/theme/theme-script.tsx new file mode 100644 index 0000000..aa8d4c2 --- /dev/null +++ b/src/components/theme/theme-script.tsx @@ -0,0 +1,23 @@ +/** + * Applies the stored theme before first paint. + * + * This has to be a blocking inline script in . A React effect runs after + * hydration, by which point the browser has already painted the default theme + * and the user sees a white flash on every navigation to a dark board. There + * is no way to fix that from inside React. + */ +const SCRIPT = ` +(function () { + try { + var stored = localStorage.getItem('theme'); + var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + var dark = stored === 'dark' || (stored !== 'light' && prefersDark); + document.documentElement.classList.toggle('dark', dark); + document.documentElement.style.colorScheme = dark ? 'dark' : 'light'; + } catch (e) {} +})(); +`; + +export function ThemeScript() { + return