import { Head, Img, Link, pixelBasedPreset } from "@react-email/components";
import React from "react";
export type EmailTheme = {
primary: string;
pageBackground: string;
backgroundColor: string;
accentBackground: string;
accentText: string;
headingFont: string;
bodyFont: string;
// Matches the publication's web `pageWidth` (px) so subscribers see the
// post at the same column width in their inbox as on the live page.
// Default 624 mirrors `ThemeProvider.tsx`'s fallback.
pageWidth: number;
showPageBackground: boolean;
};
export const defaultEmailTheme: EmailTheme = {
primary: "rgb(39, 39, 39)",
pageBackground: "rgb(255, 255, 255)",
backgroundColor: "rgb(240, 247, 250)",
accentBackground: "rgb(87, 130, 43)",
accentText: "rgb(255, 255, 255)",
headingFont: "Georgia, serif",
bodyFont: "Verdana, sans-serif",
pageWidth: 624,
showPageBackground: true,
};
// Parse rgb()/rgba()/#hex into [r, g, b]. Returns black on parse failure —
// theme colors come from a typed config so this is just defensive.
const parseColor = (input: string): [number, number, number] => {
const rgbMatch = input.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i);
if (rgbMatch)
return [Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])];
const hexMatch = input.match(/^#([0-9a-f]{6})$/i);
if (hexMatch) {
const h = hexMatch[1];
return [
parseInt(h.slice(0, 2), 16),
parseInt(h.slice(2, 4), 16),
parseInt(h.slice(4, 6), 16),
];
}
const hex3 = input.match(/^#([0-9a-f]{3})$/i);
if (hex3) {
const h = hex3[1];
return [
parseInt(h[0] + h[0], 16),
parseInt(h[1] + h[1], 16),
parseInt(h[2] + h[2], 16),
];
}
return [0, 0, 0];
};
// Linear sRGB mix of two colors. We resolve theme tints to literal rgb() at
// render time because Gmail's CSS sanitizer drops `color-mix(...)` —
// leaving borders invisible and tinted text falling back to defaults.
// Linear mixing isn't perceptually identical to the oklab original, but
// for the near-grayscale tints we use it's visually indistinguishable.
export const mixRgb = (a: string, b: string, bPercent: number): string => {
const [ar, ag, ab] = parseColor(a);
const [br, bg, bb] = parseColor(b);
const t = bPercent / 100;
const round = (x: number) => Math.round(x);
return `rgb(${round(ar * (1 - t) + br * t)}, ${round(
ag * (1 - t) + bg * t,
)}, ${round(ab * (1 - t) + bb * t)})`;
};
export type ResolvedColors = {
primary: string;
secondary: string;
tertiary: string;
border: string;
borderLight: string;
};
export const resolveColors = (theme: EmailTheme): ResolvedColors => {
// When the card background is hidden, the surface visible behind every
// block is the outer page bg, not the publication's pageBackground.
// Mix tints against that visible surface so block fills (e.g. the
// unsupported-block fallback) sit on a tone derived from what's
// actually behind them — mirroring `BaseThemeProvider`'s web behavior
// where `bgPage` collapses to `bgLeaflet` when showPageBackground is
// false.
const effectiveBg = theme.showPageBackground
? theme.pageBackground
: theme.backgroundColor;
return {
primary: theme.primary,
secondary: mixRgb(theme.primary, effectiveBg, 25),
tertiary: mixRgb(theme.primary, effectiveBg, 55),
border: mixRgb(theme.primary, effectiveBg, 75),
borderLight: mixRgb(theme.primary, effectiveBg, 85),
};
};
// Postmark fetches `` and link `href` values verbatim from the
// rendered HTML — relative paths break. Templates accept an `assetsBaseUrl`
// prop and use this builder for `/email-assets/*` references.
export const makeStaticUrl = (assetsBaseUrl: string) => {
const base = assetsBaseUrl.replace(/\/$/, "");
return (filename: string): string => `${base}/email-assets/${filename}`;
};
// Build a URL for the dynamic icon endpoint that bakes the theme color
// into a PNG at request time. We render PNG (not SVG) because Gmail's
// image proxy strips SVG for @gmail.com inboxes, and Microsoft removed
// SVG support across new Outlook + Outlook.com in late 2025.
export const makeEmailIconUrl = (
assetsBaseUrl: string,
name: "quote" | "comment",
color: string,
size = 16,
): string => {
const base = assetsBaseUrl.replace(/\/$/, "");
const params = new URLSearchParams({ color });
if (size !== 16) params.set("size", String(size));
return `${base}/api/email-assets/${name}.png?${params.toString()}`;
};
// React's TypeScript types don't include the deprecated `bgcolor` HTML
// attribute on