diff --git a/apps/web/src/components/EmailPreviewModal.tsx b/apps/web/src/components/EmailPreviewModal.tsx index 52df741..27029d1 100644 --- a/apps/web/src/components/EmailPreviewModal.tsx +++ b/apps/web/src/components/EmailPreviewModal.tsx @@ -16,6 +16,391 @@ interface EmailPreviewModalProps { type PreviewDevice = 'mobile' | 'tablet' | 'desktop'; +// Detects if HTML contains custom patterns that indicate it was written in the HTML editor +// rather than the visual editor. Custom HTML should render as-is without prose wrapper. +const detectCustomHtmlPatterns = (html: string): boolean => { + if (!html || html.trim() === '') return false; + + // Check for common patterns that indicate custom HTML + const hasInlineStyles = /<[^>]+style\s*=\s*["'][^"']*["']/i.test(html); + + // Check for custom classes (exclude TipTap's generated classes) + const classMatches = html.matchAll(/class\s*=\s*["']([^"']*)["']/gi); + let hasCustomClasses = false; + for (const match of classMatches) { + const classValue = match[1]; + if (!classValue) continue; + // Split by whitespace to get individual classes + const classes = classValue.split(/\s+/).filter((c) => c.length > 0); + // Check if any class is NOT in the allowed list + const allowedPrefixes = [ + 'prose', + 'variable-', + 'email-image', + 'ProseMirror', + 'resizable-image', + 'selected', + 'resize-handle', + ]; + const hasDisallowedClass = classes.some((cls) => !allowedPrefixes.some((prefix) => cls.startsWith(prefix))); + if (hasDisallowedClass) { + hasCustomClasses = true; + break; + } + } + + const hasCustomAttributes = /<[^>]+(?:data-|aria-|role=|id=)/i.test(html); + const hasComplexTables = /]*>[\s\S]*?]*>/i.test(html); + const hasMediaQueries = /@media/i.test(html); + const hasStyleTags = /]*>/i.test(html); + + return ( + hasInlineStyles || + hasCustomClasses || + hasCustomAttributes || + hasComplexTables || + hasCustomElements || + hasMediaQueries || + hasStyleTags + ); +}; + +// Helper function to wrap email body with necessary styles for proper rendering +// Only wraps visual editor content - custom HTML is returned as-is +const wrapEmailWithStyles = (htmlBody: string): string => { + // If this is custom HTML (from HTML editor), return it as-is without wrapping + if (detectCustomHtmlPatterns(htmlBody)) { + return htmlBody; + } + + // For visual editor content, wrap with prose styles + return ` + + + + + + + +
+ ${htmlBody} +
+ +`; +}; + export function EmailPreviewModal({ open, onOpenChange, @@ -53,6 +438,9 @@ export function EmailPreviewModal({ const displayFrom = fromName ? `${fromName} <${from}>` : from; + // Wrap the body with styles for proper rendering + const styledBody = wrapEmailWithStyles(body); + return ( @@ -156,7 +544,7 @@ export function EmailPreviewModal({