@zevra/ui 0.22.0 → 0.24.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.
@@ -6,6 +6,7 @@ export type IconId =
6
6
  | 'activite'
7
7
  | 'aide'
8
8
  | 'app-couches'
9
+ | 'applications'
9
10
  | 'archiver'
10
11
  | 'attention'
11
12
  | 'avancer'
@@ -30,6 +31,7 @@ export type IconId =
30
31
  | 'diplome'
31
32
  | 'dossier'
32
33
  | 'eclair'
34
+ | 'empreinte'
33
35
  | 'envoyer'
34
36
  | 'epingle'
35
37
  | 'equipe'
@@ -78,6 +80,7 @@ export const CORPS: Record<IconId, string> = {
78
80
  'activite': "<path d=\"M3.33 12 H8 L10.67 5.33 L14 18.67 L16 12 H20.67\" />",
79
81
  'aide': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M9.33 9.33 A2.67 2.67 0 0 1 14.53 10.13 C14.53 11.87 12 12.27 12 14\" /><rect x=\"11.2\" y=\"16.27\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" />",
80
82
  'app-couches': "<path d=\"M12 2.67 L20 6.67 L12 10.67 L4 6.67 Z\" /><path d=\"M4 12 L12 16 L20 12\" /><path d=\"M4 17.33 L12 21.33 L20 17.33\" />",
83
+ 'applications': "<rect x=\"4\" y=\"4\" width=\"6.67\" height=\"6.67\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"13.33\" y=\"4\" width=\"6.67\" height=\"6.67\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"4\" y=\"13.33\" width=\"6.67\" height=\"6.67\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"13.33\" y=\"13.33\" width=\"6.67\" height=\"6.67\" fill=\"currentColor\" stroke=\"none\" />",
81
84
  'archiver': "<rect x=\"3.33\" y=\"4\" width=\"17.33\" height=\"4.67\" /><path d=\"M5.33 8.67 V18.93 A1.07 1.07 0 0 0 6.4 20 H17.6 A1.07 1.07 0 0 0 18.67 18.93 V8.67\" /><path d=\"M9.33 12.67 H14.67\" />",
82
85
  'attention': "<path d=\"M10.67 3.87 L2.53 17.87 A1.55 1.55 0 0 0 3.87 20.19 H20.13 A1.55 1.55 0 0 0 21.47 17.87 L13.33 3.87 A1.54 1.54 0 0 0 10.67 3.87 Z\" /><path d=\"M12 9.33 V13.33\" /><rect x=\"11.2\" y=\"15.73\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" />",
83
86
  'avancer': "<path d=\"M4 6 L11 12 L4 18 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M13 6 L20 12 L13 18 Z\" fill=\"currentColor\" stroke=\"none\" />",
@@ -102,6 +105,7 @@ export const CORPS: Record<IconId, string> = {
102
105
  'diplome': "<path d=\"M2.67 9.33 L12 4.67 L21.33 9.33 L12 14 Z\" /><path d=\"M6.67 11.6 V16 C6.67 17.47 9.07 18.67 12 18.67 C14.93 18.67 17.33 17.47 17.33 16 V11.6\" /><path d=\"M21.33 9.33 V14.67\" />",
103
106
  'dossier': "<path d=\"M1.41 6.64a1.69 1.69 0 0 1 1.69-1.69h4.8l2.12 2.4h10.87a1.69 1.69 0 0 1 1.69 1.69v9.88a1.69 1.69 0 0 1-1.69 1.69H3.11A1.69 1.69 0 0 1 1.41 20.33V6.64Z\" />",
104
107
  'eclair': "<path d=\"M13.33 2.67 L5.33 13.33 H12 L10.67 21.33 L18.67 10.67 H12 Z\" />",
108
+ 'empreinte': "<path d=\"M4 18.67 V14.67 A8 9.33 0 0 1 20 14.67 V17.33\" /><path d=\"M8 17.33 A4 5.33 0 0 1 16 17.33\" /><path d=\"M12 13.33 V20\" />",
105
109
  'envoyer': "<path d=\"M21.33 2.67 L10.67 13.33\" /><path d=\"M21.33 2.67 L14.67 21.33 L10.67 13.33 L2.67 9.33 Z\" />",
106
110
  'epingle': "<path d=\"M12 21.33 C7.56 16.89 5.33 13.42 5.33 10.67 A6.67 6.67 0 0 1 18.67 10.67 C18.67 13.42 16.44 16.89 12 21.33 Z\" /><circle cx=\"12\" cy=\"10.67\" r=\"2.4\" />",
107
111
  'equipe': "<circle cx=\"9.33\" cy=\"8\" r=\"3.33\" /><path d=\"M3.33 20 C3.33 15.33 6 13.33 9.33 13.33 C12.67 13.33 15.33 15.33 15.33 20\" /><path d=\"M16 5.33 A2.93 2.93 0 0 1 16 11.07\" /><path d=\"M17.33 13.6 C19.73 14.27 20.67 16.4 20.67 20\" />",
@@ -153,6 +157,7 @@ export const NOMS: Record<IconId, string> = {
153
157
  'activite': "Activité (pouls)",
154
158
  'aide': "Aide",
155
159
  'app-couches': "Application (couches)",
160
+ 'applications': "Applications (lanceur)",
156
161
  'archiver': "Archiver",
157
162
  'attention': "Attention (avertissement)",
158
163
  'avancer': "Avancer",
@@ -177,6 +182,7 @@ export const NOMS: Record<IconId, string> = {
177
182
  'diplome': "Diplôme (Campus)",
178
183
  'dossier': "Dossier",
179
184
  'eclair': "Éclair (action rapide)",
185
+ 'empreinte': "Empreinte (biométrie, passkey)",
180
186
  'envoyer': "Envoyer",
181
187
  'epingle': "Épingle (lieu)",
182
188
  'equipe': "Équipe (membres)",
package/react/layout.tsx CHANGED
@@ -13,6 +13,9 @@ import type {
13
13
  ReactNode,
14
14
  } from 'react'
15
15
  import { cx } from './cx.js'
16
+ // `icons` est un module COMMUN aux deux couches : l'importer ici ne tire pas
17
+ // la couche applicative derrière la vitrine.
18
+ import { Icon } from './icon.js'
16
19
 
17
20
  type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
18
21
  type Span = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>
@@ -593,3 +596,183 @@ export function Carousel({ label, className, ...reste }: CarouselProps) {
593
596
  export function CarouselItem({ className, ...reste }: Div & { className?: string }) {
594
597
  return <div className={cx('zv-carousel__item', className)} {...reste} />
595
598
  }
599
+
600
+ /** La zone de droite de la barre : lanceur et compte.
601
+ *
602
+ * ⚠️ À poser À CÔTÉ de `NavLinks`, jamais DEDANS. `NavLinks` disparaît sous
603
+ * le point de rupture (et sous 1150px en `nav--riche`), remplacée par le
604
+ * burger : un compte placé dedans s'évanouit sur téléphone, et l'on ne peut
605
+ * plus se connecter — c'est pourtant la première chose qu'on y fait. */
606
+ export function NavActions({ className, ...reste }: Div & { className?: string }) {
607
+ return <div className={cx('zv-nav__actions', className)} {...reste} />
608
+ }
609
+
610
+ /* ─── Lanceur d'applications ───────────────────────────────────────── */
611
+
612
+ /** Le lanceur : passer d'un produit Zevra à l'autre, depuis n'importe lequel.
613
+ * Il se pose À DROITE de la barre, à côté du compte — c'est la convention
614
+ * que tout le monde connaît, et ce n'est pas le moment d'en inventer une.
615
+ *
616
+ * Ouvert au survol et au focus en CSS seul : la barre reste rendue au
617
+ * serveur, sans rien à hydrater. Une app qui préfère l'ouverture au CLIC
618
+ * (la convention des lanceurs) tient l'état elle-même et passe `ouvert` —
619
+ * un état se dit en prop, il ne se peint pas depuis l'app. */
620
+ export function Lanceur({
621
+ label = 'Applications Zevra',
622
+ ouvert,
623
+ className,
624
+ children,
625
+ ...reste
626
+ }: Div & { label?: string; ouvert?: boolean; className?: string; children?: ReactNode }) {
627
+ return (
628
+ <span className={cx('zv-lanceur', ouvert && 'zv-lanceur--ouvert', className)} {...reste}>
629
+ <button
630
+ type="button"
631
+ className="zv-lanceur__btn"
632
+ aria-label={label}
633
+ aria-haspopup="true"
634
+ aria-expanded={ouvert}
635
+ >
636
+ <Icon nom="applications" taille="lg" />
637
+ </button>
638
+ <span className="zv-lanceur__panneau">
639
+ <span className="zv-menu">
640
+ <span className="zv-lanceur__apps">{children}</span>
641
+ </span>
642
+ </span>
643
+ </span>
644
+ )
645
+ }
646
+
647
+ export interface LanceurAppProps
648
+ extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> {
649
+ nom: string
650
+ /** Le glyphe du produit — une image du paquet (`@zevra/ui/logos/…`). Sans
651
+ * lui, la tuile porte l'initiale : une image qui ne charge pas ne doit
652
+ * pas laisser un trou. */
653
+ glyphe?: string
654
+ /** Le produit où l'on se trouve déjà : pose `aria-current="page"`. Sans
655
+ * lui, on ne sait pas d'où l'on part. */
656
+ courant?: boolean
657
+ className?: string
658
+ }
659
+
660
+ export function LanceurApp({ nom, glyphe, courant, className, ...reste }: LanceurAppProps) {
661
+ return (
662
+ <a
663
+ aria-current={courant ? 'page' : undefined}
664
+ className={cx('zv-lanceur__app', className)}
665
+ {...reste}
666
+ >
667
+ {glyphe ? (
668
+ <img src={glyphe} alt="" className="zv-lanceur__glyphe" />
669
+ ) : (
670
+ <span className="zv-avatar zv-avatar--soft" aria-hidden="true">
671
+ {nom.slice(0, 1).toUpperCase()}
672
+ </span>
673
+ )}
674
+ <span className="zv-lanceur__nom">{nom}</span>
675
+ </a>
676
+ )
677
+ }
678
+
679
+ /* ─── Zone de compte ───────────────────────────────────────────────── */
680
+
681
+ export interface CompteProps extends Div {
682
+ /** L'utilisateur connecté. ⚠️ ABSENT = visiteur : la zone existe quand
683
+ * même, avec l'invite de connexion. Une barre où l'on ne peut pas se
684
+ * connecter est une barre où l'on ne revient pas. */
685
+ utilisateur?: { nom: string; email?: string; initiales?: string }
686
+ /** Ouverture au clic, tenue par l'app. Voir `Lanceur`. */
687
+ ouvert?: boolean
688
+ label?: string
689
+ className?: string
690
+ /** Les entrées du panneau — des `CompteLien`. */
691
+ children?: ReactNode
692
+ }
693
+
694
+ /** La zone de compte de la barre. Connecté, elle porte la pastille
695
+ * d'initiales et le menu ; visiteur, la silhouette et l'invite de connexion.
696
+ * Dans les DEUX cas elle est là : c'est ce qui fait qu'on sait toujours où
697
+ * se connecter.
698
+ *
699
+ * ⚠️ Le panneau n'est pas un `role="menu"` : ce rôle promet une navigation
700
+ * aux flèches que le CSS seul ne peut pas tenir. Ce sont des liens dans un
701
+ * panneau, atteignables par la tabulation — une promesse tenue vaut mieux
702
+ * qu'un rôle exact et menteur. */
703
+ export function Compte({
704
+ utilisateur,
705
+ ouvert,
706
+ label,
707
+ className,
708
+ children,
709
+ ...reste
710
+ }: CompteProps) {
711
+ const initiales =
712
+ utilisateur?.initiales ??
713
+ utilisateur?.nom
714
+ .split(/[\s&]+/)
715
+ .slice(0, 2)
716
+ .map((m) => m.slice(0, 1).toUpperCase())
717
+ .join('')
718
+
719
+ return (
720
+ <span className={cx('zv-compte', ouvert && 'zv-compte--ouvert', className)} {...reste}>
721
+ <button
722
+ type="button"
723
+ className="zv-compte__btn"
724
+ aria-label={label ?? (utilisateur ? `Compte de ${utilisateur.nom}` : 'Se connecter')}
725
+ aria-haspopup="true"
726
+ aria-expanded={ouvert}
727
+ >
728
+ {utilisateur ? (
729
+ <span className="zv-avatar zv-avatar--accent" aria-hidden="true">
730
+ {initiales}
731
+ </span>
732
+ ) : (
733
+ <span className="zv-avatar zv-avatar--soft" aria-hidden="true">
734
+ <Icon nom="utilisateur" taille="sm" />
735
+ </span>
736
+ )}
737
+ {utilisateur && <span className="zv-compte__nom">{utilisateur.nom}</span>}
738
+ </button>
739
+ <span className="zv-compte__panneau">
740
+ <span className="zv-menu">
741
+ <span className="zv-compte__tete">
742
+ <span className="zv-compte__tete-nom">
743
+ {utilisateur ? utilisateur.nom : 'Vous n’êtes pas connecté'}
744
+ </span>
745
+ {utilisateur?.email && (
746
+ <span className="zv-compte__tete-mail">{utilisateur.email}</span>
747
+ )}
748
+ </span>
749
+ {children}
750
+ </span>
751
+ </span>
752
+ </span>
753
+ )
754
+ }
755
+
756
+ /** Une entrée du panneau de compte. Rendue en `<a>` avec `href`, en
757
+ * `<button>` sans — se déconnecter est une ACTION, pas une navigation, et
758
+ * un lien qui n'en est pas un se copie, s'ouvre dans un onglet et casse. */
759
+ export function CompteLien({
760
+ sortie,
761
+ className,
762
+ href,
763
+ ...reste
764
+ }: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> & {
765
+ /** Détache l'entrée par un filet : la déconnexion, en pied de panneau. */
766
+ sortie?: boolean
767
+ className?: string
768
+ }) {
769
+ const classes = cx('zv-menu__item', sortie && 'zv-compte__sortie', className)
770
+ if (href) return <a href={href} className={classes} {...reste} />
771
+ return (
772
+ <button
773
+ type="button"
774
+ className={classes}
775
+ {...(reste as Omit<HTMLAttributes<HTMLButtonElement>, 'className'>)}
776
+ />
777
+ )
778
+ }
package/src/layout.css CHANGED
@@ -236,6 +236,29 @@
236
236
  text-decoration: none;
237
237
  }
238
238
 
239
+ /* ─── Zone d'actions de la barre ────────────────────────────────────────
240
+ * Le lanceur et le compte, à droite. Elle ne fait PAS partie de
241
+ * `.zv-nav__links` et c'est tout l'intérêt : cette rangée-là disparaît sous
242
+ * le point de rupture (et sous 1150px pour `.zv-nav--riche`), remplacée par
243
+ * le burger. Le compte, lui, doit rester — sinon on ne peut plus se
244
+ * connecter depuis un téléphone, et c'est la première chose qu'on y fait.
245
+ *
246
+ * Elle se pose APRÈS `.zv-nav__links` dans le balisage, et avant le burger. */
247
+ .zv-nav__actions {
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 2px;
251
+ flex: none;
252
+ /* Décolle du dernier lien de la rangée quand les deux coexistent. */
253
+ margin-left: 10px;
254
+ }
255
+
256
+ @media (max-width: 767px) {
257
+ .zv-nav__actions {
258
+ margin-left: 0;
259
+ }
260
+ }
261
+
239
262
  /* Bouton burger de la barre vitrine.
240
263
  *
241
264
  * ⚠️ HORS RELEVÉ des planches vitrine, qui sont toutes des rendus figés à
@@ -450,6 +450,16 @@
450
450
  background: var(--hairline);
451
451
  }
452
452
 
453
+ /* ⚠️ CONTRE-ORDRE. Une entrée de menu rendue en <a> (une navigation, par
454
+ * opposition à une action) hérite du soulignement par défaut du navigateur,
455
+ * là où la même entrée en <button> n'a rien : dans un même panneau,
456
+ * « Mon compte » se soulignait et « Se déconnecter » non. Les deux doivent
457
+ * se rendre à l'identique — c'est la BALISE qui change, pas l'apparence. */
458
+ a.zv-menu__item,
459
+ a.zv-menu__item:hover {
460
+ text-decoration: none;
461
+ }
462
+
453
463
  /* ⚠️ Option SURVOLÉE AU CLAVIER — ajoutée le 25/08/2026 avec la sélection
454
464
  * dessinée. Ce n'est ni :hover (la souris n'est pas dessus) ni :focus (le
455
465
  * focus reste sur le déclencheur, c'est aria-activedescendant qui porte
@@ -1018,3 +1028,243 @@
1018
1028
  min-height: var(--tap);
1019
1029
  }
1020
1030
  }
1031
+
1032
+ /* ─── Lanceur d'applications, et zone de compte ─────────────────────────
1033
+ *
1034
+ * ⚠️ CE BLOC AUSSI DOIT RESTER APRÈS `.zv-menu`, pour la même raison que
1035
+ * `.zv-navdd` : les deux panneaux portent `.zv-menu` et lui reprennent son
1036
+ * display et son ancrage à spécificité égale — c'est l'ORDRE qui tranche.
1037
+ *
1038
+ * Les deux vivent À DROITE de la barre, côte à côte : le lanceur pour passer
1039
+ * d'un produit à l'autre, le compte pour savoir qui l'on est. C'est la
1040
+ * convention que tout le monde connaît, et ce n'est pas le moment d'en
1041
+ * inventer une autre.
1042
+ *
1043
+ * ─── L'ÉTAT ────────────────────────────────────────────────────────────
1044
+ * Ouverts au survol ET au focus (`:focus-within`), en CSS seul : la barre
1045
+ * reste rendue au serveur, sans une ligne à hydrater — même mécanique que
1046
+ * `.zv-navdd`. Une app qui préfère l'ouverture au CLIC (la convention des
1047
+ * lanceurs) pose `.zv-lanceur--ouvert` / `.zv-compte--ouvert` : la prop
1048
+ * `ouvert` du composant les rend. C'est la règle du dépôt — un état se dit
1049
+ * en prop, il ne se peint pas depuis l'app.
1050
+ */
1051
+
1052
+ .zv-lanceur,
1053
+ .zv-compte {
1054
+ position: relative;
1055
+ display: inline-flex;
1056
+ flex: none;
1057
+ }
1058
+
1059
+ /* Le déclencheur du lanceur : la cible tactile du paquet, sans cadre — le
1060
+ * chrome n'est pas une action. */
1061
+ .zv-lanceur__btn {
1062
+ display: inline-flex;
1063
+ align-items: center;
1064
+ justify-content: center;
1065
+ width: var(--tap);
1066
+ height: var(--tap);
1067
+ padding: 0;
1068
+ border: 0;
1069
+ background: none;
1070
+ color: var(--ink-2);
1071
+ cursor: pointer;
1072
+ transition: color 140ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
1073
+ }
1074
+
1075
+ .zv-lanceur:hover .zv-lanceur__btn,
1076
+ .zv-lanceur:focus-within .zv-lanceur__btn {
1077
+ color: var(--accent);
1078
+ }
1079
+
1080
+ /* Le déclencheur du compte : la pastille d'initiales, cliquable. Elle garde
1081
+ * son carré — le rond ici serait un usage de réseau social, et la pastille
1082
+ * d'initiales du paquet est carrée depuis le premier jour (data.css). */
1083
+ .zv-compte__btn {
1084
+ display: inline-flex;
1085
+ align-items: center;
1086
+ gap: 8px;
1087
+ padding: 0;
1088
+ border: 0;
1089
+ background: none;
1090
+ font-family: inherit;
1091
+ cursor: pointer;
1092
+ }
1093
+
1094
+ /* Le nom, à côté de la pastille. Il tombe sous le point de rupture : une
1095
+ * barre de téléphone n'a pas la place d'un nom, et la pastille suffit. */
1096
+ .zv-compte__nom {
1097
+ font-size: var(--app);
1098
+ font-weight: 500;
1099
+ color: var(--ink-2);
1100
+ white-space: nowrap;
1101
+ }
1102
+
1103
+ /* ─── Les deux panneaux ─────────────────────────────────────────────────
1104
+ * Ancrés à DROITE et non centrés : ils vivent au bord droit de la fenêtre,
1105
+ * et un panneau centré sur son bouton en sortirait. Le pont invisible
1106
+ * (::before) couvre l'interstice, sans quoi le survol se rompt en descendant
1107
+ * vers le panneau et celui-ci se referme sous le curseur. */
1108
+
1109
+ .zv-lanceur__panneau,
1110
+ .zv-compte__panneau {
1111
+ position: absolute;
1112
+ top: 100%;
1113
+ right: 0;
1114
+ left: auto;
1115
+ translate: none;
1116
+ padding-top: 12px;
1117
+ display: none;
1118
+ z-index: 60;
1119
+ }
1120
+
1121
+ /* ⚠️ Le `.zv-menu` intérieur est LUI-MÊME en position absolue (il est fait
1122
+ * pour s'ancrer seul sous un déclencheur). Imbriqué dans un panneau qui
1123
+ * porte déjà l'ancrage, il se positionnait une SECONDE fois — à partir du
1124
+ * bord droit du panneau, donc vers la droite, et la grille sortait de
1125
+ * l'écran sur téléphone. Sur grand écran la place restante le masquait :
1126
+ * c'est le rendu à 390px qui l'a montré. Ici le panneau ancre, le menu ne
1127
+ * fournit plus que sa surface (filet, fond, ombre). */
1128
+ .zv-lanceur__panneau .zv-menu,
1129
+ .zv-compte__panneau .zv-menu {
1130
+ position: static;
1131
+ }
1132
+
1133
+ .zv-lanceur__panneau::before,
1134
+ .zv-compte__panneau::before {
1135
+ content: '';
1136
+ position: absolute;
1137
+ inset: 0 0 auto 0;
1138
+ height: 12px;
1139
+ }
1140
+
1141
+ .zv-lanceur:hover .zv-lanceur__panneau,
1142
+ .zv-lanceur:focus-within .zv-lanceur__panneau,
1143
+ .zv-lanceur--ouvert .zv-lanceur__panneau,
1144
+ .zv-compte:hover .zv-compte__panneau,
1145
+ .zv-compte:focus-within .zv-compte__panneau,
1146
+ .zv-compte--ouvert .zv-compte__panneau {
1147
+ display: flex;
1148
+ flex-direction: column;
1149
+ }
1150
+
1151
+ /* ─── La grille d'applications ──────────────────────────────────────────
1152
+ * Trois par rangée : au-delà, les noms se coupent ; en deçà, le panneau
1153
+ * s'allonge en colonne et cesse de se lire d'un coup d'œil. */
1154
+
1155
+ .zv-lanceur__apps {
1156
+ display: grid;
1157
+ grid-template-columns: repeat(3, 92px);
1158
+ gap: 4px;
1159
+ padding: 6px;
1160
+ }
1161
+
1162
+ .zv-lanceur__app {
1163
+ display: flex;
1164
+ flex-direction: column;
1165
+ align-items: center;
1166
+ gap: 8px;
1167
+ padding: 14px 6px;
1168
+ text-align: center;
1169
+ text-decoration: none;
1170
+ color: var(--ink-2);
1171
+ }
1172
+
1173
+ /* Même contre-ordre que .zv-sidelink : sans lui, la tuile vire au bleu
1174
+ * souligné dans une page qui applique le style de lien éditorial. */
1175
+ .zv-lanceur__app:hover {
1176
+ background: var(--hairline);
1177
+ color: var(--ink-2);
1178
+ text-decoration: none;
1179
+ }
1180
+
1181
+ .zv-lanceur__app:focus-visible {
1182
+ outline-offset: -2px;
1183
+ }
1184
+
1185
+ /* L'application COURANTE reste marquée : sans elle, on ne sait pas d'où
1186
+ * l'on part. aria-current="page" est le marquage attendu ; la classe existe
1187
+ * pour que l'état se pose aussi sans routeur. */
1188
+ .zv-lanceur__app--on,
1189
+ .zv-lanceur__app[aria-current='page'] {
1190
+ background: var(--accent-soft);
1191
+ color: var(--accent);
1192
+ }
1193
+
1194
+ /* Le glyphe du produit. 28px : la taille de .zv-icon--xl, pour qu'une tuile
1195
+ * à image et une tuile à icône tracée se posent sur la même ligne. */
1196
+ .zv-lanceur__glyphe {
1197
+ display: block;
1198
+ width: 28px;
1199
+ height: 28px;
1200
+ object-fit: contain;
1201
+ }
1202
+
1203
+ .zv-lanceur__nom {
1204
+ font-size: var(--app-sm);
1205
+ font-weight: 500;
1206
+ line-height: 1.3;
1207
+ /* Un nom de produit trop long ne doit pas élargir la colonne : il se coupe
1208
+ * plutôt que de déformer la grille. */
1209
+ overflow-wrap: anywhere;
1210
+ }
1211
+
1212
+ /* ─── L'en-tête du panneau de compte ────────────────────────────────────
1213
+ * Qui l'on est, avant ce qu'on peut faire. Non connecté, il porte l'invite
1214
+ * de connexion : la zone de compte existe TOUJOURS, connecté ou non — une
1215
+ * barre où l'on ne peut pas se connecter est une barre où l'on ne revient
1216
+ * pas. */
1217
+
1218
+ .zv-compte__tete {
1219
+ display: flex;
1220
+ flex-direction: column;
1221
+ gap: 2px;
1222
+ border-bottom: 1px solid var(--hairline);
1223
+ padding: 12px 14px;
1224
+ margin-bottom: 4px;
1225
+ }
1226
+
1227
+ .zv-compte__tete-nom {
1228
+ font-size: var(--app);
1229
+ font-weight: 600;
1230
+ color: var(--ink);
1231
+ }
1232
+
1233
+ .zv-compte__tete-mail {
1234
+ font-family: var(--font-mono);
1235
+ font-size: var(--mono);
1236
+ color: var(--label);
1237
+ overflow-wrap: anywhere;
1238
+ }
1239
+
1240
+ .zv-compte__panneau .zv-menu {
1241
+ min-width: 240px;
1242
+ }
1243
+
1244
+ /* Le pied du panneau : la déconnexion, séparée de ce qui précède. Elle ne
1245
+ * prend PAS l'encre d'erreur — se déconnecter n'est pas un incident. */
1246
+ .zv-compte__sortie {
1247
+ border-top: 1px solid var(--hairline);
1248
+ margin-top: 4px;
1249
+ padding-top: 4px;
1250
+ }
1251
+
1252
+ @media (max-width: 767px) {
1253
+ /* Le nom tombe : la pastille suffit à dire qui l'on est, et la barre n'a
1254
+ * plus la place. */
1255
+ .zv-compte__nom {
1256
+ display: none;
1257
+ }
1258
+
1259
+ /* Les deux panneaux sont plafonnés à l'écran, comme le menu et le
1260
+ * popover : au bord droit d'un téléphone, un panneau de 300px pousserait
1261
+ * la page en défilement horizontal. */
1262
+ .zv-lanceur__panneau .zv-menu,
1263
+ .zv-compte__panneau .zv-menu {
1264
+ max-width: calc(100vw - 32px);
1265
+ }
1266
+
1267
+ .zv-lanceur__apps {
1268
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1269
+ }
1270
+ }