mulmocast 2.2.4 → 2.2.5

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.
@@ -2,7 +2,7 @@ export const builder = (yargs) => yargs
2
2
  .positional("category", {
3
3
  describe: "Category to show info for",
4
4
  type: "string",
5
- choices: ["styles", "bgm", "templates", "voices", "images", "movies", "llm"],
5
+ choices: ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes"],
6
6
  })
7
7
  .option("format", {
8
8
  alias: "F",
@@ -2,6 +2,7 @@
2
2
  import { getMarkdownStyleNames, getMarkdownCategories, getMarkdownStylesByCategory } from "../../../../data/markdownStyles.js";
3
3
  import { bgmAssets } from "../../../../data/bgmAssets.js";
4
4
  import { templateDataSet } from "../../../../data/templateDataSet.js";
5
+ import { slideThemes } from "../../../../data/slideThemes.js";
5
6
  import { provider2TTSAgent, provider2ImageAgent, provider2MovieAgent, provider2LLMAgent } from "../../../../types/provider2agent.js";
6
7
  import YAML from "yaml";
7
8
  const formatOutput = (data, format) => {
@@ -78,6 +79,9 @@ const getLlmInfo = () => {
78
79
  }
79
80
  return { llmProviders: result };
80
81
  };
82
+ const getThemesInfo = () => {
83
+ return { themes: slideThemes, total: Object.keys(slideThemes).length };
84
+ };
81
85
  const printStylesText = () => {
82
86
  const categories = getMarkdownCategories();
83
87
  console.log("\nšŸ“Ž Markdown Styles (100 styles in 10 categories)\n");
@@ -155,6 +159,17 @@ const printLlmText = () => {
155
159
  console.log(` Models: ${config.models.join(", ")}\n`);
156
160
  }
157
161
  };
162
+ const printThemesText = () => {
163
+ const themeEntries = Object.entries(slideThemes);
164
+ console.log(`\nšŸŽØ Slide Themes (${themeEntries.length} themes)\n`);
165
+ console.log("Usage: Set 'slideParams.theme' in your script\n");
166
+ for (const [name, theme] of themeEntries) {
167
+ const { bg, primary, accent } = theme.colors;
168
+ const { title, body, mono } = theme.fonts;
169
+ console.log(` ${name.padEnd(10)} - bg: ${bg}, primary: ${primary}, accent: ${accent} | fonts: ${title}/${body}/${mono}`);
170
+ }
171
+ console.log("");
172
+ };
158
173
  const printAllCategories = () => {
159
174
  console.log("\nšŸ“š Available Info Categories\n");
160
175
  console.log(" Usage: mulmo tool info <category> [--format json|yaml]\n");
@@ -165,9 +180,10 @@ const printAllCategories = () => {
165
180
  console.log(" voices - TTS providers and voice options");
166
181
  console.log(" images - Image generation providers and models");
167
182
  console.log(" movies - Movie generation providers and models");
168
- console.log(" llm - LLM providers and models\n");
183
+ console.log(" llm - LLM providers and models");
184
+ console.log(" themes - Slide themes and color palettes\n");
169
185
  };
170
- const validCategories = ["styles", "bgm", "templates", "voices", "images", "movies", "llm"];
186
+ const validCategories = ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes"];
171
187
  const isValidCategory = (category) => {
172
188
  return validCategories.includes(category);
173
189
  };
@@ -199,6 +215,7 @@ export const handler = (argv) => {
199
215
  images: getImagesInfo,
200
216
  movies: getMoviesInfo,
201
217
  llm: getLlmInfo,
218
+ themes: getThemesInfo,
202
219
  };
203
220
  const textPrinters = {
204
221
  styles: printStylesText,
@@ -208,6 +225,7 @@ export const handler = (argv) => {
208
225
  images: printImagesText,
209
226
  movies: printMoviesText,
210
227
  llm: printLlmText,
228
+ themes: printThemesText,
211
229
  };
212
230
  if (format === "text") {
213
231
  textPrinters[category]();
@@ -1,4 +1,4 @@
1
1
  export declare const command = "info [category]";
2
- export declare const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm)";
2
+ export declare const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes)";
3
3
  export { builder } from "./builder.js";
4
4
  export { handler } from "./handler.js";
@@ -1,4 +1,4 @@
1
1
  export const command = "info [category]";
2
- export const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm)";
2
+ export const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes)";
3
3
  export { builder } from "./builder.js";
4
4
  export { handler } from "./handler.js";
@@ -1,4 +1,4 @@
1
- import { escapeHtml, c, generateSlideId, renderInlineMarkup } from "./utils.js";
1
+ import { escapeHtml, c, generateSlideId, renderInlineMarkup, blockTitle, resolveChangeColor, resolveAccent } from "./utils.js";
2
2
  // ─── Table cell rendering (shared with layouts/table.ts) ───
3
3
  export const resolveCellColor = (cellObj, isRowHeader) => {
4
4
  if (cellObj.color)
@@ -47,8 +47,7 @@ export const renderTableCore = (headers, rows, rowHeaders, striped) => {
47
47
  return parts.join("\n");
48
48
  };
49
49
  const renderTableBlock = (block) => {
50
- const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
51
- return `<div class="overflow-auto">${titleHtml}${renderTableCore(block.headers, block.rows, block.rowHeaders, block.striped)}</div>`;
50
+ return `<div class="overflow-auto">${blockTitle(block.title)}${renderTableCore(block.headers, block.rows, block.rowHeaders, block.striped)}</div>`;
52
51
  };
53
52
  /** Render a single content block to HTML */
54
53
  export const renderContentBlock = (block) => {
@@ -167,11 +166,10 @@ const renderCallout = (block) => {
167
166
  const renderMetric = (block) => {
168
167
  const lines = [];
169
168
  lines.push(`<div class="text-center">`);
170
- lines.push(` <p class="text-4xl font-bold text-${c(block.color || "primary")}">${renderInlineMarkup(block.value)}</p>`);
169
+ lines.push(` <p class="text-4xl font-bold text-${c(resolveAccent(block.color))}">${renderInlineMarkup(block.value)}</p>`);
171
170
  lines.push(` <p class="text-sm text-d-dim mt-1">${renderInlineMarkup(block.label)}</p>`);
172
171
  if (block.change) {
173
- const changeColor = block.change.startsWith("+") ? "success" : "danger";
174
- lines.push(` <p class="text-sm font-bold text-${c(changeColor)} mt-1">${escapeHtml(block.change)}</p>`);
172
+ lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1">${escapeHtml(block.change)}</p>`);
175
173
  }
176
174
  lines.push(`</div>`);
177
175
  return lines.join("\n");
@@ -191,9 +189,8 @@ const renderImageRefPlaceholder = (block) => {
191
189
  const renderChart = (block) => {
192
190
  const chartId = generateSlideId("chart");
193
191
  const chartData = JSON.stringify(block.chartData);
194
- const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
195
192
  return `<div class="flex-1 min-h-0 flex flex-col">
196
- ${titleHtml}
193
+ ${blockTitle(block.title)}
197
194
  <div class="flex-1 min-h-0 relative">
198
195
  <canvas id="${chartId}" data-chart-ready="false"></canvas>
199
196
  </div>
@@ -211,46 +208,42 @@ const renderChart = (block) => {
211
208
  };
212
209
  const renderMermaid = (block) => {
213
210
  const mermaidId = generateSlideId("mermaid");
214
- const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(block.title)}</p>` : "";
215
211
  return `<div class="flex-1 min-h-0 flex flex-col">
216
- ${titleHtml}
212
+ ${blockTitle(block.title)}
217
213
  <div class="flex-1 min-h-0 flex justify-center items-center">
218
214
  <div id="${mermaidId}" class="mermaid">${escapeHtml(block.code)}</div>
219
215
  </div>
220
216
  </div>`;
221
217
  };
218
+ /** Render the text + content blocks inside a section (shared by sidebar/default variants) */
219
+ const renderSectionContent = (block) => {
220
+ const parts = [];
221
+ if (block.text) {
222
+ parts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
223
+ }
224
+ if (block.content) {
225
+ parts.push(block.content.map(renderContentBlock).join("\n"));
226
+ }
227
+ return parts.join("\n");
228
+ };
222
229
  const renderSectionSidebar = (block) => {
223
- const color = block.color || "primary";
230
+ const color = resolveAccent(block.color);
224
231
  const chars = block.label
225
232
  .split("")
226
233
  .map((ch) => escapeHtml(ch))
227
234
  .join("<br>");
228
235
  const sidebar = `<div class="w-[48px] shrink-0 rounded-l bg-${c(color)} flex items-center justify-center"><span class="text-sm font-bold text-white font-body leading-snug text-center">${chars}</span></div>`;
229
- const contentParts = [];
230
- if (block.text) {
231
- contentParts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
232
- }
233
- if (block.content) {
234
- contentParts.push(block.content.map(renderContentBlock).join("\n"));
235
- }
236
236
  return `<div class="flex rounded overflow-hidden bg-d-card">
237
237
  ${sidebar}
238
- <div class="flex-1 space-y-2 p-3">${contentParts.join("\n")}</div>
238
+ <div class="flex-1 space-y-2 p-3">${renderSectionContent(block)}</div>
239
239
  </div>`;
240
240
  };
241
241
  const renderSectionDefault = (block) => {
242
- const color = block.color || "primary";
242
+ const color = resolveAccent(block.color);
243
243
  const badge = `<span class="min-w-[80px] px-3 py-1 rounded text-sm font-bold text-white bg-${c(color)} shrink-0">${renderInlineMarkup(block.label)}</span>`;
244
- const contentParts = [];
245
- if (block.text) {
246
- contentParts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
247
- }
248
- if (block.content) {
249
- contentParts.push(block.content.map(renderContentBlock).join("\n"));
250
- }
251
244
  return `<div class="flex gap-4 items-start">
252
245
  ${badge}
253
- <div class="flex-1 space-y-2">${contentParts.join("\n")}</div>
246
+ <div class="flex-1 space-y-2">${renderSectionContent(block)}</div>
254
247
  </div>`;
255
248
  };
256
249
  const renderSection = (block) => {
@@ -1,13 +1,13 @@
1
- import { renderInlineMarkup, c } from "../utils.js";
1
+ import { renderInlineMarkup, accentBar, resolveAccent } from "../utils.js";
2
2
  export const layoutBigQuote = (data) => {
3
- const accent = data.accentColor || "primary";
3
+ const accent = resolveAccent(data.accentColor);
4
4
  const parts = [];
5
5
  parts.push(`<div class="flex flex-col items-center justify-center h-full px-20">`);
6
- parts.push(` <div class="h-[3px] w-24 bg-${c(accent)} mb-8"></div>`);
6
+ parts.push(` ${accentBar(accent, "w-24 mb-8")}`);
7
7
  parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed">`);
8
8
  parts.push(` &ldquo;${renderInlineMarkup(data.quote)}&rdquo;`);
9
9
  parts.push(` </blockquote>`);
10
- parts.push(` <div class="h-[3px] w-24 bg-${c(accent)} mt-8 mb-6"></div>`);
10
+ parts.push(` ${accentBar(accent, "w-24 mt-8 mb-6")}`);
11
11
  if (data.author) {
12
12
  parts.push(` <p class="text-lg text-d-muted font-body">${renderInlineMarkup(data.author)}</p>`);
13
13
  }
@@ -1,7 +1,7 @@
1
- import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
2
2
  import { renderCardContentBlocks } from "../blocks.js";
3
3
  const buildColumnCard = (col) => {
4
- const accent = col.accentColor || "primary";
4
+ const accent = resolveAccent(col.accentColor);
5
5
  const inner = [];
6
6
  if (col.icon) {
7
7
  inner.push(`<div class="flex flex-col items-center mb-3">`);
@@ -45,9 +45,7 @@ export const layoutColumns = (data) => {
45
45
  parts.push(`<div class="flex gap-4 px-12 mt-5 flex-1 min-h-0 items-start">`);
46
46
  parts.push(colElements.join("\n"));
47
47
  parts.push(`</div>`);
48
- if (data.callout) {
49
- parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
50
- }
48
+ parts.push(renderOptionalCallout(data.callout));
51
49
  if (data.bottomText) {
52
50
  parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${renderInlineMarkup(data.bottomText)}</p>`);
53
51
  }
@@ -1,7 +1,7 @@
1
- import { renderInlineMarkup, c, cardWrap, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
3
  const buildPanel = (panel) => {
4
- const accent = panel.accentColor || "primary";
4
+ const accent = resolveAccent(panel.accentColor);
5
5
  const inner = [];
6
6
  inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${renderInlineMarkup(panel.title)}</h3>`);
7
7
  if (panel.content) {
@@ -20,8 +20,6 @@ export const layoutComparison = (data) => {
20
20
  parts.push(buildPanel(data.left));
21
21
  parts.push(buildPanel(data.right));
22
22
  parts.push(`</div>`);
23
- if (data.callout) {
24
- parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
25
- }
23
+ parts.push(renderOptionalCallout(data.callout));
26
24
  return parts.join("\n");
27
25
  };
@@ -1,11 +1,11 @@
1
- import { renderInlineMarkup, c, slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor } from "../utils.js";
2
2
  export const layoutFunnel = (data) => {
3
3
  const parts = [slideHeader(data)];
4
4
  const stages = data.stages || [];
5
5
  const total = stages.length;
6
6
  parts.push(`<div class="flex flex-col items-center gap-2 px-12 mt-6 flex-1">`);
7
7
  stages.forEach((stage, i) => {
8
- const color = stage.color || data.accentColor || "primary";
8
+ const color = resolveItemColor(stage.color, data.accentColor);
9
9
  const widthPct = 100 - (i / Math.max(total - 1, 1)) * 55;
10
10
  parts.push(`<div class="bg-${c(color)} rounded-lg flex items-center justify-between px-6 py-4" style="width: ${widthPct}%">`);
11
11
  parts.push(` <div class="flex items-center gap-3">`);
@@ -20,8 +20,6 @@ export const layoutFunnel = (data) => {
20
20
  parts.push(`</div>`);
21
21
  });
22
22
  parts.push(`</div>`);
23
- if (data.callout) {
24
- parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
25
- }
23
+ parts.push(renderOptionalCallout(data.callout));
26
24
  return parts.join("\n");
27
25
  };
@@ -1,16 +1,11 @@
1
- import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare } from "../utils.js";
1
+ import { renderInlineMarkup, cardWrap, numBadge, iconSquare, slideHeader, resolveAccent } from "../utils.js";
2
2
  import { renderCardContentBlocks } from "../blocks.js";
3
3
  export const layoutGrid = (data) => {
4
- const accent = data.accentColor || "primary";
5
4
  const nCols = data.gridColumns || 3;
6
- const parts = [];
7
- parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
8
- parts.push(`<div class="px-12 pt-5 shrink-0">`);
9
- parts.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
10
- parts.push(`</div>`);
5
+ const parts = [slideHeader(data)];
11
6
  parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 min-h-0 overflow-hidden content-center">`);
12
7
  (data.items || []).forEach((item) => {
13
- const itemAccent = item.accentColor || "primary";
8
+ const itemAccent = resolveAccent(item.accentColor);
14
9
  const inner = [];
15
10
  if (item.icon) {
16
11
  inner.push(`<div class="flex flex-col items-center mb-2">`);
@@ -1,4 +1,4 @@
1
- import { renderInlineMarkup, c, cardWrap, slideHeader } from "../utils.js";
1
+ import { renderInlineMarkup, c, cardWrap, slideHeader, resolveAccent } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
3
  export const layoutMatrix = (data) => {
4
4
  const parts = [slideHeader(data)];
@@ -21,7 +21,7 @@ export const layoutMatrix = (data) => {
21
21
  Array.from({ length: cols }).forEach((_col, ci) => {
22
22
  const idx = r * cols + ci;
23
23
  const cell = cells[idx] || { label: "" };
24
- const accent = cell.accentColor || "primary";
24
+ const accent = resolveAccent(cell.accentColor);
25
25
  const inner = [];
26
26
  inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body">${renderInlineMarkup(cell.label)}</h3>`);
27
27
  if (cell.items) {
@@ -1,4 +1,4 @@
1
- import { renderInlineMarkup, c } from "../utils.js";
1
+ import { renderInlineMarkup, c, accentBar, resolveAccent } from "../utils.js";
2
2
  import { renderContentBlocks } from "../blocks.js";
3
3
  const resolveValign = (valign) => {
4
4
  if (valign === "top")
@@ -34,9 +34,9 @@ const buildSplitPanel = (panel, fallbackAccent, ratio) => {
34
34
  return lines.join("\n");
35
35
  };
36
36
  export const layoutSplit = (data) => {
37
- const accent = data.accentColor || "primary";
37
+ const accent = resolveAccent(data.accentColor);
38
38
  const parts = [];
39
- parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
39
+ parts.push(accentBar(accent));
40
40
  const leftRatio = data.left?.ratio || 50;
41
41
  const rightRatio = data.right?.ratio || 50;
42
42
  parts.push(`<div class="flex h-full">`);
@@ -1,36 +1,23 @@
1
- import { renderInlineMarkup, c, renderCalloutBar } from "../utils.js";
1
+ import { renderInlineMarkup, c, resolveItemColor, resolveChangeColor, centeredSlideHeader, renderOptionalCallout } from "../utils.js";
2
2
  export const layoutStats = (data) => {
3
- const accent = data.accentColor || "primary";
4
3
  const stats = data.stats || [];
5
4
  const parts = [];
6
- parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
7
- parts.push(`<div class="flex-1 flex flex-col justify-center px-12 min-h-0">`);
8
- // Header inside centering wrapper
9
- if (data.stepLabel) {
10
- parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
11
- }
12
- parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
13
- if (data.subtitle) {
14
- parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
15
- }
5
+ parts.push(centeredSlideHeader(data));
16
6
  // Stats cards
17
7
  parts.push(`<div class="flex gap-6 mt-10">`);
18
8
  stats.forEach((stat) => {
19
- const color = stat.color || data.accentColor || "primary";
9
+ const color = resolveItemColor(stat.color, data.accentColor);
20
10
  parts.push(`<div class="flex-1 bg-d-card rounded-lg shadow-lg p-10 text-center">`);
21
11
  parts.push(` <div class="h-[3px] bg-${c(color)} rounded-full w-12 mx-auto mb-6"></div>`);
22
12
  parts.push(` <p class="text-[52px] font-bold text-${c(color)} font-body leading-none">${renderInlineMarkup(stat.value)}</p>`);
23
13
  parts.push(` <p class="text-lg text-d-muted font-body mt-4">${renderInlineMarkup(stat.label)}</p>`);
24
14
  if (stat.change) {
25
- const changeColor = /\+/.test(stat.change) ? "success" : "danger";
26
- parts.push(` <p class="text-base font-bold text-${c(changeColor)} font-body mt-3">${renderInlineMarkup(stat.change)}</p>`);
15
+ parts.push(` <p class="text-base font-bold text-${c(resolveChangeColor(stat.change))} font-body mt-3">${renderInlineMarkup(stat.change)}</p>`);
27
16
  }
28
17
  parts.push(`</div>`);
29
18
  });
30
19
  parts.push(`</div>`);
31
20
  parts.push(`</div>`);
32
- if (data.callout) {
33
- parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
34
- }
21
+ parts.push(renderOptionalCallout(data.callout));
35
22
  return parts.join("\n");
36
23
  };
@@ -1,12 +1,10 @@
1
- import { slideHeader, renderCalloutBar } from "../utils.js";
1
+ import { slideHeader, renderOptionalCallout } from "../utils.js";
2
2
  import { renderTableCore } from "../blocks.js";
3
3
  export const layoutTable = (data) => {
4
4
  const parts = [slideHeader(data)];
5
5
  parts.push(`<div class="px-12 mt-5 flex-1 overflow-auto">`);
6
6
  parts.push(renderTableCore(data.headers, data.rows, data.rowHeaders, data.striped));
7
7
  parts.push(`</div>`);
8
- if (data.callout) {
9
- parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
10
- }
8
+ parts.push(renderOptionalCallout(data.callout));
11
9
  return parts.join("\n");
12
10
  };
@@ -1,23 +1,13 @@
1
- import { renderInlineMarkup, c } from "../utils.js";
1
+ import { renderInlineMarkup, c, resolveItemColor, centeredSlideHeader } from "../utils.js";
2
2
  export const layoutTimeline = (data) => {
3
- const accent = data.accentColor || "primary";
4
3
  const parts = [];
5
4
  const items = data.items || [];
6
- parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
7
- parts.push(`<div class="flex-1 flex flex-col justify-center px-12 min-h-0">`);
8
- // Header inside centering wrapper
9
- if (data.stepLabel) {
10
- parts.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
11
- }
12
- parts.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
13
- if (data.subtitle) {
14
- parts.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
15
- }
5
+ parts.push(centeredSlideHeader(data));
16
6
  // Timeline items
17
7
  parts.push(`<div class="flex items-start mt-10 relative">`);
18
8
  parts.push(`<div class="absolute left-4 right-4 top-[52px] h-[2px] bg-d-alt"></div>`);
19
9
  items.forEach((item) => {
20
- const color = item.color || data.accentColor || "primary";
10
+ const color = resolveItemColor(item.color, data.accentColor);
21
11
  const dotBorder = item.done ? `bg-${c(color)}` : `bg-d-alt`;
22
12
  const dotInner = item.done ? "bg-d-text" : `bg-${c(color)}`;
23
13
  parts.push(`<div class="flex-1 flex flex-col items-center text-center relative z-10">`);
@@ -1,7 +1,7 @@
1
- import { renderInlineMarkup } from "../utils.js";
1
+ import { renderInlineMarkup, accentBar } from "../utils.js";
2
2
  export const layoutTitle = (data) => {
3
3
  return [
4
- `<div class="h-[3px] bg-d-primary"></div>`,
4
+ accentBar("primary"),
5
5
  `<div class="absolute -top-20 -right-8 w-[360px] h-[360px] rounded-full bg-d-primary opacity-10"></div>`,
6
6
  `<div class="absolute -bottom-12 -left-16 w-[280px] h-[280px] rounded-full bg-d-accent opacity-10"></div>`,
7
7
  `<div class="flex flex-col justify-center h-full px-16 relative z-10">`,
@@ -12,7 +12,7 @@ export const layoutTitle = (data) => {
12
12
  ? ` <div class="bg-d-card px-4 py-2 mt-6 inline-block rounded"><p class="text-sm text-d-dim font-body">${renderInlineMarkup(data.note)}</p></div>`
13
13
  : "",
14
14
  `</div>`,
15
- `<div class="absolute bottom-0 left-0 right-0 h-[3px] bg-d-accent"></div>`,
15
+ accentBar("accent", "absolute bottom-0 left-0 right-0"),
16
16
  ]
17
17
  .filter(Boolean)
18
18
  .join("\n");
@@ -1,4 +1,4 @@
1
- import type { SlideTheme, SlideLayout } from "./schema.js";
1
+ import type { SlideTheme, SlideLayout, CalloutBar } from "./schema.js";
2
2
  /** Escape HTML special characters */
3
3
  export declare const escapeHtml: (s: string) => string;
4
4
  /** Escape HTML and convert newlines to <br> */
@@ -13,6 +13,18 @@ export declare const renderInlineMarkup: (s: string) => string;
13
13
  export declare const sanitizeHex: (s: string) => string;
14
14
  /** Accent color key → Tailwind class segment: "primary" → "d-primary" */
15
15
  export declare const c: (key: string) => string;
16
+ /** Resolve accent color key with "primary" as fallback */
17
+ export declare const resolveAccent: (color: string | undefined) => string;
18
+ /** Resolve item-level color with slide-level fallback then "primary" */
19
+ export declare const resolveItemColor: (itemColor: string | undefined, slideAccent: string | undefined) => string;
20
+ /** Render a horizontal accent bar (3px full-width). Pass extraClass for width/margin variants. */
21
+ export declare const accentBar: (colorKey: string, extraClass?: string) => string;
22
+ /** Render an optional block title (chart, mermaid, table) */
23
+ export declare const blockTitle: (title: string | undefined) => string;
24
+ /** Resolve change indicator color: "success" for positive (+), "danger" for negative */
25
+ export declare const resolveChangeColor: (change: string) => string;
26
+ /** Render the optional callout bar at the bottom of a slide, or empty string */
27
+ export declare const renderOptionalCallout: (callout: CalloutBar | undefined) => string;
16
28
  /** Build the Tailwind config JSON string for theme colors and fonts */
17
29
  export declare const buildTailwindConfig: (theme: SlideTheme) => string;
18
30
  /** Render a numbered circle badge */
@@ -29,6 +41,13 @@ export declare const renderCalloutBar: (obj: {
29
41
  align?: string;
30
42
  leftBar?: boolean;
31
43
  }) => string;
44
+ /** Render header text elements (stepLabel + title + subtitle) without wrapping div */
45
+ export declare const renderHeaderText: (data: {
46
+ accentColor?: string;
47
+ stepLabel?: string;
48
+ title: string;
49
+ subtitle?: string;
50
+ }) => string;
32
51
  /** Render the common slide header (accent bar + title + subtitle) */
33
52
  export declare const slideHeader: (data: {
34
53
  accentColor?: string;
@@ -36,6 +55,13 @@ export declare const slideHeader: (data: {
36
55
  title: string;
37
56
  subtitle?: string;
38
57
  }) => string;
58
+ /** Render accent bar + vertically-centered wrapper with header text (used by stats, timeline) */
59
+ export declare const centeredSlideHeader: (data: {
60
+ accentColor?: string;
61
+ stepLabel?: string;
62
+ title: string;
63
+ subtitle?: string;
64
+ }) => string;
39
65
  /** Generate a unique ID with the given prefix (e.g. "chart-0", "mermaid-1") */
40
66
  export declare const generateSlideId: (prefix: string) => string;
41
67
  /** Reset the ID counter (for testing) */
@@ -45,6 +45,27 @@ export const sanitizeHex = (s) => {
45
45
  export const c = (key) => {
46
46
  return `d-${sanitizeCssClass(key)}`;
47
47
  };
48
+ // ═══════════════════════════════════════════════════════════
49
+ // Shared micro-helpers for HTML generation
50
+ // ═══════════════════════════════════════════════════════════
51
+ /** Default accent color used when none is specified */
52
+ const DEFAULT_ACCENT = "primary";
53
+ /** Resolve accent color key with "primary" as fallback */
54
+ export const resolveAccent = (color) => color || DEFAULT_ACCENT;
55
+ /** Resolve item-level color with slide-level fallback then "primary" */
56
+ export const resolveItemColor = (itemColor, slideAccent) => itemColor || slideAccent || DEFAULT_ACCENT;
57
+ /** Render a horizontal accent bar (3px full-width). Pass extraClass for width/margin variants. */
58
+ export const accentBar = (colorKey, extraClass) => `<div class="h-[3px] bg-${c(colorKey)} shrink-0 ${extraClass || ""}"></div>`;
59
+ /** Render an optional block title (chart, mermaid, table) */
60
+ export const blockTitle = (title) => title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${renderInlineMarkup(title)}</p>` : "";
61
+ /** Resolve change indicator color: "success" for positive (+), "danger" for negative */
62
+ export const resolveChangeColor = (change) => (/\+/.test(change) ? "success" : "danger");
63
+ /** Render the optional callout bar at the bottom of a slide, or empty string */
64
+ export const renderOptionalCallout = (callout) => {
65
+ if (!callout)
66
+ return "";
67
+ return `<div class="mt-auto pb-4">${renderCalloutBar(callout)}</div>`;
68
+ };
48
69
  const colorKeyMap = {
49
70
  bg: "bg",
50
71
  bgCard: "card",
@@ -97,7 +118,7 @@ export const iconSquare = (icon, colorKey) => {
97
118
  /** Render a card wrapper with accent top bar */
98
119
  export const cardWrap = (accentColor, innerHtml, extraClass) => {
99
120
  return `<div class="bg-d-card rounded-lg shadow-lg overflow-hidden flex flex-col min-h-0 ${sanitizeCssClass(extraClass || "")}">
100
- <div class="h-[3px] bg-${c(accentColor)} shrink-0"></div>
121
+ ${accentBar(accentColor)}
101
122
  <div class="p-5 flex flex-col flex-1 min-h-0 overflow-hidden">
102
123
  ${innerHtml}
103
124
  </div>
@@ -116,22 +137,29 @@ export const renderCalloutBar = (obj) => {
116
137
  <div class="px-4 py-3 text-sm font-body flex-1">${inner}</div>
117
138
  </div>`;
118
139
  };
119
- /** Render the common slide header (accent bar + title + subtitle) */
120
- export const slideHeader = (data) => {
121
- const accent = data.accentColor || "primary";
140
+ /** Render header text elements (stepLabel + title + subtitle) without wrapping div */
141
+ export const renderHeaderText = (data) => {
142
+ const accent = resolveAccent(data.accentColor);
122
143
  const lines = [];
123
- lines.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
124
- lines.push(`<div class="px-12 pt-5 shrink-0">`);
125
144
  if (data.stepLabel) {
126
- lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
145
+ lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
127
146
  }
128
- lines.push(` <h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
147
+ lines.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
129
148
  if (data.subtitle) {
130
- lines.push(` <p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
149
+ lines.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
131
150
  }
132
- lines.push(`</div>`);
133
151
  return lines.join("\n");
134
152
  };
153
+ /** Render the common slide header (accent bar + title + subtitle) */
154
+ export const slideHeader = (data) => {
155
+ const accent = resolveAccent(data.accentColor);
156
+ return [accentBar(accent), `<div class="px-12 pt-5 shrink-0">`, renderHeaderText(data), `</div>`].join("\n");
157
+ };
158
+ /** Render accent bar + vertically-centered wrapper with header text (used by stats, timeline) */
159
+ export const centeredSlideHeader = (data) => {
160
+ const accent = resolveAccent(data.accentColor);
161
+ return [accentBar(accent), `<div class="flex-1 flex flex-col justify-center px-12 min-h-0">`, renderHeaderText(data)].join("\n");
162
+ };
135
163
  // ═══════════════════════════════════════════════════════════
136
164
  // Counter-based ID generation (unique within a single slide)
137
165
  // ═══════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",