promote-email-templates 0.0.5 → 0.0.7
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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -9
- package/dist/index.mjs +17 -9
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
|
2
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import type { EmailsDirectory } from '../../actions/get-emails-directory-metadata';
|
|
6
|
+
import {
|
|
7
|
+
emailsDirectoryAbsolutePath,
|
|
8
|
+
pathSeparator,
|
|
9
|
+
} from '../../utils/emails-directory-absolute-path';
|
|
10
|
+
import { cn } from '../../utils';
|
|
11
|
+
import { IconFile } from '../icons/icon-file';
|
|
12
|
+
import { SidebarDirectory } from './sidebar-directory';
|
|
13
|
+
|
|
14
|
+
export const SidebarDirectoryChildren = (props: {
|
|
15
|
+
emailsDirectoryMetadata: EmailsDirectory;
|
|
16
|
+
currentEmailOpenSlug?: string;
|
|
17
|
+
open: boolean;
|
|
18
|
+
isRoot?: boolean;
|
|
19
|
+
}) => {
|
|
20
|
+
const searchParams = useSearchParams();
|
|
21
|
+
const directoryPathRelativeToEmailsDirectory =
|
|
22
|
+
props.emailsDirectoryMetadata.absolutePath
|
|
23
|
+
.replace(`${emailsDirectoryAbsolutePath}${pathSeparator}`, '')
|
|
24
|
+
.replace(emailsDirectoryAbsolutePath, '')
|
|
25
|
+
.trim();
|
|
26
|
+
const isBaseEmailsDirectory =
|
|
27
|
+
props.emailsDirectoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<AnimatePresence initial={false}>
|
|
31
|
+
{props.open ? (
|
|
32
|
+
<Collapsible.Content
|
|
33
|
+
asChild
|
|
34
|
+
className="relative data-[root=true]:mt-2 overflow-y-hidden pl-1"
|
|
35
|
+
forceMount
|
|
36
|
+
>
|
|
37
|
+
<motion.div
|
|
38
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
39
|
+
exit={{ opacity: 0, height: 0 }}
|
|
40
|
+
initial={{ opacity: 0, height: 0 }}
|
|
41
|
+
>
|
|
42
|
+
{props.isRoot ? null : (
|
|
43
|
+
<div className="line absolute left-2.5 w-px h-full bg-slate-6" />
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<div className="data-[root=true]:py-2 flex flex-col truncate">
|
|
47
|
+
<LayoutGroup id="sidebar">
|
|
48
|
+
{props.emailsDirectoryMetadata.subDirectories.map(
|
|
49
|
+
(subDirectory) => (
|
|
50
|
+
<SidebarDirectory
|
|
51
|
+
className="pl-4 py-0"
|
|
52
|
+
currentEmailOpenSlug={props.currentEmailOpenSlug}
|
|
53
|
+
emailsDirectoryMetadata={subDirectory}
|
|
54
|
+
key={subDirectory.absolutePath}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{props.emailsDirectoryMetadata.emailFilenames.map(
|
|
60
|
+
(emailFilename, index) => {
|
|
61
|
+
const emailSlug = `${directoryPathRelativeToEmailsDirectory}${
|
|
62
|
+
!isBaseEmailsDirectory ? pathSeparator : ''
|
|
63
|
+
}${emailFilename}`;
|
|
64
|
+
const removeExtensionFrom = (path: string) => {
|
|
65
|
+
if (
|
|
66
|
+
path.split('.').pop() === 'tsx' ||
|
|
67
|
+
path.split('.').pop() === 'jsx' ||
|
|
68
|
+
path.split('.').pop() === 'js'
|
|
69
|
+
) {
|
|
70
|
+
return path.split('.').slice(0, -1).join('.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return path;
|
|
74
|
+
};
|
|
75
|
+
const isCurrentPage = props.currentEmailOpenSlug
|
|
76
|
+
? removeExtensionFrom(props.currentEmailOpenSlug) ===
|
|
77
|
+
emailSlug
|
|
78
|
+
: false;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Link
|
|
82
|
+
href={{
|
|
83
|
+
pathname: `/preview/${emailSlug}`,
|
|
84
|
+
search: searchParams.toString(),
|
|
85
|
+
}}
|
|
86
|
+
key={emailSlug}
|
|
87
|
+
>
|
|
88
|
+
<motion.span
|
|
89
|
+
animate={{ x: 0, opacity: 1 }}
|
|
90
|
+
className={cn(
|
|
91
|
+
'text-[14px] flex items-center align-middle pl-3 h-8 max-w-full rounded-md text-slate-11 relative transition-colors',
|
|
92
|
+
{
|
|
93
|
+
'text-cyan-11': isCurrentPage,
|
|
94
|
+
'hover:text-slate-12':
|
|
95
|
+
props.currentEmailOpenSlug !== emailSlug,
|
|
96
|
+
},
|
|
97
|
+
)}
|
|
98
|
+
initial={{ x: -10 + -index * 1.5, opacity: 0 }}
|
|
99
|
+
transition={{
|
|
100
|
+
x: { delay: 0.03 * index, duration: 0.2 },
|
|
101
|
+
opacity: { delay: 0.03 * index, duration: 0.2 },
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{isCurrentPage ? (
|
|
105
|
+
<motion.span
|
|
106
|
+
animate={{ opacity: 1 }}
|
|
107
|
+
className="absolute left-0 right-0 top-0 bottom-0 rounded-md bg-cyan-5 opacity-0"
|
|
108
|
+
exit={{ opacity: 0 }}
|
|
109
|
+
initial={{ opacity: 0 }}
|
|
110
|
+
>
|
|
111
|
+
{!props.isRoot && (
|
|
112
|
+
<div className="bg-cyan-11 w-px absolute top-1 left-1.5 h-6" />
|
|
113
|
+
)}
|
|
114
|
+
</motion.span>
|
|
115
|
+
) : null}
|
|
116
|
+
<IconFile
|
|
117
|
+
className="absolute left-4 w-[24px] h-[24px]"
|
|
118
|
+
height="24"
|
|
119
|
+
width="24"
|
|
120
|
+
/>
|
|
121
|
+
<span className="truncate pl-8">{emailFilename}</span>
|
|
122
|
+
</motion.span>
|
|
123
|
+
</Link>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
)}
|
|
127
|
+
</LayoutGroup>
|
|
128
|
+
</div>
|
|
129
|
+
</motion.div>
|
|
130
|
+
</Collapsible.Content>
|
|
131
|
+
) : null}
|
|
132
|
+
</AnimatePresence>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
|
+
import {
|
|
6
|
+
emailsDirectoryAbsolutePath,
|
|
7
|
+
pathSeparator,
|
|
8
|
+
} from '../../utils/emails-directory-absolute-path';
|
|
9
|
+
import { type EmailsDirectory } from '../../actions/get-emails-directory-metadata';
|
|
10
|
+
import { Heading } from '../heading';
|
|
11
|
+
import { IconFolder } from '../icons/icon-folder';
|
|
12
|
+
import { IconFolderOpen } from '../icons/icon-folder-open';
|
|
13
|
+
import { IconArrowDown } from '../icons/icon-arrow-down';
|
|
14
|
+
import { SidebarDirectoryChildren } from './sidebar-directory-children';
|
|
15
|
+
|
|
16
|
+
interface SidebarDirectoryProps {
|
|
17
|
+
emailsDirectoryMetadata: EmailsDirectory;
|
|
18
|
+
className?: string;
|
|
19
|
+
currentEmailOpenSlug?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const persistedOpenDirectories = new Set<string>();
|
|
23
|
+
|
|
24
|
+
export const SidebarDirectory = ({
|
|
25
|
+
emailsDirectoryMetadata: directoryMetadata,
|
|
26
|
+
className,
|
|
27
|
+
currentEmailOpenSlug,
|
|
28
|
+
}: SidebarDirectoryProps) => {
|
|
29
|
+
const isBaseEmailsDirectory =
|
|
30
|
+
directoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
|
|
31
|
+
const directoryPathRelativeToBaseEmailsDirectory =
|
|
32
|
+
directoryMetadata.absolutePath
|
|
33
|
+
.replace(`${emailsDirectoryAbsolutePath}${pathSeparator}`, '')
|
|
34
|
+
.replace(emailsDirectoryAbsolutePath, '')
|
|
35
|
+
.trim();
|
|
36
|
+
const doesDirectoryContainCurrentEmailOpen = currentEmailOpenSlug
|
|
37
|
+
? currentEmailOpenSlug.includes(directoryPathRelativeToBaseEmailsDirectory)
|
|
38
|
+
: false;
|
|
39
|
+
|
|
40
|
+
const isEmpty =
|
|
41
|
+
directoryMetadata.emailFilenames.length > 0 ||
|
|
42
|
+
directoryMetadata.subDirectories.length > 0;
|
|
43
|
+
|
|
44
|
+
const [open, setOpen] = React.useState(
|
|
45
|
+
persistedOpenDirectories.has(directoryMetadata.absolutePath) ||
|
|
46
|
+
isBaseEmailsDirectory ||
|
|
47
|
+
doesDirectoryContainCurrentEmailOpen,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Collapsible.Root
|
|
52
|
+
className={cn('group', className)}
|
|
53
|
+
data-root={isBaseEmailsDirectory}
|
|
54
|
+
onOpenChange={(isOpening) => {
|
|
55
|
+
if (isOpening) {
|
|
56
|
+
persistedOpenDirectories.add(directoryMetadata.absolutePath);
|
|
57
|
+
} else {
|
|
58
|
+
persistedOpenDirectories.delete(directoryMetadata.absolutePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setOpen(isOpening);
|
|
62
|
+
}}
|
|
63
|
+
open={open}
|
|
64
|
+
>
|
|
65
|
+
<Collapsible.Trigger
|
|
66
|
+
className={cn(
|
|
67
|
+
'text-[14px] flex items-center font-medium gap-2 justify-between w-full my-1',
|
|
68
|
+
{
|
|
69
|
+
'cursor-pointer': !isEmpty,
|
|
70
|
+
},
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<div className="flex items-center text-slate-11 transition ease-in-out duration-200 hover:text-slate-12 gap-1">
|
|
74
|
+
{open ? (
|
|
75
|
+
<IconFolderOpen height="24" width="24" />
|
|
76
|
+
) : (
|
|
77
|
+
<IconFolder height="24" width="24" />
|
|
78
|
+
)}
|
|
79
|
+
<Heading
|
|
80
|
+
as="h3"
|
|
81
|
+
className="transition ease-in-out duration-200 hover:text-slate-12"
|
|
82
|
+
color="gray"
|
|
83
|
+
size="2"
|
|
84
|
+
weight="medium"
|
|
85
|
+
>
|
|
86
|
+
{directoryMetadata.directoryName}
|
|
87
|
+
</Heading>
|
|
88
|
+
</div>
|
|
89
|
+
{isEmpty ? (
|
|
90
|
+
<IconArrowDown
|
|
91
|
+
className="data-[open=true]:rotate-180 transition-transform opacity-60 justify-self-end"
|
|
92
|
+
data-open={open}
|
|
93
|
+
/>
|
|
94
|
+
) : null}
|
|
95
|
+
</Collapsible.Trigger>
|
|
96
|
+
|
|
97
|
+
{isEmpty ? (
|
|
98
|
+
<SidebarDirectoryChildren
|
|
99
|
+
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
100
|
+
emailsDirectoryMetadata={directoryMetadata}
|
|
101
|
+
open={open}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
</Collapsible.Root>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
5
|
+
import { useEmails } from '../../contexts/emails';
|
|
6
|
+
import { cn } from '../../utils';
|
|
7
|
+
import { Logo } from '../logo';
|
|
8
|
+
import { SidebarDirectoryChildren } from './sidebar-directory-children';
|
|
9
|
+
|
|
10
|
+
interface SidebarProps {
|
|
11
|
+
className?: string;
|
|
12
|
+
currentEmailOpenSlug?: string;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Sidebar = ({
|
|
17
|
+
className,
|
|
18
|
+
currentEmailOpenSlug,
|
|
19
|
+
style,
|
|
20
|
+
}: SidebarProps) => {
|
|
21
|
+
const { emailsDirectoryMetadata } = useEmails();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<aside
|
|
25
|
+
className={cn('border-r flex flex-col border-slate-6', className)}
|
|
26
|
+
style={{ ...style }}
|
|
27
|
+
>
|
|
28
|
+
<div className="p-4 h-[70px] flex-shrink items-center hidden lg:flex">
|
|
29
|
+
<Logo />
|
|
30
|
+
</div>
|
|
31
|
+
<nav className="p-4 flex-grow lg:pt-0 pl-0 w-screen h-[calc(100vh_-_70px)] lg:w-full lg:min-w-[275px] lg:max-w-[275px] flex flex-col overflow-y-auto">
|
|
32
|
+
<Collapsible.Root>
|
|
33
|
+
<React.Suspense>
|
|
34
|
+
<SidebarDirectoryChildren
|
|
35
|
+
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
36
|
+
emailsDirectoryMetadata={emailsDirectoryMetadata}
|
|
37
|
+
isRoot
|
|
38
|
+
open
|
|
39
|
+
/>
|
|
40
|
+
</React.Suspense>
|
|
41
|
+
</Collapsible.Root>
|
|
42
|
+
</nav>
|
|
43
|
+
</aside>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
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 TextSize = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
6
|
+
export type TextColor = 'gray' | 'white';
|
|
7
|
+
export type TextTransform = 'uppercase' | 'lowercase' | 'capitalize';
|
|
8
|
+
export type TextWeight = 'normal' | 'medium';
|
|
9
|
+
|
|
10
|
+
interface TextOwnProps {
|
|
11
|
+
size?: TextSize;
|
|
12
|
+
color?: TextColor;
|
|
13
|
+
transform?: TextTransform;
|
|
14
|
+
weight?: TextWeight;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type TextProps = As<'span', 'div', 'p'> & TextOwnProps;
|
|
18
|
+
|
|
19
|
+
export const Text = React.forwardRef<HTMLSpanElement, Readonly<TextProps>>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
as: Tag = 'span',
|
|
23
|
+
size = '2',
|
|
24
|
+
color = 'gray',
|
|
25
|
+
transform,
|
|
26
|
+
weight = 'normal',
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
forwardedRef,
|
|
32
|
+
) => (
|
|
33
|
+
<SlotPrimitive.Slot
|
|
34
|
+
className={cn(
|
|
35
|
+
className,
|
|
36
|
+
transform,
|
|
37
|
+
getSizesClassNames(size),
|
|
38
|
+
getColorClassNames(color),
|
|
39
|
+
getWeightClassNames(weight),
|
|
40
|
+
)}
|
|
41
|
+
ref={forwardedRef}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<Tag>{children}</Tag>
|
|
45
|
+
</SlotPrimitive.Slot>
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const getSizesClassNames = (size: TextSize | undefined) => {
|
|
50
|
+
switch (size) {
|
|
51
|
+
case '1':
|
|
52
|
+
return 'text-xs';
|
|
53
|
+
case undefined:
|
|
54
|
+
case '2':
|
|
55
|
+
return 'text-sm';
|
|
56
|
+
case '3':
|
|
57
|
+
return 'text-base';
|
|
58
|
+
case '4':
|
|
59
|
+
return 'text-lg';
|
|
60
|
+
case '5':
|
|
61
|
+
return ['text-17px', 'md:text-xl tracking-[-0.16px]'];
|
|
62
|
+
case '6':
|
|
63
|
+
return 'text-2xl tracking-[-0.288px]';
|
|
64
|
+
case '7':
|
|
65
|
+
return 'text-[28px] leading-[34px] tracking-[-0.416px]';
|
|
66
|
+
case '8':
|
|
67
|
+
return 'text-[35px] leading-[42px] tracking-[-0.64px]';
|
|
68
|
+
case '9':
|
|
69
|
+
return 'text-6xl leading-[73px] tracking-[-0.896px]';
|
|
70
|
+
default:
|
|
71
|
+
return unreachable(size);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getColorClassNames = (color: TextColor | undefined) => {
|
|
76
|
+
switch (color) {
|
|
77
|
+
case 'white':
|
|
78
|
+
return 'text-slate-12';
|
|
79
|
+
case undefined:
|
|
80
|
+
case 'gray':
|
|
81
|
+
return 'text-slate-11';
|
|
82
|
+
default:
|
|
83
|
+
return unreachable(color);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getWeightClassNames = (weight: TextWeight | undefined) => {
|
|
88
|
+
switch (weight) {
|
|
89
|
+
case undefined:
|
|
90
|
+
case 'normal':
|
|
91
|
+
return 'font-normal';
|
|
92
|
+
case 'medium':
|
|
93
|
+
return 'font-medium';
|
|
94
|
+
default:
|
|
95
|
+
return unreachable(weight);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
Text.displayName = 'Text';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '../utils';
|
|
4
|
+
import { inter } from '../app/inter';
|
|
5
|
+
|
|
6
|
+
type ContentElement = React.ElementRef<typeof TooltipPrimitive.Content>;
|
|
7
|
+
type ContentProps = React.ComponentPropsWithoutRef<
|
|
8
|
+
typeof TooltipPrimitive.Content
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export type TooltipProps = ContentProps;
|
|
12
|
+
|
|
13
|
+
export const TooltipContent = React.forwardRef<
|
|
14
|
+
ContentElement,
|
|
15
|
+
Readonly<TooltipProps>
|
|
16
|
+
>(({ sideOffset = 6, children, ...props }, forwardedRef) => (
|
|
17
|
+
<TooltipPrimitive.Portal>
|
|
18
|
+
<TooltipPrimitive.Content
|
|
19
|
+
{...props}
|
|
20
|
+
className={cn(
|
|
21
|
+
'bg-black border border-slate-6 z-20 px-3 py-2 rounded-md text-xs',
|
|
22
|
+
`${inter.variable} font-sans`,
|
|
23
|
+
)}
|
|
24
|
+
ref={forwardedRef}
|
|
25
|
+
sideOffset={sideOffset}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</TooltipPrimitive.Content>
|
|
29
|
+
</TooltipPrimitive.Portal>
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
TooltipContent.displayName = 'TooltipContent';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
2
|
+
import * 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
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../utils';
|
|
6
|
+
import { tabTransition } from '../utils/constants';
|
|
7
|
+
import { Heading } from './heading';
|
|
8
|
+
import { IconHideSidebar } from './icons/icon-hide-sidebar';
|
|
9
|
+
import { IconMonitor } from './icons/icon-monitor';
|
|
10
|
+
import { IconPhone } from './icons/icon-phone';
|
|
11
|
+
import { IconSource } from './icons/icon-source';
|
|
12
|
+
import { Send } from './send';
|
|
13
|
+
import { Tooltip } from './tooltip';
|
|
14
|
+
|
|
15
|
+
interface TopbarProps {
|
|
16
|
+
currentEmailOpenSlug: string;
|
|
17
|
+
activeView?: string;
|
|
18
|
+
markup?: string;
|
|
19
|
+
onToggleSidebar?: () => void;
|
|
20
|
+
setActiveView?: (view: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Topbar: React.FC<Readonly<TopbarProps>> = ({
|
|
24
|
+
currentEmailOpenSlug,
|
|
25
|
+
markup,
|
|
26
|
+
activeView,
|
|
27
|
+
setActiveView,
|
|
28
|
+
onToggleSidebar,
|
|
29
|
+
}) => {
|
|
30
|
+
return (
|
|
31
|
+
<Tooltip.Provider>
|
|
32
|
+
<header className="flex relative items-center px-4 justify-between h-[70px] border-b border-slate-6">
|
|
33
|
+
<Tooltip>
|
|
34
|
+
<Tooltip.Trigger asChild>
|
|
35
|
+
<button
|
|
36
|
+
className="hidden lg:flex rounded-lg px-2 py-2 transition ease-in-out duration-200 relative hover:bg-slate-5 text-slate-11 hover:text-slate-12"
|
|
37
|
+
onClick={() => {
|
|
38
|
+
if (onToggleSidebar) {
|
|
39
|
+
onToggleSidebar();
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
type="button"
|
|
43
|
+
>
|
|
44
|
+
<IconHideSidebar height={20} width={20} />
|
|
45
|
+
</button>
|
|
46
|
+
</Tooltip.Trigger>
|
|
47
|
+
<Tooltip.Content>Show/hide sidebar</Tooltip.Content>
|
|
48
|
+
</Tooltip>
|
|
49
|
+
|
|
50
|
+
<div className="items-center overflow-hidden hidden lg:flex text-center absolute left-1/2 transform -translate-x-1/2 top-1/2 -translate-y-1/2">
|
|
51
|
+
<Heading as="h2" className="truncate" size="2" weight="medium">
|
|
52
|
+
{currentEmailOpenSlug}
|
|
53
|
+
</Heading>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="flex gap-3 justify-between lg:justify-start w-full lg:w-fit">
|
|
57
|
+
<ToggleGroup.Root
|
|
58
|
+
aria-label="View mode"
|
|
59
|
+
className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
|
|
60
|
+
onValueChange={(value) => {
|
|
61
|
+
if (value) setActiveView?.(value);
|
|
62
|
+
}}
|
|
63
|
+
type="single"
|
|
64
|
+
value={activeView}
|
|
65
|
+
>
|
|
66
|
+
<ToggleGroup.Item value="desktop">
|
|
67
|
+
<Tooltip>
|
|
68
|
+
<Tooltip.Trigger asChild>
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
|
|
72
|
+
{
|
|
73
|
+
'text-slate-11': activeView !== 'desktop',
|
|
74
|
+
'text-slate-12': activeView === 'desktop',
|
|
75
|
+
},
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{activeView === 'desktop' && (
|
|
79
|
+
<motion.span
|
|
80
|
+
animate={{ opacity: 1 }}
|
|
81
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
82
|
+
exit={{ opacity: 0 }}
|
|
83
|
+
initial={{ opacity: 0 }}
|
|
84
|
+
layoutId="topbar-tabs"
|
|
85
|
+
transition={tabTransition}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
<IconMonitor />
|
|
89
|
+
</div>
|
|
90
|
+
</Tooltip.Trigger>
|
|
91
|
+
<Tooltip.Content>Desktop</Tooltip.Content>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
</ToggleGroup.Item>
|
|
94
|
+
<ToggleGroup.Item value="mobile">
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<Tooltip.Trigger asChild>
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
|
|
100
|
+
{
|
|
101
|
+
'text-slate-11': activeView !== 'mobile',
|
|
102
|
+
'text-slate-12': activeView === 'mobile',
|
|
103
|
+
},
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{activeView === 'mobile' && (
|
|
107
|
+
<motion.span
|
|
108
|
+
animate={{ opacity: 1 }}
|
|
109
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
110
|
+
exit={{ opacity: 0 }}
|
|
111
|
+
initial={{ opacity: 0 }}
|
|
112
|
+
layoutId="topbar-tabs"
|
|
113
|
+
transition={tabTransition}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
<IconPhone />
|
|
117
|
+
</div>
|
|
118
|
+
</Tooltip.Trigger>
|
|
119
|
+
<Tooltip.Content>Mobile</Tooltip.Content>
|
|
120
|
+
</Tooltip>
|
|
121
|
+
</ToggleGroup.Item>
|
|
122
|
+
<ToggleGroup.Item value="source">
|
|
123
|
+
<Tooltip>
|
|
124
|
+
<Tooltip.Trigger asChild>
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
|
|
128
|
+
{
|
|
129
|
+
'text-slate-11': activeView !== 'source',
|
|
130
|
+
'text-slate-12': activeView === 'source',
|
|
131
|
+
},
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{activeView === 'source' && (
|
|
135
|
+
<motion.span
|
|
136
|
+
animate={{ opacity: 1 }}
|
|
137
|
+
className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
|
|
138
|
+
exit={{ opacity: 0 }}
|
|
139
|
+
initial={{ opacity: 0 }}
|
|
140
|
+
layoutId="topbar-tabs"
|
|
141
|
+
transition={tabTransition}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
<IconSource />
|
|
145
|
+
</div>
|
|
146
|
+
</Tooltip.Trigger>
|
|
147
|
+
<Tooltip.Content>Code</Tooltip.Content>
|
|
148
|
+
</Tooltip>
|
|
149
|
+
</ToggleGroup.Item>
|
|
150
|
+
</ToggleGroup.Root>
|
|
151
|
+
|
|
152
|
+
{markup ? (
|
|
153
|
+
<div className="flex justify-end">
|
|
154
|
+
<Send markup={markup} />
|
|
155
|
+
</div>
|
|
156
|
+
) : null}
|
|
157
|
+
</div>
|
|
158
|
+
</header>
|
|
159
|
+
</Tooltip.Provider>
|
|
160
|
+
);
|
|
161
|
+
};
|