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

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 (143) hide show
  1. package/dist/build.d.ts +19 -1
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/build.js +139 -102
  4. package/dist/build.js.map +1 -1
  5. package/dist/components/Body.vue +12 -0
  6. package/dist/components/Button.vue +16 -29
  7. package/dist/components/CodeBlock.vue +5 -4
  8. package/dist/components/CodeInline.vue +9 -8
  9. package/dist/components/Column.vue +17 -4
  10. package/dist/components/Container.vue +7 -11
  11. package/dist/components/Hr.vue +1 -1
  12. package/dist/components/Img.vue +39 -22
  13. package/dist/components/Layout.vue +1 -1
  14. package/dist/components/Markdown.vue +9 -14
  15. package/dist/components/QrCode.vue +2 -2
  16. package/dist/components/Section.vue +9 -6
  17. package/dist/components/Text.vue +2 -2
  18. package/dist/components/utils.d.ts +25 -1
  19. package/dist/components/utils.d.ts.map +1 -1
  20. package/dist/components/utils.js +29 -1
  21. package/dist/components/utils.js.map +1 -1
  22. package/dist/components/utils.ts +46 -0
  23. package/dist/composables/useConfig.d.ts.map +1 -1
  24. package/dist/composables/useCurrentTemplate.d.ts.map +1 -1
  25. package/dist/composables/useEvent.d.ts.map +1 -1
  26. package/dist/composables/useFont.js.map +1 -1
  27. package/dist/config/index.js +1 -1
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/events/index.d.ts.map +1 -1
  30. package/dist/events/index.js.map +1 -1
  31. package/dist/index.js +2 -2
  32. package/dist/plaintext.js.map +1 -1
  33. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
  34. package/dist/plugins/postcss/pruneVars.js.map +1 -1
  35. package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -1
  36. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
  37. package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
  38. package/dist/plugins/postcss/resolveProps.d.ts.map +1 -1
  39. package/dist/plugins/postcss/resolveProps.js +0 -3
  40. package/dist/plugins/postcss/resolveProps.js.map +1 -1
  41. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
  42. package/dist/prepare.js +1 -1
  43. package/dist/prepare.js.map +1 -1
  44. package/dist/render/active.d.ts.map +1 -1
  45. package/dist/render/buildTemplate.d.ts +49 -0
  46. package/dist/render/buildTemplate.d.ts.map +1 -0
  47. package/dist/render/buildTemplate.js +139 -0
  48. package/dist/render/buildTemplate.js.map +1 -0
  49. package/dist/render/createRenderer.d.ts +3 -1
  50. package/dist/render/createRenderer.d.ts.map +1 -1
  51. package/dist/render/createRenderer.js +43 -10
  52. package/dist/render/createRenderer.js.map +1 -1
  53. package/dist/render/index.js +1 -1
  54. package/dist/render/index.js.map +1 -1
  55. package/dist/render/injectFonts.js.map +1 -1
  56. package/dist/render/parallel/buildWorker.d.ts +31 -0
  57. package/dist/render/parallel/buildWorker.d.ts.map +1 -0
  58. package/dist/render/parallel/buildWorker.js +66 -0
  59. package/dist/render/parallel/buildWorker.js.map +1 -0
  60. package/dist/render/parallel/worker.mjs +28 -0
  61. package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -1
  62. package/dist/render/plugins/codeBlockExtract.js.map +1 -1
  63. package/dist/render/plugins/markdownExtract.d.ts.map +1 -1
  64. package/dist/render/plugins/markdownExtract.js.map +1 -1
  65. package/dist/render/plugins/rawExtract.d.ts.map +1 -1
  66. package/dist/render/plugins/rawExtract.js.map +1 -1
  67. package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -1
  68. package/dist/render/plugins/rowSourceLocation.js.map +1 -1
  69. package/dist/serve.d.ts.map +1 -1
  70. package/dist/serve.js +73 -53
  71. package/dist/serve.js.map +1 -1
  72. package/dist/server/compatibility.d.ts.map +1 -1
  73. package/dist/server/compatibility.js.map +1 -1
  74. package/dist/server/linter.js.map +1 -1
  75. package/dist/server/sfc-utils.js +1 -1
  76. package/dist/server/sfc-utils.js.map +1 -1
  77. package/dist/server/ui/pages/Preview.vue +34 -11
  78. package/dist/server/ui/vite-env.d.ts +1 -0
  79. package/dist/tests/render/_helpers.d.ts.map +1 -1
  80. package/dist/tests/render/_helpers.js.map +1 -1
  81. package/dist/transformers/addAttributes.js +2 -3
  82. package/dist/transformers/addAttributes.js.map +1 -1
  83. package/dist/transformers/base.d.ts +1 -1
  84. package/dist/transformers/base.d.ts.map +1 -1
  85. package/dist/transformers/base.js +5 -10
  86. package/dist/transformers/base.js.map +1 -1
  87. package/dist/transformers/columnWidth.d.ts.map +1 -1
  88. package/dist/transformers/columnWidth.js +2 -7
  89. package/dist/transformers/columnWidth.js.map +1 -1
  90. package/dist/transformers/entities.js.map +1 -1
  91. package/dist/transformers/filters/defaults.js.map +1 -1
  92. package/dist/transformers/filters/index.js.map +1 -1
  93. package/dist/transformers/format.js.map +1 -1
  94. package/dist/transformers/imgWidth.d.ts +20 -0
  95. package/dist/transformers/imgWidth.d.ts.map +1 -0
  96. package/dist/transformers/imgWidth.js +76 -0
  97. package/dist/transformers/imgWidth.js.map +1 -0
  98. package/dist/transformers/index.d.ts.map +1 -1
  99. package/dist/transformers/index.js +2 -0
  100. package/dist/transformers/index.js.map +1 -1
  101. package/dist/transformers/inlineCss.d.ts +3 -2
  102. package/dist/transformers/inlineCss.d.ts.map +1 -1
  103. package/dist/transformers/inlineCss.js.map +1 -1
  104. package/dist/transformers/inlineLink.js +1 -1
  105. package/dist/transformers/inlineLink.js.map +1 -1
  106. package/dist/transformers/minify.js.map +1 -1
  107. package/dist/transformers/minifyCodeInline.js.map +1 -1
  108. package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
  109. package/dist/transformers/msoPlaceholders.js +2 -7
  110. package/dist/transformers/msoPlaceholders.js.map +1 -1
  111. package/dist/transformers/purgeCss.js.map +1 -1
  112. package/dist/transformers/replaceStrings.js.map +1 -1
  113. package/dist/transformers/safeSelectors.js.map +1 -1
  114. package/dist/transformers/shorthandCss.js.map +1 -1
  115. package/dist/transformers/tailwindComponent.js.map +1 -1
  116. package/dist/transformers/tailwindcss.js +1 -1
  117. package/dist/transformers/tailwindcss.js.map +1 -1
  118. package/dist/transformers/urlQuery.js.map +1 -1
  119. package/dist/types/config.d.ts +26 -4
  120. package/dist/types/config.d.ts.map +1 -1
  121. package/dist/utils/ast/serializer.js.map +1 -1
  122. package/dist/utils/compileTailwindCss.js.map +1 -1
  123. package/dist/utils/componentSources.js.map +1 -1
  124. package/dist/utils/cssBox.d.ts.map +1 -1
  125. package/dist/utils/cssBox.js +2 -7
  126. package/dist/utils/cssBox.js.map +1 -1
  127. package/dist/utils/decodeStyleEntities.js.map +1 -1
  128. package/dist/utils/url.js.map +1 -1
  129. package/dist/utils/watchPaths.js.map +1 -1
  130. package/node_modules/@clack/core/CHANGELOG.md +30 -0
  131. package/node_modules/@clack/core/dist/index.d.mts +109 -3
  132. package/node_modules/@clack/core/dist/index.mjs +972 -17
  133. package/node_modules/@clack/core/package.json +2 -1
  134. package/node_modules/@clack/prompts/CHANGELOG.md +42 -0
  135. package/node_modules/@clack/prompts/README.md +30 -9
  136. package/node_modules/@clack/prompts/dist/index.d.mts +458 -27
  137. package/node_modules/@clack/prompts/dist/index.mjs +1378 -141
  138. package/node_modules/@clack/prompts/package.json +2 -2
  139. package/node_modules/tinyexec/package.json +4 -4
  140. package/package.json +13 -11
  141. package/dist/components/Overlap.vue +0 -156
  142. package/node_modules/@clack/core/dist/index.mjs.map +0 -1
  143. package/node_modules/@clack/prompts/dist/index.mjs.map +0 -1
package/dist/build.d.ts CHANGED
@@ -15,6 +15,24 @@ interface BuildResult {
15
15
  * from the working directory.
16
16
  */
17
17
  declare function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult>;
18
+ /**
19
+ * Decide whether to build in parallel and with how many workers.
20
+ *
21
+ * `config.parallel`:
22
+ * - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers
23
+ * - `true` → always parallel (ignores threshold), default workers
24
+ * - `false` → always sequential
25
+ * - `{ workers, threshold }` → parallel when `count > threshold` (default 50),
26
+ * using `workers` threads (default min(CPU count − 1, 8))
27
+ *
28
+ * Workers reload the config file to recover function hooks, so parallel only
29
+ * applies to file-based configs (a path or the default cwd config) — an inline
30
+ * config object has no file to reload and always builds sequentially.
31
+ */
32
+ declare function resolveParallel(config: MaizzleConfig, count: number, configInput: Partial<MaizzleConfig> | string | undefined): {
33
+ enabled: boolean;
34
+ workers: number;
35
+ };
18
36
  //#endregion
19
- export { BuildResult, build };
37
+ export { BuildResult, build, resolveParallel };
20
38
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAeiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAa;AAAA;;;;;;;;AAAA;AAavB;;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;;;;;;;;AAAA;AAavB;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA;;;;;;;;;;;;;AAAW;AA6G/F;iBAAgB,eAAA,CACd,MAAA,EAAQ,aAAA,EACR,KAAA,UACA,WAAA,EAAa,OAAA,CAAQ,aAAA;EAClB,OAAA;EAAkB,OAAA;AAAA"}
package/dist/build.js CHANGED
@@ -1,16 +1,14 @@
1
1
  import { resolveConfig } from "./config/index.js";
2
2
  import { EventManager } from "./events/index.js";
3
- import { runTransformers } from "./transformers/index.js";
4
3
  import { normalizeComponentSources } from "./utils/componentSources.js";
5
4
  import { createRenderer } from "./render/createRenderer.js";
6
- import { createPlaintext } from "./plaintext.js";
7
- import { stripForHtml, stripForPlaintext } from "./utils/output-markers.js";
8
- import { _setCurrentTemplate } from "./composables/useCurrentTemplate.js";
9
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
- import { basename, dirname, join, parse, relative, resolve } from "node:path";
5
+ import { buildTemplate, computeContentBase } from "./render/buildTemplate.js";
6
+ import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { availableParallelism } from "node:os";
11
10
  import { glob } from "tinyglobby";
12
11
  import ora from "ora";
13
- import defu from "defu";
14
12
  //#region src/build.ts
15
13
  /**
16
14
  * Build all SFC email templates to HTML files.
@@ -48,97 +46,56 @@ async function build(configInput) {
48
46
  recursive: true,
49
47
  force: true
50
48
  });
51
- const renderer = await createRenderer({
52
- markdown: config.markdown,
53
- root: config.root,
54
- componentDirs: normalizeComponentSources(config.components?.source, process.cwd()),
55
- vite: config.vite
56
- });
57
49
  const outputFiles = [];
58
- try {
59
- for (const templatePath of templateFiles) {
60
- const absolutePath = resolve(templatePath);
61
- const parsedPath = parse(absolutePath);
62
- const template = {
63
- source: readFileSync(absolutePath, "utf-8"),
64
- path: parsedPath
65
- };
66
- _setCurrentTemplate(parsedPath);
67
- try {
68
- await events.fireBeforeRender({
69
- config,
70
- template
71
- });
72
- const rendered = await renderer.render(absolutePath, config);
73
- /**
74
- * Register SFC event handlers collected during render so they take
75
- * part in the post-render events (afterRender / afterTransform).
76
- * They're cleared at the end of the iteration so they don't
77
- * leak into the next template.
78
- */
79
- for (const { name, handler } of rendered.sfcEventHandlers) events.on(name, handler);
80
- let html = await events.fireAfterRender({
81
- config,
82
- template,
83
- html: rendered.html
84
- });
85
- /**
86
- * Use the per-template merged config (from defineConfig() in the SFC) so
87
- * that template-level overrides like css.safe: false are respected
88
- * by transformers.
89
- */
90
- const templateConfig = rendered.templateConfig;
91
- const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
92
- if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks);
93
- html = await events.fireAfterTransform({
94
- config,
95
- template,
96
- html
97
- });
98
- if (doctype) html = `${doctype}\n${html}`;
99
- const htmlOut = stripForHtml(html);
100
- const sfcOutputPath = rendered.outputPath;
101
- let outputFilePath;
102
- if (sfcOutputPath) {
103
- const parsed = parse(resolve(sfcOutputPath));
104
- const ext = parsed.ext ? parsed.ext.slice(1) : outputExtension;
105
- outputFilePath = join(parsed.dir, `${parsed.name}.${ext}`);
106
- } else outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase);
107
- mkdirSync(dirname(outputFilePath), { recursive: true });
108
- writeFileSync(outputFilePath, htmlOut);
109
- outputFiles.push(outputFilePath);
110
- const globalPlaintext = templateConfig.plaintext;
111
- const sfcPlaintext = rendered.plaintext;
112
- if (globalPlaintext || sfcPlaintext) {
113
- const globalCfg = typeof globalPlaintext === "object" ? globalPlaintext : {};
114
- const stripOptions = defu(sfcPlaintext?.options, globalCfg.options);
115
- const plaintext = createPlaintext(stripForPlaintext(html), stripOptions);
116
- const ptExtension = sfcPlaintext?.extension ?? globalCfg.extension ?? "txt";
117
- let ptOutputPath;
118
- if (sfcPlaintext?.destination) {
119
- const name = basename(templatePath).replace(/\.(vue|md)$/, "");
120
- ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`);
121
- } else if (sfcOutputPath) {
122
- const parsed = parse(outputFilePath);
123
- ptOutputPath = join(parsed.dir, `${parsed.name}.${ptExtension}`);
124
- } else if (globalCfg.destination) ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), ptExtension, contentBase);
125
- else ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase);
126
- mkdirSync(dirname(ptOutputPath), { recursive: true });
127
- writeFileSync(ptOutputPath, plaintext);
128
- }
129
- } finally {
130
- _setCurrentTemplate(void 0);
131
- events.clearSfcHandlers();
132
- }
133
- }
50
+ let droppedAfterBuild = 0;
51
+ const parallel = resolveParallel(config, templateFiles.length, configInput);
52
+ if (parallel.enabled) {
53
+ spinner.text = `Building ${templateFiles.length} templates across ${parallel.workers} workers...`;
54
+ const result = await runParallelBuild({
55
+ templateFiles,
56
+ workers: parallel.workers,
57
+ config,
58
+ configInput,
59
+ outputPath,
60
+ outputExtension,
61
+ contentBase
62
+ });
63
+ outputFiles.push(...result.files);
64
+ droppedAfterBuild = result.sfcAfterBuildCount;
134
65
  await copyStatic(config, outputPath);
135
66
  await events.fireAfterBuild({
136
67
  files: outputFiles,
137
68
  config
138
69
  });
139
- } finally {
140
- await renderer.close();
70
+ } else {
71
+ const renderer = await createRenderer({
72
+ markdown: config.markdown,
73
+ root: config.root,
74
+ componentDirs: normalizeComponentSources(config.components?.source, process.cwd()),
75
+ vite: config.vite
76
+ });
77
+ try {
78
+ for (const templatePath of templateFiles) {
79
+ const { files } = await buildTemplate(templatePath, {
80
+ config,
81
+ renderer,
82
+ events,
83
+ outputPath,
84
+ outputExtension,
85
+ contentBase
86
+ });
87
+ outputFiles.push(...files);
88
+ }
89
+ await copyStatic(config, outputPath);
90
+ await events.fireAfterBuild({
91
+ files: outputFiles,
92
+ config
93
+ });
94
+ } finally {
95
+ await renderer.close();
96
+ }
141
97
  }
98
+ if (droppedAfterBuild > 0) console.warn(`[maizzle] Skipped ${droppedAfterBuild} SFC-registered afterBuild handler(s): afterBuild can't run inside a parallel build worker. Move build-completion logic to the config's afterBuild hook.`);
142
99
  const duration = ((Date.now() - start) / 1e3).toFixed(2);
143
100
  const count = outputFiles.length;
144
101
  spinner.stopAndPersist({
@@ -151,20 +108,100 @@ async function build(configInput) {
151
108
  };
152
109
  }
153
110
  /**
154
- * Extract the static (non-glob) prefix from content patterns.
111
+ * Default template count above which parallel build turns on. Benchmarked
112
+ * crossover (with the worker cap below) is ~25 templates; 50 leaves margin so
113
+ * auto-parallel only kicks in where it's a reliable win across hardware.
114
+ * Override per project with `parallel: { threshold }`.
115
+ */
116
+ const DEFAULT_PARALLEL_THRESHOLD = 50;
117
+ /**
118
+ * Default worker cap. Each worker runs a full Vite SSR renderer, so startup +
119
+ * contention outweighs added parallelism past ~8 — benchmarks showed 8 beating
120
+ * 12/16/23 at every size. Override with `parallel: { workers }`.
121
+ */
122
+ const DEFAULT_MAX_WORKERS = 8;
123
+ /**
124
+ * Decide whether to build in parallel and with how many workers.
155
125
  *
156
- * For example, `['/abs/path/emails/**\/*.vue']` → `'/abs/path/emails'`
126
+ * `config.parallel`:
127
+ * - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers
128
+ * - `true` → always parallel (ignores threshold), default workers
129
+ * - `false` → always sequential
130
+ * - `{ workers, threshold }` → parallel when `count > threshold` (default 50),
131
+ * using `workers` threads (default min(CPU count − 1, 8))
157
132
  *
158
- * This is used to strip the content base from template paths
159
- * so the output preserves only the subdirectory structure.
133
+ * Workers reload the config file to recover function hooks, so parallel only
134
+ * applies to file-based configs (a path or the default cwd config) — an inline
135
+ * config object has no file to reload and always builds sequentially.
136
+ */
137
+ function resolveParallel(config, count, configInput) {
138
+ const setting = config.parallel;
139
+ if (setting === false) return {
140
+ enabled: false,
141
+ workers: 0
142
+ };
143
+ if (!(typeof configInput === "string" || configInput == null)) return {
144
+ enabled: false,
145
+ workers: 0
146
+ };
147
+ const cpus = availableParallelism();
148
+ let maxWorkers = Math.min(Math.max(1, cpus - 1), DEFAULT_MAX_WORKERS);
149
+ let threshold = DEFAULT_PARALLEL_THRESHOLD;
150
+ const ignoreThreshold = setting === true;
151
+ if (typeof setting === "object" && setting !== null) {
152
+ if (typeof setting.workers === "number" && setting.workers > 0) maxWorkers = Math.floor(setting.workers);
153
+ if (typeof setting.threshold === "number" && setting.threshold >= 0) threshold = Math.floor(setting.threshold);
154
+ }
155
+ if (!ignoreThreshold && count <= threshold) return {
156
+ enabled: false,
157
+ workers: 0
158
+ };
159
+ const workers = Math.min(maxWorkers, count);
160
+ return {
161
+ enabled: workers >= 2 && count >= 2,
162
+ workers
163
+ };
164
+ }
165
+ /**
166
+ * Run the build across worker threads. Each worker reloads the config (for its
167
+ * function hooks), builds its batch via the same `buildTemplate` as the
168
+ * sequential path, and returns the files it wrote. beforeCreate/afterBuild stay
169
+ * on the main thread (handled by the caller).
160
170
  */
161
- function computeContentBase(patterns) {
162
- const staticPart = (patterns.find((p) => !p.startsWith("!")) ?? patterns[0]).split(/[*{?[]/)[0];
163
- return resolve(staticPart.endsWith("/") ? staticPart : dirname(staticPart));
171
+ async function runParallelBuild(opts) {
172
+ const { templateFiles, workers, config, configInput, outputPath, outputExtension, contentBase } = opts;
173
+ const { default: Tinypool } = await import("tinypool");
174
+ const workerPath = resolve(dirname(fileURLToPath(import.meta.url)), "render/parallel/worker.mjs");
175
+ const configPath = typeof configInput === "string" ? configInput : void 0;
176
+ const configData = JSON.parse(JSON.stringify(config));
177
+ const batches = shardEvenly(templateFiles, workers);
178
+ const pool = new Tinypool({
179
+ filename: workerPath,
180
+ minThreads: batches.length,
181
+ maxThreads: batches.length
182
+ });
183
+ try {
184
+ const results = await Promise.all(batches.map((templatePaths) => pool.run({
185
+ templatePaths,
186
+ configPath,
187
+ configData,
188
+ outputPath,
189
+ outputExtension,
190
+ contentBase
191
+ })));
192
+ return {
193
+ files: results.flatMap((r) => r.files),
194
+ sfcAfterBuildCount: results.reduce((n, r) => n + r.sfcAfterBuildCount, 0)
195
+ };
196
+ } finally {
197
+ await pool.destroy();
198
+ }
164
199
  }
165
- function resolveOutputPath(templatePath, outputDir, extension, contentBase) {
166
- const name = basename(templatePath).replace(/\.(vue|md)$/, "");
167
- return join(outputDir, relative(contentBase, dirname(resolve(templatePath))), `${name}.${extension}`);
200
+ /** Round-robin items into up to `buckets` non-empty groups for even balance. */
201
+ function shardEvenly(items, buckets) {
202
+ const out = Array.from({ length: buckets }, () => []);
203
+ items.forEach((item, i) => out[i % buckets].push(item));
204
+ return out.filter((b) => b.length > 0);
168
205
  }
169
206
  async function copyStatic(config, outputPath) {
170
207
  const sources = config.static?.source ?? ["public/**/*.*"];
@@ -178,6 +215,6 @@ async function copyStatic(config, outputPath) {
178
215
  }
179
216
  }
180
217
  //#endregion
181
- export { build };
218
+ export { build, resolveParallel };
182
219
 
183
220
  //# sourceMappingURL=build.js.map
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":[],"sources":["../src/build.ts"],"sourcesContent":["import { mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, relative, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { availableParallelism } from 'node:os'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { buildTemplate, computeContentBase } from './render/buildTemplate.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 outputFiles: string[] = []\n let droppedAfterBuild = 0\n\n const parallel = resolveParallel(config, templateFiles.length, configInput)\n\n if (parallel.enabled) {\n spinner.text = `Building ${templateFiles.length} templates across ${parallel.workers} workers...`\n\n const result = await runParallelBuild({\n templateFiles,\n workers: parallel.workers,\n config,\n configInput,\n outputPath,\n outputExtension,\n contentBase,\n })\n\n outputFiles.push(...result.files)\n droppedAfterBuild = result.sfcAfterBuildCount\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } else {\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n\n try {\n for (const templatePath of templateFiles) {\n const { files } = await buildTemplate(templatePath, { config, renderer, events, outputPath, outputExtension, contentBase })\n outputFiles.push(...files)\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n }\n\n if (droppedAfterBuild > 0) {\n console.warn(`[maizzle] Skipped ${droppedAfterBuild} SFC-registered afterBuild handler(s): afterBuild can't run inside a parallel build worker. Move build-completion logic to the config's afterBuild hook.`)\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 * Default template count above which parallel build turns on. Benchmarked\n * crossover (with the worker cap below) is ~25 templates; 50 leaves margin so\n * auto-parallel only kicks in where it's a reliable win across hardware.\n * Override per project with `parallel: { threshold }`.\n */\nconst DEFAULT_PARALLEL_THRESHOLD = 50\n\n/**\n * Default worker cap. Each worker runs a full Vite SSR renderer, so startup +\n * contention outweighs added parallelism past ~8 — benchmarks showed 8 beating\n * 12/16/23 at every size. Override with `parallel: { workers }`.\n */\nconst DEFAULT_MAX_WORKERS = 8\n\n/**\n * Decide whether to build in parallel and with how many workers.\n *\n * `config.parallel`:\n * - omitted → parallel when `count > 50`, min(CPU count − 1, 8) workers\n * - `true` → always parallel (ignores threshold), default workers\n * - `false` → always sequential\n * - `{ workers, threshold }` → parallel when `count > threshold` (default 50),\n * using `workers` threads (default min(CPU count − 1, 8))\n *\n * Workers reload the config file to recover function hooks, so parallel only\n * applies to file-based configs (a path or the default cwd config) — an inline\n * config object has no file to reload and always builds sequentially.\n */\nexport function resolveParallel(\n config: MaizzleConfig,\n count: number,\n configInput: Partial<MaizzleConfig> | string | undefined,\n): { enabled: boolean; workers: number } {\n const setting = config.parallel\n if (setting === false) return { enabled: false, workers: 0 }\n\n const fileBased = typeof configInput === 'string' || configInput == null\n if (!fileBased) return { enabled: false, workers: 0 }\n\n const cpus = availableParallelism()\n const defaultWorkers = Math.min(Math.max(1, cpus - 1), DEFAULT_MAX_WORKERS)\n\n let maxWorkers = defaultWorkers\n let threshold = DEFAULT_PARALLEL_THRESHOLD\n // `true` opts in regardless of count; object/omitted stay threshold-gated.\n const ignoreThreshold = setting === true\n\n if (typeof setting === 'object' && setting !== null) {\n if (typeof setting.workers === 'number' && setting.workers > 0) maxWorkers = Math.floor(setting.workers)\n if (typeof setting.threshold === 'number' && setting.threshold >= 0) threshold = Math.floor(setting.threshold)\n }\n\n if (!ignoreThreshold && count <= threshold) return { enabled: false, workers: 0 }\n\n const workers = Math.min(maxWorkers, count)\n return { enabled: workers >= 2 && count >= 2, workers }\n}\n\n/**\n * Run the build across worker threads. Each worker reloads the config (for its\n * function hooks), builds its batch via the same `buildTemplate` as the\n * sequential path, and returns the files it wrote. beforeCreate/afterBuild stay\n * on the main thread (handled by the caller).\n */\nasync function runParallelBuild(opts: {\n templateFiles: string[]\n workers: number\n config: MaizzleConfig\n configInput: Partial<MaizzleConfig> | string | undefined\n outputPath: string\n outputExtension: string\n contentBase: string\n}): Promise<{ files: string[]; sfcAfterBuildCount: number }> {\n const { templateFiles, workers, config, configInput, outputPath, outputExtension, contentBase } = opts\n\n const { default: Tinypool } = await import('tinypool')\n const workerPath = resolve(dirname(fileURLToPath(import.meta.url)), 'render/parallel/worker.mjs')\n\n const configPath = typeof configInput === 'string' ? configInput : undefined\n // Serializable snapshot of the post-beforeCreate config (functions dropped).\n const configData = JSON.parse(JSON.stringify(config)) as Partial<MaizzleConfig>\n\n const batches = shardEvenly(templateFiles, workers)\n\n const pool = new Tinypool({ filename: workerPath, minThreads: batches.length, maxThreads: batches.length })\n\n try {\n const results = await Promise.all(\n batches.map(templatePaths => pool.run({\n templatePaths,\n configPath,\n configData,\n outputPath,\n outputExtension,\n contentBase,\n })),\n )\n\n return {\n files: results.flatMap(r => r.files),\n sfcAfterBuildCount: results.reduce((n, r) => n + r.sfcAfterBuildCount, 0),\n }\n } finally {\n await pool.destroy()\n }\n}\n\n/** Round-robin items into up to `buckets` non-empty groups for even balance. */\nfunction shardEvenly<T>(items: T[], buckets: number): T[][] {\n const out: T[][] = Array.from({ length: buckets }, () => [])\n items.forEach((item, i) => out[i % buckets].push(item))\n return out.filter(b => b.length > 0)\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