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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user