summaryrefslogtreecommitdiff
path: root/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/prosody-ui/src/themes/ThemeSwitcher.tsx')
-rw-r--r--packages/prosody-ui/src/themes/ThemeSwitcher.tsx130
1 files changed, 130 insertions, 0 deletions
diff --git a/packages/prosody-ui/src/themes/ThemeSwitcher.tsx b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
new file mode 100644
index 0000000..bae617f
--- /dev/null
+++ b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
@@ -0,0 +1,130 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from "react";
+import { themes, type Theme, type ThemeName } from "./themes";
+
+interface ThemeContextType {
+ theme: Theme;
+ themeName: ThemeName;
+ setTheme: (name: ThemeName) => void;
+ availableThemes: ThemeName[];
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+interface ThemeProviderProps {
+ children: ReactNode;
+ defaultTheme?: ThemeName;
+}
+
+export const ThemeProvider: React.FC<ThemeProviderProps> = ({
+ children,
+ defaultTheme = "light",
+}) => {
+ const [themeName, setThemeName] = useState<ThemeName>(() => {
+ const savedTheme = localStorage.getItem("theme") as ThemeName;
+ if (savedTheme && themes[savedTheme]) {
+ return savedTheme;
+ }
+
+ if (
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+ ) {
+ return "dark";
+ }
+
+ return defaultTheme;
+ });
+
+ const theme = themes[themeName];
+
+ useEffect(() => {
+ const root = document.documentElement;
+
+ root.setAttribute("data-theme", themeName);
+
+ // Set color variables
+ Object.entries(theme.colors).forEach(([key, value]) => {
+ const cssVarName = `--color-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set typography variables
+ Object.entries(theme.typography).forEach(([key, value]) => {
+ const cssVarName = `--${key
+ .replace(/([A-Z])/g, "-$1")
+ .toLowerCase()
+ .replace("font-", "font-")
+ .replace("size", "")
+ .replace("weight", "")}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set spacing variables
+ Object.entries(theme.spacing).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set radius variables
+ Object.entries(theme.radius).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set transition variables
+ Object.entries(theme.transitions).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Legacy variables for backward compatibility
+ root.style.setProperty("--text-color", theme.colors.text);
+ root.style.setProperty("--background-color", theme.colors.background);
+
+ localStorage.setItem("theme", themeName);
+ }, [themeName, theme]);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = (e: MediaQueryListEvent) => {
+ const savedTheme = localStorage.getItem("theme");
+ if (!savedTheme) {
+ setThemeName(e.matches ? "dark" : "light");
+ }
+ };
+
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, []);
+
+ const setTheme = (name: ThemeName) => {
+ if (themes[name]) {
+ setThemeName(name);
+ }
+ };
+
+ const value: ThemeContextType = {
+ theme,
+ themeName,
+ setTheme,
+ availableThemes: Object.keys(themes) as ThemeName[],
+ };
+
+ return (
+ <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
+ );
+};
+
+export const useTheme = (): ThemeContextType => {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+ return context;
+};