feat(dashboard): change UI font from the theme picker, independent of theme (#41145)

The dashboard font is now selectable from the UI, not just YAML. A new Font
section in the header theme picker overrides the UI font of whatever theme is
active; the choice is orthogonal to the theme and survives theme switches.
Each theme keeps its own font as the default — picking "Theme default" clears
the override.

- web/src/themes/fonts.ts: curated font catalog (system + Google Fonts across
  sans/serif/mono), each with a family stack and optional webfont URL. The
  catalog is the only injected-font surface — no free-text URL box, so the
  injected <link> origins stay fixed.
- web/src/themes/context.tsx: font-override state (localStorage + server),
  applied after theme typography so it wins; theme apply re-asserts it, and
  clearing re-runs theme apply to restore the theme's own font. Mono is left
  to the theme so code/terminal are untouched.
- web/src/components/ThemeSwitcher.tsx: Font section with grouped, self-
  previewing font rows and a "Theme default" clear option.
- hermes_cli/web_server.py: GET/PUT /api/dashboard/font persisting to
  config.yaml dashboard.font, with a server-side id allow-list (unknown ids
  coerce to the theme sentinel).
- i18n + types, api client methods, tests, and docs.

Validation: 6 new backend endpoint tests pass; tsc + vite build clean; live
browser test confirmed pick/persist/survive-theme-switch/clear all work.
This commit is contained in:
Teknium
2026-06-07 03:39:01 -07:00
committed by GitHub
parent 136dae779e
commit 9e63109522
11 changed files with 551 additions and 9 deletions
+63
View File
@@ -243,6 +243,69 @@ class TestWebServerEndpoints:
assert "hermes_home" in data
assert "active_sessions" in data
# ── Dashboard font override ─────────────────────────────────────────
def test_get_dashboard_font_defaults_to_theme(self):
"""With no override persisted, the active font is the theme sentinel."""
resp = self.client.get("/api/dashboard/font")
assert resp.status_code == 200
assert resp.json() == {"font": "theme"}
def test_set_dashboard_font_persists_valid_choice(self):
"""A valid catalog id is accepted, persisted, and read back."""
from hermes_cli.config import load_config
resp = self.client.put("/api/dashboard/font", json={"font": "inter"})
assert resp.status_code == 200
assert resp.json() == {"ok": True, "font": "inter"}
# Persisted to config.yaml under dashboard.font.
config = load_config()
assert config["dashboard"]["font"] == "inter"
# And reflected by the GET endpoint.
assert self.client.get("/api/dashboard/font").json() == {"font": "inter"}
def test_set_dashboard_font_clears_with_theme_sentinel(self):
"""Setting 'theme' clears any prior override."""
self.client.put("/api/dashboard/font", json={"font": "fraunces"})
resp = self.client.put("/api/dashboard/font", json={"font": "theme"})
assert resp.status_code == 200
assert resp.json() == {"ok": True, "font": "theme"}
assert self.client.get("/api/dashboard/font").json() == {"font": "theme"}
def test_set_dashboard_font_rejects_unknown_id(self):
"""An id not in the curated catalog coerces to the theme sentinel,
so a stale/hostile client can't inject an arbitrary font id."""
resp = self.client.put(
"/api/dashboard/font", json={"font": "../../etc/passwd"}
)
assert resp.status_code == 200
assert resp.json() == {"ok": True, "font": "theme"}
def test_get_dashboard_font_coerces_stale_persisted_value(self):
"""A config value no longer in the catalog reads back as 'theme'."""
from hermes_cli.config import load_config, save_config
config = load_config()
config.setdefault("dashboard", {})["font"] = "retired-font-id"
save_config(config)
assert self.client.get("/api/dashboard/font").json() == {"font": "theme"}
def test_dashboard_font_override_independent_of_theme(self):
"""The font override and the theme are stored separately — setting
one must not disturb the other."""
from hermes_cli.config import load_config
self.client.put("/api/dashboard/theme", json={"name": "ember"})
self.client.put("/api/dashboard/font", json={"font": "jetbrains-mono"})
config = load_config()
assert config["dashboard"]["theme"] == "ember"
assert config["dashboard"]["font"] == "jetbrains-mono"
def test_get_sessions_uses_only_persisted_cwd(self, monkeypatch):
"""Session rows without persisted cwd must not inherit TERMINAL_CWD.