Skip to content

Commit 0dc59e4

Browse files
committed
Implement dark mode toggle
1 parent e939287 commit 0dc59e4

File tree

9 files changed

+179
-4
lines changed

9 files changed

+179
-4
lines changed

app/components/header.hbs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
</div>
1515

1616
<nav local-class='nav'>
17+
<Dropdown data-test-dark-mode-menu as |dd|>
18+
<dd.Trigger local-class="dropdown-button dark-mode-toggle" data-test-dark-mode-toggle>
19+
{{svg-jar this.preferredDarkMode.svg local-class="dark-mode-icon"}}
20+
</dd.Trigger>
21+
22+
<dd.Menu local-class='dark-mode-options' as |menu|>
23+
{{#each this.colorSchemes as |colorScheme|}}
24+
<menu.Item>
25+
<button local-class='dark-mode-menu-item' type="button" {{on 'click' (fn this.setDarkMode colorScheme.mode)}}>
26+
{{colorScheme.mode}}
27+
</button>
28+
</menu.Item>
29+
{{/each}}
30+
</dd.Menu>
31+
</Dropdown>
32+
1733
<LinkTo @route="crates" @query={{hash letter=null page=1}} data-test-all-crates-link>
1834
Browse All Crates
1935
</LinkTo>
@@ -81,6 +97,22 @@
8197
</nav>
8298

8399
<div local-class='menu'>
100+
<Dropdown data-test-dark-mode-menu as |dd|>
101+
<dd.Trigger local-class="dropdown-button dark-mode-toggle" data-test-dark-mode-toggle>
102+
{{svg-jar this.preferredDarkMode.svg local-class="dark-mode-icon"}}
103+
</dd.Trigger>
104+
105+
<dd.Menu local-class='dark-mode-options' as |menu|>
106+
{{#each this.colorSchemes as |colorScheme|}}
107+
<menu.Item>
108+
<button local-class='dark-mode-menu-item' type="button" {{on 'click' (fn this.setDarkMode colorScheme.mode)}}>
109+
{{colorScheme.mode}}
110+
</button>
111+
</menu.Item>
112+
{{/each}}
113+
</dd.Menu>
114+
</Dropdown>
115+
84116
<Dropdown as |dd|>
85117
<dd.Trigger local-class="dropdown-button">
86118
Menu

app/components/header.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ export default class Header extends Component {
99
/** @type {import("../services/session").default} */
1010
@service session;
1111

12+
/** @type {import("../services/dark-mode").default} */
13+
@service darkMode;
14+
15+
colorSchemes = [
16+
{ mode: 'light', svg: 'sun' },
17+
{ mode: 'dark', svg: 'moon' },
18+
{ mode: 'system', svg: 'color-mode' },
19+
];
20+
1221
@action
1322
enableSudo() {
1423
this.session.setSudo(SUDO_SESSION_DURATION_MS);
@@ -18,4 +27,17 @@ export default class Header extends Component {
1827
disableSudo() {
1928
this.session.setSudo(0);
2029
}
30+
31+
get preferredDarkMode() {
32+
return this.colorSchemes.find(c => c.mode === this.darkMode.preferred) ?? {};
33+
}
34+
35+
get currentDarkMode() {
36+
return this.darkMode.mode;
37+
}
38+
39+
@action
40+
setDarkMode(mode) {
41+
this.darkMode.preferred = mode;
42+
}
2143
}

app/components/header.module.css

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,16 @@
101101
.nav {
102102
grid-area: nav;
103103
display: flex;
104-
align-items: stretch;
104+
align-items: center;
105105
justify-self: end;
106106

107107
@media only screen and (max-width: 900px) {
108108
display: none;
109109
}
110+
111+
& > div {
112+
display: inline-flex;
113+
}
110114
}
111115

112116
.menu {
@@ -117,6 +121,10 @@
117121
@media only screen and (max-width: 900px) {
118122
display: block;
119123
}
124+
125+
& > div {
126+
vertical-align: top;
127+
}
120128
}
121129

122130
.menu-item-with-separator {
@@ -144,6 +152,19 @@
144152
}
145153
}
146154

155+
.dark-mode-icon {
156+
width: calc(var(--space-m) * .85);
157+
height: auto;
158+
}
159+
160+
.dark-mode-toggle {
161+
border-radius: 9999px;
162+
padding: var(--space-3xs);
163+
margin-right: calc(var(--space-xs) * 2);
164+
/* hdie arrow */
165+
&.dropdown-button > span { display: none; }
166+
}
167+
147168
.login-icon {
148169
width: 1em;
149170
margin-right: var(--space-2xs);
@@ -158,12 +179,18 @@
158179
margin-right: var(--space-3xs);
159180
}
160181

161-
.current-user-links {
182+
.current-user-links,
183+
.dark-mode-options {
184+
top: 120%;
162185
left: auto;
163186
right: 0;
164187
min-width: 200px;
165188
}
166189

190+
.dark-mode-options {
191+
min-width: max-content;
192+
}
193+
167194
.dropdown-button {
168195
background: none;
169196
border: 0;
@@ -177,7 +204,8 @@
177204

178205
.login-menu-item,
179206
.logout-menu-item,
180-
.sudo-menu-item {
207+
.sudo-menu-item,
208+
.dark-mode-menu-item {
181209
composes: button-reset from '../styles/shared/buttons.module.css';
182210
cursor: pointer;
183211

@@ -199,3 +227,7 @@
199227
padding-top: var(--space-3xs);
200228
}
201229
}
230+
231+
.dark-mode-menu-item {
232+
text-transform: capitalize;
233+
}

app/routes/application.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ export default class ApplicationRoute extends Route {
1414
@service session;
1515
@service playground;
1616
@service sentry;
17+
@service darkMode;
1718

1819
async beforeModel(transition) {
1920
this.setSentryTransaction(transition);
2021
this.router.on('routeWillChange', transition => this.setSentryTransaction(transition));
2122
this.router.on('routeDidChange', transition => this.setSentryTransaction(transition));
23+
this.darkMode.setupOnce();
2224

2325
// trigger the task, but don't wait for the result here
2426
//

app/services/dark-mode.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Service from '@ember/service';
2+
import { tracked } from '@glimmer/tracking';
3+
import * as localStorage from '../utils/local-storage';
4+
5+
const LIGHT_OR_DARK = new Set(['dark', 'light']);
6+
7+
export default class DarkModeService extends Service {
8+
@tracked _preferred = localStorage.getItem('dark-mode') ?? 'system';
9+
@tracked _mode = null;
10+
11+
setupOnce() {
12+
let mode = this.preferred;
13+
const darkQuery = this._darkMQList();
14+
console.log('[setup]', mode, darkQuery.matches)
15+
if (mode === 'system') {
16+
mode = this.detectColorScheme(darkQuery);
17+
}
18+
console.log('[setup]', mode)
19+
this.mode = mode;
20+
darkQuery.addEventListener('change', e => {
21+
this.mode = this.detectColorScheme(e);
22+
});
23+
}
24+
25+
get preferred() {
26+
return this._formatDarkMode(this._preferred, () => 'system');
27+
}
28+
29+
set preferred(mode = '') {
30+
const preferred = this._formatDarkMode(mode, () => 'system');
31+
this._preferred = preferred;
32+
if (preferred === 'system') {
33+
localStorage.removeItem('dark-mode');
34+
} else {
35+
localStorage.setItem('dark-mode', preferred);
36+
}
37+
this.mode = preferred;
38+
}
39+
40+
get mode() {
41+
return this._formatDarkMode(this._mode, this.detectColorScheme);
42+
}
43+
44+
set mode(mode) {
45+
const darkMode = this._formatDarkMode(mode, this.detectColorScheme);
46+
this._mode = darkMode;
47+
if (LIGHT_OR_DARK.has(this.preferred)) {
48+
document.documentElement.dataset.darkMode = darkMode;
49+
} else {
50+
delete document.documentElement.dataset.darkMode;
51+
}
52+
}
53+
54+
_formatDarkMode(mode, defaultCb) {
55+
return LIGHT_OR_DARK.has(mode) ? mode : defaultCb?.();
56+
}
57+
58+
_darkMQList() {
59+
return window.matchMedia('(prefers-color-scheme: dark)');
60+
}
61+
62+
detectColorScheme(darkMQList) {
63+
return (darkMQList ?? this._darkMQList()).matches ? 'dark' : 'light';
64+
}
65+
}

app/styles/application.module.css

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,22 @@
7474
/* Custom pairs */
7575
--space-s-l: clamp(0.88rem, calc(0.34rem + 2.68vw), 2.25rem);
7676

77-
color-scheme: light dark;
77+
}
78+
79+
/* Allows setting dark mode manually, even at the element level. */
80+
/* Adapted from https://github.com/csstools/postcss-plugins/issues/1348#issuecomment-2030678571 */
81+
@layer theme {
82+
:root {
83+
color-scheme: light dark;
84+
}
85+
86+
[data-dark-mode="light"] {
87+
color-scheme: light;
88+
}
89+
90+
[data-dark-mode="dark"] {
91+
color-scheme: dark;
92+
}
7893
}
7994

8095
[data-theme="new-design"] {

public/assets/color-mode.svg

Lines changed: 1 addition & 0 deletions
Loading

public/assets/moon.svg

Lines changed: 3 additions & 0 deletions
Loading

public/assets/sun.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)