Un client de base de données desktop reste ouvert des heures. Il est souvent la première fenêtre lancée le matin et la dernière fermée le soir. Dans ces conditions, le thème n'est pas un détail cosmétique : un mauvais contraste fatigue, un mode sombre qui clignote au démarrage agace, et une couleur d'accent qui change entre la barre latérale et l'éditeur SQL trahit un manque de soin. QoreDB traite donc le thème comme un vrai sujet d'ingénierie.
Le résultat tient dans un seul hook, src/hooks/useTheme.ts, d'un peu plus de cent lignes. Pas de next-themes, pas de bibliothèque de gestion de thème. Voici comment il fonctionne et pourquoi il a été écrit ainsi.
Trois préférences, un thème résolu
QoreDB distingue deux notions. La préférence de l'utilisateur, de type ThemePreference, peut valoir 'light', 'dark' ou 'auto'. Le thème effectivement appliqué, de type ResolvedTheme, ne vaut jamais que 'light' ou 'dark'. En mode 'auto', le thème résolu suit le système d'exploitation.
Cette séparation évite un piège classique. Si on stockait directement 'dark' ou 'light' comme préférence, on perdrait l'intention de l'utilisateur : « suivre mon système ». Avec 'auto' persisté et le thème résolu calculé à la volée, QoreDB peut basculer automatiquement quand macOS ou Windows passe en mode nuit, sans que l'utilisateur ait à toucher quoi que ce soit.
export type ThemePreference = 'light' | 'dark' | 'auto';
export type ResolvedTheme = 'light' | 'dark';
const resolvedTheme = useMemo<ResolvedTheme>(() => {
return theme === 'auto' ? systemTheme : theme;
}, [theme, systemTheme]);Suivre le système en direct
Le thème système est lu via window.matchMedia('(prefers-color-scheme: dark)'). Le hook ne se contente pas de lire cette valeur au démarrage : il s'abonne à ses changements. Quand vous basculez votre OS en mode sombre à 18 h, l'événement change du media query met à jour l'état systemTheme, et si votre préférence est sur 'auto', l'interface suit immédiatement.
Le détail qui compte : le code gère les deux API de matchMedia. La moderne, addEventListener('change', …), et l'ancienne, addListener(…), pour les webviews basées sur des versions de Safari antérieures à la 14. QoreDB tournant sur Tauri 2 avec la webview native du système, cette compatibilité n'est pas théorique : elle dépend de la version du moteur WebKit installé sur la machine.
Un attribut et une classe : pourquoi les deux
C'est le choix le plus intéressant du hook. Quand le thème résolu change, QoreDB écrit deux choses sur l'élément racine du document.
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', resolvedTheme);
if (resolvedTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem(STORAGE_KEY, theme);
}, [resolvedTheme, theme]);L'attribut data-theme pilote les tokens de design propres à QoreDB. Dans src/index.css, les variables sont définies sous :root pour le clair et redéfinies sous :root[data-theme='dark'] pour le sombre. La classe .dark, elle, active le variant sombre de Tailwind et un second jeu de variables. QoreDB est sur Tailwind v4, où le variant sombre se déclare explicitement en tête de feuille de style.
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));Écrire les deux d'un même geste garantit qu'aucune couche de style ne peut se retrouver désynchronisée. Les composants qui utilisent une classe utilitaire Tailwind comme dark:bg-… et ceux qui lisent directement une variable --q-* basculent au même instant, sur le même signal.
Une seule source de vérité : les variables CSS
Toute la palette passe par des variables CSS nommées. Les neutres vont de --q-bg-0 à --q-bg-2 pour les fonds, --q-text-0 à --q-text-2 pour les textes, avec une couleur d'accent --q-accent et des tokens sémantiques --q-success, --q-warning, --q-error. Passer du clair au sombre ne change pas un nom de variable dans les composants : seule la valeur derrière change.
C'est ce que veut dire « cohérence Tailwind » ici. Plutôt que de disséminer des #0b0c0f dans les classes, on définit la couleur une fois et on la référence partout. L'accent de la barre latérale, la couleur d'un badge d'environnement et le fond d'une carte proviennent tous du même jeu de tokens. Un changement de palette se fait à un seul endroit.
Persistance et synchronisation entre fenêtres
La préférence est écrite dans localStorage sous la clé qoredb-theme. Au prochain lancement, le hook la relit dès l'initialisation de son état, ce qui évite le flash d'un thème par défaut avant application du bon.
Mais QoreDB peut avoir plusieurs fenêtres ouvertes. Pour qu'un changement dans les réglages d'une fenêtre se propage aux autres, setTheme fait deux choses : il persiste la valeur, et il émet un événement personnalisé qoredb:theme-changed. Chaque instance du hook écoute à la fois cet événement et l'événement natif storage, déclenché quand un autre contexte modifie localStorage.
const setTheme = useCallback((next) => {
setThemeState(prev => {
const computed = typeof next === 'function' ? next(prev) : next;
if (computed === prev) return prev;
localStorage.setItem(STORAGE_KEY, computed);
window.dispatchEvent(
new CustomEvent(THEME_EVENT, { detail: { theme: computed } })
);
return computed;
});
}, []);Les deux chemins convergent vers la même garde : la mise à jour n'est appliquée que si la nouvelle valeur diffère de l'ancienne, ce qui évite les rendus inutiles et les boucles d'événements entre fenêtres.
L'éditeur SQL suit le même thème
Un éditeur de code qui reste blanc éclatant dans une application sombre, c'est l'échec le plus visible d'un système de thèmes. L'éditeur SQL de QoreDB, construit sur CodeMirror 6, consomme le hook comme n'importe quel autre composant : il lit isDark, une commodité exposée par useTheme et égale à resolvedTheme === 'dark'.
En mode sombre, l'éditeur ajoute l'extension oneDark, puis un thème CodeMirror appliqué en dernier qui écrase le fond de oneDark par la même variable que le reste de l'interface, var(--q-bg-1). L'éditeur ne se contente pas d'être sombre : il est sombre de la même teinte exacte que la fenêtre qui l'entoure.
if (isDark) {
extensions.push(oneDark);
}
// Custom theme applied last so it overrides oneDark background
extensions.push(
EditorView.theme({
'&': {
height: '100%',
...(isDark ? { backgroundColor: 'var(--q-bg-1)' } : {}),
},
// ...
})
);L'ensemble des extensions est recalculé via un useMemo dont isDark fait partie des dépendances. Basculer le thème reconstruit donc la configuration de l'éditeur avec le bon jeu de styles, sans recréer l'instance à la main.
Le sélecteur, côté réglages
Dans les préférences, la section générale expose un menu à trois entrées, toujours issues du même hook :
- Système :
setTheme('auto'), et l'interface affiche entre parenthèses le thème réellement résolu. - Clair :
setTheme('light'), forcé quelle que soit la préférence système. - Sombre :
setTheme('dark'), forcé également.
Un parti pris : peu de code, contrôlé de bout en bout
On aurait pu tirer une bibliothèque de gestion de thème. On ne l'a pas fait, parce que le besoin était simple et que le maîtriser entièrement valait mieux qu'y ajouter une dépendance. Cent lignes bien lisibles, une préférence à trois valeurs, deux signaux DOM posés ensemble, un jeu de variables CSS partagé par Tailwind et par CodeMirror : cela suffit à ce que le thème soit instantané au démarrage, qu'il suive le système, qu'il se synchronise entre fenêtres et qu'il reste cohérent partout, jusque dans le fond de l'éditeur SQL.
C'est la même logique que dans le reste de QoreDB : préférer le code que l'on comprend et qui fait exactement ce qu'il faut, plutôt que l'abstraction qui en fait trop et qu'on ne contrôle plus.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.

