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 <html> to trigger CSS variable overrides Also add dark-mode color overrides for all Tailwind v4 theme variables in index.css. Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
+24
-25
@@ -24,6 +24,7 @@ import BackupsPage from "./components/BackupsPage";
|
|||||||
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
||||||
import { fetchAppVersion } from "./api/client";
|
import { fetchAppVersion } from "./api/client";
|
||||||
import { FRONTEND_VERSION_LABEL } from "./version";
|
import { FRONTEND_VERSION_LABEL } from "./version";
|
||||||
|
import { usePersistentState } from "./hooks/usePersistentState";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -58,6 +59,24 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function useDarkMode() {
|
||||||
|
const [darkMode, setDarkMode] = usePersistentState<boolean>(
|
||||||
|
"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
|
// Navigation items for sidebar
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||||
@@ -362,16 +381,7 @@ function SignInScreen({ onSignIn }: { onSignIn: () => void }) {
|
|||||||
|
|
||||||
function AuthenticatedApp() {
|
function AuthenticatedApp() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [darkMode, setDarkMode] = useState(() =>
|
const [darkMode, toggleDarkMode] = useDarkMode();
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccessToken(auth.user?.access_token ?? null);
|
setAccessToken(auth.user?.access_token ?? null);
|
||||||
@@ -409,22 +419,13 @@ function AuthenticatedApp() {
|
|||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
authLabel={authLabel}
|
authLabel={authLabel}
|
||||||
onSignOut={() => void auth.signoutRedirect()}
|
onSignOut={() => void auth.signoutRedirect()}
|
||||||
onToggleDarkMode={() => setDarkMode(!darkMode)}
|
onToggleDarkMode={toggleDarkMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppInner() {
|
function AppInner() {
|
||||||
const [prefersDarkMode, setPrefersDarkMode] = useState(() =>
|
const [darkMode, toggleDarkMode] = useDarkMode();
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
@@ -452,10 +453,8 @@ function AppInner() {
|
|||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
<ShellLayout
|
<ShellLayout
|
||||||
darkMode={prefersDarkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={() =>
|
onToggleDarkMode={toggleDarkMode}
|
||||||
setPrefersDarkMode(!prefersDarkMode)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
+36
-1
@@ -1,5 +1,5 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
@@ -39,6 +39,41 @@
|
|||||||
--color-sidebar-ring: #4f8cff;
|
--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 {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
|
|||||||
Reference in New Issue
Block a user