mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-01 16:43:37 +08:00
233 lines
11 KiB
HTML
233 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<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>
|