From 72562be961bb369ca3de9d93058d088a6ff0f760 Mon Sep 17 00:00:00 2001 From: nnnet <21066097+nnnet@users.noreply.github.com> Date: Sun, 31 May 2026 21:36:46 +0300 Subject: [PATCH] fix(dashboard): inline critical-CSS bootstrap for user themes to mitigate flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User themes (`~/.hermes/dashboard-themes/*.yaml`) reach the SPA only after `/api/dashboard/themes` resolves at React mount. The bundle paints the first frame with the default Hermes Teal canvas — the `` carries `:root{--background-base:#041c1c}`, the bundled `presets.ts` defines the same surfaces in JS — and then `ThemeProvider.applyTheme()` flips the inline CSS variables on `documentElement` once the API response lands. Visible to the user as a green canvas behind the loading SPA on every reload when the active theme is non-default. Built-in themes do not suffer the same effect because their full definitions ship inside the bundle, so the SPA already has the palette before first paint. This patch closes the gap on the backend side: `_serve_index()` injects a ``` escape — current values are well-known + # hex/font strings, but this keeps the helper safe if it is + # later extended to ship user-authored CSS literals. + def _esc(s: str) -> str: + return str(s).replace("' + ":root{" + f"--background-base:{_esc(bg_hex)};" + f"--color-background:{_esc(bg_hex)};" + f"--midground-base:{_esc(mg_hex)};" + f"--color-midground:{_esc(mg_hex)};" + f"--font-sans:{_esc(font_sans)};" + f"--font-base-size:{_esc(base_size)};" + "}" + f"html,body{{background-color:{_esc(bg_hex)};color:{_esc(mg_hex)};" + f"font-family:{_esc(font_sans)};font-size:{_esc(base_size)};}}" + "" + ) + return "" + except Exception: + _log.debug("theme bootstrap render failed", exc_info=True) + return "" + + def mount_spa(application: FastAPI): """Mount the built SPA. Falls back to index.html for client-side routing. @@ -16378,6 +16436,16 @@ def mount_spa(application: FastAPI): html = html.replace('href="/fonts/', f'href="{prefix}/fonts/') html = html.replace('href="/ds-assets/', f'href="{prefix}/ds-assets/') html = html.replace('src="/ds-assets/', f'src="{prefix}/ds-assets/') + # Theme flash mitigation: when the active theme is a user theme + # (``HERMES_HOME/dashboard-themes/.yaml``), inject a minimal + # critical-CSS block so the first paint uses the target palette. + # Without this the SPA paints the default Hermes Teal canvas, then + # ``ThemeProvider`` flips the CSS variables once + # ``/api/dashboard/themes`` resolves. Built-in themes are already + # in the bundle's ``presets.ts`` so no shim is needed for them. + theme_bootstrap = _render_active_theme_bootstrap_css() + if theme_bootstrap: + html = html.replace("", f"{theme_bootstrap}", 1) html = html.replace("", f"{bootstrap_script}", 1) return HTMLResponse( html,