Skip to content

Commit d7c11cc

Browse files
authored
Change from nightMode to theme (#1532)
Instead of a boolean theme (dark or not), change to a generic 'theme' setting. This keeps backwards-compatibility from older settings. The new themes are 'dark' and 'light' and 'system'. 'system' will allow for the user's OS to apply darkMode automatically depending on their system settings. Null and 'system' are treated as the same. 'dark' and 'light' will ignore OS settings. This also introduces reactivity from the OS's changing `prefers-color-scheme`, for example if your system is set to go into dark mode at sunrise, then the browser will switch to `prefers-color-scheme: dark`. Before this commit, the theme would not react to this change until page reload. Now, it will watch for media changes and apply the dark theme if appropriate.
1 parent 3034238 commit d7c11cc

18 files changed

+112
-67
lines changed

assets/js/entry/html.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { initialize as initSidebarContent } from '../sidebar/sidebar-list'
55
import { initialize as initSidebarSearch } from '../sidebar/sidebar-search'
66
import { initialize as initVersions } from '../sidebar/sidebar-version-select'
77
import { initialize as initSearchPage } from '../search-page'
8-
import { initialize as initNightMode } from '../night'
8+
import { initialize as initTheme } from '../theme'
99
import { initialize as initMakeup } from '../makeup'
1010
import { initialize as initModal } from '../modal'
1111
import { initialize as initKeyboardShortcuts } from '../keyboard-shortcuts'
@@ -16,7 +16,7 @@ import { initialize as initCopyButton } from '../copy-button'
1616
import { initialize as initSettings } from '../settings'
1717

1818
onDocumentReady(() => {
19-
initNightMode()
19+
initTheme()
2020
initSidebarDrawer()
2121
initSidebarContent()
2222
initSidebarSearch()

assets/js/handlebars/templates/settings-modal-body.handlebars

+9-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
<div id="settings-content">
33
<label class="switch-button-container">
44
<div>
5-
<span>Night mode</span>
6-
<p>Use the documentation UI in a dark theme</p>
5+
<span>Theme</span>
6+
<p>Use the documentation UI in a theme.</p>
77
</div>
8-
<div class="switch-button">
9-
<input class="switch-button__checkbox" type="checkbox" name="night_mode" />
10-
<div class="switch-button__bg"></div>
8+
<div>
9+
<select name="theme" class="settings-select">
10+
<option disabled>Theme</option>
11+
<option value="dark">Dark</option>
12+
<option value="light">Light</option>
13+
<option value="system">System</option>
14+
</select>
1115
</div>
1216
</label>
1317
<label class="switch-button-container">

assets/js/keyboard-shortcuts.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { qs } from './helpers'
22
import { openSidebar, toggleSidebar } from './sidebar/sidebar-drawer'
33
import { focusSearchInput } from './sidebar/sidebar-search'
4-
import { toggleNightMode } from './night'
4+
import { cycleTheme } from './theme'
55
import { openQuickSwitchModal } from './quick-switch'
66
import { closeModal, isModalOpen } from './modal'
77
import { openSettingsModal } from './settings'
@@ -16,8 +16,8 @@ export const keyboardShortcuts = [
1616
},
1717
{
1818
key: 'n',
19-
description: 'Toggle night mode',
20-
action: toggleNightMode
19+
description: 'Cycle themes',
20+
action: cycleTheme
2121
},
2222
{
2323
key: 's',

assets/js/night.js

-29
This file was deleted.

assets/js/settings-store.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ const SETTINGS_KEY = 'ex_doc:settings'
33
const DEFAULT_SETTINGS = {
44
// Whether to show tooltips on function/module links
55
tooltips: true,
6-
// Night mode preference, null if never explicitly overridden by the user
7-
nightMode: null,
6+
// Theme preference, null if never explicitly overridden by the user
7+
theme: null,
88
// Livebook URL to point the badges directly to
99
livebookUrl: null
1010
}
@@ -68,10 +68,8 @@ class SettingsStore {
6868

6969
_storeSettings () {
7070
try {
71-
const json = JSON.stringify(this._settings)
72-
localStorage.setItem(SETTINGS_KEY, json)
73-
7471
this._storeSettingsLegacy()
72+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(this._settings))
7573
} catch (error) {
7674
console.error(`Failed to persist settings: ${error}`)
7775
}
@@ -89,8 +87,12 @@ class SettingsStore {
8987
}
9088

9189
const nightMode = localStorage.getItem('night-mode')
92-
if (nightMode !== null) {
93-
this._settings = { ...this._settings, nightMode: JSON.parse(nightMode) }
90+
if (nightMode === 'true') {
91+
this._settings = { ...this._settings, nightMode: true }
92+
}
93+
94+
if (this._settings.nightMode === true) {
95+
this._settings = { ...this._settings, theme: 'dark' }
9496
}
9597
}
9698

@@ -102,8 +104,16 @@ class SettingsStore {
102104
}
103105

104106
if (this._settings.nightMode !== null) {
105-
localStorage.setItem('night-mode', JSON.stringify(this._settings.nightMode))
107+
localStorage.setItem('night-mode', this._settings.nightMode === true ? 'true' : 'false')
108+
} else {
109+
localStorage.removeItem('night-mode')
110+
}
111+
112+
if (this._settings.theme !== null) {
113+
localStorage.setItem('night-mode', this._settings.theme === 'dark' ? 'true' : 'false')
114+
this._settings.nightMode = this._settings.theme === 'dark'
106115
} else {
116+
delete this._settings.nightMode
107117
localStorage.removeItem('night-mode')
108118
}
109119
}

assets/js/settings.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import settingsModalBodyTemplate from './handlebars/templates/settings-modal-body.handlebars'
22
import { qs, qsAll } from './helpers'
33
import { openModal } from './modal'
4-
import { shouldUseNightMode } from './night'
54
import { settingsStore } from './settings-store'
65
import { keyboardShortcuts } from './keyboard-shortcuts'
76

@@ -60,13 +59,13 @@ export function openSettingsModal () {
6059

6160
const modal = qs(SETTINGS_MODAL_BODY_SELECTOR)
6261

63-
const nightModeInput = modal.querySelector(`[name="night_mode"]`)
62+
const themeInput = modal.querySelector(`[name="theme"]`)
6463
const tooltipsInput = modal.querySelector(`[name="tooltips"]`)
6564
const directLivebookUrlInput = modal.querySelector(`[name="direct_livebook_url"]`)
6665
const livebookUrlInput = modal.querySelector(`[name="livebook_url"]`)
6766

6867
settingsStore.getAndSubscribe(settings => {
69-
nightModeInput.checked = shouldUseNightMode(settings)
68+
themeInput.value = settings.theme || 'system'
7069
tooltipsInput.checked = settings.tooltips
7170

7271
if (settings.livebookUrl === null) {
@@ -81,8 +80,8 @@ export function openSettingsModal () {
8180
}
8281
})
8382

84-
nightModeInput.addEventListener('change', event => {
85-
settingsStore.update({ nightMode: event.target.checked })
83+
themeInput.addEventListener('change', event => {
84+
settingsStore.update({ theme: event.target.value })
8685
})
8786

8887
tooltipsInput.addEventListener('change', event => {

assets/js/theme.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { settingsStore } from './settings-store'
2+
3+
const DARK_MODE_CLASS = 'dark'
4+
const THEMES = ['system', 'dark', 'light']
5+
6+
/**
7+
* Sets initial night mode state and registers to settings updates.
8+
*/
9+
export function initialize () {
10+
settingsStore.getAndSubscribe(settings => {
11+
document.body.classList.toggle(DARK_MODE_CLASS, shouldUseDarkMode(settings))
12+
})
13+
listenToDarkMode()
14+
}
15+
16+
/**
17+
* Cycles through themes and saves the preference.
18+
*/
19+
export function cycleTheme () {
20+
const settings = settingsStore.get()
21+
const currentTheme = settings.theme || 'system'
22+
const nextTheme = THEMES[THEMES.indexOf(currentTheme) + 1] || THEMES[0]
23+
settingsStore.update({ theme: nextTheme })
24+
}
25+
26+
function shouldUseDarkMode (settings) {
27+
// nightMode used to be true|false|null
28+
// Now it's 'dark'|'light'|'system'|null with null treated as 'system'
29+
return (settings.theme === 'dark') ||
30+
(prefersDarkColorScheme() && (settings.theme == null || settings.theme === 'system'))
31+
}
32+
33+
function prefersDarkColorScheme () {
34+
return window.matchMedia('(prefers-color-scheme: dark)').matches
35+
}
36+
37+
function listenToDarkMode () {
38+
window.matchMedia('(prefers-color-scheme: dark)').addListener(_e => {
39+
const settings = settingsStore.get()
40+
const isNight = shouldUseDarkMode(settings)
41+
if (settings.theme == null || settings.theme === 'system') {
42+
document.body.classList.toggle(DARK_MODE_CLASS, isNight)
43+
}
44+
})
45+
}

assets/less/content.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
@import './content/bottom-actions.less';
1818
}
1919

20-
body:not(.night-mode) .content-inner img[src*="#gh-dark-mode-only"] {
20+
body:not(.dark) .content-inner img[src*="#gh-dark-mode-only"] {
2121
display: none;
2222
}

assets/less/makeup.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ code.makeup {
9595
@night-comment-color: #969386;
9696

9797
/* Originally taken from: https://tmbb.github.io/makeup_demo/elixir.html#monokai */
98-
.night-mode .makeup {
98+
.dark .makeup {
9999
color: #f8f8f2;
100100

101101
.hll {background-color: #49483e}

assets/less/night/content.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode .content-inner {
1+
body.dark .content-inner {
22
background: @nightBackground;
33
color: @nightTextBody;
44
@import './content/general';

assets/less/night/keyboard-shortcuts.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode {
1+
body.dark {
22
#keyboard-shortcuts-modal {
33
background-color: rgba(0, 0, 0, 0.75);
44

assets/less/night/night.less

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
body.night-mode {
1+
body.dark {
22
background: @nightBackground;
33

4-
.night-mode-toggle .icon-theme::before {
5-
content: "\e901";
6-
}
7-
84
#search .result-id a {
95
&:visited,
106
&:active,

assets/less/night/quick-switch.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode {
1+
body.dark {
22
#quick-switch-modal-body {
33
.ri-search-2-line {
44
color: @nightMediumGray;

assets/less/night/search.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode {
1+
body.dark {
22
#search {
33
.loading div {
44
border-top-color: @mediumGray;

assets/less/night/sidebar.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode {
1+
body.dark {
22
.sidebar-closed .sidebar-button, .sidebar-button {
33
color: @lightestGray;
44
}

assets/less/night/tooltips.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
body.night-mode {
1+
body.dark {
22
#tooltip {
33
box-shadow: 0 0 10px fade(@black, 50%);
44
.tooltip-body {

assets/less/settings.less

+15
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,19 @@
109109
color: @accentMediumGray;
110110
font-size: 18px;
111111
}
112+
113+
.settings-select {
114+
cursor: pointer;
115+
position: relative;
116+
border: none;
117+
background-color: transparent;
118+
119+
option {
120+
color: initial;
121+
}
122+
123+
&:focus {
124+
outline: none;
125+
}
126+
}
112127
}

lib/ex_doc/formatter/html/templates/head_template.eex

+7-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@
2626
<script>
2727
<%# Immediately apply night mode preference to avoid a flash effect %>
2828
try {
29-
if (localStorage.getItem('night-mode') === 'true') {
30-
document.body.classList.add('night-mode');
29+
var settings = JSON.parse(localStorage.getItem('ex_doc:settings') || '{}');
30+
31+
if (settings.theme === 'dark' ||
32+
((settings.theme === 'system' || settings.theme == null) &&
33+
window.matchMedia('(prefers-color-scheme: dark)').matches)
34+
) {
35+
document.body.classList.add('dark')
3136
}
3237
} catch (error) { }
3338
</script>

0 commit comments

Comments
 (0)