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

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 (76) hide show
  1. package/dist/_virtual/_rolldown/runtime.mjs +32 -0
  2. package/dist/build.mjs +2 -1
  3. package/dist/build.mjs.map +1 -1
  4. package/dist/components/Body.vue +105 -36
  5. package/dist/components/Button.vue +4 -1
  6. package/dist/components/CodeBlock.vue +1 -1
  7. package/dist/components/CodeInline.vue +6 -1
  8. package/dist/components/Column.vue +30 -5
  9. package/dist/components/Container.vue +10 -2
  10. package/dist/components/Divider.vue +28 -0
  11. package/dist/components/Head.vue +22 -0
  12. package/dist/components/Heading.vue +28 -0
  13. package/dist/components/Html.vue +98 -47
  14. package/dist/components/Layout.vue +93 -0
  15. package/dist/components/Link.vue +26 -0
  16. package/dist/components/Markdown.vue +22 -3
  17. package/dist/components/Outlook.vue +36 -0
  18. package/dist/components/Overlap.vue +25 -5
  19. package/dist/components/Preheader.vue +1 -1
  20. package/dist/components/Row.vue +16 -5
  21. package/dist/components/Section.vue +83 -0
  22. package/dist/components/Text.vue +29 -0
  23. package/dist/components/Vml.vue +165 -13
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.mjs +2 -1
  26. package/dist/node_modules/picomatch/index.mjs +13 -0
  27. package/dist/node_modules/picomatch/index.mjs.map +1 -0
  28. package/dist/node_modules/picomatch/lib/constants.mjs +174 -0
  29. package/dist/node_modules/picomatch/lib/constants.mjs.map +1 -0
  30. package/dist/node_modules/picomatch/lib/parse.mjs +1067 -0
  31. package/dist/node_modules/picomatch/lib/parse.mjs.map +1 -0
  32. package/dist/node_modules/picomatch/lib/picomatch.mjs +304 -0
  33. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +1 -0
  34. package/dist/node_modules/picomatch/lib/scan.mjs +296 -0
  35. package/dist/node_modules/picomatch/lib/scan.mjs.map +1 -0
  36. package/dist/node_modules/picomatch/lib/utils.mjs +53 -0
  37. package/dist/node_modules/picomatch/lib/utils.mjs.map +1 -0
  38. package/dist/plugin.mjs +11 -7
  39. package/dist/plugin.mjs.map +1 -1
  40. package/dist/plugins/postcss/tailwindCleanup.d.mts.map +1 -1
  41. package/dist/plugins/postcss/tailwindCleanup.mjs +24 -2
  42. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  43. package/dist/render/createRenderer.d.mts +3 -0
  44. package/dist/render/createRenderer.d.mts.map +1 -1
  45. package/dist/render/createRenderer.mjs +26 -7
  46. package/dist/render/createRenderer.mjs.map +1 -1
  47. package/dist/render/index.mjs +2 -1
  48. package/dist/render/index.mjs.map +1 -1
  49. package/dist/serve.d.mts.map +1 -1
  50. package/dist/serve.mjs +13 -6
  51. package/dist/serve.mjs.map +1 -1
  52. package/dist/server/compatibility.mjs +15 -1
  53. package/dist/server/compatibility.mjs.map +1 -1
  54. package/dist/server/email.mjs +2 -1
  55. package/dist/server/email.mjs.map +1 -1
  56. package/dist/server/linter.d.mts +1 -2
  57. package/dist/server/linter.d.mts.map +1 -1
  58. package/dist/server/linter.mjs +60 -71
  59. package/dist/server/linter.mjs.map +1 -1
  60. package/dist/server/ui/App.vue +9 -9
  61. package/dist/server/ui/pages/Preview.vue +215 -150
  62. package/dist/transformers/index.d.mts +10 -9
  63. package/dist/transformers/index.d.mts.map +1 -1
  64. package/dist/transformers/index.mjs +12 -9
  65. package/dist/transformers/index.mjs.map +1 -1
  66. package/dist/transformers/inlineCSS.d.mts +1 -14
  67. package/dist/transformers/inlineCSS.d.mts.map +1 -1
  68. package/dist/transformers/inlineCSS.mjs +16 -34
  69. package/dist/transformers/inlineCSS.mjs.map +1 -1
  70. package/dist/transformers/sixHex.d.mts +16 -0
  71. package/dist/transformers/sixHex.d.mts.map +1 -0
  72. package/dist/transformers/sixHex.mjs +30 -0
  73. package/dist/transformers/sixHex.mjs.map +1 -0
  74. package/dist/types/config.d.mts +57 -28
  75. package/dist/types/config.d.mts.map +1 -1
  76. package/package.json +2 -1
@@ -0,0 +1,32 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ //#region \0rolldown/runtime.js
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) {
16
+ __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ }
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
26
+ value: mod,
27
+ enumerable: true
28
+ }) : target, mod));
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ //#endregion
32
+ export { __commonJSMin, __require, __toESM };
package/dist/build.mjs CHANGED
@@ -41,7 +41,8 @@ async function build(options = {}) {
41
41
  const renderer = await createRenderer({
42
42
  markdown: config.markdown,
43
43
  root: config.root,
44
- componentDirs: [config.components?.source ?? []].flat()
44
+ componentDirs: [config.components?.source ?? []].flat(),
45
+ vite: config.vite
45
46
  });
46
47
  const outputFiles = [];
47
48
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"build.mjs","names":[],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { createPlaintext } from './plaintext.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildOptions {\n config?: Partial<MaizzleConfig> | string\n}\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n */\nexport async function build(options: BuildOptions = {}): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora('Building templates...').start()\n\n const config = await resolveConfig(options.config)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat() })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n let template = readFileSync(absolutePath, 'utf-8')\n\n template = await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n // Use the per-template merged config (from defineConfig() in the SFC) so that\n // template-level overrides like css.safe: false are respected by transformers.\n const templateConfig = rendered.templateConfig\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, html)\n outputFiles.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const stripOptions = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const plaintext = createPlaintext(html, stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`)\n } else if (typeof globalPlaintext === 'string') {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalPlaintext), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n\n // Register SFC event handlers that were collected during render\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n events.clearSfcHandlers()\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * This is used to strip the content base from template paths\n * so the output preserves only the subdirectory structure.\n */\nfunction computeContentBase(patterns: string[]): string {\n // Use the first non-negated pattern\n const pattern = patterns.find(p => !p.startsWith('!')) ?? patterns[0]\n\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n}\n\nfunction resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build.mjs","names":[],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { createPlaintext } from './plaintext.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildOptions {\n config?: Partial<MaizzleConfig> | string\n}\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n */\nexport async function build(options: BuildOptions = {}): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora('Building templates...').start()\n\n const config = await resolveConfig(options.config)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat(), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n let template = readFileSync(absolutePath, 'utf-8')\n\n template = await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n // Use the per-template merged config (from defineConfig() in the SFC) so that\n // template-level overrides like css.safe: false are respected by transformers.\n const templateConfig = rendered.templateConfig\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, html)\n outputFiles.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const stripOptions = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const plaintext = createPlaintext(html, stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`)\n } else if (typeof globalPlaintext === 'string') {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalPlaintext), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n\n // Register SFC event handlers that were collected during render\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n events.clearSfcHandlers()\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * This is used to strip the content base from template paths\n * so the output preserves only the subdirectory structure.\n */\nfunction computeContentBase(patterns: string[]): string {\n // Use the first non-negated pattern\n const pattern = patterns.find(p => !p.startsWith('!')) ?? patterns[0]\n\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n}\n\nfunction resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
@@ -1,42 +1,111 @@
1
- <script lang="ts">
2
- import { createStaticVNode } from 'vue'
1
+ <script setup lang="ts">
2
+ import { createStaticVNode, inject, useAttrs, useSlots } from 'vue'
3
3
  import type { PropType } from 'vue'
4
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
- }
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const attrs = useAttrs()
8
+ const slots = useSlots()
9
+
10
+ const props = defineProps({
11
+ /**
12
+ * Language code for the `xml:lang` attribute on `<body>`.
13
+ *
14
+ * Inherited from the parent `Html` component's `lang` prop by default.
15
+ *
16
+ * @example 'fr'
17
+ */
18
+ xmlLang: {
19
+ type: String as PropType<
20
+ | 'af' | 'ar' | 'az'
21
+ | 'be' | 'bg' | 'bs'
22
+ | 'ca' | 'cs' | 'cy'
23
+ | 'da' | 'de' | 'dv'
24
+ | 'el' | 'en' | 'es' | 'et' | 'eu'
25
+ | 'fa' | 'fi' | 'fo' | 'fr'
26
+ | 'gl' | 'gu'
27
+ | 'he' | 'hi' | 'hr' | 'hu' | 'hy'
28
+ | 'id' | 'is' | 'it'
29
+ | 'ja'
30
+ | 'ka' | 'kk' | 'kn' | 'ko' | 'ky'
31
+ | 'lt' | 'lv'
32
+ | 'mk' | 'mn' | 'mr' | 'ms' | 'mt'
33
+ | 'nb' | 'nl' | 'nn' | 'no'
34
+ | 'pa' | 'pl' | 'pt'
35
+ | 'ro' | 'ru'
36
+ | 'sa' | 'se' | 'sk' | 'sl' | 'sq' | 'sr' | 'sv' | 'sw'
37
+ | 'ta' | 'te' | 'th' | 'tr' | 'tt'
38
+ | 'uk' | 'ur' | 'uz'
39
+ | 'vi'
40
+ | 'zh'
41
+ | (string & {})
42
+ >,
43
+ default: undefined
44
+ },
45
+ /**
46
+ * Text direction of the body.
47
+ *
48
+ * - `ltr` — left to right (default)
49
+ * - `rtl` — right to left
50
+ *
51
+ * @default 'ltr'
52
+ */
53
+ dir: {
54
+ type: String as PropType<'ltr' | 'rtl'>,
55
+ default: 'ltr'
17
56
  },
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
- }
57
+ /**
58
+ * Accessible label for the email article wrapper.
59
+ *
60
+ * Used as the `aria-label` on the inner `<div role="article">`.
61
+ * Helps screen readers identify the email content.
62
+ *
63
+ * @example 'Order confirmation'
64
+ */
65
+ ariaLabel: {
66
+ type: String,
67
+ default: undefined
68
+ }
69
+ })
70
+
71
+ const htmlLang = inject<string>('htmlLang', 'en')
72
+
73
+ const render = () => {
74
+ const extraAttrs = Object.entries(attrs)
75
+ .map(([key, value]) => value === true ? key : `${key}="${value}"`)
76
+ .join(' ')
77
+
78
+ const lang = props.xmlLang ?? htmlLang
79
+
80
+ const parts = [
81
+ `xml:lang="${lang}"`,
82
+ `dir="${props.dir}"`,
83
+ 'style="margin: 0; padding: 0; width: 100%; word-break: break-word;"',
84
+ ]
85
+
86
+ if (extraAttrs) {
87
+ parts.push(extraAttrs)
40
88
  }
89
+
90
+ const articleParts = [
91
+ 'role="article"',
92
+ 'aria-roledescription="email"',
93
+ props.ariaLabel ? `aria-label="${props.ariaLabel}"` : '',
94
+ `lang="${lang}"`,
95
+ `dir="${props.dir}"`,
96
+ 'style="font-size: medium; font-size: max(16px, 1rem)"',
97
+ ].filter(Boolean).join(' ')
98
+
99
+ return [
100
+ createStaticVNode(`<body ${parts.join(' ')}>`, 1),
101
+ createStaticVNode(`<div ${articleParts}>`, 1),
102
+ slots.default?.(),
103
+ createStaticVNode('</div>', 1),
104
+ createStaticVNode('</body>', 1),
105
+ ]
41
106
  }
42
107
  </script>
108
+
109
+ <template>
110
+ <render />
111
+ </template>
@@ -10,7 +10,10 @@ const attrs = useAttrs()
10
10
 
11
11
  const props = defineProps({
12
12
  /** The URL the button links to. */
13
- href: String,
13
+ href: {
14
+ type: String,
15
+ required: true
16
+ },
14
17
  /**
15
18
  * The button style variant.
16
19
  * - `solid` — filled background (default)
@@ -9,7 +9,7 @@ export default {
9
9
  type: String,
10
10
  default: ''
11
11
  },
12
- /** The language for syntax highlighting. @default 'html' */
12
+ /** The language for syntax highlighting. @default 'html' */
13
13
  language: {
14
14
  type: String as PropType<BundledLanguage>,
15
15
  default: 'html'
@@ -4,7 +4,12 @@ import { createStaticVNode } from 'vue'
4
4
  export default {
5
5
  inheritAttrs: false,
6
6
  props: {
7
- /** The inline code text. */
7
+ /**
8
+ * The inline code text to render.
9
+ *
10
+ * If not provided, the slot content is used instead.
11
+ * The text is HTML-escaped automatically.
12
+ */
8
13
  code: {
9
14
  type: String,
10
15
  default: ''
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, createStaticVNode, inject, useAttrs } from 'vue'
2
+ import { computed, createStaticVNode, inject, provide, useAttrs } from 'vue'
3
3
  import type { ComputedRef } from 'vue'
4
4
  import { normalizeToPixels } from './utils.ts'
5
5
 
@@ -8,10 +8,26 @@ defineOptions({ inheritAttrs: false })
8
8
  const attrs = useAttrs()
9
9
 
10
10
  const props = defineProps({
11
- /** Override the auto-computed min-width. */
11
+ /**
12
+ * Override the auto-computed column width.
13
+ *
14
+ * By default, the width is calculated from the parent `Row`
15
+ * by dividing its width by the column count.
16
+ */
12
17
  width: {
13
18
  type: [String, Number],
14
19
  default: null
20
+ },
21
+ /**
22
+ * Inline CSS applied only to the MSO `<td>` element.
23
+ *
24
+ * Use for Outlook-specific styling that shouldn't affect other clients.
25
+ *
26
+ * @example 'padding: 10px'
27
+ */
28
+ msoStyle: {
29
+ type: String,
30
+ default: undefined
15
31
  }
16
32
  })
17
33
 
@@ -26,10 +42,10 @@ const minWidth = computed(() => {
26
42
  // Fallback: divide container width by 2 if available
27
43
  if (containerWidth?.value) {
28
44
  const val = containerWidth.value
29
- if (typeof val === 'number') return `${val / 2}px`
45
+ if (typeof val === 'number') return `${parseFloat((val / 2).toFixed(2))}px`
30
46
  const num = Number.parseFloat(val)
31
47
  const unit = val.replace(String(num), '') || 'px'
32
- return `${num / 2}${unit}`
48
+ return `${parseFloat((num / 2).toFixed(2))}${unit}`
33
49
  }
34
50
 
35
51
  return '18.75em'
@@ -37,12 +53,21 @@ const minWidth = computed(() => {
37
53
 
38
54
  const msoWidth = computed(() => injectedMsoWidth?.value ?? '50%')
39
55
 
56
+ // Provide column width as containerWidth for nested Rows
57
+ provide('containerWidth', minWidth)
58
+
40
59
  const styles = computed(() => {
41
60
  return `display: inline-block; min-width: ${minWidth.value}; font-size: 16px; vertical-align: top;`
42
61
  })
43
62
 
63
+ const tdStyle = computed(() => {
64
+ const parts = ['vertical-align: top']
65
+ if (props.msoStyle) parts.push(props.msoStyle)
66
+ return parts.join('; ')
67
+ })
68
+
44
69
  const MsoBefore = () => createStaticVNode(
45
- `<!--[if mso]><td width="${msoWidth.value}" style="vertical-align:top"><![endif]-->`,
70
+ `<!--[if mso]><td width="${msoWidth.value}" style="${tdStyle.value}"><![endif]-->`,
46
71
  1
47
72
  )
48
73
 
@@ -7,7 +7,15 @@ defineOptions({ inheritAttrs: false })
7
7
  const attrs = useAttrs()
8
8
 
9
9
  const props = defineProps({
10
- /** Max width of the container. */
10
+ /**
11
+ * Max width of the container.
12
+ *
13
+ * 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.
16
+ *
17
+ * @default '37.5em'
18
+ */
11
19
  width: {
12
20
  type: [String, Number],
13
21
  default: '37.5em'
@@ -21,7 +29,7 @@ const styles = computed(() => {
21
29
  })
22
30
 
23
31
  const MsoBefore = () => createStaticVNode(
24
- `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width:${normalizeToPixels(props.width)}" align="center"><tr><td><![endif]-->`,
32
+ `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${normalizeToPixels(props.width)}" align="center"><tr><td><![endif]-->`,
25
33
  1
26
34
  )
27
35
 
@@ -5,34 +5,62 @@ import { normalizeToPixels } from './utils.ts'
5
5
  const attrs = useAttrs()
6
6
 
7
7
  const props = defineProps({
8
+ /**
9
+ * Height (thickness) of the divider line.
10
+ *
11
+ * @default '1px'
12
+ */
8
13
  height: {
9
14
  type: [String, Number],
10
15
  default: '1px'
11
16
  },
17
+ /**
18
+ * Background color of the divider line.
19
+ *
20
+ * Defaults to `#cbd5e1` when no Tailwind `bg-*` class is used.
21
+ *
22
+ * @example '#e2e8f0'
23
+ */
12
24
  color: {
13
25
  type: String,
14
26
  default: null
15
27
  },
28
+ /**
29
+ * Vertical spacing (margin) above and below the divider.
30
+ *
31
+ * Overridden by `top` and `bottom` if set.
32
+ *
33
+ * @default '24px'
34
+ */
16
35
  spaceY: {
17
36
  type: [String, Number],
18
37
  default: '24px'
19
38
  },
39
+ /**
40
+ * Horizontal spacing (margin) on both sides of the divider.
41
+ *
42
+ * Overridden by `left` and `right` if set.
43
+ */
20
44
  spaceX: {
21
45
  type: [String, Number],
22
46
  default: null
23
47
  },
48
+ /** Margin above the divider. Overrides `spaceY` for the top side. */
24
49
  top: {
25
50
  type: [String, Number],
26
51
  default: null
27
52
  },
53
+ /** Margin below the divider. Overrides `spaceY` for the bottom side. */
28
54
  bottom: {
29
55
  type: [String, Number],
30
56
  default: null
31
57
  },
58
+ /** Margin to the left of the divider. Overrides `spaceX` for the left side. */
32
59
  left: {
33
60
  type: [String, Number],
34
61
  default: null
35
62
  },
63
+ /** Margin to the right of the divider. Overrides `spaceX` for the right side. */
36
64
  right: {
37
65
  type: [String, Number],
38
66
  default: null
@@ -1,4 +1,26 @@
1
+ <script setup lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+
4
+ const props = defineProps({
5
+ /**
6
+ * Render an empty `<head>` before the main head element.
7
+ *
8
+ * This is a workaround for Yahoo! Mail on Android, which
9
+ * strips styles from the first `<head>` element.
10
+ *
11
+ * @default false
12
+ */
13
+ double: {
14
+ type: [Boolean, String],
15
+ default: false
16
+ }
17
+ })
18
+
19
+ const EmptyHead = () => createStaticVNode('<head></head>', 1)
20
+ </script>
21
+
1
22
  <template>
23
+ <EmptyHead v-if="props.double === true || props.double === 'true'" />
2
24
  <head>
3
25
  <meta charset="utf-8">
4
26
  <meta name="x-apple-disable-message-reformatting">
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
4
+
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const props = defineProps({
8
+ /**
9
+ * The heading level (1-6), corresponding to `<h1>` through `<h6>`.
10
+ * @default 1
11
+ */
12
+ level: {
13
+ type: [String, Number],
14
+ default: 1,
15
+ validator: (v: string | number) => [1, 2, 3, 4, 5, 6].includes(Number(v)),
16
+ },
17
+ })
18
+
19
+ const attrs = useAttrs()
20
+ const tag = computed(() => `h${props.level}`)
21
+ const mergedClass = computed(() => twMerge('m-0', attrs.class as string))
22
+ </script>
23
+
24
+ <template>
25
+ <component :is="tag" v-bind="$attrs" :class="mergedClass">
26
+ <slot />
27
+ </component>
28
+ </template>
@@ -1,53 +1,104 @@
1
- <script lang="ts">
2
- import { createStaticVNode } from 'vue'
1
+ <script setup lang="ts">
2
+ import { createStaticVNode, provide, useAttrs, useSlots } from 'vue'
3
3
  import type { PropType } from 'vue'
4
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
- }
5
+ defineOptions({ inheritAttrs: false })
6
+
7
+ const attrs = useAttrs()
8
+ const slots = useSlots()
9
+
10
+ const props = defineProps({
11
+ /**
12
+ * Language code for the `lang` attribute on `<html>`.
13
+ *
14
+ * Also provided to the child `Body` component for `xml:lang`.
15
+ *
16
+ * @default 'en'
17
+ */
18
+ lang: {
19
+ type: String as PropType<
20
+ | 'af' | 'ar' | 'az'
21
+ | 'be' | 'bg' | 'bs'
22
+ | 'ca' | 'cs' | 'cy'
23
+ | 'da' | 'de' | 'dv'
24
+ | 'el' | 'en' | 'es' | 'et' | 'eu'
25
+ | 'fa' | 'fi' | 'fo' | 'fr'
26
+ | 'gl' | 'gu'
27
+ | 'he' | 'hi' | 'hr' | 'hu' | 'hy'
28
+ | 'id' | 'is' | 'it'
29
+ | 'ja'
30
+ | 'ka' | 'kk' | 'kn' | 'ko' | 'ky'
31
+ | 'lt' | 'lv'
32
+ | 'mk' | 'mn' | 'mr' | 'ms' | 'mt'
33
+ | 'nb' | 'nl' | 'nn' | 'no'
34
+ | 'pa' | 'pl' | 'pt'
35
+ | 'ro' | 'ru'
36
+ | 'sa' | 'se' | 'sk' | 'sl' | 'sq' | 'sr' | 'sv' | 'sw'
37
+ | 'ta' | 'te' | 'th' | 'tr' | 'tt'
38
+ | 'uk' | 'ur' | 'uz'
39
+ | 'vi'
40
+ | 'zh'
41
+ | (string & {})
42
+ >,
43
+ default: 'en'
44
+ },
45
+ /**
46
+ * Text direction of the document.
47
+ *
48
+ * - `ltr` — left to right (default)
49
+ * - `rtl` — right to left
50
+ *
51
+ * @default 'ltr'
52
+ */
53
+ dir: {
54
+ type: String as PropType<'ltr' | 'rtl'>,
55
+ default: 'ltr'
21
56
  },
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
- }
57
+ /**
58
+ * Whether to include VML and Office XML namespace declarations.
59
+ *
60
+ * Required for Outlook VML support (background images, etc.).
61
+ * Set to `false` to omit the `xmlns:v` and `xmlns:o` attributes.
62
+ *
63
+ * @default true
64
+ */
65
+ xmlns: {
66
+ type: [Boolean, String],
67
+ default: true
68
+ }
69
+ })
70
+
71
+ provide('htmlLang', props.lang)
72
+
73
+ const render = () => {
74
+ const extraAttrs = Object.entries(attrs)
75
+ .map(([key, value]) => value === true ? key : `${key}="${value}"`)
76
+ .join(' ')
77
+
78
+ const parts = [
79
+ `lang="${props.lang}"`,
80
+ `dir="${props.dir}"`,
81
+ ]
82
+
83
+ if (props.xmlns !== false && props.xmlns !== 'false') {
84
+ parts.push(
85
+ 'xmlns:v="urn:schemas-microsoft-com:vml"',
86
+ 'xmlns:o="urn:schemas-microsoft-com:office:office"',
87
+ )
88
+ }
89
+
90
+ if (extraAttrs) {
91
+ parts.push(extraAttrs)
51
92
  }
93
+
94
+ return [
95
+ createStaticVNode(`<html ${parts.join(' ')}>`, 1),
96
+ slots.default?.(),
97
+ createStaticVNode('</html>', 1),
98
+ ]
52
99
  }
53
100
  </script>
101
+
102
+ <template>
103
+ <render />
104
+ </template>