Files
tweakcn/components/theme-provider.tsx
T

139 lines
3.7 KiB
TypeScript

"use client";
import { createContext, useContext, useEffect } from "react";
import { useEditorStore } from "../store/editor-store";
import { colorFormatter } from "../utils/color-converter";
import { setShadowVariables } from "@/utils/shadows";
import { applyStyleToElement } from "@/utils/apply-style-to-element";
import { ThemeStyleProps, ThemeStyles } from "@/types/theme";
import { useThemePresetFromUrl } from "@/hooks/use-theme-preset-from-url";
import { COMMON_STYLES } from "@/config/theme";
type Theme = "dark" | "light";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
};
type Coords = { x: number; y: number };
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: (coords?: Coords) => void;
};
const COMMON_NON_COLOR_KEYS = COMMON_STYLES;
const initialState: ThemeProviderState = {
theme: "light",
setTheme: () => null,
toggleTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
// Helper functions
const applyCommonStyles = (root: HTMLElement, themeStyles: ThemeStyleProps) => {
Object.entries(themeStyles)
.filter(([key]) =>
COMMON_NON_COLOR_KEYS.includes(key as (typeof COMMON_NON_COLOR_KEYS)[number])
)
.forEach(([key, value]) => {
if (typeof value === "string") {
applyStyleToElement(root, key, value);
}
});
};
const applyThemeColors = (
root: HTMLElement,
themeStyles: ThemeStyles,
mode: Theme
) => {
Object.entries(themeStyles[mode]).forEach(([key, value]) => {
if (
typeof value === "string" &&
!COMMON_NON_COLOR_KEYS.includes(key as (typeof COMMON_NON_COLOR_KEYS)[number])
) {
const hslValue = colorFormatter(value, "hsl", "4");
applyStyleToElement(root, key, hslValue);
}
});
};
const updateThemeClass = (root: HTMLElement, mode: Theme) => {
if (mode === "light") {
root.classList.remove("dark");
} else {
root.classList.add("dark");
}
};
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const { themeState, setThemeState } = useEditorStore();
// Handle theme preset from URL
useThemePresetFromUrl();
useEffect(() => {
const root = window.document.documentElement;
const { currentMode: mode, styles: themeStyles } = themeState;
updateThemeClass(root, mode);
applyCommonStyles(root, themeStyles.light);
applyThemeColors(root, themeStyles, mode);
setShadowVariables(themeState);
}, [themeState]);
const handleThemeChange = (newMode: Theme) => {
setThemeState({ ...themeState, currentMode: newMode });
};
const handleThemeToggle = (coords?: Coords) => {
const root = document.documentElement;
const newMode = themeState.currentMode === "light" ? "dark" : "light";
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (!document.startViewTransition || prefersReducedMotion) {
handleThemeChange(newMode);
return;
}
if (coords) {
root.style.setProperty("--x", `${coords.x}px`);
root.style.setProperty("--y", `${coords.y}px`);
}
document.startViewTransition(() => {
handleThemeChange(newMode);
});
};
const value: ThemeProviderState = {
theme: themeState.currentMode,
setTheme: handleThemeChange,
toggleTheme: handleThemeToggle,
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};