react-email 4.0.0-alpha.3 → 4.0.0-alpha.5
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/CHANGELOG.md +12 -0
- package/dist/cli/index.js +7 -5
- package/dist/cli/index.mjs +11 -6
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +31 -34
- package/dist/preview/.next/build-manifest.json +14 -14
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/images/TcyzHbFXGFjrOu3wEMvDoSmqCh3qP3iiNqJf0QbED9Y/60.1741728556140.cQ5qicbpvoXZ7leVmWqG2ElLwXB1ynYeSv8MBSA-QeM.Vy8iMWM3MGUtMTk1ODcxYmIyNzMi.webp +0 -0
- package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/12.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/13.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/diagnostics/framework.json +1 -1
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +1 -1
- package/dist/preview/.next/required-server-files.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +9 -8
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/chunks/143.js +6 -0
- package/dist/preview/.next/server/chunks/409.js +5 -0
- package/dist/preview/.next/server/chunks/46.js +1 -0
- package/dist/preview/.next/server/chunks/478.js +14 -0
- package/dist/preview/.next/server/chunks/707.js +13 -0
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages/_app.js +1 -1
- package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_document.js +1 -1
- package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_error.js +1 -1
- package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/{iP6qiNn8FML_AvKcxGPhM → B4EYZiVzdylEG9lAIl-aO}/_buildManifest.js +1 -1
- package/dist/preview/.next/static/chunks/575-bc52750855c25df4.js +2 -0
- package/dist/preview/.next/static/chunks/684-0f1ef7361c499798.js +1 -0
- package/dist/preview/.next/static/chunks/684c6b30-0c65da32762fc4ee.js +1 -0
- package/dist/preview/.next/static/chunks/81-e7539b08d9d3fb4d.js +1 -0
- package/dist/preview/.next/static/chunks/883-70c8267c50bc4133.js +1 -0
- package/dist/preview/.next/static/chunks/921-d1dc8c63f49e85d6.js +1 -0
- package/dist/preview/.next/static/chunks/{afa401a5-9ebf2515b1397993.js → afa401a5-a600c227dacf3ab4.js} +1 -1
- package/dist/preview/.next/static/chunks/app/_not-found/{page-96d3eac723be3ee2.js → page-03ce767859c36d4e.js} +1 -1
- package/dist/preview/.next/static/chunks/app/layout-7cf14e28880544f1.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-065cb49b0a078541.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-656510fd180c803c.js +1 -0
- package/dist/preview/.next/static/chunks/{framework-e7cae9cecd5c9ba2.js → framework-2a724981073c3a29.js} +1 -1
- package/dist/preview/.next/static/chunks/main-552b9719bbc3a274.js +1 -0
- package/dist/preview/.next/static/chunks/main-app-914a73336fd45af5.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_app-77ca34bce25ac75c.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_error-73f611c46abbb495.js +1 -0
- package/dist/preview/.next/static/chunks/{webpack-9255716c9496e606.js → webpack-2eb145a20ee6cb77.js} +1 -1
- package/dist/preview/.next/static/css/2df96d9ee014e8de.css +3 -0
- package/dist/preview/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/dist/preview/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/dist/preview/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/dist/preview/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/dist/preview/.next/trace +22 -22
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +7 -5
- package/src/actions/email-validation/check-images.spec.tsx +23 -14
- package/src/actions/email-validation/check-images.ts +89 -87
- package/src/actions/email-validation/check-links.spec.tsx +27 -17
- package/src/actions/email-validation/check-links.ts +60 -57
- package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
- package/src/app/fonts.ts +39 -0
- package/src/app/layout.tsx +5 -2
- package/src/app/page.tsx +3 -3
- package/src/app/preview/[...slug]/preview.tsx +50 -31
- package/src/components/icons/icon-base.tsx +4 -2
- package/src/components/icons/icon-bug.tsx +19 -0
- package/src/components/icons/icon-reload.tsx +19 -0
- package/src/components/icons/icon-scanner.tsx +19 -0
- package/src/components/icons/icon-scissors.tsx +19 -0
- package/src/components/icons/icon-warning.tsx +31 -0
- package/src/components/send.tsx +1 -2
- package/src/components/shell.tsx +52 -85
- package/src/components/sidebar/file-tree-directory-children.tsx +15 -12
- package/src/components/sidebar/file-tree.tsx +1 -1
- package/src/components/sidebar/sidebar.tsx +23 -344
- package/src/components/toolbar/linter.tsx +167 -0
- package/src/components/toolbar/results-table.tsx +0 -0
- package/src/components/toolbar/results.tsx +48 -0
- package/src/components/toolbar/spam-assassin.tsx +155 -0
- package/src/components/toolbar.tsx +189 -0
- package/src/components/tooltip-content.tsx +1 -2
- package/src/components/topbar.tsx +28 -41
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/tailwind.config.ts +1 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +5 -3
- package/dist/preview/.next/server/chunks/196.js +0 -5
- package/dist/preview/.next/server/chunks/300.js +0 -13
- package/dist/preview/.next/server/chunks/509.js +0 -1
- package/dist/preview/.next/server/chunks/631.js +0 -6
- package/dist/preview/.next/server/chunks/734.js +0 -15
- package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +0 -1
- package/dist/preview/.next/static/chunks/447-886131c35ca42b91.js +0 -1
- package/dist/preview/.next/static/chunks/490-0db0db14b377daca.js +0 -1
- package/dist/preview/.next/static/chunks/5fec7a0a-5179023f3f5a9421.js +0 -1
- package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +0 -1
- package/dist/preview/.next/static/chunks/797-46f6c20952f0a280.js +0 -2
- package/dist/preview/.next/static/chunks/app/layout-f6f64b817a2cf938.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-f5f96bd66526060f.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-fb2bf0253c2dada4.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +0 -1
- package/dist/preview/.next/static/chunks/main-df761fde212f9cda.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_app-203a61b355820ccf.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_error-1764ca54938748c8.js +0 -1
- package/dist/preview/.next/static/css/778d574c88a1db3c.css +0 -3
- package/dist/preview/.next/static/css/ec5d7e66bd3b6cb8.css +0 -1
- package/src/app/inter.ts +0 -7
- package/src/components/icons/icon-circle-check.tsx +0 -21
- package/src/components/icons/icon-circle-close.tsx +0 -17
- package/src/components/icons/icon-circle-warning.tsx +0 -17
- package/src/components/sidebar/image-checker.tsx +0 -161
- package/src/components/sidebar/link-checker.tsx +0 -151
- package/tsconfig.test.json +0 -8
- /package/dist/preview/.next/static/{iP6qiNn8FML_AvKcxGPhM → B4EYZiVzdylEG9lAIl-aO}/_ssgManifest.js +0 -0
- /package/src/components/{sidebar → toolbar}/checking-results.tsx +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
import { IconWarning } from '../icons/icon-warning';
|
|
5
|
+
import { Results } from './results';
|
|
6
|
+
|
|
7
|
+
interface SpamAssassinProps {
|
|
8
|
+
result: SpamCheckingResult | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SpamCheckingResult {
|
|
12
|
+
checks: {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
points: number;
|
|
16
|
+
}[];
|
|
17
|
+
isSpam: boolean;
|
|
18
|
+
points: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toSorted<T>(array: T[], sorter: (a: T, b: T) => number): T[] {
|
|
22
|
+
const cloned = [...array];
|
|
23
|
+
cloned.sort(sorter);
|
|
24
|
+
return cloned;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useSpamAssassin = ({
|
|
28
|
+
slug,
|
|
29
|
+
markup,
|
|
30
|
+
plainText,
|
|
31
|
+
}: {
|
|
32
|
+
slug: string;
|
|
33
|
+
markup: string;
|
|
34
|
+
plainText: string;
|
|
35
|
+
}) => {
|
|
36
|
+
const cacheKey = `spam-assassin-${slug.replaceAll('/', '-')}`;
|
|
37
|
+
|
|
38
|
+
const [result, setResult] = useState<SpamCheckingResult | undefined>();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const cachedValue =
|
|
42
|
+
'localStorage' in global ? global.localStorage.getItem(cacheKey) : null;
|
|
43
|
+
if (cachedValue) {
|
|
44
|
+
try {
|
|
45
|
+
setResult(JSON.parse(cachedValue));
|
|
46
|
+
} catch (exception) {
|
|
47
|
+
setResult(undefined);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [cacheKey]);
|
|
51
|
+
|
|
52
|
+
const [loading, setLoading] = useState(false);
|
|
53
|
+
|
|
54
|
+
const load = async () => {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch('https://react.email/api/check-spam', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
html: markup,
|
|
63
|
+
plainText: plainText,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (response.ok) {
|
|
68
|
+
const responseBody = (await response.json()) as
|
|
69
|
+
| { error: string }
|
|
70
|
+
| SpamCheckingResult;
|
|
71
|
+
if ('error' in responseBody) {
|
|
72
|
+
toast.error(responseBody.error);
|
|
73
|
+
} else {
|
|
74
|
+
setResult(responseBody);
|
|
75
|
+
localStorage.setItem(cacheKey, JSON.stringify(responseBody));
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.error(await response.text());
|
|
79
|
+
toast.error('Something went wrong');
|
|
80
|
+
}
|
|
81
|
+
} catch (exception) {
|
|
82
|
+
console.error(exception);
|
|
83
|
+
toast.error(JSON.stringify(exception));
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return [result, { loading, load }] as const;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const SpamAssassin = ({ result }: SpamAssassinProps) => {
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{result ? (
|
|
96
|
+
<Results>
|
|
97
|
+
<Results.Row className="sticky border-b-2 top-0 bg-black">
|
|
98
|
+
<Results.Column className="uppercase">
|
|
99
|
+
<span className="flex gap-1 items-center">
|
|
100
|
+
<IconWarning
|
|
101
|
+
className={cn(
|
|
102
|
+
result.points > 1.5 ? 'text-yellow-200' : null,
|
|
103
|
+
result.points > 3 ? 'text-orange-300' : null,
|
|
104
|
+
result.points >= 5 ? 'text-red-400' : null,
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
Score
|
|
108
|
+
</span>
|
|
109
|
+
</Results.Column>
|
|
110
|
+
<Results.Column>Lower scores are better</Results.Column>
|
|
111
|
+
<Results.Column
|
|
112
|
+
className={cn(
|
|
113
|
+
'text-right text-2xl tracking-tighter font-mono',
|
|
114
|
+
result.points > 1.5 ? 'text-yellow-200' : null,
|
|
115
|
+
result.points > 3 ? 'text-orange-300' : null,
|
|
116
|
+
result.points >= 5 ? 'text-red-400' : null,
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{result.points.toFixed(1)}
|
|
120
|
+
</Results.Column>
|
|
121
|
+
</Results.Row>
|
|
122
|
+
{toSorted(result.checks, (a, b) => b.points - a.points).map(
|
|
123
|
+
(check) => (
|
|
124
|
+
<Results.Row key={check.name}>
|
|
125
|
+
<Results.Column className="uppercase">
|
|
126
|
+
<span className="flex gap-1 items-center">
|
|
127
|
+
<IconWarning
|
|
128
|
+
className={cn(
|
|
129
|
+
check.points > 1 ? 'text-yellow-200' : null,
|
|
130
|
+
check.points > 2 ? 'text-orange-300' : null,
|
|
131
|
+
check.points > 3 ? 'text-red-400' : null,
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
{check.name}
|
|
135
|
+
</span>
|
|
136
|
+
</Results.Column>
|
|
137
|
+
<Results.Column>{check.description}</Results.Column>
|
|
138
|
+
<Results.Column
|
|
139
|
+
className={cn(
|
|
140
|
+
'text-right font-mono tracking-tighter',
|
|
141
|
+
check.points > 1 ? 'text-yellow-200' : null,
|
|
142
|
+
check.points > 2 ? 'text-orange-300' : null,
|
|
143
|
+
check.points > 3 ? 'text-red-400' : null,
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{check.points.toFixed(1)}
|
|
147
|
+
</Results.Column>
|
|
148
|
+
</Results.Row>
|
|
149
|
+
),
|
|
150
|
+
)}
|
|
151
|
+
</Results>
|
|
152
|
+
) : null}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as Tabs from '@radix-ui/react-tabs';
|
|
2
|
+
import { LayoutGroup, motion } from 'framer-motion';
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { cn } from '../utils';
|
|
6
|
+
import { IconArrowDown } from './icons/icon-arrow-down';
|
|
7
|
+
import { IconReload } from './icons/icon-reload';
|
|
8
|
+
import { IconScanner } from './icons/icon-scanner';
|
|
9
|
+
import { IconScissors } from './icons/icon-scissors';
|
|
10
|
+
import { Linter, useLinter } from './toolbar/linter';
|
|
11
|
+
import { SpamAssassin, useSpamAssassin } from './toolbar/spam-assassin';
|
|
12
|
+
import { Tooltip } from './tooltip';
|
|
13
|
+
|
|
14
|
+
type ToolbarProps = React.ComponentProps<'div'> & {
|
|
15
|
+
emailSlug: string;
|
|
16
|
+
markup: string;
|
|
17
|
+
plainText: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ActivePanelValue = 'linter' | 'spam-assassin';
|
|
21
|
+
|
|
22
|
+
interface ToolbarButton extends React.ComponentProps<'button'> {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
active?: boolean;
|
|
25
|
+
tooltip?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ToolbarButton = ({
|
|
29
|
+
children,
|
|
30
|
+
className,
|
|
31
|
+
active,
|
|
32
|
+
tooltip,
|
|
33
|
+
...props
|
|
34
|
+
}: ToolbarButton) => {
|
|
35
|
+
return (
|
|
36
|
+
<Tooltip.Provider>
|
|
37
|
+
<Tooltip>
|
|
38
|
+
<Tooltip.Trigger asChild>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
{...props}
|
|
42
|
+
className={cn(
|
|
43
|
+
'h-full w-fit font-medium flex text-sm text-slate-10 items-center align-middle justify-center px-1 py-2 gap-1 relative',
|
|
44
|
+
'hover:text-slate-12 transition-colors',
|
|
45
|
+
active && 'data-[state=active]:text-cyan-11',
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
{active ? (
|
|
51
|
+
<motion.span
|
|
52
|
+
className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
|
|
53
|
+
layoutId="active-toolbar-button"
|
|
54
|
+
transition={{
|
|
55
|
+
type: 'spring',
|
|
56
|
+
bounce: 0.2,
|
|
57
|
+
duration: 0.6,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
) : null}
|
|
61
|
+
</button>
|
|
62
|
+
</Tooltip.Trigger>
|
|
63
|
+
{tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
|
|
64
|
+
</Tooltip>
|
|
65
|
+
</Tooltip.Provider>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Toolbar = ({
|
|
70
|
+
emailSlug,
|
|
71
|
+
markup,
|
|
72
|
+
plainText,
|
|
73
|
+
className,
|
|
74
|
+
...rest
|
|
75
|
+
}: ToolbarProps) => {
|
|
76
|
+
const pathname = usePathname();
|
|
77
|
+
const searchParams = useSearchParams();
|
|
78
|
+
const router = useRouter();
|
|
79
|
+
|
|
80
|
+
const activePanelValue = (searchParams.get('toolbar-panel') ?? undefined) as
|
|
81
|
+
| ActivePanelValue
|
|
82
|
+
| undefined;
|
|
83
|
+
|
|
84
|
+
const toggled = activePanelValue !== undefined;
|
|
85
|
+
|
|
86
|
+
const setActivePanelValue = (newValue: ActivePanelValue | undefined) => {
|
|
87
|
+
console.log(newValue);
|
|
88
|
+
const params = new URLSearchParams(searchParams);
|
|
89
|
+
if (newValue === undefined) {
|
|
90
|
+
params.delete('toolbar-panel');
|
|
91
|
+
} else {
|
|
92
|
+
params.set('toolbar-panel', newValue);
|
|
93
|
+
}
|
|
94
|
+
router.push(`${pathname}?${params.toString()}`);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const [spamCheckingResult, { load: loadSpamChecking }] = useSpamAssassin({
|
|
98
|
+
slug: emailSlug,
|
|
99
|
+
markup,
|
|
100
|
+
plainText,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const [lintingResults, { load: loadLinting }] = useLinter({
|
|
104
|
+
slug: emailSlug,
|
|
105
|
+
markup,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
loadLinting();
|
|
110
|
+
loadSpamChecking();
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
{...rest}
|
|
116
|
+
data-toggled={toggled}
|
|
117
|
+
className={cn(
|
|
118
|
+
'bg-black group/toolbar text-xs text-slate-11 h-48 transition-all',
|
|
119
|
+
'data-[toggled=false]:h-8',
|
|
120
|
+
className,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Tabs.Root
|
|
124
|
+
value={activePanelValue}
|
|
125
|
+
onValueChange={(newValue) => {
|
|
126
|
+
setActivePanelValue(newValue as ActivePanelValue);
|
|
127
|
+
}}
|
|
128
|
+
asChild
|
|
129
|
+
>
|
|
130
|
+
<div className="flex flex-col h-full">
|
|
131
|
+
<Tabs.List className="flex gap-4 px-2 border-b border-solid border-slate-6 h-7 w-full">
|
|
132
|
+
<LayoutGroup id="toolbar">
|
|
133
|
+
<Tabs.Trigger asChild value="spam-assassin">
|
|
134
|
+
<ToolbarButton active={activePanelValue === 'spam-assassin'}>
|
|
135
|
+
<IconScissors />
|
|
136
|
+
Spam Assassin
|
|
137
|
+
</ToolbarButton>
|
|
138
|
+
</Tabs.Trigger>
|
|
139
|
+
<Tabs.Trigger asChild value="linter">
|
|
140
|
+
<ToolbarButton active={activePanelValue === 'linter'}>
|
|
141
|
+
<IconScanner />
|
|
142
|
+
Linter
|
|
143
|
+
</ToolbarButton>
|
|
144
|
+
</Tabs.Trigger>
|
|
145
|
+
</LayoutGroup>
|
|
146
|
+
<div className="flex gap-1 ml-auto">
|
|
147
|
+
<ToolbarButton
|
|
148
|
+
tooltip="Reload"
|
|
149
|
+
onClick={() => {
|
|
150
|
+
if (activePanelValue === 'spam-assassin') {
|
|
151
|
+
void loadSpamChecking();
|
|
152
|
+
} else if (activePanelValue === 'linter') {
|
|
153
|
+
void loadLinting();
|
|
154
|
+
} else {
|
|
155
|
+
setActivePanelValue('linter');
|
|
156
|
+
void loadLinting();
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<IconReload />
|
|
161
|
+
</ToolbarButton>
|
|
162
|
+
<ToolbarButton
|
|
163
|
+
tooltip="Toggle toolbar"
|
|
164
|
+
onClick={() => {
|
|
165
|
+
if (activePanelValue === undefined) {
|
|
166
|
+
setActivePanelValue('linter');
|
|
167
|
+
} else {
|
|
168
|
+
setActivePanelValue(undefined);
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<IconArrowDown className="transition-transform group-data-[toggled=false]/toolbar:rotate-180" />
|
|
173
|
+
</ToolbarButton>
|
|
174
|
+
</div>
|
|
175
|
+
</Tabs.List>
|
|
176
|
+
|
|
177
|
+
<div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
|
|
178
|
+
<Tabs.Content value="linter">
|
|
179
|
+
<Linter results={lintingResults} />
|
|
180
|
+
</Tabs.Content>
|
|
181
|
+
<Tabs.Content value="spam-assassin">
|
|
182
|
+
<SpamAssassin result={spamCheckingResult} />
|
|
183
|
+
</Tabs.Content>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</Tabs.Root>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { inter } from '../app/inter';
|
|
4
3
|
import { cn } from '../utils';
|
|
5
4
|
|
|
6
5
|
type ContentElement = React.ComponentRef<typeof TooltipPrimitive.Content>;
|
|
@@ -19,7 +18,7 @@ export const TooltipContent = React.forwardRef<
|
|
|
19
18
|
{...props}
|
|
20
19
|
className={cn(
|
|
21
20
|
'z-20 rounded-md border border-slate-6 bg-black px-3 py-2 text-white text-xs',
|
|
22
|
-
|
|
21
|
+
'font-sans',
|
|
23
22
|
)}
|
|
24
23
|
ref={forwardedRef}
|
|
25
24
|
sideOffset={sideOffset}
|
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { use } from 'react';
|
|
3
4
|
import { Heading } from './heading';
|
|
4
5
|
import { IconHideSidebar } from './icons/icon-hide-sidebar';
|
|
5
|
-
import {
|
|
6
|
+
import { ShellContext } from './shell';
|
|
6
7
|
import { Tooltip } from './tooltip';
|
|
7
|
-
import { ActiveViewToggleGroup } from './topbar/active-view-toggle-group';
|
|
8
|
-
import { ViewSizeControls } from './topbar/view-size-controls';
|
|
9
8
|
|
|
10
9
|
interface TopbarProps {
|
|
11
10
|
currentEmailOpenSlug: string;
|
|
12
11
|
pathSeparator: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
activeView?: string;
|
|
16
|
-
setActiveView?: (view: string) => void;
|
|
17
|
-
viewWidth?: number;
|
|
18
|
-
setViewWidth?: (width: number) => void;
|
|
19
|
-
viewHeight?: number;
|
|
20
|
-
setViewHeight?: (height: number) => void;
|
|
12
|
+
|
|
13
|
+
children: React.ReactNode;
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export const Topbar = ({
|
|
24
17
|
currentEmailOpenSlug,
|
|
25
18
|
pathSeparator,
|
|
26
|
-
|
|
27
|
-
activeView,
|
|
28
|
-
setActiveView,
|
|
29
|
-
viewWidth,
|
|
30
|
-
setViewWidth,
|
|
31
|
-
viewHeight,
|
|
32
|
-
setViewHeight,
|
|
33
|
-
onToggleSidebar,
|
|
19
|
+
children,
|
|
34
20
|
}: TopbarProps) => {
|
|
21
|
+
const { toggleSidebar } = use(ShellContext)!;
|
|
22
|
+
|
|
35
23
|
return (
|
|
36
24
|
<Tooltip.Provider>
|
|
37
25
|
<header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3">
|
|
@@ -41,9 +29,7 @@ export const Topbar = ({
|
|
|
41
29
|
<button
|
|
42
30
|
className="relative hidden rounded-lg px-2 py-2 text-slate-11 transition duration-200 ease-in-out hover:bg-slate-5 hover:text-slate-12 lg:flex"
|
|
43
31
|
onClick={() => {
|
|
44
|
-
|
|
45
|
-
onToggleSidebar();
|
|
46
|
-
}
|
|
32
|
+
toggleSidebar();
|
|
47
33
|
}}
|
|
48
34
|
type="button"
|
|
49
35
|
>
|
|
@@ -59,25 +45,26 @@ export const Topbar = ({
|
|
|
59
45
|
</div>
|
|
60
46
|
</div>
|
|
61
47
|
<div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
48
|
+
{children}
|
|
49
|
+
{/* {setViewWidth && setViewHeight && viewWidth && viewHeight ? ( */}
|
|
50
|
+
{/* <ViewSizeControls */}
|
|
51
|
+
{/* setViewHeight={setViewHeight} */}
|
|
52
|
+
{/* setViewWidth={setViewWidth} */}
|
|
53
|
+
{/* viewHeight={viewHeight} */}
|
|
54
|
+
{/* viewWidth={viewWidth} */}
|
|
55
|
+
{/* /> */}
|
|
56
|
+
{/* ) : null} */}
|
|
57
|
+
{/* {activeView && setActiveView ? ( */}
|
|
58
|
+
{/* <ActiveViewToggleGroup */}
|
|
59
|
+
{/* activeView={activeView} */}
|
|
60
|
+
{/* setActiveView={setActiveView} */}
|
|
61
|
+
{/* /> */}
|
|
62
|
+
{/* ) : null} */}
|
|
63
|
+
{/* {markup ? ( */}
|
|
64
|
+
{/* <div className="flex justify-end"> */}
|
|
65
|
+
{/* <Send markup={markup} /> */}
|
|
66
|
+
{/* </div> */}
|
|
67
|
+
{/* ) : null} */}
|
|
81
68
|
</div>
|
|
82
69
|
</header>
|
|
83
70
|
</Tooltip.Provider>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><
|
|
3
|
+
exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>  ​</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com/teams/invite/foo</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin:16px 0">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
|
package/tailwind.config.ts
CHANGED
package/tsconfig.json
CHANGED
package/vitest.config.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { TsconfigRaw } from 'esbuild';
|
|
2
1
|
import { defineConfig } from 'vitest/config';
|
|
3
|
-
import tsconfig from './tsconfig.test.json';
|
|
4
2
|
|
|
5
3
|
export default defineConfig({
|
|
6
4
|
test: {
|
|
@@ -8,6 +6,10 @@ export default defineConfig({
|
|
|
8
6
|
environment: 'happy-dom',
|
|
9
7
|
},
|
|
10
8
|
esbuild: {
|
|
11
|
-
tsconfigRaw:
|
|
9
|
+
tsconfigRaw: {
|
|
10
|
+
compilerOptions: {
|
|
11
|
+
jsx: 'react-jsx',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
12
14
|
},
|
|
13
15
|
});
|