react-email 4.0.0-alpha.7 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +2 -48
  2. package/dist/cli/index.js +24 -17
  3. package/dist/cli/index.mjs +32 -25
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +10 -10
  6. package/dist/preview/.next/app-path-routes-manifest.json +1 -1
  7. package/dist/preview/.next/build-manifest.json +3 -3
  8. package/dist/preview/.next/cache/.rscinfo +1 -1
  9. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  13. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  14. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  15. package/dist/preview/.next/next-server.js.nft.json +1 -1
  16. package/dist/preview/.next/prerender-manifest.json +3 -3
  17. package/dist/preview/.next/required-server-files.json +3 -3
  18. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  19. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  21. package/dist/preview/.next/server/app/page.js +1 -1
  22. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  23. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js +115 -11
  25. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  26. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  27. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  28. package/dist/preview/.next/server/chunks/203.js +1 -0
  29. package/dist/preview/.next/server/chunks/600.js +3 -3
  30. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  31. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  32. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  33. package/dist/preview/.next/server/pages/500.html +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/683-017aee9270cb8999.js +1 -0
  37. package/dist/preview/.next/static/chunks/app/layout-311310b665ad8e17.js +1 -0
  38. package/dist/preview/.next/static/chunks/app/{page-9ea0bd45cd6294b0.js → page-9d038f3c5feb0570.js} +1 -1
  39. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-db4014629693f2b3.js +1 -0
  40. package/dist/preview/.next/static/chunks/{main-app-256b213b179a95cc.js → main-app-785b93ae096c4901.js} +1 -1
  41. package/dist/preview/.next/static/css/dda71861895dd2e4.css +3 -0
  42. package/dist/preview/.next/trace +26 -26
  43. package/dist/preview/.next/types/app/layout.ts +1 -1
  44. package/dist/preview/.next/types/app/page.ts +1 -1
  45. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  46. package/package.json +1 -1
  47. package/src/actions/email-validation/check-compatibility.ts +16 -4
  48. package/src/app/layout.tsx +1 -1
  49. package/src/app/page.tsx +4 -4
  50. package/src/app/preview/[...slug]/preview.tsx +35 -26
  51. package/src/components/code-container.tsx +2 -2
  52. package/src/components/code.tsx +16 -7
  53. package/src/components/resizable-wrapper.tsx +3 -3
  54. package/src/components/shell.tsx +13 -18
  55. package/src/components/sidebar/file-tree-directory-children.tsx +4 -2
  56. package/src/components/sidebar/file-tree-directory.tsx +18 -18
  57. package/src/components/sidebar/file-tree.tsx +2 -2
  58. package/src/components/sidebar/sidebar.tsx +16 -18
  59. package/src/components/toolbar/linter.tsx +35 -33
  60. package/src/components/toolbar/results.tsx +1 -1
  61. package/src/components/toolbar/spam-assassin.tsx +16 -13
  62. package/src/components/toolbar/use-cached-state.ts +2 -2
  63. package/src/components/toolbar.tsx +103 -30
  64. package/src/components/topbar.tsx +17 -5
  65. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  66. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  67. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  68. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  69. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  70. package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
  71. package/tailwind.config.ts +8 -0
  72. package/dist/preview/.next/server/chunks/943.js +0 -1
  73. package/dist/preview/.next/static/chunks/683-1fb40795502f6e63.js +0 -1
  74. package/dist/preview/.next/static/chunks/app/layout-ffdee5cc1be30e7b.js +0 -1
  75. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9e22979a25c836c0.js +0 -1
  76. package/dist/preview/.next/static/css/eaae8ce545b295f9.css +0 -3
  77. /package/dist/preview/.next/static/{Pms2orsQgT5xpttCfZfH5 → t4yeIF5ZYqVHaYIHgPxHn}/_buildManifest.js +0 -0
  78. /package/dist/preview/.next/static/{Pms2orsQgT5xpttCfZfH5 → t4yeIF5ZYqVHaYIHgPxHn}/_ssgManifest.js +0 -0
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/layout.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/release/packages/react-email/src/app/layout.tsx
2
2
  import * as entry from '../../../src/app/layout.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/page.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/release/packages/react-email/src/app/page.tsx
2
2
  import * as entry from '../../../src/app/page.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/preview/[...slug]/page.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/release/packages/react-email/src/app/preview/[...slug]/page.tsx
2
2
  import * as entry from '../../../../../src/app/preview/[...slug]/page.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.0.0-alpha.7",
3
+ "version": "4.0.0",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -154,7 +154,11 @@ export const checkCompatibility = async (
154
154
  );
155
155
  if (Object.keys(compatibilityStats.perEmailClient).length === 0)
156
156
  continue;
157
- if (compatibilityStats.status === 'success') continue;
157
+ if (
158
+ compatibilityStats.status === 'success' ||
159
+ compatibilityStats.status === 'warning'
160
+ )
161
+ continue;
158
162
 
159
163
  if (entry.category === 'html') {
160
164
  const entryElements = getElementNames(entry.title, entry.keywords);
@@ -274,8 +278,9 @@ export const checkCompatibility = async (
274
278
 
275
279
  if (cssEntryType === 'full property') {
276
280
  if (
277
- property.name === entryFullProperty?.name &&
278
- property.value === entryFullProperty.value
281
+ snakeToCamel(property.name) ===
282
+ snakeToCamel(entryFullProperty!.name) &&
283
+ property.value === entryFullProperty!.value
279
284
  ) {
280
285
  addToInsights(property);
281
286
  break;
@@ -302,7 +307,8 @@ export const checkCompatibility = async (
302
307
  }
303
308
  } else if (
304
309
  entryProperties.some(
305
- (propertyName) => property.name === propertyName,
310
+ (propertyName) =>
311
+ snakeToCamel(property.name) === snakeToCamel(propertyName),
306
312
  )
307
313
  ) {
308
314
  addToInsights(property);
@@ -318,4 +324,10 @@ export const checkCompatibility = async (
318
324
  return readableStream;
319
325
  };
320
326
 
327
+ const snakeToCamel = (snakeStr: string) => {
328
+ return snakeStr
329
+ .toLowerCase()
330
+ .replace(/-+([a-z])/g, (_match, letter) => letter.toUpperCase());
331
+ };
332
+
321
333
  export type AST = ReturnType<typeof parse>;
@@ -27,7 +27,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
27
27
  className={`${inter.variable} ${sfMono.variable} font-sans`}
28
28
  lang="en"
29
29
  >
30
- <body className="relative flex h-screen flex-col overflow-x-hidden bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
30
+ <body className="relative flex h-screen flex-col bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
31
31
  <EmailsProvider
32
32
  initialEmailsDirectoryMetadata={emailsDirectoryMetadata}
33
33
  >
package/src/app/page.tsx CHANGED
@@ -3,7 +3,7 @@ import Image from 'next/image';
3
3
  import Link from 'next/link';
4
4
  import { Button, Heading, Text } from '../components';
5
5
  import CodeSnippet from '../components/code-snippet';
6
- import { Shell, ShellContent } from '../components/shell';
6
+ import { Shell } from '../components/shell';
7
7
  import { emailsDirectoryAbsolutePath } from './env';
8
8
  import logo from './logo.png';
9
9
 
@@ -12,8 +12,8 @@ const Home = () => {
12
12
 
13
13
  return (
14
14
  <Shell>
15
- <ShellContent className="mx-auto flex max-w-lg items-center justify-center p-8">
16
- <div className="-mt-10 relative flex flex-col items-center gap-3 text-center">
15
+ <div className="w-full h-full flex items-center justify-center p-8">
16
+ <div className="-mt-10 relative max-w-lg flex flex-col items-center gap-3 text-center">
17
17
  <Image
18
18
  alt="React Email Icon"
19
19
  className="mb-8"
@@ -38,7 +38,7 @@ const Home = () => {
38
38
  <Link href="https://react.email/docs">Check the docs</Link>
39
39
  </Button>
40
40
  </div>
41
- </ShellContent>
41
+ </div>
42
42
  </Shell>
43
43
  );
44
44
  };
@@ -1,30 +1,31 @@
1
1
  'use client';
2
2
 
3
3
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
- import { use, useRef } from 'react';
4
+ import { use, useState } from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { Toaster } from 'sonner';
7
7
  import { useDebouncedCallback } from 'use-debounce';
8
8
  import { Topbar } from '../../../components';
9
9
  import { CodeContainer } from '../../../components/code-container';
10
10
  import {
11
- ResizableWarpper,
11
+ ResizableWrapper,
12
12
  makeIframeDocumentBubbleEvents,
13
13
  } from '../../../components/resizable-wrapper';
14
14
  import { Send } from '../../../components/send';
15
- import { ShellContent } from '../../../components/shell';
15
+ import { useToolbarState } from '../../../components/toolbar';
16
16
  import { Tooltip } from '../../../components/tooltip';
17
17
  import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
18
18
  import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
19
19
  import { PreviewContext } from '../../../contexts/preview';
20
20
  import { useClampedState } from '../../../hooks/use-clamped-state';
21
+ import { cn } from '../../../utils';
21
22
  import { RenderingError } from './rendering-error';
22
23
 
23
- interface PreviewProps {
24
+ interface PreviewProps extends React.ComponentProps<'div'> {
24
25
  emailTitle: string;
25
26
  }
26
27
 
27
- const Preview = ({ emailTitle }: PreviewProps) => {
28
+ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
28
29
  const { renderingResult, renderedEmailMetadata } = use(PreviewContext)!;
29
30
 
30
31
  const router = useRouter();
@@ -53,21 +54,21 @@ const Preview = ({ emailTitle }: PreviewProps) => {
53
54
  const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
54
55
  const hasErrors = 'error' in renderingResult;
55
56
 
56
- const maxWidthRef = useRef(Number.POSITIVE_INFINITY);
57
- const maxHeightRef = useRef(Number.POSITIVE_INFINITY);
58
- const minWidth = 350;
59
- const minHeight = 600;
57
+ const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
58
+ const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
59
+ const minWidth = 100;
60
+ const minHeight = 100;
60
61
  const storedWidth = searchParams.get('width');
61
62
  const storedHeight = searchParams.get('height');
62
63
  const [width, setWidth] = useClampedState(
63
64
  storedWidth ? Number.parseInt(storedWidth) : 600,
64
- 350,
65
- maxWidthRef.current,
65
+ minWidth,
66
+ maxWidth,
66
67
  );
67
68
  const [height, setHeight] = useClampedState(
68
69
  storedHeight ? Number.parseInt(storedHeight) : 1024,
69
- 600,
70
- maxHeightRef.current,
70
+ minHeight,
71
+ maxHeight,
71
72
  );
72
73
 
73
74
  const handleSaveViewSize = useDebouncedCallback(() => {
@@ -77,6 +78,8 @@ const Preview = ({ emailTitle }: PreviewProps) => {
77
78
  router.push(`${pathname}?${params.toString()}${location.hash}`);
78
79
  }, 300);
79
80
 
81
+ const { toggled: toolbarToggled } = useToolbarState();
82
+
80
83
  return (
81
84
  <>
82
85
  <Topbar emailTitle={emailTitle}>
@@ -107,14 +110,20 @@ const Preview = ({ emailTitle }: PreviewProps) => {
107
110
  ) : null}
108
111
  </Topbar>
109
112
 
110
- <ShellContent
111
- className="relative flex bg-gray-200"
113
+ <div
114
+ {...props}
115
+ className={cn(
116
+ 'h-[calc(100%-3.5rem-2.375rem)] will-change-height flex p-4 transition-all duration-300',
117
+ activeView === 'preview' && 'bg-gray-200',
118
+ toolbarToggled && 'h-[calc(100%-3.5rem-13rem)]',
119
+ className,
120
+ )}
112
121
  ref={(element) => {
113
122
  const observer = new ResizeObserver((entry) => {
114
123
  const [elementEntry] = entry;
115
124
  if (elementEntry) {
116
- maxWidthRef.current = elementEntry.contentRect.width - 80;
117
- maxHeightRef.current = elementEntry.contentRect.height - 80;
125
+ setMaxWidth(elementEntry.contentRect.width);
126
+ setMaxHeight(elementEntry.contentRect.height);
118
127
  }
119
128
  });
120
129
 
@@ -132,11 +141,11 @@ const Preview = ({ emailTitle }: PreviewProps) => {
132
141
  {hasRenderingMetadata ? (
133
142
  <>
134
143
  {activeView === 'preview' && (
135
- <ResizableWarpper
144
+ <ResizableWrapper
136
145
  minHeight={minHeight}
137
146
  minWidth={minWidth}
138
- maxHeight={maxHeightRef.current}
139
- maxWidth={maxWidthRef.current}
147
+ maxHeight={maxHeight}
148
+ maxWidth={maxWidth}
140
149
  height={height}
141
150
  onResizeEnd={() => {
142
151
  handleSaveViewSize();
@@ -145,9 +154,9 @@ const Preview = ({ emailTitle }: PreviewProps) => {
145
154
  const isHorizontal =
146
155
  direction === 'east' || direction === 'west';
147
156
  if (isHorizontal) {
148
- setWidth(value);
157
+ setWidth(Math.round(value));
149
158
  } else {
150
- setHeight(value);
159
+ setHeight(Math.round(value));
151
160
  }
152
161
  }}
153
162
  width={width}
@@ -166,12 +175,12 @@ const Preview = ({ emailTitle }: PreviewProps) => {
166
175
  }}
167
176
  title={emailTitle}
168
177
  />
169
- </ResizableWarpper>
178
+ </ResizableWrapper>
170
179
  )}
171
180
 
172
181
  {activeView === 'source' && (
173
- <div className="h-full w-full bg-black">
174
- <div className="m-auto flex max-w-3xl p-6">
182
+ <div className="h-full w-full">
183
+ <div className="m-auto h-full flex max-w-3xl p-6">
175
184
  <Tooltip.Provider>
176
185
  <CodeContainer
177
186
  activeLang={activeLang}
@@ -199,7 +208,7 @@ const Preview = ({ emailTitle }: PreviewProps) => {
199
208
  ) : null}
200
209
 
201
210
  <Toaster />
202
- </ShellContent>
211
+ </div>
203
212
  </>
204
213
  );
205
214
  };
@@ -39,7 +39,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
39
39
 
40
40
  return (
41
41
  <div
42
- className="relative w-full items-center whitespace-pre rounded-md border border-slate-6 text-sm"
42
+ className="relative max-h-[650px] w-full h-full whitespace-pre rounded-md border border-slate-6 text-sm"
43
43
  style={{
44
44
  lineHeight: '130%',
45
45
  background:
@@ -84,7 +84,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
84
84
  filename={`email.${activeMarkup.language}`}
85
85
  />
86
86
  </div>
87
- <div>
87
+ <div className="h-[calc(100%-2.25rem)]">
88
88
  <Code language={activeLang}>{activeMarkup.content}</Code>
89
89
  </div>
90
90
  </div>
@@ -3,7 +3,7 @@ import Link from 'next/link';
3
3
  import { useSearchParams } from 'next/navigation';
4
4
  import type { Language } from 'prism-react-renderer';
5
5
  import { Highlight } from 'prism-react-renderer';
6
- import { Fragment, useEffect } from 'react';
6
+ import { Fragment, useEffect, useRef } from 'react';
7
7
  import { useFragmentIdentifier } from '../hooks/use-fragment-identifier';
8
8
  import { cn } from '../utils';
9
9
 
@@ -73,12 +73,18 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
73
73
  return highlight[0] <= line && highlight[1] >= line;
74
74
  };
75
75
 
76
+ const scrollerRef = useRef<HTMLDivElement>(null);
77
+
76
78
  useEffect(() => {
77
- if (highlight) {
78
- document.getElementById(`L${highlight[0]}`)?.scrollIntoView({
79
- block: 'start',
80
- behavior: 'smooth',
81
- });
79
+ const scroller = scrollerRef.current;
80
+ if (highlight && scroller) {
81
+ const lineElement = scroller.querySelector(`#L${highlight[0]}`);
82
+ if (lineElement instanceof HTMLAnchorElement) {
83
+ scroller.scrollTo({
84
+ top: Math.max(lineElement.offsetTop - 325, 0),
85
+ behavior: 'smooth',
86
+ });
87
+ }
82
88
  }
83
89
  }, [highlight]);
84
90
 
@@ -97,7 +103,10 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
97
103
  '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%)',
98
104
  }}
99
105
  />
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">
106
+ <div
107
+ ref={scrollerRef}
108
+ className="flex max-h-[650px] h-full p-4 after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto"
109
+ >
101
110
  <div className="text-[#49494f] text-[13px] font-light font-[MonoLisa,_Menlo,_monospace]">
102
111
  {tokens.map((_, i) => (
103
112
  <Link
@@ -4,7 +4,7 @@ import { cn } from '../utils';
4
4
 
5
5
  type Direction = 'north' | 'south' | 'east' | 'west';
6
6
 
7
- type ResizableWarpperProps = {
7
+ type ResizableWrapperProps = {
8
8
  width: number;
9
9
  height: number;
10
10
 
@@ -41,7 +41,7 @@ export const makeIframeDocumentBubbleEvents = (iframe: HTMLIFrameElement) => {
41
41
  };
42
42
  };
43
43
 
44
- export const ResizableWarpper = ({
44
+ export const ResizableWrapper = ({
45
45
  width,
46
46
  height,
47
47
  onResize,
@@ -54,7 +54,7 @@ export const ResizableWarpper = ({
54
54
  minWidth,
55
55
 
56
56
  ...rest
57
- }: ResizableWarpperProps) => {
57
+ }: ResizableWrapperProps) => {
58
58
  const resizableRef = useRef<HTMLElement>(null);
59
59
 
60
60
  const mouseMoveListener = useRef<(event: MouseEvent) => void>(null);
@@ -62,36 +62,31 @@ export const Shell = ({ children, currentEmailOpenSlug }: ShellProps) => {
62
62
  </svg>
63
63
  </button>
64
64
  </div>
65
- <div className="flex w-[100dvw] h-[100dvh] flex-row">
65
+ <div className="w-[100dvw] flex h-[calc(100dvh-4.375rem)] lg:h-[100dvh]">
66
66
  <React.Suspense>
67
67
  <Sidebar
68
- className={cn('shrink [transition:width_0.2s_ease-in-out]', {
69
- '-translate-x-full lg:translate-x-0': sidebarToggled,
70
- 'lg:w-0': !sidebarToggled,
71
- })}
68
+ className={cn(
69
+ 'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-full max-w-full will-change-auto [transition:width_0.2s_ease-in-out]',
70
+ 'lg:static lg:inline-block lg:z-auto lg:max-h-full lg:w-[16rem]',
71
+ {
72
+ '-translate-x-full lg:translate-x-0': sidebarToggled,
73
+ 'lg:w-0': !sidebarToggled,
74
+ },
75
+ )}
72
76
  currentEmailOpenSlug={currentEmailOpenSlug}
73
77
  />
74
78
  </React.Suspense>
75
79
  <main
76
80
  className={cn(
77
- 'h-full max-h-full min-h-full overflow-hidden will-change-width lg:mt-0',
78
- 'grow',
81
+ 'inline-block relative overflow-hidden will-change-width',
82
+ 'w-full h-full',
79
83
  '[transition:width_0.2s_ease-in-out,_transform_0.2s_ease-in-out]',
84
+ sidebarToggled && 'lg:w-[calc(100%-16rem)]',
80
85
  )}
81
86
  >
82
- <div className="relative flex h-full w-full flex-col">{children}</div>
87
+ {children}
83
88
  </main>
84
89
  </div>
85
90
  </ShellContext.Provider>
86
91
  );
87
92
  };
88
-
89
- type ShellContentRootProps = React.ComponentProps<'div'>;
90
-
91
- export const ShellContent = ({ children, ...rest }: ShellContentRootProps) => {
92
- return (
93
- <div {...rest} className={cn('relative grow', rest.className)}>
94
- {children}
95
- </div>
96
- );
97
- };
@@ -77,7 +77,7 @@ export const FileTreeDirectoryChildren = (props: {
77
77
  <motion.span
78
78
  animate={{ x: 0, opacity: 1 }}
79
79
  className={cn(
80
- '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)]',
80
+ '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)]',
81
81
  props.isRoot ? undefined : 'pl-3',
82
82
  {
83
83
  'text-cyan-11': isCurrentPage,
@@ -116,7 +116,9 @@ export const FileTreeDirectoryChildren = (props: {
116
116
  height="20"
117
117
  width="20"
118
118
  />
119
- <span className="truncate">{emailFilename}</span>
119
+ <span className="truncate w-[calc(100%-1.25rem)]">
120
+ {emailFilename}
121
+ </span>
120
122
  </motion.span>
121
123
  </Link>
122
124
  );
@@ -51,31 +51,31 @@ export const FileTreeDirectory = ({
51
51
  >
52
52
  <Collapsible.Trigger
53
53
  className={cn(
54
- 'mt-1 mb-1.5 flex w-full items-center justify-between gap-2 font-medium text-[14px]',
54
+ 'mt-1 mb-1.5 flex w-full items-center text-start justify-between gap-2 font-medium text-[14px]',
55
55
  {
56
56
  'cursor-pointer': !isEmpty,
57
57
  },
58
58
  )}
59
59
  >
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>
60
+ {open ? (
61
+ <IconFolderOpen className="w-[20px]" height="20" width="20" />
62
+ ) : (
63
+ <IconFolder height="20" width="20" />
64
+ )}
65
+ <Heading
66
+ as="h3"
67
+ className="transition grow w-[calc(100%-40px)] truncate duration-200 ease-in-out hover:text-slate-12"
68
+ color="gray"
69
+ size="2"
70
+ weight="medium"
71
+ >
72
+ {directoryMetadata.directoryName}
73
+ </Heading>
76
74
  {!isEmpty ? (
77
75
  <IconArrowDown
78
- className="justify-self-end opacity-60 transition-transform data-[open=true]:rotate-180"
76
+ width="20"
77
+ height="20"
78
+ className="ml-auto opacity-60 transition-transform data-[open=true]:rotate-180"
79
79
  data-open={open}
80
80
  />
81
81
  ) : 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
@@ -16,28 +16,26 @@ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
16
16
  return (
17
17
  <aside
18
18
  className={cn(
19
- '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
+ 'overflow-hidden bg-black',
20
20
  'lg:static lg:z-auto lg:max-h-screen lg:w-[16rem]',
21
21
  className,
22
22
  )}
23
23
  >
24
- <div className="w-full h-full overflow-y-auto overflow-x-hidden">
25
- <div className="flex w-full h-full flex-col border-slate-6 border-r">
26
- <div
27
- className={clsx(
28
- 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
29
- )}
30
- >
31
- <h2>
32
- <Logo />
33
- </h2>
34
- </div>
35
- <div className="relative h-full w-full border-slate-4 border-t px-4 pb-3">
36
- <FileTree
37
- currentEmailOpenSlug={currentEmailOpenSlug}
38
- emailsDirectoryMetadata={emailsDirectoryMetadata}
39
- />
40
- </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
+ />
41
39
  </div>
42
40
  </div>
43
41
  </aside>
@@ -92,6 +92,22 @@ export const Linter = ({ rows }: LinterProps) => {
92
92
  const failingCheck = row.result.checks.find(
93
93
  (check) => check.passed === false,
94
94
  )!;
95
+ const metadata: React.ReactNode[] = [];
96
+ for (const check of row.result.checks) {
97
+ if (
98
+ check.type === 'fetch_attempt' &&
99
+ check.metadata.fetchStatusCode
100
+ ) {
101
+ metadata.push(<>HTTP {check.metadata.fetchStatusCode}</>);
102
+ }
103
+ }
104
+ metadata.push(
105
+ <CodePreviewLineLink
106
+ line={row.result.codeLocation.line}
107
+ column={row.result.codeLocation.column}
108
+ type="html"
109
+ />,
110
+ );
95
111
  return (
96
112
  <Result status={row.result.status} key={i}>
97
113
  <Result.Name>{sanitize(failingCheck.type)}</Result.Name>
@@ -102,20 +118,14 @@ export const Linter = ({ rows }: LinterProps) => {
102
118
  {failingCheck.type === 'fetch_attempt' &&
103
119
  failingCheck.metadata.fetchStatusCode &&
104
120
  failingCheck.metadata.fetchStatusCode >= 300 &&
105
- failingCheck.metadata.fetchStatusCode < 400 ? (
106
- <>
107
- <strong>{failingCheck.metadata.fetchStatusCode}</strong>:
108
- There was a redirect, the content may have been moved
109
- </>
110
- ) : null}
121
+ failingCheck.metadata.fetchStatusCode < 400
122
+ ? 'There was a redirect, the content may have been moved'
123
+ : null}
111
124
  {failingCheck.type === 'fetch_attempt' &&
112
125
  failingCheck.metadata.fetchStatusCode &&
113
- failingCheck.metadata.fetchStatusCode >= 400 ? (
114
- <>
115
- <strong>{failingCheck.metadata.fetchStatusCode}</strong>:
116
- The link is broken
117
- </>
118
- ) : null}
126
+ failingCheck.metadata.fetchStatusCode >= 400
127
+ ? 'The link is broken'
128
+ : null}
119
129
  {failingCheck.type === 'syntax'
120
130
  ? 'The link is broken due to invalid syntax'
121
131
  : null}
@@ -124,15 +134,7 @@ export const Linter = ({ rows }: LinterProps) => {
124
134
  {row.result.link}
125
135
  </span>
126
136
  </Result.Description>
127
- <Result.Metadata>
128
- {[
129
- <CodePreviewLineLink
130
- line={row.result.codeLocation.line}
131
- column={row.result.codeLocation.column}
132
- type="html"
133
- />,
134
- ]}
135
- </Result.Metadata>
137
+ <Result.Metadata>{metadata}</Result.Metadata>
136
138
  </Result>
137
139
  );
138
140
  }
@@ -146,6 +148,12 @@ export const Linter = ({ rows }: LinterProps) => {
146
148
  if (check.type === 'image_size' && check.metadata.byteCount) {
147
149
  metadata.push(prettyBytes(check.metadata.byteCount));
148
150
  }
151
+ if (
152
+ check.type === 'fetch_attempt' &&
153
+ check.metadata.fetchStatusCode
154
+ ) {
155
+ metadata.push(<>HTTP {check.metadata.fetchStatusCode}</>);
156
+ }
149
157
  }
150
158
  metadata.push(
151
159
  <CodePreviewLineLink
@@ -164,20 +172,14 @@ export const Linter = ({ rows }: LinterProps) => {
164
172
  {failingCheck.type === 'fetch_attempt' &&
165
173
  failingCheck.metadata.fetchStatusCode &&
166
174
  failingCheck.metadata.fetchStatusCode >= 300 &&
167
- failingCheck.metadata.fetchStatusCode < 400 ? (
168
- <>
169
- <strong>{failingCheck.metadata.fetchStatusCode}</strong>:
170
- There was a redirect, the image may have been moved
171
- </>
172
- ) : null}
175
+ failingCheck.metadata.fetchStatusCode < 400
176
+ ? 'There was a redirect, the image may have been moved'
177
+ : null}
173
178
  {failingCheck.type === 'fetch_attempt' &&
174
179
  failingCheck.metadata.fetchStatusCode &&
175
- failingCheck.metadata.fetchStatusCode >= 400 ? (
176
- <>
177
- <strong>{failingCheck.metadata.fetchStatusCode}</strong>:
178
- The image is broken
179
- </>
180
- ) : null}
180
+ failingCheck.metadata.fetchStatusCode >= 400
181
+ ? 'The image is broken'
182
+ : null}
181
183
  {failingCheck.type === 'syntax'
182
184
  ? 'The image is broken due to an invalid source'
183
185
  : null}