Files
suaveplan/packages/communication/emails/email-templates/src/components/email-button/email-button.ts
2026-05-27 23:29:53 +01:00

142 lines
4.2 KiB
TypeScript

/**
* CTA button rendered as a table-wrapped `<a>` with variant + size
* recipes drawn from the theme. Outlook-on-Windows honours the table
* geometry, which is why this is not a `<button>` element.
*/
import {
EMAIL_TEMPLATES_CODES,
EmailTemplatesError,
} from "../../error-codes/error-codes.js";
import { getEmailTemplatesInstruments } from "../../telemetry/telemetry.js";
import { defaultTheme } from "../../theme/theme.js";
import type { EmailTheme } from "../../theme/theme.types.js";
import { h } from "../../vnode/vnode.js";
import type { StyleObject, VNode } from "../../vnode/vnode.types.js";
import type {
ButtonSize,
ButtonVariant,
EmailButtonProps,
} from "./email-button.types.js";
const VARIANTS: ReadonlySet<ButtonVariant> = new Set([
"primary",
"secondary",
"outline",
]);
const SIZES: ReadonlySet<ButtonSize> = new Set(["sm", "md", "lg"]);
const SIZE_PADDING: Record<ButtonSize, string> = {
sm: "8px 16px",
md: "12px 24px",
lg: "16px 32px",
};
const SIZE_FONT: Record<ButtonSize, string> = {
sm: "13px",
md: "15px",
lg: "17px",
};
function variantStyle(variant: ButtonVariant, theme: EmailTheme): StyleObject {
switch (variant) {
case "primary":
return {
"background-color": theme.colors.primary,
color: theme.colors.primaryForeground,
border: `2px solid ${theme.colors.primary}`,
};
case "secondary":
return {
"background-color": theme.colors.secondary,
color: theme.colors.secondaryForeground,
border: `2px solid ${theme.colors.secondary}`,
};
case "outline":
return {
"background-color": "transparent",
color: theme.colors.primary,
border: `2px solid ${theme.colors.primary}`,
};
}
}
export function EmailButton(props: EmailButtonProps): VNode {
const {
href,
children,
theme = defaultTheme,
variant = "primary",
size = "md",
fullWidth = false,
align = "center",
style,
} = props;
if (!VARIANTS.has(variant)) {
getEmailTemplatesInstruments().validationFailureCounter.add(1, {
reason: "invalid_button_variant",
});
throw new EmailTemplatesError(
EMAIL_TEMPLATES_CODES.INVALID_OPTIONS,
"email_templates.invalid_options.button_variant"
);
}
if (!SIZES.has(size)) {
getEmailTemplatesInstruments().validationFailureCounter.add(1, {
reason: "invalid_button_size",
});
throw new EmailTemplatesError(
EMAIL_TEMPLATES_CODES.INVALID_OPTIONS,
"email_templates.invalid_options.button_size"
);
}
const v = variantStyle(variant, theme);
return h(
"table",
{
role: "presentation",
cellpadding: 0,
cellspacing: 0,
align,
style: {
margin: `0 0 ${theme.spacing.md} 0`,
width: fullWidth ? "100%" : undefined,
},
},
h(
"tr",
{},
h(
"td",
{ align: "center" },
h(
"a",
{
href,
target: "_blank",
rel: "noopener noreferrer",
style: {
display: "inline-block",
"text-decoration": "none",
"font-family": theme.typography.fontFamily,
"font-size": SIZE_FONT[size],
"font-weight": 600,
padding: SIZE_PADDING[size],
"border-radius": theme.borderRadius,
"text-align": "center",
width: fullWidth ? "100%" : undefined,
"box-sizing": "border-box",
...v,
...(style ?? {}),
},
},
children
)
)
)
);
}