@zevra/ui
v0.44.1
Published
Design system Zevra « Encre & Papier » : tokens, CSS des composants et logos officiels.
Maintainers
Readme
Zevra UI : design system « Encre & Papier »
Tokens, CSS des composants et logos officiels de la plateforme Zevra. Source de vérité pour tout ce qui touche à l'identité visuelle : une app qui recopie un hexadécimal au lieu de lire une variable d'ici finira par diverger.
Issu de la passation DA du 23/08/2026. L'atelier où la direction évolue reste le canvas Claude Design « Zevra DA Encre et Papier » ; ce dépôt est ce qui en sort, versionné et consommable.
Installer
pnpm add @zevra/uiUtiliser
Le CSS
/* tout d'un coup */
@import "@zevra/ui/css";
/* ou à la carte */
@import "@zevra/ui/tokens.css"; /* les variables, indispensable */
@import "@zevra/ui/base.css"; /* reset, typographie, kicker, liens */
@import "@zevra/ui/components.css"; /* boutons, badges, cartes, encarts */
@import "@zevra/ui/layout.css"; /* conteneurs, grilles, sections, chrome */
@import "@zevra/ui/surfaces.css"; /* halos, sols, voiles, manifeste */
@import "@zevra/ui/motion.css"; /* les neuf animations autorisées */
@import "@zevra/ui/icons.css"; /* dimensionnement des icônes */
@import "@zevra/ui/overlays.css"; /* voile, tiroir, modale, notification */
@import "@zevra/ui/forms.css"; /* champs, selects, interrupteurs, dépôt */
@import "@zevra/ui/feedback.css"; /* alertes, badges, états, chargement */
@import "@zevra/ui/navigation.css"; /* menus, barre, fil d'Ariane, pagination */
@import "@zevra/ui/data.css"; /* tableaux, listes, clé-valeur, avatars */L'ordre compte : les tokens d'abord, le socle ensuite. Toutes les classes sont
préfixées zv-. Aucune dépendance, aucun préprocesseur : le paquet fonctionne
à l'identique en React, en Astro, en HTML brut et dans un canvas de design.
Le CSS est fluide et responsive là où les maquettes étaient figées à
1440px : .zv-page est plafonnée, pas verrouillée, et un point de rupture
unique à 768px reprend les valeurs mobiles du relevé.
Chaque valeur vient des maquettes de la passation. Là où deux planches se contredisaient, l'arbitrage est écrit dans DECISIONS.md.
Les composants React
pnpm add @zevra/ui reactimport '@zevra/ui/tokens.css'
import '@zevra/ui/base.css'
import '@zevra/ui/forms.css'
import { Button, Field, Input } from '@zevra/ui/react'
<Field label="ADRESSE E-MAIL" htmlFor="mail" help="Votre adresse professionnelle.">
<Input id="mail" type="email" />
</Field>
<Button>Envoyer</Button>Ce ne sont pas des composants qui embarquent leur style : chacun n'est
qu'une façade typée et documentée au-dessus des classes zv-. Le CSS reste
la source de vérité, il s'importe à part, et un test
(tests/react.test.js) vérifie que toute classe qu'un composant peut émettre
existe bien dans un module CSS : c'est ce qui empêche les deux de diverger.
Rendu serveur. Un seul module est marqué "use client" : overlays
(tiroir, modale, notification), les seuls qui aient besoin du navigateur.
Tout le reste se rend au serveur : dans un composant serveur Next comme dans
une île Astro à zéro JavaScript. Les composants qui prennent un gestionnaire
d'événement (Switch, Seg, Tabs…) restent sans état : c'est un composant
client à toi qui détient l'état et les rend.
Astro. @astrojs/react rend ces composants en HTML statique à la
compilation, sans JavaScript envoyé, tant qu'aucune directive client:* n'est
posée. Un <Button> dans une .astro ne coûte donc rien de plus qu'un
<button class="zv-btn"> : avec les props typées et la documentation en
prime. ⚠️ En revanche, un composant .astro ne peut pas être passé en enfant
d'un composant React : ce sens-là ne fonctionne pas.
Les props sont en français, comme le CSS et les commentaires. Celles qui portent une décision de charte sont documentées à l'endroit où on les lit, et les avertissements ⚠️ signalent ce qui casse le système si on le contourne : pas de simples préférences.
className est accepté partout, mais c'est une sortie de secours, pas un
usage courant : une classe locale qui repeint un composant du système est une
divergence qui n'a pas dit son nom.
Le catalogue
import { CATALOGUE } from '@zevra/ui/react/catalogue'Chaque composant y a un exemple rendu, le code qui le produit, et le résumé de ce qu'il fait : et surtout de quand ne pas l'employer. C'est ce que rend la page « Design system » de l'admin, et c'est aussi le banc d'essai du test de dérive : un composant sans exemple n'est ni documenté ni couvert, et le test le refuse.
Deux couches
Le paquet en porte deux, et elles ne servent pas au même usage.
La couche vitrine (base, components, layout, surfaces, motion)
compose une page qui se lit : hero, sections, manifeste, cartes produit.
La couche applicative (forms, feedback, navigation, data, icons)
compose une interface qui s'opère : champs, alertes, menus, tableaux. Elle
est plus dense, et dispose de son échelle propre (--app, --mono).
Une app n'a aucune raison de charger les deux. Sont communs : tokens.css,
base.css, icons.css et overlays.css, une page vitrine a besoin
d'un tiroir pour son menu mobile autant qu'une application pour sa
navigation.
Le menu mobile n'est pas optionnel. Sous 768px, .zv-nav__links doit
être masqué (zv-only-desktop) et remplacé par .zv-nav__burger, qui ouvre
un .zv-drawer. Sans lui, la navigation devient inatteignable sur
téléphone.
Le tableau est à base de grille, pas de <table> : le gabarit de
colonnes se pose en --zv-cols sur le conteneur, et sous 768px la rangée
s'effondre en carte, chaque valeur reprenant son intitulé.
Le mouvement
Neuf animations, pas une de plus, sous le préfixe zv-anim-. Sept sont
pilotées par le défilement, deux tournent en continu :
<span class="zv-halo zv-halo--hero zv-anim-breathe"></span>
<h2 class="zv-h2 zv-anim-rise">…</h2>
<img class="zv-glyph zv-anim-drift" style="--zv-delay: .8s">Les règles de la passation : une seule courbe, un seul type de mouvement par scène, des entrées toujours vers le haut, et aucun mouvement sur la typographie une fois posée.
Les entrées se jouent une fois par visite (0.12.0) : elles sont armées
par html.zv-motion et libérées à l'entrée au viewport par motion.js,
import '@zevra/ui/motion.js' une fois par page, et rien d'autre à faire.
Remonter la page ne défait rien : l'ancien pilotage view() rejouait les
entrées à l'envers sous le lecteur. Sans JavaScript, rien n'est armé et la
page s'affiche complète. Un échafaudage de scènes scroll-pilotées pose
data-zv-motion-skip sur ses scènes et re-branche les @keyframes sur ses
timelines nommées. Seuls zv-anim-progress et zv-anim-dot-active restent
au défilement : suivre la lecture est leur métier. Un seul media query
neutralise les neuf pour prefers-reduced-motion.
Les tokens en JavaScript
Pour ce qui ne peut pas lire une variable CSS : graphes, images générées, gabarits d'e-mail :
import { couleurs, serieGraphique, polices } from '@zevra/ui'
couleurs.accent // '#2a2aa8'
serieGraphique // palette ordonnée, dérivée du spectreC'est un miroir de tokens.css, pas une seconde source. Un test échoue si
les deux divergent.
Les logos
logos/index.json décrit les sept marques : produit, forme, nuance et fond de
destination.
Depuis le 24/08/2026, le paquet ne livre plus que les rendus couleur des glyphes (sept depuis la 0.13.0). Les déclinaisons vectorielles (encre, navy, blanc, et les lockups) ont été retirées : elles ne suivaient plus les marques, et celle de Mémoire portait encore un logo abandonné.
import index from '@zevra/ui/logos/index.json'Nommage : <produit>-<forme>-<nuance>.png, par exemple
campus-glyph-orig.png. Produits : zevra, campus, lexform, mcp,
memoire. Formes : glyph, mark (Zevra). Nuance : orig.
Ce sont des rendus matriciels de 512 px. Ils tiennent jusqu'à ~380 px d'affichage ; au-delà ils flouteront. Pour les inliner dans un canvas où les ressources distantes sont interdites, l'admin en donne la data-URI.
⚠️ Ce qui manque, et qu'il faudra refaire : les versions monochromes (encre et blanc) et les lockups avec wordmark. La charte les impose (blanc sur navy, mono sous 24 px) mais le paquet ne les porte plus.
Polices
Trois familles Google : Outfit (titres), Public Sans (texte), Space Grotesk (labels capitales).
En production, chargez-les par balise : une @import CSS bloque le rendu :
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;500;600&family=Public+Sans:wght@400;600&family=Space+Grotesk:wght@500;600;700&display=swap">@import "@zevra/ui/fonts.css" existe pour les prototypes et les
maquettes, où la simplicité prime.
Ouvrir un enrobage
Tiroir et modale s'ouvrent par un état porté sur le conteneur. Deux écritures équivalentes, selon le gabarit :
<div class="zv-drawer" data-open> <!-- HTML, Astro -->
<div class="zv-drawer zv-drawer--open"> <!-- React, Vue -->⚠️ En React, ne pas écrire data-open={ouvert} : il rend
data-open="false", que le sélecteur d'attribut considère comme présent, le
panneau resterait ouvert. C'est précisément pour cela que la variante en
classe existe.
Fermer sur Échap relève du gabarit : le CSS ne sait pas le faire. En React,
Drawer et Modal (@zevra/ui/react) le font déjà, avec le piège de focus,
la restitution du focus au bouton d'ouverture et le verrou de défilement :
et le hook useOverlay est exporté pour un enrobage maison.
Les règles qui ne se négocient pas
Elles viennent de la passation. Les ignorer produit du « presque Zevra », qui coûte plus cher à corriger qu'à respecter.
Un seul bouton plein par écran. L'indigo porte l'action. Pas de bouton en dégradé, pas de rayon 999.
Jamais de fausse citation, jamais de lorem. Le bloc .zv-quote reste en
réserve tant qu'un verbatim client n'a pas été recueilli ; .zv-ui-preview
montre de vraies données réalistes : c'est lui qui porte la crédibilité de la
page.
Trois cartes de tarif par page au maximum, cinq questions de FAQ, une seule ligne de preuve sociale. Au-delà, la preuve se dilue au lieu de s'accumuler.
Le spectre ne sert qu'à trois choses : le filet de section (56×2 px devant un kicker), la barre de progression, les points de la méthode. Jamais en fond, jamais en texte.
Les logos ne se retouchent pas : ni recoloration, ni resaturation, ni inclinaison. Zone de protection X/2 ; hauteur de capitale du wordmark = 0,30 × glyphe.
Le système est clair par nature. Une surface sombre se fait avec
--manifeste et --accent-on-dark, pas en inversant les neutres : les
contrastes ont été réglés dans ce sens-là.
Dans un canvas Claude Design
Un canvas interdit les ressources distantes : ni feuille de style externe, ni image par URL. Pour y travailler dans la charte :
- coller le bloc
:root { … }desrc/tokens.cssen tête de l'artboard ; - coller les SVG de logo en ligne plutôt que de les référencer.
L'admin Zevra propose les deux au presse-papier (page Plateforme → Design system), c'est le chemin le plus court.
Les polices Google, elles, sont autorisées dans un canvas : la balise <link>
ci-dessus fonctionne telle quelle.
Faire évoluer
- La décision visuelle se prend dans le canvas Claude Design, avec Pierre.
- Elle atterrit ici :
src/tokens.cssd'abord, puis son miroirtokens/index.js, puis les modules CSS concernés. pnpm testdoit rester vert. Les tests ne jugent pas l'esthétique : ils attrapent le token appelé mais inexistant, la classe définie dans deux modules (où c'est l'ordre de chargement qui tranche à la place de la DA), le second point de rupture glissé au fil de l'eau, et le spectre écrit à la main hors de ses trois emplois autorisés.- Après ajout de logos :
node scripts/build-logo-index.mjs, et committer l'index régénéré. - Publier une version :
npm version <patch|minor|major>puisnpm publish.
demo/index.html exerce la couche vitrine, demo/app.html la couche
applicative. Aucune des deux n'est publiée dans le paquet.
reference/ conserve les maquettes HTML, le prototype animé et les fichiers
d'origine de la passation. Ce dossier n'est pas publié dans le paquet : il
sert d'archive et de référence visuelle, pas de dépendance.
