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:
@@ -9221,6 +9221,52 @@ async def set_dashboard_theme(body: ThemeSetBody):
|
||||
return {"ok": True, "theme": body.name}
|
||||
|
||||
|
||||
# Curated font-override ids. Kept in sync with FONT_CHOICES in
|
||||
# web/src/themes/fonts.ts — the frontend owns the stacks + webfont URLs;
|
||||
# the backend only needs the id allow-list so it can reject anything not
|
||||
# in the vetted catalog (the font's webfont URL is injected as a <link>,
|
||||
# so we never accept an arbitrary user-supplied id/URL here).
|
||||
_FONT_DEFAULT_ID = "theme"
|
||||
_FONT_CHOICES = frozenset({
|
||||
"system-sans", "system-serif", "system-mono",
|
||||
"inter", "ibm-plex-sans", "work-sans", "atkinson-hyperlegible", "dm-sans",
|
||||
"spectral", "fraunces", "source-serif",
|
||||
"jetbrains-mono", "ibm-plex-mono", "space-mono",
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/dashboard/font")
|
||||
async def get_dashboard_font():
|
||||
"""Return the active font override (``"theme"`` = use the theme's font)."""
|
||||
config = load_config()
|
||||
font = cfg_get(config, "dashboard", "font", default=_FONT_DEFAULT_ID)
|
||||
if font not in _FONT_CHOICES:
|
||||
font = _FONT_DEFAULT_ID
|
||||
return {"font": font}
|
||||
|
||||
|
||||
class FontSetBody(BaseModel):
|
||||
font: str
|
||||
|
||||
|
||||
@app.put("/api/dashboard/font")
|
||||
async def set_dashboard_font(body: FontSetBody):
|
||||
"""Set the dashboard font override (persists to config.yaml).
|
||||
|
||||
Accepts any id in the curated catalog, or ``"theme"`` to clear the
|
||||
override and fall back to the active theme's own font. Unknown ids are
|
||||
coerced to ``"theme"`` rather than 400'd so a stale client can't wedge
|
||||
the picker.
|
||||
"""
|
||||
font = body.font if body.font in _FONT_CHOICES else _FONT_DEFAULT_ID
|
||||
config = load_config()
|
||||
if "dashboard" not in config:
|
||||
config["dashboard"] = {}
|
||||
config["dashboard"]["font"] = font
|
||||
save_config(config)
|
||||
return {"ok": True, "font": font}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dashboard plugin system
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user