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 ;
+}
\ No newline at end of file
diff --git a/src/components/theme/theme-toggle.tsx b/src/components/theme/theme-toggle.tsx
new file mode 100644
index 0000000..8820354
--- /dev/null
+++ b/src/components/theme/theme-toggle.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type Theme = "light" | "dark" | "system";
+
+function apply(theme: Theme) {
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const dark = theme === "dark" || (theme === "system" && prefersDark);
+
+ document.documentElement.classList.toggle("dark", dark);
+ document.documentElement.style.colorScheme = dark ? "dark" : "light";
+
+ if (theme === "system") localStorage.removeItem("theme");
+ else localStorage.setItem("theme", theme);
+}
+
+const OPTIONS: Array<{ value: Theme; label: string }> = [
+ { value: "light", label: "Light" },
+ { value: "system", label: "Auto" },
+ { value: "dark", label: "Dark" },
+];
+
+export function ThemeToggle() {
+ const [theme, setTheme] = useState("system");
+
+ useEffect(() => {
+ const stored = localStorage.getItem("theme");
+ if (stored === "light" || stored === "dark") setTheme(stored);
+ }, []);
+
+ function choose(next: Theme) {
+ setTheme(next);
+ apply(next);
+ }
+
+ return (
+
+ {OPTIONS.map((option) => (
+
+ ))}
+
+ );
+}
\ No newline at end of file