Production's Content-Security-Policy has no script-src, so it falls back to default-src 'self' and blocks inline scripts. The inline head script that applied the saved theme never ran on prod, leaving the page on the system theme while the selector showed the saved choice. Move it to resources/js/theme-init.js, built by esbuild alongside app.js into a minified, source-mapped file loaded as a blocking script in the head. ThemeSelector now also applies the saved colour scheme, so the page and buttons can't disagree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
2.8 KiB
JavaScript
92 lines
2.8 KiB
JavaScript
class ThemeSelector {
|
|
constructor() {
|
|
this.widget = document.querySelector('#theme-selector');
|
|
this.btnLight = this.widget.querySelector('#btn-light');
|
|
this.btnDark = this.widget.querySelector('#btn-dark');
|
|
this.status = this.widget.querySelector('#theme-status');
|
|
this.currentTheme = ThemeSelector.storedTheme();
|
|
if (this.currentTheme !== 'system') {
|
|
/* backstop in case theme-init.js didn't run, so the page always matches the buttons */
|
|
document.documentElement.style.setProperty('color-scheme', this.currentTheme);
|
|
}
|
|
this.updateButtons();
|
|
}
|
|
|
|
static storedTheme() {
|
|
try {
|
|
const theme = localStorage.getItem('theme');
|
|
return (theme === 'light' || theme === 'dark') ? theme : 'system';
|
|
} catch {
|
|
return 'system';
|
|
}
|
|
}
|
|
|
|
static storeTheme(theme) {
|
|
try {
|
|
if (theme === 'system') {
|
|
localStorage.removeItem('theme');
|
|
} else {
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
} catch {
|
|
/* storage unavailable, the choice just won't persist */
|
|
}
|
|
}
|
|
|
|
updateButtons() {
|
|
this.btnLight.setAttribute('aria-pressed', String(this.currentTheme === 'light'));
|
|
this.btnDark.setAttribute('aria-pressed', String(this.currentTheme === 'dark'));
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.btnLight.addEventListener('click', () => {
|
|
this.applyTheme(this.currentTheme === 'light' ? 'system' : 'light');
|
|
});
|
|
|
|
this.btnDark.addEventListener('click', () => {
|
|
this.applyTheme(this.currentTheme === 'dark' ? 'system' : 'dark');
|
|
});
|
|
}
|
|
|
|
applyTheme(theme) {
|
|
const selectedTheme = (theme === 'light' || theme === 'dark') ? theme : 'light dark';
|
|
|
|
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
const html = document.documentElement;
|
|
let currentColorScheme = html.style.getPropertyValue('color-scheme');
|
|
if (currentColorScheme === '') {
|
|
currentColorScheme = 'light dark';
|
|
}
|
|
|
|
/* do we need to transition */
|
|
let doTransition = false;
|
|
|
|
if (selectedTheme !== currentColorScheme) {
|
|
if (selectedTheme === 'light dark') {
|
|
doTransition = currentColorScheme !== systemTheme;
|
|
} else if (currentColorScheme === 'light dark') {
|
|
doTransition = selectedTheme !== systemTheme;
|
|
} else {
|
|
doTransition = true;
|
|
}
|
|
}
|
|
|
|
const applyChange = () => {
|
|
this.currentTheme = theme;
|
|
html.style.setProperty('color-scheme', selectedTheme);
|
|
ThemeSelector.storeTheme(theme);
|
|
|
|
this.updateButtons();
|
|
this.status.textContent = theme === 'system' ? 'Theme set to system default' : '';
|
|
};
|
|
|
|
html.style.viewTransitionName = 'changing-theme';
|
|
if (doTransition && document.startViewTransition) {
|
|
document.startViewTransition(applyChange);
|
|
} else {
|
|
applyChange();
|
|
}
|
|
}
|
|
}
|
|
|
|
export { ThemeSelector };
|