@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.
- package/README.md +41 -0
- package/dist/EmailPreview.svelte +1121 -0
- package/dist/EmailPreview.svelte.d.ts +17 -0
- package/dist/EmailTreeNode.svelte +270 -0
- package/dist/EmailTreeNode.svelte.d.ts +10 -0
- package/dist/Favicon.svelte +106 -0
- package/dist/Favicon.svelte.d.ts +5 -0
- package/dist/email-tree.d.ts +13 -0
- package/dist/email-tree.js +61 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +168 -0
- package/dist/preview-fs.d.ts +34 -0
- package/dist/preview-fs.js +130 -0
- package/dist/theme.css +42 -0
- package/package.json +2 -2
|
@@ -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>
|