@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.
@@ -1 +1 @@
1
- {"version":3,"file":"css-inline.js","sourceRoot":"","sources":["../react/css-inline.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,0EAA0E;AAE1E;;;iEAGiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAW,si9PAAsi9P,CAAA;AAEzk9P;;;yBAGyB;AACzB,MAAM,CAAC,MAAM,WAAW,GAAW,03BAA03B,CAAA"}
1
+ {"version":3,"file":"css-inline.js","sourceRoot":"","sources":["../react/css-inline.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,0EAA0E;AAE1E;;;iEAGiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAW,osiQAAosiQ,CAAA;AAEvuiQ;;;yBAGyB;AACzB,MAAM,CAAC,MAAM,WAAW,GAAW,03BAA03B,CAAA"}
@@ -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"}
@@ -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, '&amp;')
51
+ .replace(/</g, '&lt;')
52
+ .replace(/>/g, '&gt;')
53
+ .replace(/"/g, '&quot;');
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;">&nbsp;</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;">&nbsp;</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.20.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",
package/react/content.tsx CHANGED
@@ -247,21 +247,61 @@ export function Faq({ className, ...reste }: Div & { className?: string }) {
247
247
  return <div className={cx('zv-faq', className)} {...reste} />
248
248
  }
249
249
 
250
- export interface FaqItemProps extends Div {
250
+ export interface FaqItemProps
251
+ extends Omit<HTMLAttributes<HTMLDetailsElement>, 'className' | 'title'> {
251
252
  question: ReactNode
253
+ /** Dépliée au chargement. Une par page au plus : si toutes le sont, le
254
+ * repli ne sert plus à rien et la page redevient un mur. */
255
+ ouvert?: boolean
252
256
  className?: string
253
257
  children?: ReactNode
254
258
  }
255
259
 
256
- /** Rendu ouvert, sans repli : la réponse est le contenu, la cacher derrière
257
- * un clic n'ajoute rien à cinq questions. Pour un vrai accordéon, c'est un
258
- * `<details>` — et il n'existe pas de style pour lui dans le paquet. */
259
- export function FaqItem({ question, className, children, ...reste }: FaqItemProps) {
260
+ /** Une question qui se replie. `<details>`, donc AUCUN JavaScript : elle
261
+ * s'ouvre au clic, à Entrée et à Espace, elle est annoncée comme repliable,
262
+ * et la recherche du navigateur (⌘F) l'ouvre toute seule pour montrer ce
263
+ * qu'elle a trouvé.
264
+ *
265
+ * ⚠️ La réponse reste DANS LE DOM quand la question est fermée — `details`
266
+ * la masque, il ne la retire pas. C'est ce qui permet à une page de porter
267
+ * un balisage `FAQPage` honnête : ce que le moteur lit est bien ce que le
268
+ * visiteur peut ouvrir. Un accordéon qui démonte son contenu annoncerait
269
+ * des réponses absentes.
270
+ *
271
+ * Le chevron fait partie du composant, comme celui de `Fold` et la flèche
272
+ * de `LinkArrow` : recopié à la main, il finit par diverger.
273
+ *
274
+ * `question` accepte un TITRE (`<h3>Ma question</h3>`) : une FAQ de vitrine
275
+ * veut ses questions dans le plan du document — c'est ce que lisent les
276
+ * moteurs, et ce que parcourt un lecteur d'écran qui saute de titre en
277
+ * titre. Le CSS lui retire ce que le navigateur lui met : le niveau reste
278
+ * au balisage, l'apparence au système. */
279
+ export function FaqItem({
280
+ question,
281
+ ouvert,
282
+ className,
283
+ children,
284
+ ...reste
285
+ }: FaqItemProps) {
260
286
  return (
261
- <div className={className} {...reste}>
262
- <p className="zv-faq__q">{question}</p>
287
+ <details className={cx('zv-faq__item', className)} open={ouvert} {...reste}>
288
+ <summary className="zv-faq__q">
289
+ {question}
290
+ <svg
291
+ className="zv-faq__chevron"
292
+ viewBox="0 0 24 24"
293
+ fill="none"
294
+ stroke="currentColor"
295
+ strokeWidth="2"
296
+ strokeLinecap="square"
297
+ strokeLinejoin="miter"
298
+ aria-hidden="true"
299
+ >
300
+ <path d="M6.67 9.33 L12 14.67 L17.33 9.33" />
301
+ </svg>
302
+ </summary>
263
303
  <p className="zv-faq__a">{children}</p>
264
- </div>
304
+ </details>
265
305
  )
266
306
  }
267
307