// Email editor type definitions // Extends the original block types with sections, rows, and enhanced block properties export type BlockType = | 'text' | 'metrics' | 'divider' | 'cta' | 'image' | 'spacer' | 'social' | 'header' | 'footer' export type ColumnLayout = '1' | '2' | '3' | '2-1' | '1-2' export interface BlockStyle { backgroundColor?: string paddingTop?: number paddingBottom?: number paddingLeft?: number paddingRight?: number marginTop?: number marginBottom?: number } export interface BaseBlock { id: string type: BlockType style?: BlockStyle } // --- Text Block --- export interface TextBlock extends BaseBlock { type: 'text' content: string // HTML content from Tiptap fontSize?: number fontFamily?: string lineHeight?: number textColor?: string } // --- Metrics Block (kept from existing) --- export interface MetricItem { id: string label: string value: string change?: string changeType?: 'positive' | 'negative' | 'neutral' } export interface MetricsBlock extends BaseBlock { type: 'metrics' title?: string metrics: MetricItem[] columns: 2 | 3 | 4 } // --- Divider Block --- export interface DividerBlock extends BaseBlock { type: 'divider' dividerStyle: 'solid' | 'dashed' | 'dotted' | 'space' color?: string thickness?: number width?: number // percentage 0-100 } // --- CTA / Button Block --- export interface CTABlock extends BaseBlock { type: 'cta' text: string url: string buttonColor?: string textColor?: string borderRadius?: number paddingH?: number paddingV?: number alignment: 'left' | 'center' | 'right' } // --- Image Block --- export interface ImageBlock extends BaseBlock { type: 'image' url: string alt: string caption?: string alignment: 'left' | 'center' | 'right' width: number // percentage 0-100 linkUrl?: string } // --- Spacer Block --- export interface SpacerBlock extends BaseBlock { type: 'spacer' height: number // px } // --- Social Block --- export interface SocialLink { id: string platform: 'linkedin' | 'twitter' | 'facebook' | 'instagram' | 'youtube' | 'github' | 'website' url: string } export interface SocialBlock extends BaseBlock { type: 'social' links: SocialLink[] iconSize?: number alignment: 'left' | 'center' | 'right' } // --- Header Block --- export interface HeaderBlock extends BaseBlock { type: 'header' logoUrl?: string companyName: string alignment: 'left' | 'center' | 'right' } // --- Footer Block --- export interface FooterBlock extends BaseBlock { type: 'footer' companyName: string address?: string showUnsubscribe: boolean unsubscribeUrl?: string alignment: 'left' | 'center' | 'right' } export type Block = | TextBlock | MetricsBlock | DividerBlock | CTABlock | ImageBlock | SpacerBlock | SocialBlock | HeaderBlock | FooterBlock // --- Row / Section --- export interface Row { id: string layout: ColumnLayout columns: Block[][] // array of columns, each column is an array of blocks } export interface Section { id: string rows: Row[] backgroundColor?: string paddingTop?: number paddingBottom?: number paddingLeft?: number paddingRight?: number } // --- Global Styles --- export interface GlobalStyles { backgroundColor: string contentWidth: number fontFamily: string theme: 'clean' | 'minimal' | 'bold' } // --- Editor State --- export interface EditorSelection { sectionIndex: number rowIndex: number columnIndex: number blockIndex: number } // --- Merge Field Definition (re-export compatible) --- export interface MergeFieldDefinition { key: string label: string example: string category: string } // --- Helper Functions --- let _idCounter = 0 function generateId(prefix: string): string { _idCounter++ return `${prefix}-${Date.now()}-${_idCounter}-${Math.random().toString(36).substr(2, 6)}` } export function createBlock(type: BlockType): Block { const id = generateId('block') switch (type) { case 'text': return { id, type: 'text', content: '' } case 'metrics': return { id, type: 'metrics', title: 'Key Metrics', metrics: [ { id: generateId('metric'), label: 'Revenue', value: '$0', change: '+0%', changeType: 'neutral' }, { id: generateId('metric'), label: 'Users', value: '0', change: '+0%', changeType: 'neutral' }, ], columns: 2, } case 'divider': return { id, type: 'divider', dividerStyle: 'solid', thickness: 1, color: '#d1d5db', width: 100 } case 'cta': return { id, type: 'cta', text: 'Learn More', url: '', buttonColor: '#2563eb', textColor: '#ffffff', borderRadius: 6, paddingH: 24, paddingV: 12, alignment: 'center', } case 'image': return { id, type: 'image', url: '', alt: '', caption: '', alignment: 'center', width: 100, } case 'spacer': return { id, type: 'spacer', height: 32 } case 'social': return { id, type: 'social', links: [ { id: generateId('social'), platform: 'linkedin', url: '' }, { id: generateId('social'), platform: 'twitter', url: '' }, ], iconSize: 24, alignment: 'center', } case 'header': return { id, type: 'header', logoUrl: '', companyName: 'Your Company', alignment: 'center', } case 'footer': return { id, type: 'footer', companyName: 'Your Company', address: '', showUnsubscribe: true, alignment: 'center', } } } export function createRow(layout: ColumnLayout = '1'): Row { const colCount = getColumnCount(layout) return { id: generateId('row'), layout, columns: Array.from({ length: colCount }, () => []), } } export function createSection(): Section { return { id: generateId('section'), rows: [createRow('1')], paddingTop: 16, paddingBottom: 16, paddingLeft: 0, paddingRight: 0, } } export function getColumnCount(layout: ColumnLayout): number { switch (layout) { case '1': return 1 case '2': case '2-1': case '1-2': return 2 case '3': return 3 } } export function getColumnWidths(layout: ColumnLayout): number[] { switch (layout) { case '1': return [100] case '2': return [50, 50] case '3': return [33.33, 33.33, 33.34] case '2-1': return [66.67, 33.33] case '1-2': return [33.33, 66.67] } } // --- Serialization (backward compatible) --- // Convert flat Block[] (old format) to Section[] (new format) export function migrateFromLegacy(legacyBlocks: Block[]): Section[] { if (legacyBlocks.length === 0) return [createSection()] const section = createSection() // Each old block becomes a single block in a 1-column row section.rows = legacyBlocks.map((block) => { const newRow = createRow('1') // Migrate old divider 'style' field to 'dividerStyle' if (block.type === 'divider') { const oldBlock = block as unknown as { style: string } const divBlock = block as unknown as DividerBlock if (!divBlock.dividerStyle && oldBlock.style) { divBlock.dividerStyle = oldBlock.style as DividerBlock['dividerStyle'] } } // Migrate old CTA to new format if (block.type === 'cta') { const oldBlock = block as unknown as { style?: string } const ctaBlock = block as unknown as CTABlock if (!ctaBlock.buttonColor && oldBlock.style) { const colorMap: Record = { primary: '#2563eb', secondary: '#f3f4f6', outline: '#ffffff', } ctaBlock.buttonColor = colorMap[oldBlock.style] || '#2563eb' ctaBlock.textColor = oldBlock.style === 'primary' ? '#ffffff' : '#1f2937' } } // Migrate old image width if (block.type === 'image') { const oldBlock = block as unknown as { width?: string | number } const imgBlock = block as unknown as ImageBlock if (typeof oldBlock.width === 'string') { const widthMap: Record = { small: 50, medium: 75, large: 90, full: 100 } imgBlock.width = widthMap[oldBlock.width] || 100 } } newRow.columns[0] = [block] return newRow }) return [section] } // Convert Section[] back to flat Block[] for backward compatibility export function flattenToLegacy(sections: Section[]): Block[] { const blocks: Block[] = [] for (const section of sections) { for (const row of section.rows) { for (const column of row.columns) { blocks.push(...column) } } } return blocks } export function serializeSections(sections: Section[]): string { return JSON.stringify(sections) } export function deserializeSections(json: string | null): Section[] { if (!json) return [createSection()] try { const parsed = JSON.parse(json) // Detect legacy flat block array format if (Array.isArray(parsed) && parsed.length > 0 && 'type' in parsed[0] && !('rows' in parsed[0])) { return migrateFromLegacy(parsed as Block[]) } return parsed as Section[] } catch { return [createSection()] } } // Also keep backward-compatible serialize/deserialize that work with flat blocks export function serializeBlocks(blocks: Block[]): string { return JSON.stringify(blocks) } export function deserializeBlocks(json: string | null): Block[] { if (!json) return [] try { return JSON.parse(json) } catch { return [] } } export const DEFAULT_GLOBAL_STYLES: GlobalStyles = { backgroundColor: '#f3f4f6', contentWidth: 600, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', theme: 'clean', }