setTheme(theme.name)} className="rounded-md p-3 sm:p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
-
setTheme(themeName)} className="rounded-md p-3 sm:p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
+
{capitalizeFirstLetter(themeName)}
diff --git a/client/app/components/themeSwitcher/ThemeSwitcher.tsx b/client/app/components/themeSwitcher/ThemeSwitcher.tsx
index 14eda1e..fdc8709 100644
--- a/client/app/components/themeSwitcher/ThemeSwitcher.tsx
+++ b/client/app/components/themeSwitcher/ThemeSwitcher.tsx
@@ -6,7 +6,7 @@ import ThemeOption from './ThemeOption';
import { AsyncButton } from '../AsyncButton';
export function ThemeSwitcher() {
- const { theme, setTheme } = useTheme();
+ const { theme, themeName, setTheme } = useTheme();
const initialTheme = {
bg: "#1e1816",
bgSecondary: "#2f2623",
@@ -30,30 +30,22 @@ export function ThemeSwitcher() {
const handleCustomTheme = () => {
console.log(custom)
try {
- const theme = JSON.parse(custom)
- theme.name = "custom"
- setCustomTheme(theme)
- delete theme.name
- setCustom(JSON.stringify(theme, null, " "))
- console.log(theme)
+ const themeData = JSON.parse(custom)
+ setCustomTheme(themeData)
+ setCustom(JSON.stringify(themeData, null, " "))
+ console.log(themeData)
} catch(err) {
console.log(err)
}
}
- useEffect(() => {
- if (theme) {
- setTheme(theme)
- }
- }, [theme]);
-
return (
Select Theme
- {themes.map((t) => (
-
+ {Object.entries(themes).map(([name, themeData]) => (
+
))}
diff --git a/client/app/providers/ThemeProvider.tsx b/client/app/providers/ThemeProvider.tsx
index 52d9ef9..1a4f9e8 100644
--- a/client/app/providers/ThemeProvider.tsx
+++ b/client/app/providers/ThemeProvider.tsx
@@ -1,9 +1,10 @@
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react';
-import { type Theme } from '~/styles/themes.css';
+import { type Theme, themes } from '~/styles/themes.css';
import { themeVars } from '~/styles/vars.css';
interface ThemeContextValue {
- theme: string;
+ themeName: string;
+ theme: Theme;
setTheme: (theme: string) => void;
setCustomTheme: (theme: Theme) => void;
getCustomTheme: () => Theme | undefined;
@@ -29,6 +30,18 @@ function clearCustomThemeVars() {
}
}
+function getStoredCustomTheme(): Theme | undefined {
+ const themeStr = localStorage.getItem('custom-theme');
+ if (!themeStr) return undefined;
+ try {
+ const parsed = JSON.parse(themeStr);
+ const { name, ...theme } = parsed;
+ return theme as Theme;
+ } catch {
+ return undefined;
+ }
+}
+
export function ThemeProvider({
theme: initialTheme,
children,
@@ -36,57 +49,60 @@ export function ThemeProvider({
theme: string;
children: ReactNode;
}) {
- const [theme, setThemeName] = useState(initialTheme);
+ const [themeName, setThemeName] = useState(initialTheme);
+ const [currentTheme, setCurrentTheme] = useState
(() => {
+ if (initialTheme === 'custom') {
+ const customTheme = getStoredCustomTheme();
+ return customTheme || themes.yuu;
+ }
+ return themes[initialTheme] || themes.yuu;
+ });
- const setTheme = (theme: string) => {
- setThemeName(theme)
+ const setTheme = (newThemeName: string) => {
+ setThemeName(newThemeName);
+ if (newThemeName === 'custom') {
+ const customTheme = getStoredCustomTheme();
+ if (customTheme) {
+ setCurrentTheme(customTheme);
+ } else {
+ // Fallback to default theme if no custom theme found
+ setThemeName('yuu');
+ setCurrentTheme(themes.yuu);
+ }
+ } else {
+ const foundTheme = themes[newThemeName];
+ if (foundTheme) {
+ setCurrentTheme(foundTheme);
+ }
+ }
}
const setCustomTheme = useCallback((customTheme: Theme) => {
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
applyCustomThemeVars(customTheme);
- setTheme('custom');
+ setThemeName('custom');
+ setCurrentTheme(customTheme);
}, []);
- const getCustomTheme = (): Theme | undefined => {
- const themeStr = localStorage.getItem('custom-theme');
- if (!themeStr) {
- return undefined
- }
- try {
- let theme = JSON.parse(themeStr) as Theme
- return theme
- } catch (err) {
- return undefined
- }
+ const getCustomTheme = (): Theme | undefined => {
+ return getStoredCustomTheme();
}
useEffect(() => {
const root = document.documentElement;
- root.setAttribute('data-theme', theme);
- localStorage.setItem('theme', theme)
- console.log(theme)
+ root.setAttribute('data-theme', themeName);
+ localStorage.setItem('theme', themeName);
- if (theme === 'custom') {
- const saved = localStorage.getItem('custom-theme');
- if (saved) {
- try {
- const parsed = JSON.parse(saved) as Theme;
- applyCustomThemeVars(parsed);
- } catch (err) {
- console.error('Invalid custom theme in localStorage', err);
- }
- } else {
- setTheme('yuu')
- }
+ if (themeName === 'custom') {
+ applyCustomThemeVars(currentTheme);
} else {
- clearCustomThemeVars()
+ clearCustomThemeVars();
}
- }, [theme]);
+ }, [themeName, currentTheme]);
return (
-
+
{children}
);
diff --git a/client/app/routes/ThemeHelper.tsx b/client/app/routes/ThemeHelper.tsx
index 6452a08..fc5b7e4 100644
--- a/client/app/routes/ThemeHelper.tsx
+++ b/client/app/routes/ThemeHelper.tsx
@@ -12,7 +12,6 @@ import { themes, type Theme } from "~/styles/themes.css"
export default function ThemeHelper() {
const initialTheme = {
- name: "custom",
bg: "#1e1816",
bgSecondary: "#2f2623",
bgTertiary: "#453733",
@@ -36,9 +35,6 @@ export default function ThemeHelper() {
console.log(custom)
try {
const theme = JSON.parse(custom) as Theme
- if (theme.name !== "custom") {
- throw new Error("theme name must be 'custom'")
- }
console.log(theme)
setCustomTheme(theme)
} catch(err) {
diff --git a/client/app/styles/themes.css.ts b/client/app/styles/themes.css.ts
index eee6b27..22f48f0 100644
--- a/client/app/styles/themes.css.ts
+++ b/client/app/styles/themes.css.ts
@@ -2,11 +2,10 @@ import { globalStyle } from "@vanilla-extract/css"
import { themeVars } from "./vars.css"
export type Theme = {
- name: string,
- bg: string
- bgSecondary: string
+ bg: string
+ bgSecondary: string
bgTertiary: string
- fg: string
+ fg: string
fgSecondary: string
fgTertiary: string
primary: string
@@ -23,9 +22,8 @@ export const THEME_KEYS = [
'--color'
]
-export const themes: Theme[] = [
- {
- name: "yuu",
+export const themes: Record = {
+ yuu: {
bg: "#1e1816",
bgSecondary: "#2f2623",
bgTertiary: "#453733",
@@ -41,8 +39,7 @@ export const themes: Theme[] = [
success: "#8fc48f",
info: "#87b8dd",
},
- {
- name: "varia",
+ varia: {
bg: "rgb(25, 25, 29)",
bgSecondary: "#222222",
bgTertiary: "#333333",
@@ -58,8 +55,7 @@ export const themes: Theme[] = [
success: "#4caf50",
info: "#2196f3",
},
- {
- name: "midnight",
+ midnight: {
bg: "rgb(8, 15, 24)",
bgSecondary: "rgb(15, 27, 46)",
bgTertiary: "rgb(15, 41, 70)",
@@ -75,8 +71,7 @@ export const themes: Theme[] = [
success: "#4caf50",
info: "#2196f3",
},
- {
- name: "catppuccin",
+ catppuccin: {
bg: "#1e1e2e",
bgSecondary: "#181825",
bgTertiary: "#11111b",
@@ -92,8 +87,7 @@ export const themes: Theme[] = [
success: "#a6e3a1",
info: "#89dceb",
},
- {
- name: "autumn",
+ autumn: {
bg: "rgb(44, 25, 18)",
bgSecondary: "rgb(70, 40, 18)",
bgTertiary: "#4b2f1c",
@@ -109,8 +103,7 @@ export const themes: Theme[] = [
success: "#6b8e23",
info: "#c084fc",
},
- {
- name: "black",
+ black: {
bg: "#000000",
bgSecondary: "#1a1a1a",
bgTertiary: "#2a2a2a",
@@ -126,8 +119,7 @@ export const themes: Theme[] = [
success: "#4caf50",
info: "#2196f3",
},
- {
- name: "wine",
+ wine: {
bg: "#23181E",
bgSecondary: "#2C1C25",
bgTertiary: "#422A37",
@@ -143,97 +135,92 @@ export const themes: Theme[] = [
success: "#bbf7d0",
info: "#bae6fd",
},
- {
- name: "pearl",
- bg: "#FFFFFF",
- bgSecondary: "#EEEEEE",
- bgTertiary: "#E0E0E0",
- fg: "#333333",
- fgSecondary: "#555555",
+ pearl: {
+ bg: "#FFFFFF",
+ bgSecondary: "#EEEEEE",
+ bgTertiary: "#E0E0E0",
+ fg: "#333333",
+ fgSecondary: "#555555",
fgTertiary: "#777777",
- primary: "#007BFF",
+ primary: "#007BFF",
primaryDim: "#0056B3",
- accent: "#28A745",
- accentDim: "#1E7E34",
- error: "#DC3545",
- warning: "#FFC107",
- success: "#28A745",
- info: "#17A2B8",
+ accent: "#28A745",
+ accentDim: "#1E7E34",
+ error: "#DC3545",
+ warning: "#FFC107",
+ success: "#28A745",
+ info: "#17A2B8",
},
- {
- name: "asuka",
- bg: "#3B1212",
- bgSecondary: "#471B1B",
- bgTertiary: "#020202",
- fg: "#F1E9E6",
- fgSecondary: "#CCB6AE",
+ asuka: {
+ bg: "#3B1212",
+ bgSecondary: "#471B1B",
+ bgTertiary: "#020202",
+ fg: "#F1E9E6",
+ fgSecondary: "#CCB6AE",
fgTertiary: "#9F8176",
- primary: "#F1E9E6",
+ primary: "#F1E9E6",
primaryDim: "#CCB6AE",
- accent: "#41CE41",
- accentDim: "#3BA03B",
- error: "#DC143C",
- warning: "#FFD700",
- success: "#32CD32",
- info: "#1E90FF",
+ accent: "#41CE41",
+ accentDim: "#3BA03B",
+ error: "#DC143C",
+ warning: "#FFD700",
+ success: "#32CD32",
+ info: "#1E90FF",
},
- {
- name: "urim",
- bg: "#101713",
- bgSecondary: "#1B2921",
- bgTertiary: "#273B30",
- fg: "#D2E79E",
- fgSecondary: "#B4DA55",
+ urim: {
+ bg: "#101713",
+ bgSecondary: "#1B2921",
+ bgTertiary: "#273B30",
+ fg: "#D2E79E",
+ fgSecondary: "#B4DA55",
fgTertiary: "#7E9F2A",
- primary: "#ead500",
+ primary: "#ead500",
primaryDim: "#C1B210",
- accent: "#28A745",
- accentDim: "#1E7E34",
- error: "#EE5237",
- warning: "#FFC107",
- success: "#28A745",
- info: "#17A2B8",
+ accent: "#28A745",
+ accentDim: "#1E7E34",
+ error: "#EE5237",
+ warning: "#FFC107",
+ success: "#28A745",
+ info: "#17A2B8",
},
- {
- name: "match",
- bg: "#071014",
- bgSecondary: "#0A181E",
- bgTertiary: "#112A34",
- fg: "#ebeaeb",
- fgSecondary: "#BDBDBD",
+ match: {
+ bg: "#071014",
+ bgSecondary: "#0A181E",
+ bgTertiary: "#112A34",
+ fg: "#ebeaeb",
+ fgSecondary: "#BDBDBD",
fgTertiary: "#A2A2A2",
- primary: "#fda827",
+ primary: "#fda827",
primaryDim: "#C78420",
- accent: "#277CFD",
- accentDim: "#1F60C1",
- error: "#F14426",
- warning: "#FFC107",
- success: "#28A745",
- info: "#17A2B8",
+ accent: "#277CFD",
+ accentDim: "#1F60C1",
+ error: "#F14426",
+ warning: "#FFC107",
+ success: "#28A745",
+ info: "#17A2B8",
},
- {
- name: "lemon",
- bg: "#1a171a",
- bgSecondary: "#2E272E",
- bgTertiary: "#443844",
- fg: "#E6E2DC",
- fgSecondary: "#B2ACA1",
+ lemon: {
+ bg: "#1a171a",
+ bgSecondary: "#2E272E",
+ bgTertiary: "#443844",
+ fg: "#E6E2DC",
+ fgSecondary: "#B2ACA1",
fgTertiary: "#968F82",
- primary: "#f5c737",
+ primary: "#f5c737",
primaryDim: "#C29D2F",
- accent: "#277CFD",
- accentDim: "#1F60C1",
- error: "#F14426",
- warning: "#FFC107",
- success: "#28A745",
- info: "#17A2B8",
+ accent: "#277CFD",
+ accentDim: "#1F60C1",
+ error: "#F14426",
+ warning: "#FFC107",
+ success: "#28A745",
+ info: "#17A2B8",
}
-];
+};
export default themes
-themes.forEach((theme) => {
- const selector = `[data-theme="${theme.name}"]`
+Object.entries(themes).forEach(([name, theme]) => {
+ const selector = `[data-theme="${name}"]`
globalStyle(selector, {
vars: {