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 <seanhatfield5@gmail.com>
Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton 2026-02-19 12:37:13 -08:00 committed by GitHub
parent a961fb8f75
commit e025df9b87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 22 deletions

View File

@ -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);

View File

@ -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);