@maizzle/framework 6.0.0-rc.21 → 6.0.0-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Body.vue +1 -1
- package/dist/components/CodeBlock.vue +1 -1
- package/dist/components/CodeInline.vue +72 -2
- package/dist/components/Column.vue +2 -1
- package/dist/components/Container.vue +1 -11
- package/dist/components/Img.vue +199 -4
- package/dist/components/Preheader.vue +33 -5
- package/dist/components/Section.vue +9 -14
- package/dist/components/Text.vue +1 -1
- package/dist/composables/defineConfig.d.ts +3 -4
- package/dist/composables/defineConfig.d.ts.map +1 -1
- package/dist/composables/defineConfig.js +3 -4
- package/dist/composables/defineConfig.js.map +1 -1
- package/dist/composables/renderContext.d.ts +0 -1
- package/dist/composables/renderContext.d.ts.map +1 -1
- package/dist/composables/renderContext.js.map +1 -1
- package/dist/composables/usePreheader.d.ts +6 -5
- package/dist/composables/usePreheader.d.ts.map +1 -1
- package/dist/composables/usePreheader.js +3 -3
- package/dist/composables/usePreheader.js.map +1 -1
- package/dist/composables/useTransformers.d.ts +1 -1
- package/dist/composables/useTransformers.js +1 -1
- package/dist/composables/useTransformers.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/render/createRenderer.js +2 -2
- package/dist/render/createRenderer.js.map +1 -1
- package/dist/transformers/addAttributes.d.ts +18 -8
- package/dist/transformers/addAttributes.d.ts.map +1 -1
- package/dist/transformers/addAttributes.js +22 -8
- package/dist/transformers/addAttributes.js.map +1 -1
- package/dist/transformers/columnWidth.d.ts.map +1 -1
- package/dist/transformers/columnWidth.js +136 -150
- package/dist/transformers/columnWidth.js.map +1 -1
- package/dist/transformers/entities.d.ts.map +1 -1
- package/dist/transformers/entities.js +1 -0
- package/dist/transformers/entities.js.map +1 -1
- package/dist/transformers/index.d.ts.map +1 -1
- package/dist/transformers/index.js +7 -5
- package/dist/transformers/index.js.map +1 -1
- package/dist/transformers/inlineCss.js +2 -7
- package/dist/transformers/inlineCss.js.map +1 -1
- package/dist/transformers/minifyCodeInline.d.ts +29 -0
- package/dist/transformers/minifyCodeInline.d.ts.map +1 -0
- package/dist/transformers/minifyCodeInline.js +36 -0
- package/dist/transformers/minifyCodeInline.js.map +1 -0
- package/dist/transformers/msoPlaceholders.d.ts +10 -5
- package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
- package/dist/transformers/msoPlaceholders.js +38 -7
- package/dist/transformers/msoPlaceholders.js.map +1 -1
- package/dist/transformers/safeSelectors.d.ts +37 -0
- package/dist/transformers/safeSelectors.d.ts.map +1 -0
- package/dist/transformers/{safeClassNames.js → safeSelectors.js} +24 -5
- package/dist/transformers/safeSelectors.js.map +1 -0
- package/dist/transformers/shorthandCss.js +38 -7
- package/dist/transformers/shorthandCss.js.map +1 -1
- package/dist/types/config.d.ts +2 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/ast/serializer.d.ts.map +1 -1
- package/dist/utils/ast/serializer.js +27 -17
- package/dist/utils/ast/serializer.js.map +1 -1
- package/dist/utils/cssBox.d.ts +42 -0
- package/dist/utils/cssBox.d.ts.map +1 -0
- package/dist/utils/cssBox.js +156 -0
- package/dist/utils/cssBox.js.map +1 -0
- package/package.json +1 -1
- package/dist/transformers/safeClassNames.d.ts +0 -22
- package/dist/transformers/safeClassNames.d.ts.map +0 -1
- package/dist/transformers/safeClassNames.js.map +0 -1
package/dist/components/Body.vue
CHANGED
|
@@ -94,7 +94,7 @@ const render = () => {
|
|
|
94
94
|
|
|
95
95
|
const parts = [
|
|
96
96
|
`dir="${props.dir}"`,
|
|
97
|
-
'style="margin: 0; padding: 0; width: 100%; word-break: break-word;"',
|
|
97
|
+
'style="margin: 0; padding: 0; width: 100%; height: 100%; word-break: break-word;"',
|
|
98
98
|
]
|
|
99
99
|
if (outlookFallback) {
|
|
100
100
|
parts.unshift(`xml:lang="${lang}"`)
|
|
@@ -61,7 +61,7 @@ export default {
|
|
|
61
61
|
const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
|
|
62
62
|
const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
|
|
63
63
|
|
|
64
|
-
const html = `<table class="w-full"><tr><td class="${props.tdClass}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
|
|
64
|
+
const html = `<table class="w-full"><tr><td class="${props.tdClass}" style="background-color:${bg}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
|
|
65
65
|
|
|
66
66
|
return () => createStaticVNode(html, 1)
|
|
67
67
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { createStaticVNode } from 'vue'
|
|
2
|
+
import { createStaticVNode, type PropType } from 'vue'
|
|
3
|
+
import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
inheritAttrs: false,
|
|
@@ -13,9 +14,27 @@ export default {
|
|
|
13
14
|
code: {
|
|
14
15
|
type: String,
|
|
15
16
|
default: ''
|
|
17
|
+
},
|
|
18
|
+
/**
|
|
19
|
+
* Language for syntax highlighting. Only consulted when `theme` is set.
|
|
20
|
+
* @default 'html'
|
|
21
|
+
*/
|
|
22
|
+
language: {
|
|
23
|
+
type: String as PropType<BundledLanguage>,
|
|
24
|
+
default: 'html'
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Shiki theme to apply. When set, the inline code is syntax-highlighted
|
|
28
|
+
* with this theme and the cell uses the theme's background color.
|
|
29
|
+
* When unset, falls back to the plain gray-styled `<code>` (no Shiki
|
|
30
|
+
* pass, faster, and visually quieter in body copy).
|
|
31
|
+
*/
|
|
32
|
+
theme: {
|
|
33
|
+
type: String as PropType<BundledTheme | undefined>,
|
|
34
|
+
default: undefined
|
|
16
35
|
}
|
|
17
36
|
},
|
|
18
|
-
setup(props, { slots, attrs }) {
|
|
37
|
+
async setup(props, { slots, attrs }) {
|
|
19
38
|
let source = props.code
|
|
20
39
|
|
|
21
40
|
if (!source) {
|
|
@@ -32,6 +51,57 @@ export default {
|
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
const classes = attrs.class ? ` class="${attrs.class}"` : ''
|
|
54
|
+
|
|
55
|
+
if (props.theme) {
|
|
56
|
+
const highlighted = await codeToHtml(source, {
|
|
57
|
+
lang: props.language,
|
|
58
|
+
theme: props.theme,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const hl = await getSingletonHighlighter({ themes: [props.theme], langs: [] })
|
|
62
|
+
const bg = hl.getTheme(props.theme).bg
|
|
63
|
+
|
|
64
|
+
const codeContent = highlighted
|
|
65
|
+
.replace(/^<pre[^>]*><code>/, '')
|
|
66
|
+
.replace(/<\/code><\/pre>$/, '')
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Replace shiki's structural `<`/`>` (the `<span>` tag delimiters)
|
|
70
|
+
* with private string markers `§MZLT§`/`§MZGT§`. Source-level
|
|
71
|
+
* entities like `<` (representing a literal `<` in the user's
|
|
72
|
+
* code) are made of `&`, `l`, `t`, `;` — no real `<` character —
|
|
73
|
+
* so they pass through untouched.
|
|
74
|
+
*
|
|
75
|
+
* Why markers and not HTML entities? Both levels of escaping would
|
|
76
|
+
* end up as `<` after a round-trip, and the decoder couldn't
|
|
77
|
+
* tell which to decode back to a real `<` (structural) vs leave
|
|
78
|
+
* as `<` (content). Using non-entity markers makes the two
|
|
79
|
+
* levels distinguishable: only `§MZ*§` gets decoded.
|
|
80
|
+
*
|
|
81
|
+
* Two pipeline passes that would otherwise mangle the shiki HTML
|
|
82
|
+
* are defused by the markers:
|
|
83
|
+
* - `format` (oxfmt with `htmlWhitespaceSensitivity: 'ignore'`)
|
|
84
|
+
* sees the `<code>` body as plain text and won't reflow the
|
|
85
|
+
* chain of `<span>` tokens onto separate lines.
|
|
86
|
+
* - The HTML5 self-close strip (`( \/>)` regex at the end of
|
|
87
|
+
* the pipeline) won't match anything inside a shiki cell,
|
|
88
|
+
* so a highlighted Vue self-closing tag like `<MyTag />`
|
|
89
|
+
* keeps its ` />` instead of being silently shortened to `>`.
|
|
90
|
+
*
|
|
91
|
+
* `minifyCodeInline` swaps the markers back to real angle brackets
|
|
92
|
+
* at the very end of the pipeline, after both passes have run.
|
|
93
|
+
*/
|
|
94
|
+
const escaped = codeContent
|
|
95
|
+
.replace(/</g, '§MZLT§')
|
|
96
|
+
.replace(/>/g, '§MZGT§')
|
|
97
|
+
|
|
98
|
+
const baseStyles = `background-color:${bg};border-radius:6px;padding:2px 6px;font-size:11px`
|
|
99
|
+
const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
|
|
100
|
+
|
|
101
|
+
const html = `<code${classes} style="${styles}" data-minify-inline>${escaped}</code>`
|
|
102
|
+
return () => createStaticVNode(html, 1)
|
|
103
|
+
}
|
|
104
|
+
|
|
35
105
|
const baseStyles = 'white-space:normal;border-radius:6px;border:1px solid #d1d5db;background-color:#f3f4f6;padding:2px 6px;font-size:11px;color:inherit'
|
|
36
106
|
const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
|
|
37
107
|
|
|
@@ -71,13 +71,14 @@ const msoWidth = computed(() => {
|
|
|
71
71
|
* `inline-table` during CSS inlining; routing both through twMerge lets
|
|
72
72
|
* the user's utility cleanly replace ours instead of being dropped.
|
|
73
73
|
*/
|
|
74
|
-
const baseClass = 'inline-block align-top text-
|
|
74
|
+
const baseClass = 'inline-block align-top text-[medium]'
|
|
75
75
|
const mergedClass = computed(() => twMerge(baseClass, (attrs.class as string) ?? ''))
|
|
76
76
|
|
|
77
77
|
const styles = computed(() => `min-width: ${minWidth.value};`)
|
|
78
78
|
|
|
79
79
|
const tdStyle = computed(() => {
|
|
80
80
|
const parts = [`width: ${msoWidth.value}`, 'vertical-align: top']
|
|
81
|
+
if (useMarker) parts.push(`__MAIZZLE_COLTDX_${colId}__`)
|
|
81
82
|
if (props.msoStyle) parts.push(props.msoStyle)
|
|
82
83
|
return parts.join('; ')
|
|
83
84
|
})
|
|
@@ -26,15 +26,6 @@ const props = defineProps({
|
|
|
26
26
|
type: [String, Number],
|
|
27
27
|
default: null
|
|
28
28
|
},
|
|
29
|
-
/**
|
|
30
|
-
* Override the Outlook (MSO) table width independently of the
|
|
31
|
-
* div's width. Highest priority — wins over `width` and any
|
|
32
|
-
* class-derived value.
|
|
33
|
-
*/
|
|
34
|
-
msoWidth: {
|
|
35
|
-
type: [String, Number],
|
|
36
|
-
default: null
|
|
37
|
-
},
|
|
38
29
|
/**
|
|
39
30
|
* Inline CSS applied only to the MSO `<td>` element.
|
|
40
31
|
*
|
|
@@ -65,7 +56,7 @@ const outlookFallback = useOutlookFallback(props.outlookFallback)
|
|
|
65
56
|
|
|
66
57
|
provide('containerWidth', computed(() => props.width))
|
|
67
58
|
|
|
68
|
-
const useMarker = outlookFallback && props.width == null
|
|
59
|
+
const useMarker = outlookFallback && props.width == null
|
|
69
60
|
const msoId = useMarker ? nextId('c') : null
|
|
70
61
|
const tdId = outlookFallback ? nextId('ct') : null
|
|
71
62
|
|
|
@@ -84,7 +75,6 @@ const mergedClass = computed(() => {
|
|
|
84
75
|
})
|
|
85
76
|
|
|
86
77
|
const msoWidth = computed(() => {
|
|
87
|
-
if (props.msoWidth != null) return normalizeToPixels(props.msoWidth)
|
|
88
78
|
if (props.width != null) return normalizeToPixels(props.width)
|
|
89
79
|
return `__MAIZZLE_MSOW_${msoId}__`
|
|
90
80
|
})
|
package/dist/components/Img.vue
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, useAttrs } from 'vue'
|
|
2
|
+
import { computed, createStaticVNode, useAttrs, type PropType } from 'vue'
|
|
3
|
+
import { outlookFallbackProp } from './utils.ts'
|
|
4
|
+
import { useOutlookFallback } from '../composables/useOutlookFallback'
|
|
5
|
+
|
|
6
|
+
type AspectRatio = '1:1' | '4:3' | '3:2' | '16:9' | '21:9' | '2:1' | '3:4' | '9:16' | (string & {})
|
|
7
|
+
type BackgroundPosition =
|
|
8
|
+
| 'top' | 'right' | 'bottom' | 'left' | 'center'
|
|
9
|
+
| 'top left' | 'top right' | 'top center'
|
|
10
|
+
| 'bottom left' | 'bottom right' | 'bottom center'
|
|
11
|
+
| 'center left' | 'center right' | 'center center'
|
|
12
|
+
| (string & {})
|
|
13
|
+
type BackgroundSize = 'cover' | 'contain' | 'auto' | (string & {})
|
|
3
14
|
|
|
4
15
|
defineOptions({ inheritAttrs: false })
|
|
5
16
|
|
|
@@ -30,9 +41,61 @@ const props = defineProps({
|
|
|
30
41
|
motionSrc: {
|
|
31
42
|
type: String,
|
|
32
43
|
default: null
|
|
33
|
-
}
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Aspect ratio for cropped images.
|
|
47
|
+
*
|
|
48
|
+
* Accepts colon or slash form: `'16:9'`, `'16/9'`, `'4:3'`, `'1:1'`, etc.
|
|
49
|
+
*
|
|
50
|
+
* Alternatively, set a Tailwind aspect class on the component:
|
|
51
|
+
* `aspect-square`, `aspect-video`, `aspect-[16/9]`, `aspect-3/2`. The
|
|
52
|
+
* prop wins when both are provided.
|
|
53
|
+
*
|
|
54
|
+
* @example '16:9'
|
|
55
|
+
* @example '4:3'
|
|
56
|
+
* @example '1:1'
|
|
57
|
+
*/
|
|
58
|
+
aspect: {
|
|
59
|
+
type: String as PropType<AspectRatio>,
|
|
60
|
+
default: ''
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* CSS `background-position` for the cropped image fill.
|
|
64
|
+
*
|
|
65
|
+
* @default 'center'
|
|
66
|
+
* @example 'top'
|
|
67
|
+
* @example 'top left'
|
|
68
|
+
* @example '20% 30%'
|
|
69
|
+
*/
|
|
70
|
+
position: {
|
|
71
|
+
type: String as PropType<BackgroundPosition>,
|
|
72
|
+
default: 'center'
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* CSS `background-size` for the cropped image fill.
|
|
76
|
+
*
|
|
77
|
+
* @default 'cover'
|
|
78
|
+
* @example 'contain'
|
|
79
|
+
* @example 'auto'
|
|
80
|
+
*/
|
|
81
|
+
size: {
|
|
82
|
+
type: String as PropType<BackgroundSize>,
|
|
83
|
+
default: 'cover'
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Toggle Outlook (MSO) and VML fallback markup for this image.
|
|
87
|
+
*
|
|
88
|
+
* Only relevant in cropped mode (`aspect`). When `false`, the VML
|
|
89
|
+
* `<v:rect>` shape is skipped and the modern padding-hack div renders
|
|
90
|
+
* to all clients including Outlook (which will show an empty area).
|
|
91
|
+
*
|
|
92
|
+
* @default true
|
|
93
|
+
*/
|
|
94
|
+
outlookFallback: outlookFallbackProp,
|
|
34
95
|
})
|
|
35
96
|
|
|
97
|
+
const outlookFallback = useOutlookFallback(props.outlookFallback)
|
|
98
|
+
|
|
36
99
|
function mimeFromExtension(src: string): string {
|
|
37
100
|
const ext = src.split('.').pop()?.toLowerCase() ?? ''
|
|
38
101
|
|
|
@@ -51,17 +114,149 @@ function mimeFromExtension(src: string): string {
|
|
|
51
114
|
return types[ext] ?? ''
|
|
52
115
|
}
|
|
53
116
|
|
|
117
|
+
const ASPECT_KEYWORDS: Record<string, string> = {
|
|
118
|
+
'aspect-square': '1/1',
|
|
119
|
+
'aspect-video': '16/9',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeClass(value: unknown): string {
|
|
123
|
+
if (!value) return ''
|
|
124
|
+
if (typeof value === 'string') return value
|
|
125
|
+
if (Array.isArray(value)) return value.map(normalizeClass).filter(Boolean).join(' ')
|
|
126
|
+
if (typeof value === 'object') {
|
|
127
|
+
return Object.entries(value as Record<string, unknown>)
|
|
128
|
+
.filter(([, v]) => v)
|
|
129
|
+
.map(([k]) => k)
|
|
130
|
+
.join(' ')
|
|
131
|
+
}
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pull Tailwind `aspect-*` tokens out of the inherited class list. Returns
|
|
137
|
+
* both the derived ratio (first match wins) and the cleaned class string
|
|
138
|
+
* so the aspect token isn't duplicated on the wrapper.
|
|
139
|
+
*/
|
|
140
|
+
const parsedClass = computed(() => {
|
|
141
|
+
const tokens = normalizeClass(attrs.class).split(/\s+/).filter(Boolean)
|
|
142
|
+
let ratio: string | null = null
|
|
143
|
+
const rest: string[] = []
|
|
144
|
+
for (const t of tokens) {
|
|
145
|
+
if (ASPECT_KEYWORDS[t]) {
|
|
146
|
+
if (!ratio) ratio = ASPECT_KEYWORDS[t]
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
const m = t.match(/^aspect-(?:\[(\d+(?:\.\d+)?)[/:](\d+(?:\.\d+)?)\]|(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?))$/)
|
|
150
|
+
if (m) {
|
|
151
|
+
if (!ratio) ratio = `${m[1] ?? m[3]}/${m[2] ?? m[4]}`
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
rest.push(t)
|
|
155
|
+
}
|
|
156
|
+
return { ratio, className: rest.join(' ') }
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const resolvedAspect = computed(() => props.aspect || parsedClass.value.ratio || '')
|
|
160
|
+
|
|
161
|
+
const ratio = computed(() => {
|
|
162
|
+
if (!resolvedAspect.value) return null
|
|
163
|
+
const [w, h] = resolvedAspect.value.split(/[:/]/).map(Number)
|
|
164
|
+
if (!w || !h || !Number.isFinite(w) || !Number.isFinite(h)) return null
|
|
165
|
+
const pct = ((h / w) * 100).toFixed(4).replace(/\.?0+$/, '')
|
|
166
|
+
return { w, h, paddingBottom: `${pct}%` }
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const isCropped = computed(() => ratio.value !== null)
|
|
170
|
+
|
|
54
171
|
const motionType = computed(() => mimeFromExtension(props.motionSrc ?? ''))
|
|
55
172
|
|
|
56
173
|
const imgWidth = computed(() => Number.parseInt(String(props.width), 10))
|
|
57
174
|
|
|
58
|
-
const
|
|
175
|
+
const heightPx = computed(() =>
|
|
176
|
+
ratio.value && Number.isFinite(imgWidth.value)
|
|
177
|
+
? Math.round((imgWidth.value * ratio.value.h) / ratio.value.w)
|
|
178
|
+
: null
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const usePicture = computed(() => !isCropped.value && (props.darkSrc || props.motionSrc))
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Escape characters that break Tailwind's `bg-[url('...')]` arbitrary value
|
|
185
|
+
* (the closing `']`, braces, spaces) and the `url()` wrapper itself (quotes,
|
|
186
|
+
* parens). Targeted replace so already-encoded URLs aren't double-encoded.
|
|
187
|
+
*
|
|
188
|
+
* Only used for the dark/motion variant classes — those have to be Tailwind
|
|
189
|
+
* arbitrary classes so they compile to `@media` rules. The base background
|
|
190
|
+
* image is set inline via `:style` to avoid the CSS pipeline rewriting it.
|
|
191
|
+
*/
|
|
192
|
+
const escapeForClass = (url: string) => url
|
|
193
|
+
.replace(/'/g, '%27')
|
|
194
|
+
.replace(/\(/g, '%28')
|
|
195
|
+
.replace(/\)/g, '%29')
|
|
196
|
+
.replace(/ /g, '%20')
|
|
197
|
+
.replace(/\]/g, '%5D')
|
|
198
|
+
.replace(/\}/g, '%7D')
|
|
199
|
+
|
|
200
|
+
/** Escape a URL for safe use inside `url('...')` in an inline style. */
|
|
201
|
+
const escapeForCssUrl = (s: string) => s
|
|
202
|
+
.replace(/\\/g, '\\\\')
|
|
203
|
+
.replace(/'/g, "\\'")
|
|
204
|
+
|
|
205
|
+
const escapeAttr = (s: string) => s
|
|
206
|
+
.replace(/&/g, '&')
|
|
207
|
+
.replace(/"/g, '"')
|
|
208
|
+
.replace(/</g, '<')
|
|
209
|
+
.replace(/>/g, '>')
|
|
210
|
+
|
|
211
|
+
const vmlAspect = computed(() => {
|
|
212
|
+
if (props.size === 'cover') return 'atleast'
|
|
213
|
+
if (props.size === 'contain') return 'atmost'
|
|
214
|
+
return ''
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const VmlRect = () => {
|
|
218
|
+
if (!isCropped.value || !heightPx.value || !Number.isFinite(imgWidth.value)) return null
|
|
219
|
+
const aspectAttr = vmlAspect.value ? ` aspect="${vmlAspect.value}"` : ''
|
|
220
|
+
return createStaticVNode(
|
|
221
|
+
`<!--[if mso]><v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:${imgWidth.value}px;height:${heightPx.value}px;"><v:fill type="frame" src="${escapeAttr(props.src)}"${aspectAttr} /></v:rect><![endif]-->`,
|
|
222
|
+
1
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const NotMsoBefore = () => createStaticVNode('<!--[if !mso]><!-->', 1)
|
|
227
|
+
const NotMsoAfter = () => createStaticVNode('<!--<![endif]-->', 1)
|
|
59
228
|
|
|
60
229
|
const imgStyle = 'max-width: 100%; vertical-align: middle;'
|
|
61
230
|
</script>
|
|
62
231
|
|
|
63
232
|
<template>
|
|
64
|
-
<
|
|
233
|
+
<template v-if="isCropped">
|
|
234
|
+
<VmlRect v-if="outlookFallback" />
|
|
235
|
+
<NotMsoBefore v-if="outlookFallback" />
|
|
236
|
+
<div
|
|
237
|
+
v-bind="{ ...attrs, class: undefined }"
|
|
238
|
+
role="img"
|
|
239
|
+
:aria-label="alt || undefined"
|
|
240
|
+
:class="['overflow-hidden table', parsedClass.className]"
|
|
241
|
+
:style="`width: ${imgWidth}px; max-width: 100%;`"
|
|
242
|
+
>
|
|
243
|
+
<div
|
|
244
|
+
:class="[
|
|
245
|
+
'table-cell w-full h-0 bg-no-repeat',
|
|
246
|
+
darkSrc ? `dark:bg-[url('${escapeForClass(darkSrc)}')]!` : '',
|
|
247
|
+
motionSrc ? `motion-safe:bg-[url('${escapeForClass(motionSrc)}')]!` : '',
|
|
248
|
+
]"
|
|
249
|
+
:style="{
|
|
250
|
+
paddingBottom: ratio!.paddingBottom,
|
|
251
|
+
backgroundImage: `url('${escapeForCssUrl(src)}')`,
|
|
252
|
+
backgroundSize: size,
|
|
253
|
+
backgroundPosition: position,
|
|
254
|
+
}"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
<NotMsoAfter v-if="outlookFallback" />
|
|
258
|
+
</template>
|
|
259
|
+
<picture v-else-if="usePicture">
|
|
65
260
|
<source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
|
|
66
261
|
<source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
|
|
67
262
|
<img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :style="imgStyle">
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { useSlots, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
/**
|
|
6
|
+
* Explicit number of filler sequences to render. When omitted, the count
|
|
7
|
+
* is auto-derived to fill our default 200-char inbox preview budget.
|
|
8
|
+
*/
|
|
4
9
|
spaces: {
|
|
5
10
|
type: Number,
|
|
6
|
-
default:
|
|
7
|
-
}
|
|
11
|
+
default: undefined,
|
|
12
|
+
},
|
|
8
13
|
})
|
|
14
|
+
|
|
15
|
+
const slots = useSlots()
|
|
16
|
+
|
|
17
|
+
function vnodesToText(nodes: unknown): string {
|
|
18
|
+
if (nodes == null || nodes === false || nodes === true) return ''
|
|
19
|
+
if (typeof nodes === 'string' || typeof nodes === 'number') return String(nodes)
|
|
20
|
+
if (Array.isArray(nodes)) return nodes.map(vnodesToText).join('')
|
|
21
|
+
if (typeof nodes === 'object' && 'children' in (nodes as Record<string, unknown>)) {
|
|
22
|
+
return vnodesToText((nodes as { children: unknown }).children)
|
|
23
|
+
}
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Inbox preview budget. Pad with invisible fillers so the
|
|
28
|
+
// client doesn't pull body content into the snippet.
|
|
29
|
+
const PREVIEW_LENGTH = 200
|
|
30
|
+
|
|
31
|
+
const text = computed(() => vnodesToText(slots.default?.()))
|
|
32
|
+
const fillerCount = computed(() =>
|
|
33
|
+
props.spaces !== undefined
|
|
34
|
+
? Math.max(0, props.spaces)
|
|
35
|
+
: Math.max(0, PREVIEW_LENGTH - text.value.length),
|
|
36
|
+
)
|
|
9
37
|
</script>
|
|
10
38
|
|
|
11
39
|
<template>
|
|
12
40
|
<Teleport to="body:start">
|
|
13
|
-
<div style="display: none"
|
|
41
|
+
<div style="display: none">{{ text }}<template v-for="i in fillerCount" :key="i"> ͏ </template> </div>
|
|
14
42
|
</Teleport>
|
|
15
43
|
</template>
|
|
@@ -63,6 +63,7 @@ const userHasWidth = computed(() => {
|
|
|
63
63
|
|
|
64
64
|
const useMarker = outlookFallback && props.width == null && userHasWidth.value
|
|
65
65
|
const msoId = useMarker ? nextId('s') : null
|
|
66
|
+
const tdId = outlookFallback ? nextId('st') : null
|
|
66
67
|
|
|
67
68
|
const divStyle = computed(() => {
|
|
68
69
|
const parts: string[] = []
|
|
@@ -76,13 +77,6 @@ const restAttrs = computed(() => {
|
|
|
76
77
|
return rest
|
|
77
78
|
})
|
|
78
79
|
|
|
79
|
-
const tdStyles = computed(() => {
|
|
80
|
-
const parts: string[] = []
|
|
81
|
-
if (userStyle.value) parts.push(userStyle.value)
|
|
82
|
-
if (props.msoStyle) parts.push(props.msoStyle)
|
|
83
|
-
return parts.length ? parts.join('; ') : ''
|
|
84
|
-
})
|
|
85
|
-
|
|
86
80
|
const msoWidth = computed(() => {
|
|
87
81
|
if (props.width != null) return normalizeToPixels(props.width)
|
|
88
82
|
if (useMarker) return `__MAIZZLE_MSOW_${msoId}__`
|
|
@@ -95,13 +89,12 @@ const colWidthSource = computed(() => {
|
|
|
95
89
|
return null
|
|
96
90
|
})
|
|
97
91
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
92
|
+
const tdMarker = tdId ? `__MAIZZLE_MSOTDSTYLE_${tdId}__` : ''
|
|
93
|
+
|
|
94
|
+
const MsoBefore = () => createStaticVNode(
|
|
95
|
+
`<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${msoWidth.value}"><tr><td${tdMarker}><![endif]-->`,
|
|
96
|
+
1
|
|
97
|
+
)
|
|
105
98
|
|
|
106
99
|
const MsoAfter = () => createStaticVNode(
|
|
107
100
|
'<!--[if mso]></td></tr></table><![endif]-->',
|
|
@@ -117,6 +110,8 @@ const MsoAfter = () => createStaticVNode(
|
|
|
117
110
|
:data-maizzle-msow-id="msoId"
|
|
118
111
|
:data-maizzle-msow-fallback="useMarker ? '100%' : null"
|
|
119
112
|
:data-maizzle-cw="colWidthSource"
|
|
113
|
+
:data-maizzle-mso-td-id="tdId"
|
|
114
|
+
:data-maizzle-mso-style="tdId && props.msoStyle ? props.msoStyle : null"
|
|
120
115
|
>
|
|
121
116
|
<slot />
|
|
122
117
|
</div>
|
package/dist/components/Text.vue
CHANGED
|
@@ -18,7 +18,7 @@ const props = defineProps({
|
|
|
18
18
|
|
|
19
19
|
const attrs = useAttrs()
|
|
20
20
|
|
|
21
|
-
const defaultClass = computed(() => props.as === 'span' ? 'text-base' : '
|
|
21
|
+
const defaultClass = computed(() => props.as === 'span' ? 'text-base' : 'mt-4 text-base')
|
|
22
22
|
const mergedClass = computed(() => twMerge(defaultClass.value, attrs.class as string))
|
|
23
23
|
</script>
|
|
24
24
|
|
|
@@ -3,10 +3,9 @@ import { MaizzleConfig } from "../types/config.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Define Maizzle config.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the result to child components via useConfig()
|
|
6
|
+
* In maizzle.config.ts: typed identity function, returns the config as-is
|
|
7
|
+
* In Vue SFC `<script setup>`: merges with the global config and provides
|
|
8
|
+
* the result to child components via `useConfig()`
|
|
10
9
|
*/
|
|
11
10
|
declare function defineConfig(data?: Partial<MaizzleConfig>): MaizzleConfig;
|
|
12
11
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineConfig.d.ts","names":[],"sources":["../../src/composables/defineConfig.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"defineConfig.d.ts","names":[],"sources":["../../src/composables/defineConfig.ts"],"mappings":";;;;;AAoBA;;;;iBAAgB,YAAA,CAAa,IAAA,GAAM,OAAA,CAAQ,aAAA,IAAsB,aAAA"}
|
|
@@ -12,10 +12,9 @@ const merge = createDefu((obj, key, value) => {
|
|
|
12
12
|
/**
|
|
13
13
|
* Define Maizzle config.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the result to child components via useConfig()
|
|
15
|
+
* In maizzle.config.ts: typed identity function, returns the config as-is
|
|
16
|
+
* In Vue SFC `<script setup>`: merges with the global config and provides
|
|
17
|
+
* the result to child components via `useConfig()`
|
|
19
18
|
*/
|
|
20
19
|
function defineConfig(data = {}) {
|
|
21
20
|
if (getCurrentInstance()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineConfig.js","names":[],"sources":["../../src/composables/defineConfig.ts"],"sourcesContent":["import { getCurrentInstance, inject, provide } from 'vue'\nimport { createDefu } from 'defu'\nimport { MaizzleConfigKey } from './useConfig.ts'\nimport { RenderContextKey } from './renderContext.ts'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nconst merge = createDefu((obj, key, value) => {\n if (Array.isArray(obj[key])) {\n obj[key] = value\n return true\n }\n})\n\n/**\n * Define Maizzle config.\n *\n *
|
|
1
|
+
{"version":3,"file":"defineConfig.js","names":[],"sources":["../../src/composables/defineConfig.ts"],"sourcesContent":["import { getCurrentInstance, inject, provide } from 'vue'\nimport { createDefu } from 'defu'\nimport { MaizzleConfigKey } from './useConfig.ts'\nimport { RenderContextKey } from './renderContext.ts'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nconst merge = createDefu((obj, key, value) => {\n if (Array.isArray(obj[key])) {\n obj[key] = value\n return true\n }\n})\n\n/**\n * Define Maizzle config.\n *\n * In maizzle.config.ts: typed identity function, returns the config as-is\n * In Vue SFC `<script setup>`: merges with the global config and provides\n * the result to child components via `useConfig()`\n */\nexport function defineConfig(data: Partial<MaizzleConfig> = {}): MaizzleConfig {\n // Inside a Vue SFC — merge with global config and provide to children\n if (getCurrentInstance()) {\n const globalConfig = inject(MaizzleConfigKey, {} as MaizzleConfig)\n const merged = merge(data, globalConfig) as MaizzleConfig\n\n const ctx = inject(RenderContextKey)\n if (ctx) ctx.sfcConfig = merged\n\n provide(MaizzleConfigKey, merged)\n\n return merged\n }\n\n // Outside Vue (maizzle.config.ts) — just return the config\n return data as MaizzleConfig\n}\n"],"mappings":";;;;;AAMA,MAAM,QAAQ,YAAY,KAAK,KAAK,UAAU;CAC5C,IAAI,MAAM,QAAQ,IAAI,KAAK,EAAE;EAC3B,IAAI,OAAO;EACX,OAAO;;EAET;;;;;;;;AASF,SAAgB,aAAa,OAA+B,EAAE,EAAiB;CAE7E,IAAI,oBAAoB,EAAE;EAExB,MAAM,SAAS,MAAM,MADA,OAAO,kBAAkB,EAAE,CACT,CAAC;EAExC,MAAM,MAAM,OAAO,iBAAiB;EACpC,IAAI,KAAK,IAAI,YAAY;EAEzB,QAAQ,kBAAkB,OAAO;EAEjC,OAAO;;CAIT,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderContext.d.ts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,gBAAA;EACf,MAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EANA;EAQA,GAAA;AAAA;AAAA,UAGe,aAAA;EACf,OAAA;EACA,SAAA;IAAc,IAAA;IAAc,WAAA;
|
|
1
|
+
{"version":3,"file":"renderContext.d.ts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,gBAAA;EACf,MAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EANA;EAQA,GAAA;AAAA;AAAA,UAGe,aAAA;EACf,OAAA;EACA,SAAA;IAAc,IAAA;IAAc,WAAA;EAAA;EAC5B,SAAA,GAAY,aAAA;EACZ,gBAAA,EAAkB,KAAA;IAAQ,IAAA,EAAM,SAAA;IAAW,OAAA,EAAS,QAAA,CAAS,SAAA;EAAA;EAC7D,SAAA,GAAY,mBAAA;EACZ,KAAA,GAAQ,gBAAA;EACR,cAAA,GAAiB,aAAA;AAAA;AAAA,cAGN,gBAAA,EAAkB,YAAA,CAAa,aAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderContext.js","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface FontRegistration {\n family: string\n slug: string\n declaration: string\n url: string\n}\n\nexport interface TailwindBlock {\n id: string\n /** Optional raw CSS from the component's `#config` slot. */\n css?: string\n}\n\nexport interface RenderContext {\n doctype?: string\n preheader?: { text: string; fillerCount: number
|
|
1
|
+
{"version":3,"file":"renderContext.js","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface FontRegistration {\n family: string\n slug: string\n declaration: string\n url: string\n}\n\nexport interface TailwindBlock {\n id: string\n /** Optional raw CSS from the component's `#config` slot. */\n css?: string\n}\n\nexport interface RenderContext {\n doctype?: string\n preheader?: { text: string; fillerCount: number }\n sfcConfig?: MaizzleConfig\n sfcEventHandlers: Array<{ name: EventName; handler: EventMap[EventName] }>\n plaintext?: UsePlaintextOptions\n fonts?: FontRegistration[]\n tailwindBlocks?: TailwindBlock[]\n}\n\nexport const RenderContextKey: InjectionKey<RenderContext> = Symbol('RenderContext')\n"],"mappings":";AA4BA,MAAa,mBAAgD,OAAO,gBAAgB"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
//#region src/composables/usePreheader.d.ts
|
|
2
2
|
interface UsePreheaderOptions {
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Explicit number of filler sequences to render. When omitted, the count
|
|
5
|
+
* is auto-derived to fill the 200-char inbox preview budget.
|
|
6
|
+
*/
|
|
7
|
+
spaces?: number;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Set the preheader text for the current email template.
|
|
@@ -15,7 +16,7 @@ interface UsePreheaderOptions {
|
|
|
15
16
|
* Usage in SFC <script setup>:
|
|
16
17
|
* ```ts
|
|
17
18
|
* usePreheader('Thanks for signing up!')
|
|
18
|
-
* usePreheader('Welcome!', {
|
|
19
|
+
* usePreheader('Welcome!', { spaces: 50 })
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
21
22
|
declare function usePreheader(text: string, options?: UsePreheaderOptions): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePreheader.d.ts","names":[],"sources":["../../src/composables/usePreheader.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"usePreheader.d.ts","names":[],"sources":["../../src/composables/usePreheader.ts"],"mappings":";UAKiB,mBAAA;EAAA;;;;EAKf,MAAA;AAAA;;;;;;;;;;;;;;iBAgBc,YAAA,CAAa,IAAA,UAAc,OAAA,GAAU,mBAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RenderContextKey } from "./renderContext.js";
|
|
2
2
|
import { inject } from "vue";
|
|
3
3
|
//#region src/composables/usePreheader.ts
|
|
4
|
+
const PREVIEW_LENGTH = 200;
|
|
4
5
|
/**
|
|
5
6
|
* Set the preheader text for the current email template.
|
|
6
7
|
*
|
|
@@ -11,15 +12,14 @@ import { inject } from "vue";
|
|
|
11
12
|
* Usage in SFC <script setup>:
|
|
12
13
|
* ```ts
|
|
13
14
|
* usePreheader('Thanks for signing up!')
|
|
14
|
-
* usePreheader('Welcome!', {
|
|
15
|
+
* usePreheader('Welcome!', { spaces: 50 })
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
function usePreheader(text, options) {
|
|
18
19
|
const ctx = inject(RenderContextKey);
|
|
19
20
|
if (ctx) ctx.preheader = {
|
|
20
21
|
text,
|
|
21
|
-
fillerCount: options?.
|
|
22
|
-
shyCount: options?.shyCount ?? 150
|
|
22
|
+
fillerCount: options?.spaces !== void 0 ? Math.max(0, options.spaces) : Math.max(0, PREVIEW_LENGTH - text.length)
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePreheader.js","names":[],"sources":["../../src/composables/usePreheader.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\nexport interface UsePreheaderOptions {\n
|
|
1
|
+
{"version":3,"file":"usePreheader.js","names":[],"sources":["../../src/composables/usePreheader.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\nconst PREVIEW_LENGTH = 200\n\nexport interface UsePreheaderOptions {\n /**\n * Explicit number of filler sequences to render. When omitted, the count\n * is auto-derived to fill the 200-char inbox preview budget.\n */\n spaces?: number\n}\n\n/**\n * Set the preheader text for the current email template.\n *\n * Injects a hidden `<div>` at the start of `<body>` with the preheader text\n * followed by filler characters that prevent email clients from pulling\n * in body content after the preheader.\n *\n * Usage in SFC <script setup>:\n * ```ts\n * usePreheader('Thanks for signing up!')\n * usePreheader('Welcome!', { spaces: 50 })\n * ```\n */\nexport function usePreheader(text: string, options?: UsePreheaderOptions): void {\n const ctx = inject(RenderContextKey)\n if (ctx) {\n const fillerCount = options?.spaces !== undefined\n ? Math.max(0, options.spaces)\n : Math.max(0, PREVIEW_LENGTH - text.length)\n ctx.preheader = { text, fillerCount }\n }\n}\n"],"mappings":";;;AAGA,MAAM,iBAAiB;;;;;;;;;;;;;;AAuBvB,SAAgB,aAAa,MAAc,SAAqC;CAC9E,MAAM,MAAM,OAAO,iBAAiB;CACpC,IAAI,KAIF,IAAI,YAAY;EAAE;EAAM,aAHJ,SAAS,WAAW,KAAA,IACpC,KAAK,IAAI,GAAG,QAAQ,OAAO,GAC3B,KAAK,IAAI,GAAG,iBAAiB,KAAK,OAAO;EACR"}
|
|
@@ -11,7 +11,7 @@ import { TransformerToggles } from "../types/config.js";
|
|
|
11
11
|
* - `useTransformers({ prettify: true, minify: true })` *enables*
|
|
12
12
|
* transformers that would otherwise no-op (boolean-driven ones:
|
|
13
13
|
* inlineCss, purgeCss, prettify, minify, shorthandCss, sixHex,
|
|
14
|
-
*
|
|
14
|
+
* safeSelectors, entities). Same effect as setting their config
|
|
15
15
|
* slice directly, scoped to one template.
|
|
16
16
|
*
|
|
17
17
|
* Data-driven transformers (filters, baseURL, urlQuery, addAttributes,
|