From 8aa8f8e9d9c7446dd957093fa93d0f24784dcfea Mon Sep 17 00:00:00 2001 From: Kaylee <65376239+KayleeWilliams@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:39:15 +0000 Subject: [PATCH 01/33] feat(react-email): added a theme switcher to the dev preview (#1749) Signed-off-by: dependabot[bot] Co-authored-by: gabriel miranda Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bu Kinoshita <6929565+bukinoshita@users.noreply.github.com> --- .changeset/dirty-needles-chew.md | 5 ++ .../src/app/preview/[...slug]/preview.tsx | 64 +++++++++----- .../src/components/icons/icon-moon.tsx | 16 ++++ .../src/components/icons/icon-sun.tsx | 16 ++++ .../components/topbar/theme-toggle-group.tsx | 84 +++++++++++++++++++ .../src/hooks/use-iframe-color-scheme.ts | 35 ++++++++ 6 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 .changeset/dirty-needles-chew.md create mode 100644 packages/preview-server/src/components/icons/icon-moon.tsx create mode 100644 packages/preview-server/src/components/icons/icon-sun.tsx create mode 100644 packages/preview-server/src/components/topbar/theme-toggle-group.tsx create mode 100644 packages/preview-server/src/hooks/use-iframe-color-scheme.ts 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/packages/preview-server/src/app/preview/[...slug]/preview.tsx b/packages/preview-server/src/app/preview/[...slug]/preview.tsx index ef5c09cde3..1cb68a7f36 100644 --- a/packages/preview-server/src/app/preview/[...slug]/preview.tsx +++ b/packages/preview-server/src/app/preview/[...slug]/preview.tsx @@ -1,7 +1,7 @@ 'use client'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { flushSync } from 'react-dom'; import { Toaster } from 'sonner'; import { useDebouncedCallback } from 'use-debounce'; @@ -15,9 +15,11 @@ import { Send } from '../../../components/send'; import { useToolbarState } from '../../../components/toolbar'; import { Tooltip } from '../../../components/tooltip'; import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group'; +import { ThemeToggleGroup } from '../../../components/topbar/theme-toggle-group'; import { ViewSizeControls } from '../../../components/topbar/view-size-controls'; import { usePreviewContext } from '../../../contexts/preview'; import { useClampedState } from '../../../hooks/use-clamped-state'; +import { useIframeColorScheme } from '../../../hooks/use-iframe-color-scheme'; import { cn } from '../../../utils'; import { ErrorOverlay } from './error-overlay'; @@ -32,9 +34,17 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => { const pathname = usePathname(); const searchParams = useSearchParams(); - const activeView = searchParams.get('view') ?? 'preview'; + const activeTheme: 'dark' | 'light' = + searchParams.get('theme') === 'dark' ? 'dark' : 'light'; + const activeView = searchParams.get('view') ?? 'desktop'; const activeLang = searchParams.get('lang') ?? 'jsx'; + const handleThemeChange = (theme: 'dark' | 'light') => { + const params = new URLSearchParams(searchParams); + params.set('theme', theme); + router.push(`${pathname}?${params.toString()}`); + }; + const handleViewChange = (view: string) => { const params = new URLSearchParams(searchParams); params.set('view', view); @@ -51,6 +61,9 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => { ); }; + const iframeRef = useRef(null); + useIframeColorScheme(iframeRef, activeTheme); + const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined'; const hasErrors = 'error' in renderingResult; @@ -83,26 +96,32 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => { return ( <> - {activeView === 'preview' && ( - { - setHeight(height); - flushSync(() => { - handleSaveViewSize(); - }); - }} - setViewWidth={(width) => { - setWidth(width); - flushSync(() => { - handleSaveViewSize(); - }); - }} - viewHeight={height} - viewWidth={width} - minWidth={minWidth} - minHeight={minHeight} - /> - )} + {activeView === 'preview' ? ( + <> + { + setHeight(height); + flushSync(() => { + handleSaveViewSize(); + }); + }} + setViewWidth={(width) => { + setWidth(width); + flushSync(() => { + handleSaveViewSize(); + }); + }} + viewHeight={height} + viewWidth={width} + minWidth={minWidth} + minHeight={minHeight} + /> + handleThemeChange(theme)} + /> + + ) : null} {