mulmocast 2.2.4 → 2.2.6

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,5 +1,6 @@
1
1
  export { generateSlideHTML } from "./render.js";
2
+ export type { ResolvedBranding } from "./render.js";
2
3
  export { renderSlideContent } from "./layouts/index.js";
3
4
  export { renderContentBlock, renderContentBlocks } from "./blocks.js";
4
- export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, } from "./schema.js";
5
- export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, ImageRefBlock, ChartBlock, MermaidBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, } from "./schema.js";
5
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, slideBrandingLogoSchema, slideBrandingSchema, } from "./schema.js";
6
+ export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, ImageRefBlock, ChartBlock, MermaidBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, SlideBrandingLogo, SlideBranding, } from "./schema.js";
@@ -4,4 +4,4 @@ export { generateSlideHTML } from "./render.js";
4
4
  export { renderSlideContent } from "./layouts/index.js";
5
5
  export { renderContentBlock, renderContentBlocks } from "./blocks.js";
6
6
  // Schemas
7
- export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, } from "./schema.js";
7
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, slideBrandingLogoSchema, slideBrandingSchema, } from "./schema.js";
@@ -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,3 +1,17 @@
1
1
  import type { SlideTheme, SlideLayout } from "./schema.js";
2
+ /** Pre-resolved branding data (all sources converted to data URLs) */
3
+ export type ResolvedBranding = {
4
+ logo?: {
5
+ dataUrl: string;
6
+ position: string;
7
+ width: number;
8
+ };
9
+ backgroundImage?: {
10
+ dataUrl: string;
11
+ size: string;
12
+ opacity: number;
13
+ bgOpacity?: number;
14
+ };
15
+ };
2
16
  /** Generate a complete HTML document for a single slide */
3
- export declare const generateSlideHTML: (theme: SlideTheme, slide: SlideLayout, reference?: string) => string;
17
+ export declare const generateSlideHTML: (theme: SlideTheme, slide: SlideLayout, reference?: string, branding?: ResolvedBranding) => string;
@@ -21,18 +21,56 @@ const buildCdnScripts = (theme, slide) => {
21
21
  }
22
22
  return scripts.join("\n");
23
23
  };
24
+ /** Map branding logo position to Tailwind CSS classes */
25
+ const logoPositionClasses = {
26
+ "top-left": "top-5 left-6",
27
+ "top-right": "top-5 right-6",
28
+ "bottom-left": "bottom-5 left-6",
29
+ "bottom-right": "bottom-5 right-6",
30
+ };
31
+ /**
32
+ * Render branding background layers.
33
+ * - Without bgOpacity: image overlaid on slide bg at given opacity
34
+ * - With bgOpacity: image at full opacity, then slide bg color as semi-transparent overlay
35
+ */
36
+ const renderBrandingBackground = (branding, bgHex) => {
37
+ if (!branding.backgroundImage)
38
+ return "";
39
+ const { dataUrl, size, opacity, bgOpacity } = branding.backgroundImage;
40
+ const bgSize = size === "fill" ? "100% 100%" : size;
41
+ if (bgOpacity !== undefined) {
42
+ const parts = [];
43
+ parts.push(`<div class="absolute inset-0 z-0" style="background-image:url('${dataUrl}');background-size:${bgSize};background-position:center;background-repeat:no-repeat;opacity:${opacity}"></div>`);
44
+ parts.push(`<div class="absolute inset-0 z-0" style="background-color:#${bgHex};opacity:${bgOpacity}"></div>`);
45
+ return parts.join("\n");
46
+ }
47
+ return `<div class="absolute inset-0 z-0" style="background-image:url('${dataUrl}');background-size:${bgSize};background-position:center;background-repeat:no-repeat;opacity:${opacity}"></div>`;
48
+ };
49
+ /** Render branding logo element */
50
+ const renderBrandingLogo = (branding) => {
51
+ if (!branding.logo)
52
+ return "";
53
+ const { dataUrl, position, width } = branding.logo;
54
+ const posClasses = logoPositionClasses[position] ?? logoPositionClasses["top-right"];
55
+ return `<img class="absolute ${posClasses} z-10" src="${dataUrl}" width="${width}" alt="" style="pointer-events:none">`;
56
+ };
24
57
  /** Generate a complete HTML document for a single slide */
25
- export const generateSlideHTML = (theme, slide, reference) => {
58
+ export const generateSlideHTML = (theme, slide, reference, branding) => {
26
59
  const content = renderSlideContent(slide);
27
60
  const twConfig = buildTailwindConfig(theme);
28
61
  const cdnScripts = buildCdnScripts(theme, slide);
29
62
  const slideStyle = slide.style;
30
- const bgCls = slideStyle?.bgColor ? "" : "bg-d-bg";
31
- const inlineStyle = slideStyle?.bgColor ? ` style="background-color:#${sanitizeHex(slideStyle.bgColor)}"` : "";
63
+ const hasBgOpacity = branding?.backgroundImage?.bgOpacity !== undefined;
64
+ const bgCls = hasBgOpacity || slideStyle?.bgColor ? "" : "bg-d-bg";
65
+ const bgColorStyle = slideStyle?.bgColor ? ` style="background-color:#${sanitizeHex(slideStyle.bgColor)}"` : "";
66
+ const inlineStyle = hasBgOpacity ? "" : bgColorStyle;
32
67
  const footer = slideStyle?.footer ? `<p class="absolute bottom-2 right-4 text-xs text-d-dim font-body">${escapeHtml(slideStyle.footer)}</p>` : "";
33
68
  const referenceHtml = reference
34
69
  ? `<div class="mt-auto px-4 pb-2"><p class="text-sm text-d-muted font-body opacity-80">${escapeHtml(reference)}</p></div>`
35
70
  : "";
71
+ const bgHex = sanitizeHex(slideStyle?.bgColor ?? theme.colors.bg);
72
+ const brandingBg = branding ? renderBrandingBackground(branding, bgHex) : "";
73
+ const brandingLogo = branding ? renderBrandingLogo(branding) : "";
36
74
  return `<!DOCTYPE html>
37
75
  <html lang="en" class="h-full">
38
76
  <head>
@@ -47,10 +85,14 @@ ${cdnScripts}
47
85
  </head>
48
86
  <body class="h-full">
49
87
  <div class="relative overflow-hidden ${bgCls} w-full h-full flex flex-col"${inlineStyle}>
88
+ ${brandingBg}
89
+ <div class="relative z-[1] flex flex-col flex-1">
50
90
  ${content}
51
91
  ${referenceHtml}
52
92
  ${footer}
53
93
  </div>
94
+ ${brandingLogo}
95
+ </div>
54
96
  </body>
55
97
  </html>`;
56
98
  };