react-email 4.0.0-alpha.0 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/cli/index.js +8 -10
  3. package/dist/cli/index.mjs +8 -13
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +18 -18
  6. package/dist/preview/.next/build-manifest.json +6 -6
  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 +1 -1
  16. package/dist/preview/.next/required-server-files.json +1 -1
  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 +6 -6
  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/196.js +1 -1
  27. package/dist/preview/.next/server/chunks/282.js +15 -0
  28. package/dist/preview/.next/server/chunks/667.js +1 -0
  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/server-reference-manifest.js +1 -1
  34. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  35. package/dist/preview/.next/static/chunks/207-7ab46c2d84f60fed.js +1 -0
  36. package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +1 -0
  37. package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
  38. package/dist/preview/.next/static/chunks/app/layout-f1bad3fcfbc7eb6b.js +1 -0
  39. package/dist/preview/.next/static/chunks/app/page-800163ba6c6d943d.js +1 -0
  40. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +1 -0
  41. package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
  42. package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
  43. package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +3 -0
  44. package/dist/preview/.next/trace +22 -21
  45. package/dist/preview/.next/types/app/layout.ts +1 -1
  46. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  47. package/module-punycode.d.ts +3 -0
  48. package/package.json +7 -9
  49. package/src/actions/email-validation/check-images.spec.tsx +90 -0
  50. package/src/actions/email-validation/check-images.ts +142 -0
  51. package/src/actions/email-validation/check-links.spec.tsx +92 -0
  52. package/src/actions/email-validation/check-links.ts +18 -15
  53. package/src/components/button.tsx +47 -36
  54. package/src/components/icons/icon-image.tsx +19 -0
  55. package/src/components/sidebar/checking-results.tsx +150 -0
  56. package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
  57. package/src/components/sidebar/image-checker.tsx +161 -0
  58. package/src/components/sidebar/link-checker.tsx +83 -223
  59. package/src/components/sidebar/sidebar.tsx +74 -26
  60. package/src/hooks/use-icon-animation.ts +4 -7
  61. package/src/utils/static-node-modules-for-vm.ts +2 -1
  62. package/dist/preview/.next/server/chunks/273.js +0 -1
  63. package/dist/preview/.next/server/chunks/594.js +0 -10
  64. package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
  65. package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
  66. package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
  67. package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
  68. package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
  69. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
  70. package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
  71. package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
  72. package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
  73. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → Mn2FuRztLqr32yO8CKHi9}/_buildManifest.js +0 -0
  74. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → Mn2FuRztLqr32yO8CKHi9}/_ssgManifest.js +0 -0
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email.git/tertiary/packages/react-email/src/app/layout.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/chore-1/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.git/tertiary/packages/react-email/src/app/preview/[...slug]/page.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/chore-1/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
 
@@ -0,0 +1,3 @@
1
+ declare module 'module-punycode' {
2
+ export * from 'node:punycode';
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.0.0-alpha.0",
3
+ "version": "4.0.0-alpha.1",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -25,18 +25,14 @@
25
25
  "chokidar": "4.0.3",
26
26
  "commander": "11.1.0",
27
27
  "debounce": "2.0.0",
28
- "esbuild": "0.19.11",
28
+ "esbuild": "0.23.0",
29
29
  "glob": "10.3.4",
30
30
  "log-symbols": "4.1.0",
31
31
  "mime-types": "2.1.35",
32
32
  "next": "15.1.2",
33
33
  "normalize-path": "3.0.0",
34
34
  "ora": "5.4.1",
35
- "socket.io": "4.8.0"
36
- },
37
- "overrides": {
38
- "react": "^19",
39
- "react-dom": "^19"
35
+ "socket.io": "4.8.1"
40
36
  },
41
37
  "devDependencies": {
42
38
  "@radix-ui/colors": "1.0.1",
@@ -60,11 +56,13 @@
60
56
  "autoprefixer": "10.4.20",
61
57
  "clsx": "2.1.0",
62
58
  "framer-motion": "12.0.0-alpha.2",
63
- "lottie-react": "^2.4.0",
59
+ "@lottiefiles/dotlottie-react": "0.12.3",
64
60
  "node-html-parser": "6.1.13",
65
61
  "postcss": "8.4.40",
66
62
  "prettier-plugin-tailwindcss": "0.6.6",
63
+ "pretty-bytes": "6.1.1",
67
64
  "prism-react-renderer": "2.1.0",
65
+ "module-punycode": "npm:punycode@2.3.1",
68
66
  "react": "^19",
69
67
  "react-dom": "^19",
70
68
  "sharp": "0.33.3",
@@ -78,7 +76,7 @@
78
76
  "tsx": "4.9.0",
79
77
  "typescript": "5.1.6",
80
78
  "vitest": "1.1.3",
81
- "@react-email/render": "1.0.4"
79
+ "@react-email/render": "1.0.5"
82
80
  },
83
81
  "scripts": {
84
82
  "build": "tsup-node && node build-preview-server.mjs",
@@ -0,0 +1,90 @@
1
+ import { render } from '@react-email/render';
2
+ import * as React from 'react';
3
+ import { type ImageCheckingResult, checkImages } from './check-images';
4
+
5
+ test('checkImages()', async () => {
6
+ expect(
7
+ await checkImages(
8
+ await render(
9
+ <div>
10
+ {/* biome-ignore lint/a11y/useAltText: This is intentional to test the checking for accessibility */}
11
+ <img src="https://resend.com/static/brand/resend-icon-white.png" />,
12
+ <img src="/static/codepen-challengers.png" alt="codepen challenges" />
13
+ ,
14
+ </div>,
15
+ ),
16
+ 'https://demo.react.email',
17
+ ),
18
+ ).toEqual([
19
+ {
20
+ source: 'https://resend.com/static/brand/resend-icon-white.png',
21
+ checks: [
22
+ {
23
+ passed: false,
24
+ type: 'accessibility',
25
+ metadata: {
26
+ alt: undefined,
27
+ },
28
+ },
29
+ {
30
+ passed: true,
31
+ type: 'syntax',
32
+ },
33
+ {
34
+ passed: true,
35
+ type: 'security',
36
+ },
37
+ {
38
+ passed: true,
39
+ type: 'fetch_attempt',
40
+ metadata: {
41
+ fetchStatusCode: 200,
42
+ },
43
+ },
44
+ {
45
+ passed: true,
46
+ type: 'image_size',
47
+ metadata: {
48
+ byteCount: 23_138,
49
+ },
50
+ },
51
+ ],
52
+ status: 'warning',
53
+ },
54
+ {
55
+ checks: [
56
+ {
57
+ metadata: {
58
+ alt: 'codepen challenges',
59
+ },
60
+ passed: true,
61
+ type: 'accessibility',
62
+ },
63
+ {
64
+ passed: true,
65
+ type: 'syntax',
66
+ },
67
+ {
68
+ passed: true,
69
+ type: 'security',
70
+ },
71
+ {
72
+ metadata: {
73
+ fetchStatusCode: 200,
74
+ },
75
+ passed: true,
76
+ type: 'fetch_attempt',
77
+ },
78
+ {
79
+ metadata: {
80
+ byteCount: 111_922,
81
+ },
82
+ passed: true,
83
+ type: 'image_size',
84
+ },
85
+ ],
86
+ source: '/static/codepen-challengers.png',
87
+ status: 'success',
88
+ },
89
+ ] satisfies ImageCheckingResult[]);
90
+ });
@@ -0,0 +1,142 @@
1
+ 'use server';
2
+
3
+ import type { IncomingMessage } from 'node:http';
4
+ import { headers } from 'next/headers';
5
+ import { parse } from 'node-html-parser';
6
+ import { quickFetch } from './quick-fetch';
7
+
8
+ type Check = { passed: boolean } & (
9
+ | {
10
+ type: 'accessibility';
11
+ metadata: {
12
+ alt: string | undefined;
13
+ };
14
+ }
15
+ | {
16
+ type: 'fetch_attempt';
17
+ metadata: {
18
+ fetchStatusCode: number | undefined;
19
+ };
20
+ }
21
+ | {
22
+ type: 'image_size';
23
+ metadata: {
24
+ byteCount: number | undefined;
25
+ };
26
+ }
27
+ | {
28
+ type: 'syntax';
29
+ }
30
+ | {
31
+ type: 'security';
32
+ }
33
+ );
34
+
35
+ export interface ImageCheckingResult {
36
+ status: 'success' | 'warning' | 'error';
37
+ source: string;
38
+ checks: Check[];
39
+ }
40
+
41
+ const getResponseSizeInBytes = async (res: IncomingMessage) => {
42
+ let totalBytes = 0;
43
+ for await (const chunk of res) {
44
+ totalBytes += chunk.byteLength;
45
+ }
46
+ return totalBytes;
47
+ };
48
+
49
+ export const checkImages = async (code: string, base: string) => {
50
+ const ast = parse(code);
51
+
52
+ const imageCheckingResults: ImageCheckingResult[] = [];
53
+
54
+ const images = ast.querySelectorAll('img');
55
+ for await (const image of images) {
56
+ const rawSource = image.attributes.src;
57
+ if (!rawSource) continue;
58
+ if (imageCheckingResults.some((result) => result.source === rawSource))
59
+ continue;
60
+
61
+ const source = rawSource?.startsWith('/')
62
+ ? `${base}${rawSource}`
63
+ : rawSource;
64
+
65
+ const result: ImageCheckingResult = {
66
+ source: rawSource,
67
+ status: 'success',
68
+ checks: [],
69
+ };
70
+
71
+ const alt = image.attributes.alt;
72
+ result.checks.push({
73
+ passed: alt !== undefined,
74
+ type: 'accessibility',
75
+ metadata: {
76
+ alt,
77
+ },
78
+ });
79
+ if (alt === undefined) {
80
+ result.status = 'warning';
81
+ }
82
+
83
+ try {
84
+ const url = new URL(source);
85
+ result.checks.push({
86
+ passed: true,
87
+ type: 'syntax',
88
+ });
89
+
90
+ if (source.startsWith('https://')) {
91
+ result.checks.push({
92
+ passed: true,
93
+ type: 'security',
94
+ });
95
+ } else {
96
+ result.checks.push({
97
+ passed: false,
98
+ type: 'security',
99
+ });
100
+ result.status = 'warning';
101
+ }
102
+
103
+ const res = await quickFetch(url);
104
+ const hasSucceeded = res.statusCode?.toString().startsWith('2') ?? false;
105
+
106
+ result.checks.push({
107
+ type: 'fetch_attempt',
108
+ passed: hasSucceeded,
109
+ metadata: {
110
+ fetchStatusCode: res.statusCode,
111
+ },
112
+ });
113
+ if (!hasSucceeded) {
114
+ result.status = res.statusCode?.toString().startsWith('3')
115
+ ? 'warning'
116
+ : 'error';
117
+ }
118
+
119
+ const responseSizeBytes = await getResponseSizeInBytes(res);
120
+ result.checks.push({
121
+ type: 'image_size',
122
+ passed: responseSizeBytes < 1_048_576, // 1024 x 1024 bytes
123
+ metadata: {
124
+ byteCount: responseSizeBytes,
125
+ },
126
+ });
127
+ if (responseSizeBytes > 1_048_576) {
128
+ result.status = 'warning';
129
+ }
130
+ } catch (exception) {
131
+ result.checks.push({
132
+ passed: false,
133
+ type: 'syntax',
134
+ });
135
+ result.status = 'error';
136
+ }
137
+
138
+ imageCheckingResults.push(result);
139
+ }
140
+
141
+ return imageCheckingResults;
142
+ };
@@ -0,0 +1,92 @@
1
+ import { render } from '@react-email/render';
2
+ import * as React from 'react';
3
+ import { type LinkCheckingResult, checkLinks } from './check-links';
4
+
5
+ test('checkLinks()', async () => {
6
+ expect(
7
+ await checkLinks(
8
+ await render(
9
+ <div>
10
+ <a href="/">Root</a>
11
+ <a href="https://resend.com">Resend</a>
12
+ <a href="https://notion.so">Notion</a>
13
+ <a href="http://example.com">Example unsafe</a>
14
+ </div>,
15
+ ),
16
+ ),
17
+ ).toEqual([
18
+ {
19
+ status: 'error',
20
+ checks: [
21
+ {
22
+ type: 'syntax',
23
+ passed: false,
24
+ },
25
+ ],
26
+ link: '/',
27
+ },
28
+ {
29
+ status: 'success',
30
+ checks: [
31
+ {
32
+ type: 'syntax',
33
+ passed: true,
34
+ },
35
+ {
36
+ type: 'security',
37
+ passed: true,
38
+ },
39
+ {
40
+ type: 'fetch_attempt',
41
+ passed: true,
42
+ metadata: {
43
+ fetchStatusCode: 200,
44
+ },
45
+ },
46
+ ],
47
+ link: 'https://resend.com',
48
+ },
49
+ {
50
+ status: 'warning',
51
+ checks: [
52
+ {
53
+ type: 'syntax',
54
+ passed: true,
55
+ },
56
+ {
57
+ type: 'security',
58
+ passed: true,
59
+ },
60
+ {
61
+ type: 'fetch_attempt',
62
+ metadata: {
63
+ fetchStatusCode: 301,
64
+ },
65
+ passed: false,
66
+ },
67
+ ],
68
+ link: 'https://notion.so',
69
+ },
70
+ {
71
+ status: 'warning',
72
+ checks: [
73
+ {
74
+ type: 'syntax',
75
+ passed: true,
76
+ },
77
+ {
78
+ type: 'security',
79
+ passed: false,
80
+ },
81
+ {
82
+ type: 'fetch_attempt',
83
+ metadata: {
84
+ fetchStatusCode: 200,
85
+ },
86
+ passed: true,
87
+ },
88
+ ],
89
+ link: 'http://example.com',
90
+ },
91
+ ] satisfies LinkCheckingResult[]);
92
+ });
@@ -33,6 +33,7 @@ export const checkLinks = async (code: string) => {
33
33
  for await (const anchor of anchors) {
34
34
  const link = anchor.attributes.href;
35
35
  if (!link) continue;
36
+ if (linkCheckingResults.some((result) => result.link === link)) continue;
36
37
  if (link.startsWith('mailto:')) continue;
37
38
 
38
39
  const result: LinkCheckingResult = {
@@ -43,23 +44,10 @@ export const checkLinks = async (code: string) => {
43
44
 
44
45
  try {
45
46
  const url = new URL(link);
46
-
47
- const res = await quickFetch(url);
48
- const hasntSucceeded =
49
- res.statusCode === undefined ||
50
- !res.statusCode.toString().startsWith('2');
51
47
  result.checks.push({
52
- type: 'fetch_attempt',
53
- passed: hasntSucceeded,
54
- metadata: {
55
- fetchStatusCode: res.statusCode,
56
- },
48
+ passed: true,
49
+ type: 'syntax',
57
50
  });
58
- if (hasntSucceeded) {
59
- result.status = res.statusCode?.toString().startsWith('3')
60
- ? 'warning'
61
- : 'error';
62
- }
63
51
 
64
52
  if (link.startsWith('https://')) {
65
53
  result.checks.push({
@@ -73,6 +61,21 @@ export const checkLinks = async (code: string) => {
73
61
  });
74
62
  result.status = 'warning';
75
63
  }
64
+
65
+ const res = await quickFetch(url);
66
+ const hasSucceeded = res.statusCode?.toString().startsWith('2') ?? false;
67
+ result.checks.push({
68
+ type: 'fetch_attempt',
69
+ passed: hasSucceeded,
70
+ metadata: {
71
+ fetchStatusCode: res.statusCode,
72
+ },
73
+ });
74
+ if (!hasSucceeded) {
75
+ result.status = res.statusCode?.toString().startsWith('3')
76
+ ? 'warning'
77
+ : 'error';
78
+ }
76
79
  } catch (exception) {
77
80
  result.checks.push({
78
81
  passed: false,
@@ -1,10 +1,12 @@
1
+ 'use client';
2
+ import { DotLottieReact } from '@lottiefiles/dotlottie-react';
1
3
  import * as SlotPrimitive from '@radix-ui/react-slot';
2
- import * as React from 'react';
4
+ import type * as React from 'react';
5
+ import animatedLoadIcon from '../animated-icons-data/load.json';
3
6
  import { cn } from '../utils/cn';
4
7
  import { unreachable } from '../utils/unreachable';
5
8
 
6
- type ButtonElement = React.ComponentRef<'button'>;
7
- type RootProps = React.ComponentPropsWithoutRef<'button'>;
9
+ type RootProps = React.ComponentProps<'button'>;
8
10
 
9
11
  type Appearance = 'white' | 'gradient';
10
12
  type Size = '1' | '2' | '3' | '4';
@@ -13,43 +15,51 @@ interface ButtonProps extends RootProps {
13
15
  asChild?: boolean;
14
16
  appearance?: Appearance;
15
17
  size?: Size;
18
+ loading?: boolean;
16
19
  }
17
20
 
18
- export const Button = React.forwardRef<ButtonElement, Readonly<ButtonProps>>(
19
- (
20
- {
21
- asChild,
22
- appearance = 'white',
23
- className,
24
- children,
25
- size = '2',
26
- ...props
27
- },
28
- forwardedRef,
29
- ) => {
30
- const classNames = cn(
31
- getSize(size),
32
- getAppearance(appearance),
33
- 'inline-flex items-center justify-center border font-medium',
34
- className,
35
- );
21
+ export const Button = ({
22
+ asChild,
23
+ appearance = 'white',
24
+ className,
25
+ children,
26
+ size = '2',
27
+ loading,
28
+ ref,
29
+ ...props
30
+ }: ButtonProps) => {
31
+ const Root = asChild ? SlotPrimitive.Slot : 'button';
36
32
 
37
- return asChild ? (
38
- <SlotPrimitive.Slot ref={forwardedRef} {...props} className={classNames}>
39
- <SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>
40
- </SlotPrimitive.Slot>
41
- ) : (
42
- <button
43
- className={classNames}
44
- ref={forwardedRef}
45
- type="button"
46
- {...props}
33
+ return (
34
+ <Root
35
+ ref={ref}
36
+ type="button"
37
+ {...props}
38
+ className={cn(
39
+ getSize(size),
40
+ getAppearance(appearance),
41
+ 'inline-flex items-center justify-center gap-2 border font-medium',
42
+ className,
43
+ )}
44
+ aria-disabled={loading}
45
+ >
46
+ <span
47
+ className={cn(
48
+ '-ml-7 opacity-0 transition-opacity duration-200',
49
+ loading && 'opacity-100',
50
+ )}
47
51
  >
48
- {children}
49
- </button>
50
- );
51
- },
52
- );
52
+ <DotLottieReact
53
+ data={animatedLoadIcon}
54
+ autoplay={false}
55
+ className="h-5 w-5"
56
+ loop={true}
57
+ />
58
+ </span>
59
+ <SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>
60
+ </Root>
61
+ );
62
+ };
53
63
 
54
64
  Button.displayName = 'Button';
55
65
 
@@ -61,6 +71,7 @@ const getAppearance = (appearance: Appearance | undefined) => {
61
71
  'border-white bg-white text-black transition-colors duration-200 ease-in-out',
62
72
  'hover:bg-white/90',
63
73
  'focus:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white/20',
74
+ 'mt-2 mb-4 aria-disabled:border-transparent aria-disabled:bg-slate-11',
64
75
  ];
65
76
  case 'gradient':
66
77
  return [
@@ -0,0 +1,19 @@
1
+ import { forwardRef } from 'react';
2
+ import type { IconElement, IconProps } from './icon-base';
3
+ import { IconBase } from './icon-base';
4
+
5
+ export const IconImage = forwardRef<IconElement, IconProps>((props, ref) => (
6
+ <IconBase {...props} ref={ref}>
7
+ <g
8
+ fill="none"
9
+ stroke="currentColor"
10
+ strokeLinecap="round"
11
+ strokeLinejoin="round"
12
+ strokeWidth="2"
13
+ >
14
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
15
+ <circle cx="9" cy="9" r="2" />
16
+ <path d="m21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
17
+ </g>
18
+ </IconBase>
19
+ ));