docs(themes): align THEMES.md with Theme × Skin architecture

THEMES.md still described the pre-#627 model where each theme was a
monolithic palette name (Dark, Light, Slate, Solarized Dark, Monokai,
Nord, OLED). The current architecture splits appearance into two
orthogonal pickers:

- Theme (System / Dark / Light) — applied as `.dark` class on <html>
- Skin (8 named accent palettes) — applied as `data-skin` attribute

Rewrite the doc to:
- Open with the Theme × Skin separation and how they combine
- List the 3 themes and 8 actual skins shipped in static/style.css
  (default, ares, mono, slate, poseidon, sisyphus, charizard, sienna),
  with the same descriptive tone as the original
- Replace "Creating a Custom Theme" with "Creating a Custom Skin" as
  the primary extension point, with paired light + dark CSS variants
- Note the WebUI extensions surface (docs/EXTENSIONS.md) as a
  no-fork path for self-hosted custom skins
- Update internals to reflect classList.toggle('dark') + dataset.skin
  + dataset.fontSize instead of the old data-theme-only model
- Add a brief Font Size section since it sits in the same picker
- Keep a smaller Custom Theme section for the rare case someone wants
  to override the core palette, redirecting most users to skins

Docs-only change; no code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael De GolsandClaude Opus 4.7 committed 2026-05-11 18:35:12 +02:00
1 parent b766b7f759
commit 0f8ba4d8d3
1 file changed
+136 -115
+136 -115
View File
@@ -1,19 +1,31 @@
# Hermes Web UI — Themes
Hermes Web UI supports pluggable color themes. Seven themes ship built-in, and
you can create your own with pure CSS — no Python changes needed.
Hermes Web UI splits **appearance** into two independent pickers:
- **Theme** — the mode: `System`, `Dark`, or `Light`. Drives the background,
text, surface, and chrome colors.
- **Skin** — the accent palette: eight named skins ship built-in. Drives only
the `--accent` family (active states, links, focus rings, primary actions).
You pick one of each and they combine, so the look adapts to your environment
without losing your favorite accent — pure CSS, no Python changes needed.
---
## Switching Themes
## Switching Appearance
**Settings panel:** Click the gear icon, select a theme from the dropdown. The
preview is instant — the UI updates as you click through options.
**Settings panel:** Click the gear icon → **Appearance**. The **Theme** card
toggles Light/Dark/System; the **Skin** grid offers eight accent palettes.
Preview is instant — the UI updates as you click.
**Slash command:** Type `/theme dark` or `/theme light` in the composer.
**Slash command:** Type `/theme <name>` in the composer. The command accepts
both theme names (`system`, `dark`, `light`) and skin names (`default`, `ares`,
`mono`, `slate`, `poseidon`, `sisyphus`, `charizard`, `sienna`). It updates the
matching axis and leaves the other one alone.
**Themes persist** across page reloads and server restarts (stored in
`settings.json` server-side, with `localStorage` for flicker-free loading).
**Persistence:** Both choices are stored in `localStorage` for flicker-free
loading, and saved server-side via `POST /api/settings` (under `theme` and
`skin` keys in `settings.json`).
---
@@ -21,125 +33,134 @@ preview is instant — the UI updates as you click through options.
| Theme | Description |
|-------|-------------|
| **Dark** (default) | Deep navy/indigo with muted blue accents. Easy on the eyes for long sessions. |
| **Light** | Warm off-white with dark text. High contrast for bright environments. |
| **Slate** | Warm charcoal, lighter than Dark. Easier on the eyes for extended use. |
| **Solarized Dark** | Ethan Schoonover's classic dark palette. Teal background, warm accents. |
| **Monokai** | Warm dark theme inspired by the Monokai editor scheme. Green/pink accents. |
| **Nord** | Arctic blue-gray palette from the Nord color system. Calm and minimal. |
| **OLED** | True black (#000) backgrounds for OLED displays. Minimizes glow and burn-in risk. |
| **Custom themes** | Any string accepted by `settings.json`, `POST /api/settings`, and `/theme` if added to the picker/command list. Pure CSS variables only. |
| **System** (default) | Follows the OS `prefers-color-scheme` preference and updates live. |
| **Dark** | Deep dark surfaces, low-glare for long sessions. |
| **Light** | Bright surfaces with dark text, high contrast for daylight environments. |
The theme is applied as a class on `<html>`: `.dark` is present for dark mode,
absent for light. System mode tracks the OS preference at runtime.
---
## Built-in Skins
| Skin | Description |
|------|-------------|
| **Default** | The original Hermes gold accent. Warm and understated. |
| **Ares** | Fiery red. High-energy and assertive. |
| **Mono** | Neutral gray. Distraction-free, for deep focus. |
| **Slate** | Slate blue-gray. Subtle and grown-up. |
| **Poseidon** | Ocean blue. Calm and focused for long sessions. |
| **Sisyphus** | Vivid purple. Distinctive without being loud. |
| **Charizard** | Warm orange. Energetic and easy on the eyes. |
| **Sienna** | Warm clay and sand earth palette. Soft and natural. |
Each skin defines paired light + dark variants so it reads cleanly on either
theme. The skin is applied as `data-skin="<name>"` on `<html>` (the default
skin clears the attribute).
---
## Creating a Custom Skin
A skin is a small CSS block that overrides the accent variables for both the
light and dark variants:
```css
/* Light variant */
:root[data-skin="my-skin"] {
--accent: #2E7D32; /* Active states, links, primary buttons */
--accent-hover: #1B5E20; /* Hover */
--accent-bg: rgba(46,125,50,0.08); /* Soft tinted backgrounds */
--accent-bg-strong: rgba(46,125,50,0.15); /* Highlighted backgrounds */
--accent-text: #1B5E20; /* Text on accent bg */
}
/* Dark variant — usually lighter or more saturated for contrast */
:root.dark[data-skin="my-skin"] {
--accent: #66BB6A;
--accent-hover: #43A047;
--accent-bg: rgba(102,187,106,0.08);
--accent-bg-strong: rgba(102,187,106,0.15);
--accent-text: #66BB6A;
}
```
Two ways to ship it:
1. **In the repo (built-in):** add the block to `static/style.css`, register it
in the Settings skin picker (`static/index.html`) and in the `/theme` command
list (`static/commands.js`), then open a PR.
2. **Self-hosted (no fork):** use the WebUI extensions surface — see
`docs/EXTENSIONS.md`. Drop your CSS in `HERMES_WEBUI_EXTENSION_DIR` and
declare it in `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS`. No code changes
needed; the skin attribute can be set from your own JS.
### Tips
- **Test both themes.** A skin that pops on Dark can be illegible on Light.
Always check `:root[data-skin]` (light) *and* `:root.dark[data-skin]` (dark).
- **Pick contrasting `--accent-text` on `--accent-bg`.** The strong variant
appears behind small labels and chips; weak contrast there reads as blur.
- **The logo gradient uses `--accent` automatically**, so it adapts to your
skin without any extra work.
- **No server changes needed.** The `skin` setting in `settings.json` accepts
any string, so your custom skin name persists without code changes once you
load the CSS.
---
## Creating a Custom Theme
A theme is a CSS block that overrides the color variables. Add it to
`static/style.css` (or a separate file that you link after the main stylesheet).
A full custom *theme* (a different overall mood, not just an accent change) is
a larger task than a skin: it has to redefine the core palette variables
(`--bg`, `--surface`, `--text`, `--border`, `--code-bg`, and friends) for one
or both modes. The contract is defined in the top `:root` and `:root.dark`
blocks of `static/style.css` — start there.
### Step 1: Define your theme block
Every color in the UI comes from these CSS variables:
```css
:root[data-theme="your-theme-name"] {
/* Core palette */
--bg: #1a1a2e; /* Main background */
--sidebar: #16213e; /* Sidebar background */
--border: rgba(255,255,255,0.08); /* Subtle borders */
--border2: rgba(255,255,255,0.14); /* Stronger borders */
--text: #e8e8f0; /* Primary text color */
--muted: #8888aa; /* Secondary/muted text */
--accent: #e94560; /* Accent color (errors, warnings, delete) */
--blue: #7cb9ff; /* Primary action color (links, active states) */
--gold: #c9a84c; /* Secondary accent (pinned items, gold highlights) */
--code-bg: #0d1117; /* Code block background */
/* Surface and chrome (required for full theme polish) */
--surface: #1a2535; /* Dropdowns, popups, toast, approval card */
--topbar-bg: rgba(22,33,62,.98); /* Topbar background */
--main-bg: rgba(26,26,46,0.5); /* Main chat area background */
--input-bg: rgba(255,255,255,.04); /* Input/button subtle backgrounds */
--hover-bg: rgba(255,255,255,.06); /* Hover state backgrounds */
--focus-ring: rgba(124,185,255,.35); /* Focus border color */
--focus-glow: rgba(124,185,255,.08); /* Focus box-shadow glow */
/* Typography (required for readable text across themes) */
--strong: #fff; /* Bold text in messages */
--em: #c9c9e8; /* Italic text in messages */
--code-text: #f0c27f; /* Inline code text color */
--code-inline-bg: rgba(0,0,0,.35); /* Inline code background */
--pre-text: #e2e8f0; /* Code block text color */
}
```
The **core palette** controls the overall mood. The **surface/chrome** and
**typography** variables are part of the standard theme contract — define all
of them for a complete theme.
For **light themes**, you also need `:root[data-theme="name"]` overrides
for elements that use `rgba(255,255,255,.XX)` hover/border effects (these
are invisible on light backgrounds). See the built-in light theme for the
full pattern — it overrides ~45 selectors for proper dark-on-light contrast
on hover states, borders, chips, role labels, session items, and
interactive elements.
### Step 2: Add it to the theme picker (optional)
To make your theme appear in the Settings dropdown, add an `<option>` to the
theme `<select>` in `static/index.html`:
```html
<option value="your-theme-name">Your Theme Name</option>
```
And update the `/theme` command's valid theme list in `static/commands.js`.
### Step 3: Test it
Switch to your theme via `/theme your-theme-name` or the Settings panel.
Check these areas:
- Sidebar session list (hover states, active state, project borders)
- Message bubbles (user vs assistant styling)
- Code blocks (background contrast, copy button visibility)
- Tool cards (running indicator, expand/collapse)
- Settings panel and login page
- Mobile layout (hamburger sidebar, bottom nav)
### Tips
- **Light themes** need scrollbar and selection overrides, plus the full
text/code set (`--strong`, `--em`, `--code-text`, `--code-inline-bg`,
`--pre-text`) or they will look broken.
- The **logo gradient** uses `--accent` automatically, so it adapts to your
theme without extra work.
- **Prism.js syntax highlighting** uses its own CDN stylesheet (Tomorrow theme).
It works well on dark themes; on light themes the contrast is acceptable but
not perfect. Custom Prism theme support is planned for a future update.
- **No server changes needed.** The `theme` setting in `settings.json` accepts
any string — your custom theme name will persist without code changes.
Most of the time, a custom **skin** is what you actually want. Reach for a
custom theme only when the existing Light/Dark modes don't fit (for example,
a high-contrast accessibility theme or an OLED black variant).
---
## How Themes Work Internally
## Font Size
1. Each theme is a `:root[data-theme="name"]` CSS block that overrides variables.
2. Switching themes sets `document.documentElement.dataset.theme = name` in JS.
3. A tiny inline `<script>` in `<head>` reads `localStorage` before the
stylesheet loads — this prevents a flash of the wrong theme on page load.
4. The theme preference is saved server-side via `POST /api/settings` and
loaded on boot via `GET /api/settings`.
5. The `/theme` command and Settings dropdown both update the DOM, localStorage,
and server settings simultaneously.
Right under Theme/Skin in **Settings → Appearance**: `Small`, `Default`,
`Large`. Applied as `data-font-size` on `<html>` and scales the WebUI's root
font size. Persists alongside theme and skin.
---
## Contributing a Theme
## How It Works Internally
To contribute a new built-in theme:
1. **Theme:** `document.documentElement.classList.toggle('dark', isDark)` —
light mode removes the class. System mode tracks
`matchMedia('(prefers-color-scheme: dark)')`.
2. **Skin:** `document.documentElement.dataset.skin = name` (or remove the
attribute for `default`).
3. **Font size:** `document.documentElement.dataset.fontSize = size` (or
remove for `default`).
4. **No flash on load:** a tiny inline `<script>` in `<head>` reads
`localStorage` before the stylesheet does, so the right look is applied
before paint.
5. **Server sync:** preferences are saved via `POST /api/settings` and
rehydrated on boot via `GET /api/settings`.
1. Add your `:root[data-theme="name"]` block to `static/style.css`
2. Add the `<option>` to the Settings panel in `static/index.html`
3. Add the theme name to the valid list in `cmdTheme()` in `static/commands.js`
4. Test on desktop and mobile
5. Open a PR — themes are pure CSS additions with no backend changes needed
---
## Contributing a Skin
Skins are the easiest extension point — pure CSS, no Python, no JS logic. To
contribute one upstream:
1. Add your `:root[data-skin="name"]` and `:root.dark[data-skin="name"]`
blocks to `static/style.css`.
2. Register it in the Settings skin picker in `static/index.html` and in the
skin list used by `cmdTheme()` in `static/commands.js`.
3. Test on desktop and mobile across both Light and Dark themes.
4. Open a PR — skins are pure CSS additions with no backend changes needed.
For a custom *theme* (overriding the base palette), prefer opening an issue
first to discuss scope, since it touches many selectors.