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(): @callback( Output("appshell", "navbar"), Input("mobile-burger", "opened"), Input("desktop-burger", "opened"), State("appshell", "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) 套用到 、
,並同步 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) 設定 / 屬性 + 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")] )