From 0e7ae4c2c2a2d171ab7b6bf8e1936115ee14d9dc Mon Sep 17 00:00:00 2001
From: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com>
Date: Thu, 16 Oct 2025 18:21:27 +0000
Subject: [PATCH 1/8] feat(demo): use Tailwind v4 on all templates (#2487)
Co-authored-by: gabriel miranda
{validationCode}
-
+ {validationCode}
+
+ {loginCode}
-
+ {loginCode}
+
+
-
+
-
+
+
-
-
+
+
-
+
+
-
-
+
+
+
-
-
+
+
-
-
-
-
+
-
+
+
+
+
+
-
-
-
-
+
-
-
+ | + style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'> |
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello alanturing,
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Alan (alan.turing@example.com) has invited you to the Enigma team on @@ -110,7 +109,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" + style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" width="64" />
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser: https://vercel.com
+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for alanturing. This invite was sent from diff --git a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap index 615acfb5ad..7e4c1a61e7 100644 --- a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap +++ b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap @@ -10,8 +10,7 @@ exports[`email export 1`] = ` -
+| + style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'> |
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello ,
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px">
() has invited you to the team on
Vercel.
@@ -106,7 +105,7 @@ exports[`email export 1`] = `
+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser:
+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for . This invite was sent from diff --git a/packages/tailwind/copy-tailwind-types.mjs b/packages/tailwind/copy-tailwind-types.mjs deleted file mode 100644 index 6fe7008614..0000000000 --- a/packages/tailwind/copy-tailwind-types.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import url from 'node:url'; - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); - -await fs.cp( - path.resolve(__dirname, './node_modules/tailwindcss/types'), - path.resolve(__dirname, './dist/tailwindcss'), - { - recursive: true, - }, -); diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 43ed1c5e28..7838e65955 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/tailwind", - "version": "1.2.2", + "version": "2.0.0-tailwindv4.4", "description": "A React component to wrap emails with Tailwind CSS", "sideEffects": false, "main": "./dist/index.js", @@ -23,8 +23,8 @@ }, "license": "MIT", "scripts": { - "build": "tsc && cross-env NODE_ENV=production vite build --mode production && node ./copy-tailwind-types.mjs", - "build:watch": "vite build --watch", + "build": "tsdown", + "build:watch": "tsdown --watch", "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest" @@ -43,7 +43,50 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "@react-email/body": "workspace:*", + "@react-email/button": "workspace:*", + "@react-email/code-block": "workspace:*", + "@react-email/code-inline": "workspace:*", + "@react-email/container": "workspace:*", + "@react-email/heading": "workspace:*", + "@react-email/hr": "workspace:*", + "@react-email/img": "workspace:*", + "@react-email/link": "workspace:*", + "@react-email/preview": "workspace:*", + "@react-email/text": "workspace:*" + }, + "peerDependenciesMeta": { + "@react-email/button": { + "optional": true + }, + "@react-email/body": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } }, "devDependencies": { "@react-email/button": "workspace:^", @@ -54,14 +97,12 @@ "@react-email/link": "workspace:*", "@react-email/render": "workspace:*", "@responsive-email/react-email": "0.0.4", + "@types/css-tree": "2.3.10", "@types/shelljs": "0.8.15", "@vitejs/plugin-react": "4.4.1", - "cross-env": "10.0.0", - "postcss": "8.5.3", - "postcss-selector-parser": "7.1.0", + "css-tree": "3.1.0", "react-dom": "^19", "shelljs": "0.9.2", - "tailwindcss": "3.4.10", "tsconfig": "workspace:*", "typescript": "5.8.3", "vite": "6.3.6", @@ -70,5 +111,8 @@ }, "publishConfig": { "access": "public" + }, + "dependencies": { + "tailwindcss": "4.1.12" } } diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 0bdcd1b93d..c6edea3134 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -1,52 +1,225 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Custom plugins config > uses custom plugins 1`] = `"
"`; +exports[`Tailwind component > elements 1`] = ` +" + + + + + + + + + + + + +" +`; + +exports[`Tailwind component > with non-inlinable styles > throws an error when used without a 1`] = ` +[Error: You are trying to use the following Tailwind classes that cannot be inlined: sm:bg-red-500. +For the media queries to work properly on rendering, they need to be added into a + + + + + + +" +`; + +exports[`Tailwind component > with non-inlinable styles > works with arbitrarily deep (in the React tree) elements 2`] = `""`; + +exports[`Tailwind component > with non-inlinable styles > works with relatively complex media query utilities 1`] = ` " - -I am some text
+ " `; -exports[`Tailwind component > works properly with 'no-underline' 1`] = `"or copy and paste this URL into your browser: https://react.email
or copy and paste this URL into your browser: https://react.email
"`; +exports[`Tailwind component > works properly with 'no-underline' 1`] = `"or copy and paste this URL into your browser: https://react.email
or copy and paste this URL into your browser: https://react.email
"`; exports[`Tailwind component > works with Heading component 1`] = `"HelloReact Email
React Email
React Email
React Email
I am some text
"`; diff --git a/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap b/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap deleted file mode 100644 index baebff2970..0000000000 --- a/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`useTailwind() 1`] = ` -".text-red-500 { - color: rgb(239 68 68 / 1) -} - @media (min-width: 640px) { - .sm\\:bg-blue-300 { - background-color: rgb(147 197 253 / 1) - } -} - .bg-slate-900 { - background-color: rgb(15 23 42 / 1) -} -" -`; diff --git a/packages/tailwind/src/hooks/use-suspensed-promise.ts b/packages/tailwind/src/hooks/use-suspended-promise.ts similarity index 100% rename from packages/tailwind/src/hooks/use-suspensed-promise.ts rename to packages/tailwind/src/hooks/use-suspended-promise.ts diff --git a/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts b/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts index a43199c247..2063a1ab49 100644 --- a/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts +++ b/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts @@ -1,5 +1,5 @@ /** biome-ignore-all lint/correctness/useHookAtTopLevel: function is not a React hook */ -import { useSuspensedPromise } from './use-suspensed-promise'; +import { useSuspensedPromise } from './use-suspended-promise'; describe('useSuspensedPromise', () => { beforeEach(() => {}); diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index cc05724cbd..292521e000 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -4,10 +4,9 @@ import { Heading } from '@react-email/heading'; import { Hr } from '@react-email/hr'; import { Html } from '@react-email/html'; import { Link } from '@react-email/link'; -import { render } from '@react-email/render'; +import { pretty, render } from '@react-email/render'; import { ResponsiveColumn, ResponsiveRow } from '@responsive-email/react-email'; import React from 'react'; -import { vi } from 'vitest'; import type { TailwindConfig } from '.'; import { Tailwind } from '.'; @@ -26,7 +25,11 @@ describe('Tailwind component', () => { it('works with blocklist', async () => { const actualOutput = await render( -I am some text
- - ); - }; + it('works with relatively complex media query utilities', async () => { + const Email = () => { + return ( +I am some text
+ + ); + }; - expect(await render(