librelad 325f8f3be8 fix(webui): make Remove instance readable on the dark themes
The button drew its text from --status-danger, which is tuned as a FILL
colour — the confirm button in the remove modal sits white text on top of
it, and there it is correct. Reused as text on the app's own background
it measures 3.79:1 on nebula, 3.84 on dark-blue and 3.95 on the example
theme, all under the 4.5:1 AA floor, which is why the control read as
barely there. Light is the only theme where the same value passes, at
4.53:1, so brightening it globally would have fixed three themes by
breaking the fourth.

Two jobs, two values. --status-danger-text is now defined per theme:
#f87171 on the dark ones (6.29-6.47:1) and #c92a37 on light (5.43:1, up
from 4.53). #f87171 is not a new invention — it is the red the CVE
severity chips already use for exactly this job, so danger-as-text now
looks the same wherever it appears.

The border follows the text so the two stay coherent, and both fall back
to --status-danger, leaving any theme without the token no worse off
than before. The filled modal button is deliberately untouched: white on
#dc3545 was never the problem.

Other danger-as-text call sites exist (loading screen, network notifier)
and would likely benefit, but they render on their own backgrounds
rather than the app surface, so they are not assumed to share the
measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:31:11 +01:00
..

Custom Themes

Drop a folder here named after your theme and refresh the browser — the topbar dropdown will pick it up automatically.

Minimum

frontend/themes/<your-name>/
  theme.css       (required) — palette inside a [data-theme="<your-name>"] block
  meta.json       (optional) — { "displayName": "Pretty Name", "author": "you" }

The folder name (<your-name>) becomes the data-theme value. Use lowercase + dashes — no spaces, no slashes.

Tokens

Copy frontend/themes/example/theme.css as a starting point. Every CSS variable defined there is what style.css and friends consume — change the values, keep the names.

The visible-essentials list (override these at minimum):

Token What it controls
--surface-bg Body background (gradient or solid)
--text-primary Default text colour
--accent Theme's signature colour (manage btn)
--accent-rgb Same colour as r,g,b for rgba() mixes
--text-rgb 255,255,255 (dark) or 0,0,0 (light)
--bg-rgb The opposite of --text-rgb

Status colours (--status-success, --status-danger, --status-warning, --status-info) are usually safe to leave as the defaults — they're brand-stable across themes.

How discovery works

On page load the frontend calls GET /api/themes/list. The backend walks this directory, returns one entry per folder containing a theme.css, and the frontend (js/system/theme-registry.js) injects each entry's CSS into <head> and adds it to the topbar dropdown. The currently-saved theme is <link>-loaded synchronously by the inline bootstrap in index.html so first paint has the right palette — no flash.

The built-in themes (nebula, dark-blue, light) live in this folder too — their meta.json files set "builtin": true so a future UI can distinguish them (e.g. a "reset to built-ins" affordance). If you delete a built-in folder you'll remove it from the dropdown; the app survives but the deleted theme is gone until you put it back.

The dropdown orders themes by:

  1. nebula, dark-blue, light (built-ins, in that fixed order)
  2. Everything else, sorted by display name.