File size: 3,680 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | "use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { THEME_CONFIG } from "@/shared/constants/appConfig";
interface ThemeState {
theme: string;
colorTheme: string;
customColor: string;
setTheme: (theme: string) => void;
setColorTheme: (colorTheme: string) => void;
setCustomColorTheme: (color: string) => void;
toggleTheme: () => void;
initTheme: () => void;
}
const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: THEME_CONFIG.defaultTheme,
colorTheme: "coral",
customColor: "#3b82f6",
setTheme: (theme) => {
set({ theme });
applyTheme(theme);
},
setColorTheme: (colorTheme) => {
set({ colorTheme });
applyColorTheme(colorTheme, get().customColor);
},
setCustomColorTheme: (color) => {
const normalized = normalizeHexColor(color);
set({ colorTheme: "custom", customColor: normalized });
applyColorTheme("custom", normalized);
},
toggleTheme: () => {
const currentTheme = get().theme;
const newTheme = currentTheme === "dark" ? "light" : "dark";
set({ theme: newTheme });
applyTheme(newTheme);
},
initTheme: () => {
const { theme, colorTheme, customColor } = get();
applyTheme(theme);
applyColorTheme(colorTheme, customColor);
},
}),
{
name: THEME_CONFIG.storageKey,
}
)
);
export const COLOR_THEMES: Record<string, string> = {
coral: "#e54d5e",
blue: "#3b82f6",
red: "#ef4444",
green: "#22c55e",
violet: "#8b5cf6",
orange: "#f97316",
cyan: "#06b6d4",
};
// Apply light/dark theme to document
function applyTheme(theme: string) {
if (typeof window === "undefined") return;
const root = document.documentElement;
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const effectiveTheme = theme === "system" ? systemTheme : theme;
if (effectiveTheme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}
function applyColorTheme(colorTheme: string, customColor: string) {
if (typeof window === "undefined") return;
const root = document.documentElement;
const baseColor =
colorTheme === "custom"
? normalizeHexColor(customColor)
: COLOR_THEMES[colorTheme] || COLOR_THEMES.coral;
const hoverColor = shadeHexColor(baseColor, -0.14);
root.style.setProperty("--color-primary", baseColor);
root.style.setProperty("--color-primary-hover", hoverColor);
}
function normalizeHexColor(color: string) {
const value = (color || "").trim();
const hex = value.startsWith("#") ? value : `#${value}`;
const valid = /^#([0-9a-fA-F]{6})$/.test(hex);
return valid ? hex.toLowerCase() : "#3b82f6";
}
function shadeHexColor(hex: string, percent: number) {
const normalized = normalizeHexColor(hex).slice(1);
const r = parseInt(normalized.slice(0, 2), 16);
const g = parseInt(normalized.slice(2, 4), 16);
const b = parseInt(normalized.slice(4, 6), 16);
const shade = (channel: number) => {
const target = percent < 0 ? 0 : 255;
const amount = Math.round((target - channel) * Math.abs(percent));
const next = percent < 0 ? channel - amount : channel + amount;
return Math.max(0, Math.min(255, next));
};
const toHex = (channel: number) => channel.toString(16).padStart(2, "0");
return `#${toHex(shade(r))}${toHex(shade(g))}${toHex(shade(b))}`;
}
export default useThemeStore;
|