fix: prevent theme flash when navigating instance pages from hub iframe

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 13:23:51 +08:00
parent e30d24173f
commit 995ee8d2e1
9 changed files with 131 additions and 22 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
<html lang="zh-CN" data-theme="dark"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script> <script src="/static/instance_theme.js?v=5"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -1
View File
@@ -2,7 +2,8 @@
<html lang="zh-CN" data-theme="dark"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script> <script src="/static/instance_theme.js?v=5"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -1
View File
@@ -2,7 +2,8 @@
<html lang="zh-CN" data-theme="dark"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script> <script src="/static/instance_theme.js?v=5"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -1
View File
@@ -2,7 +2,8 @@
<html lang="zh-CN" data-theme="dark"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script> <script src="/static/instance_theme.js?v=5"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+4
View File
@@ -50,7 +50,11 @@ def install_instance_theme_static(app) -> None:
repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
assets = { assets = {
"instance_theme.js": "application/javascript; charset=utf-8", "instance_theme.js": "application/javascript; charset=utf-8",
"instance_theme_early.css": "text/css; charset=utf-8",
"instance_theme.css": "text/css; charset=utf-8", "instance_theme.css": "text/css; charset=utf-8",
"instance_ui.js": "application/javascript; charset=utf-8",
"ai_review_render.js": "application/javascript; charset=utf-8",
"form_submit_guard.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8", "focus_chart_page.js": "application/javascript; charset=utf-8",
"focus_chart_page.css": "text/css; charset=utf-8", "focus_chart_page.css": "text/css; charset=utf-8",
} }
+3
View File
@@ -138,6 +138,8 @@
shell.classList.remove("hidden"); shell.classList.remove("hidden");
shell.setAttribute("aria-hidden", "false"); shell.setAttribute("aria-hidden", "false");
document.body.classList.add("hub-instance-frame-open"); document.body.classList.add("hub-instance-frame-open");
if (frame.dataset.themeSyncBound !== "1") {
frame.dataset.themeSyncBound = "1";
frame.addEventListener("load", function syncInstanceFrameTheme() { frame.addEventListener("load", function syncInstanceFrameTheme() {
try { try {
if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) { if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
@@ -149,6 +151,7 @@
} catch (_) {} } catch (_) {}
}); });
} }
}
function closeInstanceFrame() { function closeInstanceFrame() {
const shell = document.getElementById("instance-frame-shell"); const shell = document.getElementById("instance-frame-shell");
+1 -1
View File
@@ -250,6 +250,6 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script> <script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
<script src="/assets/app.js?v=20260604-risk-full-margin"></script> <script src="/assets/app.js?v=20260604-iframe-theme-flash"></script>
</body> </body>
</html> </html>
+76 -6
View File
@@ -3,6 +3,7 @@
*/ */
(function (global) { (function (global) {
const STANDALONE_KEY = "instance-theme"; const STANDALONE_KEY = "instance-theme";
const HUB_LINKED_THEME_KEY = "hub-linked-theme";
const META = { dark: "#0b0d14", light: "#d8e2ec" }; const META = { dark: "#0b0d14", light: "#d8e2ec" };
function normalize(theme) { function normalize(theme) {
@@ -32,6 +33,21 @@
return null; return null;
} }
function readLinkedThemeStorage() {
try {
const t = sessionStorage.getItem(HUB_LINKED_THEME_KEY);
if (t === "light" || t === "dark") return t;
} catch (_) {}
return null;
}
function writeLinkedThemeStorage(theme) {
if (!isHubLinked()) return;
try {
sessionStorage.setItem(HUB_LINKED_THEME_KEY, normalize(theme));
} catch (_) {}
}
function getStandalone() { function getStandalone() {
try { try {
return normalize(localStorage.getItem(STANDALONE_KEY)); return normalize(localStorage.getItem(STANDALONE_KEY));
@@ -46,15 +62,15 @@
} catch (_) {} } catch (_) {}
} }
let _linkedTheme = null;
function get() { function get() {
if (isHubLinked()) { if (isHubLinked()) {
return themeFromUrl() || _linkedTheme || "dark"; return themeFromUrl() || _linkedTheme || readLinkedThemeStorage() || "dark";
} }
return getStandalone(); return getStandalone();
} }
let _linkedTheme = null;
/** 模板内联暗色 → 亮色(切换时重写 style 属性) */ /** 模板内联暗色 → 亮色(切换时重写 style 属性) */
const INLINE_HEX_LIGHT = { const INLINE_HEX_LIGHT = {
"#cfd3ef": "#1a2838", "#cfd3ef": "#1a2838",
@@ -141,12 +157,43 @@
}); });
} }
function mergeHubQueryIntoHref(href, theme) {
if (!href || href.startsWith("#") || href.startsWith("javascript:")) return href;
try {
const u = new URL(href, location.origin);
if (u.origin !== location.origin) return href;
if (isHubLinked()) {
u.searchParams.set("embed", "1");
if (theme === "light" || theme === "dark") {
u.searchParams.set("hub_theme", theme);
}
}
return u.pathname + u.search + u.hash;
} catch (_) {
return href;
}
}
function patchHubNavLinks(theme) {
if (!isHubLinked()) return;
const t = normalize(theme || get());
document
.querySelectorAll(".top-nav a[href], .strategy-subnav a[href]")
.forEach((a) => {
const href = a.getAttribute("href");
if (!href) return;
const next = mergeHubQueryIntoHref(href, t);
if (next !== href) a.setAttribute("href", next);
});
}
function apply(theme, opts) { function apply(theme, opts) {
const options = opts || {}; const options = opts || {};
const linked = isHubLinked(); const linked = isHubLinked();
const t = normalize(theme); const t = normalize(theme);
if (linked) { if (linked) {
_linkedTheme = t; _linkedTheme = t;
writeLinkedThemeStorage(t);
} else if (!options.skipStore) { } else if (!options.skipStore) {
setStandalone(t); setStandalone(t);
} }
@@ -155,7 +202,19 @@
const meta = document.querySelector('meta[name="theme-color"]'); const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", META[t]); if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t; root.style.colorScheme = t;
if (document.body) {
syncInlineStyles(t); syncInlineStyles(t);
patchHubNavLinks(t);
} else {
document.addEventListener(
"DOMContentLoaded",
function onDom() {
syncInlineStyles(t);
patchHubNavLinks(t);
},
{ once: true }
);
}
syncToggleUI(); syncToggleUI();
document.dispatchEvent( document.dispatchEvent(
new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } }) new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } })
@@ -200,7 +259,7 @@
function boot() { function boot() {
if (isHubLinked()) { if (isHubLinked()) {
apply(themeFromUrl() || "dark", { skipStore: true }); apply(get(), { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
try { try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*"); window.parent.postMessage({ type: "instance-theme-ready" }, "*");
@@ -208,12 +267,16 @@
} else { } else {
apply(getStandalone()); apply(getStandalone());
} }
function observeDynamicLists() { function observeDynamicLists() {
["journal-list", "review-list"].forEach((id) => { ["journal-list", "review-list"].forEach((id) => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el || el.dataset.instThemeObserved === "1") return; if (!el || el.dataset.instThemeObserved === "1") return;
el.dataset.instThemeObserved = "1"; el.dataset.instThemeObserved = "1";
new MutationObserver(() => syncInlineStyles(get())).observe(el, { new MutationObserver(() => {
syncInlineStyles(get());
patchHubNavLinks(get());
}).observe(el, {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
@@ -223,6 +286,7 @@
const onReady = () => { const onReady = () => {
initToggleUI(); initToggleUI();
syncInlineStyles(get()); syncInlineStyles(get());
patchHubNavLinks(get());
observeDynamicLists(); observeDynamicLists();
}; };
if (document.readyState === "loading") { if (document.readyState === "loading") {
@@ -232,7 +296,10 @@
} }
document.addEventListener("instance-theme-change", (ev) => { document.addEventListener("instance-theme-change", (ev) => {
const t = ev.detail && ev.detail.theme; const t = ev.detail && ev.detail.theme;
if (t) syncInlineStyles(t); if (t) {
syncInlineStyles(t);
patchHubNavLinks(t);
}
}); });
} }
@@ -240,11 +307,14 @@
global.InstanceTheme = { global.InstanceTheme = {
STANDALONE_KEY, STANDALONE_KEY,
HUB_LINKED_THEME_KEY,
isHubLinked, isHubLinked,
get, get,
apply, apply,
initToggleUI, initToggleUI,
syncToggleUI, syncToggleUI,
syncInlineStyles, syncInlineStyles,
patchHubNavLinks,
mergeHubQueryIntoHref,
}; };
})(typeof window !== "undefined" ? window : globalThis); })(typeof window !== "undefined" ? window : globalThis);
+28
View File
@@ -0,0 +1,28 @@
/* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */
html[data-theme="light"] body {
background: #d8e2ec !important;
color: #1a2838 !important;
}
html[data-theme="light"] .header h1 {
color: #142232 !important;
}
html[data-theme="light"] .top-nav a,
html[data-theme="light"] .strategy-subnav a {
background: #fff !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] .top-nav a.active,
html[data-theme="light"] .strategy-subnav a.active {
background: rgba(0, 110, 154, 0.12) !important;
color: #142232 !important;
}
html[data-theme="light"] .card,
html[data-theme="light"] .stat-item {
background: #fff !important;
border-color: #b8c8d8 !important;
}