Skip to content

Commit db2ad78

Browse files
author
Jacek Gębal
committed
Updater theme to use light/dark/auto for switching between modes on the website
1 parent 6bbe43c commit db2ad78

3 files changed

Lines changed: 81 additions & 83 deletions

File tree

docs/assets/topbar.js

Lines changed: 81 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -31,60 +31,91 @@
3131
'<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>' +
3232
'</svg>';
3333

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"/>' +
3742
'</svg>';
3843

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"/>' +
4252
'</svg>';
4353

4454
/* 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). */
4656
var themeBtn = null;
4757
var savedBar = null;
4858

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. */
5184
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));
5493
} catch (e) {}
55-
return document.body.getAttribute('data-md-color-scheme') || 'default';
5694
}
5795

5896
function updateThemeIcon() {
5997
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');
69108
}
70-
themeBtn.innerHTML = scheme === 'slate' ? SVG_SUN : SVG_MOON;
71-
themeBtn.setAttribute('aria-label', 'Toggle light / dark mode');
72109
}
73110

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. */
76112
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();
84116
}
85117

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. */
88119
function updateActiveLink() {
89120
var bar = document.getElementById('utplsql-topbar');
90121
if (!bar) return;
@@ -104,7 +135,7 @@
104135
}
105136

106137
/* 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. */
108139
if (savedBar) {
109140
document.body.insertBefore(savedBar, document.body.firstChild);
110141
updateActiveLink();
@@ -155,26 +186,13 @@
155186
var controls = document.createElement('div');
156187
controls.className = 'utplsql-controls';
157188

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 */
160190
themeBtn = document.createElement('button');
161-
162191
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';
166194
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));
178196
updateThemeIcon();
179197
});
180198

@@ -185,17 +203,25 @@
185203
savedBar = bar;
186204
document.body.insertBefore(bar, document.body.firstChild);
187205
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+
});
188213
}
189214

215+
/* Apply initial scheme as early as possible to avoid flash. */
216+
applyScheme(effectiveScheme(getPreference()));
217+
190218
if (document.readyState === 'loading') {
191219
document.addEventListener('DOMContentLoaded', inject);
192220
} else {
193221
inject();
194222
}
195223

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. */
199225
if (typeof document$ !== 'undefined') {
200226
document$.subscribe(inject);
201227
}

docs/overrides/main.html

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,5 @@
1010
window.utplsqlBaseUrl = "{{ config.site_url }}";
1111
document.documentElement.setAttribute('data-utplsql-site', 'org');
1212
</script>
13-
{%- set palette = config.theme.palette %}
14-
{%- if palette %}
15-
{%- set entries = [palette] if palette is mapping else palette %}
16-
{%- set ns = namespace(icons=[]) %}
17-
{%- for entry in entries %}
18-
{%- if entry.toggle is defined %}
19-
{%- set svg %}{% include ".icons/" ~ entry.toggle.icon ~ ".svg" %}{%- endset %}
20-
{%- set ns.icons = ns.icons + [{"scheme": entry.scheme | default("default"), "icon": svg, "name": entry.toggle.name}] %}
21-
{%- endif %}
22-
{%- endfor %}
23-
{%- if ns.icons %}
24-
<script>window.utplsqlPaletteIcons = {{ ns.icons | tojson }};</script>
25-
{%- endif %}
26-
{%- endif %}
2713
<script src="{{ base_url }}/assets/topbar.js" defer></script>
2814
{% endblock %}

site-template/overrides/main.html

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,6 @@
77

88
{% block extrahead %}
99
{{ super() }}
10-
{%- set palette = config.theme.palette %}
11-
{%- if palette %}
12-
{%- set entries = [palette] if palette is mapping else palette %}
13-
{%- set ns = namespace(icons=[]) %}
14-
{%- for entry in entries %}
15-
{%- if entry.toggle is defined %}
16-
{%- set svg %}{% include ".icons/" ~ entry.toggle.icon ~ ".svg" %}{%- endset %}
17-
{%- set ns.icons = ns.icons + [{"scheme": entry.scheme | default("default"), "icon": svg, "name": entry.toggle.name}] %}
18-
{%- endif %}
19-
{%- endfor %}
20-
{%- if ns.icons %}
21-
<script>window.utplsqlPaletteIcons = {{ ns.icons | tojson }};</script>
22-
{%- endif %}
23-
{%- endif %}
2410
<link rel="stylesheet" href="https://utplsql.org/assets/topbar.css">
2511
<script src="https://utplsql.org/assets/topbar.js" defer></script>
2612
{% endblock %}

0 commit comments

Comments
 (0)