Theme rework: polish dark mode and previews
Refine mobile nav layout, improve dark theme UI polish, and update README previews with light/dark variants.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Theme toggle with system preference fallback.
|
||||
(function () {
|
||||
const STORAGE_KEY = 'theme_preference';
|
||||
const THEME_ATTR = 'data-theme';
|
||||
const SOURCE_ATTR = 'data-theme-source';
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const API_ENDPOINT = '/api/users/theme';
|
||||
|
||||
function normalizeTheme(value) {
|
||||
return value === 'light' || value === 'dark' ? value : null;
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
return mediaQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function setTheme(theme, source) {
|
||||
document.documentElement.setAttribute(THEME_ATTR, theme);
|
||||
document.documentElement.setAttribute(SOURCE_ATTR, source);
|
||||
updateToggleUi(theme, source);
|
||||
}
|
||||
|
||||
function getAuthToken() {
|
||||
try {
|
||||
return localStorage.getItem('auth_token');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferenceToServer(themePreference) {
|
||||
const token = getAuthToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
await fetch(API_ENDPOINT, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ themePreference: themePreference ?? null })
|
||||
});
|
||||
} catch (_) {
|
||||
// Ignore network errors to avoid blocking UI.
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPreferenceFromServer() {
|
||||
const token = getAuthToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(API_ENDPOINT, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const pref = normalizeTheme(data?.themePreference);
|
||||
if (pref) {
|
||||
localStorage.setItem(STORAGE_KEY, pref);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
applyStoredOrSystem();
|
||||
} catch (_) {
|
||||
// Ignore fetch errors.
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredOrSystem() {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
const theme = stored || getSystemTheme();
|
||||
const source = stored ? 'user' : 'system';
|
||||
setTheme(theme, source);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
const current = stored || getSystemTheme();
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
setTheme(next, 'user');
|
||||
savePreferenceToServer(next);
|
||||
}
|
||||
|
||||
function resetToSystem() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
applyStoredOrSystem();
|
||||
savePreferenceToServer(null);
|
||||
}
|
||||
|
||||
function updateToggleUi(theme, source) {
|
||||
const buttons = document.querySelectorAll('[data-theme-toggle]');
|
||||
const isDark = theme === 'dark';
|
||||
buttons.forEach((button) => {
|
||||
button.setAttribute('aria-checked', String(isDark));
|
||||
button.dataset.theme = theme;
|
||||
const label = isDark ? 'Mode sombre' : 'Mode clair';
|
||||
const sourceLabel = source === 'system' ? ' (système)' : '';
|
||||
button.setAttribute('title', `${label}${sourceLabel} — Shift+clic pour système`);
|
||||
});
|
||||
}
|
||||
|
||||
applyStoredOrSystem();
|
||||
syncPreferenceFromServer();
|
||||
|
||||
mediaQuery.addEventListener('change', () => {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
if (!stored) {
|
||||
applyStoredOrSystem();
|
||||
}
|
||||
});
|
||||
|
||||
function bindToggleButtons() {
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((button) => {
|
||||
if (button.dataset.themeBound === 'true') return;
|
||||
button.dataset.themeBound = 'true';
|
||||
button.addEventListener('click', (event) => {
|
||||
if (event.shiftKey) {
|
||||
resetToSystem();
|
||||
return;
|
||||
}
|
||||
toggleTheme();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindToggleButtons);
|
||||
} else {
|
||||
bindToggleButtons();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user