diff --git a/_includes/head.html b/_includes/head.html index 18f6f2c..e18be61 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -5,7 +5,29 @@ {% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %} - + + + + + + diff --git a/_includes/header.html b/_includes/header.html new file mode 100644 index 0000000..c23172a --- /dev/null +++ b/_includes/header.html @@ -0,0 +1,35 @@ + diff --git a/_sass/_syntax-dark.scss b/_sass/_syntax-dark.scss new file mode 100644 index 0000000..05c078f --- /dev/null +++ b/_sass/_syntax-dark.scss @@ -0,0 +1,120 @@ +// Dark syntax colors: output of `bundle exec rougify style github.dark --scope .highlight` (rouge 4.7.0). +// Included from dark-theme.scss inside the dark-theme mixin. +@mixin syntax-dark { +.highlight table td { padding: 5px; } +.highlight table pre { margin: 0; } +.highlight, .highlight .w { + color: #c9d1d9; + background-color: #161b22; +} +.highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt, .highlight .kv { + color: #ff7b72; +} +.highlight .gr { + color: #f0f6fc; +} +.highlight .gd { + color: #ffdcd7; + background-color: #67060c; +} +.highlight .nb { + color: #ffa657; +} +.highlight .nc { + color: #ffa657; +} +.highlight .no { + color: #ffa657; +} +.highlight .nn { + color: #ffa657; +} +.highlight .sr { + color: #7ee787; +} +.highlight .na { + color: #7ee787; +} +.highlight .nt { + color: #7ee787; +} +.highlight .gi { + color: #aff5b4; + background-color: #033a16; +} +.highlight .ges { + font-weight: bold; + font-style: italic; +} +.highlight .kc { + color: #79c0ff; +} +.highlight .l, .highlight .ld, .highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mx { + color: #79c0ff; +} +.highlight .sb { + color: #79c0ff; +} +.highlight .bp { + color: #79c0ff; +} +.highlight .ne { + color: #79c0ff; +} +.highlight .nl { + color: #79c0ff; +} +.highlight .py { + color: #79c0ff; +} +.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm { + color: #79c0ff; +} +.highlight .o, .highlight .ow { + color: #79c0ff; +} +.highlight .gh { + color: #1f6feb; + font-weight: bold; +} +.highlight .gu { + color: #1f6feb; + font-weight: bold; +} +.highlight .s, .highlight .sa, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .sx, .highlight .s1, .highlight .ss { + color: #a5d6ff; +} +.highlight .nd { + color: #d2a8ff; +} +.highlight .nf, .highlight .fm { + color: #d2a8ff; +} +.highlight .err { + color: #f0f6fc; + background-color: #8e1519; +} +.highlight .c, .highlight .ch, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cpf, .highlight .c1, .highlight .cs { + color: #8b949e; +} +.highlight .gl { + color: #8b949e; +} +.highlight .gt { + color: #8b949e; +} +.highlight .ni { + color: #c9d1d9; +} +.highlight .si { + color: #c9d1d9; +} +.highlight .ge { + color: #c9d1d9; + font-style: italic; +} +.highlight .gs { + color: #c9d1d9; + font-weight: bold; +} +} \ No newline at end of file diff --git a/_sass/dark-theme.scss b/_sass/dark-theme.scss new file mode 100644 index 0000000..38bfdd6 --- /dev/null +++ b/_sass/dark-theme.scss @@ -0,0 +1,205 @@ +// Dark theme (issue #117). +// +// Minima 2.5 compiles its colors into the CSS and runs lighten()/darken() on +// them, so its Sass variables can't be swapped for CSS custom properties. +// Dark mode is therefore a set of override rules, scoped so the light CSS is +// left exactly as minima emits it. +// +// Which theme applies is decided by `data-theme` on , set before first +// paint by the script in _includes/head.html and changed by assets/js/theme.js. +// Without JavaScript there is no `data-theme`, and the media query at the +// bottom follows the OS setting instead. + +@import "syntax-dark"; + +// Every foreground/background pair below meets WCAG AA (4.5:1) or better. +$dark-background-color: #1b1b1d; // also the theme-color meta value in dark +$dark-text-color: #e3e3e3; // 13.4:1 +$dark-brand-color: #6ea8fe; // 7.1:1 +$dark-visited-color: #8fb0e0; +$dark-grey-color: #a0a0a0; // 6.6:1, post meta and footer +$dark-grey-color-light: #3a3a3d; // borders +$dark-grey-color-dark: #c8c8c8; // site title, 10.3:1 +$dark-code-background: #161b22; // matches rouge github.dark + +@mixin dark-theme { + color-scheme: dark; + + body { + color: $dark-text-color; + background-color: $dark-background-color; + } + + a { + color: $dark-brand-color; + + &:visited { + color: $dark-visited-color; + } + + &:hover { + color: $dark-text-color; + } + } + + blockquote { + color: $dark-grey-color; + border-left-color: $dark-grey-color-light; + } + + pre, + code { + border-color: $dark-grey-color-light; + background-color: $dark-code-background; + } + + hr { + border-color: $dark-grey-color-light; + } + + .svg-icon { + fill: $dark-grey-color; + } + + table { + color: #d0d0d0; + border-color: $dark-grey-color-light; + + tr:nth-child(even) { + background-color: #232326; + } + + th { + background-color: #26262a; + border-color: #444448; + border-bottom-color: #55555a; + } + + td { + border-color: $dark-grey-color-light; + } + } + + .site-header { + border-top-color: $dark-grey-color-dark; + border-bottom-color: $dark-grey-color-light; + } + + .site-title { + &, + &:visited { + color: $dark-grey-color-dark; + } + } + + .site-nav { + .page-link, + .theme-toggle { + color: $dark-text-color; + } + + @include media-query($on-palm) { + background-color: $dark-background-color; + border-color: $dark-grey-color-light; + + .menu-icon > svg { + fill: $dark-grey-color-dark; + } + } + } + + .site-footer { + border-top-color: $dark-grey-color-light; + } + + .footer-col-wrapper, + .post-meta { + color: $dark-grey-color; + } + + @include syntax-dark; + + // github.dark leaves these two unset, which would keep minima's light + // colors (#888, #555) on the dark code background. + .highlight .go { + color: #8b949e; + } + + .highlight .gp { + color: #c9d1d9; + } +} + +html[data-theme="dark"] { + @include dark-theme; +} + +// Keeps native controls and scrollbars light when a visitor picks Light on a +// dark OS; the color-scheme meta would otherwise let the browser follow the OS. +html[data-theme="light"] { + color-scheme: light; +} + +// No JavaScript: no data-theme attribute, so follow the OS setting. +@media (prefers-color-scheme: dark) { + html:not([data-theme]) { + @include dark-theme; + } +} + +/** + * Theme toggle button (in _includes/header.html) + */ +.theme-toggle { + padding: 0; + border: 0; + background: none; + color: $text-color; + font: inherit; + line-height: $base-line-height; + cursor: pointer; + vertical-align: middle; + + // The class rule would otherwise beat the UA's [hidden] { display: none }. + &[hidden] { + display: none; + } + + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + border-radius: 3px; + } + + @include media-query($on-palm) { + margin-left: 20px; + padding: 5px 10px; + } +} + +.theme-icon { + display: none; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + vertical-align: middle; +} + +.theme-icon-fill { + fill: currentColor; +} + +html[data-theme-pref="light"] .theme-icon-light, +html[data-theme-pref="dark"] .theme-icon-dark, +html[data-theme-pref="system"] .theme-icon-system { + display: inline-block; +} + +// Colors fade only while theme.js has this class on (just after a +// click), so page loads and navigation never animate. +html.theme-transition, +html.theme-transition * { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important; +} diff --git a/assets/js/theme.js b/assets/js/theme.js new file mode 100644 index 0000000..5ca46cb --- /dev/null +++ b/assets/js/theme.js @@ -0,0 +1,72 @@ +// Theme toggle (issue #117). The inline script in _includes/head.html has +// already set data-theme before paint; this file wires up the header button +// and keeps the page in step with the OS setting and with other open tabs. +(function () { + var STORAGE_KEY = "theme"; + var ORDER = ["light", "dark", "system"]; + var LABELS = { light: "Light", dark: "Dark", system: "System" }; + // Browser toolbar color on mobile. Light keeps the value the site always had. + var THEME_COLORS = { light: "#424242", dark: "#1b1b1d" }; + + var root = document.documentElement; + var darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); + var button = document.getElementById("theme-toggle"); + var themeColorMeta = document.getElementById("theme-color-meta"); + + function readPref() { + var pref; + try { pref = localStorage.getItem(STORAGE_KEY); } catch (e) {} + return ORDER.indexOf(pref) === -1 ? "system" : pref; + } + + function savePref(pref) { + try { localStorage.setItem(STORAGE_KEY, pref); } catch (e) {} + } + + // Held in memory too, so the toggle still cycles when storage is blocked. + var currentPref = readPref(); + + function applyTheme(pref) { + currentPref = pref; + var theme = pref === "system" ? (darkQuery.matches ? "dark" : "light") : pref; + root.setAttribute("data-theme", theme); + root.setAttribute("data-theme-pref", pref); + if (themeColorMeta) themeColorMeta.setAttribute("content", THEME_COLORS[theme]); + if (button) { + var label = "Theme: " + LABELS[pref]; + button.setAttribute("aria-label", label); + button.setAttribute("title", label); + } + } + + // Fade colors for a click only; see html.theme-transition in dark-theme.scss. + var transitionTimer; + function withTransition() { + root.classList.add("theme-transition"); + clearTimeout(transitionTimer); + transitionTimer = setTimeout(function () { + root.classList.remove("theme-transition"); + }, 300); + } + + if (button) { + button.addEventListener("click", function () { + var next = ORDER[(ORDER.indexOf(currentPref) + 1) % ORDER.length]; + savePref(next); + withTransition(); + applyTheme(next); + }); + button.hidden = false; + } + + darkQuery.addEventListener("change", function () { + if (currentPref === "system") applyTheme("system"); + }); + + // Another tab changed the preference. + window.addEventListener("storage", function (event) { + if (event.key === STORAGE_KEY) applyTheme(readPref()); + }); + + applyTheme(currentPref); +})(); diff --git a/assets/main.scss b/assets/main.scss index 12f3a0b..a1c7e08 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -3,6 +3,7 @@ --- @import "minima"; +@import "dark-theme"; // The About page portrait. `cover` keeps the face centred whatever aspect // ratio the source photo has, so the circle never squashes it.