diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 6adc8f3e..6c85c934 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.27", + "version": "0.1.28", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/blocks.ts b/packages/email-renderer/src/blocks.ts index 264e009e..e71a72a4 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -40,7 +40,7 @@ export const ampLayoutClass = (node: BlockNode) => export function ampLayoutCss(node: BlockNode, brandLinkColor?: string): string { const props = attrs(node); if (node.type === "content.button") - return `.button.${ampLayoutClass(node)}{background-color:${buttonBackground(node, { brandLinkColor })}}`; + return `.button.${ampLayoutClass(node)}{background-color:${buttonBackground(node, { brandLinkColor })};${buttonVisualCss(node)}}`; if (node.type === "layout.section") { const background = safeCssValue(props.background, /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i) || "#ffffff"; const padding = safeCssValue(props.padding, /^\d+(?:\.\d+)?(?:px|rem|em|%)?(?:\s+\d+(?:\.\d+)?(?:px|rem|em|%)?){0,3}$/) || "24px"; @@ -106,6 +106,16 @@ const buttonBackground = (node: BlockNode, context: unknown) => { const brand = (context as { brandLinkColor?: string } | undefined)?.brandLinkColor; return override || (brand && brandColor("#2563eb").parse(brand).ok ? brand : "#2563eb"); }; +const buttonVisuals = (node: BlockNode) => ({ + color: safeCssValue(styleBase(node, "color"), /^(#[0-9a-f]{3,8}|rgba?\([0-9., %]+\))$/i) || "#ffffff", + fontSize: safeCssValue(styleBase(node, "fontSize"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/) || "16px", + fontWeight: safeCssValue(styleBase(node, "fontWeight"), /^(normal|bold|[1-9]00)$/) || "700", + radius: safeCssValue(styleBase(node, "borderRadius"), /^\d+(?:\.\d+)?(?:px|rem|em|%)$/) || "6px", +}); +const buttonVisualCss = (node: BlockNode) => { + const values = buttonVisuals(node); + return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius}`; +}; const emailCell = (body: string, node: BlockNode) => `${body}`; @@ -379,7 +389,7 @@ export const buttonBlock = leaf( { emailHtml: ({ node, context }) => emailCell( - `
${html(attrs(node).label)}
`, + `
${html(attrs(node).label)}
`, node, ), ampEmail: ({ node }) => @@ -388,7 +398,7 @@ export const buttonBlock = leaf( node, ), mjml: ({ node, context }) => - `${html(attrs(node).label)}`, + `${html(attrs(node).label)}`, }, ({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`, ); diff --git a/packages/email-renderer/test/button-brand.test.ts b/packages/email-renderer/test/button-brand.test.ts index a2eaf1f9..43642d54 100644 --- a/packages/email-renderer/test/button-brand.test.ts +++ b/packages/email-renderer/test/button-brand.test.ts @@ -2,6 +2,32 @@ import { expect, test } from "bun:test"; import type { BlockDocument } from "@wrnexus/blocks"; import { renderEmail, validateMjml } from "../src/index.ts"; +test("button typography and corners reach the clickable element in every target", async () => { + const document: BlockDocument = { + schemaVersion: 1, kind: "email", meta: { subject: "Styled CTA", lang: "en" }, theme: { tokens: {} }, + root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600" }, styles: {}, slots: { + content: [{ id: "cta", type: "content.button", version: 1, props: { label: "Read more", href: "https://example.com" }, styles: { + color: { base: "#123456" }, fontSize: { base: "22px" }, fontWeight: { base: "400" }, borderRadius: { base: "18px" }, + }, slots: {} }], + } }, + }; + const before = structuredClone(document); + const result = await renderEmail(document, { amp: true, mjml: true, ampValidationTimeoutMs: 10000 }); + expect(result.ok).toBe(true); + if (!result.ok) return; + const anchor = result.value.parts.html.content.match(/ { for (const override of [undefined, "#aabbcc"]) { const document: BlockDocument = {