better-svelte-email 0.2.0 → 0.3.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 (41) hide show
  1. package/dist/components/Button.svelte +2 -4
  2. package/dist/components/Column.svelte +3 -8
  3. package/dist/components/Column.svelte.d.ts +2 -5
  4. package/dist/components/Container.svelte +5 -6
  5. package/dist/components/Container.svelte.d.ts +2 -2
  6. package/dist/components/Heading.svelte +34 -0
  7. package/dist/components/Heading.svelte.d.ts +15 -0
  8. package/dist/components/Hr.svelte +4 -4
  9. package/dist/components/Html.svelte +4 -3
  10. package/dist/components/Html.svelte.d.ts +3 -3
  11. package/dist/components/Img.svelte +27 -0
  12. package/dist/components/Img.svelte.d.ts +10 -0
  13. package/dist/components/Link.svelte +6 -10
  14. package/dist/components/Link.svelte.d.ts +2 -2
  15. package/dist/components/Preview.svelte +35 -0
  16. package/dist/{preview → components}/Preview.svelte.d.ts +3 -3
  17. package/dist/components/Row.svelte +2 -6
  18. package/dist/components/Row.svelte.d.ts +2 -3
  19. package/dist/components/Section.svelte +2 -4
  20. package/dist/components/Section.svelte.d.ts +2 -2
  21. package/dist/components/Text.svelte +4 -6
  22. package/dist/components/Text.svelte.d.ts +0 -1
  23. package/dist/components/index.d.ts +3 -0
  24. package/dist/components/index.js +3 -0
  25. package/dist/emails/apple-receipt.svelte +222 -95
  26. package/dist/emails/demo-email.svelte +1 -1
  27. package/dist/emails/test-email.svelte +4 -2
  28. package/dist/emails/vercel-invite-user.svelte +53 -50
  29. package/dist/emails/vercel-invite-user.svelte.d.ts +3 -3
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/preview/EmailPreview.svelte +773 -0
  33. package/dist/preview/EmailPreview.svelte.d.ts +7 -0
  34. package/dist/preview/index.d.ts +21 -3
  35. package/dist/preview/index.js +27 -5
  36. package/dist/utils/index.d.ts +27 -0
  37. package/dist/utils/index.js +47 -0
  38. package/package.json +32 -7
  39. package/dist/components/__tests__/test-email.svelte +0 -13
  40. package/dist/components/__tests__/test-email.svelte.d.ts +0 -26
  41. package/dist/preview/Preview.svelte +0 -231
@@ -0,0 +1,7 @@
1
+ import type { PreviewData } from './index.js';
2
+ type $$ComponentProps = {
3
+ emailList: PreviewData;
4
+ };
5
+ declare const EmailPreview: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type EmailPreview = ReturnType<typeof EmailPreview>;
7
+ export default EmailPreview;
@@ -47,12 +47,16 @@ export declare const emailList: ({ path: emailPath, root }?: EmailListProps) =>
47
47
  * ```
48
48
  */
49
49
  export declare const createEmail: {
50
- 'create-email': (event: RequestEvent) => Promise<string | {
50
+ 'create-email': (event: RequestEvent) => Promise<{
51
51
  status: number;
52
52
  body: {
53
53
  error: string;
54
54
  };
55
55
  error?: undefined;
56
+ } | {
57
+ body: string;
58
+ status?: undefined;
59
+ error?: undefined;
56
60
  } | {
57
61
  status: number;
58
62
  error: {
@@ -72,8 +76,22 @@ export declare const SendEmailFunction: ({ from, to, subject, html }: {
72
76
  }>;
73
77
  /**
74
78
  * Sends the email using the submitted form data.
79
+ *
80
+ * @param options.resendApiKey - Your Resend API key (keep this server-side only)
81
+ * @param options.customSendEmailFunction - Optional custom function to send emails
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // In +page.server.ts
86
+ * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
87
+ *
88
+ * export const actions = {
89
+ * ...createEmail,
90
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
91
+ * };
92
+ * ```
75
93
  */
76
- export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }: {
94
+ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
77
95
  customSendEmailFunction?: typeof SendEmailFunction;
78
96
  resendApiKey?: string;
79
97
  }) => {
@@ -82,4 +100,4 @@ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }: {
82
100
  error: any;
83
101
  }>;
84
102
  };
85
- export { default as Preview } from './Preview.svelte';
103
+ export { default as EmailPreview } from './EmailPreview.svelte';
@@ -85,7 +85,13 @@ export const createEmail = {
85
85
  const { body } = render(emailComponent);
86
86
  // Remove all HTML comments from the body before formatting
87
87
  const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
88
- return prettier.format(bodyWithoutComments, { parser: 'html', plugins: [parserHtml] });
88
+ const formattedBody = await prettier.format(bodyWithoutComments, {
89
+ parser: 'html',
90
+ plugins: [parserHtml]
91
+ });
92
+ return {
93
+ body: formattedBody
94
+ };
89
95
  }
90
96
  catch (error) {
91
97
  console.error('Error rendering email:', error);
@@ -112,8 +118,22 @@ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKe
112
118
  };
113
119
  /**
114
120
  * Sends the email using the submitted form data.
121
+ *
122
+ * @param options.resendApiKey - Your Resend API key (keep this server-side only)
123
+ * @param options.customSendEmailFunction - Optional custom function to send emails
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // In +page.server.ts
128
+ * import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
129
+ *
130
+ * export const actions = {
131
+ * ...createEmail,
132
+ * ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
133
+ * };
134
+ * ```
115
135
  */
116
- export const sendEmail = ({ customSendEmailFunction, resendApiKey }) => {
136
+ export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
117
137
  return {
118
138
  'send-email': async (event) => {
119
139
  const data = await event.request.formData();
@@ -128,11 +148,11 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey }) => {
128
148
  sent = await defaultSendEmailFunction(email, resendApiKey);
129
149
  }
130
150
  else if (customSendEmailFunction) {
131
- sent = await customSendEmailFunction(email);
151
+ sent = await customSendEmailFunction(email, resendApiKey);
132
152
  }
133
153
  else if (!customSendEmailFunction && !resendApiKey) {
134
154
  const error = {
135
- message: 'Please pass your Resend API key into the "sendEmail" form action, or provide a custom function.'
155
+ message: 'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
136
156
  };
137
157
  return { success: false, error };
138
158
  }
@@ -180,4 +200,6 @@ function createEmailComponentList(root, paths) {
180
200
  return emailComponentList;
181
201
  }
182
202
  // Export the Preview component
183
- export { default as Preview } from './Preview.svelte';
203
+ // Note: The component is available via: import EmailPreview from 'better-svelte-email/preview/EmailPreview.svelte'
204
+ // or: import { EmailPreview } from 'better-svelte-email/preview'
205
+ export { default as EmailPreview } from './EmailPreview.svelte';
@@ -10,3 +10,30 @@ export declare function styleToString(style: Record<string, string | number | un
10
10
  * @returns Point value as string
11
11
  */
12
12
  export declare function pxToPt(px: string | number): string;
13
+ export type Margin = {
14
+ m?: string;
15
+ mx?: string;
16
+ my?: string;
17
+ mt?: string;
18
+ mr?: string;
19
+ mb?: string;
20
+ ml?: string;
21
+ };
22
+ /**
23
+ * Convert margin props to a CSS style object
24
+ * @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
25
+ * @returns Style object with margin properties in pixels
26
+ */
27
+ export declare function withMargin(props: Margin): any;
28
+ /**
29
+ * Combine multiple styles into a single string
30
+ * @param styles - Array of style strings
31
+ * @returns Combined style string
32
+ */
33
+ export declare function combineStyles(...styles: (string | undefined | null)[]): string;
34
+ /**
35
+ * Render HTML as plain text
36
+ * @param markup - HTML string
37
+ * @returns Plain text string
38
+ */
39
+ export declare const renderAsPlainText: (markup: string) => string;
@@ -1,3 +1,4 @@
1
+ import { convert } from 'html-to-text';
1
2
  /**
2
3
  * Convert a style object to a CSS string
3
4
  * @param style - Object containing CSS properties
@@ -22,3 +23,49 @@ export function pxToPt(px) {
22
23
  const value = typeof px === 'string' ? parseFloat(px) : px;
23
24
  return `${Math.round(value * 0.75)}pt`;
24
25
  }
26
+ /**
27
+ * Convert margin props to a CSS style object
28
+ * @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
29
+ * @returns Style object with margin properties in pixels
30
+ */
31
+ export function withMargin(props) {
32
+ const margins = [
33
+ withSpace(props.m, ['margin']),
34
+ withSpace(props.mx, ['marginLeft', 'marginRight']),
35
+ withSpace(props.my, ['marginTop', 'marginBottom']),
36
+ withSpace(props.mt, ['marginTop']),
37
+ withSpace(props.mr, ['marginRight']),
38
+ withSpace(props.mb, ['marginBottom']),
39
+ withSpace(props.ml, ['marginLeft'])
40
+ ];
41
+ return Object.assign({}, ...margins);
42
+ }
43
+ function withSpace(value, properties) {
44
+ return properties.reduce((styles, property) => {
45
+ if (value) {
46
+ return { ...styles, [property]: `${value}px` };
47
+ }
48
+ return styles;
49
+ }, {});
50
+ }
51
+ /**
52
+ * Combine multiple styles into a single string
53
+ * @param styles - Array of style strings
54
+ * @returns Combined style string
55
+ */
56
+ export function combineStyles(...styles) {
57
+ return styles.filter((style) => style !== '' && style !== undefined && style !== null).join(';');
58
+ }
59
+ /**
60
+ * Render HTML as plain text
61
+ * @param markup - HTML string
62
+ * @returns Plain text string
63
+ */
64
+ export const renderAsPlainText = (markup) => {
65
+ return convert(markup, {
66
+ selectors: [
67
+ { selector: 'img', format: 'skip' },
68
+ { selector: '#__better-svelte-email-preview', format: 'skip' }
69
+ ]
70
+ });
71
+ };
package/package.json CHANGED
@@ -1,27 +1,40 @@
1
1
  {
2
2
  "name": "better-svelte-email",
3
- "version": "0.2.0",
4
- "author": "Anatole",
3
+ "version": "0.3.0",
4
+ "author": "Konixy",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Konixy/better-svelte-email.git"
8
8
  },
9
9
  "peerDependencies": {
10
- "svelte": "^5.14.3"
10
+ "svelte": "^5.14.3",
11
+ "@sveltejs/kit": "^2.0.0"
12
+ },
13
+ "peerDependenciesMeta": {
14
+ "@sveltejs/kit": {
15
+ "optional": true
16
+ }
11
17
  },
12
18
  "dependencies": {
19
+ "html-to-text": "^9.0.5",
13
20
  "magic-string": "^0.30.19",
14
21
  "svelte-highlight": "^7.8.4",
15
22
  "tw-to-css": "^0.0.12"
16
23
  },
24
+ "optionalDependencies": {
25
+ "prettier": "^3.6.2",
26
+ "resend": "^6.1.2"
27
+ },
17
28
  "devDependencies": {
18
29
  "@eslint/compat": "^1.4.0",
19
30
  "@eslint/js": "^9.37.0",
20
31
  "@sveltejs/adapter-auto": "^6.1.1",
32
+ "@sveltejs/adapter-vercel": "^5.10.3",
21
33
  "@sveltejs/kit": "^2.43.8",
22
34
  "@sveltejs/package": "^2.5.4",
23
35
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
24
36
  "@tailwindcss/vite": "^4.1.14",
37
+ "@types/html-to-text": "^9.0.4",
25
38
  "@types/node": "^24",
26
39
  "@vitest/browser": "^3.2.4",
27
40
  "eslint": "^9.37.0",
@@ -29,11 +42,9 @@
29
42
  "eslint-plugin-svelte": "^3.12.4",
30
43
  "globals": "^16.4.0",
31
44
  "playwright": "^1.55.1",
32
- "prettier": "^3.6.2",
33
45
  "prettier-plugin-svelte": "^3.4.0",
34
46
  "prettier-plugin-tailwindcss": "^0.6.14",
35
47
  "publint": "^0.3.13",
36
- "resend": "^6.1.2",
37
48
  "svelte": "^5.39.8",
38
49
  "svelte-check": "^4.3.2",
39
50
  "tailwindcss": "^4.1.14",
@@ -59,9 +70,23 @@
59
70
  "types": "./dist/components/*.svelte.d.ts",
60
71
  "svelte": "./dist/components/*.svelte"
61
72
  },
73
+ "./preview": {
74
+ "types": "./dist/preview/index.d.ts",
75
+ "import": "./dist/preview/index.js",
76
+ "default": "./dist/preview/index.js"
77
+ },
78
+ "./preview/EmailPreview.svelte": {
79
+ "types": "./dist/preview/EmailPreview.svelte.d.ts",
80
+ "svelte": "./dist/preview/EmailPreview.svelte"
81
+ },
82
+ "./utils": {
83
+ "types": "./dist/utils/index.d.ts",
84
+ "import": "./dist/utils/index.js",
85
+ "default": "./dist/utils/index.js"
86
+ },
62
87
  "./package.json": "./package.json"
63
88
  },
64
- "description": "A Svelte 5 preprocessor that transforms Tailwind CSS classes in email components to inline styles with responsive media query support",
89
+ "description": "Svelte email renderer with Tailwind support",
65
90
  "files": [
66
91
  "dist",
67
92
  "!dist/**/*.test.*",
@@ -82,7 +107,7 @@
82
107
  "license": "MIT",
83
108
  "scripts": {
84
109
  "dev": "vite dev",
85
- "build": "vite build && npm run prepack",
110
+ "build": "bun run prepack && vite build",
86
111
  "preview": "vite preview",
87
112
  "package": "svelte-package",
88
113
  "prepare": "svelte-kit sync || echo ''",
@@ -1,13 +0,0 @@
1
- <script>
2
- import { Html, Head, Text, Button, Container } from '../index.js';
3
- </script>
4
-
5
- <Html>
6
- <Head />
7
- <Container class="bg-gray-100 p-8">
8
- <Text class="text-lg font-bold text-blue-600">Hello World</Text>
9
- <Button class="rounded bg-blue-500 px-4 py-2 text-white" href="https://example.com">
10
- Click Me
11
- </Button>
12
- </Container>
13
- </Html>
@@ -1,26 +0,0 @@
1
- export default TestEmail;
2
- type TestEmail = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
10
- [x: string]: never;
11
- }, {
12
- [evt: string]: CustomEvent<any>;
13
- }, {}, {}, string>;
14
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
- new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
- $$bindings?: Bindings;
17
- } & Exports;
18
- (internal: unknown, props: {
19
- $$events?: Events;
20
- $$slots?: Slots;
21
- }): Exports & {
22
- $set?: any;
23
- $on?: any;
24
- };
25
- z_$$bindings?: Bindings;
26
- }
@@ -1,231 +0,0 @@
1
- <script lang="ts">
2
- import { HighlightAuto } from 'svelte-highlight';
3
- import oneDark from 'svelte-highlight/styles/onedark';
4
- import type { PreviewData } from './index.js';
5
-
6
- let { emailList }: { emailList: PreviewData } = $props();
7
-
8
- let selectedEmail = $state<string | null>(null);
9
- let renderedHtml = $state<string>('');
10
- let iframeContent = $state<string>('');
11
- let loading = $state(false);
12
- let error = $state<string | null>(null);
13
-
14
- const FONT_SANS_STYLE = `<style>
15
- body {
16
- font-family:
17
- ui-sans-serif,
18
- system-ui,
19
- -apple-system,
20
- BlinkMacSystemFont,
21
- 'Segoe UI',
22
- Helvetica,
23
- Arial,
24
- 'Noto Sans',
25
- sans-serif;
26
- margin: 0;
27
- }
28
- </style>`;
29
-
30
- function withFontSans(html: string) {
31
- if (!html) return '';
32
-
33
- if (html.includes('<head')) {
34
- return html.replace('<head>', `<head>${FONT_SANS_STYLE}`);
35
- }
36
-
37
- if (html.includes('<html')) {
38
- const htmlTagEnd = html.indexOf('>', html.indexOf('<html'));
39
- if (htmlTagEnd !== -1) {
40
- const before = html.slice(0, htmlTagEnd + 1);
41
- const after = html.slice(htmlTagEnd + 1);
42
- return `${before}<head>${FONT_SANS_STYLE}</head>${after}`;
43
- }
44
- }
45
-
46
- if (html.includes('<body')) {
47
- const bodyTagEnd = html.indexOf('>', html.indexOf('<body'));
48
- if (bodyTagEnd !== -1) {
49
- const before = html.slice(0, bodyTagEnd + 1);
50
- const after = html.slice(bodyTagEnd + 1);
51
- return `${before}${FONT_SANS_STYLE}${after}`;
52
- }
53
- }
54
-
55
- return `${FONT_SANS_STYLE}${html}`;
56
- }
57
-
58
- async function previewEmail(fileName: string) {
59
- selectedEmail = fileName;
60
- loading = true;
61
- error = null;
62
- renderedHtml = '';
63
- iframeContent = '';
64
-
65
- try {
66
- const response = await fetch('?/create-email', {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
69
- body: new URLSearchParams({
70
- file: fileName,
71
- path: emailList.path || '/src/lib/emails'
72
- })
73
- });
74
-
75
- const result = await response.json();
76
-
77
- if (result.type === 'success' && result.data) {
78
- let htmlOutput = '';
79
- try {
80
- const parsed = JSON.parse(result.data);
81
- if (Array.isArray(parsed) && typeof parsed[0] === 'string') {
82
- htmlOutput = parsed[0];
83
- } else if (typeof parsed === 'string') {
84
- htmlOutput = parsed;
85
- }
86
- } catch (parseError) {
87
- htmlOutput = typeof result.data === 'string' ? result.data : '';
88
- }
89
-
90
- if (!htmlOutput) {
91
- throw new Error('Failed to parse rendered HTML response');
92
- }
93
-
94
- renderedHtml = htmlOutput;
95
- iframeContent = withFontSans(htmlOutput);
96
- } else if (result.type === 'error') {
97
- error = result.error?.message || 'Failed to render email';
98
- }
99
- } catch (e) {
100
- error = e instanceof Error ? e.message : 'Failed to preview email';
101
- } finally {
102
- loading = false;
103
- }
104
- }
105
-
106
- function copyHtml() {
107
- if (renderedHtml) {
108
- navigator.clipboard.writeText(renderedHtml);
109
- }
110
- }
111
- </script>
112
-
113
- <svelte:head>
114
- {@html oneDark}
115
- </svelte:head>
116
-
117
- <div
118
- class="grid h-screen grid-cols-[280px_1fr] bg-gray-50 font-sans max-md:grid-cols-1 max-md:grid-rows-[auto_1fr]"
119
- >
120
- <div
121
- class="flex flex-col overflow-hidden border-r border-gray-200 bg-white max-md:max-h-[40vh] max-md:border-r-0 max-md:border-b"
122
- >
123
- <div class="flex items-center justify-between gap-2 border-b border-gray-200 p-6 pb-4">
124
- <h2 class="m-0 text-lg font-semibold text-gray-900">Email Templates</h2>
125
- {#if emailList.files}
126
- <span
127
- class="min-w-6 rounded-full bg-blue-500 px-2 py-0.5 text-center text-xs font-semibold text-white"
128
- >
129
- {emailList.files.length}
130
- </span>
131
- {/if}
132
- </div>
133
-
134
- {#if !emailList.files || emailList.files.length === 0}
135
- <div class="px-4 py-8 text-center text-gray-500">
136
- <p class="my-2 text-sm">No email templates found</p>
137
- <p class="my-2 text-xs text-gray-400">
138
- Create email components in <code
139
- class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs"
140
- >{emailList.path || '/src/lib/emails'}</code
141
- >
142
- </p>
143
- </div>
144
- {:else}
145
- <ul class="m-0 flex-1 list-none overflow-y-auto p-2">
146
- {#each emailList.files as file}
147
- <li>
148
- <button
149
- class="flex w-full cursor-pointer items-center gap-3 rounded-lg border-0 bg-transparent p-3 text-left text-sm text-gray-700 transition-all duration-150 hover:bg-gray-100"
150
- class:bg-blue-50={selectedEmail === file}
151
- class:text-blue-900={selectedEmail === file}
152
- class:font-medium={selectedEmail === file}
153
- onclick={() => previewEmail(file)}
154
- >
155
- <span class="flex-shrink-0 text-xl">📧</span>
156
- <span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{file}</span>
157
- </button>
158
- </li>
159
- {/each}
160
- </ul>
161
- {/if}
162
- </div>
163
-
164
- <div class="flex flex-col overflow-hidden bg-white">
165
- {#if !selectedEmail}
166
- <div class="flex flex-1 items-center justify-center bg-gray-50">
167
- <div class="max-w-md p-8 text-center">
168
- <div class="mb-4 text-6xl">✨</div>
169
- <h3 class="mb-2 text-2xl font-semibold text-gray-900">Select an Email Template</h3>
170
- <p class="text-gray-500">
171
- Choose a template from the sidebar to preview its rendered HTML
172
- </p>
173
- </div>
174
- </div>
175
- {:else if loading}
176
- <div class="flex flex-1 items-center justify-center bg-gray-50">
177
- <div class="max-w-md p-8 text-center">
178
- <div
179
- class="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"
180
- ></div>
181
- <p class="text-gray-500">Rendering email...</p>
182
- </div>
183
- </div>
184
- {:else if error}
185
- <div class="flex flex-1 items-center justify-center bg-gray-50">
186
- <div class="max-w-md p-8 text-center">
187
- <div class="mb-4 text-5xl">⚠️</div>
188
- <h3 class="mb-2 text-2xl font-semibold text-gray-900">Error Rendering Email</h3>
189
- <p class="mb-0 text-gray-500">{error}</p>
190
- <button
191
- class="mt-4 cursor-pointer rounded-md border-0 bg-blue-500 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-600"
192
- onclick={() => selectedEmail && previewEmail(selectedEmail)}
193
- >
194
- Try Again
195
- </button>
196
- </div>
197
- </div>
198
- {:else if renderedHtml}
199
- <div class="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4">
200
- <h3 class="m-0 text-lg font-semibold text-gray-900">{selectedEmail}</h3>
201
- <div class="flex gap-2">
202
- <button
203
- class="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 transition-all duration-150 hover:border-gray-400 hover:bg-gray-50"
204
- onclick={copyHtml}
205
- title="Copy HTML"
206
- >
207
- <span class="text-base">📋</span> Copy HTML
208
- </button>
209
- </div>
210
- </div>
211
-
212
- <div class="flex-1 overflow-hidden bg-gray-50 p-4">
213
- <iframe
214
- title="Email Preview"
215
- srcdoc={iframeContent}
216
- class="h-full w-full rounded-lg border border-gray-200 bg-white"
217
- sandbox="allow-same-origin allow-scripts"
218
- ></iframe>
219
- </div>
220
-
221
- <details class="overflow-auto border-t border-gray-200 bg-gray-50">
222
- <summary
223
- class="cursor-pointer px-6 py-3 font-medium text-gray-700 select-none hover:bg-gray-100"
224
- >
225
- View HTML Source
226
- </summary>
227
- <HighlightAuto class="h-full overflow-y-scroll text-xs" code={renderedHtml} />
228
- </details>
229
- {/if}
230
- </div>
231
- </div>