Thèmes

Thème du projet (defineTheme, ThemeStyle, getThemeValues), jetons de couleur, bascule clair et sombre, hook useTheme.

core/themes porte l'apparence de tout le projet : le theme.js qui la décrit, les jetons CSS que les composants écrivent, et la bascule clair / sombre. Un composant n'écrit jamais une nuance de palette, seulement un jeton : c'est ce qui permet à un seul fichier de changer l'admin, les pages publiques, les modules, les courriels et les PDF en même temps.

Import

// Déclaration, dans le theme.js du projet. Module neutre.
import { defineTheme, resolveTheme } from '@lrvo/core/themes/define';

// Lecture côté serveur : layout racine, courriels, PDF.
import { ThemeStyle, getTheme, getThemeValues } from '@lrvo/core/themes/server';

// Bascule clair / sombre, côté client.
import {
  getStoredTheme, applyTheme, getAllowedModes, getThemeIcon,
  ThemeWatcher, useTheme, THEME_INIT_SCRIPT,
} from '@lrvo/core/themes';

@lrvo/core/themes est marqué 'use client'. @lrvo/core/themes/server ne s'importe que côté serveur. @lrvo/core/themes/define n'a aucune dépendance et se charge partout.

API

FonctionRetourRôle
defineTheme(options)objet geléValide les choix du projet. Lève sur une valeur hors liste.
resolveTheme(theme?){ light, dark, shared, shell, chrome, modes }Calcule la valeur de chaque jeton dans les deux modes.
<ThemeStyle />éléments de <head>Injecte les jetons du thème et le script anti-flash.
getTheme()thème résoluThème enregistré par initializeLrvo, ou l'apparence d'origine.
getThemeValues(mode?)Record<string, string>Valeurs hex d'un mode, pour les courriels et les PDF.
useTheme(){ theme, toggle, systemIsDark, canToggle }État de la bascule clair / sombre.
applyTheme(theme)—Applique 'light', 'dark' ou 'auto'.
getAllowedModes()string[]Modes offerts par le thème du projet.

defineTheme(options)

Toutes les options sont facultatives. Sans elles, l'apparence est celle d'origine : noir et blanc, gris neutres, sidebar.

OptionValeursDéfautEffet
primary'neutral' ou une couleur hex'neutral'Couleur d'action : bouton principal, élément actif, avatar. Avec une couleur, accent et ring la suivent.
neutral'neutral', 'stone', 'slate', 'zinc', 'gray''neutral'Teinte des fonds, textes et bordures, en clair comme en sombre.
radius'none', 'sm', 'md', 'lg''md'Arrondis des contrôles et des cartes : 0/0, 4/6, 8/12, 10/16 px.
font{ sans?, mono? }IBM PlexFamilles de police. Le projet charge lui même une police autre qu'IBM Plex.
chrome'light', 'dark''light''dark' garde la barre supérieure et la sidebar foncées, quel que soit le mode.
shell'sidebar', 'topbar''sidebar'Coquille de l'admin. 'topbar' met la navigation dans la barre du haut.
modes['light', 'dark'], ['light'], ['dark']les deuxUn seul mode l'impose, et masque toutes les bascules.
overrides{ light?, dark?, shared? }{}Valeur exacte d'un jeton, par mode. Les clés sont limitées aux jetons connus.
// theme.js
import { defineTheme } from '@lrvo/core/themes/define';

export default defineTheme({
  primary: '#2563eb',
  neutral: 'stone',
  chrome: 'dark',
  shell: 'topbar',
  overrides: { light: { canvas: '#f3f2ef' } },
});

À partir d'une seule couleur, le core calcule le reste en OKLCH : le survol, une version plus claire pour le mode sombre (une marque foncée se perdrait sur le noir), le texte posé dessus (blanc ou noir, selon le contraste), et un accent assez sombre pour rester lisible en texte sur fond blanc.

Une valeur hors liste lève au démarrage, avec le nom des valeurs admises. Une faute de frappe ne produit donc jamais un admin illisible en silence.

<ThemeStyle />

À placer dans le <head> du layout racine. Rend deux balises :

  1. Un <style> qui ne contient que les jetons dont la valeur diffère des défauts de core.css. Sans theme.js, il n'est pas rendu.
  2. Le script anti-flash, qui pose la classe dark sur <html> avant le premier rendu. Il respecte modes et pose data-lrvo-modes sur <html> pour les bascules. Sa balise porte l'identifiant lrvo-theme-init et les modes dans data-modes, que ThemeWatcher relit quand le script n'a pas été exécuté.
// app/layout.js
import { ThemeStyle } from '@lrvo/core/themes/server';

export default function RootLayout({ children }) {
  return (
    <html lang="fr" suppressHydrationWarning>
      <head>
        <ThemeStyle />
      </head>
      <body>{children}</body>
    </html>
  );
}

getTheme() et getThemeValues(mode)

Le thème est enregistré par initializeLrvo({ modules, theme }), depuis instrumentation.js. getTheme() le lit. Sans thème enregistré, il renvoie l'apparence d'origine.

getThemeValues('light') renvoie les valeurs hex d'un mode, clés au nom des jetons. Il sert à tout ce qui ne lit pas les variables CSS : un PDF, un style inline de courriel.

import { getThemeValues } from '@lrvo/core/themes/server';

function getPdfPalette() {
  const t = getThemeValues('light');
  return { ink: t.fg, muted: t['fg-muted'], rule: t.line, paid: t.success };
}

Les déclinaisons -soft et -line des statuts, calculées en CSS par color-mix(), y sont mélangées contre la surface.

Pour les courriels, @lrvo/core/email/templates expose des raccourcis : voir Courriel.

useTheme()

Retourne { theme, toggle, systemIsDark, canToggle } et synchronise l'état avec localStorage et le système au montage.

PropriétéTypeRôle
theme'light' | 'dark' | 'auto'Thème actif.
toggle() => voidPasse au thème suivant du cycle.
systemIsDarkbooleanÉtat courant de la préférence système.
canTogglebooleanfalse quand le thème du projet n'offre qu'un mode. Une bascule ne s'affiche pas.

Le cycle dépend de la préférence système, pour que le premier basculement produise toujours un changement visible :

  • Système clair : auto → dark → light → auto
  • Système sombre : auto → light → dark → auto
import { useTheme, getThemeIcon } from '@lrvo/core/themes';

export function ThemeToggle() {
  const { theme, toggle, canToggle } = useTheme();
  if (!canToggle) return null;
  const Icon = getThemeIcon(theme);
  return <button onClick={toggle}><Icon /></button>;
}

applyTheme(theme) et getStoredTheme()

getStoredTheme() retourne 'light', 'dark' ou 'auto' selon localStorage. applyTheme modifie la classe de <html> et localStorage. Il ne fait rien quand le thème du projet impose un mode.

ValeurEffet
'light'Retire la classe dark, stocke 'light'.
'dark'Ajoute la classe dark, stocke 'dark'.
'auto'Retire la valeur stockée et suit prefers-color-scheme.

getThemeIcon(theme)

ThèmeIcône
'light'Sun01Icon
'dark'Moon02Icon
'auto'ComputerIcon

L'icône du thème système ne dépend pas de la préférence système : un ordinateur dit que l'appareil décide, sans laisser croire à un troisième rendu.

ThemeWatcher

Composant sans rendu qui fait deux choses. LrvoProvider le monte déjà.

Il suit prefers-color-scheme quand aucune préférence n'est stockée, et que le thème offre les deux modes.

Il rejoue le script anti-flash quand celui-ci n'a pas tourné. Next.js rend côté client le document entier d'une page qui appelle notFound() : React insère alors le <script> de <ThemeStyle /> sans l'exécuter. Au montage, si data-lrvo-modes est absent de <html>, ThemeWatcher applique la même logique que le script : il lit les modes sur la balise lrvo-theme-init, pose data-lrvo-modes, puis la classe dark selon le mode imposé, la préférence stockée ou le système. Il le fait avant le premier affichage, donc sans flash. Sur une page rendue par le serveur, l'attribut est déjà là et rien ne se passe.

THEME_INIT_SCRIPT

Ancien script anti-flash, sans connaissance des modes du thème. Il reste exporté pour les projets qui n'ont pas encore adopté <ThemeStyle />, que lrvo-doctor signale.

Jetons

Les jetons sont déclarés dans core.css, sous le préfixe --lrvo-*, et enregistrés dans Tailwind. Chacun produit les utilitaires habituels : bg-surface, text-fg-muted, border-line, ring-ring/20, bg-danger/10.

GroupeJetonsRôle
Fondscanvas, surface, surface-muted, surface-hoverFond de page, cartes et champs, puces et fonds gris, survol d'une ligne ou d'une carte.
Textefg, fg-secondary, fg-muted, fg-subtleDu texte principal au texte le plus discret.
Lignesline, line-strongSéparations, bordures appuyées.
Contrôlescontrol, control-hover, control-lineFond, survol et bordure d'un bouton secondaire ou d'un champ. Transparent en clair, teinté en sombre.
Actionprimary, primary-hover, primary-fg, primary-softBouton principal, élément sélectionné.
Accentaccent, accent-fg, accent-soft, accent-lineLiens, cases cochées, sélection, surlignage.
FocusringAnneau de focus.
Barre et sidebarchrome, chrome-fg, chrome-fg-muted, chrome-line, chrome-hoverCoquille de l'admin, pilotée par l'option chrome.
Statutsdanger, success, warning, info, chacun avec -soft, -line, -fgCouleur, fond pâle, bordure, texte posé sur la couleur pleine.
Formesrounded-control, rounded-cardContrôles (8 px), cartes et panneaux (12 px).
Policesfont-ui, font-codeInterface, codes et montants.

font-ibm-plex-sans et font-ibm-plex-mono restent disponibles pour les gabarits de projets existants. Ils suivent désormais l'option font.

Notes

Les défauts vivent dans @layer base. Les valeurs de <ThemeStyle />, hors couche, les battent sans dépendre de l'ordre des feuilles dans <head>. Pour la même raison, une redéfinition de --lrvo-* dans le CSS du projet l'emporte sur les défauts, mais pas forcément sur <ThemeStyle /> : pour changer la valeur d'un jeton, passer par overrides.

Le thème se lit au rendu, jamais au chargement d'un fichier. Le manifeste des modules lance leur register() avant qu'initializeLrvo n'enregistre le thème. Une constante calculée au niveau racine d'un fichier garderait l'apparence d'origine.

Le thème sombre est une classe CSS, pas un état React partagé. La source de vérité est la classe dark sur <html>, posée par le script avant React, ou par ThemeWatcher quand le document est rendu côté client. useTheme la lit au montage plutôt que de l'imposer, ce qui évite un second flash à l'hydratation.

Pour les règles d'usage des jetons, voir Design system.

Sur cette page