From 9f2b5bb5d9dddcc05dcf9da0cce6bb472fe83fa7 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 21:31:06 +0530 Subject: [PATCH] Apply button visuals to the clickable surface across email targets --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/blocks.ts | 14 +++++++++----- packages/email-renderer/src/mjml.ts | 1 + .../email-renderer/test/button-surface.test.ts | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 packages/email-renderer/test/button-surface.test.ts diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index b787e6fc..c12b1a75 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.45", + "version": "0.1.46", "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 83fc242c..fbf8f1f5 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -116,18 +116,22 @@ const buttonVisuals = (node: BlockNode) => ({ fontSize: emailStyleValue("fontSize", styleBase(node, "fontSize")) || "16px", fontWeight: emailStyleValue("fontWeight", styleBase(node, "fontWeight")) || "700", radius: emailStyleValue("borderRadius", styleBase(node, "borderRadius")) || "6px", + padding: emailStyleValue("padding", styleBase(node, "padding")) || "12px 20px", }); const buttonVisualCss = (node: BlockNode) => { const values = buttonVisuals(node); - return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius}`; + return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius};padding:${values.padding}`; }; const buttonAlignment = (node: BlockNode, context: unknown) => { const value = emailStyleValue("textAlign", styleBase(node, "textAlign")) || "start"; const rtl = (context as { direction?: string } | undefined)?.direction === "rtl"; return value === "start" ? (rtl ? "right" : "left") : value === "end" ? (rtl ? "left" : "right") : value; }; -const emailCell = (body: string, node: BlockNode, align?: string) => - `${body}`; +const emailCell = (body: string, node: BlockNode, align?: string) => { + // Button visuals belong to the clickable element, never the full-width cell. + const cellNode = node.type === "content.button" ? { ...node, styles: Object.fromEntries(Object.entries(node.styles).filter(([key]) => ["textAlign", "margin"].includes(key))) } : node; + return `${body}`; +}; export const emailDocumentBlock = defineBlock({ manifest: { @@ -401,7 +405,7 @@ export const buttonBlock = leaf( { emailHtml: ({ node, context }) => emailCell( - `
${html(attrs(node).label)}
`, + `
${html(attrs(node).label)}
`, node, ), ampEmail: ({ node, context }) => @@ -411,7 +415,7 @@ export const buttonBlock = leaf( buttonAlignment(node, context), ), 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/src/mjml.ts b/packages/email-renderer/src/mjml.ts index 4b13b4df..536c85fb 100644 --- a/packages/email-renderer/src/mjml.ts +++ b/packages/email-renderer/src/mjml.ts @@ -262,6 +262,7 @@ export function importMjml( "font-size": ["fontSize", /^\d+(?:\.\d+)?(?:px|rem|em|%)$/], "font-weight": ["fontWeight", /^(normal|bold|[1-9]00)$/], "border-radius": ["borderRadius", /^\d+(?:\.\d+)?(?:px|rem|em|%)$/], + "inner-padding": ["padding", /^\d+(?:\.\d+)?(?:px|rem|em|%)(?:\s+\d+(?:\.\d+)?(?:px|rem|em|%)){0,3}$/], }; for (const [attribute, raw] of Object.entries(a)) { if (attribute === "href") continue; diff --git a/packages/email-renderer/test/button-surface.test.ts b/packages/email-renderer/test/button-surface.test.ts new file mode 100644 index 00000000..ddc7d0e1 --- /dev/null +++ b/packages/email-renderer/test/button-surface.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { buttonBlock, ampLayoutCss } from "../src/blocks.ts"; + +test("button visuals target the clickable surface rather than the full-width wrapper", async () => { + const node = {id:"cta",type:"content.button",version:1,props:{label:"Act",href:"https://example.com"},styles:{backgroundColor:{base:"#123456"},padding:{base:"8px 32px"},borderRadius:{base:"20px"}},slots:{}}; + const context = {node,context:{},slots:{}} as never; + const html = String(await buttonBlock.renderers!.emailHtml!(context)); + const wrapper = html.match(/]*>/)![0]; + expect(wrapper).not.toContain("#123456"); + expect(wrapper).not.toContain("20px"); + expect(wrapper).not.toContain("32px"); + expect(html).toContain('bgcolor="#123456"'); + expect(html.match(/]*>/)![0]).toContain("padding:8px 32px"); + expect(ampLayoutCss(node)).toContain("padding:8px 32px"); + expect(String(await buttonBlock.renderers!.mjml!(context))).toContain('inner-padding="8px 32px"'); +});