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(
- ``,
+ ``,
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: {} },