Files
cinny/src/app/hooks/useTheme.ts
T
jaredandClaude Opus 4.8 65e24bd446 feat(themes): 5 new dark theme presets — Cyberpunk/Ocean/Blood Red/Matrix/Midnight (P5-2)
Five complete vanilla-extract themes registered in useTheme (useThemes +
useThemeNames), each spreading darkThemeData so Success/Warning/Critical keep
their semantic colors and only Background/Surface/Primary/Secondary are
recolored. A code-review pass computed WCAG contrast for every theme; all body
and accent pairs clear AA except Midnight's Primary.OnMain which was 4.49:1 —
fixed by changing OnMain #0d1320 -> #000000 (5.07:1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:47:50 -04:00

180 lines
4.9 KiB
TypeScript

import { lightTheme } from 'folds';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
import {
bloodRedTheme,
butterTheme,
classicMatrixTheme,
cyberpunkTheme,
darkTheme,
lotusTerminalLightTheme,
lotusTerminalTheme,
midnightTheme,
oceanTheme,
silverTheme,
} from '../../colors.css';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
export enum ThemeKind {
Light = 'light',
Dark = 'dark',
}
export type Theme = {
id: string;
kind: ThemeKind;
classNames: string[];
};
export const LightTheme: Theme = {
id: 'light-theme',
kind: ThemeKind.Light,
classNames: [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 CyberpunkTheme: Theme = {
id: 'cyberpunk-theme',
kind: ThemeKind.Dark,
classNames: ['cyberpunk-theme', cyberpunkTheme, onDarkFontWeight, 'prism-dark'],
};
export const OceanTheme: Theme = {
id: 'ocean-theme',
kind: ThemeKind.Dark,
classNames: ['ocean-theme', oceanTheme, onDarkFontWeight, 'prism-dark'],
};
export const BloodRedTheme: Theme = {
id: 'blood-red-theme',
kind: ThemeKind.Dark,
classNames: ['blood-red-theme', bloodRedTheme, onDarkFontWeight, 'prism-dark'],
};
export const ClassicMatrixTheme: Theme = {
id: 'classic-matrix-theme',
kind: ThemeKind.Dark,
classNames: ['classic-matrix-theme', classicMatrixTheme, onDarkFontWeight, 'prism-dark'],
};
export const MidnightTheme: Theme = {
id: 'midnight-theme',
kind: ThemeKind.Dark,
classNames: ['midnight-theme', midnightTheme, onDarkFontWeight, 'prism-dark'],
};
export const LotusTerminalTheme: Theme = {
id: 'lotus-terminal-theme',
kind: ThemeKind.Dark,
classNames: ['lotus-terminal-theme', lotusTerminalTheme, onDarkFontWeight, 'prism-tds-dark'],
};
export const LotusTerminalLightTheme: Theme = {
id: 'lotus-terminal-light-theme',
kind: ThemeKind.Light,
classNames: [
'lotus-terminal-light-theme',
lotusTerminalLightTheme,
onLightFontWeight,
'prism-tds-light',
],
};
export const useThemes = (): Theme[] => {
const themes: Theme[] = useMemo(
() => [
LightTheme,
SilverTheme,
DarkTheme,
ButterTheme,
CyberpunkTheme,
OceanTheme,
BloodRedTheme,
ClassicMatrixTheme,
MidnightTheme,
],
[],
);
return themes;
};
export const useThemeNames = (): Record<string, string> =>
useMemo(
() => ({
[LightTheme.id]: 'Light',
[SilverTheme.id]: 'Silver',
[DarkTheme.id]: 'Dark',
[ButterTheme.id]: 'Butter',
[CyberpunkTheme.id]: 'Cyberpunk',
[OceanTheme.id]: 'Ocean',
[BloodRedTheme.id]: 'Blood Red',
[ClassicMatrixTheme.id]: 'Classic Matrix',
[MidnightTheme.id]: 'Midnight',
}),
[],
);
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;
};