mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
f152e03ae7
* feat: add support for loading custom themes from files --------- Co-authored-by: jeffvli <jeffvictorli@gmail.com>
260 lines
8.2 KiB
Markdown
260 lines
8.2 KiB
Markdown
# Custom Themes
|
||
|
||
Custom themes let you add your own light and dark themes to the **desktop** app without rebuilding Feishin. Drop JSON theme files into the Themes folder; Feishin watches that folder and reloads themes when files change.
|
||
|
||
Custom themes are **Electron / desktop only**. They are not available in the web or Docker builds.
|
||
|
||
---
|
||
|
||
## Getting started
|
||
|
||
1. Open **Settings → General → Theme**.
|
||
2. Under **Custom Themes**, click **Open Folder** to reveal the Themes directory.
|
||
3. Add a `.json` file (for example `my-theme.json`).
|
||
4. Select the theme from the Theme dropdown (it appears under Dark or Light based on `mode`).
|
||
|
||
You can also click **Reload** in Settings if a change was not picked up automatically.
|
||
|
||
### Themes folder location
|
||
|
||
| Platform | Typical path |
|
||
|----------|----------------|
|
||
| Windows | `%APPDATA%\feishin\Themes` |
|
||
| macOS | `~/Library/Application Support/feishin/Themes` |
|
||
| Linux | `~/.config/feishin/Themes` |
|
||
|
||
In development builds the folder name is under `feishin-dev` instead of `feishin`.
|
||
|
||
Theme files must be **`.json` files in the root of the Themes folder** (not nested in subfolders). Linked stylesheets may live next to them or in subfolders, as long as they stay inside Themes.
|
||
|
||
---
|
||
|
||
## Theme file format
|
||
|
||
Each theme is a single JSON object.
|
||
|
||
```json
|
||
{
|
||
"mode": "dark",
|
||
"extends": "defaultDark",
|
||
"colors": {
|
||
"primary": "rgb(53, 116, 252)",
|
||
"background": "rgb(12, 12, 12)",
|
||
"foreground": "rgb(225, 225, 225)"
|
||
},
|
||
"app": {
|
||
"scrollbar-size": "9px"
|
||
},
|
||
"mantineOverride": {
|
||
"primaryShade": {
|
||
"dark": 6
|
||
}
|
||
},
|
||
"stylesheets": ["my-theme.css"]
|
||
}
|
||
```
|
||
|
||
### Identity
|
||
|
||
| Derived from | Behavior |
|
||
|--------------|----------|
|
||
| **id** | Filename without `.json` (e.g. `rose-pine-custom.json` → `rose-pine-custom`) |
|
||
| **label** | Title-cased id with `-` / `_` replaced by spaces (e.g. `Rose Pine Custom`) |
|
||
|
||
Use a unique filename. The id is what appears in settings and what you use when another theme `extends` this one.
|
||
|
||
### Fields
|
||
|
||
| Field | Required | Description |
|
||
|-------|----------|-------------|
|
||
| `mode` | No (default `dark`) | `"dark"` or `"light"`. Controls which theme group the theme appears in and the app color scheme. |
|
||
| `extends` | No | Built-in theme id or another custom theme’s id (filename without `.json`). Your theme is merged on top of the base. |
|
||
| `colors` | No | Palette overrides. Invalid CSS colors are dropped and shown as a warning in Settings. |
|
||
| `app` | No | App chrome / layout CSS variable overrides. |
|
||
| `mantineOverride` | No | Partial [Mantine theme override](https://mantine.dev/theming/theme-object/). |
|
||
| `stylesheets` | No | Array of CSS file paths **relative to the Themes folder**. Contents are inlined when the theme is active. Paths that escape the Themes folder are ignored. |
|
||
|
||
Unspecified color and app values fall back to Feishin’s default theme (and then to anything provided by `extends`).
|
||
|
||
---
|
||
|
||
## Colors
|
||
|
||
Supported `colors` keys:
|
||
|
||
| Key | Typical use |
|
||
|-----|-------------|
|
||
| `background` | Main app background |
|
||
| `background-alternate` | Alternate / nested background |
|
||
| `surface` | Cards, inputs, elevated surfaces |
|
||
| `surface-foreground` | Text on surfaces |
|
||
| `foreground` | Primary text |
|
||
| `foreground-muted` | Secondary / muted text |
|
||
| `primary` | Accent / primary action color (also drives generated primary shades) |
|
||
| `black` | Black token |
|
||
| `white` | White token |
|
||
| `state-error` | Error state |
|
||
| `state-info` | Info state |
|
||
| `state-success` | Success state |
|
||
| `state-warning` | Warning state |
|
||
|
||
Values must be valid CSS colors recognized by Feishin’s color validator (for example `rgb(...)`, `rgba(...)`, `#rrggbb`, `#rgb`). Named CSS colors like `red` are rejected. Invalid entries are ignored and listed under Custom Themes warnings in Settings.
|
||
|
||
---
|
||
|
||
## App variables
|
||
|
||
Supported `app` keys (CSS values as strings):
|
||
|
||
| Key | Description |
|
||
|-----|-------------|
|
||
| `content-max-width` | Max width of main content |
|
||
| `root-font-size` | Root font size |
|
||
| `overlay-header` | Header overlay background |
|
||
| `overlay-subheader` | Subheader overlay background |
|
||
| `scrollbar-size` | Scrollbar thickness |
|
||
| `scrollbar-handle-background` | Scrollbar handle |
|
||
| `scrollbar-handle-hover-background` | Handle on hover |
|
||
| `scrollbar-handle-active-background` | Handle when active |
|
||
| `scrollbar-handle-border-radius` | Handle corner radius |
|
||
| `scrollbar-track-background` | Scrollbar track |
|
||
| `scrollbar-track-hover-background` | Track on hover |
|
||
| `scrollbar-track-active-background` | Track when active |
|
||
| `scrollbar-track-border-radius` | Track corner radius |
|
||
|
||
---
|
||
|
||
## Extending themes
|
||
|
||
Use `extends` to start from a built-in or another custom theme and only override what you need.
|
||
|
||
```json
|
||
{
|
||
"mode": "dark",
|
||
"extends": "nord",
|
||
"colors": {
|
||
"primary": "#88c0d0"
|
||
}
|
||
}
|
||
```
|
||
|
||
Rules:
|
||
|
||
- Custom theme fields always win over the theme they extend.
|
||
- Custom → custom chains are flattened when loading (depth limit of 10; cycles are ignored).
|
||
- If `extends` is a built-in id, merging with that built-in’s defaults happens in the renderer.
|
||
- If `extends` is omitted, the theme still merges onto Feishin’s shared default palette.
|
||
|
||
### Built-in theme ids
|
||
|
||
Use these as `extends` values (same ids as in Settings):
|
||
|
||
`ayuDark`, `ayuLight`, `catppuccinLatte`, `catppuccinMocha`, `defaultDark`, `defaultLight`, `dracula`, `everforestDark`, `everforestLight`, `githubDark`, `githubLight`, `glassyDark`, `gruvboxDark`, `gruvboxLight`, `highContrastDark`, `highContrastLight`, `materialDark`, `materialLight`, `monokai`, `nightOwl`, `nord`, `oneDark`, `rosePine`, `rosePineDawn`, `rosePineMoon`, `shadesOfPurple`, `solarizedDark`, `solarizedLight`, `tokyoNight`, `vscodeDarkPlus`, `vscodeLightPlus`, `zenburn`
|
||
|
||
---
|
||
|
||
## Linked stylesheets
|
||
|
||
For larger visual overrides (glass effects, layout tweaks, etc.), point `stylesheets` at CSS files inside the Themes folder:
|
||
|
||
```json
|
||
{
|
||
"mode": "dark",
|
||
"extends": "defaultDark",
|
||
"stylesheets": ["overrides/my-theme.css"]
|
||
}
|
||
```
|
||
|
||
```text
|
||
Themes/
|
||
my-theme.json
|
||
overrides/
|
||
my-theme.css
|
||
```
|
||
|
||
Notes:
|
||
|
||
- Feishin watches **JSON** theme files for automatic reload. After editing only a CSS file, click **Reload** in Settings (or touch/save the `.json` file) so stylesheets are re-read.
|
||
- Empty or unreadable stylesheet paths are skipped with a console warning.
|
||
|
||
---
|
||
|
||
## Examples
|
||
|
||
### Minimal accent-only dark theme
|
||
|
||
`accent-blue.json`:
|
||
|
||
```json
|
||
{
|
||
"mode": "dark",
|
||
"extends": "defaultDark",
|
||
"colors": {
|
||
"primary": "rgb(80, 160, 255)"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Full light palette
|
||
|
||
`paper-light.json`:
|
||
|
||
```json
|
||
{
|
||
"mode": "light",
|
||
"colors": {
|
||
"background": "rgb(250, 249, 246)",
|
||
"background-alternate": "rgb(242, 240, 235)",
|
||
"surface": "rgb(255, 255, 255)",
|
||
"surface-foreground": "rgb(40, 40, 40)",
|
||
"foreground": "rgb(30, 30, 30)",
|
||
"foreground-muted": "rgb(110, 110, 110)",
|
||
"primary": "rgb(180, 83, 9)",
|
||
"black": "rgb(0, 0, 0)",
|
||
"white": "rgb(255, 255, 255)",
|
||
"state-error": "rgb(185, 28, 28)",
|
||
"state-info": "rgb(37, 99, 235)",
|
||
"state-success": "rgb(22, 163, 74)",
|
||
"state-warning": "rgb(217, 119, 6)"
|
||
},
|
||
"app": {
|
||
"overlay-header": "linear-gradient(rgb(250 249 246 / 50%) 0%, rgb(250 249 246 / 80%))",
|
||
"scrollbar-handle-background": "rgba(120, 120, 120, 30%)"
|
||
},
|
||
"mantineOverride": {
|
||
"primaryShade": {
|
||
"light": 5
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Theme that extends another custom theme
|
||
|
||
`nord-soft.json`:
|
||
|
||
```json
|
||
{
|
||
"mode": "dark",
|
||
"extends": "nord",
|
||
"colors": {
|
||
"background": "rgb(36, 41, 51)",
|
||
"surface": "rgb(46, 52, 64)"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | What to check |
|
||
|---------|----------------|
|
||
| Theme missing from the list | File must be `.json` in the Themes root; use **Reload**. |
|
||
| Theme shows an error in Settings | JSON is invalid or not a top-level object. Fix the file and reload. |
|
||
| Theme loads with a warning | One or more `colors` values were invalid and ignored. |
|
||
| CSS changes not applying | Edit/save the `.json` or click **Reload**; stylesheet watch is tied to JSON changes. |
|
||
| Extending does nothing / looks wrong | Confirm the `extends` id matches a built-in id or another custom filename (without `.json`). Avoid circular chains. |
|
||
|
||
Broken themes still appear in Settings with their error message so you can fix them without digging through logs.
|