From d8db7856811f4341e5ef03133a93f2939eb2cefe Mon Sep 17 00:00:00 2001 From: rays Date: Mon, 28 Sep 2026 18:12:11 +0000 Subject: [PATCH] The tab's icon follows light and dark mode (#64) The logo on the page switched with the mode (#63), but the tab's icon was always favicon.png, the light logo. web/favicon-dark.png is logo-dark.svg at 128px, served beside it, and the theme script points the icon link at whichever matches data-mode, so it follows the theme the account chose, not only the system. The sign-in page, with no account, picks by the system's with two media-bound links. /favicon.ico, which a browser asks for on its own, stays the light one. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + src/web.rs | 9 +++++++++ tests/ui/app.spec.js | 7 +++++-- web/admin.html | 2 +- web/build.mjs | 2 +- web/favicon-dark.png | Bin 0 -> 3511 bytes web/index.html | 2 +- web/login.html | 3 ++- web/src/theme.ts | 2 ++ 9 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 web/favicon-dark.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 015fbe3..291107b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The Modern theme takes its colours from the new logo: its navy, the blue of its bars and the orange of its needle. - The logo follows the page: the dark version in a dark theme, the light one in a light theme. +- The browser tab's icon follows the page's light or dark mode too. - Between releases, the version on the logo says so: 0.9.1-dev, not 0.9.0. - The logo's needle is a brighter, redder orange that stands apart from the blue bars. - The logo is in the top bar beside the add-feed button, in place of the name at the top of the diff --git a/src/web.rs b/src/web.rs index 396edc8..0f10fc6 100644 --- a/src/web.rs +++ b/src/web.rs @@ -71,6 +71,7 @@ pub fn router(state: WebState) -> Router { .route("/logo-dark.svg", get(logo_dark)) .route("/favicon.ico", get(favicon)) .route("/favicon.png", get(favicon)) + .route("/favicon-dark.png", get(favicon_dark)) .route("/apple-touch-icon.png", get(touch_icon)) .route("/app.js", get(app_js)) .route("/app.css", get(app_css)) @@ -589,6 +590,14 @@ async fn favicon() -> impl IntoResponse { ) } +/// web/logo-dark.svg at 128px, the tab's icon while the page is dark. +async fn favicon_dark() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")], + include_bytes!("../web/favicon-dark.png").as_slice(), + ) +} + /// web/logo.svg at 180px, for an iPhone's home screen. It is opaque edge to edge, as iOS paints /// a transparent icon's background black and cuts its own corners. async fn touch_icon() -> impl IntoResponse { diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index d875439..a3d6691 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1193,9 +1193,12 @@ test('the logo is the dark one in dark mode and the light one in light mode', as }); test('the favicon is the logo, square, from both pages', async ({ page }) => { - await expect(page.locator('link[rel="icon"]')).toHaveAttribute('href', /^\/favicon\.png\?v=[0-9a-f]{12}$/); + await page.evaluate(() => setTheme('modern', 'light')); + await expect(page.locator('#favicon')).toHaveAttribute('href', /^\/favicon\.png\?v=[0-9a-f]{12}$/); + await page.evaluate(() => setTheme('modern', 'dark')); + await expect(page.locator('#favicon')).toHaveAttribute('href', /^\/favicon-dark\.png\?v=[0-9a-f]{12}$/); // A browser asks for /favicon.ico on its own, signed in or not. - for (const path of ['/favicon.ico', '/favicon.png', '/apple-touch-icon.png']) { + for (const path of ['/favicon.ico', '/favicon.png', '/favicon-dark.png', '/apple-touch-icon.png']) { const r = await page.request.get(path, { headers: { cookie: '' } }); expect(r.status(), path).toBe(200); expect(r.headers()['content-type'], path).toBe('image/png'); diff --git a/web/admin.html b/web/admin.html index 8f04894..25a3f89 100644 --- a/web/admin.html +++ b/web/admin.html @@ -5,7 +5,7 @@ iPX admin - + diff --git a/web/build.mjs b/web/build.mjs index 1c19cb1..fbc892c 100644 --- a/web/build.mjs +++ b/web/build.mjs @@ -75,7 +75,7 @@ export function buildPage(name, { minify = true } = {}) { // The icons are named by their contents too. They are kept a day under a fixed name, and a new // logo went unseen for that day, in browsers and at Cloudflare's edge. The server ignores the // query; /favicon.ico, which a browser asks for on its own, cannot carry one. - out = out.replace(/(href|src|srcset)="\/(favicon\.png|apple-touch-icon\.png|logo\.svg|logo-dark\.svg)"/g, + out = out.replace(/(href|src|srcset|data-light|data-dark)="\/(favicon\.png|favicon-dark\.png|apple-touch-icon\.png|logo\.svg|logo-dark\.svg)"/g, (_, attr, file) => `${attr}="/${file}?v=${hash(fs.readFileSync(path.join(here, file)))}"`); if (!minify) return { html: out, js, script }; const r = html.minifySync(out, { minifyJs: false, minifyCss: true, removeComments: true }); diff --git a/web/favicon-dark.png b/web/favicon-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5d15bd0101612fe824402f887c0cabed9663aa89 GIT binary patch literal 3511 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 iPX - + diff --git a/web/login.html b/web/login.html index f1ed182..24d2235 100644 --- a/web/login.html +++ b/web/login.html @@ -4,7 +4,8 @@ Sign in — iPX - + +