feat(themes): v2.1.0 - themes personnalisés avec presets light/dark/high-contrast/sepia

- 4 modes: dark, light, high-contrast, sepia (40+ CSS vars chacun)
- Import/export thèmes personnalisés en JSON
- Grid swatches dans les paramètres avec preview
- toggleThemeMode() cycle through all 4 modes
- CustomEvent 'themechange' dispatché
- 18 tests unitaires (structure, generators, colors, i18n keys)
- 439 tests passent au total
This commit is contained in:
2026-09-06 18:42:20 -04:00
parent 09912509cb
commit c8b861f0b6
3 changed files with 392 additions and 6 deletions
+180 -4
View File
@@ -526,26 +526,85 @@ var THEMES = {
var _currentTheme = 'defaut-obsigate';
var _currentMode = 'dark';
var AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia'];
// ── Mode generators ─────────────────────────────────────────────────
// Generate a high-contrast variant from dark-mode CSS vars.
function _genHighContrast(darkVars) {
return {
'--bg-primary': '#000000', '--bg-secondary': '#0a0a0a', '--bg-sidebar': '#000000',
'--bg-hover': '#141414', '--border': '#444444', '--text-primary': '#ffffff',
'--text-secondary': '#cccccc', '--text-muted': '#888888', '--accent': darkVars['--accent'] || '#58a6ff',
'--accent-green': '#4cff6e', '--tag-bg': '#1a2a44', '--tag-text': '#7dc4ff',
'--code-bg': '#0a0a0a', '--search-bg': '#111111', '--scrollbar': '#444444',
'--resize-handle': '#444444', '--overlay-bg': 'rgba(0,0,0,0.85)',
'--danger': '#ff6b6b', '--danger-bg': '#4a0f0f', '--success': '#4cff6e', '--success-bg': '#0a3a14',
'--accent-card': darkVars['--accent'] || '#58a6ff', '--accent-bg': '#1a2a44', '--accent-text': '#7dc4ff',
'--green': '#4cff6e', '--green-bg': '#0a3a14', '--purple': '#c084fc', '--purple-bg': '#2a1a4a',
'--surface': '#0a0a0a', '--surface2': '#000000', '--surface3': '#000000',
'--border-md': '#444444', '--text': '#ffffff', '--text-2': '#cccccc', '--text-3': '#888888',
'--ok': '#4cff6e', '--warn': '#ffcc00', '--ai': '#c084fc', '--err': '#ff6b6b'
};
}
// Generate a sepia variant from light-mode CSS vars.
function _genSepia(lightVars) {
return {
'--bg-primary': '#f5ecd5', '--bg-secondary': '#eee3c8', '--bg-sidebar': '#e8dcc0',
'--bg-hover': '#e0d4b4', '--border': '#c8b890', '--text-primary': '#3d2e1a',
'--text-secondary': '#6b5a3e', '--text-muted': '#9a8a6a', '--accent': '#b8860b',
'--accent-green': '#5a8a32', '--tag-bg': '#e8dcc0', '--tag-text': '#8b6914',
'--code-bg': '#eee3c8', '--search-bg': '#f5ecd5', '--scrollbar': '#c8b890',
'--resize-handle': '#c8b890', '--overlay-bg': 'rgba(60,40,10,0.25)',
'--danger': '#b33a2a', '--danger-bg': '#f5e0dc', '--success': '#5a8a32', '--success-bg': '#e0edd0',
'--accent-card': '#b8860b', '--accent-bg': '#f0e4c4', '--accent-text': '#8b6914',
'--green': '#5a8a32', '--green-bg': '#e0edd0', '--purple': '#7a5a9a', '--purple-bg': '#e8ddf0',
'--surface': '#eee3c8', '--surface2': '#f5ecd5', '--surface3': '#e8dcc0',
'--border-md': '#c8b890', '--text': '#3d2e1a', '--text-2': '#6b5a3e', '--text-3': '#9a8a6a',
'--ok': '#5a8a32', '--warn': '#b8860b', '--ai': '#7a5a9a', '--err': '#b33a2a'
};
}
// Get CSS vars for a theme + mode (generating HC/sepia on demand).
function _getVars(themeKey, mode) {
var theme = THEMES[themeKey];
if (!theme) return null;
if (theme.modes[mode]) return theme.modes[mode];
// Generate high-contrast / sepia from dark / light
if (mode === 'high-contrast') return _genHighContrast(theme.modes.dark || {});
if (mode === 'sepia') return _genSepia(theme.modes.light || {});
return null;
}
function applyTheme(themeKey, mode) {
var theme = THEMES[themeKey];
if (!theme) return;
mode = mode || _currentMode || 'dark';
var vars = theme.modes[mode];
var vars = _getVars(themeKey, mode);
if (!vars) return;
var root = document.documentElement;
Object.keys(vars).forEach(function(key) {
root.style.setProperty(key, vars[key]);
});
// Store resolved vars on the theme for export/preview
if (!theme.modes[mode]) theme.modes[mode] = vars;
_currentTheme = themeKey;
_currentMode = mode;
try { localStorage.setItem('obsigate-theme', themeKey); } catch(e) {}
try { localStorage.setItem('obsigate-theme-mode', mode); } catch(e) {}
// Notify other components of theme change
try {
root.setAttribute('data-theme', mode);
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: themeKey, mode: mode } }));
} catch(e) {}
}
function toggleThemeMode() {
var newMode = _currentMode === 'dark' ? 'light' : 'dark';
var idx = AVAILABLE_MODES.indexOf(_currentMode);
var newMode = AVAILABLE_MODES[(idx + 1) % AVAILABLE_MODES.length];
applyTheme(_currentTheme, newMode);
return newMode;
}
@@ -553,14 +612,131 @@ function toggleThemeMode() {
function getCurrentTheme() { return _currentTheme; }
function getCurrentMode() { return _currentMode; }
// ── Public API ──────────────────────────────────────────────────────
function setTheme(name) {
if (THEMES[name]) {
applyTheme(name, _currentMode);
}
}
function getTheme() {
return _currentTheme;
}
function listThemes() {
return Object.keys(THEMES).map(function(key) {
return { id: key, name: THEMES[key].name, desc: THEMES[key].desc };
});
}
function listModes() {
return AVAILABLE_MODES.slice();
}
// ── Import / Export ─────────────────────────────────────────────────
function exportTheme(themeKey) {
themeKey = themeKey || _currentTheme;
var theme = THEMES[themeKey];
if (!theme) return null;
// Ensure HC/sepia are resolved for export
var modes = {};
AVAILABLE_MODES.forEach(function(m) {
modes[m] = _getVars(themeKey, m);
});
return JSON.stringify({
version: 1,
id: themeKey,
name: theme.name,
desc: theme.desc,
modes: modes
}, null, 2);
}
function exportAllThemes() {
var out = {};
Object.keys(THEMES).forEach(function(key) {
out[key] = {
name: THEMES[key].name,
desc: THEMES[key].desc,
modes: {}
};
AVAILABLE_MODES.forEach(function(m) {
out[key].modes[m] = _getVars(key, m);
});
});
return JSON.stringify({ version: 1, themes: out }, null, 2);
}
function importTheme(jsonStr) {
try {
var data = JSON.parse(jsonStr);
if (!data || !data.modes) return { ok: false, error: 'Invalid theme JSON: missing modes' };
var name = data.name || data.id || ('custom-' + Date.now());
var key = (data.id || name).toLowerCase().replace(/[^a-z0-9]+/g, '-');
var modes = {};
if (data.modes.dark && typeof data.modes.dark === 'object') modes.dark = data.modes.dark;
if (data.modes.light && typeof data.modes.light === 'object') modes.light = data.modes.light;
if (Object.keys(modes).length === 0) return { ok: false, error: 'Theme must define at least dark or light mode' };
THEMES[key] = { name: name, desc: data.desc || 'Custom theme', modes: modes };
try { localStorage.setItem('obsigate-custom-' + key, JSON.stringify(THEMES[key])); } catch(e) {}
return { ok: true, key: key };
} catch(e) {
return { ok: false, error: 'JSON parse error: ' + e.message };
}
}
function loadCustomThemes() {
var keys = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var k = localStorage.key(i);
if (k && k.indexOf('obsigate-custom-') === 0) {
keys.push(k);
}
}
} catch(e) {}
keys.forEach(function(lsKey) {
try {
var data = JSON.parse(localStorage.getItem(lsKey));
if (data && data.modes) {
var key = lsKey.replace('obsigate-custom-', '');
THEMES[key] = data;
}
} catch(e) {}
});
}
function deleteCustomTheme(key) {
if (THEMES[key] && THEMES[key].desc === 'Custom theme') {
delete THEMES[key];
try { localStorage.removeItem('obsigate-custom-' + key); } catch(e) {}
return true;
}
return false;
}
function initThemes() {
// Load any custom themes from localStorage first
loadCustomThemes();
var saved = null;
var savedMode = null;
try { saved = localStorage.getItem('obsigate-theme'); } catch(e) {}
try { savedMode = localStorage.getItem('obsigate-theme-mode'); } catch(e) {}
if (saved && THEMES[saved]) _currentTheme = saved;
if (savedMode) _currentMode = savedMode;
if (savedMode && AVAILABLE_MODES.indexOf(savedMode) !== -1) _currentMode = savedMode;
applyTheme(_currentTheme, _currentMode);
}
export { THEMES, applyTheme, toggleThemeMode, getCurrentTheme, getCurrentMode, initThemes };
export {
THEMES, AVAILABLE_MODES,
applyTheme, toggleThemeMode,
getCurrentTheme, getCurrentMode,
getTheme,
setTheme, listThemes, listModes,
exportTheme, exportAllThemes, importTheme, deleteCustomTheme, loadCustomThemes,
initThemes
};
+4 -2
View File
@@ -177,14 +177,16 @@ export function toggleTheme() {
// Update the theme toggle label
const themeLabel = document.getElementById("theme-label");
if (themeLabel) {
themeLabel.textContent = newMode === "dark" ? t('theme.dark') : t('theme.light');
const modeLabelMap = { dark: 'theme.dark', light: 'theme.light', 'high-contrast': 'theme.high_contrast', sepia: 'theme.sepia' };
themeLabel.textContent = t(modeLabelMap[newMode] || 'theme.dark');
}
// Update the theme toggle icon
const themeBtn = document.getElementById("theme-toggle");
if (themeBtn) {
const icon = themeBtn.querySelector("i");
if (icon) {
icon.setAttribute("data-lucide", newMode === "dark" ? "moon" : "sun");
const isLight = newMode === "light" || newMode === "sepia";
icon.setAttribute("data-lucide", isLight ? "sun" : "moon");
}
}
safeCreateIcons();