Three states rather than two: system (follow the OS), light, dark. A two-state toggle cannot express "just do what my laptop does", which is the state most people actually want. Two details that are easy to get wrong and were both handled deliberately: - The dark rules are guarded with :root:not([data-theme="light"]). Without that guard, choosing light does nothing on a machine whose OS is set to dark, because the media query keeps winning -- the toggle appears to work in one direction only. - The stored theme is applied by an inline script placed ahead of any rendered content, so there is no flash of the wrong palette on load. A deferred script would repaint after first paint, visibly. localStorage access is wrapped in try/catch: it throws outright in some private-browsing modes, and a theme preference is not worth a broken page. The palette moved out of all four templates into _theme.html, which also removes four near-identical copies of the nav CSS that had already started to drift. Tests assert the toggle, the guard, the pre-paint ordering, and that no page redeclares the palette locally.
45 lines
2.0 KiB
HTML
45 lines
2.0 KiB
HTML
{#- Shared palette, theme toggle and nav styling.
|
|
|
|
Three states, not two: system (follow the OS), light, dark. The dark rules
|
|
are guarded with :not([data-theme="light"]) so an explicit light choice wins
|
|
over a dark OS setting -- without that guard the toggle only works one way.
|
|
-#}
|
|
<script>
|
|
// Runs before first paint. Applying the stored theme from a deferred script
|
|
// would show a flash of the wrong palette on every page load.
|
|
try {
|
|
const t = localStorage.getItem('theme');
|
|
if (t === 'dark' || t === 'light') document.documentElement.dataset.theme = t;
|
|
} catch (e) { /* private mode, blocked storage: fall back to the OS setting */ }
|
|
</script>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--fg:#16181d; --bg:#fdfdfc; --muted:#6b7280; --rule:#e4e4e7;
|
|
--accent:#7c3aed; --card:#fff; --sel:#f3f0ff;
|
|
--pos:#15803d; --neg:#b91c1c; --warn:#b45309;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--fg:#e8e8ea; --bg:#141416; --muted:#9ca3af; --rule:#2a2a2e;
|
|
--accent:#a78bfa; --card:#1b1b1f; --sel:#241f36;
|
|
--pos:#4ade80; --neg:#f87171; --warn:#fbbf24;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--fg:#e8e8ea; --bg:#141416; --muted:#9ca3af; --rule:#2a2a2e;
|
|
--accent:#a78bfa; --card:#1b1b1f; --sel:#241f36;
|
|
--pos:#4ade80; --neg:#f87171; --warn:#fbbf24;
|
|
}
|
|
|
|
.sitenav { display:flex; gap:.15rem; flex-wrap:wrap; align-items:center; }
|
|
.sitenav a { color:var(--muted); text-decoration:none; font-size:.85rem;
|
|
padding:.2rem .6rem; border-radius:6px; border:1px solid transparent; }
|
|
.sitenav a:hover { color:var(--fg); background:var(--rule); }
|
|
.sitenav a.here { color:var(--accent); border-color:var(--rule); font-weight:600; }
|
|
#theme { margin-left:.35rem; font:inherit; font-size:.95rem; line-height:1;
|
|
background:transparent; color:var(--muted); cursor:pointer;
|
|
border:1px solid var(--rule); border-radius:6px; padding:.22rem .45rem; }
|
|
#theme:hover { color:var(--fg); }
|
|
</style>
|