From c3cfd705863a854a212323218f0fa61376659942 Mon Sep 17 00:00:00 2001 From: sjhddh Date: Sun, 19 Apr 2026 16:02:01 +0200 Subject: [PATCH] fix(ui): detect hsl()/hsla() as Outlook-incompatible CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Outlook compatibility checker already warns on oklch and similar modern color functions, but misses hsl()/hsla(). Outlook strips these silently. Add them to the detection list. caniemail.com has no hsl() entry, and `pnpm caniemail:fetch` fully overwrites `caniemail-data.ts` — so this adds a hand-curated overlay at `custom-support-entries.ts`, spread into the checker loop at runtime. Closes #2947 Co-Authored-By: Claude Opus 4.7 --- .../check-compatibility.spec.ts | 54 +++++++++++++++++++ .../email-validation/check-compatibility.ts | 3 +- .../custom-support-entries.ts | 40 ++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/actions/email-validation/check-compatibility.spec.ts create mode 100644 packages/ui/src/actions/email-validation/custom-support-entries.ts diff --git a/packages/ui/src/actions/email-validation/check-compatibility.spec.ts b/packages/ui/src/actions/email-validation/check-compatibility.spec.ts new file mode 100644 index 0000000000..0079f73917 --- /dev/null +++ b/packages/ui/src/actions/email-validation/check-compatibility.spec.ts @@ -0,0 +1,54 @@ +import { + type CompatibilityCheckingResult, + checkCompatibility, +} from './check-compatibility'; + +const collect = async (reactCode: string) => { + const results: CompatibilityCheckingResult[] = []; + const stream = await checkCompatibility(reactCode, ''); + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (value) results.push(value); + if (done) break; + } + return results; +}; + +const findHslResult = (results: CompatibilityCheckingResult[]) => + results.find((result) => result.entry.slug === 'css-hsl-hsla'); + +describe('checkCompatibility() — hsl()/hsla() detection', () => { + it('flags background-color: hsl(...) as Outlook-incompatible', async () => { + const results = await collect( + "
", + ); + const hslResult = findHslResult(results); + expect(hslResult).toBeDefined(); + expect(hslResult?.status).toBe('error'); + expect(hslResult?.statsPerEmailClient.outlook?.status).toBe('error'); + }); + + it('flags color: hsla(...) as Outlook-incompatible', async () => { + const results = await collect( + "
", + ); + const hslResult = findHslResult(results); + expect(hslResult).toBeDefined(); + expect(hslResult?.status).toBe('error'); + }); + + it('tolerates whitespace variations in hsl() arguments', async () => { + const results = await collect( + "
", + ); + expect(findHslResult(results)).toBeDefined(); + }); + + it('does not flag hex or rgb() colors', async () => { + const results = await collect( + "
", + ); + expect(findHslResult(results)).toBeUndefined(); + }); +}); diff --git a/packages/ui/src/actions/email-validation/check-compatibility.ts b/packages/ui/src/actions/email-validation/check-compatibility.ts index 5852040d26..1292d7819f 100644 --- a/packages/ui/src/actions/email-validation/check-compatibility.ts +++ b/packages/ui/src/actions/email-validation/check-compatibility.ts @@ -24,6 +24,7 @@ import { getElementAttributes } from '../../utils/caniemail/get-element-attribut import { getElementNames } from '../../utils/caniemail/get-element-names'; import { snakeToCamel } from '../../utils/snake-to-camel'; import { supportEntries } from './caniemail-data'; +import { customSupportEntries } from './custom-support-entries'; export interface CompatibilityCheckingResult { location: SourceLocation; @@ -146,7 +147,7 @@ export const checkCompatibility = async ( ); const readableStream = new ReadableStream({ async start(controller) { - for (const entry of supportEntries) { + for (const entry of [...supportEntries, ...customSupportEntries]) { const compatibilityStats = getCompatibilityStatsForEntry( entry, relevantEmailClients, diff --git a/packages/ui/src/actions/email-validation/custom-support-entries.ts b/packages/ui/src/actions/email-validation/custom-support-entries.ts new file mode 100644 index 0000000000..c965724f99 --- /dev/null +++ b/packages/ui/src/actions/email-validation/custom-support-entries.ts @@ -0,0 +1,40 @@ +import type { SupportEntry } from './check-compatibility'; + +/** + * Manually curated compatibility entries that aren't covered by caniemail's + * dataset. These get merged with `supportEntries` at runtime, so regenerating + * `caniemail-data.ts` via `pnpm caniemail:fetch` won't wipe them. + */ +export const customSupportEntries: SupportEntry[] = [ + // https://github.com/resend/react-email/issues/2947 + // Outlook (Windows versions using Word's MS rendering engine) silently + // strips `hsl()`/`hsla()` color functions. Caniemail.com doesn't ship an + // entry for this, so we curate one here so the checker can flag it. + { + slug: 'css-hsl-hsla', + title: 'hsl(), hsla()', + description: + 'HSL and HSLA color functions. Outlook on Windows silently drops these declarations, so hex or rgb() is safer for broad email client support.', + url: 'https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl', + category: 'css', + tags: [], + keywords: 'color,hsl,hsla', + last_test_date: '2026-04-19', + test_url: + 'https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl', + test_results_url: null, + stats: { + outlook: { + windows: [ + { '2007': 'n' }, + { '2010': 'n' }, + { '2013': 'n' }, + { '2016': 'n' }, + { '2019': 'n' }, + ], + }, + }, + notes: null, + notes_by_num: null, + }, +];