fix(dashboard): inline critical-CSS bootstrap for user themes (#36024 salvage) - #65695
Merged
Conversation
…gate flash
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
`<link rel="stylesheet">` carries `:root{--background-base:#041c1c}`,
the bundled `presets.ts` defines the same surfaces in JS — and then
`ThemeProvider.applyTheme(<user theme>)` 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 `<style id="hermes-theme-bootstrap">` block inside `<head>` with the
six critical CSS variables (`--background-base`, `--color-background`,
`--midground-base`, `--color-midground`, `--font-sans`,
`--font-base-size`) plus an `html, body` rule painting the body in the
target palette. Because the inline `<style>` follows the bundle's
`<link>` in DOM order and matches the same `:root` specificity, the
later declaration wins the cascade — the static canvas behind the SPA is
already the right colour before any JavaScript runs.
`_render_active_theme_bootstrap_css()` looks up the active theme through
the existing `_discover_user_themes()` helper. No-op for built-in
active themes (empty string returned, no `<style>` injected). No new
API endpoints, no config flags, no frontend changes.
After `ThemeProvider` mounts and `applyTheme()` writes the same
variables as inline styles on `documentElement`, the values match what
the bootstrap block set, so there is no second-paint discrepancy on the
critical CSS variables.
…le flows through vars Review fixes for the inline critical-CSS bootstrap (PR #36024): 1. Variable names now match what the bundle actually consumes. --color-background, --color-midground, --font-sans and --font-base-size appear nowhere in web/src; the real tokens are: --background-base / --midground-base (layerVars(), context.tsx) --theme-font-sans / --theme-base-size (typographyVars(), and index.css html{font-family:var(--theme-font-sans); font-size:var(--theme-base-size)}) 2. Stale-rule bug: the injected html,body rule previously baked in literal hex/font values. Because the <style> block sits after the bundle's <link> at equal specificity and is never removed, switching themes in the picker left the old canvas/font until reload. The rule now references the same CSS variables instead of literals — applyTheme() writes those vars as inline styles on documentElement, which outrank this block in the cascade, so runtime theme switches re-resolve the rule automatically. No frontend change needed.
…ction Server-side coverage for the critical-CSS shim (PR #36024 salvage): - user theme → style block emitted with ONLY real bundle variable names (--background-base/--midground-base from layerVars(), --theme-font-sans/--theme-base-size from typographyVars()/index.css), and an html,body rule expressed via those vars so runtime theme switches never leave a stale canvas/font - built-in / unknown / non-string active theme → no block - malformed theme YAML and load_config() exceptions → no crash, index still serves - </style> breakout attempt in a theme value stays escaped - mount_spa integration: block present in <head> for user themes, absent for built-ins
tonydwb
reviewed
Jul 16, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment
Overview
- Dashboard CSS fix: inline critical-CSS bootstrap for user themes
- 265 additions
Assessment
- CSS inlining for theme flash prevention is a reasonable approach
- No obvious security concerns
- Pure frontend/style change
Reviewed by Hermes Agent
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Dashboard user themes no longer flash the default teal on first paint:
_serve_index()injects a<style id="hermes-theme-bootstrap">critical-CSS block when the active theme is a user YAML theme.Salvage of #36024 by @nnnet with all three review fixes on top (authorship preserved):
web/src/themes/context.tsx+index.css): kept--background-base/--midground-base, replaced--font-sans→--theme-font-sansand--font-base-size→--theme-base-size, dropped the two nonexistent--color-*vars.html,bodyrule now referencesvar(--background-base)etc. instead of literal values —applyTheme()writes those vars inline ondocumentElement, so live theme switches re-resolve the rule with zero frontend changes and nothing to remove.Changes
hermes_cli/web_server.py(+81),tests/hermes_cli/test_web_server.py(+183),scripts/release.pyAUTHOR_MAP.Validation
scripts/run_tests.sh tests/hermes_cli/test_web_server.py→ 402 passed, 0 failed. Ruff clean. No web/src changes.Infographic