feat: add light/dark theme to exchange instances with hub SSO sync

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 12:52:27 +08:00
parent 6f8f0968c8
commit d14c629778
24 changed files with 3134 additions and 2369 deletions
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body data-page="{{ page }}">
{% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container">
<div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
</div>
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 关键位放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -22,11 +24,26 @@
#chart{width:100%;height:100%}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+19 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>登录 · {{ exchange_display }}</title>
<style>
* {
@@ -92,7 +94,23 @@
font-weight: 600;
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<div class="login-theme-bar">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 实盘下单放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -23,11 +25,26 @@
.empty{padding:18px;color:#95a2c2}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body data-page="{{ page }}">
{% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container">
<div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
</div>
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 关键位放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -22,11 +24,26 @@
#chart{width:100%;height:100%}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+19 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>登录 · {{ exchange_display }}</title>
<style>
* {
@@ -92,7 +94,23 @@
font-weight: 600;
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<div class="login-theme-bar">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 实盘下单放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -23,11 +25,26 @@
.empty{padding:18px;color:#95a2c2}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -209,6 +211,8 @@
.stats-split-row{grid-template-columns:1fr}
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
{% macro period_metrics_cells(s) %}
@@ -243,7 +247,21 @@
<div class="container">
<div class="header">
<h1>加密货币|Gate 机器人交易监控</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
</div>
<div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 关键位放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -22,11 +24,26 @@
#chart{width:100%;height:100%}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+19 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>登录 · {{ exchange_display }}</title>
<style>
* {
@@ -92,7 +94,23 @@
font-weight: 600;
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<div class="login-theme-bar">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>{{ exchange_display }} | 实盘下单放大</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -23,11 +25,26 @@
.empty{padding:18px;color:#95a2c2}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body data-page="{{ page }}">
{% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container">
<div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
</div>
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
+18 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>关键位放大 | K线查看</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -21,11 +23,26 @@
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
#chart{width:100%;height:100%}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong>
+19 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>系统登录</title>
<style>
* {
@@ -82,7 +84,23 @@
font-size: 0.85rem;
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<div class="login-theme-bar">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<title>实盘下单放大 | 100根K线</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -22,11 +24,26 @@
#chart{width:100%;height:100%}
.empty{padding:18px;color:#95a2c2}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
+62 -4
View File
@@ -28,6 +28,46 @@ from hub_sso import (
)
def _merge_query_into_path(path: str, **params: str) -> str:
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
split = urlsplit(path or "/")
q = list(parse_qsl(split.query, keep_blank_values=True))
keys = {k for k, _ in q}
for k, v in params.items():
if not v or k in keys:
continue
q.append((k, str(v)))
return urlunsplit((split.scheme, split.netloc, split.path, urlencode(q), split.fragment))
def install_instance_theme_static(app) -> None:
"""仓库根 static/instance_theme.* 供四所页面共用。"""
import os
from flask import Response, send_file
repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
assets = {
"instance_theme.js": "application/javascript; charset=utf-8",
"instance_theme.css": "text/css; charset=utf-8",
}
for name, mime in assets.items():
path = os.path.join(repo_static, name)
def _view(p=path, m=mime):
if not os.path.isfile(p):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(p, mimetype=m)
app.add_url_rule(
f"/static/{name}",
endpoint=f"repo_static_{name.replace('.', '_')}",
view_func=_view,
)
def _hub_auth_required(f):
@wraps(f)
def wrapped(*args, **kwargs):
@@ -149,6 +189,7 @@ def install_on_app(
}
install_hub_embed_headers(app)
configure_hub_embed_session(app)
install_instance_theme_static(app)
register_hub_routes(app)
@@ -421,11 +462,22 @@ def register_hub_routes(app):
if _sso_wants_embed_auth() and request.is_secure:
boot = mint_hub_embed_bootstrap(ex, next_path)
if boot:
q = urlencode({"t": boot, "next": next_path, "embed": "1"})
return redirect(f"/hub-embed-auth?{q}")
from urllib.parse import urlencode as _ue
qdict = {"t": boot, "next": next_path, "embed": "1"}
ht0 = (request.args.get("hub_theme") or "").strip().lower()
if ht0 in ("light", "dark"):
qdict["hub_theme"] = ht0
return redirect(f"/hub-embed-auth?{_ue(qdict)}")
session["logged_in"] = True
session.modified = True
return redirect(next_path)
dest = next_path
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
dest = _merge_query_into_path(dest, embed="1")
ht = (request.args.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
dest = _merge_query_into_path(dest, hub_theme=ht)
return redirect(dest)
hint = err or "校验失败"
flash(
f"中控 SSO 未生效({hint})。"
@@ -449,7 +501,13 @@ def register_hub_routes(app):
if ok:
session["logged_in"] = True
session.modified = True
return redirect(next_path)
dest = next_path
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
dest = _merge_query_into_path(dest, embed="1")
ht = (request.args.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
dest = _merge_query_into_path(dest, hub_theme=ht)
return redirect(dest)
hint = err or "校验失败"
flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。")
return redirect("/login")
+8 -1
View File
@@ -1057,7 +1057,11 @@ def _require_hub_logged_in(request: Request) -> None:
@app.get("/api/instance/open-url")
def api_instance_open_url(
request: Request, exchange_id: str, next: str = "/", embed: str = ""
request: Request,
exchange_id: str,
next: str = "/",
embed: str = "",
hub_theme: str = "",
):
"""已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。"""
_require_hub_logged_in(request)
@@ -1079,6 +1083,9 @@ def api_instance_open_url(
params = {"token": token, "next": nxt}
if (embed or "").strip().lower() in ("1", "true", "yes", "on"):
params["embed"] = "1"
ht = (hub_theme or "").strip().lower()
if ht in ("light", "dark"):
params["hub_theme"] = ht
q = urlencode(params)
return {
"ok": True,
+13
View File
@@ -56,6 +56,9 @@
const next = nextPath || "/";
const q = new URLSearchParams({ exchange_id: String(exchangeId), next });
if (options.embed) q.set("embed", "1");
if (globalThis.HubTheme && typeof HubTheme.get === "function") {
q.set("hub_theme", HubTheme.get());
}
const r = await apiFetch("/api/instance/open-url?" + q.toString());
const j = await r.json();
if (!j.ok || !j.url) {
@@ -135,6 +138,16 @@
shell.classList.remove("hidden");
shell.setAttribute("aria-hidden", "false");
document.body.classList.add("hub-instance-frame-open");
frame.addEventListener("load", function syncInstanceFrameTheme() {
try {
if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
frame.contentWindow.postMessage(
{ type: "hub-theme-sync", theme: HubTheme.get() },
"*"
);
}
} catch (_) {}
});
}
function closeInstanceFrame() {
+3 -3
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8" />
<script src="/assets/theme.js?v=20260604-hub-theme4"></script>
<script src="/assets/theme.js?v=20260604-hub-inst-theme"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b0e18" />
<meta name="apple-mobile-web-app-title" content="中控" />
@@ -248,7 +248,7 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-hub-chart-sse"></script>
<script src="/assets/app.js?v=20260604-hub-chart-sse"></script>
<script src="/assets/chart.js?v=20260604-hub-inst-theme"></script>
<script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
</body>
</html>
+10
View File
@@ -15,6 +15,15 @@
}
}
function broadcastThemeToInstances() {
const msg = { type: "hub-theme-sync", theme: get() };
document.querySelectorAll("iframe#instance-frame, iframe.instance-frame").forEach((frame) => {
try {
if (frame.contentWindow) frame.contentWindow.postMessage(msg, "*");
} catch (_) {}
});
}
function apply(theme) {
const t = normalize(theme);
const root = document.documentElement;
@@ -26,6 +35,7 @@
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } }));
broadcastThemeToInstances();
return t;
}
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""为四所 templates 注入 instance_theme 脚本/样式与切换按钮。"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXCHANGES = ("crypto_monitor_binance", "crypto_monitor_okx", "crypto_monitor_gate", "crypto_monitor_gate_bot")
FILES = ("index.html", "login.html", "key_focus_v2.html", "order_focus_v2.html")
SCRIPT_TAG = ' <script src="/static/instance_theme.js?v=1"></script>\n'
CSS_LINK = ' <link rel="stylesheet" href="/static/instance_theme.css?v=1">\n'
THEME_TOGGLE = """ <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
"""
INDEX_HEADER_OLD = """ <div class="header">
<h1>加密货币交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
</div>"""
INDEX_HEADER_NEW = """ <div class="header">
<h1>加密货币交易监控 + AI复盘一体化</h1>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
""" + THEME_TOGGLE + """ </div>
</div>"""
def patch_file(path: Path) -> bool:
text = path.read_text(encoding="utf-8")
orig = text
if 'data-theme="dark"' not in text:
text = text.replace('<html lang="zh-CN">', '<html lang="zh-CN" data-theme="dark">', 1)
if "/static/instance_theme.js" not in text:
text = text.replace(
"<meta charset=\"UTF-8\">",
"<meta charset=\"UTF-8\">\n" + SCRIPT_TAG.strip() + "\n",
1,
)
if "/static/instance_theme.css" not in text:
text = text.replace("</style>", "</style>\n" + CSS_LINK, 1)
if path.name == "index.html" and INDEX_HEADER_OLD in text and "instance-theme-toggle" not in text:
text = text.replace(INDEX_HEADER_OLD, INDEX_HEADER_NEW)
if path.name == "login.html" and "instance-theme-toggle" not in text:
text = text.replace(
"<body>",
'<div class="login-theme-bar">\n' + THEME_TOGGLE + "</div>\n<body>",
1,
)
if path.name == "key_focus_v2.html" and "instance-theme-toggle" not in text:
marker = '<div class="row" style="justify-content:space-between">'
if marker in text:
text = text.replace(
marker,
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
1,
)
if path.name == "order_focus_v2.html" and "instance-theme-toggle" not in text:
marker = '<div class="row" style="justify-content:space-between">'
if marker in text:
text = text.replace(
marker,
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
1,
)
if text != orig:
path.write_text(text, encoding="utf-8")
return True
return False
def main() -> None:
n = 0
for ex in EXCHANGES:
for fn in FILES:
p = ROOT / ex / "templates" / fn
if p.is_file() and patch_file(p):
print("patched", p.relative_to(ROOT))
n += 1
print("done", n, "files")
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
/* 实例页亮色主题(覆盖模板内联暗色样式) */
html[data-theme="light"] {
color-scheme: light;
}
html[data-theme="light"] body {
background: #d8e2ec !important;
color: #1a2838 !important;
}
html[data-theme="light"] .header h1 {
color: #142232 !important;
}
html[data-theme="light"] .exchange-tag {
color: #087a50 !important;
background: rgba(10, 143, 92, 0.12) !important;
border-color: rgba(10, 143, 92, 0.35) !important;
}
html[data-theme="light"] .top-nav a {
background: #fff !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] .top-nav a.active {
background: rgba(0, 110, 154, 0.12) !important;
color: #142232 !important;
}
html[data-theme="light"] .stat-item,
html[data-theme="light"] .card,
html[data-theme="light"] .meta-item,
html[data-theme="light"] .list-item {
background: #fff !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .stat-item .label,
html[data-theme="light"] .status,
html[data-theme="light"] .rule-tip {
color: #4a6078 !important;
}
html[data-theme="light"] .stat-item .value,
html[data-theme="light"] .card h2 {
color: #142232 !important;
}
html[data-theme="light"] input,
html[data-theme="light"] select,
html[data-theme="light"] textarea {
background: #f6f9fc !important;
color: #142232 !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .flash {
background: rgba(0, 110, 154, 0.1) !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] th {
color: #4a6078 !important;
}
html[data-theme="light"] td {
color: #142232 !important;
border-bottom-color: #d0dae4 !important;
}
html[data-theme="light"] .ai-result,
html[data-theme="light"] .login-box {
background: #fff !important;
border-color: #b8c8d8 !important;
color: #142232 !important;
}
html[data-theme="light"] #chart-wrap {
background: #f0f4f9 !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .btn {
background: #fff !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] .btn:hover {
background: #eef3f8 !important;
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: 8px;
border: 1px solid #304164;
background: #151a2a;
}
html[data-theme="light"] .theme-toggle {
background: #fff;
border-color: #b8c8d8;
}
.theme-toggle.is-hub-linked {
display: none !important;
}
.theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 30px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: #8fc8ff;
cursor: pointer;
}
html[data-theme="light"] .theme-toggle-btn {
color: #4a6078;
}
.theme-toggle-btn.is-active {
color: #dbe4ff;
background: rgba(79, 121, 255, 0.2);
box-shadow: inset 0 0 0 1px #304164;
}
html[data-theme="light"] .theme-toggle-btn.is-active {
color: #006e9a;
background: rgba(0, 110, 154, 0.12);
box-shadow: inset 0 0 0 1px #b8c8d8;
}
.header-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.login-theme-bar {
display: flex;
justify-content: flex-end;
width: 100%;
max-width: 400px;
margin: 0 auto 10px;
}
+141
View File
@@ -0,0 +1,141 @@
/**
* 四所实例主题默认暗色单独登录用 instance-theme中控 iframe/SSO hub-theme 联动
*/
(function (global) {
const STANDALONE_KEY = "instance-theme";
const META = { dark: "#0b0d14", light: "#d8e2ec" };
function normalize(theme) {
return theme === "light" ? "light" : "dark";
}
function isHubLinked() {
try {
const p = new URLSearchParams(location.search);
if (p.get("embed") === "1") return true;
const ht = p.get("hub_theme");
if (ht === "light" || ht === "dark") return true;
} catch (_) {}
try {
if (window.self !== window.top) return true;
} catch (_) {
return true;
}
return false;
}
function themeFromUrl() {
try {
const t = new URLSearchParams(location.search).get("hub_theme");
if (t === "light" || t === "dark") return t;
} catch (_) {}
return null;
}
function getStandalone() {
try {
return normalize(localStorage.getItem(STANDALONE_KEY));
} catch (_) {
return "dark";
}
}
function setStandalone(theme) {
try {
localStorage.setItem(STANDALONE_KEY, normalize(theme));
} catch (_) {}
}
function get() {
if (isHubLinked()) {
return themeFromUrl() || _linkedTheme || "dark";
}
return getStandalone();
}
let _linkedTheme = null;
function apply(theme, opts) {
const options = opts || {};
const linked = isHubLinked();
const t = normalize(theme);
if (linked) {
_linkedTheme = t;
} else if (!options.skipStore) {
setStandalone(t);
}
const root = document.documentElement;
root.setAttribute("data-theme", t);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
syncToggleUI();
document.dispatchEvent(
new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } })
);
return t;
}
function syncToggleUI(root) {
const scope = root || document;
const linked = isHubLinked();
const toggle = scope.querySelector(".instance-theme-toggle");
if (toggle) {
toggle.classList.toggle("is-hub-linked", linked);
toggle.setAttribute("aria-hidden", linked ? "true" : "false");
}
if (linked) return;
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
const on = btn.getAttribute("data-theme-value") === getStandalone();
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
});
}
function initToggleUI(root) {
const scope = root || document;
syncToggleUI(scope);
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
if (btn.dataset.themeBound === "1") return;
btn.dataset.themeBound = "1";
btn.addEventListener("click", () => {
if (isHubLinked()) return;
apply(btn.getAttribute("data-theme-value"));
});
});
}
function initFromHubMessage(data) {
if (!data || data.type !== "hub-theme-sync") return;
if (!isHubLinked()) return;
apply(data.theme, { skipStore: true });
}
function boot() {
if (isHubLinked()) {
apply(themeFromUrl() || "dark", { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*");
} catch (_) {}
} else {
apply(getStandalone());
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initToggleUI());
} else {
initToggleUI();
}
}
boot();
global.InstanceTheme = {
STANDALONE_KEY,
isHubLinked,
get,
apply,
initToggleUI,
syncToggleUI,
};
})(typeof window !== "undefined" ? window : globalThis);