diff --git a/.changeset/dirty-needles-chew.md b/.changeset/dirty-needles-chew.md new file mode 100644 index 0000000000..acc7dd42e2 --- /dev/null +++ b/.changeset/dirty-needles-chew.md @@ -0,0 +1,5 @@ +--- +"react-email": minor +--- + +Theme switcher for email template diff --git a/.changeset/great-parrots-yell.md b/.changeset/great-parrots-yell.md new file mode 100644 index 0000000000..429c2c034d --- /dev/null +++ b/.changeset/great-parrots-yell.md @@ -0,0 +1,5 @@ +--- +"@react-email/tailwind": minor +--- + +Extract tailwind pseudo classes to stylesheet diff --git a/.changeset/ninety-apes-love.md b/.changeset/ninety-apes-love.md new file mode 100644 index 0000000000..2a3739da36 --- /dev/null +++ b/.changeset/ninety-apes-love.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +update esbuild to 0.25.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 496550d323..9b72873f37 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,10 @@ on: - main pull_request: jobs: - lint: + build: runs-on: buildjet-4vcpu-ubuntu-2204 + outputs: + cache-hit: ${{ steps.pnpm-cache.outputs.cache-hit }} container: image: node:22 steps: @@ -19,95 +21,114 @@ jobs: corepack enable corepack prepare pnpm@9.15.0 --activate pnpm config set script-shell "/usr/bin/bash" - echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - name: pnpm Cache uses: buildjet/cache@v4 with: - path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: | + ~/.pnpm-store + node_modules + */*/node_modules + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-pnpm-store- + ${{ runner.os }}-pnpm- - name: Install packages + if: steps.pnpm-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile + - name: turborepo Cache + uses: buildjet/cache@v4 + with: + path: | + .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + - name: Run Build run: pnpm build - - name: Run Lint - run: pnpm lint - - test: + lint: runs-on: buildjet-4vcpu-ubuntu-2204 + needs: [build] container: image: node:22 steps: - name: Checkout uses: actions/checkout@v4 - - name: Enable Corepack - id: pnpm-setup + - name: Setup pnpm run: | corepack enable corepack prepare pnpm@9.15.0 --activate - pnpm config set script-shell "/usr/bin/bash" - echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - - name: pnpm Cache + - name: Restore dependencies uses: buildjet/cache@v4 with: - path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + path: | + ~/.pnpm-store + node_modules + */*/node_modules + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - - name: Install packages - run: pnpm install --frozen-lockfile + - name: turborepo Cache + uses: buildjet/cache@v4 + with: + path: | + .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- - name: Run Build run: pnpm build - - name: Run Tests - run: pnpm test - env: - SPAM_ASSASSIN_HOST: ${{ secrets.SPAM_ASSASSIN_HOST }} - SPAM_ASSASSIN_PORT: ${{ secrets.SPAM_ASSASSIN_PORT }} + - name: Run Lint + run: pnpm lint - build: + test: runs-on: buildjet-4vcpu-ubuntu-2204 + needs: [build] container: image: node:22 steps: - name: Checkout uses: actions/checkout@v4 - - name: Enable Corepack - id: pnpm-setup + - name: Setup pnpm run: | corepack enable corepack prepare pnpm@9.15.0 --activate - pnpm config set script-shell "/usr/bin/bash" - echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" - - name: pnpm Cache + - name: Restore dependencies uses: buildjet/cache@v4 with: - path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + path: | + ~/.pnpm-store + node_modules + */*/node_modules + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - - name: Install packages - run: pnpm install --frozen-lockfile + - name: turborepo Cache + uses: buildjet/cache@v4 + with: + path: | + .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- - - name: Run Build - run: pnpm build + - name: Run Tests + run: pnpm test + env: + SPAM_ASSASSIN_HOST: ${{ secrets.SPAM_ASSASSIN_HOST }} + SPAM_ASSASSIN_PORT: ${{ secrets.SPAM_ASSASSIN_PORT }} dependencies: runs-on: buildjet-4vcpu-ubuntu-2204 container: - image: node:18 + image: node:22 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index d76b05d3b1..26484f7ef8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules # testing coverage +package-lock.json # next.js .next/ diff --git a/apps/docs/utilities/render.mdx b/apps/docs/utilities/render.mdx index 4adf371a4f..0bd8d234ff 100644 --- a/apps/docs/utilities/render.mdx +++ b/apps/docs/utilities/render.mdx @@ -115,6 +115,32 @@ Some title Click me [https://example.com] ``` +## 5. Customize template rendering further +With `useRenderingOptions()` you can get the render options from inside your template. + +For example, if you want some part of your template to only render when rendering as plain text, you could create your template as: + +```jsx email.jsx +import * as React from 'react'; +import { Html, Button, Hr, Text, useRenderingOptions } from "@react-email/components"; + +export function MyTemplate() { + const options = useRenderingOptions(); + return ( + + {options.plainText && + This is only visible when rendering as Plain Text. + } + Some title +
+ + + ); +} + +export default MyTemplate; +``` + ## Options @@ -126,4 +152,3 @@ Click me [https://example.com] `html-to-text` [options](https://github.com/html-to-text/node-html-to-text/tree/master/packages/html-to-text#options) used for rendering - diff --git a/package.json b/package.json index ab9f87f084..b785ad0489 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@changesets/cli": "2.27.11", + "@changesets/cli": "2.28.1", "@types/node": "22.10.2", "@types/react": "19.0.1", "@types/react-dom": "19.0.1", @@ -27,8 +27,8 @@ "tsconfig": "workspace:*", "tsup": "8.2.4", "turbo": "2.3.1", - "vite": "5.4.13", - "vitest": "2.0.5" + "vite": "5.4.14", + "vitest": "2.1.9" }, "pnpm": { "overrides": { diff --git a/packages/code-inline/package.json b/packages/code-inline/package.json index 43b8ae2144..7f334912a7 100644 --- a/packages/code-inline/package.json +++ b/packages/code-inline/package.json @@ -38,7 +38,6 @@ "@react-email/render": "workspace:*", "tsconfig": "workspace:*", "tsup": "7.2.0", - "typescript": "5.1.6", - "vitest": "1.1.0" + "typescript": "5.1.6" } } diff --git a/packages/react-email/package.json b/packages/react-email/package.json index c2b81a2ab2..43a832e769 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -29,7 +29,7 @@ "chokidar": "4.0.3", "commander": "11.1.0", "debounce": "2.0.0", - "esbuild": "0.23.0", + "esbuild": "0.25.0", "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", @@ -45,7 +45,7 @@ "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-toggle-group": "1.1.0", "@radix-ui/react-tooltip": "1.1.2", - "@react-email/render": "workspace:*", + "@react-email/components": "workspace:*", "@swc/core": "1.4.15", "@types/babel__core": "7.20.5", "@types/fs-extra": "11.0.1", @@ -57,7 +57,7 @@ "@types/webpack": "5.28.5", "@vercel/style-guide": "5.1.0", "autoprefixer": "10.4.20", - "clsx": "2.1.0", + "clsx": "2.1.1", "framer-motion": "12.0.0-alpha.2", "postcss": "8.4.40", "prism-react-renderer": "2.1.0", @@ -73,7 +73,6 @@ "tailwindcss": "3.4.0", "tsup": "7.2.0", "tsx": "4.9.0", - "typescript": "5.1.6", - "vitest": "1.1.3" + "typescript": "5.1.6" } } diff --git a/packages/react-email/src/app/preview/[...slug]/preview.tsx b/packages/react-email/src/app/preview/[...slug]/preview.tsx index e33195ede1..3f1c664aee 100644 --- a/packages/react-email/src/app/preview/[...slug]/preview.tsx +++ b/packages/react-email/src/app/preview/[...slug]/preview.tsx @@ -1,7 +1,7 @@ 'use client'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import React from 'react'; +import React, { useRef } from 'react'; import { Toaster } from 'sonner'; import type { EmailRenderingResult } from '../../../actions/render-email-by-path'; import { CodeContainer } from '../../../components/code-container'; @@ -9,6 +9,7 @@ import { Shell } from '../../../components/shell'; import { Tooltip } from '../../../components/tooltip'; import { useEmailRenderingResult } from '../../../hooks/use-email-rendering-result'; import { useHotreload } from '../../../hooks/use-hot-reload'; +import { useIframeColorScheme } from '../../../hooks/use-iframe-color-scheme'; import { useRenderingMetadata } from '../../../hooks/use-rendering-metadata'; import { RenderingError } from './rendering-error'; @@ -29,6 +30,7 @@ const Preview = ({ const pathname = usePathname(); const searchParams = useSearchParams(); + const activeTheme = searchParams.get('theme') ?? 'light'; const activeView = searchParams.get('view') ?? 'desktop'; const activeLang = searchParams.get('lang') ?? 'jsx'; @@ -43,6 +45,9 @@ const Preview = ({ serverRenderingResult, ); + const iframeRef = useRef(null); + useIframeColorScheme(iframeRef, activeTheme); + if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') { // this will not change on runtime so it doesn't violate // the rules of hooks @@ -60,28 +65,20 @@ const Preview = ({ }); } - const handleViewChange = (view: string) => { - const params = new URLSearchParams(searchParams); - params.set('view', view); - router.push(`${pathname}?${params.toString()}`); - }; + const hasNoErrors = typeof renderedEmailMetadata !== 'undefined'; - const handleLangChange = (lang: string) => { + const setActiveLang = (lang: string) => { const params = new URLSearchParams(searchParams); params.set('view', 'source'); params.set('lang', lang); router.push(`${pathname}?${params.toString()}`); }; - const hasNoErrors = typeof renderedEmailMetadata !== 'undefined'; - return ( {/* This relative is so that when there is any error the user can still switch between emails */}
@@ -93,7 +90,8 @@ const Preview = ({ <> {activeView === 'desktop' && (