From 3e0c2ae77164d9fa086b0d0bb26015e04697e89d Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Fri, 6 Feb 2026 16:44:00 +0000 Subject: [PATCH] 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 --- src/application/routes/app/mod.rs | 16 ++++++++++++++++ static/favicon-dark.svg | 5 +++++ static/favicon-light.svg | 5 +++++ templates/base.html | 5 ++++- templates/partials/nav.html | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 static/favicon-dark.svg create mode 100644 static/favicon-light.svg diff --git a/src/application/routes/app/mod.rs b/src/application/routes/app/mod.rs index ff8bbcb..19d7e84 100644 --- a/src/application/routes/app/mod.rs +++ b/src/application/routes/app/mod.rs @@ -33,6 +33,8 @@ pub(super) fn router() -> axum::Router { 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"), + ) +} diff --git a/static/favicon-dark.svg b/static/favicon-dark.svg new file mode 100644 index 0000000..afb0e8e --- /dev/null +++ b/static/favicon-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/favicon-light.svg b/static/favicon-light.svg new file mode 100644 index 0000000..072bec9 --- /dev/null +++ b/static/favicon-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/base.html b/templates/base.html index 39366e4..5aa47b3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,11 +5,14 @@ {% block title %}Brewlog{% endblock %} + diff --git a/templates/partials/nav.html b/templates/partials/nav.html index e539013..da3179e 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -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(); + });