@zyther/tzmail 0.1.4 → 0.5.0

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.
Files changed (132) hide show
  1. package/README.md +411 -78
  2. package/dist/core/enums/theme.enum.d.ts +11 -1
  3. package/dist/core/enums/theme.enum.d.ts.map +1 -1
  4. package/dist/core/enums/theme.enum.js +10 -0
  5. package/dist/core/interfaces/email.interface.d.ts +95 -5
  6. package/dist/core/interfaces/email.interface.d.ts.map +1 -1
  7. package/dist/core/interfaces/template.interface.d.ts +12 -1
  8. package/dist/core/interfaces/template.interface.d.ts.map +1 -1
  9. package/dist/factories/email-factory.d.ts +32 -6
  10. package/dist/factories/email-factory.d.ts.map +1 -1
  11. package/dist/factories/email-factory.js +110 -6
  12. package/dist/factories/template-factory.d.ts.map +1 -1
  13. package/dist/factories/template-factory.js +170 -60
  14. package/dist/index.d.ts +16 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +23 -1
  17. package/dist/providers/mailgun.transport.d.ts +10 -0
  18. package/dist/providers/mailgun.transport.d.ts.map +1 -0
  19. package/dist/providers/mailgun.transport.js +85 -0
  20. package/dist/providers/nodemailer.transport.d.ts +11 -0
  21. package/dist/providers/nodemailer.transport.d.ts.map +1 -0
  22. package/dist/providers/nodemailer.transport.js +23 -0
  23. package/dist/providers/provider-utils.d.ts +17 -0
  24. package/dist/providers/provider-utils.d.ts.map +1 -0
  25. package/dist/providers/provider-utils.js +89 -0
  26. package/dist/providers/provider.interface.d.ts +30 -0
  27. package/dist/providers/provider.interface.d.ts.map +1 -0
  28. package/dist/providers/provider.interface.js +2 -0
  29. package/dist/providers/sendgrid.transport.d.ts +10 -0
  30. package/dist/providers/sendgrid.transport.d.ts.map +1 -0
  31. package/dist/providers/sendgrid.transport.js +76 -0
  32. package/dist/queues/bullmq-email-queue.d.ts +153 -0
  33. package/dist/queues/bullmq-email-queue.d.ts.map +1 -0
  34. package/dist/queues/bullmq-email-queue.js +292 -0
  35. package/dist/services/attachment.service.d.ts +15 -37
  36. package/dist/services/attachment.service.d.ts.map +1 -1
  37. package/dist/services/attachment.service.js +58 -46
  38. package/dist/services/email.service.d.ts +22 -22
  39. package/dist/services/email.service.d.ts.map +1 -1
  40. package/dist/services/email.service.js +117 -33
  41. package/dist/services/template.service.d.ts +24 -111
  42. package/dist/services/template.service.d.ts.map +1 -1
  43. package/dist/services/template.service.js +273 -362
  44. package/dist/templates/base/color-accessibility.d.ts +4 -0
  45. package/dist/templates/base/color-accessibility.d.ts.map +1 -0
  46. package/dist/templates/base/color-accessibility.js +38 -0
  47. package/dist/templates/base/template-builder.d.ts +4 -1
  48. package/dist/templates/base/template-builder.d.ts.map +1 -1
  49. package/dist/templates/base/template-builder.js +276 -527
  50. package/dist/templates/themes/aurora.theme.d.ts +12 -0
  51. package/dist/templates/themes/aurora.theme.d.ts.map +1 -0
  52. package/dist/templates/themes/aurora.theme.js +88 -0
  53. package/dist/templates/themes/corporate.theme.d.ts +2 -1
  54. package/dist/templates/themes/corporate.theme.d.ts.map +1 -1
  55. package/dist/templates/themes/corporate.theme.js +139 -102
  56. package/dist/templates/themes/editorial.theme.d.ts +12 -0
  57. package/dist/templates/themes/editorial.theme.d.ts.map +1 -0
  58. package/dist/templates/themes/editorial.theme.js +90 -0
  59. package/dist/templates/themes/forest.theme.d.ts +12 -0
  60. package/dist/templates/themes/forest.theme.d.ts.map +1 -0
  61. package/dist/templates/themes/forest.theme.js +89 -0
  62. package/dist/templates/themes/glass.theme.d.ts +12 -0
  63. package/dist/templates/themes/glass.theme.d.ts.map +1 -0
  64. package/dist/templates/themes/glass.theme.js +88 -0
  65. package/dist/templates/themes/lavender.theme.d.ts +12 -0
  66. package/dist/templates/themes/lavender.theme.d.ts.map +1 -0
  67. package/dist/templates/themes/lavender.theme.js +91 -0
  68. package/dist/templates/themes/midnight.theme.d.ts +12 -0
  69. package/dist/templates/themes/midnight.theme.d.ts.map +1 -0
  70. package/dist/templates/themes/midnight.theme.js +91 -0
  71. package/dist/templates/themes/minimal.theme.d.ts +2 -1
  72. package/dist/templates/themes/minimal.theme.d.ts.map +1 -1
  73. package/dist/templates/themes/minimal.theme.js +142 -107
  74. package/dist/templates/themes/modern.theme.d.ts +2 -1
  75. package/dist/templates/themes/modern.theme.d.ts.map +1 -1
  76. package/dist/templates/themes/modern.theme.js +109 -74
  77. package/dist/templates/themes/monokai.theme.d.ts +2 -1
  78. package/dist/templates/themes/monokai.theme.d.ts.map +1 -1
  79. package/dist/templates/themes/monokai.theme.js +102 -66
  80. package/dist/templates/themes/neobrutal.theme.d.ts +12 -0
  81. package/dist/templates/themes/neobrutal.theme.d.ts.map +1 -0
  82. package/dist/templates/themes/neobrutal.theme.js +91 -0
  83. package/dist/templates/themes/ocean.theme.d.ts +12 -0
  84. package/dist/templates/themes/ocean.theme.d.ts.map +1 -0
  85. package/dist/templates/themes/ocean.theme.js +90 -0
  86. package/dist/templates/themes/pastel.theme.d.ts +12 -0
  87. package/dist/templates/themes/pastel.theme.d.ts.map +1 -0
  88. package/dist/templates/themes/pastel.theme.js +89 -0
  89. package/dist/templates/themes/sunset.theme.d.ts +12 -0
  90. package/dist/templates/themes/sunset.theme.d.ts.map +1 -0
  91. package/dist/templates/themes/sunset.theme.js +90 -0
  92. package/dist/templates/themes/system.theme.d.ts +2 -1
  93. package/dist/templates/themes/system.theme.d.ts.map +1 -1
  94. package/dist/templates/themes/system.theme.js +81 -47
  95. package/dist/templates/themes/theme.interface.d.ts +48 -0
  96. package/dist/templates/themes/theme.interface.d.ts.map +1 -1
  97. package/package.json +36 -14
  98. package/src/core/enums/template-part.enum.ts +7 -0
  99. package/src/core/enums/theme.enum.ts +18 -0
  100. package/src/core/interfaces/email.interface.ts +116 -0
  101. package/src/core/interfaces/template.interface.ts +71 -0
  102. package/src/factories/email-factory.ts +208 -0
  103. package/src/factories/template-factory.ts +201 -0
  104. package/src/index.ts +53 -0
  105. package/src/providers/mailgun.transport.ts +80 -0
  106. package/src/providers/nodemailer.transport.ts +20 -0
  107. package/src/providers/provider-utils.ts +91 -0
  108. package/src/providers/provider.interface.ts +26 -0
  109. package/src/providers/sendgrid.transport.ts +80 -0
  110. package/src/queues/bullmq-email-queue.ts +418 -0
  111. package/src/services/attachment.service.ts +98 -0
  112. package/src/services/email.service.ts +151 -0
  113. package/src/services/template.service.ts +431 -0
  114. package/src/templates/base/color-accessibility.ts +31 -0
  115. package/src/templates/base/template-builder.ts +438 -0
  116. package/src/templates/themes/aurora.theme.ts +91 -0
  117. package/src/templates/themes/corporate.theme.ts +156 -0
  118. package/src/templates/themes/editorial.theme.ts +93 -0
  119. package/src/templates/themes/forest.theme.ts +92 -0
  120. package/src/templates/themes/glass.theme.ts +91 -0
  121. package/src/templates/themes/lavender.theme.ts +94 -0
  122. package/src/templates/themes/midnight.theme.ts +94 -0
  123. package/src/templates/themes/minimal.theme.ts +158 -0
  124. package/src/templates/themes/modern.theme.ts +124 -0
  125. package/src/templates/themes/monokai.theme.ts +117 -0
  126. package/src/templates/themes/neobrutal.theme.ts +94 -0
  127. package/src/templates/themes/ocean.theme.ts +93 -0
  128. package/src/templates/themes/pastel.theme.ts +92 -0
  129. package/src/templates/themes/sunset.theme.ts +93 -0
  130. package/src/templates/themes/system.theme.ts +92 -0
  131. package/src/templates/themes/theme.interface.ts +95 -0
  132. package/src/tests/template.service.test.ts +1 -0
@@ -1,86 +1,88 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TemplateBuilder = void 0;
4
+ const color_accessibility_1 = require("./color-accessibility");
5
+ function escapeHtml(value) {
6
+ return String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character]));
7
+ }
8
+ function safeHref(value, allowMailto = false) {
9
+ if (typeof value !== 'string' || !value.trim() || /[\u0000-\u0020"'<>\\]/.test(value))
10
+ throw new Error('Unsafe URL in template');
11
+ let url;
12
+ try {
13
+ url = new URL(value);
14
+ }
15
+ catch {
16
+ throw new Error('Template URLs must be absolute');
17
+ }
18
+ const schemes = allowMailto ? ['http:', 'https:', 'mailto:'] : ['http:', 'https:'];
19
+ if (!schemes.includes(url.protocol.toLowerCase()))
20
+ throw new Error('Unsafe URL scheme in template');
21
+ return escapeHtml(value);
22
+ }
23
+ const DEFAULT_EMAIL_STYLES = {
24
+ maxWidthPx: 600,
25
+ container: { borderColor: 'border', borderWidth: '1px', borderRadius: '8px', boxShadow: 'none' },
26
+ header: { accentColor: 'border', accentWidth: '1px', horizontalPadding: '24px', logoColor: 'text', imageBorderRadius: '0px' },
27
+ body: { accentColor: 'none', accentWidth: '0px', borderRadius: '0px', boxShadow: 'none', horizontalPadding: '24px', lineHeight: 1.6 },
28
+ button: { appearance: 'solid', borderRadius: '6px', borderWidth: '0px', boxShadow: 'none' },
29
+ footer: { accentColor: 'border', accentWidth: '1px' }
30
+ };
31
+ function colorFor(token, colors) {
32
+ return token === 'none' ? undefined : colors[token];
33
+ }
34
+ function borderFor(token, width, colors) {
35
+ const color = colorFor(token, colors);
36
+ return !color || !width || Number.parseFloat(width) <= 0 ? 'none' : `${width} solid ${color}`;
37
+ }
4
38
  class TemplateBuilder {
39
+ template = '';
40
+ theme;
41
+ config;
42
+ variant;
5
43
  constructor(theme, config, variant) {
6
- this.template = '';
7
44
  this.theme = theme;
8
45
  this.config = config;
9
46
  this.variant = variant;
10
47
  }
48
+ get emailStyles() {
49
+ return this.theme.emailStyles ?? DEFAULT_EMAIL_STYLES;
50
+ }
51
+ buildPreheader() {
52
+ const preheader = this.config.preheader;
53
+ if (!preheader)
54
+ return this;
55
+ this.template += `<div aria-hidden="true" style="display:none!important;visibility:hidden;opacity:0;color:transparent;height:0;width:0;max-height:0;max-width:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;">${escapeHtml(preheader)}</div>`;
56
+ return this;
57
+ }
11
58
  buildHeader(content) {
12
- var _a;
13
- if (!((_a = this.config.header) === null || _a === void 0 ? void 0 : _a.show))
59
+ if (!this.config.header?.show)
14
60
  return this;
15
61
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
16
- const isMonokai = this.theme.id === 'monokai';
17
- const isModern = this.theme.id === 'modern';
18
- const isCorporate = this.theme.id === 'corporate';
19
- const isMinimal = this.theme.id === 'minimal';
20
- let headerStyles = `
21
- background-color: ${colors.background};
22
- padding: ${this.theme.spacing.lg} ${this.theme.spacing.xl};
62
+ const styles = this.emailStyles.header;
63
+ const headerBorder = borderFor(styles.accentColor, styles.accentWidth, colors);
64
+ const headerBackground = this.config.header?.backgroundColor || colors.background;
65
+ const headerTextColor = (0, color_accessibility_1.readableTextColor)(this.config.header?.textColor || colors.text, headerBackground, colors.text);
66
+ const headerStyles = `
67
+ background-color: ${headerBackground};
68
+ color: ${headerTextColor};
69
+ padding: ${this.theme.spacing.lg} ${styles.horizontalPadding};
70
+ border-bottom: ${headerBorder};
71
+ font-family: ${this.theme.typography.fontFamily};
23
72
  `;
24
- // styles específicos para tema Corporate
25
- if (isCorporate) {
26
- const corporate = this.theme;
27
- headerStyles += `
28
- border-bottom: ${corporate.borderStyle.width.medium} solid ${corporate.corporateColors.gold};
29
- box-shadow: ${corporate.elevation.shadow};
30
- text-transform: ${corporate.branding.textTransform.uppercase};
31
- letter-spacing: ${corporate.branding.letterSpacing.wide};
32
- `;
33
- }
34
- // styles específicos para tema Minimal
35
- if (isMinimal) {
36
- const minimal = this.theme;
37
- headerStyles += `
38
- border-bottom: ${minimal.borderStyle.width.thin} solid ${colors.border};
39
- padding: ${minimal.spacing.lg} ${minimal.spacing.xl};
40
- `;
41
- }
42
- // styles existentes para outros temas
43
- else if (isModern) {
44
- headerStyles += `
45
- border-bottom: 1px solid ${colors.border};
46
- backdrop-filter: blur(${this.theme.glassmorphism.blur});
47
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
48
- `;
49
- }
50
- else if (isMonokai) {
51
- headerStyles += `
52
- border-bottom: 2px solid ${colors.primary};
53
- box-shadow: ${this.theme.effects.shadow};
54
- transition: ${this.theme.effects.transition};
55
- `;
56
- }
57
- else {
58
- headerStyles += `
59
- border-bottom: 1px solid ${colors.border};
60
- `;
61
- }
62
73
  const logoHtml = this.buildLogo();
63
74
  this.template += `
64
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
75
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation" bgcolor="${headerBackground}" style="width:100%;background-color:${headerBackground};">
65
76
  <tr>
66
- <td style="${headerStyles}" align="center">
67
-
77
+ <td class="email-header-cell" style="${escapeHtml(headerStyles)}" align="center">
68
78
  ${logoHtml}
69
-
70
79
  ${content ? `
71
80
  <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
72
- <tr>
73
- <td style="
74
- padding-top: ${this.theme.spacing.md};
75
- ${isCorporate ? 'font-family: serif;' : ''}
76
- ${isMinimal ? 'font-weight: 300;' : ''}
77
- ">
78
- ${content}
79
- </td>
80
- </tr>
81
+ <tr><td style="padding-top:${this.theme.spacing.md};font-size:${this.theme.typography.fontSizes.medium};line-height:1.4;">
82
+ ${escapeHtml(content)}
83
+ </td></tr>
81
84
  </table>
82
85
  ` : ''}
83
-
84
86
  </td>
85
87
  </tr>
86
88
  </table>
@@ -88,481 +90,223 @@ class TemplateBuilder {
88
90
  return this;
89
91
  }
90
92
  buildLogo() {
91
- var _a;
92
- const logo = (_a = this.config.header) === null || _a === void 0 ? void 0 : _a.logo;
93
+ const logo = this.config.header?.logo;
93
94
  if (!logo)
94
95
  return '';
95
96
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
96
- const isMonokai = this.theme.id === 'monokai';
97
- const isModern = this.theme.id === 'modern';
98
- const isCorporate = this.theme.id === 'corporate';
99
- const isMinimal = this.theme.id === 'minimal';
97
+ const styles = this.emailStyles.header;
100
98
  if (logo.type === 'text') {
101
- let textStyles = `
99
+ const logoBackground = this.config.header?.backgroundColor || colors.background;
100
+ const preferredColor = this.config.header?.textColor || colors[styles.logoColor];
101
+ const logoColor = (0, color_accessibility_1.readableTextColor)(preferredColor, logoBackground, colors.text);
102
+ const textStyles = `
103
+ display: inline-block;
104
+ max-width: 100%;
105
+ font-family: ${styles.logoFontFamily || this.theme.typography.fontFamily};
102
106
  font-size: ${this.theme.typography.fontSizes.xlarge};
103
107
  font-weight: ${this.theme.typography.fontWeights.bold};
104
- color: ${colors.primary};
108
+ line-height: 1.2;
109
+ color: ${logoColor};
110
+ text-transform: ${styles.logoTextTransform || 'none'};
111
+ letter-spacing: ${styles.logoLetterSpacing || 'normal'};
105
112
  `;
106
- // styles para tema Corporate
107
- if (isCorporate) {
108
- const corporate = this.theme;
109
- textStyles += `
110
- font-family: 'Playfair Display', serif;
111
- letter-spacing: ${corporate.branding.letterSpacing.wider};
112
- text-transform: ${corporate.branding.textTransform.uppercase};
113
- color: ${corporate.corporateColors.gold};
114
- `;
115
- }
116
- // styles para tema Minimal
117
- else if (isMinimal) {
118
- textStyles += `
119
- font-weight: 500;
120
- letter-spacing: -0.5px;
121
- color: ${colors.text};
122
- `;
123
- }
124
- // styles existentes
125
- else if (isMonokai) {
126
- textStyles += `
127
- letter-spacing: -0.5px;
128
- text-shadow: ${this.theme.effects.glow};
129
- `;
130
- }
131
- else if (isModern) {
132
- textStyles += `
133
- background: ${this.theme.gradients.primary};
134
- -webkit-background-clip: text;
135
- -webkit-text-fill-color: transparent;
136
- background-clip: text;
137
- `;
138
- }
139
- return `<div style="${textStyles}">${logo.text || 'Logo'}</div>`;
113
+ return `<div style="${escapeHtml(textStyles)}">${escapeHtml(logo.text || 'Logo')}</div>`;
140
114
  }
141
115
  if (logo.type === 'image') {
142
- let imageStyles = `
143
- height: ${logo.size === 'small' ? '30px' : logo.size === 'large' ? '100px' : '60px'};
144
- width: auto;
145
- `;
146
- if (isModern) {
147
- imageStyles += `
148
- border-radius: ${this.theme.borderStyle.radius.small};
149
- transition: ${this.theme.animations.hover};
150
- `;
151
- }
152
- else if (isCorporate) {
153
- imageStyles += `
154
- border-radius: ${this.theme.borderStyle.radius.small};
155
- `;
156
- }
157
- return `<img src="${logo.imageUrl}" alt="${logo.alt || 'Logo'}" style="${imageStyles}" />`;
116
+ const imageHeight = logo.size === 'small' ? 30 : logo.size === 'large' ? 100 : 60;
117
+ return `<img src="${safeHref(logo.imageUrl || '')}" alt="${escapeHtml(logo.alt || 'Logo')}" height="${imageHeight}" style="display:block;width:auto;height:${imageHeight}px;max-width:100%;border:0;border-radius:${styles.imageBorderRadius};margin:0 auto;" />`;
158
118
  }
159
119
  return '';
160
120
  }
161
121
  buildBody(content) {
162
122
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
163
- const isMonokai = this.theme.id === 'monokai';
164
- const isModern = this.theme.id === 'modern';
165
- const isCorporate = this.theme.id === 'corporate';
166
- const isMinimal = this.theme.id === 'minimal';
167
- let bodyStyles = `
168
- padding: ${this.theme.spacing.xl}!important;
169
- background-color: ${colors.background};
170
- color: ${colors.text};
123
+ const styles = this.emailStyles.body;
124
+ const bodyBackground = this.config.body?.backgroundColor || colors.background;
125
+ const bodyTextColor = (0, color_accessibility_1.readableTextColor)(this.config.body?.textColor || colors.text, bodyBackground, colors.text);
126
+ const accentBorder = borderFor(styles.accentColor, styles.accentWidth, colors);
127
+ let processedContent = content;
128
+ if (this.theme.id === 'corporate')
129
+ processedContent = this.formatCorporateContent(content, bodyBackground, bodyTextColor);
130
+ else if (this.theme.id === 'minimal')
131
+ processedContent = this.formatMinimalContent(content, bodyTextColor);
132
+ else if (this.theme.id === 'monokai' && content.includes('<code>'))
133
+ processedContent = this.highlightCode(content, bodyBackground, bodyTextColor);
134
+ const bodyStyles = `
135
+ padding: ${this.theme.spacing.xl} ${styles.horizontalPadding};
136
+ background-color: ${bodyBackground};
137
+ color: ${bodyTextColor};
171
138
  font-family: ${this.theme.typography.fontFamily};
172
139
  font-size: ${this.theme.typography.fontSizes.medium};
173
- line-height: ${isMinimal ? 1.7 : 1.6};
140
+ line-height: ${styles.lineHeight};
141
+ border-left: ${accentBorder};
142
+ border-radius: ${styles.borderRadius};
143
+ box-shadow: ${styles.boxShadow};
144
+ word-wrap: break-word;
145
+ overflow-wrap: break-word;
174
146
  `;
175
- // styles para tema Corporate
176
- if (isCorporate) {
177
- const corporate = this.theme;
178
- bodyStyles += `
179
- border-left: 4px solid ${corporate.corporateColors.gold};
180
- margin: ${this.theme.spacing.md} 0;
181
- box-shadow: ${corporate.elevation.card};
182
- `;
183
- }
184
- // styles para tema Minimal
185
- else if (isMinimal) {
186
- bodyStyles += `
187
- padding: ${this.theme.spacing.xl} ${this.theme.spacing.lg}!important;
188
- max-width: ${this.theme.layout.contentWidth};
189
- margin: 0 auto;
190
- `;
191
- }
192
- // styles existentes
193
- else if (isModern) {
194
- bodyStyles += `
195
- border-radius: ${this.theme.borderStyle.radius.medium};
196
- margin: ${this.theme.spacing.md};
197
- `;
198
- }
199
- else if (isMonokai) {
200
- bodyStyles += `
201
- border-left: 4px solid ${colors.primary};
202
- margin: ${this.theme.spacing.md} 0;
203
- `;
204
- }
205
- // Processar conteúdo com formatação específica
206
- let processedContent = content;
207
- if (isCorporate) {
208
- processedContent = this.formatCorporateContent(content);
209
- }
210
- else if (isMinimal) {
211
- processedContent = this.formatMinimalContent(content);
212
- }
213
- else if (isMonokai && content.includes('<code>')) {
214
- processedContent = this.highlightCode(content);
215
- }
216
147
  this.template += `
217
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #ffffff; padding: 10px;">
218
- <tr>
219
- <td style="${bodyStyles}">
220
- ${processedContent}
221
- </td>
222
- </tr>
223
- </table>
148
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation" bgcolor="${bodyBackground}" style="width:100%;background-color:${bodyBackground};">
149
+ <tr><td class="email-body" style="${escapeHtml(bodyStyles)}">${processedContent}</td></tr>
150
+ </table>
224
151
  `;
225
152
  return this;
226
153
  }
227
- formatCorporateContent(content) {
228
- // add els corporativos como citações e destaques
154
+ formatCorporateContent(content, background, textColor) {
155
+ const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
156
+ const quoteColor = (0, color_accessibility_1.contrastRatio)(colors.textMuted, background) >= 4.5 ? colors.textMuted : textColor;
229
157
  return content
230
- .replace(/<blockquote>(.*?)<\/blockquote>/g, (match, quote) => {
231
- const corporate = this.theme;
232
- return `<blockquote style="
233
- border-left: 4px solid ${corporate.corporateColors.gold};
234
- margin: ${this.theme.spacing.md} 0;
235
- padding: ${this.theme.spacing.md}!important;
236
- font-style: italic;
237
- color: ${corporate.light.textMuted};
238
- ">${quote}</blockquote>`;
239
- })
240
- .replace(/<highlight>(.*?)<\/highlight>/g, (match, text) => {
241
- const corporate = this.theme;
242
- return `<span style="
243
- background: linear-gradient(120deg, ${corporate.corporateColors.gold}20 0%, ${corporate.corporateColors.gold}20 40%, transparent 60%);
244
- padding: 0 ${this.theme.spacing.xs}!important;
245
- font-weight: bold;
246
- ">${text}</span>`;
247
- });
158
+ .replace(/<blockquote(?:\s[^>]*)?>([\s\S]*?)<\/blockquote>/gi, (_match, quote) => `<blockquote style="border-left:3px solid ${colors.primary};margin:${this.theme.spacing.md} 0;padding:${this.theme.spacing.md};font-style:italic;color:${quoteColor};">${quote}</blockquote>`)
159
+ .replace(/<highlight(?:\s[^>]*)?>([\s\S]*?)<\/highlight>/gi, (_match, text) => `<span style="background-color:${colors.border};padding:0 ${this.theme.spacing.xs};font-weight:bold;color:${(0, color_accessibility_1.readableTextColor)(colors.text, colors.border, textColor)};">${text}</span>`);
248
160
  }
249
- formatMinimalContent(content) {
250
- const minimal = this.theme;
251
- // fmt minimalista - remove styles desnecessários
161
+ formatMinimalContent(content, textColor) {
162
+ const headingSizes = { '1': '24px', '2': '20px', '3': '18px', '4': '16px', '5': '14px', '6': '13px' };
252
163
  return content
253
- .replace(/<h[1-6]>/g, (match) => {
254
- var _a;
255
- const level = ((_a = match.match(/h([1-6])/)) === null || _a === void 0 ? void 0 : _a[1]) || '2';
256
- const size = {
257
- '1': '24px',
258
- '2': '20px',
259
- '3': '18px',
260
- '4': '16px',
261
- '5': '14px',
262
- '6': '13px'
263
- }[level];
264
- return `<h${level} style="
265
- font-size: ${size};
266
- font-weight: 500;
267
- margin: ${minimal.spacing.lg} 0 ${minimal.spacing.md} 0;
268
- line-height: 1.3;
269
- ">`;
270
- })
271
- .replace(/<p>/g, `<p style="margin: 0 0 ${minimal.layout.paragraphSpacing} 0;">`);
164
+ .replace(/<h([1-6])(?:\s[^>]*)?>/gi, (_match, level) => `<h${level} style="font-size:${headingSizes[level]};font-weight:${this.theme.typography.fontWeights.medium};margin:${this.theme.spacing.lg} 0 ${this.theme.spacing.md};line-height:1.3;color:${textColor};">`)
165
+ .replace(/<p(?:\s[^>]*)?>/gi, `<p style="margin:0 0 ${this.theme.spacing.md};">`);
272
166
  }
273
- highlightCode(content) {
167
+ highlightCode(content, background, textColor) {
168
+ const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
274
169
  const monokai = this.theme;
275
- return content.replace(/<code>(.*?)<\/code>/g, (match, code) => {
276
- return `<pre style="
277
- background: ${monokai.dark.background};
278
- color: ${monokai.dark.text};
279
- padding: ${this.theme.spacing.md}!important;
280
- border-radius: ${monokai.borderStyle.radius};
281
- overflow-x: auto;
282
- font-family: ${this.theme.typography.fontFamily};
283
- font-size: 13px;
284
- line-height: 1.5;
285
- "><code>${this.applySyntaxHighlighting(code, monokai)}</code></pre>`;
286
- });
170
+ const radius = this.emailStyles.body.borderRadius;
171
+ const codeStyles = `margin:${this.theme.spacing.md} 0;background-color:${background};color:${textColor};padding:${this.theme.spacing.md};border:1px solid ${colors.border};border-radius:${radius};font-family:${this.theme.typography.fontFamily};font-size:13px;line-height:1.5;white-space:pre-wrap;word-wrap:break-word;`;
172
+ return content.replace(/<code(?:\s[^>]*)?>([\s\S]*?)<\/code>/gi, (_match, code) => `<pre style="${escapeHtml(codeStyles)}">${this.applySyntaxHighlighting(code, monokai, background, textColor)}</pre>`);
287
173
  }
288
- applySyntaxHighlighting(code, monokai) {
289
- // smp highlighting para demonstração
290
- const keywords = ['function', 'const', 'let', 'var', 'if', 'else', 'return', 'import', 'from'];
291
- let highlighted = code;
292
- keywords.forEach(keyword => {
293
- const regex = new RegExp(`\\b(${keyword})\\b`, 'g');
294
- highlighted = highlighted.replace(regex, `<span style="color: ${monokai.codeHighlight.keyword}">$1</span>`);
295
- });
296
- // strs
297
- highlighted = highlighted.replace(/'([^']*)'/g, `<span style="color: ${monokai.codeHighlight.string}">'$1'</span>`);
298
- highlighted = highlighted.replace(/"([^"]*)"/g, `<span style="color: ${monokai.codeHighlight.string}">"$1"</span>`);
299
- // nums
300
- highlighted = highlighted.replace(/\b(\d+)\b/g, `<span style="color: ${monokai.codeHighlight.number}">$1</span>`);
301
- // funcs
302
- highlighted = highlighted.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, `<span style="color: ${monokai.codeHighlight.function}">$1</span>(`);
303
- return highlighted;
174
+ applySyntaxHighlighting(code, monokai, background, textColor) {
175
+ const keywords = new Set(['function', 'const', 'let', 'var', 'if', 'else', 'return', 'import', 'from']);
176
+ const tokenPattern = /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:function|const|let|var|if|else|return|import|from)\b|\b\d+\b|\b[A-Za-z_$][\w$]*(?=\s*\()/g;
177
+ let result = '';
178
+ let lastIndex = 0;
179
+ for (const match of code.matchAll(tokenPattern)) {
180
+ const token = match[0];
181
+ const index = match.index ?? 0;
182
+ result += escapeHtml(code.slice(lastIndex, index));
183
+ const preferredColor = token.startsWith('"') || token.startsWith("'") ? monokai.codeHighlight.string
184
+ : /^\d+$/.test(token) ? monokai.codeHighlight.number
185
+ : keywords.has(token) ? monokai.codeHighlight.keyword : monokai.codeHighlight.function;
186
+ const color = (0, color_accessibility_1.contrastRatio)(preferredColor, background) >= 4.5 ? preferredColor : textColor;
187
+ result += `<span style="color:${color};">${escapeHtml(token)}</span>`;
188
+ lastIndex = index + token.length;
189
+ }
190
+ return result + escapeHtml(code.slice(lastIndex));
304
191
  }
305
192
  buildButton(text, url, variant = 'primary') {
306
193
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
307
- const isModern = this.theme.id === 'modern';
308
- const isMonokai = this.theme.id === 'monokai';
309
- const isCorporate = this.theme.id === 'corporate';
310
- const isMinimal = this.theme.id === 'minimal';
311
- let buttonColor = variant === 'primary' ? colors.primary : colors.secondary;
312
- let buttonStyles = `
194
+ if (!['primary', 'secondary', 'success', 'danger'].includes(variant))
195
+ throw new Error('Invalid CTA variant');
196
+ const safeUrl = safeHref(url, true);
197
+ const buttonColor = variant === 'primary' ? colors.primary : variant === 'secondary' ? colors.secondary : variant === 'success' ? colors.success : colors.error;
198
+ const button = this.emailStyles.button;
199
+ const isOutline = button.appearance === 'outline';
200
+ const alignment = this.config.body?.alignment || 'left';
201
+ const buttonTextColor = isOutline
202
+ ? (0, color_accessibility_1.contrastRatio)(buttonColor, colors.background) >= 4.5 ? buttonColor : colors.text
203
+ : (0, color_accessibility_1.accessibleTextColor)(buttonColor);
204
+ const buttonStyles = `
313
205
  display: inline-block;
314
- padding: 12px 24px!important;
315
- background-color: ${buttonColor};
316
- color: white;
317
- text-decoration: none;
318
- font-weight: ${this.theme.typography.fontWeights.medium};
319
- margin: ${this.theme.spacing.md} 0;
206
+ box-sizing: border-box;
207
+ max-width: 100%;
208
+ padding: 13px 24px;
209
+ background-color: ${isOutline ? colors.background : buttonColor};
210
+ border: ${button.borderWidth} solid ${isOutline ? buttonColor : 'transparent'};
211
+ border-radius: ${button.borderRadius};
212
+ box-shadow: ${button.boxShadow};
213
+ color: ${buttonTextColor};
214
+ font-family: ${this.theme.typography.fontFamily};
215
+ font-size: ${this.theme.typography.fontSizes.medium};
216
+ font-weight: ${this.theme.typography.fontWeights.bold};
217
+ line-height: 1.3;
218
+ letter-spacing: ${button.letterSpacing || 'normal'};
219
+ text-transform: ${button.textTransform || 'none'};
220
+ text-decoration: none !important;
320
221
  `;
321
- // styles para tema Corporate
322
- if (isCorporate) {
323
- const corporate = this.theme;
324
- buttonStyles += `
325
- border-radius: ${corporate.borderStyle.radius.small};
326
- text-transform: ${corporate.branding.textTransform.uppercase};
327
- letter-spacing: ${corporate.branding.letterSpacing.wide};
328
- transition: all 0.3s ease;
329
- &:hover {
330
- transform: translateY(-2px);
331
- box-shadow: ${corporate.elevation.hover};
332
- }
333
- `;
334
- }
335
- // styles para tema Minimal
336
- else if (isMinimal) {
337
- buttonStyles += `
338
- border-radius: 0;
339
- border: 1px solid ${buttonColor};
340
- background-color: transparent;
341
- color: ${buttonColor};
342
- padding: 10px 20px!important;
343
- transition: all 0.2s ease;
344
- &:hover {
345
- background-color: ${buttonColor};
346
- color: white;
347
- }
348
- `;
349
- }
350
- // styles existentes
351
- else if (isModern) {
352
- buttonStyles += `
353
- background: ${this.theme.gradients.primary};
354
- border-radius: ${this.theme.borderStyle.radius.medium};
355
- transition: ${this.theme.animations.hover};
356
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
357
- `;
358
- }
359
- else if (isMonokai) {
360
- buttonStyles += `
361
- border: 2px solid ${buttonColor};
362
- border-radius: ${this.theme.borderStyle.radius};
363
- transition: ${this.theme.effects.transition};
364
- &:hover {
365
- transform: translateY(-2px);
366
- box-shadow: ${this.theme.effects.glow};
367
- }
368
- `;
369
- }
370
- else {
371
- buttonStyles += `
372
- border-radius: 4px;
373
- `;
374
- }
375
222
  this.template += `
376
- <table width="100%" style="padding: 10px;" cellpadding="0" cellspacing="0" role="presentation">
377
- <tr>
378
- <td align="center">
379
-
380
- <table cellpadding="0" cellspacing="0" role="presentation">
381
- <tr>
382
- <td style="${buttonStyles}">
383
- <a href="${url}" style="
384
- text-decoration: none;
385
- color: inherit;
386
- display: inline-block;
387
- font-weight: ${this.theme.typography.fontWeights.bold};
388
- padding: 12px 24px;
389
- ">
390
- ${text}
391
- </a>
392
- </td>
393
- </tr>
394
- </table>
395
-
396
- </td>
397
- </tr>
223
+ <table width="100%" class="button-wrap" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
224
+ <tr><td align="${alignment}" style="padding:${this.theme.spacing.sm} 8px;">
225
+ <table class="button" align="${alignment}" cellpadding="0" cellspacing="0" role="presentation">
226
+ <tr><td align="${alignment}"><a class="button-link" href="${safeUrl}" style="${escapeHtml(buttonStyles)}">${escapeHtml(text)}</a></td></tr>
227
+ </table>
228
+ </td></tr>
398
229
  </table>
399
230
  `;
400
231
  return this;
401
232
  }
402
233
  buildFooter() {
403
- var _a, _b, _c;
404
- if (!((_a = this.config.footer) === null || _a === void 0 ? void 0 : _a.show))
234
+ if (!this.config.footer?.show)
405
235
  return this;
406
236
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
407
- const isModern = this.theme.id === 'modern';
408
- const isMonokai = this.theme.id === 'monokai';
409
- const isCorporate = this.theme.id === 'corporate';
410
- const isMinimal = this.theme.id === 'minimal';
411
- let footerStyles = `
412
- background-color: ${colors.background};
413
- border-top: 1px solid ${colors.border};
414
- padding: ${this.theme.spacing.lg} ${this.theme.spacing.xl}!important;
237
+ const styles = this.emailStyles.footer;
238
+ const footerBorder = borderFor(styles.accentColor, styles.accentWidth, colors);
239
+ const footerBackground = this.config.footer?.backgroundColor || colors.background;
240
+ const defaultFooterTextColor = (0, color_accessibility_1.contrastRatio)(colors.textMuted, footerBackground) >= 4.5 ? colors.textMuted : colors.text;
241
+ const footerTextColor = (0, color_accessibility_1.readableTextColor)(this.config.footer?.textColor || defaultFooterTextColor, footerBackground, colors.text);
242
+ const copyrightStyles = escapeHtml(`padding-top:${this.theme.spacing.md};font-family:${this.theme.typography.fontFamily};font-size:${this.theme.typography.fontSizes.small};line-height:1.5;color:${footerTextColor};text-align:center;`);
243
+ const unsubscribeCellStyles = escapeHtml(`padding-top:${this.theme.spacing.sm};text-align:center;font-family:${this.theme.typography.fontFamily};font-size:${this.theme.typography.fontSizes.small};line-height:1.5;`);
244
+ const footerStyles = `
245
+ background-color: ${footerBackground};
246
+ border-top: ${footerBorder};
247
+ padding: ${this.theme.spacing.lg} ${this.theme.spacing.xl};
415
248
  text-align: center;
249
+ font-family: ${this.theme.typography.fontFamily};
416
250
  font-size: ${this.theme.typography.fontSizes.small};
417
- color: ${colors.textMuted};
251
+ line-height: 1.5;
252
+ color: ${footerTextColor};
418
253
  `;
419
- // styles para tema Corporate
420
- if (isCorporate) {
421
- const corporate = this.theme;
422
- footerStyles += `
423
- border-top: 2px solid ${corporate.corporateColors.gold};
424
- font-size: 11px;
425
- letter-spacing: ${corporate.branding.letterSpacing.normal};
426
- `;
427
- }
428
- // styles para tema Minimal
429
- else if (isMinimal) {
430
- footerStyles += `
431
- border-top: 1px solid ${colors.border};
432
- padding: ${this.theme.spacing.lg}!important;
433
- font-size: 12px;
434
- `;
435
- }
436
- // styles existentes
437
- else if (isModern) {
438
- footerStyles += `
439
- backdrop-filter: blur(${this.theme.glassmorphism.blur});
440
- border-radius: ${this.theme.borderStyle.radius.small};
441
- `;
442
- }
443
- else if (isMonokai) {
444
- footerStyles += `
445
- border-top-width: 2px;
446
- border-top-color: ${colors.primary};
447
- `;
448
- }
449
254
  const footerLinks = this.buildFooterLinks();
450
255
  const socialLinks = this.buildSocialLinks();
451
256
  this.template += `
452
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
453
- <tr>
454
- <td style="${footerStyles}" align="center">
455
-
456
- ${footerLinks}
457
-
458
- ${socialLinks}
459
-
460
- ${((_b = this.config.footer) === null || _b === void 0 ? void 0 : _b.copyrightText) ? `
461
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
462
- <tr>
463
- <td style="
464
- padding-top: ${this.theme.spacing.md};
465
- font-family: Arial, sans-serif;
466
- font-size: 12px;
467
- color: ${colors.textMuted};
468
- text-align: center;
469
- ">
470
- ${this.config.footer.copyrightText}
471
- </td>
472
- </tr>
473
- </table>
474
- ` : ''}
475
-
476
- ${((_c = this.config.footer) === null || _c === void 0 ? void 0 : _c.unsubscribeText) ? `
477
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
478
- <tr>
479
- <td style="
480
- padding-top: ${this.theme.spacing.sm};
481
- text-align: center;
482
- font-family: Arial, sans-serif;
483
- font-size: 12px;
484
- ">
485
- <a href="#" style="
486
- color: ${colors.textMuted};
487
- text-decoration: underline;
488
- ">
489
- ${this.config.footer.unsubscribeText}
490
- </a>
491
- </td>
492
- </tr>
493
- </table>
494
- ` : ''}
495
-
496
- </td>
497
- </tr>
257
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation" bgcolor="${footerBackground}" style="width:100%;background-color:${footerBackground};">
258
+ <tr><td class="email-footer-cell" bgcolor="${footerBackground}" style="${escapeHtml(footerStyles)}" align="center">
259
+ ${footerLinks}
260
+ ${socialLinks}
261
+ ${this.config.footer?.copyrightText ? `
262
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
263
+ <tr><td style="${copyrightStyles}">
264
+ ${escapeHtml(this.config.footer.copyrightText)}
265
+ </td></tr>
266
+ </table>
267
+ ` : ''}
268
+ ${this.config.footer?.unsubscribeText ? `
269
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
270
+ <tr><td style="${unsubscribeCellStyles}">
271
+ ${this.config.footer.unsubscribeUrl
272
+ ? `<a href="${safeHref(this.config.footer.unsubscribeUrl, true)}" style="color:${footerTextColor};${this.linkDecorationStyle()}">${escapeHtml(this.config.footer.unsubscribeText)}</a>`
273
+ : escapeHtml(this.config.footer.unsubscribeText)}
274
+ </td></tr>
275
+ </table>
276
+ ` : ''}
277
+ </td></tr>
498
278
  </table>
499
279
  `;
500
280
  return this;
501
281
  }
502
282
  buildFooterLinks() {
503
- var _a, _b;
504
- if (!((_b = (_a = this.config.footer) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.length))
283
+ if (!this.config.footer?.links?.length)
505
284
  return '';
506
285
  const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
507
- const isModern = this.theme.id === 'modern';
508
- const isCorporate = this.theme.id === 'corporate';
509
- const isMinimal = this.theme.id === 'minimal';
510
- let linkStyles = `
511
- color: ${colors.textMuted};
512
- text-decoration: none;
513
- margin: 0 ${this.theme.spacing.sm};
514
- `;
515
- if (isModern) {
516
- linkStyles += `
517
- transition: ${this.theme.animations.hover};
518
- &:hover {
519
- color: ${colors.primary};
520
- transform: translateY(-1px);
521
- }
522
- `;
523
- }
524
- else if (isCorporate) {
525
- linkStyles += `
526
- text-transform: uppercase;
527
- font-size: 11px;
528
- letter-spacing: 0.5px;
529
- &:hover {
530
- color: ${this.theme.corporateColors.gold};
531
- }
532
- `;
533
- }
534
- else if (isMinimal) {
535
- linkStyles += `
536
- &:hover {
537
- text-decoration: underline;
538
- }
539
- `;
540
- }
286
+ const footerBackground = this.config.footer.backgroundColor || colors.background;
287
+ const preferredLinkColor = (0, color_accessibility_1.contrastRatio)(colors.textMuted, footerBackground) >= 4.5 ? colors.textMuted : colors.text;
288
+ const linkColor = (0, color_accessibility_1.readableTextColor)(this.config.footer.textColor || preferredLinkColor, footerBackground, colors.text);
289
+ const linkStyles = `color:${linkColor};${this.linkDecorationStyle()}font-family:${this.theme.typography.fontFamily};font-size:${this.theme.typography.fontSizes.small};line-height:1.5;`;
541
290
  return `
542
- <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
543
- <tr>
544
- <td style="padding-bottom: ${this.theme.spacing.lg};" align="center">
545
-
546
- <table cellpadding="0" cellspacing="0" role="presentation">
547
- <tr>
548
- ${this.config.footer.links.map(link => `
549
- <td style="padding: 0 8px;">
550
- <a href="${link.url}" style="${linkStyles}">
551
- ${link.text}
552
- </a>
553
- </td>
554
- `).join('')}
555
- </tr>
556
- </table>
557
-
558
- </td>
559
- </tr>
291
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
292
+ <tr><td style="padding-bottom:${this.theme.spacing.md};" align="center">
293
+ <table class="footer-links" cellpadding="0" cellspacing="0" role="presentation">
294
+ <tr>${this.config.footer.links.map(link => `
295
+ <td class="footer-link-cell" style="padding:0 8px;">
296
+ <a href="${safeHref(link.url, true)}" style="${escapeHtml(linkStyles)}">${escapeHtml(link.text)}</a>
297
+ </td>
298
+ `).join('')}</tr>
299
+ </table>
300
+ </td></tr>
560
301
  </table>
561
302
  `;
562
303
  }
304
+ linkDecorationStyle() {
305
+ const decoration = this.config.linkDecoration === 'underline' ? 'underline' : 'none';
306
+ return `text-decoration:${decoration} !important;`;
307
+ }
563
308
  buildSocialLinks() {
564
- var _a, _b;
565
- if (!((_b = (_a = this.config.footer) === null || _a === void 0 ? void 0 : _a.socialLinks) === null || _b === void 0 ? void 0 : _b.length))
309
+ if (!this.config.footer?.socialLinks?.length)
566
310
  return '';
567
311
  return `
568
312
  <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
@@ -573,12 +317,12 @@ class TemplateBuilder {
573
317
  <tr>
574
318
  ${this.config.footer.socialLinks.map(link => `
575
319
  <td style="padding: 0 6px;">
576
- <a href="${link.url}" target="_blank">
320
+ <a href="${safeHref(link.url)}" target="_blank" rel="noopener noreferrer" style="${this.linkDecorationStyle()}">
577
321
  <img
578
322
  src="${this.getSocialIcon(link.platform)}"
579
323
  width="20"
580
324
  height="20"
581
- alt="${link.platform}"
325
+ alt="${escapeHtml(link.platform)}"
582
326
  style="display: block; border: 0;"
583
327
  />
584
328
  </a>
@@ -595,6 +339,7 @@ class TemplateBuilder {
595
339
  getSocialIcon(platform) {
596
340
  const icons = {
597
341
  facebook: 'https://cdn.simpleicons.org/facebook/1877F2',
342
+ linkedin: 'https://cdn.simpleicons.org/linkedin/0A66C2',
598
343
  twitter: 'https://cdn.simpleicons.org/x/1DA1F2',
599
344
  github: 'https://cdn.simpleicons.org/github/000000',
600
345
  instagram: 'https://cdn.simpleicons.org/instagram/E4405F',
@@ -613,61 +358,65 @@ class TemplateBuilder {
613
358
  return icons[platform] || 'https://cdn.simpleicons.org/virginmedia/5865F2';
614
359
  }
615
360
  build() {
616
- const isModern = this.theme.id === 'modern';
617
- const isMonokai = this.theme.id === 'monokai';
618
- const isCorporate = this.theme.id === 'corporate';
619
- const isMinimal = this.theme.id === 'minimal';
620
- let wrapperStyles = 'margin: 0; padding: 0;';
621
- let containerStyles = `max-width: ${isMinimal ? '640px' : '600px'}; margin: 0 auto;`;
622
- if (isModern) {
623
- containerStyles += `
624
- border-radius: ${this.theme.borderStyle.radius.large};
625
- overflow: hidden;
626
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
627
- `;
628
- }
629
- else if (isMonokai) {
630
- containerStyles += `
631
- border: 1px solid ${this.theme.light.border};
632
- border-radius: ${this.theme.borderStyle.radius};
633
- `;
634
- }
635
- else if (isCorporate) {
636
- containerStyles += `
637
- border: 1px solid ${this.theme.light.border};
638
- box-shadow: ${this.theme.elevation.card};
639
- `;
640
- }
641
- else if (isMinimal) {
642
- containerStyles += `
643
- border: none;
644
- `;
645
- }
646
- const backgroundColor = this.variant === 'light' ? '#f5f5f5' : '#1a1a1a';
647
- const finalBackgroundColor = isMinimal ? (this.variant === 'light' ? '#ffffff' : '#000000') : backgroundColor;
361
+ const colors = this.variant === 'light' ? this.theme.light : this.theme.dark;
362
+ const styles = this.emailStyles;
363
+ const direction = this.config.direction || 'ltr';
364
+ if (!['ltr', 'rtl', 'auto'].includes(direction))
365
+ throw new Error('Invalid template text direction');
366
+ const maxWidth = Number.isFinite(styles.maxWidthPx) ? Math.min(800, Math.max(320, Math.round(styles.maxWidthPx))) : 600;
367
+ const containerBorder = borderFor(styles.container.borderColor, styles.container.borderWidth, colors);
368
+ const pageBackground = this.variant === 'light' ? '#f3f4f6' : '#090d15';
369
+ const containerStyles = `
370
+ width: 100%;
371
+ max-width: ${maxWidth}px;
372
+ margin: 0 auto;
373
+ background-color: ${colors.background};
374
+ border: ${containerBorder};
375
+ border-radius: ${styles.container.borderRadius};
376
+ box-shadow: ${styles.container.boxShadow};
377
+ border-collapse: separate;
378
+ `;
648
379
  return `
649
380
  <!DOCTYPE html>
650
- <html>
381
+ <html lang="${escapeHtml(this.config.language || 'en')}" dir="${escapeHtml(direction)}">
651
382
  <head>
652
383
  <meta charset="utf-8">
653
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
654
- <title>Email Template</title>
384
+ <meta name="viewport" content="width=device-width, initial-scale=1">
385
+ <meta name="color-scheme" content="${this.variant}">
386
+ <meta name="supported-color-schemes" content="${this.variant}">
387
+ <meta name="x-apple-disable-message-reformatting">
388
+ <meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
389
+ <title>Email template</title>
655
390
  <style>
391
+ :root { color-scheme:${this.variant}; supported-color-schemes:${this.variant}; }
392
+ body { margin:0 !important; padding:0 !important; width:100% !important; background-color:${pageBackground}; }
393
+ table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
394
+ img { border:0; max-width:100%; height:auto; }
395
+ .container { width:100%; max-width:${maxWidth}px; }
396
+ .button { max-width:100%; }
656
397
  @media only screen and (max-width: 600px) {
657
- .container {
658
- width: 100% !important;
659
- }
660
- .button {
661
- display: block !important;
662
- width: 100% !important;
663
- }
398
+ .outer-cell { padding:10px 8px !important; }
399
+ .container { width:100% !important; max-width:100% !important; }
400
+ .email-header-cell { padding-left:16px !important; padding-right:16px !important; }
401
+ .email-body { padding-left:16px !important; padding-right:16px !important; }
402
+ .email-footer-cell { padding-left:16px !important; padding-right:16px !important; }
403
+ table.button { display:block !important; width:100% !important; }
404
+ .button-link { box-sizing:border-box !important; display:block !important; width:100% !important; text-align:center !important; }
405
+ .footer-links { width:100% !important; }
406
+ .footer-link-cell { box-sizing:border-box !important; display:block !important; width:100% !important; padding:5px 0 !important; text-align:center !important; }
664
407
  }
665
408
  </style>
666
409
  </head>
667
- <body style="${wrapperStyles}">
668
- <div class="container" style="${containerStyles} background-color: ${finalBackgroundColor};">
669
- ${this.template}
670
- </div>
410
+ <body style="margin:0;padding:0;background-color:${pageBackground};">
411
+ <table class="email-page" width="100%" cellpadding="0" cellspacing="0" role="presentation" bgcolor="${pageBackground}" style="width:100%;background-color:${pageBackground};">
412
+ <tr><td class="outer-cell" align="center" style="padding:20px 12px;">
413
+ <table class="container" width="${maxWidth}" cellpadding="0" cellspacing="0" role="presentation" bgcolor="${colors.background}" style="${escapeHtml(containerStyles)}">
414
+ <tr><td class="container-content" style="padding:0;">
415
+ ${this.template}
416
+ </td></tr>
417
+ </table>
418
+ </td></tr>
419
+ </table>
671
420
  </body>
672
421
  </html>
673
422
  `;