File size: 6,757 Bytes
e66cfb4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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) 套用到 <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")]
    )