fix(web): make the favicon follow the color scheme
favicon.svg embeds @media (prefers-color-scheme: dark) so the DeepSeek mark turns white under a dark browser scheme; index.html and the install manifest gain a 32x32 PNG fallback (brand blue) for Safari, which cannot render SVG favicons. pwa-manifest.e2e.ts pins the PNG link, both manifest icons, and the dark media query.
This commit is contained in:
11 files changed
+98
-15
No files matched your search
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-web-favicon-dark-mode.md
|
||||
2026-08-10-web-favicon-dark-mode.md: 45a23ea31d369a7806733edac0c0aff32784a4c9
|
||||
2026-08-10-web-favicon-dark-mode.zh.md: 161e12e9d2749fb7dd0b65382be03cf38dd199d0
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Web favicon follows the color scheme
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-10-web-favicon-dark-mode.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`apps/web/public/favicon.svg` paints the DeepSeek mark solid black (`fill="#000"`), and `index.html` declares only that single SVG icon. Under an OS or browser dark color scheme the tab strip is dark too, so the black mark is effectively invisible. Safari additionally ignores SVG favicons entirely, so its users get no tab icon in any scheme.
|
||||
|
||||
## Decision
|
||||
|
||||
The favicon stays one file and adapts through the browser's own color-scheme signal: `favicon.svg` embeds `@media (prefers-color-scheme: dark) { path { fill: #fff } }`, switching the mark to white under a dark scheme while the light scheme keeps black. `index.html` and `manifest.webmanifest` also declare a 32×32 PNG fallback (`favicon-32x32.png`, DeepSeek brand blue `#4D6BFE`) that Safari renders and that stays visible on both light and dark tab strips, extending the [web-install-manifest decision](../feature/2026-08-06-web-install-manifest.md).
|
||||
|
||||
The theme signal is the OS/browser scheme, not the GUI's in-app `dsh.theme` toggle: the favicon lives in browser chrome, whose background follows the browser scheme, so `prefers-color-scheme` is the correct semantic and needs no JavaScript. Known browser quirks — Chromium may not repaint the tab icon until reload after a scheme switch, and Safari ignores the SVG variant — are accepted and the PNG fallback covers the Safari case.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **A second `<link rel="icon" media="(prefers-color-scheme: dark)">` pointing at a separate dark SVG.** Rejected: the same scheme semantics with two files to keep in sync, and no benefit over the in-file media query.
|
||||
- **A theme-presenter that swaps the icon href on `theme/change`.** Rejected: it would follow the in-app toggle rather than the browser scheme that actually colors the tab strip, and it adds client code and a presenter for a chrome asset.
|
||||
- **No PNG fallback.** Rejected: Safari never renders SVG favicons, so the fallback is the only way it gets a tab icon at all.
|
||||
|
||||
## Consequences
|
||||
|
||||
Light scheme still shows the black mark, dark scheme shows white, and Safari shows the blue PNG in both. `apps/web/tests/pwa-manifest.e2e.ts` now pins the PNG link, both manifest icons, and the dark media query inside the shipped SVG. The Chromium repaint quirk remains a browser behavior the app cannot fix.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 网页图标随配色方案切换
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-10-web-favicon-dark-mode.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`apps/web/public/favicon.svg` 把 DeepSeek 图标绘制为纯黑色(`fill="#000"`),而 `index.html` 只声明了这一个 SVG 图标。当操作系统或浏览器处于暗色配色方案时,标签栏同样是深色,黑色图标实际上不可见。此外 Safari 完全不渲染 SVG favicon,因此无论何种配色方案,Safari 用户都看不到标签页图标。
|
||||
|
||||
## 决策
|
||||
|
||||
favicon 保持单一文件,并通过浏览器自身的配色方案信号自适应:`favicon.svg` 内嵌 `@media (prefers-color-scheme: dark) { path { fill: #fff } }`,在暗色方案下把图标切换为白色,浅色方案保持黑色。`index.html` 与 `manifest.webmanifest` 同时声明 32×32 PNG 兜底(`favicon-32x32.png`,DeepSeek 品牌蓝 `#4D6BFE`),Safari 会渲染该 PNG,且它在浅色与深色标签栏上都清晰可见;这是对 [Web 安装 manifest 决策](../feature/2026-08-06-web-install-manifest.md) 的扩展。
|
||||
|
||||
主题信号取操作系统/浏览器方案,而不是 GUI 应用内 `dsh.theme` 开关:favicon 位于浏览器 chrome 中,其背景跟随浏览器方案,因此 `prefers-color-scheme` 是正确语义,无需任何 JavaScript。已知的浏览器怪癖——Chromium 在切换方案后可能要到刷新页面才重绘标签图标,Safari 忽略 SVG 变体——均被接受,Safari 场景由 PNG 兜底覆盖。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **新增指向独立暗色 SVG 的第二个 `<link rel="icon" media="(prefers-color-scheme: dark)">`。** 不予采纳:语义相同却要多维护一个文件,相比文件内媒体查询没有任何收益。
|
||||
- **由主题 presenter 在 `theme/change` 时替换图标 href。** 不予采纳:它会跟随应用内开关,而不是真正决定标签栏颜色的浏览器方案,并且为一个 chrome 资源引入客户端代码和 presenter。
|
||||
- **不提供 PNG 兜底。** 不予采纳:Safari 从不渲染 SVG favicon,兜底是它获得标签图标的唯一途径。
|
||||
|
||||
## 后果
|
||||
|
||||
浅色方案仍显示黑色图标,暗色方案显示白色,Safari 两种方案都显示蓝色 PNG。`apps/web/tests/pwa-manifest.e2e.ts` 现在固定断言 PNG 链接、manifest 中的两个图标以及交付 SVG 内部的暗色媒体查询。Chromium 的重绘怪癖仍是浏览器行为,应用无法修复。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-web-install-manifest.md
|
||||
2026-08-06-web-install-manifest.md: d400c6e586f4b735fa8e3dcc4899c97e45ac89c1
|
||||
2026-08-06-web-install-manifest.zh.md: d32ccf25c7b0e13a4094af4364a97385032462b7
|
||||
2026-08-06-web-install-manifest.md: 1b3b852fe0d9fbe5643f5ec5d84a8a120b730e34
|
||||
2026-08-06-web-install-manifest.zh.md: 9d16b6556888eaebb0e4d501774e2ee8343b25ed
|
||||
@@ -10,7 +10,7 @@ The Web build has a document title and favicon but no manifest from which a brow
|
||||
|
||||
## Decision
|
||||
|
||||
The Web entry links `/manifest.webmanifest`, which Vite copies from `apps/web/public/` into the production build. The manifest names the product `DeepSeek Harness`, gives installed chrome the compact name `DSH`, and fixes `id`, `start_url`, and `scope` at `/`. It requests `display: "fullscreen"` so supporting browsers can give the installed editor-like surface the available display area while leaving ordinary tabs unchanged; browsers may apply user overrides or fall back to another display mode. Its icon entry reuses `/favicon.svg` as an SVG of size `any` and purpose `any`.
|
||||
The Web entry links `/manifest.webmanifest`, which Vite copies from `apps/web/public/` into the production build. The manifest names the product `DeepSeek Harness`, gives installed chrome the compact name `DSH`, and fixes `id`, `start_url`, and `scope` at `/`. It requests `display: "fullscreen"` so supporting browsers can give the installed editor-like surface the available display area while leaving ordinary tabs unchanged; browsers may apply user overrides or fall back to another display mode. Its icon entries reuse `/favicon.svg` as an SVG of size `any` and purpose `any` and add a 32×32 PNG fallback (`/favicon-32x32.png`) for browsers that cannot render SVG favicons, per the [web-favicon-dark-mode decision](../bug-fix/2026-08-10-web-favicon-dark-mode.md).
|
||||
|
||||
This follows code-server's fullscreen choice without copying its `window-controls-overlay` display override. DSH has no custom title bar or layout around native window controls, so such an override would supersede fullscreen without owning the required safe layout.
|
||||
|
||||
@@ -30,10 +30,10 @@ The built-Web test parses the emitted manifest and pins the complete metadata ob
|
||||
|
||||
**Choose one static background and theme color.** Rejected because the app resolves light and dark palettes at runtime, so either fixed value is knowingly wrong for one supported state.
|
||||
|
||||
**Ship raster and maskable icon variants immediately.** Rejected until a supported installation target demonstrates a requirement the existing scalable favicon cannot meet. New variants remain an additive manifest change rather than a prerequisite for exposing the current identity.
|
||||
**Ship raster and maskable icon variants immediately.** The raster half shipped once Safari demonstrated a requirement the scalable favicon cannot meet: it cannot render SVG favicons, so the manifest now carries a 32×32 PNG alongside the SVG ([web-favicon-dark-mode decision](../bug-fix/2026-08-10-web-favicon-dark-mode.md)). Maskable variants remain rejected until a supported installation target demonstrates a requirement.
|
||||
|
||||
**Assert only root and display fields in the built artifact.** Rejected because dropping or changing the product name, compact name, or icon is also a shipped install regression. The test intentionally requires an explicit edit whenever any manifest metadata changes.
|
||||
|
||||
## Consequences
|
||||
|
||||
Supporting browsers can discover a stable root-scoped installed identity and fullscreen preference without the application promising offline behavior. Deploying this build below a path prefix requires revisiting the absolute link, identity, launch, scope, and icon URLs together. Browser-specific icon requirements may add variants later, and every intentional metadata change updates the exact built-artifact contract.
|
||||
Supporting browsers can discover a stable root-scoped installed identity and fullscreen preference without the application promising offline behavior. Deploying this build below a path prefix requires revisiting the absolute link, identity, launch, scope, and icon URLs together. Browser-specific icon requirements added a 32×32 PNG fallback once Safari could not render SVG favicons ([web-favicon-dark-mode decision](../bug-fix/2026-08-10-web-favicon-dark-mode.md)), and every intentional metadata change updates the exact built-artifact contract.
|
||||
@@ -10,7 +10,7 @@ Web 构建产物已有文档标题和 favicon,却没有可供浏览器发现
|
||||
|
||||
## 决策
|
||||
|
||||
Web 入口链接 `/manifest.webmanifest`,Vite 会将其从 `apps/web/public/` 复制到生产构建产物。manifest 将产品命名为 `DeepSeek Harness`,为安装后的浏览器界面提供简称 `DSH`,并把 `id`、`start_url` 和 `scope` 固定为 `/`。它请求 `display: "fullscreen"`,使支持这一模式的浏览器能够把可用显示区域交给安装后的编辑器式界面,同时不改变普通标签页;浏览器可以应用用户覆盖设置,或回退到其他显示模式。其图标条目复用 `/favicon.svg`,将它作为尺寸为 `any`、用途为 `any` 的 SVG。
|
||||
Web 入口链接 `/manifest.webmanifest`,Vite 会将其从 `apps/web/public/` 复制到生产构建产物。manifest 将产品命名为 `DeepSeek Harness`,为安装后的浏览器界面提供简称 `DSH`,并把 `id`、`start_url` 和 `scope` 固定为 `/`。它请求 `display: "fullscreen"`,使支持这一模式的浏览器能够把可用显示区域交给安装后的编辑器式界面,同时不改变普通标签页;浏览器可以应用用户覆盖设置,或回退到其他显示模式。其图标条目复用 `/favicon.svg`,将它作为尺寸为 `any`、用途为 `any` 的 SVG,并额外提供 32×32 的 PNG 兜底(`/favicon-32x32.png`),供无法渲染 SVG favicon 的浏览器使用,详见[网页图标随配色方案决策](../bug-fix/2026-08-10-web-favicon-dark-mode.md)。
|
||||
|
||||
这一选择沿用了 code-server 的全屏方案,但没有照搬其 `window-controls-overlay` 显示覆盖项。DSH 没有自定义标题栏,也没有围绕原生窗口控件安排布局,因此使用这类覆盖项会在未落实所需安全布局的情况下取代全屏模式。
|
||||
|
||||
@@ -30,10 +30,10 @@ Web 构建产物测试解析输出的 manifest,并固定完整的元数据对
|
||||
|
||||
**选择一组静态背景色和主题色。** 不予采纳,因为应用会在运行时解析浅色和深色调色板,因此选择任一固定值,都是明知它与其中一种受支持状态不符。
|
||||
|
||||
**立即交付光栅和可遮罩图标变体。** 在某个受支持的安装目标证明现有可缩放 favicon 无法满足其要求之前,不予采纳。新变体只是对 manifest 的增量扩展,并非公开当前身份的前提。
|
||||
**立即交付光栅和可遮罩图标变体。** 光栅部分已交付:Safari 证明了现有可缩放 favicon 无法满足其要求——它无法渲染 SVG favicon,因此 manifest 现在在 SVG 之外同时携带 32×32 PNG(见[网页图标随配色方案决策](../bug-fix/2026-08-10-web-favicon-dark-mode.md))。可遮罩变体仍保持不予采纳,直到有受支持的安装目标证明其必要性。
|
||||
|
||||
**只断言构建产物中的根路径字段和显示字段。** 不予采纳,因为产品名称、简称或图标被删除或更改,同样属于已交付安装体验的回归。任何 manifest 元数据发生变化时,测试都有意要求显式改动。
|
||||
|
||||
## 后果
|
||||
|
||||
支持这一机制的浏览器可以发现以根路径为作用域的稳定安装身份和全屏偏好,而应用无需承诺离线行为。在路径前缀下部署该构建产物时,必须同时重新审视绝对路径的 manifest 链接,以及身份、启动、作用域和图标 URL。日后可能因浏览器特有的图标要求而新增变体;每一项有意的元数据变更都会同步更新精确的构建产物约定。
|
||||
支持这一机制的浏览器可以发现以根路径为作用域的稳定安装身份和全屏偏好,而应用无需承诺离线行为。在路径前缀下部署该构建产物时,必须同时重新审视绝对路径的 manifest 链接,以及身份、启动、作用域和图标 URL。浏览器特有的图标要求已在 Safari 无法渲染 SVG favicon 时新增了 32×32 PNG 兜底(见[网页图标随配色方案决策](../bug-fix/2026-08-10-web-favicon-dark-mode.md));每一项有意的元数据变更都会同步更新精确的构建产物约定。
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>DeepSeek Harness</title>
|
||||
</head>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #fff; }
|
||||
}
|
||||
</style>
|
||||
<path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -6,6 +6,12 @@
|
||||
"scope": "/",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
|
||||
@@ -8,6 +8,7 @@ const DIST_ROOT = fileURLToPath(new URL('../dist', import.meta.url))
|
||||
it('ships install metadata with the built web application', async () => {
|
||||
const index = await readFile(join(DIST_ROOT, 'index.html'), 'utf8')
|
||||
expect(index).toContain('<link rel="manifest" href="/manifest.webmanifest" />')
|
||||
expect(index).toContain('<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />')
|
||||
|
||||
const manifest: unknown = JSON.parse(await readFile(join(DIST_ROOT, 'manifest.webmanifest'), 'utf8'))
|
||||
expect(manifest).toEqual({
|
||||
@@ -17,11 +18,25 @@ it('ships install metadata with the built web application', async () => {
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
display: 'fullscreen',
|
||||
icons: [{
|
||||
src: '/favicon.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any',
|
||||
}],
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon-32x32.png',
|
||||
sizes: '32x32',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/favicon.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('ships a favicon that switches to a light mark under dark color scheme', async () => {
|
||||
const favicon = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8')
|
||||
expect(favicon).toContain('@media (prefers-color-scheme: dark)')
|
||||
expect(favicon).toMatch(/path\s*{[^}]*fill:\s*#fff/i)
|
||||
})
|
||||
Reference in New Issue
Block a user