Skip to content

fix(tailwind): Color-scheme media queries not getting sanitized sometimes #1929

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

Open
wants to merge 12 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/calm-birds-spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-email/tailwind": patch
---

Fix color-scheme media queries sometimes not getting sanitized at the className
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/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
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.0",
"@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"
}
}
7 changes: 3 additions & 4 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 Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions packages/react-email/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getEmailsDirectoryMetadata,
} from '../../utils/get-emails-directory-metadata';
import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping';
import { cliPacakgeLocation } from '../utils';
import { cliPackageLocation } from '../utils';

interface Args {
dir: string;
Expand Down Expand Up @@ -242,7 +242,7 @@ export const build = async ({
}

spinner.text = 'Copying preview app from CLI to `.react-email`';
await fs.promises.cp(cliPacakgeLocation, builtPreviewAppPath, {
await fs.promises.cp(cliPackageLocation, builtPreviewAppPath, {
recursive: true,
filter: (source: string) => {
// do not copy the CLI files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const safeAsyncServerListen = (server: http.Server, port: number) => {
};

export const isDev = !__filename.endsWith(path.join('cli', 'index.js'));
export const cliPacakgeLocation = isDev
export const cliPackageLocation = isDev
? path.resolve(__dirname, '../../../..')
: path.resolve(__dirname, '../..');
export const previewServerLocation = isDev
Expand Down
16 changes: 16 additions & 0 deletions packages/react-email/src/components/icons/icon-moon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type { IconElement, IconProps } from './icon-base';
import { IconBase } from './icon-base';

export const IconMoon = React.forwardRef<IconElement, Readonly<IconProps>>(
({ ...props }, forwardedRef) => (
<IconBase ref={forwardedRef} {...props}>
<path
fill="currentColor"
d="m17.75 4.09l-2.53 1.94l.91 3.06l-2.63-1.81l-2.63 1.81l.91-3.06l-2.53-1.94L12.44 4l1.06-3l1.06 3zm3.5 6.91l-1.64 1.25l.59 1.98l-1.7-1.17l-1.7 1.17l.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85c-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14c.4-.4.82-.76 1.27-1.08c.75-.53 1.93.36 1.85 1.19c-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82c-2.81 3.14-2.7 7.96.31 10.98c3.02 3.01 7.84 3.12 10.98.31"
/>
</IconBase>
),
);

IconMoon.displayName = 'IconMoon';
16 changes: 16 additions & 0 deletions packages/react-email/src/components/icons/icon-sun.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type { IconElement, IconProps } from './icon-base';
import { IconBase } from './icon-base';

export const IconSun = React.forwardRef<IconElement, Readonly<IconProps>>(
({ ...props }, forwardedRef) => (
<IconBase ref={forwardedRef} {...props}>
<path
fill="currentColor"
d="m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2"
/>
</IconBase>
),
);

IconSun.displayName = 'IconSun';
8 changes: 1 addition & 7 deletions packages/react-email/src/components/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ interface ShellProps extends RootProps {
markup?: string;
currentEmailOpenSlug?: string;
pathSeparator?: string;
activeView?: string;
setActiveView?: (view: string) => void;
}

export const Shell = ({
currentEmailOpenSlug,
children,
pathSeparator,
markup,
activeView,
setActiveView,
}: ShellProps) => {
const [sidebarToggled, setSidebarToggled] = React.useState(false);
const [triggerTransition, setTriggerTransition] = React.useState(false);
Expand Down Expand Up @@ -89,7 +85,6 @@ export const Shell = ({
>
{currentEmailOpenSlug && pathSeparator ? (
<Topbar
activeView={activeView}
currentEmailOpenSlug={currentEmailOpenSlug}
markup={markup}
onToggleSidebar={() => {
Expand All @@ -104,11 +99,10 @@ export const Shell = ({
}, 300);
}}
pathSeparator={pathSeparator}
setActiveView={setActiveView}
/>
) : null}

<div className="h-[calc(100vh_-_70px)] overflow-auto mx-auto">
<div className="h-[calc(100vh_-_70px)] overflow-auto mx-auto ">
{children}
</div>
</main>
Expand Down
Loading
Loading