From e025df9b87e25013fb4205b6c229cee8c7359361 Mon Sep 17 00:00:00 2001 From: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:37:13 -0800 Subject: [PATCH] feat: dedicated dark theme option with system preference support (#5007) * implement OS level theme switching and dark mode option * simplify * fix logo bug in login | place back useTheme comment --------- Co-authored-by: shatfield4 Co-authored-by: Timothy Carambat --- frontend/src/LogoContext.jsx | 19 +++++++++-------- frontend/src/hooks/useTheme.js | 37 +++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/frontend/src/LogoContext.jsx b/frontend/src/LogoContext.jsx index 3bf499da..6cbf0ea3 100644 --- a/frontend/src/LogoContext.jsx +++ b/frontend/src/LogoContext.jsx @@ -6,18 +6,21 @@ import DefaultLoginLogoDark from "./media/illustrations/login-logo-light.svg"; import System from "./models/system"; export const REFETCH_LOGO_EVENT = "refetch-logo"; + +function isLightMode() { + return document.documentElement.getAttribute("data-theme") === "light"; +} export const LogoContext = createContext(); export function LogoProvider({ children }) { const [logo, setLogo] = useState(""); const [loginLogo, setLoginLogo] = useState(""); const [isCustomLogo, setIsCustomLogo] = useState(false); - const DefaultLoginLogo = - localStorage.getItem("theme") !== "default" - ? DefaultLoginLogoDark - : DefaultLoginLogoLight; async function fetchInstanceLogo() { + const DefaultLoginLogo = isLightMode() + ? DefaultLoginLogoDark + : DefaultLoginLogoLight; try { const { isCustomLogo, logoURL } = await System.fetchLogo(); if (logoURL) { @@ -25,16 +28,12 @@ export function LogoProvider({ children }) { setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo); setIsCustomLogo(isCustomLogo); } else { - localStorage.getItem("theme") !== "default" - ? setLogo(AnythingLLMDark) - : setLogo(AnythingLLM); + isLightMode() ? setLogo(AnythingLLMDark) : setLogo(AnythingLLM); setLoginLogo(DefaultLoginLogo); setIsCustomLogo(false); } } catch (err) { - localStorage.getItem("theme") !== "default" - ? setLogo(AnythingLLMDark) - : setLogo(AnythingLLM); + isLightMode() ? setLogo(AnythingLLMDark) : setLogo(AnythingLLM); setLoginLogo(DefaultLoginLogo); setIsCustomLogo(false); console.error("Failed to fetch logo:", err); diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js index 0ebb9c87..ba1d82e7 100644 --- a/frontend/src/hooks/useTheme.js +++ b/frontend/src/hooks/useTheme.js @@ -2,33 +2,46 @@ import { REFETCH_LOGO_EVENT } from "@/LogoContext"; import { useState, useEffect } from "react"; const availableThemes = { - default: "Default", + system: "System", light: "Light", + dark: "Dark", }; /** - * Determines the current theme of the application - * @returns {{theme: ('default' | 'light'), setTheme: function, availableThemes: object}} The current theme, a function to set the theme, and the available themes + * Determines the current theme of the application. + * "system" follows the OS preference, "light" and "dark" force that mode. + * @returns {{theme: ('system' | 'light' | 'dark'), setTheme: function, availableThemes: object}} */ export function useTheme() { const [theme, _setTheme] = useState(() => { - return localStorage.getItem("theme") || "default"; + const stored = localStorage.getItem("theme"); + if (stored === "default") return "dark"; // migrate legacy value + return stored || "system"; }); + const [systemTheme, setSystemTheme] = useState(() => + window.matchMedia?.("(prefers-color-scheme: light)").matches + ? "light" + : "dark" + ); + + // Listen for OS level theme changes useEffect(() => { - if (localStorage.getItem("theme") !== null) return; if (!window.matchMedia) return; - if (window.matchMedia("(prefers-color-scheme: light)").matches) - return _setTheme("light"); - _setTheme("default"); + const mql = window.matchMedia("(prefers-color-scheme: light)"); + const handler = (e) => setSystemTheme(e.matches ? "light" : "dark"); + mql.addEventListener("change", handler); + return () => mql.removeEventListener("change", handler); }, []); + const resolvedTheme = theme === "system" ? systemTheme : theme; + useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); - document.body.classList.toggle("light", theme === "light"); + document.documentElement.setAttribute("data-theme", resolvedTheme); + document.body.classList.toggle("light", resolvedTheme === "light"); localStorage.setItem("theme", theme); window.dispatchEvent(new Event(REFETCH_LOGO_EVENT)); - }, [theme]); + }, [resolvedTheme, theme]); // In development, attach keybind combinations to toggle theme useEffect(() => { @@ -36,7 +49,7 @@ export function useTheme() { function toggleOnKeybind(e) { if (e.metaKey && e.key === ".") { e.preventDefault(); - setTheme((prev) => (prev === "light" ? "default" : "light")); + _setTheme((prev) => (prev === "light" ? "dark" : "light")); } } document.addEventListener("keydown", toggleOnKeybind);