mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
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
This commit is contained in:
parent
8d3c51eb3d
commit
1c605c3c1f
6 changed files with 169 additions and 161 deletions
|
|
@ -4,8 +4,24 @@ import Popup from "./Popup"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useTheme } from "~/hooks/useTheme"
|
import { useTheme } from "~/hooks/useTheme"
|
||||||
import ActivityOptsSelector from "./ActivityOptsSelector"
|
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 {
|
interface Props {
|
||||||
step?: string
|
step?: string
|
||||||
range?: number
|
range?: number
|
||||||
|
|
@ -49,9 +65,9 @@ export default function ActivityGrid({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme, themeName } = useTheme();
|
||||||
const currentTheme = themes.find(t => t.name === theme);
|
const color = getPrimaryColor(theme);
|
||||||
const color = currentTheme?.primary || '#f5a97f';
|
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -108,7 +124,7 @@ export default function ActivityGrid({
|
||||||
}
|
}
|
||||||
|
|
||||||
v = Math.min(v, t)
|
v = Math.min(v, t)
|
||||||
if (theme === "pearl") {
|
if (themeName === "pearl") {
|
||||||
// special case for the only light theme lol
|
// special case for the only light theme lol
|
||||||
// could be generalized by pragmatically comparing the
|
// could be generalized by pragmatically comparing the
|
||||||
// lightness of the bg vs the primary but eh
|
// 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 {
|
interface Props {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
|
themeName: string
|
||||||
setTheme: Function
|
setTheme: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ThemeOption({ theme, setTheme }: Props) {
|
export default function ThemeOption({ theme, themeName, setTheme }: Props) {
|
||||||
|
|
||||||
const capitalizeFirstLetter = (s: string) => {
|
const capitalizeFirstLetter = (s: string) => {
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 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(theme.name)}</div>
|
<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.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.fgSecondary}}></div>
|
||||||
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.primary}}></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';
|
import { AsyncButton } from '../AsyncButton';
|
||||||
|
|
||||||
export function ThemeSwitcher() {
|
export function ThemeSwitcher() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, themeName, setTheme } = useTheme();
|
||||||
const initialTheme = {
|
const initialTheme = {
|
||||||
bg: "#1e1816",
|
bg: "#1e1816",
|
||||||
bgSecondary: "#2f2623",
|
bgSecondary: "#2f2623",
|
||||||
|
|
@ -30,30 +30,22 @@ export function ThemeSwitcher() {
|
||||||
const handleCustomTheme = () => {
|
const handleCustomTheme = () => {
|
||||||
console.log(custom)
|
console.log(custom)
|
||||||
try {
|
try {
|
||||||
const theme = JSON.parse(custom)
|
const themeData = JSON.parse(custom)
|
||||||
theme.name = "custom"
|
setCustomTheme(themeData)
|
||||||
setCustomTheme(theme)
|
setCustom(JSON.stringify(themeData, null, " "))
|
||||||
delete theme.name
|
console.log(themeData)
|
||||||
setCustom(JSON.stringify(theme, null, " "))
|
|
||||||
console.log(theme)
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (theme) {
|
|
||||||
setTheme(theme)
|
|
||||||
}
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-10'>
|
<div className='flex flex-col gap-10'>
|
||||||
<div>
|
<div>
|
||||||
<h2>Select Theme</h2>
|
<h2>Select Theme</h2>
|
||||||
<div className="grid grid-cols-2 items-center gap-2">
|
<div className="grid grid-cols-2 items-center gap-2">
|
||||||
{themes.map((t) => (
|
{Object.entries(themes).map(([name, themeData]) => (
|
||||||
<ThemeOption setTheme={setTheme} key={t.name} theme={t} />
|
<ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
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';
|
import { themeVars } from '~/styles/vars.css';
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
theme: string;
|
themeName: string;
|
||||||
|
theme: Theme;
|
||||||
setTheme: (theme: string) => void;
|
setTheme: (theme: string) => void;
|
||||||
setCustomTheme: (theme: Theme) => void;
|
setCustomTheme: (theme: Theme) => void;
|
||||||
getCustomTheme: () => Theme | undefined;
|
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({
|
export function ThemeProvider({
|
||||||
theme: initialTheme,
|
theme: initialTheme,
|
||||||
children,
|
children,
|
||||||
|
|
@ -36,57 +49,60 @@ export function ThemeProvider({
|
||||||
theme: string;
|
theme: string;
|
||||||
children: ReactNode;
|
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) => {
|
const setTheme = (newThemeName: string) => {
|
||||||
setThemeName(theme)
|
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) => {
|
const setCustomTheme = useCallback((customTheme: Theme) => {
|
||||||
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
|
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
|
||||||
applyCustomThemeVars(customTheme);
|
applyCustomThemeVars(customTheme);
|
||||||
setTheme('custom');
|
setThemeName('custom');
|
||||||
|
setCurrentTheme(customTheme);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getCustomTheme = (): Theme | undefined => {
|
const getCustomTheme = (): Theme | undefined => {
|
||||||
const themeStr = localStorage.getItem('custom-theme');
|
return getStoredCustomTheme();
|
||||||
if (!themeStr) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let theme = JSON.parse(themeStr) as Theme
|
|
||||||
return theme
|
|
||||||
} catch (err) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
root.setAttribute('data-theme', theme);
|
root.setAttribute('data-theme', themeName);
|
||||||
localStorage.setItem('theme', theme)
|
localStorage.setItem('theme', themeName);
|
||||||
console.log(theme)
|
|
||||||
|
|
||||||
if (theme === 'custom') {
|
if (themeName === 'custom') {
|
||||||
const saved = localStorage.getItem('custom-theme');
|
applyCustomThemeVars(currentTheme);
|
||||||
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')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
clearCustomThemeVars()
|
clearCustomThemeVars();
|
||||||
}
|
}
|
||||||
}, [theme]);
|
}, [themeName, currentTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, setTheme, setCustomTheme, getCustomTheme }}>
|
<ThemeContext.Provider value={{ themeName, theme: currentTheme, setTheme, setCustomTheme, getCustomTheme }}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import { themes, type Theme } from "~/styles/themes.css"
|
||||||
|
|
||||||
export default function ThemeHelper() {
|
export default function ThemeHelper() {
|
||||||
const initialTheme = {
|
const initialTheme = {
|
||||||
name: "custom",
|
|
||||||
bg: "#1e1816",
|
bg: "#1e1816",
|
||||||
bgSecondary: "#2f2623",
|
bgSecondary: "#2f2623",
|
||||||
bgTertiary: "#453733",
|
bgTertiary: "#453733",
|
||||||
|
|
@ -36,9 +35,6 @@ export default function ThemeHelper() {
|
||||||
console.log(custom)
|
console.log(custom)
|
||||||
try {
|
try {
|
||||||
const theme = JSON.parse(custom) as Theme
|
const theme = JSON.parse(custom) as Theme
|
||||||
if (theme.name !== "custom") {
|
|
||||||
throw new Error("theme name must be 'custom'")
|
|
||||||
}
|
|
||||||
console.log(theme)
|
console.log(theme)
|
||||||
setCustomTheme(theme)
|
setCustomTheme(theme)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { globalStyle } from "@vanilla-extract/css"
|
||||||
import { themeVars } from "./vars.css"
|
import { themeVars } from "./vars.css"
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
name: string,
|
|
||||||
bg: string
|
bg: string
|
||||||
bgSecondary: string
|
bgSecondary: string
|
||||||
bgTertiary: string
|
bgTertiary: string
|
||||||
|
|
@ -23,9 +22,8 @@ export const THEME_KEYS = [
|
||||||
'--color'
|
'--color'
|
||||||
]
|
]
|
||||||
|
|
||||||
export const themes: Theme[] = [
|
export const themes: Record<string, Theme> = {
|
||||||
{
|
yuu: {
|
||||||
name: "yuu",
|
|
||||||
bg: "#1e1816",
|
bg: "#1e1816",
|
||||||
bgSecondary: "#2f2623",
|
bgSecondary: "#2f2623",
|
||||||
bgTertiary: "#453733",
|
bgTertiary: "#453733",
|
||||||
|
|
@ -41,8 +39,7 @@ export const themes: Theme[] = [
|
||||||
success: "#8fc48f",
|
success: "#8fc48f",
|
||||||
info: "#87b8dd",
|
info: "#87b8dd",
|
||||||
},
|
},
|
||||||
{
|
varia: {
|
||||||
name: "varia",
|
|
||||||
bg: "rgb(25, 25, 29)",
|
bg: "rgb(25, 25, 29)",
|
||||||
bgSecondary: "#222222",
|
bgSecondary: "#222222",
|
||||||
bgTertiary: "#333333",
|
bgTertiary: "#333333",
|
||||||
|
|
@ -58,8 +55,7 @@ export const themes: Theme[] = [
|
||||||
success: "#4caf50",
|
success: "#4caf50",
|
||||||
info: "#2196f3",
|
info: "#2196f3",
|
||||||
},
|
},
|
||||||
{
|
midnight: {
|
||||||
name: "midnight",
|
|
||||||
bg: "rgb(8, 15, 24)",
|
bg: "rgb(8, 15, 24)",
|
||||||
bgSecondary: "rgb(15, 27, 46)",
|
bgSecondary: "rgb(15, 27, 46)",
|
||||||
bgTertiary: "rgb(15, 41, 70)",
|
bgTertiary: "rgb(15, 41, 70)",
|
||||||
|
|
@ -75,8 +71,7 @@ export const themes: Theme[] = [
|
||||||
success: "#4caf50",
|
success: "#4caf50",
|
||||||
info: "#2196f3",
|
info: "#2196f3",
|
||||||
},
|
},
|
||||||
{
|
catppuccin: {
|
||||||
name: "catppuccin",
|
|
||||||
bg: "#1e1e2e",
|
bg: "#1e1e2e",
|
||||||
bgSecondary: "#181825",
|
bgSecondary: "#181825",
|
||||||
bgTertiary: "#11111b",
|
bgTertiary: "#11111b",
|
||||||
|
|
@ -92,8 +87,7 @@ export const themes: Theme[] = [
|
||||||
success: "#a6e3a1",
|
success: "#a6e3a1",
|
||||||
info: "#89dceb",
|
info: "#89dceb",
|
||||||
},
|
},
|
||||||
{
|
autumn: {
|
||||||
name: "autumn",
|
|
||||||
bg: "rgb(44, 25, 18)",
|
bg: "rgb(44, 25, 18)",
|
||||||
bgSecondary: "rgb(70, 40, 18)",
|
bgSecondary: "rgb(70, 40, 18)",
|
||||||
bgTertiary: "#4b2f1c",
|
bgTertiary: "#4b2f1c",
|
||||||
|
|
@ -109,8 +103,7 @@ export const themes: Theme[] = [
|
||||||
success: "#6b8e23",
|
success: "#6b8e23",
|
||||||
info: "#c084fc",
|
info: "#c084fc",
|
||||||
},
|
},
|
||||||
{
|
black: {
|
||||||
name: "black",
|
|
||||||
bg: "#000000",
|
bg: "#000000",
|
||||||
bgSecondary: "#1a1a1a",
|
bgSecondary: "#1a1a1a",
|
||||||
bgTertiary: "#2a2a2a",
|
bgTertiary: "#2a2a2a",
|
||||||
|
|
@ -126,8 +119,7 @@ export const themes: Theme[] = [
|
||||||
success: "#4caf50",
|
success: "#4caf50",
|
||||||
info: "#2196f3",
|
info: "#2196f3",
|
||||||
},
|
},
|
||||||
{
|
wine: {
|
||||||
name: "wine",
|
|
||||||
bg: "#23181E",
|
bg: "#23181E",
|
||||||
bgSecondary: "#2C1C25",
|
bgSecondary: "#2C1C25",
|
||||||
bgTertiary: "#422A37",
|
bgTertiary: "#422A37",
|
||||||
|
|
@ -143,8 +135,7 @@ export const themes: Theme[] = [
|
||||||
success: "#bbf7d0",
|
success: "#bbf7d0",
|
||||||
info: "#bae6fd",
|
info: "#bae6fd",
|
||||||
},
|
},
|
||||||
{
|
pearl: {
|
||||||
name: "pearl",
|
|
||||||
bg: "#FFFFFF",
|
bg: "#FFFFFF",
|
||||||
bgSecondary: "#EEEEEE",
|
bgSecondary: "#EEEEEE",
|
||||||
bgTertiary: "#E0E0E0",
|
bgTertiary: "#E0E0E0",
|
||||||
|
|
@ -160,8 +151,7 @@ export const themes: Theme[] = [
|
||||||
success: "#28A745",
|
success: "#28A745",
|
||||||
info: "#17A2B8",
|
info: "#17A2B8",
|
||||||
},
|
},
|
||||||
{
|
asuka: {
|
||||||
name: "asuka",
|
|
||||||
bg: "#3B1212",
|
bg: "#3B1212",
|
||||||
bgSecondary: "#471B1B",
|
bgSecondary: "#471B1B",
|
||||||
bgTertiary: "#020202",
|
bgTertiary: "#020202",
|
||||||
|
|
@ -177,8 +167,7 @@ export const themes: Theme[] = [
|
||||||
success: "#32CD32",
|
success: "#32CD32",
|
||||||
info: "#1E90FF",
|
info: "#1E90FF",
|
||||||
},
|
},
|
||||||
{
|
urim: {
|
||||||
name: "urim",
|
|
||||||
bg: "#101713",
|
bg: "#101713",
|
||||||
bgSecondary: "#1B2921",
|
bgSecondary: "#1B2921",
|
||||||
bgTertiary: "#273B30",
|
bgTertiary: "#273B30",
|
||||||
|
|
@ -194,8 +183,7 @@ export const themes: Theme[] = [
|
||||||
success: "#28A745",
|
success: "#28A745",
|
||||||
info: "#17A2B8",
|
info: "#17A2B8",
|
||||||
},
|
},
|
||||||
{
|
match: {
|
||||||
name: "match",
|
|
||||||
bg: "#071014",
|
bg: "#071014",
|
||||||
bgSecondary: "#0A181E",
|
bgSecondary: "#0A181E",
|
||||||
bgTertiary: "#112A34",
|
bgTertiary: "#112A34",
|
||||||
|
|
@ -211,8 +199,7 @@ export const themes: Theme[] = [
|
||||||
success: "#28A745",
|
success: "#28A745",
|
||||||
info: "#17A2B8",
|
info: "#17A2B8",
|
||||||
},
|
},
|
||||||
{
|
lemon: {
|
||||||
name: "lemon",
|
|
||||||
bg: "#1a171a",
|
bg: "#1a171a",
|
||||||
bgSecondary: "#2E272E",
|
bgSecondary: "#2E272E",
|
||||||
bgTertiary: "#443844",
|
bgTertiary: "#443844",
|
||||||
|
|
@ -228,12 +215,12 @@ export const themes: Theme[] = [
|
||||||
success: "#28A745",
|
success: "#28A745",
|
||||||
info: "#17A2B8",
|
info: "#17A2B8",
|
||||||
}
|
}
|
||||||
];
|
};
|
||||||
|
|
||||||
export default themes
|
export default themes
|
||||||
|
|
||||||
themes.forEach((theme) => {
|
Object.entries(themes).forEach(([name, theme]) => {
|
||||||
const selector = `[data-theme="${theme.name}"]`
|
const selector = `[data-theme="${name}"]`
|
||||||
|
|
||||||
globalStyle(selector, {
|
globalStyle(selector, {
|
||||||
vars: {
|
vars: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue