themes-noir/email_wrapper.templ
Alex Dunmow 1bebbea5ad initial: theme plugin noir
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/noir.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 14:11:40 +08:00

137 lines
5.2 KiB
Plaintext

package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// NoirEmailWrapper wraps body content in a pure black 600px canvas with a
// Tenor Sans masthead and a mono caption strip above the footer.
func NoirEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = noirEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// noirEmailTemplate renders the inline-styled HTML email wrapper.
templ noirEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", noirEmailBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{ emailCtx.PreviewText }</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("background-color: %s;", noirEmailBg(emailCtx)) }>
<tr>
<td align="center" style="padding: 48px 12px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("width: 600px; max-width: 600px; background-color: %s; border: 1px solid %s;", noirEmailCard(emailCtx), noirEmailBorder(emailCtx)) }>
<!-- Masthead -->
<tr>
<td align="left" style={ fmt.Sprintf("padding: 28px 32px; border-bottom: 1px solid %s;", noirEmailBorder(emailCtx)) }>
if emailCtx.SiteSettings.SiteName != "" {
<div style={ fmt.Sprintf("margin: 0; font-size: 18px; font-weight: 400; font-family: 'Tenor Sans', Georgia, serif; letter-spacing: 0.05em; color: %s;", noirEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</div>
}
</td>
</tr>
<!-- Cover image (16:10) -->
if emailCtx.SiteSettings.LogoURL != "" {
<tr>
<td style="padding: 0; line-height: 0;">
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } width="600" height="375" style="display: block; width: 600px; height: 375px; object-fit: cover; max-width: 100%;"/>
</td>
</tr>
}
<!-- Body -->
<tr>
<td style={ fmt.Sprintf("padding: 32px; color: %s; font-size: 16px; line-height: 1.7;", noirEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- Mono caption strip -->
<tr>
<td style={ fmt.Sprintf("padding: 12px 32px; border-top: 1px solid %s; border-bottom: 1px solid %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: %s;", noirEmailBorder(emailCtx), noirEmailBorder(emailCtx), noirEmailMutedFg(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="left">
if emailCtx.SiteSettings.SiteName != "" {
{ "© " + emailCtx.SiteSettings.SiteName }
}
</td>
<td align="right">
if emailCtx.UnsubscribeURL != "" {
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)) }>
Unsubscribe
</a>
}
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td align="center" style={ fmt.Sprintf("padding: 20px 32px; color: %s; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;", noirEmailMutedFg(emailCtx)) }>
if emailCtx.SiteSettings.SiteURL != "" {
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", noirEmailMutedFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteURL }
</a>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// noirEmailBg returns the canvas colour (pure black per spec) with a fallback
// derived from the EmailContext.
func noirEmailBg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Background != "" {
return emailCtx.Colors.Background
}
return "#000000"
}
func noirEmailCard(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Card != "" {
return emailCtx.Colors.Card
}
return "#0a0a0a"
}
func noirEmailFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Foreground != "" {
return emailCtx.Colors.Foreground
}
return "#f5f5f5"
}
func noirEmailMutedFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.MutedForeground != "" {
return emailCtx.Colors.MutedForeground
}
return "#8c8c8c"
}
func noirEmailBorder(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Border != "" {
return emailCtx.Colors.Border
}
return "#1f1f1f"
}