Skip to content

Commit f462a1d

Browse files
authored
feat(website): replace the theme dropdown with a toggle button (#484)
Signed-off-by: Anurag Yadav <anuragyadav2787@gmail.com>
1 parent ba661de commit f462a1d

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

‎website/astro.config.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export default defineConfig({
4545
social: [
4646
{ icon: 'github', label: 'GitHub', href: 'https://github.com/codercops/chatcops' },
4747
],
48+
components: {
49+
ThemeSelect: './src/components/ThemeToggle.astro',
50+
},
4851
head: [
4952
{
5053
tag: 'script',
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
import { Icon } from '@astrojs/starlight/components';
3+
---
4+
5+
<theme-toggle class="theme-toggle">
6+
<button
7+
type="button"
8+
class="theme-toggle-btn"
9+
aria-label="Switch to dark theme"
10+
title="Switch to dark theme"
11+
>
12+
<Icon name="sun" size="1.2rem" class="sun" />
13+
<Icon name="moon" size="1.2rem" class="moon" />
14+
</button>
15+
</theme-toggle>
16+
17+
<script is:inline>
18+
(() => {
19+
const isLight = document.documentElement.dataset.theme === 'light';
20+
const label = isLight ? 'Switch to dark theme' : 'Switch to light theme';
21+
document.querySelectorAll('.theme-toggle-btn').forEach((btn) => {
22+
btn.setAttribute('aria-label', label);
23+
btn.setAttribute('title', label);
24+
});
25+
})();
26+
</script>
27+
28+
<script>
29+
type Theme = 'auto' | 'dark' | 'light';
30+
31+
const storageKey = 'starlight-theme';
32+
33+
const parseTheme = (theme: unknown): Theme =>
34+
theme === 'auto' || theme === 'dark' || theme === 'light' ? theme : 'auto';
35+
36+
const loadTheme = (): Theme => {
37+
try {
38+
if (typeof localStorage !== 'undefined') {
39+
return parseTheme(localStorage.getItem(storageKey));
40+
}
41+
} catch {}
42+
return 'auto';
43+
};
44+
45+
const storeTheme = (theme: 'dark' | 'light'): void => {
46+
try {
47+
if (typeof localStorage !== 'undefined') {
48+
localStorage.setItem(storageKey, theme);
49+
}
50+
} catch {}
51+
};
52+
53+
const getPreferredColorScheme = (): 'dark' | 'light' =>
54+
matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
55+
56+
const getCurrentTheme = (): 'dark' | 'light' =>
57+
document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
58+
59+
function updateButtons(): void {
60+
const isLight = getCurrentTheme() === 'light';
61+
const label = isLight ? 'Switch to dark theme' : 'Switch to light theme';
62+
document.querySelectorAll<HTMLButtonElement>('.theme-toggle-btn').forEach((btn) => {
63+
btn.setAttribute('aria-label', label);
64+
btn.setAttribute('title', label);
65+
});
66+
}
67+
68+
function setTheme(theme: 'dark' | 'light'): void {
69+
document.documentElement.dataset.theme = theme;
70+
storeTheme(theme);
71+
updateButtons();
72+
}
73+
74+
// React to system color scheme changes when visitor hasn't explicitly chosen a theme
75+
matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
76+
if (loadTheme() === 'auto') {
77+
document.documentElement.dataset.theme = e.matches ? 'light' : 'dark';
78+
updateButtons();
79+
}
80+
});
81+
82+
// React to cross-tab storage changes
83+
window.addEventListener('storage', (e) => {
84+
if (e.key === storageKey) {
85+
const saved = parseTheme(e.newValue);
86+
document.documentElement.dataset.theme =
87+
saved === 'auto' ? getPreferredColorScheme() : saved;
88+
updateButtons();
89+
}
90+
});
91+
92+
// Keep buttons synced if theme attribute changes from anywhere
93+
const observer = new MutationObserver(() => {
94+
updateButtons();
95+
});
96+
observer.observe(document.documentElement, {
97+
attributes: true,
98+
attributeFilter: ['data-theme'],
99+
});
100+
101+
class ThemeToggle extends HTMLElement {
102+
constructor() {
103+
super();
104+
const button = this.querySelector<HTMLButtonElement>('.theme-toggle-btn');
105+
button?.addEventListener('click', () => {
106+
const current = getCurrentTheme();
107+
const next = current === 'light' ? 'dark' : 'light';
108+
setTheme(next);
109+
});
110+
}
111+
112+
connectedCallback() {
113+
updateButtons();
114+
}
115+
}
116+
117+
if (!customElements.get('theme-toggle')) {
118+
customElements.define('theme-toggle', ThemeToggle);
119+
}
120+
</script>
121+
122+
<style>
123+
theme-toggle {
124+
display: inline-flex;
125+
align-items: center;
126+
}
127+
128+
.theme-toggle-btn {
129+
display: inline-flex;
130+
align-items: center;
131+
justify-content: center;
132+
width: 2.25rem;
133+
height: 2.25rem;
134+
padding: 0;
135+
border: 1px solid var(--sl-color-gray-5);
136+
border-radius: 0.5rem;
137+
background-color: var(--sl-color-black);
138+
color: var(--sl-color-gray-2);
139+
cursor: pointer;
140+
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
141+
}
142+
143+
.theme-toggle-btn:hover {
144+
border-color: var(--sl-color-gray-2);
145+
color: var(--sl-color-white);
146+
background-color: var(--sl-color-hairline-light, rgba(255, 255, 255, 0.05));
147+
}
148+
149+
.theme-toggle-btn:focus:not(:focus-visible) {
150+
outline: none;
151+
}
152+
153+
.theme-toggle-btn:focus-visible {
154+
outline: 2px solid var(--sl-color-accent);
155+
outline-offset: 2px;
156+
}
157+
158+
/* Theme-based icon visibility */
159+
:global(:root[data-theme='light'] .theme-toggle .sun) {
160+
display: block;
161+
}
162+
:global(:root[data-theme='light'] .theme-toggle .moon) {
163+
display: none;
164+
}
165+
166+
:global(:root[data-theme='dark'] .theme-toggle .sun) {
167+
display: none;
168+
}
169+
:global(:root[data-theme='dark'] .theme-toggle .moon) {
170+
display: block;
171+
}
172+
173+
/* Fallbacks before data-theme attribute is set */
174+
:global(:root:not([data-theme]) .theme-toggle .sun) {
175+
display: none;
176+
}
177+
:global(:root:not([data-theme]) .theme-toggle .moon) {
178+
display: block;
179+
}
180+
181+
@media (prefers-color-scheme: light) {
182+
:global(:root:not([data-theme]) .theme-toggle .sun) {
183+
display: block;
184+
}
185+
:global(:root:not([data-theme]) .theme-toggle .moon) {
186+
display: none;
187+
}
188+
}
189+
</style>

0 commit comments

Comments
 (0)