From 776f966edfbc33120d61edef0e65ddcdba44263a Mon Sep 17 00:00:00 2001 From: lberki Date: Thu, 5 May 2022 11:12:10 +0200 Subject: [PATCH] Add dark mode Add dark mode See #65 --- assets/scss/tale/_base.scss | 12 +++--- assets/scss/tale/_catalogue.scss | 12 +++--- assets/scss/tale/_code.scss | 14 +++++-- assets/scss/tale/_layout.scss | 29 ++++++++++---- assets/scss/tale/_pagination.scss | 12 +++--- assets/scss/tale/_post.scss | 8 ++-- assets/scss/tale/_toc.scss | 15 +++++-- assets/scss/tale/_variables.scss | 54 +++++++++++++++++++------ layouts/_default/baseof.html | 1 + layouts/partials/darkmode.html | 3 ++ layouts/partials/head.html | 2 + static/js/darkmode.js | 65 +++++++++++++++++++++++++++++++ 12 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 layouts/partials/darkmode.html create mode 100644 static/js/darkmode.js diff --git a/assets/scss/tale/_base.scss b/assets/scss/tale/_base.scss index 6c2471b8..c096ebbb 100644 --- a/assets/scss/tale/_base.scss +++ b/assets/scss/tale/_base.scss @@ -5,8 +5,8 @@ html, body { - color: $default-color; - background-color: #fff; + color: var(--default-color); + background-color: var(--background-color); margin: 0; padding: 0; } @@ -31,19 +31,19 @@ h3, h4, h5, h6 { - color: $default-shade; + color: var(--default-shade); font-family: $sans-serif; line-height: normal; } a { - color: $blue; + color: var(--blue); text-decoration: none; } blockquote { - border-left: .25rem solid $grey-2; - color: $grey-1; + border-left: .25rem solid var(--grey-2); + color: var(--grey-1); margin: .8rem 0; padding: .5rem 1rem; diff --git a/assets/scss/tale/_catalogue.scss b/assets/scss/tale/_catalogue.scss index 0587bc5d..ec065a16 100644 --- a/assets/scss/tale/_catalogue.scss +++ b/assets/scss/tale/_catalogue.scss @@ -1,7 +1,7 @@ .catalogue { &-item { - border-bottom: 1px solid $grey-2; - color: $default-color; + border-bottom: 1px solid var(--grey-2); + color: var(--default-color); display: block; padding: 2rem 0; @@ -16,13 +16,13 @@ } &-time { - color: $default-tint; + color: var(--default-tint); font-family: $serif-secondary; letter-spacing: .5px; } &-title { - color: $default-shade; + color: var(--default-shade); display: block; font-family: $sans-serif; font-size: 2rem; @@ -31,8 +31,8 @@ } &-line { - @include transition(all .3s ease-out); - border-top: .2rem solid $default-shade; + @include transition($color-transition, width .3s ease-out); + border-top: .2rem solid var(--default-shade); display: block; width: 2rem; } diff --git a/assets/scss/tale/_code.scss b/assets/scss/tale/_code.scss index 11f1c537..6d97161d 100644 --- a/assets/scss/tale/_code.scss +++ b/assets/scss/tale/_code.scss @@ -4,9 +4,9 @@ code { } code { - background-color: $grey-4; + background-color: var(--grey-4); border-radius: 3px; - color: $code-color; + color: var(--code-color); font-size: 85%; padding: .25em .5em; white-space: pre-wrap; @@ -23,8 +23,14 @@ pre code { padding: 0; } +pre code > span { + // This isn't very pretty, but the precise RGB values are generated + // server-side, the usual trick of putting the colors in CSS variables does + // not quite work and this is the best I could come up with. + filter: var(--code-filter); +} .highlight { - background-color: $grey-4; + background-color: var(--grey-4); border-radius: 3px; line-height: 1.4; margin: 0 0 1rem; @@ -36,7 +42,7 @@ pre code { } .lineno { - color: $default-tint; + color: var(--default-tint); display: inline-block; // Ensures the null space also isn't selectable padding: 0 .75rem 0 .25rem; // Make sure numbers aren't selectable diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index 8d56834b..c459510e 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -1,3 +1,7 @@ +* { + @include transition($color-transition); +} + .container { margin: 0 auto; max-width: 800px; @@ -17,8 +21,19 @@ footer, width: 80%; } +#darkModeToggle { + float: right; + position: sticky; + top: 2rem; + margin-right: 2rem; + margin-top: 2rem; + font-size: 2rem; + + cursor: pointer; +} + .nav { - box-shadow: 0 2px 2px -2px $shadow-color; + box-shadow: 0 2px 2px -2px var(--shadow-color); overflow: auto; &-container { @@ -28,8 +43,8 @@ footer, } &-title { - @include transition(all .2s ease-out); - color: $default-color; + @include transition($color-transition, opacity .2s ease-out); + color: var(--default-color); display: inline-block; margin: 0; padding-right: .2rem; @@ -48,8 +63,8 @@ footer, } li { - @include transition(all .2s ease-out); - color: $default-color; + @include transition($color-transition, opacity .2s ease-out); + color: var(--default-color); display: inline-block; opacity: .6; padding: 0 2rem 0 0; @@ -65,7 +80,7 @@ footer, } a { - color: $default-color; + color: var(--default-color); font-family: $sans-serif; } } @@ -90,7 +105,7 @@ footer { text-align: center; span { - color: $default-color; + color: var(--default-color); font-size: .8rem; } } diff --git a/assets/scss/tale/_pagination.scss b/assets/scss/tale/_pagination.scss index 3700e152..8b391b5d 100644 --- a/assets/scss/tale/_pagination.scss +++ b/assets/scss/tale/_pagination.scss @@ -1,18 +1,18 @@ .pagination { - border-top: .5px solid $grey-2; + border-top: .5px solid var(--grey-2); font-family: $serif-secondary; padding-top: 2rem; position: relative; text-align: center; span { - color: $default-shade; + color: var(--default-shade); font-size: 1.1rem; } .top { - @include transition(all .3s ease-out); - color: $default-color; + @include transition($color-transition, opacity .3s ease-out); + color: var(--default-color); font-family: $sans-serif; font-size: 1.1rem; opacity: .6; @@ -23,8 +23,8 @@ } .arrow { - @include transition(all .3s ease-out); - color: $default-color; + @include transition($color-transition, opacity .3s ease-out); + color: var(--default-color); position: absolute; &:hover, diff --git a/assets/scss/tale/_post.scss b/assets/scss/tale/_post.scss index 3680d80b..61cff18e 100644 --- a/assets/scss/tale/_post.scss +++ b/assets/scss/tale/_post.scss @@ -2,7 +2,7 @@ padding: 3rem 0; &-info { - color: $default-tint; + color: var(--default-tint); font-family: $serif-secondary; letter-spacing: 0.5px; text-align: center; @@ -13,7 +13,7 @@ } &-title { - color: $default-shade; + color: var(--default-shade); font-family: $sans-serif; font-size: 4rem; margin: 1rem 0; @@ -21,7 +21,7 @@ } &-line { - border-top: 0.4rem solid $default-shade; + border-top: 0.4rem solid var(--default-shade); display: block; margin: 0 auto 3rem; width: 4rem; @@ -41,7 +41,7 @@ } img + em { - color: $default-tint; + color: var(--default-tint); display: block; font-family: $sans-serif; font-size: 0.9rem; diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index 6492ba74..08ccd475 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -17,6 +17,10 @@ aside.toc { float: left; height: 0; overflow: display; + + // This is apparenty necessary so that the table of contents covers elements + // with the "filter:" property set. + z-index: 1; } #tocTitle { @@ -34,7 +38,7 @@ aside.toc { } #tocContainer { - background: $grey-3; + background-color: var(--grey-3); border-radius: 1rem; margin: 2rem; padding: 1rem; @@ -54,11 +58,14 @@ aside.toc { width: calc(2rem + var(--measured-title-width)); --measured-title-width: 2.4rem; - @include transition(all .1s ease-out); +@include transition( + $color-transition, + width .1s ease-out, + height .1s ease-out); } #tocContainer > div { - border-left: 0.4rem solid black; + border-left: 0.4rem solid var(--default-shade); padding-left: 1rem; } @@ -72,7 +79,7 @@ aside.toc { // No transition on width so that there is no re-wrapping during the // opening/closing transition - @include transition(height .1s ease-out); + @include transition($color-transition, height .1s ease-out); } nav#TableOfContents ul { diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 92d1114d..43914baf 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -1,15 +1,47 @@ // Colors -$default-color: #555; -$default-shade: #353535; -$default-tint: #aaa; -$grey-1: #979797; -$grey-2: #e5e5e5; -$grey-3: #f0f0f0; -$grey-4: #f9f9f9; -$white: #fff; -$blue: #4a9ae1; -$shadow-color: rgba(0, 0, 0, .2); -$code-color: #bf616a; +// This is necessary because some elements want to set transitions themselves, +// which would result in deleting the color transitions specified in less +// specific selectors. +$color-transition: + background-color .4s ease-out, + color .4s ease-out, + border-color .4s ease-out, + box-shadow .4s ease-out; + + +// Make the default be dark mode. That way, the more complicated case is the +// default and thus bugs are discovered earlier. +:root.light { + --default-color: #555; + --background-color: #fff; + --default-shade: #353535; + --default-tint: #aaa; + --grey-1: #979797; + --grey-2: #e5e5e5; + --grey-3: #f0f0f0; + --grey-4: #f9f9f9; + --white: #fff; + --blue: #4a9ae1; + --shadow-color: rgba(0, 0, 0, .2); + --code-color: #bf616a; + --code-filter: ; +} + +:root:not(.light) { + --default-color: #888; + --background-color: #000; + --default-shade: #989898; + --default-tint: #555; + --grey-1: #606060; + --grey-2: #404040; + --grey-3: #202020; + --grey-4: #181818; + --white: #fff; + --blue: #1d6baf; + --shadow-color: rgba(0, 0, 0, .2); + --code-color: #a3434c; + --code-filter: contrast(0.4) brightness(0.9); +} // Fonts $serif-primary: 'Libre Baskerville', 'Times New Roman', Times, serif; diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 866a593c..4bf1dc87 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -3,6 +3,7 @@ {{ partial "head.html" . }} {{ partial "header.html" . }} + {{ partial "darkmode.html" . }} {{ block "main" . }}{{ end }} {{ partial "footer.html" . }} diff --git a/layouts/partials/darkmode.html b/layouts/partials/darkmode.html new file mode 100644 index 00000000..1016b536 --- /dev/null +++ b/layouts/partials/darkmode.html @@ -0,0 +1,3 @@ +
+ ◐ +
diff --git a/layouts/partials/head.html b/layouts/partials/head.html index fe10a913..0d9b5127 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -30,4 +30,6 @@ + + diff --git a/static/js/darkmode.js b/static/js/darkmode.js new file mode 100644 index 00000000..8161f9a5 --- /dev/null +++ b/static/js/darkmode.js @@ -0,0 +1,65 @@ +"use strict"; + +const LOCALSTORAGE_KEY = "color-scheme"; +const LIGHT_CLASS = "light"; +let mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + +// true means "dark scheme", false means "light scheme". Ugly, bit simple. +function storedToBool(s) { + if (s === "dark") { + return true; + } else if (s === "light") { + return false; + } else { + return null; + } +} + +function ensureScheme(desiredScheme) { + let osScheme = mediaQuery.matches; + + // Only store the preference if it's not the same as the OS one. + if (desiredScheme === osScheme) { + localStorage.removeItem(LOCALSTORAGE_KEY); + } else { + localStorage.setItem(LOCALSTORAGE_KEY, desiredScheme ? "dark" : "light"); + } + + if (desiredScheme) { + document.documentElement.classList.remove(LIGHT_CLASS); + } else { + document.documentElement.classList.add(LIGHT_CLASS); + } +} + +function initDarkMode() { + let storedScheme = storedToBool(localStorage.getItem(LOCALSTORAGE_KEY)); + let osScheme = mediaQuery.matches; + + // When the class of the document element is changed from a script running in the + // element, no CSS transition defined on the changed properties takes place. + ensureScheme(storedScheme === null ? osScheme : storedScheme); + mediaQuery.addEventListener("change", osDarkModeChanged); +} + +function osDarkModeChanged(query) { + let osScheme = query.matches; + let pageScheme = !document.documentElement.classList.contains(LIGHT_CLASS); + + // If the current preference is the same as that of the OS, we assume that the user also wants + // to change the color scheme of the web page. If not, we assume that they consciously overrode + // the OS and we don't change anything. + if (pageScheme != osScheme) { + ensureScheme(!pageScheme); + } +} + +function toggleDarkMode() { + // The easiest is to decide based on the current color scheme. + let currentScheme = !document.documentElement.classList.contains(LIGHT_CLASS); + ensureScheme(!currentScheme); +} + +// Doing this in an onload handler would let the initial color appear for a +// split second. +initDarkMode();