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

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 (72) hide show
  1. package/dist/components/Body.vue +42 -0
  2. package/dist/components/CodeBlock.vue +12 -19
  3. package/dist/components/Column.vue +61 -0
  4. package/dist/components/Container.vue +40 -0
  5. package/dist/components/Head.vue +8 -0
  6. package/dist/components/Html.vue +53 -0
  7. package/dist/components/Image.vue +70 -0
  8. package/dist/components/Markdown.vue +70 -0
  9. package/dist/components/Overlap.vue +60 -0
  10. package/dist/components/Row.vue +80 -0
  11. package/dist/components/Spacer.vue +50 -7
  12. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  13. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  14. package/dist/render/createRenderer.d.mts +2 -3
  15. package/dist/render/createRenderer.d.mts.map +1 -1
  16. package/dist/render/createRenderer.mjs +55 -4
  17. package/dist/render/createRenderer.mjs.map +1 -1
  18. package/dist/serve.d.mts.map +1 -1
  19. package/dist/serve.mjs +83 -3
  20. package/dist/serve.mjs.map +1 -1
  21. package/dist/server/compatibility.d.mts +1 -2
  22. package/dist/server/compatibility.d.mts.map +1 -1
  23. package/dist/server/compatibility.mjs +15 -15
  24. package/dist/server/compatibility.mjs.map +1 -1
  25. package/dist/server/email.d.mts +17 -0
  26. package/dist/server/email.d.mts.map +1 -0
  27. package/dist/server/email.mjs +40 -0
  28. package/dist/server/email.mjs.map +1 -0
  29. package/dist/server/ui/App.vue +204 -68
  30. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  31. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  32. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  33. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  34. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  35. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  36. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  37. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  38. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  39. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  40. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  41. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  42. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  43. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  44. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  45. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  46. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  47. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  48. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  49. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  50. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  51. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  52. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  53. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  54. package/dist/server/ui/main.css +20 -20
  55. package/dist/server/ui/pages/Home.vue +12 -5
  56. package/dist/server/ui/pages/Preview.vue +369 -150
  57. package/dist/transformers/inlineCSS.mjs +9 -0
  58. package/dist/transformers/inlineCSS.mjs.map +1 -1
  59. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  60. package/dist/transformers/purgeCSS.mjs +67 -1
  61. package/dist/transformers/purgeCSS.mjs.map +1 -1
  62. package/dist/transformers/tailwindcss.mjs +3 -7
  63. package/dist/transformers/tailwindcss.mjs.map +1 -1
  64. package/dist/types/config.d.mts +38 -4
  65. package/dist/types/config.d.mts.map +1 -1
  66. package/dist/types/index.d.mts +2 -2
  67. package/package.json +7 -3
  68. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  69. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  70. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  71. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  72. /package/dist/components/{Preview.vue → Preheader.vue} +0 -0
@@ -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>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- import { createStaticVNode } from 'vue'
3
- import { codeToHtml, getSingletonHighlighter } from 'shiki'
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
4
4
 
5
5
  export default {
6
6
  props: {
@@ -9,33 +9,26 @@ export default {
9
9
  type: String,
10
10
  default: ''
11
11
  },
12
- /** Base64-encoded code, set by the Vite transform for slot content. */
13
- encodedCode: {
14
- type: String,
15
- default: ''
16
- },
17
- /** The language for syntax highlighting. @default 'html' */
18
- lang: {
19
- type: String,
12
+ /** The language for syntax highlighting. @default 'html' */
13
+ language: {
14
+ type: String as PropType<BundledLanguage>,
20
15
  default: 'html'
21
16
  },
22
17
  /** The shiki theme to use. @default 'github-light' */
23
18
  theme: {
24
- type: String,
19
+ type: String as PropType<BundledTheme>,
25
20
  default: 'github-light'
26
21
  },
27
- /** CSS class for the wrapping table cell. @default 'max-w-0 mso-padding-alt-6' */
22
+ /** CSS class for the wrapping table cell. @default 'max-w-0 mso-padding-alt-4' */
28
23
  tdClass: {
29
24
  type: String,
30
- default: 'max-w-0 mso-padding-alt-6'
25
+ default: 'max-w-0 mso-padding-alt-4'
31
26
  }
32
27
  },
33
28
  inheritAttrs: false,
34
29
  async setup(props, { slots, attrs }) {
35
- // Prefer encodedCode (from Vite transform) → code prop → slot text
36
- let source = props.encodedCode
37
- ? Buffer.from(props.encodedCode, 'base64').toString('utf-8')
38
- : props.code
30
+ // Prefer code prop → slot text
31
+ let source = props.code
39
32
 
40
33
  if (!source) {
41
34
  const slotContent = slots.default?.()
@@ -51,7 +44,7 @@ export default {
51
44
  }
52
45
 
53
46
  const highlighted = await codeToHtml(source, {
54
- lang: props.lang,
47
+ lang: props.language,
55
48
  theme: props.theme,
56
49
  })
57
50
 
@@ -64,7 +57,7 @@ export default {
64
57
  .replace(/<\/code><\/pre>$/, '')
65
58
 
66
59
  const classes = ['font-mono', attrs.class].filter(Boolean).join(' ')
67
- const baseStyles = `background-color:${bg};padding:24px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
60
+ const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
68
61
  const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
69
62
 
70
63
  const html = `<table class="w-full"><tr><td class="${props.tdClass}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
@@ -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,70 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { createMarkdownExit } from 'markdown-exit'
4
+ import { codeToHtml, type BundledTheme } from 'shiki'
5
+
6
+ export default {
7
+ props: {
8
+ /** Markdown content string. Overrides slot content. */
9
+ content: {
10
+ type: String,
11
+ default: ''
12
+ },
13
+ /** Path to a markdown file to render. Resolved at build time. */
14
+ src: {
15
+ type: String,
16
+ default: ''
17
+ },
18
+ /** Shiki theme for fenced code blocks. @default 'github-dark-high-contrast' */
19
+ shikiTheme: {
20
+ type: String as PropType<BundledTheme>,
21
+ default: 'github-dark-high-contrast'
22
+ },
23
+ /** Wrap output in a div element. @default true */
24
+ wrapper: {
25
+ type: Boolean,
26
+ default: true
27
+ },
28
+ },
29
+ inheritAttrs: false,
30
+ async setup(props, { slots, attrs }) {
31
+ let source = props.content
32
+
33
+ if (!source) {
34
+ const slotContent = slots.default?.()
35
+ source = slotContent
36
+ ?.map((vnode: any) => (typeof vnode.children === 'string' ? vnode.children : ''))
37
+ .join('') ?? ''
38
+ }
39
+
40
+ source = source.trim()
41
+
42
+ if (!source) {
43
+ return () => createStaticVNode('', 0)
44
+ }
45
+
46
+ const md = createMarkdownExit({
47
+ html: true,
48
+ linkify: true,
49
+ typographer: true,
50
+ highlight: async (code, lang) => {
51
+ try {
52
+ return await codeToHtml(code, { lang, theme: props.shikiTheme })
53
+ } catch {
54
+ return ''
55
+ }
56
+ },
57
+ })
58
+
59
+ let html = await md.renderAsync(source)
60
+
61
+ if (props.wrapper) {
62
+ const classes = attrs.class ? ` class="${attrs.class}"` : ''
63
+ const style = attrs.style ? ` style="${attrs.style}"` : ''
64
+ html = `<div${classes}${style}>${html}</div>`
65
+ }
66
+
67
+ return () => createStaticVNode(html, 1)
68
+ }
69
+ }
70
+ </script>
@@ -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>
@@ -14,20 +14,29 @@ const DEFAULT_AT_RULES = ["layer", "property"];
14
14
  function tailwindCleanup(config) {
15
15
  const selectors = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS;
16
16
  const atRules = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES;
17
- return [{
18
- postcssPlugin: "tailwind-cleanup-selectors",
19
- Rule(rule) {
20
- const parts = rule.selector.split(",").map((s) => s.trim());
21
- const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
22
- if (kept.length === 0) rule.remove();
23
- else if (kept.length < parts.length) rule.selector = kept.join(", ");
17
+ return [
18
+ {
19
+ postcssPlugin: "tailwind-cleanup-selectors",
20
+ Rule(rule) {
21
+ const parts = rule.selector.split(",").map((s) => s.trim());
22
+ const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
23
+ if (kept.length === 0) rule.remove();
24
+ else if (kept.length < parts.length) rule.selector = kept.join(", ");
25
+ }
26
+ },
27
+ {
28
+ postcssPlugin: "tailwind-cleanup-at-rules",
29
+ AtRule(rule) {
30
+ if (atRules.includes(rule.name)) rule.remove();
31
+ }
32
+ },
33
+ {
34
+ postcssPlugin: "tailwind-cleanup-text-decoration",
35
+ Declaration(decl) {
36
+ if (decl.prop === "text-decoration-line") decl.prop = "text-decoration";
37
+ }
24
38
  }
25
- }, {
26
- postcssPlugin: "tailwind-cleanup-at-rules",
27
- AtRule(rule) {
28
- if (atRules.includes(rule.name)) rule.remove();
29
- }
30
- }];
39
+ ];
31
40
  }
32
41
 
33
42
  //#endregion