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.
Files changed (92) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/index.js +10 -10
  3. package/dist/cli/index.mjs +10 -13
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +19 -19
  6. package/dist/preview/.next/app-path-routes-manifest.json +1 -1
  7. package/dist/preview/.next/build-manifest.json +6 -6
  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 +1 -1
  17. package/dist/preview/.next/required-server-files.json +1 -1
  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 +6 -6
  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/196.js +2 -2
  29. package/dist/preview/.next/server/chunks/590.js +1 -0
  30. package/dist/preview/.next/server/chunks/631.js +2 -2
  31. package/dist/preview/.next/server/chunks/734.js +15 -0
  32. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  33. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  34. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  35. package/dist/preview/.next/server/pages/500.html +1 -1
  36. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  37. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  38. package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +1 -0
  39. package/dist/preview/.next/static/chunks/490-d26ba2019ccd4d2f.js +1 -0
  40. package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
  41. package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
  42. package/dist/preview/.next/static/chunks/app/layout-b13c19549e2d3e57.js +1 -0
  43. package/dist/preview/.next/static/chunks/app/page-8f366f3c14282f33.js +1 -0
  44. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.js +1 -0
  45. package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
  46. package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
  47. package/dist/preview/.next/static/css/b60917edfd15a496.css +3 -0
  48. package/dist/preview/.next/trace +22 -21
  49. package/dist/preview/.next/types/app/layout.ts +1 -1
  50. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  51. package/module-punycode.d.ts +3 -0
  52. package/package.json +9 -9
  53. package/src/actions/email-validation/check-images.spec.tsx +89 -0
  54. package/src/actions/email-validation/check-images.ts +141 -0
  55. package/src/actions/email-validation/check-links.spec.tsx +91 -0
  56. package/src/actions/email-validation/check-links.ts +18 -15
  57. package/src/app/preview/[...slug]/preview.tsx +105 -19
  58. package/src/components/button.tsx +47 -36
  59. package/src/components/code-snippet.tsx +0 -2
  60. package/src/components/icons/icon-image.tsx +19 -0
  61. package/src/components/logo.tsx +0 -2
  62. package/src/components/resizable-wrapper.tsx +176 -0
  63. package/src/components/shell.tsx +17 -3
  64. package/src/components/sidebar/checking-results.tsx +150 -0
  65. package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
  66. package/src/components/sidebar/image-checker.tsx +161 -0
  67. package/src/components/sidebar/link-checker.tsx +83 -223
  68. package/src/components/sidebar/sidebar.tsx +75 -27
  69. package/src/components/topbar/active-view-toggle-group.tsx +86 -0
  70. package/src/components/topbar/view-size-controls.tsx +247 -0
  71. package/src/components/topbar.tsx +50 -125
  72. package/src/hooks/use-clamped-state.ts +24 -0
  73. package/src/hooks/use-icon-animation.ts +4 -7
  74. package/src/utils/static-node-modules-for-vm.ts +2 -1
  75. package/tailwind.config.ts +12 -17
  76. package/tsconfig.json +6 -2
  77. package/tsconfig.test.json +8 -0
  78. package/vitest.config.ts +13 -0
  79. package/dist/preview/.next/server/chunks/273.js +0 -1
  80. package/dist/preview/.next/server/chunks/594.js +0 -10
  81. package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
  82. package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
  83. package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
  84. package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
  85. package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
  86. package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
  87. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
  88. package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
  89. package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
  90. package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
  91. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
  92. /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 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 [
@@ -1,5 +1,3 @@
1
- import React from 'react';
2
-
3
1
  const CodeSnippet = ({ children }) => {
4
2
  return (
5
3
  <code className="m-0.5 inline-block rounded-md bg-white/10 p-1 font-mono leading-none text-slate-12">
@@ -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
+ ));
@@ -1,5 +1,3 @@
1
- import * as React from 'react';
2
-
3
1
  export const Logo = () => (
4
2
  <svg
5
3
  fill="none"
@@ -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
+ };
@@ -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(100vw)] lg:translate-x-0': sidebarToggled,
79
- 'lg:w-[calc(100vw-20rem)] lg:translate-x-0': !sidebarToggled,
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(100vh-3.3125rem)] grow md:h-full">
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 data-[root=true]:mt-2 overflow-y-hidden pl-1"
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="data-[root=true]:py-2 flex flex-col truncate">
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 = isBaseEmailsDirectory
48
+ const emailSlug = props.isRoot
52
49
  ? emailFilename
53
50
  : `${props.emailsDirectoryMetadata.relativePath}/${emailFilename}`;
54
51