Files
OmniRoute/src/store/themeStore.ts
diegosouzapw c4f1990aff fix: address agent review issues for theme color settings (#174)
Code Quality Improvements:
- Export COLOR_THEMES from themeStore.ts for reuse (DRY)
- Add coral preset to color list (fixes default inconsistency)
- Sync local customThemeColor state reactively via Zustand subscribe
- Add hex color validation with visual feedback (red border + disabled button)
- Remove dead /themes route from Header.tsx (page doesn't exist)
- Add CSS color-mix() fallback for older browsers
- Add themeCoral i18n key to all 30 locale files
2026-03-02 12:12:52 -03:00

124 lines
3.5 KiB
TypeScript

"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { THEME_CONFIG } from "@/shared/constants/config";
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;