mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(emails): align transactional emails with the platform neutral design system (#5309)
* improvement(emails): align transactional emails with the platform neutral design system Re-skin all ~25 transactional email templates to match the platform/newsletter: - Color tokens in _styles/base.ts now mirror globals.css light mode (#fefefe canvas, #1a1a1a text, #525252/#5c5c5c/#707070 greys, #dedede border) instead of the old lavender + green (#33C482) scheme; accent defaults to neutral #1a1a1a but still honors a whitelabel primaryColor. - Primary CTA now matches the platform's primary Chip (inverse #1a1a1a fill, white text, rounded-lg, text-sm, normal weight); links carry an underline since the neutral color no longer signals a link. - Card uses 8px radius + hairline border; info/code boxes use a subtle token fill. - Logo is the dark, text-only 'sim' wordmark (green icon removed). - Footer: LinkedIn replaces Discord. - Every template routes through the centralized colors tokens — zero hardcoded hex (payment-failed error box tokenized to platform error surface values). * fix(emails): keep card boundary robust + don't reshape custom logos - Canvas goes to #f8f8f8 (off Andres's newsletter) so the white card reads via contrast in clients that flatten inline borders, not the border alone (Greptile P2). - Logo pins exact 70x34 only for the bundled wordmark; whitelabel brand.logoUrl now constrains by height with auto width so custom logos keep their aspect (Greptile P1). * improvement(emails): use Season Sans to match the newsletter and platform Register the Season Sans web font (the woff2 shipped with the newsletter, #5302) via react-email's <Font> in the layout head, and lead the email font stack with 'Season Sans' (system stack still the fallback for clients that ignore @font-face).
This commit is contained in:
@@ -10,30 +10,36 @@ function buildColors() {
|
||||
const brand = getBrandConfig()
|
||||
const isWhitelabeled = brand.isWhitelabeled
|
||||
const accentColor =
|
||||
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#33C482'
|
||||
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#1a1a1a'
|
||||
|
||||
return {
|
||||
/** Main canvas background */
|
||||
bgOuter: '#F7F9FC',
|
||||
/** Card/container background - pure white */
|
||||
/** Main canvas background — a hair off-white so the white card reads via contrast, not the border alone */
|
||||
bgOuter: '#f8f8f8',
|
||||
/** Card/container background — platform `--surface-2` */
|
||||
bgCard: '#ffffff',
|
||||
/** Primary text color */
|
||||
textPrimary: '#2d2d2d',
|
||||
/** Secondary text color */
|
||||
textSecondary: '#404040',
|
||||
/** Tertiary text color */
|
||||
/** Primary text — platform `--text-primary` */
|
||||
textPrimary: '#1a1a1a',
|
||||
/** Secondary text — platform `--text-secondary` */
|
||||
textSecondary: '#525252',
|
||||
/** Tertiary text — platform `--text-tertiary` */
|
||||
textTertiary: '#5c5c5c',
|
||||
/** Muted text (footer) */
|
||||
textMuted: '#737373',
|
||||
/** Brand primary color */
|
||||
/** Muted text (footer) — platform `--text-muted` */
|
||||
textMuted: '#707070',
|
||||
/** Brand primary — neutral by default, brand color when whitelabeled */
|
||||
brandPrimary:
|
||||
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#33C482',
|
||||
/** Brand accent - used for buttons and links */
|
||||
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#1a1a1a',
|
||||
/** Accent for buttons and links — neutral by default, brand color when whitelabeled */
|
||||
brandTertiary: accentColor,
|
||||
/** Border/divider color */
|
||||
divider: '#ededed',
|
||||
/** Footer background */
|
||||
footerBg: '#F7F9FC',
|
||||
/** Border/divider — platform `--border` */
|
||||
divider: '#dedede',
|
||||
/** Subtle fill for info/code boxes on the white card */
|
||||
surfaceSubtle: '#f7f7f7',
|
||||
/** Error surface fill — platform `--terminal-status-error-bg` */
|
||||
errorBg: '#fef2f2',
|
||||
/** Error surface border — platform `--error-muted` */
|
||||
errorBorder: '#fecaca',
|
||||
/** Footer background — matches the canvas */
|
||||
footerBg: '#f8f8f8',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +47,8 @@ export const colors = buildColors()
|
||||
|
||||
/** Typography settings */
|
||||
export const typography = {
|
||||
fontFamily: "-apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica', sans-serif",
|
||||
fontFamily:
|
||||
"'Season Sans', -apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica', sans-serif",
|
||||
fontSize: {
|
||||
body: '16px',
|
||||
small: '14px',
|
||||
@@ -79,12 +86,13 @@ export const baseStyles = {
|
||||
margin: '0 auto',
|
||||
},
|
||||
|
||||
/** Main card container with rounded corners */
|
||||
/** Main card container — white surface, chip-radius, hairline border on the near-white canvas */
|
||||
container: {
|
||||
maxWidth: `${spacing.containerWidth}px`,
|
||||
margin: '0 auto',
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: '16px',
|
||||
border: `1px solid ${colors.divider}`,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -120,26 +128,27 @@ export const baseStyles = {
|
||||
display: 'inline',
|
||||
},
|
||||
|
||||
/** Primary CTA button - matches app tertiary button style */
|
||||
/** Primary CTA button - matches the platform's primary Chip (inverse fill, rounded-lg, h-30, text-sm) */
|
||||
button: {
|
||||
display: 'inline-block',
|
||||
backgroundColor: colors.brandTertiary,
|
||||
color: '#ffffff',
|
||||
fontWeight: 500,
|
||||
fontWeight: 400,
|
||||
fontSize: '14px',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '5px',
|
||||
lineHeight: '30px',
|
||||
padding: '0 12px',
|
||||
borderRadius: '8px',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center' as const,
|
||||
margin: '4px 0',
|
||||
fontFamily: typography.fontFamily,
|
||||
},
|
||||
|
||||
/** Link text style */
|
||||
/** Link text style - neutral color, so it carries an underline to read as a link */
|
||||
link: {
|
||||
color: colors.brandTertiary,
|
||||
fontWeight: 'bold' as const,
|
||||
textDecoration: 'none',
|
||||
fontWeight: 400,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
|
||||
/** Horizontal divider */
|
||||
@@ -169,8 +178,8 @@ export const baseStyles = {
|
||||
codeContainer: {
|
||||
margin: '12px 0',
|
||||
padding: '12px 16px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: colors.surfaceSubtle,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${colors.divider}`,
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
@@ -198,9 +207,9 @@ export const baseStyles = {
|
||||
|
||||
/** Highlighted info box (e.g., "What you get with Pro") */
|
||||
infoBox: {
|
||||
backgroundColor: colors.bgOuter,
|
||||
backgroundColor: colors.surfaceSubtle,
|
||||
padding: '16px 18px',
|
||||
borderRadius: '6px',
|
||||
borderRadius: '8px',
|
||||
margin: '16px 0',
|
||||
},
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function CreditsExhaustedEmail({
|
||||
|
||||
<Section
|
||||
style={{
|
||||
backgroundColor: '#f8faf9',
|
||||
backgroundColor: colors.surfaceSubtle,
|
||||
border: `1px solid ${colors.brandTertiary}20`,
|
||||
borderRadius: '8px',
|
||||
padding: '16px 20px',
|
||||
|
||||
@@ -39,7 +39,7 @@ export function FreeTierUpgradeEmail({
|
||||
{/* Pro Features */}
|
||||
<Section
|
||||
style={{
|
||||
backgroundColor: '#f8faf9',
|
||||
backgroundColor: colors.surfaceSubtle,
|
||||
border: `1px solid ${colors.brandTertiary}20`,
|
||||
borderRadius: '8px',
|
||||
padding: '16px 20px',
|
||||
|
||||
@@ -48,8 +48,8 @@ export function PaymentFailedEmail({
|
||||
|
||||
<Section
|
||||
style={{
|
||||
backgroundColor: '#fff5f5',
|
||||
border: '1px solid #fed7d7',
|
||||
backgroundColor: colors.errorBg,
|
||||
border: `1px solid ${colors.errorBorder}`,
|
||||
borderRadius: '6px',
|
||||
padding: '16px 18px',
|
||||
margin: '16px 0',
|
||||
|
||||
@@ -81,12 +81,15 @@ export function EmailFooter({
|
||||
</Link>
|
||||
</td>
|
||||
<td align='left' style={{ padding: '0 8px' }}>
|
||||
<Link href={`${baseUrl}/discord`} rel='noopener noreferrer'>
|
||||
<Link
|
||||
href='https://www.linkedin.com/company/simdotai'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Img
|
||||
src={`${baseUrl}/static/discord-icon.png`}
|
||||
src={`${baseUrl}/static/linkedin-icon.png`}
|
||||
width='20'
|
||||
height='20'
|
||||
alt='Discord'
|
||||
alt='LinkedIn'
|
||||
/>
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Container, Head, Html, Img, Preview, Section } from '@react-email/components'
|
||||
import { Body, Container, Font, Head, Html, Img, Preview, Section } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailFooter } from '@/components/emails/components/email-footer'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -30,10 +30,22 @@ export function EmailLayout({
|
||||
}: EmailLayoutProps) {
|
||||
const brand = getBrandConfig()
|
||||
const baseUrl = getBaseUrl()
|
||||
const hasCustomLogo = Boolean(brand.logoUrl)
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Head>
|
||||
<Font
|
||||
fontFamily='Season Sans'
|
||||
fallbackFontFamily={['Helvetica', 'sans-serif']}
|
||||
webFont={{
|
||||
url: `${baseUrl}/brand/fonts/SeasonSansUprightsVF.woff2`,
|
||||
format: 'woff2',
|
||||
}}
|
||||
fontWeight='300 800'
|
||||
fontStyle='normal'
|
||||
/>
|
||||
</Head>
|
||||
<Preview>{preview}</Preview>
|
||||
<Body style={baseStyles.main}>
|
||||
{/* Main card container */}
|
||||
@@ -42,10 +54,10 @@ export function EmailLayout({
|
||||
<Section style={baseStyles.header}>
|
||||
<Img
|
||||
src={brand.logoUrl || `${baseUrl}/brand/color/email/wordmark.png`}
|
||||
width='107'
|
||||
height='33'
|
||||
height='34'
|
||||
{...(hasCustomLogo ? {} : { width: '70' })}
|
||||
alt={brand.name}
|
||||
style={{ display: 'block' }}
|
||||
style={hasCustomLogo ? { display: 'block', width: 'auto' } : { display: 'block' }}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user