From f0b26141eddbe797f251ea4cadb96bcbe98b62e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 7 Jun 2026 14:24:59 +0200 Subject: [PATCH] Default to the dark theme for a fresh visitor With no stored preference the app now resolves to dark instead of tracking the OS. An explicit choice still wins: light/dark persist as before, and "system" is now persisted verbatim rather than as a cleared key, so an explicit OS-tracking choice stays distinguishable from a never-chosen state and survives a reload. The first-paint bootstrap script mirrors the same resolution so a fresh visitor sees no flash to light. --- src/app/layout.tsx | 2 +- src/components/providers.tsx | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9bae20a19..422457f1e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -90,7 +90,7 @@ export const viewport: Viewport = { }; // Inline script to apply theme before first paint (prevents FOUC) -const themeScript = `(function(){try{var t=localStorage.getItem("healthlog-theme");var c=(t==="light"||t==="dark")?t:(window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");document.documentElement.classList.add(c)}catch(e){document.documentElement.classList.add("dark")}})()`; +const themeScript = `(function(){try{var t=localStorage.getItem("healthlog-theme");var c=(t==="light"||t==="dark")?t:(t==="system"?(window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light"):"dark");document.documentElement.classList.add(c)}catch(e){document.documentElement.classList.add("dark")}})()`; export default async function RootLayout({ children, diff --git a/src/components/providers.tsx b/src/components/providers.tsx index efe789ca6..9f6167d22 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -49,16 +49,21 @@ function applyTheme(resolved: "dark" | "light") { function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState(() => { - if (typeof window === "undefined") return "system"; + if (typeof window === "undefined") return "dark"; const saved = localStorage.getItem("healthlog-theme") as Theme | null; - return saved === "light" || saved === "dark" ? saved : "system"; + // A stored "light"/"dark"/"system" choice wins. With no stored + // preference the app defaults to dark rather than tracking the OS. + if (saved === "light" || saved === "dark") return saved; + if (saved === "system") return "system"; + return "dark"; }); const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() => { if (typeof window === "undefined") return "dark"; const saved = localStorage.getItem("healthlog-theme") as Theme | null; - const t = saved === "light" || saved === "dark" ? saved : "system"; - return t === "system" ? getSystemTheme() : t; + if (saved === "light" || saved === "dark") return saved; + if (saved === "system") return getSystemTheme(); + return "dark"; }); // Apply theme on mount (inline script handles FOUC, this ensures classes are in sync) @@ -82,13 +87,16 @@ function ThemeProvider({ children }: { children: ReactNode }) { const setTheme = useCallback((next: Theme) => { setThemeState(next); + // Persist every explicit choice, including "system" — the absence of a + // stored value is reserved for a fresh visitor and now defaults to dark. + // Storing "system" verbatim keeps an explicit OS-tracking choice + // distinguishable from "never chose", so a reload honours it. + localStorage.setItem("healthlog-theme", next); if (next === "system") { - localStorage.removeItem("healthlog-theme"); const resolved = getSystemTheme(); setResolvedTheme(resolved); applyTheme(resolved); } else { - localStorage.setItem("healthlog-theme", next); setResolvedTheme(next); applyTheme(next); }