@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.7
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/dist/components/CodeBlock.vue +12 -19
- package/dist/components/Markdown.vue +70 -0
- package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
- package/dist/render/createRenderer.d.mts +2 -3
- package/dist/render/createRenderer.d.mts.map +1 -1
- package/dist/render/createRenderer.mjs +55 -4
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +83 -3
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +1 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +15 -15
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/email.d.mts +17 -0
- package/dist/server/email.d.mts.map +1 -0
- package/dist/server/email.mjs +40 -0
- package/dist/server/email.mjs.map +1 -0
- package/dist/server/ui/App.vue +204 -68
- package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
- package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
- package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
- package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
- package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
- package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
- package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
- package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
- package/dist/server/ui/components/ui/toggle/index.ts +3 -3
- package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
- package/dist/server/ui/main.css +20 -20
- package/dist/server/ui/pages/Home.vue +12 -5
- package/dist/server/ui/pages/Preview.vue +369 -150
- package/dist/transformers/inlineCSS.mjs +9 -0
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.d.mts.map +1 -1
- package/dist/transformers/purgeCSS.mjs +67 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +3 -7
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +38 -4
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +7 -3
- package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
- package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
- package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
- package/dist/server/ui/components/ui/resizable/index.ts +0 -3
- /package/dist/components/{Preview.vue → Preheader.vue} +0 -0
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useRoute } from 'vue-router'
|
|
4
|
-
import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
|
|
4
|
+
import { ChevronUp, ChevronDown, Check, ExternalLink } from 'lucide-vue-next'
|
|
5
5
|
import {
|
|
6
6
|
DropdownMenu,
|
|
7
7
|
DropdownMenuContent,
|
|
8
8
|
DropdownMenuItem,
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
} from '@/components/ui/dropdown-menu'
|
|
11
|
-
import {
|
|
12
|
-
ResizableHandle,
|
|
13
|
-
ResizablePanel,
|
|
14
|
-
ResizablePanelGroup,
|
|
15
|
-
} from '@/components/ui/resizable'
|
|
16
11
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
17
12
|
import { Button } from '@/components/ui/button'
|
|
13
|
+
import { Input } from '@/components/ui/input'
|
|
14
|
+
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
|
15
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
16
|
+
import {
|
|
17
|
+
TagsInput,
|
|
18
|
+
TagsInputInput,
|
|
19
|
+
TagsInputItem,
|
|
20
|
+
TagsInputItemDelete,
|
|
21
|
+
TagsInputItemText,
|
|
22
|
+
} from '@/components/ui/tags-input'
|
|
18
23
|
|
|
19
24
|
import stripesUrl from '../stripes.svg'
|
|
20
25
|
|
|
@@ -40,27 +45,20 @@ const sourceView = ref<'compiled' | 'vue' | 'plaintext'>('compiled')
|
|
|
40
45
|
const copied = ref(false)
|
|
41
46
|
|
|
42
47
|
const iframeEl = ref<HTMLIFrameElement>()
|
|
48
|
+
const compiledSourceEl = ref<HTMLElement>()
|
|
43
49
|
const vueSourceEl = ref<HTMLElement>()
|
|
44
50
|
const containerEl = ref<HTMLElement>()
|
|
45
|
-
const
|
|
46
|
-
const leftPanel = ref<InstanceType<typeof ResizablePanel>>()
|
|
47
|
-
const rightPanel = ref<InstanceType<typeof ResizablePanel>>()
|
|
48
|
-
const topPanel = ref<InstanceType<typeof ResizablePanel>>()
|
|
49
|
-
const bottomPanel = ref<InstanceType<typeof ResizablePanel>>()
|
|
51
|
+
const wrapperEl = ref<HTMLElement>()
|
|
50
52
|
|
|
51
53
|
const panelWidth = defineModel<number>('panelWidth', { default: 0 })
|
|
52
54
|
const panelHeight = defineModel<number>('panelHeight', { default: 0 })
|
|
53
55
|
const isDragging = defineModel<boolean>('isDragging', { default: false })
|
|
54
56
|
const isFullSize = defineModel<boolean>('isFullSize', { default: true })
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
&& sideSizes.value.right < 0.5
|
|
61
|
-
&& sideSizes.value.top < 0.5
|
|
62
|
-
&& sideSizes.value.bottom < 0.5
|
|
63
|
-
}
|
|
58
|
+
// Custom resizable: width/height of the iframe wrapper (null = fill container)
|
|
59
|
+
const iframeWidth = ref<number | null>(null)
|
|
60
|
+
const iframeHeight = ref<number | null>(null)
|
|
61
|
+
const iframeContentHeight = ref<number | null>(null)
|
|
64
62
|
|
|
65
63
|
async function copySource() {
|
|
66
64
|
if (sourceView.value === 'compiled') {
|
|
@@ -99,14 +97,102 @@ interface TemplateStats {
|
|
|
99
97
|
|
|
100
98
|
const compatibilityIssues = ref<CompatibilityIssue[]>([])
|
|
101
99
|
const compatibilityLoading = ref(false)
|
|
100
|
+
const compatibilityError = ref('')
|
|
102
101
|
const lintIssues = ref<LintIssue[]>([])
|
|
103
102
|
const lintLoading = ref(false)
|
|
104
103
|
const stats = ref<TemplateStats | null>(null)
|
|
105
104
|
const statsLoading = ref(false)
|
|
106
105
|
|
|
106
|
+
// Email test state
|
|
107
|
+
const emailTo = ref<string[]>([])
|
|
108
|
+
const emailSubject = ref('')
|
|
109
|
+
const emailSending = ref(false)
|
|
110
|
+
const emailPreventThreading = ref(true)
|
|
111
|
+
const emailResult = ref<{ success: boolean; message: string; previewUrl?: string } | null>(null)
|
|
112
|
+
|
|
113
|
+
async function fetchEmailConfig() {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch('/__maizzle/email-config')
|
|
116
|
+
const data = await res.json()
|
|
117
|
+
if (data.to?.length && !emailTo.value.length) emailTo.value = data.to
|
|
118
|
+
if (data.subject && !emailSubject.value) emailSubject.value = data.subject
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function sendTestEmail() {
|
|
123
|
+
if (!emailTo.value.length) return
|
|
124
|
+
emailSending.value = true
|
|
125
|
+
emailResult.value = null
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`/__maizzle/email/${route.params.template}`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
to: emailTo.value,
|
|
133
|
+
subject: (() => {
|
|
134
|
+
let subj = emailSubject.value || String(route.params.template)
|
|
135
|
+
if (emailPreventThreading.value) {
|
|
136
|
+
subj += ` | ${new Date().toISOString().slice(0, 19)}`
|
|
137
|
+
}
|
|
138
|
+
return subj
|
|
139
|
+
})(),
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
emailResult.value = await res.json()
|
|
143
|
+
} catch (error: any) {
|
|
144
|
+
emailResult.value = { success: false, message: error.message }
|
|
145
|
+
} finally {
|
|
146
|
+
emailSending.value = false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let renderedHtml = ''
|
|
151
|
+
|
|
152
|
+
function updateIframeContentHeight() {
|
|
153
|
+
const iframe = iframeEl.value
|
|
154
|
+
const doc = iframe?.contentDocument
|
|
155
|
+
if (!iframe || !doc?.documentElement) return
|
|
156
|
+
|
|
157
|
+
// Hide iframe body overflow — scrolling is handled by the outer ScrollArea
|
|
158
|
+
if (doc.body) doc.body.style.overflow = 'hidden'
|
|
159
|
+
|
|
160
|
+
// Save scroll position of the ScrollArea viewport
|
|
161
|
+
const viewport = wrapperEl.value?.querySelector('[data-slot="scroll-area-viewport"]')
|
|
162
|
+
const scrollTop = viewport?.scrollTop ?? 0
|
|
163
|
+
|
|
164
|
+
// Temporarily collapse to measure true content height
|
|
165
|
+
iframe.style.height = '0'
|
|
166
|
+
iframeContentHeight.value = doc.documentElement.scrollHeight
|
|
167
|
+
iframe.style.height = `${iframeContentHeight.value}px`
|
|
168
|
+
|
|
169
|
+
// Restore scroll position
|
|
170
|
+
if (viewport) {
|
|
171
|
+
viewport.scrollTop = scrollTop
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
107
175
|
async function fetchTemplate() {
|
|
108
176
|
const res = await fetch(`/__maizzle/render/${route.params.template}`)
|
|
109
|
-
|
|
177
|
+
renderedHtml = await res.text()
|
|
178
|
+
|
|
179
|
+
const iframe = iframeEl.value
|
|
180
|
+
const doc = iframe?.contentDocument
|
|
181
|
+
|
|
182
|
+
// Write directly into the iframe document to avoid a full reload,
|
|
183
|
+
// which preserves scroll position natively.
|
|
184
|
+
if (doc) {
|
|
185
|
+
doc.open()
|
|
186
|
+
doc.write(renderedHtml)
|
|
187
|
+
doc.close()
|
|
188
|
+
// Hide iframe body overflow — scrolling is handled by the outer ScrollArea
|
|
189
|
+
if (doc.body) doc.body.style.overflow = 'hidden'
|
|
190
|
+
await nextTick()
|
|
191
|
+
updateIframeContentHeight()
|
|
192
|
+
} else {
|
|
193
|
+
// Fallback for initial load
|
|
194
|
+
srcdoc.value = renderedHtml
|
|
195
|
+
}
|
|
110
196
|
}
|
|
111
197
|
|
|
112
198
|
async function fetchSource() {
|
|
@@ -138,9 +224,19 @@ async function fetchStats() {
|
|
|
138
224
|
|
|
139
225
|
async function fetchCompatibility() {
|
|
140
226
|
compatibilityLoading.value = true
|
|
227
|
+
compatibilityError.value = ''
|
|
141
228
|
try {
|
|
142
|
-
const res = await fetch(
|
|
143
|
-
|
|
229
|
+
const res = await fetch('/__maizzle/compatibility', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
body: renderedHtml,
|
|
232
|
+
})
|
|
233
|
+
const data = await res.json()
|
|
234
|
+
if (data?.error) {
|
|
235
|
+
compatibilityError.value = data.error
|
|
236
|
+
compatibilityIssues.value = []
|
|
237
|
+
} else {
|
|
238
|
+
compatibilityIssues.value = data
|
|
239
|
+
}
|
|
144
240
|
} catch {
|
|
145
241
|
compatibilityIssues.value = []
|
|
146
242
|
} finally {
|
|
@@ -165,13 +261,15 @@ watch(() => route.params.template, () => {
|
|
|
165
261
|
vueSourceHtml.value = ''
|
|
166
262
|
plaintextContent.value = ''
|
|
167
263
|
compatibilityIssues.value = []
|
|
264
|
+
compatibilityError.value = ''
|
|
168
265
|
lintIssues.value = []
|
|
169
266
|
stats.value = null
|
|
267
|
+
emailResult.value = null
|
|
170
268
|
sourceView.value = 'compiled'
|
|
171
|
-
fetchTemplate()
|
|
172
|
-
fetchCompatibility()
|
|
269
|
+
fetchTemplate().then(fetchCompatibility)
|
|
173
270
|
fetchLint()
|
|
174
271
|
fetchStats()
|
|
272
|
+
fetchEmailConfig()
|
|
175
273
|
if (viewMode.value === 'source') fetchSource()
|
|
176
274
|
}, { immediate: true })
|
|
177
275
|
|
|
@@ -191,8 +289,7 @@ watch(sourceView, (view) => {
|
|
|
191
289
|
|
|
192
290
|
if ((import.meta as any).hot) {
|
|
193
291
|
;(import.meta as any).hot.on('maizzle:template-updated', () => {
|
|
194
|
-
fetchTemplate()
|
|
195
|
-
fetchCompatibility()
|
|
292
|
+
fetchTemplate().then(fetchCompatibility)
|
|
196
293
|
fetchLint()
|
|
197
294
|
fetchStats()
|
|
198
295
|
|
|
@@ -237,74 +334,105 @@ async function goToLine(line: number) {
|
|
|
237
334
|
}
|
|
238
335
|
}
|
|
239
336
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
337
|
+
async function goToCompiledLine(line: number) {
|
|
338
|
+
viewMode.value = 'source'
|
|
339
|
+
sourceView.value = 'compiled'
|
|
243
340
|
|
|
244
|
-
|
|
341
|
+
if (!sourceHtml.value) {
|
|
342
|
+
await fetchSource()
|
|
343
|
+
}
|
|
245
344
|
|
|
246
|
-
|
|
247
|
-
function onHDragEnd() { setTimeout(() => { hDragging = false }, 50); isDragging.value = false }
|
|
248
|
-
function onVDragStart() { vDragging = true; isDragging.value = true; emit('clear-device') }
|
|
249
|
-
function onVDragEnd() { setTimeout(() => { vDragging = false }, 50); isDragging.value = false }
|
|
345
|
+
await nextTick()
|
|
250
346
|
|
|
251
|
-
|
|
252
|
-
if (!
|
|
347
|
+
const el = compiledSourceEl.value
|
|
348
|
+
if (!el) return
|
|
253
349
|
|
|
254
|
-
|
|
255
|
-
if (Math.abs(left - right) < 0.5) return
|
|
350
|
+
el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
|
|
256
351
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
352
|
+
const lineEl = el.querySelector(`[data-line="${line}"]`)
|
|
353
|
+
if (lineEl) {
|
|
354
|
+
lineEl.classList.add('shiki-highlight-line')
|
|
355
|
+
lineEl.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
356
|
+
}
|
|
261
357
|
}
|
|
262
358
|
|
|
263
|
-
|
|
264
|
-
if (!vDragging) return
|
|
359
|
+
const emit = defineEmits<{ 'clear-device': [] }>()
|
|
265
360
|
|
|
266
|
-
|
|
267
|
-
if (Math.abs(top - bottom) < 0.5) return
|
|
361
|
+
type Edge = 'left' | 'right' | 'top' | 'bottom'
|
|
268
362
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
363
|
+
function onEdgeDrag(e: MouseEvent, edge: Edge) {
|
|
364
|
+
e.preventDefault()
|
|
365
|
+
isDragging.value = true
|
|
366
|
+
emit('clear-device')
|
|
274
367
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (!el) return
|
|
368
|
+
const container = containerEl.value
|
|
369
|
+
if (!container) return
|
|
278
370
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
371
|
+
const startX = e.clientX
|
|
372
|
+
const startY = e.clientY
|
|
373
|
+
const rect = container.getBoundingClientRect()
|
|
374
|
+
const gutter = 40 // 20px padding on each side
|
|
375
|
+
const maxW = rect.width - gutter
|
|
376
|
+
const maxH = rect.height - gutter
|
|
377
|
+
const startW = iframeWidth.value ?? maxW
|
|
378
|
+
const startH = iframeHeight.value ?? maxH
|
|
379
|
+
|
|
380
|
+
const isHorizontal = edge === 'left' || edge === 'right'
|
|
381
|
+
const sign = (edge === 'left' || edge === 'top') ? -1 : 1
|
|
382
|
+
|
|
383
|
+
const onMove = (ev: MouseEvent) => {
|
|
384
|
+
if (isHorizontal) {
|
|
385
|
+
// Symmetric: each side moves by the delta, so total change is 2x
|
|
386
|
+
const delta = (ev.clientX - startX) * sign
|
|
387
|
+
iframeWidth.value = Math.max(200, Math.min(maxW, startW + delta * 2))
|
|
388
|
+
} else {
|
|
389
|
+
const delta = (ev.clientY - startY) * sign
|
|
390
|
+
iframeHeight.value = Math.max(100, Math.min(maxH, startH + delta * 2))
|
|
285
391
|
}
|
|
286
|
-
return
|
|
287
392
|
}
|
|
288
393
|
|
|
289
|
-
const
|
|
290
|
-
|
|
394
|
+
const onUp = () => {
|
|
395
|
+
isDragging.value = false
|
|
396
|
+
updateFullSize()
|
|
397
|
+
document.removeEventListener('mousemove', onMove)
|
|
398
|
+
document.removeEventListener('mouseup', onUp)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
document.addEventListener('mousemove', onMove)
|
|
402
|
+
document.addEventListener('mouseup', onUp)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function updateFullSize() {
|
|
406
|
+
const container = containerEl.value
|
|
407
|
+
if (!container) return
|
|
408
|
+
const rect = container.getBoundingClientRect()
|
|
409
|
+
const gutter = 40
|
|
410
|
+
isFullSize.value = (iframeWidth.value === null || iframeWidth.value >= rect.width - gutter - 2)
|
|
411
|
+
&& (iframeHeight.value === null || iframeHeight.value >= rect.height - gutter - 2)
|
|
412
|
+
}
|
|
291
413
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
414
|
+
function applyDeviceSize(device: Device | null | undefined) {
|
|
415
|
+
if (!device) {
|
|
416
|
+
iframeWidth.value = null
|
|
417
|
+
iframeHeight.value = null
|
|
418
|
+
updateFullSize()
|
|
419
|
+
return
|
|
420
|
+
}
|
|
295
421
|
|
|
296
|
-
const
|
|
297
|
-
|
|
422
|
+
const container = containerEl.value
|
|
423
|
+
if (!container) return
|
|
424
|
+
const rect = container.getBoundingClientRect()
|
|
425
|
+
const gutter = 40
|
|
298
426
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
bottomPanel.value?.resize(vSide)
|
|
427
|
+
iframeWidth.value = Math.min(device.width, rect.width - gutter)
|
|
428
|
+
iframeHeight.value = Math.min(device.height, rect.height - gutter)
|
|
429
|
+
updateFullSize()
|
|
303
430
|
}
|
|
304
431
|
|
|
305
432
|
watch(() => props.device, (device) => {
|
|
306
433
|
if (viewMode.value === 'source') return
|
|
307
|
-
|
|
434
|
+
// Only apply when a device is selected, not when cleared (drag start clears device)
|
|
435
|
+
if (device) applyDeviceSize(device)
|
|
308
436
|
})
|
|
309
437
|
|
|
310
438
|
watch(() => props.resetKey, () => {
|
|
@@ -339,9 +467,9 @@ function forwardIframeKeys(iframe: HTMLIFrameElement) {
|
|
|
339
467
|
}
|
|
340
468
|
|
|
341
469
|
onMounted(() => {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
const rect =
|
|
470
|
+
const wrapper = wrapperEl.value
|
|
471
|
+
if (wrapper) {
|
|
472
|
+
const rect = wrapper.getBoundingClientRect()
|
|
345
473
|
panelWidth.value = Math.round(rect.width)
|
|
346
474
|
panelHeight.value = Math.round(rect.height)
|
|
347
475
|
observer = new ResizeObserver((entries) => {
|
|
@@ -349,8 +477,13 @@ onMounted(() => {
|
|
|
349
477
|
panelWidth.value = Math.round(entry.contentRect.width)
|
|
350
478
|
panelHeight.value = Math.round(entry.contentRect.height)
|
|
351
479
|
}
|
|
480
|
+
updateIframeContentHeight()
|
|
352
481
|
})
|
|
353
|
-
observer.observe(
|
|
482
|
+
observer.observe(wrapper)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const el = iframeEl.value
|
|
486
|
+
if (el) {
|
|
354
487
|
el.addEventListener('load', () => forwardIframeKeys(el))
|
|
355
488
|
}
|
|
356
489
|
})
|
|
@@ -366,7 +499,7 @@ const activeTab = ref<string | undefined>(undefined)
|
|
|
366
499
|
function toggleBottomPanel() {
|
|
367
500
|
bottomPanelOpen.value = !bottomPanelOpen.value
|
|
368
501
|
if (bottomPanelOpen.value) {
|
|
369
|
-
tabsPanelHeight.value =
|
|
502
|
+
tabsPanelHeight.value = 300
|
|
370
503
|
if (!activeTab.value) activeTab.value = 'compatibility'
|
|
371
504
|
} else {
|
|
372
505
|
tabsPanelHeight.value = 40
|
|
@@ -384,7 +517,7 @@ function onTabClick(tab: string) {
|
|
|
384
517
|
activeTab.value = tab
|
|
385
518
|
if (!bottomPanelOpen.value) {
|
|
386
519
|
bottomPanelOpen.value = true
|
|
387
|
-
tabsPanelHeight.value =
|
|
520
|
+
tabsPanelHeight.value = 300
|
|
388
521
|
}
|
|
389
522
|
}
|
|
390
523
|
|
|
@@ -396,8 +529,11 @@ function onTabsDragStart(e: MouseEvent) {
|
|
|
396
529
|
const startY = e.clientY
|
|
397
530
|
const startHeight = tabsPanelHeight.value
|
|
398
531
|
|
|
532
|
+
const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
|
|
533
|
+
const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
|
|
534
|
+
|
|
399
535
|
const onMouseMove = (e: MouseEvent) => {
|
|
400
|
-
const newHeight = Math.max(40, startHeight + startY - e.clientY)
|
|
536
|
+
const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - e.clientY))
|
|
401
537
|
tabsPanelHeight.value = newHeight
|
|
402
538
|
bottomPanelOpen.value = newHeight > 40
|
|
403
539
|
|
|
@@ -426,120 +562,152 @@ const stripeBg = {
|
|
|
426
562
|
</script>
|
|
427
563
|
|
|
428
564
|
<template>
|
|
429
|
-
<div class="
|
|
430
|
-
<div class="
|
|
565
|
+
<div class="relative h-full">
|
|
566
|
+
<div class="absolute inset-0 bottom-10 overflow-hidden">
|
|
431
567
|
<!-- Source code view -->
|
|
432
568
|
<div v-show="viewMode === 'source'" class="absolute inset-0 min-w-0 overflow-hidden">
|
|
433
569
|
<div class="absolute top-3 left-6 z-10">
|
|
434
570
|
<DropdownMenu :modal="false">
|
|
435
|
-
<DropdownMenuTrigger class="inline-flex items-center gap-1 rounded-md bg-white/10 px-2.5 h-7 text-xs font-medium text-gray-300 hover:bg-white/15 transition-colors">
|
|
571
|
+
<DropdownMenuTrigger class="inline-flex items-center gap-1 rounded-md bg-white/80 dark:bg-white/10 backdrop-blur-md px-2.5 h-7 text-xs font-medium text-gray-600 dark:text-gray-300 hover:bg-white/90 dark:hover:bg-white/15 transition-colors">
|
|
436
572
|
{{ sourceView === 'compiled' ? 'HTML' : sourceView === 'vue' ? 'Source' : 'Plaintext' }}
|
|
437
573
|
<ChevronDown class="size-3 opacity-50" />
|
|
438
574
|
</DropdownMenuTrigger>
|
|
439
|
-
<DropdownMenuContent align="start" class="min-w-
|
|
440
|
-
<DropdownMenuItem class="text-xs font-medium text-gray-
|
|
441
|
-
<Check v-if="sourceView === 'vue'" class="size-3
|
|
442
|
-
<span :class="sourceView === 'vue' ? '' : 'pl-5
|
|
575
|
+
<DropdownMenuContent align="start" class="min-w-32 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md dark:border-white/10">
|
|
576
|
+
<DropdownMenuItem class="text-xs font-medium text-gray-600 dark:text-gray-400 focus:text-gray-900 dark:focus:text-gray-200 dark:focus:bg-white/10" @click="sourceView = 'vue'">
|
|
577
|
+
<Check v-if="sourceView === 'vue'" class="size-3 text-gray-900 dark:text-gray-200" />
|
|
578
|
+
<span :class="[sourceView === 'vue' ? 'text-gray-900 dark:text-gray-200' : 'pl-5']">Source</span>
|
|
443
579
|
</DropdownMenuItem>
|
|
444
|
-
<DropdownMenuItem class="text-xs font-medium text-gray-
|
|
445
|
-
<Check v-if="sourceView === 'compiled'" class="size-3
|
|
446
|
-
<span :class="sourceView === 'compiled' ? '' : 'pl-5
|
|
580
|
+
<DropdownMenuItem class="text-xs font-medium text-gray-600 dark:text-gray-400 focus:text-gray-900 dark:focus:text-gray-200 dark:focus:bg-white/10" @click="sourceView = 'compiled'">
|
|
581
|
+
<Check v-if="sourceView === 'compiled'" class="size-3 text-gray-900 dark:text-gray-200" />
|
|
582
|
+
<span :class="[sourceView === 'compiled' ? 'text-gray-900 dark:text-gray-200' : 'pl-5']">HTML</span>
|
|
447
583
|
</DropdownMenuItem>
|
|
448
|
-
<DropdownMenuItem class="text-xs font-medium text-gray-
|
|
449
|
-
<Check v-if="sourceView === 'plaintext'" class="size-3
|
|
450
|
-
<span :class="sourceView === 'plaintext' ? '' : 'pl-5
|
|
584
|
+
<DropdownMenuItem class="text-xs font-medium text-gray-600 dark:text-gray-400 focus:text-gray-900 dark:focus:text-gray-200 dark:focus:bg-white/10" @click="sourceView = 'plaintext'">
|
|
585
|
+
<Check v-if="sourceView === 'plaintext'" class="size-3 text-gray-900 dark:text-gray-200" />
|
|
586
|
+
<span :class="[sourceView === 'plaintext' ? 'text-gray-900 dark:text-gray-200' : 'pl-5']">Plaintext</span>
|
|
451
587
|
</DropdownMenuItem>
|
|
452
588
|
</DropdownMenuContent>
|
|
453
589
|
</DropdownMenu>
|
|
454
590
|
</div>
|
|
455
591
|
<button
|
|
456
|
-
class="absolute top-3 right-
|
|
592
|
+
class="absolute top-3 right-3 z-10 inline-flex items-center justify-center rounded-md size-9 backdrop-blur-sm bg-white/10 hover:bg-white/20 group disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
|
457
593
|
:disabled="copied"
|
|
458
594
|
@click="copySource"
|
|
459
595
|
>
|
|
460
596
|
<svg v-if="!copied" class="size-5 text-gray-400 group-hover:text-gray-300" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
461
597
|
<svg v-else class="size-5 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
|
|
462
598
|
</button>
|
|
463
|
-
<
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
599
|
+
<ScrollArea v-show="sourceView === 'compiled'" class="h-full">
|
|
600
|
+
<div
|
|
601
|
+
ref="compiledSourceEl"
|
|
602
|
+
class="shiki-line-numbers [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full dark:[&_pre]:bg-gray-950!"
|
|
603
|
+
v-html="sourceHtml"
|
|
604
|
+
/>
|
|
605
|
+
<ScrollBar orientation="horizontal" />
|
|
606
|
+
</ScrollArea>
|
|
607
|
+
<ScrollArea v-show="sourceView === 'vue'" class="h-full">
|
|
608
|
+
<div
|
|
609
|
+
ref="vueSourceEl"
|
|
610
|
+
class="shiki-line-numbers [&_pre]:p-6 [&_pre]:pt-14 [&_pre]:text-base [&_pre]:leading-6 [&_pre]:min-h-full dark:[&_pre]:bg-gray-950!"
|
|
611
|
+
v-html="vueSourceHtml"
|
|
612
|
+
/>
|
|
613
|
+
<ScrollBar orientation="horizontal" />
|
|
614
|
+
</ScrollArea>
|
|
615
|
+
<ScrollArea v-show="sourceView === 'plaintext'" class="h-full">
|
|
616
|
+
<pre
|
|
617
|
+
class="p-6 pt-14 text-sm leading-6 min-h-full text-gray-300 bg-[#27212e] dark:bg-gray-950 whitespace-pre-wrap break-words"
|
|
618
|
+
>{{ plaintextContent }}</pre>
|
|
619
|
+
</ScrollArea>
|
|
478
620
|
</div>
|
|
479
621
|
|
|
622
|
+
<!-- Blocks iframe from stealing pointer events while dragging tabs -->
|
|
623
|
+
<div v-if="tabsDragging" class="fixed inset-0 z-50" />
|
|
624
|
+
|
|
480
625
|
<!-- Preview view -->
|
|
481
626
|
<div v-show="viewMode !== 'source'" class="absolute inset-0">
|
|
482
627
|
<div class="relative h-full opacity-5" :style="stripeBg" />
|
|
483
628
|
</div>
|
|
484
629
|
|
|
485
|
-
<div v-show="viewMode !== 'source'" ref="containerEl" class="absolute inset-0 z-10 flex
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
<
|
|
507
|
-
</
|
|
630
|
+
<div v-show="viewMode !== 'source'" ref="containerEl" class="absolute inset-0 z-10 flex items-center justify-center">
|
|
631
|
+
<!-- Blocks iframe from stealing pointer events while dragging -->
|
|
632
|
+
<div v-if="isDragging" class="absolute inset-0 z-20" />
|
|
633
|
+
<div
|
|
634
|
+
class="relative"
|
|
635
|
+
:style="{
|
|
636
|
+
width: iframeWidth != null ? `${iframeWidth + 40}px` : '100%',
|
|
637
|
+
height: iframeHeight != null ? `${iframeHeight + 40}px` : '100%',
|
|
638
|
+
transition: isDragging ? 'none' : 'width 0.2s ease, height 0.2s ease',
|
|
639
|
+
}"
|
|
640
|
+
>
|
|
641
|
+
<!-- Top handle -->
|
|
642
|
+
<div class="group absolute top-0 left-5 right-5 h-5 flex items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'top')">
|
|
643
|
+
<div class="h-1 w-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
|
|
644
|
+
</div>
|
|
645
|
+
<!-- Bottom handle -->
|
|
646
|
+
<div class="group absolute bottom-0 left-5 right-5 h-5 flex items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'bottom')">
|
|
647
|
+
<div class="h-1 w-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
|
|
648
|
+
</div>
|
|
649
|
+
<!-- Left handle -->
|
|
650
|
+
<div class="group absolute left-0 top-5 bottom-5 w-5 flex items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'left')">
|
|
651
|
+
<div class="w-1 h-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
|
|
652
|
+
</div>
|
|
653
|
+
<!-- Right handle -->
|
|
654
|
+
<div class="group absolute right-0 top-5 bottom-5 w-5 flex items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'right')">
|
|
655
|
+
<div class="w-1 h-12 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-gray-400 group-active:bg-gray-500 dark:group-hover:bg-gray-500 dark:group-active:bg-gray-400 transition-colors" />
|
|
656
|
+
</div>
|
|
657
|
+
<!-- Iframe -->
|
|
658
|
+
<div ref="wrapperEl" class="absolute inset-5 border border-gray-200 dark:border-gray-800">
|
|
659
|
+
<ScrollArea class="h-full w-full bg-white dark:bg-gray-950">
|
|
660
|
+
<iframe
|
|
661
|
+
ref="iframeEl"
|
|
662
|
+
:srcdoc="srcdoc"
|
|
663
|
+
@load="updateIframeContentHeight"
|
|
664
|
+
class="w-full border-0 bg-white dark:bg-gray-950"
|
|
665
|
+
:style="{ height: iframeContentHeight ? `${iframeContentHeight}px` : '100%' }"
|
|
666
|
+
/>
|
|
667
|
+
</ScrollArea>
|
|
668
|
+
</div>
|
|
508
669
|
</div>
|
|
509
670
|
</div>
|
|
510
671
|
</div>
|
|
511
672
|
|
|
512
|
-
<!-- Tabs panel (
|
|
673
|
+
<!-- Tabs panel (overlay) -->
|
|
513
674
|
<div
|
|
514
|
-
class="
|
|
515
|
-
:class="
|
|
675
|
+
class="absolute bottom-0 left-0 right-0 z-20 overflow-hidden border-t border-gray-200 dark:border-gray-800/50"
|
|
676
|
+
:class="[
|
|
677
|
+
!tabsDragging ? 'transition-[height] duration-200 ease-in-out' : '',
|
|
678
|
+
'bg-white dark:bg-gray-950',
|
|
679
|
+
]"
|
|
516
680
|
:style="{ height: `${tabsPanelHeight}px` }"
|
|
517
681
|
>
|
|
518
682
|
<div
|
|
519
|
-
class="relative h-
|
|
683
|
+
class="relative h-0 cursor-row-resize before:absolute before:top-0 before:left-0 before:right-0 before:h-3.25 before:content-['']"
|
|
520
684
|
@mousedown="onTabsDragStart"
|
|
521
685
|
/>
|
|
522
686
|
<Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
|
|
523
687
|
<div class="flex items-center justify-between min-h-10 px-4 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
|
|
524
688
|
<TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
|
|
525
|
-
<TabsTrigger value="compatibility" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('compatibility')">
|
|
689
|
+
<TabsTrigger value="compatibility" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('compatibility')">
|
|
526
690
|
Compatibility
|
|
527
691
|
</TabsTrigger>
|
|
528
|
-
<TabsTrigger value="lint" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('lint')">
|
|
692
|
+
<TabsTrigger value="lint" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('lint')">
|
|
529
693
|
Linter
|
|
530
694
|
</TabsTrigger>
|
|
531
|
-
<TabsTrigger value="stats" class="text-xs px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('stats')">
|
|
695
|
+
<TabsTrigger value="stats" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('stats')">
|
|
532
696
|
Stats
|
|
533
697
|
</TabsTrigger>
|
|
698
|
+
<TabsTrigger value="test" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('test')">
|
|
699
|
+
Test
|
|
700
|
+
</TabsTrigger>
|
|
534
701
|
</TabsList>
|
|
535
702
|
<Button variant="ghost" size="icon" class="h-7 w-7 hover:bg-transparent!" @click="toggleBottomPanel">
|
|
536
|
-
<ChevronUp v-if="!bottomPanelOpen" class="size-4" />
|
|
537
|
-
<ChevronDown v-else class="size-4" />
|
|
703
|
+
<ChevronUp v-if="!bottomPanelOpen" class="size-4 dark:text-gray-400" :stroke-width="1" />
|
|
704
|
+
<ChevronDown v-else class="size-4 dark:text-gray-400" :stroke-width="1" />
|
|
538
705
|
</Button>
|
|
539
706
|
</div>
|
|
540
|
-
<div class="flex-1
|
|
541
|
-
<TabsContent value="compatibility" class="mt-0">
|
|
707
|
+
<div class="flex-1 min-h-0">
|
|
708
|
+
<TabsContent value="compatibility" class="mt-0 h-full"><ScrollArea class="h-full">
|
|
542
709
|
<p v-if="compatibilityLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Checking compatibility...</p>
|
|
710
|
+
<p v-else-if="compatibilityError" class="px-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
|
|
543
711
|
<p v-else-if="compatibilityIssues.length === 0" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No compatibility issues found.</p>
|
|
544
712
|
<ul v-else class="text-xs divide-y">
|
|
545
713
|
<li
|
|
@@ -561,12 +729,12 @@ const stripeBg = {
|
|
|
561
729
|
</div>
|
|
562
730
|
</div>
|
|
563
731
|
</div>
|
|
564
|
-
<button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="
|
|
732
|
+
<button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="goToCompiledLine(issue.line!)">L{{ issue.line }}</button>
|
|
565
733
|
</div>
|
|
566
734
|
</li>
|
|
567
735
|
</ul>
|
|
568
|
-
</TabsContent>
|
|
569
|
-
<TabsContent value="lint" class="mt-0">
|
|
736
|
+
</ScrollArea></TabsContent>
|
|
737
|
+
<TabsContent value="lint" class="mt-0 h-full"><ScrollArea class="h-full">
|
|
570
738
|
<p v-if="lintLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
|
|
571
739
|
<p v-else-if="lintIssues.length === 0" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
|
|
572
740
|
<ul v-else class="text-xs divide-y">
|
|
@@ -586,8 +754,8 @@ const stripeBg = {
|
|
|
586
754
|
</div>
|
|
587
755
|
</li>
|
|
588
756
|
</ul>
|
|
589
|
-
</TabsContent>
|
|
590
|
-
<TabsContent value="stats" class="mt-0">
|
|
757
|
+
</ScrollArea></TabsContent>
|
|
758
|
+
<TabsContent value="stats" class="mt-0 h-full"><ScrollArea class="h-full">
|
|
591
759
|
<p v-if="statsLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
|
|
592
760
|
<p v-else-if="!stats" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
|
|
593
761
|
<div v-else class="px-4 py-3 flex items-center gap-6 text-xs">
|
|
@@ -595,7 +763,7 @@ const stripeBg = {
|
|
|
595
763
|
<span class="text-gray-500 dark:text-gray-400">Size</span>
|
|
596
764
|
<span
|
|
597
765
|
class="font-medium tabular-nums"
|
|
598
|
-
:class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-
|
|
766
|
+
:class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'"
|
|
599
767
|
>{{ stats.size.formatted }}</span>
|
|
600
768
|
</div>
|
|
601
769
|
<div class="flex items-center gap-1.5">
|
|
@@ -607,7 +775,58 @@ const stripeBg = {
|
|
|
607
775
|
<span class="font-medium tabular-nums">{{ stats.links }}</span>
|
|
608
776
|
</div>
|
|
609
777
|
</div>
|
|
610
|
-
</TabsContent>
|
|
778
|
+
</ScrollArea></TabsContent>
|
|
779
|
+
<TabsContent value="test" class="mt-0 h-full"><ScrollArea class="h-full">
|
|
780
|
+
<div class="px-4 py-3 max-w-md">
|
|
781
|
+
<div class="space-y-2">
|
|
782
|
+
<div class="flex items-center gap-2">
|
|
783
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">To</label>
|
|
784
|
+
<TagsInput v-model="emailTo" delimiter=" " add-on-paste class="flex-1 min-h-7 gap-1 px-2 py-1">
|
|
785
|
+
<TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
|
|
786
|
+
<TagsInputItemText class="px-1.5 py-0 text-xs" />
|
|
787
|
+
<TagsInputItemDelete class="size-3.5" />
|
|
788
|
+
</TagsInputItem>
|
|
789
|
+
<TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
|
|
790
|
+
</TagsInput>
|
|
791
|
+
</div>
|
|
792
|
+
<div class="flex items-center gap-2">
|
|
793
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">Subject</label>
|
|
794
|
+
<div class="flex-1 flex items-center gap-3">
|
|
795
|
+
<Input v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
|
|
796
|
+
<label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
|
|
797
|
+
<Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
|
|
798
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
|
|
799
|
+
</label>
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
</div>
|
|
803
|
+
<div class="flex items-center gap-3 mt-3">
|
|
804
|
+
<Button
|
|
805
|
+
size="sm"
|
|
806
|
+
class="h-7 text-xs px-3"
|
|
807
|
+
:disabled="!emailTo.length || emailSending"
|
|
808
|
+
@click="sendTestEmail"
|
|
809
|
+
>
|
|
810
|
+
<svg v-if="emailSending" class="size-3.5 animate-spin [animation-duration:0.6s]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
|
|
811
|
+
{{ emailSending ? 'Sending' : 'Send' }}
|
|
812
|
+
</Button>
|
|
813
|
+
</div>
|
|
814
|
+
<div v-if="emailResult" class="mt-2">
|
|
815
|
+
<p class="text-xs" :class="emailResult.success ? 'text-gray-950 dark:text-white' : 'text-red-600'">
|
|
816
|
+
{{ emailResult.message }}
|
|
817
|
+
<a
|
|
818
|
+
v-if="emailResult.previewUrl"
|
|
819
|
+
:href="emailResult.previewUrl"
|
|
820
|
+
target="_blank"
|
|
821
|
+
rel="noopener"
|
|
822
|
+
class="inline-flex items-center gap-0.5 text-gray-500 dark:text-gray-400 hover:underline ml-1"
|
|
823
|
+
>
|
|
824
|
+
View <ExternalLink class="size-3" />
|
|
825
|
+
</a>
|
|
826
|
+
</p>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
</ScrollArea></TabsContent>
|
|
611
830
|
</div>
|
|
612
831
|
</Tabs>
|
|
613
832
|
</div>
|