react-email 4.0.0-alpha.6 → 4.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/cli/index.js +18 -13
- package/dist/cli/index.mjs +26 -21
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +14 -13
- package/dist/preview/.next/build-manifest.json +3 -3
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +3 -3
- package/dist/preview/.next/required-server-files.json +3 -3
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +133 -25
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/chunks/42.js +1 -0
- package/dist/preview/.next/server/chunks/600.js +3 -3
- package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages-manifest.json +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
- package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
- package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
- package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
- package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
- package/dist/preview/.next/trace +26 -26
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/page.ts +84 -0
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +1 -1
- package/src/actions/email-validation/check-compatibility.ts +16 -5
- package/src/actions/email-validation/check-images.spec.tsx +13 -11
- package/src/actions/email-validation/check-images.ts +6 -0
- package/src/actions/email-validation/check-links.spec.tsx +23 -11
- package/src/actions/email-validation/check-links.ts +6 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/render-email-by-path.tsx +2 -2
- package/src/app/env.ts +3 -0
- package/src/app/preview/[...slug]/page.tsx +24 -11
- package/src/app/preview/[...slug]/preview.tsx +15 -12
- package/src/components/code-container.tsx +90 -71
- package/src/components/code.tsx +106 -42
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-reload.tsx +13 -14
- package/src/components/logo.tsx +3 -2
- package/src/components/resizable-wrapper.tsx +1 -4
- package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
- package/src/components/sidebar/file-tree-directory.tsx +26 -18
- package/src/components/sidebar/file-tree.tsx +2 -2
- package/src/components/sidebar/sidebar.tsx +16 -19
- 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 +69 -111
- package/src/components/toolbar/results.tsx +5 -2
- package/src/components/toolbar/spam-assassin.tsx +31 -20
- package/src/components/toolbar/toolbar-button.tsx +4 -2
- package/src/components/toolbar/use-cached-state.ts +2 -2
- package/src/components/toolbar.tsx +152 -30
- package/src/components/tooltip-content.tsx +1 -1
- package/src/components/topbar/view-size-controls.tsx +1 -2
- package/src/components/topbar.tsx +1 -20
- package/src/contexts/fragment-identifier.tsx +46 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- 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-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
- package/src/utils/get-email-component.ts +1 -1
- 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/index.ts +1 -0
- package/src/utils/linting.ts +5 -30
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/sanitize.ts +6 -0
- package/dist/preview/.next/server/chunks/833.js +0 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
- package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
- package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
- package/src/components/icons/icon-scanner.tsx +0 -19
- package/src/components/icons/icon-scissors.tsx +0 -19
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
|
@@ -27,52 +27,19 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
|
|
|
27
27
|
activeLang,
|
|
28
28
|
setActiveLang,
|
|
29
29
|
}) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<a
|
|
40
|
-
className="text-slate-11 transition duration-200 ease-in-out 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]);
|
|
30
|
+
const activeMarkup = markups.find(({ language }) => activeLang === language);
|
|
31
|
+
if (!activeMarkup) {
|
|
32
|
+
throw new Error('No markup found for the active language!', {
|
|
33
|
+
cause: {
|
|
34
|
+
activeLang,
|
|
35
|
+
markups,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
72
39
|
|
|
73
40
|
return (
|
|
74
41
|
<div
|
|
75
|
-
className="relative w-full items-center whitespace-pre rounded-md border border-slate-6 text-sm
|
|
42
|
+
className="relative w-full items-center whitespace-pre rounded-md border border-slate-6 text-sm"
|
|
76
43
|
style={{
|
|
77
44
|
lineHeight: '130%',
|
|
78
45
|
background:
|
|
@@ -111,35 +78,87 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
|
|
|
111
78
|
})}
|
|
112
79
|
</LayoutGroup>
|
|
113
80
|
</div>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</Tooltip>
|
|
123
|
-
<Tooltip>
|
|
124
|
-
<Tooltip.Trigger
|
|
125
|
-
asChild
|
|
126
|
-
className="text-gray-11 absolute right-8 top-2 hidden md:block"
|
|
127
|
-
>
|
|
128
|
-
{renderDownloadIcon()}
|
|
129
|
-
</Tooltip.Trigger>
|
|
130
|
-
<Tooltip.Content>Download</Tooltip.Content>
|
|
131
|
-
</Tooltip>
|
|
81
|
+
<CopyToClipboardButton content={activeMarkup.content} />
|
|
82
|
+
<DownloadButton
|
|
83
|
+
content={activeMarkup.content}
|
|
84
|
+
filename={`email.${activeMarkup.language}`}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<Code language={activeLang}>{activeMarkup.content}</Code>
|
|
132
89
|
</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
90
|
</div>
|
|
144
91
|
);
|
|
145
92
|
};
|
|
93
|
+
|
|
94
|
+
interface CopyToClipboardButtonProps {
|
|
95
|
+
content: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const CopyToClipboardButton = ({ content }: CopyToClipboardButtonProps) => {
|
|
99
|
+
const [isCopied, setIsCopied] = React.useState(false);
|
|
100
|
+
|
|
101
|
+
const unsetIsCopiedTimeout = React.useRef<NodeJS.Timeout>(undefined);
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
setIsCopied(false);
|
|
104
|
+
clearTimeout(unsetIsCopiedTimeout.current);
|
|
105
|
+
unsetIsCopiedTimeout.current = undefined;
|
|
106
|
+
}, [content]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Tooltip>
|
|
110
|
+
<Tooltip.Trigger
|
|
111
|
+
asChild
|
|
112
|
+
className="absolute right-2 top-2 hidden md:block"
|
|
113
|
+
>
|
|
114
|
+
<IconButton
|
|
115
|
+
onClick={async () => {
|
|
116
|
+
setIsCopied(true);
|
|
117
|
+
await copyTextToClipboard(content);
|
|
118
|
+
unsetIsCopiedTimeout.current = setTimeout(() => {
|
|
119
|
+
setIsCopied(false);
|
|
120
|
+
}, 3000);
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{isCopied ? <IconCheck /> : <IconClipboard />}
|
|
124
|
+
</IconButton>
|
|
125
|
+
</Tooltip.Trigger>
|
|
126
|
+
<Tooltip.Content>Copy to Clipboard</Tooltip.Content>
|
|
127
|
+
</Tooltip>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
interface DownloadButtonProps {
|
|
132
|
+
content: string;
|
|
133
|
+
filename: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const DownloadButton = ({ content, filename }: DownloadButtonProps) => {
|
|
137
|
+
const generatedUrl = React.useMemo(() => {
|
|
138
|
+
const file = new File([content], filename);
|
|
139
|
+
return URL.createObjectURL(file);
|
|
140
|
+
}, [content, filename]);
|
|
141
|
+
const url = React.useSyncExternalStore(
|
|
142
|
+
() => () => {},
|
|
143
|
+
() => generatedUrl,
|
|
144
|
+
() => undefined,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Tooltip>
|
|
149
|
+
<Tooltip.Trigger
|
|
150
|
+
asChild
|
|
151
|
+
className="text-gray-11 absolute right-8 top-2 hidden md:block"
|
|
152
|
+
>
|
|
153
|
+
<a
|
|
154
|
+
className="text-slate-11 transition duration-200 ease-in-out hover:text-slate-12"
|
|
155
|
+
download={filename}
|
|
156
|
+
href={url}
|
|
157
|
+
>
|
|
158
|
+
<IconDownload />
|
|
159
|
+
</a>
|
|
160
|
+
</Tooltip.Trigger>
|
|
161
|
+
<Tooltip.Content>Download</Tooltip.Content>
|
|
162
|
+
</Tooltip>
|
|
163
|
+
);
|
|
164
|
+
};
|
package/src/components/code.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { useSearchParams } from 'next/navigation';
|
|
1
4
|
import type { Language } from 'prism-react-renderer';
|
|
2
5
|
import { Highlight } from 'prism-react-renderer';
|
|
3
|
-
import
|
|
6
|
+
import { Fragment, useEffect } from 'react';
|
|
7
|
+
import { useFragmentIdentifier } from '../hooks/use-fragment-identifier';
|
|
4
8
|
import { cn } from '../utils';
|
|
5
9
|
|
|
6
10
|
interface CodeProps {
|
|
@@ -43,10 +47,43 @@ const theme = {
|
|
|
43
47
|
],
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
const lineHashRegex = /#L(?<start>\d+)(?:,(?<end>\d+))?/;
|
|
51
|
+
|
|
46
52
|
export const Code: React.FC<Readonly<CodeProps>> = ({
|
|
47
53
|
children,
|
|
48
54
|
language = 'html',
|
|
49
55
|
}) => {
|
|
56
|
+
const locationHash = useFragmentIdentifier();
|
|
57
|
+
const highlight = (() => {
|
|
58
|
+
if (locationHash) {
|
|
59
|
+
const match = locationHash.match(lineHashRegex);
|
|
60
|
+
if (match?.groups?.start) {
|
|
61
|
+
const start = Number.parseInt(match.groups.start);
|
|
62
|
+
const end = match.groups.end
|
|
63
|
+
? Number.parseInt(match.groups.end)
|
|
64
|
+
: start;
|
|
65
|
+
return [start, end] as const;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
const isHighlighting = (line: number) => {
|
|
71
|
+
if (!highlight) return false;
|
|
72
|
+
|
|
73
|
+
return highlight[0] <= line && highlight[1] >= line;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (highlight) {
|
|
78
|
+
document.getElementById(`L${highlight[0]}`)?.scrollIntoView({
|
|
79
|
+
block: 'start',
|
|
80
|
+
behavior: 'smooth',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}, [highlight]);
|
|
84
|
+
|
|
85
|
+
const searchParams = useSearchParams();
|
|
86
|
+
|
|
50
87
|
const value = children.trim();
|
|
51
88
|
|
|
52
89
|
return (
|
|
@@ -60,50 +97,77 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
|
|
|
60
97
|
'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
98
|
}}
|
|
62
99
|
/>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
return (
|
|
70
|
-
<div
|
|
71
|
-
{...lineProps}
|
|
72
|
-
className={cn('whitespace-pre', {
|
|
73
|
-
"before:mr-2 before:text-slate-11 before:content-['$']":
|
|
74
|
-
language === 'bash' && tokens.length === 1,
|
|
75
|
-
})}
|
|
100
|
+
<div className="flex h-[650px] p-4 max-h-[calc(100vh-10rem)] after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto">
|
|
101
|
+
<div className="text-[#49494f] text-[13px] font-light font-[MonoLisa,_Menlo,_monospace]">
|
|
102
|
+
{tokens.map((_, i) => (
|
|
103
|
+
<Link
|
|
104
|
+
id={`L${i + 1}`}
|
|
76
105
|
key={i}
|
|
106
|
+
href={{
|
|
107
|
+
hash: `#L${i + 1}`,
|
|
108
|
+
search: searchParams.toString(),
|
|
109
|
+
}}
|
|
110
|
+
scroll={false}
|
|
111
|
+
className={cn(
|
|
112
|
+
'align-middle block scroll-mt-[325px] rounded-l-sm select-none pr-3 cursor-pointer hover:text-slate-12',
|
|
113
|
+
isHighlighting(i + 1) &&
|
|
114
|
+
'text-cyan-11 hover:text-cyan-11 bg-cyan-5',
|
|
115
|
+
)}
|
|
116
|
+
type="button"
|
|
77
117
|
>
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
{i + 1}
|
|
119
|
+
</Link>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
<pre>
|
|
123
|
+
{tokens.map((line, i) => {
|
|
124
|
+
const lineProps = getLineProps({
|
|
125
|
+
line,
|
|
126
|
+
key: i,
|
|
127
|
+
});
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
{...lineProps}
|
|
131
|
+
className={cn(
|
|
132
|
+
'whitespace-pre flex transition-colors rounded-r-sm',
|
|
133
|
+
isHighlighting(i + 1) && 'bg-cyan-5',
|
|
134
|
+
{
|
|
135
|
+
"before:mr-2 before:text-slate-11 before:content-['$']":
|
|
136
|
+
language === 'bash' && tokens.length === 1,
|
|
137
|
+
},
|
|
138
|
+
)}
|
|
139
|
+
key={i}
|
|
140
|
+
>
|
|
141
|
+
{line.map((token, key) => {
|
|
142
|
+
const tokenProps = getTokenProps({
|
|
143
|
+
token,
|
|
144
|
+
});
|
|
145
|
+
const isException =
|
|
146
|
+
token.content === 'from' &&
|
|
147
|
+
line[key + 1]?.content === ':';
|
|
148
|
+
const newTypes = isException
|
|
149
|
+
? [...token.types, 'key-white']
|
|
150
|
+
: token.types;
|
|
151
|
+
token.types = newTypes;
|
|
89
152
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
return (
|
|
154
|
+
<Fragment key={key}>
|
|
155
|
+
<span {...tokenProps} />
|
|
156
|
+
</Fragment>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</pre>
|
|
163
|
+
<div
|
|
164
|
+
className="absolute bottom-0 left-0 h-px w-[200px]"
|
|
165
|
+
style={{
|
|
166
|
+
background:
|
|
167
|
+
'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%)',
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
107
171
|
</>
|
|
108
172
|
)}
|
|
109
173
|
</Highlight>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconInfo = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
6
|
+
({ ...props }, forwardedRef) => (
|
|
7
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
8
|
+
<path
|
|
9
|
+
d="M12 4C7.58173 4 4 7.58172 4 12C4 16.4182 7.58173 20 12 20C16.4183 20 20 16.4182 20 12C20 7.58172 16.4183 4 12 4ZM5.14754 12C5.14754 8.21549 8.21551 5.14754 12 5.14754C15.7845 5.14754 18.8525 8.21549 18.8525 12C18.8525 15.7844 15.7845 18.8525 12 18.8525C8.21551 18.8525 5.14754 15.7844 5.14754 12ZM12.906 8.37648C12.906 8.87682 12.5004 9.28243 12 9.28243C11.4997 9.28243 11.0941 8.87682 11.0941 8.37648C11.0941 7.87613 11.4997 7.47053 12 7.47053C12.5004 7.47053 12.906 7.87613 12.906 8.37648ZM10.1883 10.1884H10.7922H12.0002C12.3337 10.1884 12.6041 10.4588 12.6041 10.7924V15.0201H13.2081H13.8121V16.2281H13.2081H12.0002H10.7922H10.1883V15.0201H10.7922H11.3962V11.3963H10.7922H10.1883V10.1884Z"
|
|
10
|
+
fill="currentColor"
|
|
11
|
+
fillRule="evenodd"
|
|
12
|
+
clipRule="evenodd"
|
|
13
|
+
/>
|
|
14
|
+
</IconBase>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
IconInfo.displayName = 'IconInfo';
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
-
{...props}
|
|
10
|
-
>
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconReload = React.forwardRef<IconElement, Readonly<IconProps>>(
|
|
6
|
+
({ ...props }, forwardedRef) => (
|
|
7
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
11
8
|
<path
|
|
12
9
|
fillRule="evenodd"
|
|
13
10
|
clipRule="evenodd"
|
|
14
|
-
d="
|
|
11
|
+
d="M17.9354 12C17.9354 9.01537 15.5828 6.05264 11.9202 6.05264C8.96229 6.05264 7.50033 8.21724 6.87735 9.36841H8.72625C9.02024 9.36841 9.25858 9.60406 9.25858 9.89473C9.25858 10.1854 9.02024 10.421 8.72625 10.421H5.53232C5.23833 10.421 5 10.1854 5 9.89473V6.73684C5 6.44617 5.23833 6.21052 5.53232 6.21052C5.82631 6.21052 6.06465 6.44617 6.06465 6.73684V8.64548C6.81471 7.33819 8.54959 5 11.9202 5C16.2456 5 19 8.51094 19 12C19 15.4891 16.2456 19 11.9202 19C9.8507 19 8.12769 18.1904 6.9009 16.9562C6.24405 16.2954 5.73107 15.5148 5.38132 14.6744C5.26942 14.4057 5.39911 14.0981 5.67098 13.9875C5.94285 13.8768 6.25395 14.0051 6.36583 14.2738C6.66482 14.9921 7.10262 15.6574 7.66023 16.2183C8.69486 17.2593 10.1475 17.9474 11.9202 17.9474C15.5828 17.9474 17.9354 14.9846 17.9354 12Z"
|
|
15
12
|
fill="currentColor"
|
|
16
13
|
/>
|
|
17
|
-
</
|
|
18
|
-
)
|
|
19
|
-
|
|
14
|
+
</IconBase>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
IconReload.displayName = 'IconReload';
|
package/src/components/logo.tsx
CHANGED
|
@@ -100,10 +100,7 @@ export const ResizableWarpper = ({
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
102
102
|
{...rest}
|
|
103
|
-
className={cn(
|
|
104
|
-
'relative mx-auto my-auto box-content px-4 py-2',
|
|
105
|
-
rest.className,
|
|
106
|
-
)}
|
|
103
|
+
className={cn('relative mx-auto my-auto box-content', rest.className)}
|
|
107
104
|
>
|
|
108
105
|
<div
|
|
109
106
|
aria-label="resize-west"
|
|
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
|
|
|
5
5
|
import { cn } from '../../utils';
|
|
6
6
|
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
7
7
|
import { IconFile } from '../icons/icon-file';
|
|
8
|
+
import { Tooltip } from '../tooltip';
|
|
8
9
|
import { FileTreeDirectory } from './file-tree-directory';
|
|
9
10
|
|
|
10
11
|
export const FileTreeDirectoryChildren = (props: {
|
|
@@ -71,12 +72,13 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
71
72
|
pathname: `/preview/${emailSlug}`,
|
|
72
73
|
search: searchParams.toString(),
|
|
73
74
|
}}
|
|
75
|
+
prefetch
|
|
74
76
|
key={emailSlug}
|
|
75
77
|
>
|
|
76
78
|
<motion.span
|
|
77
79
|
animate={{ x: 0, opacity: 1 }}
|
|
78
80
|
className={cn(
|
|
79
|
-
'relative flex h-8
|
|
81
|
+
'relative flex h-8 w-full items-center text-start gap-2 rounded-md align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
|
|
80
82
|
props.isRoot ? undefined : 'pl-3',
|
|
81
83
|
{
|
|
82
84
|
'text-cyan-11': isCurrentPage,
|
|
@@ -115,7 +117,16 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
115
117
|
height="20"
|
|
116
118
|
width="20"
|
|
117
119
|
/>
|
|
118
|
-
<
|
|
120
|
+
<Tooltip.Provider>
|
|
121
|
+
<Tooltip>
|
|
122
|
+
<Tooltip.Trigger asChild>
|
|
123
|
+
<span className="truncate w-[calc(100%-1.25rem)]">
|
|
124
|
+
{emailFilename}
|
|
125
|
+
</span>
|
|
126
|
+
</Tooltip.Trigger>
|
|
127
|
+
<Tooltip.Content>{emailFilename}</Tooltip.Content>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
</Tooltip.Provider>
|
|
119
130
|
</motion.span>
|
|
120
131
|
</Link>
|
|
121
132
|
);
|
|
@@ -7,6 +7,7 @@ import { Heading } from '../heading';
|
|
|
7
7
|
import { IconArrowDown } from '../icons/icon-arrow-down';
|
|
8
8
|
import { IconFolder } from '../icons/icon-folder';
|
|
9
9
|
import { IconFolderOpen } from '../icons/icon-folder-open';
|
|
10
|
+
import { Tooltip } from '../tooltip';
|
|
10
11
|
import { FileTreeDirectoryChildren } from './file-tree-directory-children';
|
|
11
12
|
|
|
12
13
|
interface SidebarDirectoryProps {
|
|
@@ -51,31 +52,38 @@ export const FileTreeDirectory = ({
|
|
|
51
52
|
>
|
|
52
53
|
<Collapsible.Trigger
|
|
53
54
|
className={cn(
|
|
54
|
-
'mt-1 mb-1.5 flex w-full items-center justify-between gap-2 font-medium text-[14px]',
|
|
55
|
+
'mt-1 mb-1.5 flex w-full items-center text-start justify-between gap-2 font-medium text-[14px]',
|
|
55
56
|
{
|
|
56
57
|
'cursor-pointer': !isEmpty,
|
|
57
58
|
},
|
|
58
59
|
)}
|
|
59
60
|
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
{open ? (
|
|
62
|
+
<IconFolderOpen className="w-[20px]" height="20" width="20" />
|
|
63
|
+
) : (
|
|
64
|
+
<IconFolder height="20" width="20" />
|
|
65
|
+
)}
|
|
66
|
+
<Tooltip.Provider>
|
|
67
|
+
<Tooltip>
|
|
68
|
+
<Tooltip.Trigger asChild>
|
|
69
|
+
<Heading
|
|
70
|
+
as="h3"
|
|
71
|
+
className="transition grow w-[calc(100%-40px)] truncate duration-200 ease-in-out hover:text-slate-12"
|
|
72
|
+
color="gray"
|
|
73
|
+
size="2"
|
|
74
|
+
weight="medium"
|
|
75
|
+
>
|
|
76
|
+
{directoryMetadata.directoryName}
|
|
77
|
+
</Heading>
|
|
78
|
+
</Tooltip.Trigger>
|
|
79
|
+
<Tooltip.Content>{directoryMetadata.directoryName}</Tooltip.Content>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
</Tooltip.Provider>
|
|
76
82
|
{!isEmpty ? (
|
|
77
83
|
<IconArrowDown
|
|
78
|
-
|
|
84
|
+
width="20"
|
|
85
|
+
height="20"
|
|
86
|
+
className="ml-auto opacity-60 transition-transform data-[open=true]:rotate-180"
|
|
79
87
|
data-open={open}
|
|
80
88
|
/>
|
|
81
89
|
) : null}
|
|
@@ -13,8 +13,8 @@ export const FileTree = ({
|
|
|
13
13
|
emailsDirectoryMetadata,
|
|
14
14
|
}: FileTreeProps) => {
|
|
15
15
|
return (
|
|
16
|
-
<div className="flex
|
|
17
|
-
<nav className="flex
|
|
16
|
+
<div className="flex w-full h-full flex-col lg:w-full lg:min-w-[14.5rem]">
|
|
17
|
+
<nav className="flex flex-grow flex-col p-4 pr-0 pl-0">
|
|
18
18
|
<Collapsible.Root open>
|
|
19
19
|
<React.Suspense>
|
|
20
20
|
<FileTreeDirectoryChildren
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
3
|
import { useEmails } from '../../contexts/emails';
|
|
4
4
|
import { cn } from '../../utils';
|
|
5
|
-
import { Heading } from '../heading';
|
|
6
5
|
import { Logo } from '../logo';
|
|
7
6
|
import { FileTree } from './file-tree';
|
|
8
7
|
|
|
@@ -17,28 +16,26 @@ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
|
|
|
17
16
|
return (
|
|
18
17
|
<aside
|
|
19
18
|
className={cn(
|
|
20
|
-
'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-screen max-w-
|
|
19
|
+
'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-screen max-w-full bg-black will-change-auto',
|
|
21
20
|
'lg:static lg:z-auto lg:max-h-screen lg:w-[16rem]',
|
|
22
21
|
className,
|
|
23
22
|
)}
|
|
24
23
|
>
|
|
25
|
-
<div className="w-full h-full overflow-
|
|
26
|
-
<div
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/>
|
|
41
|
-
</div>
|
|
24
|
+
<div className="flex w-full h-full overflow-hidden flex-col border-slate-6 border-r">
|
|
25
|
+
<div
|
|
26
|
+
className={clsx(
|
|
27
|
+
'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<h2>
|
|
31
|
+
<Logo />
|
|
32
|
+
</h2>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="relative grow w-full h-full overflow-y-auto overflow-x-hidden border-slate-4 border-t px-4 pb-3">
|
|
35
|
+
<FileTree
|
|
36
|
+
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
37
|
+
emailsDirectoryMetadata={emailsDirectoryMetadata}
|
|
38
|
+
/>
|
|
42
39
|
</div>
|
|
43
40
|
</div>
|
|
44
41
|
</aside>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { useSearchParams } from 'next/navigation';
|
|
3
|
+
|
|
4
|
+
interface CodePreviewLineLinkProps {
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
|
|
8
|
+
type: 'react' | 'html';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CodePreviewLineLink = ({
|
|
12
|
+
line,
|
|
13
|
+
column,
|
|
14
|
+
type,
|
|
15
|
+
}: CodePreviewLineLinkProps) => {
|
|
16
|
+
const searchParams = useSearchParams();
|
|
17
|
+
|
|
18
|
+
const newSearchParams = new URLSearchParams(searchParams);
|
|
19
|
+
newSearchParams.set('view', 'source');
|
|
20
|
+
if (type === 'html') {
|
|
21
|
+
newSearchParams.set('lang', 'markup');
|
|
22
|
+
} else if (type === 'react') {
|
|
23
|
+
newSearchParams.set('lang', 'jsx');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fragmentIdentifier = `#L${line}`;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Link
|
|
30
|
+
href={{
|
|
31
|
+
search: newSearchParams.toString(),
|
|
32
|
+
hash: fragmentIdentifier,
|
|
33
|
+
}}
|
|
34
|
+
scroll={false}
|
|
35
|
+
className="appearance-none underline mx-2"
|
|
36
|
+
>
|
|
37
|
+
L{line.toString().padStart(2, '0')}
|
|
38
|
+
</Link>
|
|
39
|
+
);
|
|
40
|
+
};
|