fix(brand): close three icon gaps the first pass missed

Two reviews of the icon swap, run independently from opposite directions,
turned up three places the new mark never reached.

The og:image was the worst. `site/index.html:20` points at
`/images/banner.png`, and that file does not come from `docs/images/banner.png`
— the one this rebrand replaced. `site/scripts/prepare-static-output.mjs:129`
copies `docs/public/` wholesale into the site root before the two selective
image passes run, and neither of those matches an absolute https:// URL, so
`docs/public/images/banner.png` is what shipped. It was a 1200x630 screenshot
of the old site: blue panels, old circular CC badge. Every link shared to
WeChat or Slack would have previewed the pre-rebrand brand while the site
itself rendered the new one. Replaced with a card built from the new lockup;
`site/dist/images/banner.png` now hashes to it.

`desktop/src-tauri/app-icon.png` is the canonical 1024 RGBA source the platform
icon set gets regenerated from. That rule lived only in the body of 3f2ce2a6c,
and nothing references the file in code, so the rebrand skipped it — breaking
an invariant that had held since the file was introduced, where it and
`desktop/public/app-icon.png` were the same git blob. Left as it was, whoever
regenerated icons next would have restored the entire old set.

Linux had no icon above 310px. electron-builder points `linux.icon` at the
whole icons directory and keeps only files named NxN, so `icon.png` (512, no
dimensions in the name) and `128x128@2x.png` (256, collides with
`128x128.png`) were both dropped, leaving a Windows Store asset as the largest
entry. Adding 256x256.png and 512x512.png takes the resolved set from 12
entries topping out at 310 to 14 topping out at 512, confirmed by running
app-builder's icon resolver against the directory.

index.html also had no favicon, and that document is what the H5 remote client
loads in a phone browser.

CI could not have caught any of this — `scripts/quality-gate/package-smoke/`
asserts nothing about icons. `desktop/icon-assets.test.ts` now pins the source
invariant, the Linux sizes, the three packaged icons and the favicon; each
assertion was checked to fail when its subject is reverted.
This commit is contained in:
程序员阿江(Relakkes) 2026-07-27 04:24:22 +08:00
parent 19357fcd87
commit 49f1974007
7 changed files with 79 additions and 0 deletions

View File

@ -0,0 +1,57 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* Guards on the icon assets, because nothing else does.
*
* `scripts/quality-gate/package-smoke/` makes no icon assertions, so a build
* that ships a stale or half-replaced icon set goes green on every pipeline.
* The rules below were each learned by breaking them.
*/
const desktopRoot = __dirname
const icons = path.join(desktopRoot, 'src-tauri', 'icons')
const sha = (file: string) => createHash('sha256').update(readFileSync(file)).digest('hex')
describe('icon assets', () => {
it('keeps src-tauri/app-icon.png byte-identical to public/app-icon.png', () => {
// src-tauri/app-icon.png is the canonical 1024 RGBA source the platform
// icon set gets regenerated from — a directive that lived only in a commit
// body, which is exactly why a later rebrand replaced everything except
// this file and left a stale source primed to overwrite the new set.
const source = path.join(desktopRoot, 'src-tauri', 'app-icon.png')
const runtime = path.join(desktopRoot, 'public', 'app-icon.png')
expect(sha(source)).toBe(sha(runtime))
})
it('ships the sizes Linux actually looks for', () => {
// electron-builder points `linux.icon` at the whole icons/ directory and
// keeps only files named NxN. `icon.png` (512) has no dimensions in its
// name and `128x128@2x.png` (256) collides with `128x128.png`, so both are
// dropped — which once left the largest Linux icon at 310px, from a
// Windows Store asset, and blurry on HiDPI.
const present = new Set(readdirSync(icons))
for (const size of [32, 64, 128, 256, 512]) {
expect(present.has(`${size}x${size}.png`)).toBe(true)
}
})
it('carries a packaged icon for every desktop platform', () => {
// mac.icon / win.icon in package.json name these two directly; a missing
// file makes electron-builder silently fall back to the Electron logo.
for (const file of ['icon.icns', 'icon.ico', 'icon.png']) {
const full = path.join(icons, file)
expect(existsSync(full)).toBe(true)
expect(readFileSync(full).byteLength).toBeGreaterThan(1024)
}
})
it('serves a favicon to the H5 client', () => {
// The same index.html the Electron window loads is what a phone browser
// gets over H5 remote access, where a missing icon means a blank tab.
const html = readFileSync(path.join(desktopRoot, 'index.html'), 'utf8')
expect(html).toMatch(/<link\s+rel="icon"/)
})
})

View File

@ -16,6 +16,12 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Code Haha</title>
<!--
The Electron window draws its own chrome, but this same document is what
the H5 remote client loads in a phone browser, where a missing favicon
means a blank tab and a screenshot for "add to home screen".
-->
<link rel="icon" href="/app-icon.png" type="image/png" />
<!-- Fonts are self-hosted in /fonts/ and declared via @font-face in globals.css -->
<script>
if (new URLSearchParams(window.location.search).get('petWindow') === '1') {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,16 @@
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="630" fill="#F6F0E1"/>
<rect x="0" y="0" width="1200" height="630" fill="none" stroke="#E5DBC3" stroke-width="16"/>
<g transform="translate(370.47,-83.35) scale(0.44917)"><path d="M437.75 695.01A131 131 0 1 1 437.75 436.99" fill="none" stroke="#251E11" stroke-width="60" stroke-linecap="round"/>
<path d="M505.06 535.98A117 117 0 0 1 610.92 459.07" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
<path d="M635.32 691.22A117 117 0 0 1 515.78 638.00" fill="none" stroke="#251E11" stroke-width="58" stroke-linecap="round"/>
<path d="M441.5 576H517.5" fill="none" stroke="#96442B" stroke-width="35" stroke-linecap="round"/>
<path d="M683.79 384.80Q698.00 380.00 692.43 393.93L665.57 461.07Q660.00 475.00 655.71 460.63L645.00 424.71Q643.00 418.00 636.59 415.18L629.25 411.95Q618.00 407.00 631.26 402.52L683.79 384.80Z" fill="#96442B"/>
<path d="M717.00 308.00Q724.07 325.43 741.50 332.50Q724.07 339.57 717.00 357.00Q709.93 339.57 692.50 332.50Q709.93 325.43 717.00 308.00Z" fill="#96442B"/>
<path d="M755.50 365.00Q759.04 374.96 769.00 378.50Q759.04 382.04 755.50 392.00Q751.96 382.04 742.00 378.50Q751.96 374.96 755.50 365.00Z" fill="#96442B"/></g>
<text x="600.0" y="330" text-anchor="middle" fill="#251E11" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="76" font-weight="800" letter-spacing="-1">Claude Code <tspan fill="#96442B">Haha</tspan></text>
<path d="M410.0 380H790.0" stroke="#96442B" stroke-width="5" stroke-linecap="round"/>
<text x="600.0" y="452" text-anchor="middle" fill="#251E11" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="38" font-weight="600">桌面端 Claude Code 工作台</text>
<text x="600.0" y="516" text-anchor="middle" fill="#6E6146" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="26" font-weight="400">会话 · 多项目 · Worktree · 代码 Diff · 权限审批 · H5 远程访问</text>
<text x="600.0" y="576" text-anchor="middle" fill="#9A8B6C" font-family="SF Pro Display, -apple-system, BlinkMacSystemFont, Avenir Next, Inter, Segoe UI, Helvetica Neue, Arial, sans-serif" font-size="23" font-weight="500" letter-spacing="1">github.com/NanmiCoder/cc-haha</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB