Rendu des e-mails (Track M)Depuis 0.2.0-preview.45
ÉTAT : LE CŒUR A ÉTÉ LIVRÉ EN 0.2.0-PREVIEW.45 (fusionné le 2026-08-25). EmailRealizer et EmailRenderer existent dans eQuantic.UI.Email avec le vocabulaire ci-dessous. Encore devant nous : la matrice de clients réels (un exemple généré face à Gmail, Outlook et Apple Mail) et les pins HTML une fois la sortie stabilisée.
Écrivez l'e-mail en C# avec les mêmes composants qu'une page, et récupérez du HTML qu'un client de messagerie va réellement rendre.
Pourquoi cela épouse l'architecture au lieu de la combattre
Un composant ici est un arbre de VisualNode qui ne sait rien de sa cible. Le réalisateur web en fait du DOM et du CSS ; Photon en fait de la peinture GPU. L'e-mail est un troisième réalisateur : une cible dont le moteur de rendu se trouve être très restrictif, ce qui a architecturalement la même forme qu'une cible sans aucun DOM.
Rien de la couche d'écriture ne change. Les mêmes Column, Text, Image et tokens de thème, le même Build(context).
Deux pièces sont dans l'arbre aujourd'hui, vérifiées en les lisant :
•
WebRealizer.Lower(node, theme, typeScale, styles: null) : sans puits de styles, les styles restent en ligne sur l'élément au lieu de devenir des classes atomiques dédupliquées. Le style en ligne est la contrainte la plus dure de l'e-mail, et ce chemin le produit déjà.
•
HtmlRenderer.RenderNode(...) transforme l'arbre abaissé en une chaîne HTML.
Donc "composant C# → HTML avec styles en ligne" ce sont deux appels qui existent déjà. Ce qui manque, c'est tout ce qui concerne le médium.
Ce que l'e-mail exige vraiment
Exigence
Pourquoi
En quoi le réalisateur web diffère
Layout en <table> imbriquées
Outlook sous Windows rend avec le moteur de Word : pas de flexbox, pas de grid
Row/Column s'abaissent en FlexDirection
Uniquement du style="" en ligne
Les blocs <style> et le CSS externe sont retirés ou ignorés par plusieurs clients
Classes atomiques + une feuille de styles (le chemin avec puits)
Couleurs littérales
Les custom properties CSS ne sont pas résolues
Les tokens émettent var(--eq-color-…, fallback)
URL d'images absolues ou CID
Gmail jette les URI data:
Les assets supposent une origine servie
Une largeur contrainte
~600px est la convention que tout client tolère
Les pages sont fluides
Aucune interaction
Pas de JS, pas de survol chez la plupart des clients, pas de conteneurs défilants
Pressable, ScrollView et les états de survol existent
Un nouveau paquet, eQuantic.UI.Email, qui ne dépend que de Primitives et de Core, jamais d'eQuantic.UI.Web, pour que les deux réalisateurs ne puissent pas dériver l'un vers l'autre.
// Écrit exactement comme une page.
public sealed class WelcomeEmail(string name) : StatelessComponent
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Image(Assets.Logo, width: 132),
Text($"Welcome, {name}", TypeRole.Heading),
Button("Confirm your address", href: "https://…"),
// Rendu là où il est envoyé.
EmailMessage message = EmailRenderer.Render(new WelcomeEmail("Edgar"), theme);
// message.Html abaissé en tables, entièrement en ligne
// message.PlainText généré depuis le MÊME arbre, pas écrit deux fois
EmailMessage porte les deux parties parce qu'un message multipart a besoin des deux, et générer l'alternative texte depuis l'arbre est la seule façon qu'elle reste au pas du HTML.
M0 : mesurer avant de construire. Faite, et elle a changé le plan. Outre les deux constats attendus (le layout flex, les couleurs light-dark()), un troisième que le plan n'avait pas : la typographie vit dans des classes eq-type-*, et un e-mail n'a pas de feuille de styles, donc tout le texte se rendait à une seule taille. M2 a grandi pour mettre la rampe du thème en ligne sur chaque texte. La moitié restante de M0 est humaine : l'exemple généré face à de vraies boîtes.
M1 : abaisser en tables. Faite. Box, Row, Column et Gap deviennent des <table>/<tr>/<td> imbriquées avec du padding de cellule et des lignes d'espacement. L'alignement se mappe sur align/valign. C'est le gros de la voie.
M2 : styles en ligne et thème littéral. Faite (plus la rampe typographique en ligne). Réutilise le chemin sans puits ; résout chaque token en une couleur et une longueur littérales pour que rien ne dépende des custom properties. La densité et l'échelle typographique se résolvent une fois, au rendu.
M3 : coque du document. Faite. EmailDocument : doctype, le jeu de <meta>, le conteneur de 600px, la ligne de pré-en-tête invisible, et le meta de mode sombre que les clients honorent.
M4 : limite du vocabulaire. Faite. Des diagnostics qui nomment ce qu'un e-mail ne peut pas faire : ScrollView, Pressable, Draggable, le style de survol et de focus, position: absolute. Un Link devient un <a> ; un Button devient un <a> à toute épreuve stylé en bouton. Une Image sans URL absolue est une erreur, pas une image cassée dans la boîte de quelqu'un.
M5 : alternative en texte brut. Faite. Parcourir le même arbre pour la partie text/plain.
Ce que la revue a appris au contrat
La tranche a traversé cinq tours de revue (32 constats) et les survivants sont des règles qui méritent d'être énoncées :
•
Une propriété que le médium ne peut pas exprimer est délimitée, pas approchée : l'encre en dégradé et MaxLines lèvent en nommant la sortie, parce qu'un uni à la place est une autre encre que personne n'a choisie, et montrer plus de texte que l'auteur n'en a borné est une divergence de contenu.
•
Une propriété que le médium exprime autrement est honorée : le padding du conteneur (une table enveloppe d'une cellule), Cross et l'AlignSelf par enfant (alignement de cellule), les rayons par coin, les bordures, les éléments de titre, l'aria-label venu de Link.Label.
•
MainAlign est vide de sens ici, documenté plutôt que délimité : une table d'e-mail se dimensionne à son contenu, il n'y a donc pas d'espace libre à distribuer, et chaque valeur rend donc à l'identique, ce qui est fidèle.
•
Un composant cassé fait échouer l'ENVOI. Les composants se déplient via Build, pas via le BuildContained du web : une boîte qui décrit l'erreur est juste sur une page vivante que qui développe regarde, et fausse dans un message sur le point d'atteindre la boîte d'une lectrice, déguisée en contenu. Depuis la 0.2.0-preview.55 un cycle le fait échouer de la même façon : le parcours partage le compte de profondeur de la couture via ComponentBoundary.Enter, et au-delà de la borne il lève une InvalidOperationException au lieu de contenir. Avant, un composant qui se construisait lui-même était une StackOverflowException, que rien ne peut attraper, et le processus d'envoi mourait sans rien rapporter.
•
Les adresses sont parsées, pas vérifiées par préfixe ; les valeurs d'attributs sont encodées comme des attributs ; un Link autour de runs liés refuse d'imbriquer des ancres, en nommant le choix.
•
Aucun bloc <style>, jamais, même là où un client le prend en charge. Une règle bat une matrice d'exceptions.
•
Aucune interactivité. Un e-mail est une page imprimée qui se trouve avoir des liens.
•
Aucun hack propre à un client dans les composants. Tout ce dont Outlook a besoin vit dans le réalisateur, jamais dans quelque chose qu'un auteur écrit.
•
Les pièces jointes et l'acheminement ne sont pas les nôtres. La sortie est du HTML plus du texte ; envoyer est le travail de l'application (MailKit, SES, ce qu'elle utilise déjà).
Les deux mêmes filets que le reste du framework utilise :
•
Des pins HTML, régénérés à dessein comme les fixtures transpilées, pour qu'un changement de layout soit visible comme un diff plutôt que comme une surprise.
•
Une matrice de clients comme conformité, exécutée face à de vrais clients et enregistrée : la règle selon laquelle le navigateur est l'arbitre s'applique ici plus durement encore, parce que les clients de messagerie sont plus loin d'une spécification que les navigateurs.