@react-email/preview-server 1.0.0-canary.0
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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +44 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +57 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +41 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +311 -0
- package/.next/routes-manifest.json +64 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/page.js +1 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/preview/[...slug]/page.js +321 -0
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -0
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/134.js +6 -0
- package/.next/server/chunks/235.js +15 -0
- package/.next/server/chunks/278.js +1 -0
- package/.next/server/chunks/343.js +20 -0
- package/.next/server/chunks/428.js +14 -0
- package/.next/server/chunks/963.js +1 -0
- package/.next/server/chunks/999.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +5 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_buildManifest.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_ssgManifest.js +1 -0
- package/.next/static/chunks/107-3043079e7cb8bcae.js +1 -0
- package/.next/static/chunks/293-67391ef0e44ffa4f.js +1 -0
- package/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +1 -0
- package/.next/static/chunks/45-1021fac82f766268.js +1 -0
- package/.next/static/chunks/484-1969fe871b27074e.js +1 -0
- package/.next/static/chunks/484-e02de792ff5f9ea5.js +1 -0
- package/.next/static/chunks/589-817d8691661d370e.js +1 -0
- package/.next/static/chunks/902-c34acb56733e0ce1.js +1 -0
- package/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +1 -0
- package/.next/static/chunks/app/layout-74628781c0b7e7bf.js +1 -0
- package/.next/static/chunks/app/layout-daeba68330ab58bb.js +1 -0
- package/.next/static/chunks/app/page-55cf199b7ca71958.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-07dd9a701d0b3e56.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-61b0ea70a8d72916.js +1 -0
- package/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +6 -0
- package/.next/static/chunks/framework-b887e9fc751a9906.js +1 -0
- package/.next/static/chunks/main-9a03e7ba8acb1900.js +1 -0
- package/.next/static/chunks/main-app-5bc2d814f500db60.js +1 -0
- package/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +1 -0
- package/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-31c45daa2bd82a7b.js +1 -0
- package/.next/static/css/35e8811589f0962b.css +3 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_buildManifest.js +1 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_ssgManifest.js +1 -0
- package/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
- package/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
- package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
- package/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
- package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
- package/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
- package/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/.next/static/media/logo.2ce2a759.png +0 -0
- package/.next/trace +27 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/app/preview/[...slug]/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/_index.js +4 -0
- package/license.md +7 -0
- package/module-punycode.d.ts +3 -0
- package/next-env.d.ts +5 -0
- package/next.config.js +22 -0
- package/package.json +82 -0
- package/postcss.config.js +8 -0
- package/scripts/build-preview-server.mjs +29 -0
- package/scripts/fill-caniemail-data.mjs +36 -0
- package/src/actions/email-validation/caniemail-data.ts +85993 -0
- package/src/actions/email-validation/check-compatibility.ts +333 -0
- package/src/actions/email-validation/check-images.spec.tsx +100 -0
- package/src/actions/email-validation/check-images.ts +160 -0
- package/src/actions/email-validation/check-links.spec.tsx +113 -0
- package/src/actions/email-validation/check-links.ts +113 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/email-validation/quick-fetch.ts +14 -0
- package/src/actions/get-email-path-from-slug.ts +32 -0
- package/src/actions/get-emails-directory-metadata-action.ts +19 -0
- package/src/actions/render-email-by-path.tsx +121 -0
- package/src/animated-icons-data/help.json +1082 -0
- package/src/animated-icons-data/link.json +1309 -0
- package/src/animated-icons-data/load.json +443 -0
- package/src/animated-icons-data/mail.json +1320 -0
- package/src/app/env.ts +15 -0
- package/src/app/favicon.ico +0 -0
- 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/globals.css +15 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/logo.png +0 -0
- package/src/app/page.tsx +46 -0
- package/src/app/preview/[...slug]/page.tsx +157 -0
- package/src/app/preview/[...slug]/preview.tsx +216 -0
- package/src/app/preview/[...slug]/rendering-error.tsx +40 -0
- package/src/components/button.tsx +101 -0
- package/src/components/code-container.tsx +164 -0
- package/src/components/code-snippet.tsx +9 -0
- package/src/components/code.tsx +184 -0
- package/src/components/heading.tsx +113 -0
- package/src/components/icons/icon-arrow-down.tsx +16 -0
- package/src/components/icons/icon-base.tsx +26 -0
- package/src/components/icons/icon-bug.tsx +19 -0
- package/src/components/icons/icon-button.tsx +23 -0
- package/src/components/icons/icon-check.tsx +19 -0
- package/src/components/icons/icon-clipboard.tsx +40 -0
- package/src/components/icons/icon-download.tsx +19 -0
- package/src/components/icons/icon-email.tsx +18 -0
- package/src/components/icons/icon-file.tsx +19 -0
- package/src/components/icons/icon-folder-open.tsx +19 -0
- package/src/components/icons/icon-folder.tsx +18 -0
- package/src/components/icons/icon-hide-sidebar.tsx +23 -0
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-link.tsx +14 -0
- package/src/components/icons/icon-monitor.tsx +19 -0
- package/src/components/icons/icon-phone.tsx +26 -0
- package/src/components/icons/icon-reload.tsx +18 -0
- package/src/components/icons/icon-source.tsx +19 -0
- package/src/components/icons/icon-stamp.tsx +14 -0
- package/src/components/icons/icon-warning.tsx +31 -0
- package/src/components/index.ts +7 -0
- package/src/components/logo.tsx +63 -0
- package/src/components/resizable-wrapper.tsx +173 -0
- package/src/components/send.tsx +134 -0
- package/src/components/shell.tsx +92 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +139 -0
- package/src/components/sidebar/file-tree-directory.tsx +92 -0
- package/src/components/sidebar/file-tree.tsx +31 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar.tsx +43 -0
- package/src/components/text.tsx +99 -0
- package/src/components/toolbar/checking-results.tsx +150 -0
- package/src/components/toolbar/code-preview-line-link.tsx +40 -0
- package/src/components/toolbar/compatibility.tsx +113 -0
- package/src/components/toolbar/linter.tsx +278 -0
- package/src/components/toolbar/results-table.tsx +0 -0
- package/src/components/toolbar/results.tsx +51 -0
- package/src/components/toolbar/spam-assassin.tsx +155 -0
- package/src/components/toolbar/toolbar-button.tsx +52 -0
- package/src/components/toolbar/use-cached-state.ts +33 -0
- package/src/components/toolbar.tsx +349 -0
- package/src/components/tooltip-content.tsx +31 -0
- package/src/components/tooltip.tsx +19 -0
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +59 -0
- package/src/contexts/emails.tsx +59 -0
- package/src/contexts/fragment-identifier.tsx +48 -0
- package/src/contexts/preview.tsx +79 -0
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-email-rendering-result.ts +58 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/hooks/use-hot-reload.ts +31 -0
- package/src/hooks/use-icon-animation.ts +41 -0
- package/src/hooks/use-rendering-metadata.ts +36 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
- package/src/utils/caniemail/all-css-properties.ts +358 -0
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
- package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
- package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
- package/src/utils/caniemail/get-css-functions.ts +25 -0
- package/src/utils/caniemail/get-css-property-names.ts +32 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
- package/src/utils/caniemail/get-css-unit.ts +3 -0
- package/src/utils/caniemail/get-element-attributes.ts +7 -0
- package/src/utils/caniemail/get-element-names.ts +20 -0
- package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +187 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
- package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/constants.ts +6 -0
- package/src/utils/contains-email-template.spec.ts +124 -0
- package/src/utils/contains-email-template.ts +33 -0
- package/src/utils/copy-text-to-clipboard.ts +7 -0
- package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
- package/src/utils/get-email-component.spec.ts +41 -0
- package/src/utils/get-email-component.ts +134 -0
- package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
- package/src/utils/get-emails-directory-metadata.ts +141 -0
- package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
- package/src/utils/get-line-and-column-from-offset.ts +11 -0
- package/src/utils/improve-error-with-sourcemap.ts +85 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/js-email-detection.spec.ts +24 -0
- package/src/utils/language-map.ts +7 -0
- package/src/utils/linting.ts +60 -0
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/register-spinner-autostopping.ts +28 -0
- package/src/utils/result.ts +49 -0
- package/src/utils/run-bundled-code.ts +64 -0
- package/src/utils/sanitize.ts +6 -0
- package/src/utils/static-node-modules-for-vm.ts +93 -0
- package/src/utils/testing/js-email-export-default.js +17 -0
- package/src/utils/testing/js-email-test.js +18 -0
- package/src/utils/testing/mdx-email-test.js +128 -0
- package/src/utils/testing/request-response-email.tsx +9 -0
- package/src/utils/types/as.ts +26 -0
- package/src/utils/types/email-template.ts +8 -0
- package/src/utils/types/error-object.ts +11 -0
- package/src/utils/types/hot-reload-change.ts +13 -0
- package/src/utils/unreachable.ts +8 -0
- package/tailwind-internals.d.ts +133 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +45 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { cn, sanitize } 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
|
+
export 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
|
+
markup,
|
|
29
|
+
plainText,
|
|
30
|
+
|
|
31
|
+
initialResult,
|
|
32
|
+
}: {
|
|
33
|
+
markup: string;
|
|
34
|
+
plainText: string;
|
|
35
|
+
|
|
36
|
+
initialResult?: SpamCheckingResult;
|
|
37
|
+
}) => {
|
|
38
|
+
const [result, setResult] = useState<SpamCheckingResult | undefined>(
|
|
39
|
+
initialResult,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const [loading, setLoading] = useState(false);
|
|
43
|
+
const isLoadingRef = useRef(false);
|
|
44
|
+
|
|
45
|
+
const load = async () => {
|
|
46
|
+
if (isLoadingRef.current) return;
|
|
47
|
+
isLoadingRef.current = true;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch('https://react.email/api/check-spam', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
html: markup,
|
|
56
|
+
plainText: plainText,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const responseBody = (await response.json()) as
|
|
61
|
+
| { error: string }
|
|
62
|
+
| SpamCheckingResult;
|
|
63
|
+
if ('error' in responseBody) {
|
|
64
|
+
toast.error(responseBody.error);
|
|
65
|
+
} else {
|
|
66
|
+
setResult(responseBody);
|
|
67
|
+
return responseBody;
|
|
68
|
+
}
|
|
69
|
+
} catch (exception) {
|
|
70
|
+
console.error(exception);
|
|
71
|
+
toast.error(JSON.stringify(exception));
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
isLoadingRef.current = false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return [result, { loading, load }] as const;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const SpamAssassin = ({ result }: SpamAssassinProps) => {
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{result ? (
|
|
85
|
+
<Results>
|
|
86
|
+
<Results.Row className="sticky border-b top-0">
|
|
87
|
+
<Results.Column className="uppercase">
|
|
88
|
+
<span className="flex gap-2 items-center">
|
|
89
|
+
<IconWarning
|
|
90
|
+
className={cn(
|
|
91
|
+
result.points === 0 ? 'text-green-400' : null,
|
|
92
|
+
result.points > 0 && result.points <= 1.5 ? null : null,
|
|
93
|
+
result.points > 1.5 ? 'text-yellow-100' : null,
|
|
94
|
+
result.points > 3 ? 'text-orange-400' : null,
|
|
95
|
+
result.points >= 5 ? 'text-red-400' : null,
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
Score
|
|
99
|
+
</span>
|
|
100
|
+
</Results.Column>
|
|
101
|
+
<Results.Column>
|
|
102
|
+
{result.points === 0
|
|
103
|
+
? 'Congratulations! Your email is clean of abuse indicators.'
|
|
104
|
+
: 'Higher scores are better'}
|
|
105
|
+
</Results.Column>
|
|
106
|
+
<Results.Column className="text-right tracking-tighter font-bold">
|
|
107
|
+
<span
|
|
108
|
+
className={cn(
|
|
109
|
+
'text-3xl',
|
|
110
|
+
result.points === 0 ? 'text-green-400' : null,
|
|
111
|
+
result.points > 0 && result.points <= 1.5 ? null : null,
|
|
112
|
+
result.points > 1.5 ? 'text-yellow-200' : null,
|
|
113
|
+
result.points > 3 ? 'text-orange-400' : null,
|
|
114
|
+
result.points >= 5 ? 'text-red-400' : null,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{(10 - result.points).toFixed(1)}
|
|
118
|
+
</span>{' '}
|
|
119
|
+
<span className="text-lg">/ 10</span>
|
|
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-2 items-center">
|
|
127
|
+
<IconWarning
|
|
128
|
+
className={cn(
|
|
129
|
+
check.points > 1 ? 'text-yellow-200' : null,
|
|
130
|
+
check.points > 2 ? 'text-orange-400' : null,
|
|
131
|
+
check.points > 3 ? 'text-red-400' : null,
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
{sanitize(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-400' : 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,52 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
import { Tooltip } from '../tooltip';
|
|
4
|
+
|
|
5
|
+
interface ToolbarButtonProps extends React.ComponentProps<'button'> {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
tooltip?: React.ReactNode;
|
|
9
|
+
delayDuration?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ToolbarButton = ({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
active,
|
|
16
|
+
tooltip,
|
|
17
|
+
delayDuration = 500,
|
|
18
|
+
...props
|
|
19
|
+
}: ToolbarButtonProps) => {
|
|
20
|
+
return (
|
|
21
|
+
<Tooltip.Provider>
|
|
22
|
+
<Tooltip delayDuration={delayDuration}>
|
|
23
|
+
<Tooltip.Trigger asChild>
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
{...props}
|
|
27
|
+
className={cn(
|
|
28
|
+
'h-full w-fit font-regular flex text-sm text-slate-10 items-center align-middle justify-center px-1 gap-2 relative',
|
|
29
|
+
'hover:text-slate-12 transition-colors',
|
|
30
|
+
active && 'data-[state=active]:text-cyan-11',
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
{active ? (
|
|
36
|
+
<motion.span
|
|
37
|
+
className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
|
|
38
|
+
layoutId="active-toolbar-button"
|
|
39
|
+
transition={{
|
|
40
|
+
type: 'spring',
|
|
41
|
+
bounce: 0.2,
|
|
42
|
+
duration: 0.6,
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
) : null}
|
|
46
|
+
</button>
|
|
47
|
+
</Tooltip.Trigger>
|
|
48
|
+
{tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
|
|
49
|
+
</Tooltip>
|
|
50
|
+
</Tooltip.Provider>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useCachedState = <T>(key: string) => {
|
|
4
|
+
let value: T | undefined = undefined;
|
|
5
|
+
if ('localStorage' in global) {
|
|
6
|
+
const storedValue = global.localStorage.getItem(key);
|
|
7
|
+
if (storedValue !== null && storedValue !== 'undefined') {
|
|
8
|
+
try {
|
|
9
|
+
value = JSON.parse(storedValue) as T;
|
|
10
|
+
} catch (exception) {
|
|
11
|
+
console.warn(
|
|
12
|
+
'Failed to load stored value for',
|
|
13
|
+
key,
|
|
14
|
+
'with value',
|
|
15
|
+
storedValue,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
useSyncExternalStore(
|
|
23
|
+
() => () => {},
|
|
24
|
+
() => value,
|
|
25
|
+
() => undefined,
|
|
26
|
+
),
|
|
27
|
+
function setValue(newValue: T | undefined) {
|
|
28
|
+
if ('localStorage' in global) {
|
|
29
|
+
global.localStorage.setItem(key, JSON.stringify(newValue));
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
] as const;
|
|
33
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as Tabs from '@radix-ui/react-tabs';
|
|
3
|
+
import { LayoutGroup } from 'framer-motion';
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { use, useEffect } from 'react';
|
|
6
|
+
import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility';
|
|
7
|
+
import { isBuilding } from '../app/env';
|
|
8
|
+
import { PreviewContext } from '../contexts/preview';
|
|
9
|
+
import { cn } from '../utils';
|
|
10
|
+
import { IconArrowDown } from './icons/icon-arrow-down';
|
|
11
|
+
import { IconCheck } from './icons/icon-check';
|
|
12
|
+
import { IconInfo } from './icons/icon-info';
|
|
13
|
+
import { IconReload } from './icons/icon-reload';
|
|
14
|
+
import { Compatibility, useCompatibility } from './toolbar/compatibility';
|
|
15
|
+
import { Linter, type LintingRow, useLinter } from './toolbar/linter';
|
|
16
|
+
import {
|
|
17
|
+
SpamAssassin,
|
|
18
|
+
type SpamCheckingResult,
|
|
19
|
+
useSpamAssassin,
|
|
20
|
+
} from './toolbar/spam-assassin';
|
|
21
|
+
import { ToolbarButton } from './toolbar/toolbar-button';
|
|
22
|
+
import { useCachedState } from './toolbar/use-cached-state';
|
|
23
|
+
|
|
24
|
+
export type ToolbarTabValue = 'linter' | 'compatibility' | 'spam-assassin';
|
|
25
|
+
|
|
26
|
+
export const useToolbarState = () => {
|
|
27
|
+
const searchParams = useSearchParams();
|
|
28
|
+
const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
|
|
29
|
+
| ToolbarTabValue
|
|
30
|
+
| undefined;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
activeTab,
|
|
34
|
+
|
|
35
|
+
toggled: activeTab !== undefined,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ToolbarInner = ({
|
|
40
|
+
serverLintingRows,
|
|
41
|
+
serverSpamCheckingResult,
|
|
42
|
+
serverCompatibilityResults,
|
|
43
|
+
|
|
44
|
+
markup,
|
|
45
|
+
reactMarkup,
|
|
46
|
+
plainText,
|
|
47
|
+
emailPath,
|
|
48
|
+
emailSlug,
|
|
49
|
+
}: ToolbarProps & {
|
|
50
|
+
markup: string;
|
|
51
|
+
reactMarkup: string;
|
|
52
|
+
plainText: string;
|
|
53
|
+
emailSlug: string;
|
|
54
|
+
emailPath: string;
|
|
55
|
+
}) => {
|
|
56
|
+
const pathname = usePathname();
|
|
57
|
+
const searchParams = useSearchParams();
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
|
|
60
|
+
const { activeTab, toggled } = useToolbarState();
|
|
61
|
+
|
|
62
|
+
const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
|
|
63
|
+
const params = new URLSearchParams(searchParams);
|
|
64
|
+
if (newValue === undefined) {
|
|
65
|
+
params.delete('toolbar-panel');
|
|
66
|
+
} else {
|
|
67
|
+
params.set('toolbar-panel', newValue);
|
|
68
|
+
}
|
|
69
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
|
|
73
|
+
useCachedState<SpamCheckingResult>(
|
|
74
|
+
`spam-assassin-${emailSlug.replaceAll('/', '-')}`,
|
|
75
|
+
);
|
|
76
|
+
const [spamCheckingResult, { load: loadSpamChecking, loading: spamLoading }] =
|
|
77
|
+
useSpamAssassin({
|
|
78
|
+
markup,
|
|
79
|
+
plainText,
|
|
80
|
+
|
|
81
|
+
initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [cachedLintingRows, setCachedLintingRows] = useCachedState<
|
|
85
|
+
LintingRow[]
|
|
86
|
+
>(`linter-${emailSlug.replaceAll('/', '-')}`);
|
|
87
|
+
const [lintingRows, { load: loadLinting, loading: lintLoading }] = useLinter({
|
|
88
|
+
markup,
|
|
89
|
+
|
|
90
|
+
initialRows: serverLintingRows ?? cachedLintingRows,
|
|
91
|
+
});
|
|
92
|
+
const [cachedCompatibilityResults, setCachedCompatibilityResults] =
|
|
93
|
+
useCachedState<CompatibilityCheckingResult[]>(
|
|
94
|
+
`compatibility-${emailSlug.replaceAll('/', '-')}`,
|
|
95
|
+
);
|
|
96
|
+
const [
|
|
97
|
+
compatibilityCheckingResults,
|
|
98
|
+
{ load: loadCompatibility, loading: compatibilityLoading },
|
|
99
|
+
] = useCompatibility({
|
|
100
|
+
emailPath,
|
|
101
|
+
reactMarkup,
|
|
102
|
+
|
|
103
|
+
initialResults: serverCompatibilityResults ?? cachedCompatibilityResults,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!isBuilding) {
|
|
107
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: This is fine since isBuilding does not change at runtime
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
(async () => {
|
|
110
|
+
const lintingRows = await loadLinting();
|
|
111
|
+
setCachedLintingRows(lintingRows);
|
|
112
|
+
|
|
113
|
+
const spamCheckingResult = await loadSpamChecking();
|
|
114
|
+
setCachedSpamCheckingResult(spamCheckingResult);
|
|
115
|
+
|
|
116
|
+
const compatibilityCheckingResults = await loadCompatibility();
|
|
117
|
+
setCachedCompatibilityResults(compatibilityCheckingResults);
|
|
118
|
+
})();
|
|
119
|
+
}, []);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-toggled={toggled}
|
|
125
|
+
className={cn(
|
|
126
|
+
'absolute bottom-0 left-0 right-0',
|
|
127
|
+
'border-t border-slate-6 group/toolbar text-xs text-slate-11 h-52 transition-transform',
|
|
128
|
+
'data-[toggled=false]:translate-y-[10.625rem]',
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<Tabs.Root
|
|
132
|
+
value={activeTab ?? ''}
|
|
133
|
+
onValueChange={(newValue) => {
|
|
134
|
+
setActivePanelValue(newValue as ToolbarTabValue);
|
|
135
|
+
}}
|
|
136
|
+
asChild
|
|
137
|
+
>
|
|
138
|
+
<div className="flex flex-col h-full">
|
|
139
|
+
<Tabs.List className="flex gap-4 px-4 border-b border-solid border-slate-6 h-10 w-full flex-shrink-0">
|
|
140
|
+
<LayoutGroup id="toolbar">
|
|
141
|
+
<Tabs.Trigger asChild value="linter">
|
|
142
|
+
<ToolbarButton active={activeTab === 'linter'}>
|
|
143
|
+
Linter
|
|
144
|
+
</ToolbarButton>
|
|
145
|
+
</Tabs.Trigger>
|
|
146
|
+
<Tabs.Trigger asChild value="compatibility">
|
|
147
|
+
<ToolbarButton active={activeTab === 'compatibility'}>
|
|
148
|
+
Compatibility
|
|
149
|
+
</ToolbarButton>
|
|
150
|
+
</Tabs.Trigger>
|
|
151
|
+
<Tabs.Trigger asChild value="spam-assassin">
|
|
152
|
+
<ToolbarButton active={activeTab === 'spam-assassin'}>
|
|
153
|
+
Spam
|
|
154
|
+
</ToolbarButton>
|
|
155
|
+
</Tabs.Trigger>
|
|
156
|
+
</LayoutGroup>
|
|
157
|
+
<div className="flex gap-0.5 ml-auto">
|
|
158
|
+
<ToolbarButton
|
|
159
|
+
delayDuration={0}
|
|
160
|
+
tooltip={
|
|
161
|
+
(activeTab === 'linter' &&
|
|
162
|
+
'The Linter tab checks all the images and links for common issues like missing alt text, broken URLs, insecure HTTP methods, and more.') ||
|
|
163
|
+
(activeTab === 'spam-assassin' &&
|
|
164
|
+
'The Spam tab will look at the content and use a robust scoring framework to determine if the email is likely to be spam. Powered by SpamAssassin.') ||
|
|
165
|
+
(activeTab === 'compatibility' &&
|
|
166
|
+
'The Compatibility tab shows how well the HTML/CSS is supported across mail clients like Outlook, Gmail, etc. Powered by Can I Email.') ||
|
|
167
|
+
'Info'
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
<IconInfo size={24} />
|
|
171
|
+
</ToolbarButton>
|
|
172
|
+
{isBuilding ? null : (
|
|
173
|
+
<ToolbarButton
|
|
174
|
+
tooltip="Reload"
|
|
175
|
+
disabled={lintLoading || spamLoading}
|
|
176
|
+
onClick={async () => {
|
|
177
|
+
if (activeTab === undefined) {
|
|
178
|
+
setActivePanelValue('linter');
|
|
179
|
+
}
|
|
180
|
+
if (activeTab === 'spam-assassin') {
|
|
181
|
+
await loadSpamChecking();
|
|
182
|
+
} else if (activeTab === 'linter') {
|
|
183
|
+
await loadLinting();
|
|
184
|
+
} else if (activeTab === 'compatibility') {
|
|
185
|
+
await loadCompatibility();
|
|
186
|
+
}
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<IconReload
|
|
190
|
+
size={24}
|
|
191
|
+
className={cn({
|
|
192
|
+
'animate-spin opacity-60 animate-spin-fast':
|
|
193
|
+
lintLoading || spamLoading,
|
|
194
|
+
})}
|
|
195
|
+
/>
|
|
196
|
+
</ToolbarButton>
|
|
197
|
+
)}
|
|
198
|
+
<ToolbarButton
|
|
199
|
+
tooltip="Toggle toolbar"
|
|
200
|
+
onClick={() => {
|
|
201
|
+
if (activeTab === undefined) {
|
|
202
|
+
setActivePanelValue('linter');
|
|
203
|
+
} else {
|
|
204
|
+
setActivePanelValue(undefined);
|
|
205
|
+
}
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<IconArrowDown
|
|
209
|
+
size={24}
|
|
210
|
+
className="transition-transform group-data-[toggled=false]/toolbar:rotate-180"
|
|
211
|
+
/>
|
|
212
|
+
</ToolbarButton>
|
|
213
|
+
</div>
|
|
214
|
+
</Tabs.List>
|
|
215
|
+
|
|
216
|
+
<div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto pr-3 pl-4 pt-3">
|
|
217
|
+
<Tabs.Content value="linter">
|
|
218
|
+
{lintLoading ? (
|
|
219
|
+
<LoadingState message="Analyzing your code for linting issues..." />
|
|
220
|
+
) : lintingRows?.length === 0 ? (
|
|
221
|
+
<SuccessWrapper>
|
|
222
|
+
<SuccessIcon />
|
|
223
|
+
<SuccessTitle>All good</SuccessTitle>
|
|
224
|
+
<SuccessDescription>
|
|
225
|
+
No linting issues found.
|
|
226
|
+
</SuccessDescription>
|
|
227
|
+
</SuccessWrapper>
|
|
228
|
+
) : (
|
|
229
|
+
<Linter rows={lintingRows ?? []} />
|
|
230
|
+
)}
|
|
231
|
+
</Tabs.Content>
|
|
232
|
+
<Tabs.Content value="compatibility">
|
|
233
|
+
{compatibilityLoading ? (
|
|
234
|
+
<LoadingState message="Checking email compatibility..." />
|
|
235
|
+
) : compatibilityCheckingResults?.length === 0 ? (
|
|
236
|
+
<SuccessWrapper>
|
|
237
|
+
<SuccessIcon />
|
|
238
|
+
<SuccessTitle>Great compatibility</SuccessTitle>
|
|
239
|
+
<SuccessDescription>
|
|
240
|
+
Template should render properly everywhere.
|
|
241
|
+
</SuccessDescription>
|
|
242
|
+
</SuccessWrapper>
|
|
243
|
+
) : (
|
|
244
|
+
<Compatibility results={compatibilityCheckingResults ?? []} />
|
|
245
|
+
)}
|
|
246
|
+
</Tabs.Content>
|
|
247
|
+
<Tabs.Content value="spam-assassin">
|
|
248
|
+
{spamLoading ? (
|
|
249
|
+
<LoadingState message="Evaluating your email for spam indicators..." />
|
|
250
|
+
) : spamCheckingResult?.isSpam === false ? (
|
|
251
|
+
<SuccessWrapper>
|
|
252
|
+
<SuccessIcon />
|
|
253
|
+
<SuccessTitle>10/10</SuccessTitle>
|
|
254
|
+
<SuccessDescription>
|
|
255
|
+
Your email is clean of abuse indicators.
|
|
256
|
+
</SuccessDescription>
|
|
257
|
+
</SuccessWrapper>
|
|
258
|
+
) : (
|
|
259
|
+
<SpamAssassin result={spamCheckingResult} />
|
|
260
|
+
)}
|
|
261
|
+
</Tabs.Content>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</Tabs.Root>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const LoadingState = ({ message }: { message: string }) => {
|
|
270
|
+
return (
|
|
271
|
+
<div className="flex flex-col items-center justify-center pt-8">
|
|
272
|
+
<div className="relative mb-8 flex items-center justify-center">
|
|
273
|
+
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-cyan-400/80 to-cyan-600/80 opacity-10 blur-xl absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 animate-pulse" />
|
|
274
|
+
<div className="h-12 w-12 rounded-full border border-slate-4 absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
275
|
+
<div className="h-10 w-10 rounded-full flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
276
|
+
<div className="h-5 w-5 rounded-full border-2 border-white/50 border-t-transparent animate-spin-fast" />
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<h3 className="text-slate-12 font-medium text-base mb-1">Processing</h3>
|
|
280
|
+
<p className="text-slate-11 text-sm text-center max-w-[320px]">
|
|
281
|
+
{message}
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const SuccessWrapper = ({ children }: { children: React.ReactNode }) => {
|
|
288
|
+
return (
|
|
289
|
+
<div className="flex flex-col items-center justify-center pt-8">
|
|
290
|
+
{children}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const SuccessIcon = () => {
|
|
296
|
+
return (
|
|
297
|
+
<div className="relative mb-8 flex items-center justify-center">
|
|
298
|
+
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-green-300/20 opacity-80 to-emerald-500/30 blur-md absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
299
|
+
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-400/80 opacity-10 to-emerald-600/80 absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-lg" />
|
|
300
|
+
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-green-400 to-emerald-600 flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-[inset_0_1px_1px_rgba(255,255,255,0.4)]">
|
|
301
|
+
<IconCheck size={24} className="text-white drop-shadow-sm" />
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const SuccessTitle = ({ children }) => {
|
|
308
|
+
return (
|
|
309
|
+
<h3 className="text-slate-12 font-medium text-base mb-1">{children}</h3>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const SuccessDescription = ({ children }) => {
|
|
314
|
+
return (
|
|
315
|
+
<p className="text-slate-11 text-sm text-center max-w-[320px]">
|
|
316
|
+
{children}
|
|
317
|
+
</p>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
interface ToolbarProps {
|
|
322
|
+
serverSpamCheckingResult: SpamCheckingResult | undefined;
|
|
323
|
+
serverLintingRows: LintingRow[] | undefined;
|
|
324
|
+
serverCompatibilityResults: CompatibilityCheckingResult[] | undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export const Toolbar = ({
|
|
328
|
+
serverLintingRows,
|
|
329
|
+
serverSpamCheckingResult,
|
|
330
|
+
serverCompatibilityResults,
|
|
331
|
+
}: ToolbarProps) => {
|
|
332
|
+
const { emailPath, emailSlug, renderedEmailMetadata } = use(PreviewContext)!;
|
|
333
|
+
|
|
334
|
+
if (renderedEmailMetadata === undefined) return null;
|
|
335
|
+
const { markup, plainText, reactMarkup } = renderedEmailMetadata;
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<ToolbarInner
|
|
339
|
+
emailPath={emailPath}
|
|
340
|
+
emailSlug={emailSlug}
|
|
341
|
+
markup={markup}
|
|
342
|
+
reactMarkup={reactMarkup}
|
|
343
|
+
plainText={plainText}
|
|
344
|
+
serverLintingRows={serverLintingRows}
|
|
345
|
+
serverSpamCheckingResult={serverSpamCheckingResult}
|
|
346
|
+
serverCompatibilityResults={serverCompatibilityResults}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '../utils';
|
|
4
|
+
|
|
5
|
+
type ContentElement = React.ComponentRef<typeof TooltipPrimitive.Content>;
|
|
6
|
+
type ContentProps = React.ComponentPropsWithoutRef<
|
|
7
|
+
typeof TooltipPrimitive.Content
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export type TooltipProps = ContentProps;
|
|
11
|
+
|
|
12
|
+
export const TooltipContent = React.forwardRef<
|
|
13
|
+
ContentElement,
|
|
14
|
+
Readonly<TooltipProps>
|
|
15
|
+
>(({ sideOffset = 6, children, ...props }, forwardedRef) => (
|
|
16
|
+
<TooltipPrimitive.Portal>
|
|
17
|
+
<TooltipPrimitive.Content
|
|
18
|
+
{...props}
|
|
19
|
+
className={cn(
|
|
20
|
+
'z-20 rounded-md border border-slate-6 bg-black px-3 py-2 text-white text-xs',
|
|
21
|
+
'font-sans max-w-60',
|
|
22
|
+
)}
|
|
23
|
+
ref={forwardedRef}
|
|
24
|
+
sideOffset={sideOffset}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</TooltipPrimitive.Content>
|
|
28
|
+
</TooltipPrimitive.Portal>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
TooltipContent.displayName = 'TooltipContent';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { TooltipContent } from './tooltip-content';
|
|
4
|
+
|
|
5
|
+
type RootProps = React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>;
|
|
6
|
+
|
|
7
|
+
export type TooltipProps = RootProps;
|
|
8
|
+
|
|
9
|
+
export const TooltipRoot: React.FC<Readonly<TooltipProps>> = ({
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}) => <TooltipPrimitive.Root {...props}>{children}</TooltipPrimitive.Root>;
|
|
13
|
+
|
|
14
|
+
export const Tooltip = Object.assign(TooltipRoot, {
|
|
15
|
+
Arrow: TooltipPrimitive.TooltipArrow,
|
|
16
|
+
Provider: TooltipPrimitive.TooltipProvider,
|
|
17
|
+
Content: TooltipContent,
|
|
18
|
+
Trigger: TooltipPrimitive.TooltipTrigger,
|
|
19
|
+
});
|