- Add Jinja2 templates with data-testid attributes for testing - Create light/dark themes based on Gitea color scheme - Add theme switching with localStorage persistence - Create base CSS, components, and layout styles - Add mock web routes for UI demonstration - Register web router and static files in main.py - Add data-testid requirements to AGENTS.md - Install jinja2 dependency
164 lines
4.3 KiB
JavaScript
164 lines
4.3 KiB
JavaScript
/**
|
|
* Theme switching functionality for blog application.
|
|
*
|
|
* Handles theme persistence in localStorage and applies
|
|
* the selected theme to the document root element.
|
|
* Supports system preference detection and manual theme switching.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
const STORAGE_KEY = 'blog-theme';
|
|
const THEME_ATTRIBUTE = 'data-theme';
|
|
const THEME_LIGHT = 'light';
|
|
const THEME_DARK = 'dark';
|
|
|
|
/**
|
|
* Get the currently stored theme preference.
|
|
* Falls back to system preference if no stored value.
|
|
*
|
|
* @returns {string} The theme name ('light' or 'dark')
|
|
*/
|
|
function getStoredTheme() {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored === THEME_LIGHT || stored === THEME_DARK) {
|
|
return stored;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to access localStorage:', e);
|
|
}
|
|
|
|
return getSystemPreference();
|
|
}
|
|
|
|
/**
|
|
* Detect system color scheme preference.
|
|
*
|
|
* @returns {string} 'dark' if system prefers dark mode, 'light' otherwise
|
|
*/
|
|
function getSystemPreference() {
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return THEME_DARK;
|
|
}
|
|
return THEME_LIGHT;
|
|
}
|
|
|
|
/**
|
|
* Apply the specified theme to the document.
|
|
* Updates the data-theme attribute on the html element.
|
|
*
|
|
* @param {string} theme - The theme to apply ('light' or 'dark')
|
|
*/
|
|
function applyTheme(theme) {
|
|
const html = document.documentElement;
|
|
if (html) {
|
|
html.setAttribute(THEME_ATTRIBUTE, theme);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the theme preference to localStorage.
|
|
*
|
|
* @param {string} theme - The theme to save ('light' or 'dark')
|
|
*/
|
|
function saveTheme(theme) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, theme);
|
|
} catch (e) {
|
|
console.warn('Failed to save theme to localStorage:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set and apply the specified theme.
|
|
* Updates both the DOM and localStorage.
|
|
*
|
|
* @param {string} theme - The theme to set ('light' or 'dark')
|
|
*/
|
|
function setTheme(theme) {
|
|
if (theme !== THEME_LIGHT && theme !== THEME_DARK) {
|
|
console.warn('Invalid theme:', theme);
|
|
return;
|
|
}
|
|
applyTheme(theme);
|
|
saveTheme(theme);
|
|
updateThemeIcons(theme);
|
|
}
|
|
|
|
/**
|
|
* Toggle between light and dark themes.
|
|
*/
|
|
function toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute(THEME_ATTRIBUTE);
|
|
const newTheme = currentTheme === THEME_DARK ? THEME_LIGHT : THEME_DARK;
|
|
setTheme(newTheme);
|
|
}
|
|
|
|
/**
|
|
* Update theme toggle icons based on current theme.
|
|
* Shows/hides sun/moon icons appropriately.
|
|
*
|
|
* @param {string} theme - The current theme
|
|
*/
|
|
function updateThemeIcons(theme) {
|
|
const lightIcons = document.querySelectorAll('[data-testid="theme-light-icon"]');
|
|
const darkIcons = document.querySelectorAll('[data-testid="theme-dark-icon"]');
|
|
|
|
lightIcons.forEach(icon => {
|
|
icon.style.display = theme === THEME_LIGHT ? 'none' : 'block';
|
|
});
|
|
|
|
darkIcons.forEach(icon => {
|
|
icon.style.display = theme === THEME_DARK ? 'none' : 'block';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize theme on page load.
|
|
* Applies stored theme and sets up event listeners.
|
|
*/
|
|
function init() {
|
|
const theme = getStoredTheme();
|
|
applyTheme(theme);
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateThemeIcons(theme);
|
|
|
|
const toggleBtn = document.querySelector('[data-testid="theme-toggle"]');
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', toggleTheme);
|
|
}
|
|
});
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
|
try {
|
|
const hasUserPreference = localStorage.getItem(STORAGE_KEY);
|
|
if (!hasUserPreference) {
|
|
const newTheme = e.matches ? THEME_DARK : THEME_LIGHT;
|
|
applyTheme(newTheme);
|
|
updateThemeIcons(newTheme);
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to handle system theme change:', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
const BlogTheme = {
|
|
setTheme: setTheme,
|
|
toggleTheme: toggleTheme,
|
|
getStoredTheme: getStoredTheme,
|
|
getSystemPreference: getSystemPreference,
|
|
THEME_LIGHT: THEME_LIGHT,
|
|
THEME_DARK: THEME_DARK
|
|
};
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.BlogTheme = BlogTheme;
|
|
}
|
|
|
|
init();
|
|
})();
|