Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name

Split name out of the Theme struct to simplify custom theme saving/reading
pull/76/head
Michael Landry 2 months ago
parent 8d3c51eb3d
commit 1c605c3c1f

@ -4,8 +4,24 @@ import Popup from "./Popup"
import { useState } from "react"
import { useTheme } from "~/hooks/useTheme"
import ActivityOptsSelector from "./ActivityOptsSelector"
import { themes } from "~/styles/themes.css"
import type { Theme } from "~/styles/themes.css"
function getPrimaryColor(theme: Theme): string {
const value = theme.primary;
const rgbMatch = value.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return (
'#' +
[r, g, b]
.map((n) => n.toString(16).padStart(2, '0'))
.join('')
);
}
return value;
}
interface Props {
step?: string
range?: number
@ -49,9 +65,9 @@ export default function ActivityGrid({
});
const { theme } = useTheme();
const currentTheme = themes.find(t => t.name === theme);
const color = currentTheme?.primary || '#f5a97f';
const { theme, themeName } = useTheme();
const color = getPrimaryColor(theme);
if (isPending) {
return (
@ -108,7 +124,7 @@ export default function ActivityGrid({
}
v = Math.min(v, t)
if (theme === "pearl") {
if (themeName === "pearl") {
// special case for the only light theme lol
// could be generalized by pragmatically comparing the
// lightness of the bg vs the primary but eh

@ -1,19 +1,20 @@
import type { Theme } from "~/providers/ThemeProvider";
import type { Theme } from "~/styles/themes.css";
interface Props {
theme: Theme
themeName: string
setTheme: Function
}
export default function ThemeOption({ theme, setTheme }: Props) {
export default function ThemeOption({ theme, themeName, setTheme }: Props) {
const capitalizeFirstLetter = (s: string) => {
return s.charAt(0).toUpperCase() + s.slice(1);
}
return (
<div onClick={() => 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}}>
<div className="text-xs sm:text-sm">{capitalizeFirstLetter(theme.name)}</div>
<div onClick={() => 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}}>
<div className="text-xs sm:text-sm">{capitalizeFirstLetter(themeName)}</div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.bgSecondary}}></div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.fgSecondary}}></div>
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.primary}}></div>

@ -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 (
<div className='flex flex-col gap-10'>
<div>
<h2>Select Theme</h2>
<div className="grid grid-cols-2 items-center gap-2">
{themes.map((t) => (
<ThemeOption setTheme={setTheme} key={t.name} theme={t} />
{Object.entries(themes).map(([name, themeData]) => (
<ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} />
))}
</div>
</div>

@ -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<Theme>(() => {
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
}
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 (
<ThemeContext.Provider value={{ theme, setTheme, setCustomTheme, getCustomTheme }}>
<ThemeContext.Provider value={{ themeName, theme: currentTheme, setTheme, setCustomTheme, getCustomTheme }}>
{children}
</ThemeContext.Provider>
);

@ -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) {

@ -2,7 +2,6 @@ import { globalStyle } from "@vanilla-extract/css"
import { themeVars } from "./vars.css"
export type Theme = {
name: string,
bg: string
bgSecondary: string
bgTertiary: string
@ -23,9 +22,8 @@ export const THEME_KEYS = [
'--color'
]
export const themes: Theme[] = [
{
name: "yuu",
export const themes: Record<string, Theme> = {
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,8 +135,7 @@ export const themes: Theme[] = [
success: "#bbf7d0",
info: "#bae6fd",
},
{
name: "pearl",
pearl: {
bg: "#FFFFFF",
bgSecondary: "#EEEEEE",
bgTertiary: "#E0E0E0",
@ -160,8 +151,7 @@ export const themes: Theme[] = [
success: "#28A745",
info: "#17A2B8",
},
{
name: "asuka",
asuka: {
bg: "#3B1212",
bgSecondary: "#471B1B",
bgTertiary: "#020202",
@ -177,8 +167,7 @@ export const themes: Theme[] = [
success: "#32CD32",
info: "#1E90FF",
},
{
name: "urim",
urim: {
bg: "#101713",
bgSecondary: "#1B2921",
bgTertiary: "#273B30",
@ -194,8 +183,7 @@ export const themes: Theme[] = [
success: "#28A745",
info: "#17A2B8",
},
{
name: "match",
match: {
bg: "#071014",
bgSecondary: "#0A181E",
bgTertiary: "#112A34",
@ -211,8 +199,7 @@ export const themes: Theme[] = [
success: "#28A745",
info: "#17A2B8",
},
{
name: "lemon",
lemon: {
bg: "#1a171a",
bgSecondary: "#2E272E",
bgTertiary: "#443844",
@ -228,12 +215,12 @@ export const themes: Theme[] = [
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: {

Loading…
Cancel
Save