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:
1 parent
b766b7f759
commit
0f8ba4d8d3
1 file changed
+136
-115
@@ -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.
|
||||
Reference in new issue
Block a user