Skip to content

Commit 7aaa622

Browse files
committed
Add theme switcher
1 parent ae57101 commit 7aaa622

3 files changed

Lines changed: 111 additions & 1 deletion

File tree

_includes/default.njk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<link rel="stylesheet" href="/styles/basic.css" />
1515
<link rel="stylesheet" href="/styles/main.css" />
1616
<link rel="stylesheet" href="/styles/blog.css" />
17+
<script src="/scripts/theme.js"></script>
1718
</head>
1819
<!-- Google tag (gtag.js) -->
1920
<script
@@ -26,6 +27,7 @@
2627
{% for item in header %}
2728
<a href="{{ item.url }}">{{ item.title }}</a>
2829
{% endfor %}
30+
<button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
2931
</nav>
3032
<main class="{{ class }} page">{{ content | safe }}</main>
3133
</body>

scripts/theme.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
var THEME_LABELS = {
2+
auto: '⚙️ Theme (Auto)',
3+
light: '☀️ Light',
4+
dark: '🌙 Dark'
5+
};
6+
7+
var CYCLE_ORDER = ['auto', 'light', 'dark'];
8+
9+
function getPreference() {
10+
return localStorage.getItem('theme-preference') || 'auto';
11+
}
12+
13+
function applyTheme(pref) {
14+
if (pref === 'auto') {
15+
document.documentElement.removeAttribute('data-theme');
16+
} else {
17+
document.documentElement.setAttribute('data-theme', pref);
18+
}
19+
var btn = document.getElementById('theme-toggle');
20+
if (btn) {
21+
btn.textContent = THEME_LABELS[pref] || THEME_LABELS.auto;
22+
}
23+
}
24+
25+
function setPreference(value) {
26+
localStorage.setItem('theme-preference', value);
27+
applyTheme(value);
28+
}
29+
30+
function cycleTheme() {
31+
var current = getPreference();
32+
var idx = CYCLE_ORDER.indexOf(current);
33+
var next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length];
34+
setPreference(next);
35+
}
36+
37+
function setupToggle() {
38+
var btn = document.getElementById('theme-toggle');
39+
if (btn) {
40+
btn.textContent = THEME_LABELS[getPreference()] || THEME_LABELS.auto;
41+
btn.addEventListener('click', cycleTheme);
42+
}
43+
}
44+
45+
applyTheme(getPreference());
46+
47+
if (document.readyState === 'loading') {
48+
document.addEventListener('DOMContentLoaded', setupToggle);
49+
} else {
50+
setupToggle();
51+
}
52+
53+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
54+
if (getPreference() === 'auto') {
55+
applyTheme('auto');
56+
}
57+
});

styles/main.css

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@
2525
}
2626
}
2727

28+
/* Light mode override */
29+
html[data-theme="light"] {
30+
--body-bg: #f4f4f4;
31+
--body-color: #333333;
32+
--link-color: #0066b2;
33+
--visited-link-color: #7f4982;
34+
--nav-bg: #ffffff;
35+
}
36+
37+
/* Dark mode override */
38+
html[data-theme="dark"] {
39+
--body-bg: #1e1e1e;
40+
--body-color: #eaeaea;
41+
--link-color: #3399ff;
42+
--visited-link-color: #b066b0;
43+
--nav-bg: #2a2a2a;
44+
}
45+
2846
/* Mobile-first responsive design */
2947
body {
3048
padding: 0 var(--page-padding);
@@ -45,6 +63,14 @@ blockquote {
4563
}
4664
}
4765

66+
html[data-theme="light"] blockquote {
67+
background-color: rgba(0, 0, 0, 0.03);
68+
}
69+
70+
html[data-theme="dark"] blockquote {
71+
background-color: rgba(255, 255, 255, 0.05);
72+
}
73+
4874
/* Responsive heading size */
4975
h1 {
5076
line-height: 1.2;
@@ -80,14 +106,39 @@ h2 {
80106
}
81107

82108
.primary-nav {
109+
display: flex;
110+
align-items: center;
83111
padding: clamp(0.75rem, 2vw, 1rem);
84112
margin: 0 auto 2rem;
85113
max-width: 600px;
86-
text-align: left;
87114
background: var(--nav-bg);
88115
border-radius: 8px;
89116
}
90117

91118
.primary-nav a {
92119
margin-right: 1rem;
93120
}
121+
122+
#theme-toggle {
123+
margin-left: auto;
124+
flex-shrink: 0;
125+
background: transparent;
126+
border: 1px solid var(--body-color);
127+
color: var(--body-color);
128+
padding: 0.3rem 0.6rem;
129+
border-radius: 6px;
130+
cursor: pointer;
131+
font-size: 0.85rem;
132+
line-height: 1;
133+
transition: background-color 0.2s, color 0.2s;
134+
}
135+
136+
#theme-toggle:hover {
137+
background-color: var(--body-color);
138+
color: var(--nav-bg);
139+
}
140+
141+
#theme-toggle:focus-visible {
142+
outline: 2px solid var(--link-color);
143+
outline-offset: 2px;
144+
}

0 commit comments

Comments
 (0)