@emailens/engine 0.10.4 → 0.11.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.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
- import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, I as ImageReport, g as InboxPreview, h as SizeReport, i as TemplateReport, O as OverflowReport, V as VisualReport, j as DeliverabilityReport } from './types-BLR3-Fzo.cjs';
2
- export { k as AccessibilityIssue, B as BaseIssue, l as ClientTruncation, m as DeliverabilityCheck, n as DeliverabilityIssue, o as EstimateOptions, p as ExportScope, q as FixType, r as ImageInfo, s as ImageIssue, t as InboxPreviewIssue, u as InputFormat, v as LinkIssue, w as OverflowIssue, P as PreviewResult, x as Severity, y as SizeIssue, z as SourceLocation, G as SpamIssue, H as TemplateIssue, J as TokenEstimate, K as TokenEstimateWithWarnings, M as VisualIssue, N as estimateAiFixTokens, Q as generateFixPrompt, R as heuristicTokenCount } from './types-BLR3-Fzo.cjs';
3
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-DTzVpGgB.cjs';
1
+ import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VisualReport, k as DesignReport, l as DeliverabilityReport } from './types-BFhPXh3c.cjs';
2
+ export { B as BaseIssue, m as ClientTruncation, n as DeliverabilityCheck, o as DeliverabilityIssue, p as DesignIssue, q as EstimateOptions, r as ExportScope, s as FixType, t as ImageInfo, u as ImageIssue, v as InboxPreviewIssue, w as InputFormat, x as LinkIssue, y as OverflowIssue, P as PreviewResult, z as Severity, G as SizeIssue, H as SourceLocation, J as SpamIssue, K as TemplateIssue, M as TokenEstimate, N as TokenEstimateWithWarnings, Q as VisualIssue, R as estimateAiFixTokens, U as generateFixPrompt, W as heuristicTokenCount } from './types-BFhPXh3c.cjs';
3
+ import * as cheerio from 'cheerio';
4
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-BWIfwG7U.cjs';
4
5
 
5
6
  declare const EMAIL_CLIENTS: EmailClient[];
6
7
  declare function getClient(id: string): EmailClient | undefined;
@@ -13,7 +14,7 @@ interface ParseOptions {
13
14
  /**
14
15
  * Record source positions so issues carry a `loc`. Costs a little parse time
15
16
  * (parse5 tracks a location record per node and per attribute), so it is
16
- * opt-in for callers that need to point at the source editors, CI
17
+ * opt-in for callers that need to point at the source: editors, CI
17
18
  * annotations, agents.
18
19
  */
19
20
  positions?: boolean;
@@ -24,7 +25,7 @@ interface ParseOptions {
24
25
  * for all target email clients.
25
26
  *
26
27
  * The `framework` parameter controls which fix snippets are attached
27
- * to warnings it does NOT change which warnings fire. Analysis always
28
+ * to warnings; it does NOT change which warnings fire. Analysis always
28
29
  * runs on compiled HTML (what email clients actually receive). Fix
29
30
  * snippets reference source-level constructs so users know how to
30
31
  * modify their framework source code.
@@ -128,7 +129,7 @@ interface GenerateAiFixOptions extends ExportPromptOptions {
128
129
  * to build a structured prompt, then delegates to an LLM for context-aware
129
130
  * structural fixes that static snippets cannot handle.
130
131
  *
131
- * The engine stays provider-agnostic consumers pass their own `AiProvider`
132
+ * The engine stays provider-agnostic: consumers pass their own `AiProvider`
132
133
  * callback (Anthropic SDK, Vercel AI SDK, OpenRouter, etc.).
133
134
  *
134
135
  * @example
@@ -166,10 +167,10 @@ declare function generateAiFix(options: GenerateAiFixOptions): Promise<AiFixResu
166
167
  * System prompt for the AI fix provider. Consumers should pass this as
167
168
  * the `system` parameter to their LLM call for best results.
168
169
  */
169
- declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer specializing in cross-client HTML email compatibility. You fix emails to render correctly across all email clients.\n\nRules:\n- Return ONLY the fixed code inside a single code fence. No explanations before or after.\n- Preserve all existing content, text, links, and visual design.\n- For structural issues (fixType: \"structural\"), you MUST restructure the HTML \u2014 CSS-only changes will not work.\n- Common structural patterns:\n - word-break/overflow-wrap unsupported \u2192 wrap text in <table><tr><td> with constrained width\n - display:flex/grid \u2192 convert to <table> layout (match the original column count and proportions)\n - border-radius in Outlook \u2192 use VML <v:roundrect> with <!--[if mso]> conditionals\n - background-image in Outlook \u2192 use VML <v:rect> with <v:fill>\n - max-width in Outlook \u2192 wrap in <!--[if mso]><table width=\"N\"> conditional\n - position:absolute \u2192 use <table> cells for layout\n - <svg> \u2192 replace with <img> pointing to a hosted PNG\n- For CSS-only issues (fixType: \"css\"), swap properties or add fallbacks.\n- For \"Layout & Visual Issues\", apply the provided Fix directly: add the background-color fallback beneath a gradient/image, append a web-safe font to the stack, and constrain fixed widths wider than the frame to width:100% with max-width.\n- Apply ALL fixes from the issues list \u2014 do not skip any.\n- Use the framework syntax specified (JSX/MJML/Maizzle/HTML).\n- For JSX: use camelCase style props, React Email components, and proper TypeScript types.\n- For MJML: use mj-* elements and attributes.\n- For Maizzle: use Tailwind CSS classes.";
170
+ declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer specializing in cross-client HTML email compatibility. You fix emails to render correctly across all email clients.\n\nRules:\n- Return ONLY the fixed code inside a single code fence. No explanations before or after.\n- Preserve all existing content, text, links, and visual design.\n- For structural issues (fixType: \"structural\"), you MUST restructure the HTML; CSS-only changes will not work.\n- Common structural patterns:\n - word-break/overflow-wrap unsupported \u2192 wrap text in <table><tr><td> with constrained width\n - display:flex/grid \u2192 convert to <table> layout (match the original column count and proportions)\n - border-radius in Outlook \u2192 use VML <v:roundrect> with <!--[if mso]> conditionals\n - background-image in Outlook \u2192 use VML <v:rect> with <v:fill>\n - max-width in Outlook \u2192 wrap in <!--[if mso]><table width=\"N\"> conditional\n - position:absolute \u2192 use <table> cells for layout\n - <svg> \u2192 replace with <img> pointing to a hosted PNG\n- For CSS-only issues (fixType: \"css\"), swap properties or add fallbacks.\n- For \"Layout & Visual Issues\", apply the provided Fix directly: add the background-color fallback beneath a gradient/image, append a web-safe font to the stack, and constrain fixed widths wider than the frame to width:100% with max-width.\n- Apply ALL fixes from the issues list: do not skip any.\n- Use the framework syntax specified (JSX/MJML/Maizzle/HTML).\n- For JSX: use camelCase style props, React Email components, and proper TypeScript types.\n- For MJML: use mj-* elements and attributes.\n- For Maizzle: use Tailwind CSS classes.";
170
171
 
171
172
  /**
172
- * CSS/HTML feature support matrix auto-generated from caniemail.com.
173
+ * CSS/HTML feature support matrix, auto-generated from caniemail.com.
173
174
  * Last synced: 2026-07-28
174
175
  * caniemail last updated: 2026-07-20 09:56:37 +0000
175
176
  *
@@ -186,11 +187,11 @@ declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer speciali
186
187
  * - hey-mail: caniemail.com (WebKit-based)
187
188
  * - superhuman: Manual overrides (Chromium/Blink-based, best-effort estimates)
188
189
  *
189
- * DO NOT EDIT regenerate with: bun run sync:caniemail
190
+ * DO NOT EDIT; regenerate with: bun run sync:caniemail
190
191
  */
191
192
  declare const CSS_SUPPORT: Record<string, Record<string, SupportLevel>>;
192
193
  /**
193
- * Caveat notes per (feature, client) from caniemail the "why" behind a
194
+ * Caveat notes per (feature, client) from caniemail: the "why" behind a
194
195
  * partial/buggy/unsupported rating. Sparse: only cells with a note appear.
195
196
  */
196
197
  declare const CSS_SUPPORT_NOTES: Record<string, Record<string, string[]>>;
@@ -212,10 +213,10 @@ declare const CSS_FUNCTION_FEATURES: readonly ["calc", "clamp", "conic-gradient"
212
213
  * caniemail's page for each feature, for a diagnostic's `codeDescription`.
213
214
  *
214
215
  * 250 of 255 features. The rest have no caniemail entry of
215
- * their own and get no link, which is the honest outcome a link to a 404
216
+ * their own and get no link, which is the honest outcome: a link to a 404
216
217
  * is worse than none.
217
218
  *
218
- * DO NOT EDIT regenerate with: bun run sync:feature-urls
219
+ * DO NOT EDIT; regenerate with: bun run sync:feature-urls
219
220
  */
220
221
  declare const FEATURE_URLS: Record<string, string>;
221
222
  /** Where to read about a rule, when there is somewhere. */
@@ -239,7 +240,7 @@ declare const VALUE_CAVEAT_PROPS: ReadonlySet<string>;
239
240
  * applies if either does.
240
241
  *
241
242
  * Returns true (report it) for a property that isn't value-gated, and for one
242
- * where we never saw a value an at-rule, a pseudo-class, a detected CSS
243
+ * where we never saw a value: an at-rule, a pseudo-class, a detected CSS
243
244
  * function.
244
245
  */
245
246
  declare function caveatApplies(prop: string, values: readonly string[] | undefined, notes: string[] | undefined): boolean;
@@ -267,6 +268,20 @@ declare function analyzeSpam(html: string, options?: SpamAnalysisOptions): SpamR
267
268
  */
268
269
  declare function validateLinks(html: string, options?: ParseOptions): LinkReport;
269
270
 
271
+ /**
272
+ * The render a cascade is being resolved for.
273
+ *
274
+ * An email has more than one correct answer: the same CSS produces a different
275
+ * palette on a phone and in dark mode, and a contrast bug can live in only one
276
+ * of them. Resolving against an explicit context is what lets the same
277
+ * machinery grade all three.
278
+ */
279
+ interface RenderContext {
280
+ /** Viewport width in px, for width media queries. */
281
+ width: number;
282
+ /** Whether `prefers-color-scheme: dark` matches. */
283
+ dark: boolean;
284
+ }
270
285
  /**
271
286
  * Audit an HTML email for accessibility issues.
272
287
  *
@@ -275,6 +290,43 @@ declare function validateLinks(html: string, options?: ParseOptions): LinkReport
275
290
  * color contrast. Returns a 0–100 score and detailed issues.
276
291
  */
277
292
  declare function checkAccessibility(html: string, options?: ParseOptions): AccessibilityReport;
293
+ /**
294
+ * Contrast failures inside the email's own `@media (prefers-color-scheme: dark)`
295
+ * styles.
296
+ *
297
+ * This is the other half of dark mode, and the half the author actually
298
+ * controls: Apple Mail, Superhuman, Thunderbird and friends do not invert
299
+ * anything, they just apply the dark block as written. A dark background
300
+ * paired with body text that was never re-coloured is invisible there, and no
301
+ * amount of inversion analysis finds it; the bug is in the CSS, not in what
302
+ * the client does to it.
303
+ *
304
+ * Free to run: same DOM, resolved against a dark render, so positions stay
305
+ * valid. Emails with no dark block resolve identically to the light pass and
306
+ * report nothing.
307
+ */
308
+ declare function checkDarkStylesContrastFromDom($: cheerio.CheerioAPI, lightIssues?: AccessibilityIssue[]): AccessibilityIssue[];
309
+ /**
310
+ * Contrast failures that only appear at mobile width. See
311
+ * {@link checkMobileContrastFromDom}.
312
+ */
313
+ declare function checkMobileContrast(html: string, options?: ParseOptions): AccessibilityIssue[];
314
+ /**
315
+ * Contrast failures an email has *only* in dark mode.
316
+ *
317
+ * Clients that invert colours do it selectively, so a palette that passes in
318
+ * light mode can collapse to unreadable once inverted, mid-tone text left
319
+ * alone on a background repainted near-black. This runs the ordinary contrast
320
+ * check over the simulated dark render and reports what is new there.
321
+ *
322
+ * Pass `lightIssues` when the light-mode report is already in hand (the audit
323
+ * has it) to skip re-running the light pass.
324
+ *
325
+ * ponytail: deduped by the element description, so two genuinely identical
326
+ * elements collapse into one report. Track them individually only if the
327
+ * per-element count turns out to matter.
328
+ */
329
+ declare function checkDarkModeContrast(html: string, lightIssues?: AccessibilityIssue[]): AccessibilityIssue[];
278
330
 
279
331
  /**
280
332
  * Analyze images in an HTML email for best practices.
@@ -327,12 +379,18 @@ declare function checkOverflow(html: string, options?: ParseOptions): OverflowRe
327
379
  */
328
380
  declare function checkVisual(html: string, options?: ParseOptions): VisualReport;
329
381
 
382
+ /**
383
+ * Report design drift in an HTML email: near-identical colours, and properties
384
+ * carrying more distinct values than a design system would.
385
+ */
386
+ declare function checkDesignConsistency(html: string, options?: ParseOptions): DesignReport;
387
+
330
388
  interface AuditOptions extends ParseOptions {
331
389
  framework?: Framework;
332
390
  /** Options for spam analysis */
333
391
  spam?: SpamAnalysisOptions;
334
392
  /** Skip specific checks */
335
- skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility" | "inboxPreview" | "size" | "templateVariables" | "overflow" | "visual">;
393
+ skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility" | "inboxPreview" | "size" | "templateVariables" | "overflow" | "visual" | "darkContrast" | "mobileContrast" | "design">;
336
394
  }
337
395
  interface AuditReport {
338
396
  compatibility: {
@@ -353,6 +411,12 @@ interface AuditReport {
353
411
  templateVariables: TemplateReport;
354
412
  overflow: OverflowReport;
355
413
  visual: VisualReport;
414
+ /** Contrast failures present only once a client inverts colours. */
415
+ darkContrast: AccessibilityIssue[];
416
+ /** Contrast failures present only below the email's mobile breakpoint. */
417
+ mobileContrast: AccessibilityIssue[];
418
+ /** Design drift: near-identical colours, and properties with no system left. */
419
+ design: DesignReport;
356
420
  }
357
421
  /**
358
422
  * Run all email analysis checks in a single call.
@@ -453,7 +517,7 @@ interface EmailSession {
453
517
  * Simulate dark mode for a specific client.
454
518
  *
455
519
  * Creates an isolated DOM copy per call (simulation mutates the DOM).
456
- * Operates on the **original** HTML if you need dark mode on
520
+ * Operates on the **original** HTML, if you need dark mode on
457
521
  * already-transformed HTML, use the standalone `simulateDarkMode()` instead.
458
522
  */
459
523
  simulateDarkMode(clientId: string): {
@@ -466,7 +530,7 @@ interface EmailSession {
466
530
  * DOM across all read-only analysis operations.
467
531
  *
468
532
  * Use this when you need to call multiple analysis functions on the
469
- * same HTML it eliminates redundant `cheerio.load()` calls.
533
+ * same HTML: it eliminates redundant `cheerio.load()` calls.
470
534
  *
471
535
  * @example
472
536
  * ```typescript
@@ -548,6 +612,21 @@ declare function wcagGrade(ratio: number): WcagGrade;
548
612
  * Returns the flattened [r, g, b] as 0-255 values.
549
613
  */
550
614
  declare function alphaBlend(fg: RGBA, bgR: number, bgG: number, bgB: number): [number, number, number];
615
+ /**
616
+ * Convert sRGB to OKLab.
617
+ *
618
+ * OKLab is built so that straight-line distance between two points matches how
619
+ * different the colours look, which sRGB emphatically does not: `#333` and
620
+ * `#343434` are far apart in RGB terms and identical to a reader.
621
+ */
622
+ declare function rgbToOklab(rgba: RGBA): [number, number, number];
623
+ /**
624
+ * Perceptual distance between two colours, as OKLab delta-E.
625
+ *
626
+ * Roughly: under ~0.02 the two are the same colour to a reader, and around
627
+ * ~0.1 they are clearly different.
628
+ */
629
+ declare function colorDistance(a: RGBA, b: RGBA): number;
551
630
 
552
631
  /**
553
632
  * Downlevel modern CSS in HTML emails to maximize email client compatibility.
@@ -575,4 +654,4 @@ declare function alphaBlend(fg: RGBA, bgR: number, bgG: number, bgB: number): [n
575
654
  */
576
655
  declare function downlevelCSS(html: string): string;
577
656
 
578
- export { AI_FIX_SYSTEM_PROMPT, AT_RULE_FEATURES, AccessibilityReport, AiFixResult, AiProvider, type AuditOptions, type AuditReport, COMPOUND_VALUE_FEATURES, CSSWarning, CSS_FUNCTION_FEATURES, CSS_SUPPORT, CSS_SUPPORT_NOTES, CodeFix, type CreateSessionOptions, DeliverabilityReport, DiffResult, EMAIL_CLIENTS, EMPTY_DELIVERABILITY, EmailClient, type EmailSession, ExportPromptOptions, FEATURE_URLS, Framework, GENERIC_LINK_TEXT, type GenerateAiFixOptions, HTML_ELEMENT_FEATURES, ImageReport, InboxPreview, LinkReport, MAX_HTML_SIZE, MAX_WARNING_LOCATIONS, OverflowReport, type ParseOptions, type RGBA, STRUCTURAL_FIX_PROPERTIES, SizeReport, SpamAnalysisOptions, SpamReport, SupportLevel, TemplateReport, TransformResult, VALUE_CAVEAT_PROPS, VisualReport, type WcagGrade, alphaBlend, analyzeEmail, analyzeImages, analyzeSpam, auditEmail, caveatApplies, checkAccessibility, checkOverflow, checkSize, checkTemplateVariables, checkVisual, contrastRatio, createSession, diffResults, downlevelCSS, errorWarnings, extractInboxPreview, featureUrl, formatRgb, generateAiFix, generateCompatibilityScore, getClient, getCodeFix, getSuggestion, parseColor, relativeLuminance, simulateDarkMode, structuralWarnings, toPlainText, transformForAllClients, transformForClient, validateLinks, warningsForClient, wcagGrade };
657
+ export { AI_FIX_SYSTEM_PROMPT, AT_RULE_FEATURES, AccessibilityIssue, AccessibilityReport, AiFixResult, AiProvider, type AuditOptions, type AuditReport, COMPOUND_VALUE_FEATURES, CSSWarning, CSS_FUNCTION_FEATURES, CSS_SUPPORT, CSS_SUPPORT_NOTES, CodeFix, type CreateSessionOptions, DeliverabilityReport, DesignReport, DiffResult, EMAIL_CLIENTS, EMPTY_DELIVERABILITY, EmailClient, type EmailSession, ExportPromptOptions, FEATURE_URLS, Framework, GENERIC_LINK_TEXT, type GenerateAiFixOptions, HTML_ELEMENT_FEATURES, ImageReport, InboxPreview, LinkReport, MAX_HTML_SIZE, MAX_WARNING_LOCATIONS, OverflowReport, type ParseOptions, type RGBA, type RenderContext, STRUCTURAL_FIX_PROPERTIES, SizeReport, SpamAnalysisOptions, SpamReport, SupportLevel, TemplateReport, TransformResult, VALUE_CAVEAT_PROPS, VisualReport, type WcagGrade, alphaBlend, analyzeEmail, analyzeImages, analyzeSpam, auditEmail, caveatApplies, checkAccessibility, checkDarkModeContrast, checkDarkStylesContrastFromDom, checkDesignConsistency, checkMobileContrast, checkOverflow, checkSize, checkTemplateVariables, checkVisual, colorDistance, contrastRatio, createSession, diffResults, downlevelCSS, errorWarnings, extractInboxPreview, featureUrl, formatRgb, generateAiFix, generateCompatibilityScore, getClient, getCodeFix, getSuggestion, parseColor, relativeLuminance, rgbToOklab, simulateDarkMode, structuralWarnings, toPlainText, transformForAllClients, transformForClient, validateLinks, warningsForClient, wcagGrade };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, I as ImageReport, g as InboxPreview, h as SizeReport, i as TemplateReport, O as OverflowReport, V as VisualReport, j as DeliverabilityReport } from './types-BLR3-Fzo.js';
2
- export { k as AccessibilityIssue, B as BaseIssue, l as ClientTruncation, m as DeliverabilityCheck, n as DeliverabilityIssue, o as EstimateOptions, p as ExportScope, q as FixType, r as ImageInfo, s as ImageIssue, t as InboxPreviewIssue, u as InputFormat, v as LinkIssue, w as OverflowIssue, P as PreviewResult, x as Severity, y as SizeIssue, z as SourceLocation, G as SpamIssue, H as TemplateIssue, J as TokenEstimate, K as TokenEstimateWithWarnings, M as VisualIssue, N as estimateAiFixTokens, Q as generateFixPrompt, R as heuristicTokenCount } from './types-BLR3-Fzo.js';
3
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-DwBWB2kr.js';
1
+ import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VisualReport, k as DesignReport, l as DeliverabilityReport } from './types-BFhPXh3c.js';
2
+ export { B as BaseIssue, m as ClientTruncation, n as DeliverabilityCheck, o as DeliverabilityIssue, p as DesignIssue, q as EstimateOptions, r as ExportScope, s as FixType, t as ImageInfo, u as ImageIssue, v as InboxPreviewIssue, w as InputFormat, x as LinkIssue, y as OverflowIssue, P as PreviewResult, z as Severity, G as SizeIssue, H as SourceLocation, J as SpamIssue, K as TemplateIssue, M as TokenEstimate, N as TokenEstimateWithWarnings, Q as VisualIssue, R as estimateAiFixTokens, U as generateFixPrompt, W as heuristicTokenCount } from './types-BFhPXh3c.js';
3
+ import * as cheerio from 'cheerio';
4
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-D24HkW2Z.js';
4
5
 
5
6
  declare const EMAIL_CLIENTS: EmailClient[];
6
7
  declare function getClient(id: string): EmailClient | undefined;
@@ -13,7 +14,7 @@ interface ParseOptions {
13
14
  /**
14
15
  * Record source positions so issues carry a `loc`. Costs a little parse time
15
16
  * (parse5 tracks a location record per node and per attribute), so it is
16
- * opt-in for callers that need to point at the source editors, CI
17
+ * opt-in for callers that need to point at the source: editors, CI
17
18
  * annotations, agents.
18
19
  */
19
20
  positions?: boolean;
@@ -24,7 +25,7 @@ interface ParseOptions {
24
25
  * for all target email clients.
25
26
  *
26
27
  * The `framework` parameter controls which fix snippets are attached
27
- * to warnings it does NOT change which warnings fire. Analysis always
28
+ * to warnings; it does NOT change which warnings fire. Analysis always
28
29
  * runs on compiled HTML (what email clients actually receive). Fix
29
30
  * snippets reference source-level constructs so users know how to
30
31
  * modify their framework source code.
@@ -128,7 +129,7 @@ interface GenerateAiFixOptions extends ExportPromptOptions {
128
129
  * to build a structured prompt, then delegates to an LLM for context-aware
129
130
  * structural fixes that static snippets cannot handle.
130
131
  *
131
- * The engine stays provider-agnostic consumers pass their own `AiProvider`
132
+ * The engine stays provider-agnostic: consumers pass their own `AiProvider`
132
133
  * callback (Anthropic SDK, Vercel AI SDK, OpenRouter, etc.).
133
134
  *
134
135
  * @example
@@ -166,10 +167,10 @@ declare function generateAiFix(options: GenerateAiFixOptions): Promise<AiFixResu
166
167
  * System prompt for the AI fix provider. Consumers should pass this as
167
168
  * the `system` parameter to their LLM call for best results.
168
169
  */
169
- declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer specializing in cross-client HTML email compatibility. You fix emails to render correctly across all email clients.\n\nRules:\n- Return ONLY the fixed code inside a single code fence. No explanations before or after.\n- Preserve all existing content, text, links, and visual design.\n- For structural issues (fixType: \"structural\"), you MUST restructure the HTML \u2014 CSS-only changes will not work.\n- Common structural patterns:\n - word-break/overflow-wrap unsupported \u2192 wrap text in <table><tr><td> with constrained width\n - display:flex/grid \u2192 convert to <table> layout (match the original column count and proportions)\n - border-radius in Outlook \u2192 use VML <v:roundrect> with <!--[if mso]> conditionals\n - background-image in Outlook \u2192 use VML <v:rect> with <v:fill>\n - max-width in Outlook \u2192 wrap in <!--[if mso]><table width=\"N\"> conditional\n - position:absolute \u2192 use <table> cells for layout\n - <svg> \u2192 replace with <img> pointing to a hosted PNG\n- For CSS-only issues (fixType: \"css\"), swap properties or add fallbacks.\n- For \"Layout & Visual Issues\", apply the provided Fix directly: add the background-color fallback beneath a gradient/image, append a web-safe font to the stack, and constrain fixed widths wider than the frame to width:100% with max-width.\n- Apply ALL fixes from the issues list \u2014 do not skip any.\n- Use the framework syntax specified (JSX/MJML/Maizzle/HTML).\n- For JSX: use camelCase style props, React Email components, and proper TypeScript types.\n- For MJML: use mj-* elements and attributes.\n- For Maizzle: use Tailwind CSS classes.";
170
+ declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer specializing in cross-client HTML email compatibility. You fix emails to render correctly across all email clients.\n\nRules:\n- Return ONLY the fixed code inside a single code fence. No explanations before or after.\n- Preserve all existing content, text, links, and visual design.\n- For structural issues (fixType: \"structural\"), you MUST restructure the HTML; CSS-only changes will not work.\n- Common structural patterns:\n - word-break/overflow-wrap unsupported \u2192 wrap text in <table><tr><td> with constrained width\n - display:flex/grid \u2192 convert to <table> layout (match the original column count and proportions)\n - border-radius in Outlook \u2192 use VML <v:roundrect> with <!--[if mso]> conditionals\n - background-image in Outlook \u2192 use VML <v:rect> with <v:fill>\n - max-width in Outlook \u2192 wrap in <!--[if mso]><table width=\"N\"> conditional\n - position:absolute \u2192 use <table> cells for layout\n - <svg> \u2192 replace with <img> pointing to a hosted PNG\n- For CSS-only issues (fixType: \"css\"), swap properties or add fallbacks.\n- For \"Layout & Visual Issues\", apply the provided Fix directly: add the background-color fallback beneath a gradient/image, append a web-safe font to the stack, and constrain fixed widths wider than the frame to width:100% with max-width.\n- Apply ALL fixes from the issues list: do not skip any.\n- Use the framework syntax specified (JSX/MJML/Maizzle/HTML).\n- For JSX: use camelCase style props, React Email components, and proper TypeScript types.\n- For MJML: use mj-* elements and attributes.\n- For Maizzle: use Tailwind CSS classes.";
170
171
 
171
172
  /**
172
- * CSS/HTML feature support matrix auto-generated from caniemail.com.
173
+ * CSS/HTML feature support matrix, auto-generated from caniemail.com.
173
174
  * Last synced: 2026-07-28
174
175
  * caniemail last updated: 2026-07-20 09:56:37 +0000
175
176
  *
@@ -186,11 +187,11 @@ declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer speciali
186
187
  * - hey-mail: caniemail.com (WebKit-based)
187
188
  * - superhuman: Manual overrides (Chromium/Blink-based, best-effort estimates)
188
189
  *
189
- * DO NOT EDIT regenerate with: bun run sync:caniemail
190
+ * DO NOT EDIT; regenerate with: bun run sync:caniemail
190
191
  */
191
192
  declare const CSS_SUPPORT: Record<string, Record<string, SupportLevel>>;
192
193
  /**
193
- * Caveat notes per (feature, client) from caniemail the "why" behind a
194
+ * Caveat notes per (feature, client) from caniemail: the "why" behind a
194
195
  * partial/buggy/unsupported rating. Sparse: only cells with a note appear.
195
196
  */
196
197
  declare const CSS_SUPPORT_NOTES: Record<string, Record<string, string[]>>;
@@ -212,10 +213,10 @@ declare const CSS_FUNCTION_FEATURES: readonly ["calc", "clamp", "conic-gradient"
212
213
  * caniemail's page for each feature, for a diagnostic's `codeDescription`.
213
214
  *
214
215
  * 250 of 255 features. The rest have no caniemail entry of
215
- * their own and get no link, which is the honest outcome a link to a 404
216
+ * their own and get no link, which is the honest outcome: a link to a 404
216
217
  * is worse than none.
217
218
  *
218
- * DO NOT EDIT regenerate with: bun run sync:feature-urls
219
+ * DO NOT EDIT; regenerate with: bun run sync:feature-urls
219
220
  */
220
221
  declare const FEATURE_URLS: Record<string, string>;
221
222
  /** Where to read about a rule, when there is somewhere. */
@@ -239,7 +240,7 @@ declare const VALUE_CAVEAT_PROPS: ReadonlySet<string>;
239
240
  * applies if either does.
240
241
  *
241
242
  * Returns true (report it) for a property that isn't value-gated, and for one
242
- * where we never saw a value an at-rule, a pseudo-class, a detected CSS
243
+ * where we never saw a value: an at-rule, a pseudo-class, a detected CSS
243
244
  * function.
244
245
  */
245
246
  declare function caveatApplies(prop: string, values: readonly string[] | undefined, notes: string[] | undefined): boolean;
@@ -267,6 +268,20 @@ declare function analyzeSpam(html: string, options?: SpamAnalysisOptions): SpamR
267
268
  */
268
269
  declare function validateLinks(html: string, options?: ParseOptions): LinkReport;
269
270
 
271
+ /**
272
+ * The render a cascade is being resolved for.
273
+ *
274
+ * An email has more than one correct answer: the same CSS produces a different
275
+ * palette on a phone and in dark mode, and a contrast bug can live in only one
276
+ * of them. Resolving against an explicit context is what lets the same
277
+ * machinery grade all three.
278
+ */
279
+ interface RenderContext {
280
+ /** Viewport width in px, for width media queries. */
281
+ width: number;
282
+ /** Whether `prefers-color-scheme: dark` matches. */
283
+ dark: boolean;
284
+ }
270
285
  /**
271
286
  * Audit an HTML email for accessibility issues.
272
287
  *
@@ -275,6 +290,43 @@ declare function validateLinks(html: string, options?: ParseOptions): LinkReport
275
290
  * color contrast. Returns a 0–100 score and detailed issues.
276
291
  */
277
292
  declare function checkAccessibility(html: string, options?: ParseOptions): AccessibilityReport;
293
+ /**
294
+ * Contrast failures inside the email's own `@media (prefers-color-scheme: dark)`
295
+ * styles.
296
+ *
297
+ * This is the other half of dark mode, and the half the author actually
298
+ * controls: Apple Mail, Superhuman, Thunderbird and friends do not invert
299
+ * anything, they just apply the dark block as written. A dark background
300
+ * paired with body text that was never re-coloured is invisible there, and no
301
+ * amount of inversion analysis finds it; the bug is in the CSS, not in what
302
+ * the client does to it.
303
+ *
304
+ * Free to run: same DOM, resolved against a dark render, so positions stay
305
+ * valid. Emails with no dark block resolve identically to the light pass and
306
+ * report nothing.
307
+ */
308
+ declare function checkDarkStylesContrastFromDom($: cheerio.CheerioAPI, lightIssues?: AccessibilityIssue[]): AccessibilityIssue[];
309
+ /**
310
+ * Contrast failures that only appear at mobile width. See
311
+ * {@link checkMobileContrastFromDom}.
312
+ */
313
+ declare function checkMobileContrast(html: string, options?: ParseOptions): AccessibilityIssue[];
314
+ /**
315
+ * Contrast failures an email has *only* in dark mode.
316
+ *
317
+ * Clients that invert colours do it selectively, so a palette that passes in
318
+ * light mode can collapse to unreadable once inverted, mid-tone text left
319
+ * alone on a background repainted near-black. This runs the ordinary contrast
320
+ * check over the simulated dark render and reports what is new there.
321
+ *
322
+ * Pass `lightIssues` when the light-mode report is already in hand (the audit
323
+ * has it) to skip re-running the light pass.
324
+ *
325
+ * ponytail: deduped by the element description, so two genuinely identical
326
+ * elements collapse into one report. Track them individually only if the
327
+ * per-element count turns out to matter.
328
+ */
329
+ declare function checkDarkModeContrast(html: string, lightIssues?: AccessibilityIssue[]): AccessibilityIssue[];
278
330
 
279
331
  /**
280
332
  * Analyze images in an HTML email for best practices.
@@ -327,12 +379,18 @@ declare function checkOverflow(html: string, options?: ParseOptions): OverflowRe
327
379
  */
328
380
  declare function checkVisual(html: string, options?: ParseOptions): VisualReport;
329
381
 
382
+ /**
383
+ * Report design drift in an HTML email: near-identical colours, and properties
384
+ * carrying more distinct values than a design system would.
385
+ */
386
+ declare function checkDesignConsistency(html: string, options?: ParseOptions): DesignReport;
387
+
330
388
  interface AuditOptions extends ParseOptions {
331
389
  framework?: Framework;
332
390
  /** Options for spam analysis */
333
391
  spam?: SpamAnalysisOptions;
334
392
  /** Skip specific checks */
335
- skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility" | "inboxPreview" | "size" | "templateVariables" | "overflow" | "visual">;
393
+ skip?: Array<"spam" | "links" | "accessibility" | "images" | "compatibility" | "inboxPreview" | "size" | "templateVariables" | "overflow" | "visual" | "darkContrast" | "mobileContrast" | "design">;
336
394
  }
337
395
  interface AuditReport {
338
396
  compatibility: {
@@ -353,6 +411,12 @@ interface AuditReport {
353
411
  templateVariables: TemplateReport;
354
412
  overflow: OverflowReport;
355
413
  visual: VisualReport;
414
+ /** Contrast failures present only once a client inverts colours. */
415
+ darkContrast: AccessibilityIssue[];
416
+ /** Contrast failures present only below the email's mobile breakpoint. */
417
+ mobileContrast: AccessibilityIssue[];
418
+ /** Design drift: near-identical colours, and properties with no system left. */
419
+ design: DesignReport;
356
420
  }
357
421
  /**
358
422
  * Run all email analysis checks in a single call.
@@ -453,7 +517,7 @@ interface EmailSession {
453
517
  * Simulate dark mode for a specific client.
454
518
  *
455
519
  * Creates an isolated DOM copy per call (simulation mutates the DOM).
456
- * Operates on the **original** HTML if you need dark mode on
520
+ * Operates on the **original** HTML, if you need dark mode on
457
521
  * already-transformed HTML, use the standalone `simulateDarkMode()` instead.
458
522
  */
459
523
  simulateDarkMode(clientId: string): {
@@ -466,7 +530,7 @@ interface EmailSession {
466
530
  * DOM across all read-only analysis operations.
467
531
  *
468
532
  * Use this when you need to call multiple analysis functions on the
469
- * same HTML it eliminates redundant `cheerio.load()` calls.
533
+ * same HTML: it eliminates redundant `cheerio.load()` calls.
470
534
  *
471
535
  * @example
472
536
  * ```typescript
@@ -548,6 +612,21 @@ declare function wcagGrade(ratio: number): WcagGrade;
548
612
  * Returns the flattened [r, g, b] as 0-255 values.
549
613
  */
550
614
  declare function alphaBlend(fg: RGBA, bgR: number, bgG: number, bgB: number): [number, number, number];
615
+ /**
616
+ * Convert sRGB to OKLab.
617
+ *
618
+ * OKLab is built so that straight-line distance between two points matches how
619
+ * different the colours look, which sRGB emphatically does not: `#333` and
620
+ * `#343434` are far apart in RGB terms and identical to a reader.
621
+ */
622
+ declare function rgbToOklab(rgba: RGBA): [number, number, number];
623
+ /**
624
+ * Perceptual distance between two colours, as OKLab delta-E.
625
+ *
626
+ * Roughly: under ~0.02 the two are the same colour to a reader, and around
627
+ * ~0.1 they are clearly different.
628
+ */
629
+ declare function colorDistance(a: RGBA, b: RGBA): number;
551
630
 
552
631
  /**
553
632
  * Downlevel modern CSS in HTML emails to maximize email client compatibility.
@@ -575,4 +654,4 @@ declare function alphaBlend(fg: RGBA, bgR: number, bgG: number, bgB: number): [n
575
654
  */
576
655
  declare function downlevelCSS(html: string): string;
577
656
 
578
- export { AI_FIX_SYSTEM_PROMPT, AT_RULE_FEATURES, AccessibilityReport, AiFixResult, AiProvider, type AuditOptions, type AuditReport, COMPOUND_VALUE_FEATURES, CSSWarning, CSS_FUNCTION_FEATURES, CSS_SUPPORT, CSS_SUPPORT_NOTES, CodeFix, type CreateSessionOptions, DeliverabilityReport, DiffResult, EMAIL_CLIENTS, EMPTY_DELIVERABILITY, EmailClient, type EmailSession, ExportPromptOptions, FEATURE_URLS, Framework, GENERIC_LINK_TEXT, type GenerateAiFixOptions, HTML_ELEMENT_FEATURES, ImageReport, InboxPreview, LinkReport, MAX_HTML_SIZE, MAX_WARNING_LOCATIONS, OverflowReport, type ParseOptions, type RGBA, STRUCTURAL_FIX_PROPERTIES, SizeReport, SpamAnalysisOptions, SpamReport, SupportLevel, TemplateReport, TransformResult, VALUE_CAVEAT_PROPS, VisualReport, type WcagGrade, alphaBlend, analyzeEmail, analyzeImages, analyzeSpam, auditEmail, caveatApplies, checkAccessibility, checkOverflow, checkSize, checkTemplateVariables, checkVisual, contrastRatio, createSession, diffResults, downlevelCSS, errorWarnings, extractInboxPreview, featureUrl, formatRgb, generateAiFix, generateCompatibilityScore, getClient, getCodeFix, getSuggestion, parseColor, relativeLuminance, simulateDarkMode, structuralWarnings, toPlainText, transformForAllClients, transformForClient, validateLinks, warningsForClient, wcagGrade };
657
+ export { AI_FIX_SYSTEM_PROMPT, AT_RULE_FEATURES, AccessibilityIssue, AccessibilityReport, AiFixResult, AiProvider, type AuditOptions, type AuditReport, COMPOUND_VALUE_FEATURES, CSSWarning, CSS_FUNCTION_FEATURES, CSS_SUPPORT, CSS_SUPPORT_NOTES, CodeFix, type CreateSessionOptions, DeliverabilityReport, DesignReport, DiffResult, EMAIL_CLIENTS, EMPTY_DELIVERABILITY, EmailClient, type EmailSession, ExportPromptOptions, FEATURE_URLS, Framework, GENERIC_LINK_TEXT, type GenerateAiFixOptions, HTML_ELEMENT_FEATURES, ImageReport, InboxPreview, LinkReport, MAX_HTML_SIZE, MAX_WARNING_LOCATIONS, OverflowReport, type ParseOptions, type RGBA, type RenderContext, STRUCTURAL_FIX_PROPERTIES, SizeReport, SpamAnalysisOptions, SpamReport, SupportLevel, TemplateReport, TransformResult, VALUE_CAVEAT_PROPS, VisualReport, type WcagGrade, alphaBlend, analyzeEmail, analyzeImages, analyzeSpam, auditEmail, caveatApplies, checkAccessibility, checkDarkModeContrast, checkDarkStylesContrastFromDom, checkDesignConsistency, checkMobileContrast, checkOverflow, checkSize, checkTemplateVariables, checkVisual, colorDistance, contrastRatio, createSession, diffResults, downlevelCSS, errorWarnings, extractInboxPreview, featureUrl, formatRgb, generateAiFix, generateCompatibilityScore, getClient, getCodeFix, getSuggestion, parseColor, relativeLuminance, rgbToOklab, simulateDarkMode, structuralWarnings, toPlainText, transformForAllClients, transformForClient, validateLinks, warningsForClient, wcagGrade };