Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/invinciblycool/lekh.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRahul Tiwari <jprrahultiwari@gmail.com>2020-04-02 15:59:13 +0300
committerRahul Tiwari <jprrahultiwari@gmail.com>2020-04-02 15:59:13 +0300
commit047bcedb336788ff71a2869b10908470aabf7676 (patch)
tree462e6ab56fd387d102a56f955ac57d101f1aead9
parent7b5de715c4ee63f6f7b904b48f53ac5be36c91c4 (diff)
Add dark mode
-rw-r--r--README.md4
-rw-r--r--images/screenshot.pngbin169775 -> 145705 bytes
-rw-r--r--images/tn.pngbin107842 -> 71368 bytes
-rw-r--r--layouts/partials/head.html2
-rw-r--r--static/js/dark.js451
5 files changed, 455 insertions, 2 deletions
diff --git a/README.md b/README.md
index 88cfd4e..db58bc0 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,7 @@ Simple, text-focussed and minimal personal portfolio theme based on https://gith
* Markdown supported
* Easy to personalize
* RSS feed
- - TODO
- - Dark mode
+* Dark mode (taken from https://www.gwern.net/ as it is.)
## Installation
@@ -79,5 +78,6 @@ Too much to rant :(
## Credits
* Thanks to [Vegard's](https://github.com/vegarsti) personal site from which the theme was heavily inspired.
+* Also to https://www.gwern.net/ for the dark mode.
Feel free to contribute and open issues.
diff --git a/images/screenshot.png b/images/screenshot.png
index 7437b49..0ce8fde 100644
--- a/images/screenshot.png
+++ b/images/screenshot.png
Binary files differ
diff --git a/images/tn.png b/images/tn.png
index e0d8d78..7fbfe7a 100644
--- a/images/tn.png
+++ b/images/tn.png
Binary files differ
diff --git a/layouts/partials/head.html b/layouts/partials/head.html
index 8df8052..7cc35fb 100644
--- a/layouts/partials/head.html
+++ b/layouts/partials/head.html
@@ -10,4 +10,6 @@
<link rel="icon" type="image/png" href="/assets/img/favicon.ico" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
+ <script src="/js/dark.js" defer=""></script>
+
</head> \ No newline at end of file
diff --git a/static/js/dark.js b/static/js/dark.js
new file mode 100644
index 0000000..cacb561
--- /dev/null
+++ b/static/js/dark.js
@@ -0,0 +1,451 @@
+// darkmode.js: Javascript library for controlling page appearance, toggling between regular white and 'dark mode'
+// Author: Said Achmiz
+// Date: 2020-03-20
+// When: Time-stamp: "2020-03-23 09:36:20 gwern"
+// license: PD
+
+/* Experimental 'dark mode': Mac OS (Safari) lets users specify via an OS widget 'dark'/'light' to make everything appear */
+/* bright-white or darker (eg for darker at evening to avoid straining eyes & disrupting circadian rhyhms); this then is */
+/* exposed by Safari as a CSS variable which can be selected on. This is also currently supported by Firefox weakly as an */
+/* about:config variable. Hypothetically, iOS in the future might use its camera or the clock to set 'dark mode' */
+/* automatically. https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme */
+/* https://webkit.org/blog/8718/new-webkit-features-in-safari-12-1/ */
+/* https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
+
+/* Because many users do not have access to a browser/OS which explicitly supports dark mode, cannot modify the browser/OS setting without undesired side-effects, wish to opt in only for specific websites, or simply forget that they turned on dark mode & dislike it, we make dark mode controllable by providing a widget at the top of the page. */
+
+/* For gwern.net, the default white-black */
+/* scheme is 'light', and it can be flipped to a 'dark' scheme fairly easily by inverting it; the main visual problem is */
+/* that blockquotes appear to become much harder to see & image-focus.js doesn't work well without additional tweaks. */
+/* Known bugs: images get inverted on zoom or hover; invert filters are slow, leading to 'janky' slow rendering on scrolling. */
+
+/****************/
+/* MISC HELPERS */
+/****************/
+
+/* Given an HTML string, creates an element from that HTML, adds it to
+ #ui-elements-container (creating the latter if it does not exist), and
+ returns the created element.
+ */
+function addUIElement(element_html) {
+ var ui_elements_container = document.querySelector("#ui-elements-container");
+ if (!ui_elements_container) {
+ ui_elements_container = document.createElement("div");
+ ui_elements_container.id = "ui-elements-container";
+ document.querySelector("body").appendChild(ui_elements_container);
+ }
+
+ ui_elements_container.insertAdjacentHTML("beforeend", element_html);
+ return ui_elements_container.lastElementChild;
+}
+
+if (typeof window.GW == "undefined")
+ window.GW = { };
+GW.temp = { };
+
+if (GW.mediaQueries == null)
+ GW.mediaQueries = { };
+GW.mediaQueries.mobileNarrow = matchMedia("(max-width: 520px)");
+GW.mediaQueries.mobileWide = matchMedia("(max-width: 900px)");
+GW.mediaQueries.mobileMax = matchMedia("(max-width: 960px)");
+GW.mediaQueries.hover = matchMedia("only screen and (hover: hover) and (pointer: fine)");
+GW.mediaQueries.systemDarkModeActive = matchMedia("(prefers-color-scheme: dark)");
+
+GW.modeOptions = [
+ [ 'auto', 'Auto', 'Set light or dark mode automatically, according to system-wide setting' ],
+ [ 'light', 'Light', 'Light mode at all times' ],
+ [ 'dark', 'Dark', 'Dark mode at all times' ]
+];
+GW.modeStyles = `
+ :root {
+ --GW-blockquote-background-color: #ddd
+ }
+ body::before,
+ body > * {
+ filter: invert(90%)
+ }
+ body::before {
+ content: '';
+ width: 100vw;
+ height: 100%;
+ position: fixed;
+ left: 0;
+ top: 0;
+ background-color: #fff;
+ z-index: -1
+ }
+ img,
+ video {
+ filter: invert(100%);
+ }
+ #markdownBody, #mode-selector button {
+ text-shadow: 0 0 0 #000
+ }
+ article > :not(#TOC) a:link {
+ text-shadow:
+ 0 0 #777,
+ .03em 0 #fff,
+ -.03em 0 #fff,
+ 0 .03em #fff,
+ 0 -.03em #fff,
+ .06em 0 #fff,
+ -.06em 0 #fff,
+ .09em 0 #fff,
+ -.09em 0 #fff,
+ .12em 0 #fff,
+ -.12em 0 #fff,
+ .15em 0 #fff,
+ -.15em 0 #fff
+ }
+ article > :not(#TOC) blockquote a:link {
+ text-shadow:
+ 0 0 #777,
+ .03em 0 var(--GW-blockquote-background-color),
+ -.03em 0 var(--GW-blockquote-background-color),
+ 0 .03em var(--GW-blockquote-background-color),
+ 0 -.03em var(--GW-blockquote-background-color),
+ .06em 0 var(--GW-blockquote-background-color),
+ -.06em 0 var(--GW-blockquote-background-color),
+ .09em 0 var(--GW-blockquote-background-color),
+ -.09em 0 var(--GW-blockquote-background-color),
+ .12em 0 var(--GW-blockquote-background-color),
+ -.12em 0 var(--GW-blockquote-background-color),
+ .15em 0 var(--GW-blockquote-background-color),
+ -.15em 0 var(--GW-blockquote-background-color)
+ }
+ #logo img {
+ filter: none;
+ }
+ #mode-selector {
+ opacity: 0.6;
+ }
+ #mode-selector:hover {
+ background-color: #fff;
+ }
+`;
+
+/****************/
+/* DEBUG OUTPUT */
+/****************/
+
+function GWLog (string) {
+ if (GW.loggingEnabled || localStorage.getItem("logging-enabled") == "true")
+ console.log(string);
+}
+
+/***********/
+/* HELPERS */
+/***********/
+
+/* Run the given function immediately if the page is already loaded, or add
+ a listener to run it as soon as the page loads.
+ */
+function doWhenPageLoaded(f) {
+ if (document.readyState == "complete")
+ f();
+ else
+ window.addEventListener("load", f);
+}
+
+/* Adds an event listener to a button (or other clickable element), attaching
+ it to both "click" and "keyup" events (for use with keyboard navigation).
+ Optionally also attaches the listener to the 'mousedown' event, making the
+ element activate on mouse down instead of mouse up.
+ */
+Element.prototype.addActivateEvent = function(func, includeMouseDown) {
+ let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) };
+ if (includeMouseDown) this.addEventListener("mousedown", ael);
+ this.addEventListener("click", ael);
+ this.addEventListener("keyup", ael);
+}
+
+/* Adds a scroll event listener to the page.
+ */
+function addScrollListener(fn, name) {
+ let wrapper = (event) => {
+ requestAnimationFrame(() => {
+ fn(event);
+ document.addEventListener("scroll", wrapper, { once: true, passive: true });
+ });
+ }
+ document.addEventListener("scroll", wrapper, { once: true, passive: true });
+
+ // Retain a reference to the scroll listener, if a name is provided.
+ if (typeof name != "undefined")
+ GW[name] = wrapper;
+}
+
+/************************/
+/* ACTIVE MEDIA QUERIES */
+/************************/
+
+/* This function provides two slightly different versions of its functionality,
+ depending on how many arguments it gets.
+
+ If one function is given (in addition to the media query and its name), it
+ is called whenever the media query changes (in either direction).
+
+ If two functions are given (in addition to the media query and its name),
+ then the first function is called whenever the media query starts matching,
+ and the second function is called whenever the media query stops matching.
+
+ If you want to call a function for a change in one direction only, pass an
+ empty closure (NOT null!) as one of the function arguments.
+
+ There is also an optional fifth argument. This should be a function to be
+ called when the active media query is canceled.
+ */
+function doWhenMatchMedia(mediaQuery, name, ifMatchesOrAlwaysDo, otherwiseDo = null, whenCanceledDo = null) {
+ if (typeof GW.mediaQueryResponders == "undefined")
+ GW.mediaQueryResponders = { };
+
+ let mediaQueryResponder = (event, canceling = false) => {
+ if (canceling) {
+ GWLog(`Canceling media query “${name}”`);
+
+ if (whenCanceledDo != null)
+ whenCanceledDo(mediaQuery);
+ } else {
+ let matches = (typeof event == "undefined") ? mediaQuery.matches : event.matches;
+
+ GWLog(`Media query “${name}” triggered (matches: ${matches ? "YES" : "NO"})`);
+
+ if (otherwiseDo == null || matches) ifMatchesOrAlwaysDo(mediaQuery);
+ else otherwiseDo(mediaQuery);
+ }
+ };
+ mediaQueryResponder();
+ mediaQuery.addListener(mediaQueryResponder);
+
+ GW.mediaQueryResponders[name] = mediaQueryResponder;
+}
+
+/* Deactivates and discards an active media query, after calling the function
+ that was passed as the whenCanceledDo parameter when the media query was
+ added.
+ */
+function cancelDoWhenMatchMedia(name) {
+ GW.mediaQueryResponders[name](null, true);
+
+ for ([ key, mediaQuery ] of Object.entries(GW.mediaQueries))
+ mediaQuery.removeListener(GW.mediaQueryResponders[name]);
+
+ GW.mediaQueryResponders[name] = null;
+}
+
+/******************/
+/* MODE SELECTION */
+/******************/
+
+function injectModeSelector() {
+ GWLog("injectModeSelector");
+
+ // Get saved mode setting (or default).
+ let currentMode = localStorage.getItem("selected-mode") || 'auto';
+
+ // Inject the mode selector widget and activate buttons.
+ let modeSelector = addUIElement(
+ "<div id='mode-selector'>" +
+ String.prototype.concat.apply("", GW.modeOptions.map(modeOption => {
+ let [ name, label, desc ] = modeOption;
+ let selected = (name == currentMode ? ' selected' : '');
+ let disabled = (name == currentMode ? ' disabled' : '');
+ return `<button type='button' class='select-mode-${name}${selected}'${disabled} tabindex='-1' data-name='${name}' title='${desc}'>${label}</button>`})) +
+ "</div>");
+
+ modeSelector.querySelectorAll("button").forEach(button => {
+ button.addActivateEvent(GW.modeSelectButtonClicked = (event) => {
+ GWLog("GW.modeSelectButtonClicked");
+
+ // Determine which setting was chosen (i.e., which button was clicked).
+ let selectedMode = event.target.dataset.name;
+
+ // Save the new setting.
+ if (selectedMode == "auto") localStorage.removeItem("selected-mode");
+ else localStorage.setItem("selected-mode", selectedMode);
+
+ // Actually change the mode.
+ setMode(selectedMode);
+ });
+ });
+
+ document.querySelector("head").insertAdjacentHTML("beforeend", `<style id='mode-selector-styles'>
+ #mode-selector {
+ position: absolute;
+ right: 3px;
+ top: 4px;
+ display: flex;
+ background-color: #fff;
+ padding: 0.125em 0.25em;
+ border: 3px solid transparent;
+ opacity: 0.3;
+ transition:
+ opacity 2s ease;
+ }
+ #mode-selector.hidden {
+ opacity: 0;
+ }
+ #mode-selector:hover {
+ transition: none;
+ opacity: 1.0;
+ border: 3px double #aaa;
+ }
+ #mode-selector button {
+ -moz-appearance: none;
+ appearance: none;
+ border: none;
+ background-color: transparent;
+ padding: 0.5em;
+ margin: 0;
+ line-height: 1;
+ font-family: Lucida Sans Unicode, Source Sans Pro, Helvetica, Trebuchet MS, sans-serif;
+ font-size: 0.75rem;
+ text-align: center;
+ color: #777;
+ position: relative;
+ }
+ #mode-selector button:hover,
+ #mode-selector button.selected {
+ box-shadow:
+ 0 2px 0 6px #fff inset,
+ 0 1px 0 6px currentColor inset;
+ }
+ #mode-selector button:not(:disabled):hover {
+ color: #000;
+ cursor: pointer;
+ }
+ #mode-selector button:not(:disabled):active {
+ transform: translateY(2px);
+ box-shadow:
+ 0 0px 0 6px #fff inset,
+ 0 -1px 0 6px currentColor inset;
+ }
+ #mode-selector button.active:not(:hover)::after {
+ content: "";
+ position: absolute;
+ bottom: 0.25em;
+ left: 0;
+ right: 0;
+ border-bottom: 1px dotted currentColor;
+ width: calc(100% - 12px);
+ margin: auto;
+ }
+ </style>`);
+
+ document.querySelector("head").insertAdjacentHTML("beforeend", `<style id='mode-styles'></style>`);
+
+ setMode(currentMode);
+
+ // We pre-query the relevant elements, so we don’t have to run queryAll on
+ // every firing of the scroll listener.
+ GW.scrollState = {
+ "lastScrollTop": window.pageYOffset || document.documentElement.scrollTop,
+ "unbrokenDownScrollDistance": 0,
+ "unbrokenUpScrollDistance": 0,
+ "modeSelector": document.querySelectorAll("#mode-selector"),
+ };
+ addScrollListener(updateModeSelectorVisibility, "updateModeSelectorVisibilityScrollListener");
+ GW.scrollState.modeSelector[0].addEventListener("mouseover", () => { showModeSelector(); });
+ doWhenMatchMedia(GW.mediaQueries.systemDarkModeActive, "updateModeSelectorStateForSystemDarkMode", () => { updateModeSelectorState(); });
+}
+
+/* Show/hide the mode selector in response to scrolling.
+
+ Called by the ‘updateModeSelectorVisibilityScrollListener’ scroll listener.
+ */
+function updateModeSelectorVisibility(event) {
+ GWLog("updateModeSelectorVisibility");
+
+ let newScrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ GW.scrollState.unbrokenDownScrollDistance = (newScrollTop > GW.scrollState.lastScrollTop) ?
+ (GW.scrollState.unbrokenDownScrollDistance + newScrollTop - GW.scrollState.lastScrollTop) :
+ 0;
+ GW.scrollState.unbrokenUpScrollDistance = (newScrollTop < GW.scrollState.lastScrollTop) ?
+ (GW.scrollState.unbrokenUpScrollDistance + GW.scrollState.lastScrollTop - newScrollTop) :
+ 0;
+ GW.scrollState.lastScrollTop = newScrollTop;
+
+ // Hide mode selector when scrolling a full page down.
+ if (GW.scrollState.unbrokenDownScrollDistance > window.innerHeight) {
+ hideModeSelector();
+ }
+
+ // On desktop, show mode selector when scrolling to top of page,
+ // or a full page up.
+ // On mobile, show mode selector on ANY scroll up.
+ if (GW.mediaQueries.mobileNarrow.matches) {
+ if (GW.scrollState.unbrokenUpScrollDistance > 0 || GW.scrollState.lastScrollTop <= 0)
+ showModeSelector();
+ } else if ( GW.scrollState.unbrokenUpScrollDistance > window.innerHeight
+ || GW.scrollState.lastScrollTop == 0) {
+ showModeSelector();
+ }
+}
+
+function hideModeSelector() {
+ GWLog("hideModeSelector");
+
+ GW.scrollState.modeSelector[0].classList.add("hidden");
+}
+
+function showModeSelector() {
+ GWLog("showModeSelector");
+
+ GW.scrollState.modeSelector[0].classList.remove("hidden");
+}
+
+/* Update the states of the mode selector buttons.
+ */
+function updateModeSelectorState() {
+ // Get saved mode setting (or default).
+ let currentMode = localStorage.getItem("selected-mode") || 'auto';
+
+ // Clear current buttons state.
+ let modeSelector = document.querySelector("#mode-selector");
+ modeSelector.childNodes.forEach(button => {
+ button.classList.remove("active", "selected");
+ button.disabled = false;
+ });
+
+ // Set the correct button to be selected.
+ modeSelector.querySelectorAll(`.select-mode-${currentMode}`).forEach(button => {
+ button.classList.add("selected");
+ button.disabled = true;
+ });
+
+ // Ensure the right button (light or dark) has the “currently active”
+ // indicator, if the current mode is ‘auto’.
+ if (currentMode == "auto") {
+ if (GW.mediaQueries.systemDarkModeActive.matches)
+ modeSelector.querySelector(".select-mode-dark").classList.add("active");
+ else
+ modeSelector.querySelector(".select-mode-light").classList.add("active");
+ }
+}
+
+/* Set specified color mode (auto, light, dark).
+ */
+function setMode(modeOption) {
+ GWLog("setMode");
+
+ // Inject the appropriate styles.
+ let modeStyles = document.querySelector("#mode-styles");
+ if (modeOption == 'auto') {
+ modeStyles.innerHTML = `@media (prefers-color-scheme:dark) {${GW.modeStyles}}`;
+ } else if (modeOption == 'dark') {
+ modeStyles.innerHTML = GW.modeStyles;
+ } else {
+ modeStyles.innerHTML = "";
+ }
+
+ // Update selector state.
+ updateModeSelectorState();
+}
+
+/******************/
+/* INITIALIZATION */
+/******************/
+
+doWhenPageLoaded(() => {
+ injectModeSelector();
+});