@maizzle/framework 6.0.0-rc.14 → 6.0.0-rc.16

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 (67) hide show
  1. package/README.md +3 -3
  2. package/dist/components/Body.vue +9 -2
  3. package/dist/components/Button.vue +13 -8
  4. package/dist/components/Column.vue +22 -8
  5. package/dist/components/Container.vue +9 -5
  6. package/dist/components/Head.vue +1 -1
  7. package/dist/components/Html.vue +7 -2
  8. package/dist/components/Layout.vue +45 -15
  9. package/dist/components/Outlook.vue +38 -11
  10. package/dist/components/Overlap.vue +8 -3
  11. package/dist/components/Raw.vue +28 -0
  12. package/dist/components/Row.vue +72 -11
  13. package/dist/components/Section.vue +9 -5
  14. package/dist/components/Spacer.vue +9 -4
  15. package/dist/components/utils.d.mts +11 -1
  16. package/dist/components/utils.d.mts.map +1 -1
  17. package/dist/components/utils.mjs +11 -1
  18. package/dist/components/utils.mjs.map +1 -1
  19. package/dist/components/utils.ts +12 -0
  20. package/dist/composables/useOutlookFallback.d.mts +21 -0
  21. package/dist/composables/useOutlookFallback.d.mts.map +1 -0
  22. package/dist/composables/useOutlookFallback.mjs +30 -0
  23. package/dist/composables/useOutlookFallback.mjs.map +1 -0
  24. package/dist/index.d.mts +3 -1
  25. package/dist/index.mjs +3 -1
  26. package/dist/prepare.d.mts +17 -0
  27. package/dist/prepare.d.mts.map +1 -0
  28. package/dist/prepare.mjs +44 -0
  29. package/dist/prepare.mjs.map +1 -0
  30. package/dist/render/createRenderer.d.mts.map +1 -1
  31. package/dist/render/createRenderer.mjs +9 -75
  32. package/dist/render/createRenderer.mjs.map +1 -1
  33. package/dist/render/plugins/codeBlockExtract.d.mts +14 -0
  34. package/dist/render/plugins/codeBlockExtract.d.mts.map +1 -0
  35. package/dist/render/plugins/codeBlockExtract.mjs +34 -0
  36. package/dist/render/plugins/codeBlockExtract.mjs.map +1 -0
  37. package/dist/render/plugins/markdownExtract.d.mts +12 -0
  38. package/dist/render/plugins/markdownExtract.d.mts.map +1 -0
  39. package/dist/render/plugins/markdownExtract.mjs +50 -0
  40. package/dist/render/plugins/markdownExtract.mjs.map +1 -0
  41. package/dist/render/plugins/rawExtract.d.mts +14 -0
  42. package/dist/render/plugins/rawExtract.d.mts.map +1 -0
  43. package/dist/render/plugins/rawExtract.mjs +34 -0
  44. package/dist/render/plugins/rawExtract.mjs.map +1 -0
  45. package/dist/render/plugins/rowSourceLocation.d.mts +18 -0
  46. package/dist/render/plugins/rowSourceLocation.d.mts.map +1 -0
  47. package/dist/render/plugins/rowSourceLocation.mjs +45 -0
  48. package/dist/render/plugins/rowSourceLocation.mjs.map +1 -0
  49. package/dist/server/ui/App.vue +47 -2
  50. package/dist/server/ui/pages/Preview.vue +32 -3
  51. package/dist/transformers/columnWidth.d.mts +9 -9
  52. package/dist/transformers/columnWidth.d.mts.map +1 -1
  53. package/dist/transformers/columnWidth.mjs +422 -41
  54. package/dist/transformers/columnWidth.mjs.map +1 -1
  55. package/dist/transformers/filters/index.mjs +1 -1
  56. package/dist/transformers/filters/index.mjs.map +1 -1
  57. package/dist/transformers/index.mjs +1 -1
  58. package/dist/transformers/index.mjs.map +1 -1
  59. package/node_modules/maizzle/dist/commands/new.mjs +3 -3
  60. package/node_modules/maizzle/dist/index.d.mts +1 -0
  61. package/node_modules/maizzle/dist/index.mjs +3 -0
  62. package/node_modules/maizzle/package.json +3 -2
  63. package/node_modules/nypm/dist/cli.mjs +28 -5
  64. package/node_modules/nypm/dist/index.d.mts +0 -8
  65. package/node_modules/nypm/dist/index.mjs +27 -4
  66. package/node_modules/nypm/package.json +12 -12
  67. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  </picture>
8
8
  </a>
9
9
  </p>
10
- <p>Quickly build HTML emails with Tailwind CSS</p>
10
+ <p>The modern email development framework</p>
11
11
  <div>
12
12
 
13
13
  [![Version][npm-version-shield]][npm]
@@ -20,9 +20,9 @@
20
20
 
21
21
  ## About
22
22
 
23
- > **Note:** This repository contains the core code of the Maizzle framework. If you want to build HTML emails using Maizzle, visit the [Starter repository](https://github.com/maizzle/maizzle).
23
+ > **Note:** This repository contains the core code of the Maizzle framework. If you want to start building HTML emails using Maizzle, visit the [Starter repository](https://github.com/maizzle/maizzle).
24
24
 
25
- Maizzle is a framework that helps you quickly build HTML emails with [Tailwind CSS](https://tailwindcss.com/).
25
+ Maizzle is a Vite-powered framework for building HTML emails with Vue and Tailwind CSS.
26
26
 
27
27
  ## Documentation
28
28
 
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { createStaticVNode, inject, useAttrs, useSlots } from 'vue'
3
3
  import type { PropType } from 'vue'
4
+ import { outlookFallbackProp } from './utils.ts'
5
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
4
6
 
5
7
  defineOptions({ inheritAttrs: false })
6
8
 
@@ -65,9 +67,12 @@ const props = defineProps({
65
67
  ariaLabel: {
66
68
  type: String,
67
69
  default: undefined
68
- }
70
+ },
71
+ outlookFallback: outlookFallbackProp,
69
72
  })
70
73
 
74
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
75
+
71
76
  const htmlLang = inject<string>('htmlLang', 'en')
72
77
 
73
78
  const render = () => {
@@ -78,10 +83,12 @@ const render = () => {
78
83
  const lang = props.xmlLang ?? htmlLang
79
84
 
80
85
  const parts = [
81
- `xml:lang="${lang}"`,
82
86
  `dir="${props.dir}"`,
83
87
  'style="margin: 0; padding: 0; width: 100%; word-break: break-word;"',
84
88
  ]
89
+ if (outlookFallback) {
90
+ parts.unshift(`xml:lang="${lang}"`)
91
+ }
85
92
 
86
93
  if (extraAttrs) {
87
94
  parts.push(extraAttrs)
@@ -3,6 +3,8 @@ import { computed, useAttrs } from 'vue'
3
3
  import type { PropType } from 'vue'
4
4
  import { twMerge } from 'tailwind-merge'
5
5
  import Outlook from './Outlook.vue'
6
+ import { outlookFallbackProp } from './utils.ts'
7
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
6
8
 
7
9
  defineOptions({ inheritAttrs: false })
8
10
 
@@ -103,9 +105,12 @@ const props = defineProps({
103
105
  iconClass: {
104
106
  type: String,
105
107
  default: ''
106
- }
108
+ },
109
+ outlookFallback: outlookFallbackProp,
107
110
  })
108
111
 
112
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
113
+
109
114
  const parsedIconWidth = computed(() => parseInt(String(props.iconWidth), 10))
110
115
 
111
116
  const alignClass = computed(() => props.align ? ({
@@ -171,21 +176,21 @@ const mergedClass = computed(() => twMerge(defaultClasses.value, attrs.class as
171
176
  :class="mergedClass"
172
177
  >
173
178
  <template v-if="!isLink">
174
- <Outlook><i class="mso-font-width-[150%]" :style="`mso-text-raise: ${msoPb};`" hidden>&emsp;</i></Outlook>
179
+ <Outlook v-if="outlookFallback"><i class="mso-font-width-[150%]" :style="`mso-text-raise: ${msoPb};`" hidden>&emsp;</i></Outlook>
175
180
  <template v-if="icon && iconPosition === 'left'">
176
- <span :style="`mso-text-raise: ${msoPt}`">
181
+ <span :style="outlookFallback ? `mso-text-raise: ${msoPt}` : undefined">
177
182
  <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`" alt="">
178
183
  </span>
179
- <Outlook><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
184
+ <Outlook v-if="outlookFallback"><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
180
185
  </template>
181
- <span :class="icon ? (iconPosition === 'right' ? 'mr-2' : 'ml-2') : ''" :style="`mso-text-raise: ${msoPt}`"><slot /></span>
186
+ <span :class="icon ? (iconPosition === 'right' ? 'mr-2' : 'ml-2') : ''" :style="outlookFallback ? `mso-text-raise: ${msoPt}` : undefined"><slot /></span>
182
187
  <template v-if="icon && iconPosition === 'right'">
183
- <Outlook><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
184
- <span :style="`mso-text-raise: ${msoPt}`">
188
+ <Outlook v-if="outlookFallback"><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
189
+ <span :style="outlookFallback ? `mso-text-raise: ${msoPt}` : undefined">
185
190
  <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`" alt="">
186
191
  </span>
187
192
  </template>
188
- <Outlook><i class="mso-font-width-[150%]" hidden>&emsp;&#8203;</i></Outlook>
193
+ <Outlook v-if="outlookFallback"><i class="mso-font-width-[150%]" hidden>&emsp;&#8203;</i></Outlook>
189
194
  </template>
190
195
  <template v-else>
191
196
  <slot />
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, inject, useAttrs } from 'vue'
3
3
  import type { ComputedRef } from 'vue'
4
- import { nextId, normalizeToPixels } from './utils.ts'
4
+ import { twMerge } from 'tailwind-merge'
5
+ import { nextId, normalizeToPixels, outlookFallbackProp } from './utils.ts'
6
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
5
7
 
6
8
  defineOptions({ inheritAttrs: false })
7
9
 
@@ -29,9 +31,12 @@ const props = defineProps({
29
31
  msoStyle: {
30
32
  type: String,
31
33
  default: undefined
32
- }
34
+ },
35
+ outlookFallback: outlookFallbackProp,
33
36
  })
34
37
 
38
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
39
+
35
40
  const columnCount = inject<ComputedRef<number> | null>('columnCount', null)
36
41
 
37
42
  const count = computed(() => columnCount?.value ?? 2)
@@ -49,9 +54,17 @@ const msoWidth = computed(() => {
49
54
  return `__MAIZZLE_COLW_${colId}__`
50
55
  })
51
56
 
52
- const styles = computed(() =>
53
- `display: inline-block; min-width: ${minWidth.value}; font-size: 16px; vertical-align: top;`
54
- )
57
+ /**
58
+ * Baseline display/typography lives in classes not inline `:style` —
59
+ * so the user can override any of them via tailwind utilities. Inline
60
+ * `display: inline-block` would silently shadow a class like
61
+ * `inline-table` during CSS inlining; routing both through twMerge lets
62
+ * the user's utility cleanly replace ours instead of being dropped.
63
+ */
64
+ const baseClass = 'inline-block align-top text-base'
65
+ const mergedClass = computed(() => twMerge(baseClass, (attrs.class as string) ?? ''))
66
+
67
+ const styles = computed(() => `min-width: ${minWidth.value};`)
55
68
 
56
69
  const tdStyle = computed(() => {
57
70
  const parts = [`width: ${msoWidth.value}`, 'vertical-align: top']
@@ -71,14 +84,15 @@ const MsoAfter = () => createStaticVNode(
71
84
  </script>
72
85
 
73
86
  <template>
74
- <MsoBefore />
87
+ <MsoBefore v-if="outlookFallback" />
75
88
  <div
76
- v-bind="attrs"
89
+ v-bind="{ ...attrs, class: undefined }"
90
+ :class="mergedClass"
77
91
  :style="styles"
78
92
  :data-maizzle-cw-id="colId"
79
93
  :data-maizzle-cw-count="useMarker ? count : null"
80
94
  >
81
95
  <slot />
82
96
  </div>
83
- <MsoAfter />
97
+ <MsoAfter v-if="outlookFallback" />
84
98
  </template>
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, provide, createStaticVNode, useAttrs } from 'vue'
3
3
  import { twMerge } from 'tailwind-merge'
4
- import { hasWidthUtility, nextId, normalizeToPixels } from './utils.ts'
4
+ import { hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp } from './utils.ts'
5
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
5
6
 
6
7
  defineOptions({ inheritAttrs: false })
7
8
 
@@ -33,12 +34,15 @@ const props = defineProps({
33
34
  msoWidth: {
34
35
  type: [String, Number],
35
36
  default: null
36
- }
37
+ },
38
+ outlookFallback: outlookFallbackProp,
37
39
  })
38
40
 
41
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
42
+
39
43
  provide('containerWidth', computed(() => props.width))
40
44
 
41
- const useMarker = props.width == null && props.msoWidth == null
45
+ const useMarker = outlookFallback && props.width == null && props.msoWidth == null
42
46
  const msoId = useMarker ? nextId('c') : null
43
47
 
44
48
  const styles = computed(() => {
@@ -77,7 +81,7 @@ const MsoAfter = () => createStaticVNode(
77
81
  </script>
78
82
 
79
83
  <template>
80
- <MsoBefore />
84
+ <MsoBefore v-if="outlookFallback" />
81
85
  <div
82
86
  v-bind="{ ...attrs, class: undefined }"
83
87
  :class="mergedClass"
@@ -87,5 +91,5 @@ const MsoAfter = () => createStaticVNode(
87
91
  >
88
92
  <slot />
89
93
  </div>
90
- <MsoAfter />
94
+ <MsoAfter v-if="outlookFallback" />
91
95
  </template>
@@ -6,7 +6,7 @@ const props = defineProps({
6
6
  * Render an empty `<head>` before the main head element.
7
7
  *
8
8
  * This is a workaround for Yahoo! Mail on Android, which
9
- * strips styles from the first `<head>` element.
9
+ * strips the first `<head>` element it finds.
10
10
  *
11
11
  * @default false
12
12
  */
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { createStaticVNode, provide, useAttrs, useSlots } from 'vue'
3
3
  import type { PropType } from 'vue'
4
+ import { outlookFallbackProp } from './utils.ts'
5
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
4
6
 
5
7
  defineOptions({ inheritAttrs: false })
6
8
 
@@ -65,9 +67,12 @@ const props = defineProps({
65
67
  xmlns: {
66
68
  type: [Boolean, String],
67
69
  default: true
68
- }
70
+ },
71
+ outlookFallback: outlookFallbackProp,
69
72
  })
70
73
 
74
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
75
+
71
76
  provide('htmlLang', props.lang)
72
77
 
73
78
  const render = () => {
@@ -80,7 +85,7 @@ const render = () => {
80
85
  `dir="${props.dir}"`,
81
86
  ]
82
87
 
83
- if (props.xmlns !== false && props.xmlns !== 'false') {
88
+ if (outlookFallback && props.xmlns !== false && props.xmlns !== 'false') {
84
89
  parts.push(
85
90
  'xmlns:v="urn:schemas-microsoft-com:vml"',
86
91
  'xmlns:o="urn:schemas-microsoft-com:office:office"',
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { computed, useAttrs, type PropType } from 'vue'
2
+ import { computed, useAttrs, createStaticVNode, type PropType } from 'vue'
3
3
  import { twMerge } from 'tailwind-merge'
4
+ import { outlookFallbackProp } from './utils.ts'
5
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
4
6
 
5
7
  defineOptions({ inheritAttrs: false })
6
8
 
@@ -30,6 +32,18 @@ const props = defineProps({
30
32
  type: String as PropType<'ltr' | 'rtl'>,
31
33
  default: 'ltr'
32
34
  },
35
+ /**
36
+ * Render an empty `<head>` before the main head element.
37
+ *
38
+ * This is a workaround for Yahoo! Mail on Android, which
39
+ * strips the first `<head>` element it finds.
40
+ *
41
+ * @default false
42
+ */
43
+ doubleHead: {
44
+ type: [Boolean, String],
45
+ default: false
46
+ },
33
47
  /**
34
48
  * Accessible label for the email article wrapper.
35
49
  *
@@ -40,24 +54,20 @@ const props = defineProps({
40
54
  ariaLabel: {
41
55
  type: String,
42
56
  default: undefined
43
- }
57
+ },
58
+ outlookFallback: outlookFallbackProp,
44
59
  })
45
60
 
61
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
62
+
46
63
  const attrs = useAttrs()
47
64
  const bodyMergedClass = computed(() => twMerge('m-0 p-0 size-full [word-break:break-word]', props.bodyClass))
48
65
  const articleMergedClass = computed(() => twMerge('[font-size:max(16px,1rem)] font-inter', attrs.class as string))
49
- </script>
50
66
 
51
- <template>
52
- <html :lang="lang" :dir="dir" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
53
- <head>
54
- <meta charset="utf-8">
55
- <meta name="x-apple-disable-message-reformatting">
56
- <meta name="viewport" content="width=device-width, initial-scale=1">
57
- <meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
58
- <meta name="color-scheme" content="light dark">
59
- <meta name="supported-color-schemes" content="light dark">
60
- <!--[if mso]>
67
+ const EmptyHead = () => createStaticVNode('<head></head>', 1)
68
+
69
+ const MsoHead = () => createStaticVNode(
70
+ `<!--[if mso]>
61
71
  <noscript>
62
72
  <xml>
63
73
  <o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
@@ -69,7 +79,27 @@ const articleMergedClass = computed(() => twMerge('[font-size:max(16px,1rem)] fo
69
79
  td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
70
80
  .mso-break-all {word-break: break-all;}
71
81
  </style>
72
- <![endif]-->
82
+ <![endif]-->`,
83
+ 1
84
+ )
85
+
86
+ const htmlXmlns = computed(() => outlookFallback ? {
87
+ 'xmlns:v': 'urn:schemas-microsoft-com:vml',
88
+ 'xmlns:o': 'urn:schemas-microsoft-com:office:office',
89
+ } : {})
90
+ </script>
91
+
92
+ <template>
93
+ <html :lang="lang" :dir="dir" v-bind="htmlXmlns">
94
+ <EmptyHead v-if="props.doubleHead === true || props.doubleHead === 'true'" />
95
+ <head>
96
+ <meta charset="utf-8">
97
+ <meta name="x-apple-disable-message-reformatting">
98
+ <meta name="viewport" content="width=device-width, initial-scale=1">
99
+ <meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
100
+ <meta name="color-scheme" content="light dark">
101
+ <meta name="supported-color-schemes" content="light dark">
102
+ <MsoHead v-if="outlookFallback" />
73
103
  <link rel="preconnect" href="https://fonts.googleapis.com">
74
104
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
75
105
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" media="screen">
@@ -81,7 +111,7 @@ const articleMergedClass = computed(() => twMerge('[font-size:max(16px,1rem)] fo
81
111
  }
82
112
  </style>
83
113
  </head>
84
- <body :xml:lang="lang" :class="bodyMergedClass">
114
+ <body :xml:lang="outlookFallback ? lang : null" :class="bodyMergedClass">
85
115
  <div
86
116
  role="article"
87
117
  aria-roledescription="email"
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { computed, createStaticVNode } from 'vue'
2
+ import { computed, createStaticVNode, type PropType } from 'vue'
3
3
 
4
4
  const VERSION_MAP = {
5
5
  2003: 11,
@@ -10,7 +10,10 @@ const VERSION_MAP = {
10
10
  2019: 16
11
11
  }
12
12
 
13
- const toMso = (v: string) => VERSION_MAP[v]
13
+ type Year = `${keyof typeof VERSION_MAP}`
14
+ type YearList = Year | (string & {})
15
+
16
+ const toMso = (v: string) => VERSION_MAP[v as unknown as keyof typeof VERSION_MAP]
14
17
 
15
18
  const parseList = (value: string) =>
16
19
  value
@@ -30,7 +33,7 @@ export default {
30
33
  * @example '2013'
31
34
  * @example '2013,2016'
32
35
  */
33
- only: String,
36
+ only: String as PropType<YearList>,
34
37
  /**
35
38
  * Render content in all Outlook versions except the specified one(s).
36
39
  *
@@ -39,31 +42,55 @@ export default {
39
42
  * @example '2007'
40
43
  * @example '2007,2010'
41
44
  */
42
- not: String,
45
+ not: String as PropType<YearList>,
43
46
  /**
44
47
  * Render content in Outlook versions lower than the specified year.
45
48
  *
46
49
  * @example '2013'
47
50
  */
48
- lt: String,
51
+ lt: String as PropType<Year>,
49
52
  /**
50
53
  * Render content in Outlook versions lower than or equal to the specified year.
51
54
  *
52
55
  * @example '2013'
53
56
  */
54
- lte: String,
57
+ lte: String as PropType<Year>,
55
58
  /**
56
59
  * Render content in Outlook versions greater than the specified year.
57
60
  *
58
61
  * @example '2010'
59
62
  */
60
- gt: String,
63
+ gt: String as PropType<Year>,
61
64
  /**
62
65
  * Render content in Outlook versions greater than or equal to the specified year.
63
66
  *
64
67
  * @example '2010'
65
68
  */
66
- gte: String
69
+ gte: String as PropType<Year>,
70
+ /**
71
+ * Raw HTML inserted at the start of the conditional comment, before the slot.
72
+ *
73
+ * Bypasses Vue's template parser, so unbalanced tags are preserved — useful
74
+ * for MSO ghost tables where the opening `<table><tr><td>` must live inside
75
+ * the conditional comment.
76
+ *
77
+ * @example '<table align="center" width="600"><tr><td>'
78
+ */
79
+ open: {
80
+ type: String,
81
+ default: ''
82
+ },
83
+ /**
84
+ * Raw HTML inserted at the end of the conditional comment, after the slot.
85
+ *
86
+ * Pair with `open` to close ghost-table tags inside the conditional.
87
+ *
88
+ * @example '</td></tr></table>'
89
+ */
90
+ close: {
91
+ type: String,
92
+ default: ''
93
+ }
67
94
  },
68
95
  setup(props, { slots }) {
69
96
  const condition = computed(() => {
@@ -97,13 +124,13 @@ export default {
97
124
  return 'mso'
98
125
  })
99
126
 
100
- const start = computed(() => `<!--[if ${condition.value}]>`)
101
- const end = `<![endif]-->`
127
+ const start = computed(() => `<!--[if ${condition.value}]>${props.open}`)
128
+ const end = computed(() => `${props.close}<![endif]-->`)
102
129
 
103
130
  return () => [
104
131
  createStaticVNode(start.value, 1),
105
132
  slots.default?.(),
106
- createStaticVNode(end, 1),
133
+ createStaticVNode(end.value, 1),
107
134
  ]
108
135
  }
109
136
  }
@@ -6,8 +6,10 @@ import {
6
6
  hasWidthInStyle,
7
7
  hasWidthUtility,
8
8
  nextId,
9
- normalizeToPixels
9
+ normalizeToPixels,
10
+ outlookFallbackProp
10
11
  } from './utils.ts'
12
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
11
13
 
12
14
  defineOptions({ inheritAttrs: false })
13
15
 
@@ -60,8 +62,11 @@ const props = defineProps({
60
62
  type: String,
61
63
  default: '0,-60px,0,0'
62
64
  },
65
+ outlookFallback: outlookFallbackProp,
63
66
  })
64
67
 
68
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
69
+
65
70
  const userStyle = computed(() => {
66
71
  const s = attrs.style
67
72
  if (!s) return ''
@@ -132,9 +137,9 @@ const VmlAfter = () => createStaticVNode('<!--[if mso]></v:textbox></v:rect><![e
132
137
  <table style="max-height: 0; position: relative; opacity: 0.999;">
133
138
  <tr>
134
139
  <td :style="tdStyle">
135
- <VmlBefore />
140
+ <VmlBefore v-if="outlookFallback" />
136
141
  <slot name="overlay" />
137
- <VmlAfter />
142
+ <VmlAfter v-if="outlookFallback" />
138
143
  </td>
139
144
  </tr>
140
145
  </table>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+
4
+ export default {
5
+ inheritAttrs: false,
6
+ props: {
7
+ /**
8
+ * Raw content to emit verbatim.
9
+ *
10
+ * Auto-populated from slot content by
11
+ * the `maizzle:raw-extract` Vite plugin
12
+ * before Vue compiles the template,
13
+ * so `{{ }}` and other Vue/ESP
14
+ * syntax pass through untouched.
15
+ */
16
+ content: {
17
+ type: String,
18
+ default: '',
19
+ },
20
+ },
21
+ setup(props) {
22
+ if (!props.content) {
23
+ return () => createStaticVNode('', 0)
24
+ }
25
+ return () => createStaticVNode(props.content, 1)
26
+ },
27
+ }
28
+ </script>
@@ -1,7 +1,14 @@
1
+ <script lang="ts">
2
+ const warnedLocations = new Set<string>()
3
+ </script>
4
+
1
5
  <script setup lang="ts">
2
- import { Comment, computed, createStaticVNode, provide, useAttrs, useSlots, Fragment } from 'vue'
6
+ import { Comment, Text, computed, createStaticVNode, provide, useAttrs, useSlots, Fragment } from 'vue'
3
7
  import type { VNode } from 'vue'
4
- import { hasWidthInStyle, hasWidthUtility, normalizeToPixels } from './utils.ts'
8
+ import { twMerge } from 'tailwind-merge'
9
+ import Column from './Column.vue'
10
+ import { hasWidthInStyle, hasWidthUtility, normalizeToPixels, outlookFallbackProp } from './utils.ts'
11
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
5
12
 
6
13
  defineOptions({ inheritAttrs: false })
7
14
 
@@ -30,9 +37,12 @@ const props = defineProps({
30
37
  cols: {
31
38
  type: Number,
32
39
  default: null
33
- }
40
+ },
41
+ outlookFallback: outlookFallbackProp,
34
42
  })
35
43
 
44
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
45
+
36
46
  const slots = useSlots()
37
47
 
38
48
  function countChildren(vnodes: VNode[]): number {
@@ -49,6 +59,41 @@ function countChildren(vnodes: VNode[]): number {
49
59
  return count
50
60
  }
51
61
 
62
+ function hasColumnChild(vnodes: VNode[]): boolean {
63
+ for (const vnode of vnodes) {
64
+ if (vnode.type === Fragment && Array.isArray(vnode.children)) {
65
+ if (hasColumnChild(vnode.children as VNode[])) return true
66
+ } else if (vnode.type === Column) {
67
+ return true
68
+ } else if (
69
+ typeof vnode.type === 'object'
70
+ && vnode.type !== null
71
+ && '__name' in vnode.type
72
+ && (vnode.type as { __name?: string }).__name === 'Column'
73
+ ) {
74
+ return true
75
+ }
76
+ }
77
+ return false
78
+ }
79
+
80
+ function hasMeaningfulContent(vnodes: VNode[]): boolean {
81
+ for (const vnode of vnodes) {
82
+ if (vnode.type === Comment) continue
83
+ if (vnode.type === Fragment && Array.isArray(vnode.children)) {
84
+ if (hasMeaningfulContent(vnode.children as VNode[])) return true
85
+ continue
86
+ }
87
+ if (vnode.type === Text) {
88
+ if (typeof vnode.children === 'string' && vnode.children.trim()) return true
89
+ continue
90
+ }
91
+ if (typeof vnode.type === 'symbol') continue
92
+ return true
93
+ }
94
+ return false
95
+ }
96
+
52
97
  const columnCount = computed(() => {
53
98
  if (props.cols) return props.cols
54
99
 
@@ -78,15 +123,20 @@ const colWidthSource = computed(() => {
78
123
  })
79
124
 
80
125
  const restAttrs = computed(() => {
81
- const { style: _, ...rest } = attrs
126
+ const { style: _, class: __, 'data-maizzle-loc': ___, ...rest } = attrs
82
127
  return rest
83
128
  })
84
129
 
85
- const divStyle = computed(() => {
86
- const parts: string[] = ['font-size: 0;']
87
- if (userStyle.value) parts.push(userStyle.value)
88
- return parts.join(' ')
89
- })
130
+ /**
131
+ * `font-size: 0;` removes the whitespace gap between inline-block
132
+ * children. Lives in a class so users can override (e.g. via a custom
133
+ * `text-*`) and twMerge resolves the conflict cleanly instead of the
134
+ * inline declaration silently shadowing the user's class.
135
+ */
136
+ const baseClass = 'text-0'
137
+ const mergedClass = computed(() => twMerge(baseClass, (attrs.class as string) ?? ''))
138
+
139
+ const divStyle = computed(() => userStyle.value || undefined)
90
140
 
91
141
  const MsoBefore = () => createStaticVNode(
92
142
  '<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: 100%"><tr><![endif]-->',
@@ -97,16 +147,27 @@ const MsoAfter = () => createStaticVNode(
97
147
  '<!--[if mso]></tr></table><![endif]-->',
98
148
  1
99
149
  )
150
+
151
+ const initialChildren = slots.default?.() ?? []
152
+ if (outlookFallback && hasMeaningfulContent(initialChildren) && !hasColumnChild(initialChildren)) {
153
+ const loc = (attrs['data-maizzle-loc'] as string | undefined) ?? '<unknown location>'
154
+ if (!warnedLocations.has(loc)) {
155
+ warnedLocations.add(loc)
156
+ const display = loc.split('/').pop() ?? loc
157
+ console.warn(`[maizzle] <Row> in ${display} has no <Column> inside it. Layout will break in Outlook.`)
158
+ }
159
+ }
100
160
  </script>
101
161
 
102
162
  <template>
103
- <MsoBefore />
163
+ <MsoBefore v-if="outlookFallback" />
104
164
  <div
105
165
  v-bind="restAttrs"
166
+ :class="mergedClass"
106
167
  :style="divStyle"
107
168
  :data-maizzle-cw="colWidthSource"
108
169
  >
109
170
  <slot />
110
171
  </div>
111
- <MsoAfter />
172
+ <MsoAfter v-if="outlookFallback" />
112
173
  </template>