@maizzle/framework 6.0.0-rc.5 → 6.0.0-rc.6

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.
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+ import type { PropType } from 'vue'
4
+
5
+ export default {
6
+ name: 'Body',
7
+ inheritAttrs: false,
8
+ props: {
9
+ xmlLang: {
10
+ type: String,
11
+ default: 'en'
12
+ },
13
+ dir: {
14
+ type: String as PropType<'ltr' | 'rtl'>,
15
+ default: 'ltr'
16
+ }
17
+ },
18
+ setup(props, { slots, attrs }) {
19
+ return () => {
20
+ const extraAttrs = Object.entries(attrs)
21
+ .map(([key, value]) => value === true ? key : `${key}="${value}"`)
22
+ .join(' ')
23
+
24
+ const parts = [
25
+ `xml:lang="${props.xmlLang}"`,
26
+ `dir="${props.dir}"`,
27
+ 'style="margin: 0; padding: 0; width: 100%; word-break: break-word;"',
28
+ ]
29
+
30
+ if (extraAttrs) {
31
+ parts.push(extraAttrs)
32
+ }
33
+
34
+ return [
35
+ createStaticVNode(`<body ${parts.join(' ')}>`, 1),
36
+ slots.default?.(),
37
+ createStaticVNode('</body>', 1),
38
+ ]
39
+ }
40
+ }
41
+ }
42
+ </script>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import { computed, createStaticVNode, inject, useAttrs } from 'vue'
3
+ import type { ComputedRef } from 'vue'
4
+ import { normalizeToPixels } from './utils.ts'
5
+
6
+ defineOptions({ inheritAttrs: false })
7
+
8
+ const attrs = useAttrs()
9
+
10
+ const props = defineProps({
11
+ /** Override the auto-computed min-width. */
12
+ width: {
13
+ type: [String, Number],
14
+ default: null
15
+ }
16
+ })
17
+
18
+ const injectedMinWidth = inject<ComputedRef<string> | null>('columnMinWidth', null)
19
+ const containerWidth = inject<ComputedRef<string | number> | null>('containerWidth', null)
20
+ const injectedMsoWidth = inject<ComputedRef<string> | null>('columnMsoWidth', null)
21
+
22
+ const minWidth = computed(() => {
23
+ if (props.width) return normalizeToPixels(props.width)
24
+ if (injectedMinWidth?.value) return injectedMinWidth.value
25
+
26
+ // Fallback: divide container width by 2 if available
27
+ if (containerWidth?.value) {
28
+ const val = containerWidth.value
29
+ if (typeof val === 'number') return `${val / 2}px`
30
+ const num = Number.parseFloat(val)
31
+ const unit = val.replace(String(num), '') || 'px'
32
+ return `${num / 2}${unit}`
33
+ }
34
+
35
+ return '18.75em'
36
+ })
37
+
38
+ const msoWidth = computed(() => injectedMsoWidth?.value ?? '50%')
39
+
40
+ const styles = computed(() => {
41
+ return `display: inline-block; min-width: ${minWidth.value}; font-size: 16px; vertical-align: top;`
42
+ })
43
+
44
+ const MsoBefore = () => createStaticVNode(
45
+ `<!--[if mso]><td width="${msoWidth.value}" style="vertical-align:top"><![endif]-->`,
46
+ 1
47
+ )
48
+
49
+ const MsoAfter = () => createStaticVNode(
50
+ '<!--[if mso]></td><![endif]-->',
51
+ 1
52
+ )
53
+ </script>
54
+
55
+ <template>
56
+ <MsoBefore />
57
+ <div v-bind="attrs" :style="styles">
58
+ <slot />
59
+ </div>
60
+ <MsoAfter />
61
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, createStaticVNode, useAttrs } from 'vue'
3
+ import { normalizeToPixels } from './utils.ts'
4
+
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const attrs = useAttrs()
8
+
9
+ const props = defineProps({
10
+ /** Max width of the container. */
11
+ width: {
12
+ type: [String, Number],
13
+ default: '37.5em'
14
+ }
15
+ })
16
+
17
+ provide('containerWidth', computed(() => props.width))
18
+
19
+ const styles = computed(() => {
20
+ return `max-width: ${normalizeToPixels(props.width)}; margin: 0 auto;`
21
+ })
22
+
23
+ const MsoBefore = () => createStaticVNode(
24
+ `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width:${normalizeToPixels(props.width)}" align="center"><tr><td><![endif]-->`,
25
+ 1
26
+ )
27
+
28
+ const MsoAfter = () => createStaticVNode(
29
+ '<!--[if mso]></td></tr></table><![endif]-->',
30
+ 1
31
+ )
32
+ </script>
33
+
34
+ <template>
35
+ <MsoBefore />
36
+ <div v-bind="attrs" :style="styles">
37
+ <slot />
38
+ </div>
39
+ <MsoAfter />
40
+ </template>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <head>
3
+ <meta charset="utf-8">
4
+ <meta name="x-apple-disable-message-reformatting">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <slot />
7
+ </head>
8
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+ import type { PropType } from 'vue'
4
+
5
+ export default {
6
+ name: 'Html',
7
+ inheritAttrs: false,
8
+ props: {
9
+ lang: {
10
+ type: String,
11
+ default: 'en'
12
+ },
13
+ dir: {
14
+ type: String as PropType<'ltr' | 'rtl'>,
15
+ default: 'ltr'
16
+ },
17
+ xmlns: {
18
+ type: String,
19
+ default: null
20
+ }
21
+ },
22
+ setup(props, { slots, attrs }) {
23
+ return () => {
24
+ const extraAttrs = Object.entries(attrs)
25
+ .map(([key, value]) => value === true ? key : `${key}="${value}"`)
26
+ .join(' ')
27
+
28
+ const parts = [
29
+ `lang="${props.lang}"`,
30
+ `dir="${props.dir}"`,
31
+ ]
32
+
33
+ if (props.xmlns) {
34
+ parts.push(
35
+ `xmlns="${props.xmlns}"`,
36
+ 'xmlns:v="urn:schemas-microsoft-com:vml"',
37
+ 'xmlns:o="urn:schemas-microsoft-com:office:office"',
38
+ )
39
+ }
40
+
41
+ if (extraAttrs) {
42
+ parts.push(extraAttrs)
43
+ }
44
+
45
+ return [
46
+ createStaticVNode(`<html ${parts.join(' ')}>`, 1),
47
+ slots.default?.(),
48
+ createStaticVNode('</html>', 1),
49
+ ]
50
+ }
51
+ }
52
+ }
53
+ </script>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs } from 'vue'
3
+
4
+ defineOptions({ inheritAttrs: false })
5
+
6
+ const attrs = useAttrs()
7
+
8
+ const props = defineProps({
9
+ /** The image source URL. When reducedMotionSrc is used, this becomes the static fallback. */
10
+ src: {
11
+ type: String,
12
+ required: true
13
+ },
14
+ /** Alt text for the image. */
15
+ alt: {
16
+ type: String,
17
+ default: ''
18
+ },
19
+ /** Image source for dark mode. */
20
+ darkSrc: {
21
+ type: String,
22
+ default: null
23
+ },
24
+ /** The width of the image, rendered without units. */
25
+ width: {
26
+ type: [String, Number],
27
+ required: true
28
+ },
29
+ /** Animated image source, shown when user has no reduced motion preference. */
30
+ reducedMotionSrc: {
31
+ type: String,
32
+ default: null
33
+ }
34
+ })
35
+
36
+ function mimeFromExtension(src: string): string {
37
+ const ext = src.split('.').pop()?.toLowerCase() ?? ''
38
+
39
+ const types: Record<string, string> = {
40
+ apng: 'image/apng',
41
+ avif: 'image/avif',
42
+ gif: 'image/gif',
43
+ jpg: 'image/jpeg',
44
+ jpeg: 'image/jpeg',
45
+ jfif: 'image/jpeg',
46
+ png: 'image/png',
47
+ svg: 'image/svg+xml',
48
+ webp: 'image/webp',
49
+ }
50
+
51
+ return types[ext] ?? ''
52
+ }
53
+
54
+ const reducedMotionType = computed(() => mimeFromExtension(props.reducedMotionSrc ?? ''))
55
+
56
+ const imgWidth = computed(() => Number.parseInt(String(props.width), 10))
57
+
58
+ const usePicture = computed(() => props.darkSrc || props.reducedMotionSrc)
59
+
60
+ const imgStyle = 'max-width: 100%; vertical-align: middle;'
61
+ </script>
62
+
63
+ <template>
64
+ <picture v-if="usePicture">
65
+ <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
66
+ <source v-if="reducedMotionSrc" :srcset="reducedMotionSrc" :type="reducedMotionType || undefined" media="(prefers-reduced-motion: no-preference)">
67
+ <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :style="imgStyle">
68
+ </picture>
69
+ <img v-else v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :style="imgStyle">
70
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs, createStaticVNode } from 'vue'
3
+ import { normalizeToPixels } from './utils.ts'
4
+
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const attrs = useAttrs()
8
+
9
+ const props = defineProps({
10
+ /** Max height of the overlapped (background) content. */
11
+ height: {
12
+ type: [String, Number],
13
+ required: true
14
+ },
15
+ /** Width of the overlay table and VML rect. */
16
+ width: {
17
+ type: [String, Number],
18
+ required: true
19
+ },
20
+ /** Height of the VML rect for Outlook. Defaults to height. */
21
+ msoHeight: {
22
+ type: [String, Number],
23
+ default: null
24
+ },
25
+ /** VML textbox inset value for Outlook positioning. */
26
+ msoInset: {
27
+ type: String,
28
+ default: '0,-60px,0,0'
29
+ },
30
+ })
31
+
32
+ const backgroundStyles = computed(() => {
33
+ return `max-height: ${normalizeToPixels(props.height)}; margin: 0 auto; text-align: center;`
34
+ })
35
+
36
+ const vmlOpen = computed(() => {
37
+ const w = normalizeToPixels(props.width)
38
+ const h = normalizeToPixels(props.msoHeight ?? props.height)
39
+
40
+ return `<!--[if mso]><v:rect xmlns:v="urn:schemas-microsoft-com:vml" stroked="f" filled="f" style="width:${w};height:${h};"><v:textbox inset="${props.msoInset}"><![endif]-->`
41
+ })
42
+
43
+ const VmlBefore = () => createStaticVNode(vmlOpen.value, 1)
44
+ const VmlAfter = () => createStaticVNode('<!--[if mso]></v:textbox></v:rect><![endif]-->', 1)
45
+ </script>
46
+
47
+ <template>
48
+ <div v-bind="attrs" :style="backgroundStyles">
49
+ <slot />
50
+ </div>
51
+ <table style="max-height: 0; position: relative; opacity: 0.999;">
52
+ <tr>
53
+ <td :style="`width: ${normalizeToPixels(props.width)}; max-width: 100%; vertical-align: top;`">
54
+ <VmlBefore />
55
+ <slot name="overlay" />
56
+ <VmlAfter />
57
+ </td>
58
+ </tr>
59
+ </table>
60
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import { Comment, computed, createStaticVNode, inject, provide, useAttrs, useSlots, Fragment } from 'vue'
3
+ import type { ComputedRef, VNode } from 'vue'
4
+
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const attrs = useAttrs()
8
+
9
+ const props = defineProps({
10
+ /** Override the inherited container width. */
11
+ width: {
12
+ type: [String, Number],
13
+ default: null
14
+ },
15
+ /** Override the auto-detected column count. */
16
+ cols: {
17
+ type: Number,
18
+ default: null
19
+ }
20
+ })
21
+
22
+ const slots = useSlots()
23
+
24
+ function countChildren(vnodes: VNode[]): number {
25
+ let count = 0
26
+
27
+ for (const vnode of vnodes) {
28
+ if (vnode.type === Fragment && Array.isArray(vnode.children)) {
29
+ count += countChildren(vnode.children as VNode[])
30
+ } else if (vnode.type !== Comment && typeof vnode.type !== 'symbol') {
31
+ count++
32
+ }
33
+ }
34
+
35
+ return count
36
+ }
37
+
38
+ const columnCount = computed(() => {
39
+ if (props.cols) return props.cols
40
+
41
+ const children = slots.default?.() ?? []
42
+ return countChildren(children) || 1
43
+ })
44
+
45
+ const containerWidth = inject<ComputedRef<string | number> | null>('containerWidth', null)
46
+
47
+ const rowWidth = computed(() => props.width ?? containerWidth?.value ?? '37.5em')
48
+
49
+ function divideValue(value: string | number, divisor: number): string {
50
+ if (typeof value === 'number') {
51
+ return `${value / divisor}px`
52
+ }
53
+
54
+ const num = Number.parseFloat(value)
55
+ const unit = value.replace(String(num), '') || 'px'
56
+
57
+ return `${num / divisor}${unit}`
58
+ }
59
+
60
+ provide('columnMinWidth', computed(() => divideValue(rowWidth.value, columnCount.value)))
61
+ provide('columnMsoWidth', computed(() => `${Math.round(100 / columnCount.value)}%`))
62
+
63
+ const MsoBefore = () => createStaticVNode(
64
+ '<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" width="100%"><tr><![endif]-->',
65
+ 1
66
+ )
67
+
68
+ const MsoAfter = () => createStaticVNode(
69
+ '<!--[if mso]></tr></table><![endif]-->',
70
+ 1
71
+ )
72
+ </script>
73
+
74
+ <template>
75
+ <MsoBefore />
76
+ <div v-bind="attrs" style="width: 100%; font-size: 0;">
77
+ <slot />
78
+ </div>
79
+ <MsoAfter />
80
+ </template>
@@ -3,11 +3,21 @@ import { computed } from 'vue'
3
3
  import { normalizeToPixels } from './utils.ts'
4
4
 
5
5
  const props = defineProps({
6
- /** The height of the spacer. */
7
- size: {
6
+ /** The type of spacer. */
7
+ type: {
8
+ type: String as () => 'vertical' | 'horizontal',
9
+ default: 'vertical'
10
+ },
11
+ /** The height of the spacer (vertical). */
12
+ height: {
8
13
  type: [String, Number],
9
14
  default: null
10
15
  },
16
+ /** The width of the spacer (horizontal). */
17
+ width: {
18
+ type: [String, Number],
19
+ default: 16
20
+ },
11
21
  /** The alternative height to use in Outlook. */
12
22
  msoHeight: {
13
23
  type: [String, Number],
@@ -15,11 +25,16 @@ const props = defineProps({
15
25
  }
16
26
  })
17
27
 
18
- const styles = computed(() => {
28
+ function parsePixelValue(value: string | number): number {
29
+ if (typeof value === 'number') return value
30
+ return Number.parseFloat(value) || 0
31
+ }
32
+
33
+ const verticalStyles = computed(() => {
19
34
  const s = []
20
35
 
21
- if (props.size) {
22
- s.push(`line-height: ${normalizeToPixels(props.size)};`)
36
+ if (props.height) {
37
+ s.push(`line-height: ${normalizeToPixels(props.height)};`)
23
38
  }
24
39
 
25
40
  if (props.msoHeight) {
@@ -28,9 +43,37 @@ const styles = computed(() => {
28
43
 
29
44
  return s.join('')
30
45
  })
46
+
47
+ const horizontalStyles = computed(() => {
48
+ return `display:inline-block; width: ${normalizeToPixels(props.width)}; font-size: 16px;${msoFontWidth.value}`
49
+ })
50
+
51
+ const msoFontWidth = computed(() => {
52
+ const widthPx = parsePixelValue(props.width)
53
+ const emspBase = 16
54
+ const maxPercent = 500
55
+ const maxPerEmsp = emspBase * (maxPercent / 100)
56
+ const numEmsps = Math.ceil(widthPx / maxPerEmsp)
57
+ const percent = Math.round((widthPx / (numEmsps * emspBase)) * 100)
58
+
59
+ return ` mso-font-width:${percent}%;`
60
+ })
61
+
62
+ const emspCount = computed(() => {
63
+ const widthPx = parsePixelValue(props.width)
64
+ const maxPerEmsp = 16 * 5
65
+ return Math.ceil(widthPx / maxPerEmsp)
66
+ })
67
+
68
+ const emsps = computed(() => '\u2003'.repeat(emspCount.value))
31
69
  </script>
32
70
 
33
71
  <template>
34
- <div v-if="size" role="separator" :style="styles">&zwj;</div>
35
- <div v-else role="separator">&zwj;</div>
72
+ <template v-if="type === 'horizontal'">
73
+ <i :style="horizontalStyles">{{ emsps }}</i>
74
+ </template>
75
+ <template v-else>
76
+ <div v-if="height" role="separator" :style="verticalStyles">&zwj;</div>
77
+ <div v-else role="separator">&zwj;</div>
78
+ </template>
36
79
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-rc.5",
3
+ "version": "6.0.0-rc.6",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",