|
|
|
import React, {
|
|
|
|
createContext,
|
|
|
|
ReactElement,
|
|
|
|
useContext,
|
|
|
|
useEffect,
|
|
|
|
useState,
|
|
|
|
} from "react";
|
|
|
|
|
|
|
|
type BuiltInThemes = "light" | "dark";
|
|
|
|
|
|
|
|
export interface Theme {
|
|
|
|
name: BuiltInThemes | "custom";
|
|
|
|
palette: Palette;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type SetThemeInput = BuiltInThemes | Partial<Palette>;
|
|
|
|
|
|
|
|
export const PALETTE_NAMES = [
|
|
|
|
"--primary-background",
|
|
|
|
"--secondary-background",
|
|
|
|
|
|
|
|
"--primary-accent",
|
|
|
|
"--primary-accent-soft",
|
|
|
|
"--primary-accent-light",
|
|
|
|
"--primary-accent-lighter",
|
|
|
|
"--primary-accent-lightest",
|
|
|
|
|
|
|
|
"--secondary-accent",
|
|
|
|
"--secondary-accent-light",
|
|
|
|
|
|
|
|
"--primary-heading",
|
|
|
|
"--secondary-heading",
|
|
|
|
|
|
|
|
"--text",
|
|
|
|
|
|
|
|
"--form-invalid",
|
|
|
|
|
|
|
|
"--input-background",
|
|
|
|
"--input-placeholder-text",
|
|
|
|
"--input-text",
|
|
|
|
|
|
|
|
"--code-background",
|
|
|
|
|
|
|
|
"--navbar-page-overlay",
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
export const emptyPalette = PALETTE_NAMES.reduce(
|
|
|
|
(partial, varName) => ({
|
|
|
|
...partial,
|
|
|
|
[varName]: "#c4e0f8",
|
|
|
|
}),
|
|
|
|
{} as Palette
|
|
|
|
);
|
|
|
|
|
|
|
|
const ThemeContext =
|
|
|
|
createContext<
|
|
|
|
| {
|
|
|
|
theme: Theme;
|
|
|
|
setTheme(input: SetThemeInput): void;
|
|
|
|
save(): void;
|
|
|
|
clearSaved(): void;
|
|
|
|
}
|
|
|
|
| undefined
|
|
|
|
>(undefined);
|
|
|
|
|
|
|
|
export interface Props {
|
|
|
|
children?: ReactElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function ThemeProvider(props: Props) {
|
|
|
|
const update = useForcedUpdate();
|
|
|
|
const [themeName, setThemeName] =
|
|
|
|
useState<Theme["name"] | undefined>(undefined);
|
|
|
|
|
|
|
|
const setTheme = (input: SetThemeInput) => {
|
|
|
|
if (typeof input === "string") {
|
|
|
|
PALETTE_NAMES.forEach((name) =>
|
|
|
|
document.body.style.setProperty(name, "")
|
|
|
|
);
|
|
|
|
|
|
|
|
if (input === "light") {
|
|
|
|
document.body.classList.remove("dark");
|
|
|
|
} else if (input === "dark") {
|
|
|
|
document.body.classList.add("dark");
|
|
|
|
}
|
|
|
|
|
|
|
|
setThemeName(input);
|
|
|
|
} else {
|
|
|
|
const properties = Object.keys(input) as PaletteNames[];
|
|
|
|
|
|
|
|
properties.forEach((property) =>
|
|
|
|
document.body.style.setProperty(
|
|
|
|
property,
|
|
|
|
input[property]?.trim() ?? null
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
setThemeName("custom");
|
|
|
|
}
|
|
|
|
|
|
|
|
update();
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const customPalette = getSavedPalette();
|
|
|
|
|
|
|
|
if (customPalette == null) {
|
|
|
|
setThemeName("light");
|
|
|
|
} else {
|
|
|
|
setTheme(customPalette);
|
|
|
|
setThemeName("custom");
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ThemeContext.Provider
|
|
|
|
value={
|
|
|
|
themeName == null
|
|
|
|
? undefined
|
|
|
|
: {
|
|
|
|
theme: {
|
|
|
|
name: themeName,
|
|
|
|
get palette() {
|
|
|
|
return getCurrentPalette();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
setTheme,
|
|
|
|
save: () => savePalette(getCurrentPalette()),
|
|
|
|
clearSaved: clearSavedPalette,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{props.children}
|
|
|
|
</ThemeContext.Provider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useThemeContext() {
|
|
|
|
return useContext(ThemeContext);
|
|
|
|
}
|
|
|
|
|
|
|
|
export type PaletteNames = typeof PALETTE_NAMES[number];
|
|
|
|
|
|
|
|
export type Palette = {
|
|
|
|
[key in PaletteNames]: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
function getCurrentPalette() {
|
|
|
|
const styles = getComputedStyle(document.body);
|
|
|
|
|
|
|
|
return PALETTE_NAMES.reduce(
|
|
|
|
(partial, varName) => ({
|
|
|
|
...partial,
|
|
|
|
[varName]: styles.getPropertyValue(varName).trim(),
|
|
|
|
}),
|
|
|
|
{} as Palette
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function useForcedUpdate() {
|
|
|
|
const [fakeState, setFakeState] = useState(true);
|
|
|
|
|
|
|
|
return () => setFakeState(!fakeState);
|
|
|
|
}
|
|
|
|
|
|
|
|
const STORAGE_KEY = "csc-theme-palette";
|
|
|
|
|
|
|
|
function getSavedPalette() {
|
|
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
|
|
|
|
|
|
return raw == null ? undefined : (JSON.parse(raw) as Palette);
|
|
|
|
}
|
|
|
|
|
|
|
|
function savePalette(palette: Palette) {
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(palette));
|
|
|
|
}
|
|
|
|
|
|
|
|
function clearSavedPalette() {
|
|
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
|
|
}
|