From 615e02e9702f393afeb87ab6ad45c032ed16cee2 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Thu, 11 Jun 2026 21:50:13 +0200 Subject: [PATCH] fix(frontend): make dark mode toggle actually work The dark mode button was toggling React state but never applying it to the DOM. Add a useDarkMode hook that: - Persists the preference in localStorage - Defaults to the OS prefers-color-scheme on first visit - Adds/removes the 'dark' class on to trigger CSS variable overrides Also add dark-mode color overrides for all Tailwind v4 theme variables in index.css. Co-authored-by: openhands --- frontend/src/App.tsx | 49 +++++++++++++++++++++--------------------- frontend/src/index.css | 37 ++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c353c5..dc901fa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import BackupsPage from "./components/BackupsPage"; import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth"; import { fetchAppVersion } from "./api/client"; import { FRONTEND_VERSION_LABEL } from "./version"; +import { usePersistentState } from "./hooks/usePersistentState"; import { Button } from "@/components/ui/button"; import { Tooltip, @@ -58,6 +59,24 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } }, }); +function useDarkMode() { + const [darkMode, setDarkMode] = usePersistentState( + "dark-mode", + () => window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + + useEffect(() => { + const root = document.documentElement; + if (darkMode) { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + }, [darkMode]); + + return [darkMode, () => setDarkMode((prev) => !prev)] as const; +} + // Navigation items for sidebar const navItems = [ { path: "/", label: "Dashboard", icon: LayoutDashboard }, @@ -362,16 +381,7 @@ function SignInScreen({ onSignIn }: { onSignIn: () => void }) { function AuthenticatedApp() { const auth = useAuth(); - const [darkMode, setDarkMode] = useState(() => - window.matchMedia("(prefers-color-scheme: dark)").matches, - ); - - useEffect(() => { - const mql = window.matchMedia("(prefers-color-scheme: dark)"); - const handler = (e: MediaQueryListEvent) => setDarkMode(e.matches); - mql.addEventListener("change", handler); - return () => mql.removeEventListener("change", handler); - }, []); + const [darkMode, toggleDarkMode] = useDarkMode(); useEffect(() => { setAccessToken(auth.user?.access_token ?? null); @@ -409,22 +419,13 @@ function AuthenticatedApp() { darkMode={darkMode} authLabel={authLabel} onSignOut={() => void auth.signoutRedirect()} - onToggleDarkMode={() => setDarkMode(!darkMode)} + onToggleDarkMode={toggleDarkMode} /> ); } function AppInner() { - const [prefersDarkMode, setPrefersDarkMode] = useState(() => - window.matchMedia("(prefers-color-scheme: dark)").matches, - ); - - useEffect(() => { - const mql = window.matchMedia("(prefers-color-scheme: dark)"); - const handler = (e: MediaQueryListEvent) => setPrefersDarkMode(e.matches); - mql.addEventListener("change", handler); - return () => mql.removeEventListener("change", handler); - }, []); + const [darkMode, toggleDarkMode] = useDarkMode(); return ( @@ -452,10 +453,8 @@ function AppInner() { - setPrefersDarkMode(!prefersDarkMode) - } + darkMode={darkMode} + onToggleDarkMode={toggleDarkMode} /> } > diff --git a/frontend/src/index.css b/frontend/src/index.css index b07c63e..2aed269 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,5 @@ -@import "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@import "tailwindcss"; @theme { --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; @@ -39,6 +39,41 @@ --color-sidebar-ring: #4f8cff; } +.dark { + --color-background: #0f172a; + --color-foreground: #f8fafc; + --color-card: #1e293b; + --color-card-foreground: #f8fafc; + --color-popover: #1e293b; + --color-popover-foreground: #f8fafc; + --color-primary: #4f8cff; + --color-primary-foreground: #ffffff; + --color-secondary: #1e293b; + --color-secondary-foreground: #f8fafc; + --color-muted: #334155; + --color-muted-foreground: #94a3b8; + --color-accent: #334155; + --color-accent-foreground: #f8fafc; + --color-destructive: #ef4444; + --color-destructive-foreground: #ffffff; + --color-border: #334155; + --color-input: #334155; + --color-ring: #4f8cff; + --color-chart-1: #4f8cff; + --color-chart-2: #22c55e; + --color-chart-3: #f59e0b; + --color-chart-4: #ef4444; + --color-chart-5: #8b5cf6; + --color-sidebar-background: #0f172a; + --color-sidebar-foreground: #f8fafc; + --color-sidebar-primary: #4f8cff; + --color-sidebar-primary-foreground: #ffffff; + --color-sidebar-accent: #334155; + --color-sidebar-accent-foreground: #f8fafc; + --color-sidebar-border: #334155; + --color-sidebar-ring: #4f8cff; +} + @layer base { body { background-color: var(--color-background);