Renderizado de email (Track M)Desde 0.2.0-preview.45
ESTADO: EL NÚCLEO SE ENTREGÓ EN LA 0.2.0-PREVIEW.45 (mergeado el 2026-08-25). EmailRealizer y EmailRenderer existen en eQuantic.UI.Email con el vocabulario de abajo. Todavía por delante: la matriz de clientes reales (un ejemplo generado contra Gmail, Outlook y Apple Mail) y los pins de HTML una vez que la salida se asiente.
Escribe el email en C# con los mismos componentes con los que se escribe una página, y recibe HTML que un cliente de email va a renderizar de verdad.
Por qué esto encaja con la arquitectura en lugar de pelearse con ella
Un componente aquí es un árbol de VisualNode que no sabe nada de su destino. El realizador web lo convierte en DOM y CSS; Photon lo convierte en pintura de GPU. El email es un tercer realizador: un destino cuyo motor de renderizado resulta ser muy restrictivo, lo que arquitectónicamente tiene la misma forma que un destino sin DOM alguno.
Nada de la capa de autoría cambia. Los mismos Column, Text, Image y tokens de tema, el mismo Build(context).
Hoy hay dos piezas en el árbol, verificadas leyéndolas:
•
WebRealizer.Lower(node, theme, typeScale, styles: null): sin el sink de estilos, los estilos se quedan inline en el elemento en lugar de convertirse en clases atómicas deduplicadas. El estilo inline es la restricción más dura del email, y este camino ya lo produce.
•
HtmlRenderer.RenderNode(...) convierte el árbol bajado en una cadena de HTML.
Así que "componente C# → HTML con estilos inline" son dos llamadas que ya existen. Lo que falta es todo lo relativo al medio.
Lo que el email exige de verdad
Exigencia
Por qué
En qué difiere el realizador web
Layout con <table> anidadas
Outlook en Windows renderiza con el motor de Word: sin flexbox, sin grid
Row/Column bajan a FlexDirection
Solo style="" inline
Los bloques <style> y el CSS externo los quitan o los ignoran varios clientes
Clases atómicas + una hoja de estilos (el camino con sink)
Colores literales
Las custom properties de CSS no se resuelven
Los tokens emiten var(--eq-color-…, fallback)
URLs de imagen absolutas o CID
Gmail descarta las URIs data:
Los assets suponen un origen servido
Un ancho acotado
~600px es la convención que todo cliente tolera
Las páginas son fluidas
Ninguna interacción
Sin JS, sin hover en la mayoría de clientes, sin contenedores con scroll
Pressable, ScrollView y los estados de hover existen
Un paquete nuevo, eQuantic.UI.Email, que depende solo de Primitives y Core, nunca de eQuantic.UI.Web, para que los dos realizadores no puedan desviarse el uno hacia el otro.
// Escrito exactamente como una página.
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://…"),
// Renderizado allí donde se envía.
EmailMessage message = EmailRenderer.Render(new WelcomeEmail("Edgar"), theme);
// message.Html bajado a tablas, totalmente inline
// message.PlainText generado del MISMO árbol, no escrito dos veces
EmailMessage lleva las dos partes porque un mensaje multipart necesita las dos, y generar la alternativa de texto a partir del árbol es la única manera de que se mantenga al paso del HTML.
M0: medir antes de construir. Hecha, y cambió el plan. Además de los dos hallazgos esperados (el layout flex, los colores light-dark()), un tercero que el plan no tenía: la tipografía vive en clases eq-type-*, y un email no tiene hoja de estilos, así que todo el texto se renderizaba a un solo tamaño. M2 creció para poner la rampa del tema inline en cada texto. La mitad restante de M0 es humana: el ejemplo generado contra buzones reales.
M1: bajar a tablas. Hecha. Box, Row, Column y Gap se vuelven <table>/<tr>/<td> anidadas con padding de celda y filas espaciadoras. La alineación se mapea a align/valign. Esto es el grueso de la vía.
M2: estilos inline y tema literal. Hecha (más la rampa tipográfica inline). Reutiliza el camino sin sink; resuelve cada token a un color y una longitud literales para que nada dependa de custom properties. La densidad y la escala tipográfica se resuelven una vez, al renderizar.
M3: casca del documento. Hecha. EmailDocument: doctype, el conjunto de <meta>, el contenedor de 600px, la línea de preencabezado invisible, y el meta de modo oscuro que los clientes respetan.
M4: cerca del vocabulario. Hecha. Diagnósticos que nombran lo que un email no puede hacer: ScrollView, Pressable, Draggable, el estilo de hover y de focus, position: absolute. Un Link se vuelve <a>; un Button se vuelve un <a> a prueba de balas con estilo de botón. Una Image sin URL absoluta es un error, no una imagen rota en la bandeja de alguien.
M5: alternativa en texto puro. Hecha. Recorrer el mismo árbol para la parte text/plain.
Lo que la revisión enseñó al contrato
La porción pasó por cinco rondas de revisión (32 hallazgos) y lo que sobrevivió son reglas que vale declarar:
•
Una propiedad que el medio no puede expresar se valla, no se aproxima: la tinta en degradado y MaxLines lanzan nombrando la salida, porque un sólido en su lugar es otra tinta que nadie eligió, y mostrar más texto del que el autor acotó es una divergencia de contenido.
•
Una propiedad que el medio expresa de otra manera se honra: el padding del contenedor (una tabla envoltorio de una celda), Cross y el AlignSelf por hijo (alineación de celda), los radios por esquina, los bordes, los elementos de encabezado, el aria-label que viene de Link.Label.
•
MainAlign es vacuo aquí, documentado en lugar de vallado: una tabla de email se dimensiona a su contenido, así que no hay espacio libre que distribuir, y entonces todo valor renderiza igual, lo cual es fiel.
•
Un componente roto hace fallar el ENVÍO. Los componentes se expanden vía Build, no vía el BuildContained de la web: una caja que describe el error es lo correcto en una página en vivo que quien programa está mirando, y es incorrecta en un mensaje a punto de llegar a la bandeja de alguien disfrazada de contenido. Desde la 0.2.0-preview.55 un ciclo lo hace fallar igual: el recorrido comparte la cuenta de profundidad de la costura a través de ComponentBoundary.Enter, y pasado el límite lanza una InvalidOperationException en lugar de contener. Antes, un componente que se construía a sí mismo era una StackOverflowException, que nada puede capturar, y el proceso de envío moría sin reportar nada.
•
Las direcciones se parsean, no se comprueban por prefijo; los valores de atributo se codifican como atributo; un Link alrededor de runs enlazados se niega a anidar anclas, nombrando la elección.
•
Ningún bloque <style>, nunca, incluso donde un cliente lo soporte. Una regla le gana a una matriz de excepciones.
•
Ninguna interactividad. Un email es una página impresa que resulta tener enlaces.
•
Ningún hack específico de cliente en los componentes. Todo lo que Outlook necesita vive en el realizador, nunca en algo que escriba quien escribe.
•
Los adjuntos y la entrega no son nuestros. La salida es HTML más texto; enviar es trabajo de la aplicación (MailKit, SES, lo que ya use).
Las mismas dos redes que usa el resto del framework:
•
Pins de HTML, regenerados a propósito como los fixtures transpilados, para que un cambio de layout sea visible como un diff y no como una sorpresa.
•
Una matriz de clientes como conformidad, ejecutada contra clientes reales y registrada: la regla de que el navegador es el árbitro se aplica aquí incluso con más fuerza, porque los clientes de email están más lejos de una especificación que los navegadores.