Spaces:
Running
修明暗切換在使用者自己的 Chrome 沒作用(Gradio 容器 class 蓋過我的切換)
Browse files千卉回報「你開的 Chrome 可以,我自己開的不行,已 Ctrl+Shift+R」。這個差異
正是關鍵線索——追出兩個真因:
1. 切換只作用在 <html>,蓋不過 Gradio 自己的 class
Gradio 會依自己的主題設定/系統偵測,在 .gradio-container 上掛 dark class。
CSS 自訂屬性是「離內容最近的祖先勝出」,html.light 蓋不過
.gradio-container.dark,所以點了畫面完全沒變化。我的 Playwright Chrome
是淺色系統、Gradio 不會加那個 class,因此自動測試一直測不出來。
修法:偏好同時套到 html/body/.gradio-container 三處,並在既有 400ms 輪詢
裡持續自我修正(Gradio 重繪會把自己的 class 加回來),沿用本檔既有的
「輪詢自我修正」解法。
2. _effectiveDark() 判斷的是「系統設定」而非「畫面實際狀態」
沒有存過偏好時原本直接問 matchMedia(prefers-color-scheme)。但畫面可能已經
因為 Gradio 而是深色,matchMedia 卻回報淺色 → 第一次點擊算成「要轉深」→
使用者看到「按了沒變化」。改成先看畫面上真的有沒有 dark class,再退回問系統。
順帶把圖示切換改成結構上不可能壞的做法:兩個 SVG 都常駐 DOM,只用 CSS 依
data-theme-icon 切換顯示,JS 不再改寫 innerHTML(改寫會銷毀重建 <svg>,真人
點擊的 mousedown/mouseup 一旦跨到重建就不會產生 click)。移除已無用的
_MOON/_SUN JS 常數。
驗證方式:手動在 .gradio-container 加 dark class 重現「Gradio 深色」情境(我的
系統是淺色,無法直接複現),實測點擊可正確移除/加回該 class、chat-col 與氣泡
底色雙向切換、輪詢 1.6 秒後不會被搶回去。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app_agent.py +63 -18
|
@@ -2827,6 +2827,12 @@ img.button-icon { opacity: .82; }
|
|
| 2827 |
.nav-collapse-toggle:active, .nav-icon-btn:active {
|
| 2828 |
background: rgba(var(--c-accent-rgb),.16);
|
| 2829 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2830 |
.nav-collapse-toggle:focus-visible, .nav-drawer-close:focus-visible,
|
| 2831 |
.mobile-nav-toggle:focus-visible, .nav-icon-btn:focus-visible {
|
| 2832 |
outline: 2px solid var(--c-accent); outline-offset: 2px;
|
|
@@ -3758,14 +3764,49 @@ _APP_HEAD_SCRIPT = """
|
|
| 3758 |
=系統深色。所以 html.dark→強制深、html.light→強制淺(贏過系統深色的
|
| 3759 |
媒體規則)、都沒有→跟隨系統。切換鈕在 nav-col 裡(工作區才有),但
|
| 3760 |
_loadTheme 在頁面一載入就套用存好的偏好,首頁也吃得到。 */
|
| 3761 |
-
|
| 3762 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3763 |
function _effectiveDark() {
|
| 3764 |
-
var
|
| 3765 |
-
if (
|
| 3766 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3767 |
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 3768 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3769 |
function _applyThemeIcon() {
|
| 3770 |
var btn = document.getElementById('theme_toggle');
|
| 3771 |
if (!btn) return;
|
|
@@ -3779,27 +3820,20 @@ _APP_HEAD_SCRIPT = """
|
|
| 3779 |
修法:狀態沒變就不要動 DOM。 */
|
| 3780 |
var want = dark ? 'dark' : 'light';
|
| 3781 |
if (btn.dataset.themeIcon === want) return;
|
|
|
|
|
|
|
| 3782 |
btn.dataset.themeIcon = want;
|
| 3783 |
-
/* 顯示「切換後會變成」的那一個:目前深色就顯示太陽(點了轉淺)。 */
|
| 3784 |
-
btn.innerHTML = dark ? _SUN : _MOON;
|
| 3785 |
btn.setAttribute('aria-label', dark ? '切換為淺色模式' : '切換為深色模式');
|
| 3786 |
}
|
| 3787 |
function _loadTheme() {
|
| 3788 |
-
|
| 3789 |
-
try { saved = localStorage.getItem('spectra_theme'); } catch (err) {}
|
| 3790 |
-
var r = document.documentElement;
|
| 3791 |
-
r.classList.remove('dark', 'light');
|
| 3792 |
-
if (saved === 'dark') r.classList.add('dark');
|
| 3793 |
-
else if (saved === 'light') r.classList.add('light');
|
| 3794 |
_applyThemeIcon();
|
| 3795 |
}
|
| 3796 |
document.addEventListener('click', function (e) {
|
| 3797 |
if (!(e.target.closest && e.target.closest('#theme_toggle'))) return;
|
| 3798 |
var toDark = !_effectiveDark();
|
| 3799 |
-
var r = document.documentElement;
|
| 3800 |
-
r.classList.remove('dark', 'light');
|
| 3801 |
-
r.classList.add(toDark ? 'dark' : 'light');
|
| 3802 |
try { localStorage.setItem('spectra_theme', toDark ? 'dark' : 'light'); } catch (err) {}
|
|
|
|
| 3803 |
_applyThemeIcon();
|
| 3804 |
});
|
| 3805 |
_loadTheme();
|
|
@@ -4162,7 +4196,11 @@ _APP_HEAD_SCRIPT = """
|
|
| 4162 |
_initActivePageIfNeeded();
|
| 4163 |
_reapplyActivePage();
|
| 4164 |
_syncUnloadGuard();
|
| 4165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4166 |
}, 400);
|
| 4167 |
}
|
| 4168 |
if (document.readyState === 'loading') {
|
|
@@ -4328,8 +4366,15 @@ def create_app():
|
|
| 4328 |
'<div class="nav-top-row">'
|
| 4329 |
'<span class="nav-brand en">SpectraEngine</span>'
|
| 4330 |
'<div class="nav-top-actions">'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4331 |
f'<button type="button" id="theme_toggle" class="nav-icon-btn" '
|
| 4332 |
-
f'aria-label="切換為深色模式">
|
|
|
|
|
|
|
|
|
|
| 4333 |
f'<button type="button" id="nav_collapse_toggle" class="nav-collapse-toggle" '
|
| 4334 |
f'aria-label="收合導覽列" aria-expanded="true">{_icon("chevron-double-left", 18)}</button>'
|
| 4335 |
'</div>'
|
|
|
|
| 2827 |
.nav-collapse-toggle:active, .nav-icon-btn:active {
|
| 2828 |
background: rgba(var(--c-accent-rgb),.16);
|
| 2829 |
}
|
| 2830 |
+
/* 明暗切換鈕:兩個圖示常駐 DOM,只用 CSS 切換顯示(預設淺色→顯示月亮,
|
| 2831 |
+
代表「點了會變深色」)。JS 只改 data-theme-icon,不動 DOM 結構。 */
|
| 2832 |
+
#theme_toggle .ico-moon { display: inline-flex; }
|
| 2833 |
+
#theme_toggle .ico-sun { display: none; }
|
| 2834 |
+
#theme_toggle[data-theme-icon="dark"] .ico-moon { display: none; }
|
| 2835 |
+
#theme_toggle[data-theme-icon="dark"] .ico-sun { display: inline-flex; }
|
| 2836 |
.nav-collapse-toggle:focus-visible, .nav-drawer-close:focus-visible,
|
| 2837 |
.mobile-nav-toggle:focus-visible, .nav-icon-btn:focus-visible {
|
| 2838 |
outline: 2px solid var(--c-accent); outline-offset: 2px;
|
|
|
|
| 3764 |
=系統深色。所以 html.dark→強制深、html.light→強制淺(贏過系統深色的
|
| 3765 |
媒體規則)、都沒有→跟隨系統。切換鈕在 nav-col 裡(工作區才有),但
|
| 3766 |
_loadTheme 在頁面一載入就套用存好的偏好,首頁也吃得到。 */
|
| 3767 |
+
/* 圖示 SVG 直接寫在按鈕裡(見 create_app 的 theme_toggle),顯示切換
|
| 3768 |
+
交給 CSS 的 #theme_toggle[data-theme-icon],這裡不再持有 SVG 字串。 */
|
| 3769 |
+
/* 踩坑(2026-07-24,千卉回報「我自己開的 Chrome 按了沒用,你開的可以」):
|
| 3770 |
+
原本只在 <html> 加 .dark/.light。但**當使用者的系統是深色時,Gradio 自己
|
| 3771 |
+
會偵測並在它的容器(.gradio-container/body)上加 dark class**,而 CSS 自訂
|
| 3772 |
+
屬性是「離內容最近的祖先勝出」——html.light 蓋不過 .gradio-container.dark,
|
| 3773 |
+
所以點了畫面完全沒變化。我的 Playwright Chrome 是淺色系統、Gradio 不會加
|
| 3774 |
+
那個 class,因此自動測試一直測不出來。
|
| 3775 |
+
修法:偏好要同時套到 html/body/.gradio-container 三處,並在輪詢裡持續
|
| 3776 |
+
自我修正(Gradio 重繪時會把自己的 class 加回來)。 */
|
| 3777 |
+
function _themeTargets() {
|
| 3778 |
+
return [document.documentElement, document.body,
|
| 3779 |
+
document.querySelector('.gradio-container')].filter(Boolean);
|
| 3780 |
+
}
|
| 3781 |
+
function _savedTheme() {
|
| 3782 |
+
try { return localStorage.getItem('spectra_theme'); } catch (err) { return null; }
|
| 3783 |
+
}
|
| 3784 |
+
/* 判斷「目前畫面實際是深是淺」。踩坑:原本沒有偏好時直接問
|
| 3785 |
+
matchMedia(prefers-color-scheme),但 Gradio 可能因為自己的主題設定或
|
| 3786 |
+
系統偵測,已經在容器上掛了 dark class——此時畫面明明是深色,matchMedia
|
| 3787 |
+
卻可能回報淺色,於是第一次點擊算出「要轉深」,使用者看到的是「按了
|
| 3788 |
+
沒變化」。改成先看畫面上真的有沒有 dark class,再退回問系統。 */
|
| 3789 |
function _effectiveDark() {
|
| 3790 |
+
var saved = _savedTheme();
|
| 3791 |
+
if (saved === 'dark') return true;
|
| 3792 |
+
if (saved === 'light') return false;
|
| 3793 |
+
var applied = _themeTargets().some(function (el) {
|
| 3794 |
+
return el.classList.contains('dark');
|
| 3795 |
+
});
|
| 3796 |
+
if (applied) return true;
|
| 3797 |
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 3798 |
}
|
| 3799 |
+
/* 把使用者選的偏好壓在三個層級上;沒有偏好(跟隨系統)就完全不干預,
|
| 3800 |
+
讓 Gradio 與媒體查詢自己決定。 */
|
| 3801 |
+
function _applyThemeClasses() {
|
| 3802 |
+
var saved = _savedTheme();
|
| 3803 |
+
if (!saved) return;
|
| 3804 |
+
var dark = saved === 'dark';
|
| 3805 |
+
_themeTargets().forEach(function (el) {
|
| 3806 |
+
if (el.classList.contains('dark') !== dark) el.classList.toggle('dark', dark);
|
| 3807 |
+
if (el.classList.contains('light') === dark) el.classList.toggle('light', !dark);
|
| 3808 |
+
});
|
| 3809 |
+
}
|
| 3810 |
function _applyThemeIcon() {
|
| 3811 |
var btn = document.getElementById('theme_toggle');
|
| 3812 |
if (!btn) return;
|
|
|
|
| 3820 |
修法:狀態沒變就不要動 DOM。 */
|
| 3821 |
var want = dark ? 'dark' : 'light';
|
| 3822 |
if (btn.dataset.themeIcon === want) return;
|
| 3823 |
+
/* 只改屬性,圖示顯示交給 CSS(#theme_toggle[data-theme-icon])——
|
| 3824 |
+
DOM 結構完全不動,click 不會因為子元素被重建而消失。 */
|
| 3825 |
btn.dataset.themeIcon = want;
|
|
|
|
|
|
|
| 3826 |
btn.setAttribute('aria-label', dark ? '切換為淺色模式' : '切換為深色模式');
|
| 3827 |
}
|
| 3828 |
function _loadTheme() {
|
| 3829 |
+
_applyThemeClasses();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3830 |
_applyThemeIcon();
|
| 3831 |
}
|
| 3832 |
document.addEventListener('click', function (e) {
|
| 3833 |
if (!(e.target.closest && e.target.closest('#theme_toggle'))) return;
|
| 3834 |
var toDark = !_effectiveDark();
|
|
|
|
|
|
|
|
|
|
| 3835 |
try { localStorage.setItem('spectra_theme', toDark ? 'dark' : 'light'); } catch (err) {}
|
| 3836 |
+
_applyThemeClasses();
|
| 3837 |
_applyThemeIcon();
|
| 3838 |
});
|
| 3839 |
_loadTheme();
|
|
|
|
| 4196 |
_initActivePageIfNeeded();
|
| 4197 |
_reapplyActivePage();
|
| 4198 |
_syncUnloadGuard();
|
| 4199 |
+
/* Gradio 重繪時會把自己偵測系統主題加的 dark class 加回容器上,
|
| 4200 |
+
這裡持續把使用者選的偏好壓回去(跟本檔其他「輪詢自我修正」同一套
|
| 4201 |
+
解法)。theme_toggle 在工作區才掛載,圖示也一併補正。 */
|
| 4202 |
+
_applyThemeClasses();
|
| 4203 |
+
_applyThemeIcon();
|
| 4204 |
}, 400);
|
| 4205 |
}
|
| 4206 |
if (document.readyState === 'loading') {
|
|
|
|
| 4366 |
'<div class="nav-top-row">'
|
| 4367 |
'<span class="nav-brand en">SpectraEngine</span>'
|
| 4368 |
'<div class="nav-top-actions">'
|
| 4369 |
+
# 兩個圖示都先放進去、用 CSS 依 data-theme-icon 切換顯示。
|
| 4370 |
+
# 不用 JS 改寫 innerHTML:改寫會把 <svg> 銷毀重建,真人點擊的
|
| 4371 |
+
# mousedown/mouseup 一旦跨到重建就不會產生 click(見
|
| 4372 |
+
# _applyThemeIcon 註解)。DOM 不動就沒有這個風險。
|
| 4373 |
f'<button type="button" id="theme_toggle" class="nav-icon-btn" '
|
| 4374 |
+
f'aria-label="切換為深色模式">'
|
| 4375 |
+
f'<span class="ico-moon">{_icon("moon", 18)}</span>'
|
| 4376 |
+
f'<span class="ico-sun">{_icon("sun", 18)}</span>'
|
| 4377 |
+
f'</button>'
|
| 4378 |
f'<button type="button" id="nav_collapse_toggle" class="nav-collapse-toggle" '
|
| 4379 |
f'aria-label="收合導覽列" aria-expanded="true">{_icon("chevron-double-left", 18)}</button>'
|
| 4380 |
'</div>'
|