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.
- package/README.md +43 -0
- package/lib/actions/pdf.js +18 -15
- package/lib/slide/blocks.d.ts +5 -0
- package/lib/slide/blocks.js +97 -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 +28 -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 +29 -0
- package/lib/slide/schema.d.ts +4009 -0
- package/lib/slide/schema.js +330 -0
- package/lib/slide/utils.d.ts +32 -0
- package/lib/slide/utils.js +112 -0
- package/lib/types/schema.d.ts +4487 -38
- package/lib/types/schema.js +11 -0
- package/lib/types/slide.d.ts +4009 -0
- package/lib/types/slide.js +330 -0
- package/lib/utils/context.d.ts +1169 -9
- package/lib/utils/image_plugins/index.js +14 -1
- package/lib/utils/image_plugins/slide.d.ts +5 -0
- package/lib/utils/image_plugins/slide.js +35 -0
- package/package.json +8 -8
- package/scripts/test/golden_age_of_discovery.json +270 -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_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
|
+
- **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:
|
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,
|
|
@@ -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,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 { 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,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,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 { 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,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,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">•</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,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,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
|
+
};
|