From b0fca36bca673fb260e87837feb019d8bef1fe9b Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 17:56:46 +0530 Subject: [PATCH] Apply brand defaults to MJML survey and product buttons --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/blocks.ts | 2 +- packages/email-renderer/src/mjml.ts | 3 +++ packages/email-renderer/test/mjml-brand.test.ts | 3 ++- .../email-renderer/test/survey-brand.test.ts | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 packages/email-renderer/test/survey-brand.test.ts diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index bcf2e4b8..4ed4361e 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.34", + "version": "0.1.35", "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 31926479..e6d8e354 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -36,7 +36,7 @@ const isEmailRootChild = (node: BlockNode, context: unknown) => (context as { rootBlockIds?: string[] } | undefined)?.rootBlockIds?.includes(node.id) ?? false; const mjmlDocumentHead = (node: BlockNode, context: unknown) => { const meta = context as { emailSubject?: string; emailPreheader?: string } | undefined; - return `${html(meta?.emailSubject)}${meta?.emailPreheader ? `${html(meta.emailPreheader)}` : ""}`; + return `${html(meta?.emailSubject)}${meta?.emailPreheader ? `${html(meta.emailPreheader)}` : ""}`; }; export const ampLayoutClass = (node: BlockNode) => "wrn-layout-" + Array.from(node.id, char => char.codePointAt(0)!.toString(16)).join("-"); diff --git a/packages/email-renderer/src/mjml.ts b/packages/email-renderer/src/mjml.ts index c86070cf..4b13b4df 100644 --- a/packages/email-renderer/src/mjml.ts +++ b/packages/email-renderer/src/mjml.ts @@ -140,6 +140,9 @@ export function importMjml( } else if (defaults.tagName === "mj-text") { onlyAttributes(defaults, ["color"]); if (defaults.attributes?.color) brand.textColor = decode(defaults.attributes.color); + } else if (defaults.tagName === "mj-button") { + onlyAttributes(defaults, ["background-color"]); + if (defaults.attributes?.["background-color"]) brand.linkColor = decode(defaults.attributes["background-color"]); } else attributeError(defaults); } } else if (node.tagName === "mj-breakpoint") { diff --git a/packages/email-renderer/test/mjml-brand.test.ts b/packages/email-renderer/test/mjml-brand.test.ts index b2b28d9e..8657fe41 100644 --- a/packages/email-renderer/test/mjml-brand.test.ts +++ b/packages/email-renderer/test/mjml-brand.test.ts @@ -8,7 +8,7 @@ const compile = createRequire(import.meta.url)("mjml"); test("MJML preserves document metadata and global typography in compiled HTML", async () => { const document: BlockDocument = { schemaVersion: 1, kind: "email", meta: { subject: 'Brand & "News" ', preheader: 'Preview & useful', lang: "en" }, theme: { tokens: {} }, - root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600", fontFamily: "Georgia, serif", textColor: "#123456" }, styles: {}, slots: { + root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600", fontFamily: "Georgia, serif", textColor: "#123456", linkColor: "#a12345" }, styles: {}, slots: { content: [{ id: "text", type: "content.text", version: 1, props: { text: "Brand text" }, styles: {}, slots: {} }, { id: "button", type: "content.button", version: 1, props: { label: "Brand action", href: "https://example.com" }, styles: {}, slots: {} }], } }, }; @@ -35,6 +35,7 @@ test("MJML preserves document metadata and global typography in compiled HTML", expect(imported.value.meta.preheader).toBe(document.meta.preheader); expect(imported.value.root.props.fontFamily).toBe("Georgia, serif"); expect(imported.value.root.props.textColor).toBe("#123456"); + expect(imported.value.root.props.linkColor).toBe("#a12345"); const rerendered = await renderEmail(imported.value, { mjml: true }); expect(rerendered.ok).toBe(true); } diff --git a/packages/email-renderer/test/survey-brand.test.ts b/packages/email-renderer/test/survey-brand.test.ts new file mode 100644 index 00000000..a976df4c --- /dev/null +++ b/packages/email-renderer/test/survey-brand.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { createRequire } from "node:module"; +import type { BlockDocument } from "@wrnexus/blocks"; +import { renderEmail } from "../src/index.ts"; +const compile = createRequire(import.meta.url)("mjml"); + +test("survey MJML button inherits brand color and preserves safe text fallback", async () => { + const document: BlockDocument = { schemaVersion: 1, kind: "email", meta: { subject: "Survey" }, theme: { tokens: {} }, root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600", linkColor: "#a12345" }, styles: {}, slots: { content: [{ id: "survey", type: "content.survey", version: 1, props: { question: "How was your visit?", buttonLabel: "Answer & share", href: "https://example.com/survey" }, styles: {}, slots: {} }] } } }; + const result = await renderEmail(document, { mjml: true }); + if (!result.ok) throw new Error("Render failed"); + expect(result.value.parts.text.content).toContain("How was your visit?\nAnswer & share: https://example.com/survey"); + const compiled = await compile(result.value.exports!.mjml!, { validationLevel: "strict" }); + expect(compiled.errors).toEqual([]); + expect(compiled.html).toContain("background:#a12345"); + expect(compiled.html).toContain("Answer & share"); +});