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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
- The Modern theme takes its colours from the new logo: its navy, the blue of its bars and the
|
||||||
orange of its needle.
|
orange of its needle.
|
||||||
- The logo follows the page: the dark version in a dark theme, the light one in a light theme.
|
- 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.
|
- 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'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
|
- The logo is in the top bar beside the add-feed button, in place of the name at the top of the
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ pub fn router(state: WebState) -> Router {
|
|||||||
.route("/logo-dark.svg", get(logo_dark))
|
.route("/logo-dark.svg", get(logo_dark))
|
||||||
.route("/favicon.ico", get(favicon))
|
.route("/favicon.ico", get(favicon))
|
||||||
.route("/favicon.png", get(favicon))
|
.route("/favicon.png", get(favicon))
|
||||||
|
.route("/favicon-dark.png", get(favicon_dark))
|
||||||
.route("/apple-touch-icon.png", get(touch_icon))
|
.route("/apple-touch-icon.png", get(touch_icon))
|
||||||
.route("/app.js", get(app_js))
|
.route("/app.js", get(app_js))
|
||||||
.route("/app.css", get(app_css))
|
.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
|
/// 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.
|
/// a transparent icon's background black and cuts its own corners.
|
||||||
async fn touch_icon() -> impl IntoResponse {
|
async fn touch_icon() -> impl IntoResponse {
|
||||||
|
|||||||
@@ -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 }) => {
|
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.
|
// 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: '' } });
|
const r = await page.request.get(path, { headers: { cookie: '' } });
|
||||||
expect(r.status(), path).toBe(200);
|
expect(r.status(), path).toBe(200);
|
||||||
expect(r.headers()['content-type'], path).toBe('image/png');
|
expect(r.headers()['content-type'], path).toBe('image/png');
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>iPX admin</title>
|
<title>iPX admin</title>
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png">
|
<link rel="icon" type="image/png" sizes="128x128" id="favicon" href="/favicon.png" data-light="/favicon.png" data-dark="/favicon-dark.png">
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<link rel="stylesheet" data-src="app.css">
|
<link rel="stylesheet" data-src="app.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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.
|
// 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)))}"`);
|
(_, attr, file) => `${attr}="/${file}?v=${hash(fs.readFileSync(path.join(here, file)))}"`);
|
||||||
if (!minify) return { html: out, js, script };
|
if (!minify) return { html: out, js, script };
|
||||||
const r = html.minifySync(out, { minifyJs: false, minifyCss: true, removeComments: true });
|
const r = html.minifySync(out, { minifyJs: false, minifyCss: true, removeComments: true });
|
||||||
|
|||||||
BIN
web/favicon-dark.png
Normal file
BIN
web/favicon-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>iPX</title>
|
<title>iPX</title>
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png">
|
<link rel="icon" type="image/png" sizes="128x128" id="favicon" href="/favicon.png" data-light="/favicon.png" data-dark="/favicon-dark.png">
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<link rel="stylesheet" data-src="app.css">
|
<link rel="stylesheet" data-src="app.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>Sign in — iPX</title>
|
<title>Sign in — iPX</title>
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png">
|
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png" media="(prefers-color-scheme:light)">
|
||||||
|
<link rel="icon" type="image/png" sizes="128x128" href="/favicon-dark.png" media="(prefers-color-scheme:dark)">
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<style>
|
<style>
|
||||||
/* Inter, from ipx itself; see the same rule in index.html. */
|
/* Inter, from ipx itself; see the same rule in index.html. */
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ function setTheme(name = theme.name, mode = theme.mode, save = false){
|
|||||||
// A theme with one palette has it whatever the mode; both of those are light.
|
// A theme with one palette has it whatever the mode; both of those are light.
|
||||||
root.dataset.mode = !both ? 'light'
|
root.dataset.mode = !both ? 'light'
|
||||||
: theme.mode === 'auto' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme.mode;
|
: theme.mode === 'auto' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme.mode;
|
||||||
|
// The tab's icon in the same variant as the logo on the page.
|
||||||
|
const fav = $('#favicon'); if(fav) fav.href = fav.dataset[root.dataset.mode];
|
||||||
const sel = $('#stheme'); if(sel) sel.value = theme.name;
|
const sel = $('#stheme'); if(sel) sel.value = theme.name;
|
||||||
const ms = $('#smode'); if(ms) ms.value = theme.mode;
|
const ms = $('#smode'); if(ms) ms.value = theme.mode;
|
||||||
const mf = $('#smodefield'); if(mf) mf.hidden = !both;
|
const mf = $('#smodefield'); if(mf) mf.hidden = !both;
|
||||||
|
|||||||
Reference in New Issue
Block a user