@maizzle/framework 6.0.0-rc.18 → 6.0.0-rc.19

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 (221) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +10 -6
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/Button.vue +17 -39
  5. package/dist/components/Container.vue +6 -6
  6. package/dist/components/Hr.vue +20 -120
  7. package/dist/components/Html.vue +19 -1
  8. package/dist/components/NotPlaintext.vue +14 -0
  9. package/dist/components/{Vml.vue → OutlookBg.vue} +1 -1
  10. package/dist/components/Plaintext.vue +14 -0
  11. package/dist/components/QrCode.vue +157 -0
  12. package/dist/components/Spacer.vue +28 -27
  13. package/dist/components/utils.js +1 -1
  14. package/dist/components/utils.js.map +1 -1
  15. package/dist/composables/defineConfig.js +1 -2
  16. package/dist/composables/defineConfig.js.map +1 -1
  17. package/dist/composables/renderContext.js +1 -1
  18. package/dist/composables/useBaseUrl.js +3 -4
  19. package/dist/composables/useBaseUrl.js.map +1 -1
  20. package/dist/composables/useConfig.js +1 -2
  21. package/dist/composables/useConfig.js.map +1 -1
  22. package/dist/composables/useDoctype.js +1 -2
  23. package/dist/composables/useDoctype.js.map +1 -1
  24. package/dist/composables/useEvent.js +1 -2
  25. package/dist/composables/useEvent.js.map +1 -1
  26. package/dist/composables/useFont.js +1 -2
  27. package/dist/composables/useFont.js.map +1 -1
  28. package/dist/composables/useOutlookFallback.js +1 -2
  29. package/dist/composables/useOutlookFallback.js.map +1 -1
  30. package/dist/composables/usePlaintext.d.ts +2 -0
  31. package/dist/composables/usePlaintext.d.ts.map +1 -1
  32. package/dist/composables/usePlaintext.js +2 -2
  33. package/dist/composables/usePlaintext.js.map +1 -1
  34. package/dist/composables/usePreheader.js +1 -2
  35. package/dist/composables/usePreheader.js.map +1 -1
  36. package/dist/composables/useTransformers.d.ts +3 -3
  37. package/dist/composables/useTransformers.js +4 -5
  38. package/dist/composables/useTransformers.js.map +1 -1
  39. package/dist/composables/useUrlQuery.js +3 -4
  40. package/dist/composables/useUrlQuery.js.map +1 -1
  41. package/dist/config/defaults.js +1 -1
  42. package/dist/config/index.js +1 -2
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/events/index.js +1 -1
  45. package/dist/events/index.js.map +1 -1
  46. package/dist/index.d.ts +9 -9
  47. package/dist/index.js +4 -5
  48. package/dist/plaintext.js +3 -4
  49. package/dist/plaintext.js.map +1 -1
  50. package/dist/plugin.js +1 -2
  51. package/dist/plugin.js.map +1 -1
  52. package/dist/plugins/postcss/mergeMediaQueries.js +1 -2
  53. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
  54. package/dist/plugins/postcss/pruneVars.js +1 -1
  55. package/dist/plugins/postcss/pruneVars.js.map +1 -1
  56. package/dist/plugins/postcss/quoteFontFamilies.js +1 -1
  57. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
  58. package/dist/plugins/postcss/removeDeclarations.js +1 -1
  59. package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
  60. package/dist/plugins/postcss/resolveMaizzleImports.js +1 -2
  61. package/dist/plugins/postcss/resolveMaizzleImports.js.map +1 -1
  62. package/dist/plugins/postcss/resolveProps.js +1 -1
  63. package/dist/plugins/postcss/resolveProps.js.map +1 -1
  64. package/dist/plugins/postcss/tailwindCleanup.js +1 -1
  65. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
  66. package/dist/prepare.js +1 -2
  67. package/dist/prepare.js.map +1 -1
  68. package/dist/render/active.d.ts +8 -0
  69. package/dist/render/active.d.ts.map +1 -0
  70. package/dist/render/active.js +12 -0
  71. package/dist/render/active.js.map +1 -0
  72. package/dist/render/createRenderer.d.ts.map +1 -1
  73. package/dist/render/createRenderer.js +6 -9
  74. package/dist/render/createRenderer.js.map +1 -1
  75. package/dist/render/index.d.ts.map +1 -1
  76. package/dist/render/index.js +13 -6
  77. package/dist/render/index.js.map +1 -1
  78. package/dist/render/injectFonts.js +1 -2
  79. package/dist/render/injectFonts.js.map +1 -1
  80. package/dist/render/plugins/codeBlockExtract.js +1 -1
  81. package/dist/render/plugins/codeBlockExtract.js.map +1 -1
  82. package/dist/render/plugins/markdownExtract.js +1 -2
  83. package/dist/render/plugins/markdownExtract.js.map +1 -1
  84. package/dist/render/plugins/rawExtract.js +1 -1
  85. package/dist/render/plugins/rawExtract.js.map +1 -1
  86. package/dist/render/plugins/rowSourceLocation.js +1 -1
  87. package/dist/render/plugins/rowSourceLocation.js.map +1 -1
  88. package/dist/serve.d.ts +2 -0
  89. package/dist/serve.d.ts.map +1 -1
  90. package/dist/serve.js +12 -7
  91. package/dist/serve.js.map +1 -1
  92. package/dist/server/compatibility.js +1 -2
  93. package/dist/server/compatibility.js.map +1 -1
  94. package/dist/server/email.js +1 -2
  95. package/dist/server/email.js.map +1 -1
  96. package/dist/server/linter.js +1 -2
  97. package/dist/server/linter.js.map +1 -1
  98. package/dist/server/sfc-utils.js +1 -2
  99. package/dist/server/sfc-utils.js.map +1 -1
  100. package/dist/tests/render/_helpers.d.ts +6 -0
  101. package/dist/tests/render/_helpers.d.ts.map +1 -0
  102. package/dist/tests/render/_helpers.js +16 -0
  103. package/dist/tests/render/_helpers.js.map +1 -0
  104. package/dist/transformers/addAttributes.js +3 -4
  105. package/dist/transformers/addAttributes.js.map +1 -1
  106. package/dist/transformers/attributeToStyle.d.ts +27 -14
  107. package/dist/transformers/attributeToStyle.d.ts.map +1 -1
  108. package/dist/transformers/attributeToStyle.js +34 -20
  109. package/dist/transformers/attributeToStyle.js.map +1 -1
  110. package/dist/transformers/base.d.ts +66 -3
  111. package/dist/transformers/base.d.ts.map +1 -1
  112. package/dist/transformers/base.js +50 -24
  113. package/dist/transformers/base.js.map +1 -1
  114. package/dist/transformers/columnWidth.js +1 -2
  115. package/dist/transformers/columnWidth.js.map +1 -1
  116. package/dist/transformers/entities.d.ts +31 -2
  117. package/dist/transformers/entities.d.ts.map +1 -1
  118. package/dist/transformers/entities.js +39 -7
  119. package/dist/transformers/entities.js.map +1 -1
  120. package/dist/transformers/filters/defaults.js +1 -1
  121. package/dist/transformers/filters/defaults.js.map +1 -1
  122. package/dist/transformers/filters/index.d.ts +31 -10
  123. package/dist/transformers/filters/index.d.ts.map +1 -1
  124. package/dist/transformers/filters/index.js +36 -14
  125. package/dist/transformers/filters/index.js.map +1 -1
  126. package/dist/transformers/format.d.ts +14 -7
  127. package/dist/transformers/format.d.ts.map +1 -1
  128. package/dist/transformers/format.js +15 -11
  129. package/dist/transformers/format.js.map +1 -1
  130. package/dist/transformers/index.js +49 -29
  131. package/dist/transformers/index.js.map +1 -1
  132. package/dist/transformers/inlineCss.d.ts +84 -0
  133. package/dist/transformers/inlineCss.d.ts.map +1 -0
  134. package/dist/transformers/{inlineCSS.js → inlineCss.js} +24 -14
  135. package/dist/transformers/inlineCss.js.map +1 -0
  136. package/dist/transformers/inlineLink.d.ts +26 -5
  137. package/dist/transformers/inlineLink.d.ts.map +1 -1
  138. package/dist/transformers/inlineLink.js +31 -7
  139. package/dist/transformers/inlineLink.js.map +1 -1
  140. package/dist/transformers/minify.d.ts +13 -9
  141. package/dist/transformers/minify.d.ts.map +1 -1
  142. package/dist/transformers/minify.js +14 -13
  143. package/dist/transformers/minify.js.map +1 -1
  144. package/dist/transformers/msoPlaceholders.js +1 -2
  145. package/dist/transformers/msoPlaceholders.js.map +1 -1
  146. package/dist/transformers/purgeCss.d.ts +43 -0
  147. package/dist/transformers/purgeCss.d.ts.map +1 -0
  148. package/dist/transformers/{purgeCSS.js → purgeCss.js} +32 -25
  149. package/dist/transformers/purgeCss.js.map +1 -0
  150. package/dist/transformers/removeAttributes.d.ts +43 -20
  151. package/dist/transformers/removeAttributes.d.ts.map +1 -1
  152. package/dist/transformers/removeAttributes.js +34 -27
  153. package/dist/transformers/removeAttributes.js.map +1 -1
  154. package/dist/transformers/replaceStrings.js +1 -1
  155. package/dist/transformers/replaceStrings.js.map +1 -1
  156. package/dist/transformers/safeClassNames.js +1 -2
  157. package/dist/transformers/safeClassNames.js.map +1 -1
  158. package/dist/transformers/shorthandCss.d.ts +47 -0
  159. package/dist/transformers/shorthandCss.d.ts.map +1 -0
  160. package/dist/transformers/shorthandCss.js +61 -0
  161. package/dist/transformers/shorthandCss.js.map +1 -0
  162. package/dist/transformers/sixHex.d.ts +16 -7
  163. package/dist/transformers/sixHex.d.ts.map +1 -1
  164. package/dist/transformers/sixHex.js +21 -9
  165. package/dist/transformers/sixHex.js.map +1 -1
  166. package/dist/transformers/tailwindComponent.js +1 -2
  167. package/dist/transformers/tailwindComponent.js.map +1 -1
  168. package/dist/transformers/tailwindcss.js +1 -2
  169. package/dist/transformers/tailwindcss.js.map +1 -1
  170. package/dist/transformers/urlQuery.d.ts +26 -14
  171. package/dist/transformers/urlQuery.d.ts.map +1 -1
  172. package/dist/transformers/urlQuery.js +32 -20
  173. package/dist/transformers/urlQuery.js.map +1 -1
  174. package/dist/types/config.d.ts +71 -19
  175. package/dist/types/config.d.ts.map +1 -1
  176. package/dist/types/config.js +1 -1
  177. package/dist/types/index.d.ts +2 -2
  178. package/dist/types/index.js +1 -1
  179. package/dist/utils/ast/index.js +1 -2
  180. package/dist/utils/ast/parser.js +1 -2
  181. package/dist/utils/ast/parser.js.map +1 -1
  182. package/dist/utils/ast/serializer.js +1 -2
  183. package/dist/utils/ast/serializer.js.map +1 -1
  184. package/dist/utils/ast/walker.js +1 -1
  185. package/dist/utils/ast/walker.js.map +1 -1
  186. package/dist/utils/compileTailwindCss.js +1 -2
  187. package/dist/utils/compileTailwindCss.js.map +1 -1
  188. package/dist/utils/decodeStyleEntities.js +1 -1
  189. package/dist/utils/decodeStyleEntities.js.map +1 -1
  190. package/dist/utils/detect.js +1 -2
  191. package/dist/utils/detect.js.map +1 -1
  192. package/dist/utils/output-markers.d.ts +29 -0
  193. package/dist/utils/output-markers.d.ts.map +1 -0
  194. package/dist/utils/output-markers.js +68 -0
  195. package/dist/utils/output-markers.js.map +1 -0
  196. package/dist/utils/url.js +1 -2
  197. package/dist/utils/url.js.map +1 -1
  198. package/node_modules/maizzle/README.md +24 -0
  199. package/node_modules/maizzle/dist/commands/make/component.mjs +1 -1
  200. package/node_modules/maizzle/dist/commands/make/config.mjs +1 -1
  201. package/node_modules/maizzle/dist/commands/make/layout.mjs +3 -3
  202. package/node_modules/maizzle/dist/commands/make/scaffold.mjs +1 -1
  203. package/node_modules/maizzle/dist/commands/make/stubs/Layout.vue +146 -0
  204. package/node_modules/maizzle/dist/commands/make/stubs/component.vue +2 -4
  205. package/node_modules/maizzle/dist/commands/make/stubs/config.ts +1 -5
  206. package/node_modules/maizzle/dist/commands/make/template.mjs +1 -1
  207. package/node_modules/maizzle/dist/commands/new.mjs +29 -24
  208. package/node_modules/maizzle/dist/index.mjs +28 -8
  209. package/node_modules/maizzle/package.json +1 -1
  210. package/package.json +2 -2
  211. package/dist/transformers/inlineCSS.d.ts +0 -17
  212. package/dist/transformers/inlineCSS.d.ts.map +0 -1
  213. package/dist/transformers/inlineCSS.js.map +0 -1
  214. package/dist/transformers/purgeCSS.d.ts +0 -23
  215. package/dist/transformers/purgeCSS.d.ts.map +0 -1
  216. package/dist/transformers/purgeCSS.js.map +0 -1
  217. package/dist/transformers/shorthandCSS.d.ts +0 -24
  218. package/dist/transformers/shorthandCSS.d.ts.map +0 -1
  219. package/dist/transformers/shorthandCSS.js +0 -48
  220. package/dist/transformers/shorthandCSS.js.map +0 -1
  221. package/node_modules/maizzle/dist/commands/make/stubs/layout.vue +0 -39
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAWiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAA;AAAA;;;;;;;;;AAaV;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA"}
1
+ {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAaiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAA;AAAA;;;;;;;;;AAaV;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA"}
package/dist/build.js CHANGED
@@ -3,11 +3,12 @@ import { EventManager } from "./events/index.js";
3
3
  import { runTransformers } from "./transformers/index.js";
4
4
  import { createRenderer } from "./render/createRenderer.js";
5
5
  import { createPlaintext } from "./plaintext.js";
6
+ import { stripForHtml, stripForPlaintext } from "./utils/output-markers.js";
6
7
  import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
8
  import { basename, dirname, join, relative, resolve } from "node:path";
8
9
  import { glob } from "tinyglobby";
9
10
  import ora from "ora";
10
-
11
+ import defu from "defu";
11
12
  //#region src/build.ts
12
13
  /**
13
14
  * Build all SFC email templates to HTML files.
@@ -76,20 +77,23 @@ async function build(configInput) {
76
77
  html
77
78
  });
78
79
  html = `${doctype}\n${html}`;
80
+ const htmlOut = stripForHtml(html);
79
81
  const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase);
80
82
  mkdirSync(dirname(outputFilePath), { recursive: true });
81
- writeFileSync(outputFilePath, html);
83
+ writeFileSync(outputFilePath, htmlOut);
82
84
  outputFiles.push(outputFilePath);
83
85
  const globalPlaintext = templateConfig.plaintext;
84
86
  const sfcPlaintext = rendered.plaintext;
85
87
  if (globalPlaintext || sfcPlaintext) {
86
- const plaintext = createPlaintext(html, typeof globalPlaintext === "object" ? globalPlaintext : {});
87
- const ptExtension = sfcPlaintext?.extension ?? "txt";
88
+ const globalCfg = typeof globalPlaintext === "object" ? globalPlaintext : {};
89
+ const stripOptions = defu(sfcPlaintext?.options, globalCfg.options);
90
+ const plaintext = createPlaintext(stripForPlaintext(html), stripOptions);
91
+ const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? "txt";
88
92
  let ptOutputPath;
89
93
  if (sfcPlaintext?.destination) {
90
94
  const name = basename(templatePath).replace(/\.(vue|md)$/, "");
91
95
  ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`);
92
- } else if (typeof globalPlaintext === "string") ptOutputPath = resolveOutputPath(templatePath, resolve(globalPlaintext), ptExtension, contentBase);
96
+ } else if (globalCfg.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase);
93
97
  else ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase);
94
98
  mkdirSync(dirname(ptOutputPath), { recursive: true });
95
99
  writeFileSync(ptOutputPath, plaintext);
@@ -142,7 +146,7 @@ async function copyStatic(config, outputPath) {
142
146
  cpSync(file, destPath);
143
147
  }
144
148
  }
145
-
146
149
  //#endregion
147
150
  export { build };
151
+
148
152
  //# sourceMappingURL=build.js.map
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","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 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: [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 // Register SFC event handlers collected during render so they participate\n // in the post-render events (afterRender / afterTransform). They're cleared\n // at the end of the iteration so they don't leak into the next template.\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 // 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, rendered.tailwindBlocks)\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 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.js","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 { stripForHtml, stripForPlaintext } from './utils/output-markers.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: [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 // Register SFC event handlers collected during render so they participate\n // in the post-render events (afterRender / afterTransform). They're cleared\n // at the end of the iteration so they don't leak into the next template.\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 // 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, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\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 (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\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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"}
@@ -36,24 +36,6 @@ const props = defineProps({
36
36
  type: String as PropType<'left' | 'center' | 'right' | null>,
37
37
  default: null
38
38
  },
39
- /**
40
- * Background color for `solid` and `outline` variants.
41
- * Also used as the text color for `outline` and `ghost` variants when `color` is not set.
42
- * @default '#4338ca'
43
- */
44
- bgColor: {
45
- type: String,
46
- default: '#4338ca'
47
- },
48
- /**
49
- * Explicit text color. When omitted, `solid` buttons use `#fffffe`
50
- * and all other variants fall back to `bgColor`.
51
- * @default null
52
- */
53
- color: {
54
- type: String,
55
- default: null
56
- },
57
39
  /**
58
40
  * `mso-text-raise` value applied to the inner `<span>` elements.
59
41
  * Controls vertical text alignment inside the button in old Outlook.
@@ -147,18 +129,9 @@ const alignClass = computed(() => props.align ? ({
147
129
  right: 'text-right',
148
130
  })[props.align] || '' : '')
149
131
 
150
- const textColor = computed(() => {
151
- if (props.color) return props.color
152
-
153
- return props.variant === 'solid' ? '#fffffe' : props.bgColor
154
- })
155
-
156
132
  const styles = computed(() => {
157
133
  if (props.variant === 'link') {
158
- return [
159
- 'text-decoration: none;',
160
- `color: ${textColor.value};`,
161
- ].join('')
134
+ return 'text-decoration: none; color: #4338ca;'
162
135
  }
163
136
 
164
137
  const base = [
@@ -168,18 +141,24 @@ const styles = computed(() => {
168
141
  'font-size: 16px;',
169
142
  'line-height: 1;',
170
143
  'border-radius: 4px;',
171
- `color: ${textColor.value};`,
172
144
  ]
173
145
 
174
146
  if (props.variant === 'outline') {
175
147
  base.push(
176
148
  'background-color: transparent;',
177
- `border: 1px solid ${props.bgColor};`,
149
+ 'border: 1px solid #4338ca;',
150
+ 'color: #4338ca;',
178
151
  )
179
152
  } else if (props.variant === 'ghost') {
180
- base.push('background-color: transparent;')
153
+ base.push(
154
+ 'background-color: transparent;',
155
+ 'color: #4338ca;',
156
+ )
181
157
  } else {
182
- base.push(`background-color: ${props.bgColor};`)
158
+ base.push(
159
+ 'background-color: #4338ca;',
160
+ 'color: #fffffe;',
161
+ )
183
162
  }
184
163
 
185
164
  return base.join('')
@@ -187,12 +166,11 @@ const styles = computed(() => {
187
166
 
188
167
  const isLink = computed(() => props.variant === 'link')
189
168
 
190
- const defaultClasses = computed(() => {
191
- if (props.variant === 'ghost') return 'hover:bg-indigo-50'
192
- return ''
193
- })
169
+ const variantClasses = computed(() =>
170
+ props.variant === 'ghost' ? 'hover:bg-indigo-50' : '',
171
+ )
194
172
 
195
- const mergedClass = computed(() => twMerge(defaultClasses.value, attrs.class as string))
173
+ const mergedClass = computed(() => twMerge(variantClasses.value, attrs.class as string))
196
174
 
197
175
  const textSpanStyle = computed(() =>
198
176
  outlookFallback ? `mso-text-raise: ${props.msoPt};` : undefined,
@@ -229,9 +207,9 @@ const MsoIconGap = () => createStaticVNode(
229
207
  <template>
230
208
  <div :class="alignClass">
231
209
  <a
232
- v-bind="{ ...$attrs, class: undefined }"
210
+ v-bind="{ ...$attrs, class: undefined, style: undefined }"
233
211
  :href="href"
234
- :style="styles"
212
+ :style="[styles, $attrs.style as any]"
235
213
  :class="mergedClass"
236
214
  >
237
215
  <template v-if="!isLink">
@@ -16,11 +16,11 @@ const props = defineProps({
16
16
  * Also used as the width source for descendant `Row`/`Column`
17
17
  * components when computing column widths.
18
18
  *
19
- * When not set, the div defaults to `w-150 mx-auto` (600px,
20
- * centered) — overridable via Tailwind classes such as
21
- * `w-[400px]` or `max-w-xl`. The MSO table width is auto-derived
22
- * from the resolved width/max-width after CSS inlining, falling
23
- * back to 600px when unresolvable.
19
+ * When not set, the div defaults to `max-w-150 mx-auto` (max
20
+ * 600px, centered, shrinks below) — overridable via Tailwind
21
+ * classes such as `w-[400px]` or `max-w-xl`. The MSO table
22
+ * width is auto-derived from the resolved width/max-width after
23
+ * CSS inlining, falling back to 600px when unresolvable.
24
24
  */
25
25
  width: {
26
26
  type: [String, Number],
@@ -79,7 +79,7 @@ const mergedClass = computed(() => {
79
79
  const userClass = (attrs.class as string) ?? ''
80
80
  const defaultClass = hasWidthUtility(userClass)
81
81
  ? 'm-0 mx-auto'
82
- : 'w-150 m-0 mx-auto'
82
+ : 'max-w-150 m-0 mx-auto'
83
83
  return twMerge(defaultClass, userClass)
84
84
  })
85
85
 
@@ -1,133 +1,33 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, useAttrs } from 'vue'
3
- import { normalizeToPixels } from './utils.ts'
3
+ import { twMerge } from 'tailwind-merge'
4
4
 
5
- const attrs = useAttrs()
6
-
7
- const props = defineProps({
8
- /**
9
- * Height (thickness) of the divider line.
10
- *
11
- * @default '1px'
12
- */
13
- height: {
14
- type: [String, Number],
15
- default: '1px'
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
- */
24
- color: {
25
- type: String,
26
- default: null
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
- */
35
- spaceY: {
36
- type: [String, Number],
37
- default: '24px'
38
- },
39
- /**
40
- * Horizontal spacing (margin) on both sides of the divider.
41
- *
42
- * Overridden by `left` and `right` if set.
43
- */
44
- spaceX: {
45
- type: [String, Number],
46
- default: null
47
- },
48
- /** Margin above the divider. Overrides `spaceY` for the top side. */
49
- top: {
50
- type: [String, Number],
51
- default: null
52
- },
53
- /** Margin below the divider. Overrides `spaceY` for the bottom side. */
54
- bottom: {
55
- type: [String, Number],
56
- default: null
57
- },
58
- /** Margin to the left of the divider. Overrides `spaceX` for the left side. */
59
- left: {
60
- type: [String, Number],
61
- default: null
62
- },
63
- /** Margin to the right of the divider. Overrides `spaceX` for the right side. */
64
- right: {
65
- type: [String, Number],
66
- default: null
67
- }
68
- })
69
-
70
- const hasBgClass = computed(() =>
71
- typeof attrs.class === 'string' &&
72
- attrs.class.split(' ').some(c => c.startsWith('bg-'))
73
- )
5
+ defineOptions({ inheritAttrs: false })
74
6
 
75
- const styles = computed(() => {
76
- const s = []
77
- const height = normalizeToPixels(props.height || '1px')
78
-
79
- s.push(`height: ${height};`)
80
- s.push(`line-height: ${height};`)
81
-
82
- // Color
83
- if (props.color) {
84
- s.push(`background-color: ${props.color};`)
85
- } else if (!hasBgClass.value) {
86
- s.push('background-color: #cbd5e1;')
87
- }
88
-
89
- // Margins reset
90
- if (
91
- props.top != null ||
92
- props.bottom != null ||
93
- props.left != null ||
94
- props.right != null ||
95
- props.spaceY != null ||
96
- props.spaceX != null
97
- ) {
98
- s.push('margin: 0;')
99
- }
7
+ const attrs = useAttrs()
100
8
 
101
- // space-y
102
- if (props.spaceY != null) {
103
- const v = props.spaceY === 0 ? '0px' : normalizeToPixels(props.spaceY)
104
- s.push(`margin-top: ${v}; margin-bottom: ${v};`)
105
- }
9
+ const HEIGHT_RE = /(?:^|\s)h-([\w./\-[\]%]+)/g
10
+ const LEADING_RE = /(?:^|\s)leading-/
106
11
 
107
- // space-x
108
- if (props.spaceX != null) {
109
- const v = props.spaceX === 0 ? '0px' : normalizeToPixels(props.spaceX)
110
- s.push(`margin-left: ${v}; margin-right: ${v};`)
111
- }
12
+ const mergedClass = computed(() => {
13
+ const userClass = (attrs.class as string) || ''
14
+ const heights = [...userClass.matchAll(HEIGHT_RE)]
15
+ const userHeight = heights.length ? heights[heights.length - 1][1] : null
16
+ const userHasLeading = LEADING_RE.test(userClass)
112
17
 
113
- // individual margins
114
- if (props.top != null) {
115
- s.push(`margin-top: ${normalizeToPixels(props.top)};`)
116
- }
117
- if (props.bottom != null) {
118
- s.push(`margin-bottom: ${normalizeToPixels(props.bottom)};`)
119
- }
120
- if (props.left != null) {
121
- s.push(`margin-left: ${normalizeToPixels(props.left)};`)
122
- }
123
- if (props.right != null) {
124
- s.push(`margin-right: ${normalizeToPixels(props.right)};`)
125
- }
18
+ const defaults = ['my-6', 'bg-slate-300']
19
+ if (!userHeight) defaults.push('h-px')
20
+ if (!userHasLeading && !userHeight) defaults.push('leading-px')
126
21
 
127
- return s.join('')
22
+ const derived = userHeight && !userHasLeading ? `leading-${userHeight}` : ''
23
+ return twMerge(defaults.join(' '), userClass, derived)
128
24
  })
129
25
  </script>
130
26
 
131
27
  <template>
132
- <div role="separator" :style="styles">&zwj;</div>
28
+ <div
29
+ role="separator"
30
+ v-bind="{ ...$attrs, class: undefined }"
31
+ :class="mergedClass"
32
+ >&zwj;</div>
133
33
  </template>
@@ -1,8 +1,9 @@
1
1
  <script setup lang="ts">
2
- import { createStaticVNode, provide, useAttrs, useSlots } from 'vue'
2
+ import { createStaticVNode, inject, provide, useAttrs, useSlots } from 'vue'
3
3
  import type { PropType } from 'vue'
4
4
  import { outlookFallbackProp } from './utils.ts'
5
5
  import { useOutlookFallback } from '../composables/useOutlookFallback'
6
+ import { RenderContextKey } from '../composables/renderContext'
6
7
 
7
8
  defineOptions({ inheritAttrs: false })
8
9
 
@@ -79,10 +80,27 @@ const props = defineProps({
79
80
  * @default true
80
81
  */
81
82
  outlookFallback: outlookFallbackProp,
83
+ /**
84
+ * DOCTYPE declaration prepended to the rendered HTML.
85
+ *
86
+ * Overrides `doctype` in the Maizzle config and any value
87
+ * set via `useDoctype()` in the same template.
88
+ *
89
+ * @default '<!DOCTYPE html>'
90
+ */
91
+ doctype: {
92
+ type: String,
93
+ default: undefined,
94
+ },
82
95
  })
83
96
 
84
97
  const outlookFallback = useOutlookFallback(props.outlookFallback)
85
98
 
99
+ if (props.doctype !== undefined) {
100
+ const ctx = inject(RenderContextKey, undefined)
101
+ if (ctx) ctx.doctype = props.doctype
102
+ }
103
+
86
104
  provide('htmlLang', props.lang)
87
105
 
88
106
  const render = () => {
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { h } from 'vue'
3
+
4
+ export default {
5
+ name: 'NotPlaintext',
6
+ setup(_, { slots }) {
7
+ return () => h(
8
+ 'div',
9
+ { 'data-maizzle-html-only': '' },
10
+ slots.default?.(),
11
+ )
12
+ },
13
+ }
14
+ </script>
@@ -4,7 +4,7 @@ import type { PropType } from 'vue'
4
4
  import { normalizeToPixels } from './utils.ts'
5
5
 
6
6
  export default {
7
- name: 'Vml',
7
+ name: 'OutlookBg',
8
8
  props: {
9
9
  /**
10
10
  * Width of the VML rectangle.
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { h } from 'vue'
3
+
4
+ export default {
5
+ name: 'Plaintext',
6
+ setup(_, { slots }) {
7
+ return () => h(
8
+ 'div',
9
+ { 'data-maizzle-plaintext-only': '' },
10
+ slots.default?.(),
11
+ )
12
+ },
13
+ }
14
+ </script>
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
4
+ import { encode } from 'uqr'
5
+
6
+ type Ecc = 'L' | 'M' | 'Q' | 'H'
7
+
8
+ const escapeAttr = (v: string) =>
9
+ v
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+
15
+ /**
16
+ * Parse a Tailwind sizing token's pixel equivalent.
17
+ *
18
+ * Assumes the default v4 spacing scale (1 unit = 0.25rem),
19
+ * which resolves to 4px at the standard 16px root font.
20
+ * Arbitrary values accept `px` and `rem`; other units
21
+ * fall through to the caller's default.
22
+ */
23
+ function tokenToPx(token: string): number {
24
+ const seg = token.split(':').at(-1) ?? ''
25
+ const m = seg.match(/^(?:size|w|h)-(.+)$/)
26
+ if (!m) return 0
27
+ const v = m[1]
28
+ if (v.startsWith('[') && v.endsWith(']')) {
29
+ const um = v.slice(1, -1).match(/^([\d.]+)(px|rem)?$/)
30
+ if (!um) return 0
31
+ const n = Number.parseFloat(um[1])
32
+ return um[2] === 'rem' ? n * 16 : n
33
+ }
34
+ if (/^\d+(?:\.\d+)?$/.test(v)) return Number.parseFloat(v) * 4
35
+ return 0
36
+ }
37
+
38
+ /**
39
+ * Partition a class string into two buckets:
40
+ *
41
+ * - `sizing` — width/height/size utilities used for cell px math.
42
+ * - `neutral` — everything else; lands on the table verbatim.
43
+ */
44
+ function partition(cls: string): { neutral: string[]; sizing: string[] } {
45
+ const neutral: string[] = []
46
+ const sizing: string[] = []
47
+ for (const t of cls.split(/\s+/).filter(Boolean)) {
48
+ const last = t.split(':').at(-1) ?? ''
49
+ if (/^(?:size|w|h|min-w|min-h|max-w|max-h)-/.test(last)) sizing.push(t)
50
+ else neutral.push(t)
51
+ }
52
+ return { neutral, sizing }
53
+ }
54
+
55
+ export default {
56
+ name: 'QrCode',
57
+ inheritAttrs: false,
58
+ props: {
59
+ /** Data to encode (URL or arbitrary text). */
60
+ value: {
61
+ type: String,
62
+ required: true,
63
+ },
64
+ /**
65
+ * Error correction level: redundancy that keeps the code
66
+ * scannable when partially obscured (e.g. a logo overlay),
67
+ * at the cost of a larger matrix.
68
+ *
69
+ * - `L` ~7% recovery
70
+ * - `M` ~15% recovery (default, fine for on-screen display)
71
+ * - `Q` ~25% recovery
72
+ * - `H` ~30% recovery
73
+ */
74
+ ecc: {
75
+ type: String as PropType<Ecc>,
76
+ default: 'M',
77
+ validator: (v: string) => ['L', 'M', 'Q', 'H'].includes(v),
78
+ },
79
+ /**
80
+ * Width of the light "quiet zone" around the code, in modules.
81
+ * Spec recommends ≥ 4; 1 is plenty for on-screen scans.
82
+ */
83
+ border: {
84
+ type: Number,
85
+ default: 1,
86
+ },
87
+ /**
88
+ * Accessible label exposed via `aria-label` on the table.
89
+ */
90
+ alt: {
91
+ type: String,
92
+ default: '',
93
+ },
94
+ },
95
+ setup(props, { attrs }) {
96
+ const userClass = String(attrs.class ?? '')
97
+ const { neutral, sizing } = partition(userClass)
98
+
99
+ /**
100
+ * Effective pixel size from the user's sizing token, else
101
+ * 120px (= `size-30` on the default v4 spacing scale).
102
+ */
103
+ const sizingToken = sizing[0]
104
+ const effectivePx = sizingToken ? (tokenToPx(sizingToken) || 120) : 120
105
+
106
+ const result = encode(props.value, {
107
+ ecc: props.ecc,
108
+ border: props.border,
109
+ boostEcc: true,
110
+ })
111
+ const matrix = result.data
112
+ const dim = matrix.length
113
+ const cellPx = Math.max(1, Math.floor(effectivePx / dim))
114
+ const totalPx = cellPx * dim
115
+
116
+ /**
117
+ * Table base classes:
118
+ *
119
+ * - `size-[Npx]` matches the outer to cell math (no stripe).
120
+ * - `[&_td]:*` sizes each cell and zeroes its font-size.
121
+ * - `bg-*` paints the table; light cells stay transparent.
122
+ * - `qr:*` paints dark cells via the registered variant.
123
+ *
124
+ * Defaults and user tokens share each form, so `twMerge`
125
+ * resolves overrides cleanly.
126
+ */
127
+ const baseTable = [
128
+ `size-[${totalPx}px]`,
129
+ `[&_td]:w-[${cellPx}px]`,
130
+ `[&_td]:h-[${cellPx}px]`,
131
+ '[&_td]:text-[0px]',
132
+ 'bg-white',
133
+ 'dark:bg-gray-950',
134
+ 'qr:bg-gray-950',
135
+ 'dark:qr:bg-white',
136
+ ]
137
+
138
+ const merged = twMerge([...baseTable, ...neutral].join(' '))
139
+
140
+ let rows = ''
141
+ for (let y = 0; y < dim; y++) {
142
+ let cells = ''
143
+ const row = matrix[y]
144
+ for (let x = 0; x < dim; x++) {
145
+ cells += row[x] ? '<td class="qd"></td>' : '<td></td>'
146
+ }
147
+ rows += `<tr>${cells}</tr>`
148
+ }
149
+
150
+ const altAttr = props.alt ? ` aria-label="${escapeAttr(props.alt)}"` : ''
151
+ const styleAttr = attrs.style ? ` style="${String(attrs.style)}"` : ''
152
+ const html = `<table class="${escapeAttr(merged)}" role="img"${altAttr} cellpadding="0" cellspacing="0" border="0"${styleAttr}>${rows}</table>`
153
+
154
+ return () => createStaticVNode(html, 1)
155
+ },
156
+ }
157
+ </script>