Files
cinny/src/app/hooks/useTheme.ts
litruv 61d41900cc Add Stationery and Stationery Dark themes with notebook chrome.
Manila folders, ruled chat, post-it nav, and folder tabs; dark variant uses Catppuccin Mocha colors with muted stickies and soft glows.
2026-07-23 00:24:10 +10:00

204 lines
6.7 KiB
TypeScript

import { lightTheme } from 'folds';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
import { butterTheme, catppuccinMochaTheme, darkTheme, discordTheme, discordDarkerTheme, mochaTheme, silverTheme, stationeryDarkTheme, stationeryTheme, twilightTheme } from '../../colors.css';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
import { pluginRegistry } from '../features/settings/plugins/PluginAPI';
export enum ThemeKind {
Light = 'light',
Dark = 'dark',
}
export type Theme = {
id: string;
kind: ThemeKind;
classNames: string[];
};
export const isStationeryTheme = (theme: Theme | string): boolean => {
const id = typeof theme === 'string' ? theme : theme.id;
return id === 'stationery-theme' || id === 'stationery-dark-theme';
};
export const LightTheme: Theme = {
id: 'light-theme',
kind: ThemeKind.Light,
classNames: ['light-theme', lightTheme, onLightFontWeight, 'prism-light'],
};
export const SilverTheme: Theme = {
id: 'silver-theme',
kind: ThemeKind.Light,
classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'],
};
export const DarkTheme: Theme = {
id: 'dark-theme',
kind: ThemeKind.Dark,
classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'],
};
export const ButterTheme: Theme = {
id: 'butter-theme',
kind: ThemeKind.Dark,
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
};
export const DiscordTheme: Theme = {
id: 'discord-theme',
kind: ThemeKind.Dark,
classNames: ['discord-theme', discordTheme, onDarkFontWeight, 'prism-dark'],
};
export const DiscordDarkerTheme: Theme = {
id: 'discord-darker-theme',
kind: ThemeKind.Dark,
classNames: ['discord-darker-theme', discordDarkerTheme, onDarkFontWeight, 'prism-dark'],
};
export const TwilightTheme: Theme = {
id: 'twilight-theme',
kind: ThemeKind.Dark,
classNames: ['twilight-theme', twilightTheme, onDarkFontWeight, 'prism-dark'],
};
export const MochaTheme: Theme = {
id: 'mocha-theme',
kind: ThemeKind.Dark,
classNames: ['mocha-theme', mochaTheme, onDarkFontWeight, 'prism-dark'],
};
export const CatppuccinMochaTheme: Theme = {
id: 'catppuccin-mocha-theme',
kind: ThemeKind.Dark,
classNames: ['catppuccin-mocha-theme', catppuccinMochaTheme, onDarkFontWeight, 'prism-dark'],
};
export const StationeryTheme: Theme = {
id: 'stationery-theme',
kind: ThemeKind.Light,
classNames: ['stationery-theme', 'stationery', stationeryTheme, onLightFontWeight, 'prism-light'],
};
export const StationeryDarkTheme: Theme = {
id: 'stationery-dark-theme',
kind: ThemeKind.Dark,
classNames: ['stationery-dark-theme', 'stationery', stationeryDarkTheme, onDarkFontWeight, 'prism-dark'],
};
export const useThemes = (): Theme[] => {
const [pluginThemesUpdate, setPluginThemesUpdate] = useState(0);
// Force update when plugins change (this is a simple approach)
useEffect(() => {
const interval = setInterval(() => {
setPluginThemesUpdate(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
const builtInThemes: Theme[] = useMemo(() => [
LightTheme, SilverTheme, StationeryTheme, StationeryDarkTheme, DarkTheme, ButterTheme,
DiscordTheme, DiscordDarkerTheme, TwilightTheme,
MochaTheme, CatppuccinMochaTheme
], []);
// Convert plugin themes to Theme format
const pluginThemes: Theme[] = useMemo(() => {
const themes = pluginRegistry.getPluginThemes();
return themes.map(pluginTheme => ({
id: pluginTheme.id,
kind: pluginTheme.kind === 'dark' ? ThemeKind.Dark : ThemeKind.Light,
classNames: [
pluginTheme.className,
pluginTheme.kind === 'dark' ? onDarkFontWeight : onLightFontWeight,
pluginTheme.kind === 'dark' ? 'prism-dark' : 'prism-light'
]
}));
}, [pluginThemesUpdate]);
return [...builtInThemes, ...pluginThemes];
};
export const useThemeNames = (): Record<string, string> => {
const [pluginThemesUpdate, setPluginThemesUpdate] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setPluginThemesUpdate(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return useMemo(() => {
const builtInNames = {
[LightTheme.id]: 'Light',
[SilverTheme.id]: 'Silver',
[StationeryTheme.id]: 'Stationery',
[StationeryDarkTheme.id]: 'Stationery Dark',
[DarkTheme.id]: 'Dark',
[ButterTheme.id]: 'Butter',
[DiscordTheme.id]: 'Discord',
[DiscordDarkerTheme.id]: 'Discord Darker',
[TwilightTheme.id]: 'Twilight',
[MochaTheme.id]: 'Mocha',
[CatppuccinMochaTheme.id]: 'Catppuccin Mocha',
};
const pluginThemes = pluginRegistry.getPluginThemes();
const pluginNames = pluginThemes.reduce((acc, theme) => {
acc[theme.id] = theme.name;
return acc;
}, {} as Record<string, string>);
return { ...builtInNames, ...pluginNames };
}, [pluginThemesUpdate]);
};
export const useSystemThemeKind = (): ThemeKind => {
const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []);
const [themeKind, setThemeKind] = useState<ThemeKind>(
darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light
);
useEffect(() => {
const handleMediaQueryChange = () => {
setThemeKind(darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light);
};
darkModeQueryList.addEventListener('change', handleMediaQueryChange);
return () => {
darkModeQueryList.removeEventListener('change', handleMediaQueryChange);
};
}, [darkModeQueryList, setThemeKind]);
return themeKind;
};
export const useActiveTheme = (): Theme => {
const systemThemeKind = useSystemThemeKind();
const themes = useThemes();
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [themeId] = useSetting(settingsAtom, 'themeId');
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
if (!systemTheme) {
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
return selectedTheme;
}
const selectedTheme =
systemThemeKind === ThemeKind.Dark
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
return selectedTheme;
};
const ThemeContext = createContext<Theme | null>(null);
export const ThemeContextProvider = ThemeContext.Provider;
export const useTheme = (): Theme => {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error('No theme provided!');
}
return theme;
};