mulmocast 2.1.39 → 2.2.0
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.
- package/README.md +43 -0
- package/lib/actions/image_agents.d.ts +2 -1
- package/lib/actions/image_agents.js +3 -3
- package/lib/actions/images.d.ts +3 -1
- package/lib/actions/images.js +1 -0
- package/lib/actions/pdf.js +18 -15
- package/lib/methods/mulmo_media_source.d.ts +1 -0
- package/lib/methods/mulmo_media_source.js +17 -3
- package/lib/slide/blocks.d.ts +7 -0
- package/lib/slide/blocks.js +149 -0
- package/lib/slide/index.d.ts +5 -0
- package/lib/slide/index.js +7 -0
- package/lib/slide/layouts/big_quote.d.ts +2 -0
- package/lib/slide/layouts/big_quote.js +19 -0
- package/lib/slide/layouts/columns.d.ts +2 -0
- package/lib/slide/layouts/columns.js +56 -0
- package/lib/slide/layouts/comparison.d.ts +2 -0
- package/lib/slide/layouts/comparison.js +29 -0
- package/lib/slide/layouts/funnel.d.ts +2 -0
- package/lib/slide/layouts/funnel.js +27 -0
- package/lib/slide/layouts/grid.d.ts +2 -0
- package/lib/slide/layouts/grid.js +43 -0
- package/lib/slide/layouts/index.d.ts +3 -0
- package/lib/slide/layouts/index.js +43 -0
- package/lib/slide/layouts/matrix.d.ts +2 -0
- package/lib/slide/layouts/matrix.js +53 -0
- package/lib/slide/layouts/split.d.ts +2 -0
- package/lib/slide/layouts/split.js +38 -0
- package/lib/slide/layouts/stats.d.ts +2 -0
- package/lib/slide/layouts/stats.js +23 -0
- package/lib/slide/layouts/table.d.ts +2 -0
- package/lib/slide/layouts/table.js +46 -0
- package/lib/slide/layouts/timeline.d.ts +2 -0
- package/lib/slide/layouts/timeline.js +24 -0
- package/lib/slide/layouts/title.d.ts +2 -0
- package/lib/slide/layouts/title.js +17 -0
- package/lib/slide/render.d.ts +3 -0
- package/lib/slide/render.js +52 -0
- package/lib/slide/schema.d.ts +4463 -0
- package/lib/slide/schema.js +349 -0
- package/lib/slide/utils.d.ts +43 -0
- package/lib/slide/utils.js +165 -0
- package/lib/types/schema.d.ts +4935 -38
- package/lib/types/schema.js +11 -0
- package/lib/types/slide.d.ts +4463 -0
- package/lib/types/slide.js +349 -0
- package/lib/types/type.d.ts +1 -0
- package/lib/utils/context.d.ts +1351 -9
- package/lib/utils/html_render.js +44 -6
- package/lib/utils/image_plugins/index.js +14 -1
- package/lib/utils/image_plugins/slide.d.ts +19 -0
- package/lib/utils/image_plugins/slide.js +134 -0
- package/package.json +8 -8
- package/scripts/test/golden_age_of_discovery.json +270 -0
- package/scripts/test/img_detector.png +0 -0
- package/scripts/test/img_higgs.png +0 -0
- package/scripts/test/img_lhc.png +0 -0
- package/scripts/test/test_slide_01.json +105 -0
- package/scripts/test/test_slide_11.json +144 -0
- package/scripts/test/test_slide_12.json +887 -0
- package/scripts/test/test_slide_chart_mermaid.json +148 -0
- package/scripts/test/test_slide_image_ref.json +261 -0
- package/scripts/test/test_slide_image_ref_en.json +287 -0
- package/scripts/test/test_slide_showcase_corporate.json +497 -0
- package/scripts/test/test_slide_showcase_creative.json +545 -0
- package/scripts/test/test_slide_showcase_minimal.json +501 -0
- package/scripts/test/test_slide_showcase_pop.json +547 -0
- 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
|
+
- **10 Content Block Types**: text, bullets, code, callout, metric, divider, image, imageRef, chart, mermaid
|
|
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:
|
|
@@ -52,12 +52,13 @@ export declare const imagePreprocessAgent: (namedInputs: {
|
|
|
52
52
|
context: MulmoStudioContext;
|
|
53
53
|
beat: MulmoBeat;
|
|
54
54
|
index: number;
|
|
55
|
-
imageRefs
|
|
55
|
+
imageRefs?: Record<string, string>;
|
|
56
56
|
}) => Promise<ImagePreprocessAgentResponse>;
|
|
57
57
|
export declare const imagePluginAgent: (namedInputs: {
|
|
58
58
|
context: MulmoStudioContext;
|
|
59
59
|
beat: MulmoBeat;
|
|
60
60
|
index: number;
|
|
61
|
+
imageRefs?: Record<string, string>;
|
|
61
62
|
}) => Promise<void>;
|
|
62
63
|
export declare const htmlImageGeneratorAgent: (namedInputs: {
|
|
63
64
|
file: string;
|
|
@@ -88,18 +88,18 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
88
88
|
return { ...returnValue, imagePath, imageFromMovie: true }; // no image prompt, only movie prompt
|
|
89
89
|
}
|
|
90
90
|
// referenceImages for "edit_image", openai agent.
|
|
91
|
-
const referenceImages = MulmoBeatMethods.getImageReferenceForImageGenerator(beat, imageRefs);
|
|
91
|
+
const referenceImages = MulmoBeatMethods.getImageReferenceForImageGenerator(beat, imageRefs ?? {});
|
|
92
92
|
const prompt = imagePrompt(beat, imageAgentInfo.imageParams.style);
|
|
93
93
|
// ImageGenearalPreprocessAgentResponse
|
|
94
94
|
return { ...returnValue, imagePath, referenceImageForMovie: imagePath, imageAgentInfo, prompt, referenceImages };
|
|
95
95
|
};
|
|
96
96
|
export const imagePluginAgent = async (namedInputs) => {
|
|
97
|
-
const { context, beat, index } = namedInputs;
|
|
97
|
+
const { context, beat, index, imageRefs } = namedInputs;
|
|
98
98
|
const { imagePath } = getBeatPngImagePath(context, index);
|
|
99
99
|
const plugin = MulmoBeatMethods.getPlugin(beat);
|
|
100
100
|
try {
|
|
101
101
|
MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, true);
|
|
102
|
-
const processorParams = { beat, context, imagePath, ...htmlStyle(context, beat) };
|
|
102
|
+
const processorParams = { beat, context, imagePath, imageRefs, ...htmlStyle(context, beat) };
|
|
103
103
|
await plugin.process(processorParams);
|
|
104
104
|
MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, false);
|
|
105
105
|
}
|
package/lib/actions/images.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare const beat_graph_data: {
|
|
|
29
29
|
context: MulmoStudioContext;
|
|
30
30
|
beat: import("../types/type.js").MulmoBeat;
|
|
31
31
|
index: number;
|
|
32
|
-
imageRefs
|
|
32
|
+
imageRefs?: Record<string, string>;
|
|
33
33
|
}) => Promise<({
|
|
34
34
|
imageParams?: MulmoImageParams;
|
|
35
35
|
movieFile?: string;
|
|
@@ -162,11 +162,13 @@ export declare const beat_graph_data: {
|
|
|
162
162
|
context: MulmoStudioContext;
|
|
163
163
|
beat: import("../types/type.js").MulmoBeat;
|
|
164
164
|
index: number;
|
|
165
|
+
imageRefs?: Record<string, string>;
|
|
165
166
|
}) => Promise<void>;
|
|
166
167
|
inputs: {
|
|
167
168
|
context: string;
|
|
168
169
|
beat: string;
|
|
169
170
|
index: string;
|
|
171
|
+
imageRefs: string;
|
|
170
172
|
onComplete: string[];
|
|
171
173
|
};
|
|
172
174
|
};
|
package/lib/actions/images.js
CHANGED
package/lib/actions/pdf.js
CHANGED
|
@@ -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
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
margin:
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MulmoMediaSource, MulmoMediaMermaidSource, MulmoStudioContext, ImageType } from "../types/index.js";
|
|
2
2
|
export declare const getExtention: (contentType: string | null, url: string) => string;
|
|
3
|
+
export declare const pathToDataUrl: (filePath: string) => string;
|
|
3
4
|
export declare const MulmoMediaSourceMethods: {
|
|
4
5
|
getText(mediaSource: MulmoMediaMermaidSource, context: MulmoStudioContext): Promise<string | null>;
|
|
5
6
|
resolve(mediaSource: MulmoMediaSource | undefined, context: MulmoStudioContext): string | null;
|
|
@@ -60,12 +60,26 @@ const urlToDataUrl = async (url, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
|
|
|
60
60
|
clearTimeout(timeoutId);
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
|
+
/** Map file extension to MIME type for data URLs */
|
|
64
|
+
const extensionToMimeType = (ext) => {
|
|
65
|
+
const mimeMap = {
|
|
66
|
+
jpg: "image/jpeg",
|
|
67
|
+
jpeg: "image/jpeg",
|
|
68
|
+
png: "image/png",
|
|
69
|
+
gif: "image/gif",
|
|
70
|
+
webp: "image/webp",
|
|
71
|
+
svg: "image/svg+xml",
|
|
72
|
+
bmp: "image/bmp",
|
|
73
|
+
ico: "image/x-icon",
|
|
74
|
+
};
|
|
75
|
+
return mimeMap[ext.toLowerCase()] ?? `image/${ext}`;
|
|
76
|
+
};
|
|
63
77
|
// Convert local file path to data URL (base64 encoded)
|
|
64
|
-
const pathToDataUrl = (filePath) => {
|
|
78
|
+
export const pathToDataUrl = (filePath) => {
|
|
65
79
|
assert(fs.existsSync(filePath), `File not found: ${filePath}`, false, mediaSourceFileNotFoundError(filePath));
|
|
66
80
|
const buffer = fs.readFileSync(filePath);
|
|
67
|
-
const
|
|
68
|
-
const mimeType =
|
|
81
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "png";
|
|
82
|
+
const mimeType = extensionToMimeType(ext);
|
|
69
83
|
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
70
84
|
};
|
|
71
85
|
// Convert base64 string to data URL format
|
|
@@ -0,0 +1,7 @@
|
|
|
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;
|
|
6
|
+
/** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
|
|
7
|
+
export declare const renderCardContentBlocks: (blocks: ContentBlock[]) => string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { escapeHtml, nl2br, c, generateSlideId } 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
|
+
case "imageRef":
|
|
20
|
+
return renderImageRefPlaceholder(block);
|
|
21
|
+
case "chart":
|
|
22
|
+
return renderChart(block);
|
|
23
|
+
case "mermaid":
|
|
24
|
+
return renderMermaid(block);
|
|
25
|
+
default:
|
|
26
|
+
return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** Render an array of content blocks to HTML */
|
|
30
|
+
export const renderContentBlocks = (blocks) => {
|
|
31
|
+
return blocks.map(renderContentBlock).join("\n");
|
|
32
|
+
};
|
|
33
|
+
/** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
|
|
34
|
+
export const renderCardContentBlocks = (blocks) => {
|
|
35
|
+
return blocks
|
|
36
|
+
.map((block) => {
|
|
37
|
+
if (block.type === "image") {
|
|
38
|
+
return `<div class="aspect-video shrink-0 overflow-hidden">${renderContentBlock(block)}</div>`;
|
|
39
|
+
}
|
|
40
|
+
return renderContentBlock(block);
|
|
41
|
+
})
|
|
42
|
+
.join("\n");
|
|
43
|
+
};
|
|
44
|
+
const resolveTextColor = (block) => {
|
|
45
|
+
if (block.color)
|
|
46
|
+
return `text-${c(block.color)}`;
|
|
47
|
+
if (block.dim)
|
|
48
|
+
return "text-d-dim";
|
|
49
|
+
return "text-d-muted";
|
|
50
|
+
};
|
|
51
|
+
const resolveAlign = (align) => {
|
|
52
|
+
if (align === "center")
|
|
53
|
+
return "text-center";
|
|
54
|
+
if (align === "right")
|
|
55
|
+
return "text-right";
|
|
56
|
+
return "";
|
|
57
|
+
};
|
|
58
|
+
const renderText = (block) => {
|
|
59
|
+
const color = resolveTextColor(block);
|
|
60
|
+
const bold = block.bold ? "font-bold" : "";
|
|
61
|
+
const size = block.fontSize !== undefined && block.fontSize >= 18 ? "text-xl" : "text-[15px]";
|
|
62
|
+
const alignCls = resolveAlign(block.align);
|
|
63
|
+
return `<p class="${size} ${color} ${bold} ${alignCls} font-body leading-relaxed">${nl2br(block.value)}</p>`;
|
|
64
|
+
};
|
|
65
|
+
const renderBullets = (block) => {
|
|
66
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
67
|
+
const items = block.items
|
|
68
|
+
.map((item, i) => {
|
|
69
|
+
const marker = block.ordered ? `${i + 1}.` : escapeHtml(block.icon || "\u2022");
|
|
70
|
+
return ` <li class="flex gap-2"><span class="text-d-dim shrink-0">${marker}</span><span>${escapeHtml(item)}</span></li>`;
|
|
71
|
+
})
|
|
72
|
+
.join("\n");
|
|
73
|
+
return `<${tag} class="space-y-2 text-[15px] text-d-muted font-body">\n${items}\n</${tag}>`;
|
|
74
|
+
};
|
|
75
|
+
const renderCode = (block) => {
|
|
76
|
+
return `<pre class="bg-[#0D1117] p-4 rounded text-sm font-mono text-d-dim leading-relaxed whitespace-pre-wrap">${escapeHtml(block.code)}</pre>`;
|
|
77
|
+
};
|
|
78
|
+
const renderCallout = (block) => {
|
|
79
|
+
const isQuote = block.style === "quote";
|
|
80
|
+
const resolveBorderCls = (style) => {
|
|
81
|
+
if (style === "warning")
|
|
82
|
+
return `border-l-2 border-${c("warning")}`;
|
|
83
|
+
if (style === "info")
|
|
84
|
+
return `border-l-2 border-${c("info")}`;
|
|
85
|
+
return "";
|
|
86
|
+
};
|
|
87
|
+
const borderCls = resolveBorderCls(block.style);
|
|
88
|
+
const bg = isQuote ? "bg-d-alt" : "bg-d-card";
|
|
89
|
+
const textCls = isQuote ? "italic text-d-muted" : "text-d-muted";
|
|
90
|
+
const content = block.label
|
|
91
|
+
? `<span class="font-bold text-${c(block.color || "warning")}">${escapeHtml(block.label)}:</span> <span class="text-d-muted">${escapeHtml(block.text)}</span>`
|
|
92
|
+
: `<span class="${textCls}">${nl2br(block.text)}</span>`;
|
|
93
|
+
return `<div class="${bg} ${borderCls} p-3 rounded text-sm font-body">${content}</div>`;
|
|
94
|
+
};
|
|
95
|
+
const renderMetric = (block) => {
|
|
96
|
+
const lines = [];
|
|
97
|
+
lines.push(`<div class="text-center">`);
|
|
98
|
+
lines.push(` <p class="text-4xl font-bold text-${c(block.color || "primary")}">${escapeHtml(block.value)}</p>`);
|
|
99
|
+
lines.push(` <p class="text-sm text-d-dim mt-1">${escapeHtml(block.label)}</p>`);
|
|
100
|
+
if (block.change) {
|
|
101
|
+
const changeColor = block.change.startsWith("+") ? "success" : "danger";
|
|
102
|
+
lines.push(` <p class="text-sm font-bold text-${c(changeColor)} mt-1">${escapeHtml(block.change)}</p>`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(`</div>`);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
};
|
|
107
|
+
const renderDivider = (block) => {
|
|
108
|
+
const divColor = block.color ? `bg-${c(block.color)}` : "bg-d-alt";
|
|
109
|
+
return `<div class="h-[2px] ${divColor} my-2 rounded-full"></div>`;
|
|
110
|
+
};
|
|
111
|
+
const renderImage = (block) => {
|
|
112
|
+
const fit = block.fit === "cover" ? "object-cover" : "object-contain";
|
|
113
|
+
return `<div class="min-h-0 flex-1 overflow-hidden flex items-center"><img src="${escapeHtml(block.src)}" alt="${escapeHtml(block.alt || "")}" class="rounded ${fit} w-full h-full" /></div>`;
|
|
114
|
+
};
|
|
115
|
+
/** Placeholder for unresolved imageRef blocks — should be resolved before rendering */
|
|
116
|
+
const renderImageRefPlaceholder = (block) => {
|
|
117
|
+
return `<div class="min-h-0 flex-1 overflow-hidden flex items-center justify-center bg-d-alt rounded"><p class="text-sm text-d-dim font-body">[imageRef: ${escapeHtml(block.ref)}]</p></div>`;
|
|
118
|
+
};
|
|
119
|
+
const renderChart = (block) => {
|
|
120
|
+
const chartId = generateSlideId("chart");
|
|
121
|
+
const chartData = JSON.stringify(block.chartData);
|
|
122
|
+
const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${escapeHtml(block.title)}</p>` : "";
|
|
123
|
+
return `<div class="flex-1 min-h-0 flex flex-col">
|
|
124
|
+
${titleHtml}
|
|
125
|
+
<div class="flex-1 min-h-0 relative">
|
|
126
|
+
<canvas id="${chartId}" data-chart-ready="false"></canvas>
|
|
127
|
+
</div>
|
|
128
|
+
<script>(function(){
|
|
129
|
+
const ctx=document.getElementById('${chartId}');
|
|
130
|
+
const d=${chartData};
|
|
131
|
+
if(!d.options)d.options={};
|
|
132
|
+
d.options.animation=false;
|
|
133
|
+
d.options.responsive=true;
|
|
134
|
+
d.options.maintainAspectRatio=false;
|
|
135
|
+
new Chart(ctx,d);
|
|
136
|
+
requestAnimationFrame(()=>requestAnimationFrame(()=>{ctx.dataset.chartReady="true"}));
|
|
137
|
+
})()</script>
|
|
138
|
+
</div>`;
|
|
139
|
+
};
|
|
140
|
+
const renderMermaid = (block) => {
|
|
141
|
+
const mermaidId = generateSlideId("mermaid");
|
|
142
|
+
const titleHtml = block.title ? `<p class="text-sm font-bold text-d-text font-body mb-2">${escapeHtml(block.title)}</p>` : "";
|
|
143
|
+
return `<div class="flex-1 min-h-0 flex flex-col">
|
|
144
|
+
${titleHtml}
|
|
145
|
+
<div class="flex-1 min-h-0 flex justify-center items-center">
|
|
146
|
+
<div id="${mermaidId}" class="mermaid">${escapeHtml(block.code)}</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>`;
|
|
149
|
+
};
|
|
@@ -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, 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";
|
|
@@ -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, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, } from "./schema.js";
|
|
@@ -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(` “${nl2br(data.quote)}”`);
|
|
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,56 @@
|
|
|
1
|
+
import { escapeHtml, c, cardWrap, numBadge, iconSquare, slideHeader, renderCalloutBar } from "../utils.js";
|
|
2
|
+
import { renderCardContentBlocks } 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 flex-1 min-h-0 overflow-hidden flex flex-col ${centerCls}">`);
|
|
27
|
+
inner.push(renderCardContentBlocks(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 min-h-0 items-stretch">`);
|
|
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,29 @@
|
|
|
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 flex-1 min-h-0 overflow-hidden flex flex-col">`);
|
|
9
|
+
inner.push(renderContentBlocks(panel.content));
|
|
10
|
+
inner.push(`</div>`);
|
|
11
|
+
}
|
|
12
|
+
if (panel.footer) {
|
|
13
|
+
if (!panel.content)
|
|
14
|
+
inner.push(`<div class="flex-1"></div>`);
|
|
15
|
+
inner.push(`<p class="text-sm text-d-dim font-body mt-3">${escapeHtml(panel.footer)}</p>`);
|
|
16
|
+
}
|
|
17
|
+
return cardWrap(accent, inner.join("\n"), "flex-1");
|
|
18
|
+
};
|
|
19
|
+
export const layoutComparison = (data) => {
|
|
20
|
+
const parts = [slideHeader(data)];
|
|
21
|
+
parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1 min-h-0 items-stretch">`);
|
|
22
|
+
parts.push(buildPanel(data.left));
|
|
23
|
+
parts.push(buildPanel(data.right));
|
|
24
|
+
parts.push(`</div>`);
|
|
25
|
+
if (data.callout) {
|
|
26
|
+
parts.push(`<div class="mt-auto pb-4">${renderCalloutBar(data.callout)}</div>`);
|
|
27
|
+
}
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
};
|
|
@@ -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,43 @@
|
|
|
1
|
+
import { escapeHtml, nl2br, c, cardWrap, numBadge, iconSquare } from "../utils.js";
|
|
2
|
+
import { renderCardContentBlocks } 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 min-h-0 overflow-hidden 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 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(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
|
+
};
|