@cyrilld/zestds 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,18 +10,20 @@ import "@cyrilld/zestds/dist/index.css";
10
10
  <Home01 width={20} height={20} />
11
11
  ```
12
12
 
13
- Today the package publishes **the tokens and the 121 icons**. Interface components are phase 2
14
- see "Ce qui reste à faire".
13
+ Today the package publishes **the tokens, the icon families, the logo and the button family**.
14
+ Phase 2 started on 2026-09-09 with `Button`, `CloseButton`, `UtilityButton` and `Spinner` — see
15
+ "Ce qui reste à faire" for what follows.
15
16
 
16
17
  **Both imports matter.** The code and the styles travel separately — `dist/index.js` on one
17
18
  side, `dist/index.css` on the other. Importing the first without the second gives components
18
19
  that render, structurally, with no background, no spacing and no radius. Nothing fails and
19
20
  nothing warns.
20
21
 
21
- Its Storybook is the design system's documentation, written in French. Seven Foundations pages
22
- are written — colors, typography, spacing, radius, shadows, the focus ring, icons and the rest
23
- of the plan sits alongside them as **announced but empty pages**: the remaining foundations, then
24
- Base components and Application components.
22
+ Its Storybook is the design system's documentation, written in French. Twelve Foundations pages
23
+ are written — colors, typography, spacing, radius, shadows, the focus ring, icons, file icons,
24
+ flag icons, cursors, avatars, logo plus the first Base components page, Boutons. The rest of the
25
+ plan sits alongside them as **announced but empty pages**: two remaining foundations, then the
26
+ other Base components and the Application components.
25
27
 
26
28
  That isn't decoration. A sidebar that only shows what exists gives the illusion of a complete
27
29
  system: someone looking for "Badges" and not finding it can't tell whether it doesn't exist, is
@@ -38,7 +40,8 @@ src/
38
40
  styles/ theme.css (the tokens), fonts.css, typography.css, base.css
39
41
  fonts/ the six Averta .woff2 files — commercial license, see fonts/README.md
40
42
  components/
41
- base/ interface components empty, phase 2
43
+ base/ Button/ CloseButton/ UtilityButton/ Spinner/ Badge/ Avatar/ Tooltip/
44
+ Checkbox/ Toggle/ — phase 2, since 2026-09-09
42
45
  foundations/ the icons: catalogue.ts generated from Figma, index.tsx built on it
43
46
  docs/ the Storybook documentation — MDX pages and their boards. NOT shipped.
44
47
  Introduction.mdx the Storybook landing page
@@ -69,7 +72,7 @@ Colors are layered, and a component only ever cites the last two:
69
72
  | Ramps | `--zds-color-brand-600` | Never |
70
73
  | Utility palettes | `--zds-color-utility-lime-500` | To categorize |
71
74
  | Roles | `--zds-color-text-primary` | Yes |
72
- | Component | `--zds-color-toggle-border` | Last resort |
75
+ | Component | `--zds-color-toggle-bg-pressed` | Last resort |
73
76
 
74
77
  Beyond colors: the type scale (thirteen sizes, each with its line height), the named spacing
75
78
  scale, thirteen widths, eleven radii, seven shadows plus the relief, the focus ring, and the
@@ -79,14 +82,14 @@ eight brand gradients in three angles each.
79
82
 
80
83
  | Import | What | Weight |
81
84
  | --- | --- | --- |
82
- | `@cyrilld/zestds` | tokens, 121 icons, 27 file icons | 197 KB |
83
- | `@cyrilld/zestds/flags` | 234 country flags | 328 KB |
85
+ | `@cyrilld/zestds` | tokens, 135 icons, 27 file icons, the logo, the nine base components | 249 KB (+ 70 KB CSS) |
86
+ | `@cyrilld/zestds/flags` | 234 country flags | 329 KB |
84
87
  | `@cyrilld/zestds/cursors` | 50 cursors | 94 KB |
85
88
 
86
89
  ⚠️ **The two catalogues are deliberately NOT re-exported from the root.** A catalogue is a data
87
90
  object queried by key at runtime — that is the whole point of `<FlagIcon code={country} />` — so
88
91
  no bundler can prove an entry is unused and nothing gets tree-shaken away. A single `export *`
89
- added to `src/index.ts` for convenience would take the root from 197 KB to nearly 600 KB without
92
+ added to `src/index.ts` for convenience would take the root from 249 KB to over 670 KB without
90
93
  breaking anything visible: typecheck passes, build passes, the package silently triples.
91
94
 
92
95
  Flags need no CSS at all — their colours are national facts, not tokens. Cursors need
@@ -159,8 +162,16 @@ To try local changes in another project before publishing a new version:
159
162
  | `yarn typecheck` | Type-check the package |
160
163
  | `yarn lint` | Lint the package |
161
164
  | `yarn build` | Build to `dist/` (ESM, CJS, types, CSS) |
165
+ | `yarn verify:interrupteur` | Les QUATRE géométries, le déplacement PEINT du bouton, la bordure déclarée ET peinte, le survol dans les deux sens, et cinq contrastes épinglés |
162
166
  | `yarn verify:render` | Drive Edge over the pages: fonts, boards, red flags, console errors |
163
167
  | `yarn verify:labels` | Contraste des étiquettes d'icône de fichier, sans navigateur |
168
+ | `yarn verify:tokens` | Les composants ne citent que les couleurs de Zest, et aucun jeton de composant ne redouble un rôle |
169
+ | `yarn verify:foundations` | Le thème se lit en entier, aucune valeur brute qu'un jeton nomme déjà, aucune mention du fournisseur tiers |
170
+ | `yarn verify:buttons` | Géométrie, spinner, contrastes et anneau de focus des boutons — mesurés sur la peinture |
171
+ | `yarn verify:badges` | Les 207 badges contre le modèle relevé sur les 477 symboles du Figma, plus 26 contrastes |
172
+ | `yarn verify:avatars` | Les six boîtes, les deux géométries du point de statut, les chevauchements, trois contrastes |
173
+ | `yarn verify:info-bulles` | Les deux anatomies, le débord PEINT de la flèche sur les quatre côtés, les six placements, et que la flèche pointe sur le centre du déclencheur |
174
+ | `yarn verify:checkboxes` | Les DEUX pages en une passe : les tailles et les sept valeurs qui changent avec elles, l'épaisseur PEINTE de la marque, et cinq contrastes épinglés |
164
175
 
165
176
  `verify:render` needs a dev server running and `storybook-static/index.json` present (it reads
166
177
  the story ids from there rather than guessing them). Point it elsewhere with
@@ -187,10 +198,18 @@ git tag automatically (e.g. `v0.2.0`) — `--follow-tags` pushes both the commit
187
198
 
188
199
  ## Testing
189
200
 
190
- ⚠️ **There is no test suite right now**, because there is no component to test: removing `Button`
191
- emptied it. `yarn test` runs with `--passWithNoTests` so it stays green a red `yarn test` that
192
- only means "nothing to run" trains people to ignore red. **Drop the flag with the first phase-2
193
- component.**
201
+ **127 tests across nine component suites** (counted 2026-09-11): 27 on the avatars, 23 on the
202
+ tooltip, 18 on the checkbox and its radio, 17 on the badge, 15 on the toggle, and 27 over the
203
+ button family `Button` 15, `CloseButton` 5, `UtilityButton` 4, `Spinner` 3. `--passWithNoTests` was dropped on
204
+ 2026-09-09, as planned: the suite is no longer empty, so a red `yarn test` means something again.
205
+
206
+ The tests check **behaviour, not paint**. They do not assert a padding or a colour: those live in
207
+ the component's CSS sheet, and jsdom does not apply imported sheets — a test claiming
208
+ `padding: 10px` here would lie the day the CSS moved, without ever failing. The browser boards on
209
+ the Boutons page measure those, for real. What the tests do pin down is the three places the
210
+ button departs from reflex: loading blocks activation WITHOUT leaving the tab order, "icon only"
211
+ is derived from the absence of a label rather than declared, and an unnamed icon-only button
212
+ warns.
194
213
 
195
214
  When components arrive, they are tested with [Vitest](https://vitest.dev) and
196
215
  [React Testing Library](https://testing-library.com/react), driving them the way a user would
@@ -231,15 +250,17 @@ both `tsc` and `storybook build`, and only a real browser found them:
231
250
  - [x] **Icônes** — 121 publiées, les neuf catégories de la page « Icônes » du Figma.
232
251
  Navigateur et règles d'usage. Les conflits de nom et les styles à revoir sont sortis de
233
252
  la page le 2026-09-08 : voir « Les décisions ouvertes sur les icônes » plus bas.
234
- - [x] **File icons** — 27 types × 3 habillages, page en ligne. Les dessins viennent
253
+ - [x] **Icônes de fichier** — 27 types × 3 habillages, page en ligne. Les dessins viennent
235
254
  **du Figma** (`3847:2911`), re-teintés sur les jetons : dix substitutions,
236
- l'écart mesuré en Lab. ⚠️ Le cadre Figma `3847:2911` est la bibliothèque d'Untitled UI
237
- collée telle quelle — variables d'origine, collection en anglais, et en retard sur le
238
- paquet. Il ne décide que la LISTE des 27 types. ⚠️ Les trois règles d'usage des habillages
239
- (Default / Gray / Solid) sont une **proposition** : ni la page de ressources d'origine ni
240
- le Figma n'en donnent, elles attendent un arbitrage.
241
- - [x] **Flag icons** — 234 drapeaux, page en ligne, renommée « Flag icons » à la demande de
242
- Micka. Générés depuis le Figma (`3853:17761`), pastille ronde, une seule forme. Les
255
+ l'écart mesuré en Lab. ⚠️ Le cadre Figma `3847:2911` est une bibliothèque **tierce** collée
256
+ telle quelle — variables d'origine, collection en anglais, et en retard sur le paquet. Il
257
+ ne décide que la LISTE des 27 types. ⚠️ Les trois règles d'usage des habillages
258
+ (Default / Gray / Solid) sont une **proposition** : ni la source d'origine ni le Figma
259
+ n'en donnent, elles attendent un arbitrage.
260
+ - [x] **Drapeaux** — 234 drapeaux, page en ligne. ⚠️ **Cette page a changé de nom deux
261
+ fois** : « Flag icons » à la demande de Micka, puis « Drapeaux » de nouveau le
262
+ 2026-09-11, quand les titres de pages sont repassés en français — voir « Décidé ».
263
+ Générés depuis le Figma (`3853:17761`), pastille ronde, une seule forme. Les
243
264
  couleurs ne sont **pas** portées sur les jetons : celles d'un drapeau sont des faits
244
265
  nationaux. Les noms français viennent d'`Intl.DisplayNames`, pas d'une liste recopiée.
245
266
  ⚠️ Cinq codes du Figma sont faux et corrigés dans le catalogue — voir « En attente ».
@@ -263,14 +284,110 @@ both `tsc` and `storybook build`, and only a real browser found them:
263
284
  COPIE du catalogue : Storybook réclame un fichier, pas un composant. Si le logo change,
264
285
  régénérer le catalogue **et** recopier le favicon.
265
286
  - [ ] **Phase 2** — base components : boutons, badges, champs, sélecteurs, info-bulles
266
- - ⚠️ **Un `Button` a été retiré le 2026-09-08**, plutôt que corrigé à moitié. Hérité d'avant
267
- les fondations, il peignait son primaire en `#4f46e5` un indigo qui n'est pas le bleu de
268
- Zest — dimensionnait son `sm` à 13 px, taille absente de l'échelle, et ne citait aucun
269
- jeton : six de ses sept couleurs n'existaient nulle part dans le thème. Un composant faux
270
- dans un paquet enseigne le faux, et celui-là contredisait les pages qui documentent les
271
- jetons. Récupérable par `git checkout 69a30fa -- src/Button` si on veut le relire.
272
- - ⚠️ **Bloquant pour les badges et les boutons pleins** : les deux aplats de statut sous
273
- l'AA, voir « En attente ».
287
+ - [x] **Boutons** page en ligne, quatre composants livrés le 2026-09-09. `Button`
288
+ (11 hiérarchies × 5 tailles × 5 états × icône seule), `CloseButton` (3 tailles + fond
289
+ sombre), `UtilityButton` (28 et 32 px) et `Spinner`. Relevés sur cinq cadres du Figma.
290
+ Le `Button` retiré le 2026-09-08 est celui-ci, recodé : l'ancien peignait son primaire en
291
+ `#4f46e5` un indigo qui n'est pas le bleu de Zest dimensionnait son `sm` à 13 px,
292
+ taille absente de l'échelle, et ne citait aucun jeton. Récupérable par
293
+ `git checkout 69a30fa -- src/Button` si on veut le relire.
294
+ ⚠️ **Un écart au Figma, arbitré** — voir « Décidé ».
295
+ ⚠️ **Trois questions restent ouvertes** — voir « En attente » : les bordures sous les 3:1,
296
+ le tracé du spinner, et le soulignement des liens.
297
+ ✅ Retire `.zds-doc-bouton` du provisoire de `docs.css`.
298
+ - [x] **Badges** — page en ligne, un composant livré le 2026-09-10. `Badge` : 3 habillages
299
+ (`pill`, `badge`, `modern`) × 3 tailles × **13 couleurs**, plus quatre emplacements — le
300
+ point, la croix de retrait, une icône de tête, une icône de fin. Relevé sur trois cadres :
301
+ `Badge` (3869:2612, **477 symboles**), `_Dot` (3874:1192) et `_Badge close X` (3869:4461).
302
+ ⚠️ **ILS N'ÉTAIENT PAS BLOQUÉS**, et cette ligne a longtemps dit le contraire. Les trois
303
+ habillages sont deux fonds DOUX et un fond blanc : le Figma ne dessine aucun badge en
304
+ aplat, donc `success-solid` et `warning-solid` n'y sont employés par personne. L'arbitrage
305
+ de ces deux jetons reste ouvert — il concerne le premier bouton plein, pas le badge.
306
+ ✅ **Les treize couleurs tiennent leurs deux seuils de contraste** depuis l'arbitrage du
307
+ 2026-09-10 : turquoise, tilleul et jaune, les trois teintes claires, descendent de deux
308
+ crans là où les dix autres gardent les leurs. Voir « Décidé ».
309
+ ⚠️ **Une anomalie du Figma est reproduite** en taille `sm`, et **deux neutres presque
310
+ identiques** sont livrés faute d'arbitrage. Voir « En attente ».
311
+ ✅ Ajoute `verify:badges` : géométrie rendue contre le modèle des 477 symboles, 26
312
+ contrastes, anneau de focus de la croix.
313
+ - [x] **Avatars** — page en ligne, trois composants livrés le 2026-09-10. `Avatar`
314
+ (6 tailles × 4 contenus, bordure et statut optionnels), `AvatarGroup` et
315
+ `AvatarLabelGroup`. Relevé sur CINQ cadres : `Avatar` (3881:5036, 48 symboles),
316
+ `Status icon` (3881:5020), `Avatar group` (3881:4989), `_Avatar add button` (3881:4946) et
317
+ `Avatar label group` (3881:4930).
318
+ ⚠️ **CE DÉPÔT AFFIRMAIT QU'IL N'Y AVAIT PAS DE COMPOSANT AVATAR.** C'était faux : le relevé
319
+ s'était arrêté sur le cadre de la banque de photos. Voir « Décidé ».
320
+ ✅ Les **initiales** entrent dans le système, à leur place dans un ordre de priorité —
321
+ photo > initiales > picto. La règle « jamais d'initiales » de la page Foundations est
322
+ abrogée, et l'échelle de tailles qu'elle proposait était fausse.
323
+ ✅ Ajoute deux icônes au catalogue (133 → **135**) et `verify:avatars`.
324
+ ⚠️ Le statut est **absent par défaut** : la production ne le gère pas encore.
325
+ - [x] **Info-bulles** — page en ligne, trois composants livrés le 2026-09-10. `Tooltip`
326
+ (6 positions × flèche optionnelle × ligne de support optionnelle), `HelpTooltip` — l'icône
327
+ d'aide de 16 px qui porte sa propre bulle — et `TooltipArrow`, le dessin de la flèche.
328
+ Relevés sur deux cadres : `Tooltip` (3266:935, 14 symboles) et `Help icon` (3266:1096,
329
+ 28 symboles).
330
+ ⚠️ **Le développeur choisit la position, et rien ne se retourne tout seul.** Arbitrage du
331
+ lead front : le paquet reste à zéro dépendance d'exécution, donc les six placements sont
332
+ du CSS. La conséquence est à connaître avant de s'en servir — une bulle posée près du bord
333
+ de l'écran en sort, et rien ne la retournera. Voir « Décidé ».
334
+ ⚠️ **Une décision prise le matin même a été renversée** : un premier jet passait par
335
+ `react-aria-components`, approuvé en dépendance paire puis retiré. Ce qui survit du détour,
336
+ ce sont ses MESURES, relevées dans un vrai navigateur et reprises à l'identique. Voir
337
+ « Décidé ».
338
+ ⚠️ `--zds-color-tooltip-supporting-text` existait déjà, porté du thème de référence, et
339
+ portait une valeur fausse. Ses trois voisins de bloc sont dans le même cas — des ports que
340
+ rien n'a confrontés au Figma, et qu'aucun contrôle ne regarde tant que rien ne les cite.
341
+ ✅ Retire `.zds-doc-bulle` du provisoire de `docs.css` — et ce retrait a découvert que la
342
+ bulle de la page Icônes n'avait **jamais** peint la bonne couleur. Retire aussi le `title`
343
+ natif du bouton d'ajout des groupes d'avatars, qui porte désormais une vraie info-bulle.
344
+ ✅ Ajoute `verify:info-bulles`.
345
+ - [x] **Cases à cocher et boutons radio** — deux pages en ligne, deux composants livrés le
346
+ 2026-09-10. `Checkbox` (2 tailles × cochée/indéterminée × 4 états, libellé et indication
347
+ optionnels) et `Radio`, le même atome en rond, dont le `name` est OBLIGATOIRE. Relevés sur
348
+ deux cadres : `_Checkbox base` (3891:11902, 40 symboles) et `Checkbox` (3891:11629,
349
+ 74 symboles).
350
+ ⚠️ **Deux pages, une seule feuille de style.** « Cases à cocher » et « Boutons radio » sont
351
+ rangés séparément — demande produit, alignée sur la bibliothèque de référence — mais les
352
+ deux composants restent UN atome : `Checkbox.css` habille les deux et `verify:checkboxes`
353
+ visite les DEUX pages en une passe. Voir « Décidé ».
354
+ ⚠️ **Sept valeurs changent avec la taille, et trois avaient été uniformisées** — dont
355
+ l'épaisseur PEINTE de la marque, qui sortait à 1 px au lieu de 1,6666 et se lisait pâle.
356
+ C'est l'œil de Micka qui l'a signalé, pas les contrôles. Voir « Décidé ».
357
+ ⚠️ **Deux écarts au Figma, arbitrés, à remonter dans le Figma** : la bordure de la case
358
+ décochée est foncée à navy-70, le cadre la peignant à 1,44 de contraste — le plus bas
359
+ mesuré sur ce design system, et ici la boîte EST le contrôle ; et l'état de survol, que le
360
+ Figma dessine identique au repos sur ses quarante symboles, est dessiné. Voir « Décidé ».
361
+ ⚠️ **L'indication sous le libellé reste sous le seuil AA**, et ce n'est pas la faute du
362
+ composant : le Figma la peint `text-secondary`, un rôle qui est sous le seuil partout où il
363
+ porte du texte. Épinglée à l'envers dans le vérificateur — voir « En attente ».
364
+ ✅ L'anneau de focus se montre enfin sur une planche, par le crochet `data-zds-demo="focus"`
365
+ que la page Boutons avait déjà et qui n'avait pas été repris.
366
+ ✅ Ajoute `verify:checkboxes`.
367
+ - [x] **Interrupteur** — page en ligne, un composant livré le 2026-09-11. `Toggle` :
368
+ 2 tailles × 2 types (`default`, `slim`) × 4 états, libellé et aide optionnels. Relevé sur
369
+ deux cadres : `_Toggle base` (3896:1480, 32 symboles) et `Toggle` (3896:1553, 64 symboles).
370
+ L'ORGANISATION de la page est celle de la page « Toggles » de la bibliothèque de référence,
371
+ à la demande de Micka : intro, un exemple de tête, Installation, puis les variantes dans son
372
+ ordre — nu, libellé, libellé + aide, désactivé, tailles, slim, slim + libellé et aide.
373
+ ⚠️ **Ce n'est pas une case à cocher, et ça ne se remplace pas** : l'interrupteur applique son
374
+ effet tout de suite. D'où `role="switch"` sur l'input, pour qu'un lecteur d'écran annonce
375
+ « activé » au lieu de « coché ».
376
+ ⚠️ **`slim` n'est pas `default` en plus petit** : en `md`, 40 × 20 contre 44 × 24, pour le
377
+ même bouton de 20. Ce qui change est le retrait, donc la piste du `slim` fait exactement la
378
+ hauteur de son bouton, et c'est sa bordure qui le détache au lieu de son ombre.
379
+ ✅ **Les trois jetons `toggle-*` du thème étaient faux, tous les trois**, et le relevé l'a dit
380
+ d'un coup — dont un dans la rampe grise que le design system n'emploie pas. Voir « Décidé ».
381
+ ✅ **La collection d'ombres de Zest est lue pour la première fois** : elle est teintée
382
+ #0a0d12, et pas #101828 comme la phase 1b le supposait. Voir « Décidé ».
383
+ ✅ **Deux écarts au Figma, arbitrés le jour même** : la bordure de la piste éteinte est
384
+ foncée à navy-70 comme celle de la case à cocher, et l'état éteint gagne un survol que le
385
+ Figma ne dessine pas. **Les deux sont à remonter dans le Figma.** Voir « Décidé ».
386
+ ⚠️ **Une question reste ouverte, et elle vaut aussi pour la case à cocher** : le bouton
387
+ blanc tient 1,11 contre sa piste. Voir « En attente ».
388
+ ✅ Ajoute `verify:interrupteur`.
389
+ - [ ] Champs et liste déroulante — ce qui reste de la phase 2. Les deux pages sont annoncées
390
+ et vides sous `src/docs/a-venir/` : `Champs.mdx` et `Selecteurs.mdx`.
274
391
  - Chaque composant livré retire un morceau du provisoire de `docs.css` — le tableau est
275
392
  dans le `CLAUDE.md`.
276
393
  - [ ] **Phase 3** — application components : navigation, tableaux, modales
@@ -278,8 +395,149 @@ both `tsc` and `storybook build`, and only a real browser found them:
278
395
 
279
396
  ## En attente
280
397
 
398
+ - ⚠️ **LES TABLEAUX MARKDOWN NE SE RENDENT PAS DANS LA DOCUMENTATION, ET 15 S'AFFICHENT EN MUR DE
399
+ BARRES VERTICALES** (constaté le 2026-09-11 en relisant une capture de la page « Interrupteur »).
400
+ Un `| … | … |` écrit dans un `.mdx` ressort tel quel, en une seule ligne de texte : l'extension
401
+ GFM de remark n'est branchée nulle part dans la configuration de Storybook, et `remark-gfm` n'est
402
+ même pas installé.
403
+
404
+ Le compte, relevé page par page dans le navigateur : « Info-bulles » 5, « Cases à cocher » 3,
405
+ « Boutons radio » 3, « Introduction » 2, « Logo » 2. Ce sont pour l'essentiel les tableaux de
406
+ CHOIX (« quel contrôle pour quelle question ») et les tableaux d'API — c'est-à-dire les deux
407
+ endroits où un développeur va chercher sa réponse.
408
+
409
+ ⚠️ **Les tableaux qui se rendent bien sont ceux qui sont des COMPOSANTS.** `<Tableau>` construit
410
+ un vrai `<table>` en React, donc il ne dépend pas du Markdown : c'est pour ça que les tableaux de
411
+ tailles et d'états ont toujours eu l'air corrects, et que le défaut est resté invisible. La page
412
+ « Interrupteur » est passée sur `<Tableau>` pour ses deux tableaux, donc elle n'en porte plus.
413
+
414
+ **Deux réparations possibles, et c'est un arbitrage :** brancher `remark-gfm` dans
415
+ `.storybook/main.ts`, ce qui répare les 15 d'un coup mais ajoute une dépendance de développement
416
+ et change le rendu de cinq pages ; ou convertir les 15 en `<Tableau>`, sans dépendance, mais à la
417
+ main.
418
+
419
+ *Repère : le défaut ne se voit pas en écrivant la page — il se voit en la REGARDANT rendue. Aucun
420
+ contrôle du dépôt ne lit le texte d'une page pour y chercher du Markdown non interprété.*
421
+
422
+ - ⚠️ **LE BOUTON BLANC DE L'INTERRUPTEUR ÉTEINT TIENT 1,11 CONTRE SA PISTE, ET RIEN NE LE RATTRAPE**
423
+ (mesuré le 2026-09-11, en tranchant les deux questions ci-dessus). Une fois la bordure de la piste
424
+ montée à navy-70, la limite du contrôle passe son seuil — mais la POSITION du bouton, qui est ce
425
+ qui porte l'état, reste portée par un disque blanc sur un fond navy-20. En type `default` il n'a
426
+ même pas de bordure : c'est son ombre seule qui le détache, et une ombre ne compte pas dans un
427
+ calcul de contraste.
428
+
429
+ La WCAG 1.4.11 demande 3:1 pour « les parties d'un objet graphique nécessaires à la
430
+ compréhension ». La position du bouton en est une.
431
+
432
+ **Ce n'est pas arbitré, et je ne l'ai pas arbitré tout seul**, pour deux raisons : réparer
433
+ demanderait de changer le dessin du bouton — une bordure sur le type `default`, ou un fond qui
434
+ n'est plus blanc — c'est-à-dire de s'écarter du Figma sur la pièce la plus visible du composant ;
435
+ et c'est un dessin que toutes les bibliothèques du marché livrent tel quel. **La même question se
436
+ pose à l'identique sur la case à cocher décochée**, dont la boîte blanche tient 1,11 contre la
437
+ page. À trancher pour les deux, ou pour aucun.
438
+
439
+ - ⚠️ **`text-secondary` EST SOUS LE SEUIL AA, PARTOUT OÙ IL PORTE DU TEXTE** (relevé le
440
+ 2026-09-10). Le rôle vaut navy-70 (#7e8694), qui tient **3,67** sur du blanc pour un seuil de 4,50
441
+ sur du petit texte. Ce n'est pas le problème d'un composant : c'est le rôle.
442
+
443
+ Découvert en codant la case à cocher, dont le Figma peint le texte d'aide avec ce rôle. Le foncer
444
+ dans ce composant seulement rendrait cette aide différente de toutes les autres du design system,
445
+ donc il est resté tel quel et **épinglé** dans `verify:checkboxes` — le jour où le rôle sera
446
+ foncé, ce contrôle rougira, et ce sera le rappel qu'il faut retirer l'exception.
447
+
448
+ Les paliers : navy-80 (#4d5971) tient 7,04, navy-70 tient 3,67. Il n'y a **rien entre les deux**
449
+ dans la rampe — c'est la même pénurie de paliers sombres que l'échelle de texte documente déjà
450
+ (quatre paliers foncés pour huit rangs). Foncer `text-secondary` sur navy-80 le rendrait égal à
451
+ `text-tertiary`, donc la question n'est pas seulement « quel palier » mais « combien de rangs de
452
+ texte gris nous faut-il vraiment ». **À arbitrer par l'équipe produit, pas par un composant.**
453
+
454
+
281
455
  Des décisions qui appartiennent à Micka ou à l'équipe produit, pas au code.
282
456
 
457
+ - ⚠️ **LA COLLECTION SÉMANTIQUE DU FIGMA NE COUVRE PAS LES FONDS.** C'est la question de fond
458
+ soulevée le 2026-09-09, et elle est plus large qu'un jeton mal choisi.
459
+
460
+ Relevé sur la page « Couleurs » (`4:8067`) : la collection « 1. Usage des couleurs » nomme
461
+ `Couleurs/Texte/`, `Couleurs/Bordure/` et `Couleurs/Effets/Ombres/`. **Il n'y a pas de
462
+ `Couleurs/Fond/`.** Conséquence directe et vérifiable : le Figma peint lui-même l'aplat du
463
+ `Secondary gray` avec `Couleurs/Bleu marine/20`, une RAMPE BRUTE, parce qu'aucun jeton sémantique
464
+ ne nomme cette surface.
465
+
466
+ Côté thème, la couture est la même vue d'un autre angle : `text-*` et `border-*` sont passés sur
467
+ le navy de Zest le 2026-09-07, `bg-*` et `fg-*` sont restés sur la rampe gris NEUTRE, celle que
468
+ Zest n'emploie pas pour son interface. `--zds-color-bg-secondary` vaut `gray-50` (`#FAFAFA`) quand le Figma peint
469
+ `navy-20` (`#F1F3F5`) — deux familles différentes, donc aucun rôle existant ne peut nommer ces
470
+ aplats sans mentir.
471
+
472
+ Les quatre aplats du bouton sont donc écrits en jetons de COMPOSANT
473
+ (`--zds-color-button-secondary-bg` & co) plutôt qu'en repointant `bg-secondary`, ce qui aurait
474
+ déplacé toutes les surfaces du paquet. **Deux décisions à prendre, dans cet ordre :**
475
+
476
+ 1. **ajouter un groupe `Couleurs/Fond/` à la collection sémantique du Figma** — c'est lui qui
477
+ manque, et tant qu'il manque chaque composant réinventera ses noms d'aplat ;
478
+ 2. **repointer `bg-*` et `fg-*` sur le navy** une fois ce groupe écrit.
479
+
480
+ Le bouton est le premier composant à s'asseoir sur cette couture ; le deuxième la retrouvera.
481
+ `yarn verify:tokens` garde la trace : il échoue si un jeton de composant redouble un rôle
482
+ existant de la MÊME catégorie, et laisse passer les fonds — l'exception est explicite dans le
483
+ script, pas oubliée.
484
+ - ⚠️ **LE CORPS DE TEXTE DE LA DOCUMENTATION N'EST PAS SUR L'ÉCHELLE DU DESIGN SYSTEM.** Mesuré
485
+ dans le navigateur le 2026-09-09 : les paragraphes des pages Storybook rendent à **14 px** en
486
+ `#2E3438` — ni le `body-md` de 16 px de l'échelle, ni aucune couleur de Zest (`text-tertiary`
487
+ vaut `#4D5971`). La POLICE avait été corrigée en septembre via `fontBase` dans `preview.ts` ; la
488
+ taille et la couleur ne l'ont pas été.
489
+
490
+ ⚠️ **Et `.prose` ne peut pas servir à ça**, ce qui a été vérifié empiriquement plutôt que
491
+ supposé : posée sur `.sbdocs-content`, elle ne change QUE la couleur héritée du conteneur. Les
492
+ paragraphes restent à 14 px, les titres à 24, la largeur à 1000. C'est par construction — son
493
+ en-tête dit que « chaque sélecteur est enveloppé dans `:where()`, donc à spécificité nulle :
494
+ cette feuille propose une mise en page, elle ne l'impose pas ». Les règles de Storybook la
495
+ battent partout. Deux autres raisons l'excluent de toute façon : elle plafonne à **720 px**, ce
496
+ qui écraserait les aperçus et les planches pleine largeur, et la page « Typographie » la
497
+ DÉMONTRE — un spécimen posé dans un conteneur identique à lui ne démontre plus rien.
498
+
499
+ **La voie praticable** est celle que `preview.ts` a déjà prise pour la police : des règles dans
500
+ `docs.css`, avec la spécificité nécessaire pour battre Storybook. Ça touche l'aspect des douze
501
+ pages, d'où l'arbitrage. **Faut-il aligner le corps de texte de la doc sur l'échelle ?**
502
+ - **Les deux bornes du biseau skéuomorphique** (2026-09-09). Le MCP Figma rapporte le contour des
503
+ aplats comme plat, sur deux lectures ; le dégradé est donc implémenté d'après la recette standard
504
+ (`rgba(255,255,255,0.12)` en haut, extinction complète en bas) et non d'après un relevé. **À
505
+ confirmer dans le Figma**, ou à corriger sur les deux jetons `--zds-color-button-bevel-*`.
506
+ - **Le tracé du spinner est une construction, pas un relevé** (2026-09-09). Le cadre
507
+ `Buttons/Button loading icon` (3861:2509) existe et donne deux tailles, 20 et 24 px, mais son
508
+ export SVG passe par une URL que la machine de relevé n'a pas le droit d'appeler. Ce qui est
509
+ livré est un anneau conventionnel — piste à 30 % du `currentColor`, tête d'un quart de tour —
510
+ qui a l'avantage de suivre la couleur du texte, donc de servir en blanc sur un Primary comme en
511
+ rouge sur un destructif. **À remplacer par le vrai tracé**, ou à valider tel quel. Et de toute
512
+ façon un cadre statique ne décrit ni durée, ni sens, ni courbe d'accélération.
513
+ - **Le soulignement des liens au survol** (2026-09-09). Le Figma le peint plus CLAIR que le
514
+ libellé — texte au palier 700, trait au 500 — et il fait pareil sur le lien destructif. Mais
515
+ `link-gray` ne suit pas : son trait suit son texte. Reproduit tel quel, pas harmonisé. **Motif
516
+ ou oubli ?**
517
+
518
+
519
+ - ⚠️ **LE BADGE OFFRE DEUX NEUTRES, ET ILS SE RESSEMBLENT** (2026-09-10). `navy`, le neutre de
520
+ Zest, et `gray`, celui de la livraison de référence. Trois indices font penser à une migration
521
+ inachevée plutôt qu'à un choix : le fond du badge `navy` est encore peint dans le Figma par
522
+ `Component colors/Utility/Slate/utility-slate-50` (#f8fafc), une variable d'une **collection
523
+ tierce** étrangère à celle de Zest ; le `gray` porte son libellé sur la rampe grise, que Zest
524
+ n'emploie pas pour ses textes ; et rien ne dit lequel employer quand.
525
+
526
+ Les deux sont livrés, parce que les deux sont dessinés. `verify:tokens` porte une exception
527
+ **énumérée** — cinq citations nommées dans `GRIS_ADMIS` — et il échoue aussi si l'une d'elles
528
+ cesse d'être citée : le jour où `gray` disparaît, la dispense part avec lui. **À trancher.**
529
+
530
+ - **Quatre fonds de badge s'écartent du Figma, et c'est mesuré** (2026-09-10). Le Figma peint le
531
+ fond de `brand`, `success`, `horizon` et `fuschia` avec le palier `Zest/10`, celui que notre
532
+ échelle 50 → 950 laisse dehors. Les quatre prennent le `-50` : ΔE 5,46 · 5,79 · 4,45 · 8,79.
533
+ Les quatre restent au-dessus de l'AA sous leur libellé — l'écart coûte une nuance, pas une
534
+ lisibilité. Récupérer `Zest/10` demanderait un palier `25` sur les rampes, ce qui est la
535
+ décision de design system déjà listée dans le `CLAUDE.md`.
536
+
537
+ Le badge `navy` est dans le même cas, l'écart en moins : `utility-slate-50` #f8fafc contre notre
538
+ `navy-10` #f9fafb, **ΔE 0,61** — invisible à l'œil et mesurable. Substitué sans hésitation, mais
539
+ le Figma reste à repointer.
540
+
283
541
  - ⚠️ **Les deux aplats de statut ne tiennent pas l'AA — et ils ont maintenant des victimes.**
284
542
  `yarn verify:labels` sort en échec sur **dix étiquettes d'icône de fichier** : CSV, XLS et XLSX
285
543
  portent du blanc sur `success-600` (3,59:1), PPT et PPTX sur `warning-600` (3,49:1), en Default
@@ -287,10 +545,36 @@ Des décisions qui appartiennent à Micka ou à l'équipe produit, pas au code.
287
545
  contrôle échoue exprès pour qu'elle ne s'oublie pas. `bg-warning-solid` (`#DC6803`) tient
288
546
  **3,49:1** et `bg-success-solid` (`#119B57`) **3,59:1** sous du texte blanc, pour un seuil AA
289
547
  de 4,5:1. `bg-error-solid` (4,83) et `bg-brand-solid` (4,54) passent, de justesse pour le
290
- second. Les fonds **doux** vont bien (4,69 à 6,05) : le défaut ne touche que les aplats. Trois
291
- issues, à choisir un badge et un bouton sous les yeux — descendre les deux aplats sur le palier
292
- **700** (5,43 et 5,08) ; ne jamais poser de texte blanc dessus, en privilégiant les badges en
293
- fond doux ; ou assumer, si ces aplats ne portent jamais de libellé.
548
+ second. Les fonds **doux** vont bien : le défaut ne touche que les aplats.
549
+
550
+ ⚠️ **CETTE ENTRÉE A LONGTEMPS DIT DEUX CHOSES FAUSSES**, corrigées le 2026-09-10.
551
+
552
+ **Un.** Elle annonçait que ces deux jetons bloquaient « le premier badge ». C'est faux : le
553
+ cadre `Badge` ne dessine aucun aplat, ses trois habillages sont deux fonds doux et un fond
554
+ blanc. Aucun des deux jetons n'y est cité. Seul le premier bouton PLEIN de statut les attend.
555
+
556
+ **Deux.** Elle présentait les dix étiquettes d'icône de fichier comme des « victimes » de cette
557
+ décision. Elles n'en sont pas, et ça a été vérifié plutôt que déduit : en basculant les deux
558
+ jetons sur le 700, `verify:labels` sort **les dix mêmes échecs, aux mêmes 3,59 et 3,49**. Les
559
+ SVG citent `--zds-color-success-600` et `--zds-color-warning-600` DIRECTEMENT, pas le jeton
560
+ d'aplat. C'est un problème indépendant, dont l'en-tête du catalogue nomme déjà la cause : « le
561
+ design system n'a ni orange, ni vert, ni rouge utilitaires », donc CSV et PPT empruntent les
562
+ rampes sémantiques faute de teinte de catégorisation. À trancher à part, et `verify:labels`
563
+ devrait cesser d'accuser `bg-*-solid`.
564
+
565
+ **Ce qui reste, et ce que la mesure ajoute.** Descendre les deux aplats sur le **700** (5,08 et
566
+ 5,43) répare le statut et rien de plus : mesuré sur les 13 couleurs de la palette, un aplat
567
+ saturé sous du texte blanc échoue pour **7 couleurs sur 13** au palier 600 et encore **3 sur 13**
568
+ au palier 700 — lime, jaune et turquoise, qu'il faudrait descendre au 800 ou au 900, où le lime
569
+ n'est plus du lime. Autrement dit : **il n'existe aucun palier qui fasse marcher un BOUTON PLEIN
570
+ sur toute la palette.**
571
+
572
+ ⚠️ **Ne pas lire cette dernière phrase comme un verdict sur le badge.** Le badge, lui, a été
573
+ réglé le 2026-09-10 en descendant ces trois teintes de deux crans — voir « Décidé » — et ça
574
+ marche parce que son FOND reste au palier 50 : c'est le fond pâle qui porte l'identité de la
575
+ teinte, et seuls le texte et la pièce foncent. Sur un aplat saturé c'est l'inverse : le fond EST
576
+ la teinte, et le foncer la détruit. La même couleur, deux conclusions opposées, parce que ce
577
+ n'est pas la même surface qui porte l'information.
294
578
  - **La ligne de `.prose` fait 103 caractères**, mesurés ici (Averta 16 px sur 720 px, médiane sur
295
579
  les lignes pleines de l'article d'exemple). Le confort de lecture se situe entre 45 et 75, et
296
580
  85 est la limite haute usuelle. Les 720 px viennent pourtant du Figma (`paragraph-max-width`)
@@ -309,19 +593,53 @@ Des décisions qui appartiennent à Micka ou à l'équipe produit, pas au code.
309
593
  du **texte** mesuré, pas du réglage : les deux articles d'exemple n'ont ni les mêmes mots ni la
310
594
  même densité d'élisions. C'est une raison de plus de ne pas recopier un chiffre d'un dépôt à
311
595
  l'autre. À arbitrer, parce que ça touche un jeton du Figma.
312
- - ⚠️ **Il n'y a pas de composant `Avatar`, et il en faut un.** Le Figma V2.0 n'en contient aucun :
313
- `3857:23794` est une banque de photos. Trois spécimens existent dans l'ancienne bibliothèque
314
- *Design System - ZestMeUp* — `Zestie avatar`, `Anonymous avatar`, `Avatars` — dernière mise à
315
- jour 2024, et ce n'est pas la source déclarée du V2.0. **Faut-il les porter, ou en dessiner un
316
- dans le V2.0 ?** Rien n'a été codé en attendant : la page montre des règles, pas un composant.
317
- - **Les avatars à initiales.** La règle Zest les interdit ; la livraison de référence a tranché
318
- que la règle vaut pour les *produits*, pas pour une bibliothèque un composant *offre* un
319
- état, l'écran choisit de le rendre. ⚠️ `quote-avatar.example.tsx` énonce aujourd'hui la règle
320
- produit comme si c'était la règle de la bibliothèque. La page « Avatars » montre la variante à
321
- initiales **uniquement pour dire qu'on ne l'emploie pas** ; elle n'existe nulle part ailleurs.
322
- - **L'échelle de tailles d'avatar**24, 32, 40, 48, 64, 96 est une **proposition**. Le Figma
323
- n'en donne aucune ; ces paliers viennent de l'échelle d'espacement du thème parce qu'ils
324
- existent déjà. À arbitrer en même temps que le composant.
596
+ - ⚠️ **DEUX ICÔNES ET UN STATUT À FINIR DE CADRER, côté Figma** (2026-09-10). Le composant Avatar
597
+ est livré, mais trois choses lui restent dues :
598
+
599
+ `message-alert-square-01` et `-02` ont été **ajoutées au catalogue à la main** depuis le cadre
600
+ `31:9082`. Le catalogue se dit « généré » : il n'existe en réalité aucun générateur dans le
601
+ dépôt, et les 135 entrées ont toutes été relevées de la même façon. Ce n'est pas un défaut, mais
602
+ l'en-tête du catalogue promet une régénération qui n'est pas outillée. **À écrire, ou à
603
+ reformuler.**
604
+
605
+ Le statut « Com. sensible » porte une **étiquette abrégée** dans le Figma, et rien dans les
606
+ projets Zest ne la définissaitil a fallu demander. L'API l'appelle `sensitive-comment` et la
607
+ page écrit « commentaire sensible ». **À renommer dans le Figma** pour que le prochain relevé
608
+ n'ait pas à demander.
609
+
610
+ Enfin ses trois cadres portent des variables d'une **collection tierce** :
611
+ `utility-neutral-300` peint le statut hors ligne, `Colors/Background/bg-primary` l'anneau du
612
+ groupe, et `Bleu marine/100` existe en double — une fois dans `Couleurs/`, une fois hors
613
+ collection. Substitués par mesure, **le Figma reste à repointer**.
614
+
615
+ - ⚠️ **LE RAYON DE L'INFO-BULLE EST ÉCRIT AVEC UN JETON D'ESPACEMENT DANS LE FIGMA** (relevé le
616
+ 2026-09-10 sur le spécimen `Size=md, State=Hover` du cadre `_Avatar add button`, 3881:4984) —
617
+ `rounded-[var(--spacing-md)]`, et non un jeton de rayon. Les 8 px tombent juste par
618
+ coïncidence : c'est aussi la valeur du `radius-sm` du Figma. **À repointer** avant que quelqu'un
619
+ ne change l'échelle d'espacement.
620
+
621
+ ✅ **Le reste de cette entrée est clos depuis le 2026-09-10.** Elle disait que l'info-bulle du
622
+ bouton d'ajout d'un groupe d'avatars attendait le composant `Tooltip`, et que le bouton portait
623
+ un `title` de navigateur en attendant. `Tooltip` est livré, le bouton le porte, et le `title` a
624
+ été retiré — voir « Décidé ». Le relevé du spécimen reste ici, puisque c'est lui qui porte la
625
+ question du rayon ouverte ci-dessus, et qu'il dit à quoi la bulle du bouton d'ajout doit
626
+ ressembler :
627
+
628
+ | Ce que c'est | Ce que le Figma peint |
629
+ |---|---|
630
+ | Fond | `Couleurs/Bleu marine/100` (#21304e) |
631
+ | Libellé | `caption-md` 12/18 semibold, blanc, centré, sans retour |
632
+ | Padding | `spacing-lg` 12 px à l'horizontale, `spacing-md` 8 px à la verticale |
633
+ | Rayon | 8 px |
634
+ | Flèche | 16 × 6, sous la bulle, centrée |
635
+ | Ombre | `0 12px 8px rgb(16 24 40 / 0.08)`, `0 4px 3px rgb(16 24 40 / 0.03)` |
636
+ | Position | au-dessus du bouton, centrée, base à 43 px de son centre |
637
+
638
+ - ⚠️ **LA PLANCHE DU REPLI DE LA PAGE FOUNDATIONS DESSINE SES RONDS À LA MAIN** (2026-09-10). Elle
639
+ a été faite quand aucun composant n'existait. La basculer sur `<Avatar>` changerait le dessin de
640
+ cette page — un arbitrage de design et non un rangement — donc c'est signalé et non appliqué,
641
+ comme `.zds-doc-copie` l'est pour les boutons. Même remarque pour la planche des tailles, dont
642
+ l'échelle est désormais fausse : la page le dit en rouge au lieu de la refaire.
325
643
  - **L'aire de protection et la taille plancher du logo** sont une **proposition** elles aussi :
326
644
  une demi-hauteur d'isotype, et 24 px sous lesquels on passe à l'isotype seul. Le Figma ne dit
327
645
  rien des deux. Dessinées dans la page pour être discutées.
@@ -360,6 +678,796 @@ Des décisions qui appartiennent à Micka ou à l'équipe produit, pas au code.
360
678
  Le journal des arbitrages. Plusieurs de ces choix paraissent arbitraires et sont le résultat
361
679
  d'une mesure ou d'une décision produit — ne pas les « harmoniser » sans lire l'entrée.
362
680
 
681
+ - ⚠️ **LES TITRES DE PAGES SONT PASSÉS EN FRANÇAIS LE 2026-09-11** (demande de Micka). Quatre
682
+ restaient en anglais : « Checkboxes » devient **Cases à cocher**, « Radio buttons » **Boutons
683
+ radio**, « File icons » **Icônes de fichier** et « Flag icons » **Drapeaux**.
684
+
685
+ Les trois SECTIONS restent en anglais — « Foundations », « Base components », « Application
686
+ components ». Ce n'est pas un oubli : le préfixe du titre est ce sur quoi `verify:foundations`
687
+ partage les pages de composant des pages de fondations, et le renommer changerait l'identifiant
688
+ de toutes les pages du dépôt d'un coup.
689
+
690
+ ⚠️ **« Drapeaux » ANNULE UNE DÉCISION DE MICKA LUI-MÊME.** Cette page avait été renommée
691
+ « Flag icons » — en anglais — à sa demande ; la règle du jour la ramène au français. Les deux
692
+ fois, c'est bien lui qui a tranché. C'est écrit ici pour que personne ne « répare » le nom en
693
+ tombant sur l'ancienne entrée.
694
+
695
+ **Un titre n'est pas une étiquette, c'est l'identifiant de la page.** Storybook le dérive du
696
+ `<Meta title>` : « Cases à cocher » donne `base-components-cases-à-cocher--docs`, accents
697
+ compris, à encoder en URL comme `Foundations/Icônes` l'était déjà. Ont dû suivre le renommage :
698
+ l'ordre de la barre latérale dans `.storybook/preview.ts`, les deux pages et leurs identifiants
699
+ de repli dans `verify:checkboxes`, les renvois d'une page à l'autre, et les commentaires des
700
+ planches. Et `verify:render` lit les identifiants dans `storybook-static/index.json` : sans un
701
+ `yarn build-storybook` rejoué, il visite les anciens.
702
+
703
+ - ⚠️ **LA BORDURE DE L'INTERRUPTEUR ÉTEINT EST FONCÉE À navy-70, COMME CELLE DE LA CASE À COCHER**
704
+ (2026-09-11, arbitrage délégué après mesure). Le Figma peint la limite du contrôle
705
+ `border-primary` (navy-40), qui tient **1,44** sur du blanc pour un seuil de 3:1, sur un fond
706
+ navy-20 qui tient 1,11.
707
+
708
+ **Ce dépôt tranche ce cas sur une seule question, et elle est mesurable :** y a-t-il autre chose,
709
+ DANS le contrôle, qui le rende identifiable à 3:1 ? Le bouton garde son libellé à 11,82, donc sa
710
+ bordure est restée au Figma à 2,17. La case à cocher n'a rien, donc elle est montée à navy-70.
711
+ L'interrupteur éteint, mesuré : son bouton blanc tient **1,11** contre sa piste, et en `slim` la
712
+ bordure de ce bouton tient **1,30**. Rien n'atteint 3 — il est dans le cas de la case, pas dans
713
+ celui du bouton. navy-70 tient 3,67.
714
+
715
+ ⚠️ **ET LA BORDURE DU BOUTON DU TYPE `slim` SUIT, pas seulement la piste.** En `slim` le bouton
716
+ remplit la piste : sa bordure est la SEULE chose qui l'en sépare, et la position du bouton EST
717
+ l'état. La foncer d'un côté seulement donnerait un anneau extérieur net autour d'un bouton
718
+ invisible — 3,30 contre la piste au lieu de 1,30.
719
+
720
+ ⚠️ **TROISIÈME FOIS QUE CET ARBITRAGE EST RENDU À PARTIR DE navy-40** — la case à cocher, le point
721
+ « hors ligne » de l'avatar, l'interrupteur. **Ce n'est plus une exception, c'est la règle du
722
+ dépôt**, et elle s'énonce en une phrase : *une limite de contrôle qui n'est rattrapée par rien
723
+ d'autre monte à navy-70.* À remonter dans le Figma, pour les trois.
724
+
725
+ ⚠️ **Le jeton retrouve son NOM d'origine.** `--zds-color-toggle-border` était le jeton porté du
726
+ thème de référence, supprimé le matin même parce qu'il valait `gray-300` et qu'un rôle suffisait.
727
+ Il revient le soir, à navy-70 : **son nom était juste, sa valeur était fausse, et le rôle ne
728
+ suffisait plus une fois l'arbitrage rendu.**
729
+
730
+ - ⚠️ **L'INTERRUPTEUR ÉTEINT GAGNE UN SURVOL, QUE LE FIGMA NE DESSINE PAS** (2026-09-11, arbitrage
731
+ délégué). Les seize symboles `Pressed=False, State=Hover` sont identiques, propriété par
732
+ propriété, à leurs `State=Default`. Même constat que sur la case à cocher le 2026-09-10, et même
733
+ décision : un contrôle qui applique son effet AU CLIC et qui ne répond pas au survol paraît inerte.
734
+
735
+ **Une seule règle couvre les deux états** — « au survol, la piste descend d'un cran de sa rampe » :
736
+ navy-20 → navy-30 éteinte, brand-500 → brand-700 allumée. Micka doit pouvoir la réexpliquer de
737
+ mémoire, et une exception greffée sur l'éteint l'en empêcherait.
738
+
739
+ ⚠️ **LE CRAN EST DISCRET, ET C'EST MESURÉ AVANT D'ÊTRE ASSUMÉ : 1,12** contre la piste au repos.
740
+ Le palier suivant, navy-40, tiendrait 1,30 — et ce ne serait plus un cran. Un survol n'est pas un
741
+ état qu'on lit, c'est une confirmation que le contrôle est vivant ; le rendre spectaculaire le
742
+ ferait concurrencer l'état allumé, qui lui porte l'information. À remonter dans le Figma.
743
+
744
+ ✅ **`verify:interrupteur` vérifie les deux sens.** Le survol doit exister sur l'éteint ET sur
745
+ l'allumé : un ajout au Figma que rien ne garde disparaît sans laisser de trace — la piste
746
+ resterait plausible, et ni un test ni une capture ne le diraient.
747
+
748
+ - ⚠️ **ET LE CONTRASTE ÉPINGLÉ A ATTRAPÉ UN DÉFAUT DE LA SONDE, PAS DU COMPOSANT** (2026-09-11).
749
+ En ajoutant le survol de l'éteint, `verify:interrupteur` s'est mis à mesurer la piste au repos à
750
+ **1,24 au lieu de 1,11** : le test de survol laisse la souris posée sur un interrupteur, et la
751
+ mesure des contrastes, qui vient après, lisait donc une piste SURVOLÉE.
752
+
753
+ La sonde éloigne maintenant la souris et attend la transition de 0,15 s. **Sans l'épingle, elle
754
+ aurait rapporté une valeur plausible et fausse** — 1,24 ne ressemble pas à une erreur.
755
+
756
+ *Repère : une sonde qui simule une interaction laisse le navigateur dans l'état où elle l'a mis.
757
+ Ce qu'elle mesure ensuite n'est pas l'état au repos.*
758
+
759
+ - ⚠️ **LES TROIS JETONS `toggle-*` PORTÉS DANS LE THÈME ÉTAIENT FAUX, TOUS LES TROIS** (2026-09-11).
760
+ `--zds-color-toggle-border` valait `gray-300` — la rampe que ce design system n'emploie pas —
761
+ quand le Figma peint la piste éteinte avec `border-primary`, un RÔLE qui existait déjà : le jeton
762
+ de composant n'avait même pas lieu d'être, et il a été supprimé plutôt que corrigé. Les deux
763
+ autres peignaient la bordure du bouton fin en brand-600 et son survol en brand-700, quand le Figma
764
+ dit brand-500 et brand-700.
765
+
766
+ C'est exactement ce qui était arrivé à `--zds-color-tooltip-supporting-text` la veille, et pour la
767
+ même raison : **rien ne les citait, donc aucun contrôle ne les lisait.** La prédiction écrite ici
768
+ le 2026-09-10 — « des ports, pas des relevés, à vérifier au moment où l'interrupteur sera codé » —
769
+ s'est vérifiée à 3 sur 3.
770
+
771
+ *Repère : un jeton porté d'un autre thème est une hypothèse, pas une valeur. Tant que rien ne le
772
+ cite, il est invérifiable ; le jour où on le cite, il faut le relire au Figma AVANT de s'en
773
+ servir.*
774
+
775
+ ⚠️ **ET LE SUPPRIMER A LAISSÉ DEUX ÉCHOS.** `--zds-color-toggle-border` servait d'EXEMPLE de
776
+ jeton de composant à deux endroits — la page « Couleurs » et l'exemple de prose de la page
777
+ « Typographie » — qui ont donc cité un jeton mort le temps d'un commit. Les deux pointent
778
+ désormais sur `--zds-color-toggle-bg-pressed`, qui existe et qui illustre mieux la règle : aucun
779
+ rôle de FOND ne porte brand-500. Aucun contrôle ne l'a vu, parce qu'un nom de jeton écrit dans
780
+ une page est du texte, pas une citation.
781
+
782
+ - ⚠️ **LA COLLECTION D'OMBRES DE ZEST EST TEINTÉE #0a0d12, ET NON #101828** (relevé le 2026-09-11,
783
+ premier accès à `Couleurs/Effets/Ombres`). Le relevé donne `shadow-xs` à 5 %, `shadow-md_01` à
784
+ 10 % et `shadow-md_02` à 6 %, tous sur #0a0d12 — c'est-à-dire le `gray-950` de la livraison de
785
+ référence, pas la teinte que l'entrée ouverte de la phase 1b annonçait.
786
+
787
+ La teinte est **substituée** au noir de notre rampe sur l'ombre du bouton de l'interrupteur, comme
788
+ elle l'avait été sur l'info-bulle : à 10 % sur du blanc, l'écart ne fait pas trois niveaux sur
789
+ 255, et la cohérence du thème vaut plus. **La question des sept ombres reste ouverte** — ce n'est
790
+ pas depuis un composant qu'on la tranche — mais elle a maintenant sa vraie valeur de départ.
791
+
792
+ ⚠️ **Et la géométrie, elle, diverge.** L'`ombre-md` du Figma est `0 4px 6px 0` puis
793
+ `0 2px 4px -2px`, là où `--zds-shadow-md` écrit `0 4px 6px -1px` sur sa première couche. Une
794
+ couche sur deux : le jeton ne se réemploie pas, d'où `--zds-shadow-toggle-knob`. L'entrée de la
795
+ phase 1b disait « la géométrie est la même de part et d'autre, la teinte changera » : **les deux
796
+ moitiés de cette phrase sont fausses.**
797
+
798
+ - ⚠️ **0,5 px ET 1 px SONT LE MÊME PIXEL SUR UN ÉCRAN À 1×** (2026-09-11). Le Figma donne deux
799
+ épaisseurs de bordure à la piste éteinte selon le type — 0,5 sur le `default`, 1 sur le `slim` —
800
+ et Chromium arrondit une bordure sous-pixel à un pixel d'écran. Les deux rendent donc 1 px, et la
801
+ distinction n'apparaît qu'à partir de 2×.
802
+
803
+ `verify:interrupteur` mesure les DEUX : l'épaisseur que la feuille déclare, où la distinction
804
+ vit, et celle que le navigateur peint, comparée à ce que la densité de l'écran permet. Le premier
805
+ jet ne mesurait que la peinte, et reprochait au composant un arrondi du navigateur.
806
+
807
+ *Repère : une mesure prise dans un navigateur est prise sur UNE densité d'écran. Ce qui s'y voit
808
+ n'est pas toujours ce que la feuille demande.*
809
+
810
+ - ⚠️ **L'HABITUDE D'AVANT-COMMIT COMPTE QUATRE CONTRÔLES DEPUIS LE 2026-09-10**, et le quatrième est
811
+ celui qui manquait : **aucun récit de fabrication dans une page de composant**. Micka l'a demandé
812
+ deux fois ; à la seconde, il est devenu un script.
813
+
814
+ `verify:foundations` relit donc les pages dont le `<Meta>` commence par « Base components/ » et
815
+ refuse huit tournures — « relevé », « mesuré », « un premier jet », « arbitré », « le Figma
816
+ dessine… », « à remonter dans le Figma », un numéro de nœud, « symbole ». Les pages Foundations sont
817
+ écartées : elles MESURENT par conception, le mot y est le sujet. Le partage se fait sur le TITRE,
818
+ pas sur un nom de fichier — c'est le titre qui décide du rangement.
819
+
820
+ ⚠️ **UNE EXCEPTION ÉNUMÉRÉE, et elle est instructive.** La page « Badges » écrit « le choix entre
821
+ eux n'est pas arbitré » : le mot est le même, l'usage est l'inverse — cette phrase DIT au
822
+ développeur que le choix lui revient. Le contrôle échoue aussi si l'exception cesse d'être
823
+ employée, comme `GRIS_ADMIS`.
824
+
825
+ Le contrôle a trouvé quatre passages sur trois pages écrites plus tôt : les notes de clôture de
826
+ « Badges » et « Avatars » qui décrivaient notre outillage de vérification, une phrase de « Boutons »
827
+ sur le même sujet, et une note de l'avatar **périmée en plus d'être narrative** — elle annonçait
828
+ encore une info-bulle provisoire alors que `Tooltip` était arrivé et que le bouton d'ajout
829
+ l'employait.
830
+
831
+ *Repère : une règle qu'on s'écrit ne tient pas. Le réflexe d'écrire ce qu'on vient de comprendre là
832
+ où on vient de le comprendre est plus fort qu'une consigne — il faut un contrôle.*
833
+
834
+ - ⚠️ **`git checkout -- <fichier>` A EFFACÉ DU TRAVAIL NON COMMITÉ, POUR LA DEUXIÈME FOIS**
835
+ (2026-09-10). Employé pour défaire une mutation de test, il a ramené trois pages à HEAD — et
836
+ emporté avec la mutation **trois corrections de documentation venant d'être écrites**.
837
+
838
+ La première fois, c'étaient quatre correctifs de `verifier-jetons.mjs`, et la leçon était déjà dans
839
+ ce README. Elle n'a pas tenu, parce qu'au moment du nettoyage on pense « annuler la mutation », pas
840
+ « ce fichier contient aussi autre chose ». La commande est la même ; git ne connaît pas l'intention.
841
+
842
+ Et l'effacement est **silencieux** : la commande réussit, le fichier existe, les tests passent —
843
+ ils portaient sur autre chose. Ce qui l'a révélé, c'est un `grep -c` sur les phrases censées avoir
844
+ disparu.
845
+
846
+ **La règle : restaurer une mutation depuis une COPIE prise juste avant (`cp`), jamais depuis git**,
847
+ et vérifier la restauration par son CONTENU, pas par l'absence d'erreur.
848
+
849
+ - ⚠️ **UN ÉTAT QU'AUCUNE PLANCHE NE MONTRE EST UN ÉTAT QUE PERSONNE NE RELIT** (2026-09-10, question
850
+ de Micka : « pourquoi il n'y a pas le focus ring pour le state focus ? »). L'anneau de la case à
851
+ cocher était juste, le vérificateur le mesurait, et **aucune planche ne l'affichait** : le tableau
852
+ des états en annonçait quatre pour trois visibles.
853
+
854
+ Deux raisons se cumulaient, et les deux sont normales : `:focus-visible` ne se déclenche qu'à la
855
+ navigation clavier — un clic ne montre rien, mesuré à `box-shadow: none` — et un seul élément du
856
+ document tient le focus à la fois, donc une planche ne peut pas le montrer sur trois spécimens.
857
+
858
+ La page Boutons avait déjà résolu ça avec `data-zds-demo="focus"`, un crochet qui **partage une
859
+ seule déclaration** avec `:focus-visible` pour que la démonstration ne puisse pas dériver. **Il n'a
860
+ simplement pas été repris.** Il l'est, sur les deux pages, et `verify:checkboxes` referme la porte
861
+ comme `verify:buttons` : un vrai focus clavier contre un forcé, comparés octet pour octet, avec un
862
+ refus de conclure si l'un des deux ressort vide — deux absences se comparent égales.
863
+
864
+ *Repère : quand un composant a un état que le Figma dessine, vérifier que la PAGE peut le montrer,
865
+ pas seulement que le CSS le déclare. Les deux questions sont différentes, et la seconde ne répond
866
+ pas à la première.*
867
+
868
+ ⚠️ **Et la sonde marque son témoin par un ATTRIBUT, pas par son identifiant.** Le `useId` de React
869
+ fabrique des identifiants à deux-points (« :r0: ») : valides en HTML, corrects pour un `for`, et
870
+ invalides dans un sélecteur CSS. Une sonde bâtie dessus échoue sur son propre sélecteur, pas sur un
871
+ défaut du composant.
872
+
873
+ - ⚠️ **UNE PAGE DE COMPOSANT EST UNE CONSIGNE, PAS UN CARNET DE BORD** (2026-09-10, demande de
874
+ Micka). Les pages « Cases à cocher » et « Info-bulles » avaient dérivé vers le récit de leur
875
+ FABRICATION : ce que le Figma dessine, ce qui a été mesuré, ce qu'un premier jet rendait, qui a
876
+ arbitré quoi, quel nœud est cité. Rien de tout ça n'aide un développeur à choisir une taille ou à
877
+ écrire un libellé.
878
+
879
+ Une page sous « Base components » répond à trois questions et pas une de plus : **quand l'employer**,
880
+ **quelle variante**, **ce que l'API prend**. Le reste — les relevés, les écarts à la maquette, les
881
+ défauts trouvés et comment — vit dans ce README et dans le CLAUDE.md, où il était déjà. Les pages
882
+ Foundations sont l'exception et continuent de mesurer : mesurer EST leur sujet.
883
+
884
+ Les tournures à chercher avant de livrer une page : « relevé », « mesuré », « un premier jet »,
885
+ « arbitré », « le Figma dessine », « à remonter dans le Figma », un numéro de nœud, « symbole ».
886
+
887
+ *Repère : j'avais déjà cette règle en note et je l'ai quand même enfreinte deux fois. Le réflexe
888
+ d'écrire ce qu'on vient de comprendre là où on vient de le comprendre est fort — la relecture de la
889
+ page doit se faire en se demandant « qu'est-ce qu'un dev fait de cette phrase ? ».*
890
+
891
+ - ⚠️ **SEPT VALEURS CHANGENT AVEC LA TAILLE DE LA CASE, ET TROIS AVAIENT ÉTÉ UNIFORMISÉES**
892
+ (2026-09-10). La boîte, le rayon, l'écart au texte, l'écart entre les deux textes, le palier
893
+ typographique, **la part de la marque** et **son épaisseur de trait**.
894
+
895
+ taille boîte marque part trait peint point radio
896
+ sm 16 12 75 % 1,6666 37,5 %
897
+ md 20 14 70 % 2 40 %
898
+
899
+ Les trois dernières colonnes avaient une seule valeur, et **ça se voyait** : la coche sortait à 1 px
900
+ au lieu de 1,6666, donc pâle. **C'est l'œil de Micka qui l'a signalé, pas les contrôles** — qui
901
+ mesuraient la part de la marque à une seule valeur et ne regardaient pas le trait du tout.
902
+
903
+ ⚠️ **Et le trait ne suit pas l'échelle du dessin.** Les coordonnées du tracé la suivent
904
+ exactement — celles du Figma sont celles de notre catalogue multipliées par 12/24 puis 14/24, au
905
+ chiffre près. Le TRAIT, lui, reste épais : une réduction proportionnelle donnerait 1 px en `sm`.
906
+ C'est un choix de dessin — une coche fine dans une petite boîte disparaît. `stroke-width`
907
+ s'exprimant dans les unités du `viewBox` (24 pour nos icônes), peindre une épaisseur E sur une
908
+ marque de M pixels demande `E × 24 / M`. `verify:checkboxes` mesure l'épaisseur PEINTE, jamais
909
+ l'attribut.
910
+
911
+ *Repère : quand une taille change, lister ce qui change AVEC elle avant de coder. Une proportion
912
+ qui vaut pour une taille est une hypothèse, pas un relevé.*
913
+
914
+ - ⚠️ **« CASES À COCHER » ET « BOUTONS RADIO » SONT DEUX PAGES, ET UNE SEULE FEUILLE DE STYLE**
915
+ (2026-09-10, demande produit, alignée sur la façon dont la bibliothèque de référence les range).
916
+ Les deux composants restent UN atome : `Checkbox.css` habille les deux, et `verify:checkboxes`
917
+ visite les DEUX pages en une passe.
918
+
919
+ Ne lire que la première laisserait le radio non vérifié, et un correctif fait pour l'un peut casser
920
+ l'autre sans qu'aucune page ne le dise. Le radio doit apparaître dans ses DEUX tailles : son point
921
+ fait 37,5 % en `sm` et 40 % en `md`, donc une page qui n'en montrerait qu'une laisserait la
922
+ seconde sans mesure.
923
+
924
+ - ⚠️ **LA BORDURE DE LA CASE DÉCOCHÉE A ÉTÉ FONCÉE, ET C'EST UN ÉCART ARBITRÉ AU FIGMA**
925
+ (2026-09-10, décision produit). Le cadre la peint `border-primary` (navy-40), qui tient **1,44**
926
+ sur du blanc pour un seuil de 3:1 — **le plus bas mesuré sur ce design system**.
927
+
928
+ Et c'est plus grave que sur le bouton, où le même compromis a été accepté à 2,17 : un bouton garde
929
+ son libellé à 11,82, donc il reste identifiable, tandis qu'ici **la boîte EST le contrôle**. Une
930
+ case décochée à 1,44 est presque invisible. `navy-70` tient 3,67 : le palier le plus CLAIR de nos
931
+ neutres qui passe le seuil, donc le plus proche de l'intention du dessin. Exactement l'arbitrage
932
+ rendu pour le point de statut « hors ligne » de l'avatar, qui partait lui aussi de navy-40.
933
+
934
+ ⚠️ **Et aucun rôle de bordure ne porte navy-70.** Les rôles de bordure vont de navy-40 à navy-20,
935
+ plus les teintes de marque et d'erreur. navy-70 est porté par `text-secondary`, un rôle de TEXTE :
936
+ s'en servir pour peindre une BORDURE serait la confusion que la seconde vérification de
937
+ `verify:tokens` existe pour empêcher. D'où un jeton de composant. À remonter dans le Figma.
938
+
939
+ - ⚠️ **LE FOND COCHÉ RESTE SUR brand-500, LÀ OÙ LE BOUTON EST DESCENDU À brand-600** (2026-09-10).
940
+ Ce n'est pas une incohérence : le bouton a bougé parce que du TEXTE blanc sur brand-500 tient 3,60
941
+ pour un seuil AA de 4,50. Ici la marque blanche n'est pas du texte, c'est un objet graphique, et la
942
+ WCAG 1.4.11 en demande 3:1 — les 3,60 passent. Descendre d'un cran « par cohérence » foncerait un
943
+ contrôle qui n'en a pas besoin.
944
+
945
+ - ⚠️ **L'ÉTAT « HOVER » DU FIGMA NE DESSINE RIEN, ET LE NÔTRE EST UN AJOUT** (2026-09-10, décision
946
+ produit). Les quarante symboles portent bien une variante `Hover`, et elle est **identique au
947
+ PIXEL** à l'état `Default` — établi en échantillonnant la capture du cadre aux coordonnées que
948
+ `get_metadata` donne, sur les décochés, les cochés, les indéterminés et les radios.
949
+
950
+ La variante existe, donc l'intention y était ; elle dessine juste la même chose. Un contrôle sans
951
+ retour au survol paraît inerte, donc on en dessine un : décoché il prend un fond navy-20, coché il
952
+ descend d'un cran. Ce cran ne demande aucun jeton — `bg-brand-solid` EST brand-600, c'est un rôle,
953
+ et de la bonne catégorie. À remonter dans le Figma.
954
+
955
+ *Repère : une variante qui existe et ne dessine rien n'est pas un choix, c'est un oubli de
956
+ maquette — et elle ne se voit qu'en comparant deux symboles au pixel.*
957
+
958
+ - ⚠️ **LE DÉSACTIVÉ EST DEUX CHANGEMENTS À LA FOIS, ET CE SONT LES PIXELS QUI L'ONT TRANCHÉ**
959
+ (2026-09-10). On pouvait croire à une simple opacité posée sur l'état par défaut : un blanc à 50 %
960
+ sur du blanc reste blanc, et la bordure aurait donné #e9ebef, alors que la case désactivée mesure
961
+ **#f8f9fa** — soit exactement navy-20 à 50 %. Le symbole confirme : fond navy-20, bordure
962
+ conservée, `opacity: 0.5`.
963
+
964
+ L'opacité est le même mécanisme que le bouton désactivé, et c'est elle qui explique la marque :
965
+ elle reste blanche parce que l'opacité s'applique au GROUPE, pas à chaque couche. **Elle est posée
966
+ sur la LIGNE, pas sur la case** — sur la case seule, le libellé resterait à pleine intensité à côté
967
+ d'un contrôle éteint.
968
+
969
+ - ⚠️ **L'ANNEAU DE FOCUS EST UN RELEVÉ, POUR UNE FOIS, PAS UN AJOUT** (2026-09-10). Le Figma l'écrit
970
+ `0 0 0 2px bg-primary, 0 0 0 4px Bleu/100` — soit exactement la recette que le bouton avait posée
971
+ et que la croix de fermeture, la croix du badge et l'icône d'aide de l'info-bulle réemploient.
972
+ Quatre pièces l'avaient ajoutée de leur côté, faute de la trouver dans les cadres ; celle-ci
973
+ confirme qu'elle était juste.
974
+
975
+ - ⚠️ **L'ÉLÉMENT EST UN VRAI `<input>`, ET C'EST CE QUI REND `:indeterminate` POSSIBLE** (2026-09-10).
976
+ Il porte `appearance: none` et se laisse habiller, mais il garde ce qu'un `div` n'a pas : le
977
+ clavier, la participation au formulaire, le groupement des radios par `name`, et les
978
+ pseudo-classes natives `:checked`, `:disabled`, `:focus-visible` et `:indeterminate`.
979
+
980
+ `verify:checkboxes` vérifie explicitement `appearance: none` : sans lui, le navigateur peint SA
981
+ case par-dessus la nôtre. Le résultat n'est pas vide — c'est une case native, à la bonne place, qui
982
+ ignore tous nos jetons.
983
+
984
+ ⚠️ **`indeterminate` est une propriété du NŒUD, pas un attribut, et elle se réécrit à chaque
985
+ rendu.** Aucun attribut HTML ne la déclare, donc React ne la synchronise jamais : sans l'écriture
986
+ répétée, une case rendue indéterminée le RESTERAIT après être repassée à `false`, et l'écart entre
987
+ l'état demandé et le DOM n'apparaît ni dans le balisage, ni dans un diff, ni sur une capture.
988
+
989
+ ⚠️ **Et elle est indépendante de `checked`, contre le Figma.** Le cadre ne la dessine qu'avec
990
+ `Checked=True`, mais obliger à cocher une case pour la rendre indéterminée serait l'inverse de son
991
+ sens. HTML gagne.
992
+
993
+ - ⚠️ **LE LIBELLÉ EST RELIÉ PAR `htmlFor`, IL N'ENVELOPPE PAS** (2026-09-10). Un `<label>` qui
994
+ envelopperait le contrôle ET le texte d'aide ferait de l'aide une partie du NOM accessible : un
995
+ lecteur d'écran annoncerait les deux d'un bloc, à chaque rencontre du contrôle. Le libellé est le
996
+ nom, l'aide est une DESCRIPTION via `aria-describedby`.
997
+
998
+ Conséquence assumée : cliquer le libellé bascule la case, cliquer l'aide ne fait rien. **Et la
999
+ mutation mord** — envelopper dans un `<label>` fait échouer le test de nom/description de la case
1000
+ ET celui du radio. C'est précisément l'implémentation « évidente », celle qui rend toute la ligne
1001
+ cliquable, qui casse la sémantique.
1002
+
1003
+ - ⚠️ **LE TEXTE D'AIDE RESTE SOUS L'AA, ET CE N'EST PAS UN PROBLÈME DE CE COMPOSANT** (2026-09-10).
1004
+ Il tient 3,67 pour un seuil de 4,50 — mais le Figma le peint `text-secondary`, et ce RÔLE est
1005
+ navy-70 : il est sous le seuil **partout où il porte du texte**. Le foncer ici seulement rendrait
1006
+ cette aide différente de toutes les autres du design system.
1007
+
1008
+ Épinglé dans `verify:checkboxes` **dans l'autre sens** : le jour où le rôle sera foncé, ce contrôle
1009
+ rougira, et ce sera le rappel qu'il faut retirer l'exception. **C'est une décision de fondation, et
1010
+ elle est ouverte** — voir « En attente ».
1011
+
1012
+ - ⚠️ **LE PLACEMENT DE L'INFO-BULLE EST CHOISI PAR LE DÉVELOPPEUR, ET LE PAQUET N'A TOUJOURS AUCUNE
1013
+ DÉPENDANCE D'EXÉCUTION** (2026-09-10, arbitrage du lead dev front). Les six placements de la
1014
+ maquette sont en CSS, et l'appelant en choisit un.
1015
+
1016
+ **La conséquence est à connaître avant de s'en servir : rien ne retourne la bulle si elle ne tient
1017
+ pas.** Une info-bulle placée près du bord de l'écran en sortira. C'est le prix de zéro dépendance,
1018
+ et la page de documentation énonce les trois cas où il faut changer de placement.
1019
+
1020
+ ⚠️ **CETTE DÉCISION EN A ANNULÉ UNE PRISE LE MÊME JOUR, ET LE DÉTOUR A SERVI.** Une première
1021
+ version passait par `react-aria-components`, déjà en production sur `liste-evaluations` et
1022
+ `export-rapport-entretien` ; l'équipe produit l'avait validée en `peerDependency`, en connaissance
1023
+ du coût. Le lead dev front a tranché l'autre branche — celle que la recommandation d'origine
1024
+ proposait — et le paquet est revenu à **zéro dépendance d'exécution**.
1025
+
1026
+ Ce qui SURVIT du détour, ce sont ses MESURES : le recouvrement de la flèche, son retrait de 12 px
1027
+ sur les placements décalés et l'écart de 4 px au déclencheur ont tous été relevés dans un vrai
1028
+ navigateur pendant ce passage, et sont reproduits à l'identique. Trois problèmes sont partis avec
1029
+ la bibliothèque :
1030
+
1031
+ · son `transform` en style EN LIGNE sur l'enveloppe de la flèche écrasait notre rotation sans
1032
+ bruit ;
1033
+ · les bulles forcées ouvertes étaient mal placées dans une page MDX longue — un relevé donnait
1034
+ 4 px d'écart, le suivant 118 px sur le MÊME code — ce qui avait obligé à rendre deux planches
1035
+ « en statique » ;
1036
+ · une bulle « froide » était intestable sous jsdom, donc le fichier de test devait réchauffer la
1037
+ bibliothèque avant CHAQUE cas, ce qui masquait justement la question.
1038
+
1039
+ Et une chose est devenue MEILLEURE que la bibliothèque : elle ancrait la flèche au CENTRE du
1040
+ déclencheur, donc les 12 px du Figma ne tombaient juste que sur une icône de 16 px et dérivaient à
1041
+ 70,64 px sur un bouton `sm`. En CSS, les placements décalés s'ancrent sur la FLÈCHE : le retrait
1042
+ vaut 12 px quelle que soit la largeur du déclencheur, et `verify:info-bulles` le mesure sur trois
1043
+ largeurs différentes.
1044
+
1045
+ *Repère : une décision produit peut être renversée par une décision technique le jour même, et
1046
+ c'est normal. Ce qu'il faut sauver du travail annulé, ce sont ses MESURES — pas son architecture.*
1047
+
1048
+ - ⚠️ **LE COMPOSANT N'AJOUTE QU'UN SEUL NŒUD : UNE ANCRE** (2026-09-10). Un `span` qui sert de repère
1049
+ de position à la bulle et qui ne prend pas de `tabIndex`. Envelopper le déclencheur dans un bouton
1050
+ est ce que fait l'implémentation en production, et ça y a créé **six arrêts de tabulation morts** —
1051
+ six boutons focusables sans action. Un test vérifie qu'un seul bouton est rendu, et qu'une seconde
1052
+ tabulation SORT de l'ensemble.
1053
+
1054
+ ⚠️ **UN DÉCLENCHEUR NON FOCUSABLE EN DEVIENT UN, ET C'EST VOULU** — une aide qu'on ne peut pas
1055
+ atteindre au clavier n'aide qu'une partie des gens. Un `span` reçoit un `tabIndex` ; un élément
1056
+ déjà focusable (bouton, lien, champ) n'est pas touché, et un `tabIndex` donné par l'appelant est
1057
+ respecté. Trois tests couvrent les trois cas, parce que la règle est invisible dans le balisage.
1058
+
1059
+ ⚠️ **LES GESTIONNAIRES SONT COMPOSÉS, PAS REMPLACÉS.** `cloneElement` écrase les props qu'il
1060
+ reçoit : poser `onMouseEnter` sur un déclencheur qui en avait déjà un le ferait disparaître en
1061
+ silence — la bulle s'ouvrirait, et l'action attendue par l'appelant ne se ferait plus, sans aucune
1062
+ erreur. Celui de l'appelant passe en premier.
1063
+
1064
+ - ⚠️ **ÉCHAP FERME LA BULLE, ET C'EST UN AJOUT** (2026-09-10). Le Figma ne dessine que « ouverte » et
1065
+ « fermée ». Sans Échap, une bulle ouverte au clavier recouvre ce qui l'entoure jusqu'au prochain
1066
+ changement de focus — et une bulle à texte de soutien fait 320 px de large. L'écoute est posée sur
1067
+ le DOCUMENT, pas sur le déclencheur : la touche doit fonctionner même si le focus a bougé.
1068
+
1069
+ - ⚠️ **LE RECOUVREMENT DE LA FLÈCHE EST DEUX NOMBRES DIFFÉRENTS, et c'est de la géométrie**
1070
+ (2026-09-10). À la verticale la flèche s'enfonce de 8,51471 − 6 = 2,5147 px. Tournée d'un quart de
1071
+ tour, elle pivote autour de son CENTRE alors que sa boîte garde la largeur NON tournée (16) : son
1072
+ centre est à 8 px du bord, plus une demi-hauteur de 4,2574 — elle dépasserait de 12,2574 et doit
1073
+ s'enfoncer de 6,2574. Un seul nombre pour les quatre côtés mesurait 9,74 px sur les côtés contre
1074
+ 5,99 en haut, et paraissait cohérent.
1075
+
1076
+ - ⚠️ **`box-sizing: border-box` PORTE LA LARGEUR DE 320 PX** (2026-09-10). Le Figma cale la bulle à
1077
+ texte de soutien sur 320 px TOTAL. En `content-box` — le défaut de CSS, et ce dépôt n'a aucune
1078
+ remise à zéro globale — les 12 px de padding s'ajoutent de chaque côté : mesurée, la bulle sortait
1079
+ à **344 px**. Et 344 ressemble à 320. Le contrôle vérifie les DEUX, la largeur et le modèle de
1080
+ boîte, parce que « corriger » la largeur à 296 ferait disparaître le symptôme sans la cause.
1081
+
1082
+ - ⚠️ **L'OMBRE DE L'INFO-BULLE EST UN `filter`, ET C'EST LA FLÈCHE QUI L'IMPOSE** (2026-09-10,
1083
+ demande produit). Un `box-shadow` ombre la BOÎTE : il dessinerait un rectangle autour de la bulle
1084
+ ET de sa flèche, en laissant la flèche plate. `drop-shadow()` suit la silhouette PEINTE.
1085
+
1086
+ Le prix : aucun de nos jetons `--zds-shadow-*` n'est réutilisable, ils portent un étalement négatif
1087
+ que `drop-shadow()` ne sait pas exprimer — d'où `--zds-shadow-tooltip-filter`. Sa teinte est aussi
1088
+ une SUBSTITUTION délibérée : le Figma la pose en `rgba(16, 24, 40, …)`, une couleur d'une
1089
+ collection TIERCE, là où toute notre rampe d'ombres est en noir. À 8 % d'opacité sur du blanc,
1090
+ l'écart mesure 2,3 niveaux sur 255.
1091
+
1092
+ - ⚠️ **LE HELP ICON EST L'UN DES CINQ DESSINS QUE LE CATALOGUE ÉCARTE** (2026-09-10). Deux
1093
+ générations de `help-circle-01` cohabitent dans le Figma sous le même nom : le catalogue publie la
1094
+ silhouette REMPLIE, et le cadre de l'info-bulle cite le nœud `3167:12028`, le tracé au CONTOUR
1095
+ rangé dans `ZEST_ICONS_EN_CONFLIT`. Un `<HelpCircle01 />` rendrait donc l'autre dessin.
1096
+
1097
+ Le composite va le chercher PAR SON NUMÉRO DE NŒUD — aucun troisième exemplaire du tracé — avec un
1098
+ repli si l'arbitrage le fait un jour monter dans le catalogue publié. `HelpCircle01` n'est employé
1099
+ nulle part aujourd'hui, donc cet arbitrage ne coûte rien en code.
1100
+
1101
+ ⚠️ **ET L'EN-TÊTE DU CATALOGUE AFFIRME QUELQUE CHOSE DE FAUX** : il dit que « la page Icônes les
1102
+ montre à côté de ceux qui gagnent, pour qu'on tranche en regardant ». Elle ne les montre pas.
1103
+ `ZEST_ICONS_EN_CONFLIT` est exporté de `src/index.ts` et rendu par aucune page. **À faire, ou à
1104
+ cesser de promettre.**
1105
+
1106
+ - ⚠️ **`verify:info-bulles` ÉTAIT AVEUGLE À UNE INVERSION DE PLACEMENT, ET C'EST UNE MUTATION QUI L'A
1107
+ DIT** (2026-09-10). Échanger `left: 50%` contre `right: 50%` sur `top-right` envoie la bulle du
1108
+ côté opposé, et le contrôle restait vert : le retrait de 12 px est mesuré depuis le bord de la
1109
+ BULLE, donc il vaut 12 où que la bulle se trouve.
1110
+
1111
+ L'invariant qui manquait est le SÉMANTIQUE — **la flèche doit pointer sur le centre du
1112
+ déclencheur** — et il est désormais mesuré sur chaque placement. Les deux sont gardés : le retrait
1113
+ décrit la bulle, le centrage décrit sa RELATION au déclencheur.
1114
+
1115
+ ⚠️ **ET UNE AUTRE MUTATION A EU L'AIR DE NE PAS MORDRE ALORS QU'ELLE N'AVAIT PAS ÉTÉ CHARGÉE.**
1116
+ Deux mutations CSS ressortaient vertes à 10 s d'attente et rouges à 14 : le rechargement à chaud de
1117
+ Vite ne les avait pas appliquées. *Avant de conclure qu'un contrôle est aveugle, lire la règle
1118
+ SERVIE dans le DOM.* Sur ces deux mutations, une seule l'était réellement.
1119
+
1120
+ - ⚠️ **UN JETON DÉCLARÉ ET JAMAIS CITÉ ÉCHAPPE À TOUS LES CONTRÔLES** (2026-09-10).
1121
+ `--zds-color-tooltip-supporting-text` existait déjà, porté du thème de référence, et valait
1122
+ `gray-300` — là où le Figma de Zest donne `navy-20`. Le redéclarer a créé un DOUBLON silencieux :
1123
+ deux déclarations d'une même propriété, la première morte.
1124
+
1125
+ Personne n'avait vu la mauvaise valeur parce que rien ne la CITAIT : `verify:tokens` résout les
1126
+ jetons qu'un composant emploie. Ses quatre voisins du même bloc étaient dans le même cas — **des
1127
+ ports, pas des relevés**. Les trois `toggle-*` ont été relus le 2026-09-11 : **les trois étaient
1128
+ faux**. Seul `slider-handle-*` reste invérifié, et il attend le curseur.
1129
+
1130
+ - ⚠️ **LE PROVISOIRE `.zds-doc-bulle` PEIGNAIT LA MAUVAISE COULEUR DEPUIS LE JOUR OÙ IL A ÉTÉ ÉCRIT**
1131
+ (2026-09-10). Son fond citait `bg-primary-solid`, qui aboutit sur `gray-950` (#0a0d12) ; la
1132
+ maquette peint la bulle en `Bleu marine/100` (#21304e). Deux noirs différents, et le mauvais.
1133
+ `docs.css` n'étant pas une feuille de composant, `verify:tokens` ne l'a jamais lu.
1134
+
1135
+ La leçon vaut plus que le correctif : **un dessin fait à côté d'un composant absent ne se contente
1136
+ pas de vieillir, il peut être faux dès le premier jour, et rien ne le surveille.** Le même soupçon
1137
+ porte sur les trois provisoires qui restent dans ce bloc.
1138
+
1139
+ - ⚠️ **RETIRER LE `title` DU BOUTON D'AJOUT ÉTAIT INVISIBLE AUX 25 TESTS DE L'AVATAR** (2026-09-10).
1140
+ Tous sont restés verts : aucun ne gardait le provisoire. Le remplacement n'apparaissait que dans le
1141
+ diff — exactement la régression par remplacement que rien ne voit. Deux tests ont été ajoutés
1142
+ après coup, et ils affirment les DEUX sens : la bulle native a disparu, et l'`aria-label` est
1143
+ resté. Sans le second, retirer le libellé « puisque la bulle le dit » passerait sans bruit, et le
1144
+ bouton s'annoncerait « bouton » — une info-bulle n'existe qu'au survol.
1145
+
1146
+ - ⚠️ **UN DÉLIMITEUR DE COMMENTAIRE EN TROP DANS `theme.css` SUPPRIME LA DÉCLARATION QUI LE SUIT**
1147
+ (2026-09-10). Un `*` suivi d'un `/` surnuméraire refermait le commentaire du bloc AVATAR en son
1148
+ milieu : le paragraphe d'après devenait du CSS invalide, et la récupération d'erreur du navigateur
1149
+ **saute jusqu'au prochain `;`**. Ce qui a disparu n'est donc pas le paragraphe, mais
1150
+ `--zds-color-avatar-bg`, la déclaration d'après. Un `var()` sur un jeton inexistant ne peint rien,
1151
+ sans erreur ni avertissement : les trois avatars en pastille — picto, initiales, masque — sont
1152
+ sortis **transparents**.
1153
+
1154
+ `tsc`, les 69 tests, `verify:tokens` et `verify:avatars` étaient tous verts. C'est Micka qui l'a vu
1155
+ à l'œil, et c'est le genre de défaut qui donne un résultat plausible : un rond blanc bordé
1156
+ ressemble à un choix.
1157
+
1158
+ Trois choses à en garder :
1159
+
1160
+ · **chercher AU-DESSUS du jeton manquant, pas à son endroit.** Le fichier se lit parfaitement à la
1161
+ ligne de la déclaration disparue ; le dégât est en amont d'elle ;
1162
+ · **compter les ouvertures contre les fermetures ne suffit pas**, même si ça a suffi ici (60 contre
1163
+ 61). Le comptage est aveugle au cas « un commentaire, du texte, un second commentaire » : ça
1164
+ s'équilibre et il reste pourtant du texte dehors. `verify:foundations` retire donc les
1165
+ commentaires comme le fait un navigateur, puis exige que tout ce qui reste dans `:root` soit une
1166
+ déclaration. Les deux cas sont éprouvés par mutation, avec un troisième — un commentaire jamais
1167
+ refermé — où le contrôle **refuse de conclure** au lieu de passer ;
1168
+ · **une sonde géométrique ne voit pas une couleur absente.** `verify:avatars` était vert parce que
1169
+ rien n'avait bougé. Il mesure désormais le fond de la pastille ET exige que celui d'une photo
1170
+ reste transparent : sans cette seconde moitié, le contrôle passerait sur une page où plus rien
1171
+ n'est peint — deux fois du vide comparé à lui-même.
1172
+
1173
+ Effet de bord mesuré : `dist/index.css` est passé de 58,39 à 58,02 Ko en réparant, parce que le
1174
+ texte orphelin était **embarqué tel quel dans le CSS publié**.
1175
+
1176
+ - ⚠️ **TROIS VÉRIFICATIONS AVANT CHAQUE COMMIT, ET C'EST UNE HABITUDE, PAS UN RÉFLEXE AU CAS PAR
1177
+ CAS** (2026-09-10, demande produit). Les jetons sémantiques (`verify:tokens`), l'application des
1178
+ FOUNDATIONS et l'absence du nom du fournisseur tiers (`verify:foundations`, écrit ce jour-là).
1179
+
1180
+ **La raison est que les deux défauts qui ont motivé la règle étaient invisibles là où ils
1181
+ étaient écrits.** Les 13 sauts de couche du premier badge rendaient juste, passaient `tsc`, les
1182
+ 44 tests et les deux contrôles de jetons existants — l'audit qui les a trouvés était manuel, et
1183
+ personne ne l'aurait lancé sur un changement qui « ne touche pas aux couleurs ». Le nom du
1184
+ fournisseur, lui, est revenu deux fois dans les notes pendant qu'on le retirait des pages. Un
1185
+ contrôle qu'on ne lance que quand on le soupçonne ne trouve que ce qu'on soupçonne.
1186
+
1187
+ Ce que `verify:foundations` cherche et ce qu'il laisse passer :
1188
+
1189
+ · il ne signale que les valeurs brutes **qu'un jeton nomme déjà**, et **par famille de
1190
+ propriété** — l'échelle d'espacement pour `padding` / `margin` / `gap`, celle des rayons pour
1191
+ `border-radius`, celle du texte pour `font-size` et `line-height`. Comparer un padding à tous
1192
+ les jetons confondus reprocherait un `padding: 10px` au nom d'un jeton de RAYON, ce qui ne veut
1193
+ rien dire : deux jetons qui partagent une valeur ne partagent pas un sens — c'est déjà le
1194
+ raisonnement de la CATÉGORIE dans `verify:tokens` ;
1195
+ · une valeur **hors échelle** passe en silence, et c'est voulu : les 10 px de padding du badge,
1196
+ ses 3 px de rayon de croix, les filets sous-pixel de l'avatar sont des relevés du Figma, il n'y
1197
+ a rien à citer à leur place. Le contrôle attrape l'oubli, pas l'exception ;
1198
+ · **une exception énumérée**, comme `GRIS_ADMIS` dans `verify:tokens` : le `padding: 2px` du
1199
+ biseau des boutons pleins est une ÉPAISSEUR de trait, pas un espacement, et citer
1200
+ `--zds-space-xxs` là serait exactement la confusion que le contrôle existe pour empêcher. Il
1201
+ **échoue aussi si l'exception cesse d'être employée** — une dispense ne survit pas à son
1202
+ bénéficiaire ;
1203
+ · le motif du nom du fournisseur est **reconstitué à l'exécution**. Un contrôle qui écrirait la
1204
+ chaîne qu'il interdit se trouverait lui-même et rougirait à chaque passage. Les binaires sont
1205
+ écartés : les octets d'un WebP peuvent épeler n'importe quoi, et les 282 avatars produiraient
1206
+ un faux positif par lot.
1207
+
1208
+ Les quatre mutations qui prouvent qu'il mord ont été jouées : un `margin: 8px` dans le badge,
1209
+ un `border-radius` / `font-size` / `line-height` bruts, le biseau du bouton passé à 1 px, et le
1210
+ nom du fournisseur remis dans une page. Chacune fait sortir 1.
1211
+
1212
+ - ⚠️ **UNE ANOMALIE DU FIGMA EST REPRODUITE DANS LE BADGE, EN TAILLE `sm`** (2026-09-10). Un
1213
+ `Badge color` ou un `Badge modern` de taille `sm` portant une icône gagne 2 px du côté OPPOSÉ à
1214
+ l'icône — 8 au lieu de 6 — et tombe alors à la largeur exacte du `Pill color`, alors que ces
1215
+ deux habillages ne se distinguent QUE par leur padding. Les autres tailles ne le font pas.
1216
+ Vraisemblablement des variantes recopiées depuis le pill sans réajuster le padding.
1217
+
1218
+ Reproduite plutôt que corrigée : les maquettes font foi, et une correction silencieuse aurait
1219
+ fait diverger le code et le Figma sans laisser de trace. Elle est **épinglée** dans
1220
+ `verify:badges`, donc **la corriger dans le Figma fera échouer le contrôle** — c'est le rappel
1221
+ qu'il faudra retirer les deux règles CSS et la section de la page avec elle.
1222
+
1223
+ - **La géométrie du badge est une TABLE, et la table a été prouvée** (2026-09-10). Quatre règles —
1224
+ hauteur = interligne + 2 × padding ; le côté du LIBELLÉ porte le padding de base du type ; le
1225
+ côté d'une PIÈCE le perd de 2 px sur le pill et le garde sur les deux autres, sauf l'icône qui
1226
+ le perd sur les deux ; la croix ne suit pas l'échelle (3, 4, 6). Un script a recalculé les
1227
+ largeurs ET les hauteurs des **477 symboles** du cadre depuis ces règles : les 477 tombent
1228
+ juste, au pixel. C'est ce modèle que `verify:badges` vérifie dans le navigateur.
1229
+
1230
+ - ⚠️ **LE COMPOSANT AVATAR EXISTAIT, ET CE DÉPÔT AFFIRMAIT LE CONTRAIRE** (2026-09-10). Le
1231
+ `CLAUDE.md`, le README et la page Foundations écrivaient tous les trois qu'il n'y avait pas de
1232
+ composant `Avatar` dans le V2.0 et que c'était « délibéré ». Le relevé s'était arrêté sur le
1233
+ cadre `3857:23794` — la banque de 282 photos — et n'avait pas cherché plus loin. Le composant
1234
+ vit dans **cinq cadres**, il a six tailles, quatre contenus, une bordure, trois statuts, un
1235
+ groupe et un avatar-libellé.
1236
+
1237
+ ⚠️ **Un cadre qui répond à une question ne prouve pas qu'aucun autre n'y répond mieux.** C'est la
1238
+ leçon, et elle vaut pour les prochains relevés : la banque de photos était une réponse plausible
1239
+ à « où sont les avatars », donc personne n'a cherché la suivante.
1240
+
1241
+ - ⚠️ **LES INITIALES ENTRENT DANS LE SYSTÈME, ET UNE RÈGLE EN GRAS EST ABROGÉE** (2026-09-10). La
1242
+ page Foundations écrivait « chez Zest, un avatar est une photo ou un picto — jamais des
1243
+ initiales », avec une planche marquée en rouge et un paragraphe d'arguments. Le V2.0 les dessine
1244
+ dans les six tailles ; l'équipe produit a tranché.
1245
+
1246
+ Ce qui remplace l'interdiction est un **ordre** : photo > initiales > picto. Le raisonnement
1247
+ d'origine n'était pas faux — deux lettres désignent mal — il était trop absolu : elles désignent
1248
+ mieux qu'un pictogramme quand on connaît le nom. Le composant dérive donc son contenu de ce
1249
+ qu'on lui donne au lieu d'exiger trois interrupteurs.
1250
+
1251
+ ⚠️ **L'ANONYME PASSE DEVANT TOUT, y compris une photo présente**, et il interdit le point de
1252
+ statut. Ce n'est pas une préférence : il sert les réponses anonymisées d'une campagne de
1253
+ feedback, et si une photo pouvait le battre, une photo suffirait à lever l'anonymat de quelqu'un
1254
+ qui l'a demandé.
1255
+
1256
+ L'échelle de tailles proposée par la page — 24, 32, 40, 48, 64, 96 — était **fausse**. Le Figma
1257
+ donne 24, 32, 40, 48, **56**, 64.
1258
+
1259
+ - **Le statut de l'avatar est ABSENT par défaut** (2026-09-10), sur décision produit : la
1260
+ production ne gère pas encore cet affichage, c'est une anticipation. Le composant n'en dessine
1261
+ aucun sans qu'on passe `status`, et la page ne le montre que dans sa propre section.
1262
+ `verify:avatars` vérifie qu'il reste absent de la majorité des avatars de la page — sans quoi
1263
+ elle laisserait croire à un défaut par défaut.
1264
+
1265
+ ⚠️ **LES TROIS POINTS TIENNENT LES 3:1, ET DEUX ONT ÉTÉ FONCÉS POUR ÇA.** Sur décision produit du
1266
+ 2026-09-10, le seuil de la WCAG 1.4.11 est exigé et non plus toléré : le disque d'un point de
1267
+ statut porte une information, donc sa couleur doit se distinguer du fond.
1268
+
1269
+ | | avant | après | sur blanc |
1270
+ |---|---|---|---|
1271
+ | En ligne | `success-600` | inchangé | 3,59 |
1272
+ | Hors ligne | `navy-40` — 1,44 | **`navy-70`** | 3,67 |
1273
+ | Commentaire sensible | `error-200` — 1,42 | **`error-500`** | 3,76 |
1274
+
1275
+ ⚠️ **UNE OBJECTION QUE J'AVAIS FAITE ÉTAIT TROP LARGE.** J'avais écrit que foncer le gris ferait
1276
+ lire « hors ligne » comme un état ACTIF, et Micka l'avait acceptée. C'est vrai d'un `navy-80` ou
1277
+ d'un `navy-100` ; ce n'est pas vrai de `navy-70`, un gris moyen qui se lit toujours comme une
1278
+ absence de couleur à côté du vert. L'objection valait pour l'ampleur du geste, pas pour le geste
1279
+ — et elle a coûté un aller-retour.
1280
+
1281
+ ⚠️ **LE DISQUE SENSIBLE ET SON PICTO NE POUVAIENT PAS TENIR ENSEMBLE.** Le Figma peint un picto
1282
+ `Rouge/200` sur un disque `Rouge/40` : le picto tient bien 3,40 sur son disque, mais le disque ne
1283
+ tient que 1,42 sur du blanc. Et un disque assez foncé pour tenir 3:1 ne laisse plus passer aucun
1284
+ rouge — au palier 500, un `error-600` tombe à 1,29. **Les deux exigences s'excluent sur la même
1285
+ teinte.** Le disque monte donc au 500 et le picto devient BLANC, à 3,76 : c'est la forme
1286
+ conventionnelle d'une alerte, et elle aligne ce point sur les deux autres. À répercuter dans le
1287
+ Figma, qui vient justement de repeindre ce picto en rouge.
1288
+
1289
+ - **Le gris du statut hors ligne est une substitution mesurée** (2026-09-10). Le Figma le peint
1290
+ `utility-neutral-300` (#d4d4d4), d'une collection tierce. Le plus proche chez Zest était
1291
+ `gray-300` (ΔE 1,99), écarté parce qu'il est dans la rampe grise que le contrôle interdit, et
1292
+ `navy-40` (ΔE 4,03) a servi un temps — avant d'être abandonné faute de contraste. `navy-70` est
1293
+ le palier le plus CLAIR des neutres de Zest qui tienne 3:1, donc le plus proche de l'intention.
1294
+
1295
+ - ⚠️ **LE DÉPÔT ANNONÇAIT « 121 ICÔNES » EN NEUF ENDROITS ALORS QU'IL Y EN AVAIT 133**
1296
+ (2026-09-10). La dérive est antérieure à l'ajout des deux icônes du statut : douze icônes avaient
1297
+ été ajoutées sans que la prose suive. Les neuf mentions sont passées à **135**. Le nombre est
1298
+ recopié à la main dans six fichiers — c'est exactement le piège que ce dépôt documente pour les
1299
+ pages qui recopient une valeur au lieu de la lire.
1300
+
1301
+ - ⚠️ **LES TROIS TEINTES CLAIRES DU BADGE DESCENDENT DE DEUX CRANS** (2026-09-10). Telles que le
1302
+ Figma les peignait, turquoise, tilleul et jaune ne tenaient aucun de leurs deux seuils : libellé
1303
+ 3,77 / 4,05 / 4,35 pour 4,50, et **pièce** 2,55 / 2,34 / 2,69 pour 3,00. Le Figma les avait déjà
1304
+ foncées d'un cran pour deux d'entre elles ; ça ne suffisait pas.
1305
+
1306
+ Un cran de plus sur la pièce ET sur le texte fait passer les six mesures :
1307
+
1308
+ | Couleur | Pièce | Libellé |
1309
+ |---|---|---|
1310
+ | turquoise | 600 → **700** (2,55 → 3,77) | 700 → **800** (3,77 → 5,69) |
1311
+ | tilleul | 700 → **800** (2,34 → 4,05) | 800 → **900** (4,05 → 7,00) |
1312
+ | jaune | 700 → **800** (2,69 → 4,35) | 800 → **900** (4,35 → 7,59) |
1313
+
1314
+ ⚠️ **LE RAPPORT PIÈCE/LIBELLÉ EST PRÉSERVÉ**, et c'est ce qui rend l'arbitrage propre : la
1315
+ nouvelle valeur de pièce est l'ANCIENNE valeur de libellé. Les treize couleurs partagent donc la
1316
+ même règle — la pièce est un cran plus claire que le libellé — et ces trois-là sont simplement
1317
+ assises deux crans plus bas. Ce ne sont plus des exceptions, et `verify:badges` ne porte plus
1318
+ aucun contraste épinglé : les seuils eux-mêmes les gardent.
1319
+
1320
+ ⚠️ **ON N'A TOUCHÉ QUE CE QUI BLOQUAIT.** Les dix autres couleurs tiennent leurs deux seuils
1321
+ (libellé 4,69 à 12,58 ; pièce 3,31 à 6,84) et gardent leurs paliers. Les aligner « par
1322
+ cohérence » aurait assombri dix couleurs justes pour rien. Le FOND ne bouge pas non plus : il
1323
+ reste au palier 50, donc ces badges restent aussi pâles que les autres — et l'objection écrite
1324
+ la veille, « un tilleul au 900 n'est plus du tilleul », portait sur un APLAT saturé sous du texte
1325
+ blanc, où c'est le fond qui porte l'identité de la teinte. La bordure ne bouge pas : elle est
1326
+ décorative, un badge n'étant pas une commande.
1327
+
1328
+ - ⚠️ **LA RÈGLE DES CALQUES ÉTAIT ÉCRITE MAIS VÉRIFIÉE PAR RIEN** (2026-09-10). Un composant ne
1329
+ cite que le calque des rôles (3) ou celui des jetons de composant (4) — jamais une rampe brute
1330
+ (1), jamais une palette utilitaire (2). Un audit des cinq feuilles de composant a trouvé
1331
+ **13 citations en faute**, toutes dans le premier badge : le fond du survol de sa croix, écrit
1332
+ en `var(--zds-color-brand-100)`, `var(--zds-color-navy-30)` et huit `var(--zds-color-utility-*-100)`.
1333
+ Des valeurs justes, un rendu juste, et un défaut que rien ne voyait — ni le typage, ni les 44
1334
+ tests, ni les deux vérifications de `verify:tokens`, qui ne regardaient que la rampe grise et les
1335
+ doublons de rôle.
1336
+
1337
+ La réparation est la partie instructive : **quatre des treize n'ont demandé aucun jeton neuf**,
1338
+ `bg-brand-secondary` et ses trois voisins portant déjà exactement le palier 100 de leur rampe. Le
1339
+ navy était un piège — le seul rôle qui porte `navy-30` est `border-secondary`, un rôle de
1340
+ BORDURE, et l'employer pour peindre un aplat aurait été la confusion même que la seconde
1341
+ vérification existe pour empêcher. Les neuf autres ont pris un jeton de composant.
1342
+
1343
+ `verify:tokens` porte désormais un **troisième contrôle**, avec son témoin : il échoue si un
1344
+ composant cite un calque 1 ou 2.
1345
+
1346
+ - **La liste de navigation à droite des pages de doc** (2026-09-10), par `parameters.docs.toc`.
1347
+ ⚠️ Son `headingSelector` n'est pas un détail : Storybook ne liste que les `h3` par défaut, et sur
1348
+ une page dont les chapitres sont en `h1` et les sections en `h2`, la liste s'affichait avec UNE
1349
+ entrée — elle avait l'air de marcher. ⚠️ Elle est **globale**, et pas par choix : le conteneur de
1350
+ doc de Storybook 8.6 ne lit `toc` que dans les paramètres du PROJET, un `<Meta parameters>` par
1351
+ page n'activant rien — vérifié dans le navigateur sur trois pages. Demandée pour les badges et
1352
+ les boutons, elle est donc posée sur les quinze pages.
1353
+
1354
+ - ⚠️ **LES QUATRE PIÈCES DU BADGE NE FONT QU'UNE COULEUR — le palier 600 — et seul le LIBELLÉ est
1355
+ au 700** (2026-09-10). Relevé sur quatre symboles de trois habillages : chacun ne cite que deux
1356
+ valeurs de couleur, `Zest/200` pour sa pièce et `Zest/300` pour son libellé.
1357
+
1358
+ Un premier jet peignait les pièces en `currentColor`, donc à la couleur du libellé : un cran trop
1359
+ sombre partout, et sur un `modern` carrément navy — **treize badges identiques là où la maquette
1360
+ en dessine treize colorés**. Le typage, les 44 tests et la sonde de géométrie passaient tous : le
1361
+ badge se rendait parfaitement. C'est la planche des treize couleurs, dessinée en restructurant la
1362
+ page, qui l'a montré. `verify:badges` compare désormais chaque icône et chaque croix à la teinte
1363
+ MESURÉE du point de sa couleur — 126 comparaisons — pour que ça ne revienne pas.
1364
+
1365
+ C'est aussi ce qui a corrigé une lecture fausse des 108 symboles absents du cadre. Ils sont tous
1366
+ des `Badge modern` dans les douze couleurs autres que Gray, mais **seuls 36 sont légitimes** —
1367
+ ceux sans aucune pièce, qui n'ont rien à colorer. Les 72 autres, `X close` et `Icon trailing`,
1368
+ sont un TROU de la maquette : leur unique spécimen Gray peint bien sa pièce en `Couleurs/Gris/200`.
1369
+ **À dessiner dans le Figma.**
1370
+
1371
+ - **Le badge nomme ses couleurs avec les mots du THÈME, pas ceux du Figma** (2026-09-10).
1372
+ `horizon`, `lime`, `yellow` et non `Bleu horizon`, `Vert tilleul`, `Jaune` : le thème les
1373
+ déclare ainsi depuis le portage, et traduire à chaque aller-retour entre une classe CSS et un
1374
+ jeton coûterait plus que ça ne rapporte. La page « Badges » affiche les deux vocabulaires côte à
1375
+ côte, et c'est le seul endroit où la correspondance a besoin d'être visible.
1376
+
1377
+ - **Les quatre emplacements du badge sont des propriétés INDÉPENDANTES**, là où le Figma les range
1378
+ sur un axe unique et donc exclusif (2026-09-10). C'est l'inverse du choix fait sur `Button`,
1379
+ dont les onze hiérarchies sont une liste plate, et la différence est réelle : un jeu de
1380
+ variantes Figma ne sait pas exprimer « deux emplacements optionnels » sans multiplier les
1381
+ variantes par quatre. L'exclusivité est une contrainte de l'OUTIL, pas une règle du produit — et
1382
+ un badge qui porte à la fois un point d'état et une croix de retrait est une pièce courante.
1383
+ Rien n'est inventé au passage : chaque emplacement porte sa géométrie relevée.
1384
+
1385
+ ⚠️ Deux cas que le Figma ne dessine pas ont dû être tranchés ici : quand un point ET une icône
1386
+ de tête sont présents, le padding suit le POINT, qui est la pièce la plus au bord ; quand une
1387
+ icône de fin ET une croix sont présentes, il suit la CROIX. Ce sont des choix, et deux tests les
1388
+ fixent.
1389
+
1390
+ - **L'anneau de focus de la croix du badge est un AJOUT** (2026-09-10). Le cadre `_Badge close X`
1391
+ ne dessine que `Default` et `Hover`. Une commande qu'on ne voit pas quand on l'atteint au
1392
+ clavier ne s'emploie pas sans souris : l'anneau reprend la recette du `CloseButton`, à l'octet.
1393
+ À répercuter dans le Figma.
1394
+
1395
+ - ⚠️ **Un symbole du `_Badge close X` est MAL ÉTIQUETÉ** (2026-09-10). La variante `Color=Orange`
1396
+ peint `Couleurs/Rose/200` (#dd2590) : c'est du ROSE. Aucun symbole n'y porte l'étiquette `Rose`,
1397
+ et `Color=Warning` peint bien l'orange. C'est l'étiquette qui est fausse, pas le dessin — le
1398
+ même piège que les cinq codes de drapeau. Le code nomme cette couleur `rose`. **Le Figma reste à
1399
+ corriger.**
1400
+
1401
+ - **Cinq fonds de badge citent un RÔLE et non un jeton de composant** (2026-09-10), et c'est
1402
+ `verify:tokens` qui l'a établi plutôt qu'une relecture : `bg-brand-primary`, `bg-error-primary`,
1403
+ `bg-warning-primary`, `bg-success-primary` et `bg-primary` portent exactement les valeurs que le
1404
+ premier jet allait redéclarer. Les rôles `bg-*-primary` sont les paliers 50 des rampes, donc
1405
+ exactement le fond doux que le badge peint. Le message du contrôle laisse entendre que les fonds
1406
+ sont dispensés : la dispense ne vaut que là où AUCUN rôle ne nomme la valeur, ce qui est le cas
1407
+ des huit couleurs utilitaires.
1408
+
1409
+ - ⚠️ **Les bordures des boutons restent celles du Figma, sous les 3:1, et c'est un compromis
1410
+ ASSUMÉ** (2026-09-09). `border-primary` (navy-40, `#D3D7DE`) pour les neutres,
1411
+ `border-error-subtle` (error-300) pour le destructif. Mesuré en navigateur, le trait PEINT — donc
1412
+ composé sous le contour de 18 % du relief — tient **2,17:1** et **2,87:1** sur du blanc, contre
1413
+ les 3:1 que la WCAG 1.4.11 demande à ce qui délimite une commande. Et l'aplat ne rattrape rien :
1414
+ navy-20 sur blanc fait 1,11:1.
1415
+
1416
+ Un premier jet avait remonté ces bordures à `navy-70` (3,67:1) et `border-error` (3,76:1), qui
1417
+ tiennent le seuil ; l'équipe produit a préféré la **fidélité au Figma**, le dessin s'alourdissant
1418
+ visiblement. Le libellé tient 11,82:1 : le bouton reste identifiable, c'est sa LIMITE qui ne
1419
+ l'est pas.
1420
+
1421
+ ⚠️ La page « Boutons » continue de mesurer et d'afficher ces trois valeurs **en rouge**. Un
1422
+ compromis assumé se documente, il ne se cache pas — et si on revient dessus un jour, les deux
1423
+ paliers qui tiennent le seuil sont écrits ci-dessus.
1424
+ - **Le relief skéuomorphique est COMPLET sur toutes les pièces** (2026-09-09), contour de 1 px
1425
+ compris, et il est peint PAR-DESSUS la bordure — l'ordre du Figma, qui pose son style d'effet sur
1426
+ un calque au-dessus du cadre. Conséquence à connaître : sur une pièce encadrée, la bordure peinte
1427
+ n'est pas la bordure déclarée. navy-40 sous 18 % de noir donne `#ADB0B6`. C'est pour ça que la
1428
+ planche de contraste lit le `box-shadow` CALCULÉ et compose ses couches, au lieu de lire le
1429
+ jeton.
1430
+ - **Le biseau des aplats est un DÉGRADÉ, pas un liseré plat** (2026-09-09). Le liseré clair d'un
1431
+ bouton plein s'allume en haut et s'éteint en bas : il simule une lumière venant du dessus. Il est
1432
+ posé en pseudo-élément masqué — un `border` CSS ne porte pas de dégradé, et une vraie bordure de
1433
+ 2 px ferait passer un `md` de 40 à 44 px.
1434
+
1435
+ ⚠️ **Les deux bornes du dégradé sont À CONFIRMER.** Le MCP Figma rapporte ce contour comme PLAT
1436
+ (`rgba(255,255,255,0.12)`) sur deux lectures, `forceCode` compris, et `get_metadata` ne montre
1437
+ aucun calque de dégradé : l'outil l'aplatit, ou il l'est. La borne haute vient de cette valeur
1438
+ relevée ; l'extinction complète en bas est la recette standard, pas un relevé. Les jetons
1439
+ `--zds-color-button-bevel-from` et `-to` isolent les deux valeurs à corriger.
1440
+ - **Les composants ne citent que les couleurs de Zest, et c'est contrôlé** (2026-09-09).
1441
+ `yarn verify:tokens` résout chaque jeton cité par une feuille de composant jusqu'à sa valeur et
1442
+ échoue si l'un aboutit dans la rampe `gray-*` que Zest laisse de côté. Il porte un témoin —
1443
+ `bg-secondary`, qui doit être détecté — et refuse de conclure si le témoin ne mord pas. Vert sur
1444
+ les quatre composants, 90 citations résolues.
1445
+ - ⚠️ **Le Primary descend d'un palier par rapport au Figma** (2026-09-09). Le Figma peint le
1446
+ Primary AU REPOS en `Bleu/100` — notre `brand-500`, `#4481FF`. Mesuré, du blanc dessus tient
1447
+ **3,60:1**, sous le seuil AA de 4,5, et son propre survol tient 4,54 : le bouton était donc plus
1448
+ lisible au survol qu'au repos. Chaque état descend d'un palier — repos sur `bg-brand-solid`
1449
+ (600, 4,54:1), survol sur `bg-brand-solid-hover` (700, 6,08:1). Ce n'est pas seulement une
1450
+ correction d'accessibilité : **le destructif était DÉJÀ sur le palier 600 au repos**, donc le
1451
+ Figma se contredisait entre ses deux familles et le décalage les aligne. ⚠️ **À répercuter dans
1452
+ le Figma.**
1453
+ - **« Destructif » est une liste plate de onze valeurs, pas un drapeau** (2026-09-09). La famille
1454
+ destructive du Figma ne reprend que quatre des sept hiérarchies : il n'existe ni
1455
+ `secondary-white` destructif, ni `tertiary-gray` destructif. Un booléen `destructive` rendrait
1456
+ exprimables quatre combinaisons que personne n'a dessinées, et le composant devrait inventer
1457
+ quoi peindre. Une union plate laisse le typage refuser.
1458
+ - **Le chargement n'est pas un `disabled`** (2026-09-09). Un `disabled` sort le bouton de l'ordre
1459
+ de tabulation : le focus est perdu à l'instant où l'on presse « Envoyer » et retombe sur le
1460
+ `body`, en haut du document. Le composant pose `aria-disabled`, qui annonce l'indisponibilité
1461
+ sans retirer la pièce, et bloque l'activation en avalant le clic — Entrée et Espace passent par
1462
+ le même garde, puisque les deux déclenchent un clic.
1463
+ - **La bordure du `UtilityButton` cite le jeton de Zest, pas celui du Figma** (2026-09-09). Le
1464
+ Figma la peint `#D4D4D4`, en dur dans l'état par défaut et liée à `Colors/Border/` — la
1465
+ collection anglaise résiduelle, antérieure au calque sémantique de Zest — au survol. Zest, c'est
1466
+ `Couleurs/Bordure/border-primary`, `#D3D7DE`, et c'est bien ce que le `Button` principal
1467
+ emploie. Mesuré, l'écart est de 1,48:1 contre 1,44:1 : l'œil ne le voit pas, c'est le
1468
+ vocabulaire qui est en jeu. Même piège que les icônes de fichier. ⚠️ **À corriger dans le
1469
+ Figma.**
1470
+
363
1471
  - **Le logo porte ses couleurs EN DUR, et il est à la racine** (2026-09-08). Elles correspondent
364
1472
  pourtant exactement à deux jetons — mesuré, `#00D85D` = `--zds-color-mark` et `#21304E` =
365
1473
  `--zds-color-navy-100`, ΔE 0,0 tous les deux. Les écrire en `var(--zds-…)` serait une faute :