Fix email template rendering for Outlook-safe branded content

This commit is contained in:
2026-03-03 17:20:19 +13:00
parent d80b1e5e4f
commit caa6aa76d6
6 changed files with 67 additions and 66 deletions

View File

@@ -1 +1 @@
0303261702
0303261719

File diff suppressed because one or more lines are too long

View File

@@ -139,17 +139,17 @@ TEMPLATE_PRESENTATION: Dict[str, Dict[str, str]] = {
def _build_email_stat_card(label: str, value: str, detail: str = "") -> str:
detail_html = (
f"<div style=\"margin-top:8px; font-size:13px; line-height:1.6; color:#9aa3b8; word-break:break-word;\">"
f"<div style=\"margin-top:8px; font-size:13px; line-height:1.6; color:#5c687d; word-break:break-word;\">"
f"{html.escape(detail)}</div>"
if detail
else ""
)
return (
"<div style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); "
"border-radius:16px; color:#e9ecf5;\">"
f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; "
"<div style=\"padding:16px; background:#f8fafc; border:1px solid #d9e2ef; "
"border-radius:16px; color:#132033;\">"
f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#6b778c; "
f"margin-bottom:8px;\">{html.escape(label)}</div>"
f"<div style=\"font-size:20px; font-weight:800; line-height:1.45; word-break:break-word;\">"
f"<div style=\"font-size:20px; font-weight:800; line-height:1.45; word-break:break-word; color:#132033;\">"
f"{html.escape(value)}</div>"
f"{detail_html}"
"</div>"
@@ -184,7 +184,7 @@ def _build_email_list(items: list[str], *, ordered: bool = False) -> str:
f"<li style=\"margin:0 0 8px;\">{html.escape(item)}</li>" for item in items if item
)
return (
f"<{tag} style=\"margin:0; {marker} color:#dbe5ff; line-height:1.8; font-size:14px;\">"
f"<{tag} style=\"margin:0; {marker} color:#132033; line-height:1.8; font-size:14px;\">"
f"{rendered_items}"
f"</{tag}>"
)
@@ -193,40 +193,40 @@ def _build_email_list(items: list[str], *, ordered: bool = False) -> str:
def _build_email_panel(title: str, body_html: str, *, variant: str = "neutral") -> str:
styles = {
"neutral": {
"background": "#101726",
"border": "rgba(255,255,255,0.08)",
"eyebrow": "#9aa3b8",
"text": "#dbe5ff",
"background": "#f8fafc",
"border": "#d9e2ef",
"eyebrow": "#6b778c",
"text": "#132033",
},
"brand": {
"background": "#101726",
"border": "rgba(59,130,246,0.22)",
"eyebrow": "#9dbfff",
"text": "#dbe5ff",
"background": "#eef4ff",
"border": "#bfd2ff",
"eyebrow": "#2754b6",
"text": "#132033",
},
"success": {
"background": "#122016",
"border": "rgba(34,197,94,0.24)",
"eyebrow": "#9de7b5",
"text": "#d9f9e4",
"background": "#edf9f0",
"border": "#bfe4c6",
"eyebrow": "#1f7a3f",
"text": "#132033",
},
"warning": {
"background": "#241814",
"border": "rgba(251,146,60,0.34)",
"eyebrow": "#fbbd7b",
"text": "#ffe0ba",
"background": "#fff5ea",
"border": "#ffd5a8",
"eyebrow": "#c46a10",
"text": "#132033",
},
"danger": {
"background": "#251418",
"border": "rgba(239,68,68,0.32)",
"eyebrow": "#ff9b9b",
"text": "#ffd0d0",
"background": "#fff0f0",
"border": "#f3c1c1",
"eyebrow": "#bb2d2d",
"text": "#132033",
},
}.get(variant, {
"background": "#101726",
"border": "rgba(255,255,255,0.08)",
"eyebrow": "#9aa3b8",
"text": "#dbe5ff",
"background": "#f8fafc",
"border": "#d9e2ef",
"eyebrow": "#6b778c",
"text": "#132033",
})
return (
f"<div style=\"margin:0 0 18px; padding:18px; background:{styles['background']}; "
@@ -255,7 +255,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"Build: {{build_number}}\n"
),
"body_html": (
"<div style=\"margin:0 0 20px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"<div style=\"margin:0 0 20px; color:#132033; font-size:15px; line-height:1.7;\">"
"A new invitation has been prepared for <strong>{{recipient_email}}</strong>. Use the details below to sign up."
"</div>"
+ _build_email_stat_grid(
@@ -305,7 +305,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n"
),
"body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Your account is live and ready to use. Everything below mirrors the current site behavior."
"</div>"
+ _build_email_stat_grid(
@@ -345,7 +345,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"If you need help, contact the admin.\n"
),
"body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Please review this account notice carefully. This message was sent by an administrator."
"</div>"
+ _build_email_stat_grid(
@@ -388,7 +388,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n"
),
"body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Your account access has changed. Review the details below."
"</div>"
+ _build_email_stat_grid(
@@ -507,13 +507,14 @@ def _looks_like_full_html_document(value: str) -> bool:
def _build_email_action_button(label: str, url: str, *, primary: bool) -> str:
background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#151c2d"
border = "1px solid rgba(59, 130, 246, 0.32)" if primary else "1px solid rgba(255, 255, 255, 0.12)"
color = "#ffffff"
background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#ffffff"
fallback = "#1c6bff" if primary else "#ffffff"
border = "1px solid rgba(28, 107, 255, 0.28)" if primary else "1px solid #d5deed"
color = "#ffffff" if primary else "#132033"
return (
f"<a href=\"{html.escape(url)}\" "
f"style=\"display:inline-block; padding:12px 20px; margin:0 12px 12px 0; border-radius:999px; "
f"background:{background}; border:{border}; color:{color}; text-decoration:none; font-size:14px; "
f"background-color:{fallback}; background:{background}; border:{border}; color:{color}; text-decoration:none; font-size:14px; "
f"font-weight:800; letter-spacing:0.01em;\">{html.escape(label)}</a>"
)
@@ -560,39 +561,39 @@ def _wrap_email_html(
return (
"<!doctype html>"
"<html><body style=\"margin:0; padding:0; background:#05070d;\">"
"<html><body style=\"margin:0; padding:0; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<div style=\"display:none; max-height:0; overflow:hidden; opacity:0;\">"
f"{html.escape(title)} - {html.escape(subtitle)}"
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"width:100%; border-collapse:collapse; background:radial-gradient(circle at top, rgba(17,33,74,0.9) 0%, rgba(8,12,22,1) 55%, #05070d 100%);\">"
"<tr><td style=\"padding:32px 18px;\">"
"style=\"width:100%; border-collapse:collapse; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<tr><td style=\"padding:32px 18px;\" bgcolor=\"#eef2f7\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"max-width:680px; margin:0 auto; border-collapse:collapse;\">"
"<tr><td style=\"padding:0 0 18px;\">"
f"<div style=\"padding:24px 28px; background:#0b0f18; border:1px solid rgba(255,255,255,0.08); border-radius:28px; box-shadow:0 24px 60px rgba(0,0,0,0.42);\">"
f"<div style=\"padding:24px 28px; background:#ffffff; border:1px solid #d5deed; border-radius:28px; box-shadow:0 18px 48px rgba(15,23,42,0.08);\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
f"<td style=\"vertical-align:middle; width:64px; padding:0 18px 0 0;\">{logo_block}</td>"
"<td style=\"vertical-align:middle;\">"
f"<div style=\"font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:#9aa3b8; margin-bottom:6px;\">{html.escape(app_name)}</div>"
f"<div style=\"font-size:30px; line-height:1.1; font-weight:900; color:#e9ecf5; margin:0 0 6px;\">{html.escape(title)}</div>"
f"<div style=\"font-size:15px; line-height:1.6; color:#9aa3b8;\">{html.escape(subtitle or EMAIL_TAGLINE)}</div>"
f"<div style=\"font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:#6b778c; margin-bottom:6px;\">{html.escape(app_name)}</div>"
f"<div style=\"font-size:30px; line-height:1.1; font-weight:900; color:#132033; margin:0 0 6px;\">{html.escape(title)}</div>"
f"<div style=\"font-size:15px; line-height:1.6; color:#5c687d;\">{html.escape(subtitle or EMAIL_TAGLINE)}</div>"
"</td>"
"</tr>"
"</table>"
f"<div style=\"height:6px; margin:22px 0 22px; border-radius:999px; background:linear-gradient(90deg, {styles['accent_a']} 0%, {styles['accent_b']} 100%);\"></div>"
f"<div style=\"height:6px; margin:22px 0 22px; border-radius:999px; background-color:{styles['accent_b']}; background:linear-gradient(90deg, {styles['accent_a']} 0%, {styles['accent_b']} 100%);\"></div>"
f"<div style=\"display:inline-block; padding:7px 12px; margin:0 0 16px; background:{styles['chip_bg']}; "
f"border:1px solid {styles['chip_border']}; border-radius:999px; color:{styles['chip_text']}; "
"font-size:11px; font-weight:800; letter-spacing:0.14em; text-transform:uppercase;\">"
f"{html.escape(EMAIL_TAGLINE)}</div>"
f"<div style=\"color:#e9ecf5;\">{body_html}</div>"
f"<div style=\"color:#132033;\">{body_html}</div>"
f"<div style=\"margin:24px 0 0;\">{actions_html}</div>"
"<div style=\"margin:28px 0 0; padding:18px 0 0; border-top:1px solid rgba(255,255,255,0.08);\">"
"<div style=\"margin:28px 0 0; padding:18px 0 0; border-top:1px solid #e2e8f0;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
f"<td style=\"font-size:12px; line-height:1.7; color:#9aa3b8;\">{html.escape(footer)}</td>"
f"<td style=\"font-size:12px; line-height:1.7; color:#9aa3b8; text-align:right;\">Build {html.escape(build_number)}</td>"
f"<td style=\"font-size:12px; line-height:1.7; color:#6b778c;\">{html.escape(footer)}</td>"
f"<td style=\"font-size:12px; line-height:1.7; color:#6b778c; text-align:right;\">Build {html.escape(build_number)}</td>"
"</tr>"
"</table>"
"</div>"
@@ -1014,7 +1015,7 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st
subtitle="This confirms Magent can generate and hand off branded mail.",
tone="brand",
body_html=(
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"This is a live test email from Magent. If this renders correctly, the HTML template shell and SMTP handoff are both working."
"</div>"
+ _build_email_stat_grid(
@@ -1110,7 +1111,7 @@ async def send_password_reset_email(
subtitle=f"This will update the credentials used for {provider_label}.",
tone="brand",
body_html=(
f"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
f"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
f"A password reset was requested for <strong>{html.escape(username)}</strong>."
"</div>"
+ _build_email_stat_grid(

View File

@@ -2296,14 +2296,6 @@ export default function SettingsPage({ section }: SettingsPageProps) {
/>
</label>
) : null}
<button
type="button"
className="settings-action-button"
onClick={() => void saveSettingGroup(sectionGroup)}
disabled={sectionSaving[sectionGroup.key] || sectionTesting[sectionGroup.key]}
>
{sectionSaving[sectionGroup.key] ? 'Saving...' : 'Save section'}
</button>
{getSectionTestLabel(sectionGroup.key) ? (
<button
type="button"
@@ -2316,6 +2308,14 @@ export default function SettingsPage({ section }: SettingsPageProps) {
: getSectionTestLabel(sectionGroup.key)}
</button>
) : null}
<button
type="button"
className="settings-action-button"
onClick={() => void saveSettingGroup(sectionGroup)}
disabled={sectionSaving[sectionGroup.key] || sectionTesting[sectionGroup.key]}
>
{sectionSaving[sectionGroup.key] ? 'Saving...' : 'Save section'}
</button>
</div>
</section>
))}

View File

@@ -1,12 +1,12 @@
{
"name": "magent-frontend",
"version": "0303261702",
"version": "0303261719",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "magent-frontend",
"version": "0303261702",
"version": "0303261719",
"dependencies": {
"next": "16.1.6",
"react": "19.2.4",

View File

@@ -1,7 +1,7 @@
{
"name": "magent-frontend",
"private": true,
"version": "0303261702",
"version": "0303261719",
"scripts": {
"dev": "next dev",
"build": "next build",