@zevra/ui 0.20.0 → 0.22.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/DECISIONS.md +123 -0
- package/dist/content.d.ts +24 -5
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +21 -5
- package/dist/content.js.map +1 -1
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/dist/email-tokens.d.ts +56 -0
- package/dist/email-tokens.d.ts.map +1 -0
- package/dist/email-tokens.js +58 -0
- package/dist/email-tokens.js.map +1 -0
- package/dist/email.d.ts +77 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +166 -0
- package/dist/email.js.map +1 -0
- package/package.json +6 -2
- package/react/content.tsx +48 -8
- package/react/css-inline.ts +1 -1
- package/react/email-tokens.ts +60 -0
- package/react/email.ts +219 -0
- package/src/components.css +50 -3
- package/src/layout.css +28 -5
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// GÉNÉRÉ par scripts/build-email-tokens.mjs — ne pas éditer à la main.
|
|
2
|
+
// Source de vérité : tokens/index.js, lui-même miroir de src/tokens.css.
|
|
3
|
+
|
|
4
|
+
export const COULEURS = {
|
|
5
|
+
"ink": "#0b1020",
|
|
6
|
+
"ink2": "#4a5468",
|
|
7
|
+
"muted": "#5a637a",
|
|
8
|
+
"label": "#8a93a8",
|
|
9
|
+
"line": "#e3e6ef",
|
|
10
|
+
"hairline": "#eef1f8",
|
|
11
|
+
"paper": "#f5f6fa",
|
|
12
|
+
"panel": "#fbfcfe",
|
|
13
|
+
"white": "#ffffff",
|
|
14
|
+
"accentDeep": "#1e1b8c",
|
|
15
|
+
"accent": "#2a2aa8",
|
|
16
|
+
"accentLine": "#c9d6f5",
|
|
17
|
+
"accentSoft": "#eaf1ff",
|
|
18
|
+
"violet": "#8a3af8",
|
|
19
|
+
"iris": "#5848f8",
|
|
20
|
+
"bleu": "#2f6ef8",
|
|
21
|
+
"cyan": "#14c2f8",
|
|
22
|
+
"lavande": "#b8a8f8",
|
|
23
|
+
"accentOnDark": "#a5b4fc",
|
|
24
|
+
"ok": "#1c6b4a",
|
|
25
|
+
"okSoft": "#e6f3ec",
|
|
26
|
+
"okLine": "#bfdccb",
|
|
27
|
+
"warn": "#b98a1f",
|
|
28
|
+
"warnSoft": "#faf3e2",
|
|
29
|
+
"warnLine": "#ebdcb4",
|
|
30
|
+
"warnInk": "#8a6d1e",
|
|
31
|
+
"err": "#b5202c",
|
|
32
|
+
"errSoft": "#fdece9",
|
|
33
|
+
"errLine": "#efc9cd",
|
|
34
|
+
"fmtPdf": "#c4362b",
|
|
35
|
+
"fmtDocx": "#2a5ba8"
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
export const POLICES = {
|
|
39
|
+
"display": "\"Outfit\", \"Helvetica Neue\", Arial, sans-serif",
|
|
40
|
+
"texte": "\"Public Sans\", \"Helvetica Neue\", Arial, sans-serif",
|
|
41
|
+
"mono": "\"Space Grotesk\", \"Menlo\", monospace",
|
|
42
|
+
"code": "ui-monospace, \"SF Mono\", \"Menlo\", \"Consolas\", monospace"
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
export const TAILLES = {
|
|
46
|
+
"h1": "76px",
|
|
47
|
+
"h2": "56px",
|
|
48
|
+
"h3": "38px",
|
|
49
|
+
"cardTitle": "24px",
|
|
50
|
+
"lead": "18px",
|
|
51
|
+
"body": "15px",
|
|
52
|
+
"appLg": "14px",
|
|
53
|
+
"app": "13px",
|
|
54
|
+
"appSm": "12px",
|
|
55
|
+
"appH2": "20px",
|
|
56
|
+
"appH3": "16px",
|
|
57
|
+
"monoLg": "11px",
|
|
58
|
+
"mono": "10px",
|
|
59
|
+
"monoSm": "9px"
|
|
60
|
+
} as const
|
package/react/email.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Gabarits d'e-mail « Encre & Papier ».
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ CE MODULE N'EST PAS DU REACT ET N'EMPLOIE PAS LE CSS DU PAQUET. Il rend
|
|
4
|
+
// des chaînes de HTML. C'est la seule dérogation à la règle première du dépôt
|
|
5
|
+
// (« le CSS est la source de vérité, les composants n'en sont qu'une
|
|
6
|
+
// façade »), et elle est imposée par le support, pas choisie :
|
|
7
|
+
//
|
|
8
|
+
// · il n'y a pas de feuille de style à charger dans un e-mail ;
|
|
9
|
+
// · Outlook et Gmail SUPPRIMENT les propriétés personnalisées : un
|
|
10
|
+
// `var(--accent)` s'y rend en couleur nulle, donc en texte noir sur fond
|
|
11
|
+
// transparent. Les valeurs sont donc écrites en clair — mais tirées du
|
|
12
|
+
// miroir JS des tokens, que tests/tokens.test.js tient aligné sur le CSS ;
|
|
13
|
+
// · ni flexbox ni grid ne sont fiables : la mise en page est en TABLES ;
|
|
14
|
+
// · les dégradés ne se rendent pas dans Outlook — le bouton plein perd le
|
|
15
|
+
// sien et devient un aplat, le filet de spectre devient trois aplats.
|
|
16
|
+
//
|
|
17
|
+
// Il vit dans react/ parce que c'est la racine de compilation du paquet
|
|
18
|
+
// (`rootDir`), pas parce qu'il en dépend : il n'importe pas une ligne de
|
|
19
|
+
// React et n'émet aucune classe `zv-`.
|
|
20
|
+
//
|
|
21
|
+
// ⚠️ Ce module ÉCHAPPE à ta feuille de style : ce qui n'est pas ici ne
|
|
22
|
+
// s'affichera pas. Ne pas y ajouter de classes en espérant qu'elles soient
|
|
23
|
+
// stylées.
|
|
24
|
+
|
|
25
|
+
import { COULEURS, POLICES as PILES } from './email-tokens.js'
|
|
26
|
+
|
|
27
|
+
/** ⚠️ Les piles de polices du paquet contiennent des GUILLEMETS DOUBLES
|
|
28
|
+
* (`"Public Sans", …`), et tout ici s'écrit dans un attribut `style="…"`
|
|
29
|
+
* délimité par ces mêmes guillemets : l'attribut se refermerait au premier
|
|
30
|
+
* guillemet interne, et TOUT le style du nœud serait perdu. En HTML, un
|
|
31
|
+
* guillemet simple est équivalent à l'intérieur d'une valeur de police.
|
|
32
|
+
* Cette faute ne se voit pas à la relecture — elle se voit au rendu, où
|
|
33
|
+
* l'e-mail entier repasse en Times sans style. */
|
|
34
|
+
const pourAttribut = (pile: string) => pile.replace(/"/g, "'")
|
|
35
|
+
|
|
36
|
+
const POLICES = {
|
|
37
|
+
display: pourAttribut(PILES.display),
|
|
38
|
+
texte: pourAttribut(PILES.texte),
|
|
39
|
+
mono: pourAttribut(PILES.mono),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ─── Réglages communs ─────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
/** 600px : la largeur qui traverse tous les clients depuis vingt ans, dont
|
|
45
|
+
* le volet de lecture d'Outlook. Ce n'est pas --page-max, et ça n'a rien à
|
|
46
|
+
* voir : c'est une contrainte du support. */
|
|
47
|
+
const LARGEUR = 600
|
|
48
|
+
|
|
49
|
+
const CORPS = `font-family:${POLICES.texte};font-size:15px;line-height:1.6;color:${COULEURS.muted};`
|
|
50
|
+
const TITRE = `font-family:${POLICES.display};font-weight:300;letter-spacing:-0.02em;color:${COULEURS.ink};`
|
|
51
|
+
|
|
52
|
+
/** Échappe ce qui entre dans le HTML. Un objet d'e-mail vient souvent d'une
|
|
53
|
+
* base de données : un nom de client avec une esperluette casserait le
|
|
54
|
+
* document, et un `<` bien placé y injecterait du balisage. */
|
|
55
|
+
export function echappe(texte: string): string {
|
|
56
|
+
return texte
|
|
57
|
+
.replace(/&/g, '&')
|
|
58
|
+
.replace(/</g, '<')
|
|
59
|
+
.replace(/>/g, '>')
|
|
60
|
+
.replace(/"/g, '"')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ─── Blocs ────────────────────────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
/** Sur-titre : le filet de spectre suivi du label en capitales.
|
|
66
|
+
* PREMIER des trois emplois autorisés du spectre. Le dégradé est rendu en
|
|
67
|
+
* TROIS APLATS de 19px (violet, bleu, cyan) : un `linear-gradient` ne se
|
|
68
|
+
* rend pas dans Outlook, qui afficherait une bande vide. */
|
|
69
|
+
export function emailKicker(texte: string): string {
|
|
70
|
+
const brin = (couleur: string) =>
|
|
71
|
+
`<td width="19" height="2" style="background:${couleur};font-size:0;line-height:0;"> </td>`
|
|
72
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 14px;"><tr>
|
|
73
|
+
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>${brin(COULEURS.violet)}${brin(COULEURS.bleu)}${brin(COULEURS.cyan)}</tr></table></td>
|
|
74
|
+
<td style="padding-left:12px;font-family:${POLICES.mono};font-size:11px;font-weight:500;letter-spacing:0.2em;text-transform:uppercase;color:${COULEURS.accent};">${echappe(texte)}</td>
|
|
75
|
+
</tr></table>`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Le titre du message. Un seul par e-mail : c'est la promesse de l'objet,
|
|
79
|
+
* tenue à l'ouverture. */
|
|
80
|
+
export function emailTitre(texte: string): string {
|
|
81
|
+
return `<h1 style="${TITRE}font-size:26px;line-height:1.2;margin:0 0 18px;">${echappe(texte)}</h1>`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Un paragraphe. Le HTML d'enrichissement (`<strong>`, `<a>`) est admis :
|
|
85
|
+
* c'est du texte rédigé, pas une saisie utilisateur — l'échapper
|
|
86
|
+
* afficherait des chevrons. Passer par `echappe` toute valeur qui vient
|
|
87
|
+
* d'une base. */
|
|
88
|
+
export function emailTexte(html: string): string {
|
|
89
|
+
return `<p style="${CORPS}margin:0 0 16px;">${html}</p>`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** L'action. UN SEUL bouton plein par e-mail, comme par page — c'est ce qui
|
|
93
|
+
* fait qu'on sait où cliquer.
|
|
94
|
+
* Construit en table : le `padding` d'un `<a>` est ignoré par Outlook, qui
|
|
95
|
+
* rendrait un lien nu au milieu du message. Aplat indigo et non dégradé,
|
|
96
|
+
* pour la même raison. Angle vif, comme partout. */
|
|
97
|
+
export function emailBouton({ href, libelle }: { href: string; libelle: string }): string {
|
|
98
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:26px 0;"><tr>
|
|
99
|
+
<td align="center" bgcolor="${COULEURS.accentDeep}" style="background:${COULEURS.accentDeep};">
|
|
100
|
+
<a href="${echappe(href)}" style="display:inline-block;padding:15px 30px;font-family:${POLICES.texte};font-size:15px;font-weight:600;color:${COULEURS.white};text-decoration:none;">${echappe(libelle)}</a>
|
|
101
|
+
</td></tr></table>`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Le lien secondaire, sous le bouton : l'autre chemin, pour qui ne veut pas
|
|
105
|
+
* du premier. Souligné — dans un e-mail, la couleur seule ne suffit pas à
|
|
106
|
+
* annoncer un lien. */
|
|
107
|
+
export function emailLienSecondaire({ href, libelle }: { href: string; libelle: string }): string {
|
|
108
|
+
return `<p style="${CORPS}font-size:14px;margin:0 0 16px;"><a href="${echappe(href)}" style="color:${COULEURS.accent};text-decoration:underline;">${echappe(libelle)}</a></p>`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** La mention discrète : ce qu'on lit après avoir décidé. */
|
|
112
|
+
export function emailNote(html: string): string {
|
|
113
|
+
return `<p style="font-family:${POLICES.texte};font-size:13px;line-height:1.55;color:${COULEURS.label};margin:0 0 12px;">${html}</p>`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** L'encart : un extrait, un récapitulatif de commande, un code. */
|
|
117
|
+
export function emailEncart(html: string): string {
|
|
118
|
+
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 18px;"><tr>
|
|
119
|
+
<td style="background:${COULEURS.panel};border:1px solid ${COULEURS.hairline};border-left:2px solid ${COULEURS.accentLine};padding:14px 18px;font-family:${POLICES.texte};font-size:13px;line-height:1.6;color:${COULEURS.ink};">${html}</td>
|
|
120
|
+
</tr></table>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Le filet de séparation. */
|
|
124
|
+
export function emailFilet(): string {
|
|
125
|
+
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:22px 0;"><tr>
|
|
126
|
+
<td height="1" style="background:${COULEURS.line};font-size:0;line-height:0;"> </td></tr></table>`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ─── Le gabarit ───────────────────────────────────────────────────── */
|
|
130
|
+
|
|
131
|
+
export interface EmailOptions {
|
|
132
|
+
/** Repris en `<title>`. Ce n'est PAS l'objet du message : l'objet se pose
|
|
133
|
+
* à l'envoi, dans l'en-tête SMTP. */
|
|
134
|
+
titre: string
|
|
135
|
+
/** Le texte gris qui suit l'objet dans la liste des messages. Sans lui,
|
|
136
|
+
* le client y affiche les premiers mots du HTML — souvent « Voir cet
|
|
137
|
+
* e-mail dans votre navigateur », ce qui gâche la seule ligne dont on
|
|
138
|
+
* dispose pour convaincre d'ouvrir. */
|
|
139
|
+
preheader?: string
|
|
140
|
+
/** Le nom de la marque, écrit en toutes lettres. ⚠️ OBLIGATOIRE même avec
|
|
141
|
+
* un logo : une image sur deux est bloquée par défaut, et un e-mail qui
|
|
142
|
+
* ne dit pas de qui il vient part à la corbeille. */
|
|
143
|
+
marque: string
|
|
144
|
+
/** L'URL ABSOLUE du logo — une image d'e-mail ne se résout pas depuis un
|
|
145
|
+
* chemin relatif, et les data-URI sont bloquées par Gmail. */
|
|
146
|
+
logo?: { src: string; hauteur?: number }
|
|
147
|
+
/** Les blocs, concaténés. */
|
|
148
|
+
contenu: string
|
|
149
|
+
/** Sous la carte : mentions légales, désinscription. */
|
|
150
|
+
pied?: string
|
|
151
|
+
lang?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Assemble un e-mail complet.
|
|
156
|
+
*
|
|
157
|
+
* ⚠️ Le mode sombre est REFUSÉ explicitement (`color-scheme: light`) : la
|
|
158
|
+
* charte est claire par nature, et l'inversion automatique d'Apple Mail
|
|
159
|
+
* retourne les neutres sans toucher aux aplats — l'indigo du bouton
|
|
160
|
+
* resterait sur un fond devenu noir, et les contrastes réglés dans un sens
|
|
161
|
+
* partiraient dans l'autre. C'est la même règle que pour les surfaces
|
|
162
|
+
* sombres du système : elles se font avec la bande manifeste, jamais en
|
|
163
|
+
* inversant les neutres.
|
|
164
|
+
*/
|
|
165
|
+
export function email({
|
|
166
|
+
titre,
|
|
167
|
+
preheader,
|
|
168
|
+
marque,
|
|
169
|
+
logo,
|
|
170
|
+
contenu,
|
|
171
|
+
pied,
|
|
172
|
+
lang = 'fr',
|
|
173
|
+
}: EmailOptions): string {
|
|
174
|
+
const enTete = logo
|
|
175
|
+
? `<img src="${echappe(logo.src)}" height="${logo.hauteur ?? 28}" alt="${echappe(marque)}" style="display:block;border:0;height:${logo.hauteur ?? 28}px;width:auto;">`
|
|
176
|
+
: `<span style="font-family:${POLICES.display};font-weight:600;font-size:20px;letter-spacing:-0.02em;color:${COULEURS.ink};">${echappe(marque)}</span>`
|
|
177
|
+
|
|
178
|
+
return `<!doctype html>
|
|
179
|
+
<html lang="${lang}">
|
|
180
|
+
<head>
|
|
181
|
+
<meta charset="utf-8">
|
|
182
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
183
|
+
<meta name="color-scheme" content="light">
|
|
184
|
+
<meta name="supported-color-schemes" content="light">
|
|
185
|
+
<title>${echappe(titre)}</title>
|
|
186
|
+
<style>
|
|
187
|
+
/* Le seul <style> du document, et il ne porte QUE du responsive : ce que
|
|
188
|
+
Gmail en supprime ne fait rien perdre, tout le reste est en ligne. */
|
|
189
|
+
@media (max-width:620px) {
|
|
190
|
+
.zv-carte { padding:26px 20px !important; }
|
|
191
|
+
.zv-marge { padding:20px 12px !important; }
|
|
192
|
+
}
|
|
193
|
+
</style>
|
|
194
|
+
</head>
|
|
195
|
+
<body style="margin:0;padding:0;width:100%;background:${COULEURS.paper};-webkit-text-size-adjust:100%;">
|
|
196
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">${preheader ? echappe(preheader) : ''}</div>
|
|
197
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${COULEURS.paper};">
|
|
198
|
+
<tr><td align="center" class="zv-marge" style="padding:32px 16px;">
|
|
199
|
+
<table role="presentation" width="${LARGEUR}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${LARGEUR}px;">
|
|
200
|
+
|
|
201
|
+
<tr><td style="padding:0 0 20px;">${enTete}</td></tr>
|
|
202
|
+
|
|
203
|
+
<tr><td class="zv-carte" bgcolor="${COULEURS.white}" style="background:${COULEURS.white};border:1px solid ${COULEURS.line};padding:34px 36px;">
|
|
204
|
+
${contenu}
|
|
205
|
+
</td></tr>
|
|
206
|
+
|
|
207
|
+
${
|
|
208
|
+
pied
|
|
209
|
+
? `<tr><td style="padding:20px 4px 0;font-family:${POLICES.texte};font-size:12px;line-height:1.6;color:${COULEURS.label};">${pied}</td></tr>`
|
|
210
|
+
: ''
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
</table>
|
|
214
|
+
</td></tr>
|
|
215
|
+
</table>
|
|
216
|
+
</body>
|
|
217
|
+
</html>
|
|
218
|
+
`
|
|
219
|
+
}
|
package/src/components.css
CHANGED
|
@@ -636,8 +636,23 @@
|
|
|
636
636
|
/* ─── FAQ ───────────────────────────────────────────────────────────────
|
|
637
637
|
* Cinq questions par page au maximum. */
|
|
638
638
|
|
|
639
|
+
/* Le filet, comme `.zv-fold` : sans lui, la FAQ est un aplat blanc posé sur
|
|
640
|
+
* le papier, sans début ni fin. Toutes les autres surfaces blanches de la
|
|
641
|
+
* vitrine — carte, encart, tarif — en portent un ; celle-ci faisait tache. */
|
|
639
642
|
.zv-faq {
|
|
640
643
|
background: var(--white);
|
|
644
|
+
border: 1px solid var(--line);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* Chaque question est un `<details>` : le repli est natif, sans JavaScript.
|
|
648
|
+
* Le filet sépare les questions ENTRE elles ; la première n'en porte pas, il
|
|
649
|
+
* tomberait sur celui du bloc et ferait un trait double. */
|
|
650
|
+
.zv-faq__item {
|
|
651
|
+
border-top: 1px solid var(--hairline);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.zv-faq__item:first-child {
|
|
655
|
+
border-top: 0;
|
|
641
656
|
}
|
|
642
657
|
|
|
643
658
|
.zv-faq__q {
|
|
@@ -646,22 +661,54 @@
|
|
|
646
661
|
align-items: center;
|
|
647
662
|
gap: 16px;
|
|
648
663
|
padding: 18px 24px;
|
|
649
|
-
border-bottom: 1px solid var(--hairline);
|
|
650
664
|
font-size: 14px;
|
|
651
665
|
font-weight: 600;
|
|
652
666
|
cursor: pointer;
|
|
667
|
+
/* Le marqueur natif du `<summary>` tombe : le chevron du composant fait le
|
|
668
|
+
* même travail sans imposer le triangle du navigateur. Même traitement que
|
|
669
|
+
* `.zv-fold__titre`. */
|
|
670
|
+
list-style: none;
|
|
653
671
|
}
|
|
654
672
|
|
|
655
|
-
.zv-faq__q
|
|
673
|
+
.zv-faq__q::-webkit-details-marker {
|
|
674
|
+
display: none;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* Une FAQ de vitrine veut ses questions dans le PLAN DU DOCUMENT : c'est ce
|
|
678
|
+
* que lisent les moteurs, et ce que parcourt un lecteur d'écran qui saute de
|
|
679
|
+
* titre en titre. On accepte donc un `<h2>`/`<h3>`/`<h4>` comme question,
|
|
680
|
+
* et on lui retire ce que le navigateur lui met — taille, graisse, marges —
|
|
681
|
+
* pour qu'il se rende comme le reste des questions. Le niveau reste au
|
|
682
|
+
* balisage, l'apparence au système. */
|
|
683
|
+
.zv-faq__q > :is(h2, h3, h4) {
|
|
684
|
+
margin: 0;
|
|
685
|
+
font: inherit;
|
|
686
|
+
color: inherit;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.zv-faq__q:hover {
|
|
690
|
+
background: var(--panel);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* Le chevron porte sa classe : cibler tout `svg` du titre attraperait aussi
|
|
694
|
+
* une icône posée dans le libellé, qui se verrait rétrécie et retournée. */
|
|
695
|
+
.zv-faq__chevron {
|
|
656
696
|
width: 14px;
|
|
657
697
|
height: 14px;
|
|
658
698
|
flex: none;
|
|
659
699
|
color: var(--accent);
|
|
700
|
+
transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.zv-faq__item[open] .zv-faq__chevron {
|
|
704
|
+
rotate: 180deg;
|
|
660
705
|
}
|
|
661
706
|
|
|
662
707
|
.zv-faq__a {
|
|
663
|
-
|
|
708
|
+
margin: 0;
|
|
709
|
+
padding: 0 24px 18px;
|
|
664
710
|
font-size: 13px;
|
|
711
|
+
line-height: 1.6;
|
|
665
712
|
color: var(--muted);
|
|
666
713
|
}
|
|
667
714
|
|
package/src/layout.css
CHANGED
|
@@ -395,11 +395,15 @@
|
|
|
395
395
|
padding-top: 22px;
|
|
396
396
|
padding-bottom: 12px;
|
|
397
397
|
z-index: 40;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
/* Le voile est PLEIN derrière la barre, et il se dégrade EN DESSOUS d'elle
|
|
399
|
+
* (::after). Il était auparavant dégradé sur toute sa hauteur : à sa base
|
|
400
|
+
* il ne restait donc presque rien, et un titre de 38 ou 56px qui passait
|
|
401
|
+
* dessous se retrouvait COUPÉ EN DEUX — moitié voilée, moitié nette, avec
|
|
402
|
+
* les entrées de nav posées sur ses lettres. Deux textes superposés, aucun
|
|
403
|
+
* lisible. Signalé en recette sur mcp-factory.
|
|
404
|
+
* Le dégradé n'est pas perdu : il vit sous la barre, là où il adoucit le
|
|
405
|
+
* passage sans se disputer la place avec la navigation. */
|
|
406
|
+
background: color-mix(in srgb, var(--paper) 92%, transparent);
|
|
403
407
|
/* ⚠️ LE PRÉFIXÉ D'ABORD. Lightning CSS — le compilateur de Tailwind v4,
|
|
404
408
|
* donc celui de toutes nos applications Next — SUPPRIME `backdrop-filter`
|
|
405
409
|
* non préfixé quand il précède la forme `-webkit-`. Or Firefox ne connaît
|
|
@@ -419,6 +423,25 @@
|
|
|
419
423
|
backdrop-filter: blur(6px);
|
|
420
424
|
}
|
|
421
425
|
|
|
426
|
+
/* La frange de dégradé, posée SOUS la barre. `pointer-events: none` : elle
|
|
427
|
+
* couvre le haut du contenu, elle ne doit rien intercepter. */
|
|
428
|
+
.zv-header::after {
|
|
429
|
+
content: '';
|
|
430
|
+
position: absolute;
|
|
431
|
+
top: 100%;
|
|
432
|
+
left: 0;
|
|
433
|
+
right: 0;
|
|
434
|
+
height: 24px;
|
|
435
|
+
background: linear-gradient(
|
|
436
|
+
180deg,
|
|
437
|
+
color-mix(in srgb, var(--paper) 92%, transparent),
|
|
438
|
+
color-mix(in srgb, var(--paper) 0%, transparent)
|
|
439
|
+
);
|
|
440
|
+
-webkit-backdrop-filter: blur(6px);
|
|
441
|
+
backdrop-filter: blur(6px);
|
|
442
|
+
pointer-events: none;
|
|
443
|
+
}
|
|
444
|
+
|
|
422
445
|
.zv-header:has(> .zv-nav) {
|
|
423
446
|
display: block;
|
|
424
447
|
padding-top: 0;
|