feat(ui): add theme-aware SVG favicon that follows light/dark mode

- Create light (orange-700) and dark (orange-600) coffee cup SVG favicons
- Add explicit routes for both SVG variants
- Set correct favicon on initial load before body renders
- Swap favicon href on manual theme toggle
- Listen for OS prefers-color-scheme changes when no manual override set
This commit is contained in:
Jon Seager 2026-02-06 16:44:00 +00:00
parent 44174b4361
commit 3e0c2ae771
No known key found for this signature in database
5 changed files with 46 additions and 1 deletions

View file

@ -33,6 +33,8 @@ pub(super) fn router() -> axum::Router<AppState> {
get(searchable_select_js),
)
.route("/favicon.ico", get(favicon))
.route("/favicon-light.svg", get(favicon_light))
.route("/favicon-dark.svg", get(favicon_dark))
}
async fn scan_redirect() -> Redirect {
@ -73,3 +75,17 @@ async fn favicon() -> impl IntoResponse {
include_bytes!("../../../../static/favicon.ico").as_ref(),
)
}
async fn favicon_light() -> impl IntoResponse {
(
[("content-type", "image/svg+xml")],
include_str!("../../../../static/favicon-light.svg"),
)
}
async fn favicon_dark() -> impl IntoResponse {
(
[("content-type", "image/svg+xml")],
include_str!("../../../../static/favicon-dark.svg"),
)
}

5
static/favicon-dark.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#ea580c" d="M4 12h18v13a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V12Z"/>
<path fill="none" stroke="#ea580c" stroke-linecap="round" stroke-width="2.5" d="M22 16c3.5 0 5.5 2.5 5.5 4.5S25.5 25 22 25"/>
<path fill="none" stroke="#ea580c" stroke-linecap="round" stroke-width="2" d="M10 9V4m6 5V4"/>
</svg>

After

Width:  |  Height:  |  Size: 391 B

5
static/favicon-light.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#c2410c" d="M4 12h18v13a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V12Z"/>
<path fill="none" stroke="#c2410c" stroke-linecap="round" stroke-width="2.5" d="M22 16c3.5 0 5.5 2.5 5.5 4.5S25.5 25 22 25"/>
<path fill="none" stroke="#c2410c" stroke-linecap="round" stroke-width="2" d="M10 9V4m6 5V4"/>
</svg>

After

Width:  |  Height:  |  Size: 391 B

View file

@ -5,11 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Brewlog{% endblock %}</title>
<link rel="stylesheet" href="/styles.css" />
<link rel="icon" id="favicon" type="image/svg+xml" href="/favicon-light.svg" />
<script>
(() => {
const stored = localStorage.getItem("theme");
if (stored === "dark" || (!stored && matchMedia("(prefers-color-scheme: dark)").matches)) {
const isDark = stored === "dark" || (!stored && matchMedia("(prefers-color-scheme: dark)").matches);
if (isDark) {
document.documentElement.setAttribute("data-theme", "dark");
document.getElementById("favicon").href = "/favicon-dark.svg";
}
})();
</script>

View file

@ -66,8 +66,24 @@
document.querySelectorAll(".theme-icon-dark").forEach((el) => {
el.classList.toggle("hidden", !isDark);
});
const favicon = document.getElementById("favicon");
if (favicon) {
favicon.href = isDark ? "/favicon-dark.svg" : "/favicon-light.svg";
}
};
// Set correct icon state on load
updateThemeIcons();
// Follow OS theme changes when the user hasn't manually chosen
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (localStorage.getItem("theme")) return;
const html = document.documentElement;
if (e.matches) {
html.setAttribute("data-theme", "dark");
} else {
html.removeAttribute("data-theme");
}
updateThemeIcons();
});
</script>