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,773 @@
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
+ import type { ActionResult } from '@sveltejs/kit';
6
+ import { applyAction, deserialize } from '$app/forms';
7
+
8
+ let { emailList }: { emailList: PreviewData } = $props();
9
+
10
+ let selectedEmail = $state<string | null>(null);
11
+ let renderedHtml = $state<string>('');
12
+ let iframeContent = $state<string>('');
13
+ let loading = $state(false);
14
+ let error = $state<string | null>(null);
15
+ let showSendModal = $state(false);
16
+ let sendLoading = $state(false);
17
+ let sendSuccess = $state(false);
18
+ let sendError = $state<string | null>(null);
19
+ let recipientEmail = $state('');
20
+
21
+ const FONT_SANS_STYLE = `<style>
22
+ body {
23
+ font-family:
24
+ ui-sans-serif,
25
+ system-ui,
26
+ -apple-system,
27
+ BlinkMacSystemFont,
28
+ 'Segoe UI',
29
+ Helvetica,
30
+ Arial,
31
+ 'Noto Sans',
32
+ sans-serif;
33
+ margin: 0;
34
+ }
35
+ </style>`;
36
+
37
+ function withFontSans(html: string) {
38
+ if (!html) return '';
39
+
40
+ if (html.includes('<head')) {
41
+ return html.replace('<head>', `<head>${FONT_SANS_STYLE}`);
42
+ }
43
+
44
+ if (html.includes('<html')) {
45
+ const htmlTagEnd = html.indexOf('>', html.indexOf('<html'));
46
+ if (htmlTagEnd !== -1) {
47
+ const before = html.slice(0, htmlTagEnd + 1);
48
+ const after = html.slice(htmlTagEnd + 1);
49
+ return `${before}<head>${FONT_SANS_STYLE}</head>${after}`;
50
+ }
51
+ }
52
+
53
+ if (html.includes('<body')) {
54
+ const bodyTagEnd = html.indexOf('>', html.indexOf('<body'));
55
+ if (bodyTagEnd !== -1) {
56
+ const before = html.slice(0, bodyTagEnd + 1);
57
+ const after = html.slice(bodyTagEnd + 1);
58
+ return `${before}${FONT_SANS_STYLE}${after}`;
59
+ }
60
+ }
61
+
62
+ return `${FONT_SANS_STYLE}${html}`;
63
+ }
64
+
65
+ async function previewEmail(fileName: string) {
66
+ selectedEmail = fileName;
67
+ loading = true;
68
+ error = null;
69
+ renderedHtml = '';
70
+ iframeContent = '';
71
+
72
+ try {
73
+ const response = await fetch('?/create-email', {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
76
+ body: new URLSearchParams({
77
+ file: fileName,
78
+ path: emailList.path || '/src/lib/emails'
79
+ })
80
+ });
81
+
82
+ const result: ActionResult<{ body: string }> = deserialize(await response.text());
83
+
84
+ if (result.type === 'success' && result.data?.body) {
85
+ renderedHtml = result.data.body;
86
+ iframeContent = withFontSans(result.data.body);
87
+ } else if (result.type === 'error') {
88
+ error = result.error?.message || 'Failed to render email';
89
+ }
90
+ } catch (e) {
91
+ error = e instanceof Error ? e.message : 'Failed to preview email';
92
+ } finally {
93
+ loading = false;
94
+ }
95
+ }
96
+
97
+ function copyHtml() {
98
+ if (renderedHtml) {
99
+ navigator.clipboard.writeText(renderedHtml);
100
+ }
101
+ }
102
+
103
+ function openSendModal() {
104
+ showSendModal = true;
105
+ sendError = null;
106
+ sendSuccess = false;
107
+ }
108
+
109
+ function closeSendModal() {
110
+ showSendModal = false;
111
+ recipientEmail = '';
112
+ sendError = null;
113
+ sendSuccess = false;
114
+ }
115
+
116
+ async function sendTestEmail() {
117
+ if (!recipientEmail) {
118
+ sendError = 'Please provide an email address';
119
+ return;
120
+ }
121
+
122
+ sendLoading = true;
123
+ sendError = null;
124
+ sendSuccess = false;
125
+
126
+ try {
127
+ const response = await fetch('?/send-email', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
130
+ body: new URLSearchParams({
131
+ to: recipientEmail,
132
+ component: selectedEmail || 'Email Template',
133
+ html: renderedHtml
134
+ })
135
+ });
136
+
137
+ const result: ActionResult = deserialize(await response.text());
138
+
139
+ if (result.type === 'success' && result.data?.success) {
140
+ sendSuccess = true;
141
+ setTimeout(() => {
142
+ closeSendModal();
143
+ }, 2000);
144
+ } else if (result.type === 'error' || result.type === 'failure') {
145
+ sendError =
146
+ result.type === 'error'
147
+ ? result.error?.message
148
+ : result.data?.error?.message || 'Failed to send email';
149
+ }
150
+ } catch (e) {
151
+ sendError = e instanceof Error ? e.message : 'Failed to send email';
152
+ } finally {
153
+ sendLoading = false;
154
+ }
155
+ }
156
+ </script>
157
+
158
+ <svelte:head>
159
+ {@html oneDark}
160
+ </svelte:head>
161
+
162
+ <div class="container">
163
+ <div class="sidebar">
164
+ <div class="sidebar-header">
165
+ <h2 class="sidebar-title">Email Templates</h2>
166
+ {#if emailList.files}
167
+ <span class="badge">
168
+ {emailList.files.length}
169
+ </span>
170
+ {/if}
171
+ </div>
172
+
173
+ {#if !emailList.files || emailList.files.length === 0}
174
+ <div class="empty-state">
175
+ <p>No email templates found</p>
176
+ <p>
177
+ Create email components in <code>{emailList.path || '/src/lib/emails'}</code>
178
+ </p>
179
+ </div>
180
+ {:else}
181
+ <ul class="email-list">
182
+ {#each emailList.files as file}
183
+ <li>
184
+ <button
185
+ class="email-button"
186
+ class:active={selectedEmail === file}
187
+ onclick={() => previewEmail(file)}
188
+ >
189
+ <span class="email-icon">📧</span>
190
+ <span class="email-name">{file}</span>
191
+ </button>
192
+ </li>
193
+ {/each}
194
+ </ul>
195
+ {/if}
196
+ </div>
197
+
198
+ <div class="main-content">
199
+ {#if !selectedEmail}
200
+ <div class="centered-state">
201
+ <div class="centered-content">
202
+ <div class="emoji-large">✨</div>
203
+ <h3 class="heading">Select an Email Template</h3>
204
+ <p class="text-gray">Choose a template from the sidebar to preview its rendered HTML</p>
205
+ </div>
206
+ </div>
207
+ {:else if loading}
208
+ <div class="centered-state">
209
+ <div class="centered-content">
210
+ <div class="spinner"></div>
211
+ <p class="text-gray">Rendering email...</p>
212
+ </div>
213
+ </div>
214
+ {:else if error}
215
+ <div class="centered-state">
216
+ <div class="centered-content">
217
+ <div class="emoji-xlarge">⚠️</div>
218
+ <h3 class="heading">Error Rendering Email</h3>
219
+ <p class="text-gray">{error}</p>
220
+ <button class="btn" onclick={() => selectedEmail && previewEmail(selectedEmail)}>
221
+ Try Again
222
+ </button>
223
+ </div>
224
+ </div>
225
+ {:else if renderedHtml}
226
+ <div class="preview-header">
227
+ <h3 class="preview-title">{selectedEmail}</h3>
228
+ <div class="button-group">
229
+ <button class="btn-secondary" onclick={copyHtml} title="Copy HTML">
230
+ <span class="btn-icon">📋</span> Copy HTML
231
+ </button>
232
+ <button class="btn-primary" onclick={openSendModal} title="Send Test Email">
233
+ <span class="btn-icon">📨</span> Send Test
234
+ </button>
235
+ </div>
236
+ </div>
237
+
238
+ <div class="preview-container">
239
+ <iframe
240
+ title="Email Preview"
241
+ srcdoc={iframeContent}
242
+ class="preview-iframe"
243
+ sandbox="allow-same-origin allow-scripts"
244
+ ></iframe>
245
+ </div>
246
+
247
+ <details class="code-section">
248
+ <summary class="code-summary"> View HTML Source </summary>
249
+ <div class="code-content">
250
+ <HighlightAuto code={renderedHtml} />
251
+ </div>
252
+ </details>
253
+ {/if}
254
+ </div>
255
+ </div>
256
+
257
+ {#if showSendModal}
258
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
259
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
260
+ <div class="modal-overlay" onclick={closeSendModal}>
261
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
262
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
263
+ <div
264
+ class="modal"
265
+ role="dialog"
266
+ aria-modal="true"
267
+ aria-labelledby="modal-title"
268
+ tabindex="-1"
269
+ onclick={(e) => e.stopPropagation()}
270
+ >
271
+ <h3 id="modal-title" class="modal-title">Send Test Email</h3>
272
+
273
+ {#if sendSuccess}
274
+ <div class="success-message">
275
+ <div class="success-icon">✅</div>
276
+ <p class="success-text">Email sent successfully!</p>
277
+ </div>
278
+ {:else}
279
+ <div class="form-group">
280
+ <label for="recipient-email" class="form-label"> Recipient Email </label>
281
+ <input
282
+ id="recipient-email"
283
+ type="email"
284
+ bind:value={recipientEmail}
285
+ placeholder="your@email.com"
286
+ class="form-input"
287
+ />
288
+ <p class="form-help">
289
+ The email will be sent using the Resend API key configured on the server.
290
+ </p>
291
+ </div>
292
+
293
+ {#if sendError}
294
+ <div class="error-message">
295
+ {sendError}
296
+ </div>
297
+ {/if}
298
+
299
+ <div class="modal-actions">
300
+ <button class="btn-cancel" onclick={closeSendModal} disabled={sendLoading}>
301
+ Cancel
302
+ </button>
303
+ <button class="btn-submit" onclick={sendTestEmail} disabled={sendLoading}>
304
+ {sendLoading ? 'Sending...' : 'Send Email'}
305
+ </button>
306
+ </div>
307
+ {/if}
308
+ </div>
309
+ </div>
310
+ {/if}
311
+
312
+ <style>
313
+ * {
314
+ box-sizing: border-box;
315
+ }
316
+
317
+ .container {
318
+ display: grid;
319
+ grid-template-columns: 280px 1fr;
320
+ height: 100vh;
321
+ width: 100vw;
322
+ background-color: #f9fafb;
323
+ font-family:
324
+ ui-sans-serif,
325
+ system-ui,
326
+ -apple-system,
327
+ BlinkMacSystemFont,
328
+ 'Segoe UI',
329
+ Helvetica,
330
+ Arial,
331
+ 'Noto Sans',
332
+ sans-serif;
333
+ }
334
+
335
+ @media (max-width: 768px) {
336
+ .container {
337
+ grid-template-columns: 1fr;
338
+ grid-template-rows: auto 1fr;
339
+ }
340
+ }
341
+
342
+ .sidebar {
343
+ display: flex;
344
+ flex-direction: column;
345
+ overflow: hidden;
346
+ border-right: 1px solid #e5e7eb;
347
+ background-color: white;
348
+ }
349
+
350
+ @media (max-width: 768px) {
351
+ .sidebar {
352
+ max-height: 40vh;
353
+ border-right: 0;
354
+ border-bottom: 1px solid #e5e7eb;
355
+ }
356
+ }
357
+
358
+ .sidebar-header {
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: space-between;
362
+ gap: 0.5rem;
363
+ border-bottom: 1px solid #e5e7eb;
364
+ padding: 1.5rem;
365
+ padding-bottom: 1rem;
366
+ }
367
+
368
+ .sidebar-title {
369
+ margin: 0;
370
+ font-size: 1.125rem;
371
+ font-weight: 600;
372
+ color: #111827;
373
+ }
374
+
375
+ .badge {
376
+ min-width: 1.5rem;
377
+ border-radius: 9999px;
378
+ background-color: #3b82f6;
379
+ padding: 0.125rem 0.5rem;
380
+ text-align: center;
381
+ font-size: 0.75rem;
382
+ font-weight: 600;
383
+ color: white;
384
+ }
385
+
386
+ .empty-state {
387
+ padding: 2rem 1rem;
388
+ text-align: center;
389
+ color: #6b7280;
390
+ }
391
+
392
+ .empty-state p {
393
+ margin: 0.5rem 0;
394
+ font-size: 0.875rem;
395
+ }
396
+
397
+ .empty-state p:last-child {
398
+ font-size: 0.75rem;
399
+ color: #9ca3af;
400
+ }
401
+
402
+ .empty-state code {
403
+ border-radius: 0.25rem;
404
+ background-color: #f3f4f6;
405
+ padding: 0.125rem 0.375rem;
406
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
407
+ font-size: 0.75rem;
408
+ }
409
+
410
+ .email-list {
411
+ margin: 0;
412
+ padding: 0.5rem;
413
+ flex: 1;
414
+ list-style: none;
415
+ overflow-y: auto;
416
+ }
417
+
418
+ .email-list li {
419
+ list-style: none;
420
+ }
421
+
422
+ .email-button {
423
+ display: flex;
424
+ width: 100%;
425
+ cursor: pointer;
426
+ align-items: center;
427
+ gap: 0.75rem;
428
+ border-radius: 0.5rem;
429
+ border: 0;
430
+ background-color: transparent;
431
+ padding: 0.75rem;
432
+ text-align: left;
433
+ font-size: 0.875rem;
434
+ color: #374151;
435
+ transition: all 0.15s;
436
+ }
437
+
438
+ .email-button:hover {
439
+ background-color: #f3f4f6;
440
+ }
441
+
442
+ .email-button.active {
443
+ background-color: #eff6ff;
444
+ color: #1e3a8a;
445
+ font-weight: 500;
446
+ }
447
+
448
+ .email-icon {
449
+ flex-shrink: 0;
450
+ font-size: 1.25rem;
451
+ }
452
+
453
+ .email-name {
454
+ flex: 1;
455
+ overflow: hidden;
456
+ text-overflow: ellipsis;
457
+ white-space: nowrap;
458
+ }
459
+
460
+ .main-content {
461
+ display: flex;
462
+ flex-direction: column;
463
+ overflow: hidden;
464
+ background-color: white;
465
+ }
466
+
467
+ .centered-state {
468
+ display: flex;
469
+ flex: 1;
470
+ align-items: center;
471
+ justify-content: center;
472
+ background-color: #f9fafb;
473
+ }
474
+
475
+ .centered-content {
476
+ max-width: 28rem;
477
+ padding: 2rem;
478
+ text-align: center;
479
+ }
480
+
481
+ .emoji-large {
482
+ margin-bottom: 1rem;
483
+ font-size: 3.75rem;
484
+ }
485
+
486
+ .emoji-xlarge {
487
+ margin-bottom: 1rem;
488
+ font-size: 4rem;
489
+ }
490
+
491
+ .spinner {
492
+ margin: 0 auto 1rem;
493
+ height: 3rem;
494
+ width: 3rem;
495
+ border-radius: 9999px;
496
+ border: 4px solid #e5e7eb;
497
+ border-top-color: #3b82f6;
498
+ animation: spin 1s linear infinite;
499
+ }
500
+
501
+ @keyframes spin {
502
+ to {
503
+ transform: rotate(360deg);
504
+ }
505
+ }
506
+
507
+ .heading {
508
+ margin-bottom: 0.5rem;
509
+ font-size: 1.5rem;
510
+ font-weight: 600;
511
+ color: #111827;
512
+ }
513
+
514
+ .text-gray {
515
+ margin: 0;
516
+ color: #6b7280;
517
+ }
518
+
519
+ .btn {
520
+ margin-top: 1rem;
521
+ cursor: pointer;
522
+ border-radius: 0.375rem;
523
+ border: 0;
524
+ background-color: #3b82f6;
525
+ padding: 0.5rem 1rem;
526
+ font-weight: 500;
527
+ color: white;
528
+ transition: background-color 0.15s;
529
+ }
530
+
531
+ .btn:hover {
532
+ background-color: #2563eb;
533
+ }
534
+
535
+ .preview-header {
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: space-between;
539
+ border-bottom: 1px solid #e5e7eb;
540
+ background-color: white;
541
+ padding: 1rem 1.5rem;
542
+ }
543
+
544
+ .preview-title {
545
+ margin: 0;
546
+ font-size: 1.125rem;
547
+ font-weight: 600;
548
+ color: #111827;
549
+ }
550
+
551
+ .button-group {
552
+ display: flex;
553
+ gap: 0.5rem;
554
+ }
555
+
556
+ .btn-secondary {
557
+ display: flex;
558
+ cursor: pointer;
559
+ align-items: center;
560
+ gap: 0.375rem;
561
+ border-radius: 0.375rem;
562
+ border: 1px solid #d1d5db;
563
+ background-color: white;
564
+ padding: 0.5rem 0.875rem;
565
+ font-size: 0.875rem;
566
+ font-weight: 500;
567
+ color: #374151;
568
+ transition: all 0.15s;
569
+ }
570
+
571
+ .btn-secondary:hover {
572
+ border-color: #9ca3af;
573
+ background-color: #f9fafb;
574
+ }
575
+
576
+ .btn-primary {
577
+ display: flex;
578
+ cursor: pointer;
579
+ align-items: center;
580
+ gap: 0.375rem;
581
+ border-radius: 0.375rem;
582
+ border: 0;
583
+ background-color: #3b82f6;
584
+ padding: 0.5rem 0.875rem;
585
+ font-size: 0.875rem;
586
+ font-weight: 500;
587
+ color: white;
588
+ transition: all 0.15s;
589
+ }
590
+
591
+ .btn-primary:hover {
592
+ background-color: #2563eb;
593
+ }
594
+
595
+ .btn-icon {
596
+ font-size: 1rem;
597
+ }
598
+
599
+ .preview-container {
600
+ flex: 1;
601
+ overflow: hidden;
602
+ background-color: #f9fafb;
603
+ padding: 1rem;
604
+ }
605
+
606
+ .preview-iframe {
607
+ height: 100%;
608
+ width: 100%;
609
+ border-radius: 0.5rem;
610
+ border: 1px solid #e5e7eb;
611
+ background-color: white;
612
+ }
613
+
614
+ .code-section {
615
+ overflow: auto;
616
+ border-top: 1px solid #e5e7eb;
617
+ background-color: #f9fafb;
618
+ }
619
+
620
+ .code-summary {
621
+ cursor: pointer;
622
+ padding: 0.75rem 1.5rem;
623
+ font-weight: 500;
624
+ color: #374151;
625
+ user-select: none;
626
+ }
627
+
628
+ .code-summary:hover {
629
+ background-color: #f3f4f6;
630
+ }
631
+
632
+ .code-content {
633
+ height: 100%;
634
+ overflow-y: scroll;
635
+ font-size: 0.75rem;
636
+ }
637
+
638
+ .modal-overlay {
639
+ position: fixed;
640
+ inset: 0;
641
+ z-index: 50;
642
+ display: flex;
643
+ align-items: center;
644
+ justify-content: center;
645
+ background-color: rgba(0, 0, 0, 0.5);
646
+ }
647
+
648
+ .modal {
649
+ width: 100%;
650
+ max-width: 28rem;
651
+ border-radius: 0.5rem;
652
+ background-color: white;
653
+ padding: 1.5rem;
654
+ box-shadow:
655
+ 0 20px 25px -5px rgb(0 0 0 / 0.1),
656
+ 0 8px 10px -6px rgb(0 0 0 / 0.1);
657
+ }
658
+
659
+ .modal-title {
660
+ margin-bottom: 1rem;
661
+ font-size: 1.25rem;
662
+ font-weight: 600;
663
+ color: #111827;
664
+ }
665
+
666
+ .success-message {
667
+ margin-bottom: 1rem;
668
+ border-radius: 0.375rem;
669
+ background-color: #f0fdf4;
670
+ padding: 1rem;
671
+ text-align: center;
672
+ }
673
+
674
+ .success-icon {
675
+ margin-bottom: 0.5rem;
676
+ font-size: 1.875rem;
677
+ }
678
+
679
+ .success-text {
680
+ margin: 0;
681
+ font-weight: 500;
682
+ color: #166534;
683
+ }
684
+
685
+ .form-group {
686
+ margin-bottom: 1rem;
687
+ }
688
+
689
+ .form-label {
690
+ display: block;
691
+ margin-bottom: 0.25rem;
692
+ font-size: 0.875rem;
693
+ font-weight: 500;
694
+ color: #374151;
695
+ }
696
+
697
+ .form-input {
698
+ width: 100%;
699
+ border-radius: 0.375rem;
700
+ border: 1px solid #d1d5db;
701
+ padding: 0.5rem 0.75rem;
702
+ font-size: 0.875rem;
703
+ }
704
+
705
+ .form-input:focus {
706
+ outline: none;
707
+ border-color: #3b82f6;
708
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
709
+ }
710
+
711
+ .form-help {
712
+ margin-top: 0.25rem;
713
+ font-size: 0.75rem;
714
+ color: #6b7280;
715
+ }
716
+
717
+ .error-message {
718
+ margin-bottom: 1rem;
719
+ border-radius: 0.375rem;
720
+ background-color: #fef2f2;
721
+ padding: 0.75rem;
722
+ font-size: 0.875rem;
723
+ color: #991b1b;
724
+ }
725
+
726
+ .modal-actions {
727
+ display: flex;
728
+ justify-content: flex-end;
729
+ gap: 0.5rem;
730
+ }
731
+
732
+ .btn-cancel {
733
+ cursor: pointer;
734
+ border-radius: 0.375rem;
735
+ border: 1px solid #d1d5db;
736
+ background-color: white;
737
+ padding: 0.5rem 1rem;
738
+ font-size: 0.875rem;
739
+ font-weight: 500;
740
+ color: #374151;
741
+ transition: all 0.15s;
742
+ }
743
+
744
+ .btn-cancel:hover {
745
+ background-color: #f9fafb;
746
+ }
747
+
748
+ .btn-cancel:disabled {
749
+ cursor: not-allowed;
750
+ opacity: 0.5;
751
+ }
752
+
753
+ .btn-submit {
754
+ cursor: pointer;
755
+ border-radius: 0.375rem;
756
+ border: 0;
757
+ background-color: #3b82f6;
758
+ padding: 0.5rem 1rem;
759
+ font-size: 0.875rem;
760
+ font-weight: 500;
761
+ color: white;
762
+ transition: all 0.15s;
763
+ }
764
+
765
+ .btn-submit:hover {
766
+ background-color: #2563eb;
767
+ }
768
+
769
+ .btn-submit:disabled {
770
+ cursor: not-allowed;
771
+ opacity: 0.5;
772
+ }
773
+ </style>