@maizzle/framework 6.0.0-rc.11 → 6.0.0-rc.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/build.mjs +4 -1
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/serve.d.mts.map +1 -1
  4. package/dist/serve.mjs +23 -11
  5. package/dist/serve.mjs.map +1 -1
  6. package/dist/server/compatibility.d.mts +54 -2
  7. package/dist/server/compatibility.d.mts.map +1 -1
  8. package/dist/server/compatibility.mjs +890 -76
  9. package/dist/server/compatibility.mjs.map +1 -1
  10. package/dist/server/linter.d.mts +15 -2
  11. package/dist/server/linter.d.mts.map +1 -1
  12. package/dist/server/linter.mjs +194 -43
  13. package/dist/server/linter.mjs.map +1 -1
  14. package/dist/server/sfc-utils.d.mts +18 -0
  15. package/dist/server/sfc-utils.d.mts.map +1 -0
  16. package/dist/server/sfc-utils.mjs +184 -0
  17. package/dist/server/sfc-utils.mjs.map +1 -0
  18. package/dist/server/ui/App.vue +4 -41
  19. package/dist/server/ui/components/SidebarClose.vue +12 -0
  20. package/dist/server/ui/components/ui/command/Command.vue +1 -0
  21. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  22. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  23. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
  24. package/dist/server/ui/pages/Preview.vue +194 -151
  25. package/dist/transformers/addAttributes.mjs +10 -6
  26. package/dist/transformers/addAttributes.mjs.map +1 -1
  27. package/dist/transformers/inlineCSS.mjs +2 -2
  28. package/dist/transformers/inlineCSS.mjs.map +1 -1
  29. package/dist/transformers/purgeCSS.mjs +1 -1
  30. package/dist/transformers/purgeCSS.mjs.map +1 -1
  31. package/dist/transformers/tailwindcss.mjs +2 -4
  32. package/dist/transformers/tailwindcss.mjs.map +1 -1
  33. package/dist/types/config.d.mts +42 -2
  34. package/dist/types/config.d.mts.map +1 -1
  35. package/dist/types/index.d.mts +2 -2
  36. package/package.json +1 -3
  37. package/dist/_virtual/_rolldown/runtime.mjs +0 -32
  38. package/dist/node_modules/picomatch/index.mjs +0 -13
  39. package/dist/node_modules/picomatch/index.mjs.map +0 -1
  40. package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
  41. package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
  42. package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
  43. package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
  44. package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
  45. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
  46. package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
  47. package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
  48. package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
  49. 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, Info } from 'lucide-vue-next'
4
+ import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
5
5
  import {
6
6
  DropdownMenu,
7
7
  DropdownMenuContent,
@@ -20,7 +20,6 @@ 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'
26
25
 
@@ -88,20 +87,51 @@ function copySource() {
88
87
  })
89
88
  }
90
89
 
91
- interface CompatibilityIssue {
92
- type: 'error' | 'warning'
90
+ interface CheckIssue {
91
+ kind: 'compat' | 'lint'
92
+ slug?: string
93
93
  title: string
94
- category: string
95
- clients: Array<{ name: string, notes: string[] }>
96
94
  url?: string
95
+ category: string
97
96
  line?: number
97
+ file: string
98
+ // compat-only
99
+ supportLevel?: 'unsupported' | 'mitigated' | 'unknown'
100
+ supportLabel?: string
101
+ affectedClients?: string[]
102
+ // lint-only
103
+ severity?: 'error' | 'warning'
104
+ message?: string
98
105
  }
99
106
 
100
- interface LintIssue {
101
- type: 'error' | 'warning'
102
- title: string
103
- message: string
104
- line?: number
107
+ function supportPrefix(issue: CheckIssue): string {
108
+ if (issue.supportLevel === 'unsupported') return 'Not supported in'
109
+ if (issue.supportLevel === 'mitigated') return 'Partial support in'
110
+ return 'Support unknown in'
111
+ }
112
+
113
+ /**
114
+ * Split a message on backtick-delimited code spans. Returns alternating
115
+ * { text } and { code } segments so the template can render <code> inline
116
+ * without needing v-html.
117
+ */
118
+ function messageSegments(raw: string | undefined): Array<{ code: boolean, text: string }> {
119
+ if (!raw) return []
120
+ const out: Array<{ code: boolean, text: string }> = []
121
+ const parts = raw.split('`')
122
+ for (let i = 0; i < parts.length; i++) {
123
+ if (parts[i]) out.push({ code: i % 2 === 1, text: parts[i] })
124
+ }
125
+ return out
126
+ }
127
+
128
+ function issueColorClass(issue: CheckIssue): string {
129
+ if (issue.kind === 'lint') {
130
+ return issue.severity === 'error' ? 'text-rose-600' : 'text-amber-600'
131
+ }
132
+ if (issue.supportLevel === 'unsupported') return 'text-rose-600'
133
+ if (issue.supportLevel === 'mitigated') return 'text-amber-600'
134
+ return 'text-gray-500 dark:text-gray-400'
105
135
  }
106
136
 
107
137
  interface TemplateStats {
@@ -110,10 +140,16 @@ interface TemplateStats {
110
140
  links: number
111
141
  }
112
142
 
113
- const compatibilityIssues = ref<CompatibilityIssue[]>([])
143
+ const compatibilityIssues = ref<CheckIssue[]>([])
114
144
  const compatibilityLoading = ref(false)
115
145
  const compatibilityError = ref('')
116
146
  const compatibilityCategory = ref('')
147
+ // Injected by serveDevUI into index.html — synchronous, available before
148
+ // any HTTP calls, so the Checks tab never flashes in when disabled.
149
+ const checksConfig = (window as any).__MAIZZLE_CONFIG__?.checks
150
+ const compatibilityDisabled = ref(checksConfig === false)
151
+ const expandedIssueKeys = ref(new Set<string>())
152
+ const issueKey = (issue: CheckIssue, i: number): string => `${issue.file}|${issue.line ?? 0}|${issue.slug ?? issue.title}|${i}`
117
153
  const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
118
154
  const activeCompatibilityCategories = computed(() =>
119
155
  compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
@@ -122,8 +158,6 @@ const filteredCompatibilityIssues = computed(() => {
122
158
  if (!compatibilityCategory.value) return compatibilityIssues.value
123
159
  return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
124
160
  })
125
- const lintIssues = ref<LintIssue[]>([])
126
- const lintLoading = ref(false)
127
161
  const stats = ref<TemplateStats | null>(null)
128
162
  const statsLoading = ref(false)
129
163
 
@@ -247,22 +281,30 @@ async function fetchStats() {
247
281
  }
248
282
 
249
283
  async function fetchCompatibility() {
284
+ if (compatibilityDisabled.value) return
285
+ const template = props.templates?.find(t => t.href === '/' + route.params.template)
286
+ if (!template) return
287
+
250
288
  compatibilityLoading.value = true
251
289
  compatibilityError.value = ''
252
290
  try {
253
- const res = await fetch('/__maizzle/compatibility', {
254
- method: 'POST',
255
- body: renderedHtml,
256
- })
291
+ const res = await fetch(`/__maizzle/compatibility/${template.path}`)
257
292
  const data = await res.json()
258
- if (data?.error) {
293
+ if (!Array.isArray(data) && data?.error) {
259
294
  compatibilityError.value = data.error
260
295
  compatibilityIssues.value = []
261
296
  } else {
262
- compatibilityIssues.value = data
263
- // Default to first category that has issues
264
- const firstCat = compatibilityCategories.find(cat => data.some((i: CompatibilityIssue) => i.category === cat))
265
- compatibilityCategory.value = firstCat || ''
297
+ const issues: CheckIssue[] = Array.isArray(data) ? data : []
298
+ compatibilityIssues.value = issues
299
+ // Keep the current category if it still has issues; otherwise fall
300
+ // back to the first category that does. Prevents a "refresh" during
301
+ // edits from snapping back to CSS when the user is on HTML/Image.
302
+ const current = compatibilityCategory.value
303
+ const currentStillActive = current && issues.some((i) => i.category === current)
304
+ if (!currentStillActive) {
305
+ const firstCat = compatibilityCategories.find(cat => issues.some((i) => i.category === cat))
306
+ compatibilityCategory.value = firstCat || ''
307
+ }
266
308
  }
267
309
  } catch {
268
310
  compatibilityIssues.value = []
@@ -271,19 +313,21 @@ async function fetchCompatibility() {
271
313
  }
272
314
  }
273
315
 
274
- async function fetchLint() {
275
- lintLoading.value = true
276
- try {
277
- const template = props.templates?.find(t => t.href === '/' + route.params.template)
278
- const filePath = template?.path ?? route.params.template
279
- const res = await fetch(`/__maizzle/lint/${filePath}`)
280
- const data = await res.json()
281
- lintIssues.value = Array.isArray(data) ? data.filter((i: LintIssue) => i.title) : []
282
- } catch {
283
- lintIssues.value = []
284
- } finally {
285
- lintLoading.value = false
286
- }
316
+ /** Check if an issue is from the currently viewed template file */
317
+ function isCurrentFile(issue: { file: string }): boolean {
318
+ const template = props.templates?.find(t => t.href === '/' + route.params.template)
319
+ if (!template) return true
320
+ return issue.file.endsWith(template.path)
321
+ }
322
+
323
+ /** Get a short display name for a component file path */
324
+ function componentName(filePath: string): string {
325
+ const parts = filePath.replace(/\\/g, '/').split('/')
326
+ return parts[parts.length - 1]?.replace(/\.vue$/, '') ?? filePath
327
+ }
328
+
329
+ function openInEditor(file: string, line: number) {
330
+ fetch(`/__open-in-editor?file=${encodeURIComponent(file + ':' + line)}`)
287
331
  }
288
332
 
289
333
  watch(() => route.params.template, () => {
@@ -292,17 +336,23 @@ watch(() => route.params.template, () => {
292
336
  plaintextContent.value = ''
293
337
  compatibilityIssues.value = []
294
338
  compatibilityError.value = ''
295
- lintIssues.value = []
296
339
  stats.value = null
297
340
  emailResult.value = null
298
341
  sourceView.value = 'compiled'
299
- fetchTemplate().then(fetchCompatibility)
300
- fetchLint()
342
+ fetchTemplate()
343
+ fetchCompatibility()
301
344
  fetchStats()
302
345
  fetchEmailConfig()
303
346
  if (viewMode.value === 'source') fetchSource()
304
347
  }, { immediate: true })
305
348
 
349
+ // Templates list loads async from App.vue — re-trigger once available
350
+ watch(() => props.templates, (templates) => {
351
+ if (templates?.length && !compatibilityIssues.value.length && !compatibilityLoading.value) {
352
+ fetchCompatibility()
353
+ }
354
+ })
355
+
306
356
  watch(viewMode, (mode) => {
307
357
  if (mode === 'source') {
308
358
  if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
@@ -319,8 +369,8 @@ watch(sourceView, (view) => {
319
369
 
320
370
  if ((import.meta as any).hot) {
321
371
  ;(import.meta as any).hot.on('maizzle:template-updated', () => {
322
- fetchTemplate().then(fetchCompatibility)
323
- fetchLint()
372
+ fetchTemplate()
373
+ fetchCompatibility()
324
374
  fetchStats()
325
375
 
326
376
  // Always clear all source views so they re-fetch when switched to
@@ -335,33 +385,22 @@ if ((import.meta as any).hot) {
335
385
  if (sourceView.value === 'plaintext') fetchPlaintext()
336
386
  }
337
387
  })
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
388
 
356
- // Remove previous highlight
357
- el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
358
-
359
- // Find and highlight the line
360
- const lineEl = el.querySelector(`[data-line="${line}"]`)
361
- if (lineEl) {
362
- lineEl.classList.add('shiki-highlight-line')
363
- lineEl.scrollIntoView({ block: 'center', behavior: 'smooth' })
364
- }
389
+ // Keep the UI in sync with live config edits. Payload is the same shape
390
+ // as the initial `window.__MAIZZLE_CONFIG__` inject — we replace it and
391
+ // derive per-feature flags from there.
392
+ ;(import.meta as any).hot.on('maizzle:config-updated', (data: Record<string, unknown>) => {
393
+ ;(window as any).__MAIZZLE_CONFIG__ = data
394
+ const wasDisabled = compatibilityDisabled.value
395
+ const nowDisabled = data?.checks === false
396
+ compatibilityDisabled.value = nowDisabled
397
+ if (nowDisabled) {
398
+ compatibilityIssues.value = []
399
+ if (activeTab.value === 'compatibility') activeTab.value = 'stats'
400
+ } else if (wasDisabled) {
401
+ fetchCompatibility()
402
+ }
403
+ })
365
404
  }
366
405
 
367
406
  async function goToCompiledLine(line: number) {
@@ -412,6 +451,8 @@ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
412
451
  const isHorizontal = edge === 'left' || edge === 'right'
413
452
  const sign = (edge === 'left' || edge === 'top') ? -1 : 1
414
453
 
454
+ document.documentElement.style.cursor = isHorizontal ? 'ew-resize' : 'ns-resize'
455
+
415
456
  const onMove = (ev: MouseEvent | TouchEvent) => {
416
457
  const point = ev.type === 'touchmove' ? (ev as TouchEvent).touches[0] : (ev as MouseEvent)
417
458
  if (isHorizontal) {
@@ -426,6 +467,7 @@ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
426
467
 
427
468
  const onUp = () => {
428
469
  isDragging.value = false
470
+ document.documentElement.style.cursor = ''
429
471
  updateFullSize()
430
472
  document.removeEventListener('mousemove', onMove)
431
473
  document.removeEventListener('mouseup', onUp)
@@ -533,11 +575,13 @@ const bottomPanelOpen = ref(false)
533
575
  const tabsPanelHeight = ref(40)
534
576
  const activeTab = ref<string | undefined>(undefined)
535
577
 
578
+ const defaultTab = () => compatibilityDisabled.value ? 'stats' : 'compatibility'
579
+
536
580
  function toggleBottomPanel() {
537
581
  bottomPanelOpen.value = !bottomPanelOpen.value
538
582
  if (bottomPanelOpen.value) {
539
583
  tabsPanelHeight.value = 300
540
- if (!activeTab.value) activeTab.value = 'compatibility'
584
+ if (!activeTab.value) activeTab.value = defaultTab()
541
585
  } else {
542
586
  tabsPanelHeight.value = 40
543
587
  activeTab.value = undefined
@@ -560,35 +604,39 @@ function onTabClick(tab: string) {
560
604
 
561
605
  const tabsDragging = ref(false)
562
606
 
563
- function onTabsDragStart(e: MouseEvent) {
607
+ function onTabsDragStart(e: MouseEvent | TouchEvent) {
564
608
  e.preventDefault()
565
609
  tabsDragging.value = true
566
- const startY = e.clientY
610
+ const isTouch = e.type === 'touchstart'
611
+ const startY = isTouch ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
567
612
  const startHeight = tabsPanelHeight.value
568
613
 
569
614
  const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
570
615
  const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
571
616
 
572
- const onMouseMove = (e: MouseEvent) => {
573
- const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - e.clientY))
617
+ const onMove = (e: MouseEvent | TouchEvent) => {
618
+ const clientY = e.type === 'touchmove' ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
619
+ const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - clientY))
574
620
  tabsPanelHeight.value = newHeight
575
621
  bottomPanelOpen.value = newHeight > 40
576
622
 
577
623
  if (!bottomPanelOpen.value) {
578
624
  activeTab.value = undefined
579
625
  } else if (!activeTab.value) {
580
- activeTab.value = 'compatibility'
626
+ activeTab.value = defaultTab()
581
627
  }
582
628
  }
583
629
 
584
- const onMouseUp = () => {
630
+ const onEnd = () => {
585
631
  tabsDragging.value = false
586
- document.removeEventListener('mousemove', onMouseMove)
587
- document.removeEventListener('mouseup', onMouseUp)
632
+ document.removeEventListener('mousemove', onMove)
633
+ document.removeEventListener('mouseup', onEnd)
634
+ document.removeEventListener('touchmove', onMove)
635
+ document.removeEventListener('touchend', onEnd)
588
636
  }
589
637
 
590
- document.addEventListener('mousemove', onMouseMove)
591
- document.addEventListener('mouseup', onMouseUp)
638
+ document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onMove)
639
+ document.addEventListener(isTouch ? 'touchend' : 'mouseup', onEnd)
592
640
  }
593
641
 
594
642
  const stripeBg = {
@@ -719,20 +767,18 @@ const stripeBg = {
719
767
  <div
720
768
  class="relative h-0 cursor-row-resize before:absolute before:top-0 before:left-0 before:right-0 before:h-3.25 before:content-['']"
721
769
  @mousedown="onTabsDragStart"
770
+ @touchstart.prevent="onTabsDragStart"
722
771
  />
723
772
  <Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
724
773
  <div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
725
774
  <TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
726
- <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')">
727
- Compatibility
728
- </TabsTrigger>
729
- <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')">
730
- Linter
775
+ <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')">
776
+ Checks
731
777
  </TabsTrigger>
732
- <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')">
778
+ <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')">
733
779
  Stats
734
780
  </TabsTrigger>
735
- <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')">
781
+ <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')">
736
782
  Test
737
783
  </TabsTrigger>
738
784
  </TabsList>
@@ -746,7 +792,7 @@ const stripeBg = {
746
792
  <button
747
793
  v-for="cat in activeCompatibilityCategories"
748
794
  :key="cat"
749
- class="px-2 py-0.5 text-[11px] rounded-full cursor-pointer transition-colors"
795
+ class="px-2 py-0.5 text-[11px] rounded-full cursor-default transition-colors"
750
796
  :class="compatibilityCategory === cat
751
797
  ? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
752
798
  : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
@@ -757,48 +803,45 @@ const stripeBg = {
757
803
  </button>
758
804
  </div>
759
805
  <ScrollArea class="h-full flex-1 min-h-0 pl-5">
760
- <p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Checking compatibility...</p>
806
+ <p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Running checks...</p>
761
807
  <p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
762
- <p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No compatibility issues found.</p>
808
+ <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>
763
809
  <ul v-else class="text-xs divide-y">
764
810
  <li
765
811
  v-for="(issue, i) in filteredCompatibilityIssues"
766
812
  :key="i"
767
- class="pr-4 py-1.5 hover:bg-gray-50 dark:hover:bg-white/5"
813
+ class="pr-4 py-2"
768
814
  >
769
- <div class="flex items-center gap-2">
770
- <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'">
771
- {{ issue.title }}
772
- </a>
773
- <span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
774
- {{ issue.title }}
775
- </span>
776
- <span class="text-gray-400 dark:text-gray-600 shrink-0">&middot;</span>
777
- <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>
778
- <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>
779
- </div>
780
- </li>
781
- </ul>
782
- </ScrollArea>
783
- </TabsContent>
784
- <TabsContent value="lint" class="mt-0 h-full">
785
- <ScrollArea class="h-full pl-5">
786
- <p v-if="lintLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
787
- <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>
788
- <ul v-else class="text-xs divide-y">
789
- <li
790
- v-for="(issue, i) in lintIssues"
791
- :key="i"
792
- class="pr-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
793
- >
794
- <div class="flex items-start justify-between gap-4">
815
+ <div class="flex items-center justify-between gap-4">
795
816
  <div>
796
- <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
817
+ <a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issueColorClass(issue)">
818
+ {{ issue.title }}
819
+ </a>
820
+ <span v-else class="font-medium" :class="issueColorClass(issue)">
797
821
  {{ issue.title }}
798
822
  </span>
799
- <div class="text-gray-500 dark:text-gray-400 mt-0.5">{{ issue.message }}</div>
823
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">
824
+ <template v-if="issue.kind === 'lint'">
825
+ <template v-for="(seg, j) in messageSegments(issue.message)" :key="j">
826
+ <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>
827
+ <template v-else>{{ seg.text }}</template>
828
+ </template>
829
+ </template>
830
+ <template v-else>
831
+ {{ supportPrefix(issue) }}
832
+ <template v-if="(issue.affectedClients?.length ?? 0) <= 4 || expandedIssueKeys.has(issueKey(issue, i))">
833
+ {{ (issue.affectedClients ?? []).join(', ') }}
834
+ </template>
835
+ <template v-else>
836
+ {{ issue.affectedClients!.slice(0, 4).join(', ') }}
837
+ <button class="underline cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" @click="expandedIssueKeys.add(issueKey(issue, i)); expandedIssueKeys = new Set(expandedIssueKeys)">
838
+ + {{ issue.affectedClients!.length - 4 }} others
839
+ </button>
840
+ </template>
841
+ </template>
842
+ </div>
800
843
  </div>
801
- <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="goToLine(issue.line!)">L{{ issue.line }}</button>
844
+ <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>
802
845
  </div>
803
846
  </li>
804
847
  </ul>
@@ -808,35 +851,35 @@ const stripeBg = {
808
851
  <ScrollArea class="h-full pl-5">
809
852
  <p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
810
853
  <p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
811
- <div v-else class="pr-4 py-3 flex items-center gap-6 text-xs">
812
- <div class="flex items-center gap-1.5">
813
- <span class="text-gray-500 dark:text-gray-400">Size</span>
814
- <span
815
- class="font-medium tabular-nums"
816
- :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'"
817
- >{{ stats.size.formatted }}</span>
818
- <TooltipProvider :delay-duration="0">
819
- <Tooltip>
820
- <TooltipTrigger as-child>
821
- <button type="button">
822
- <Info class="size-3 text-gray-400 dark:text-gray-500" />
823
- </button>
824
- </TooltipTrigger>
825
- <TooltipContent class="max-w-60">
826
- Compiled HTML size, excludes image files. Gmail clips content at ~100KB.
827
- </TooltipContent>
828
- </Tooltip>
829
- </TooltipProvider>
830
- </div>
831
- <div class="flex items-center gap-1.5">
832
- <span class="text-gray-500 dark:text-gray-400">Images</span>
833
- <span class="font-medium tabular-nums">{{ stats.images }}</span>
834
- </div>
835
- <div class="flex items-center gap-1.5">
836
- <span class="text-gray-500 dark:text-gray-400">Links</span>
837
- <span class="font-medium tabular-nums">{{ stats.links }}</span>
838
- </div>
839
- </div>
854
+ <ul v-else class="text-xs divide-y divide-gray-200 dark:divide-white/10">
855
+ <li class="pr-4 py-2">
856
+ <div class="flex items-center justify-between gap-4">
857
+ <div>
858
+ <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>
859
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Compiled HTML size. Gmail clips emails larger than ~100KB.</div>
860
+ </div>
861
+ <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>
862
+ </div>
863
+ </li>
864
+ <li class="pr-4 py-2">
865
+ <div class="flex items-center justify-between gap-4">
866
+ <div>
867
+ <span class="font-medium text-gray-900 dark:text-gray-300">Images</span>
868
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Total from &lt;img&gt; tags and CSS background images.</div>
869
+ </div>
870
+ <span class="font-medium tabular-nums shrink-0">{{ stats.images }}</span>
871
+ </div>
872
+ </li>
873
+ <li class="pr-4 py-2">
874
+ <div class="flex items-center justify-between gap-4">
875
+ <div>
876
+ <span class="font-medium text-gray-900 dark:text-gray-300">Links</span>
877
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">Total &lt;a&gt; tags with an href attribute.</div>
878
+ </div>
879
+ <span class="font-medium tabular-nums shrink-0">{{ stats.links }}</span>
880
+ </div>
881
+ </li>
882
+ </ul>
840
883
  </ScrollArea>
841
884
  </TabsContent>
842
885
  <TabsContent value="test" class="mt-0 h-full">
@@ -844,19 +887,19 @@ const stripeBg = {
844
887
  <div class="pr-4 py-3 max-w-md">
845
888
  <div class="space-y-2">
846
889
  <div class="flex items-center gap-2">
847
- <label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">To</label>
890
+ <label for="email-to" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">To</label>
848
891
  <TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
849
892
  <TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
850
893
  <TagsInputItemText class="px-1.5 py-0 text-xs" />
851
894
  <TagsInputItemDelete class="size-3.5" />
852
895
  </TagsInputItem>
853
- <TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
896
+ <TagsInputInput id="email-to" class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
854
897
  </TagsInput>
855
898
  </div>
856
899
  <div class="flex items-center gap-2">
857
- <label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">Subject</label>
900
+ <label for="email-subject" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">Subject</label>
858
901
  <div class="flex-1 flex items-center gap-3">
859
- <Input v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
902
+ <Input id="email-subject" v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
860
903
  <label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
861
904
  <Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
862
905
  <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)) if (attrName === "class" && el.attribs.class) {
54
- const existingClasses = el.attribs.class.split(/\s+/).filter(Boolean);
55
- const newClasses = String(attrValue).split(/\s+/).filter(Boolean);
56
- const mergedClasses = [...new Set([...existingClasses, ...newClasses])];
57
- if (mergedClasses.join(" ") !== el.attribs.class) el.attribs.class = mergedClasses.join(" ");
58
- } else if (!(attrName in el.attribs)) el.attribs[attrName] = String(attrValue);
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, Record<string, string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;EAE3E,MAAM,YAAY,gBAAgB,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS;GAClB,MAAM,KAAK;AACX,OAAI,CAAC,GAAG,KAAM;AAKd,OAFgB,UAAU,MAAK,aAAY,eAAe,IAAI,SAAS,CAAC,EAE3D;AAEX,QAAI,CAAC,GAAG,QACN,IAAG,UAAU,EAAE;AAGjB,SAAK,MAAM,CAAC,UAAU,cAAc,OAAO,QAAQ,WAAW,CAE5D,KAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;KAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;KACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;KACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,SAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;eAIxC,EAAE,YAAY,GAAG,SACnB,IAAG,QAAQ,YAAY,OAAO,UAAU;;IAKhD;;AAGJ,QAAO;;;;;;AAOT,SAAS,eAAe,IAAa,UAA2B;AAE9D,YAAW,SAAS,MAAM;CAG1B,MAAM,YAAY,SAAS,MAAM,gCAAgC;AACjE,KAAI,WAAW;EACb,MAAM,GAAG,UAAU,aAAa;AAChC,MAAI,cAAc,OAEhB,QAAO,aAAa,GAAG,WAAW,EAAE;MAGpC,QAAO,GAAG,UAAU,cAAc;;AAKtC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,YAAY,SAAS,MAAM,EAAE;AAEnC,UADgB,GAAG,SAAS,OAAO,MAAM,MAAM,IAAI,EAAE,EACtC,SAAS,UAAU;;AAIpC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,KAAK,SAAS,MAAM,EAAE;AAC5B,SAAO,GAAG,SAAS,OAAO;;CAK5B,MAAM,eAAe,SAAS,MAAM,kCAAkC;AACtE,KAAI,cAAc;EAChB,MAAM,GAAG,SAAS,YAAY;AAC9B,MAAI,GAAG,SAAS,QAAS,QAAO;EAGhC,MAAM,cAAc,SAAS,MAAM,sBAAsB;AACzD,MAAI,aAAa;GACf,MAAM,GAAG,UAAU,aAAa;AAChC,OAAI,cAAc,OAChB,QAAO,aAAa,GAAG,WAAW,EAAE;OAEpC,QAAO,GAAG,UAAU,cAAc;;AAGtC,SAAO;;AAIT,QAAO,GAAG,SAAS"}
1
+ {"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, false | Record<string, false | string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // User opted out of this selector entirely (e.g. `table: false`)\n if (attributes === false) continue\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // User opted out of this specific attribute (e.g. `role: false`)\n if (attrValue === false) continue\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;AAE3E,MAAI,eAAe,MAAO;EAE1B,MAAM,YAAY,gBAAgB,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS;GAClB,MAAM,KAAK;AACX,OAAI,CAAC,GAAG,KAAM;AAKd,OAFgB,UAAU,MAAK,aAAY,eAAe,IAAI,SAAS,CAAC,EAE3D;AAEX,QAAI,CAAC,GAAG,QACN,IAAG,UAAU,EAAE;AAGjB,SAAK,MAAM,CAAC,UAAU,cAAc,OAAO,QAAQ,WAAW,EAAE;AAE9D,SAAI,cAAc,MAAO;AAEzB,SAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;MAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;MACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;MACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,UAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;gBAIxC,EAAE,YAAY,GAAG,SACnB,IAAG,QAAQ,YAAY,OAAO,UAAU;;;IAKhD;;AAGJ,QAAO;;;;;;AAOT,SAAS,eAAe,IAAa,UAA2B;AAE9D,YAAW,SAAS,MAAM;CAG1B,MAAM,YAAY,SAAS,MAAM,gCAAgC;AACjE,KAAI,WAAW;EACb,MAAM,GAAG,UAAU,aAAa;AAChC,MAAI,cAAc,OAEhB,QAAO,aAAa,GAAG,WAAW,EAAE;MAGpC,QAAO,GAAG,UAAU,cAAc;;AAKtC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,YAAY,SAAS,MAAM,EAAE;AAEnC,UADgB,GAAG,SAAS,OAAO,MAAM,MAAM,IAAI,EAAE,EACtC,SAAS,UAAU;;AAIpC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,KAAK,SAAS,MAAM,EAAE;AAC5B,SAAO,GAAG,SAAS,OAAO;;CAK5B,MAAM,eAAe,SAAS,MAAM,kCAAkC;AACtE,KAAI,cAAc;EAChB,MAAM,GAAG,SAAS,YAAY;AAC9B,MAAI,GAAG,SAAS,QAAS,QAAO;EAGhC,MAAM,cAAc,SAAS,MAAM,sBAAsB;AACzD,MAAI,aAAa;GACf,MAAM,GAAG,UAAU,aAAa;AAChC,OAAI,cAAc,OAChB,QAAO,aAAa,GAAG,WAAW,EAAE;OAEpC,QAAO,GAAG,UAAU,cAAc;;AAGtC,SAAO;;AAIT,QAAO,GAAG,SAAS"}
@@ -28,8 +28,8 @@ function inlineCSS(dom, config = {}) {
28
28
  walk(dom, (node) => {
29
29
  const el = node;
30
30
  if (el.name === "style" && el.attribs) {
31
- if (el.attribs.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
32
- if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
31
+ if ("embed" in el.attribs && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
32
+ if ("data-embed" in el.attribs && !("embed" in el.attribs)) el.attribs.embed = "";
33
33
  if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
34
34
  }
35
35
  });