un2266 Claude Opus 4.8 commited on
Commit
5282a9f
·
1 Parent(s): fbf28e3

修明暗切換在使用者自己的 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>

Files changed (1) hide show
  1. app_agent.py +63 -18
app_agent.py CHANGED
@@ -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
- var _MOON = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8z"/></svg>';
3762
- var _SUN = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3763
  function _effectiveDark() {
3764
- var r = document.documentElement;
3765
- if (r.classList.contains('dark')) return true;
3766
- if (r.classList.contains('light')) return false;
 
 
 
 
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
- var saved = null;
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
- _applyThemeIcon(); /* theme_toggle 在工作區才掛載,掛上後補正圖示 */
 
 
 
 
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="切換為深色模式">{_icon("moon", 18)}</button>'
 
 
 
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>'