mulmocast 2.1.39 → 2.1.40

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 (54) hide show
  1. package/README.md +43 -0
  2. package/lib/actions/pdf.js +18 -15
  3. package/lib/slide/blocks.d.ts +5 -0
  4. package/lib/slide/blocks.js +97 -0
  5. package/lib/slide/index.d.ts +5 -0
  6. package/lib/slide/index.js +7 -0
  7. package/lib/slide/layouts/big_quote.d.ts +2 -0
  8. package/lib/slide/layouts/big_quote.js +19 -0
  9. package/lib/slide/layouts/columns.d.ts +2 -0
  10. package/lib/slide/layouts/columns.js +56 -0
  11. package/lib/slide/layouts/comparison.d.ts +2 -0
  12. package/lib/slide/layouts/comparison.js +28 -0
  13. package/lib/slide/layouts/funnel.d.ts +2 -0
  14. package/lib/slide/layouts/funnel.js +27 -0
  15. package/lib/slide/layouts/grid.d.ts +2 -0
  16. package/lib/slide/layouts/grid.js +43 -0
  17. package/lib/slide/layouts/index.d.ts +3 -0
  18. package/lib/slide/layouts/index.js +43 -0
  19. package/lib/slide/layouts/matrix.d.ts +2 -0
  20. package/lib/slide/layouts/matrix.js +53 -0
  21. package/lib/slide/layouts/split.d.ts +2 -0
  22. package/lib/slide/layouts/split.js +38 -0
  23. package/lib/slide/layouts/stats.d.ts +2 -0
  24. package/lib/slide/layouts/stats.js +23 -0
  25. package/lib/slide/layouts/table.d.ts +2 -0
  26. package/lib/slide/layouts/table.js +46 -0
  27. package/lib/slide/layouts/timeline.d.ts +2 -0
  28. package/lib/slide/layouts/timeline.js +24 -0
  29. package/lib/slide/layouts/title.d.ts +2 -0
  30. package/lib/slide/layouts/title.js +17 -0
  31. package/lib/slide/render.d.ts +3 -0
  32. package/lib/slide/render.js +29 -0
  33. package/lib/slide/schema.d.ts +4009 -0
  34. package/lib/slide/schema.js +330 -0
  35. package/lib/slide/utils.d.ts +32 -0
  36. package/lib/slide/utils.js +112 -0
  37. package/lib/types/schema.d.ts +4487 -38
  38. package/lib/types/schema.js +11 -0
  39. package/lib/types/slide.d.ts +4009 -0
  40. package/lib/types/slide.js +330 -0
  41. package/lib/utils/context.d.ts +1169 -9
  42. package/lib/utils/image_plugins/index.js +14 -1
  43. package/lib/utils/image_plugins/slide.d.ts +5 -0
  44. package/lib/utils/image_plugins/slide.js +35 -0
  45. package/package.json +8 -8
  46. package/scripts/test/golden_age_of_discovery.json +270 -0
  47. package/scripts/test/test_slide_01.json +105 -0
  48. package/scripts/test/test_slide_11.json +144 -0
  49. package/scripts/test/test_slide_12.json +887 -0
  50. package/scripts/test/test_slide_showcase_corporate.json +497 -0
  51. package/scripts/test/test_slide_showcase_creative.json +545 -0
  52. package/scripts/test/test_slide_showcase_minimal.json +501 -0
  53. package/scripts/test/test_slide_showcase_pop.json +547 -0
  54. package/scripts/test/test_slide_showcase_warm.json +486 -0
package/README.md CHANGED
@@ -343,6 +343,49 @@ To force regeneration, delete the old files — including temporary files — un
343
343
 
344
344
  If you modify the text or instruction fields in a MulmoScript, mulmo will automatically detect the changes and regenerate the corresponding audio content upon re-run.
345
345
 
346
+ ## Slide Presentations
347
+
348
+ MulmoCast includes a powerful **Slide DSL** (`type: "slide"`) for creating structured presentation slides with JSON. Slides are rendered via Tailwind CSS + Puppeteer into images.
349
+
350
+ ### Features
351
+
352
+ - **11 Layouts**: title, columns, comparison, grid, bigQuote, stats, timeline, split, matrix, table, funnel
353
+ - **7 Content Block Types**: text, bullets, code, callout, metric, divider, image
354
+ - **13-Color Theme System**: Semantic color palette with dark/light support
355
+ - **6 Preset Themes**: dark, pop, warm, creative, minimal, corporate
356
+
357
+ ### Usage
358
+
359
+ Set a theme once with `slideParams.theme`, then use `"type": "slide"` in each beat:
360
+
361
+ ```json
362
+ {
363
+ "$mulmocast": { "version": "1.1" },
364
+ "slideParams": {
365
+ "theme": { "colors": { "bg": "0F172A", "bgCard": "1E293B", "bgCardAlt": "334155", "text": "F8FAFC", "textMuted": "CBD5E1", "textDim": "64748B", "primary": "3B82F6", "accent": "8B5CF6", "success": "22C55E", "warning": "F59E0B", "danger": "EF4444", "info": "14B8A6", "highlight": "EC4899" }, "fonts": { "title": "Georgia", "body": "Calibri", "mono": "Consolas" } }
366
+ },
367
+ "beats": [
368
+ {
369
+ "text": "Welcome to the presentation",
370
+ "image": {
371
+ "type": "slide",
372
+ "slide": { "layout": "title", "title": "Main Title", "subtitle": "Subtitle" }
373
+ }
374
+ }
375
+ ]
376
+ }
377
+ ```
378
+
379
+ Or use a preset presentation style:
380
+
381
+ ```bash
382
+ mulmo tool complete beats.json -s slide_dark -o presentation.json
383
+ ```
384
+
385
+ Available preset styles: `slide_dark`, `slide_pop`, `slide_warm`, `slide_creative`, `slide_minimal`, `slide_corporate`
386
+
387
+ For detailed layout specifications and content block reference, see the [Slide DSL documentation](./.claude/skills/slide/SKILL.md).
388
+
346
389
  ## Markdown Slide Styles
347
390
 
348
391
  MulmoCast includes 100 pre-designed CSS styles for markdown slides, organized in 10 categories:
@@ -96,6 +96,13 @@ const getHandoutTemplateData = (isLandscapeImage) => ({
96
96
  text_size: isLandscapeImage ? "width: 55%;" : "height: 40%;",
97
97
  item_flex: isLandscapeImage ? "flex: 1;" : "",
98
98
  });
99
+ const resolvePageSize = (pdfMode, pdfSize, imageWidth, imageHeight, isLandscape) => {
100
+ if (pdfMode === "slide")
101
+ return `${imageWidth}px ${imageHeight}px`;
102
+ if (pdfMode === "handout")
103
+ return `${getPdfSize(pdfSize)} portrait`;
104
+ return `${getPdfSize(pdfSize)} ${isLandscape ? "landscape" : "portrait"}`;
105
+ };
99
106
  const generatePDFHTML = async (context, pdfMode, pdfSize) => {
100
107
  const { studio, multiLingual, lang = "en" } = context;
101
108
  const { width: imageWidth, height: imageHeight } = MulmoPresentationStyleMethods.getCanvasSize(context.presentationStyle);
@@ -103,8 +110,7 @@ const generatePDFHTML = async (context, pdfMode, pdfSize) => {
103
110
  const imageFiles = studio.beats.map((beat) => beat.htmlImageFile ?? beat.imageFile);
104
111
  const texts = studio.script.beats.map((beat, index) => localizedText(beat, multiLingual?.[index], lang));
105
112
  const imageDataUrls = await Promise.all(imageFiles.map(loadImage));
106
- const defaultPageSize = `${getPdfSize(pdfSize)} ${isLandscapeImage ? "landscape" : "portrait"}`;
107
- const pageSize = pdfMode === "handout" ? `${getPdfSize(pdfSize)} portrait` : defaultPageSize;
113
+ const pageSize = resolvePageSize(pdfMode, pdfSize, imageWidth, imageHeight, isLandscapeImage);
108
114
  const pagesHTML = generatePagesHTML(pdfMode, imageDataUrls, texts);
109
115
  const template = getHTMLFile(`pdf_${pdfMode}`);
110
116
  const baseTemplateData = {
@@ -116,33 +122,30 @@ const generatePDFHTML = async (context, pdfMode, pdfSize) => {
116
122
  const templateData = pdfMode === "handout" ? { ...baseTemplateData, ...getHandoutTemplateData(isLandscapeImage) } : baseTemplateData;
117
123
  return interpolate(template, templateData);
118
124
  };
119
- const createPDFOptions = (pdfSize, pdfMode) => {
120
- const baseOptions = {
121
- format: getPdfSize(pdfSize),
122
- margin: {
123
- top: "0",
124
- bottom: "0",
125
- left: "0",
126
- right: "0",
127
- },
128
- };
129
- // handout mode always uses portrait orientation
125
+ const zeroMargin = { top: "0", bottom: "0", left: "0", right: "0" };
126
+ const createPDFOptions = (pdfSize, pdfMode, canvasSize) => {
127
+ if (pdfMode === "slide") {
128
+ return { width: `${canvasSize.width}px`, height: `${canvasSize.height}px`, margin: zeroMargin };
129
+ }
130
+ const baseOptions = { format: getPdfSize(pdfSize), margin: zeroMargin };
130
131
  return pdfMode === "handout" ? { ...baseOptions, landscape: false } : baseOptions;
131
132
  };
132
133
  export const pdfFilePath = (context, pdfMode) => {
133
134
  const { studio, fileDirs, lang = "en" } = context;
134
135
  return getOutputPdfFilePath(fileDirs.outDirPath, studio.filename, pdfMode, lang);
135
136
  };
137
+ const PDF_CONTENT_TIMEOUT_MS = 60000;
136
138
  const generatePDF = async (context, pdfMode, pdfSize) => {
137
139
  const outputPdfPath = pdfFilePath(context, pdfMode);
138
140
  const html = await generatePDFHTML(context, pdfMode, pdfSize);
139
- const pdfOptions = createPDFOptions(pdfSize, pdfMode);
141
+ const canvasSize = MulmoPresentationStyleMethods.getCanvasSize(context.presentationStyle);
142
+ const pdfOptions = createPDFOptions(pdfSize, pdfMode, canvasSize);
140
143
  const browser = await puppeteer.launch({
141
144
  args: isCI ? ["--no-sandbox"] : [],
142
145
  });
143
146
  try {
144
147
  const page = await browser.newPage();
145
- await page.setContent(html, { waitUntil: "domcontentloaded" });
148
+ await page.setContent(html, { waitUntil: "domcontentloaded", timeout: PDF_CONTENT_TIMEOUT_MS });
146
149
  await sleep(1000);
147
150
  await page.pdf({
148
151
  path: outputPdfPath,
@@ -0,0 +1,5 @@
1
+ import type { ContentBlock } from "./schema.js";
2
+ /** Render a single content block to HTML */
3
+ export declare const renderContentBlock: (block: ContentBlock) => string;
4
+ /** Render an array of content blocks to HTML */
5
+ export declare const renderContentBlocks: (blocks: ContentBlock[]) => string;
@@ -0,0 +1,97 @@
1
+ import { escapeHtml, nl2br, c } from "./utils.js";
2
+ /** Render a single content block to HTML */
3
+ export const renderContentBlock = (block) => {
4
+ switch (block.type) {
5
+ case "text":
6
+ return renderText(block);
7
+ case "bullets":
8
+ return renderBullets(block);
9
+ case "code":
10
+ return renderCode(block);
11
+ case "callout":
12
+ return renderCallout(block);
13
+ case "metric":
14
+ return renderMetric(block);
15
+ case "divider":
16
+ return renderDivider(block);
17
+ case "image":
18
+ return renderImage(block);
19
+ default:
20
+ return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
21
+ }
22
+ };
23
+ /** Render an array of content blocks to HTML */
24
+ export const renderContentBlocks = (blocks) => {
25
+ return blocks.map(renderContentBlock).join("\n");
26
+ };
27
+ const resolveTextColor = (block) => {
28
+ if (block.color)
29
+ return `text-${c(block.color)}`;
30
+ if (block.dim)
31
+ return "text-d-dim";
32
+ return "text-d-muted";
33
+ };
34
+ const resolveAlign = (align) => {
35
+ if (align === "center")
36
+ return "text-center";
37
+ if (align === "right")
38
+ return "text-right";
39
+ return "";
40
+ };
41
+ const renderText = (block) => {
42
+ const color = resolveTextColor(block);
43
+ const bold = block.bold ? "font-bold" : "";
44
+ const size = block.fontSize !== undefined && block.fontSize >= 18 ? "text-xl" : "text-[15px]";
45
+ const alignCls = resolveAlign(block.align);
46
+ return `<p class="${size} ${color} ${bold} ${alignCls} font-body leading-relaxed">${nl2br(block.value)}</p>`;
47
+ };
48
+ const renderBullets = (block) => {
49
+ const tag = block.ordered ? "ol" : "ul";
50
+ const items = block.items
51
+ .map((item, i) => {
52
+ const marker = block.ordered ? `${i + 1}.` : escapeHtml(block.icon || "\u2022");
53
+ return ` <li class="flex gap-2"><span class="text-d-dim shrink-0">${marker}</span><span>${escapeHtml(item)}</span></li>`;
54
+ })
55
+ .join("\n");
56
+ return `<${tag} class="space-y-2 text-[15px] text-d-muted font-body">\n${items}\n</${tag}>`;
57
+ };
58
+ const renderCode = (block) => {
59
+ return `<pre class="bg-[#0D1117] p-4 rounded text-sm font-mono text-d-dim leading-relaxed whitespace-pre-wrap">${escapeHtml(block.code)}</pre>`;
60
+ };
61
+ const renderCallout = (block) => {
62
+ const isQuote = block.style === "quote";
63
+ const resolveBorderCls = (style) => {
64
+ if (style === "warning")
65
+ return `border-l-2 border-${c("warning")}`;
66
+ if (style === "info")
67
+ return `border-l-2 border-${c("info")}`;
68
+ return "";
69
+ };
70
+ const borderCls = resolveBorderCls(block.style);
71
+ const bg = isQuote ? "bg-d-alt" : "bg-d-card";
72
+ const textCls = isQuote ? "italic text-d-muted" : "text-d-muted";
73
+ const content = block.label
74
+ ? `<span class="font-bold text-${c(block.color || "warning")}">${escapeHtml(block.label)}:</span> <span class="text-d-muted">${escapeHtml(block.text)}</span>`
75
+ : `<span class="${textCls}">${nl2br(block.text)}</span>`;
76
+ return `<div class="${bg} ${borderCls} p-3 rounded text-sm font-body">${content}</div>`;
77
+ };
78
+ const renderMetric = (block) => {
79
+ const lines = [];
80
+ lines.push(`<div class="text-center">`);
81
+ lines.push(` <p class="text-4xl font-bold text-${c(block.color || "primary")}">${escapeHtml(block.value)}</p>`);
82
+ lines.push(` <p class="text-sm text-d-dim mt-1">${escapeHtml(block.label)}</p>`);
83
+ if (block.change) {
84
+ const changeColor = block.change.startsWith("+") ? "success" : "danger";
85
+ lines.push(` <p class="text-sm font-bold text-${c(changeColor)} mt-1">${escapeHtml(block.change)}</p>`);
86
+ }
87
+ lines.push(`</div>`);
88
+ return lines.join("\n");
89
+ };
90
+ const renderDivider = (block) => {
91
+ const divColor = block.color ? `bg-${c(block.color)}` : "bg-d-alt";
92
+ return `<div class="h-[2px] ${divColor} my-2 rounded-full"></div>`;
93
+ };
94
+ const renderImage = (block) => {
95
+ const fit = block.fit === "cover" ? "object-cover" : "object-contain";
96
+ return `<img src="${escapeHtml(block.src)}" alt="${escapeHtml(block.alt || "")}" class="rounded ${fit} max-h-full w-full" />`;
97
+ };
@@ -0,0 +1,5 @@
1
+ export { generateSlideHTML } from "./render.js";
2
+ export { renderSlideContent } from "./layouts/index.js";
3
+ export { renderContentBlock, renderContentBlocks } from "./blocks.js";
4
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, accentColorKeySchema } from "./schema.js";
5
+ export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, } from "./schema.js";
@@ -0,0 +1,7 @@
1
+ // Public API for the slide module
2
+ // This module is self-contained and can be extracted into a standalone package
3
+ export { generateSlideHTML } from "./render.js";
4
+ export { renderSlideContent } from "./layouts/index.js";
5
+ export { renderContentBlock, renderContentBlocks } from "./blocks.js";
6
+ // Schemas
7
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, accentColorKeySchema } from "./schema.js";
@@ -0,0 +1,2 @@
1
+ import type { BigQuoteSlide } from "../schema.js";
2
+ export declare const layoutBigQuote: (data: BigQuoteSlide) => string;
@@ -0,0 +1,19 @@
1
+ import { escapeHtml, nl2br, c } from "../utils.js";
2
+ export const layoutBigQuote = (data) => {
3
+ const accent = data.accentColor || "primary";
4
+ const parts = [];
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>`);
7
+ parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed">`);
8
+ parts.push(` &ldquo;${nl2br(data.quote)}&rdquo;`);
9
+ parts.push(` </blockquote>`);
10
+ parts.push(` <div class="h-[3px] w-24 bg-${c(accent)} mt-8 mb-6"></div>`);
11
+ if (data.author) {
12
+ parts.push(` <p class="text-lg text-d-muted font-body">${escapeHtml(data.author)}</p>`);
13
+ }
14
+ if (data.role) {
15
+ parts.push(` <p class="text-sm text-d-dim font-body mt-1">${escapeHtml(data.role)}</p>`);
16
+ }
17
+ parts.push(`</div>`);
18
+ return parts.join("\n");
19
+ };
@@ -0,0 +1,2 @@
1
+ import type { ColumnsSlide } from "../schema.js";
2
+ export declare const layoutColumns: (data: ColumnsSlide) => string;
@@ -0,0 +1,56 @@
1
+ import { escapeHtml, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ const buildColumnCard = (col) => {
4
+ const accent = col.accentColor || "primary";
5
+ const inner = [];
6
+ if (col.icon) {
7
+ inner.push(`<div class="flex flex-col items-center mb-3">`);
8
+ inner.push(` ${iconSquare(col.icon, accent)}`);
9
+ inner.push(`</div>`);
10
+ inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${escapeHtml(col.title)}</h3>`);
11
+ }
12
+ else if (col.num != null) {
13
+ inner.push(`<div class="flex items-center gap-3 mb-1">`);
14
+ inner.push(` ${numBadge(col.num, accent)}`);
15
+ inner.push(` <h3 class="text-lg font-bold text-d-text font-body">${escapeHtml(col.title)}</h3>`);
16
+ inner.push(`</div>`);
17
+ }
18
+ else {
19
+ if (col.label) {
20
+ inner.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${escapeHtml(col.label)}</p>`);
21
+ }
22
+ inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1">${escapeHtml(col.title)}</h3>`);
23
+ }
24
+ if (col.content) {
25
+ const centerCls = col.icon ? "text-center" : "";
26
+ inner.push(`<div class="mt-3 space-y-3 ${centerCls}">`);
27
+ inner.push(renderContentBlocks(col.content));
28
+ inner.push(`</div>`);
29
+ }
30
+ if (col.footer) {
31
+ inner.push(`<div class="flex-1"></div>`);
32
+ inner.push(`<p class="text-sm text-d-dim font-body mt-3">${escapeHtml(col.footer)}</p>`);
33
+ }
34
+ return cardWrap(accent, inner.join("\n"), "flex-1");
35
+ };
36
+ export const layoutColumns = (data) => {
37
+ const cols = data.columns || [];
38
+ const parts = [slideHeader(data)];
39
+ const colElements = [];
40
+ cols.forEach((col, i) => {
41
+ colElements.push(buildColumnCard(col));
42
+ if (data.showArrows && i < cols.length - 1) {
43
+ colElements.push(`<div class="flex items-center shrink-0"><span class="text-2xl text-d-dim">\u25B6</span></div>`);
44
+ }
45
+ });
46
+ parts.push(`<div class="flex gap-4 px-12 mt-5 flex-1">`);
47
+ parts.push(colElements.join("\n"));
48
+ parts.push(`</div>`);
49
+ if (data.callout) {
50
+ parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
51
+ }
52
+ if (data.bottomText) {
53
+ parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${escapeHtml(data.bottomText)}</p>`);
54
+ }
55
+ return parts.join("\n");
56
+ };
@@ -0,0 +1,2 @@
1
+ import type { ComparisonSlide } from "../schema.js";
2
+ export declare const layoutComparison: (data: ComparisonSlide) => string;
@@ -0,0 +1,28 @@
1
+ import { escapeHtml, c, cardWrap, slideHeader, renderCalloutBar } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ const buildPanel = (panel) => {
4
+ const accent = panel.accentColor || "primary";
5
+ const inner = [];
6
+ inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${escapeHtml(panel.title)}</h3>`);
7
+ if (panel.content) {
8
+ inner.push(`<div class="mt-4 space-y-3">`);
9
+ inner.push(renderContentBlocks(panel.content));
10
+ inner.push(`</div>`);
11
+ }
12
+ if (panel.footer) {
13
+ inner.push(`<div class="flex-1"></div>`);
14
+ inner.push(`<p class="text-sm text-d-dim font-body mt-3">${escapeHtml(panel.footer)}</p>`);
15
+ }
16
+ return cardWrap(accent, inner.join("\n"), "flex-1");
17
+ };
18
+ export const layoutComparison = (data) => {
19
+ const parts = [slideHeader(data)];
20
+ parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1">`);
21
+ parts.push(buildPanel(data.left));
22
+ parts.push(buildPanel(data.right));
23
+ parts.push(`</div>`);
24
+ if (data.callout) {
25
+ parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
26
+ }
27
+ return parts.join("\n");
28
+ };
@@ -0,0 +1,2 @@
1
+ import type { FunnelSlide } from "../schema.js";
2
+ export declare const layoutFunnel: (data: FunnelSlide) => string;
@@ -0,0 +1,27 @@
1
+ import { escapeHtml, c, slideHeader, renderCalloutBar } from "../utils.js";
2
+ export const layoutFunnel = (data) => {
3
+ const parts = [slideHeader(data)];
4
+ const stages = data.stages || [];
5
+ const total = stages.length;
6
+ parts.push(`<div class="flex flex-col items-center gap-2 px-12 mt-6 flex-1">`);
7
+ stages.forEach((stage, i) => {
8
+ const color = stage.color || data.accentColor || "primary";
9
+ const widthPct = 100 - (i / Math.max(total - 1, 1)) * 55;
10
+ parts.push(`<div class="bg-${c(color)} rounded-lg flex items-center justify-between px-6 py-4" style="width: ${widthPct}%">`);
11
+ parts.push(` <div class="flex items-center gap-3">`);
12
+ parts.push(` <span class="text-base font-bold text-white font-body">${escapeHtml(stage.label)}</span>`);
13
+ if (stage.description) {
14
+ parts.push(` <span class="text-sm text-white/70 font-body">${escapeHtml(stage.description)}</span>`);
15
+ }
16
+ parts.push(` </div>`);
17
+ if (stage.value) {
18
+ parts.push(` <span class="text-lg font-bold text-white font-body">${escapeHtml(stage.value)}</span>`);
19
+ }
20
+ parts.push(`</div>`);
21
+ });
22
+ parts.push(`</div>`);
23
+ if (data.callout) {
24
+ parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
25
+ }
26
+ return parts.join("\n");
27
+ };
@@ -0,0 +1,2 @@
1
+ import type { GridSlide } from "../schema.js";
2
+ export declare const layoutGrid: (data: GridSlide) => string;
@@ -0,0 +1,43 @@
1
+ import { escapeHtml, nl2br, c, cardWrap, numBadge, iconSquare } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ export const layoutGrid = (data) => {
4
+ const accent = data.accentColor || "primary";
5
+ 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">${nl2br(data.title)}</h2>`);
10
+ parts.push(`</div>`);
11
+ parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 content-start">`);
12
+ (data.items || []).forEach((item) => {
13
+ const itemAccent = item.accentColor || "primary";
14
+ const inner = [];
15
+ if (item.icon) {
16
+ inner.push(`<div class="flex flex-col items-center mb-2">`);
17
+ inner.push(` ${iconSquare(item.icon, itemAccent)}`);
18
+ inner.push(`</div>`);
19
+ inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${escapeHtml(item.title)}</h3>`);
20
+ }
21
+ else if (item.num != null) {
22
+ inner.push(`<div class="flex items-center gap-3">`);
23
+ inner.push(` ${numBadge(item.num, itemAccent)}`);
24
+ inner.push(` <h3 class="text-sm font-bold text-d-text font-body">${escapeHtml(item.title)}</h3>`);
25
+ inner.push(`</div>`);
26
+ }
27
+ else {
28
+ inner.push(`<h3 class="text-lg font-bold text-d-text font-body">${escapeHtml(item.title)}</h3>`);
29
+ }
30
+ if (item.description) {
31
+ inner.push(`<p class="text-sm text-d-muted font-body mt-3">${escapeHtml(item.description)}</p>`);
32
+ }
33
+ if (item.content) {
34
+ inner.push(`<div class="mt-3 space-y-3">${renderContentBlocks(item.content)}</div>`);
35
+ }
36
+ parts.push(cardWrap(itemAccent, inner.join("\n")));
37
+ });
38
+ parts.push(`</div>`);
39
+ if (data.footer) {
40
+ parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3">${escapeHtml(data.footer)}</p>`);
41
+ }
42
+ return parts.join("\n");
43
+ };
@@ -0,0 +1,3 @@
1
+ import type { SlideLayout } from "../schema.js";
2
+ /** Render the inner content of a slide (without the wrapper div) */
3
+ export declare const renderSlideContent: (slide: SlideLayout) => string;
@@ -0,0 +1,43 @@
1
+ import { layoutTitle } from "./title.js";
2
+ import { layoutColumns } from "./columns.js";
3
+ import { layoutComparison } from "./comparison.js";
4
+ import { layoutGrid } from "./grid.js";
5
+ import { layoutBigQuote } from "./big_quote.js";
6
+ import { layoutStats } from "./stats.js";
7
+ import { layoutTimeline } from "./timeline.js";
8
+ import { layoutSplit } from "./split.js";
9
+ import { layoutMatrix } from "./matrix.js";
10
+ import { layoutTable } from "./table.js";
11
+ import { layoutFunnel } from "./funnel.js";
12
+ import { escapeHtml } from "../utils.js";
13
+ /** Render the inner content of a slide (without the wrapper div) */
14
+ export const renderSlideContent = (slide) => {
15
+ switch (slide.layout) {
16
+ case "title":
17
+ return layoutTitle(slide);
18
+ case "columns":
19
+ return layoutColumns(slide);
20
+ case "comparison":
21
+ return layoutComparison(slide);
22
+ case "grid":
23
+ return layoutGrid(slide);
24
+ case "bigQuote":
25
+ return layoutBigQuote(slide);
26
+ case "stats":
27
+ return layoutStats(slide);
28
+ case "timeline":
29
+ return layoutTimeline(slide);
30
+ case "split":
31
+ return layoutSplit(slide);
32
+ case "matrix":
33
+ return layoutMatrix(slide);
34
+ case "table":
35
+ return layoutTable(slide);
36
+ case "funnel":
37
+ return layoutFunnel(slide);
38
+ default: {
39
+ const _exhaustive = slide;
40
+ return `<p class="text-white p-8">Unknown layout: ${escapeHtml(String(_exhaustive.layout))}</p>`;
41
+ }
42
+ }
43
+ };
@@ -0,0 +1,2 @@
1
+ import type { MatrixSlide } from "../schema.js";
2
+ export declare const layoutMatrix: (data: MatrixSlide) => string;
@@ -0,0 +1,53 @@
1
+ import { escapeHtml, c, cardWrap, slideHeader } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ export const layoutMatrix = (data) => {
4
+ const parts = [slideHeader(data)];
5
+ const rows = data.rows || 2;
6
+ const cols = data.cols || 2;
7
+ const cells = data.cells || [];
8
+ parts.push(`<div class="flex flex-1 px-12 mt-4 gap-2">`);
9
+ if (data.yAxis) {
10
+ parts.push(`<div class="flex flex-col justify-between items-center w-6 shrink-0 py-4">`);
11
+ parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.high || "")}</span>`);
12
+ if (data.yAxis.label) {
13
+ parts.push(` <span class="text-xs font-bold text-d-muted font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.label)}</span>`);
14
+ }
15
+ parts.push(` <span class="text-xs text-d-dim font-body [writing-mode:vertical-lr] rotate-180">${escapeHtml(data.yAxis.low || "")}</span>`);
16
+ parts.push(`</div>`);
17
+ }
18
+ parts.push(`<div class="flex-1 flex flex-col gap-3">`);
19
+ Array.from({ length: rows }).forEach((_row, r) => {
20
+ parts.push(`<div class="flex gap-3 flex-1">`);
21
+ Array.from({ length: cols }).forEach((_col, ci) => {
22
+ const idx = r * cols + ci;
23
+ const cell = cells[idx] || { label: "" };
24
+ const accent = cell.accentColor || "primary";
25
+ const inner = [];
26
+ inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body">${escapeHtml(cell.label)}</h3>`);
27
+ if (cell.items) {
28
+ inner.push(`<ul class="mt-2 space-y-1 text-sm text-d-muted font-body">`);
29
+ cell.items.forEach((item) => {
30
+ inner.push(` <li class="flex gap-2"><span class="text-d-dim shrink-0">&bull;</span><span>${escapeHtml(item)}</span></li>`);
31
+ });
32
+ inner.push(`</ul>`);
33
+ }
34
+ if (cell.content) {
35
+ inner.push(`<div class="mt-2 space-y-2">${renderContentBlocks(cell.content)}</div>`);
36
+ }
37
+ parts.push(cardWrap(accent, inner.join("\n"), "flex-1"));
38
+ });
39
+ parts.push(`</div>`);
40
+ });
41
+ if (data.xAxis) {
42
+ parts.push(`<div class="flex justify-between px-2 mt-1">`);
43
+ parts.push(` <span class="text-xs text-d-dim font-body">${escapeHtml(data.xAxis.low || "")}</span>`);
44
+ if (data.xAxis.label) {
45
+ parts.push(` <span class="text-xs font-bold text-d-muted font-body">${escapeHtml(data.xAxis.label)}</span>`);
46
+ }
47
+ parts.push(` <span class="text-xs text-d-dim font-body">${escapeHtml(data.xAxis.high || "")}</span>`);
48
+ parts.push(`</div>`);
49
+ }
50
+ parts.push(`</div>`);
51
+ parts.push(`</div>`);
52
+ return parts.join("\n");
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { SplitSlide } from "../schema.js";
2
+ export declare const layoutSplit: (data: SplitSlide) => string;
@@ -0,0 +1,38 @@
1
+ import { escapeHtml, nl2br, c } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ const buildSplitPanel = (panel, fallbackAccent, ratio) => {
4
+ const accent = panel.accentColor || fallbackAccent;
5
+ const bg = panel.dark ? "bg-d-card" : "";
6
+ const lines = [];
7
+ lines.push(`<div class="${bg} flex flex-col justify-center px-10 py-8" style="flex: ${ratio}">`);
8
+ if (panel.label) {
9
+ lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body mb-2">${escapeHtml(panel.label)}</p>`);
10
+ }
11
+ if (panel.title) {
12
+ lines.push(` <h2 class="text-[36px] leading-tight font-title font-bold text-d-text">${nl2br(panel.title)}</h2>`);
13
+ }
14
+ if (panel.subtitle) {
15
+ lines.push(` <p class="text-base text-d-dim font-body mt-3">${nl2br(panel.subtitle)}</p>`);
16
+ }
17
+ if (panel.content) {
18
+ lines.push(` <div class="mt-6 space-y-3">${renderContentBlocks(panel.content)}</div>`);
19
+ }
20
+ lines.push(`</div>`);
21
+ return lines.join("\n");
22
+ };
23
+ export const layoutSplit = (data) => {
24
+ const accent = data.accentColor || "primary";
25
+ const parts = [];
26
+ parts.push(`<div class="h-[3px] bg-${c(accent)} shrink-0"></div>`);
27
+ const leftRatio = data.left?.ratio || 50;
28
+ const rightRatio = data.right?.ratio || 50;
29
+ parts.push(`<div class="flex h-full">`);
30
+ if (data.left) {
31
+ parts.push(buildSplitPanel(data.left, accent, leftRatio));
32
+ }
33
+ if (data.right) {
34
+ parts.push(buildSplitPanel(data.right, accent, rightRatio));
35
+ }
36
+ parts.push(`</div>`);
37
+ return parts.join("\n");
38
+ };
@@ -0,0 +1,2 @@
1
+ import type { StatsSlide } from "../schema.js";
2
+ export declare const layoutStats: (data: StatsSlide) => string;
@@ -0,0 +1,23 @@
1
+ import { escapeHtml, c, slideHeader, renderCalloutBar } from "../utils.js";
2
+ export const layoutStats = (data) => {
3
+ const parts = [slideHeader(data)];
4
+ const stats = data.stats || [];
5
+ parts.push(`<div class="flex gap-6 px-12 mt-8 flex-1 items-start">`);
6
+ stats.forEach((stat) => {
7
+ const color = stat.color || data.accentColor || "primary";
8
+ parts.push(`<div class="flex-1 bg-d-card rounded-lg shadow-lg p-8 text-center">`);
9
+ parts.push(` <div class="h-[3px] bg-${c(color)} rounded-full w-12 mx-auto mb-6"></div>`);
10
+ parts.push(` <p class="text-[48px] font-bold text-${c(color)} font-body leading-none">${escapeHtml(stat.value)}</p>`);
11
+ parts.push(` <p class="text-base text-d-muted font-body mt-3">${escapeHtml(stat.label)}</p>`);
12
+ if (stat.change) {
13
+ const changeColor = stat.change.startsWith("+") ? "success" : "danger";
14
+ parts.push(` <p class="text-sm font-bold text-${c(changeColor)} font-body mt-2">${escapeHtml(stat.change)}</p>`);
15
+ }
16
+ parts.push(`</div>`);
17
+ });
18
+ parts.push(`</div>`);
19
+ if (data.callout) {
20
+ parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
21
+ }
22
+ return parts.join("\n");
23
+ };
@@ -0,0 +1,2 @@
1
+ import type { TableSlide } from "../schema.js";
2
+ export declare const layoutTable: (data: TableSlide) => string;