Apply button visuals to the clickable surface across email targets
This commit is contained in:
1 parent
45343760e0
commit
9f2b5bb5d9
4 files changed
+27
-6
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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) =>
|
||||
`<tr><td data-block-id="${html(node.id)}"${align ? ` align="${html(align)}"` : ""} style="padding:0;${semanticStyle(node)}">${body}</td></tr>`;
|
||||
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 `<tr><td data-block-id="${html(node.id)}"${align ? ` align="${html(align)}"` : ""} style="padding:0;${semanticStyle(cellNode)}">${body}</td></tr>`;
|
||||
};
|
||||
|
||||
export const emailDocumentBlock = defineBlock({
|
||||
manifest: {
|
||||
@@ -401,7 +405,7 @@ export const buttonBlock = leaf(
|
||||
{
|
||||
emailHtml: ({ node, context }) =>
|
||||
emailCell(
|
||||
`<table role="presentation" align="${buttonAlignment(node, context)}" cellpadding="0" cellspacing="0" border="0"><tr><td bgcolor="${html(buttonBackground(node, context))}" style="border-radius:${html(buttonVisuals(node).radius)};"><a href="${html(attrs(node).href)}" style="${html(buttonVisualCss(node))};display:inline-block;font-family:inherit;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||
`<table role="presentation" align="${buttonAlignment(node, context)}" cellpadding="0" cellspacing="0" border="0"><tr><td bgcolor="${html(buttonBackground(node, context))}" style="border-radius:${html(buttonVisuals(node).radius)};"><a href="${html(attrs(node).href)}" style="${html(buttonVisualCss(node))};display:inline-block;font-family:inherit;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node, context }) =>
|
||||
@@ -411,7 +415,7 @@ export const buttonBlock = leaf(
|
||||
buttonAlignment(node, context),
|
||||
),
|
||||
mjml: ({ node, context }) =>
|
||||
`<mj-button align="${buttonAlignment(node, context)}" background-color="${html(buttonBackground(node, context))}" color="${html(buttonVisuals(node).color)}" font-size="${html(buttonVisuals(node).fontSize)}" font-weight="${html(buttonVisuals(node).fontWeight)}" border-radius="${html(buttonVisuals(node).radius)}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
||||
`<mj-button align="${buttonAlignment(node, context)}" background-color="${html(buttonBackground(node, context))}" color="${html(buttonVisuals(node).color)}" font-size="${html(buttonVisuals(node).fontSize)}" font-weight="${html(buttonVisuals(node).fontWeight)}" border-radius="${html(buttonVisuals(node).radius)}"${styleBase(node, "padding") ? ` inner-padding="${html(buttonVisuals(node).padding)}"` : ""} href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(/<td data-block-id="cta"[^>]*>/)![0];
|
||||
expect(wrapper).not.toContain("#123456");
|
||||
expect(wrapper).not.toContain("20px");
|
||||
expect(wrapper).not.toContain("32px");
|
||||
expect(html).toContain('bgcolor="#123456"');
|
||||
expect(html.match(/<a [^>]*>/)![0]).toContain("padding:8px 32px");
|
||||
expect(ampLayoutCss(node)).toContain("padding:8px 32px");
|
||||
expect(String(await buttonBlock.renderers!.mjml!(context))).toContain('inner-padding="8px 32px"');
|
||||
});
|
||||
Reference in new issue
Block a user