package mail
import (
"html"
"strings"
)
// Shared visual tokens for Silo's branded emails, mirroring the web UI's
// default "Midnight Cinema" theme (web/src/app.css): a near-black canvas,
// monochrome type, and a white primary action. Feature packages compose body
// fragments with these tokens and wrap them with RenderLayout so every email
// the server sends looks like it came from the same product.
//
// Email-client constraints shape everything here: styles must be inline,
// layout must be tables, and colors must be explicit on every element (no
// inheritance through client-rewritten DOM). Web fonts don't load in most
// clients, so the stacks lead with the brand font and degrade to common
// system faces.
const (
EmailFont = "'Outfit','Avenir Next','Segoe UI',Helvetica,Arial,sans-serif"
EmailFontMono = "'SF Mono',SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace"
EmailColorCanvas = "#141417" // page background
EmailColorCard = "#1c1c20" // content card surface
EmailColorBorder = "#2e2e35" // card outline
EmailColorText = "#e8e8ec" // primary text
EmailColorMuted = "#9696a0" // secondary text, badges, footer
EmailColorRule = "#26262c" // hairline row separators
EmailColorAction = "#e8e8ec" // primary button background (white-on-dark)
EmailColorOnAct = "#141417" // primary button label
)
// LayoutOptions is the content RenderLayout places into the branded shell.
type LayoutOptions struct {
// Preheader is the hidden inbox-preview snippet shown next to the subject
// line. Plain text; optional.
Preheader string
// Title is the headline at the top of the card. Plain text; optional.
Title string
// BodyHTML is the card content below the title. Trusted HTML — callers
// must escape any user-controlled values before building it.
BodyHTML string
// FooterHTML is the fine print under the card. Trusted HTML; optional.
FooterHTML string
}
// RenderLayout wraps content in Silo's dark branded email shell: wordmark,
// content card, and footer. It adds no links of its own, so an email whose
// options carry no hrefs renders fully link-free (some features require
// that when no external URL is configured).
func RenderLayout(opts LayoutOptions) string {
preheader := ""
if opts.Preheader != "" {
// The trailing zwnj/nbsp run pads the preview so clients don't pull
// body markup into the snippet after the real preheader text.
preheader = `
` +
html.EscapeString(opts.Preheader) +
strings.Repeat(" ", 40) + `
` + "\n"
}
title := ""
if opts.Title != "" {
title = `` + html.EscapeString(opts.Title) + `
` + "\n"
}
footer := ""
if opts.FooterHTML != "" {
footer = `| ` + opts.FooterHTML + ` |
` + "\n"
}
return strings.NewReplacer(
"{{preheader}}", preheader,
"{{title}}", title,
"{{body}}", opts.BodyHTML,
"{{footer}}", footer,
"{{font}}", EmailFont,
"{{canvas}}", EmailColorCanvas,
"{{card}}", EmailColorCard,
"{{border}}", EmailColorBorder,
"{{text}}", EmailColorText,
).Replace(emailShell)
}
// EmailButton renders the primary call-to-action: a white pill on the dark
// card, matching the web UI's primary action style. Both arguments are
// escaped here. The wrapping table keeps the button shape in Outlook, which
// ignores padding on anchors.
func EmailButton(label, href string) string {
return ``
}
// EmailParagraph renders one body paragraph in the standard text style,
// escaping the given plain text.
func EmailParagraph(text string) string {
return `` + html.EscapeString(text) + `
`
}
// emailShell is the document skeleton. The color-scheme meta plus explicit
// bgcolor attributes keep dark-mode-aware clients from inverting the design;
// the small stylesheet only tightens padding on narrow screens (supported by
// Gmail/Apple Mail, harmlessly ignored elsewhere).
const emailShell = `
{{preheader}}
| ▸︎ SILO |
|
{{title}}{{body}}
|
{{footer}}
|
`