Spaces:
Sleeping
Sleeping
| import dash | |
| from dash import Dash, html, dcc | |
| from dash import Input, Output, State, callback, clientside_callback | |
| import dash_mantine_components as dmc | |
| from dash_iconify import DashIconify | |
| def generate_toggle_navbar(): | |
| def toggle_navbar(mobile_opened, desktop_opened, navbar): | |
| navbar["collapsed"] = { | |
| "mobile": not mobile_opened, | |
| "desktop": not desktop_opened, | |
| } | |
| return navbar | |
| return toggle_navbar | |
| def switch_light_dark(): | |
| """ | |
| 前端切換 Mantine 亮/暗主題 + 修正文字/背景的自訂 inline 色彩 | |
| 依據邏輯: | |
| 1) 先決定 desired('light' 或 'dark'),優先用 Switch,其次讀 localStorage | |
| 2) 套用到 <html>、<body>,並同步 localStorage | |
| 3) ★ 套用到根部的 MantineProvider(需要在 app.py: MantineProvider 加 id="mantine-provider") | |
| 4) 同步 Switch 外觀,避免與實際狀態不同步 | |
| 5) 僅對「原本就有 inline color/background」或帶 data-theme-aware 的元素做對應轉色 | |
| """ | |
| return clientside_callback( | |
| """ | |
| function(switchOn, pathname) { | |
| // 0) 決定這次要套用的主題 | |
| let desired = null; | |
| if (typeof switchOn === 'boolean') { | |
| desired = switchOn ? 'dark' : 'light'; | |
| } else { | |
| try { desired = localStorage.getItem('mantine-color-scheme') || 'dark'; } | |
| catch(e) { desired = 'dark'; } | |
| } | |
| const isDark = (desired === 'dark'); | |
| // 1) 設定 <html>/<body> 屬性 + localStorage(Mantine 會吃到) | |
| document.documentElement.setAttribute('data-mantine-color-scheme', desired); | |
| if (document.body) document.body.setAttribute('data-mantine-color-scheme', desired); | |
| try { | |
| document.documentElement.dataset.mantineColorScheme = desired; | |
| if (document.body) document.body.dataset.mantineColorScheme = desired; | |
| localStorage.setItem('mantine-color-scheme', desired); | |
| } catch (e) {} | |
| // 2) ★ 套用到 MantineProvider 的根節點(請在 app.py 的 MantineProvider 加 id="mantine-provider") | |
| // 這能讓 Mantine/DMC 的元件樣式(文字、背景、Border)整體跟著切換 | |
| const root = document.querySelector('#mantine-provider'); | |
| if (root) { | |
| root.setAttribute('data-mantine-color-scheme', desired); | |
| try { root.dataset.mantineColorScheme = desired; } catch (e) {} | |
| } | |
| // 3) 同步 Switch 外觀(避免 UI 與實際主題不同步) | |
| const sw = document.getElementById('color-scheme-switch'); | |
| if (sw && typeof switchOn !== 'boolean') { | |
| try { sw.checked = isDark; } catch (e) {} | |
| } | |
| // 4) 文字色的亮<->暗對照(只轉「原本就有 inline color」的元素) | |
| const textLightToDark = { | |
| 'black': '#f2f2f2', '#000000': '#f2f2f2', | |
| 'gray': '#cccccc', '#808080': '#cccccc', | |
| 'blue': '#90caf9', 'darkblue': '#64b5f6', | |
| 'teal': '#80cbc4', | |
| 'red': '#ef9a9a', | |
| 'orange': '#ffcc80', | |
| '#ffa6ff': '#cc66cc' | |
| }; | |
| const textDarkToLight = {}; | |
| Object.keys(textLightToDark).forEach(k => { textDarkToLight[textLightToDark[k]] = k; }); | |
| // 5) 背景暗色 fallback(僅對「原本就有 inline 背景」的元素覆蓋) | |
| const DARK_BG_FALLBACK = 'var(--mantine-color-body)'; | |
| const norm = (s) => (s || '').trim().toLowerCase(); | |
| // 6) 只處理三種元素: | |
| // - 有 inline color | |
| // - 有 inline background/background-color | |
| // - 或者主動標記 data-theme-aware 的元素(即使沒有 inline style 也會處理) | |
| const candidates = document.querySelectorAll( | |
| '[style*="color:"], [style*="background:"], [style*="background-color:"], [data-theme-aware]' | |
| ); | |
| candidates.forEach(el => { | |
| const cs = getComputedStyle(el); | |
| const styleAttr = el.getAttribute('style') || ''; | |
| // A) 記錄原始文字色(第一次遇到時才記) | |
| const hadInlineColor = /(^|;)\\s*color\\s*:/.test(styleAttr) || el.hasAttribute('data-theme-aware'); | |
| if (hadInlineColor && !el.dataset.originalColor) { | |
| // 優先記 inline,其次 computed | |
| el.dataset.originalColor = norm(el.style.color || cs.color || ''); | |
| } | |
| // B) 記錄原始背景(第一次遇到時才記) | |
| const hadInlineBg = /(^|;)\\s*background(-color)?\\s*:/.test(styleAttr) || el.hasAttribute('data-theme-aware'); | |
| if (hadInlineBg && !el.dataset.hadInlineBg) { | |
| el.dataset.hadInlineBg = '1'; | |
| el.dataset.originalInlineBg = norm(el.style.backgroundColor || cs.backgroundColor || ''); | |
| } else if (!el.dataset.hadInlineBg) { | |
| el.dataset.hadInlineBg = '0'; | |
| } | |
| // === 文字色反轉:僅動「原來就有 inline color」或 data-theme-aware 的元素 === | |
| if (el.dataset.originalColor) { | |
| const cur = norm(el.style.color || cs.color || ''); | |
| let nextColor; | |
| if (isDark) { | |
| nextColor = textLightToDark[cur] || textLightToDark[el.dataset.originalColor] || cur; | |
| } else { | |
| nextColor = textDarkToLight[cur] || el.dataset.originalColor || cur; | |
| } | |
| el.style.setProperty('color', nextColor, 'important'); | |
| } | |
| // === 背景色反轉:僅動「原來就有 inline 背景」或 data-theme-aware === | |
| if (el.dataset.hadInlineBg === '1') { | |
| if (isDark) { | |
| el.style.setProperty('background-color', DARK_BG_FALLBACK, 'important'); | |
| } else { | |
| const restore = el.dataset.originalInlineBg || ''; | |
| if (restore) { | |
| el.style.setProperty('background-color', restore, 'important'); | |
| } else { | |
| el.style.removeProperty('background-color'); | |
| } | |
| } | |
| } | |
| }); | |
| // 回傳字串避免 undefined(觸發 dash 更新) | |
| return desired + ':' + Date.now(); | |
| } | |
| """, | |
| Output("theme-dummy", "children"), | |
| # 兩個輸入:切換開關 + 換頁/首次載入 | |
| [Input("color-scheme-switch", "checked"), | |
| Input("_pages_location", "pathname")] | |
| ) | |