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

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 (58) hide show
  1. package/dist/components/Button.vue +2 -2
  2. package/dist/components/CodeBlock.vue +2 -1
  3. package/dist/components/Column.vue +28 -22
  4. package/dist/components/Container.vue +47 -9
  5. package/dist/components/Font.vue +96 -0
  6. package/dist/components/Layout.vue +9 -4
  7. package/dist/components/Overlap.vue +75 -18
  8. package/dist/components/Row.vue +40 -19
  9. package/dist/components/Section.vue +35 -8
  10. package/dist/components/utils.d.mts +14 -1
  11. package/dist/components/utils.d.mts.map +1 -1
  12. package/dist/components/utils.mjs +32 -1
  13. package/dist/components/utils.mjs.map +1 -1
  14. package/dist/components/utils.ts +39 -0
  15. package/dist/composables/renderContext.d.mts +8 -1
  16. package/dist/composables/renderContext.d.mts.map +1 -1
  17. package/dist/composables/renderContext.mjs.map +1 -1
  18. package/dist/composables/useFont.d.mts +50 -0
  19. package/dist/composables/useFont.d.mts.map +1 -0
  20. package/dist/composables/useFont.mjs +93 -0
  21. package/dist/composables/useFont.mjs.map +1 -0
  22. package/dist/index.d.mts +2 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
  25. package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
  26. package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
  27. package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
  28. package/dist/render/createRenderer.mjs +8 -2
  29. package/dist/render/createRenderer.mjs.map +1 -1
  30. package/dist/render/injectFonts.d.mts +15 -0
  31. package/dist/render/injectFonts.d.mts.map +1 -0
  32. package/dist/render/injectFonts.mjs +46 -0
  33. package/dist/render/injectFonts.mjs.map +1 -0
  34. package/dist/serve.d.mts.map +1 -1
  35. package/dist/serve.mjs +6 -2
  36. package/dist/serve.mjs.map +1 -1
  37. package/dist/server/ui/App.vue +25 -11
  38. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  39. package/dist/server/ui/pages/Preview.vue +24 -5
  40. package/dist/transformers/columnWidth.d.mts +31 -0
  41. package/dist/transformers/columnWidth.d.mts.map +1 -0
  42. package/dist/transformers/columnWidth.mjs +166 -0
  43. package/dist/transformers/columnWidth.mjs.map +1 -0
  44. package/dist/transformers/index.d.mts.map +1 -1
  45. package/dist/transformers/index.mjs +4 -0
  46. package/dist/transformers/index.mjs.map +1 -1
  47. package/dist/transformers/msoWidthFromClass.d.mts +19 -0
  48. package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
  49. package/dist/transformers/msoWidthFromClass.mjs +61 -0
  50. package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
  51. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  52. package/dist/transformers/tailwindcss.mjs +4 -12
  53. package/dist/transformers/tailwindcss.mjs.map +1 -1
  54. package/dist/utils/decodeStyleEntities.d.mts +15 -0
  55. package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
  56. package/dist/utils/decodeStyleEntities.mjs +18 -0
  57. package/dist/utils/decodeStyleEntities.mjs.map +1 -0
  58. package/package.json +2 -1
@@ -174,7 +174,7 @@ const mergedClass = computed(() => twMerge(defaultClasses.value, attrs.class as
174
174
  <Outlook><i class="mso-font-width-[150%]" :style="`mso-text-raise: ${msoPb};`" hidden>&emsp;</i></Outlook>
175
175
  <template v-if="icon && iconPosition === 'left'">
176
176
  <span :style="`mso-text-raise: ${msoPt}`">
177
- <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`">
177
+ <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`" alt="">
178
178
  </span>
179
179
  <Outlook><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
180
180
  </template>
@@ -182,7 +182,7 @@ const mergedClass = computed(() => twMerge(defaultClasses.value, attrs.class as
182
182
  <template v-if="icon && iconPosition === 'right'">
183
183
  <Outlook><i class="mso-font-width-[30%]" hidden>&emsp;&#8203;</i></Outlook>
184
184
  <span :style="`mso-text-raise: ${msoPt}`">
185
- <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`">
185
+ <img :src="icon" :width="parsedIconWidth" :class="`align-baseline max-w-full ${iconClass}`" alt="">
186
186
  </span>
187
187
  </template>
188
188
  <Outlook><i class="mso-font-width-[150%]" hidden>&emsp;&#8203;</i></Outlook>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { createStaticVNode, type PropType } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
  import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
4
5
 
5
6
  export default {
@@ -56,7 +57,7 @@ export default {
56
57
  .replace(/^<pre[^>]*><code>/, '')
57
58
  .replace(/<\/code><\/pre>$/, '')
58
59
 
59
- const classes = ['font-mono', attrs.class].filter(Boolean).join(' ')
60
+ const classes = twMerge('font-mono', attrs.class as string)
60
61
  const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
61
62
  const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
62
63
 
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { computed, createStaticVNode, inject, provide, useAttrs } from 'vue'
2
+ import { computed, createStaticVNode, inject, useAttrs } from 'vue'
3
3
  import type { ComputedRef } from 'vue'
4
- import { normalizeToPixels } from './utils.ts'
4
+ import { nextId, normalizeToPixels } from './utils.ts'
5
5
 
6
6
  defineOptions({ inheritAttrs: false })
7
7
 
@@ -11,8 +11,9 @@ const props = defineProps({
11
11
  /**
12
12
  * Override the auto-computed column width.
13
13
  *
14
- * By default, the width is calculated from the parent `Row`
15
- * by dividing its width by the column count.
14
+ * By default, the width is calculated from the nearest sized
15
+ * ancestor (`Container`, `Section`, `Row`, or outer `Column`)
16
+ * divided by the column count detected on the parent `Row`.
16
17
  */
17
18
  width: {
18
19
  type: [String, Number],
@@ -31,35 +32,35 @@ const props = defineProps({
31
32
  }
32
33
  })
33
34
 
34
- const injectedMinWidth = inject<ComputedRef<string> | null>('columnMinWidth', null)
35
- const containerWidth = inject<ComputedRef<string | number> | null>('containerWidth', null)
36
- const injectedMsoWidth = inject<ComputedRef<string> | null>('columnMsoWidth', null)
35
+ const columnCount = inject<ComputedRef<number> | null>('columnCount', null)
37
36
 
38
- const minWidth = computed(() => {
39
- if (props.width) return normalizeToPixels(props.width)
40
-
41
- return injectedMinWidth?.value ?? null
42
- })
37
+ const count = computed(() => columnCount?.value ?? 2)
43
38
 
44
- const msoWidth = computed(() => injectedMsoWidth?.value ?? '50%')
39
+ const useMarker = props.width == null
40
+ const colId = useMarker ? nextId('co') : null
45
41
 
46
- // Provide column width as containerWidth for nested Rows
47
- provide('containerWidth', computed(() => minWidth.value ?? containerWidth?.value ?? null))
42
+ const minWidth = computed(() => {
43
+ if (props.width != null) return normalizeToPixels(props.width)
44
+ return `__MAIZZLE_COLW_${colId}__`
45
+ })
48
46
 
49
- const styles = computed(() => {
50
- const parts = ['display: inline-block', 'font-size: 16px', 'vertical-align: top']
51
- if (minWidth.value) parts.splice(1, 0, `min-width: ${minWidth.value}`)
52
- return `${parts.join('; ')};`
47
+ const msoWidth = computed(() => {
48
+ if (props.width != null) return normalizeToPixels(props.width)
49
+ return `__MAIZZLE_COLW_${colId}__`
53
50
  })
54
51
 
52
+ const styles = computed(() =>
53
+ `display: inline-block; min-width: ${minWidth.value}; font-size: 16px; vertical-align: top;`
54
+ )
55
+
55
56
  const tdStyle = computed(() => {
56
- const parts = ['vertical-align: top']
57
+ const parts = [`width: ${msoWidth.value}`, 'vertical-align: top']
57
58
  if (props.msoStyle) parts.push(props.msoStyle)
58
59
  return parts.join('; ')
59
60
  })
60
61
 
61
62
  const MsoBefore = () => createStaticVNode(
62
- `<!--[if mso]><td width="${msoWidth.value}" style="${tdStyle.value}"><![endif]-->`,
63
+ `<!--[if mso]><td style="${tdStyle.value}"><![endif]-->`,
63
64
  1
64
65
  )
65
66
 
@@ -71,7 +72,12 @@ const MsoAfter = () => createStaticVNode(
71
72
 
72
73
  <template>
73
74
  <MsoBefore />
74
- <div v-bind="attrs" :style="styles">
75
+ <div
76
+ v-bind="attrs"
77
+ :style="styles"
78
+ :data-maizzle-cw-id="colId"
79
+ :data-maizzle-cw-count="useMarker ? count : null"
80
+ >
75
81
  <slot />
76
82
  </div>
77
83
  <MsoAfter />
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, provide, createStaticVNode, useAttrs } from 'vue'
3
- import { normalizeToPixels } from './utils.ts'
3
+ import { twMerge } from 'tailwind-merge'
4
+ import { hasWidthUtility, nextId, normalizeToPixels } from './utils.ts'
4
5
 
5
6
  defineOptions({ inheritAttrs: false })
6
7
 
@@ -11,27 +12,58 @@ const props = defineProps({
11
12
  * Max width of the container.
12
13
  *
13
14
  * Applied as `max-width` on the div and as `width` on the MSO table.
14
- * Also provided to child `Row` and `Column` components for
15
- * automatic column width calculation.
15
+ * Also used as the width source for descendant `Row`/`Column`
16
+ * components when computing column widths.
16
17
  *
17
- * When not set, no inline `max-width` is applied to the div
18
- * use Tailwind classes like `max-w-xl mx-auto` instead.
19
- * The MSO table defaults to 600px.
18
+ * When not set, the div defaults to `w-150 mx-auto` (600px,
19
+ * centered) — overridable via Tailwind classes such as
20
+ * `w-[400px]` or `max-w-xl`. The MSO table width is auto-derived
21
+ * from the resolved width/max-width after CSS inlining, falling
22
+ * back to 600px when unresolvable.
20
23
  */
21
24
  width: {
22
25
  type: [String, Number],
23
26
  default: null
27
+ },
28
+ /**
29
+ * Override the Outlook (MSO) table width independently of the
30
+ * div's width. Highest priority — wins over `width` and any
31
+ * class-derived value.
32
+ */
33
+ msoWidth: {
34
+ type: [String, Number],
35
+ default: null
24
36
  }
25
37
  })
26
38
 
27
39
  provide('containerWidth', computed(() => props.width))
28
40
 
41
+ const useMarker = props.width == null && props.msoWidth == null
42
+ const msoId = useMarker ? nextId('c') : null
43
+
29
44
  const styles = computed(() => {
30
- if (!props.width) return 'margin: 0 auto;'
45
+ if (props.width == null) return undefined
31
46
  return `max-width: ${normalizeToPixels(props.width)}; margin: 0 auto;`
32
47
  })
33
48
 
34
- const msoWidth = computed(() => normalizeToPixels(props.width ?? 600))
49
+ const mergedClass = computed(() => {
50
+ if (props.width != null) return attrs.class as string | undefined
51
+ const userClass = (attrs.class as string) ?? ''
52
+ const defaultClass = hasWidthUtility(userClass)
53
+ ? 'm-0 mx-auto'
54
+ : 'w-150 m-0 mx-auto'
55
+ return twMerge(defaultClass, userClass)
56
+ })
57
+
58
+ const msoWidth = computed(() => {
59
+ if (props.msoWidth != null) return normalizeToPixels(props.msoWidth)
60
+ if (props.width != null) return normalizeToPixels(props.width)
61
+ return `__MAIZZLE_MSOW_${msoId}__`
62
+ })
63
+
64
+ const colWidthSource = computed(() =>
65
+ props.width != null ? normalizeToPixels(props.width) : ''
66
+ )
35
67
 
36
68
  const MsoBefore = () => createStaticVNode(
37
69
  `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${msoWidth.value}" align="center"><tr><td><![endif]-->`,
@@ -46,7 +78,13 @@ const MsoAfter = () => createStaticVNode(
46
78
 
47
79
  <template>
48
80
  <MsoBefore />
49
- <div v-bind="attrs" :style="styles">
81
+ <div
82
+ v-bind="{ ...attrs, class: undefined }"
83
+ :class="mergedClass"
84
+ :style="styles"
85
+ :data-maizzle-msow-id="msoId"
86
+ :data-maizzle-cw="colWidthSource"
87
+ >
50
88
  <slot />
51
89
  </div>
52
90
  <MsoAfter />
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { type PropType } from 'vue'
3
+ import { useFont } from '../composables/useFont'
4
+
5
+ type PopularGoogleFont =
6
+ // Sans-serif
7
+ | 'Roboto' | 'Open Sans' | 'Inter' | 'Lato' | 'Montserrat'
8
+ // Serif
9
+ | 'Merriweather' | 'Playfair Display' | 'Lora' | 'PT Serif' | 'Noto Serif'
10
+ // Display
11
+ | 'Oswald' | 'Bebas Neue' | 'Anton' | 'Lobster' | 'Pacifico'
12
+ // Handwriting
13
+ | 'Dancing Script' | 'Caveat' | 'Shadows Into Light' | 'Satisfy' | 'Great Vibes'
14
+ // Monospace
15
+ | 'Roboto Mono' | 'Source Code Pro' | 'JetBrains Mono' | 'Fira Code' | 'Inconsolata'
16
+
17
+ const props = defineProps({
18
+ /**
19
+ * A single font family name, e.g. `"Roboto"` or `"Open Sans"`.
20
+ *
21
+ * For fallback fonts, use the `fallback` prop instead of a
22
+ * comma-separated list here. Popular Google Fonts are suggested
23
+ * in the IDE, but any string is accepted.
24
+ *
25
+ * @example "Open Sans"
26
+ */
27
+ family: {
28
+ type: String as PropType<PopularGoogleFont | (string & {})>,
29
+ required: true,
30
+ validator: (v: string) => v.trim().length > 0,
31
+ },
32
+ /**
33
+ * CSS fallback list appended to the `font-family` declaration.
34
+ *
35
+ * @example "Verdana, sans-serif"
36
+ */
37
+ fallback: {
38
+ type: String,
39
+ default: '',
40
+ },
41
+ /**
42
+ * Font provider used to build the stylesheet URL when `url` is omitted.
43
+ * Bunny Fonts is a drop-in, privacy-friendly Google Fonts mirror.
44
+ */
45
+ provider: {
46
+ type: String as PropType<'google' | 'bunny'>,
47
+ default: 'google',
48
+ validator: (v: string) => ['google', 'bunny'].includes(v),
49
+ },
50
+ /**
51
+ * Stylesheet URL. When provided, used as-is for the `<link href>`.
52
+ * When omitted, a Google Fonts URL is built from `family`, `weights`,
53
+ * `display` and `styles`.
54
+ */
55
+ url: {
56
+ type: String,
57
+ default: '',
58
+ },
59
+ /**
60
+ * Font weights to load. Ignored when `url` is provided.
61
+ */
62
+ weights: {
63
+ type: Array as () => number[],
64
+ default: () => [400],
65
+ },
66
+ /**
67
+ * `font-display` value. Ignored when `url` is provided.
68
+ */
69
+ display: {
70
+ type: String as PropType<'auto' | 'block' | 'swap' | 'fallback' | 'optional'>,
71
+ default: 'swap',
72
+ validator: (v: string) => ['auto', 'block', 'swap', 'fallback', 'optional'].includes(v),
73
+ },
74
+ /**
75
+ * Font styles to load. Ignored when `url` is provided.
76
+ *
77
+ * @example ['normal', 'italic']
78
+ */
79
+ styles: {
80
+ type: Array as () => Array<'normal' | 'italic'>,
81
+ default: () => ['normal'],
82
+ },
83
+ })
84
+
85
+ useFont({
86
+ family: props.family,
87
+ fallback: props.fallback || undefined,
88
+ provider: props.provider,
89
+ url: props.url || undefined,
90
+ weights: props.weights,
91
+ display: props.display,
92
+ styles: props.styles,
93
+ })
94
+ </script>
95
+
96
+ <template></template>
@@ -1,9 +1,10 @@
1
1
  <script setup lang="ts">
2
- import type { PropType } from 'vue'
2
+ import { computed, useAttrs, type PropType } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
 
4
5
  defineOptions({ inheritAttrs: false })
5
6
 
6
- defineProps({
7
+ const props = defineProps({
7
8
  /**
8
9
  * Classes to add to the `<body>` tag.
9
10
  */
@@ -41,6 +42,10 @@ defineProps({
41
42
  default: undefined
42
43
  }
43
44
  })
45
+
46
+ const attrs = useAttrs()
47
+ const bodyMergedClass = computed(() => twMerge('m-0 p-0 size-full [word-break:break-word]', props.bodyClass))
48
+ const articleMergedClass = computed(() => twMerge('[font-size:max(16px,1rem)] font-inter', attrs.class as string))
44
49
  </script>
45
50
 
46
51
  <template>
@@ -76,7 +81,7 @@ defineProps({
76
81
  }
77
82
  </style>
78
83
  </head>
79
- <body :xml:lang="lang" :class="['m-0 p-0 size-full [word-break:break-word]', bodyClass]">
84
+ <body :xml:lang="lang" :class="bodyMergedClass">
80
85
  <div
81
86
  role="article"
82
87
  aria-roledescription="email"
@@ -84,7 +89,7 @@ defineProps({
84
89
  :lang="lang"
85
90
  :dir="dir"
86
91
  style="font-size: medium;"
87
- :class="['[font-size:max(16px,1rem)] font-inter', $attrs.class]"
92
+ :class="articleMergedClass"
88
93
  >
89
94
  <slot />
90
95
  </div>
@@ -1,37 +1,48 @@
1
1
  <script setup lang="ts">
2
- import { computed, inject, useAttrs, createStaticVNode } from 'vue'
3
- import { normalizeToPixels } from './utils.ts'
2
+ import { computed, useAttrs, createStaticVNode } from 'vue'
3
+ import {
4
+ hasHeightInStyle,
5
+ hasHeightUtility,
6
+ hasWidthInStyle,
7
+ hasWidthUtility,
8
+ nextId,
9
+ normalizeToPixels
10
+ } from './utils.ts'
4
11
 
5
12
  defineOptions({ inheritAttrs: false })
6
13
 
7
14
  const attrs = useAttrs()
8
15
 
9
- const containerWidth = inject('containerWidth', undefined)
10
-
11
16
  const props = defineProps({
12
17
  /**
13
18
  * Max height of the background (default slot) content.
14
19
  *
15
- * This constrains the visible area of the background layer.
20
+ * Applied as `max-height` on the background div and as `height`
21
+ * on the VML rectangle. When not set, the height is taken from
22
+ * a Tailwind utility (e.g. `h-50`, `max-h-[200px]`) or inline
23
+ * `height`/`max-height` style on the component, after CSS inlining.
16
24
  */
17
25
  height: {
18
26
  type: [String, Number],
19
- required: true
27
+ default: null
20
28
  },
21
29
  /**
22
30
  * Width of the overlay table and VML rectangle.
23
31
  *
24
- * When used inside a Container, defaults to the container's width.
32
+ * When not set, derived from a width utility class or inline
33
+ * style on the component itself, otherwise from the nearest
34
+ * sized ancestor (`Container`, `Section`, outer `Column`).
35
+ * Falls back to `100%` when no source is found.
25
36
  */
26
37
  width: {
27
38
  type: [String, Number],
28
- default: undefined
39
+ default: null
29
40
  },
30
41
  /**
31
42
  * Height of the VML rectangle in Outlook.
32
43
  *
33
- * Defaults to the `height` prop value. Use this to fine-tune
34
- * the overlay height specifically for Outlook rendering.
44
+ * Defaults to the resolved `height`. Use this to fine-tune the
45
+ * overlay height specifically for Outlook rendering.
35
46
  */
36
47
  msoHeight: {
37
48
  type: [String, Number],
@@ -51,17 +62,56 @@ const props = defineProps({
51
62
  },
52
63
  })
53
64
 
54
- const resolvedWidth = computed(() => props.width ?? containerWidth?.value)
65
+ const userStyle = computed(() => {
66
+ const s = attrs.style
67
+ if (!s) return ''
68
+ return typeof s === 'object'
69
+ ? Object.entries(s).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`).join('; ')
70
+ : String(s)
71
+ })
72
+
73
+ const userClass = computed(() => (attrs.class as string) ?? '')
74
+
75
+ const userHasWidth = computed(() =>
76
+ hasWidthUtility(userClass.value) || hasWidthInStyle(userStyle.value)
77
+ )
78
+ const userHasHeight = computed(() =>
79
+ hasHeightUtility(userClass.value) || hasHeightInStyle(userStyle.value)
80
+ )
81
+
82
+ const useWidthMarker = props.width == null
83
+ const useHeightMarker = props.height == null && props.msoHeight == null
84
+ const id = (useWidthMarker || useHeightMarker) ? nextId('o') : null
85
+
86
+ const widthValue = computed(() =>
87
+ useWidthMarker ? `__MAIZZLE_COLW_${id}__` : normalizeToPixels(props.width)
88
+ )
89
+
90
+ const heightValue = computed(() => {
91
+ if (props.msoHeight != null) return normalizeToPixels(props.msoHeight)
92
+ if (props.height != null) return normalizeToPixels(props.height)
93
+ return `__MAIZZLE_OH_${id}__`
94
+ })
55
95
 
56
96
  const backgroundStyles = computed(() => {
57
- return `max-height: ${normalizeToPixels(props.height)}; margin: 0 auto; text-align: center;`
97
+ const parts: string[] = []
98
+ if (props.height != null) parts.push(`max-height: ${normalizeToPixels(props.height)}`)
99
+ parts.push('margin: 0 auto', 'text-align: center')
100
+ if (userStyle.value) parts.push(userStyle.value)
101
+ return parts.join('; ') + ';'
58
102
  })
59
103
 
60
- const vmlOpen = computed(() => {
61
- const w = normalizeToPixels(resolvedWidth.value)
62
- const h = normalizeToPixels(props.msoHeight ?? props.height)
104
+ const tdStyle = computed(() =>
105
+ `width: ${widthValue.value}; max-width: 100%; vertical-align: top;`
106
+ )
107
+
108
+ const vmlOpen = computed(() =>
109
+ `<!--[if mso]><v:rect xmlns:v="urn:schemas-microsoft-com:vml" stroked="f" filled="f" style="width: ${widthValue.value}; height: ${heightValue.value};"><v:textbox inset="${props.msoInset}"><![endif]-->`
110
+ )
63
111
 
64
- 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]-->`
112
+ const restAttrs = computed(() => {
113
+ const { style: _, ...rest } = attrs
114
+ return rest
65
115
  })
66
116
 
67
117
  const VmlBefore = () => createStaticVNode(vmlOpen.value, 1)
@@ -69,12 +119,19 @@ const VmlAfter = () => createStaticVNode('<!--[if mso]></v:textbox></v:rect><![e
69
119
  </script>
70
120
 
71
121
  <template>
72
- <div v-bind="attrs" :style="backgroundStyles">
122
+ <div
123
+ v-bind="restAttrs"
124
+ :style="backgroundStyles"
125
+ :data-maizzle-cw-id="useWidthMarker ? id : null"
126
+ :data-maizzle-cw-count="useWidthMarker ? 1 : null"
127
+ :data-maizzle-cw-self="useWidthMarker && userHasWidth ? '' : null"
128
+ :data-maizzle-oh-id="useHeightMarker && userHasHeight ? id : null"
129
+ >
73
130
  <slot />
74
131
  </div>
75
132
  <table style="max-height: 0; position: relative; opacity: 0.999;">
76
133
  <tr>
77
- <td :style="`width: ${normalizeToPixels(resolvedWidth)}; max-width: 100%; vertical-align: top;`">
134
+ <td :style="tdStyle">
78
135
  <VmlBefore />
79
136
  <slot name="overlay" />
80
137
  <VmlAfter />
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { Comment, computed, createStaticVNode, inject, provide, useAttrs, useSlots, Fragment } from 'vue'
3
- import type { ComputedRef, VNode } from 'vue'
2
+ import { Comment, computed, createStaticVNode, provide, useAttrs, useSlots, Fragment } from 'vue'
3
+ import type { VNode } from 'vue'
4
+ import { hasWidthInStyle, hasWidthUtility, normalizeToPixels } from './utils.ts'
4
5
 
5
6
  defineOptions({ inheritAttrs: false })
6
7
 
@@ -8,10 +9,12 @@ const attrs = useAttrs()
8
9
 
9
10
  const props = defineProps({
10
11
  /**
11
- * Override the inherited container width.
12
+ * Explicit row width.
12
13
  *
13
- * Used to calculate column widths. Inherited from the
14
- * parent `Container` by default.
14
+ * Used as the width source for column min-width calculation.
15
+ * When not set, the nearest sized ancestor (`Container`, `Section`,
16
+ * outer `Column`, or this row's own width class/inline style) is
17
+ * used instead.
15
18
  */
16
19
  width: {
17
20
  type: [String, Number],
@@ -53,26 +56,40 @@ const columnCount = computed(() => {
53
56
  return countChildren(children) || 1
54
57
  })
55
58
 
56
- const containerWidth = inject<ComputedRef<string | number> | null>('containerWidth', null)
59
+ provide('columnCount', columnCount)
57
60
 
58
- const rowWidth = computed(() => props.width ?? containerWidth?.value ?? null)
61
+ const userStyle = computed(() => {
62
+ const s = attrs.style
63
+ if (!s) return ''
64
+ return typeof s === 'object'
65
+ ? Object.entries(s).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`).join('; ')
66
+ : String(s)
67
+ })
59
68
 
60
- function divideValue(value: string | number, divisor: number): string {
61
- if (typeof value === 'number') {
62
- return `${parseFloat((value / divisor).toFixed(2))}px`
63
- }
69
+ const userHasWidth = computed(() => {
70
+ const cls = (attrs.class as string) ?? ''
71
+ return hasWidthUtility(cls) || hasWidthInStyle(userStyle.value)
72
+ })
64
73
 
65
- const num = Number.parseFloat(value)
66
- const unit = value.replace(String(num), '') || 'px'
74
+ const colWidthSource = computed(() => {
75
+ if (props.width != null) return normalizeToPixels(props.width)
76
+ if (userHasWidth.value) return ''
77
+ return null
78
+ })
67
79
 
68
- return `${parseFloat((num / divisor).toFixed(2))}${unit}`
69
- }
80
+ const restAttrs = computed(() => {
81
+ const { style: _, ...rest } = attrs
82
+ return rest
83
+ })
70
84
 
71
- provide('columnMinWidth', computed(() => rowWidth.value ? divideValue(rowWidth.value, columnCount.value) : null))
72
- provide('columnMsoWidth', computed(() => `${Math.round(100 / columnCount.value)}%`))
85
+ const divStyle = computed(() => {
86
+ const parts: string[] = ['font-size: 0;']
87
+ if (userStyle.value) parts.push(userStyle.value)
88
+ return parts.join(' ')
89
+ })
73
90
 
74
91
  const MsoBefore = () => createStaticVNode(
75
- '<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" width="100%"><tr><![endif]-->',
92
+ '<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: 100%"><tr><![endif]-->',
76
93
  1
77
94
  )
78
95
 
@@ -84,7 +101,11 @@ const MsoAfter = () => createStaticVNode(
84
101
 
85
102
  <template>
86
103
  <MsoBefore />
87
- <div v-bind="attrs" style="font-size: 0;">
104
+ <div
105
+ v-bind="restAttrs"
106
+ :style="divStyle"
107
+ :data-maizzle-cw="colWidthSource"
108
+ >
88
109
  <slot />
89
110
  </div>
90
111
  <MsoAfter />