promote-email-templates 0.0.19 → 0.1.1
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/.react-email/.eslintrc.js +52 -0
- package/.react-email/.prettierignore +3 -0
- package/.react-email/.prettierrc.js +8 -0
- package/.react-email/license.md +7 -0
- package/.react-email/next.config.js +36 -0
- package/.react-email/package.json +1 -0
- package/.react-email/postcss.config.js +8 -0
- package/.react-email/readme.md +44 -0
- package/.react-email/src/actions/get-email-path-from-slug.ts +26 -0
- package/.react-email/src/actions/get-emails-directory-metadata.spec.ts +73 -0
- package/.react-email/src/actions/get-emails-directory-metadata.ts +91 -0
- package/.react-email/src/actions/render-email-by-path.tsx +59 -0
- package/.react-email/src/app/favicon.ico +0 -0
- package/.react-email/src/app/globals.css +35 -0
- package/.react-email/src/app/inter.ts +7 -0
- package/.react-email/src/app/layout.tsx +36 -0
- package/.react-email/src/app/logo.png +0 -0
- package/.react-email/src/app/page.tsx +47 -0
- package/.react-email/src/app/preview/[...slug]/page.tsx +65 -0
- package/.react-email/src/app/preview/[...slug]/preview.tsx +141 -0
- package/.react-email/src/app/preview/[...slug]/rendering-error.tsx +40 -0
- package/.react-email/src/components/button.tsx +90 -0
- package/.react-email/src/components/code-container.tsx +145 -0
- package/.react-email/src/components/code.tsx +112 -0
- package/.react-email/src/components/heading.tsx +113 -0
- package/.react-email/src/components/icons/icon-arrow-down.tsx +16 -0
- package/.react-email/src/components/icons/icon-base.tsx +24 -0
- package/.react-email/src/components/icons/icon-button.tsx +23 -0
- package/.react-email/src/components/icons/icon-check.tsx +19 -0
- package/.react-email/src/components/icons/icon-clipboard.tsx +40 -0
- package/.react-email/src/components/icons/icon-download.tsx +19 -0
- package/.react-email/src/components/icons/icon-file.tsx +19 -0
- package/.react-email/src/components/icons/icon-folder-open.tsx +19 -0
- package/.react-email/src/components/icons/icon-folder.tsx +18 -0
- package/.react-email/src/components/icons/icon-hide-sidebar.tsx +23 -0
- package/.react-email/src/components/icons/icon-monitor.tsx +19 -0
- package/.react-email/src/components/icons/icon-phone.tsx +26 -0
- package/.react-email/src/components/icons/icon-source.tsx +19 -0
- package/.react-email/src/components/index.ts +7 -0
- package/.react-email/src/components/logo.tsx +64 -0
- package/.react-email/src/components/send.tsx +135 -0
- package/.react-email/src/components/shell.tsx +115 -0
- package/.react-email/src/components/sidebar/index.ts +1 -0
- package/.react-email/src/components/sidebar/sidebar-directory-children.tsx +134 -0
- package/.react-email/src/components/sidebar/sidebar-directory.tsx +106 -0
- package/.react-email/src/components/sidebar/sidebar.tsx +45 -0
- package/.react-email/src/components/text.tsx +99 -0
- package/.react-email/src/components/tooltip-content.tsx +32 -0
- package/.react-email/src/components/tooltip.tsx +19 -0
- package/.react-email/src/components/topbar.tsx +161 -0
- package/.react-email/src/contexts/emails.tsx +127 -0
- package/.react-email/src/hooks/use-hot-reload.ts +35 -0
- package/.react-email/src/hooks/use-rendering-metadata.ts +36 -0
- package/.react-email/src/utils/cn.ts +6 -0
- package/.react-email/src/utils/constants.ts +6 -0
- package/.react-email/src/utils/copy-text-to-clipboard.ts +7 -0
- package/.react-email/src/utils/emails-directory-absolute-path.ts +34 -0
- package/.react-email/src/utils/get-email-component.ts +108 -0
- package/.react-email/src/utils/improve-error-with-sourcemap.ts +55 -0
- package/.react-email/src/utils/index.ts +5 -0
- package/.react-email/src/utils/language-map.ts +7 -0
- package/.react-email/src/utils/static-node-modules-for-vm.ts +92 -0
- package/.react-email/src/utils/types/as.ts +26 -0
- package/.react-email/src/utils/types/email-template.ts +8 -0
- package/.react-email/src/utils/types/error-object.ts +11 -0
- package/.react-email/src/utils/types/hot-reload-change.ts +6 -0
- package/.react-email/src/utils/types/hot-reload-event.ts +6 -0
- package/.react-email/src/utils/unreachable.ts +8 -0
- package/.react-email/tailwind.config.ts +94 -0
- package/dist/index.d.mts +238 -83
- package/dist/index.d.ts +238 -83
- package/dist/index.js +364 -105
- package/dist/index.mjs +360 -105
- package/package.json +1 -1
- package/emails/admin/abort-order-request.tsx +0 -58
- package/emails/admin/index.ts +0 -2
- package/emails/admin/revert-payment-request-admin.tsx +0 -54
- package/emails/all/index.ts +0 -1
- package/emails/all/welcome.tsx +0 -104
- package/emails/brand/evidences-accepted-brand.tsx +0 -59
- package/emails/brand/evidences-submitted-brand.tsx +0 -65
- package/emails/brand/index.ts +0 -6
- package/emails/brand/new-order-created-brand.tsx +0 -74
- package/emails/brand/order-accepted-brand.tsx +0 -63
- package/emails/brand/order-cancelled-brand.tsx +0 -68
- package/emails/brand/order-rejected-brand.tsx +0 -79
- package/emails/creator/evidences-approved-creator.tsx +0 -62
- package/emails/creator/evidences-rejected-creator.tsx +0 -74
- package/emails/creator/index.ts +0 -5
- package/emails/creator/new-order-created-creator.tsx +0 -70
- package/emails/creator/order-cancelled-creator.tsx +0 -69
- package/emails/creator/order-payment-creator.tsx +0 -58
- package/emails/index.ts +0 -5
- package/emails/shared/components/base-head.tsx +0 -19
- package/emails/shared/components/comment-component.tsx +0 -32
- package/emails/shared/components/footer.tsx +0 -84
- package/emails/shared/components/header.tsx +0 -24
- package/emails/shared/components/new-order-info.tsx +0 -60
- package/emails/shared/components/payment-amount.tsx +0 -37
- package/emails/shared/components/user-Info.tsx +0 -55
- package/emails/shared/index.ts +0 -3
- package/emails/shared/styles.ts +0 -72
- package/emails/shared/types.ts +0 -179
- package/emails/shared/values.ts +0 -19
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Toaster } from 'sonner';
|
|
6
|
+
import { useHotreload } from '../../../hooks/use-hot-reload';
|
|
7
|
+
import type { EmailRenderingResult } from '../../../actions/render-email-by-path';
|
|
8
|
+
import { CodeContainer } from '../../../components/code-container';
|
|
9
|
+
import { Shell } from '../../../components/shell';
|
|
10
|
+
import { Tooltip } from '../../../components/tooltip';
|
|
11
|
+
import { useEmails } from '../../../contexts/emails';
|
|
12
|
+
import { useRenderingMetadata } from '../../../hooks/use-rendering-metadata';
|
|
13
|
+
import { RenderingError } from './rendering-error';
|
|
14
|
+
|
|
15
|
+
interface PreviewProps {
|
|
16
|
+
slug: string;
|
|
17
|
+
emailPath: string;
|
|
18
|
+
renderingResult: EmailRenderingResult;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Preview = ({
|
|
22
|
+
slug,
|
|
23
|
+
emailPath,
|
|
24
|
+
renderingResult: initialRenderingResult,
|
|
25
|
+
}: PreviewProps) => {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const pathname = usePathname();
|
|
28
|
+
const searchParams = useSearchParams();
|
|
29
|
+
|
|
30
|
+
const activeView = searchParams.get('view') ?? 'desktop';
|
|
31
|
+
const activeLang = searchParams.get('lang') ?? 'jsx';
|
|
32
|
+
const { useEmailRenderingResult } = useEmails();
|
|
33
|
+
|
|
34
|
+
const renderingResult = useEmailRenderingResult(
|
|
35
|
+
emailPath,
|
|
36
|
+
initialRenderingResult,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const renderedEmailMetadata = useRenderingMetadata(
|
|
40
|
+
emailPath,
|
|
41
|
+
renderingResult,
|
|
42
|
+
initialRenderingResult,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
|
|
46
|
+
// this will not change on runtime so it doesn't violate
|
|
47
|
+
// the rules of hooks
|
|
48
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
49
|
+
useHotreload((changes) => {
|
|
50
|
+
const changeForThisEmail = changes.find((change) =>
|
|
51
|
+
change.filename.includes(slug),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (typeof changeForThisEmail !== 'undefined') {
|
|
55
|
+
if (changeForThisEmail.event === 'unlink') {
|
|
56
|
+
router.push('/');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleViewChange = (view: string) => {
|
|
63
|
+
const params = new URLSearchParams(searchParams);
|
|
64
|
+
params.set('view', view);
|
|
65
|
+
router.push(`${pathname}?${params.toString()}`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleLangChange = (lang: string) => {
|
|
69
|
+
const params = new URLSearchParams(searchParams);
|
|
70
|
+
params.set('view', 'source');
|
|
71
|
+
params.set('lang', lang);
|
|
72
|
+
router.push(`${pathname}?${params.toString()}`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hasNoErrors = typeof renderedEmailMetadata !== 'undefined';
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Shell
|
|
79
|
+
activeView={hasNoErrors ? activeView : undefined}
|
|
80
|
+
currentEmailOpenSlug={slug}
|
|
81
|
+
markup={renderedEmailMetadata?.markup}
|
|
82
|
+
setActiveView={hasNoErrors ? handleViewChange : undefined}
|
|
83
|
+
>
|
|
84
|
+
{/* This relative is so that when there is any error the user can still switch between emails */}
|
|
85
|
+
<div className="relative h-full">
|
|
86
|
+
{'error' in renderingResult ? (
|
|
87
|
+
<RenderingError error={renderingResult.error} />
|
|
88
|
+
) : null}
|
|
89
|
+
|
|
90
|
+
{/* If this is undefined means that the initial server render of the email had errors */}
|
|
91
|
+
{hasNoErrors ? (
|
|
92
|
+
<>
|
|
93
|
+
{activeView === 'desktop' && (
|
|
94
|
+
<iframe
|
|
95
|
+
className="w-full bg-white h-[calc(100vh_-_140px)] lg:h-[calc(100vh_-_70px)]"
|
|
96
|
+
srcDoc={renderedEmailMetadata.markup}
|
|
97
|
+
title={slug}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{activeView === 'mobile' && (
|
|
102
|
+
<iframe
|
|
103
|
+
className="w-[360px] bg-white h-[calc(100vh_-_140px)] lg:h-[calc(100vh_-_70px)] mx-auto"
|
|
104
|
+
srcDoc={renderedEmailMetadata.markup}
|
|
105
|
+
title={slug}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{activeView === 'source' && (
|
|
110
|
+
<div className="flex gap-6 mx-auto p-6 max-w-3xl">
|
|
111
|
+
<Tooltip.Provider>
|
|
112
|
+
<CodeContainer
|
|
113
|
+
activeLang={activeLang}
|
|
114
|
+
markups={[
|
|
115
|
+
{
|
|
116
|
+
language: 'jsx',
|
|
117
|
+
content: renderedEmailMetadata.reactMarkup,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
language: 'markup',
|
|
121
|
+
content: renderedEmailMetadata.markup,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
language: 'markdown',
|
|
125
|
+
content: renderedEmailMetadata.plainText,
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
setActiveLang={handleLangChange}
|
|
129
|
+
/>
|
|
130
|
+
</Tooltip.Provider>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</>
|
|
134
|
+
) : null}
|
|
135
|
+
<Toaster />
|
|
136
|
+
</div>
|
|
137
|
+
</Shell>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default Preview;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ErrorObject } from '../../../utils/types/error-object';
|
|
3
|
+
|
|
4
|
+
export const RenderingError = (props: { error: ErrorObject }) => {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<div className="absolute inset-0 z-50 bg-black/80" />
|
|
8
|
+
<div className="md:max-w-[568px] lg:max-w-[968px] absolute left-[50%] top-[50%] min-h-[50vh] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-t-4 bg-white text-black p-6 shadow-lg duration-200 sm:rounded-lg rounded-t-sm">
|
|
9
|
+
<div className="flex flex-col max-w-full min-w-0 space-y-1.5">
|
|
10
|
+
<h2 className="text-lg flex items-center flex-shrink gap-4 font-semibold pb-2 leading-none tracking-tight">
|
|
11
|
+
<svg
|
|
12
|
+
className="h-6 w-6 text-red-600 font-extrabold"
|
|
13
|
+
fill="none"
|
|
14
|
+
height="24"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
width="24"
|
|
21
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
+
>
|
|
23
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
24
|
+
<path d="M12 9v4" />
|
|
25
|
+
<path d="M12 17h.01" />
|
|
26
|
+
</svg>
|
|
27
|
+
{props.error.name}: {props.error.message}
|
|
28
|
+
</h2>
|
|
29
|
+
{props.error.stack ? (
|
|
30
|
+
<div className="text-sm p-2 flex-grow scroll-px-4 overflow-x-auto bg-red-500 rounded-lg text-gray-100">
|
|
31
|
+
<pre className="font-mono w-full min-w-0 leading-7">
|
|
32
|
+
{props.error.stack}
|
|
33
|
+
</pre>
|
|
34
|
+
</div>
|
|
35
|
+
) : undefined}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as SlotPrimitive from '@radix-ui/react-slot';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { unreachable } from '../utils/unreachable';
|
|
4
|
+
import { cn } from '../utils/cn';
|
|
5
|
+
|
|
6
|
+
type ButtonElement = React.ElementRef<'button'>;
|
|
7
|
+
type RootProps = React.ComponentPropsWithoutRef<'button'>;
|
|
8
|
+
|
|
9
|
+
type Appearance = 'white' | 'gradient';
|
|
10
|
+
type Size = '1' | '2' | '3' | '4';
|
|
11
|
+
|
|
12
|
+
interface ButtonProps extends RootProps {
|
|
13
|
+
asChild?: boolean;
|
|
14
|
+
appearance?: Appearance;
|
|
15
|
+
size?: Size;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Button = React.forwardRef<ButtonElement, Readonly<ButtonProps>>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
asChild,
|
|
22
|
+
appearance = 'white',
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
size = '2',
|
|
26
|
+
...props
|
|
27
|
+
},
|
|
28
|
+
forwardedRef,
|
|
29
|
+
) => {
|
|
30
|
+
const classNames = cn(
|
|
31
|
+
getSize(size),
|
|
32
|
+
getAppearance(appearance),
|
|
33
|
+
'inline-flex items-center justify-center border font-medium',
|
|
34
|
+
className,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return asChild ? (
|
|
38
|
+
<SlotPrimitive.Slot ref={forwardedRef} {...props} className={classNames}>
|
|
39
|
+
<SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>
|
|
40
|
+
</SlotPrimitive.Slot>
|
|
41
|
+
) : (
|
|
42
|
+
<button
|
|
43
|
+
className={classNames}
|
|
44
|
+
ref={forwardedRef}
|
|
45
|
+
type="button"
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Button.displayName = 'Button';
|
|
55
|
+
|
|
56
|
+
const getAppearance = (appearance: Appearance | undefined) => {
|
|
57
|
+
switch (appearance) {
|
|
58
|
+
case undefined:
|
|
59
|
+
case 'white':
|
|
60
|
+
return [
|
|
61
|
+
'bg-white text-black',
|
|
62
|
+
'hover:bg-white/90',
|
|
63
|
+
'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-white/90',
|
|
64
|
+
];
|
|
65
|
+
case 'gradient':
|
|
66
|
+
return [
|
|
67
|
+
'bg-gradient backdrop-blur-[20px] border-[#34343A]',
|
|
68
|
+
'hover:bg-gradientHover',
|
|
69
|
+
'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-gradientHover',
|
|
70
|
+
];
|
|
71
|
+
default:
|
|
72
|
+
unreachable(appearance);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getSize = (size: Size | undefined) => {
|
|
77
|
+
switch (size) {
|
|
78
|
+
case '1':
|
|
79
|
+
return '';
|
|
80
|
+
case undefined:
|
|
81
|
+
case '2':
|
|
82
|
+
return 'text-[14px] h-8 px-3 rounded-md gap-2';
|
|
83
|
+
case '3':
|
|
84
|
+
return 'text-[14px] h-10 px-4 rounded-md gap-2';
|
|
85
|
+
case '4':
|
|
86
|
+
return 'text-base h-11 px-4 rounded-md gap-2';
|
|
87
|
+
default:
|
|
88
|
+
unreachable(size);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { LayoutGroup, motion } from 'framer-motion';
|
|
2
|
+
import type { Language } from 'prism-react-renderer';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { copyTextToClipboard } from '../utils';
|
|
5
|
+
import languageMap from '../utils/language-map';
|
|
6
|
+
import { tabTransition } from '../utils/constants';
|
|
7
|
+
import { Code } from './code';
|
|
8
|
+
import { IconButton } from './icons/icon-button';
|
|
9
|
+
import { IconCheck } from './icons/icon-check';
|
|
10
|
+
import { IconClipboard } from './icons/icon-clipboard';
|
|
11
|
+
import { IconDownload } from './icons/icon-download';
|
|
12
|
+
import { Tooltip } from './tooltip';
|
|
13
|
+
|
|
14
|
+
interface CodeContainerProps {
|
|
15
|
+
markups: MarkupProps[];
|
|
16
|
+
activeLang: string;
|
|
17
|
+
setActiveLang: (lang: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MarkupProps {
|
|
21
|
+
language: Language;
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
|
|
26
|
+
markups,
|
|
27
|
+
activeLang,
|
|
28
|
+
setActiveLang,
|
|
29
|
+
}) => {
|
|
30
|
+
const [isCopied, setIsCopied] = React.useState(false);
|
|
31
|
+
|
|
32
|
+
const renderDownloadIcon = () => {
|
|
33
|
+
const value = markups.filter((markup) => markup.language === activeLang);
|
|
34
|
+
if (typeof value[0] === 'undefined') return;
|
|
35
|
+
const file = new File([value[0].content], `email.${value[0].language}`);
|
|
36
|
+
const url = URL.createObjectURL(file);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<a
|
|
40
|
+
className="text-slate-11 transition ease-in-out duration-200 hover:text-slate-12"
|
|
41
|
+
download={file.name}
|
|
42
|
+
href={url}
|
|
43
|
+
>
|
|
44
|
+
<IconDownload />
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const renderClipboardIcon = () => {
|
|
50
|
+
const handleClipboard = async () => {
|
|
51
|
+
const activeContent = markups.filter(({ language }) => {
|
|
52
|
+
return activeLang === language;
|
|
53
|
+
});
|
|
54
|
+
setIsCopied(true);
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
56
|
+
await copyTextToClipboard(activeContent[0]!.content);
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
setIsCopied(false);
|
|
59
|
+
}, 3000);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<IconButton onClick={() => void handleClipboard()}>
|
|
64
|
+
{isCopied ? <IconCheck /> : <IconClipboard />}
|
|
65
|
+
</IconButton>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
setIsCopied(false);
|
|
71
|
+
}, [activeLang]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className="border-slate-6 relative w-full items-center whitespace-pre rounded-md border text-sm backdrop-blur-md"
|
|
76
|
+
style={{
|
|
77
|
+
lineHeight: '130%',
|
|
78
|
+
background:
|
|
79
|
+
'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',
|
|
80
|
+
boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<div className="h-9 border-b border-slate-6">
|
|
84
|
+
<div className="flex">
|
|
85
|
+
<LayoutGroup id="code">
|
|
86
|
+
{markups.map(({ language }) => {
|
|
87
|
+
const isCurrentLang = activeLang === language;
|
|
88
|
+
return (
|
|
89
|
+
<motion.button
|
|
90
|
+
className={`relative py-[8px] px-4 text-sm font-medium font-sans transition ease-in-out duration-200 hover:text-slate-12 ${
|
|
91
|
+
activeLang !== language ? 'text-slate-11' : 'text-slate-12'
|
|
92
|
+
}`}
|
|
93
|
+
key={language}
|
|
94
|
+
onClick={() => {
|
|
95
|
+
setActiveLang(language);
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{isCurrentLang ? (
|
|
99
|
+
<motion.span
|
|
100
|
+
animate={{ opacity: 1 }}
|
|
101
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
102
|
+
exit={{ opacity: 0 }}
|
|
103
|
+
initial={{ opacity: 0 }}
|
|
104
|
+
layoutId="code"
|
|
105
|
+
transition={tabTransition}
|
|
106
|
+
/>
|
|
107
|
+
) : null}
|
|
108
|
+
{languageMap[language]}
|
|
109
|
+
</motion.button>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</LayoutGroup>
|
|
113
|
+
</div>
|
|
114
|
+
<Tooltip>
|
|
115
|
+
<Tooltip.Trigger
|
|
116
|
+
asChild
|
|
117
|
+
className="absolute top-2 right-2 hidden md:block"
|
|
118
|
+
>
|
|
119
|
+
{renderClipboardIcon()}
|
|
120
|
+
</Tooltip.Trigger>
|
|
121
|
+
<Tooltip.Content>Copy to Clipboard</Tooltip.Content>
|
|
122
|
+
</Tooltip>
|
|
123
|
+
<Tooltip>
|
|
124
|
+
<Tooltip.Trigger
|
|
125
|
+
asChild
|
|
126
|
+
className="text-gray-11 absolute top-2 right-8 hidden md:block"
|
|
127
|
+
>
|
|
128
|
+
{renderDownloadIcon()}
|
|
129
|
+
</Tooltip.Trigger>
|
|
130
|
+
<Tooltip.Content>Download</Tooltip.Content>
|
|
131
|
+
</Tooltip>
|
|
132
|
+
</div>
|
|
133
|
+
{markups.map(({ language, content }) => {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className={`${activeLang !== language && 'hidden'}`}
|
|
137
|
+
key={language}
|
|
138
|
+
>
|
|
139
|
+
<Code language={language}>{content}</Code>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Language } from 'prism-react-renderer';
|
|
2
|
+
import { Highlight } from 'prism-react-renderer';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../utils';
|
|
5
|
+
|
|
6
|
+
interface CodeProps {
|
|
7
|
+
children: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
language?: Language;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const theme = {
|
|
13
|
+
plain: {
|
|
14
|
+
color: '#EDEDEF',
|
|
15
|
+
fontSize: 13,
|
|
16
|
+
fontFamily: 'MonoLisa, Menlo, monospace',
|
|
17
|
+
},
|
|
18
|
+
styles: [
|
|
19
|
+
{
|
|
20
|
+
types: ['comment'],
|
|
21
|
+
style: {
|
|
22
|
+
color: '#706F78',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
types: ['atrule', 'keyword', 'attr-name', 'selector'],
|
|
27
|
+
style: {
|
|
28
|
+
color: '#7E7D86',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
types: ['punctuation', 'operator'],
|
|
33
|
+
style: {
|
|
34
|
+
color: '#706F78',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
types: ['class-name', 'function', 'tag', 'key-white'],
|
|
39
|
+
style: {
|
|
40
|
+
color: '#EDEDEF',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Code: React.FC<Readonly<CodeProps>> = ({
|
|
47
|
+
children,
|
|
48
|
+
language = 'html',
|
|
49
|
+
}) => {
|
|
50
|
+
const value = children.trim();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Highlight code={value} language={language} theme={theme}>
|
|
54
|
+
{({ tokens, getLineProps, getTokenProps }) => (
|
|
55
|
+
<>
|
|
56
|
+
<div
|
|
57
|
+
className="absolute right-0 top-0 h-px w-[200px]"
|
|
58
|
+
style={{
|
|
59
|
+
background:
|
|
60
|
+
'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
<pre className="p-4 h-[650px] overflow-auto">
|
|
64
|
+
{tokens.map((line, i) => {
|
|
65
|
+
const lineProps = getLineProps({
|
|
66
|
+
line,
|
|
67
|
+
key: i,
|
|
68
|
+
});
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
key={i}
|
|
72
|
+
{...lineProps}
|
|
73
|
+
className={cn('whitespace-pre', {
|
|
74
|
+
"before:text-slate-11 before:mr-2 before:content-['$']":
|
|
75
|
+
language === 'bash' && tokens.length === 1,
|
|
76
|
+
})}
|
|
77
|
+
>
|
|
78
|
+
{line.map((token, key) => {
|
|
79
|
+
const tokenProps = getTokenProps({
|
|
80
|
+
token,
|
|
81
|
+
key,
|
|
82
|
+
});
|
|
83
|
+
const isException =
|
|
84
|
+
token.content === 'from' &&
|
|
85
|
+
line[key + 1]?.content === ':';
|
|
86
|
+
const newTypes = isException
|
|
87
|
+
? [...token.types, 'key-white']
|
|
88
|
+
: token.types;
|
|
89
|
+
token.types = newTypes;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<React.Fragment key={key}>
|
|
93
|
+
<span {...tokenProps} />
|
|
94
|
+
</React.Fragment>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</pre>
|
|
101
|
+
<div
|
|
102
|
+
className="absolute left-0 bottom-0 h-px w-[200px]"
|
|
103
|
+
style={{
|
|
104
|
+
background:
|
|
105
|
+
'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</Highlight>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as SlotPrimitive from '@radix-ui/react-slot';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { type As, unreachable, cn } from '../utils';
|
|
4
|
+
|
|
5
|
+
export type HeadingSize =
|
|
6
|
+
| '1'
|
|
7
|
+
| '2'
|
|
8
|
+
| '3'
|
|
9
|
+
| '4'
|
|
10
|
+
| '5'
|
|
11
|
+
| '6'
|
|
12
|
+
| '7'
|
|
13
|
+
| '8'
|
|
14
|
+
| '9'
|
|
15
|
+
| '10';
|
|
16
|
+
export type HeadingColor = 'white' | 'gray';
|
|
17
|
+
export type HeadingWeight = 'medium' | 'bold';
|
|
18
|
+
|
|
19
|
+
interface HeadingOwnProps {
|
|
20
|
+
size?: HeadingSize;
|
|
21
|
+
color?: HeadingColor;
|
|
22
|
+
weight?: HeadingWeight;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type HeadingProps = As<'h1', 'h2', 'h3', 'h4', 'h5', 'h6'> & HeadingOwnProps;
|
|
26
|
+
|
|
27
|
+
export const Heading = React.forwardRef<
|
|
28
|
+
HTMLHeadingElement,
|
|
29
|
+
Readonly<HeadingProps>
|
|
30
|
+
>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
as: Tag = 'h1',
|
|
34
|
+
size = '3',
|
|
35
|
+
className,
|
|
36
|
+
color = 'white',
|
|
37
|
+
children,
|
|
38
|
+
weight = 'bold',
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
forwardedRef,
|
|
42
|
+
) => (
|
|
43
|
+
<SlotPrimitive.Slot
|
|
44
|
+
className={cn(
|
|
45
|
+
className,
|
|
46
|
+
getSizesClassNames(size),
|
|
47
|
+
getColorClassNames(color),
|
|
48
|
+
getWeightClassNames(weight),
|
|
49
|
+
)}
|
|
50
|
+
ref={forwardedRef}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
<Tag>{children}</Tag>
|
|
54
|
+
</SlotPrimitive.Slot>
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const getSizesClassNames = (size: HeadingSize | undefined) => {
|
|
59
|
+
switch (size) {
|
|
60
|
+
case '1':
|
|
61
|
+
return 'text-xs';
|
|
62
|
+
case '2':
|
|
63
|
+
return 'text-sm';
|
|
64
|
+
case undefined:
|
|
65
|
+
case '3':
|
|
66
|
+
return 'text-base';
|
|
67
|
+
case '4':
|
|
68
|
+
return 'text-lg';
|
|
69
|
+
case '5':
|
|
70
|
+
return 'text-xl tracking-[-0.16px]';
|
|
71
|
+
case '6':
|
|
72
|
+
return 'text-2xl tracking-[-0.288px]';
|
|
73
|
+
case '7':
|
|
74
|
+
return 'text-[28px] leading-[34px] tracking-[-0.416px]';
|
|
75
|
+
case '8':
|
|
76
|
+
return 'text-[35px] leading-[42px] tracking-[-0.64px]';
|
|
77
|
+
case '9':
|
|
78
|
+
return 'text-6xl leading-[73px] tracking-[-0.896px]';
|
|
79
|
+
case '10':
|
|
80
|
+
return [
|
|
81
|
+
'text-[38px] leading-[46px]',
|
|
82
|
+
'md:text-[70px] md:leading-[85px] tracking-[-1.024px;]',
|
|
83
|
+
];
|
|
84
|
+
default:
|
|
85
|
+
return unreachable(size);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getColorClassNames = (color: HeadingColor | undefined) => {
|
|
90
|
+
switch (color) {
|
|
91
|
+
case 'gray':
|
|
92
|
+
return 'text-slate-11';
|
|
93
|
+
case 'white':
|
|
94
|
+
case undefined:
|
|
95
|
+
return 'text-slate-12';
|
|
96
|
+
default:
|
|
97
|
+
return unreachable(color);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getWeightClassNames = (weight: HeadingWeight | undefined) => {
|
|
102
|
+
switch (weight) {
|
|
103
|
+
case 'medium':
|
|
104
|
+
return 'font-medium';
|
|
105
|
+
case 'bold':
|
|
106
|
+
case undefined:
|
|
107
|
+
return 'font-bold';
|
|
108
|
+
default:
|
|
109
|
+
return unreachable(weight);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
Heading.displayName = 'Heading';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconArrowDown = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
6
|
+
({ ...props }, forwardedRef) => (
|
|
7
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
8
|
+
<path
|
|
9
|
+
d="M12 16L6 9.85966L6.84 9L12 14.2808L17.16 9L18 9.85966L12 16Z"
|
|
10
|
+
fill="currentColor"
|
|
11
|
+
/>
|
|
12
|
+
</IconBase>
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
IconArrowDown.displayName = 'IconArrowDown';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export type IconElement = React.ElementRef<'svg'>;
|
|
4
|
+
export type RootProps = React.ComponentPropsWithoutRef<'svg'>;
|
|
5
|
+
|
|
6
|
+
export interface IconProps extends RootProps {
|
|
7
|
+
size?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const IconBase = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
11
|
+
({ size = 20, ...props }, forwardedRef) => (
|
|
12
|
+
<svg
|
|
13
|
+
fill="none"
|
|
14
|
+
height={size}
|
|
15
|
+
ref={forwardedRef}
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
width={size}
|
|
18
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
IconBase.displayName = 'IconBase';
|