feat: add light/dark theme to exchange instances with hub SSO sync
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user