From b6726900a754adb34a964504db81b5ff8a3bc2b8 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 04:09:30 +0000 Subject: [PATCH 01/19] Add table of contents. It can be enabled using the `toc: true` field in the frontmatter. It auto-expands on hover. If JS is enabled, the expansion is animated. The animation is apparently impossible to implement without JS :( On mobile, expands and unexpands on tap instead of hover. --- assets/scss/tale.scss | 1 + assets/scss/tale/_code.scss | 4 +- assets/scss/tale/_layout.scss | 6 ++- assets/scss/tale/_toc.scss | 81 ++++++++++++++++++++++++++++++++ assets/scss/tale/_variables.scss | 3 +- layouts/_default/single.html | 4 +- layouts/partials/toc.html | 13 +++++ static/js/toc.js | 53 +++++++++++++++++++++ 8 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 assets/scss/tale/_toc.scss create mode 100644 layouts/partials/toc.html create mode 100644 static/js/toc.js diff --git a/assets/scss/tale.scss b/assets/scss/tale.scss index 9d1c12fd..53f9decb 100644 --- a/assets/scss/tale.scss +++ b/assets/scss/tale.scss @@ -4,6 +4,7 @@ @import 'tale/post'; @import 'tale/syntax'; @import 'tale/layout'; +@import 'tale/toc'; @import 'tale/pagination'; @import 'tale/catalogue'; @import 'tale/disqus'; diff --git a/assets/scss/tale/_code.scss b/assets/scss/tale/_code.scss index 8d3db57c..11f1c537 100644 --- a/assets/scss/tale/_code.scss +++ b/assets/scss/tale/_code.scss @@ -4,7 +4,7 @@ code { } code { - background-color: $grey-3; + background-color: $grey-4; border-radius: 3px; color: $code-color; font-size: 85%; @@ -24,7 +24,7 @@ pre code { } .highlight { - background-color: $grey-3; + background-color: $grey-4; border-radius: 3px; line-height: 1.4; margin: 0 0 1rem; diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index ba4a7b0f..8d56834b 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -4,7 +4,11 @@ width: 80%; } -main, +// Carve out an exception from the 80% narrowing of the viewport for the table +// of contents. This is ugly, but there isn't a better way, since the table of +// contents must be a child of the "main" element so that it its scrolling +// behavior is intuitive. +main > *, footer, .nav-container { display: block; diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss new file mode 100644 index 00000000..4240c366 --- /dev/null +++ b/assets/scss/tale/_toc.scss @@ -0,0 +1,81 @@ +aside.toc { + position: sticky; + top: 0; + max-width: 30%; + + // We want the table of contents to be on the left (horizontally), to be + // positioned at a specific point (vertically), stick to the top on scroll + // and it should not affect the layout of other elements. + // "float: left" accomplishes the horizontal positioning, position in the + // element tree the vertical positioning and "position: sticky" does the + // stickiness. "height: 0" makes sure that the flow of other elements is not + // affected. + // + // There does not seem to be a better way to do this unless one is willing + // to implement scrolling in JS: "position" must be set to sticky so neither + // fixed, nor absolute, nor relative positioning can be used. + float: left; + height: 0; + overflow: display; +} + +#tocTitle { + // This is so that we can measure it in JS + width: fit-content; +} + +#tocContainer:hover { + width: calc(2rem + var(--measured-expanded-width)); +} + +#tocContainer:hover div#tocCollapsible { + height: var(--measured-height); + width: var(--measured-expanded-width); +} + +#tocContainer { + background: $grey-3; + border-radius: 1rem; + margin: 2rem; + padding: 1rem; + + // This makes the gap between the two contained divs vanish. Why that gap + // exists, no clue. + display: flex; + flex-direction: column; + + // We want to hide the table of contents before revealing it on hover + overflow: hidden; + + // In addition to the measured width of the title, we need to add the two + // rems for the border (we are using box-sizing: border-box). Also add a + // reasonable default value to minimize visual changes while the page is + // loading. + width: calc(2rem + var(--measured-title-width)); + --measured-title-width: 2.4rem; + + @include transition(all .1s ease-out); +} + +#tocContainer > div { + border-left: 0.4rem solid black; + padding-left: 1rem; +} + +#tocContainer div#tocCollapsible { + // Collapsed by default + height: 0; + + // If we did not force this element to a given width, it would keep + // re-wrapping during the opening/closing transition. + width: var(--measured-expanded-width); + + // No transition on width so that there is no re-wrapping during the + // opening/closing transition + @include transition(height .1s ease-out); +} + +nav#TableOfContents ul { + list-style-type: none; + padding-inline-start: 0px; +} diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 8a064f32..92d1114d 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -4,7 +4,8 @@ $default-shade: #353535; $default-tint: #aaa; $grey-1: #979797; $grey-2: #e5e5e5; -$grey-3: #f9f9f9; +$grey-3: #f0f0f0; +$grey-4: #f9f9f9; $white: #fff; $blue: #4a9ae1; $shadow-color: rgba(0, 0, 0, .2); diff --git a/layouts/_default/single.html b/layouts/_default/single.html index dba83445..f5f6f6e1 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -1,7 +1,9 @@ {{ define "main" }}
-
+ {{ partial "toc.html" . }} + +
{{ partial "single/post-info.html" . }} {{ partial "single/title.html" . }} diff --git a/layouts/partials/toc.html b/layouts/partials/toc.html new file mode 100644 index 00000000..81d524a8 --- /dev/null +++ b/layouts/partials/toc.html @@ -0,0 +1,13 @@ +{{ if .Params.toc }} + +{{ end }} diff --git a/static/js/toc.js b/static/js/toc.js new file mode 100644 index 00000000..41f2665a --- /dev/null +++ b/static/js/toc.js @@ -0,0 +1,53 @@ +"use strict"; + +function sizeFinal() { + let tocContainer = document.getElementById("tocContainer"); + let tocTitle = document.getElementById("tocTitle"); + let tocCollapsible = document.getElementById("tocCollapsible"); + + let oldContainerWidth = tocContainer.style.width; + let oldCollapsibleWidth = tocCollapsible.style.width; + let oldCollapsibleHeight = tocCollapsible.style.height; + + // Set relevant elements to automatic sizing. + // Setting these properties to the empty string is not enough because the stylesheet might have + // set them already and we need to override that. + tocContainer.style.width = "fit-content"; + tocCollapsible.style.width = "fit-content"; + tocCollapsible.style.height = "fit-content"; + + // Apparently, this call is necessary so that the CSS properties set above take effect. + requestAnimationFrame(() => { + // These properties apparently round to the nearest integer but rounding down would make + // them wrap text differently than when autosizing. Add a pixel because it's better to wrap + // too little than to wrap too much, + let titleWidth = tocTitle.offsetWidth + 1; + let containerWidth = tocContainer.offsetWidth + 1; + let collapsibleWidth = tocCollapsible.offsetWidth + 1; + let collapsibleHeight = tocCollapsible.offsetHeight + 1; + + // Make sure the TOC width cannot shrink under that of the title. Sadly, + // calc() does not have min() / max() operators. + let clampedCollapsibleWidth = Math.max(collapsibleWidth, titleWidth); + + tocContainer.style.setProperty("--measured-title-width", titleWidth + "px"); + tocContainer.style.setProperty("--measured-expanded-width", clampedCollapsibleWidth + "px"); + tocCollapsible.style.setProperty("--measured-expanded-width", clampedCollapsibleWidth + "px"); + tocCollapsible.style.setProperty("--measured-height", collapsibleHeight + "px"); + + tocContainer.style.width = oldContainerWidth; + tocCollapsible.style.width = oldCollapsibleWidth; + tocCollapsible.style.height = oldCollapsibleHeight; + }); +} + +function measureToc() { + // Chrome sometimes does not finish layout when the resize event handler is called, so wait a + // bit before recalculating sizes. This does not usually result in weird visual effects because + // it's really hard to resize a window while hovering over the table of contents so we can + // assume that it's closed when the measurement is running. + setTimeout(sizeFinal, 200); +} + +window.addEventListener("load", measureToc); +window.addEventListener("resize", measureToc); From 1480cb794c26f9ca2660e38a2ba92e3a0533603d Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 12:39:17 +0000 Subject: [PATCH 02/19] Limit the frequency of size recalculations during resizing. --- static/js/toc.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/static/js/toc.js b/static/js/toc.js index 41f2665a..fb15fc58 100644 --- a/static/js/toc.js +++ b/static/js/toc.js @@ -1,6 +1,6 @@ "use strict"; -function sizeFinal() { +function measureToc() { let tocContainer = document.getElementById("tocContainer"); let tocTitle = document.getElementById("tocTitle"); let tocCollapsible = document.getElementById("tocCollapsible"); @@ -41,13 +41,17 @@ function sizeFinal() { }); } -function measureToc() { +let resizeTimeout = null; +function measureTocAfterResize() { // Chrome sometimes does not finish layout when the resize event handler is called, so wait a // bit before recalculating sizes. This does not usually result in weird visual effects because // it's really hard to resize a window while hovering over the table of contents so we can // assume that it's closed when the measurement is running. - setTimeout(sizeFinal, 200); + if (resizeTimeout != null) { + clearTimeout(resizeTimeout); + } + resizeTimeout = setTimeout(measureToc, 200); } window.addEventListener("load", measureToc); -window.addEventListener("resize", measureToc); +window.addEventListener("resize", measureTocAfterResize); From 0750974a0a81fee0a6cb3300bd8adca2ee48ea4d Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 14:41:01 +0000 Subject: [PATCH 03/19] Fix scrolling bug on Firefox 98.2.0 on Android. Why this works is a mystery, but it sure does. --- static/js/toc.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/static/js/toc.js b/static/js/toc.js index fb15fc58..7229c045 100644 --- a/static/js/toc.js +++ b/static/js/toc.js @@ -1,17 +1,23 @@ "use strict"; +let oldWidth = -1, oldHeight = -1; + function measureToc() { + if (window.innerWidth === oldWidth && window.innerHeight === oldHeight) { + // In addition to being a bit of optimization, this clause somehow prevents triggering a + // bug in Firefox 98.2.0 on Android that makes the stickiness of the table of contents + // wonky after scrolling to the bottom, then scrolling up. + return; + } + + oldWidth = window.innerWidth; + oldHeight = window.innerHeight; + let tocContainer = document.getElementById("tocContainer"); let tocTitle = document.getElementById("tocTitle"); let tocCollapsible = document.getElementById("tocCollapsible"); - let oldContainerWidth = tocContainer.style.width; - let oldCollapsibleWidth = tocCollapsible.style.width; - let oldCollapsibleHeight = tocCollapsible.style.height; - // Set relevant elements to automatic sizing. - // Setting these properties to the empty string is not enough because the stylesheet might have - // set them already and we need to override that. tocContainer.style.width = "fit-content"; tocCollapsible.style.width = "fit-content"; tocCollapsible.style.height = "fit-content"; @@ -22,7 +28,6 @@ function measureToc() { // them wrap text differently than when autosizing. Add a pixel because it's better to wrap // too little than to wrap too much, let titleWidth = tocTitle.offsetWidth + 1; - let containerWidth = tocContainer.offsetWidth + 1; let collapsibleWidth = tocCollapsible.offsetWidth + 1; let collapsibleHeight = tocCollapsible.offsetHeight + 1; @@ -35,13 +40,14 @@ function measureToc() { tocCollapsible.style.setProperty("--measured-expanded-width", clampedCollapsibleWidth + "px"); tocCollapsible.style.setProperty("--measured-height", collapsibleHeight + "px"); - tocContainer.style.width = oldContainerWidth; - tocCollapsible.style.width = oldCollapsibleWidth; - tocCollapsible.style.height = oldCollapsibleHeight; + tocContainer.style.removeProperty("width"); + tocCollapsible.style.removeProperty("width"); + tocCollapsible.style.removeProperty("height"); }); } let resizeTimeout = null; + function measureTocAfterResize() { // Chrome sometimes does not finish layout when the resize event handler is called, so wait a // bit before recalculating sizes. This does not usually result in weird visual effects because From 94c59bd1bea1abbb968f082d08663dbddeb37a06 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 22:01:52 +0000 Subject: [PATCH 04/19] Add space between items in the table of contents. --- assets/scss/tale/_toc.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index 4240c366..6492ba74 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -77,5 +77,13 @@ aside.toc { nav#TableOfContents ul { list-style-type: none; - padding-inline-start: 0px; + padding-inline-start: 1rem; +} + +nav#TableOfContents > ul { + padding-inline-start: 0; +} + +nav#TableOfContents li { + margin-top: 0.4rem; } From 55e6f531e323eca3f05f0e7def6d1b7e0ef4503d Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 08:31:33 +0000 Subject: [PATCH 05/19] Dark mode: initial checkin of the skeleton. That means: a little HTML template, a JS file plus some CSS to make the icon appear roughly properly. --- layouts/partials/darkmode.html | 4 ++++ static/js/darkmode.js | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 layouts/partials/darkmode.html create mode 100644 static/js/darkmode.js diff --git a/layouts/partials/darkmode.html b/layouts/partials/darkmode.html new file mode 100644 index 00000000..3cfe7af2 --- /dev/null +++ b/layouts/partials/darkmode.html @@ -0,0 +1,4 @@ + +
+ ◐ +
diff --git a/static/js/darkmode.js b/static/js/darkmode.js new file mode 100644 index 00000000..235d424a --- /dev/null +++ b/static/js/darkmode.js @@ -0,0 +1,12 @@ +function initDarkMode() { + let isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + // Apparently if the background color is changed here, the previous + // color (in the HTML page) doesn't appear even briefly +} + +function toggleDarkMode() { + console.log("toggle"); +} + +window.addEventListener("load", initDarkMode); From d98f78fce4b0d87f4ceda5755846a01e4ed2f9c1 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 08:46:35 +0000 Subject: [PATCH 06/19] Replace color references with CSS variables. Except in _syntax.scss. That will be annoying. Eventually, I I should also figure out what that file is for. --- assets/scss/tale/_base.scss | 12 ++++++------ assets/scss/tale/_catalogue.scss | 10 +++++----- assets/scss/tale/_code.scss | 8 ++++---- assets/scss/tale/_layout.scss | 26 +++++++++++++++++++++----- assets/scss/tale/_pagination.scss | 8 ++++---- assets/scss/tale/_post.scss | 8 ++++---- assets/scss/tale/_toc.scss | 2 +- assets/scss/tale/_variables.scss | 27 ++++++++++++++++----------- layouts/_default/baseof.html | 1 + 9 files changed, 62 insertions(+), 40 deletions(-) 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..6308d0ea 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; @@ -32,7 +32,7 @@ &-line { @include transition(all .3s ease-out); - border-top: .2rem solid $default-shade; + 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..6fdba12e 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; @@ -24,7 +24,7 @@ pre code { } .highlight { - background-color: $grey-4; + background-color: var(--grey-4); border-radius: 3px; line-height: 1.4; margin: 0 0 1rem; @@ -36,7 +36,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..210216c5 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -1,3 +1,7 @@ +body { + --test-var: #ff0; +} + .container { margin: 0 auto; max-width: 800px; @@ -17,8 +21,20 @@ footer, width: 80%; } +#darkModeToggle { + background: var(--test-var); + float: right; + position: sticky; + top: 2rem; + margin-right: 2rem; + margin-top: 2rem; + font-size: 2rem; + + cursor: default; +} + .nav { - box-shadow: 0 2px 2px -2px $shadow-color; + box-shadow: 0 2px 2px -2px var(--shadow-color); overflow: auto; &-container { @@ -29,7 +45,7 @@ footer, &-title { @include transition(all .2s ease-out); - color: $default-color; + color: var(--default-color); display: inline-block; margin: 0; padding-right: .2rem; @@ -49,7 +65,7 @@ footer, li { @include transition(all .2s ease-out); - color: $default-color; + color: var(--default-color); display: inline-block; opacity: .6; padding: 0 2rem 0 0; @@ -65,7 +81,7 @@ footer, } a { - color: $default-color; + color: var(--default-color); font-family: $sans-serif; } } @@ -90,7 +106,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..a41b447b 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; + color: var(--default-color); font-family: $sans-serif; font-size: 1.1rem; opacity: .6; @@ -24,7 +24,7 @@ .arrow { @include transition(all .3s ease-out); - color: $default-color; + 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..a5c06481 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -34,7 +34,7 @@ aside.toc { } #tocContainer { - background: $grey-3; + background: var(--grey-3); border-radius: 1rem; margin: 2rem; padding: 1rem; diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 92d1114d..15d79624 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -1,15 +1,20 @@ // 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; + +body { + // The idea is that these are going to be changed in dark mode + --default-color: #555; + --background-color: #fff; + --default-share: #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; +} // Fonts $serif-primary: 'Libre Baskerville', 'Times New Roman', Times, serif; diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 866a593c..a20572e6 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" . }} From f8e59df61f1be1e51fafcd115fb5fff9cdc45b31 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 09:51:03 +0000 Subject: [PATCH 07/19] Make the dark mode button actually work. Implement nice transitions, add first version of dark color scheme, merge with other transitions properly. --- assets/scss/tale/_catalogue.scss | 2 +- assets/scss/tale/_layout.scss | 9 ++++----- assets/scss/tale/_pagination.scss | 6 +++--- assets/scss/tale/_toc.scss | 9 ++++++--- assets/scss/tale/_variables.scss | 26 +++++++++++++++++++++++--- static/js/darkmode.js | 2 +- 6 files changed, 38 insertions(+), 16 deletions(-) diff --git a/assets/scss/tale/_catalogue.scss b/assets/scss/tale/_catalogue.scss index 6308d0ea..ec065a16 100644 --- a/assets/scss/tale/_catalogue.scss +++ b/assets/scss/tale/_catalogue.scss @@ -31,7 +31,7 @@ } &-line { - @include transition(all .3s ease-out); + @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/_layout.scss b/assets/scss/tale/_layout.scss index 210216c5..879bcafb 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -1,5 +1,5 @@ -body { - --test-var: #ff0; +* { + @include transition($color-transition); } .container { @@ -22,7 +22,6 @@ footer, } #darkModeToggle { - background: var(--test-var); float: right; position: sticky; top: 2rem; @@ -44,7 +43,7 @@ footer, } &-title { - @include transition(all .2s ease-out); + @include transition($color-transition, opacity .2s ease-out); color: var(--default-color); display: inline-block; margin: 0; @@ -64,7 +63,7 @@ footer, } li { - @include transition(all .2s ease-out); + @include transition($color-transition, opacity .2s ease-out); color: var(--default-color); display: inline-block; opacity: .6; diff --git a/assets/scss/tale/_pagination.scss b/assets/scss/tale/_pagination.scss index a41b447b..8b391b5d 100644 --- a/assets/scss/tale/_pagination.scss +++ b/assets/scss/tale/_pagination.scss @@ -11,7 +11,7 @@ } .top { - @include transition(all .3s ease-out); + @include transition($color-transition, opacity .3s ease-out); color: var(--default-color); font-family: $sans-serif; font-size: 1.1rem; @@ -23,8 +23,8 @@ } .arrow { - @include transition(all .3s ease-out); - color: (var--default-color); + @include transition($color-transition, opacity .3s ease-out); + color: var(--default-color); position: absolute; &:hover, diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index a5c06481..af17c762 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -34,7 +34,7 @@ aside.toc { } #tocContainer { - background: var(--grey-3); + background-color: var(--grey-3); border-radius: 1rem; margin: 2rem; padding: 1rem; @@ -54,7 +54,10 @@ 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 { @@ -72,7 +75,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 15d79624..8da9212a 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -1,10 +1,15 @@ // Colors -body { - // The idea is that these are going to be changed in dark mode +$color-transition: + background-color .5s ease-out, + color .5s ease-out, + border-color .5s ease-out, + box-shadow .5s ease-out; + +body:not(.dark) { --default-color: #555; --background-color: #fff; - --default-share: #353535; + --default-shade: #353535; --default-tint: #aaa; --grey-1: #979797; --grey-2: #e5e5e5; @@ -16,6 +21,21 @@ body { --code-color: #bf616a; } +body.dark { + --default-color: #888; + --background-color: #000; + --default-shade: #aaa; + --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); // TODO + --code-color: #a3434c; +} + // Fonts $serif-primary: 'Libre Baskerville', 'Times New Roman', Times, serif; $serif-secondary: Palatino, 'Palatino LT STD', 'Palatino Linotype', 'Book Antiqua', 'Georgia', serif; diff --git a/static/js/darkmode.js b/static/js/darkmode.js index 235d424a..34674d2a 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -6,7 +6,7 @@ function initDarkMode() { } function toggleDarkMode() { - console.log("toggle"); + document.body.classList.toggle("dark"); } window.addEventListener("load", initDarkMode); From e82f0868e6f82f91574c62525441d71354a7649c Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 11:11:27 +0000 Subject: [PATCH 08/19] Made the default scheme be the OS default. Also a few minor tweaks, --- assets/scss/tale/_layout.scss | 4 ++++ assets/scss/tale/_toc.scss | 2 +- assets/scss/tale/_variables.scss | 21 ++++++++++++++------- static/js/darkmode.js | 18 ++++++++++++++---- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index 879bcafb..c8392140 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -2,6 +2,10 @@ @include transition($color-transition); } +:root { + --color-scheme-transition-time: 0s; +} + .container { margin: 0 auto; max-width: 800px; diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index af17c762..00726115 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -61,7 +61,7 @@ aside.toc { } #tocContainer > div { - border-left: 0.4rem solid black; + border-left: 0.4rem solid var(--default-shade); padding-left: 1rem; } diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 8da9212a..3bdf3032 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -1,12 +1,19 @@ // Colors $color-transition: - background-color .5s ease-out, - color .5s ease-out, - border-color .5s ease-out, - box-shadow .5s ease-out; + // We indirect the transition time through a variable whose default value + // is zero so that the color scheme change on page load happens immediately. + // Then, after the desired scheme is determined, the variable is changed to + // a non-zero value in JS. + background-color var(--color-scheme-transition-time) ease-out, + color var(--color-scheme-transition-time) ease-out, + border-color var(--color-scheme-transition-time) ease-out, + box-shadow var(--color-scheme-transition-time) ease-out; -body:not(.dark) { + +// Make the default be light mode. That way, the more complicated case is the +// default and thus bugs are discovered earlier. +body.light { --default-color: #555; --background-color: #fff; --default-shade: #353535; @@ -21,10 +28,10 @@ body:not(.dark) { --code-color: #bf616a; } -body.dark { +body:not(.light) { --default-color: #888; --background-color: #000; - --default-shade: #aaa; + --default-shade: #989898; --default-tint: #555; --grey-1: #606060; --grey-2: #404040; diff --git a/static/js/darkmode.js b/static/js/darkmode.js index 34674d2a..c63d2c5b 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -1,12 +1,22 @@ function initDarkMode() { let isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - // Apparently if the background color is changed here, the previous - // color (in the HTML page) doesn't appear even briefly + if (!isDark) { + toggleDarkMode(); + } + + // This is needed because without the requestAnimationFrame() call, the change below takes + // effect immediately, even on the transition on the initial page load, which is not + // desirable. + requestAnimationFrame(() => { + document.documentElement.style.setProperty("--color-scheme-transition-time", ".4s"); + }); } function toggleDarkMode() { - document.body.classList.toggle("dark"); + document.body.classList.toggle("light"); } -window.addEventListener("load", initDarkMode); +// Doing this in an onload handler would let the initial color appear for a +// split second. +initDarkMode(); From 6bda5c61987fb946ec690fb92781afae19310a71 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 11:37:12 +0000 Subject: [PATCH 09/19] Persist dark mode setting to local storage. --- static/js/darkmode.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/static/js/darkmode.js b/static/js/darkmode.js index c63d2c5b..ea6a1795 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -1,5 +1,14 @@ function initDarkMode() { - let isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + let storedScheme = localStorage.getItem("color-scheme"); + let isDark; + + if (storedScheme === "dark") { + isDark = true; + } else if (storedScheme === "light") { + isDark = false; + } else { + isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + } if (!isDark) { toggleDarkMode(); @@ -15,6 +24,9 @@ function initDarkMode() { function toggleDarkMode() { document.body.classList.toggle("light"); + let isDark = !document.body.classList.contains("light"); + + localStorage.setItem("color-scheme", isDark ? "dark" : "light"); } // Doing this in an onload handler would let the initial color appear for a From 9b39ac22a7b9c5ab08aa4b5a4c84bf74c64d4ffc Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 11:53:06 +0000 Subject: [PATCH 10/19] Fix initial color flash. Hopefully. --- static/js/darkmode.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/static/js/darkmode.js b/static/js/darkmode.js index ea6a1795..6cb6ae02 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -14,12 +14,14 @@ function initDarkMode() { toggleDarkMode(); } - // This is needed because without the requestAnimationFrame() call, the change below takes - // effect immediately, even on the transition on the initial page load, which is not - // desirable. - requestAnimationFrame(() => { + // This is needed because without the setTimeout() call, the change below takes effect + // immediately, even on the transition on the initial page load, which is not desirable. + // + // One might think that this is exactly what requestAnimationFrame() is for, but alas, that only + // works about 90% of the time (on Chrome 99) + setTimeout(() => { document.documentElement.style.setProperty("--color-scheme-transition-time", ".4s"); - }); + }, 50); } function toggleDarkMode() { From 9fc89d42ed16519a46261ed3a9f7d7ee22414ca7 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 12:38:33 +0000 Subject: [PATCH 11/19] Add strict mode to darkmode.js . --- static/js/darkmode.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/js/darkmode.js b/static/js/darkmode.js index 6cb6ae02..7593eb0c 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -1,3 +1,5 @@ +"use strict"; + function initDarkMode() { let storedScheme = localStorage.getItem("color-scheme"); let isDark; From 6c23745d38fcc42b753473f0c87f2909fb0e1169 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 13:00:23 +0000 Subject: [PATCH 12/19] Dial down the opacity of code in dark mode. This is ugly, but the RGB values are generated server side so some workaround had to be found. --- assets/scss/tale/_code.scss | 6 ++++++ assets/scss/tale/_variables.scss | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/scss/tale/_code.scss b/assets/scss/tale/_code.scss index 6fdba12e..41e31550 100644 --- a/assets/scss/tale/_code.scss +++ b/assets/scss/tale/_code.scss @@ -23,6 +23,12 @@ 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. + opacity: var(--code-opacity); +} .highlight { background-color: var(--grey-4); border-radius: 3px; diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 3bdf3032..aacddb32 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -26,6 +26,7 @@ body.light { --blue: #4a9ae1; --shadow-color: rgba(0, 0, 0, .2); --code-color: #bf616a; + --code-opacity: 1.0; } body:not(.light) { @@ -39,8 +40,9 @@ body:not(.light) { --grey-4: #181818; --white: #fff; --blue: #1d6baf; - --shadow-color: rgba(0, 0, 0, .2); // TODO + --shadow-color: rgba(0, 0, 0, .2); --code-color: #a3434c; + --code-opacity: 0.7; } // Fonts From 3d3403be28d7624db7fafaca90f396119a282bc3 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 13:04:59 +0000 Subject: [PATCH 13/19] Do not save darkness in local storage by default. Only when the toggle button is actually clicked. --- static/js/darkmode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/darkmode.js b/static/js/darkmode.js index 7593eb0c..45f9dd4b 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -13,7 +13,7 @@ function initDarkMode() { } if (!isDark) { - toggleDarkMode(); + document.body.classList.toggle("light"); } // This is needed because without the setTimeout() call, the change below takes effect From 786847cbfcebd19054678b002189857cc28a95f4 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 13:21:51 +0000 Subject: [PATCH 14/19] Set dark mode before the element is created. This makes the default background color not flash for a split second after the page is loaded. --- assets/scss/tale/_layout.scss | 4 ---- assets/scss/tale/_variables.scss | 19 +++++++++---------- layouts/partials/darkmode.html | 1 - layouts/partials/head.html | 2 ++ static/js/darkmode.js | 17 +++++------------ 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index c8392140..879bcafb 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -2,10 +2,6 @@ @include transition($color-transition); } -:root { - --color-scheme-transition-time: 0s; -} - .container { margin: 0 auto; max-width: 800px; diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index aacddb32..7d560902 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -1,19 +1,18 @@ // Colors +// 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: - // We indirect the transition time through a variable whose default value - // is zero so that the color scheme change on page load happens immediately. - // Then, after the desired scheme is determined, the variable is changed to - // a non-zero value in JS. - background-color var(--color-scheme-transition-time) ease-out, - color var(--color-scheme-transition-time) ease-out, - border-color var(--color-scheme-transition-time) ease-out, - box-shadow var(--color-scheme-transition-time) ease-out; + background-color .4s ease-out, + color .4s ease-out, + border-color .4s ease-out, + box-shadow .4s ease-out; // Make the default be light mode. That way, the more complicated case is the // default and thus bugs are discovered earlier. -body.light { +:root.light { --default-color: #555; --background-color: #fff; --default-shade: #353535; @@ -29,7 +28,7 @@ body.light { --code-opacity: 1.0; } -body:not(.light) { +:root:not(.light) { --default-color: #888; --background-color: #000; --default-shade: #989898; diff --git a/layouts/partials/darkmode.html b/layouts/partials/darkmode.html index 3cfe7af2..1016b536 100644 --- a/layouts/partials/darkmode.html +++ b/layouts/partials/darkmode.html @@ -1,4 +1,3 @@ -
diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 81e8bf4a..74b54f8a 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 index 45f9dd4b..35c32e76 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -13,22 +13,15 @@ function initDarkMode() { } if (!isDark) { - document.body.classList.toggle("light"); + // When this is executed in the element, the specified + // transition apparently does not take effect. + document.documentElement.classList.toggle("light"); } - - // This is needed because without the setTimeout() call, the change below takes effect - // immediately, even on the transition on the initial page load, which is not desirable. - // - // One might think that this is exactly what requestAnimationFrame() is for, but alas, that only - // works about 90% of the time (on Chrome 99) - setTimeout(() => { - document.documentElement.style.setProperty("--color-scheme-transition-time", ".4s"); - }, 50); } function toggleDarkMode() { - document.body.classList.toggle("light"); - let isDark = !document.body.classList.contains("light"); + document.documentElement.classList.toggle("light"); + let isDark = !document.documentElement.classList.contains("light"); localStorage.setItem("color-scheme", isDark ? "dark" : "light"); } From 83c1ecbf298915f7bdedb809e6335b5409d3ca61 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 13:24:41 +0000 Subject: [PATCH 15/19] Cosmetic changes to the code. --- assets/scss/tale/_toc.scss | 2 +- layouts/_default/baseof.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index 00726115..4fb1eaf6 100644 --- a/assets/scss/tale/_toc.scss +++ b/assets/scss/tale/_toc.scss @@ -57,7 +57,7 @@ aside.toc { @include transition( $color-transition, width .1s ease-out, - height .1s ease-out); + height .1s ease-out); } #tocContainer > div { diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index a20572e6..4bf1dc87 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -3,7 +3,7 @@ {{ partial "head.html" . }} {{ partial "header.html" . }} - {{ partial "darkmode.html" . }} + {{ partial "darkmode.html" . }} {{ block "main" . }}{{ end }} {{ partial "footer.html" . }} From d6166d5a3d0b8177f2c0b9fa210fb50a8baadf77 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 14:45:03 +0000 Subject: [PATCH 16/19] Change cursor on dark mode toggle to indicate clickability. --- assets/scss/tale/_layout.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scss/tale/_layout.scss b/assets/scss/tale/_layout.scss index 879bcafb..c459510e 100644 --- a/assets/scss/tale/_layout.scss +++ b/assets/scss/tale/_layout.scss @@ -29,7 +29,7 @@ footer, margin-top: 2rem; font-size: 2rem; - cursor: default; + cursor: pointer; } .nav { From a1c5f33dadbbe88fbfb997724e62cfc0fedb7e30 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Fri, 25 Mar 2022 22:18:29 +0000 Subject: [PATCH 17/19] Make table of contents hide code blocks. The relationship of `z-index:` and `filter:` is complicated. --- assets/scss/tale/_code.scss | 2 +- assets/scss/tale/_toc.scss | 4 ++++ assets/scss/tale/_variables.scss | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/scss/tale/_code.scss b/assets/scss/tale/_code.scss index 41e31550..6d97161d 100644 --- a/assets/scss/tale/_code.scss +++ b/assets/scss/tale/_code.scss @@ -27,7 +27,7 @@ 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. - opacity: var(--code-opacity); + filter: var(--code-filter); } .highlight { background-color: var(--grey-4); diff --git a/assets/scss/tale/_toc.scss b/assets/scss/tale/_toc.scss index 4fb1eaf6..ed45c821 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 { diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index 7d560902..cb54f409 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -25,7 +25,7 @@ $color-transition: --blue: #4a9ae1; --shadow-color: rgba(0, 0, 0, .2); --code-color: #bf616a; - --code-opacity: 1.0; + --code-filter: ; } :root:not(.light) { @@ -41,7 +41,7 @@ $color-transition: --blue: #1d6baf; --shadow-color: rgba(0, 0, 0, .2); --code-color: #a3434c; - --code-opacity: 0.7; + --code-filter: brightness(0.7); } // Fonts From 1cddca97b4a94493c23350dc9357db26239fc218 Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Sat, 26 Mar 2022 12:34:16 +0000 Subject: [PATCH 18/19] Tweak syntax highlighting in dark mode. --- assets/scss/tale/_variables.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/scss/tale/_variables.scss b/assets/scss/tale/_variables.scss index cb54f409..9915b5c8 100644 --- a/assets/scss/tale/_variables.scss +++ b/assets/scss/tale/_variables.scss @@ -10,7 +10,7 @@ $color-transition: box-shadow .4s ease-out; -// Make the default be light mode. That way, the more complicated case is the +// 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; @@ -41,7 +41,7 @@ $color-transition: --blue: #1d6baf; --shadow-color: rgba(0, 0, 0, .2); --code-color: #a3434c; - --code-filter: brightness(0.7); + --code-filter: contrast(0.4) brightness(0.9); } // Fonts From 4cc865d8d002509bdb67e4c780ac0eaffd7b3c9d Mon Sep 17 00:00:00 2001 From: "Lukacs T. Berki" Date: Sat, 26 Mar 2022 19:43:42 +0000 Subject: [PATCH 19/19] Handle changes to the OS-level dark mode setting. This required a little refactoring of the code, but it hopefully became more robust this way. --- static/js/darkmode.js | 66 ++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/static/js/darkmode.js b/static/js/darkmode.js index 35c32e76..8161f9a5 100644 --- a/static/js/darkmode.js +++ b/static/js/darkmode.js @@ -1,29 +1,63 @@ "use strict"; -function initDarkMode() { - let storedScheme = localStorage.getItem("color-scheme"); - let isDark; +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; - if (storedScheme === "dark") { - isDark = true; - } else if (storedScheme === "light") { - isDark = false; + // Only store the preference if it's not the same as the OS one. + if (desiredScheme === osScheme) { + localStorage.removeItem(LOCALSTORAGE_KEY); } else { - isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + localStorage.setItem(LOCALSTORAGE_KEY, desiredScheme ? "dark" : "light"); } - if (!isDark) { - // When this is executed in the element, the specified - // transition apparently does not take effect. - document.documentElement.classList.toggle("light"); + if (desiredScheme) { + document.documentElement.classList.remove(LIGHT_CLASS); + } else { + document.documentElement.classList.add(LIGHT_CLASS); } } -function toggleDarkMode() { - document.documentElement.classList.toggle("light"); - let isDark = !document.documentElement.classList.contains("light"); +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); - localStorage.setItem("color-scheme", isDark ? "dark" : "light"); + // 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