mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
On a phone the H5 client rendered under a gray band where the status bar sits, instead of running its own background up to the top edge the way other mobile sites do. iOS WebKit reads the viewport meta once, while parsing the document. touchH5.ts rewrote it at runtime to pin the scale and carried viewport-fit=cover along, but that rewrite cannot turn safe areas on retroactively — so every env(safe-area-inset-*) in globals.css resolved to 0px and the browser painted its own chrome color behind the status bar. Declaring cover in index.html is what actually takes effect. Add a theme-color meta on the same pass. With the page now running under the status bar, the browser chrome needs to match the palette rather than guess at it; the pre-hydration script sets it before first paint and applyTheme() keeps it in step, including for a palette another window picked.
233 lines
11 KiB
HTML
233 lines
11 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'"
|
|
/>
|
|
<!--
|
|
`viewport-fit=cover` MUST live here, in the markup. iOS WebKit reads the
|
|
viewport meta once while parsing the document; rewriting it later from JS
|
|
(touchH5.ts does, to pin the scale) never re-runs that decision. Without
|
|
cover the page is laid out inside the safe area, every
|
|
env(safe-area-inset-*) resolves to 0px, and the browser paints its own
|
|
gray bar behind the status bar instead of the app's own background.
|
|
-->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
<!--
|
|
Colors the iOS status bar / Android address bar. Kept in step with the
|
|
resolved palette by the theme script below and by applyTheme() at runtime,
|
|
so a dark palette does not sit under light browser chrome. Safari only
|
|
guesses from the page otherwise, and guesses gray here.
|
|
-->
|
|
<meta name="theme-color" content="#FFFFFF" />
|
|
<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) }
|
|
// Mirrors THEME_BACKGROUNDS in src/theme/systemAppearance.ts; the
|
|
// <style> block below and bootstrapScript.test.ts hold all three
|
|
// copies to the same values.
|
|
var BACKGROUND = {
|
|
white: '#FFFFFF',
|
|
paper: '#FBF9F4',
|
|
'warm-classic': '#F6F0E1',
|
|
celadon: '#F5F8F5',
|
|
dark: '#201D17',
|
|
'ink-blue': '#1A1D24'
|
|
}
|
|
var paintBrowserChrome = function (value) {
|
|
var meta = document.querySelector('meta[name="theme-color"]')
|
|
if (meta && BACKGROUND[value]) meta.setAttribute('content', BACKGROUND[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'
|
|
paintBrowserChrome(theme)
|
|
} 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>
|