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.
Files changed (110) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli/index.js +18 -13
  3. package/dist/cli/index.mjs +26 -21
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +14 -13
  6. package/dist/preview/.next/build-manifest.json +3 -3
  7. package/dist/preview/.next/cache/.rscinfo +1 -1
  8. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  14. package/dist/preview/.next/next-server.js.nft.json +1 -1
  15. package/dist/preview/.next/prerender-manifest.json +3 -3
  16. package/dist/preview/.next/required-server-files.json +3 -3
  17. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  20. package/dist/preview/.next/server/app/page.js +1 -1
  21. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  22. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  23. package/dist/preview/.next/server/app/preview/[...slug]/page.js +133 -25
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  25. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  26. package/dist/preview/.next/server/chunks/42.js +1 -0
  27. package/dist/preview/.next/server/chunks/600.js +3 -3
  28. package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
  29. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  30. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  31. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  32. package/dist/preview/.next/server/pages/500.html +1 -1
  33. package/dist/preview/.next/server/pages-manifest.json +1 -1
  34. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  35. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  36. package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
  37. package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
  38. package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
  39. package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
  40. package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
  41. package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
  42. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
  43. package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
  44. package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
  45. package/dist/preview/.next/trace +26 -26
  46. package/dist/preview/.next/types/app/layout.ts +1 -1
  47. package/dist/preview/.next/types/app/page.ts +84 -0
  48. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  49. package/package.json +1 -1
  50. package/src/actions/email-validation/check-compatibility.ts +16 -5
  51. package/src/actions/email-validation/check-images.spec.tsx +13 -11
  52. package/src/actions/email-validation/check-images.ts +6 -0
  53. package/src/actions/email-validation/check-links.spec.tsx +23 -11
  54. package/src/actions/email-validation/check-links.ts +6 -0
  55. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  56. package/src/actions/render-email-by-path.tsx +2 -2
  57. package/src/app/env.ts +3 -0
  58. package/src/app/preview/[...slug]/page.tsx +24 -11
  59. package/src/app/preview/[...slug]/preview.tsx +15 -12
  60. package/src/components/code-container.tsx +90 -71
  61. package/src/components/code.tsx +106 -42
  62. package/src/components/icons/icon-info.tsx +18 -0
  63. package/src/components/icons/icon-reload.tsx +13 -14
  64. package/src/components/logo.tsx +3 -2
  65. package/src/components/resizable-wrapper.tsx +1 -4
  66. package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
  67. package/src/components/sidebar/file-tree-directory.tsx +26 -18
  68. package/src/components/sidebar/file-tree.tsx +2 -2
  69. package/src/components/sidebar/sidebar.tsx +16 -19
  70. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  71. package/src/components/toolbar/compatibility.tsx +113 -0
  72. package/src/components/toolbar/linter.tsx +69 -111
  73. package/src/components/toolbar/results.tsx +5 -2
  74. package/src/components/toolbar/spam-assassin.tsx +31 -20
  75. package/src/components/toolbar/toolbar-button.tsx +4 -2
  76. package/src/components/toolbar/use-cached-state.ts +2 -2
  77. package/src/components/toolbar.tsx +152 -30
  78. package/src/components/tooltip-content.tsx +1 -1
  79. package/src/components/topbar/view-size-controls.tsx +1 -2
  80. package/src/components/topbar.tsx +1 -20
  81. package/src/contexts/fragment-identifier.tsx +46 -0
  82. package/src/hooks/use-fragment-identifier.ts +14 -0
  83. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  84. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  85. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  86. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  87. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  88. package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
  89. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
  90. package/src/utils/get-email-component.ts +1 -1
  91. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  92. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/linting.ts +5 -30
  95. package/src/utils/load-stream.ts +15 -0
  96. package/src/utils/sanitize.ts +6 -0
  97. package/dist/preview/.next/server/chunks/833.js +0 -1
  98. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
  99. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
  100. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
  101. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
  102. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
  103. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
  104. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
  105. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
  106. package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
  107. package/src/components/icons/icon-scanner.tsx +0 -19
  108. package/src/components/icons/icon-scissors.tsx +0 -19
  109. /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
  110. /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 [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 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 backdrop-blur-md"
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
- <Tooltip>
115
- <Tooltip.Trigger
116
- asChild
117
- className="absolute right-2 top-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 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
+ };
@@ -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 * as React from 'react';
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
- <pre className="h-[650px] overflow-auto p-4">
64
- {tokens.map((line, i) => {
65
- const lineProps = getLineProps({
66
- line,
67
- key: i,
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
- {line.map((token, key) => {
79
- const tokenProps = getTokenProps({
80
- token,
81
- });
82
- const isException =
83
- token.content === 'from' &&
84
- line[key + 1]?.content === ':';
85
- const newTypes = isException
86
- ? [...token.types, 'key-white']
87
- : token.types;
88
- token.types = newTypes;
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
- return (
91
- <React.Fragment key={key}>
92
- <span {...tokenProps} />
93
- </React.Fragment>
94
- );
95
- })}
96
- </div>
97
- );
98
- })}
99
- </pre>
100
- <div
101
- className="absolute bottom-0 left-0 h-px w-[200px]"
102
- style={{
103
- background:
104
- '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%)',
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
- export const IconReload = (props: React.ComponentProps<'svg'>) => {
2
- return (
3
- <svg
4
- width="12"
5
- height="12"
6
- viewBox="0 0 12 12"
7
- fill="none"
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="M10.52 6C10.52 3.73168 8.75221 1.48 6.00006 1.48C3.77741 1.48 2.67886 3.1251 2.21074 3.99999H3.60005C3.82096 3.99999 4.00005 4.17908 4.00005 4.39999C4.00005 4.6209 3.82096 4.79999 3.60005 4.79999H1.20005C0.979137 4.79999 0.800049 4.6209 0.800049 4.39999V1.99999C0.800049 1.77908 0.979137 1.59999 1.20005 1.59999C1.42096 1.59999 1.60005 1.77908 1.60005 1.99999V3.45056C2.16367 2.45702 3.4673 0.679993 6.00006 0.679993C9.25029 0.679993 11.32 3.34831 11.32 6C11.32 8.65169 9.25029 11.32 6.00006 11.32C4.44499 11.32 3.15027 10.7047 2.22843 9.76673C1.73486 9.26449 1.34939 8.67121 1.08658 8.03257C1.0025 7.8283 1.09995 7.59453 1.30424 7.51046C1.50853 7.42638 1.7423 7.52384 1.82637 7.72812C2.05104 8.27401 2.38001 8.77961 2.79901 9.20593C3.57646 9.99705 4.66802 10.52 6.00006 10.52C8.75221 10.52 10.52 8.26833 10.52 6Z"
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
- </svg>
18
- );
19
- };
14
+ </IconBase>
15
+ ),
16
+ );
17
+
18
+ IconReload.displayName = 'IconReload';
@@ -1,10 +1,11 @@
1
1
  export const Logo = () => (
2
2
  <svg
3
3
  fill="none"
4
- height="32"
4
+ height="30"
5
5
  viewBox="0 0 119 32"
6
- width="119"
6
+ width="113"
7
7
  xmlns="http://www.w3.org/2000/svg"
8
+ style={{ opacity: 0.9 }}
8
9
  >
9
10
  <g clipPath="url(#clip0_27_291)">
10
11
  <path
@@ -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 max-w-full items-center gap-2 rounded-md align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
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
- <span className="truncate">{emailFilename}</span>
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
- <div className="flex items-center gap-2 text-slate-11 transition duration-200 ease-in-out hover:text-slate-12">
61
- {open ? (
62
- <IconFolderOpen height="20" width="20" />
63
- ) : (
64
- <IconFolder height="20" width="20" />
65
- )}
66
- <Heading
67
- as="h3"
68
- className="transition duration-200 ease-in-out hover:text-slate-12"
69
- color="gray"
70
- size="2"
71
- weight="medium"
72
- >
73
- {directoryMetadata.directoryName}
74
- </Heading>
75
- </div>
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
- className="justify-self-end opacity-60 transition-transform data-[open=true]:rotate-180"
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 h-full w-full flex-col overflow-hidden lg:w-full lg:min-w-[14.5rem]">
17
- <nav className="flex w-full flex-grow flex-col overflow-y-auto p-4 pr-0 pl-0">
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-ful overflow-hidden bg-black will-change-auto',
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-y-auto overflow-x-hidden">
26
- <div className="flex w-full h-full flex-col border-slate-6 border-r">
27
- <div
28
- className={clsx(
29
- 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
30
- )}
31
- >
32
- <Heading as="h2" className="truncate" size="2" weight="medium">
33
- <Logo />
34
- </Heading>
35
- </div>
36
- <div className="relative h-full w-full border-slate-4 border-t px-4 pb-3">
37
- <FileTree
38
- currentEmailOpenSlug={currentEmailOpenSlug}
39
- emailsDirectoryMetadata={emailsDirectoryMetadata}
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
+ };