react-email 4.0.0-alpha.0 → 4.0.0-alpha.2
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 +38 -0
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.mjs +10 -13
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +19 -19
- package/dist/preview/.next/app-path-routes-manifest.json +1 -1
- package/dist/preview/.next/build-manifest.json +6 -6
- 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 +1 -1
- package/dist/preview/.next/required-server-files.json +1 -1
- 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 +6 -6
- 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/app-paths-manifest.json +1 -1
- package/dist/preview/.next/server/chunks/196.js +2 -2
- package/dist/preview/.next/server/chunks/590.js +1 -0
- package/dist/preview/.next/server/chunks/631.js +2 -2
- package/dist/preview/.next/server/chunks/734.js +15 -0
- 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/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +1 -0
- package/dist/preview/.next/static/chunks/490-d26ba2019ccd4d2f.js +1 -0
- package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
- package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
- package/dist/preview/.next/static/chunks/app/layout-b13c19549e2d3e57.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-8f366f3c14282f33.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.js +1 -0
- package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
- package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
- package/dist/preview/.next/static/css/b60917edfd15a496.css +3 -0
- package/dist/preview/.next/trace +22 -21
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/module-punycode.d.ts +3 -0
- package/package.json +9 -9
- package/src/actions/email-validation/check-images.spec.tsx +89 -0
- package/src/actions/email-validation/check-images.ts +141 -0
- package/src/actions/email-validation/check-links.spec.tsx +91 -0
- package/src/actions/email-validation/check-links.ts +18 -15
- package/src/app/preview/[...slug]/preview.tsx +105 -19
- package/src/components/button.tsx +47 -36
- package/src/components/code-snippet.tsx +0 -2
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/logo.tsx +0 -2
- package/src/components/resizable-wrapper.tsx +176 -0
- package/src/components/shell.tsx +17 -3
- package/src/components/sidebar/checking-results.tsx +150 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
- package/src/components/sidebar/image-checker.tsx +161 -0
- package/src/components/sidebar/link-checker.tsx +83 -223
- package/src/components/sidebar/sidebar.tsx +75 -27
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +50 -125
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-icon-animation.ts +4 -7
- package/src/utils/static-node-modules-for-vm.ts +2 -1
- package/tailwind.config.ts +12 -17
- package/tsconfig.json +6 -2
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
- package/dist/preview/.next/server/chunks/273.js +0 -1
- package/dist/preview/.next/server/chunks/594.js +0 -10
- package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
- package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
- package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
- package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
- package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_ssgManifest.js +0 -0
|
@@ -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
|
|
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 =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
+
));
|
package/src/components/logo.tsx
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { cn } from '../utils';
|
|
4
|
+
|
|
5
|
+
type Direction = 'north' | 'south' | 'east' | 'west';
|
|
6
|
+
|
|
7
|
+
type ResizableWarpperProps = {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
|
|
11
|
+
maxWidth: number;
|
|
12
|
+
maxHeight: number;
|
|
13
|
+
minWidth: number;
|
|
14
|
+
minHeight: number;
|
|
15
|
+
|
|
16
|
+
onResize: (newSize: number, direction: Direction) => void;
|
|
17
|
+
onResizeEnd?: () => void;
|
|
18
|
+
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
} & Omit<ComponentProps<'div'>, 'onResize' | 'children'>;
|
|
21
|
+
|
|
22
|
+
export const makeIframeDocumentBubbleEvents = (iframe: HTMLIFrameElement) => {
|
|
23
|
+
const mouseMoveBubbler = (event: MouseEvent) => {
|
|
24
|
+
const bounds = iframe.getBoundingClientRect();
|
|
25
|
+
document.dispatchEvent(
|
|
26
|
+
new MouseEvent('mousemove', {
|
|
27
|
+
...event,
|
|
28
|
+
clientX: event.clientX + bounds.x,
|
|
29
|
+
clientY: event.clientY + bounds.y,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
const mouseUpBubbler = (event: MouseEvent) => {
|
|
34
|
+
document.dispatchEvent(new MouseEvent('mouseup', event));
|
|
35
|
+
};
|
|
36
|
+
iframe.contentDocument?.addEventListener('mousemove', mouseMoveBubbler);
|
|
37
|
+
iframe.contentDocument?.addEventListener('mouseup', mouseUpBubbler);
|
|
38
|
+
return () => {
|
|
39
|
+
iframe.contentDocument?.removeEventListener('mousemove', mouseMoveBubbler);
|
|
40
|
+
iframe.contentDocument?.removeEventListener('mouseup', mouseUpBubbler);
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ResizableWarpper = ({
|
|
45
|
+
width,
|
|
46
|
+
height,
|
|
47
|
+
onResize,
|
|
48
|
+
onResizeEnd,
|
|
49
|
+
children,
|
|
50
|
+
|
|
51
|
+
maxHeight,
|
|
52
|
+
maxWidth,
|
|
53
|
+
minHeight,
|
|
54
|
+
minWidth,
|
|
55
|
+
|
|
56
|
+
...rest
|
|
57
|
+
}: ResizableWarpperProps) => {
|
|
58
|
+
const resizableRef = useRef<HTMLElement>(null);
|
|
59
|
+
|
|
60
|
+
const mouseMoveListener = useRef<(event: MouseEvent) => void>(null);
|
|
61
|
+
|
|
62
|
+
const handleStopResizing = useCallback(() => {
|
|
63
|
+
if (mouseMoveListener.current) {
|
|
64
|
+
document.removeEventListener('mousemove', mouseMoveListener.current);
|
|
65
|
+
}
|
|
66
|
+
document.removeEventListener('mouseup', handleStopResizing);
|
|
67
|
+
onResizeEnd?.();
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleStartResizing = (direction: Direction) => {
|
|
71
|
+
mouseMoveListener.current = (event) => {
|
|
72
|
+
if (event.button === 0 && resizableRef.current) {
|
|
73
|
+
const isHorizontal = direction === 'east' || direction === 'west';
|
|
74
|
+
|
|
75
|
+
const mousePosition = isHorizontal ? event.clientX : event.clientY;
|
|
76
|
+
const resizableBoundingRect =
|
|
77
|
+
resizableRef.current.getBoundingClientRect();
|
|
78
|
+
const center = isHorizontal
|
|
79
|
+
? resizableBoundingRect.x + resizableBoundingRect.width / 2
|
|
80
|
+
: resizableBoundingRect.y + resizableBoundingRect.height / 2;
|
|
81
|
+
onResize(Math.abs(mousePosition - center) * 2, direction);
|
|
82
|
+
} else {
|
|
83
|
+
handleStopResizing();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
document.addEventListener('mouseup', handleStopResizing);
|
|
88
|
+
document.addEventListener('mousemove', mouseMoveListener.current);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!window.document) return;
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
handleStopResizing();
|
|
96
|
+
};
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
{...rest}
|
|
103
|
+
className={cn(
|
|
104
|
+
'relative mx-auto my-auto box-content px-4 py-2',
|
|
105
|
+
rest.className,
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
aria-label="resize-west"
|
|
110
|
+
aria-valuenow={width}
|
|
111
|
+
aria-valuemin={minWidth}
|
|
112
|
+
aria-valuemax={maxWidth}
|
|
113
|
+
className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-2 cursor-w-resize p-2 [user-drag:none]"
|
|
114
|
+
onDragStart={(event) => event.preventDefault()}
|
|
115
|
+
draggable="false"
|
|
116
|
+
onMouseDown={() => {
|
|
117
|
+
handleStartResizing('west');
|
|
118
|
+
}}
|
|
119
|
+
role="slider"
|
|
120
|
+
tabIndex={0}
|
|
121
|
+
>
|
|
122
|
+
<div className="h-8 w-1 rounded-md bg-black/30" />
|
|
123
|
+
</div>
|
|
124
|
+
<div
|
|
125
|
+
aria-label="resize-east"
|
|
126
|
+
aria-valuenow={width}
|
|
127
|
+
aria-valuemin={minWidth}
|
|
128
|
+
aria-valuemax={maxWidth}
|
|
129
|
+
onDragStart={(event) => event.preventDefault()}
|
|
130
|
+
className="-translate-x-full -translate-y-1/2 absolute top-1/2 left-full cursor-e-resize p-2 [user-drag:none]"
|
|
131
|
+
draggable="false"
|
|
132
|
+
onMouseDown={() => {
|
|
133
|
+
handleStartResizing('east');
|
|
134
|
+
}}
|
|
135
|
+
role="slider"
|
|
136
|
+
tabIndex={0}
|
|
137
|
+
>
|
|
138
|
+
<div className="h-8 w-1 rounded-md bg-black/30" />
|
|
139
|
+
</div>
|
|
140
|
+
<div
|
|
141
|
+
aria-label="resize-north"
|
|
142
|
+
aria-valuenow={height}
|
|
143
|
+
aria-valuemin={minHeight}
|
|
144
|
+
aria-valuemax={maxHeight}
|
|
145
|
+
onDragStart={(event) => event.preventDefault()}
|
|
146
|
+
className="-translate-x-1/2 -translate-y-1/2 absolute top-0 left-1/2 cursor-n-resize p-2 [user-drag:none]"
|
|
147
|
+
draggable="false"
|
|
148
|
+
onMouseDown={() => {
|
|
149
|
+
handleStartResizing('north');
|
|
150
|
+
}}
|
|
151
|
+
role="slider"
|
|
152
|
+
tabIndex={0}
|
|
153
|
+
>
|
|
154
|
+
<div className="h-1 w-8 rounded-md bg-black/30" />
|
|
155
|
+
</div>
|
|
156
|
+
<div
|
|
157
|
+
aria-label="resize-south"
|
|
158
|
+
aria-valuenow={height}
|
|
159
|
+
aria-valuemin={minHeight}
|
|
160
|
+
aria-valuemax={maxHeight}
|
|
161
|
+
onDragStart={(event) => event.preventDefault()}
|
|
162
|
+
className="-translate-x-1/2 -translate-y-1/2 absolute top-full left-1/2 cursor-s-resize p-2 [user-drag:none]"
|
|
163
|
+
draggable="false"
|
|
164
|
+
onMouseDown={() => {
|
|
165
|
+
handleStartResizing('south');
|
|
166
|
+
}}
|
|
167
|
+
role="slider"
|
|
168
|
+
tabIndex={0}
|
|
169
|
+
>
|
|
170
|
+
<div className="h-1 w-8 rounded-md bg-black/30" />
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<Slot ref={resizableRef}>{children}</Slot>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
package/src/components/shell.tsx
CHANGED
|
@@ -12,8 +12,14 @@ interface ShellProps extends RootProps {
|
|
|
12
12
|
markup?: string;
|
|
13
13
|
currentEmailOpenSlug?: string;
|
|
14
14
|
pathSeparator?: string;
|
|
15
|
+
|
|
15
16
|
activeView?: string;
|
|
16
17
|
setActiveView?: (view: string) => void;
|
|
18
|
+
|
|
19
|
+
viewWidth?: number;
|
|
20
|
+
setViewWidth?: (width: number) => void;
|
|
21
|
+
viewHeight?: number;
|
|
22
|
+
setViewHeight?: (height: number) => void;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export const Shell = ({
|
|
@@ -23,6 +29,10 @@ export const Shell = ({
|
|
|
23
29
|
markup,
|
|
24
30
|
activeView,
|
|
25
31
|
setActiveView,
|
|
32
|
+
viewHeight,
|
|
33
|
+
viewWidth,
|
|
34
|
+
setViewHeight,
|
|
35
|
+
setViewWidth,
|
|
26
36
|
}: ShellProps) => {
|
|
27
37
|
const [sidebarToggled, setSidebarToggled] = React.useState(false);
|
|
28
38
|
const [triggerTransition, setTriggerTransition] = React.useState(false);
|
|
@@ -75,8 +85,8 @@ export const Shell = ({
|
|
|
75
85
|
className={cn(
|
|
76
86
|
'relative h-full max-h-full min-h-screen w-[100vw] overflow-hidden will-change-width sm:mt-[4.375rem] md:absolute md:right-0 lg:mt-0',
|
|
77
87
|
{
|
|
78
|
-
'lg:w-[calc(
|
|
79
|
-
'lg:w-[calc(
|
|
88
|
+
'lg:w-[calc(100dvw)] lg:translate-x-0': sidebarToggled,
|
|
89
|
+
'lg:w-[calc(100dvw-20rem)] lg:translate-x-0': !sidebarToggled,
|
|
80
90
|
},
|
|
81
91
|
)}
|
|
82
92
|
style={{
|
|
@@ -104,9 +114,13 @@ export const Shell = ({
|
|
|
104
114
|
}}
|
|
105
115
|
pathSeparator={pathSeparator}
|
|
106
116
|
setActiveView={setActiveView}
|
|
117
|
+
setViewHeight={setViewHeight}
|
|
118
|
+
setViewWidth={setViewWidth}
|
|
119
|
+
viewHeight={viewHeight}
|
|
120
|
+
viewWidth={viewWidth}
|
|
107
121
|
/>
|
|
108
122
|
) : null}
|
|
109
|
-
<div className="relative mx-auto h-[calc(
|
|
123
|
+
<div className="relative mx-auto h-[calc(100dvh-3.3125rem)] grow md:h-full">
|
|
110
124
|
{children}
|
|
111
125
|
</div>
|
|
112
126
|
</div>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export type ResultStatus = 'error' | 'warning' | 'success';
|
|
7
|
+
|
|
8
|
+
const statusStyles = {
|
|
9
|
+
error: 'text-red-600 hover:bg-red-600/10',
|
|
10
|
+
warning: 'text-yellow-300 hover:bg-yellow-400/10',
|
|
11
|
+
success: 'text-green-600 hover:bg-green-600/10',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ResultListProps {
|
|
15
|
+
status: ResultStatus;
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
defaultOpen?: boolean;
|
|
20
|
+
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ResultList = ({
|
|
25
|
+
status,
|
|
26
|
+
label,
|
|
27
|
+
|
|
28
|
+
disabled,
|
|
29
|
+
defaultOpen,
|
|
30
|
+
|
|
31
|
+
children,
|
|
32
|
+
}: ResultListProps) => {
|
|
33
|
+
return (
|
|
34
|
+
<Collapsible.Root className="group" defaultOpen={defaultOpen && !disabled}>
|
|
35
|
+
<Collapsible.Trigger
|
|
36
|
+
className={cn(
|
|
37
|
+
'group flex w-full items-center gap-1 rounded p-2 transition-colors duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
38
|
+
statusStyles[status],
|
|
39
|
+
disabled && 'cursor-not-allowed opacity-70',
|
|
40
|
+
)}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
>
|
|
43
|
+
<span
|
|
44
|
+
className={cn(
|
|
45
|
+
'-mt-[.125rem] transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
46
|
+
'rotate-0 group-data-[state=open]:rotate-90',
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<svg
|
|
50
|
+
fill="none"
|
|
51
|
+
height="15"
|
|
52
|
+
viewBox="0 0 15 15"
|
|
53
|
+
width="15"
|
|
54
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
clipRule="evenodd"
|
|
58
|
+
d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
|
|
59
|
+
fill="currentColor"
|
|
60
|
+
fillRule="evenodd"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
</span>
|
|
64
|
+
<div className="flex flex-1 items-center gap-1 font-bold text-[.625rem] uppercase tracking-wide">
|
|
65
|
+
{label}
|
|
66
|
+
</div>
|
|
67
|
+
</Collapsible.Trigger>
|
|
68
|
+
{children ? (
|
|
69
|
+
<Collapsible.Content>
|
|
70
|
+
<ol className="mt-2 mb-1 flex list-none flex-col gap-4">
|
|
71
|
+
{children}
|
|
72
|
+
</ol>
|
|
73
|
+
</Collapsible.Content>
|
|
74
|
+
) : null}
|
|
75
|
+
</Collapsible.Root>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ResultProps = {
|
|
80
|
+
status: ResultStatus;
|
|
81
|
+
} & ComponentProps<typeof motion.li>;
|
|
82
|
+
|
|
83
|
+
const resultAnimation = {
|
|
84
|
+
hidden: { opacity: 0, y: 10 },
|
|
85
|
+
visible: {
|
|
86
|
+
opacity: 1,
|
|
87
|
+
y: 0,
|
|
88
|
+
transition: { duration: 0.6, ease: 'easeOut', staggerChildren: 0.1 },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Result = ({ children, status, ...rest }: ResultProps) => {
|
|
93
|
+
return (
|
|
94
|
+
<AnimatePresence mode="wait">
|
|
95
|
+
<motion.li
|
|
96
|
+
data-status={status}
|
|
97
|
+
initial="hidden"
|
|
98
|
+
layout
|
|
99
|
+
variants={resultAnimation}
|
|
100
|
+
animate="visible"
|
|
101
|
+
{...rest}
|
|
102
|
+
className={cn(
|
|
103
|
+
'group/item relative w-full rounded-md p-2 pl-4 transition-colors duration-300 ease-out hover:bg-slate-5',
|
|
104
|
+
rest.className,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</motion.li>
|
|
109
|
+
</AnimatePresence>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const titleStatusAnimation = {
|
|
114
|
+
hidden: { opacity: 0, y: 5 },
|
|
115
|
+
visible: {
|
|
116
|
+
opacity: 1,
|
|
117
|
+
y: 0,
|
|
118
|
+
transition: { duration: 0.4, ease: 'easeOut' },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
interface ResultStatusDescriptionProps {
|
|
123
|
+
children: React.ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Result.StatusDescription = ({ children }: ResultStatusDescriptionProps) => {
|
|
127
|
+
return (
|
|
128
|
+
<motion.div
|
|
129
|
+
className="mt-1 font-semibold text-[.625rem] uppercase"
|
|
130
|
+
variants={titleStatusAnimation}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</motion.div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
interface ResultTitleProps {
|
|
138
|
+
children: React.ReactNode;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Result.Title = ({ children }: ResultTitleProps) => {
|
|
142
|
+
return (
|
|
143
|
+
<motion.div
|
|
144
|
+
className="flex w-full items-center gap-2 text-xs group-data-[status=error]/item:text-red-400 group-data-[status=success]/item:text-green-400 group-data-[status=warning]/item:text-yellow-300 "
|
|
145
|
+
variants={titleStatusAnimation}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</motion.div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
@@ -3,7 +3,6 @@ import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { cn } from '../../utils';
|
|
6
|
-
import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
|
|
7
6
|
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
8
7
|
import { IconFile } from '../icons/icon-file';
|
|
9
8
|
import { FileTreeDirectory } from './file-tree-directory';
|
|
@@ -15,15 +14,13 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
15
14
|
isRoot?: boolean;
|
|
16
15
|
}) => {
|
|
17
16
|
const searchParams = useSearchParams();
|
|
18
|
-
const isBaseEmailsDirectory =
|
|
19
|
-
props.emailsDirectoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
|
|
20
17
|
|
|
21
18
|
return (
|
|
22
19
|
<AnimatePresence initial={false}>
|
|
23
20
|
{props.open ? (
|
|
24
21
|
<Collapsible.Content
|
|
25
22
|
asChild
|
|
26
|
-
className="relative
|
|
23
|
+
className="relative overflow-y-hidden pl-1"
|
|
27
24
|
forceMount
|
|
28
25
|
>
|
|
29
26
|
<motion.div
|
|
@@ -34,7 +31,7 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
34
31
|
{props.isRoot ? null : (
|
|
35
32
|
<div className="line absolute left-2.5 h-full w-px bg-slate-6" />
|
|
36
33
|
)}
|
|
37
|
-
<div className="
|
|
34
|
+
<div className="flex flex-col truncate">
|
|
38
35
|
<LayoutGroup id="sidebar">
|
|
39
36
|
{props.emailsDirectoryMetadata.subDirectories.map(
|
|
40
37
|
(subDirectory) => (
|
|
@@ -48,7 +45,7 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
48
45
|
)}
|
|
49
46
|
{props.emailsDirectoryMetadata.emailFilenames.map(
|
|
50
47
|
(emailFilename, index) => {
|
|
51
|
-
const emailSlug =
|
|
48
|
+
const emailSlug = props.isRoot
|
|
52
49
|
? emailFilename
|
|
53
50
|
: `${props.emailsDirectoryMetadata.relativePath}/${emailFilename}`;
|
|
54
51
|
|