@zevra/ui 0.5.0 → 0.7.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/src/forms.css CHANGED
@@ -183,6 +183,15 @@
183
183
  border: 1px solid var(--line);
184
184
  background: var(--white);
185
185
  padding-inline: 13px;
186
+ /* La prise de focus s'ANIME (25/08) : sans transition, le cadre sautait
187
+ * d'un état à l'autre et le retour se lisait comme un défaut de rendu. */
188
+ transition:
189
+ border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),
190
+ box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);
191
+ }
192
+
193
+ .zv-search:hover {
194
+ border-color: var(--accent-line);
186
195
  }
187
196
 
188
197
  /* DÉRIVÉ : la bordure de focus relevée sur .zv-input, portée par le cadre.
@@ -196,16 +205,42 @@
196
205
  * · supprimer ce filet priverait la souris de tout retour, alors que le
197
206
  * relevé lui en donne un sur les champs bordés.
198
207
  * La redondance coûte moins cher que l'une ou l'autre perte. */
208
+ /* ⚠️ REPRIS le 25/08/2026 — la DA jugeait la prise de focus « boff ». Elle
209
+ * l'était : le cadre passait à --accent-line, un filet si pâle qu'à côté de
210
+ * l'anneau du champ intérieur il ne se voyait pas. Trois corrections :
211
+ * · le filet passe à --accent PLEIN, pas à sa version diluée ;
212
+ * · un halo --accent-soft de 3px double le trait — c'est lui qui rend le
213
+ * retour visible du coin de l'œil, sans déplacer un pixel de la mise en
214
+ * page (box-shadow ne prend pas de place) ;
215
+ * · la loupe s'allume avec le cadre : elle cesse d'être une invite grise
216
+ * pour devenir la marque du champ actif.
217
+ *
218
+ * L'anneau :focus-visible du champ INTÉRIEUR est neutralisé ici, et
219
+ * seulement ici : le cadre le remplace, et il le remplace pour la souris
220
+ * comme pour le clavier. C'est ce qui lève la double marque assumée jusque-là
221
+ * — un seul objet à l'écran, un seul retour. Le champ nu reste parfaitement
222
+ * repérable au clavier, puisque c'est le cadre entier qui s'allume. */
199
223
  .zv-search:focus-within {
200
- border-color: var(--accent-line);
224
+ border-color: var(--accent);
225
+ box-shadow: 0 0 0 3px var(--accent-soft);
226
+ }
227
+
228
+ .zv-search:focus-within .zv-search__icon {
229
+ stroke: var(--accent);
230
+ }
231
+
232
+ .zv-search__input:focus-visible {
233
+ outline: none;
201
234
  }
202
235
 
203
236
  .zv-search__icon {
204
237
  flex: none;
205
238
  width: 13px;
206
239
  height: 13px;
207
- /* Même gris que le placeholder : la loupe est une invite, pas une donnée. */
240
+ /* Même gris que le placeholder : la loupe est une invite, pas une donnée.
241
+ * Elle s'allume avec le cadre — voir :focus-within plus bas. */
208
242
  stroke: var(--label);
243
+ transition: stroke 140ms cubic-bezier(0.2, 0.7, 0.2, 1);
209
244
  }
210
245
 
211
246
  .zv-search__input {
@@ -279,6 +314,63 @@
279
314
  max-width: 100%;
280
315
  }
281
316
 
317
+ /* Ancre pleine largeur : le cas d'un CHAMP de formulaire, où la liste doit
318
+ * tomber exactement sous le contrôle et faire la même largeur que lui. La
319
+ * variante en ligne ci-dessus reste celle des filtres, où le déclencheur se
320
+ * dimensionne sur son libellé. */
321
+ .zv-select-anchor--block {
322
+ display: block;
323
+ width: 100%;
324
+ }
325
+
326
+ .zv-select-anchor--block .zv-menu {
327
+ width: 100%;
328
+ min-width: 0;
329
+ }
330
+
331
+ /* ─── Déclencheur de sélection DESSINÉE ─────────────────────────────────
332
+ * ⚠️ AJOUTÉ le 25/08/2026 : « le select, faut le designer jusque dans
333
+ * l'affichage de la liste » (DA, sur pièces).
334
+ *
335
+ * Le <select> natif (.zv-input--select) reste dans le paquet et garde son
336
+ * usage — mais il ne peut PAS être habillé : la liste déroulée est peinte
337
+ * par le système, pas par la page. Sur un macOS en mode sombre, elle sort
338
+ * en menu noir au milieu d'une interface claire. Aucun CSS ne corrige cela,
339
+ * c'est une limite du contrôle.
340
+ *
341
+ * D'où ce déclencheur, qui a l'apparence exacte d'un champ et ouvre le
342
+ * .zv-menu du paquet. Ce qu'il COÛTE, et qu'il faut assumer en le
343
+ * choisissant : le clavier, l'annonce et la liste native sont à réimplémenter
344
+ * — c'est fait dans le composant React (SelectMenu), pas dans ce fichier.
345
+ * Un gabarit qui pose ces classes à la main sans ce travail livre un select
346
+ * inutilisable au clavier. Dans ce cas, prendre le natif. */
347
+ .zv-input--trigger {
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: space-between;
351
+ gap: 10px;
352
+ text-align: left;
353
+ line-height: 1.4;
354
+ cursor: pointer;
355
+ transition: border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);
356
+ }
357
+
358
+ .zv-input--trigger:hover {
359
+ border-color: var(--accent-line);
360
+ }
361
+
362
+ /* Ouvert, le déclencheur reste marqué : sans cela, la liste flotte au-dessus
363
+ * d'un champ qui a l'air au repos, et on ne sait plus lequel elle sert. */
364
+ .zv-input--trigger[aria-expanded='true'] {
365
+ border-color: var(--accent);
366
+ }
367
+
368
+ /* Le placeholder d'un déclencheur est du texte, pas un ::placeholder : il
369
+ * lui faut donc sa propre teinte, celle du champ vide. */
370
+ .zv-input--trigger[data-vide='oui'] {
371
+ color: var(--label);
372
+ }
373
+
282
374
  .zv-select {
283
375
  display: inline-flex;
284
376
  align-items: center;
@@ -384,37 +476,70 @@
384
476
  * elle est portée par l'aplat plein et par le libellé accolé, pas par cette
385
477
  * nuance-là. */
386
478
 
479
+ /* ⚠️ REDESSINÉ le 25/08/2026 — la DA a tranché « switch horrible » sur
480
+ * pièces, et elle avait raison sur trois points :
481
+ * · 28×16 avec un bouton rond de 12, c'est un jouet posé au milieu de
482
+ * contrôles carrés de 16px de haut ; la ligne de réglage n'avait aucune
483
+ * unité ;
484
+ * · la piste éteinte était --accent-line, un indigo très pâle : éteint et
485
+ * allumé se distinguaient par la SATURATION d'une même teinte, ce qui
486
+ * tombe au premier écran mal calibré et disparaît en daltonisme ;
487
+ * · le bouton rond contredisait l'arbitrage « carré » du même jour.
488
+ * La piste passe donc à 36×20, à angle vif, avec un filet qui la pose comme
489
+ * les champs voisins ; éteint elle est BLANCHE au filet gris, allumée elle
490
+ * est un aplat indigo plein. L'écart n'est plus une nuance, c'est un
491
+ * contraste. */
387
492
  .zv-switch {
388
493
  position: relative;
389
494
  display: inline-block;
390
- width: 28px;
391
- height: 16px;
495
+ width: 36px;
496
+ height: 20px;
392
497
  flex: none;
393
- background: var(--accent-line);
498
+ border: 1px solid var(--line);
499
+ background: var(--white);
394
500
  /* La courbe est celle du paquet (motion.css, `--zv-easing`), recopiée en
395
501
  * clair : forms.css ne dépend pas de motion.css et n'a pas à en dépendre
396
502
  * pour une transition de couleur. Ce n'est PAS `ease` — le défaut d'un
397
503
  * framework tiers n'a rien à faire dans le système. */
398
- transition: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
504
+ transition:
505
+ background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1),
506
+ border-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
507
+ }
508
+
509
+ .zv-switch-hit:hover .zv-switch {
510
+ border-color: var(--accent-line);
399
511
  }
400
512
 
401
513
  .zv-switch--on {
514
+ border-color: var(--accent);
402
515
  background: var(--accent);
403
516
  }
404
517
 
518
+ .zv-switch-hit:hover .zv-switch--on {
519
+ border-color: var(--accent-deep);
520
+ background: var(--accent-deep);
521
+ }
522
+
523
+ /* Bouton CARRÉ : le rond ici n'est pas une forme porteuse de sens (ce que
524
+ * sont la pastille d'état ou le radio), c'était un style — il tombe avec les
525
+ * autres rayons. Éteint il est gris comme un contrôle inactif, allumé il
526
+ * passe en blanc sur l'aplat indigo. */
405
527
  .zv-switch__knob {
406
528
  position: absolute;
407
529
  inset-block-start: 2px;
408
530
  inset-inline-start: 2px;
409
- width: 12px;
410
- height: 12px;
411
- border-radius: 9999px;
412
- background: var(--white);
413
- transition: inset-inline-start 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
531
+ width: 14px;
532
+ height: 14px;
533
+ border-radius: 0;
534
+ background: var(--label);
535
+ transition:
536
+ inset-inline-start 150ms cubic-bezier(0.2, 0.7, 0.2, 1),
537
+ background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
414
538
  }
415
539
 
416
540
  .zv-switch--on .zv-switch__knob {
417
- inset-inline-start: 14px;
541
+ inset-inline-start: 18px;
542
+ background: var(--white);
418
543
  }
419
544
 
420
545
  /* Cible tactile portée par une enveloppe : agrandir la piste elle-même
package/src/layout.css CHANGED
@@ -61,7 +61,9 @@
61
61
 
62
62
  .zv-col-3 { grid-column: span 3; }
63
63
  .zv-col-4 { grid-column: span 4; }
64
+ .zv-col-5 { grid-column: span 5; }
64
65
  .zv-col-6 { grid-column: span 6; }
66
+ .zv-col-7 { grid-column: span 7; }
65
67
  .zv-col-8 { grid-column: span 8; }
66
68
  .zv-col-9 { grid-column: span 9; }
67
69
 
@@ -520,7 +522,9 @@
520
522
 
521
523
  .zv-col-3,
522
524
  .zv-col-4,
525
+ .zv-col-5,
523
526
  .zv-col-6,
527
+ .zv-col-7,
524
528
  .zv-col-8,
525
529
  .zv-col-9,
526
530
  .zv-section-head__main,
@@ -429,6 +429,15 @@
429
429
  background: var(--hairline);
430
430
  }
431
431
 
432
+ /* ⚠️ Option SURVOLÉE AU CLAVIER — ajoutée le 25/08/2026 avec la sélection
433
+ * dessinée. Ce n'est ni :hover (la souris n'est pas dessus) ni :focus (le
434
+ * focus reste sur le déclencheur, c'est aria-activedescendant qui porte
435
+ * l'annonce). Sans cette classe, parcourir la liste aux flèches ne se voit
436
+ * pas : le lecteur d'écran suit, l'œil non. */
437
+ .zv-menu__item--survol {
438
+ background: var(--hairline);
439
+ }
440
+
432
441
  .zv-menu__item:focus-visible {
433
442
  outline-offset: -2px;
434
443
  }
package/src/tokens.css CHANGED
@@ -74,11 +74,27 @@
74
74
  --lead: 18px;
75
75
  --body: 15px;
76
76
 
77
- /* Rayons */
78
- --r-btn: 5px;
79
- --r-pill: 3px;
80
- --r-panel: 10px;
81
- --r-card: 16px;
77
+ /* Rayons — ARBITRÉS À ZÉRO le 25/08/2026.
78
+ *
79
+ * La DA a tranché sur pièces : « très carré, ça fait pro et inspire
80
+ * confiance » ; le rond disait « flexible, jovial », qui n'est pas ce que
81
+ * vend Zevra. Le relevé donnait 5/3/10/16px, hérités d'un état antérieur
82
+ * de la charte — et déjà contredits sur place, puisque les champs de
83
+ * saisie, les étiquettes et les pastilles d'initiales étaient à angle vif
84
+ * depuis le premier jour. Les quatre tokens s'alignent sur eux.
85
+ *
86
+ * ⚠️ Les tokens RESTENT, à zéro : ils nomment quatre intentions
87
+ * distinctes (action, pastille, panneau flottant, carte). Le jour où la DA
88
+ * veut rendre de la rondeur à l'une d'elles, c'est une ligne ici — pas une
89
+ * chasse aux `border-radius` dans douze fichiers.
90
+ *
91
+ * ⚠️ Ce qui n'est PAS concerné : le rond de forme, qui porte du sens et
92
+ * non du style — pastille d'état, bouton d'interrupteur, radio, halo. Ils
93
+ * écrivent leur `border-radius` en clair, sans passer par un token. */
94
+ --r-btn: 0;
95
+ --r-pill: 0;
96
+ --r-panel: 0;
97
+ --r-card: 0;
82
98
 
83
99
  /* Ombres */
84
100
  --shadow-card: 0 2px 10px rgba(11, 16, 32, 0.05);
package/tokens/index.js CHANGED
@@ -99,11 +99,17 @@ export const mesures = {
99
99
  specFilet: '56px',
100
100
  }
101
101
 
102
+ /** Rayons — À ZÉRO depuis l'arbitrage DA du 25/08/2026 (« très carré, ça
103
+ * fait pro et inspire confiance »). Les quatre clés restent : elles nomment
104
+ * quatre intentions distinctes, et rendre de la rondeur à l'une d'elles
105
+ * doit rester une ligne à changer, pas une chasse dans douze fichiers.
106
+ * Le rond de FORME — pastille d'état, bouton d'interrupteur, radio, halo —
107
+ * ne passe pas par ici : il porte du sens, pas du style. */
102
108
  export const rayons = {
103
- btn: '5px',
104
- pill: '3px',
105
- panel: '10px',
106
- card: '16px',
109
+ btn: '0',
110
+ pill: '0',
111
+ panel: '0',
112
+ card: '0',
107
113
  }
108
114
 
109
115
  export const ombres = {