From 7110e1a7f1992265ad1aa5c1e37d4e2b7a6505e0 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 16:21:08 +0530 Subject: [PATCH] Align email buttons consistently across formats and directions --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/blocks.ts | 16 +++++++++----- .../email-renderer/test/button-brand.test.ts | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 6c85c934..f35a8259 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.28", + "version": "0.1.29", "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 e71a72a4..f3e7cc5e 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -116,8 +116,13 @@ 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}`; +const buttonAlignment = (node: BlockNode, context: unknown) => { + const value = safeCssValue(styleBase(node, "textAlign"), /^(left|center|right|start|end)$/) || "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}`; export const emailDocumentBlock = defineBlock({ manifest: { @@ -389,16 +394,17 @@ export const buttonBlock = leaf( { emailHtml: ({ node, context }) => emailCell( - `
${html(attrs(node).label)}
`, + `
${html(attrs(node).label)}
`, node, ), - ampEmail: ({ node }) => + ampEmail: ({ node, context }) => emailCell( `${html(attrs(node).label)}`, node, + 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/test/button-brand.test.ts b/packages/email-renderer/test/button-brand.test.ts index 43642d54..a3226582 100644 --- a/packages/email-renderer/test/button-brand.test.ts +++ b/packages/email-renderer/test/button-brand.test.ts @@ -2,6 +2,27 @@ import { expect, test } from "bun:test"; import type { BlockDocument } from "@wrnexus/blocks"; import { renderEmail, validateMjml } from "../src/index.ts"; +test("button alignment maps physical and logical positions consistently in LTR and RTL", async () => { + for (const direction of ["ltr", "rtl"] as const) { + for (const alignment of [undefined, "left", "center", "right", "start", "end"]) { + const expected = !alignment || alignment === "start" ? (direction === "rtl" ? "right" : "left") : alignment === "end" ? (direction === "rtl" ? "left" : "right") : alignment; + const document: BlockDocument = { + schemaVersion: 1, kind: "email", meta: { subject: "Alignment", 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: "Act", href: "https://example.com" }, styles: alignment ? { textAlign: { base: alignment } } : {}, slots: {} }], + } }, + }; + const rendered = await renderEmail(document, { direction, amp: true, mjml: true, ampValidationTimeoutMs: 10000 }); + expect(rendered.ok).toBe(true); + if (!rendered.ok) continue; + expect(rendered.value.parts.html.content).toContain(` { const document: BlockDocument = { schemaVersion: 1, kind: "email", meta: { subject: "Styled CTA", lang: "en" }, theme: { tokens: {} },