@better-svelte-email/preview 2.0.0-beta.0 → 2.0.0-beta.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.
@@ -0,0 +1,1121 @@
1
+ <script lang="ts">
2
+ import type { PreviewData } from '.';
3
+ import type { ActionResult } from '@sveltejs/kit';
4
+ import { deserialize } from '$app/forms';
5
+ import { goto } from '$app/navigation';
6
+ import { codeToHtml } from 'shiki';
7
+ import './theme.css';
8
+ import EmailTreeNode from './EmailTreeNode.svelte';
9
+ import { buildEmailTree } from './email-tree';
10
+ import { onMount } from 'svelte';
11
+ import Favicon from './Favicon.svelte';
12
+
13
+ type Page = {
14
+ params: {
15
+ email?: string | null;
16
+ };
17
+ data: {
18
+ emails?: PreviewData;
19
+ };
20
+ url: URL;
21
+ };
22
+
23
+ type ViewMode = 'render' | 'code' | 'source';
24
+
25
+ function parseViewMode(url: URL): ViewMode {
26
+ const viewParam = url.searchParams.get('view');
27
+ if (viewParam === 'code' || viewParam === 'source') {
28
+ return viewParam;
29
+ }
30
+ return 'render';
31
+ }
32
+
33
+ let { page }: { page: Page } = $props();
34
+ let emailList = $derived(page.data.emails ?? { files: null, path: null });
35
+ let emailTree = $derived.by(() => buildEmailTree(emailList.files ?? []));
36
+
37
+ // Derive selected email from URL params
38
+ let selectedEmail = $derived(page.params.email || null);
39
+
40
+ // Derive the base path for navigation by removing the email param from current path
41
+ let basePath = $derived.by(() => {
42
+ const pathname = page.url.pathname;
43
+ if (selectedEmail) {
44
+ // Remove the selected email from the path
45
+ return pathname.replace(`/${selectedEmail}`, '').replace(/\/$/, '');
46
+ }
47
+ // If no email selected, remove trailing slash if any
48
+ return pathname.replace(/\/$/, '');
49
+ });
50
+
51
+ let renderedHtml = $state<string>('');
52
+ let highlightedHtml = $state<string>('');
53
+ let highlightedSource = $state<string>('');
54
+ let iframeContent = $state<string>('');
55
+ let loading = $state(false);
56
+ let error = $state<string | null>(null);
57
+ let showSendModal = $state(false);
58
+ let sendLoading = $state(false);
59
+ let sendSuccess = $state(false);
60
+ let sendError = $state<string | null>(null);
61
+ let recipientEmail = $state('');
62
+ let viewMode = $derived<ViewMode>(parseViewMode(page.url));
63
+
64
+ const FONT_SANS_STYLE = `<style>
65
+ body {
66
+ font-family:
67
+ ui-sans-serif,
68
+ system-ui,
69
+ -apple-system,
70
+ BlinkMacSystemFont,
71
+ 'Segoe UI',
72
+ Helvetica,
73
+ Arial,
74
+ 'Noto Sans',
75
+ sans-serif;
76
+ margin: 0;
77
+ }</style>`;
78
+
79
+ function withFontSans(html: string) {
80
+ if (!html) return '';
81
+
82
+ if (html.includes('<head')) {
83
+ return html.replace('<head>', `<head>${FONT_SANS_STYLE}`);
84
+ }
85
+
86
+ if (html.includes('<html')) {
87
+ const htmlTagEnd = html.indexOf('>', html.indexOf('<html'));
88
+ if (htmlTagEnd !== -1) {
89
+ const before = html.slice(0, htmlTagEnd + 1);
90
+ const after = html.slice(htmlTagEnd + 1);
91
+ return `${before}<head>${FONT_SANS_STYLE}</head>${after}`;
92
+ }
93
+ }
94
+
95
+ if (html.includes('<body')) {
96
+ const bodyTagEnd = html.indexOf('>', html.indexOf('<body'));
97
+ if (bodyTagEnd !== -1) {
98
+ const before = html.slice(0, bodyTagEnd + 1);
99
+ const after = html.slice(bodyTagEnd + 1);
100
+ return `${before}${FONT_SANS_STYLE}${after}`;
101
+ }
102
+ }
103
+
104
+ return `${FONT_SANS_STYLE}${html}`;
105
+ }
106
+
107
+ // Navigate to the email's URL instead of updating state
108
+ function selectEmail(fileName: string) {
109
+ const search = page.url.search;
110
+ const hash = page.url.hash;
111
+ goto(`${basePath}/${fileName}${search}${hash}`);
112
+ }
113
+
114
+ // Load the email content when selectedEmail changes
115
+ async function loadEmailContent(fileName: string) {
116
+ loading = true;
117
+ error = null;
118
+ renderedHtml = '';
119
+ highlightedHtml = '';
120
+ highlightedSource = '';
121
+ iframeContent = '';
122
+
123
+ try {
124
+ const response = await fetch('?/create-email', {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
127
+ body: new URLSearchParams({
128
+ file: fileName,
129
+ path: emailList.path || '/src/lib/emails'
130
+ })
131
+ });
132
+
133
+ const result: ActionResult<{ body: string; source?: string | null }> = deserialize(
134
+ await response.text()
135
+ );
136
+
137
+ if (result.type === 'success' && result.data?.body) {
138
+ renderedHtml = result.data.body;
139
+ iframeContent = withFontSans(result.data.body);
140
+ highlightedHtml = await codeToHtml(result.data.body, {
141
+ lang: 'html',
142
+ theme: 'vesper'
143
+ });
144
+ const source = result.data.source ?? '';
145
+ highlightedSource = source
146
+ ? await codeToHtml(source, {
147
+ lang: 'svelte',
148
+ theme: 'vesper'
149
+ })
150
+ : '';
151
+ } else if (result.type === 'error') {
152
+ error = result.error?.message || 'Failed to render email';
153
+ }
154
+ } catch (e) {
155
+ error = e instanceof Error ? e.message : 'Failed to preview email';
156
+ } finally {
157
+ loading = false;
158
+ }
159
+ }
160
+
161
+ // Effect to load email content when the URL changes
162
+ $effect(() => {
163
+ if (selectedEmail) {
164
+ loadEmailContent(selectedEmail);
165
+ }
166
+ });
167
+
168
+ $effect(() => {
169
+ const parsed = parseViewMode(page.url);
170
+ if (parsed !== viewMode) {
171
+ viewMode = parsed;
172
+ }
173
+ });
174
+
175
+ function updateViewMode(mode: ViewMode) {
176
+ if (viewMode === mode) return;
177
+
178
+ viewMode = mode;
179
+
180
+ const url = new URL(page.url);
181
+ url.searchParams.set('view', mode);
182
+
183
+ void goto(`${url.pathname}${url.search}${url.hash}`, {
184
+ replaceState: true,
185
+ noScroll: true,
186
+ keepFocus: true
187
+ });
188
+ }
189
+
190
+ function copyHtml() {
191
+ if (renderedHtml) {
192
+ navigator.clipboard.writeText(renderedHtml);
193
+ }
194
+ }
195
+
196
+ function openSendModal() {
197
+ showSendModal = true;
198
+ sendError = null;
199
+ sendSuccess = false;
200
+ }
201
+
202
+ function closeSendModal() {
203
+ showSendModal = false;
204
+ recipientEmail = '';
205
+ sendError = null;
206
+ sendSuccess = false;
207
+ }
208
+
209
+ async function sendTestEmail() {
210
+ if (!recipientEmail) {
211
+ sendError = 'Please provide an email address';
212
+ return;
213
+ }
214
+
215
+ sendLoading = true;
216
+ sendError = null;
217
+ sendSuccess = false;
218
+
219
+ try {
220
+ const response = await fetch('?/send-email', {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
223
+ body: new URLSearchParams({
224
+ to: recipientEmail,
225
+ component: selectedEmail || 'Email Template',
226
+ path: emailList.path || '/src/lib/emails',
227
+ file: selectedEmail as string
228
+ })
229
+ });
230
+
231
+ const result: ActionResult = deserialize(await response.text());
232
+
233
+ if (result.type === 'success' && result.data?.success) {
234
+ sendSuccess = true;
235
+ setTimeout(() => {
236
+ closeSendModal();
237
+ }, 2000);
238
+ } else if (result.type === 'error' || result.type === 'failure') {
239
+ sendError =
240
+ result.type === 'error'
241
+ ? result.error?.message
242
+ : result.data?.error?.message || 'Failed to send email';
243
+ }
244
+ } catch (e) {
245
+ sendError = e instanceof Error ? e.message : 'Failed to send email';
246
+ } finally {
247
+ sendLoading = false;
248
+ }
249
+ }
250
+
251
+ onMount(() => {
252
+ const html = document.querySelector('html');
253
+ if (window.localStorage.getItem('bse_preview_mode') === 'dark') {
254
+ if (html && !html.classList.contains('dark')) {
255
+ setTimeout(() => {
256
+ html.classList.toggle('dark');
257
+ }, 100);
258
+ }
259
+ } else {
260
+ if (html && html.classList.contains('dark')) {
261
+ setTimeout(() => {
262
+ html.classList.remove('dark');
263
+ }, 100);
264
+ }
265
+ }
266
+ });
267
+
268
+ function toggleMode() {
269
+ const html = document.querySelector('html');
270
+ html?.classList.toggle('dark');
271
+ window.localStorage.setItem(
272
+ 'bse_preview_mode',
273
+ html?.classList.contains('dark') ? 'dark' : 'light'
274
+ );
275
+ }
276
+ </script>
277
+
278
+ <div class="container">
279
+ <div class="sidebar">
280
+ <div class="sidebar-header">
281
+ <div class="sidebar-brand">
282
+ <Favicon class="brand-logo" />
283
+
284
+ <h2 class="sidebar-title">Email Preview</h2>
285
+ </div>
286
+ <div class="sidebar-header-right">
287
+ <a
288
+ class="sidebar-header-docs-link"
289
+ href="https://better-svelte-email.konixy.dev/docs"
290
+ title="Documentation"
291
+ target="_blank"
292
+ rel="noopener noreferrer"
293
+ >
294
+ <svg
295
+ class="sidebar-header-docs-link-icon"
296
+ xmlns="http://www.w3.org/2000/svg"
297
+ width="24"
298
+ height="24"
299
+ viewBox="0 0 24 24"
300
+ fill="none"
301
+ stroke="currentColor"
302
+ stroke-width="2"
303
+ stroke-linecap="round"
304
+ stroke-linejoin="round"
305
+ ><path d="M12 7v14" /><path
306
+ d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"
307
+ /></svg
308
+ >
309
+ </a>
310
+ <button class="theme-toggle-btn" onclick={toggleMode} aria-label="Toggle theme">
311
+ <svg
312
+ class="theme-icon theme-icon-dark"
313
+ xmlns="http://www.w3.org/2000/svg"
314
+ width="24"
315
+ height="24"
316
+ viewBox="0 0 24 24"
317
+ fill="none"
318
+ stroke="currentColor"
319
+ stroke-width="2"
320
+ stroke-linecap="round"
321
+ stroke-linejoin="round"
322
+ >
323
+ <path
324
+ d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"
325
+ />
326
+ </svg>
327
+ <svg
328
+ class="theme-icon theme-icon-light"
329
+ xmlns="http://www.w3.org/2000/svg"
330
+ width="24"
331
+ height="24"
332
+ viewBox="0 0 24 24"
333
+ fill="none"
334
+ stroke="currentColor"
335
+ stroke-width="2"
336
+ stroke-linecap="round"
337
+ stroke-linejoin="round"
338
+ >
339
+ <circle cx="12" cy="12" r="4" />
340
+ <path d="M12 2v2" />
341
+ <path d="M12 20v2" />
342
+ <path d="m4.93 4.93 1.41 1.41" />
343
+ <path d="m17.66 17.66 1.41 1.41" />
344
+ <path d="M2 12h2" />
345
+ <path d="M20 12h2" />
346
+ <path d="m6.34 17.66-1.41 1.41" />
347
+ <path d="m19.07 4.93-1.41 1.41" />
348
+ </svg>
349
+ </button>
350
+ </div>
351
+ </div>
352
+
353
+ {#if emailTree.length === 0}
354
+ <div class="empty-state">
355
+ <p>No email templates found</p>
356
+ <p>
357
+ Create email components in <code>{emailList.path || '/src/lib/emails'}</code>
358
+ </p>
359
+ </div>
360
+ {:else}
361
+ <ul class="email-list email-tree">
362
+ {#each emailTree as node (node.path)}
363
+ <EmailTreeNode {node} {selectedEmail} onSelect={selectEmail} />
364
+ {/each}
365
+ </ul>
366
+ {/if}
367
+ </div>
368
+
369
+ <div class="main-content">
370
+ {#if !selectedEmail}
371
+ <div class="centered-state">
372
+ <div class="centered-content">
373
+ <div class="emoji-large">✨</div>
374
+ <h3 class="heading">Select an Email Template</h3>
375
+ <p class="text-gray">Choose a template from the sidebar to preview its rendered HTML</p>
376
+ </div>
377
+ </div>
378
+ {:else if loading}
379
+ <div class="centered-state">
380
+ <div class="centered-content">
381
+ <div class="spinner"></div>
382
+ <p class="text-gray">Rendering email...</p>
383
+ </div>
384
+ </div>
385
+ {:else if error}
386
+ <div class="centered-state">
387
+ <div class="centered-content">
388
+ <div class="emoji-xlarge">⚠️</div>
389
+ <h3 class="heading">Error Rendering Email</h3>
390
+ <p class="text-gray">{error}</p>
391
+ <button class="btn" onclick={() => selectedEmail && loadEmailContent(selectedEmail)}>
392
+ Try Again
393
+ </button>
394
+ </div>
395
+ </div>
396
+ {:else if renderedHtml}
397
+ <div class="preview-header">
398
+ <h3 class="preview-title">
399
+ <svg
400
+ class="preview-title-icon"
401
+ xmlns="http://www.w3.org/2000/svg"
402
+ width="24"
403
+ height="24"
404
+ viewBox="0 0 24 24"
405
+ fill="none"
406
+ stroke="currentColor"
407
+ stroke-width="2"
408
+ stroke-linecap="round"
409
+ stroke-linejoin="round"
410
+ ><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" /><rect
411
+ x="2"
412
+ y="4"
413
+ width="20"
414
+ height="16"
415
+ rx="2"
416
+ /></svg
417
+ >
418
+ {selectedEmail}
419
+ </h3>
420
+ <div class="preview-actions">
421
+ <div class="view-toggle" role="group" aria-label="Preview view selection">
422
+ <button
423
+ class="view-toggle-btn"
424
+ class:active={viewMode === 'render'}
425
+ onclick={() => updateViewMode('render')}
426
+ aria-pressed={viewMode === 'render'}
427
+ type="button"
428
+ >
429
+ Render
430
+ </button>
431
+ <button
432
+ class="view-toggle-btn"
433
+ class:active={viewMode === 'code'}
434
+ onclick={() => updateViewMode('code')}
435
+ aria-pressed={viewMode === 'code'}
436
+ type="button"
437
+ >
438
+ Code
439
+ </button>
440
+ <button
441
+ class="view-toggle-btn"
442
+ class:active={viewMode === 'source'}
443
+ onclick={() => updateViewMode('source')}
444
+ aria-pressed={viewMode === 'source'}
445
+ type="button"
446
+ >
447
+ Source
448
+ </button>
449
+ </div>
450
+
451
+ <button class="btn-primary send-btn" onclick={openSendModal} title="Send Test Email">
452
+ <svg
453
+ class="send-btn-icon"
454
+ xmlns="http://www.w3.org/2000/svg"
455
+ width="24"
456
+ height="24"
457
+ viewBox="0 0 24 24"
458
+ fill="none"
459
+ stroke="currentColor"
460
+ stroke-width="2"
461
+ stroke-linecap="round"
462
+ stroke-linejoin="round"
463
+ ><path
464
+ d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"
465
+ /><path d="m21.854 2.147-10.94 10.939" /></svg
466
+ >
467
+ Send Test Email
468
+ </button>
469
+ </div>
470
+ </div>
471
+
472
+ {#if viewMode === 'render'}
473
+ <div class="preview-container">
474
+ <iframe
475
+ title="Email Preview"
476
+ srcdoc={iframeContent}
477
+ class="preview-iframe"
478
+ sandbox="allow-same-origin allow-scripts allow-popups"
479
+ ></iframe>
480
+ </div>
481
+ {:else if viewMode === 'code'}
482
+ <div class="code-view">
483
+ <div class="code-view-content">
484
+ <div class="code-content">
485
+ {@html highlightedHtml}
486
+ </div>
487
+ </div>
488
+ </div>
489
+ {:else}
490
+ <div class="code-view">
491
+ <div class="code-view-content">
492
+ {#if highlightedSource}
493
+ <div class="code-content">
494
+ {@html highlightedSource}
495
+ </div>
496
+ {:else}
497
+ <div class="code-empty">Source could not be loaded.</div>
498
+ {/if}
499
+ </div>
500
+ </div>
501
+ {/if}
502
+ {/if}
503
+ </div>
504
+ </div>
505
+
506
+ {#if showSendModal}
507
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
508
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
509
+ <div class="modal-overlay" onclick={closeSendModal}>
510
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
511
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
512
+ <div
513
+ class="modal"
514
+ role="dialog"
515
+ aria-modal="true"
516
+ aria-labelledby="modal-title"
517
+ tabindex="-1"
518
+ onclick={(e) => e.stopPropagation()}
519
+ >
520
+ <h3 id="modal-title" class="modal-title">Send Test Email</h3>
521
+
522
+ {#if sendSuccess}
523
+ <div class="success-message">
524
+ <div class="success-icon">✅</div>
525
+ <p class="success-text">Email sent successfully!</p>
526
+ </div>
527
+ {:else}
528
+ <div class="form-group">
529
+ <label for="recipient-email" class="form-label"> Recipient Email </label>
530
+ <input
531
+ id="recipient-email"
532
+ type="email"
533
+ bind:value={recipientEmail}
534
+ placeholder="your@email.com"
535
+ class="form-input"
536
+ />
537
+ <p class="form-help">
538
+ The email will be sent using the Resend API key configured on the server.
539
+ </p>
540
+ </div>
541
+
542
+ {#if sendError}
543
+ <div class="error-message">
544
+ {sendError}
545
+ </div>
546
+ {/if}
547
+
548
+ <div class="modal-actions">
549
+ <button class="btn-cancel" onclick={closeSendModal} disabled={sendLoading}>
550
+ Cancel
551
+ </button>
552
+ <button class="btn-submit" onclick={sendTestEmail} disabled={sendLoading}>
553
+ {sendLoading ? 'Sending...' : 'Send Email'}
554
+ </button>
555
+ </div>
556
+ {/if}
557
+ </div>
558
+ </div>
559
+ {/if}
560
+
561
+ <style>
562
+ * {
563
+ box-sizing: border-box;
564
+ }
565
+
566
+ .container {
567
+ display: grid;
568
+ grid-template-columns: 280px 1fr;
569
+ height: 100vh;
570
+ width: 100vw;
571
+ max-width: none;
572
+ background-color: var(--background);
573
+ font-family:
574
+ ui-sans-serif,
575
+ system-ui,
576
+ -apple-system,
577
+ BlinkMacSystemFont,
578
+ 'Segoe UI',
579
+ Helvetica,
580
+ Arial,
581
+ 'Noto Sans',
582
+ sans-serif;
583
+ }
584
+
585
+ @media (max-width: 768px) {
586
+ .container {
587
+ grid-template-columns: 1fr;
588
+ grid-template-rows: auto 1fr;
589
+ }
590
+ }
591
+
592
+ .sidebar {
593
+ display: flex;
594
+ flex-direction: column;
595
+ overflow: hidden;
596
+ border-right: 1px solid var(--border);
597
+ background-color: var(--card);
598
+ }
599
+
600
+ @media (max-width: 768px) {
601
+ .sidebar {
602
+ max-height: 40vh;
603
+ border-right: 0;
604
+ border-bottom: 1px solid var(--border);
605
+ }
606
+ }
607
+
608
+ .sidebar-header {
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: space-between;
612
+ gap: 0.5rem;
613
+ padding-inline: 1rem;
614
+ height: 4rem;
615
+ border-bottom: 1px solid var(--border);
616
+ }
617
+
618
+ .sidebar-brand {
619
+ display: flex;
620
+ align-items: center;
621
+ gap: 0.75rem;
622
+ }
623
+
624
+ .sidebar-brand > :global(.brand-logo) {
625
+ width: 1.5rem;
626
+ height: 1.5rem;
627
+ }
628
+
629
+ .sidebar-title {
630
+ margin: 0;
631
+ font-size: 0.875rem;
632
+ font-weight: 600;
633
+ letter-spacing: 0.025em;
634
+ color: var(--card-foreground);
635
+ }
636
+ .sidebar-header-right {
637
+ display: flex;
638
+ align-items: center;
639
+ gap: 0.2rem;
640
+ }
641
+
642
+ .sidebar-header-docs-link {
643
+ display: flex;
644
+ align-items: center;
645
+ gap: 0.5rem;
646
+ cursor: pointer;
647
+ border-radius: 0.75rem;
648
+ border: 0;
649
+ background-color: transparent;
650
+ padding: 0.5rem;
651
+ font-size: 0.875rem;
652
+ color: var(--muted-foreground);
653
+ transition: all 0.15s;
654
+ display: flex;
655
+ align-items: center;
656
+ justify-content: center;
657
+ }
658
+ .sidebar-header-docs-link:hover {
659
+ background-color: var(--muted);
660
+ color: var(--foreground);
661
+ }
662
+
663
+ .sidebar-header-docs-link-icon {
664
+ width: 1rem;
665
+ height: 1rem;
666
+ }
667
+
668
+ .theme-toggle-btn {
669
+ cursor: pointer;
670
+ border-radius: 0.75rem;
671
+ border: 0;
672
+ background-color: transparent;
673
+ padding: 0.5rem;
674
+ font-size: 0.875rem;
675
+ color: var(--muted-foreground);
676
+ transition: all 0.15s;
677
+ display: flex;
678
+ align-items: center;
679
+ justify-content: center;
680
+ }
681
+
682
+ .theme-toggle-btn:hover {
683
+ background-color: var(--muted);
684
+ color: var(--foreground);
685
+ }
686
+
687
+ .theme-icon {
688
+ width: 1rem;
689
+ height: 1rem;
690
+ }
691
+
692
+ :global(html:not(.dark)) .theme-icon-dark {
693
+ display: none;
694
+ }
695
+
696
+ :global(html.dark) .theme-icon-light {
697
+ display: none;
698
+ }
699
+
700
+ :global(html.dark) .theme-icon-dark {
701
+ display: block;
702
+ }
703
+
704
+ :global(html:not(.dark)) .theme-icon-light {
705
+ display: block;
706
+ }
707
+
708
+ .empty-state {
709
+ padding: 2rem 1.5rem;
710
+ text-align: center;
711
+ color: var(--muted-foreground);
712
+ }
713
+
714
+ .empty-state p {
715
+ margin: 0.5rem 0;
716
+ font-size: 0.875rem;
717
+ line-height: 1.5;
718
+ }
719
+
720
+ .empty-state p:first-child {
721
+ font-weight: 500;
722
+ color: var(--foreground);
723
+ }
724
+
725
+ .empty-state p:last-child {
726
+ font-size: 0.8125rem;
727
+ color: var(--muted-foreground);
728
+ opacity: 0.8;
729
+ }
730
+
731
+ .empty-state code {
732
+ border-radius: 0.375rem;
733
+ background-color: var(--muted);
734
+ padding: 0.125rem 0.375rem;
735
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
736
+ font-size: 0.75rem;
737
+ font-weight: 500;
738
+ }
739
+
740
+ .email-list {
741
+ margin: 0;
742
+ padding: 0.5rem;
743
+ flex: 1;
744
+ list-style: none;
745
+ overflow-y: auto;
746
+ }
747
+
748
+ .email-tree {
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 0.125rem;
752
+ }
753
+
754
+ .main-content {
755
+ display: flex;
756
+ flex-direction: column;
757
+ overflow: hidden;
758
+ background-color: var(--card);
759
+ width: 100%;
760
+ min-width: 0;
761
+ }
762
+
763
+ .centered-state {
764
+ display: flex;
765
+ flex: 1;
766
+ align-items: center;
767
+ justify-content: center;
768
+ background-color: var(--background);
769
+ }
770
+
771
+ .centered-content {
772
+ max-width: 28rem;
773
+ padding: 2rem;
774
+ text-align: center;
775
+ }
776
+
777
+ .emoji-large {
778
+ margin-bottom: 1rem;
779
+ font-size: 3.75rem;
780
+ }
781
+
782
+ .emoji-xlarge {
783
+ margin-bottom: 1rem;
784
+ font-size: 4rem;
785
+ }
786
+
787
+ .spinner {
788
+ margin: 0 auto 1rem;
789
+ height: 3rem;
790
+ width: 3rem;
791
+ border-radius: 9999px;
792
+ border: 4px solid var(--border);
793
+ border-top-color: var(--svelte);
794
+ animation: spin 1s linear infinite;
795
+ }
796
+
797
+ @keyframes spin {
798
+ to {
799
+ transform: rotate(360deg);
800
+ }
801
+ }
802
+
803
+ .heading {
804
+ margin-bottom: 0.5rem;
805
+ margin-top: 0;
806
+ font-size: 1.5rem;
807
+ font-weight: 600;
808
+ line-height: 1.2;
809
+ color: var(--foreground);
810
+ }
811
+
812
+ .text-gray {
813
+ margin: 0;
814
+ color: var(--muted-foreground);
815
+ }
816
+
817
+ .btn {
818
+ margin-top: 1rem;
819
+ cursor: pointer;
820
+ border-radius: 0.75rem;
821
+ border: 0;
822
+ background-color: color-mix(in srgb, var(--svelte) 90%, transparent);
823
+ padding: 0.5rem 1rem;
824
+ font-weight: 600;
825
+ color: var(--primary-foreground);
826
+ transition: all 0.15s;
827
+ box-shadow: 0 1px 3px 0 color-mix(in srgb, var(--svelte) 50%, transparent);
828
+ }
829
+
830
+ .btn:hover {
831
+ background-color: var(--svelte);
832
+ box-shadow: 0 4px 6px -1px color-mix(in srgb, var(--svelte) 50%, transparent);
833
+ }
834
+
835
+ .preview-header {
836
+ display: flex;
837
+ align-items: center;
838
+ justify-content: space-between;
839
+ border-bottom: 1px solid var(--border);
840
+ background-color: var(--card);
841
+ padding-inline: 1rem;
842
+ height: 4rem;
843
+ }
844
+
845
+ .preview-title {
846
+ margin: 0;
847
+ font-size: 1rem;
848
+ font-weight: 600;
849
+ letter-spacing: -0.01em;
850
+ color: var(--card-foreground);
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 0.5rem;
854
+ padding-left: 0.5rem;
855
+ }
856
+
857
+ .preview-title-icon {
858
+ color: var(--muted-foreground);
859
+ width: 1.15rem;
860
+ height: 1.15rem;
861
+ }
862
+
863
+ .preview-actions {
864
+ display: flex;
865
+ align-items: center;
866
+ gap: 0.75rem;
867
+ }
868
+
869
+ .view-toggle {
870
+ display: inline-flex;
871
+ border: 1px solid var(--border);
872
+ border-radius: 0.75rem;
873
+ background-color: var(--muted);
874
+ padding: 0.25rem;
875
+ }
876
+
877
+ .view-toggle-btn {
878
+ cursor: pointer;
879
+ border: 0;
880
+ background-color: transparent;
881
+ padding: 0.375rem 0.75rem;
882
+ font-size: 0.8125rem;
883
+ font-weight: 600;
884
+ color: var(--secondary-foreground);
885
+ border-radius: 0.6rem;
886
+ transition: all 0.15s;
887
+ }
888
+
889
+ .view-toggle-btn:hover {
890
+ color: var(--foreground);
891
+ }
892
+
893
+ .view-toggle-btn.active {
894
+ background-color: var(--card);
895
+ color: var(--card-foreground);
896
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
897
+ }
898
+
899
+ .btn-primary {
900
+ display: flex;
901
+ cursor: pointer;
902
+ align-items: center;
903
+ gap: 0.5rem;
904
+ border-radius: 0.75rem;
905
+ border: 0;
906
+ background-color: color-mix(in srgb, var(--svelte) 90%, transparent);
907
+ padding: 0.5rem 1rem;
908
+ font-size: 0.85rem;
909
+ font-weight: 500;
910
+ color: white;
911
+ transition: all 0.15s;
912
+ box-shadow: 0 1px 3px 0 color-mix(in srgb, var(--svelte) 50%, transparent);
913
+ }
914
+
915
+ .btn-primary:hover {
916
+ background-color: var(--svelte);
917
+ box-shadow: 0 4px 6px -1px color-mix(in srgb, var(--svelte) 50%, transparent);
918
+ }
919
+
920
+ .send-btn-icon {
921
+ width: 1rem;
922
+ height: 1rem;
923
+ }
924
+
925
+ .preview-container {
926
+ flex: 1;
927
+ overflow: hidden;
928
+ background-color: var(--background);
929
+ padding: 1rem;
930
+ }
931
+
932
+ .preview-iframe {
933
+ height: 100%;
934
+ width: 100%;
935
+ border-radius: 0.75rem;
936
+ border: 1px solid var(--border);
937
+ background-color: var(--card);
938
+ }
939
+
940
+ .code-view {
941
+ flex: 1;
942
+ overflow: auto;
943
+ background-color: var(--background);
944
+ padding: 1rem;
945
+ }
946
+
947
+ .code-view-content {
948
+ height: 100%;
949
+ width: 100%;
950
+ border-radius: 0.75rem;
951
+ border: 1px solid var(--border);
952
+ background-color: #101010;
953
+ overflow: auto;
954
+ }
955
+
956
+ .code-content {
957
+ font-size: 0.75rem;
958
+ padding: 1rem 1.25rem;
959
+ }
960
+
961
+ .code-empty {
962
+ padding: 1.5rem;
963
+ font-size: 0.875rem;
964
+ color: var(--muted-foreground);
965
+ text-align: center;
966
+ }
967
+
968
+ .modal-overlay {
969
+ position: fixed;
970
+ inset: 0;
971
+ z-index: 50;
972
+ display: flex;
973
+ align-items: center;
974
+ justify-content: center;
975
+ background-color: rgba(0, 0, 0, 0.5);
976
+ }
977
+
978
+ .modal {
979
+ width: 100%;
980
+ max-width: 28rem;
981
+ border-radius: 0.75rem;
982
+ background-color: var(--card);
983
+ padding: 1.5rem;
984
+ box-shadow:
985
+ 0 20px 25px -5px rgb(0 0 0 / 0.1),
986
+ 0 8px 10px -6px rgb(0 0 0 / 0.1);
987
+ }
988
+
989
+ .modal-title {
990
+ margin: 0 0 1.5rem;
991
+ font-size: 1.25rem;
992
+ font-weight: 600;
993
+ line-height: 1.2;
994
+ letter-spacing: -0.01em;
995
+ color: var(--card-foreground);
996
+ }
997
+
998
+ .success-message {
999
+ margin-bottom: 1rem;
1000
+ border-radius: 0.75rem;
1001
+ background-color: #f0fdf4;
1002
+ border: 1px solid #bbf7d0;
1003
+ padding: 1.25rem;
1004
+ text-align: center;
1005
+ }
1006
+
1007
+ .success-icon {
1008
+ margin-bottom: 0.75rem;
1009
+ font-size: 2rem;
1010
+ }
1011
+
1012
+ .success-text {
1013
+ margin: 0;
1014
+ font-size: 0.875rem;
1015
+ font-weight: 600;
1016
+ line-height: 1.5;
1017
+ color: #166534;
1018
+ }
1019
+
1020
+ .form-group {
1021
+ margin-bottom: 1rem;
1022
+ }
1023
+
1024
+ .form-label {
1025
+ display: block;
1026
+ margin-bottom: 0.5rem;
1027
+ font-size: 0.875rem;
1028
+ font-weight: 600;
1029
+ color: var(--foreground);
1030
+ }
1031
+
1032
+ .form-input {
1033
+ width: 100%;
1034
+ border-radius: 0.75rem;
1035
+ border: 1px solid var(--input);
1036
+ padding: 0.625rem 0.875rem;
1037
+ font-size: 0.875rem;
1038
+ background-color: var(--card);
1039
+ color: var(--card-foreground);
1040
+ transition: all 0.15s;
1041
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1042
+ }
1043
+
1044
+ .form-input:focus {
1045
+ outline: none;
1046
+ border-color: var(--svelte);
1047
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--svelte) 10%, transparent);
1048
+ }
1049
+
1050
+ .form-help {
1051
+ margin-top: 0.5rem;
1052
+ font-size: 0.8125rem;
1053
+ line-height: 1.5;
1054
+ color: var(--muted-foreground);
1055
+ }
1056
+
1057
+ .error-message {
1058
+ margin-bottom: 1rem;
1059
+ border-radius: 0.75rem;
1060
+ background-color: #fef2f2;
1061
+ border: 1px solid #fecaca;
1062
+ padding: 0.75rem 1rem;
1063
+ font-size: 0.875rem;
1064
+ font-weight: 500;
1065
+ line-height: 1.5;
1066
+ color: #991b1b;
1067
+ }
1068
+
1069
+ .modal-actions {
1070
+ display: flex;
1071
+ justify-content: flex-end;
1072
+ gap: 0.75rem;
1073
+ margin-top: 0.5rem;
1074
+ }
1075
+
1076
+ .btn-cancel {
1077
+ cursor: pointer;
1078
+ border-radius: 0.75rem;
1079
+ border: 1px solid var(--border);
1080
+ background-color: var(--muted);
1081
+ padding: 0.5rem 1rem;
1082
+ font-size: 0.875rem;
1083
+ font-weight: 600;
1084
+ color: var(--secondary-foreground);
1085
+ transition: all 0.15s;
1086
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1087
+ }
1088
+
1089
+ .btn-cancel:hover:not(:disabled) {
1090
+ background-color: var(--secondary);
1091
+ color: var(--foreground);
1092
+ }
1093
+
1094
+ .btn-cancel:disabled {
1095
+ cursor: not-allowed;
1096
+ opacity: 0.5;
1097
+ }
1098
+
1099
+ .btn-submit {
1100
+ cursor: pointer;
1101
+ border-radius: 0.75rem;
1102
+ border: 0;
1103
+ background-color: color-mix(in srgb, var(--svelte) 90%, transparent);
1104
+ padding: 0.5rem 1rem;
1105
+ font-size: 0.875rem;
1106
+ font-weight: 600;
1107
+ color: white;
1108
+ transition: all 0.15s;
1109
+ box-shadow: 0 1px 3px 0 color-mix(in srgb, var(--svelte) 50%, transparent);
1110
+ }
1111
+
1112
+ .btn-submit:hover:not(:disabled) {
1113
+ background-color: var(--svelte);
1114
+ box-shadow: 0 4px 6px -1px color-mix(in srgb, var(--svelte) 50%, transparent);
1115
+ }
1116
+
1117
+ .btn-submit:disabled {
1118
+ cursor: not-allowed;
1119
+ opacity: 0.5;
1120
+ }
1121
+ </style>