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,15 +4,31 @@ 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
|
||||
month?: number
|
||||
year?: number
|
||||
artistId?: number
|
||||
albumId?: number
|
||||
step?: string
|
||||
range?: number
|
||||
month?: number
|
||||
year?: number
|
||||
artistId?: number
|
||||
albumId?: number
|
||||
trackId?: number
|
||||
configurable?: boolean
|
||||
autoAdjust?: boolean
|
||||
|
|
@ -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
|
||||
}
|
||||
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 (
|
||||
<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,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<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,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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue