Creating Custom Themes
Relevant source files
The following files were used as context for generating this wiki page:The Endfield-Pomodoro application features a dynamic and extensible theme system. This system allows for complete customization of the application's appearance, from basic color palettes to complex, animated background and foreground visual effects. The architecture is built upon CSS Variables, which are dynamically updated by React based on user selection.
This guide outlines the process for creating and integrating a new custom theme. The process involves defining a color palette, adding the theme to the UI, and optionally creating custom React components for unique visual effects.
Sources: README.md:129-149
Theme System Architecture
The theme system's core is the dynamic application of CSS variables to the root HTML element (:root). A set of predefined CSS variable names for colors are used throughout the application's stylesheets. When a user selects a theme, the application retrieves the corresponding color values from a configuration file and injects them into the document's style, effectively changing the entire UI's color scheme instantly.
The flow is as follows:
- A user selects a new theme from the "SYSTEM CONFIG" panel.
- The
settingsstate in the mainAppcomponent is updated with the newThemePreset. - A
useLayoutEffecthook is triggered by the state change. - This effect iterates through the color definitions for the selected theme and applies each one as a CSS variable to
document.documentElement. - The browser re-renders the UI using the new CSS variable values.
This diagram illustrates the theme application flow:
Sources: src/App.tsx:231-237, src/index.css:33-43
Step 1: Define Theme Colors and Preset
The first step is to define the color palette for the new theme and add a corresponding preset identifier.
Add a ThemePreset Enum
All available themes are identified by an enum ThemePreset. Add your new theme's name to this enum.
File: src/types.ts
export const ThemePreset = {
ORIGIN: 'ORIGIN',
AZURE: 'AZURE',
NEON: 'NEON',
MATRIX: 'MATRIX',
TACTICAL: 'TACTICAL',
ROYAL: 'ROYAL',
INDUSTRIAL: 'INDUSTRIAL',
LABORATORY: 'LABORATORY',
MIKU: 'MIKU',
YOUR_THEME: 'YOUR_THEME' // Add new theme here
} as const;
export type ThemePreset = typeof ThemePreset[keyof typeof ThemePreset];
Sources: src/types.ts:24-35
Define the Color Palette
All theme color definitions are stored in the THEMES object in src/config/themes.ts. Add a new entry using the ThemePreset key you just created. You must define a value for each of the standard CSS color variables.
File: src/config/themes.ts
import { ThemePreset } from '../types';
export const THEMES: Record<ThemePreset, Record<string, string>> = {
// ... other themes
[ThemePreset.YOUR_THEME]: {
'--color-base': '#121212',
'--color-surface': '#232323',
'--color-highlight': '#343434',
'--color-primary': '#bb86fc',
'--color-secondary': '#03dac6',
'--color-accent': '#cf6679',
'--color-text': '#ffffff',
'--color-dim': '#a0a0a0',
'--color-success': '#4caf50',
'--color-error': '#f44336'
}
};
Sources: src/config/themes.ts:6-10, README.md:138-149
The following table lists the required CSS variables and their intended purpose.
| Variable Name | Description |
|---|---|
--color-base | The main background color of the application. |
--color-surface | Background color for elevated surfaces like panels. |
--color-highlight | Color for borders, dividers, and subtle highlights. |
--color-primary | The primary accent and interactive element color. |
--color-secondary | A secondary accent color. |
--color-accent | A third accent color, often for specific states. |
--color-text | The primary text color. |
--color-dim | A dimmer text color for secondary information. |
--color-success | Color for success states or indicators. |
--color-error | Color for error states or indicators. |
Sources: src/config/themes.ts:7-16, src/index.css:12-21
Step 2: Integrate into the Settings UI
To make the new theme selectable, it must be added to the theme dropdown in the settings panel and given a display name.
Add to Theme Dropdown
In src/App.tsx, locate the CustomSelect component for themes and add your new ThemePreset to the options array.
File: src/App.tsx
<CustomSelect
value={settings.theme}
options={[
{ value: ThemePreset.ORIGIN, label: t('THEME_ORIGIN') },
// ... other themes
{ value: ThemePreset.MIKU, label: t('THEME_MIKU') },
{ value: ThemePreset.YOUR_THEME, label: t('THEME_YOUR_THEME') } // Add new option
]}
onChange={(value) => setSettings({ ...settings, theme: value as ThemePreset })}
/>
Sources: src/App.tsx:500-512
Add Internationalization Key
Add the translation key (e.g., THEME_YOUR_THEME) to the translations object in src/utils/i18n.ts for both English (EN) and Chinese (CN).
File: src/utils/i18n.ts
export const translations = {
[Language.EN]: {
// ...
THEME_MIKU: 'PRESET_09: MIKU',
THEME_YOUR_THEME: 'PRESET_10: YOUR_THEME_NAME', // Add EN name
},
[Language.CN]: {
// ...
THEME_MIKU: '预设_09: MIKU',
THEME_YOUR_THEME: '预设_10: 你的主题', // Add CN name
}
};
Sources: src/utils/i18n.ts:43-52, src/utils/i18n.ts:139-148
Step 3: Theme Application Logic
The core logic for applying themes is handled automatically by the App component. A useLayoutEffect hook observes changes to settings.theme and applies the corresponding colors to the document.documentElement.
This sequence diagram shows the interaction when a user changes the theme.
The code responsible for this is concise:
File: src/App.tsx
// Apply theme
useLayoutEffect(() => {
const root = document.documentElement;
const themeColors = THEMES[settings.theme];
Object.entries(themeColors).forEach(([key, value]) => {
root.style.setProperty(key, value as string);
});
}, [settings.theme]);
Sources: src/App.tsx:231-237
Step 4: Add Custom Visual Effects (Optional)
The theme system supports modular, theme-specific visual effects through dedicated React components. These are managed by the BackgroundLayer and ForegroundLayer components.
The selection logic is handled by a switch statement inside these layer components, which render a specific effect component based on the active theme.
This diagram shows how BackgroundLayer selects which effect to render. The same logic applies to ForegroundLayer.
Sources: src/components/TerminalUI.tsx:30-43
Creating a Background Effect
- Create a new React component for your background effect in
src/components/themes/BackgroundEffects.tsx. This can be any valid JSX, including inline SVG, CSS animations, etc. - Import your new component into
src/components/TerminalUI.tsx. - Add a
casefor yourThemePresetin theswitchstatement inside theBackgroundLayercomponent to render your effect.
Example: Adding a case for YOUR_THEME
File: src/components/TerminalUI.tsx
// ... imports
import { YourThemeBackground } from './themes/BackgroundEffects'; // 1. Import
export const BackgroundLayer: React.FC<{ theme?: ThemePreset }> = ({ theme = ThemePreset.ORIGIN }) => {
const renderContent = () => {
switch (theme) {
// ... other cases
case ThemePreset.MIKU: return <MikuBackgroundLayer />;
case ThemePreset.YOUR_THEME: return <YourThemeBackground />; // 2. Add case
default: return <OriginGrid />;
}
};
// ...
};
Sources: src/components/TerminalUI.tsx:27-43, src/components/themes/BackgroundEffects.tsx
Creating a Foreground Effect
Foreground effects typically respond to mouse movement and are rendered in a layer above the main content but do not intercept pointer events. The process is identical to creating a background effect, but you will modify ForegroundLayer in src/components/TerminalUI.tsx and add your component to src/components/themes/ForegroundEffects.tsx.
The ForegroundLayer component already contains a mouse-tracking hook that passes the mousePos prop to the effect components.
Sources: src/components/TerminalUI.tsx:50-83, src/components/themes/ForegroundEffects.tsx
Creating Theme-Specific Decorative Components
For effects that are more complex or need to be positioned relative to other UI elements (like the footer), you can create a standalone decorative component. The MikuDecorations component is a prime example. It is conditionally rendered directly within App.tsx only when the Miku theme is active. This approach offers more flexibility for positioning and interaction with the main application layout.
File: src/App.tsx
<div className="h-[100dvh] ...">
{/* Background visual effects (Z-0) */}
<BackgroundLayer theme={settings.theme} />
{/* Miku theme exclusive decorative elements (Z-5) */}
<MikuDecorations theme={settings.theme} footerHeight={footerHeight} />
{/* Foreground HUD visual effects (Z-50) */}
<ForegroundLayer theme={settings.theme} />
{/* ... rest of the app */}
</div>
Sources: src/App.tsx:265-271, src/components/MikuDecorations.tsx:148-154
Summary
Creating a custom theme in Endfield-Pomodoro is a structured process that leverages a highly modular and extensible architecture. By defining a color palette, adding a ThemePreset, and updating the UI, a new look-and-feel can be quickly established. For deeper customization, the system allows developers to create and plug in unique React components for background, foreground, and decorative effects, enabling a high degree of visual creativity for each theme.