mirror of https://github.com/gabehf/Koito.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.5 KiB
132 lines
3.5 KiB
import {
|
|
createContext,
|
|
useEffect,
|
|
useState,
|
|
useCallback,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { type Theme, themes } from "~/styles/themes.css";
|
|
import { themeVars } from "~/styles/vars.css";
|
|
import { useAppContext } from "./AppProvider";
|
|
|
|
interface ThemeContextValue {
|
|
themeName: string;
|
|
theme: Theme;
|
|
setTheme: (theme: string) => void;
|
|
resetTheme: () => void;
|
|
setCustomTheme: (theme: Theme) => void;
|
|
getCustomTheme: () => Theme | undefined;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
|
|
function toKebabCase(str: string) {
|
|
return str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
}
|
|
|
|
function applyCustomThemeVars(theme: Theme) {
|
|
const root = document.documentElement;
|
|
for (const [key, value] of Object.entries(theme)) {
|
|
if (key === "name") continue;
|
|
root.style.setProperty(`--color-${toKebabCase(key)}`, value);
|
|
}
|
|
}
|
|
|
|
function clearCustomThemeVars() {
|
|
for (const cssVar of Object.values(themeVars)) {
|
|
document.documentElement.style.removeProperty(cssVar);
|
|
}
|
|
}
|
|
|
|
function getStoredCustomTheme(): Theme | undefined {
|
|
const themeStr = localStorage.getItem("custom-theme");
|
|
if (!themeStr) return undefined;
|
|
try {
|
|
const parsed = JSON.parse(themeStr);
|
|
const { name, ...theme } = parsed;
|
|
return theme as Theme;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
let defaultTheme = useAppContext().defaultTheme;
|
|
let initialTheme = localStorage.getItem("theme") ?? defaultTheme;
|
|
const [themeName, setThemeName] = useState(initialTheme);
|
|
const [currentTheme, setCurrentTheme] = useState<Theme>(() => {
|
|
if (initialTheme === "custom") {
|
|
const customTheme = getStoredCustomTheme();
|
|
return customTheme || themes[defaultTheme];
|
|
}
|
|
return themes[initialTheme] || themes[defaultTheme];
|
|
});
|
|
|
|
const setTheme = (newThemeName: string) => {
|
|
setThemeName(newThemeName);
|
|
if (newThemeName === "custom") {
|
|
const customTheme = getStoredCustomTheme();
|
|
if (customTheme) {
|
|
setCurrentTheme(customTheme);
|
|
} else {
|
|
// Fallback to default theme if no custom theme found
|
|
setThemeName(defaultTheme);
|
|
setCurrentTheme(themes[defaultTheme]);
|
|
}
|
|
} else {
|
|
const foundTheme = themes[newThemeName];
|
|
if (foundTheme) {
|
|
localStorage.setItem("theme", newThemeName);
|
|
setCurrentTheme(foundTheme);
|
|
}
|
|
}
|
|
};
|
|
|
|
const resetTheme = () => {
|
|
setThemeName(defaultTheme);
|
|
localStorage.removeItem("theme");
|
|
setCurrentTheme(themes[defaultTheme]);
|
|
};
|
|
|
|
const setCustomTheme = useCallback((customTheme: Theme) => {
|
|
localStorage.setItem("custom-theme", JSON.stringify(customTheme));
|
|
applyCustomThemeVars(customTheme);
|
|
setThemeName("custom");
|
|
localStorage.setItem("theme", "custom");
|
|
setCurrentTheme(customTheme);
|
|
}, []);
|
|
|
|
const getCustomTheme = (): Theme | undefined => {
|
|
return getStoredCustomTheme();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
|
|
root.setAttribute("data-theme", themeName);
|
|
|
|
if (themeName === "custom") {
|
|
applyCustomThemeVars(currentTheme);
|
|
} else {
|
|
clearCustomThemeVars();
|
|
}
|
|
}, [themeName, currentTheme]);
|
|
|
|
return (
|
|
<ThemeContext.Provider
|
|
value={{
|
|
themeName,
|
|
theme: currentTheme,
|
|
setTheme,
|
|
resetTheme,
|
|
setCustomTheme,
|
|
getCustomTheme,
|
|
}}
|
|
>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export { ThemeContext };
|