|
31 | 31 | '<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>' + |
32 | 32 | '</svg>'; |
33 | 33 |
|
34 | | - var SVG_SUN = |
35 | | - '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">' + |
36 | | - '<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>' + |
| 34 | + /* Icons from Material for MkDocs icon set, matching the org's mkdocs.yml palette config. |
| 35 | + Cycle order: auto → light → dark → auto |
| 36 | + auto → material/lightbulb-auto-outline (following OS preference) |
| 37 | + light → material/lightbulb-outline (forced light, click for dark) |
| 38 | + dark → material/lightbulb (forced dark, click for auto) */ |
| 39 | + var SVG_AUTO = |
| 40 | + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' + |
| 41 | + '<path d="M9 2c3.87 0 7 3.13 7 7 0 2.38-1.19 4.47-3 5.74V17c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2.26C3.19 13.47 2 11.38 2 9c0-3.87 3.13-7 7-7M6 21v-1h6v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1M9 4C6.24 4 4 6.24 4 9c0 2.05 1.23 3.81 3 4.58V16h4v-2.42c1.77-.77 3-2.53 3-4.58 0-2.76-2.24-5-5-5m10 9h-2l-3.2 9h1.9l.7-2h3.2l.7 2h1.9zm-2.15 5.65L18 15l1.15 3.65z"/>' + |
37 | 42 | '</svg>'; |
38 | 43 |
|
39 | | - var SVG_MOON = |
40 | | - '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">' + |
41 | | - '<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/>' + |
| 44 | + var SVG_LIGHT = |
| 45 | + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' + |
| 46 | + '<path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7M9 21v-1h6v1a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1m3-17a5 5 0 0 0-5 5c0 2.05 1.23 3.81 3 4.58V16h4v-2.42c1.77-.77 3-2.53 3-4.58a5 5 0 0 0-5-5"/>' + |
| 47 | + '</svg>'; |
| 48 | + |
| 49 | + var SVG_DARK = |
| 50 | + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' + |
| 51 | + '<path d="M12 2a7 7 0 0 0-7 7c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26c1.81-1.27 3-3.36 3-5.74a7 7 0 0 0-7-7M9 21a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1H9z"/>' + |
42 | 52 | '</svg>'; |
43 | 53 |
|
44 | 54 | /* Module-level references so the built bar can be re-inserted on instant |
45 | | - navigation without recreating it (prevents the logo img from reloading). */ |
| 55 | + navigation without recreating it (prevents the logo from reloading). */ |
46 | 56 | var themeBtn = null; |
47 | 57 | var savedBar = null; |
48 | 58 |
|
49 | | - function getScheme() { |
50 | | - /* Prefer our own stored key — avoids Material overwriting it on navigation */ |
| 59 | + /* ── Theme state ───────────────────────────────────────────────────────── |
| 60 | + 'utplsql-scheme' in localStorage holds one of three values: |
| 61 | + 'auto' — follow the OS prefers-color-scheme (default when unset) |
| 62 | + 'default' — forced light mode |
| 63 | + 'slate' — forced dark mode |
| 64 | + ─────────────────────────────────────────────────────────────────────── */ |
| 65 | + |
| 66 | + function getPreference() { |
| 67 | + try { return localStorage.getItem('utplsql-scheme') || 'auto'; } catch (e) {} |
| 68 | + return 'auto'; |
| 69 | + } |
| 70 | + |
| 71 | + function osPrefersDark() { |
| 72 | + return window.matchMedia('(prefers-color-scheme: dark)').matches; |
| 73 | + } |
| 74 | + |
| 75 | + /* Returns the effective Material colour scheme for the current preference. */ |
| 76 | + function effectiveScheme(pref) { |
| 77 | + if (pref === 'slate' || pref === 'default') return pref; |
| 78 | + return osPrefersDark() ? 'slate' : 'default'; /* auto */ |
| 79 | + } |
| 80 | + |
| 81 | + function applyScheme(scheme) { |
| 82 | + document.body.setAttribute('data-md-color-scheme', scheme); |
| 83 | + /* Keep Material's own __palette in sync so its JS doesn't fight us. */ |
51 | 84 | try { |
52 | | - var own = localStorage.getItem('utplsql-scheme'); |
53 | | - if (own) return own; |
| 85 | + var stored = JSON.parse(localStorage.getItem('__palette') || 'null'); |
| 86 | + if (stored && stored.color) { |
| 87 | + stored.color.scheme = scheme; |
| 88 | + stored.index = scheme === 'slate' ? 1 : 0; |
| 89 | + } else { |
| 90 | + stored = { index: scheme === 'slate' ? 1 : 0, color: { scheme: scheme } }; |
| 91 | + } |
| 92 | + localStorage.setItem('__palette', JSON.stringify(stored)); |
54 | 93 | } catch (e) {} |
55 | | - return document.body.getAttribute('data-md-color-scheme') || 'default'; |
56 | 94 | } |
57 | 95 |
|
58 | 96 | function updateThemeIcon() { |
59 | 97 | if (!themeBtn) return; |
60 | | - var scheme = getScheme(); |
61 | | - var icons = window.utplsqlPaletteIcons; |
62 | | - if (icons) { |
63 | | - var entry = icons.filter(function (e) { return e.scheme === scheme; })[0]; |
64 | | - if (entry) { |
65 | | - themeBtn.innerHTML = entry.icon; |
66 | | - themeBtn.setAttribute('aria-label', entry.name); |
67 | | - return; |
68 | | - } |
| 98 | + var pref = getPreference(); |
| 99 | + if (pref === 'auto') { |
| 100 | + themeBtn.innerHTML = SVG_AUTO; |
| 101 | + themeBtn.setAttribute('aria-label', 'Theme: auto (following OS) — click for light'); |
| 102 | + } else if (pref === 'default') { |
| 103 | + themeBtn.innerHTML = SVG_LIGHT; |
| 104 | + themeBtn.setAttribute('aria-label', 'Theme: light — click for dark'); |
| 105 | + } else { |
| 106 | + themeBtn.innerHTML = SVG_DARK; |
| 107 | + themeBtn.setAttribute('aria-label', 'Theme: dark — click for auto'); |
69 | 108 | } |
70 | | - themeBtn.innerHTML = scheme === 'slate' ? SVG_SUN : SVG_MOON; |
71 | | - themeBtn.setAttribute('aria-label', 'Toggle light / dark mode'); |
72 | 109 | } |
73 | 110 |
|
74 | | - /* Re-applies our stored colour scheme to <body> after Material may have |
75 | | - overwritten it during instant-navigation re-initialisation. */ |
| 111 | + /* Re-applies our stored preference after Material's navigation re-init. */ |
76 | 112 | function applyStoredScheme() { |
77 | | - try { |
78 | | - var scheme = localStorage.getItem('utplsql-scheme'); |
79 | | - if (scheme) { |
80 | | - document.body.setAttribute('data-md-color-scheme', scheme); |
81 | | - updateThemeIcon(); |
82 | | - } |
83 | | - } catch (e) {} |
| 113 | + var pref = getPreference(); |
| 114 | + applyScheme(effectiveScheme(pref)); |
| 115 | + updateThemeIcon(); |
84 | 116 | } |
85 | 117 |
|
86 | | - /* Updates the active class on nav links based on the current URL. |
87 | | - Called on first inject and on every instant-navigation page swap. */ |
| 118 | + /* Updates the active class on nav links based on the current URL. */ |
88 | 119 | function updateActiveLink() { |
89 | 120 | var bar = document.getElementById('utplsql-topbar'); |
90 | 121 | if (!bar) return; |
|
104 | 135 | } |
105 | 136 |
|
106 | 137 | /* Material's instant navigation removed the topbar — re-insert the same |
107 | | - DOM node so the logo <img> is not recreated and doesn't flash/reload. */ |
| 138 | + DOM node so the CSS background-image icon doesn't flash. */ |
108 | 139 | if (savedBar) { |
109 | 140 | document.body.insertBefore(savedBar, document.body.firstChild); |
110 | 141 | updateActiveLink(); |
|
155 | 186 | var controls = document.createElement('div'); |
156 | 187 | controls.className = 'utplsql-controls'; |
157 | 188 |
|
158 | | - /* Theme toggle — assigned to module-level themeBtn so updateThemeIcon |
159 | | - can be called from the navigation callback without re-injecting. */ |
| 189 | + /* Theme toggle — cycles: auto → light → dark → auto */ |
160 | 190 | themeBtn = document.createElement('button'); |
161 | | - |
162 | 191 | themeBtn.addEventListener('click', function () { |
163 | | - var next = getScheme() === 'slate' ? 'default' : 'slate'; |
164 | | - document.body.setAttribute('data-md-color-scheme', next); |
165 | | - /* Store under our own key so we can reliably restore after navigation */ |
| 192 | + var current = getPreference(); |
| 193 | + var next = current === 'auto' ? 'default' : current === 'default' ? 'slate' : 'auto'; |
166 | 194 | try { localStorage.setItem('utplsql-scheme', next); } catch (e) {} |
167 | | - /* Also update Material's __palette so its own JS stays in sync */ |
168 | | - try { |
169 | | - var stored = JSON.parse(localStorage.getItem('__palette') || 'null'); |
170 | | - if (stored && stored.color) { |
171 | | - stored.color.scheme = next; |
172 | | - stored.index = next === 'slate' ? 1 : 0; |
173 | | - } else { |
174 | | - stored = { index: next === 'slate' ? 1 : 0, color: { scheme: next } }; |
175 | | - } |
176 | | - localStorage.setItem('__palette', JSON.stringify(stored)); |
177 | | - } catch (e) {} |
| 195 | + applyScheme(effectiveScheme(next)); |
178 | 196 | updateThemeIcon(); |
179 | 197 | }); |
180 | 198 |
|
|
185 | 203 | savedBar = bar; |
186 | 204 | document.body.insertBefore(bar, document.body.firstChild); |
187 | 205 | updateActiveLink(); |
| 206 | + |
| 207 | + /* When auto mode is active, re-apply if the OS theme changes live. */ |
| 208 | + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { |
| 209 | + if (getPreference() === 'auto') { |
| 210 | + applyScheme(effectiveScheme('auto')); |
| 211 | + } |
| 212 | + }); |
188 | 213 | } |
189 | 214 |
|
| 215 | + /* Apply initial scheme as early as possible to avoid flash. */ |
| 216 | + applyScheme(effectiveScheme(getPreference())); |
| 217 | + |
190 | 218 | if (document.readyState === 'loading') { |
191 | 219 | document.addEventListener('DOMContentLoaded', inject); |
192 | 220 | } else { |
193 | 221 | inject(); |
194 | 222 | } |
195 | 223 |
|
196 | | - /* Material instant navigation — fires after each page swap. |
197 | | - inject() returns early (topbar exists) and re-applies our stored scheme |
198 | | - to override anything Material changed during its re-initialisation. */ |
| 224 | + /* Material instant navigation — fires after each page swap. */ |
199 | 225 | if (typeof document$ !== 'undefined') { |
200 | 226 | document$.subscribe(inject); |
201 | 227 | } |
|
0 commit comments