@maizzle/framework 6.0.0-rc.24 → 6.0.0-rc.25

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 (122) hide show
  1. package/dist/build.js.map +1 -1
  2. package/dist/components/Body.vue +12 -0
  3. package/dist/components/Button.vue +16 -29
  4. package/dist/components/CodeBlock.vue +5 -4
  5. package/dist/components/CodeInline.vue +9 -8
  6. package/dist/components/Column.vue +17 -4
  7. package/dist/components/Container.vue +7 -11
  8. package/dist/components/Hr.vue +1 -1
  9. package/dist/components/Img.vue +39 -22
  10. package/dist/components/Layout.vue +1 -1
  11. package/dist/components/Markdown.vue +9 -14
  12. package/dist/components/QrCode.vue +2 -2
  13. package/dist/components/Section.vue +9 -6
  14. package/dist/components/Text.vue +2 -2
  15. package/dist/components/utils.d.ts +25 -1
  16. package/dist/components/utils.d.ts.map +1 -1
  17. package/dist/components/utils.js +29 -1
  18. package/dist/components/utils.js.map +1 -1
  19. package/dist/components/utils.ts +46 -0
  20. package/dist/composables/useConfig.d.ts.map +1 -1
  21. package/dist/composables/useCurrentTemplate.d.ts.map +1 -1
  22. package/dist/composables/useEvent.d.ts.map +1 -1
  23. package/dist/composables/useFont.js.map +1 -1
  24. package/dist/config/index.js.map +1 -1
  25. package/dist/events/index.d.ts.map +1 -1
  26. package/dist/events/index.js.map +1 -1
  27. package/dist/plaintext.js.map +1 -1
  28. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
  29. package/dist/plugins/postcss/pruneVars.js.map +1 -1
  30. package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -1
  31. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
  32. package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
  33. package/dist/plugins/postcss/resolveProps.d.ts.map +1 -1
  34. package/dist/plugins/postcss/resolveProps.js +0 -3
  35. package/dist/plugins/postcss/resolveProps.js.map +1 -1
  36. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
  37. package/dist/prepare.js +1 -1
  38. package/dist/prepare.js.map +1 -1
  39. package/dist/render/active.d.ts.map +1 -1
  40. package/dist/render/createRenderer.d.ts.map +1 -1
  41. package/dist/render/createRenderer.js +3 -7
  42. package/dist/render/createRenderer.js.map +1 -1
  43. package/dist/render/index.js.map +1 -1
  44. package/dist/render/injectFonts.js.map +1 -1
  45. package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -1
  46. package/dist/render/plugins/codeBlockExtract.js.map +1 -1
  47. package/dist/render/plugins/markdownExtract.d.ts.map +1 -1
  48. package/dist/render/plugins/markdownExtract.js.map +1 -1
  49. package/dist/render/plugins/rawExtract.d.ts.map +1 -1
  50. package/dist/render/plugins/rawExtract.js.map +1 -1
  51. package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -1
  52. package/dist/render/plugins/rowSourceLocation.js.map +1 -1
  53. package/dist/serve.js.map +1 -1
  54. package/dist/server/compatibility.d.ts.map +1 -1
  55. package/dist/server/compatibility.js.map +1 -1
  56. package/dist/server/linter.js.map +1 -1
  57. package/dist/server/sfc-utils.js.map +1 -1
  58. package/dist/tests/render/_helpers.d.ts.map +1 -1
  59. package/dist/tests/render/_helpers.js.map +1 -1
  60. package/dist/transformers/addAttributes.js +2 -3
  61. package/dist/transformers/addAttributes.js.map +1 -1
  62. package/dist/transformers/base.d.ts +1 -1
  63. package/dist/transformers/base.d.ts.map +1 -1
  64. package/dist/transformers/base.js +5 -10
  65. package/dist/transformers/base.js.map +1 -1
  66. package/dist/transformers/columnWidth.d.ts.map +1 -1
  67. package/dist/transformers/columnWidth.js +2 -7
  68. package/dist/transformers/columnWidth.js.map +1 -1
  69. package/dist/transformers/entities.js.map +1 -1
  70. package/dist/transformers/filters/defaults.js.map +1 -1
  71. package/dist/transformers/filters/index.js.map +1 -1
  72. package/dist/transformers/format.js.map +1 -1
  73. package/dist/transformers/imgWidth.d.ts +20 -0
  74. package/dist/transformers/imgWidth.d.ts.map +1 -0
  75. package/dist/transformers/imgWidth.js +76 -0
  76. package/dist/transformers/imgWidth.js.map +1 -0
  77. package/dist/transformers/index.d.ts.map +1 -1
  78. package/dist/transformers/index.js +2 -0
  79. package/dist/transformers/index.js.map +1 -1
  80. package/dist/transformers/inlineCss.d.ts +3 -2
  81. package/dist/transformers/inlineCss.d.ts.map +1 -1
  82. package/dist/transformers/inlineCss.js.map +1 -1
  83. package/dist/transformers/inlineLink.js +1 -1
  84. package/dist/transformers/inlineLink.js.map +1 -1
  85. package/dist/transformers/minify.js.map +1 -1
  86. package/dist/transformers/minifyCodeInline.js.map +1 -1
  87. package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
  88. package/dist/transformers/msoPlaceholders.js +2 -7
  89. package/dist/transformers/msoPlaceholders.js.map +1 -1
  90. package/dist/transformers/purgeCss.js.map +1 -1
  91. package/dist/transformers/replaceStrings.js.map +1 -1
  92. package/dist/transformers/safeSelectors.js.map +1 -1
  93. package/dist/transformers/shorthandCss.js.map +1 -1
  94. package/dist/transformers/tailwindComponent.js.map +1 -1
  95. package/dist/transformers/tailwindcss.js +1 -1
  96. package/dist/transformers/tailwindcss.js.map +1 -1
  97. package/dist/transformers/urlQuery.js.map +1 -1
  98. package/dist/types/config.d.ts +5 -4
  99. package/dist/types/config.d.ts.map +1 -1
  100. package/dist/utils/ast/serializer.js.map +1 -1
  101. package/dist/utils/compileTailwindCss.js.map +1 -1
  102. package/dist/utils/componentSources.js.map +1 -1
  103. package/dist/utils/cssBox.d.ts.map +1 -1
  104. package/dist/utils/cssBox.js +2 -7
  105. package/dist/utils/cssBox.js.map +1 -1
  106. package/dist/utils/decodeStyleEntities.js.map +1 -1
  107. package/dist/utils/url.js.map +1 -1
  108. package/dist/utils/watchPaths.js.map +1 -1
  109. package/node_modules/@clack/core/CHANGELOG.md +30 -0
  110. package/node_modules/@clack/core/dist/index.d.mts +109 -3
  111. package/node_modules/@clack/core/dist/index.mjs +972 -17
  112. package/node_modules/@clack/core/package.json +2 -1
  113. package/node_modules/@clack/prompts/CHANGELOG.md +42 -0
  114. package/node_modules/@clack/prompts/README.md +30 -9
  115. package/node_modules/@clack/prompts/dist/index.d.mts +458 -27
  116. package/node_modules/@clack/prompts/dist/index.mjs +1378 -141
  117. package/node_modules/@clack/prompts/package.json +2 -2
  118. package/node_modules/tinyexec/package.json +4 -4
  119. package/package.json +8 -7
  120. package/dist/components/Overlap.vue +0 -156
  121. package/node_modules/@clack/core/dist/index.mjs.map +0 -1
  122. package/node_modules/@clack/prompts/dist/index.mjs.map +0 -1
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","names":["parsePath"],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, parse as parsePath } 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 { stripForHtml, stripForPlaintext } from './utils/output-markers.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { _setCurrentTemplate } from './composables/useCurrentTemplate.ts'\nimport defu from 'defu'\nimport type { MaizzleConfig } from './types/index.ts'\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 *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\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: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n\n _setCurrentTemplate(parsedPath)\n\n try {\n await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n /**\n * Register SFC event handlers collected during render so they take\n * part in the post-render events (afterRender / afterTransform).\n * They're cleared at the end of the iteration so they don't\n * leak into the next template.\n */\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n /**\n * Use the per-template merged config (from defineConfig() in the SFC) so\n * that template-level overrides like css.safe: false are respected\n * by transformers.\n */\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, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n if (doctype) html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const sfcOutputPath = rendered.outputPath\n let outputFilePath: string\n\n if (sfcOutputPath) {\n const parsed = parsePath(resolve(sfcOutputPath))\n const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension\n outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`)\n } else {\n outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n }\n\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\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 globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.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 (sfcOutputPath) {\n const parsed = parsePath(outputFilePath)\n ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`)\n } else if (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), 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 } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\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.js","names":["parsePath"],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, parse as parsePath } 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 { stripForHtml, stripForPlaintext } from './utils/output-markers.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { _setCurrentTemplate } from './composables/useCurrentTemplate.ts'\nimport defu from 'defu'\nimport type { MaizzleConfig } from './types/index.ts'\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 *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\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: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n\n _setCurrentTemplate(parsedPath)\n\n try {\n await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n /**\n * Register SFC event handlers collected during render so they take\n * part in the post-render events (afterRender / afterTransform).\n * They're cleared at the end of the iteration so they don't\n * leak into the next template.\n */\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n /**\n * Use the per-template merged config (from defineConfig() in the SFC) so\n * that template-level overrides like css.safe: false are respected\n * by transformers.\n */\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, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n if (doctype) html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const sfcOutputPath = rendered.outputPath\n let outputFilePath: string\n\n if (sfcOutputPath) {\n const parsed = parsePath(resolve(sfcOutputPath))\n const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension\n outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`)\n } else {\n outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n }\n\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\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 globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.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 (sfcOutputPath) {\n const parsed = parsePath(outputFilePath)\n ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`)\n } else if (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), 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 } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\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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"}
@@ -85,6 +85,17 @@ const outlookFallback = useOutlookFallback(props.outlookFallback)
85
85
 
86
86
  const htmlLang = inject<string>('htmlLang', 'en')
87
87
 
88
+ const msoBody = `<!--[if mso]>
89
+ <xml>
90
+ <o:OfficeDocumentSettings>
91
+ <o:PixelsPerInch>96</o:PixelsPerInch>
92
+ </o:OfficeDocumentSettings>
93
+ <w:WordDocument>
94
+ <w:DontUseAdvancedTypographyReadingMail />
95
+ </w:WordDocument>
96
+ </xml>
97
+ <![endif]-->`
98
+
88
99
  const render = () => {
89
100
  const extraAttrs = Object.entries(attrs)
90
101
  .map(([key, value]) => value === true ? key : `${key}="${value}"`)
@@ -115,6 +126,7 @@ const render = () => {
115
126
 
116
127
  return [
117
128
  createStaticVNode(`<body ${parts.join(' ')}>`, 1),
129
+ outlookFallback ? createStaticVNode(`<span style="display: none">${msoBody}</span>`, 1) : null,
118
130
  createStaticVNode(`<div ${articleParts}>`, 1),
119
131
  slots.default?.(),
120
132
  createStaticVNode('</div>', 1),
@@ -129,48 +129,35 @@ const alignClass = computed(() => props.align ? ({
129
129
  right: 'text-right',
130
130
  })[props.align] || '' : '')
131
131
 
132
- const styles = computed(() => {
132
+ const baseClasses = computed(() => {
133
133
  if (props.variant === 'link') {
134
- return 'text-decoration: none; color: #4338ca;'
134
+ return 'no-underline text-gray-950'
135
135
  }
136
136
 
137
- const base = [
138
- 'display: inline-block;',
139
- 'text-decoration: none;',
140
- 'padding: 16px 24px;',
141
- 'font-size: 16px;',
142
- 'line-height: 1;',
143
- 'border-radius: 4px;',
137
+ const classes = [
138
+ 'inline-block',
139
+ 'no-underline',
140
+ 'px-6',
141
+ 'py-4',
142
+ 'text-base',
143
+ 'leading-none',
144
+ 'rounded',
144
145
  ]
145
146
 
146
147
  if (props.variant === 'outline') {
147
- base.push(
148
- 'background-color: transparent;',
149
- 'border: 1px solid #4338ca;',
150
- 'color: #4338ca;',
151
- )
148
+ classes.push('bg-transparent', 'border', 'border-solid', 'border-indigo-700', 'text-indigo-700')
152
149
  } else if (props.variant === 'ghost') {
153
- base.push(
154
- 'background-color: transparent;',
155
- 'color: #4338ca;',
156
- )
150
+ classes.push('bg-transparent', 'text-indigo-700', 'hover:bg-indigo-50')
157
151
  } else {
158
- base.push(
159
- 'background-color: #4338ca;',
160
- 'color: #fffffe;',
161
- )
152
+ classes.push('bg-indigo-700', 'text-white')
162
153
  }
163
154
 
164
- return base.join('')
155
+ return classes.join(' ')
165
156
  })
166
157
 
167
158
  const isLink = computed(() => props.variant === 'link')
168
159
 
169
- const variantClasses = computed(() =>
170
- props.variant === 'ghost' ? 'hover:bg-indigo-50' : '',
171
- )
172
-
173
- const mergedClass = computed(() => twMerge(variantClasses.value, attrs.class as string))
160
+ const mergedClass = computed(() => twMerge(baseClasses.value, attrs.class as string))
174
161
 
175
162
  const textSpanStyle = computed(() =>
176
163
  outlookFallback ? `mso-text-raise: ${props.msoPt};` : undefined,
@@ -209,7 +196,7 @@ const MsoIconGap = () => createStaticVNode(
209
196
  <a
210
197
  v-bind="{ ...$attrs, class: undefined, style: undefined }"
211
198
  :href="href"
212
- :style="[styles, $attrs.style as any]"
199
+ :style="$attrs.style as any"
213
200
  :class="mergedClass"
214
201
  >
215
202
  <template v-if="!isLink">
@@ -2,6 +2,7 @@
2
2
  import { createStaticVNode, type PropType } from 'vue'
3
3
  import { twMerge } from 'tailwind-merge'
4
4
  import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
5
+ import { buildCodeBlock, codeBlockPreClass } from './utils'
5
6
 
6
7
  export default {
7
8
  props: {
@@ -57,11 +58,11 @@ export default {
57
58
  .replace(/^<pre[^>]*><code>/, '')
58
59
  .replace(/<\/code><\/pre>$/, '')
59
60
 
60
- const classes = twMerge('font-mono', attrs.class as string)
61
- const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
62
- const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
61
+ const preClass = twMerge(codeBlockPreClass(bg), attrs.class as string)
62
+ const tdClass = twMerge(`bg-[${bg}]`, props.tdClass)
63
+ const styleAttr = attrs.style ? ` style="${attrs.style}"` : ''
63
64
 
64
- const html = `<table class="w-full"><tr><td class="${props.tdClass}" style="background-color:${bg}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
65
+ const html = buildCodeBlock(codeContent, bg, { preClass, tdClass, styleAttr })
65
66
 
66
67
  return () => createStaticVNode(html, 1)
67
68
  }
@@ -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 {
@@ -50,8 +51,6 @@ export default {
50
51
  return () => createStaticVNode('', 0)
51
52
  }
52
53
 
53
- const classes = attrs.class ? ` class="${attrs.class}"` : ''
54
-
55
54
  if (props.theme) {
56
55
  const highlighted = await codeToHtml(source, {
57
56
  lang: props.language,
@@ -95,15 +94,17 @@ export default {
95
94
  .replace(/</g, '§MZLT§')
96
95
  .replace(/>/g, '§MZGT§')
97
96
 
98
- const baseStyles = `background-color:${bg};border-radius:6px;padding:2px 6px;font-size:11px`
99
- const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
97
+ const base = `bg-[${bg}] rounded-md py-0.5 px-1.5 text-[11px]`
98
+ const merged = twMerge(base, (attrs.class as string) ?? '')
99
+ const styleAttr = attrs.style ? ` style="${attrs.style}"` : ''
100
100
 
101
- const html = `<code${classes} style="${styles}" data-minify-inline>${escaped}</code>`
101
+ const html = `<code class="${merged}"${styleAttr} data-minify-inline>${escaped}</code>`
102
102
  return () => createStaticVNode(html, 1)
103
103
  }
104
104
 
105
- const baseStyles = 'white-space:normal;border-radius:6px;border:1px solid #d1d5db;background-color:#f3f4f6;padding:2px 6px;font-size:11px;color:inherit'
106
- const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
105
+ const base = 'whitespace-normal rounded-md [border:1px_solid_#d1d5db] bg-gray-100 py-0.5 px-1.5 text-[11px] text-inherit'
106
+ const merged = twMerge(base, (attrs.class as string) ?? '')
107
+ const styleAttr = attrs.style ? ` style="${attrs.style}"` : ''
107
108
 
108
109
  const escaped = source
109
110
  .replace(/&/g, '&amp;')
@@ -111,7 +112,7 @@ export default {
111
112
  .replace(/>/g, '&gt;')
112
113
  .replace(/"/g, '&quot;')
113
114
 
114
- const html = `<code${classes} style="${styles}">${escaped}</code>`
115
+ const html = `<code class="${merged}"${styleAttr}>${escaped}</code>`
115
116
 
116
117
  return () => createStaticVNode(html, 1)
117
118
  }
@@ -70,14 +70,27 @@ const msoWidth = computed(() => {
70
70
  * `display: inline-block` would silently shadow a class like
71
71
  * `inline-table` during CSS inlining; routing both through twMerge lets
72
72
  * the user's utility cleanly replace ours instead of being dropped.
73
+ *
74
+ * When `width` is set as a prop the resolved pixel value also goes
75
+ * through the class list (`min-w-[Npx]`) so it dedupes against the
76
+ * user's `min-w-*` utility. The marker path (no prop) has to stay
77
+ * inline because the placeholder string is replaced post-render and
78
+ * Tailwind's content scanner can't compile a class whose value is
79
+ * still a marker.
73
80
  */
74
- const baseClass = 'inline-block align-top text-[medium]'
75
- const mergedClass = computed(() => twMerge(baseClass, (attrs.class as string) ?? ''))
81
+ const baseClass = 'inline-block text-[medium]'
82
+ const mergedClass = computed(() => {
83
+ const parts = [baseClass]
84
+ if (props.width != null) parts.push(`min-w-[${normalizeToPixels(props.width)}]`)
85
+ return twMerge(parts.join(' '), (attrs.class as string) ?? '')
86
+ })
76
87
 
77
- const styles = computed(() => `min-width: ${minWidth.value};`)
88
+ const styles = computed(() =>
89
+ props.width != null ? undefined : `min-width: ${minWidth.value};`
90
+ )
78
91
 
79
92
  const tdStyle = computed(() => {
80
- const parts = [`width: ${msoWidth.value}`, 'vertical-align: top']
93
+ const parts = [`width: ${msoWidth.value}`]
81
94
  if (useMarker) parts.push(`__MAIZZLE_COLTDX_${colId}__`)
82
95
  if (props.msoStyle) parts.push(props.msoStyle)
83
96
  return parts.join('; ')
@@ -60,18 +60,15 @@ const useMarker = outlookFallback && props.width == null
60
60
  const msoId = useMarker ? nextId('c') : null
61
61
  const tdId = outlookFallback ? nextId('ct') : null
62
62
 
63
- const styles = computed(() => {
64
- if (props.width == null) return undefined
65
- return `max-width: ${normalizeToPixels(props.width)}; margin: 0 auto;`
66
- })
67
-
68
63
  const mergedClass = computed(() => {
69
- if (props.width != null) return attrs.class as string | undefined
70
64
  const userClass = (attrs.class as string) ?? ''
71
- const defaultClass = hasWidthUtility(userClass)
72
- ? 'm-0 mx-auto'
73
- : 'max-w-150 m-0 mx-auto'
74
- return twMerge(defaultClass, userClass)
65
+ const parts: string[] = ['m-0', 'mx-auto']
66
+ if (props.width != null) {
67
+ parts.push(`max-w-[${normalizeToPixels(props.width)}]`)
68
+ } else if (!hasWidthUtility(userClass)) {
69
+ parts.push('max-w-150')
70
+ }
71
+ return twMerge(parts.join(' '), userClass)
75
72
  })
76
73
 
77
74
  const msoWidth = computed(() => {
@@ -101,7 +98,6 @@ const MsoAfter = () => createStaticVNode(
101
98
  <div
102
99
  v-bind="{ ...attrs, class: undefined }"
103
100
  :class="mergedClass"
104
- :style="styles"
105
101
  :data-maizzle-msow-id="msoId"
106
102
  :data-maizzle-cw="colWidthSource"
107
103
  :data-maizzle-mso-td-id="tdId"
@@ -15,7 +15,7 @@ const mergedClass = computed(() => {
15
15
  const userHeight = heights.length ? heights[heights.length - 1][1] : null
16
16
  const userHasLeading = LEADING_RE.test(userClass)
17
17
 
18
- const defaults = ['my-6', 'bg-slate-300']
18
+ const defaults = ['my-6', 'bg-gray-300']
19
19
  if (!userHeight) defaults.push('h-px')
20
20
  if (!userHasLeading && !userHeight) defaults.push('leading-px')
21
21
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, useAttrs, type PropType } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
  import { outlookFallbackProp } from './utils.ts'
4
5
  import { useOutlookFallback } from '../composables/useOutlookFallback'
5
6
 
@@ -32,15 +33,22 @@ const props = defineProps({
32
33
  type: String,
33
34
  default: null
34
35
  },
35
- /** The width of the image, rendered without units. */
36
+ /**
37
+ * The width of the image, rendered without units.
38
+ *
39
+ * Optional: when omitted, the width is auto-derived post-render from
40
+ * the nearest sized ancestor (Container/Section/Column or any element
41
+ * with a pixel width). Falls back to fluid when no pixel width is
42
+ * resolvable. The `aspect` crop mode still requires an explicit width.
43
+ */
36
44
  width: {
37
45
  type: [String, Number],
38
- required: true
46
+ default: undefined
39
47
  },
40
48
  /** Animated image source, shown when user has no reduced motion preference. */
41
49
  motionSrc: {
42
50
  type: String,
43
- default: null
51
+ default: ''
44
52
  },
45
53
  /**
46
54
  * Aspect ratio for cropped images.
@@ -111,7 +119,7 @@ const props = defineProps({
111
119
  const outlookFallback = useOutlookFallback(props.outlookFallback)
112
120
 
113
121
  function mimeFromExtension(src: string): string {
114
- const ext = src.split('.').pop()?.toLowerCase() ?? ''
122
+ const ext = src.slice(src.lastIndexOf('.') + 1).toLowerCase()
115
123
 
116
124
  const types: Record<string, string> = {
117
125
  apng: 'image/apng',
@@ -133,17 +141,12 @@ const ASPECT_KEYWORDS: Record<string, string> = {
133
141
  'aspect-video': '16/9',
134
142
  }
135
143
 
144
+ /**
145
+ * Vue normalizes a component's `class` attr to a string before it
146
+ * reaches `attrs`, so only the string/empty cases can occur here.
147
+ */
136
148
  function normalizeClass(value: unknown): string {
137
- if (!value) return ''
138
- if (typeof value === 'string') return value
139
- if (Array.isArray(value)) return value.map(normalizeClass).filter(Boolean).join(' ')
140
- if (typeof value === 'object') {
141
- return Object.entries(value as Record<string, unknown>)
142
- .filter(([, v]) => v)
143
- .map(([k]) => k)
144
- .join(' ')
145
- }
146
- return ''
149
+ return typeof value === 'string' ? value : ''
147
150
  }
148
151
 
149
152
  /**
@@ -182,10 +185,18 @@ const ratio = computed(() => {
182
185
 
183
186
  const isCropped = computed(() => ratio.value !== null)
184
187
 
185
- const motionType = computed(() => mimeFromExtension(props.motionSrc ?? ''))
188
+ const motionType = computed(() => mimeFromExtension(props.motionSrc))
186
189
 
187
190
  const imgWidth = computed(() => Number.parseInt(String(props.width), 10))
188
191
 
192
+ /**
193
+ * Whether an explicit, usable pixel width was supplied. When false, the
194
+ * non-cropped `<img>` is emitted without a width attribute plus a
195
+ * `data-maizzle-img-width` marker the `imgWidth` transformer reads to
196
+ * backfill the width from the nearest sized ancestor.
197
+ */
198
+ const hasWidth = computed(() => props.width != null && props.width !== '' && Number.isFinite(imgWidth.value))
199
+
189
200
  const heightPx = computed(() =>
190
201
  ratio.value && Number.isFinite(imgWidth.value)
191
202
  ? Math.round((imgWidth.value * ratio.value.h) / ratio.value.w)
@@ -243,6 +254,13 @@ const NotMsoBefore = () => createStaticVNode('<!--[if !mso]><!-->', 1)
243
254
  const NotMsoAfter = () => createStaticVNode('<!--<![endif]-->', 1)
244
255
 
245
256
  const imgClass = 'max-w-full align-middle'
257
+
258
+ const cropClass = computed(() =>
259
+ twMerge(
260
+ `overflow-hidden table max-w-full${hasWidth.value ? ` w-[${imgWidth.value}px]` : ''}`,
261
+ parsedClass.value.className,
262
+ )
263
+ )
246
264
  </script>
247
265
 
248
266
  <template>
@@ -254,8 +272,7 @@ const imgClass = 'max-w-full align-middle'
254
272
  v-bind="{ ...attrs, class: undefined }"
255
273
  role="img"
256
274
  :aria-label="alt || undefined"
257
- :class="['overflow-hidden table max-w-full', parsedClass.className]"
258
- :style="`width: ${imgWidth}px;`"
275
+ :class="cropClass"
259
276
  >
260
277
  <div
261
278
  :class="[
@@ -278,7 +295,7 @@ const imgClass = 'max-w-full align-middle'
278
295
  role="img"
279
296
  :aria-label="alt || undefined"
280
297
  :class="['overflow-hidden table max-w-full', parsedClass.className]"
281
- :style="`width: ${imgWidth}px;`"
298
+ :style="hasWidth ? `width: ${imgWidth}px;` : undefined"
282
299
  >
283
300
  <div
284
301
  :class="[
@@ -300,16 +317,16 @@ const imgClass = 'max-w-full align-middle'
300
317
  <picture>
301
318
  <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
302
319
  <source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
303
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
320
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
304
321
  </picture>
305
322
  </a>
306
323
  <picture v-else-if="usePicture">
307
324
  <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
308
325
  <source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
309
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
326
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
310
327
  </picture>
311
328
  <a v-else-if="href" :href="href">
312
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
329
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
313
330
  </a>
314
- <img v-else v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
331
+ <img v-else v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
315
332
  </template>
@@ -116,7 +116,7 @@ const htmlXmlns = computed(() => outlookFallback ? {
116
116
  <MsoHead v-if="outlookFallback" />
117
117
  <link rel="preconnect" href="https://fonts.googleapis.com">
118
118
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
119
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" media="screen">
119
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" media="screen">
120
120
  <style>
121
121
  @import "@maizzle/tailwindcss";
122
122
 
@@ -3,7 +3,8 @@ import { createStaticVNode, inject, type PropType } from 'vue'
3
3
  import { createMarkdownExit, type MarkdownExitOptions } from 'markdown-exit'
4
4
  import { codeToHtml, type BundledTheme } from 'shiki'
5
5
  import { defu } from 'defu'
6
- import { MaizzleConfigKey } from '../composables/useConfig.ts'
6
+ import { MaizzleConfigKey } from '../composables/useConfig'
7
+ import { shikiToCodeBlock } from './utils'
7
8
 
8
9
  export default {
9
10
  props: {
@@ -62,9 +63,9 @@ export default {
62
63
  * still works standalone, when no config is provided.
63
64
  */
64
65
  const mdConfig = inject(MaizzleConfigKey, undefined)?.markdown ?? {}
65
- const markdownOptions = mdConfig.markdownOptions ?? mdConfig.markdownItOptions
66
- const markdownUses = mdConfig.markdownUses ?? mdConfig.markdownItUses
67
- const markdownSetup = mdConfig.markdownSetup ?? mdConfig.markdownItSetup
66
+ const markdownOptions = mdConfig.markdownOptions
67
+ const markdownUses = mdConfig.markdownUses
68
+ const markdownSetup = mdConfig.markdownSetup
68
69
  const theme = props.shikiTheme ?? mdConfig.shikiTheme ?? 'github-dark-high-contrast'
69
70
 
70
71
  const md = createMarkdownExit(defu(
@@ -89,23 +90,17 @@ export default {
89
90
  * code-block wrapping below composes over whatever they emit.
90
91
  */
91
92
  for (const use of markdownUses ?? []) {
92
- if (Array.isArray(use)) md.use(...use)
93
+ if (Array.isArray(use)) md.use(use[0], ...use.slice(1))
93
94
  else md.use(use)
94
95
  }
95
96
  await markdownSetup?.(md)
96
97
 
97
- const wrapPre = (html: string) =>
98
- `<table class="w-full"><tr><td class="max-w-0 mso-padding-alt-4">${html}</td></tr></table>\n`
99
-
100
98
  const defaultFence = md.renderer.rules.fence!
101
- md.renderer.rules.fence = (...args) => {
102
- const result = defaultFence(...args)
103
- if (typeof result === 'string') return wrapPre(result)
104
- return result.then(wrapPre)
105
- }
99
+ md.renderer.rules.fence = (...args) =>
100
+ Promise.resolve(defaultFence(...args)).then(shikiToCodeBlock)
106
101
 
107
102
  const defaultCodeBlock = md.renderer.rules.code_block!
108
- md.renderer.rules.code_block = (...args) => wrapPre(defaultCodeBlock(...args) as string)
103
+ md.renderer.rules.code_block = (...args) => shikiToCodeBlock(defaultCodeBlock(...args) as string)
109
104
 
110
105
  let html = await md.renderAsync(source)
111
106
 
@@ -21,7 +21,7 @@ const escapeAttr = (v: string) =>
21
21
  * fall through to the caller's default.
22
22
  */
23
23
  function tokenToPx(token: string): number {
24
- const seg = token.split(':').at(-1) ?? ''
24
+ const seg = token.slice(token.lastIndexOf(':') + 1)
25
25
  const m = seg.match(/^(?:size|w|h)-(.+)$/)
26
26
  if (!m) return 0
27
27
  const v = m[1]
@@ -45,7 +45,7 @@ function partition(cls: string): { neutral: string[]; sizing: string[] } {
45
45
  const neutral: string[] = []
46
46
  const sizing: string[] = []
47
47
  for (const t of cls.split(/\s+/).filter(Boolean)) {
48
- const last = t.split(':').at(-1) ?? ''
48
+ const last = t.slice(t.lastIndexOf(':') + 1)
49
49
  if (/^(?:size|w|h|min-w|min-h|max-w|max-h)-/.test(last)) sizing.push(t)
50
50
  else neutral.push(t)
51
51
  }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, useAttrs } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
  import { hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp } from './utils.ts'
4
5
  import { useOutlookFallback } from '../composables/useOutlookFallback'
5
6
 
@@ -65,15 +66,16 @@ const useMarker = outlookFallback && props.width == null && userHasWidth.value
65
66
  const msoId = useMarker ? nextId('s') : null
66
67
  const tdId = outlookFallback ? nextId('st') : null
67
68
 
68
- const divStyle = computed(() => {
69
- const parts: string[] = []
70
- if (props.width != null) parts.push(`max-width: ${normalizeToPixels(props.width)}`)
71
- if (userStyle.value) parts.push(userStyle.value)
72
- return parts.length ? parts.join('; ') : undefined
69
+ const mergedClass = computed(() => {
70
+ const userClass = (attrs.class as string) ?? ''
71
+ if (props.width == null) return userClass || undefined
72
+ return twMerge(`max-w-[${normalizeToPixels(props.width)}]`, userClass)
73
73
  })
74
74
 
75
+ const divStyle = computed(() => userStyle.value || undefined)
76
+
75
77
  const restAttrs = computed(() => {
76
- const { style: _, ...rest } = attrs
78
+ const { style: _, class: __, ...rest } = attrs
77
79
  return rest
78
80
  })
79
81
 
@@ -106,6 +108,7 @@ const MsoAfter = () => createStaticVNode(
106
108
  <MsoBefore v-if="outlookFallback" />
107
109
  <div
108
110
  v-bind="restAttrs"
111
+ :class="mergedClass"
109
112
  :style="divStyle"
110
113
  :data-maizzle-msow-id="msoId"
111
114
  :data-maizzle-msow-fallback="useMarker ? '100%' : null"
@@ -18,8 +18,8 @@ const props = defineProps({
18
18
 
19
19
  const attrs = useAttrs()
20
20
 
21
- const defaultClass = computed(() => props.as === 'span' ? 'text-base' : 'mt-4 text-base')
22
- const mergedClass = computed(() => twMerge(defaultClass.value, attrs.class as string))
21
+ const defaultClass = computed(() => props.as === 'span' ? '' : 'mt-4 text-base')
22
+ const mergedClass = computed(() => twMerge(defaultClass.value, attrs.class as string) || undefined)
23
23
  </script>
24
24
 
25
25
  <template>
@@ -23,6 +23,30 @@ declare const outlookFallbackProp: {
23
23
  readonly type: BooleanConstructor;
24
24
  readonly default: null;
25
25
  };
26
+ /**
27
+ * Default utility classes for a code-block `<pre>`. `whitespace-pre!` is
28
+ * forced important so Gmail's stylesheet can't reset it to `normal`, and
29
+ * `mb-0` strips the browser's default `<pre>` bottom margin.
30
+ */
31
+ declare function codeBlockPreClass(bg: string): string;
32
+ /**
33
+ * Build the email-safe table wrapper around highlighted code. Shared by the
34
+ * `<CodeBlock>` component and the Markdown fenced/indented code-block
35
+ * rules so both render identical markup: a full-width table whose
36
+ * cell carries the theme background, wrapping a `<pre>` styled
37
+ * with utility classes (not Shiki's raw inline styles).
38
+ */
39
+ declare function buildCodeBlock(codeContent: string, bg: string, options?: {
40
+ preClass?: string;
41
+ tdClass?: string;
42
+ styleAttr?: string;
43
+ }): string;
44
+ /**
45
+ * Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,
46
+ * pulling the inner code and the theme background out of the highlighted
47
+ * HTML. Falls back to a white background for unhighlighted blocks.
48
+ */
49
+ declare function shikiToCodeBlock(highlighted: string): string;
26
50
  //#endregion
27
- export { hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp };
51
+ export { buildCodeBlock, codeBlockPreClass, hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp, shikiToCodeBlock };
28
52
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAsB;AAAxD;;;;AAAwD;AAiBxD;;;AAjBA,iBAiBgB,MAAA,CAAO,MAAc;AAAA,iBAKrB,eAAA,CAAgB,QAAgB;AAAA,iBAQhC,eAAA,CAAgB,QAAgB;AAAA,iBAIhC,gBAAA,CAAiB,QAAgB;AAAA,iBAQjC,gBAAA,CAAiB,QAAgB;;AApBD;AAQhD;;;;cAsBa,mBAAA;EAAA,eAGH,kBAAA;EAAA"}
1
+ {"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAsB;AAAxD;;;;AAAwD;AAiBxD;;;AAjBA,iBAiBgB,MAAA,CAAO,MAAc;AAAA,iBAKrB,eAAA,CAAgB,QAAgB;AAAA,iBAQhC,eAAA,CAAgB,QAAgB;AAAA,iBAIhC,gBAAA,CAAiB,QAAgB;AAAA,iBAQjC,gBAAA,CAAiB,QAAgB;;AApBD;AAQhD;;;;cAsBa,mBAAA;EAAA,eAGH,kBAAA;EAAA;;;AArBuC;AAQjD;;;iBAoBgB,iBAAA,CAAkB,EAAU;AApBK;AAUjD;;;;;;AAViD,iBA+BjC,cAAA,CACd,WAAA,UACA,EAAA,UACA,OAAA;EAAW,QAAA;EAAmB,OAAA;EAAkB,SAAA;AAAA;;;AAdN;AAW5C;;iBAoBgB,gBAAA,CAAiB,WAAmB"}