@zevra/ui 0.21.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 +69 -0
- 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/email-tokens.ts +60 -0
- package/react/email.ts +219 -0
package/DECISIONS.md
CHANGED
|
@@ -1179,3 +1179,72 @@ rien intercepter.
|
|
|
1179
1179
|
|
|
1180
1180
|
Aucun changement de gabarit : le `::after` est en absolu sous une barre déjà
|
|
1181
1181
|
fixée, il ne prend pas de place dans le flux.
|
|
1182
|
+
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
## Les e-mails (27/08/2026)
|
|
1186
|
+
|
|
1187
|
+
Le paquet gagne des gabarits d'e-mail (`@zevra/ui/email`). Ils étaient
|
|
1188
|
+
jusqu'ici refaits par chaque produit, hors charte — fond crème, angles
|
|
1189
|
+
ronds, surligné jaune, titres en serif : rien de tout cela n'est dans le
|
|
1190
|
+
système.
|
|
1191
|
+
|
|
1192
|
+
### La dérogation à la règle première, et pourquoi elle est imposée
|
|
1193
|
+
|
|
1194
|
+
« Le CSS est la source de vérité, les composants n'en sont qu'une façade » —
|
|
1195
|
+
sauf ici. Ce n'est pas un choix, c'est le support :
|
|
1196
|
+
|
|
1197
|
+
- il n'y a **aucune feuille de style** à charger dans un e-mail ;
|
|
1198
|
+
- Outlook et Gmail **suppriment les propriétés personnalisées** : un
|
|
1199
|
+
`var(--accent)` s'y rend en couleur nulle, donc en texte noir sur fond
|
|
1200
|
+
transparent ;
|
|
1201
|
+
- ni flexbox ni grid ne sont fiables : la mise en page est en **tables** ;
|
|
1202
|
+
- les **dégradés** ne se rendent pas dans Outlook.
|
|
1203
|
+
|
|
1204
|
+
Les valeurs sont donc écrites en clair — mais tirées du **miroir JS des
|
|
1205
|
+
tokens**, que `tokens/index.js` destinait déjà explicitement à cet usage
|
|
1206
|
+
(« couleurs de thème d'un e-mail ») et que `tests/tokens.test.js` tient
|
|
1207
|
+
aligné sur le CSS. Aucune seconde source n'apparaît. Le détour par
|
|
1208
|
+
`react/email-tokens.ts` (généré) est imposé par le `rootDir` du tsconfig.
|
|
1209
|
+
|
|
1210
|
+
### Ce que le support fait perdre, et ce qu'on met à la place
|
|
1211
|
+
|
|
1212
|
+
| Dans le système | Dans l'e-mail | Pourquoi |
|
|
1213
|
+
|---|---|---|
|
|
1214
|
+
| Bouton plein en dégradé indigo | Aplat `--accent-deep` | Les dégradés ne se rendent pas dans Outlook |
|
|
1215
|
+
| Filet de spectre en `linear-gradient` | Trois aplats de 19 px | Idem — une bande vide sinon |
|
|
1216
|
+
| `padding` sur le `<a>` du bouton | Table + `bgcolor` | Outlook ignore le padding d'un `<a>` |
|
|
1217
|
+
| Largeur `--page-max` | 600 px | Le volet de lecture d'Outlook |
|
|
1218
|
+
|
|
1219
|
+
Ce qui NE change pas : l'angle vif, le papier, l'encre, les trois familles,
|
|
1220
|
+
un seul bouton plein par message, et le filet de spectre comme signature —
|
|
1221
|
+
premier de ses trois emplois autorisés.
|
|
1222
|
+
|
|
1223
|
+
### Le mode sombre est refusé
|
|
1224
|
+
|
|
1225
|
+
`color-scheme: light`, explicitement. La charte est claire par nature :
|
|
1226
|
+
l'inversion automatique d'Apple Mail retourne les neutres **sans toucher aux
|
|
1227
|
+
aplats**, et l'indigo du bouton se retrouverait sur un fond devenu noir. Même
|
|
1228
|
+
règle que pour les surfaces sombres du système — elles se font avec la bande
|
|
1229
|
+
manifeste, jamais en inversant les neutres.
|
|
1230
|
+
|
|
1231
|
+
### Deux règles qui ne sont pas décoratives
|
|
1232
|
+
|
|
1233
|
+
**La marque s'écrit en toutes lettres, même avec un logo.** Une image sur
|
|
1234
|
+
deux est bloquée par défaut ; un e-mail qui ne dit pas de qui il vient part à
|
|
1235
|
+
la corbeille. D'où `marque` obligatoire, et le logo en option.
|
|
1236
|
+
|
|
1237
|
+
**Le preheader est un contenu, pas un détail.** Sans lui, le client affiche
|
|
1238
|
+
les premiers mots du HTML dans la liste des messages — c'est la seule ligne
|
|
1239
|
+
dont on dispose pour convaincre d'ouvrir.
|
|
1240
|
+
|
|
1241
|
+
### Un piège rencontré, et gardé
|
|
1242
|
+
|
|
1243
|
+
Les piles de polices du paquet contiennent des **guillemets doubles**
|
|
1244
|
+
(`"Public Sans", …`) et tout s'écrit dans un `style="…"` délimité par ces
|
|
1245
|
+
mêmes guillemets : l'attribut se referme au premier guillemet interne et le
|
|
1246
|
+
nœud perd **tout** son style. L'e-mail entier repassait en Times, sans
|
|
1247
|
+
couleurs, avec un libellé de bouton blanc devenu illisible sur l'aplat
|
|
1248
|
+
indigo. Cela ne se voit pas à la relecture — cela s'est vu au rendu. Les
|
|
1249
|
+
piles sont converties en guillemets simples, et un test refuse désormais
|
|
1250
|
+
tout attribut `style` contenant un guillemet double.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const COULEURS: {
|
|
2
|
+
readonly ink: "#0b1020";
|
|
3
|
+
readonly ink2: "#4a5468";
|
|
4
|
+
readonly muted: "#5a637a";
|
|
5
|
+
readonly label: "#8a93a8";
|
|
6
|
+
readonly line: "#e3e6ef";
|
|
7
|
+
readonly hairline: "#eef1f8";
|
|
8
|
+
readonly paper: "#f5f6fa";
|
|
9
|
+
readonly panel: "#fbfcfe";
|
|
10
|
+
readonly white: "#ffffff";
|
|
11
|
+
readonly accentDeep: "#1e1b8c";
|
|
12
|
+
readonly accent: "#2a2aa8";
|
|
13
|
+
readonly accentLine: "#c9d6f5";
|
|
14
|
+
readonly accentSoft: "#eaf1ff";
|
|
15
|
+
readonly violet: "#8a3af8";
|
|
16
|
+
readonly iris: "#5848f8";
|
|
17
|
+
readonly bleu: "#2f6ef8";
|
|
18
|
+
readonly cyan: "#14c2f8";
|
|
19
|
+
readonly lavande: "#b8a8f8";
|
|
20
|
+
readonly accentOnDark: "#a5b4fc";
|
|
21
|
+
readonly ok: "#1c6b4a";
|
|
22
|
+
readonly okSoft: "#e6f3ec";
|
|
23
|
+
readonly okLine: "#bfdccb";
|
|
24
|
+
readonly warn: "#b98a1f";
|
|
25
|
+
readonly warnSoft: "#faf3e2";
|
|
26
|
+
readonly warnLine: "#ebdcb4";
|
|
27
|
+
readonly warnInk: "#8a6d1e";
|
|
28
|
+
readonly err: "#b5202c";
|
|
29
|
+
readonly errSoft: "#fdece9";
|
|
30
|
+
readonly errLine: "#efc9cd";
|
|
31
|
+
readonly fmtPdf: "#c4362b";
|
|
32
|
+
readonly fmtDocx: "#2a5ba8";
|
|
33
|
+
};
|
|
34
|
+
export declare const POLICES: {
|
|
35
|
+
readonly display: "\"Outfit\", \"Helvetica Neue\", Arial, sans-serif";
|
|
36
|
+
readonly texte: "\"Public Sans\", \"Helvetica Neue\", Arial, sans-serif";
|
|
37
|
+
readonly mono: "\"Space Grotesk\", \"Menlo\", monospace";
|
|
38
|
+
readonly code: "ui-monospace, \"SF Mono\", \"Menlo\", \"Consolas\", monospace";
|
|
39
|
+
};
|
|
40
|
+
export declare const TAILLES: {
|
|
41
|
+
readonly h1: "76px";
|
|
42
|
+
readonly h2: "56px";
|
|
43
|
+
readonly h3: "38px";
|
|
44
|
+
readonly cardTitle: "24px";
|
|
45
|
+
readonly lead: "18px";
|
|
46
|
+
readonly body: "15px";
|
|
47
|
+
readonly appLg: "14px";
|
|
48
|
+
readonly app: "13px";
|
|
49
|
+
readonly appSm: "12px";
|
|
50
|
+
readonly appH2: "20px";
|
|
51
|
+
readonly appH3: "16px";
|
|
52
|
+
readonly monoLg: "11px";
|
|
53
|
+
readonly mono: "10px";
|
|
54
|
+
readonly monoSm: "9px";
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=email-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-tokens.d.ts","sourceRoot":"","sources":["../react/email-tokens.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;aACnB,GAAK,EAAE,SAAS;aAChB,IAAM,EAAE,SAAS;aACjB,KAAO,EAAE,SAAS;aAClB,KAAO,EAAE,SAAS;aAClB,IAAM,EAAE,SAAS;aACjB,QAAU,EAAE,SAAS;aACrB,KAAO,EAAE,SAAS;aAClB,KAAO,EAAE,SAAS;aAClB,KAAO,EAAE,SAAS;aAClB,UAAY,EAAE,SAAS;aACvB,MAAQ,EAAE,SAAS;aACnB,UAAY,EAAE,SAAS;aACvB,UAAY,EAAE,SAAS;aACvB,MAAQ,EAAE,SAAS;aACnB,IAAM,EAAE,SAAS;aACjB,IAAM,EAAE,SAAS;aACjB,IAAM,EAAE,SAAS;aACjB,OAAS,EAAE,SAAS;aACpB,YAAc,EAAE,SAAS;aACzB,EAAI,EAAE,SAAS;aACf,MAAQ,EAAE,SAAS;aACnB,MAAQ,EAAE,SAAS;aACnB,IAAM,EAAE,SAAS;aACjB,QAAU,EAAE,SAAS;aACrB,QAAU,EAAE,SAAS;aACrB,OAAS,EAAE,SAAS;aACpB,GAAK,EAAE,SAAS;aAChB,OAAS,EAAE,SAAS;aACpB,OAAS,EAAE,SAAS;aACpB,MAAQ,EAAE,SAAS;aACnB,OAAS,EAAE,SAAS;CACZ,CAAA;AAEV,eAAO,MAAM,OAAO;aAClB,OAAS,EAAE,mDAAmD;aAC9D,KAAO,EAAE,wDAAwD;aACjE,IAAM,EAAE,yCAAyC;aACjD,IAAM,EAAE,+DAA+D;CAC/D,CAAA;AAEV,eAAO,MAAM,OAAO;aAClB,EAAI,EAAE,MAAM;aACZ,EAAI,EAAE,MAAM;aACZ,EAAI,EAAE,MAAM;aACZ,SAAW,EAAE,MAAM;aACnB,IAAM,EAAE,MAAM;aACd,IAAM,EAAE,MAAM;aACd,KAAO,EAAE,MAAM;aACf,GAAK,EAAE,MAAM;aACb,KAAO,EAAE,MAAM;aACf,KAAO,EAAE,MAAM;aACf,KAAO,EAAE,MAAM;aACf,MAAQ,EAAE,MAAM;aAChB,IAAM,EAAE,MAAM;aACd,MAAQ,EAAE,KAAK;CACP,CAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
export const COULEURS = {
|
|
4
|
+
"ink": "#0b1020",
|
|
5
|
+
"ink2": "#4a5468",
|
|
6
|
+
"muted": "#5a637a",
|
|
7
|
+
"label": "#8a93a8",
|
|
8
|
+
"line": "#e3e6ef",
|
|
9
|
+
"hairline": "#eef1f8",
|
|
10
|
+
"paper": "#f5f6fa",
|
|
11
|
+
"panel": "#fbfcfe",
|
|
12
|
+
"white": "#ffffff",
|
|
13
|
+
"accentDeep": "#1e1b8c",
|
|
14
|
+
"accent": "#2a2aa8",
|
|
15
|
+
"accentLine": "#c9d6f5",
|
|
16
|
+
"accentSoft": "#eaf1ff",
|
|
17
|
+
"violet": "#8a3af8",
|
|
18
|
+
"iris": "#5848f8",
|
|
19
|
+
"bleu": "#2f6ef8",
|
|
20
|
+
"cyan": "#14c2f8",
|
|
21
|
+
"lavande": "#b8a8f8",
|
|
22
|
+
"accentOnDark": "#a5b4fc",
|
|
23
|
+
"ok": "#1c6b4a",
|
|
24
|
+
"okSoft": "#e6f3ec",
|
|
25
|
+
"okLine": "#bfdccb",
|
|
26
|
+
"warn": "#b98a1f",
|
|
27
|
+
"warnSoft": "#faf3e2",
|
|
28
|
+
"warnLine": "#ebdcb4",
|
|
29
|
+
"warnInk": "#8a6d1e",
|
|
30
|
+
"err": "#b5202c",
|
|
31
|
+
"errSoft": "#fdece9",
|
|
32
|
+
"errLine": "#efc9cd",
|
|
33
|
+
"fmtPdf": "#c4362b",
|
|
34
|
+
"fmtDocx": "#2a5ba8"
|
|
35
|
+
};
|
|
36
|
+
export const POLICES = {
|
|
37
|
+
"display": "\"Outfit\", \"Helvetica Neue\", Arial, sans-serif",
|
|
38
|
+
"texte": "\"Public Sans\", \"Helvetica Neue\", Arial, sans-serif",
|
|
39
|
+
"mono": "\"Space Grotesk\", \"Menlo\", monospace",
|
|
40
|
+
"code": "ui-monospace, \"SF Mono\", \"Menlo\", \"Consolas\", monospace"
|
|
41
|
+
};
|
|
42
|
+
export const TAILLES = {
|
|
43
|
+
"h1": "76px",
|
|
44
|
+
"h2": "56px",
|
|
45
|
+
"h3": "38px",
|
|
46
|
+
"cardTitle": "24px",
|
|
47
|
+
"lead": "18px",
|
|
48
|
+
"body": "15px",
|
|
49
|
+
"appLg": "14px",
|
|
50
|
+
"app": "13px",
|
|
51
|
+
"appSm": "12px",
|
|
52
|
+
"appH2": "20px",
|
|
53
|
+
"appH3": "16px",
|
|
54
|
+
"monoLg": "11px",
|
|
55
|
+
"mono": "10px",
|
|
56
|
+
"monoSm": "9px"
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=email-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-tokens.js","sourceRoot":"","sources":["../react/email-tokens.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,yEAAyE;AAEzE,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,SAAS;IACrB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,SAAS;IACvB,QAAQ,EAAE,SAAS;IACnB,YAAY,EAAE,SAAS;IACvB,YAAY,EAAE,SAAS;IACvB,QAAQ,EAAE,SAAS;IACnB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,SAAS;IACpB,cAAc,EAAE,SAAS;IACzB,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,SAAS;IACnB,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,SAAS;IACrB,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,SAAS;IACpB,QAAQ,EAAE,SAAS;IACnB,SAAS,EAAE,SAAS;CACZ,CAAA;AAEV,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,mDAAmD;IAC9D,OAAO,EAAE,wDAAwD;IACjE,MAAM,EAAE,yCAAyC;IACjD,MAAM,EAAE,+DAA+D;CAC/D,CAAA;AAEV,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,MAAM;IACnB,MAAM,EAAE,MAAM;IACd,MAAM,EAAE,MAAM;IACd,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,MAAM;IACf,OAAO,EAAE,MAAM;IACf,OAAO,EAAE,MAAM;IACf,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,KAAK;CACP,CAAA"}
|
package/dist/email.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Échappe ce qui entre dans le HTML. Un objet d'e-mail vient souvent d'une
|
|
2
|
+
* base de données : un nom de client avec une esperluette casserait le
|
|
3
|
+
* document, et un `<` bien placé y injecterait du balisage. */
|
|
4
|
+
export declare function echappe(texte: string): string;
|
|
5
|
+
/** Sur-titre : le filet de spectre suivi du label en capitales.
|
|
6
|
+
* PREMIER des trois emplois autorisés du spectre. Le dégradé est rendu en
|
|
7
|
+
* TROIS APLATS de 19px (violet, bleu, cyan) : un `linear-gradient` ne se
|
|
8
|
+
* rend pas dans Outlook, qui afficherait une bande vide. */
|
|
9
|
+
export declare function emailKicker(texte: string): string;
|
|
10
|
+
/** Le titre du message. Un seul par e-mail : c'est la promesse de l'objet,
|
|
11
|
+
* tenue à l'ouverture. */
|
|
12
|
+
export declare function emailTitre(texte: string): string;
|
|
13
|
+
/** Un paragraphe. Le HTML d'enrichissement (`<strong>`, `<a>`) est admis :
|
|
14
|
+
* c'est du texte rédigé, pas une saisie utilisateur — l'échapper
|
|
15
|
+
* afficherait des chevrons. Passer par `echappe` toute valeur qui vient
|
|
16
|
+
* d'une base. */
|
|
17
|
+
export declare function emailTexte(html: string): string;
|
|
18
|
+
/** L'action. UN SEUL bouton plein par e-mail, comme par page — c'est ce qui
|
|
19
|
+
* fait qu'on sait où cliquer.
|
|
20
|
+
* Construit en table : le `padding` d'un `<a>` est ignoré par Outlook, qui
|
|
21
|
+
* rendrait un lien nu au milieu du message. Aplat indigo et non dégradé,
|
|
22
|
+
* pour la même raison. Angle vif, comme partout. */
|
|
23
|
+
export declare function emailBouton({ href, libelle }: {
|
|
24
|
+
href: string;
|
|
25
|
+
libelle: string;
|
|
26
|
+
}): string;
|
|
27
|
+
/** Le lien secondaire, sous le bouton : l'autre chemin, pour qui ne veut pas
|
|
28
|
+
* du premier. Souligné — dans un e-mail, la couleur seule ne suffit pas à
|
|
29
|
+
* annoncer un lien. */
|
|
30
|
+
export declare function emailLienSecondaire({ href, libelle }: {
|
|
31
|
+
href: string;
|
|
32
|
+
libelle: string;
|
|
33
|
+
}): string;
|
|
34
|
+
/** La mention discrète : ce qu'on lit après avoir décidé. */
|
|
35
|
+
export declare function emailNote(html: string): string;
|
|
36
|
+
/** L'encart : un extrait, un récapitulatif de commande, un code. */
|
|
37
|
+
export declare function emailEncart(html: string): string;
|
|
38
|
+
/** Le filet de séparation. */
|
|
39
|
+
export declare function emailFilet(): string;
|
|
40
|
+
export interface EmailOptions {
|
|
41
|
+
/** Repris en `<title>`. Ce n'est PAS l'objet du message : l'objet se pose
|
|
42
|
+
* à l'envoi, dans l'en-tête SMTP. */
|
|
43
|
+
titre: string;
|
|
44
|
+
/** Le texte gris qui suit l'objet dans la liste des messages. Sans lui,
|
|
45
|
+
* le client y affiche les premiers mots du HTML — souvent « Voir cet
|
|
46
|
+
* e-mail dans votre navigateur », ce qui gâche la seule ligne dont on
|
|
47
|
+
* dispose pour convaincre d'ouvrir. */
|
|
48
|
+
preheader?: string;
|
|
49
|
+
/** Le nom de la marque, écrit en toutes lettres. ⚠️ OBLIGATOIRE même avec
|
|
50
|
+
* un logo : une image sur deux est bloquée par défaut, et un e-mail qui
|
|
51
|
+
* ne dit pas de qui il vient part à la corbeille. */
|
|
52
|
+
marque: string;
|
|
53
|
+
/** L'URL ABSOLUE du logo — une image d'e-mail ne se résout pas depuis un
|
|
54
|
+
* chemin relatif, et les data-URI sont bloquées par Gmail. */
|
|
55
|
+
logo?: {
|
|
56
|
+
src: string;
|
|
57
|
+
hauteur?: number;
|
|
58
|
+
};
|
|
59
|
+
/** Les blocs, concaténés. */
|
|
60
|
+
contenu: string;
|
|
61
|
+
/** Sous la carte : mentions légales, désinscription. */
|
|
62
|
+
pied?: string;
|
|
63
|
+
lang?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Assemble un e-mail complet.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Le mode sombre est REFUSÉ explicitement (`color-scheme: light`) : la
|
|
69
|
+
* charte est claire par nature, et l'inversion automatique d'Apple Mail
|
|
70
|
+
* retourne les neutres sans toucher aux aplats — l'indigo du bouton
|
|
71
|
+
* resterait sur un fond devenu noir, et les contrastes réglés dans un sens
|
|
72
|
+
* partiraient dans l'autre. C'est la même règle que pour les surfaces
|
|
73
|
+
* sombres du système : elles se font avec la bande manifeste, jamais en
|
|
74
|
+
* inversant les neutres.
|
|
75
|
+
*/
|
|
76
|
+
export declare function email({ titre, preheader, marque, logo, contenu, pied, lang, }: EmailOptions): string;
|
|
77
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../react/email.ts"],"names":[],"mappings":"AAmDA;;gEAEgE;AAChE,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAID;;;6DAG6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;2BAC2B;AAC3B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;kBAGkB;AAClB,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;qDAIqD;AACrD,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAKxF;AAED;;wBAEwB;AACxB,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAEhG;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,8BAA8B;AAC9B,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAID,MAAM,WAAW,YAAY;IAC3B;0CACsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb;;;4CAGwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;0DAEsD;IACtD,MAAM,EAAE,MAAM,CAAA;IACd;mEAC+D;IAC/D,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,SAAS,EACT,MAAM,EACN,IAAI,EACJ,OAAO,EACP,IAAI,EACJ,IAAW,GACZ,EAAE,YAAY,GAAG,MAAM,CA8CvB"}
|
package/dist/email.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
import { COULEURS, POLICES as PILES } from './email-tokens.js';
|
|
25
|
+
/** ⚠️ Les piles de polices du paquet contiennent des GUILLEMETS DOUBLES
|
|
26
|
+
* (`"Public Sans", …`), et tout ici s'écrit dans un attribut `style="…"`
|
|
27
|
+
* délimité par ces mêmes guillemets : l'attribut se refermerait au premier
|
|
28
|
+
* guillemet interne, et TOUT le style du nœud serait perdu. En HTML, un
|
|
29
|
+
* guillemet simple est équivalent à l'intérieur d'une valeur de police.
|
|
30
|
+
* Cette faute ne se voit pas à la relecture — elle se voit au rendu, où
|
|
31
|
+
* l'e-mail entier repasse en Times sans style. */
|
|
32
|
+
const pourAttribut = (pile) => pile.replace(/"/g, "'");
|
|
33
|
+
const POLICES = {
|
|
34
|
+
display: pourAttribut(PILES.display),
|
|
35
|
+
texte: pourAttribut(PILES.texte),
|
|
36
|
+
mono: pourAttribut(PILES.mono),
|
|
37
|
+
};
|
|
38
|
+
/* ─── Réglages communs ─────────────────────────────────────────────── */
|
|
39
|
+
/** 600px : la largeur qui traverse tous les clients depuis vingt ans, dont
|
|
40
|
+
* le volet de lecture d'Outlook. Ce n'est pas --page-max, et ça n'a rien à
|
|
41
|
+
* voir : c'est une contrainte du support. */
|
|
42
|
+
const LARGEUR = 600;
|
|
43
|
+
const CORPS = `font-family:${POLICES.texte};font-size:15px;line-height:1.6;color:${COULEURS.muted};`;
|
|
44
|
+
const TITRE = `font-family:${POLICES.display};font-weight:300;letter-spacing:-0.02em;color:${COULEURS.ink};`;
|
|
45
|
+
/** Échappe ce qui entre dans le HTML. Un objet d'e-mail vient souvent d'une
|
|
46
|
+
* base de données : un nom de client avec une esperluette casserait le
|
|
47
|
+
* document, et un `<` bien placé y injecterait du balisage. */
|
|
48
|
+
export function echappe(texte) {
|
|
49
|
+
return texte
|
|
50
|
+
.replace(/&/g, '&')
|
|
51
|
+
.replace(/</g, '<')
|
|
52
|
+
.replace(/>/g, '>')
|
|
53
|
+
.replace(/"/g, '"');
|
|
54
|
+
}
|
|
55
|
+
/* ─── Blocs ────────────────────────────────────────────────────────── */
|
|
56
|
+
/** Sur-titre : le filet de spectre suivi du label en capitales.
|
|
57
|
+
* PREMIER des trois emplois autorisés du spectre. Le dégradé est rendu en
|
|
58
|
+
* TROIS APLATS de 19px (violet, bleu, cyan) : un `linear-gradient` ne se
|
|
59
|
+
* rend pas dans Outlook, qui afficherait une bande vide. */
|
|
60
|
+
export function emailKicker(texte) {
|
|
61
|
+
const brin = (couleur) => `<td width="19" height="2" style="background:${couleur};font-size:0;line-height:0;"> </td>`;
|
|
62
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 14px;"><tr>
|
|
63
|
+
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>${brin(COULEURS.violet)}${brin(COULEURS.bleu)}${brin(COULEURS.cyan)}</tr></table></td>
|
|
64
|
+
<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>
|
|
65
|
+
</tr></table>`;
|
|
66
|
+
}
|
|
67
|
+
/** Le titre du message. Un seul par e-mail : c'est la promesse de l'objet,
|
|
68
|
+
* tenue à l'ouverture. */
|
|
69
|
+
export function emailTitre(texte) {
|
|
70
|
+
return `<h1 style="${TITRE}font-size:26px;line-height:1.2;margin:0 0 18px;">${echappe(texte)}</h1>`;
|
|
71
|
+
}
|
|
72
|
+
/** Un paragraphe. Le HTML d'enrichissement (`<strong>`, `<a>`) est admis :
|
|
73
|
+
* c'est du texte rédigé, pas une saisie utilisateur — l'échapper
|
|
74
|
+
* afficherait des chevrons. Passer par `echappe` toute valeur qui vient
|
|
75
|
+
* d'une base. */
|
|
76
|
+
export function emailTexte(html) {
|
|
77
|
+
return `<p style="${CORPS}margin:0 0 16px;">${html}</p>`;
|
|
78
|
+
}
|
|
79
|
+
/** L'action. UN SEUL bouton plein par e-mail, comme par page — c'est ce qui
|
|
80
|
+
* fait qu'on sait où cliquer.
|
|
81
|
+
* Construit en table : le `padding` d'un `<a>` est ignoré par Outlook, qui
|
|
82
|
+
* rendrait un lien nu au milieu du message. Aplat indigo et non dégradé,
|
|
83
|
+
* pour la même raison. Angle vif, comme partout. */
|
|
84
|
+
export function emailBouton({ href, libelle }) {
|
|
85
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:26px 0;"><tr>
|
|
86
|
+
<td align="center" bgcolor="${COULEURS.accentDeep}" style="background:${COULEURS.accentDeep};">
|
|
87
|
+
<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>
|
|
88
|
+
</td></tr></table>`;
|
|
89
|
+
}
|
|
90
|
+
/** Le lien secondaire, sous le bouton : l'autre chemin, pour qui ne veut pas
|
|
91
|
+
* du premier. Souligné — dans un e-mail, la couleur seule ne suffit pas à
|
|
92
|
+
* annoncer un lien. */
|
|
93
|
+
export function emailLienSecondaire({ href, libelle }) {
|
|
94
|
+
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>`;
|
|
95
|
+
}
|
|
96
|
+
/** La mention discrète : ce qu'on lit après avoir décidé. */
|
|
97
|
+
export function emailNote(html) {
|
|
98
|
+
return `<p style="font-family:${POLICES.texte};font-size:13px;line-height:1.55;color:${COULEURS.label};margin:0 0 12px;">${html}</p>`;
|
|
99
|
+
}
|
|
100
|
+
/** L'encart : un extrait, un récapitulatif de commande, un code. */
|
|
101
|
+
export function emailEncart(html) {
|
|
102
|
+
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 18px;"><tr>
|
|
103
|
+
<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>
|
|
104
|
+
</tr></table>`;
|
|
105
|
+
}
|
|
106
|
+
/** Le filet de séparation. */
|
|
107
|
+
export function emailFilet() {
|
|
108
|
+
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:22px 0;"><tr>
|
|
109
|
+
<td height="1" style="background:${COULEURS.line};font-size:0;line-height:0;"> </td></tr></table>`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Assemble un e-mail complet.
|
|
113
|
+
*
|
|
114
|
+
* ⚠️ Le mode sombre est REFUSÉ explicitement (`color-scheme: light`) : la
|
|
115
|
+
* charte est claire par nature, et l'inversion automatique d'Apple Mail
|
|
116
|
+
* retourne les neutres sans toucher aux aplats — l'indigo du bouton
|
|
117
|
+
* resterait sur un fond devenu noir, et les contrastes réglés dans un sens
|
|
118
|
+
* partiraient dans l'autre. C'est la même règle que pour les surfaces
|
|
119
|
+
* sombres du système : elles se font avec la bande manifeste, jamais en
|
|
120
|
+
* inversant les neutres.
|
|
121
|
+
*/
|
|
122
|
+
export function email({ titre, preheader, marque, logo, contenu, pied, lang = 'fr', }) {
|
|
123
|
+
const enTete = logo
|
|
124
|
+
? `<img src="${echappe(logo.src)}" height="${logo.hauteur ?? 28}" alt="${echappe(marque)}" style="display:block;border:0;height:${logo.hauteur ?? 28}px;width:auto;">`
|
|
125
|
+
: `<span style="font-family:${POLICES.display};font-weight:600;font-size:20px;letter-spacing:-0.02em;color:${COULEURS.ink};">${echappe(marque)}</span>`;
|
|
126
|
+
return `<!doctype html>
|
|
127
|
+
<html lang="${lang}">
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="utf-8">
|
|
130
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
131
|
+
<meta name="color-scheme" content="light">
|
|
132
|
+
<meta name="supported-color-schemes" content="light">
|
|
133
|
+
<title>${echappe(titre)}</title>
|
|
134
|
+
<style>
|
|
135
|
+
/* Le seul <style> du document, et il ne porte QUE du responsive : ce que
|
|
136
|
+
Gmail en supprime ne fait rien perdre, tout le reste est en ligne. */
|
|
137
|
+
@media (max-width:620px) {
|
|
138
|
+
.zv-carte { padding:26px 20px !important; }
|
|
139
|
+
.zv-marge { padding:20px 12px !important; }
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
142
|
+
</head>
|
|
143
|
+
<body style="margin:0;padding:0;width:100%;background:${COULEURS.paper};-webkit-text-size-adjust:100%;">
|
|
144
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">${preheader ? echappe(preheader) : ''}</div>
|
|
145
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${COULEURS.paper};">
|
|
146
|
+
<tr><td align="center" class="zv-marge" style="padding:32px 16px;">
|
|
147
|
+
<table role="presentation" width="${LARGEUR}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${LARGEUR}px;">
|
|
148
|
+
|
|
149
|
+
<tr><td style="padding:0 0 20px;">${enTete}</td></tr>
|
|
150
|
+
|
|
151
|
+
<tr><td class="zv-carte" bgcolor="${COULEURS.white}" style="background:${COULEURS.white};border:1px solid ${COULEURS.line};padding:34px 36px;">
|
|
152
|
+
${contenu}
|
|
153
|
+
</td></tr>
|
|
154
|
+
|
|
155
|
+
${pied
|
|
156
|
+
? `<tr><td style="padding:20px 4px 0;font-family:${POLICES.texte};font-size:12px;line-height:1.6;color:${COULEURS.label};">${pied}</td></tr>`
|
|
157
|
+
: ''}
|
|
158
|
+
|
|
159
|
+
</table>
|
|
160
|
+
</td></tr>
|
|
161
|
+
</table>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../react/email.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zevra/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Design system Zevra « Encre & Papier » — tokens, CSS des composants et logos officiels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/css-inline.d.ts",
|
|
38
38
|
"default": "./dist/css-inline.js"
|
|
39
39
|
},
|
|
40
|
+
"./email": {
|
|
41
|
+
"types": "./dist/email.d.ts",
|
|
42
|
+
"default": "./dist/email.js"
|
|
43
|
+
},
|
|
40
44
|
"./react/table": {
|
|
41
45
|
"types": "./dist/data-table.d.ts",
|
|
42
46
|
"default": "./dist/data-table.js"
|
|
@@ -74,7 +78,7 @@
|
|
|
74
78
|
"DECISIONS.md"
|
|
75
79
|
],
|
|
76
80
|
"scripts": {
|
|
77
|
-
"build": "node scripts/build-icon-react.mjs && node scripts/build-css-bundle.mjs && tsc -p tsconfig.json",
|
|
81
|
+
"build": "node scripts/build-icon-react.mjs && node scripts/build-css-bundle.mjs && node scripts/build-email-tokens.mjs && tsc -p tsconfig.json",
|
|
78
82
|
"test": "npm run build && vitest run",
|
|
79
83
|
"prepublishOnly": "npm run build",
|
|
80
84
|
"storybook": "npm run build && node scripts/build-stories.mjs && storybook dev -p 6006 --no-open",
|
|
@@ -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
|
+
}
|