react-email 1.7.2 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_preview/components.js +1 -74
- package/dist/_preview/pages.js +1 -19
- package/dist/_preview/root.js +1 -26
- package/dist/_preview/styles.js +1 -6
- package/dist/_preview/utils.js +1 -22
- package/dist/package.json +54 -0
- package/dist/source/_preview/components.d.ts +4 -0
- package/dist/source/_preview/components.js +77 -0
- package/dist/source/_preview/pages.d.ts +9 -0
- package/dist/source/_preview/pages.js +22 -0
- package/dist/source/_preview/root.d.ts +4 -0
- package/dist/source/_preview/root.js +29 -0
- package/dist/source/_preview/styles.d.ts +4 -0
- package/dist/source/_preview/styles.js +9 -0
- package/dist/source/_preview/utils.d.ts +4 -0
- package/dist/source/_preview/utils.js +25 -0
- package/dist/source/commands/dev.d.ts +1 -0
- package/dist/source/commands/dev.js +186 -0
- package/dist/source/commands/export.d.ts +2 -0
- package/dist/source/commands/export.js +80 -0
- package/dist/source/index.d.ts +2 -0
- package/dist/source/index.js +27 -0
- package/dist/source/utils/check-directory-exist.d.ts +1 -0
- package/dist/source/utils/check-directory-exist.js +9 -0
- package/dist/source/utils/check-empty-directory.d.ts +1 -0
- package/dist/source/utils/check-empty-directory.js +12 -0
- package/dist/source/utils/check-is-up-to-date.d.ts +2 -0
- package/dist/source/utils/check-is-up-to-date.js +26 -0
- package/dist/source/utils/constants.d.ts +10 -0
- package/dist/source/utils/constants.js +22 -0
- package/dist/source/utils/create-directory.d.ts +1 -0
- package/dist/source/utils/create-directory.js +9 -0
- package/dist/source/utils/index.d.ts +6 -0
- package/dist/source/utils/index.js +22 -0
- package/dist/source/utils/watcher.d.ts +3 -0
- package/dist/source/utils/watcher.js +35 -0
- package/package.json +1 -1
- package/preview/package.json +2 -1
- package/preview/src/components/code-container.tsx +32 -11
- package/preview/src/components/icon-button.tsx +1 -1
- package/preview/src/components/sidebar.tsx +70 -48
- package/preview/src/components/topbar.tsx +64 -39
- package/preview/src/pages/preview/[slug].tsx +2 -7
- package/source/_preview/components.ts +4 -4
- package/source/_preview/pages.ts +1 -1
- package/source/_preview/root.ts +1 -1
- package/source/index.ts +2 -1
- package/source/utils/constants.ts +1 -1
- package/source/utils/watcher.ts +10 -10
- package/tsconfig.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import classnames from 'classnames';
|
|
3
3
|
import { Heading } from './heading';
|
|
4
|
-
import { Text } from './text';
|
|
5
4
|
import { Send } from './send';
|
|
6
5
|
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
|
6
|
+
import { AnimateSharedLayout, motion } from 'framer-motion';
|
|
7
7
|
|
|
8
8
|
type TopbarElement = React.ElementRef<'header'>;
|
|
9
9
|
type RootProps = React.ComponentPropsWithoutRef<'header'>;
|
|
@@ -20,6 +20,7 @@ export const Topbar = React.forwardRef<TopbarElement, Readonly<TopbarProps>>(
|
|
|
20
20
|
{ className, title, markup, viewMode, setViewMode, ...props },
|
|
21
21
|
forwardedRef,
|
|
22
22
|
) => {
|
|
23
|
+
const [hovered, setHovered] = React.useState('');
|
|
23
24
|
const columnWidth = 'w-[200px]';
|
|
24
25
|
|
|
25
26
|
return (
|
|
@@ -38,45 +39,69 @@ export const Topbar = React.forwardRef<TopbarElement, Readonly<TopbarProps>>(
|
|
|
38
39
|
</div>
|
|
39
40
|
|
|
40
41
|
<div className={`${columnWidth}`}>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<ToggleGroup.Item
|
|
55
|
-
className={classnames(
|
|
56
|
-
'text-sm text-slate-11 rounded px-1.5 py-0.5',
|
|
57
|
-
{
|
|
58
|
-
'text-slate-12 bg-slate-3 font-medium':
|
|
59
|
-
viewMode === 'desktop',
|
|
60
|
-
},
|
|
61
|
-
)}
|
|
62
|
-
value="desktop"
|
|
42
|
+
<AnimateSharedLayout>
|
|
43
|
+
{setViewMode && (
|
|
44
|
+
<ToggleGroup.Root
|
|
45
|
+
className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden"
|
|
46
|
+
type="single"
|
|
47
|
+
value={viewMode}
|
|
48
|
+
aria-label="View mode"
|
|
49
|
+
onValueChange={(value) => {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return setViewMode('desktop');
|
|
52
|
+
}
|
|
53
|
+
setViewMode(value);
|
|
54
|
+
}}
|
|
63
55
|
>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
<ToggleGroup.Item value="desktop">
|
|
57
|
+
<motion.div
|
|
58
|
+
className={classnames(
|
|
59
|
+
'text-sm text-slate-11 font-medium px-3 py-2 transition ease-in-out duration-200 relative',
|
|
60
|
+
{
|
|
61
|
+
'text-slate-12': viewMode === 'desktop',
|
|
62
|
+
},
|
|
63
|
+
)}
|
|
64
|
+
onHoverStart={() => setHovered('desktop')}
|
|
65
|
+
onHoverEnd={() => setHovered('')}
|
|
66
|
+
>
|
|
67
|
+
{hovered === 'desktop' && (
|
|
68
|
+
<motion.span
|
|
69
|
+
layoutId="nav"
|
|
70
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
71
|
+
initial={{ opacity: 0 }}
|
|
72
|
+
animate={{ opacity: 1 }}
|
|
73
|
+
exit={{ opacity: 0 }}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
Desktop
|
|
77
|
+
</motion.div>
|
|
78
|
+
</ToggleGroup.Item>
|
|
79
|
+
<ToggleGroup.Item value="source">
|
|
80
|
+
<motion.div
|
|
81
|
+
className={classnames(
|
|
82
|
+
'text-sm text-slate-11 font-medium px-3 py-2 transition ease-in-out duration-200 relative',
|
|
83
|
+
{
|
|
84
|
+
'text-slate-12': viewMode === 'source',
|
|
85
|
+
},
|
|
86
|
+
)}
|
|
87
|
+
onHoverStart={() => setHovered('source')}
|
|
88
|
+
onHoverEnd={() => setHovered('')}
|
|
89
|
+
>
|
|
90
|
+
{hovered === 'source' && (
|
|
91
|
+
<motion.span
|
|
92
|
+
layoutId="nav"
|
|
93
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
94
|
+
initial={{ opacity: 0 }}
|
|
95
|
+
animate={{ opacity: 1 }}
|
|
96
|
+
exit={{ opacity: 0 }}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
Source
|
|
100
|
+
</motion.div>
|
|
101
|
+
</ToggleGroup.Item>
|
|
102
|
+
</ToggleGroup.Root>
|
|
103
|
+
)}
|
|
104
|
+
</AnimateSharedLayout>
|
|
80
105
|
</div>
|
|
81
106
|
|
|
82
107
|
{markup && (
|
|
@@ -5,7 +5,6 @@ import { render } from '@react-email/render';
|
|
|
5
5
|
import { GetStaticPaths } from 'next';
|
|
6
6
|
import { Layout } from '../../components/layout';
|
|
7
7
|
import { CodeContainer } from '../../components/code-container';
|
|
8
|
-
import { Code } from '../../components';
|
|
9
8
|
import Head from 'next/head';
|
|
10
9
|
import { useRouter } from 'next/router';
|
|
11
10
|
|
|
@@ -109,13 +108,9 @@ const Preview: React.FC<Readonly<PreviewProps>> = ({
|
|
|
109
108
|
<title>{title}</title>
|
|
110
109
|
</Head>
|
|
111
110
|
{viewMode === 'desktop' ? (
|
|
112
|
-
<iframe
|
|
113
|
-
srcDoc={markup}
|
|
114
|
-
frameBorder="0"
|
|
115
|
-
className="w-full h-[calc(100vh_-_70px)]"
|
|
116
|
-
/>
|
|
111
|
+
<iframe srcDoc={markup} className="w-full h-[calc(100vh_-_70px)]" />
|
|
117
112
|
) : (
|
|
118
|
-
<div className="flex gap-6 mx-auto p-6">
|
|
113
|
+
<div className="flex gap-6 mx-auto p-6 max-w-3xl">
|
|
119
114
|
<CodeContainer
|
|
120
115
|
markups={[
|
|
121
116
|
{ language: 'jsx', content: reactMarkup },
|
|
@@ -7,7 +7,7 @@ export const components = [
|
|
|
7
7
|
{
|
|
8
8
|
title: 'code-container.tsx',
|
|
9
9
|
content:
|
|
10
|
-
"import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a
|
|
10
|
+
"import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [hovered, setHovered] = React.useState('');\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a\n href={url}\n download={file.name}\n className=\"text-slate-11 transition ease-in-out duration-200 hover:text-slate-12\"\n >\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"flex\">\n <AnimateSharedLayout>\n {markups.map(({ language }) => {\n const isHovered = hovered === language;\n return (\n <motion.button\n className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 ${\n activeTab !== language ? 'text-slate-11' : 'text-slate-12'\n }`}\n onClick={() => setActiveTab(language)}\n onHoverStart={() => setHovered(language)}\n onHoverEnd={() => setHovered('')}\n key={language}\n >\n {isHovered && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n {languageMap[language]}\n </motion.button>\n );\n })}\n </AnimateSharedLayout>\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
title: 'code.tsx',
|
|
@@ -27,7 +27,7 @@ export const components = [
|
|
|
27
27
|
{
|
|
28
28
|
title: 'icon-button.tsx',
|
|
29
29
|
content:
|
|
30
|
-
"import classnames from 'classnames';\nimport * as React from 'react';\n\nexport interface IconButtonProps\n extends React.ComponentPropsWithoutRef<'button'> {}\n\nexport const IconButton = React.forwardRef<\n HTMLButtonElement,\n Readonly<IconButtonProps>\n>(({ children, className, ...props }, forwardedRef) => (\n <button\n {...props}\n ref={forwardedRef}\n className={classnames(\n 'rounded text-
|
|
30
|
+
"import classnames from 'classnames';\nimport * as React from 'react';\n\nexport interface IconButtonProps\n extends React.ComponentPropsWithoutRef<'button'> {}\n\nexport const IconButton = React.forwardRef<\n HTMLButtonElement,\n Readonly<IconButtonProps>\n>(({ children, className, ...props }, forwardedRef) => (\n <button\n {...props}\n ref={forwardedRef}\n className={classnames(\n 'rounded text-slate-11 focus:text-slate-12 ease-in-out transition duration-200 focus:outline-none focus:ring-2 focus:ring-gray-8 hover:text-slate-12',\n className,\n )}\n >\n {children}\n </button>\n));\n\nIconButton.displayName = 'IconButton';\n",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
title: 'icon-check.tsx',
|
|
@@ -67,7 +67,7 @@ export const components = [
|
|
|
67
67
|
{
|
|
68
68
|
title: 'sidebar.tsx',
|
|
69
69
|
content:
|
|
70
|
-
'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center">\n <Heading
|
|
70
|
+
'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\nimport { AnimateSharedLayout, motion } from \'framer-motion\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const [hovered, setHovered] = React.useState(\'\');\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center text-slate-11 transition ease-in-out duration-200 hover:text-slate-12">\n <Heading\n as="h3"\n color="gray"\n size="2"\n weight="medium"\n className="transition ease-in-out duration-200 hover:text-slate-12"\n >\n All emails\n </Heading>\n {navItems && navItems.length > 0 && (\n <svg\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M12 15L8.5359 9.75L15.4641 9.75L12 15Z"\n fill="currentColor"\n />\n </svg>\n )}\n </div>\n </Collapsible.Trigger>\n\n {navItems && navItems.length > 0 && (\n <Collapsible.Content className="relative mt-3">\n <div className="absolute left-2.5 w-px h-full bg-slate-6" />\n\n <div className="py-2 flex flex-col truncate">\n <AnimateSharedLayout>\n {navItems &&\n navItems.map((item) => {\n const isHovered = hovered === item;\n return (\n <Link key={item} href={`/preview/${item}`}>\n <motion.span\n className={classnames(\n \'text-[14px] flex items-center font-medium gap-2 w-full pl-4 h-8 rounded-md text-slate-11 relative block transition ease-in-out duration-200\',\n {\n \'bg-cyan-3 text-cyan-11\': query.slug === item,\n \'hover:text-slate-12\': query.slug !== item,\n },\n )}\n onHoverStart={() => setHovered(item)}\n onHoverEnd={() => setHovered(\'\')}\n >\n {isHovered && (\n <motion.span\n layoutId="sidebar"\n className="absolute left-0 right-0 top-0 bottom-0 rounded-md bg-slate-5"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n >\n <div className="bg-cyan-11 w-px absolute top-1 left-2.5 h-6" />\n </motion.span>\n )}\n <svg\n className="flex-shrink-0"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M7.75 19.25H16.25C17.3546 19.25 18.25 18.3546 18.25 17.25V9L14 4.75H7.75C6.64543 4.75 5.75 5.64543 5.75 6.75V17.25C5.75 18.3546 6.64543 19.25 7.75 19.25Z"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M18 9.25H13.75V5"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n {item}\n </motion.span>\n </Link>\n );\n })}\n </AnimateSharedLayout>\n </div>\n </Collapsible.Content>\n )}\n </Collapsible.Root>\n </nav>\n </aside>\n );\n },\n);\n\nSidebar.displayName = \'Sidebar\';\n',
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
title: 'text.tsx',
|
|
@@ -87,6 +87,6 @@ export const components = [
|
|
|
87
87
|
{
|
|
88
88
|
title: 'topbar.tsx',
|
|
89
89
|
content:
|
|
90
|
-
"import * as React from 'react';\nimport classnames from 'classnames';\nimport { Heading } from './heading';\nimport {
|
|
90
|
+
"import * as React from 'react';\nimport classnames from 'classnames';\nimport { Heading } from './heading';\nimport { Send } from './send';\nimport * as ToggleGroup from '@radix-ui/react-toggle-group';\nimport { AnimateSharedLayout, motion } from 'framer-motion';\n\ntype TopbarElement = React.ElementRef<'header'>;\ntype RootProps = React.ComponentPropsWithoutRef<'header'>;\n\ninterface TopbarProps extends RootProps {\n title: string;\n viewMode?: string;\n markup?: string;\n setViewMode?: (viewMode: string) => void;\n}\n\nexport const Topbar = React.forwardRef<TopbarElement, Readonly<TopbarProps>>(\n (\n { className, title, markup, viewMode, setViewMode, ...props },\n forwardedRef,\n ) => {\n const [hovered, setHovered] = React.useState('');\n const columnWidth = 'w-[200px]';\n\n return (\n <header\n ref={forwardedRef}\n className={classnames(\n 'bg-black flex relative items-center px-6 justify-between h-[70px] border-b border-slate-6',\n className,\n )}\n {...props}\n >\n <div className={`flex items-center overflow-hidden ${columnWidth}`}>\n <Heading as=\"h2\" size=\"2\" weight=\"medium\" className=\"truncate\">\n {title}\n </Heading>\n </div>\n\n <div className={`${columnWidth}`}>\n <AnimateSharedLayout>\n {setViewMode && (\n <ToggleGroup.Root\n className=\"inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden\"\n type=\"single\"\n value={viewMode}\n aria-label=\"View mode\"\n onValueChange={(value) => {\n if (!value) {\n return setViewMode('desktop');\n }\n setViewMode(value);\n }}\n >\n <ToggleGroup.Item value=\"desktop\">\n <motion.div\n className={classnames(\n 'text-sm text-slate-11 font-medium px-3 py-2 transition ease-in-out duration-200 relative',\n {\n 'text-slate-12': viewMode === 'desktop',\n },\n )}\n onHoverStart={() => setHovered('desktop')}\n onHoverEnd={() => setHovered('')}\n >\n {hovered === 'desktop' && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n Desktop\n </motion.div>\n </ToggleGroup.Item>\n <ToggleGroup.Item value=\"source\">\n <motion.div\n className={classnames(\n 'text-sm text-slate-11 font-medium px-3 py-2 transition ease-in-out duration-200 relative',\n {\n 'text-slate-12': viewMode === 'source',\n },\n )}\n onHoverStart={() => setHovered('source')}\n onHoverEnd={() => setHovered('')}\n >\n {hovered === 'source' && (\n <motion.span\n layoutId=\"nav\"\n className=\"absolute left-0 right-0 top-0 bottom-0 bg-slate-4\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n />\n )}\n Source\n </motion.div>\n </ToggleGroup.Item>\n </ToggleGroup.Root>\n )}\n </AnimateSharedLayout>\n </div>\n\n {markup && (\n <div className={`flex justify-end ${columnWidth}`}>\n <Send markup={markup} />\n </div>\n )}\n </header>\n );\n },\n);\n\nTopbar.displayName = 'Topbar';\n",
|
|
91
91
|
},
|
|
92
92
|
];
|
package/source/_preview/pages.ts
CHANGED
|
@@ -18,6 +18,6 @@ export const pages = [
|
|
|
18
18
|
dir: 'preview',
|
|
19
19
|
title: '[slug].tsx',
|
|
20
20
|
content:
|
|
21
|
-
"import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { CodeContainer } from '../../components/code-container';\nimport
|
|
21
|
+
"import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { CodeContainer } from '../../components/code-container';\nimport Head from 'next/head';\nimport { useRouter } from 'next/router';\n\ninterface PreviewProps {\n navItems: string;\n markup: string;\n reactMarkup: string;\n slug: string;\n}\n\nexport const CONTENT_DIR = 'emails';\n\nconst getEmails = async () => {\n const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);\n const filenames = await fs.readdir(emailsDirectory);\n const emails = filenames\n .map((file) => file.replace(/\\.(jsx|tsx)$/g, ''))\n .filter((file) => file !== 'components');\n return { emails, filenames };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n const { emails } = await getEmails();\n const paths = emails.map((email) => {\n return { params: { slug: email } };\n });\n return { paths, fallback: true };\n};\n\nexport async function getStaticProps({ params }) {\n try {\n const { emails, filenames } = await getEmails();\n const template = filenames.filter((email) => {\n const [fileName] = email.split('.');\n return params.slug === fileName;\n });\n\n const Email = (await import(`../../../emails/${params.slug}`)).default;\n const markup = render(<Email />, { pretty: true });\n const plainText = render(<Email />, { plainText: true });\n const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;\n const reactMarkup = await fs.readFile(path, {\n encoding: 'utf-8',\n });\n\n return emails\n ? {\n props: {\n navItems: emails,\n slug: params.slug,\n markup,\n reactMarkup,\n plainText,\n },\n }\n : { notFound: true };\n } catch (error) {\n console.error(error);\n return { notFound: true };\n }\n}\n\nconst Preview: React.FC<Readonly<PreviewProps>> = ({\n navItems,\n markup,\n reactMarkup,\n plainText,\n slug,\n}: any) => {\n const title = `${slug} — React Email`;\n const router = useRouter();\n const [viewMode, setViewMode] = React.useState('desktop');\n\n const handleViewMode = (mode: string) => {\n setViewMode(mode);\n\n router.push({\n pathname: router.pathname,\n query: {\n ...router.query,\n view: mode,\n },\n });\n };\n\n React.useEffect(() => {\n if (router.query.view === 'source' || router.query.view === 'desktop') {\n setViewMode(router.query.view);\n }\n }, [router.query.view]);\n\n return (\n <Layout\n navItems={navItems}\n title={slug}\n viewMode={viewMode}\n setViewMode={handleViewMode}\n markup={markup}\n >\n <Head>\n <title>{title}</title>\n </Head>\n {viewMode === 'desktop' ? (\n <iframe srcDoc={markup} className=\"w-full h-[calc(100vh_-_70px)]\" />\n ) : (\n <div className=\"flex gap-6 mx-auto p-6 max-w-3xl\">\n <CodeContainer\n markups={[\n { language: 'jsx', content: reactMarkup },\n { language: 'markup', content: markup },\n { language: 'markdown', content: plainText },\n ]}\n />\n </div>\n )}\n </Layout>\n );\n};\n\nexport default Preview;\n",
|
|
22
22
|
},
|
|
23
23
|
];
|
package/source/_preview/root.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const root = [
|
|
|
12
12
|
{
|
|
13
13
|
title: 'package.json',
|
|
14
14
|
content:
|
|
15
|
-
'{\n "name": "react-email-preview",\n "version": "0.0.
|
|
15
|
+
'{\n "name": "react-email-preview",\n "version": "0.0.9",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.6",\n "classnames": "2.3.2",\n "framer-motion": "8.4.6",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
title: 'postcss.config.js',
|
package/source/index.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { program } from '@commander-js/extra-typings';
|
|
|
3
3
|
import { PACKAGE_NAME } from './utils/constants';
|
|
4
4
|
import { dev } from './commands/dev';
|
|
5
5
|
import { exportTemplates } from './commands/export';
|
|
6
|
+
import packageJson from '../package.json';
|
|
6
7
|
|
|
7
8
|
program
|
|
8
9
|
.name(PACKAGE_NAME)
|
|
9
10
|
.description('A live preview of your emails right in your browser')
|
|
10
|
-
.version(
|
|
11
|
+
.version(packageJson.version);
|
|
11
12
|
|
|
12
13
|
program
|
|
13
14
|
.command('dev')
|
|
@@ -8,7 +8,7 @@ export const PACKAGE_NAME = 'react-email';
|
|
|
8
8
|
export const CURRENT_PATH = process.cwd();
|
|
9
9
|
|
|
10
10
|
// Client paths
|
|
11
|
-
export const CLIENT_PACKAGE_JSON =
|
|
11
|
+
export const CLIENT_PACKAGE_JSON = path.join(CURRENT_PATH, 'package.json');
|
|
12
12
|
|
|
13
13
|
// React Email paths
|
|
14
14
|
export const REACT_EMAIL_ROOT = path.join(CURRENT_PATH, '.react-email');
|
package/source/utils/watcher.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
CLIENT_EMAILS_PATH,
|
|
4
4
|
CURRENT_PATH,
|
|
5
5
|
EVENT_FILE_DELETED,
|
|
6
|
-
PACKAGE_EMAILS_PATH,
|
|
7
6
|
REACT_EMAIL_ROOT,
|
|
8
7
|
} from './constants';
|
|
9
8
|
import fs from 'fs';
|
|
@@ -21,20 +20,18 @@ export const watcher = () =>
|
|
|
21
20
|
if (event === EVENT_FILE_DELETED) {
|
|
22
21
|
const file = filename.split(path.sep);
|
|
23
22
|
|
|
24
|
-
if (file[1] === 'static') {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
23
|
+
if (file[1] === 'static' && file[2]) {
|
|
24
|
+
await fs.promises.rm(
|
|
25
|
+
path.join(REACT_EMAIL_ROOT, 'public', 'static', file[2]),
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
await fs.promises.rm(path.join(REACT_EMAIL_ROOT, filename));
|
|
34
31
|
} else {
|
|
35
32
|
const file = filename.split(path.sep);
|
|
36
33
|
|
|
37
|
-
if (file[1] === 'static') {
|
|
34
|
+
if (file[1] === 'static' && file[2]) {
|
|
38
35
|
await copy(
|
|
39
36
|
`${CLIENT_EMAILS_PATH}/static/${file[2]}`,
|
|
40
37
|
`${REACT_EMAIL_ROOT}/public/static`,
|
|
@@ -42,6 +39,9 @@ export const watcher = () =>
|
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
await copy(
|
|
42
|
+
await copy(
|
|
43
|
+
path.join(CURRENT_PATH, filename),
|
|
44
|
+
path.join(REACT_EMAIL_ROOT, file.slice(0, -1).join(path.sep)),
|
|
45
|
+
);
|
|
46
46
|
}
|
|
47
47
|
});
|
package/tsconfig.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"target": "ES2020", // Node.js 14
|
|
6
6
|
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
7
7
|
"allowSyntheticDefaultImports": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. This flag tells TypeScript that it's okay to use import on CommonJS modules.
|
|
8
|
-
"resolveJsonModule":
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
9
|
"jsx": "react",
|
|
10
10
|
"declaration": true,
|
|
11
11
|
"pretty": true,
|