summaryrefslogtreecommitdiff
path: root/front/src/styles/ThemeProvider.tsx
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-09-11 01:48:14 +0700
committerpolwex <polwex@sortug.com>2025-09-11 01:48:14 +0700
commitb1d68ac307ed87d63e83820cbdf843fff0fd9f7f (patch)
treed6a684a70a80509e68ff667b842aa4e4c091906f /front/src/styles/ThemeProvider.tsx
init
Diffstat (limited to 'front/src/styles/ThemeProvider.tsx')
-rw-r--r--front/src/styles/ThemeProvider.tsx302
1 files changed, 302 insertions, 0 deletions
diff --git a/front/src/styles/ThemeProvider.tsx b/front/src/styles/ThemeProvider.tsx
new file mode 100644
index 0000000..2cc0ca6
--- /dev/null
+++ b/front/src/styles/ThemeProvider.tsx
@@ -0,0 +1,302 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from "react";
+
+export type ThemeName =
+ | "light"
+ | "dark"
+ | "sepia"
+ | "noir"
+ | "ocean"
+ | "forest"
+ | "gruvbox";
+
+export interface ThemeColors {
+ primary: string;
+ primaryHover: string;
+ secondary: string;
+ background: string;
+ surface: string;
+ surfaceHover: string;
+ text: string;
+ textSecondary: string;
+ textMuted: string;
+ border: string;
+ borderLight: string;
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ link: string;
+ linkHover: string;
+ shadow: string;
+ overlay: string;
+}
+
+export interface Theme {
+ name: ThemeName;
+ colors: ThemeColors;
+}
+
+const themes: Record<ThemeName, Theme> = {
+ light: {
+ name: "light",
+ colors: {
+ primary: "#543fd7",
+ primaryHover: "#4532b8",
+ secondary: "#f39c12",
+ background: "#ffffff",
+ surface: "#f8f9fa",
+ surfaceHover: "#e9ecef",
+ text: "#212529",
+ textSecondary: "#495057",
+ textMuted: "#6c757d",
+ border: "#dee2e6",
+ borderLight: "#e9ecef",
+ success: "#28a745",
+ warning: "#ffc107",
+ error: "#dc3545",
+ info: "#17a2b8",
+ link: "#543fd7",
+ linkHover: "#4532b8",
+ shadow: "rgba(0, 0, 0, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.5)",
+ },
+ },
+ dark: {
+ name: "dark",
+ colors: {
+ primary: "#7c6ef7",
+ primaryHover: "#9085f9",
+ secondary: "#f39c12",
+ background: "#0d1117",
+ surface: "#161b22",
+ surfaceHover: "#21262d",
+ text: "#c9d1d9",
+ textSecondary: "#8b949e",
+ textMuted: "#6e7681",
+ border: "#30363d",
+ borderLight: "#21262d",
+ success: "#3fb950",
+ warning: "#d29922",
+ error: "#f85149",
+ info: "#58a6ff",
+ link: "#58a6ff",
+ linkHover: "#79b8ff",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(0, 0, 0, 0.7)",
+ },
+ },
+ sepia: {
+ name: "sepia",
+ colors: {
+ primary: "#8b4513",
+ primaryHover: "#6b3410",
+ secondary: "#d2691e",
+ background: "#f4e8d0",
+ surface: "#ede0c8",
+ surfaceHover: "#e6d9c0",
+ text: "#3e2723",
+ textSecondary: "#5d4037",
+ textMuted: "#6d4c41",
+ border: "#d7ccc8",
+ borderLight: "#e0d5d0",
+ success: "#689f38",
+ warning: "#ff9800",
+ error: "#d32f2f",
+ info: "#0288d1",
+ link: "#8b4513",
+ linkHover: "#6b3410",
+ shadow: "rgba(62, 39, 35, 0.1)",
+ overlay: "rgba(62, 39, 35, 0.5)",
+ },
+ },
+ noir: {
+ name: "noir",
+ colors: {
+ primary: "#ffffff",
+ primaryHover: "#e0e0e0",
+ secondary: "#808080",
+ background: "#000000",
+ surface: "#0a0a0a",
+ surfaceHover: "#1a1a1a",
+ text: "#ffffff",
+ textSecondary: "#b0b0b0",
+ textMuted: "#808080",
+ border: "#333333",
+ borderLight: "#1a1a1a",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#2196f3",
+ link: "#b0b0b0",
+ linkHover: "#ffffff",
+ shadow: "rgba(255, 255, 255, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.9)",
+ },
+ },
+ ocean: {
+ name: "ocean",
+ colors: {
+ primary: "#006994",
+ primaryHover: "#005577",
+ secondary: "#00acc1",
+ background: "#e1f5fe",
+ surface: "#b3e5fc",
+ surfaceHover: "#81d4fa",
+ text: "#01579b",
+ textSecondary: "#0277bd",
+ textMuted: "#4fc3f7",
+ border: "#81d4fa",
+ borderLight: "#b3e5fc",
+ success: "#00c853",
+ warning: "#ffab00",
+ error: "#d50000",
+ info: "#00b0ff",
+ link: "#0277bd",
+ linkHover: "#01579b",
+ shadow: "rgba(1, 87, 155, 0.1)",
+ overlay: "rgba(1, 87, 155, 0.5)",
+ },
+ },
+ forest: {
+ name: "forest",
+ colors: {
+ primary: "#2e7d32",
+ primaryHover: "#1b5e20",
+ secondary: "#689f38",
+ background: "#f1f8e9",
+ surface: "#dcedc8",
+ surfaceHover: "#c5e1a5",
+ text: "#1b5e20",
+ textSecondary: "#33691e",
+ textMuted: "#558b2f",
+ border: "#aed581",
+ borderLight: "#c5e1a5",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#03a9f4",
+ link: "#388e3c",
+ linkHover: "#2e7d32",
+ shadow: "rgba(27, 94, 32, 0.1)",
+ overlay: "rgba(27, 94, 32, 0.5)",
+ },
+ },
+ gruvbox: {
+ name: "gruvbox",
+ colors: {
+ primary: "#fe8019",
+ primaryHover: "#d65d0e",
+ secondary: "#fabd2f",
+ background: "#282828",
+ surface: "#3c3836",
+ surfaceHover: "#504945",
+ text: "#ebdbb2",
+ textSecondary: "#d5c4a1",
+ textMuted: "#bdae93",
+ border: "#665c54",
+ borderLight: "#504945",
+ success: "#b8bb26",
+ warning: "#fabd2f",
+ error: "#fb4934",
+ info: "#83a598",
+ link: "#8ec07c",
+ linkHover: "#b8bb26",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(40, 40, 40, 0.8)",
+ },
+ },
+};
+
+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);
+
+ Object.entries(theme.colors).forEach(([key, value]) => {
+ const cssVarName = `--color-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ 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;
+};