@maizzle/framework 6.0.0-rc.7 → 6.0.0-rc.9

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 (76) hide show
  1. package/dist/_virtual/_rolldown/runtime.mjs +32 -0
  2. package/dist/build.mjs +2 -1
  3. package/dist/build.mjs.map +1 -1
  4. package/dist/components/Body.vue +105 -36
  5. package/dist/components/Button.vue +4 -1
  6. package/dist/components/CodeBlock.vue +1 -1
  7. package/dist/components/CodeInline.vue +6 -1
  8. package/dist/components/Column.vue +30 -5
  9. package/dist/components/Container.vue +10 -2
  10. package/dist/components/Divider.vue +28 -0
  11. package/dist/components/Head.vue +22 -0
  12. package/dist/components/Heading.vue +28 -0
  13. package/dist/components/Html.vue +98 -47
  14. package/dist/components/Layout.vue +93 -0
  15. package/dist/components/Link.vue +26 -0
  16. package/dist/components/Markdown.vue +22 -3
  17. package/dist/components/Outlook.vue +36 -0
  18. package/dist/components/Overlap.vue +25 -5
  19. package/dist/components/Preheader.vue +1 -1
  20. package/dist/components/Row.vue +16 -5
  21. package/dist/components/Section.vue +83 -0
  22. package/dist/components/Text.vue +29 -0
  23. package/dist/components/Vml.vue +165 -13
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.mjs +2 -1
  26. package/dist/node_modules/picomatch/index.mjs +13 -0
  27. package/dist/node_modules/picomatch/index.mjs.map +1 -0
  28. package/dist/node_modules/picomatch/lib/constants.mjs +174 -0
  29. package/dist/node_modules/picomatch/lib/constants.mjs.map +1 -0
  30. package/dist/node_modules/picomatch/lib/parse.mjs +1067 -0
  31. package/dist/node_modules/picomatch/lib/parse.mjs.map +1 -0
  32. package/dist/node_modules/picomatch/lib/picomatch.mjs +304 -0
  33. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +1 -0
  34. package/dist/node_modules/picomatch/lib/scan.mjs +296 -0
  35. package/dist/node_modules/picomatch/lib/scan.mjs.map +1 -0
  36. package/dist/node_modules/picomatch/lib/utils.mjs +53 -0
  37. package/dist/node_modules/picomatch/lib/utils.mjs.map +1 -0
  38. package/dist/plugin.mjs +11 -7
  39. package/dist/plugin.mjs.map +1 -1
  40. package/dist/plugins/postcss/tailwindCleanup.d.mts.map +1 -1
  41. package/dist/plugins/postcss/tailwindCleanup.mjs +24 -2
  42. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  43. package/dist/render/createRenderer.d.mts +3 -0
  44. package/dist/render/createRenderer.d.mts.map +1 -1
  45. package/dist/render/createRenderer.mjs +26 -7
  46. package/dist/render/createRenderer.mjs.map +1 -1
  47. package/dist/render/index.mjs +2 -1
  48. package/dist/render/index.mjs.map +1 -1
  49. package/dist/serve.d.mts.map +1 -1
  50. package/dist/serve.mjs +13 -6
  51. package/dist/serve.mjs.map +1 -1
  52. package/dist/server/compatibility.mjs +15 -1
  53. package/dist/server/compatibility.mjs.map +1 -1
  54. package/dist/server/email.mjs +2 -1
  55. package/dist/server/email.mjs.map +1 -1
  56. package/dist/server/linter.d.mts +1 -2
  57. package/dist/server/linter.d.mts.map +1 -1
  58. package/dist/server/linter.mjs +60 -71
  59. package/dist/server/linter.mjs.map +1 -1
  60. package/dist/server/ui/App.vue +9 -9
  61. package/dist/server/ui/pages/Preview.vue +215 -150
  62. package/dist/transformers/index.d.mts +10 -9
  63. package/dist/transformers/index.d.mts.map +1 -1
  64. package/dist/transformers/index.mjs +12 -9
  65. package/dist/transformers/index.mjs.map +1 -1
  66. package/dist/transformers/inlineCSS.d.mts +1 -14
  67. package/dist/transformers/inlineCSS.d.mts.map +1 -1
  68. package/dist/transformers/inlineCSS.mjs +16 -34
  69. package/dist/transformers/inlineCSS.mjs.map +1 -1
  70. package/dist/transformers/sixHex.d.mts +16 -0
  71. package/dist/transformers/sixHex.d.mts.map +1 -0
  72. package/dist/transformers/sixHex.mjs +30 -0
  73. package/dist/transformers/sixHex.mjs.map +1 -0
  74. package/dist/types/config.d.mts +57 -28
  75. package/dist/types/config.d.mts.map +1 -1
  76. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
2
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
- import { ChevronUp, ChevronDown, Check, ExternalLink } from 'lucide-vue-next'
4
+ import { ChevronUp, ChevronDown, Check, Info } from 'lucide-vue-next'
5
5
  import {
6
6
  DropdownMenu,
7
7
  DropdownMenuContent,
@@ -20,6 +20,7 @@ import {
20
20
  TagsInputItemDelete,
21
21
  TagsInputItemText,
22
22
  } from '@/components/ui/tags-input'
23
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
23
24
 
24
25
  import stripesUrl from '../stripes.svg'
25
26
 
@@ -29,9 +30,16 @@ interface Device {
29
30
  height: number
30
31
  }
31
32
 
33
+ interface Template {
34
+ name: string
35
+ path: string
36
+ href: string
37
+ }
38
+
32
39
  const props = defineProps<{
33
40
  device?: Device | null
34
41
  resetKey?: number
42
+ templates?: Template[]
35
43
  }>()
36
44
 
37
45
  const viewMode = defineModel<'preview' | 'source'>('viewMode', { default: 'preview' })
@@ -60,23 +68,30 @@ const iframeWidth = ref<number | null>(null)
60
68
  const iframeHeight = ref<number | null>(null)
61
69
  const iframeContentHeight = ref<number | null>(null)
62
70
 
63
- async function copySource() {
71
+ function copySource() {
72
+ let text: string
64
73
  if (sourceView.value === 'compiled') {
65
- await navigator.clipboard.writeText(srcdoc.value)
74
+ text = srcdoc.value
66
75
  } else if (sourceView.value === 'plaintext') {
67
- await navigator.clipboard.writeText(plaintextContent.value)
76
+ text = plaintextContent.value
68
77
  } else {
69
78
  const el = document.createElement('div')
70
79
  el.innerHTML = vueSourceHtml.value
71
- await navigator.clipboard.writeText(el.textContent || '')
80
+ text = el.textContent || ''
72
81
  }
73
- copied.value = true
74
- setTimeout(() => { copied.value = false }, 2000)
82
+
83
+ const blob = new Blob([text], { type: 'text/plain' })
84
+ const item = new ClipboardItem({ 'text/plain': blob })
85
+ navigator.clipboard.write([item]).then(() => {
86
+ copied.value = true
87
+ setTimeout(() => { copied.value = false }, 2000)
88
+ })
75
89
  }
76
90
 
77
91
  interface CompatibilityIssue {
78
92
  type: 'error' | 'warning'
79
93
  title: string
94
+ category: string
80
95
  clients: Array<{ name: string, notes: string[] }>
81
96
  url?: string
82
97
  line?: number
@@ -98,6 +113,15 @@ interface TemplateStats {
98
113
  const compatibilityIssues = ref<CompatibilityIssue[]>([])
99
114
  const compatibilityLoading = ref(false)
100
115
  const compatibilityError = ref('')
116
+ const compatibilityCategory = ref('')
117
+ const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
118
+ const activeCompatibilityCategories = computed(() =>
119
+ compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
120
+ )
121
+ const filteredCompatibilityIssues = computed(() => {
122
+ if (!compatibilityCategory.value) return compatibilityIssues.value
123
+ return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
124
+ })
101
125
  const lintIssues = ref<LintIssue[]>([])
102
126
  const lintLoading = ref(false)
103
127
  const stats = ref<TemplateStats | null>(null)
@@ -236,6 +260,9 @@ async function fetchCompatibility() {
236
260
  compatibilityIssues.value = []
237
261
  } else {
238
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 || ''
239
266
  }
240
267
  } catch {
241
268
  compatibilityIssues.value = []
@@ -247,8 +274,11 @@ async function fetchCompatibility() {
247
274
  async function fetchLint() {
248
275
  lintLoading.value = true
249
276
  try {
250
- const res = await fetch(`/__maizzle/lint/${route.params.template}`)
251
- lintIssues.value = await res.json()
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) : []
252
282
  } catch {
253
283
  lintIssues.value = []
254
284
  } finally {
@@ -360,7 +390,7 @@ const emit = defineEmits<{ 'clear-device': [] }>()
360
390
 
361
391
  type Edge = 'left' | 'right' | 'top' | 'bottom'
362
392
 
363
- function onEdgeDrag(e: MouseEvent, edge: Edge) {
393
+ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
364
394
  e.preventDefault()
365
395
  isDragging.value = true
366
396
  emit('clear-device')
@@ -368,8 +398,10 @@ function onEdgeDrag(e: MouseEvent, edge: Edge) {
368
398
  const container = containerEl.value
369
399
  if (!container) return
370
400
 
371
- const startX = e.clientX
372
- const startY = e.clientY
401
+ const isTouch = e.type === 'touchstart'
402
+ const startPoint = isTouch ? (e as TouchEvent).touches[0] : (e as MouseEvent)
403
+ const startX = startPoint.clientX
404
+ const startY = startPoint.clientY
373
405
  const rect = container.getBoundingClientRect()
374
406
  const gutter = 40 // 20px padding on each side
375
407
  const maxW = rect.width - gutter
@@ -380,13 +412,14 @@ function onEdgeDrag(e: MouseEvent, edge: Edge) {
380
412
  const isHorizontal = edge === 'left' || edge === 'right'
381
413
  const sign = (edge === 'left' || edge === 'top') ? -1 : 1
382
414
 
383
- const onMove = (ev: MouseEvent) => {
415
+ const onMove = (ev: MouseEvent | TouchEvent) => {
416
+ const point = ev.type === 'touchmove' ? (ev as TouchEvent).touches[0] : (ev as MouseEvent)
384
417
  if (isHorizontal) {
385
418
  // Symmetric: each side moves by the delta, so total change is 2x
386
- const delta = (ev.clientX - startX) * sign
419
+ const delta = (point.clientX - startX) * sign
387
420
  iframeWidth.value = Math.max(200, Math.min(maxW, startW + delta * 2))
388
421
  } else {
389
- const delta = (ev.clientY - startY) * sign
422
+ const delta = (point.clientY - startY) * sign
390
423
  iframeHeight.value = Math.max(100, Math.min(maxH, startH + delta * 2))
391
424
  }
392
425
  }
@@ -396,10 +429,14 @@ function onEdgeDrag(e: MouseEvent, edge: Edge) {
396
429
  updateFullSize()
397
430
  document.removeEventListener('mousemove', onMove)
398
431
  document.removeEventListener('mouseup', onUp)
432
+ document.removeEventListener('touchmove', onMove)
433
+ document.removeEventListener('touchend', onUp)
399
434
  }
400
435
 
401
436
  document.addEventListener('mousemove', onMove)
402
437
  document.addEventListener('mouseup', onUp)
438
+ document.addEventListener('touchmove', onMove, { passive: false })
439
+ document.addEventListener('touchend', onUp)
403
440
  }
404
441
 
405
442
  function updateFullSize() {
@@ -568,53 +605,53 @@ const stripeBg = {
568
605
  <div v-show="viewMode === 'source'" class="absolute inset-0 min-w-0 overflow-hidden">
569
606
  <div class="absolute top-3 left-6 z-10">
570
607
  <DropdownMenu :modal="false">
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">
608
+ <DropdownMenuTrigger class="inline-flex items-center gap-1 rounded-md bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border border-white/10 px-2.5 h-7 text-xs font-medium text-gray-300 hover:bg-[#27212e] dark:hover:bg-gray-950 transition-colors">
572
609
  {{ sourceView === 'compiled' ? 'HTML' : sourceView === 'vue' ? 'Source' : 'Plaintext' }}
573
610
  <ChevronDown class="size-3 opacity-50" />
574
611
  </DropdownMenuTrigger>
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>
612
+ <DropdownMenuContent align="start" class="min-w-32 bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border-white/10">
613
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'vue'">
614
+ <Check v-if="sourceView === 'vue'" class="size-3 text-gray-200" />
615
+ <span :class="[sourceView === 'vue' ? 'text-gray-200' : 'pl-5']">Source</span>
579
616
  </DropdownMenuItem>
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>
617
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'compiled'">
618
+ <Check v-if="sourceView === 'compiled'" class="size-3 text-gray-200" />
619
+ <span :class="[sourceView === 'compiled' ? 'text-gray-200' : 'pl-5']">HTML</span>
583
620
  </DropdownMenuItem>
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>
621
+ <DropdownMenuItem class="text-xs font-medium text-gray-400 focus:text-gray-200 focus:bg-white/10" @click="sourceView = 'plaintext'">
622
+ <Check v-if="sourceView === 'plaintext'" class="size-3 text-gray-200" />
623
+ <span :class="[sourceView === 'plaintext' ? 'text-gray-200' : 'pl-5']">Plaintext</span>
587
624
  </DropdownMenuItem>
588
625
  </DropdownMenuContent>
589
626
  </DropdownMenu>
590
627
  </div>
591
628
  <button
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"
629
+ class="absolute top-3 right-[26px] z-10 inline-flex items-center justify-center rounded-md size-7 bg-[#27212e]/80 dark:bg-gray-950/80 backdrop-blur-md border border-white/10 hover:bg-[#27212e] dark:hover:bg-gray-950 group disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
593
630
  :disabled="copied"
594
631
  @click="copySource"
595
632
  >
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>
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>
633
+ <svg v-if="!copied" class="size-3.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>
634
+ <svg v-else class="size-3.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>
598
635
  </button>
599
- <ScrollArea v-show="sourceView === 'compiled'" class="h-full">
636
+ <ScrollArea v-show="sourceView === 'compiled'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
600
637
  <div
601
638
  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!"
639
+ class="flex-1 bg-[#27212e] dark:bg-gray-950 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
640
  v-html="sourceHtml"
604
641
  />
605
642
  <ScrollBar orientation="horizontal" />
606
643
  </ScrollArea>
607
- <ScrollArea v-show="sourceView === 'vue'" class="h-full">
644
+ <ScrollArea v-show="sourceView === 'vue'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
608
645
  <div
609
646
  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!"
647
+ class="flex-1 bg-[#27212e] dark:bg-gray-950 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
648
  v-html="vueSourceHtml"
612
649
  />
613
650
  <ScrollBar orientation="horizontal" />
614
651
  </ScrollArea>
615
- <ScrollArea v-show="sourceView === 'plaintext'" class="h-full">
652
+ <ScrollArea v-show="sourceView === 'plaintext'" class="h-full [&_[data-slot=scroll-area-viewport]>div]:flex [&_[data-slot=scroll-area-viewport]>div]:flex-col [&_[data-slot=scroll-area-viewport]>div]:min-h-full">
616
653
  <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"
654
+ class="p-6 pt-14 text-sm leading-6 flex-1 text-gray-300 bg-[#27212e] dark:bg-gray-950 whitespace-pre-wrap break-words"
618
655
  >{{ plaintextContent }}</pre>
619
656
  </ScrollArea>
620
657
  </div>
@@ -639,23 +676,23 @@ const stripeBg = {
639
676
  }"
640
677
  >
641
678
  <!-- 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')">
679
+ <div class="group hidden min-[430px]:flex absolute top-0 left-5 right-5 h-5 items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'top')" @touchstart.prevent="onEdgeDrag($event, 'top')">
643
680
  <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
681
  </div>
645
682
  <!-- 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')">
683
+ <div class="group hidden min-[430px]:flex absolute bottom-0 left-5 right-5 h-5 items-center justify-center cursor-ns-resize" @mousedown="onEdgeDrag($event, 'bottom')" @touchstart.prevent="onEdgeDrag($event, 'bottom')">
647
684
  <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
685
  </div>
649
686
  <!-- 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')">
687
+ <div class="group hidden min-[430px]:flex absolute left-0 top-5 bottom-5 w-5 items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'left')" @touchstart.prevent="onEdgeDrag($event, 'left')">
651
688
  <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
689
  </div>
653
690
  <!-- 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')">
691
+ <div class="group hidden min-[430px]:flex absolute right-0 top-5 bottom-5 w-5 items-center justify-center cursor-ew-resize" @mousedown="onEdgeDrag($event, 'right')" @touchstart.prevent="onEdgeDrag($event, 'right')">
655
692
  <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
693
  </div>
657
694
  <!-- Iframe -->
658
- <div ref="wrapperEl" class="absolute inset-5 border border-gray-200 dark:border-gray-800">
695
+ <div ref="wrapperEl" class="absolute inset-0 min-[430px]:inset-5 border border-gray-200 dark:border-gray-800">
659
696
  <ScrollArea class="h-full w-full bg-white dark:bg-gray-950">
660
697
  <iframe
661
698
  ref="iframeEl"
@@ -684,7 +721,7 @@ const stripeBg = {
684
721
  @mousedown="onTabsDragStart"
685
722
  />
686
723
  <Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
687
- <div class="flex items-center justify-between min-h-10 px-4 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
724
+ <div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
688
725
  <TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
689
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')">
690
727
  Compatibility
@@ -705,128 +742,156 @@ const stripeBg = {
705
742
  </Button>
706
743
  </div>
707
744
  <div class="flex-1 min-h-0">
708
- <TabsContent value="compatibility" class="mt-0 h-full"><ScrollArea class="h-full">
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>
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>
712
- <ul v-else class="text-xs divide-y">
713
- <li
714
- v-for="(issue, i) in compatibilityIssues"
715
- :key="i"
716
- class="px-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
745
+ <TabsContent value="compatibility" class="mt-0 h-full flex flex-col"><div v-if="!compatibilityLoading && !compatibilityError && compatibilityIssues.length > 0" class="flex gap-1 pl-3 pr-4 py-2 border-b border-gray-200 dark:border-white/10 shrink-0">
746
+ <button
747
+ v-for="cat in activeCompatibilityCategories"
748
+ :key="cat"
749
+ class="px-2 py-0.5 text-[11px] rounded-full cursor-pointer transition-colors"
750
+ :class="compatibilityCategory === cat
751
+ ? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
752
+ : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
753
+ @click="compatibilityCategory = cat"
717
754
  >
718
- <div class="flex items-start justify-between gap-4">
719
- <div>
720
- <a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
755
+ {{ cat === 'css' ? 'CSS' : cat === 'html' ? 'HTML' : cat.charAt(0).toUpperCase() + cat.slice(1) }}
756
+ <span class="ml-0.5 tabular-nums">{{ compatibilityIssues.filter(i => i.category === cat).length }}</span>
757
+ </button>
758
+ </div>
759
+ <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>
761
+ <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>
763
+ <ul v-else class="text-xs divide-y">
764
+ <li
765
+ v-for="(issue, i) in filteredCompatibilityIssues"
766
+ :key="i"
767
+ class="pr-4 py-1.5 hover:bg-gray-50 dark:hover:bg-white/5"
768
+ >
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'">
721
771
  {{ issue.title }}
722
772
  </a>
723
- <span v-else class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
773
+ <span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
724
774
  {{ issue.title }}
725
775
  </span>
726
- <div class="text-gray-500 dark:text-gray-400 mt-1 space-y-0.5">
727
- <div v-for="client in issue.clients" :key="client.name">
728
- <span class="text-gray-700 dark:text-gray-300">{{ client.name }}</span><span v-if="client.notes.length">: {{ client.notes.join('. ') }}</span>
729
- </div>
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">
795
+ <div>
796
+ <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
797
+ {{ issue.title }}
798
+ </span>
799
+ <div class="text-gray-500 dark:text-gray-400 mt-0.5">{{ issue.message }}</div>
730
800
  </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>
731
802
  </div>
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>
803
+ </li>
804
+ </ul>
805
+ </ScrollArea>
806
+ </TabsContent>
807
+ <TabsContent value="stats" class="mt-0 h-full">
808
+ <ScrollArea class="h-full pl-5">
809
+ <p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
810
+ <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>
733
830
  </div>
734
- </li>
735
- </ul>
736
- </ScrollArea></TabsContent>
737
- <TabsContent value="lint" class="mt-0 h-full"><ScrollArea class="h-full">
738
- <p v-if="lintLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
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>
740
- <ul v-else class="text-xs divide-y">
741
- <li
742
- v-for="(issue, i) in lintIssues"
743
- :key="i"
744
- class="px-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
745
- >
746
- <div class="flex items-start justify-between gap-4">
747
- <div>
748
- <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
749
- {{ issue.title }}
750
- </span>
751
- <div class="text-gray-500 dark:text-gray-400 mt-0.5">{{ issue.message }}</div>
752
- </div>
753
- <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>
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>
754
834
  </div>
755
- </li>
756
- </ul>
757
- </ScrollArea></TabsContent>
758
- <TabsContent value="stats" class="mt-0 h-full"><ScrollArea class="h-full">
759
- <p v-if="statsLoading" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
760
- <p v-else-if="!stats" class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
761
- <div v-else class="px-4 py-3 flex items-center gap-6 text-xs">
762
- <div class="flex items-center gap-1.5">
763
- <span class="text-gray-500 dark:text-gray-400">Size</span>
764
- <span
765
- class="font-medium tabular-nums"
766
- :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'"
767
- >{{ stats.size.formatted }}</span>
768
- </div>
769
- <div class="flex items-center gap-1.5">
770
- <span class="text-gray-500 dark:text-gray-400">Images</span>
771
- <span class="font-medium tabular-nums">{{ stats.images }}</span>
772
- </div>
773
- <div class="flex items-center gap-1.5">
774
- <span class="text-gray-500 dark:text-gray-400">Links</span>
775
- <span class="font-medium tabular-nums">{{ stats.links }}</span>
776
- </div>
777
- </div>
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>
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>
791
838
  </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>
839
+ </div>
840
+ </ScrollArea>
841
+ </TabsContent>
842
+ <TabsContent value="test" class="mt-0 h-full">
843
+ <ScrollArea class="h-full pl-5">
844
+ <div class="pr-4 py-3 max-w-md">
845
+ <div class="space-y-2">
846
+ <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>
848
+ <TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
849
+ <TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
850
+ <TagsInputItemText class="px-1.5 py-0 text-xs" />
851
+ <TagsInputItemDelete class="size-3.5" />
852
+ </TagsInputItem>
853
+ <TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
854
+ </TagsInput>
855
+ </div>
856
+ <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>
858
+ <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" />
860
+ <label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
861
+ <Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
862
+ <span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
863
+ </label>
864
+ </div>
800
865
  </div>
801
866
  </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"
867
+ <div class="flex items-center gap-3 mt-3">
868
+ <Button
869
+ size="sm"
870
+ class="h-7 text-xs px-3"
871
+ :disabled="!emailTo.length || emailSending"
872
+ @click="sendTestEmail"
823
873
  >
824
- View <ExternalLink class="size-3" />
825
- </a>
826
- </p>
874
+ <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>
875
+ {{ emailSending ? 'Sending' : 'Send' }}
876
+ </Button>
877
+ </div>
878
+ <div v-if="emailResult" class="mt-2">
879
+ <p class="text-xs" :class="emailResult.success ? 'text-gray-950 dark:text-white' : 'text-red-600'">
880
+ {{ emailResult.message }}
881
+ <a
882
+ v-if="emailResult.previewUrl"
883
+ :href="emailResult.previewUrl"
884
+ target="_blank"
885
+ rel="noopener"
886
+ class="text-gray-500 dark:text-gray-400 hover:underline"
887
+ >
888
+ (view)
889
+ </a>
890
+ </p>
891
+ </div>
827
892
  </div>
828
- </div>
829
- </ScrollArea></TabsContent>
893
+ </ScrollArea>
894
+ </TabsContent>
830
895
  </div>
831
896
  </Tabs>
832
897
  </div>
@@ -19,16 +19,17 @@ import { MaizzleConfig } from "../types/config.mjs";
19
19
  * 4. CSS inliner
20
20
  * 5. Remove attributes
21
21
  * 6. Shorthand CSS
22
- * 7. Add attributes
23
- * 8. Filters
24
- * 9. Base URL
25
- * 10. URL query
26
- * 11. Purge CSS (serializes/parses internally around email-comb)
27
- * 12. Entities
22
+ * 7. Six-digit HEX
23
+ * 8. Add attributes
24
+ * 9. Filters
25
+ * 10. Base URL
26
+ * 11. URL query
27
+ * 12. Purge CSS (serializes/parses internally around email-comb)
28
+ * 13. Entities
28
29
  * + Vue-generated comments stripped here (on serialized string)
29
- * 13. Replace strings
30
- * 14. Prettify
31
- * 15. Minify
30
+ * 14. Replace strings
31
+ * 15. Prettify
32
+ * 16. Minify
32
33
  */
33
34
  declare function runTransformers(html: string, config: MaizzleConfig, filePath?: string, doctype?: string): Promise<string>;
34
35
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AAgDA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,WACA,OAAA,YACC,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AAkDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,WACA,OAAA,YACC,OAAA"}
@@ -8,6 +8,7 @@ import { attributeToStyle } from "./attributeToStyle.mjs";
8
8
  import { inlineCSS } from "./inlineCSS.mjs";
9
9
  import { removeAttributes } from "./removeAttributes.mjs";
10
10
  import { shorthandCSS } from "./shorthandCSS.mjs";
11
+ import { sixHex } from "./sixHex.mjs";
11
12
  import { addAttributes } from "./addAttributes.mjs";
12
13
  import { filters } from "./filters/index.mjs";
13
14
  import { base } from "./base.mjs";
@@ -37,16 +38,17 @@ import { minify } from "./minify.mjs";
37
38
  * 4. CSS inliner
38
39
  * 5. Remove attributes
39
40
  * 6. Shorthand CSS
40
- * 7. Add attributes
41
- * 8. Filters
42
- * 9. Base URL
43
- * 10. URL query
44
- * 11. Purge CSS (serializes/parses internally around email-comb)
45
- * 12. Entities
41
+ * 7. Six-digit HEX
42
+ * 8. Add attributes
43
+ * 9. Filters
44
+ * 10. Base URL
45
+ * 11. URL query
46
+ * 12. Purge CSS (serializes/parses internally around email-comb)
47
+ * 13. Entities
46
48
  * + Vue-generated comments stripped here (on serialized string)
47
- * 13. Replace strings
48
- * 14. Prettify
49
- * 15. Minify
49
+ * 14. Replace strings
50
+ * 15. Prettify
51
+ * 16. Minify
50
52
  */
51
53
  async function runTransformers(html, config, filePath, doctype) {
52
54
  let dom = parse(html);
@@ -57,6 +59,7 @@ async function runTransformers(html, config, filePath, doctype) {
57
59
  dom = inlineCSS(dom, config.css);
58
60
  dom = removeAttributes(dom, config.html?.attributes);
59
61
  dom = shorthandCSS(dom, config.css);
62
+ dom = sixHex(dom, config.css);
60
63
  dom = addAttributes(dom, config.html?.attributes);
61
64
  dom = filters(dom, config.filters);
62
65
  dom = base(dom, config.url);