@startsimpli/ui 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandPalette.tsx +344 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +3 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/index.ts +14 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +791 -0
- package/src/components/email-editor/email-editor.tsx +886 -0
- package/src/components/email-editor/index.ts +50 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttChart.tsx +25 -25
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/index.ts +6 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Email Editor - barrel export
|
|
2
|
+
export { EmailEditor } from './email-editor'
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type {
|
|
6
|
+
Block,
|
|
7
|
+
BlockType,
|
|
8
|
+
TextBlock,
|
|
9
|
+
MetricsBlock,
|
|
10
|
+
MetricItem,
|
|
11
|
+
DividerBlock,
|
|
12
|
+
CTABlock,
|
|
13
|
+
ImageBlock,
|
|
14
|
+
SpacerBlock,
|
|
15
|
+
SocialBlock,
|
|
16
|
+
SocialLink,
|
|
17
|
+
HeaderBlock,
|
|
18
|
+
FooterBlock,
|
|
19
|
+
Section,
|
|
20
|
+
Row,
|
|
21
|
+
ColumnLayout,
|
|
22
|
+
GlobalStyles,
|
|
23
|
+
BlockStyle,
|
|
24
|
+
MergeFieldDefinition,
|
|
25
|
+
EditorSelection,
|
|
26
|
+
} from './types'
|
|
27
|
+
|
|
28
|
+
// Helpers
|
|
29
|
+
export {
|
|
30
|
+
createBlock,
|
|
31
|
+
createRow,
|
|
32
|
+
createSection,
|
|
33
|
+
getColumnCount,
|
|
34
|
+
getColumnWidths,
|
|
35
|
+
DEFAULT_GLOBAL_STYLES,
|
|
36
|
+
// Serialization
|
|
37
|
+
serializeSections,
|
|
38
|
+
deserializeSections,
|
|
39
|
+
serializeBlocks,
|
|
40
|
+
deserializeBlocks,
|
|
41
|
+
// Migration
|
|
42
|
+
migrateFromLegacy,
|
|
43
|
+
flattenToLegacy,
|
|
44
|
+
} from './types'
|
|
45
|
+
|
|
46
|
+
// Renderer
|
|
47
|
+
export { renderToEmailHtml, renderToPreviewHtml } from './renderer/email-html-renderer'
|
|
48
|
+
|
|
49
|
+
// Utilities
|
|
50
|
+
export { createInvestorUpdateTemplate, createEmptyTemplate, THEME_PRESETS } from './utils/defaults'
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Block,
|
|
3
|
+
TextBlock,
|
|
4
|
+
MetricsBlock,
|
|
5
|
+
DividerBlock,
|
|
6
|
+
CTABlock,
|
|
7
|
+
ImageBlock,
|
|
8
|
+
SpacerBlock,
|
|
9
|
+
SocialBlock,
|
|
10
|
+
HeaderBlock,
|
|
11
|
+
FooterBlock,
|
|
12
|
+
GlobalStyles,
|
|
13
|
+
} from '../types'
|
|
14
|
+
|
|
15
|
+
// Render a single block to email-safe HTML (table-based, inline CSS)
|
|
16
|
+
export function renderBlockToHtml(block: Block, globalStyles: GlobalStyles): string {
|
|
17
|
+
switch (block.type) {
|
|
18
|
+
case 'text':
|
|
19
|
+
return renderTextBlock(block, globalStyles)
|
|
20
|
+
case 'metrics':
|
|
21
|
+
return renderMetricsBlock(block)
|
|
22
|
+
case 'divider':
|
|
23
|
+
return renderDividerBlock(block)
|
|
24
|
+
case 'cta':
|
|
25
|
+
return renderButtonBlock(block)
|
|
26
|
+
case 'image':
|
|
27
|
+
return renderImageBlock(block)
|
|
28
|
+
case 'spacer':
|
|
29
|
+
return renderSpacerBlock(block)
|
|
30
|
+
case 'social':
|
|
31
|
+
return renderSocialBlock(block)
|
|
32
|
+
case 'header':
|
|
33
|
+
return renderHeaderBlock(block)
|
|
34
|
+
case 'footer':
|
|
35
|
+
return renderFooterBlock(block)
|
|
36
|
+
default:
|
|
37
|
+
return ''
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function blockPaddingStyle(block: Block): string {
|
|
42
|
+
const s = block.style
|
|
43
|
+
if (!s) return ''
|
|
44
|
+
const parts: string[] = []
|
|
45
|
+
if (s.paddingTop) parts.push(`padding-top:${s.paddingTop}px`)
|
|
46
|
+
if (s.paddingBottom) parts.push(`padding-bottom:${s.paddingBottom}px`)
|
|
47
|
+
if (s.paddingLeft) parts.push(`padding-left:${s.paddingLeft}px`)
|
|
48
|
+
if (s.paddingRight) parts.push(`padding-right:${s.paddingRight}px`)
|
|
49
|
+
if (s.marginTop) parts.push(`margin-top:${s.marginTop}px`)
|
|
50
|
+
if (s.marginBottom) parts.push(`margin-bottom:${s.marginBottom}px`)
|
|
51
|
+
if (s.backgroundColor) parts.push(`background-color:${s.backgroundColor}`)
|
|
52
|
+
return parts.join(';')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderTextBlock(block: TextBlock, globalStyles: GlobalStyles): string {
|
|
56
|
+
const fontFamily = block.fontFamily || globalStyles.fontFamily
|
|
57
|
+
const fontSize = block.fontSize ? `${block.fontSize}px` : '16px'
|
|
58
|
+
const lineHeight = block.lineHeight ? `${block.lineHeight}` : '1.6'
|
|
59
|
+
const color = block.textColor || '#1f2937'
|
|
60
|
+
const extraStyle = blockPaddingStyle(block)
|
|
61
|
+
|
|
62
|
+
return `<div style="font-family:${fontFamily};font-size:${fontSize};line-height:${lineHeight};color:${color};${extraStyle}">${block.content}</div>`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderMetricsBlock(block: MetricsBlock): string {
|
|
66
|
+
const cols = block.columns || 2
|
|
67
|
+
const extraStyle = blockPaddingStyle(block)
|
|
68
|
+
|
|
69
|
+
const title = block.title
|
|
70
|
+
? `<div style="text-align:center;margin-bottom:16px;font-size:18px;font-weight:600;${extraStyle}">${block.title}</div>`
|
|
71
|
+
: ''
|
|
72
|
+
|
|
73
|
+
const cells = block.metrics
|
|
74
|
+
.map((m) => {
|
|
75
|
+
const changeColor =
|
|
76
|
+
m.changeType === 'positive'
|
|
77
|
+
? '#16a34a'
|
|
78
|
+
: m.changeType === 'negative'
|
|
79
|
+
? '#dc2626'
|
|
80
|
+
: '#6b7280'
|
|
81
|
+
const changeHtml = m.change
|
|
82
|
+
? `<div style="font-size:12px;color:${changeColor};margin-top:4px;">${m.change}</div>`
|
|
83
|
+
: ''
|
|
84
|
+
return `<td style="text-align:center;padding:16px;background:#f9fafb;border-radius:8px;width:${Math.floor(100 / cols)}%;">
|
|
85
|
+
<div style="font-size:24px;font-weight:bold;">${m.value}</div>
|
|
86
|
+
<div style="font-size:14px;color:#6b7280;margin-top:4px;">${m.label}</div>
|
|
87
|
+
${changeHtml}
|
|
88
|
+
</td>`
|
|
89
|
+
})
|
|
90
|
+
.join('<td style="width:16px;"></td>')
|
|
91
|
+
|
|
92
|
+
return `${title}<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:16px 0;"><tr>${cells}</tr></table>`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderDividerBlock(block: DividerBlock): string {
|
|
96
|
+
const extraStyle = blockPaddingStyle(block)
|
|
97
|
+
if (block.dividerStyle === 'space') {
|
|
98
|
+
return `<div style="height:32px;${extraStyle}"></div>`
|
|
99
|
+
}
|
|
100
|
+
const color = block.color || '#d1d5db'
|
|
101
|
+
const thickness = block.thickness || 1
|
|
102
|
+
const width = block.width || 100
|
|
103
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;${extraStyle}"><tr><td align="center"><div style="width:${width}%;border-top:${thickness}px ${block.dividerStyle} ${color};"></div></td></tr></table>`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderButtonBlock(block: CTABlock): string {
|
|
107
|
+
const bg = block.buttonColor || '#2563eb'
|
|
108
|
+
const color = block.textColor || '#ffffff'
|
|
109
|
+
const radius = block.borderRadius ?? 6
|
|
110
|
+
const paddingH = block.paddingH ?? 24
|
|
111
|
+
const paddingV = block.paddingV ?? 12
|
|
112
|
+
const extraStyle = blockPaddingStyle(block)
|
|
113
|
+
|
|
114
|
+
const align = block.alignment || 'center'
|
|
115
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;${extraStyle}"><tr><td align="${align}">
|
|
116
|
+
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="${block.url || '#'}" style="height:${paddingV * 2 + 20}px;v-text-anchor:middle;width:auto;" arcsize="${Math.round((radius / (paddingV * 2 + 20)) * 100)}%" strokecolor="${bg}" fillcolor="${bg}"><center style="color:${color};font-family:sans-serif;font-size:14px;font-weight:500;">${block.text}</center></v:roundrect><![endif]-->
|
|
117
|
+
<!--[if !mso]><!-->
|
|
118
|
+
<a href="${block.url || '#'}" style="display:inline-block;padding:${paddingV}px ${paddingH}px;background-color:${bg};color:${color};border-radius:${radius}px;text-decoration:none;font-weight:500;font-size:14px;font-family:sans-serif;">${block.text}</a>
|
|
119
|
+
<!--<![endif]-->
|
|
120
|
+
</td></tr></table>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderImageBlock(block: ImageBlock): string {
|
|
124
|
+
if (!block.url) return ''
|
|
125
|
+
const width = block.width || 100
|
|
126
|
+
const align = block.alignment || 'center'
|
|
127
|
+
const extraStyle = blockPaddingStyle(block)
|
|
128
|
+
const captionHtml = block.caption
|
|
129
|
+
? `<div style="font-size:14px;color:#6b7280;margin-top:8px;text-align:center;">${block.caption}</div>`
|
|
130
|
+
: ''
|
|
131
|
+
|
|
132
|
+
const imgTag = `<img src="${block.url}" alt="${block.alt || ''}" width="${Math.round(600 * (width / 100))}" style="display:block;max-width:100%;height:auto;border-radius:4px;" />`
|
|
133
|
+
const linked = block.linkUrl
|
|
134
|
+
? `<a href="${block.linkUrl}" target="_blank">${imgTag}</a>`
|
|
135
|
+
: imgTag
|
|
136
|
+
|
|
137
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;${extraStyle}"><tr><td align="${align}">${linked}${captionHtml}</td></tr></table>`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderSpacerBlock(block: SpacerBlock): string {
|
|
141
|
+
const height = block.height || 32
|
|
142
|
+
return `<div style="height:${height}px;line-height:${height}px;font-size:1px;"> </div>`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderSocialBlock(block: SocialBlock): string {
|
|
146
|
+
const align = block.alignment || 'center'
|
|
147
|
+
const size = block.iconSize || 24
|
|
148
|
+
const extraStyle = blockPaddingStyle(block)
|
|
149
|
+
|
|
150
|
+
// Using text-based fallback icons for email compatibility
|
|
151
|
+
const platformLabels: Record<string, string> = {
|
|
152
|
+
linkedin: 'LinkedIn',
|
|
153
|
+
twitter: 'Twitter',
|
|
154
|
+
facebook: 'Facebook',
|
|
155
|
+
instagram: 'Instagram',
|
|
156
|
+
youtube: 'YouTube',
|
|
157
|
+
github: 'GitHub',
|
|
158
|
+
website: 'Website',
|
|
159
|
+
}
|
|
160
|
+
const platformColors: Record<string, string> = {
|
|
161
|
+
linkedin: '#0A66C2',
|
|
162
|
+
twitter: '#1DA1F2',
|
|
163
|
+
facebook: '#1877F2',
|
|
164
|
+
instagram: '#E4405F',
|
|
165
|
+
youtube: '#FF0000',
|
|
166
|
+
github: '#333333',
|
|
167
|
+
website: '#6b7280',
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const links = block.links
|
|
171
|
+
.filter((l) => l.url)
|
|
172
|
+
.map((l) => {
|
|
173
|
+
const label = platformLabels[l.platform] || l.platform
|
|
174
|
+
const color = platformColors[l.platform] || '#6b7280'
|
|
175
|
+
return `<td style="padding:0 6px;"><a href="${l.url}" target="_blank" style="color:${color};text-decoration:none;font-size:${Math.max(12, size - 8)}px;font-weight:500;">${label}</a></td>`
|
|
176
|
+
})
|
|
177
|
+
.join('')
|
|
178
|
+
|
|
179
|
+
if (!links) return ''
|
|
180
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;${extraStyle}"><tr><td align="${align}"><table cellpadding="0" cellspacing="0"><tr>${links}</tr></table></td></tr></table>`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderHeaderBlock(block: HeaderBlock): string {
|
|
184
|
+
const align = block.alignment || 'center'
|
|
185
|
+
const extraStyle = blockPaddingStyle(block)
|
|
186
|
+
const logo = block.logoUrl
|
|
187
|
+
? `<img src="${block.logoUrl}" alt="${block.companyName}" style="height:40px;width:auto;margin-right:12px;vertical-align:middle;" />`
|
|
188
|
+
: ''
|
|
189
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;${extraStyle}"><tr><td align="${align}" style="padding:16px 0;">
|
|
190
|
+
${logo}<span style="font-size:20px;font-weight:600;vertical-align:middle;">${block.companyName}</span>
|
|
191
|
+
</td></tr></table>`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderFooterBlock(block: FooterBlock): string {
|
|
195
|
+
const align = block.alignment || 'center'
|
|
196
|
+
const extraStyle = blockPaddingStyle(block)
|
|
197
|
+
const addressHtml = block.address
|
|
198
|
+
? `<div style="margin-top:4px;">${block.address}</div>`
|
|
199
|
+
: ''
|
|
200
|
+
const unsubHtml = block.showUnsubscribe
|
|
201
|
+
? `<div style="margin-top:8px;"><a href="${block.unsubscribeUrl || '#'}" style="color:#6b7280;text-decoration:underline;">Unsubscribe</a> from these emails</div>`
|
|
202
|
+
: ''
|
|
203
|
+
|
|
204
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;${extraStyle}"><tr><td align="${align}" style="font-size:12px;color:#6b7280;padding:16px 0;">
|
|
205
|
+
<div>${block.companyName}</div>
|
|
206
|
+
${addressHtml}
|
|
207
|
+
${unsubHtml}
|
|
208
|
+
</td></tr></table>`
|
|
209
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Section, GlobalStyles, DEFAULT_GLOBAL_STYLES, getColumnWidths } from '../types'
|
|
2
|
+
import { renderBlockToHtml } from './block-renderers'
|
|
3
|
+
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
subject?: string
|
|
6
|
+
preheaderText?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts sections to a full email-safe HTML document.
|
|
11
|
+
* Uses table-based layout with inline CSS for maximum email client compatibility.
|
|
12
|
+
*/
|
|
13
|
+
export function renderToEmailHtml(
|
|
14
|
+
sections: Section[],
|
|
15
|
+
globalStyles: GlobalStyles = DEFAULT_GLOBAL_STYLES,
|
|
16
|
+
options: RenderOptions = {}
|
|
17
|
+
): string {
|
|
18
|
+
const { subject = '', preheaderText = '' } = options
|
|
19
|
+
const contentWidth = globalStyles.contentWidth || 600
|
|
20
|
+
const bgColor = globalStyles.backgroundColor || '#f3f4f6'
|
|
21
|
+
const fontFamily = globalStyles.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
22
|
+
|
|
23
|
+
const sectionsHtml = sections.map((section) => renderSection(section, globalStyles, contentWidth)).join('')
|
|
24
|
+
|
|
25
|
+
const preheader = preheaderText
|
|
26
|
+
? `<div style="display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">${preheaderText}</div>`
|
|
27
|
+
: ''
|
|
28
|
+
|
|
29
|
+
return `<!DOCTYPE html>
|
|
30
|
+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
34
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
35
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
36
|
+
<title>${subject}</title>
|
|
37
|
+
<!--[if mso]>
|
|
38
|
+
<noscript>
|
|
39
|
+
<xml>
|
|
40
|
+
<o:OfficeDocumentSettings>
|
|
41
|
+
<o:AllowPNG/>
|
|
42
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
43
|
+
</o:OfficeDocumentSettings>
|
|
44
|
+
</xml>
|
|
45
|
+
</noscript>
|
|
46
|
+
<![endif]-->
|
|
47
|
+
<style type="text/css">
|
|
48
|
+
body { margin:0; padding:0; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; }
|
|
49
|
+
table, td { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
|
|
50
|
+
img { -ms-interpolation-mode:bicubic; border:0; height:auto; line-height:100%; outline:none; text-decoration:none; }
|
|
51
|
+
a { color:inherit; }
|
|
52
|
+
@media only screen and (max-width:620px) {
|
|
53
|
+
.email-container { width:100% !important; max-width:100% !important; }
|
|
54
|
+
.stack-column { display:block !important; width:100% !important; max-width:100% !important; }
|
|
55
|
+
.stack-column-center { text-align:center !important; }
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body style="margin:0;padding:0;background-color:${bgColor};font-family:${fontFamily};-webkit-font-smoothing:antialiased;">
|
|
60
|
+
${preheader}
|
|
61
|
+
<!-- Email wrapper table -->
|
|
62
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color:${bgColor};">
|
|
63
|
+
<tr>
|
|
64
|
+
<td align="center" style="padding:24px 16px;">
|
|
65
|
+
<!-- Content container -->
|
|
66
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="${contentWidth}" class="email-container" style="max-width:${contentWidth}px;width:100%;">
|
|
67
|
+
${sectionsHtml}
|
|
68
|
+
</table>
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</table>
|
|
72
|
+
</body>
|
|
73
|
+
</html>`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderSection(section: Section, globalStyles: GlobalStyles, contentWidth: number): string {
|
|
77
|
+
const bgColor = section.backgroundColor || '#ffffff'
|
|
78
|
+
const pt = section.paddingTop ?? 16
|
|
79
|
+
const pb = section.paddingBottom ?? 16
|
|
80
|
+
const pl = section.paddingLeft ?? 0
|
|
81
|
+
const pr = section.paddingRight ?? 0
|
|
82
|
+
|
|
83
|
+
const rowsHtml = section.rows.map((row) => {
|
|
84
|
+
const widths = getColumnWidths(row.layout)
|
|
85
|
+
const colCount = widths.length
|
|
86
|
+
|
|
87
|
+
if (colCount === 1) {
|
|
88
|
+
// Single column: simple
|
|
89
|
+
const blocksHtml = row.columns[0]
|
|
90
|
+
?.map((block) => renderBlockToHtml(block, globalStyles))
|
|
91
|
+
.join('') || ''
|
|
92
|
+
return `<tr><td style="padding:0;">${blocksHtml}</td></tr>`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Multi-column: use nested table
|
|
96
|
+
const columnsHtml = widths
|
|
97
|
+
.map((widthPct, i) => {
|
|
98
|
+
const colBlocks = row.columns[i] || []
|
|
99
|
+
const blocksHtml = colBlocks
|
|
100
|
+
.map((block) => renderBlockToHtml(block, globalStyles))
|
|
101
|
+
.join('')
|
|
102
|
+
const pxWidth = Math.round((contentWidth - pl - pr) * (widthPct / 100))
|
|
103
|
+
return `<td class="stack-column" valign="top" width="${pxWidth}" style="width:${widthPct}%;padding:0 4px;vertical-align:top;">${blocksHtml}</td>`
|
|
104
|
+
})
|
|
105
|
+
.join('')
|
|
106
|
+
|
|
107
|
+
return `<tr><td style="padding:0;">
|
|
108
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"><tr>${columnsHtml}</tr></table>
|
|
109
|
+
</td></tr>`
|
|
110
|
+
}).join('')
|
|
111
|
+
|
|
112
|
+
return `<tr><td style="background-color:${bgColor};padding:${pt}px ${pr}px ${pb}px ${pl}px;border-radius:8px;">
|
|
113
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
|
114
|
+
${rowsHtml}
|
|
115
|
+
</table>
|
|
116
|
+
</td></tr>
|
|
117
|
+
<tr><td style="height:4px;font-size:1px;line-height:1px;"> </td></tr>`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Render sections to a simple HTML string (for preview iframe, not for sending).
|
|
122
|
+
*/
|
|
123
|
+
export function renderToPreviewHtml(
|
|
124
|
+
sections: Section[],
|
|
125
|
+
globalStyles: GlobalStyles = DEFAULT_GLOBAL_STYLES
|
|
126
|
+
): string {
|
|
127
|
+
return renderToEmailHtml(sections, globalStyles)
|
|
128
|
+
}
|