dark/light theme toggle

This commit is contained in:
Michael Pilosov 2026-04-21 21:51:16 -06:00
parent ca59516f26
commit 61e9221b3a
5 changed files with 149 additions and 18 deletions

View File

@ -5,18 +5,18 @@
const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_NS = "http://www.w3.org/2000/svg";
// Qualitative palette — muted, matches the notebook aesthetic. Cycles if // Qualitative palette — defined as CSS custom properties on :root (and
// there are more runs than colours. // overridden under [data-theme="dark"]) so the theme toggle automatically
const PALETTE = [ // swaps plot colours. Read fresh on every render.
"#1f4e5f", // accent teal function getPalette() {
"#8a3a2a", // rust const cs = getComputedStyle(document.documentElement);
"#a77a2c", // warm amber const out = [];
"#3a6f3f", // olive for (let i = 1; i <= 8; i++) {
"#5d4a7b", // slate purple const v = cs.getPropertyValue(`--plot-${i}`).trim();
"#7a5c4b", // brown if (v) out.push(v);
"#2b5d7a", // deeper blue }
"#6b6b3e", // moss return out.length ? out : ["#1f4e5f"];
]; }
const state = { const state = {
raw: [], raw: [],
@ -50,6 +50,7 @@ async function init() {
populateFilters(); populateFilters();
wireEvents(); wireEvents();
document.addEventListener("themechange", render);
render(); render();
} }
@ -163,9 +164,10 @@ function render() {
// Stable colour assignment per legend row (across the full filtered set), // Stable colour assignment per legend row (across the full filtered set),
// so toggling selection doesn't shuffle colours. // so toggling selection doesn't shuffle colours.
const palette = getPalette();
const colored = runs.map((r, i) => ({ const colored = runs.map((r, i) => ({
run: r, run: r,
color: PALETTE[i % PALETTE.length], color: palette[i % palette.length],
selected: state.selected.has(runKey(r)), selected: state.selected.has(runKey(r)),
})); }));
const forPlot = state.selected.size === 0 ? colored : colored.filter((c) => c.selected); const forPlot = state.selected.size === 0 ? colored : colored.filter((c) => c.selected);

View File

@ -25,6 +25,17 @@
--warm: #a77a2c; --warm: #a77a2c;
--good: #3a6f3f; --good: #3a6f3f;
/* Qualitative palette for plot lines. JS reads these via getComputedStyle
so the palette flips with the theme. */
--plot-1: #1f4e5f;
--plot-2: #8a3a2a;
--plot-3: #a77a2c;
--plot-4: #3a6f3f;
--plot-5: #5d4a7b;
--plot-6: #7a5c4b;
--plot-7: #2b5d7a;
--plot-8: #6b6b3e;
--serif: Georgia, "Iowan Old Style", "Palatino Linotype", serif; --serif: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
"Inter", "Arial", sans-serif; "Inter", "Arial", sans-serif;
@ -36,6 +47,57 @@
--space: 1rem; --space: 1rem;
} }
/* Dark mode warm near-black, preserving the scientific-notebook feel.
Toggled by setting [data-theme="dark"] on <html>. */
[data-theme="dark"] {
--page: #1a1917;
--panel: #1f1e1c;
--ink: #e8e4da;
--mute: #9a968c;
--faint: #5f5b52;
--rule: #2b2925;
--rule-2: #3a3834;
--accent: #84bcc9;
--accent-tint: #1c2930;
--alarm: #d18774;
--warm: #cba368;
--good: #8fb695;
--plot-1: #84bcc9;
--plot-2: #d18774;
--plot-3: #cba368;
--plot-4: #8fb695;
--plot-5: #a695c4;
--plot-6: #bc9f8a;
--plot-7: #8fa7c5;
--plot-8: #adad80;
}
html { color-scheme: light dark; }
[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
/* Hardcoded light-mode colour patches. These predate the theme system;
overriding them here keeps the original selectors untouched. */
[data-theme="dark"] button.submit:hover,
[data-theme="dark"] .picker-footer .continue:not(:disabled):hover {
background: #a5d0da;
}
[data-theme="dark"] .flash.err,
[data-theme="dark"] .badge.cancelled {
background: #3a2520;
}
[data-theme="dark"] .badge.completed {
background: #1f2e21;
}
[data-theme="dark"] .dataset-picker {
--picker-panel: #252420;
--picker-hair: #3a3834;
}
[data-theme="dark"] .dataset-picker .card-desc {
color: var(--ink);
}
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { html, body {
@ -880,6 +942,26 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c
} }
.masthead-link:hover { border-bottom-color: var(--accent); } .masthead-link:hover { border-bottom-color: var(--accent); }
.theme-toggle {
background: transparent;
border: 0;
color: var(--mute);
font-size: 1.05rem;
line-height: 1;
cursor: pointer;
padding: 0 0 0 0.55rem;
vertical-align: middle;
transition: color 120ms ease, transform 240ms ease;
user-select: none;
}
.theme-toggle:hover { color: var(--accent); }
.theme-toggle:focus-visible {
outline: 1px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
}
[data-theme="dark"] .theme-toggle { transform: rotate(180deg); }
.masthead .nav-link { .masthead .nav-link {
font-family: var(--mono); font-family: var(--mono);
font-size: 0.78rem; font-size: 0.78rem;

29
app/web/static/theme.js Normal file
View File

@ -0,0 +1,29 @@
// theme.js — toggle UI wire-up. The initial theme is set by a tiny inline
// script in the <head> so there's no flash of the wrong palette on load;
// this module only handles click-to-toggle + broadcasting the change so
// anything that cached a CSS-var value (e.g. the metrics page's plot
// palette) can re-render.
(function () {
function current() {
return document.documentElement.getAttribute("data-theme") || "light";
}
function apply(theme) {
document.documentElement.setAttribute("data-theme", theme);
try { localStorage.setItem("theme", theme); } catch (e) {}
document.dispatchEvent(new CustomEvent("themechange", { detail: { theme } }));
const btn = document.getElementById("theme-toggle");
if (btn) btn.setAttribute("aria-label", theme === "dark" ? "switch to light mode" : "switch to dark mode");
}
function wire() {
const btn = document.getElementById("theme-toggle");
if (!btn) return;
btn.addEventListener("click", () => apply(current() === "dark" ? "light" : "dark"));
apply(current());
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", wire);
} else {
wire();
}
})();

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>embedding notebook — web1</title> <title>embedding notebook — web1</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css?v=5" />
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script type="importmap"> <script type="importmap">
{ {
@ -15,6 +15,13 @@
} }
</script> </script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" /> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" />
<script>
(function(){try{
var t=localStorage.getItem('theme');
if(!t)t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
document.documentElement.setAttribute('data-theme',t);
}catch(e){}})();
</script>
</head> </head>
<body> <body>
@ -25,7 +32,8 @@
<div class="meta"> <div class="meta">
<span class="dot {% if not deployment_id %}bad{% endif %}"></span> <span class="dot {% if not deployment_id %}bad{% endif %}"></span>
{% if deployment_id %}prefect · deployment {{ deployment_id[:8] }}{% else %}prefect · unreachable{% endif %} {% if deployment_id %}prefect · deployment {{ deployment_id[:8] }}{% else %}prefect · unreachable{% endif %}
<a href="/metrics" class="nav-link">metrics &rarr;</a><br/> <a href="/metrics" class="nav-link">metrics &rarr;</a>
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="toggle theme"></button><br/>
<span style="color:var(--faint)">{{ prefect_api }}</span> <span style="color:var(--faint)">{{ prefect_api }}</span>
</div> </div>
</header> </header>
@ -185,6 +193,7 @@
<span><span class="k">web</span> · scientific instrument · port 8001</span> <span><span class="k">web</span> · scientific instrument · port 8001</span>
</footer> </footer>
<script src="/static/theme.js"></script>
<script type="module" src="/static/dataset-picker.js"></script> <script type="module" src="/static/dataset-picker.js"></script>
</body> </body>

View File

@ -4,8 +4,15 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>metrics — embedding notebook</title> <title>metrics — embedding notebook</title>
<link rel="stylesheet" href="/static/style.css?v=4" /> <link rel="stylesheet" href="/static/style.css?v=5" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" /> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" />
<script>
(function(){try{
var t=localStorage.getItem('theme');
if(!t)t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
document.documentElement.setAttribute('data-theme',t);
}catch(e){}})();
</script>
</head> </head>
<body> <body>
@ -14,7 +21,8 @@
<h1 class="title">embedding notebook <em>&mdash; stability metrics</em></h1> <h1 class="title">embedding notebook <em>&mdash; stability metrics</em></h1>
</div> </div>
<div class="meta"> <div class="meta">
<a href="/" class="masthead-link">&larr; runs</a><br/> <a href="/" class="masthead-link">&larr; runs</a>
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="toggle theme"></button><br/>
<span style="color:var(--faint)">{{ prefect_api }}</span> <span style="color:var(--faint)">{{ prefect_api }}</span>
</div> </div>
</header> </header>
@ -85,7 +93,8 @@
<span><span class="k">web</span> · metrics · port 8001</span> <span><span class="k">web</span> · metrics · port 8001</span>
</footer> </footer>
<script type="module" src="/static/metrics.js?v=4"></script> <script src="/static/theme.js?v=5"></script>
<script type="module" src="/static/metrics.js?v=5"></script>
</body> </body>
</html> </html>