@maizzle/framework 6.0.0-rc.12 → 6.0.0-rc.14
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/build.mjs +4 -1
- package/dist/build.mjs.map +1 -1
- package/dist/components/Button.vue +2 -2
- package/dist/components/CodeBlock.vue +2 -1
- package/dist/components/Column.vue +28 -22
- package/dist/components/Container.vue +47 -9
- package/dist/components/Font.vue +96 -0
- package/dist/components/Layout.vue +9 -4
- package/dist/components/Overlap.vue +75 -18
- package/dist/components/Row.vue +40 -19
- package/dist/components/Section.vue +35 -8
- package/dist/components/utils.d.mts +14 -1
- package/dist/components/utils.d.mts.map +1 -1
- package/dist/components/utils.mjs +32 -1
- package/dist/components/utils.mjs.map +1 -1
- package/dist/components/utils.ts +39 -0
- package/dist/composables/renderContext.d.mts +8 -1
- package/dist/composables/renderContext.d.mts.map +1 -1
- package/dist/composables/renderContext.mjs.map +1 -1
- package/dist/composables/useFont.d.mts +50 -0
- package/dist/composables/useFont.d.mts.map +1 -0
- package/dist/composables/useFont.mjs +93 -0
- package/dist/composables/useFont.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
- package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
- package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
- package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
- package/dist/render/createRenderer.mjs +8 -2
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/render/injectFonts.d.mts +15 -0
- package/dist/render/injectFonts.d.mts.map +1 -0
- package/dist/render/injectFonts.mjs +46 -0
- package/dist/render/injectFonts.mjs.map +1 -0
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +28 -12
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +54 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +890 -76
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/linter.d.mts +15 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +194 -43
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/sfc-utils.d.mts +18 -0
- package/dist/server/sfc-utils.d.mts.map +1 -0
- package/dist/server/sfc-utils.mjs +184 -0
- package/dist/server/sfc-utils.mjs.map +1 -0
- package/dist/server/ui/App.vue +27 -50
- package/dist/server/ui/components/SidebarClose.vue +12 -0
- package/dist/server/ui/components/ui/command/Command.vue +1 -0
- package/dist/server/ui/components/ui/input/Input.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
- package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
- package/dist/server/ui/pages/Preview.vue +215 -156
- package/dist/transformers/addAttributes.mjs +10 -6
- package/dist/transformers/addAttributes.mjs.map +1 -1
- package/dist/transformers/columnWidth.d.mts +31 -0
- package/dist/transformers/columnWidth.d.mts.map +1 -0
- package/dist/transformers/columnWidth.mjs +166 -0
- package/dist/transformers/columnWidth.mjs.map +1 -0
- package/dist/transformers/index.d.mts.map +1 -1
- package/dist/transformers/index.mjs +4 -0
- package/dist/transformers/index.mjs.map +1 -1
- package/dist/transformers/inlineCSS.mjs +2 -2
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/msoWidthFromClass.d.mts +19 -0
- package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
- package/dist/transformers/msoWidthFromClass.mjs +61 -0
- package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
- package/dist/transformers/purgeCSS.mjs +1 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.d.mts.map +1 -1
- package/dist/transformers/tailwindcss.mjs +6 -16
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +42 -2
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/utils/decodeStyleEntities.d.mts +15 -0
- package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
- package/dist/utils/decodeStyleEntities.mjs +18 -0
- package/dist/utils/decodeStyleEntities.mjs.map +1 -0
- package/package.json +2 -3
- package/dist/_virtual/_rolldown/runtime.mjs +0 -32
- package/dist/node_modules/picomatch/index.mjs +0 -13
- package/dist/node_modules/picomatch/index.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
- package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
- package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
- package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
- package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
- package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useRoute } from 'vue-router'
|
|
4
|
-
import { ChevronUp, ChevronDown, Check
|
|
4
|
+
import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
|
|
5
5
|
import {
|
|
6
6
|
DropdownMenu,
|
|
7
7
|
DropdownMenuContent,
|
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
TagsInputItemDelete,
|
|
21
21
|
TagsInputItemText,
|
|
22
22
|
} from '@/components/ui/tags-input'
|
|
23
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
24
23
|
|
|
25
24
|
import stripesUrl from '../stripes.svg'
|
|
25
|
+
import { applyColorInversion, undoColorInversion } from '@/lib/emulated-dark-mode'
|
|
26
26
|
|
|
27
27
|
interface Device {
|
|
28
28
|
name: string
|
|
@@ -43,6 +43,7 @@ const props = defineProps<{
|
|
|
43
43
|
}>()
|
|
44
44
|
|
|
45
45
|
const viewMode = defineModel<'preview' | 'source'>('viewMode', { default: 'preview' })
|
|
46
|
+
const darkMode = defineModel<boolean>('darkMode', { default: false })
|
|
46
47
|
|
|
47
48
|
const route = useRoute()
|
|
48
49
|
const srcdoc = ref('')
|
|
@@ -71,7 +72,9 @@ const iframeContentHeight = ref<number | null>(null)
|
|
|
71
72
|
function copySource() {
|
|
72
73
|
let text: string
|
|
73
74
|
if (sourceView.value === 'compiled') {
|
|
74
|
-
|
|
75
|
+
// `renderedHtml` holds the raw compiled HTML (srcdoc is only populated
|
|
76
|
+
// for the initial iframe load; subsequent renders use doc.write).
|
|
77
|
+
text = renderedHtml || srcdoc.value
|
|
75
78
|
} else if (sourceView.value === 'plaintext') {
|
|
76
79
|
text = plaintextContent.value
|
|
77
80
|
} else {
|
|
@@ -80,28 +83,59 @@ function copySource() {
|
|
|
80
83
|
text = el.textContent || ''
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
const item = new ClipboardItem({ 'text/plain': blob })
|
|
85
|
-
navigator.clipboard.write([item]).then(() => {
|
|
86
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
86
87
|
copied.value = true
|
|
87
88
|
setTimeout(() => { copied.value = false }, 2000)
|
|
89
|
+
}).catch((err) => {
|
|
90
|
+
console.error('Copy failed:', err)
|
|
88
91
|
})
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
interface
|
|
92
|
-
|
|
94
|
+
interface CheckIssue {
|
|
95
|
+
kind: 'compat' | 'lint'
|
|
96
|
+
slug?: string
|
|
93
97
|
title: string
|
|
94
|
-
category: string
|
|
95
|
-
clients: Array<{ name: string, notes: string[] }>
|
|
96
98
|
url?: string
|
|
99
|
+
category: string
|
|
97
100
|
line?: number
|
|
101
|
+
file: string
|
|
102
|
+
// compat-only
|
|
103
|
+
supportLevel?: 'unsupported' | 'mitigated' | 'unknown'
|
|
104
|
+
supportLabel?: string
|
|
105
|
+
affectedClients?: string[]
|
|
106
|
+
// lint-only
|
|
107
|
+
severity?: 'error' | 'warning'
|
|
108
|
+
message?: string
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
function supportPrefix(issue: CheckIssue): string {
|
|
112
|
+
if (issue.supportLevel === 'unsupported') return 'Not supported in'
|
|
113
|
+
if (issue.supportLevel === 'mitigated') return 'Partial support in'
|
|
114
|
+
return 'Support unknown in'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Split a message on backtick-delimited code spans. Returns alternating
|
|
119
|
+
* { text } and { code } segments so the template can render <code> inline
|
|
120
|
+
* without needing v-html.
|
|
121
|
+
*/
|
|
122
|
+
function messageSegments(raw: string | undefined): Array<{ code: boolean, text: string }> {
|
|
123
|
+
if (!raw) return []
|
|
124
|
+
const out: Array<{ code: boolean, text: string }> = []
|
|
125
|
+
const parts = raw.split('`')
|
|
126
|
+
for (let i = 0; i < parts.length; i++) {
|
|
127
|
+
if (parts[i]) out.push({ code: i % 2 === 1, text: parts[i] })
|
|
128
|
+
}
|
|
129
|
+
return out
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function issueColorClass(issue: CheckIssue): string {
|
|
133
|
+
if (issue.kind === 'lint') {
|
|
134
|
+
return issue.severity === 'error' ? 'text-rose-600' : 'text-amber-600'
|
|
135
|
+
}
|
|
136
|
+
if (issue.supportLevel === 'unsupported') return 'text-rose-600'
|
|
137
|
+
if (issue.supportLevel === 'mitigated') return 'text-amber-600'
|
|
138
|
+
return 'text-gray-500 dark:text-gray-400'
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
interface TemplateStats {
|
|
@@ -110,10 +144,16 @@ interface TemplateStats {
|
|
|
110
144
|
links: number
|
|
111
145
|
}
|
|
112
146
|
|
|
113
|
-
const compatibilityIssues = ref<
|
|
147
|
+
const compatibilityIssues = ref<CheckIssue[]>([])
|
|
114
148
|
const compatibilityLoading = ref(false)
|
|
115
149
|
const compatibilityError = ref('')
|
|
116
150
|
const compatibilityCategory = ref('')
|
|
151
|
+
// Injected by serveDevUI into index.html — synchronous, available before
|
|
152
|
+
// any HTTP calls, so the Checks tab never flashes in when disabled.
|
|
153
|
+
const checksConfig = (window as any).__MAIZZLE_CONFIG__?.checks
|
|
154
|
+
const compatibilityDisabled = ref(checksConfig === false)
|
|
155
|
+
const expandedIssueKeys = ref(new Set<string>())
|
|
156
|
+
const issueKey = (issue: CheckIssue, i: number): string => `${issue.file}|${issue.line ?? 0}|${issue.slug ?? issue.title}|${i}`
|
|
117
157
|
const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
|
|
118
158
|
const activeCompatibilityCategories = computed(() =>
|
|
119
159
|
compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
|
|
@@ -122,8 +162,6 @@ const filteredCompatibilityIssues = computed(() => {
|
|
|
122
162
|
if (!compatibilityCategory.value) return compatibilityIssues.value
|
|
123
163
|
return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
|
|
124
164
|
})
|
|
125
|
-
const lintIssues = ref<LintIssue[]>([])
|
|
126
|
-
const lintLoading = ref(false)
|
|
127
165
|
const stats = ref<TemplateStats | null>(null)
|
|
128
166
|
const statsLoading = ref(false)
|
|
129
167
|
|
|
@@ -196,6 +234,19 @@ function updateIframeContentHeight() {
|
|
|
196
234
|
}
|
|
197
235
|
}
|
|
198
236
|
|
|
237
|
+
function onIframeLoad() {
|
|
238
|
+
updateIframeContentHeight()
|
|
239
|
+
const iframe = iframeEl.value
|
|
240
|
+
if (darkMode.value && iframe) applyColorInversion(iframe)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
watch(darkMode, (on) => {
|
|
244
|
+
const iframe = iframeEl.value
|
|
245
|
+
if (!iframe) return
|
|
246
|
+
if (on) applyColorInversion(iframe)
|
|
247
|
+
else undoColorInversion(iframe)
|
|
248
|
+
})
|
|
249
|
+
|
|
199
250
|
async function fetchTemplate() {
|
|
200
251
|
const res = await fetch(`/__maizzle/render/${route.params.template}`)
|
|
201
252
|
renderedHtml = await res.text()
|
|
@@ -211,6 +262,7 @@ async function fetchTemplate() {
|
|
|
211
262
|
doc.close()
|
|
212
263
|
// Hide iframe body overflow — scrolling is handled by the outer ScrollArea
|
|
213
264
|
if (doc.body) doc.body.style.overflow = 'hidden'
|
|
265
|
+
if (darkMode.value && iframe) applyColorInversion(iframe)
|
|
214
266
|
await nextTick()
|
|
215
267
|
updateIframeContentHeight()
|
|
216
268
|
} else {
|
|
@@ -247,22 +299,30 @@ async function fetchStats() {
|
|
|
247
299
|
}
|
|
248
300
|
|
|
249
301
|
async function fetchCompatibility() {
|
|
302
|
+
if (compatibilityDisabled.value) return
|
|
303
|
+
const template = props.templates?.find(t => t.href === '/' + route.params.template)
|
|
304
|
+
if (!template) return
|
|
305
|
+
|
|
250
306
|
compatibilityLoading.value = true
|
|
251
307
|
compatibilityError.value = ''
|
|
252
308
|
try {
|
|
253
|
-
const res = await fetch(
|
|
254
|
-
method: 'POST',
|
|
255
|
-
body: renderedHtml,
|
|
256
|
-
})
|
|
309
|
+
const res = await fetch(`/__maizzle/compatibility/${template.path}`)
|
|
257
310
|
const data = await res.json()
|
|
258
|
-
if (data?.error) {
|
|
311
|
+
if (!Array.isArray(data) && data?.error) {
|
|
259
312
|
compatibilityError.value = data.error
|
|
260
313
|
compatibilityIssues.value = []
|
|
261
314
|
} else {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
315
|
+
const issues: CheckIssue[] = Array.isArray(data) ? data : []
|
|
316
|
+
compatibilityIssues.value = issues
|
|
317
|
+
// Keep the current category if it still has issues; otherwise fall
|
|
318
|
+
// back to the first category that does. Prevents a "refresh" during
|
|
319
|
+
// edits from snapping back to CSS when the user is on HTML/Image.
|
|
320
|
+
const current = compatibilityCategory.value
|
|
321
|
+
const currentStillActive = current && issues.some((i) => i.category === current)
|
|
322
|
+
if (!currentStillActive) {
|
|
323
|
+
const firstCat = compatibilityCategories.find(cat => issues.some((i) => i.category === cat))
|
|
324
|
+
compatibilityCategory.value = firstCat || ''
|
|
325
|
+
}
|
|
266
326
|
}
|
|
267
327
|
} catch {
|
|
268
328
|
compatibilityIssues.value = []
|
|
@@ -271,19 +331,21 @@ async function fetchCompatibility() {
|
|
|
271
331
|
}
|
|
272
332
|
}
|
|
273
333
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
334
|
+
/** Check if an issue is from the currently viewed template file */
|
|
335
|
+
function isCurrentFile(issue: { file: string }): boolean {
|
|
336
|
+
const template = props.templates?.find(t => t.href === '/' + route.params.template)
|
|
337
|
+
if (!template) return true
|
|
338
|
+
return issue.file.endsWith(template.path)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Get a short display name for a component file path */
|
|
342
|
+
function componentName(filePath: string): string {
|
|
343
|
+
const parts = filePath.replace(/\\/g, '/').split('/')
|
|
344
|
+
return parts[parts.length - 1]?.replace(/\.vue$/, '') ?? filePath
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function openInEditor(file: string, line: number) {
|
|
348
|
+
fetch(`/__open-in-editor?file=${encodeURIComponent(file + ':' + line)}`)
|
|
287
349
|
}
|
|
288
350
|
|
|
289
351
|
watch(() => route.params.template, () => {
|
|
@@ -292,17 +354,23 @@ watch(() => route.params.template, () => {
|
|
|
292
354
|
plaintextContent.value = ''
|
|
293
355
|
compatibilityIssues.value = []
|
|
294
356
|
compatibilityError.value = ''
|
|
295
|
-
lintIssues.value = []
|
|
296
357
|
stats.value = null
|
|
297
358
|
emailResult.value = null
|
|
298
359
|
sourceView.value = 'compiled'
|
|
299
|
-
fetchTemplate()
|
|
300
|
-
|
|
360
|
+
fetchTemplate()
|
|
361
|
+
fetchCompatibility()
|
|
301
362
|
fetchStats()
|
|
302
363
|
fetchEmailConfig()
|
|
303
364
|
if (viewMode.value === 'source') fetchSource()
|
|
304
365
|
}, { immediate: true })
|
|
305
366
|
|
|
367
|
+
// Templates list loads async from App.vue — re-trigger once available
|
|
368
|
+
watch(() => props.templates, (templates) => {
|
|
369
|
+
if (templates?.length && !compatibilityIssues.value.length && !compatibilityLoading.value) {
|
|
370
|
+
fetchCompatibility()
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
|
|
306
374
|
watch(viewMode, (mode) => {
|
|
307
375
|
if (mode === 'source') {
|
|
308
376
|
if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
|
|
@@ -319,8 +387,8 @@ watch(sourceView, (view) => {
|
|
|
319
387
|
|
|
320
388
|
if ((import.meta as any).hot) {
|
|
321
389
|
;(import.meta as any).hot.on('maizzle:template-updated', () => {
|
|
322
|
-
fetchTemplate()
|
|
323
|
-
|
|
390
|
+
fetchTemplate()
|
|
391
|
+
fetchCompatibility()
|
|
324
392
|
fetchStats()
|
|
325
393
|
|
|
326
394
|
// Always clear all source views so they re-fetch when switched to
|
|
@@ -335,33 +403,22 @@ if ((import.meta as any).hot) {
|
|
|
335
403
|
if (sourceView.value === 'plaintext') fetchPlaintext()
|
|
336
404
|
}
|
|
337
405
|
})
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
async function goToLine(line: number) {
|
|
342
|
-
// Switch to source view showing Vue source
|
|
343
|
-
viewMode.value = 'source'
|
|
344
|
-
sourceView.value = 'vue'
|
|
345
|
-
|
|
346
|
-
// Ensure vue source is loaded
|
|
347
|
-
if (!vueSourceHtml.value) {
|
|
348
|
-
await fetchVueSource()
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
await nextTick()
|
|
352
|
-
|
|
353
|
-
const el = vueSourceEl.value
|
|
354
|
-
if (!el) return
|
|
355
|
-
|
|
356
|
-
// Remove previous highlight
|
|
357
|
-
el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
|
|
358
406
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
407
|
+
// Keep the UI in sync with live config edits. Payload is the same shape
|
|
408
|
+
// as the initial `window.__MAIZZLE_CONFIG__` inject — we replace it and
|
|
409
|
+
// derive per-feature flags from there.
|
|
410
|
+
;(import.meta as any).hot.on('maizzle:config-updated', (data: Record<string, unknown>) => {
|
|
411
|
+
;(window as any).__MAIZZLE_CONFIG__ = data
|
|
412
|
+
const wasDisabled = compatibilityDisabled.value
|
|
413
|
+
const nowDisabled = data?.checks === false
|
|
414
|
+
compatibilityDisabled.value = nowDisabled
|
|
415
|
+
if (nowDisabled) {
|
|
416
|
+
compatibilityIssues.value = []
|
|
417
|
+
if (activeTab.value === 'compatibility') activeTab.value = 'stats'
|
|
418
|
+
} else if (wasDisabled) {
|
|
419
|
+
fetchCompatibility()
|
|
420
|
+
}
|
|
421
|
+
})
|
|
365
422
|
}
|
|
366
423
|
|
|
367
424
|
async function goToCompiledLine(line: number) {
|
|
@@ -501,6 +558,7 @@ function forwardIframeKeys(iframe: HTMLIFrameElement) {
|
|
|
501
558
|
metaKey: e.metaKey,
|
|
502
559
|
shiftKey: e.shiftKey,
|
|
503
560
|
altKey: e.altKey,
|
|
561
|
+
bubbles: true,
|
|
504
562
|
}))
|
|
505
563
|
})
|
|
506
564
|
} catch {}
|
|
@@ -536,11 +594,13 @@ const bottomPanelOpen = ref(false)
|
|
|
536
594
|
const tabsPanelHeight = ref(40)
|
|
537
595
|
const activeTab = ref<string | undefined>(undefined)
|
|
538
596
|
|
|
597
|
+
const defaultTab = () => compatibilityDisabled.value ? 'stats' : 'compatibility'
|
|
598
|
+
|
|
539
599
|
function toggleBottomPanel() {
|
|
540
600
|
bottomPanelOpen.value = !bottomPanelOpen.value
|
|
541
601
|
if (bottomPanelOpen.value) {
|
|
542
602
|
tabsPanelHeight.value = 300
|
|
543
|
-
if (!activeTab.value) activeTab.value =
|
|
603
|
+
if (!activeTab.value) activeTab.value = defaultTab()
|
|
544
604
|
} else {
|
|
545
605
|
tabsPanelHeight.value = 40
|
|
546
606
|
activeTab.value = undefined
|
|
@@ -563,35 +623,39 @@ function onTabClick(tab: string) {
|
|
|
563
623
|
|
|
564
624
|
const tabsDragging = ref(false)
|
|
565
625
|
|
|
566
|
-
function onTabsDragStart(e: MouseEvent) {
|
|
626
|
+
function onTabsDragStart(e: MouseEvent | TouchEvent) {
|
|
567
627
|
e.preventDefault()
|
|
568
628
|
tabsDragging.value = true
|
|
569
|
-
const
|
|
629
|
+
const isTouch = e.type === 'touchstart'
|
|
630
|
+
const startY = isTouch ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
|
570
631
|
const startHeight = tabsPanelHeight.value
|
|
571
632
|
|
|
572
633
|
const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
|
|
573
634
|
const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
|
|
574
635
|
|
|
575
|
-
const
|
|
576
|
-
const
|
|
636
|
+
const onMove = (e: MouseEvent | TouchEvent) => {
|
|
637
|
+
const clientY = e.type === 'touchmove' ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
|
638
|
+
const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - clientY))
|
|
577
639
|
tabsPanelHeight.value = newHeight
|
|
578
640
|
bottomPanelOpen.value = newHeight > 40
|
|
579
641
|
|
|
580
642
|
if (!bottomPanelOpen.value) {
|
|
581
643
|
activeTab.value = undefined
|
|
582
644
|
} else if (!activeTab.value) {
|
|
583
|
-
activeTab.value =
|
|
645
|
+
activeTab.value = defaultTab()
|
|
584
646
|
}
|
|
585
647
|
}
|
|
586
648
|
|
|
587
|
-
const
|
|
649
|
+
const onEnd = () => {
|
|
588
650
|
tabsDragging.value = false
|
|
589
|
-
document.removeEventListener('mousemove',
|
|
590
|
-
document.removeEventListener('mouseup',
|
|
651
|
+
document.removeEventListener('mousemove', onMove)
|
|
652
|
+
document.removeEventListener('mouseup', onEnd)
|
|
653
|
+
document.removeEventListener('touchmove', onMove)
|
|
654
|
+
document.removeEventListener('touchend', onEnd)
|
|
591
655
|
}
|
|
592
656
|
|
|
593
|
-
document.addEventListener('mousemove',
|
|
594
|
-
document.addEventListener('mouseup',
|
|
657
|
+
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onMove)
|
|
658
|
+
document.addEventListener(isTouch ? 'touchend' : 'mouseup', onEnd)
|
|
595
659
|
}
|
|
596
660
|
|
|
597
661
|
const stripeBg = {
|
|
@@ -700,7 +764,7 @@ const stripeBg = {
|
|
|
700
764
|
<iframe
|
|
701
765
|
ref="iframeEl"
|
|
702
766
|
:srcdoc="srcdoc"
|
|
703
|
-
@load="
|
|
767
|
+
@load="onIframeLoad"
|
|
704
768
|
class="w-full border-0 bg-white dark:bg-gray-950"
|
|
705
769
|
:style="{ height: iframeContentHeight ? `${iframeContentHeight}px` : '100%' }"
|
|
706
770
|
/>
|
|
@@ -722,20 +786,18 @@ const stripeBg = {
|
|
|
722
786
|
<div
|
|
723
787
|
class="relative h-0 cursor-row-resize before:absolute before:top-0 before:left-0 before:right-0 before:h-3.25 before:content-['']"
|
|
724
788
|
@mousedown="onTabsDragStart"
|
|
789
|
+
@touchstart.prevent="onTabsDragStart"
|
|
725
790
|
/>
|
|
726
791
|
<Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
|
|
727
792
|
<div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
|
|
728
793
|
<TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
|
|
729
|
-
<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')">
|
|
730
|
-
|
|
731
|
-
</TabsTrigger>
|
|
732
|
-
<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')">
|
|
733
|
-
Linter
|
|
794
|
+
<TabsTrigger v-if="!compatibilityDisabled" 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 dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('compatibility')">
|
|
795
|
+
Checks
|
|
734
796
|
</TabsTrigger>
|
|
735
|
-
<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')">
|
|
797
|
+
<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 dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('stats')">
|
|
736
798
|
Stats
|
|
737
799
|
</TabsTrigger>
|
|
738
|
-
<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')">
|
|
800
|
+
<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 dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('test')">
|
|
739
801
|
Test
|
|
740
802
|
</TabsTrigger>
|
|
741
803
|
</TabsList>
|
|
@@ -749,7 +811,7 @@ const stripeBg = {
|
|
|
749
811
|
<button
|
|
750
812
|
v-for="cat in activeCompatibilityCategories"
|
|
751
813
|
:key="cat"
|
|
752
|
-
class="px-2 py-0.5 text-[11px] rounded-full cursor-
|
|
814
|
+
class="px-2 py-0.5 text-[11px] rounded-full cursor-default transition-colors"
|
|
753
815
|
:class="compatibilityCategory === cat
|
|
754
816
|
? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
|
|
755
817
|
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
|
|
@@ -760,48 +822,45 @@ const stripeBg = {
|
|
|
760
822
|
</button>
|
|
761
823
|
</div>
|
|
762
824
|
<ScrollArea class="h-full flex-1 min-h-0 pl-5">
|
|
763
|
-
<p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">
|
|
825
|
+
<p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Running checks...</p>
|
|
764
826
|
<p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
|
|
765
|
-
<p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No
|
|
827
|
+
<p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
|
|
766
828
|
<ul v-else class="text-xs divide-y">
|
|
767
829
|
<li
|
|
768
830
|
v-for="(issue, i) in filteredCompatibilityIssues"
|
|
769
831
|
:key="i"
|
|
770
|
-
class="pr-4 py-
|
|
832
|
+
class="pr-4 py-2"
|
|
771
833
|
>
|
|
772
|
-
<div class="flex items-center gap-
|
|
773
|
-
<a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
|
|
774
|
-
{{ issue.title }}
|
|
775
|
-
</a>
|
|
776
|
-
<span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
|
|
777
|
-
{{ issue.title }}
|
|
778
|
-
</span>
|
|
779
|
-
<span class="text-gray-400 dark:text-gray-600 shrink-0">·</span>
|
|
780
|
-
<span class="text-gray-500 dark:text-gray-400 truncate">{{ issue.type === 'error' ? 'Not supported' : 'Partial support' }} in {{ issue.clients.map((c: any) => c.name).join(', ') }}</span>
|
|
781
|
-
<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 ml-auto" @click="goToCompiledLine(issue.line!)">L{{ issue.line }}</button>
|
|
782
|
-
</div>
|
|
783
|
-
</li>
|
|
784
|
-
</ul>
|
|
785
|
-
</ScrollArea>
|
|
786
|
-
</TabsContent>
|
|
787
|
-
<TabsContent value="lint" class="mt-0 h-full">
|
|
788
|
-
<ScrollArea class="h-full pl-5">
|
|
789
|
-
<p v-if="lintLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
|
|
790
|
-
<p v-else-if="lintIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
|
|
791
|
-
<ul v-else class="text-xs divide-y">
|
|
792
|
-
<li
|
|
793
|
-
v-for="(issue, i) in lintIssues"
|
|
794
|
-
:key="i"
|
|
795
|
-
class="pr-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
|
|
796
|
-
>
|
|
797
|
-
<div class="flex items-start justify-between gap-4">
|
|
834
|
+
<div class="flex items-center justify-between gap-4">
|
|
798
835
|
<div>
|
|
799
|
-
<
|
|
836
|
+
<a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issueColorClass(issue)">
|
|
837
|
+
{{ issue.title }}
|
|
838
|
+
</a>
|
|
839
|
+
<span v-else class="font-medium" :class="issueColorClass(issue)">
|
|
800
840
|
{{ issue.title }}
|
|
801
841
|
</span>
|
|
802
|
-
<div class="text-gray-500 dark:text-gray-400 mt-0.5">
|
|
842
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">
|
|
843
|
+
<template v-if="issue.kind === 'lint'">
|
|
844
|
+
<template v-for="(seg, j) in messageSegments(issue.message)" :key="j">
|
|
845
|
+
<code v-if="seg.code" class="px-1 py-0.5 rounded bg-gray-100 dark:bg-white/10 font-mono text-[11px]">{{ seg.text }}</code>
|
|
846
|
+
<template v-else>{{ seg.text }}</template>
|
|
847
|
+
</template>
|
|
848
|
+
</template>
|
|
849
|
+
<template v-else>
|
|
850
|
+
{{ supportPrefix(issue) }}
|
|
851
|
+
<template v-if="(issue.affectedClients?.length ?? 0) <= 4 || expandedIssueKeys.has(issueKey(issue, i))">
|
|
852
|
+
{{ (issue.affectedClients ?? []).join(', ') }}
|
|
853
|
+
</template>
|
|
854
|
+
<template v-else>
|
|
855
|
+
{{ issue.affectedClients!.slice(0, 4).join(', ') }}
|
|
856
|
+
<button class="underline cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" @click="expandedIssueKeys.add(issueKey(issue, i)); expandedIssueKeys = new Set(expandedIssueKeys)">
|
|
857
|
+
+ {{ issue.affectedClients!.length - 4 }} others
|
|
858
|
+
</button>
|
|
859
|
+
</template>
|
|
860
|
+
</template>
|
|
861
|
+
</div>
|
|
803
862
|
</div>
|
|
804
|
-
<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="
|
|
863
|
+
<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="openInEditor(issue.file, issue.line!)">{{ isCurrentFile(issue) ? `L${issue.line}` : `${componentName(issue.file)}:${issue.line}` }}</button>
|
|
805
864
|
</div>
|
|
806
865
|
</li>
|
|
807
866
|
</ul>
|
|
@@ -811,35 +870,35 @@ const stripeBg = {
|
|
|
811
870
|
<ScrollArea class="h-full pl-5">
|
|
812
871
|
<p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
|
|
813
872
|
<p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
|
|
814
|
-
<
|
|
815
|
-
<
|
|
816
|
-
<
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
</
|
|
828
|
-
<
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
</
|
|
842
|
-
</
|
|
873
|
+
<ul v-else class="text-xs divide-y divide-gray-200 dark:divide-white/10">
|
|
874
|
+
<li class="pr-4 py-2">
|
|
875
|
+
<div class="flex items-center justify-between gap-4">
|
|
876
|
+
<div>
|
|
877
|
+
<span class="font-medium" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">Size</span>
|
|
878
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Compiled HTML size. Gmail clips emails larger than ~100KB.</div>
|
|
879
|
+
</div>
|
|
880
|
+
<span class="font-medium tabular-nums shrink-0" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">{{ stats.size.formatted }}</span>
|
|
881
|
+
</div>
|
|
882
|
+
</li>
|
|
883
|
+
<li class="pr-4 py-2">
|
|
884
|
+
<div class="flex items-center justify-between gap-4">
|
|
885
|
+
<div>
|
|
886
|
+
<span class="font-medium text-gray-900 dark:text-gray-300">Images</span>
|
|
887
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Total from <img> tags and CSS background images.</div>
|
|
888
|
+
</div>
|
|
889
|
+
<span class="font-medium tabular-nums shrink-0">{{ stats.images }}</span>
|
|
890
|
+
</div>
|
|
891
|
+
</li>
|
|
892
|
+
<li class="pr-4 py-2">
|
|
893
|
+
<div class="flex items-center justify-between gap-4">
|
|
894
|
+
<div>
|
|
895
|
+
<span class="font-medium text-gray-900 dark:text-gray-300">Links</span>
|
|
896
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Total <a> tags with an href attribute.</div>
|
|
897
|
+
</div>
|
|
898
|
+
<span class="font-medium tabular-nums shrink-0">{{ stats.links }}</span>
|
|
899
|
+
</div>
|
|
900
|
+
</li>
|
|
901
|
+
</ul>
|
|
843
902
|
</ScrollArea>
|
|
844
903
|
</TabsContent>
|
|
845
904
|
<TabsContent value="test" class="mt-0 h-full">
|
|
@@ -847,19 +906,19 @@ const stripeBg = {
|
|
|
847
906
|
<div class="pr-4 py-3 max-w-md">
|
|
848
907
|
<div class="space-y-2">
|
|
849
908
|
<div class="flex items-center gap-2">
|
|
850
|
-
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">To</label>
|
|
909
|
+
<label for="email-to" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">To</label>
|
|
851
910
|
<TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
|
|
852
911
|
<TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
|
|
853
912
|
<TagsInputItemText class="px-1.5 py-0 text-xs" />
|
|
854
913
|
<TagsInputItemDelete class="size-3.5" />
|
|
855
914
|
</TagsInputItem>
|
|
856
|
-
<TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
|
|
915
|
+
<TagsInputInput id="email-to" class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
|
|
857
916
|
</TagsInput>
|
|
858
917
|
</div>
|
|
859
918
|
<div class="flex items-center gap-2">
|
|
860
|
-
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">Subject</label>
|
|
919
|
+
<label for="email-subject" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">Subject</label>
|
|
861
920
|
<div class="flex-1 flex items-center gap-3">
|
|
862
|
-
<Input v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
|
|
921
|
+
<Input id="email-subject" v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
|
|
863
922
|
<label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
|
|
864
923
|
<Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
|
|
865
924
|
<span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
|
|
@@ -44,18 +44,22 @@ function addAttributes(dom, config = {}) {
|
|
|
44
44
|
const attributesToAdd = defu(typeof addConfig === "object" ? addConfig : {}, DEFAULT_ATTRIBUTES);
|
|
45
45
|
if (Object.keys(attributesToAdd).length === 0) return dom;
|
|
46
46
|
for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {
|
|
47
|
+
if (attributes === false) continue;
|
|
47
48
|
const selectors = selectorPattern.split(",").map((s) => s.trim());
|
|
48
49
|
walk(dom, (node) => {
|
|
49
50
|
const el = node;
|
|
50
51
|
if (!el.name) return;
|
|
51
52
|
if (selectors.some((selector) => elementMatches(el, selector))) {
|
|
52
53
|
if (!el.attribs) el.attribs = {};
|
|
53
|
-
for (const [attrName, attrValue] of Object.entries(attributes))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
for (const [attrName, attrValue] of Object.entries(attributes)) {
|
|
55
|
+
if (attrValue === false) continue;
|
|
56
|
+
if (attrName === "class" && el.attribs.class) {
|
|
57
|
+
const existingClasses = el.attribs.class.split(/\s+/).filter(Boolean);
|
|
58
|
+
const newClasses = String(attrValue).split(/\s+/).filter(Boolean);
|
|
59
|
+
const mergedClasses = [...new Set([...existingClasses, ...newClasses])];
|
|
60
|
+
if (mergedClasses.join(" ") !== el.attribs.class) el.attribs.class = mergedClasses.join(" ");
|
|
61
|
+
} else if (!(attrName in el.attribs)) el.attribs[attrName] = String(attrValue);
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
});
|
|
61
65
|
}
|