ui: theme system, routing fixes, major overhaul, still need to fix dropdowns
This commit is contained in:
@@ -1,21 +1,279 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
card: string;
|
||||
cardForeground: string;
|
||||
popover: string;
|
||||
popoverForeground: string;
|
||||
primary: string;
|
||||
primaryForeground: string;
|
||||
secondary: string;
|
||||
secondaryForeground: string;
|
||||
muted: string;
|
||||
mutedForeground: string;
|
||||
accent: string;
|
||||
accentForeground: string;
|
||||
destructive: string;
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
// Chart colors
|
||||
chart1: string;
|
||||
chart2: string;
|
||||
chart3: string;
|
||||
chart4: string;
|
||||
chart5: string;
|
||||
// Sidebar colors
|
||||
sidebar: string;
|
||||
sidebarForeground: string;
|
||||
sidebarPrimary: string;
|
||||
sidebarPrimaryForeground: string;
|
||||
sidebarAccent: string;
|
||||
sidebarAccentForeground: string;
|
||||
sidebarBorder: string;
|
||||
sidebarRing: string;
|
||||
}
|
||||
|
||||
export interface ThemeDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
mode: "light" | "dark";
|
||||
colors: ThemeColors;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
// Fixed themes using proper OKLCH format
|
||||
const DEFAULT_THEMES: ThemeDefinition[] = [
|
||||
{
|
||||
id: "default-light",
|
||||
name: "Default Light",
|
||||
mode: "light",
|
||||
colors: {
|
||||
background: "oklch(1 0 0)",
|
||||
foreground: "oklch(0.145 0 0)",
|
||||
card: "oklch(1 0 0)",
|
||||
cardForeground: "oklch(0.145 0 0)",
|
||||
popover: "oklch(1 0 0)",
|
||||
popoverForeground: "oklch(0.145 0 0)",
|
||||
primary: "oklch(0.205 0 0)",
|
||||
primaryForeground: "oklch(0.985 0 0)",
|
||||
secondary: "oklch(0.97 0 0)",
|
||||
secondaryForeground: "oklch(0.205 0 0)",
|
||||
muted: "oklch(0.97 0 0)",
|
||||
mutedForeground: "oklch(0.556 0 0)",
|
||||
accent: "oklch(0.97 0 0)",
|
||||
accentForeground: "oklch(0.205 0 0)",
|
||||
destructive: "oklch(0.577 0.245 27.325)",
|
||||
border: "oklch(0.922 0 0)",
|
||||
input: "oklch(0.922 0 0)",
|
||||
ring: "oklch(0.708 0 0)",
|
||||
chart1: "oklch(0.646 0.222 41.116)",
|
||||
chart2: "oklch(0.6 0.118 184.704)",
|
||||
chart3: "oklch(0.398 0.07 227.392)",
|
||||
chart4: "oklch(0.828 0.189 84.429)",
|
||||
chart5: "oklch(0.769 0.188 70.08)",
|
||||
sidebar: "oklch(0.985 0 0)",
|
||||
sidebarForeground: "oklch(0.145 0 0)",
|
||||
sidebarPrimary: "oklch(0.205 0 0)",
|
||||
sidebarPrimaryForeground: "oklch(0.985 0 0)",
|
||||
sidebarAccent: "oklch(0.97 0 0)",
|
||||
sidebarAccentForeground: "oklch(0.205 0 0)",
|
||||
sidebarBorder: "oklch(0.922 0 0)",
|
||||
sidebarRing: "oklch(0.708 0 0)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "default-dark",
|
||||
name: "Default Dark",
|
||||
mode: "dark",
|
||||
colors: {
|
||||
background: "oklch(0.145 0 0)",
|
||||
foreground: "oklch(0.985 0 0)",
|
||||
card: "oklch(0.205 0 0)",
|
||||
cardForeground: "oklch(0.985 0 0)",
|
||||
popover: "oklch(0.205 0 0)",
|
||||
popoverForeground: "oklch(0.985 0 0)",
|
||||
primary: "oklch(0.922 0 0)",
|
||||
primaryForeground: "oklch(0.205 0 0)",
|
||||
secondary: "oklch(0.269 0 0)",
|
||||
secondaryForeground: "oklch(0.985 0 0)",
|
||||
muted: "oklch(0.269 0 0)",
|
||||
mutedForeground: "oklch(0.708 0 0)",
|
||||
accent: "oklch(0.269 0 0)",
|
||||
accentForeground: "oklch(0.985 0 0)",
|
||||
destructive: "oklch(0.704 0.191 22.216)",
|
||||
border: "oklch(1 0 0 / 10%)",
|
||||
input: "oklch(1 0 0 / 15%)",
|
||||
ring: "oklch(0.556 0 0)",
|
||||
chart1: "oklch(0.488 0.243 264.376)",
|
||||
chart2: "oklch(0.696 0.17 162.48)",
|
||||
chart3: "oklch(0.769 0.188 70.08)",
|
||||
chart4: "oklch(0.627 0.265 303.9)",
|
||||
chart5: "oklch(0.645 0.246 16.439)",
|
||||
sidebar: "oklch(0.205 0 0)",
|
||||
sidebarForeground: "oklch(0.985 0 0)",
|
||||
sidebarPrimary: "oklch(0.488 0.243 264.376)",
|
||||
sidebarPrimaryForeground: "oklch(0.985 0 0)",
|
||||
sidebarAccent: "oklch(0.269 0 0)",
|
||||
sidebarAccentForeground: "oklch(0.985 0 0)",
|
||||
sidebarBorder: "oklch(1 0 0 / 10%)",
|
||||
sidebarRing: "oklch(0.556 0 0)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "paper-light",
|
||||
name: "Paper",
|
||||
description: "Clean paper-like theme",
|
||||
mode: "light",
|
||||
colors: {
|
||||
background: "oklch(0.99 0.01 85)",
|
||||
foreground: "oklch(0.15 0.02 65)",
|
||||
card: "oklch(0.99 0.01 85)",
|
||||
cardForeground: "oklch(0.15 0.02 65)",
|
||||
popover: "oklch(1 0 0)",
|
||||
popoverForeground: "oklch(0.15 0.02 65)",
|
||||
primary: "oklch(0.25 0.03 45)",
|
||||
primaryForeground: "oklch(0.98 0.01 85)",
|
||||
secondary: "oklch(0.96 0.01 75)",
|
||||
secondaryForeground: "oklch(0.25 0.03 45)",
|
||||
muted: "oklch(0.96 0.01 75)",
|
||||
mutedForeground: "oklch(0.45 0.02 55)",
|
||||
accent: "oklch(0.96 0.01 75)",
|
||||
accentForeground: "oklch(0.25 0.03 45)",
|
||||
destructive: "oklch(0.577 0.245 27.325)",
|
||||
border: "oklch(0.90 0.01 65)",
|
||||
input: "oklch(0.90 0.01 65)",
|
||||
ring: "oklch(0.25 0.03 45)",
|
||||
chart1: "oklch(0.646 0.222 41.116)",
|
||||
chart2: "oklch(0.6 0.118 184.704)",
|
||||
chart3: "oklch(0.398 0.07 227.392)",
|
||||
chart4: "oklch(0.828 0.189 84.429)",
|
||||
chart5: "oklch(0.769 0.188 70.08)",
|
||||
sidebar: "oklch(0.97 0.01 80)",
|
||||
sidebarForeground: "oklch(0.15 0.02 65)",
|
||||
sidebarPrimary: "oklch(0.25 0.03 45)",
|
||||
sidebarPrimaryForeground: "oklch(0.98 0.01 85)",
|
||||
sidebarAccent: "oklch(0.94 0.01 75)",
|
||||
sidebarAccentForeground: "oklch(0.25 0.03 45)",
|
||||
sidebarBorder: "oklch(0.88 0.01 65)",
|
||||
sidebarRing: "oklch(0.25 0.03 45)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "comfy-brown-dark",
|
||||
name: "Comfy Brown",
|
||||
description: "Warm brown theme for dark mode",
|
||||
mode: "dark",
|
||||
colors: {
|
||||
background: "oklch(0.15 0.03 65)",
|
||||
foreground: "oklch(0.95 0.01 85)",
|
||||
card: "oklch(0.20 0.03 55)",
|
||||
cardForeground: "oklch(0.95 0.01 85)",
|
||||
popover: "oklch(0.20 0.03 55)",
|
||||
popoverForeground: "oklch(0.95 0.01 85)",
|
||||
primary: "oklch(0.65 0.15 45)",
|
||||
primaryForeground: "oklch(0.95 0.01 85)",
|
||||
secondary: "oklch(0.25 0.04 50)",
|
||||
secondaryForeground: "oklch(0.95 0.01 85)",
|
||||
muted: "oklch(0.25 0.04 50)",
|
||||
mutedForeground: "oklch(0.70 0.02 65)",
|
||||
accent: "oklch(0.25 0.04 50)",
|
||||
accentForeground: "oklch(0.95 0.01 85)",
|
||||
destructive: "oklch(0.704 0.191 22.216)",
|
||||
border: "oklch(0.30 0.04 55)",
|
||||
input: "oklch(0.30 0.04 55)",
|
||||
ring: "oklch(0.65 0.15 45)",
|
||||
chart1: "oklch(0.65 0.15 45)",
|
||||
chart2: "oklch(0.55 0.12 85)",
|
||||
chart3: "oklch(0.75 0.18 25)",
|
||||
chart4: "oklch(0.60 0.14 105)",
|
||||
chart5: "oklch(0.70 0.16 65)",
|
||||
sidebar: "oklch(0.18 0.03 60)",
|
||||
sidebarForeground: "oklch(0.95 0.01 85)",
|
||||
sidebarPrimary: "oklch(0.65 0.15 45)",
|
||||
sidebarPrimaryForeground: "oklch(0.95 0.01 85)",
|
||||
sidebarAccent: "oklch(0.22 0.04 50)",
|
||||
sidebarAccentForeground: "oklch(0.95 0.01 85)",
|
||||
sidebarBorder: "oklch(0.28 0.04 55)",
|
||||
sidebarRing: "oklch(0.65 0.15 45)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "midnight-dark",
|
||||
name: "Midnight",
|
||||
description: "Deep blue midnight theme",
|
||||
mode: "dark",
|
||||
colors: {
|
||||
background: "oklch(0.12 0.08 250)",
|
||||
foreground: "oklch(0.95 0.01 230)",
|
||||
card: "oklch(0.18 0.06 240)",
|
||||
cardForeground: "oklch(0.95 0.01 230)",
|
||||
popover: "oklch(0.18 0.06 240)",
|
||||
popoverForeground: "oklch(0.95 0.01 230)",
|
||||
primary: "oklch(0.60 0.20 240)",
|
||||
primaryForeground: "oklch(0.95 0.01 230)",
|
||||
secondary: "oklch(0.22 0.05 235)",
|
||||
secondaryForeground: "oklch(0.95 0.01 230)",
|
||||
muted: "oklch(0.22 0.05 235)",
|
||||
mutedForeground: "oklch(0.70 0.02 230)",
|
||||
accent: "oklch(0.22 0.05 235)",
|
||||
accentForeground: "oklch(0.95 0.01 230)",
|
||||
destructive: "oklch(0.704 0.191 22.216)",
|
||||
border: "oklch(0.25 0.05 235)",
|
||||
input: "oklch(0.25 0.05 235)",
|
||||
ring: "oklch(0.60 0.20 240)",
|
||||
chart1: "oklch(0.60 0.20 240)",
|
||||
chart2: "oklch(0.50 0.15 200)",
|
||||
chart3: "oklch(0.65 0.18 280)",
|
||||
chart4: "oklch(0.55 0.16 160)",
|
||||
chart5: "oklch(0.70 0.22 300)",
|
||||
sidebar: "oklch(0.15 0.07 245)",
|
||||
sidebarForeground: "oklch(0.95 0.01 230)",
|
||||
sidebarPrimary: "oklch(0.60 0.20 240)",
|
||||
sidebarPrimaryForeground: "oklch(0.95 0.01 230)",
|
||||
sidebarAccent: "oklch(0.20 0.05 235)",
|
||||
sidebarAccentForeground: "oklch(0.95 0.01 230)",
|
||||
sidebarBorder: "oklch(0.22 0.05 235)",
|
||||
sidebarRing: "oklch(0.60 0.20 240)",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type ThemeMode = "light" | "dark" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
defaultTheme?: ThemeMode;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
mode: ThemeMode;
|
||||
currentTheme: ThemeDefinition;
|
||||
currentLightTheme: ThemeDefinition;
|
||||
currentDarkTheme: ThemeDefinition;
|
||||
themes: ThemeDefinition[];
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
setTheme: (themeId: string) => void;
|
||||
addCustomTheme: (theme: Omit<ThemeDefinition, "id" | "isCustom">) => void;
|
||||
removeCustomTheme: (themeId: string) => void;
|
||||
getThemesForMode: (mode: "light" | "dark") => ThemeDefinition[];
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
mode: "system",
|
||||
currentTheme: DEFAULT_THEMES[1], // Default to dark theme
|
||||
currentLightTheme: DEFAULT_THEMES[0], // Default light
|
||||
currentDarkTheme: DEFAULT_THEMES[1], // Default dark
|
||||
themes: DEFAULT_THEMES,
|
||||
setMode: () => null,
|
||||
setTheme: () => null,
|
||||
addCustomTheme: () => null,
|
||||
removeCustomTheme: () => null,
|
||||
getThemesForMode: () => [],
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
@@ -23,37 +281,206 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
storageKey = "concord-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||
const [mode, setMode] = useState<ThemeMode>(
|
||||
() =>
|
||||
(localStorage.getItem(storageKey + "-mode") as ThemeMode) || defaultTheme,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const [themes, setThemes] = useState<ThemeDefinition[]>(() => {
|
||||
const saved = localStorage.getItem(storageKey + "-themes");
|
||||
const customThemes = saved ? JSON.parse(saved) : [];
|
||||
return [...DEFAULT_THEMES, ...customThemes];
|
||||
});
|
||||
|
||||
const [currentLightThemeId, setCurrentLightThemeId] = useState<string>(() => {
|
||||
const saved = localStorage.getItem(storageKey + "-light");
|
||||
return saved || "default-light";
|
||||
});
|
||||
|
||||
const [currentDarkThemeId, setCurrentDarkThemeId] = useState<string>(() => {
|
||||
const saved = localStorage.getItem(storageKey + "-dark");
|
||||
return saved || "default-dark";
|
||||
});
|
||||
|
||||
const currentLightTheme =
|
||||
themes.find((t) => t.id === currentLightThemeId) || DEFAULT_THEMES[0];
|
||||
const currentDarkTheme =
|
||||
themes.find((t) => t.id === currentDarkThemeId) || DEFAULT_THEMES[2];
|
||||
|
||||
// Determine the current theme based on mode and system preference
|
||||
const getCurrentTheme = (): ThemeDefinition => {
|
||||
switch (mode) {
|
||||
case "light":
|
||||
return currentLightTheme;
|
||||
case "dark":
|
||||
return currentDarkTheme;
|
||||
case "system":
|
||||
const systemPrefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
return systemPrefersDark ? currentDarkTheme : currentLightTheme;
|
||||
default:
|
||||
return currentDarkTheme;
|
||||
}
|
||||
};
|
||||
|
||||
const currentTheme = getCurrentTheme();
|
||||
|
||||
const applyTheme = (theme: ThemeDefinition) => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
// Remove existing theme classes
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
// Apply mode class
|
||||
root.classList.add(theme.mode);
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
// Apply CSS custom properties with proper mapping
|
||||
Object.entries(theme.colors).forEach(([key, value]) => {
|
||||
// Convert camelCase to kebab-case and map to CSS variables
|
||||
const cssVarMap: Record<string, string> = {
|
||||
background: "--background",
|
||||
foreground: "--foreground",
|
||||
card: "--card",
|
||||
cardForeground: "--card-foreground",
|
||||
popover: "--popover",
|
||||
popoverForeground: "--popover-foreground",
|
||||
primary: "--primary",
|
||||
primaryForeground: "--primary-foreground",
|
||||
secondary: "--secondary",
|
||||
secondaryForeground: "--secondary-foreground",
|
||||
muted: "--muted",
|
||||
mutedForeground: "--muted-foreground",
|
||||
accent: "--accent",
|
||||
accentForeground: "--accent-foreground",
|
||||
destructive: "--destructive",
|
||||
border: "--border",
|
||||
input: "--input",
|
||||
ring: "--ring",
|
||||
chart1: "--chart-1",
|
||||
chart2: "--chart-2",
|
||||
chart3: "--chart-3",
|
||||
chart4: "--chart-4",
|
||||
chart5: "--chart-5",
|
||||
sidebar: "--sidebar",
|
||||
sidebarForeground: "--sidebar-foreground",
|
||||
sidebarPrimary: "--sidebar-primary",
|
||||
sidebarPrimaryForeground: "--sidebar-primary-foreground",
|
||||
sidebarAccent: "--sidebar-accent",
|
||||
sidebarAccentForeground: "--sidebar-accent-foreground",
|
||||
sidebarBorder: "--sidebar-border",
|
||||
sidebarRing: "--sidebar-ring",
|
||||
};
|
||||
|
||||
const cssVar = cssVarMap[key];
|
||||
if (cssVar) {
|
||||
root.style.setProperty(cssVar, value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(currentTheme);
|
||||
}, [currentTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = () => {
|
||||
if (mode === "system") {
|
||||
// Theme will be recalculated due to getCurrentTheme dependency
|
||||
const newTheme = getCurrentTheme();
|
||||
applyTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [mode, currentLightTheme, currentDarkTheme]);
|
||||
|
||||
const setTheme = (themeId: string) => {
|
||||
const theme = themes.find((t) => t.id === themeId);
|
||||
if (!theme) return;
|
||||
|
||||
// Update the appropriate theme based on the theme's mode
|
||||
if (theme.mode === "light") {
|
||||
setCurrentLightThemeId(themeId);
|
||||
localStorage.setItem(storageKey + "-light", themeId);
|
||||
} else {
|
||||
setCurrentDarkThemeId(themeId);
|
||||
localStorage.setItem(storageKey + "-dark", themeId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetMode = (newMode: ThemeMode) => {
|
||||
setMode(newMode);
|
||||
localStorage.setItem(storageKey + "-mode", newMode);
|
||||
};
|
||||
|
||||
const addCustomTheme = (
|
||||
themeData: Omit<ThemeDefinition, "id" | "isCustom">,
|
||||
) => {
|
||||
const newTheme: ThemeDefinition = {
|
||||
...themeData,
|
||||
id: `custom-${Date.now()}`,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
const updatedThemes = [...themes, newTheme];
|
||||
setThemes(updatedThemes);
|
||||
|
||||
// Save only custom themes to localStorage
|
||||
const customThemes = updatedThemes.filter((t) => t.isCustom);
|
||||
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
|
||||
};
|
||||
|
||||
const removeCustomTheme = (themeId: string) => {
|
||||
const updatedThemes = themes.filter((t) => t.id !== themeId);
|
||||
setThemes(updatedThemes);
|
||||
|
||||
// If removing current theme, switch to default
|
||||
if (currentLightThemeId === themeId) {
|
||||
const defaultLight = updatedThemes.find(
|
||||
(t) => t.mode === "light" && !t.isCustom,
|
||||
);
|
||||
if (defaultLight) {
|
||||
setCurrentLightThemeId(defaultLight.id);
|
||||
localStorage.setItem(storageKey + "-light", defaultLight.id);
|
||||
}
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
if (currentDarkThemeId === themeId) {
|
||||
const defaultDark = updatedThemes.find(
|
||||
(t) => t.mode === "dark" && !t.isCustom,
|
||||
);
|
||||
if (defaultDark) {
|
||||
setCurrentDarkThemeId(defaultDark.id);
|
||||
localStorage.setItem(storageKey + "-dark", defaultDark.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Save only custom themes to localStorage
|
||||
const customThemes = updatedThemes.filter((t) => t.isCustom);
|
||||
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
|
||||
};
|
||||
|
||||
const getThemesForMode = (targetMode: "light" | "dark") => {
|
||||
return themes.filter((t) => t.mode === targetMode);
|
||||
};
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
mode,
|
||||
currentTheme,
|
||||
currentLightTheme,
|
||||
currentDarkTheme,
|
||||
themes,
|
||||
setMode: handleSetMode,
|
||||
setTheme,
|
||||
addCustomTheme,
|
||||
removeCustomTheme,
|
||||
getThemesForMode,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user