Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(render): Add useRenderingOptions #1926

Open
wants to merge 17 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d2c1b54
feat(react-email): added a theme switcher to the dev preview (#1749)
KayleeWilliams Feb 17, 2025
eafcd74
fix(cli): Correct typo in cliPacakgeLocation to cliPackageLocation (#…
hautest Feb 19, 2025
049a808
fix(deps): update dependency esbuild to v0.25.0 [security] (#1904)
renovate[bot] Feb 19, 2025
bb43d98
chore(deps-dev): bump vite from 5.4.13 to 5.4.14 (#1899)
dependabot[bot] Feb 19, 2025
1128776
chore(deps-dev): bump vitest from 2.0.5 to 2.1.9 (#1898)
dependabot[bot] Feb 19, 2025
3057904
chore(all): remove vitest from individual pacakges
gabrielmfern Feb 19, 2025
d63d0b7
chore(deps): update dependency clsx to v2.1.1 (#1894)
renovate[bot] Feb 19, 2025
b1fb267
chore(deps): update dependency @changesets/cli to v2.28.0 (#1893)
renovate[bot] Feb 19, 2025
0421473
feat(tailwind): extract pseudo classes to stylesheet (#1864)
Sjoertjuh Feb 26, 2025
15c792f
fix(tailwind): Infinite loop during sanitization
gabrielmfern Feb 27, 2025
6f5f70e
chore(tailwind): Improve code for running Tailwind integration test (…
gabrielmfern Feb 27, 2025
c18d48d
chore(root): Improve caching of CI (#1936)
gabrielmfern Feb 27, 2025
dad0a60
chore(render): Remove duplicate code from renderAsync (#1934)
gabrielmfern Feb 27, 2025
cac5524
chore(deps): update dependency @types/node to v20.17.22 (#1940)
renovate[bot] Mar 3, 2025
8dfb96a
chore(deps): update dependency @changesets/cli to v2.28.1 (#1939)
renovate[bot] Mar 3, 2025
d5fdaa5
feat(render): Add useRenderingOptions
thena-seer Feb 21, 2025
20136ef
use client directive
thena-seer Mar 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-needles-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": minor
---

Theme switcher for email template
5 changes: 5 additions & 0 deletions .changeset/great-parrots-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-email/tailwind": minor
---

Extract tailwind pseudo classes to stylesheet
5 changes: 5 additions & 0 deletions .changeset/ninety-apes-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": patch
---

update esbuild to 0.25.0
101 changes: 61 additions & 40 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -19,95 +21,114 @@ jobs:
corepack enable
corepack prepare [email protected] --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 [email protected] --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 [email protected] --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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ node_modules

# testing
coverage
package-lock.json

# next.js
.next/
Expand Down
27 changes: 26 additions & 1 deletion apps/docs/utilities/render.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Html lang="en">
{options.plainText &&
<Text>This is only visible when rendering as Plain Text.</Text>
}
<Text>Some title</Text>
<Hr />
<Button href="https://example.com">Click me</Button>
</Html>
);
}

export default MyTemplate;
```

## Options

<ResponseField name="pretty" type="boolean">
Expand All @@ -126,4 +152,3 @@ Click me [https://example.com]
<ResponseField name="htmlToTextOptions" type="HtmlToTextOptions">
`html-to-text` [options](https://github.com/html-to-text/node-html-to-text/tree/master/packages/html-to-text#options) used for rendering
</ResponseField>

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
3 changes: 1 addition & 2 deletions packages/code-inline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
9 changes: 4 additions & 5 deletions packages/react-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"
}
}
29 changes: 14 additions & 15 deletions packages/react-email/src/app/preview/[...slug]/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'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';
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';

Expand All @@ -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';

Expand All @@ -43,6 +45,9 @@ const Preview = ({
serverRenderingResult,
);

const iframeRef = useRef<HTMLIFrameElement>(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
Expand All @@ -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 (
<Shell
activeView={hasNoErrors ? activeView : undefined}
currentEmailOpenSlug={slug}
markup={renderedEmailMetadata?.markup}
pathSeparator={pathSeparator}
setActiveView={hasNoErrors ? handleViewChange : undefined}
>
{/* This relative is so that when there is any error the user can still switch between emails */}
<div className="relative h-full">
Expand All @@ -93,22 +90,24 @@ const Preview = ({
<>
{activeView === 'desktop' && (
<iframe
className="w-full bg-white h-[calc(100vh_-_140px)] lg:h-[calc(100vh_-_70px)]"
className="h-[calc(100vh_-_140px)] w-full bg-white lg:h-[calc(100vh_-_70px)]"
ref={iframeRef}
srcDoc={renderedEmailMetadata.markup}
title={slug}
/>
)}

{activeView === 'mobile' && (
<iframe
className="w-[360px] bg-white h-[calc(100vh_-_140px)] lg:h-[calc(100vh_-_70px)] mx-auto"
className="mx-auto h-[calc(100vh_-_140px)] w-[360px] bg-white lg:h-[calc(100vh_-_70px)]"
ref={iframeRef}
srcDoc={renderedEmailMetadata.markup}
title={slug}
/>
)}

{activeView === 'source' && (
<div className="flex gap-6 mx-auto p-6 max-w-3xl">
<div className="mx-auto flex max-w-3xl gap-6 p-6">
<Tooltip.Provider>
<CodeContainer
activeLang={activeLang}
Expand All @@ -126,7 +125,7 @@ const Preview = ({
content: renderedEmailMetadata.plainText,
},
]}
setActiveLang={handleLangChange}
setActiveLang={setActiveLang}
/>
</Tooltip.Provider>
</div>
Expand Down
Loading
Loading