import { StyleOption, ElementStyle, FontConfig, ColorPalette } from '../types'; /** * Resolves a color reference (e.g., 'accent', 'background') or returns the color directly */ export function resolveColor(color: string | undefined, palette: ColorPalette): string { if (!color) return '#000000'; // Check if it's a palette reference if (palette[color as keyof ColorPalette]) { return '#' + palette[color as keyof ColorPalette]; } // Return as-is with # prefix if not already present return color.startsWith('#') ? color : '#' + color; } /** * Resolves a font reference (e.g., 'heading', 'body', 'code') to actual font name */ export function resolveFont(font: string | undefined, fonts: FontConfig): string { if (!font) return 'serif'; // Check if it's a font reference if (fonts[font as keyof FontConfig]) { return fonts[font as keyof FontConfig]!; } // Return as-is (already an actual font name) return font; } /** * Converts element style to CSS string */ function elementToCss( selector: string, style: ElementStyle | undefined, fonts: FontConfig, palette: ColorPalette ): string { if (!style) return ''; const css: string[] = []; if (style.font) css.push(`font-family: ${resolveFont(style.font, fonts)}`); if (style.size) css.push(`font-size: ${style.size}pt`); if (style.color) css.push(`color: ${resolveColor(style.color, palette)}`); if (style.background) css.push(`background: ${resolveColor(style.background, palette)}`); if (style.bold) css.push('font-weight: 700'); if (style.italic) css.push('font-style: italic'); if (style.underline) css.push('text-decoration: underline'); if (style.allCaps) css.push('text-transform: uppercase'); if (style.align) css.push(`text-align: ${style.align === 'both' ? 'justify' : style.align}`); if (style.spacing) { if (style.spacing.before) css.push(`margin-top: ${style.spacing.before}pt`); if (style.spacing.after) css.push(`margin-bottom: ${style.spacing.after}pt`); if (style.spacing.line) css.push(`line-height: ${style.spacing.line}`); } if (style.indent) css.push(`text-indent: ${style.indent}pt`); if (style.padding) css.push(`padding: ${style.padding}pt`); if (style.border) { css.push(`border: ${style.border.width}px ${style.border.style} ${resolveColor(style.border.color, palette)}`); } if (style.borderTop) { css.push(`border-top: ${style.borderTop.width}px ${style.borderTop.style} ${resolveColor(style.borderTop.color, palette)}`); } if (style.borderBottom) { css.push(`border-bottom: ${style.borderBottom.width}px ${style.borderBottom.style} ${resolveColor(style.borderBottom.color, palette)}`); } if (style.borderLeft) { css.push(`border-left: ${style.borderLeft.width}px ${style.borderLeft.style} ${resolveColor(style.borderLeft.color, palette)}`); } if (style.borderRight) { css.push(`border-right: ${style.borderRight.width}px ${style.borderRight.style} ${resolveColor(style.borderRight.color, palette)}`); } if (css.length === 0) return ''; return `${selector} { ${css.join('; ')} }`; } /** * Generates preview CSS from unified template structure * This is the SINGLE SOURCE OF TRUTH for both preview and export styling * All selectors are scoped to .page to avoid affecting the app background */ export function generatePreviewCss(template: StyleOption): string { const fonts = template.typography?.fonts || { heading: 'serif', body: 'serif', code: 'monospace' }; const palette = template.typography?.colors || { text: '000000', background: 'FFFFFF' }; const elements = template.elements || {}; const cssParts: string[] = []; // .page base styles - scoped to avoid affecting app background const baseStyles: string[] = []; baseStyles.push(`font-family: ${resolveFont('body', fonts)}`); baseStyles.push(`background: ${resolveColor('background', palette)}`); baseStyles.push(`color: ${resolveColor('text', palette)}`); baseStyles.push(`line-height: 1.5`); cssParts.push(`.page { ${baseStyles.join('; ')}; min-height: 100%; }`); cssParts.push(`.page blockquote, .page table, .page img { break-inside: avoid; }`); // Headings - scoped to .page cssParts.push(elementToCss('.page h1', elements.h1, fonts, palette)); cssParts.push(elementToCss('.page h2', elements.h2, fonts, palette)); cssParts.push(elementToCss('.page h3', elements.h3, fonts, palette)); cssParts.push(elementToCss('.page h4', elements.h4, fonts, palette)); cssParts.push(elementToCss('.page h5', elements.h5, fonts, palette)); cssParts.push(elementToCss('.page h6', elements.h6, fonts, palette)); // Paragraph - scoped to .page cssParts.push(elementToCss('.page p', elements.p, fonts, palette)); // Blockquote - scoped to .page cssParts.push(elementToCss('.page blockquote', elements.blockquote, fonts, palette)); // Code - scoped to .page cssParts.push(elementToCss('.page code', elements.code, fonts, palette)); cssParts.push(elementToCss('.page pre', elements.pre, fonts, palette)); cssParts.push('.page pre code { background: transparent !important; padding: 0 !important }'); // Lists - scoped to .page cssParts.push(elementToCss('.page ul', elements.ul, fonts, palette)); cssParts.push(elementToCss('.page ol', elements.ol, fonts, palette)); cssParts.push(elementToCss('.page li', elements.li, fonts, palette)); // Inline styles - scoped to .page cssParts.push(elementToCss('.page strong', elements.strong, fonts, palette)); cssParts.push(elementToCss('.page em', elements.em, fonts, palette)); cssParts.push(elementToCss('.page a', elements.a, fonts, palette)); // Tables - scoped to .page cssParts.push('.page table { border-collapse: collapse; border-spacing: 0; width: 100%; }'); cssParts.push(elementToCss('.page table', elements.table, fonts, palette)); cssParts.push(elementToCss('.page th', elements.th, fonts, palette)); cssParts.push(elementToCss('.page td', elements.td, fonts, palette)); // Horizontal rule - scoped to .page if (elements.hr?.border) { const borderColor = resolveColor(elements.hr.border.color, palette); cssParts.push(`.page hr { border: none; border-top: ${elements.hr.border.width}px ${elements.hr.border.style} ${borderColor}; margin: ${elements.hr.spacing?.before || 12}pt 0 }`); } // Images - scoped to .page if (elements.img?.align) { cssParts.push(`.page img { display: block; text-align: ${elements.img.align}; margin: ${elements.img.spacing?.before || 12}pt auto }`); } // Remove empty rules and join return cssParts.filter(part => part.trim()).join('\n'); } /** * Generates CSS from legacy wordConfig structure * Used when unified elements/typography structure isn't available * All selectors are scoped to .page to avoid affecting the app background */ function generateCssFromWordConfig(template: StyleOption): string { const cfg = template.wordConfig; if (!cfg) return '.page { font-family: serif; }'; const cssParts: string[] = []; // .page base styles - scoped to avoid affecting app background const pageStyles: string[] = []; if (cfg.body?.font) pageStyles.push(`font-family: ${cfg.body.font}`); if (cfg.body?.size) pageStyles.push(`font-size: ${cfg.body.size}pt`); if (cfg.body?.color) pageStyles.push(`color: #${cfg.body.color}`); if (cfg.body?.align) pageStyles.push(`text-align: ${cfg.body.align === 'both' ? 'justify' : cfg.body.align}`); if (cfg.body?.spacing?.line) { pageStyles.push(`line-height: ${cfg.body.spacing.line / 240}`); } // White background for the document page pageStyles.push('background: #ffffff'); cssParts.push(`.page { ${pageStyles.join('; ')} }`); // Headings - scoped to .page if (cfg.heading1) { const h1Styles: string[] = []; if (cfg.heading1.font) h1Styles.push(`font-family: ${cfg.heading1.font}`); if (cfg.heading1.size) h1Styles.push(`font-size: ${cfg.heading1.size}pt`); if (cfg.heading1.color) h1Styles.push(`color: #${cfg.heading1.color}`); if (cfg.heading1.bold) h1Styles.push('font-weight: 700'); if (cfg.heading1.italic) h1Styles.push('font-style: italic'); if (cfg.heading1.allCaps) h1Styles.push('text-transform: uppercase'); if (cfg.heading1.align) h1Styles.push(`text-align: ${cfg.heading1.align}`); if (cfg.heading1.spacing?.before) { h1Styles.push(`margin-top: ${cfg.heading1.spacing.before / 20}pt`); } if (cfg.heading1.spacing?.after) { h1Styles.push(`margin-bottom: ${cfg.heading1.spacing.after / 20}pt`); } cssParts.push(`.page h1 { ${h1Styles.join('; ')} }`); } if (cfg.heading2) { const h2Styles: string[] = []; if (cfg.heading2.font) h2Styles.push(`font-family: ${cfg.heading2.font}`); if (cfg.heading2.size) h2Styles.push(`font-size: ${cfg.heading2.size}pt`); if (cfg.heading2.color) h2Styles.push(`color: #${cfg.heading2.color}`); if (cfg.heading2.bold) h2Styles.push('font-weight: 700'); if (cfg.heading2.italic) h2Styles.push('font-style: italic'); if (cfg.heading2.allCaps) h2Styles.push('text-transform: uppercase'); if (cfg.heading2.align) h2Styles.push(`text-align: ${cfg.heading2.align}`); if (cfg.heading2.spacing?.before) { h2Styles.push(`margin-top: ${cfg.heading2.spacing.before / 20}pt`); } if (cfg.heading2.spacing?.after) { h2Styles.push(`margin-bottom: ${cfg.heading2.spacing.after / 20}pt`); } cssParts.push(`.page h2 { ${h2Styles.join('; ')} }`); } // Paragraph - scoped to .page if (cfg.body) { const pStyles: string[] = []; if (cfg.body.font) pStyles.push(`font-family: ${cfg.body.font}`); if (cfg.body.size) pStyles.push(`font-size: ${cfg.body.size}pt`); if (cfg.body.color) pStyles.push(`color: #${cfg.body.color}`); if (cfg.body.align) pStyles.push(`text-align: ${cfg.body.align === 'both' ? 'justify' : cfg.body.align}`); if (cfg.body.spacing?.line) { pStyles.push(`line-height: ${cfg.body.spacing.line / 240}`); } if (cfg.body.spacing?.after) { pStyles.push(`margin-bottom: ${cfg.body.spacing.after / 20}pt`); } cssParts.push(`.page p { ${pStyles.join('; ')} }`); } // All other elements scoped to .page cssParts.push('.page blockquote { border-left: 3px solid #' + (cfg.accentColor || 'cccccc') + '; padding-left: 16px; margin: 16px 0; font-style: italic; }'); cssParts.push('.page ul, .page ol { margin: 16px 0; padding-left: 32px; }'); cssParts.push('.page li { margin-bottom: 8px; }'); cssParts.push('.page strong { font-weight: 700; }'); cssParts.push('.page em { font-style: italic; }'); cssParts.push('.page code { font-family: monospace; background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }'); cssParts.push('.page pre { background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; }'); cssParts.push('.page table { border-collapse: collapse; width: 100%; margin: 16px 0; }'); cssParts.push('.page th, .page td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }'); cssParts.push('.page th { background: #f5f5f5; font-weight: 700; }'); cssParts.push('.page hr { border: none; border-top: 1px solid #' + (cfg.accentColor || 'cccccc') + '; margin: 24px 0; }'); return cssParts.join('\n'); } /** * Gets complete CSS for preview * Always generates fresh CSS from unified template structure */ export function getPreviewCss(template: StyleOption): string { // Always generate CSS from unified structure if available if (template.elements && template.typography) { return generatePreviewCss(template); } // Generate CSS from legacy wordConfig structure if (template.wordConfig) { return generateCssFromWordConfig(template); } // Final fallback return 'body { font-family: serif; color: #000000; background: #ffffff; }'; } /** * Generates Word-compatible wordConfig from unified template structure */ // Export this function to be used by templateLoader export function generateWordConfig( elements: Record, fonts: FontConfig, palette: ColorPalette ) { const h1 = elements.h1; const h2 = elements.h2; const body = elements.p; const toTwips = (pts?: number) => pts; const toLineTwips = (line?: number) => line; // Helper to ensure borders are preserved in the config const mapBorder = (el?: ElementStyle, side?: 'top' | 'bottom' | 'left' | 'right') => { // If specific side border exists let sideBorder; if (side === 'left') sideBorder = el?.borderLeft; else if (side === 'top') sideBorder = el?.borderTop; else if (side === 'bottom') sideBorder = el?.borderBottom; else if (side === 'right') sideBorder = el?.borderRight; if (sideBorder) { return { color: resolveColor(sideBorder.color, palette).replace('#', ''), space: 24, // Standard space style: sideBorder.style || 'single', size: (sideBorder.width || 1) * 8 }; } // If global border exists if (el?.border) { return { color: resolveColor(el.border.color, palette).replace('#', ''), space: 24, // Standard space style: el.border.style || 'single', size: (el.border.width || 1) * 8 }; } return undefined; }; return { heading1: h1 ? { font: resolveFont(h1.font, fonts), size: h1.size || 24, color: h1.color ? resolveColor(h1.color, palette).replace('#', '') : '000000', bold: h1.bold, italic: h1.italic, allCaps: h1.allCaps, align: h1.align, spacing: h1.spacing ? { before: toTwips(h1.spacing.before) || 0, after: toTwips(h1.spacing.after) || 0, line: toLineTwips(h1.spacing.line) || 240 } : undefined, // Map borders specifically for H1 if they exist border: { left: mapBorder(h1, 'left'), bottom: mapBorder(h1, 'bottom'), top: mapBorder(h1, 'top'), right: mapBorder(h1, 'right') }, shading: h1.background ? { fill: resolveColor(h1.background, palette).replace('#', ''), type: 'clear', // Always use clear for shading to avoid black blocks color: 'auto' } : undefined } : undefined, heading2: h2 ? { font: resolveFont(h2.font, fonts), size: h2.size || 18, color: h2.color ? resolveColor(h2.color, palette).replace('#', '') : '333333', bold: h2.bold, italic: h2.italic, allCaps: h2.allCaps, align: h2.align, spacing: h2.spacing ? { before: toTwips(h2.spacing.before) || 0, after: toTwips(h2.spacing.after) || 0, line: toLineTwips(h2.spacing.line) || 240 } : undefined, shading: h2.background ? { fill: resolveColor(h2.background, palette).replace('#', ''), type: 'clear', color: 'auto' } : undefined } : undefined, body: body ? { font: resolveFont(body.font, fonts), size: body.size || 11, color: body.color ? resolveColor(body.color, palette).replace('#', '') : '000000', align: body.align, spacing: body.spacing ? { before: toTwips(body.spacing.before) || 0, after: toTwips(body.spacing.after) || 0, line: toLineTwips(body.spacing.line) || 1.2 } : undefined } : undefined, accentColor: palette.accent || '000000', heading3: elements.h3 ? { font: resolveFont(elements.h3.font, fonts), size: elements.h3.size || 14, color: elements.h3.color ? resolveColor(elements.h3.color, palette).replace('#', '') : '111111', bold: elements.h3.bold, italic: elements.h3.italic, allCaps: elements.h3.allCaps, align: elements.h3.align, spacing: elements.h3.spacing ? { before: toTwips(elements.h3.spacing.before) || 0, after: toTwips(elements.h3.spacing.after) || 0, line: toLineTwips(elements.h3.spacing.line) || 240 } : undefined } : undefined, heading4: elements.h4 ? { font: resolveFont(elements.h4.font, fonts), size: elements.h4.size || 12, color: elements.h4.color ? resolveColor(elements.h4.color, palette).replace('#', '') : '111111', bold: elements.h4.bold, italic: elements.h4.italic, align: elements.h4.align, spacing: elements.h4.spacing ? { before: toTwips(elements.h4.spacing.before) || 0, after: toTwips(elements.h4.spacing.after) || 0, line: toLineTwips(elements.h4.spacing.line) || 240 } : undefined } : undefined, heading5: elements.h5 ? { font: resolveFont(elements.h5.font, fonts), size: elements.h5.size || 11, color: elements.h5.color ? resolveColor(elements.h5.color, palette).replace('#', '') : '111111', bold: elements.h5.bold, italic: elements.h5.italic, align: elements.h5.align, spacing: elements.h5.spacing ? { before: toTwips(elements.h5.spacing.before) || 0, after: toTwips(elements.h5.spacing.after) || 0, line: toLineTwips(elements.h5.spacing.line) || 240 } : undefined } : undefined, heading6: elements.h6 ? { font: resolveFont(elements.h6.font, fonts), size: elements.h6.size || 10, color: elements.h6.color ? resolveColor(elements.h6.color, palette).replace('#', '') : '111111', bold: elements.h6.bold, italic: elements.h6.italic, align: elements.h6.align, spacing: elements.h6.spacing ? { before: toTwips(elements.h6.spacing.before) || 0, after: toTwips(elements.h6.spacing.after) || 0, line: toLineTwips(elements.h6.spacing.line) || 240 } : undefined } : undefined }; } /** * Gets complete wordConfig, preferring generated config over legacy */ export function getWordConfig(template: StyleOption) { // If template has unified structure, generate config from it if (template.elements && template.typography) { const fonts = template.typography.fonts || {}; const palette = template.typography.colors || {}; const elements = template.elements; return generateWordConfig(elements, fonts, palette); } // Fall back to legacy wordConfig return template.wordConfig; }