Honor button typography and radius across email formats
This commit is contained in:
1 parent
a1704877e8
commit
f9cf2a648a
3 files changed
+40
-4
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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) =>
|
||||
`<tr><td data-block-id="${html(node.id)}" style="padding:0;${semanticStyle(node)}">${body}</td></tr>`;
|
||||
|
||||
@@ -379,7 +389,7 @@ export const buttonBlock = leaf(
|
||||
{
|
||||
emailHtml: ({ node, context }) =>
|
||||
emailCell(
|
||||
`<table role="presentation"><tr><td bgcolor="${html(buttonBackground(node, context))}" style="border-radius:6px;"><a href="${html(attrs(node).href)}" style="color:#ffffff;display:inline-block;font-family:inherit;font-weight:700;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||
`<table role="presentation"><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>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) =>
|
||||
@@ -388,7 +398,7 @@ export const buttonBlock = leaf(
|
||||
node,
|
||||
),
|
||||
mjml: ({ node, context }) =>
|
||||
`<mj-button background-color="${html(buttonBackground(node, context))}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
||||
`<mj-button 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>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`,
|
||||
);
|
||||
|
||||
@@ -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(/<a href="https:\/\/example.com" style="([^"]+)"/)?.[1];
|
||||
for (const declaration of ["color:#123456", "font-size:22px", "font-weight:400", "border-radius:18px"]) {
|
||||
expect(anchor).toContain(declaration);
|
||||
expect(result.value.parts.amp?.content).toContain(declaration);
|
||||
}
|
||||
expect(result.value.parts.amp?.validation.valid).toBe(true);
|
||||
for (const attribute of ['color="#123456"', 'font-size="22px"', 'font-weight="400"', 'border-radius="18px"']) {
|
||||
expect(result.value.exports?.mjml).toContain(attribute);
|
||||
}
|
||||
expect(await validateMjml(result.value.exports!.mjml!)).toEqual([]);
|
||||
expect(document).toEqual(before);
|
||||
});
|
||||
|
||||
test("button background inherits brand color with explicit semantic overrides across formats", async () => {
|
||||
for (const override of [undefined, "#aabbcc"]) {
|
||||
const document: BlockDocument = {
|
||||
|
||||
Reference in new issue
Block a user