cc-haha/desktop/index.html
程序员阿江(Relakkes) 49f1974007 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.
2026-07-27 04:27:41 +08:00

202 lines
9.1 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<!--
Conservative CSP: hardens default/object/base-uri without breaking the app.
script/style keep 'unsafe-inline' + 'unsafe-eval' on purpose - Emotion CSS-in-JS
injects runtime <style> tags, the startup watchdog below is inline, and shiki/
mermaid/Vite rely on eval. connect-src stays permissive (localhost sidecar + ws +
https) so renderer fetches and dev HMR keep working. Tighten only after verifying
on a packaged build.
-->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: https: http://127.0.0.1:* http://localhost:*; connect-src 'self' https: http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
/>
<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') {
document.documentElement.dataset.windowKind = 'pet'
}
</script>
<script>
/*
Resolve the theme synchronously, before any stylesheet is parsed.
The app bundle loads as a module and only reaches initializeTheme()
after its dynamic imports resolve, which is long enough for a dark
mode user to get flashed by a white window on every launch.
Mirrors resolveAppliedTheme() in src/theme/systemAppearance.ts;
src/theme/bootstrapScript.test.ts checks the two agree.
*/
(function () {
try {
var read = function (key) {
try { return window.localStorage.getItem(key) } catch (_) { return null }
}
// The six palettes, split by ground. Keep in step with
// LIGHT_THEME_MODES / DARK_THEME_MODES in src/types/settings.ts.
var LIGHT = ['white', 'paper', 'warm-classic', 'celadon']
var DARK = ['dark', 'ink-blue']
var has = function (list, value) { return list.indexOf(value) !== -1 }
var isDark = function (value) { return has(DARK, value) }
var stored = read('cc-haha-theme')
var theme = (has(LIGHT, stored) || isDark(stored)) ? stored : 'white'
var followRaw = read('cc-haha-follow-system-theme')
// Unset means: new install follows the system, existing install
// (recognised by having a stored theme) keeps its fixed pick.
var follow = followRaw === '1' || (followRaw !== '0' && stored === null)
if (follow) {
var storedLight = read('cc-haha-light-theme')
var lightTheme = has(LIGHT, storedLight)
? storedLight
: (has(LIGHT, theme) ? theme : 'white')
var storedDark = read('cc-haha-dark-theme')
var darkTheme = isDark(storedDark) ? storedDark : (isDark(theme) ? theme : 'dark')
var prefersDark = false
try {
// Some WebViews reject the query outright. Treating that as
// "light" matches getSystemAppearance() in the app bundle.
prefersDark = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
} catch (_) { /* no color-scheme signal available */ }
theme = prefersDark ? darkTheme : lightTheme
}
document.documentElement.setAttribute('data-theme', theme)
document.documentElement.style.colorScheme = isDark(theme) ? 'dark' : 'light'
} catch (_) {
// `theme` is function-scoped, so a failure after it was resolved
// still applies the user's stored choice rather than flashing white.
document.documentElement.setAttribute('data-theme', theme || 'white')
document.documentElement.style.colorScheme =
(theme === 'dark' || theme === 'ink-blue') ? 'dark' : 'light'
}
})()
</script>
<style>
/* Paint the resolved theme's ground before globals.css arrives, so the
window never shows a palette it is about to replace. Values track
--cc-bg per theme in src/theme/globals.css.
The bare `html` rule only applies if the script above never ran (a CSP
that strips it would remove the element entirely, so its try/catch is
no help there) — it matches the default palette in globals.css rather
than a fixed white, so that degraded case stays self-consistent. */
html { background: #FFFFFF; }
html[data-theme="white"] { background: #FFFFFF; }
html[data-theme="paper"] { background: #FBF9F4; }
html[data-theme="warm-classic"] { background: #F6F0E1; }
html[data-theme="celadon"] { background: #F5F8F5; }
html[data-theme="dark"] { background: #201D17; }
html[data-theme="ink-blue"] { background: #1A1D24; }
html[data-window-kind="pet"],
html[data-window-kind="pet"] body,
html[data-window-kind="pet"] #root { background: transparent !important; }
html, body, #root { min-height: 100%; margin: 0; }
</style>
<script>
(function () {
var bootDeadlineMs = 8000
function summarize(value) {
if (!value) return 'Unknown startup error'
if (typeof value === 'string') return value
if (value.message) return value.message
try {
return JSON.stringify(value)
} catch (_) {
return String(value)
}
}
function renderStartupError(reason) {
if (window.__CC_HAHA_BOOTSTRAPPED__) return
var root = document.getElementById('root')
if (!root) return
var summary = summarize(reason)
var details = [
summary,
'',
'URL: ' + window.location.href,
'User Agent: ' + navigator.userAgent,
'Time: ' + new Date().toISOString()
].join('\n')
root.innerHTML = ''
var shell = document.createElement('main')
shell.style.cssText = [
'box-sizing:border-box',
'min-height:100vh',
'padding:32px',
'background:#fff',
'color:#151515',
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
'display:flex',
'align-items:center',
'justify-content:center'
].join(';')
var panel = document.createElement('section')
panel.style.cssText = 'width:min(760px,100%);border:1px solid #d8d8d8;border-radius:8px;padding:24px;background:#fff'
var title = document.createElement('h1')
title.textContent = 'Desktop startup failed'
title.style.cssText = 'margin:0 0 8px;font-size:20px;line-height:1.3;font-weight:700;color:#111'
var body = document.createElement('p')
body.textContent = 'The app could not finish booting. Keep this text when reporting the issue.'
body.style.cssText = 'margin:0 0 16px;font-size:14px;line-height:1.6;color:#555'
var pre = document.createElement('pre')
pre.textContent = details
pre.style.cssText = 'box-sizing:border-box;max-height:360px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;padding:14px;border-radius:6px;background:#f6f6f6;color:#222;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace'
panel.appendChild(title)
panel.appendChild(body)
panel.appendChild(pre)
shell.appendChild(panel)
root.appendChild(shell)
}
window.__CC_HAHA_SHOW_STARTUP_ERROR__ = renderStartupError
window.addEventListener('error', function (event) {
var target = event.target
if (target && target !== window) {
var tagName = target.tagName
if (tagName === 'SCRIPT' || tagName === 'LINK') {
renderStartupError('Startup resource failed to load: ' + (target.src || target.href || tagName))
}
return
}
renderStartupError(event.message || event.error || 'Startup script error')
}, true)
window.addEventListener('unhandledrejection', function (event) {
renderStartupError(event.reason || 'Unhandled startup promise rejection')
})
window.setTimeout(function () {
renderStartupError('Desktop app did not finish bootstrapping within ' + bootDeadlineMs + 'ms. This usually means the WebView could not execute the app bundle or startup code stopped before React mounted.')
}, bootDeadlineMs)
})()
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>