@mulmocast/deck 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Satoshi Nakajima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @mulmocast/deck
2
+
3
+ Self-contained deck DSL for MulmoCast.
4
+
5
+ A `SlideLayout` JSON object describes a single semantic slide (e.g. `stats`, `comparison`, `timeline`, `table`, `columns`, …). `generateSlideHTML()` renders it to a single HTML string styled with Tailwind via CDN. No Puppeteer, no filesystem — pure data → HTML. Works in both Node.js and the browser.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ yarn add @mulmocast/deck
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { generateSlideHTML, type SlideLayout, type SlideTheme } from "@mulmocast/deck";
17
+
18
+ const slide: SlideLayout = {
19
+ layout: "stats",
20
+ title: "Quarterly Highlights",
21
+ subtitle: "FY2026 Q1",
22
+ stats: [
23
+ { value: "+42%", label: "Revenue YoY", color: "success" },
24
+ { value: "1.8M", label: "Active Users", color: "primary" },
25
+ { value: "4.6", label: "Avg NPS", color: "info" },
26
+ { value: "98%", label: "Uptime", color: "accent" },
27
+ ],
28
+ };
29
+
30
+ const theme: SlideTheme = {
31
+ colors: {
32
+ bg: "FFFBEB", bgCard: "FFFFFF", bgCardAlt: "FEF3C7",
33
+ text: "1C1917", textMuted: "57534E", textDim: "A8A29E",
34
+ primary: "EA580C", accent: "D946EF",
35
+ success: "16A34A", warning: "CA8A04", danger: "DC2626",
36
+ info: "0284C7", highlight: "E11D48",
37
+ },
38
+ fonts: { title: "Georgia", body: "Calibri", mono: "Consolas" },
39
+ };
40
+
41
+ const html = generateSlideHTML(theme, slide);
42
+ // → self-contained HTML string. Drop into an iframe srcdoc for live preview,
43
+ // or pass to Puppeteer for PNG/PDF rendering (see mulmocast-cli).
44
+ ```
45
+
46
+ ## Available layouts
47
+
48
+ `title` · `bigQuote` · `columns` · `comparison` · `stats` · `table` · `timeline` · `matrix` · `grid` · `split` · `funnel` · `waterfall`
49
+
50
+ Each layout has its own Zod schema under `slideLayoutSchema` (a discriminated union). See `src/schema.ts` for the full shape.
51
+
52
+ ## Design
53
+
54
+ - **Data → HTML, no side effects.** Pure functions, easy to test and use anywhere.
55
+ - **Tailwind via CDN.** Themes resolve to CSS variables; no compile step.
56
+ - **Schema-first.** All shapes are validated with [Zod](https://zod.dev), so types are derived (not duplicated).
57
+ - **Browser-safe.** No Node-only APIs. Just import in a Vite/Vue/React app and render into an iframe.
58
+
59
+ ## Consumers
60
+
61
+ - [`mulmocast`](https://www.npmjs.com/package/mulmocast) — CLI uses `generateSlideHTML()` then snapshots to PNG with Puppeteer.
62
+ - `@mulmocast/deck-web` *(WIP)* — Browser editor with live preview and schema-driven inspector.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { ContentBlock, TableCellValue } from "./schema.js";
2
+ export declare const resolveCellColor: (cellObj: {
3
+ color?: string;
4
+ }, isRowHeader: boolean) => string;
5
+ export declare const renderBadge: (text: string, color: string) => string;
6
+ export declare const renderCellValue: (cell: TableCellValue, isRowHeader: boolean) => string;
7
+ export declare const renderTableCore: (headers: string[] | undefined, rows: TableCellValue[][], rowHeaders?: boolean, striped?: boolean) => string;
8
+ /** Render a single content block to HTML */
9
+ export declare const renderContentBlock: (block: ContentBlock) => string;
10
+ /** Render an array of content blocks to HTML */
11
+ export declare const renderContentBlocks: (blocks: ContentBlock[]) => string;
12
+ /** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
13
+ export declare const renderCardContentBlocks: (blocks: ContentBlock[]) => string;
package/lib/blocks.js ADDED
@@ -0,0 +1,251 @@
1
+ import { escapeHtml, c, generateSlideId, renderInlineMarkup, blockTitle, resolveChangeColor, resolveAccent } from "./utils.js";
2
+ // ─── Table cell rendering (shared with layouts/table.ts) ───
3
+ export const resolveCellColor = (cellObj, isRowHeader) => {
4
+ if (cellObj.color)
5
+ return `text-${c(cellObj.color)}`;
6
+ if (isRowHeader)
7
+ return "text-d-text";
8
+ return "text-d-muted";
9
+ };
10
+ export const renderBadge = (text, color) => {
11
+ return `<span class="px-2 py-0.5 rounded-full text-xs font-bold text-white bg-${c(color)}">${renderInlineMarkup(text)}</span>`;
12
+ };
13
+ export const renderCellValue = (cell, isRowHeader) => {
14
+ const cellObj = typeof cell === "object" && cell !== null ? cell : { text: String(cell) };
15
+ if (cellObj.badge && cellObj.color) {
16
+ return `<td class="px-4 py-3 text-sm font-body border-b border-d-alt">${renderBadge(cellObj.text, cellObj.color)}</td>`;
17
+ }
18
+ const colorCls = resolveCellColor(cellObj, isRowHeader);
19
+ const boldCls = cellObj.bold || isRowHeader ? "font-bold" : "";
20
+ return `<td class="px-4 py-3 text-sm ${colorCls} ${boldCls} font-body border-b border-d-alt">${renderInlineMarkup(cellObj.text)}</td>`;
21
+ };
22
+ export const renderTableCore = (headers, rows, rowHeaders, striped) => {
23
+ const parts = [];
24
+ const isStriped = striped !== false;
25
+ parts.push(`<table class="w-full border-collapse">`);
26
+ if (headers && headers.length > 0) {
27
+ parts.push(`<thead>`);
28
+ parts.push(`<tr>`);
29
+ headers.forEach((h) => {
30
+ parts.push(` <th class="text-left px-4 py-3 text-sm font-bold text-d-text font-body border-b-2 border-d-alt">${renderInlineMarkup(h)}</th>`);
31
+ });
32
+ parts.push(`</tr>`);
33
+ parts.push(`</thead>`);
34
+ }
35
+ parts.push(`<tbody>`);
36
+ rows.forEach((row, ri) => {
37
+ const bgCls = isStriped && ri % 2 === 1 ? "bg-d-alt/30" : "";
38
+ parts.push(`<tr class="${bgCls}">`);
39
+ (row || []).forEach((cell, ci) => {
40
+ const isRowHeader = ci === 0 && !!rowHeaders;
41
+ parts.push(` ${renderCellValue(cell, isRowHeader)}`);
42
+ });
43
+ parts.push(`</tr>`);
44
+ });
45
+ parts.push(`</tbody>`);
46
+ parts.push(`</table>`);
47
+ return parts.join("\n");
48
+ };
49
+ const renderTableBlock = (block) => {
50
+ return `<div class="overflow-auto">${blockTitle(block.title)}${renderTableCore(block.headers, block.rows, block.rowHeaders, block.striped)}</div>`;
51
+ };
52
+ /** Render a single content block to HTML */
53
+ export const renderContentBlock = (block) => {
54
+ switch (block.type) {
55
+ case "text":
56
+ return renderText(block);
57
+ case "bullets":
58
+ return renderBullets(block);
59
+ case "code":
60
+ return renderCode(block);
61
+ case "callout":
62
+ return renderCallout(block);
63
+ case "metric":
64
+ return renderMetric(block);
65
+ case "divider":
66
+ return renderDivider(block);
67
+ case "image":
68
+ return renderImage(block);
69
+ case "imageRef":
70
+ return renderImageRefPlaceholder(block);
71
+ case "chart":
72
+ return renderChart(block);
73
+ case "mermaid":
74
+ return renderMermaid(block);
75
+ case "section":
76
+ return renderSection(block);
77
+ case "table":
78
+ return renderTableBlock(block);
79
+ default:
80
+ return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
81
+ }
82
+ };
83
+ /** Render an array of content blocks to HTML */
84
+ export const renderContentBlocks = (blocks) => {
85
+ return blocks.map(renderContentBlock).join("\n");
86
+ };
87
+ /** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
88
+ export const renderCardContentBlocks = (blocks) => {
89
+ return blocks
90
+ .map((block) => {
91
+ if (block.type === "image") {
92
+ return `<div class="aspect-video shrink-0 overflow-hidden">${renderContentBlock(block)}</div>`;
93
+ }
94
+ return renderContentBlock(block);
95
+ })
96
+ .join("\n");
97
+ };
98
+ const resolveTextColor = (block) => {
99
+ if (block.color)
100
+ return `text-${c(block.color)}`;
101
+ if (block.dim)
102
+ return "text-d-dim";
103
+ return "text-d-muted";
104
+ };
105
+ const resolveAlign = (align) => {
106
+ if (align === "center")
107
+ return "text-center";
108
+ if (align === "right")
109
+ return "text-right";
110
+ return "";
111
+ };
112
+ const renderText = (block) => {
113
+ const color = resolveTextColor(block);
114
+ const bold = block.bold ? "font-bold" : "";
115
+ const size = block.fontSize !== undefined && block.fontSize >= 18 ? "text-xl" : "text-[15px]";
116
+ const alignCls = resolveAlign(block.align);
117
+ return `<p class="${size} ${color} ${bold} ${alignCls} font-body leading-relaxed">${renderInlineMarkup(block.value)}</p>`;
118
+ };
119
+ /** Extract text from a bullet item (string or object) */
120
+ const bulletItemText = (item) => {
121
+ return typeof item === "string" ? item : item.text;
122
+ };
123
+ /** Render sub-bullets for a nested bullet item */
124
+ const renderSubBullets = (item) => {
125
+ if (typeof item === "string" || !item.items || item.items.length === 0)
126
+ return "";
127
+ const subs = item.items
128
+ .map((sub) => {
129
+ return ` <li class="flex gap-2 ml-6 text-[14px]"><span class="text-d-dim shrink-0">\u25E6</span><span>${renderInlineMarkup(bulletItemText(sub))}</span></li>`;
130
+ })
131
+ .join("\n");
132
+ return `\n${subs}`;
133
+ };
134
+ const renderBullets = (block) => {
135
+ const tag = block.ordered ? "ol" : "ul";
136
+ const items = block.items
137
+ .map((item, i) => {
138
+ const marker = block.ordered ? `${i + 1}.` : escapeHtml(block.icon || "\u2022");
139
+ const text = bulletItemText(item);
140
+ const subHtml = renderSubBullets(item);
141
+ return ` <li class="flex flex-col gap-1"><div class="flex gap-2"><span class="text-d-dim shrink-0">${marker}</span><span>${renderInlineMarkup(text)}</span></div>${subHtml}</li>`;
142
+ })
143
+ .join("\n");
144
+ return `<${tag} class="space-y-2 text-[15px] text-d-muted font-body">\n${items}\n</${tag}>`;
145
+ };
146
+ const renderCode = (block) => {
147
+ return `<pre class="bg-[#0D1117] p-4 rounded text-sm font-mono text-d-dim leading-relaxed whitespace-pre-wrap">${escapeHtml(block.code)}</pre>`;
148
+ };
149
+ const renderCallout = (block) => {
150
+ const isQuote = block.style === "quote";
151
+ const resolveBorderCls = (style) => {
152
+ if (style === "warning")
153
+ return `border-l-2 border-${c("warning")}`;
154
+ if (style === "info")
155
+ return `border-l-2 border-${c("info")}`;
156
+ return "";
157
+ };
158
+ const borderCls = resolveBorderCls(block.style);
159
+ const bg = isQuote ? "bg-d-alt" : "bg-d-card";
160
+ const textCls = isQuote ? "italic text-d-muted" : "text-d-muted";
161
+ const content = block.label
162
+ ? `<span class="font-bold text-${c(block.color || "warning")}">${renderInlineMarkup(block.label)}:</span> <span class="text-d-muted">${renderInlineMarkup(block.text)}</span>`
163
+ : `<span class="${textCls}">${renderInlineMarkup(block.text)}</span>`;
164
+ return `<div class="${bg} ${borderCls} p-3 rounded text-sm font-body">${content}</div>`;
165
+ };
166
+ const renderMetric = (block) => {
167
+ const lines = [];
168
+ lines.push(`<div class="text-center">`);
169
+ lines.push(` <p class="text-4xl font-bold text-${c(resolveAccent(block.color))}">${renderInlineMarkup(block.value)}</p>`);
170
+ lines.push(` <p class="text-sm text-d-dim mt-1">${renderInlineMarkup(block.label)}</p>`);
171
+ if (block.change) {
172
+ lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1">${escapeHtml(block.change)}</p>`);
173
+ }
174
+ lines.push(`</div>`);
175
+ return lines.join("\n");
176
+ };
177
+ const renderDivider = (block) => {
178
+ const divColor = block.color ? `bg-${c(block.color)}` : "bg-d-alt";
179
+ return `<div class="h-[2px] ${divColor} my-2 rounded-full"></div>`;
180
+ };
181
+ const renderImage = (block) => {
182
+ const fit = block.fit === "cover" ? "object-cover" : "object-contain";
183
+ 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>`;
184
+ };
185
+ /** Placeholder for unresolved imageRef blocks — should be resolved before rendering */
186
+ const renderImageRefPlaceholder = (block) => {
187
+ 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>`;
188
+ };
189
+ const renderChart = (block) => {
190
+ const chartId = generateSlideId("chart");
191
+ const chartData = JSON.stringify(block.chartData);
192
+ return `<div class="flex-1 min-h-0 flex flex-col">
193
+ ${blockTitle(block.title)}
194
+ <div class="flex-1 min-h-0 relative">
195
+ <canvas id="${chartId}" data-chart-ready="false"></canvas>
196
+ </div>
197
+ <script>(function(){
198
+ const ctx=document.getElementById('${chartId}');
199
+ const d=${chartData};
200
+ if(!d.options)d.options={};
201
+ d.options.animation=false;
202
+ d.options.responsive=true;
203
+ d.options.maintainAspectRatio=false;
204
+ new Chart(ctx,d);
205
+ requestAnimationFrame(()=>requestAnimationFrame(()=>{ctx.dataset.chartReady="true"}));
206
+ })()</script>
207
+ </div>`;
208
+ };
209
+ const renderMermaid = (block) => {
210
+ const mermaidId = generateSlideId("mermaid");
211
+ return `<div class="flex-1 min-h-0 flex flex-col">
212
+ ${blockTitle(block.title)}
213
+ <div class="flex-1 min-h-0 flex justify-center items-center">
214
+ <div id="${mermaidId}" class="mermaid">${escapeHtml(block.code)}</div>
215
+ </div>
216
+ </div>`;
217
+ };
218
+ /** Render the text + content blocks inside a section (shared by sidebar/default variants) */
219
+ const renderSectionContent = (block) => {
220
+ const parts = [];
221
+ if (block.text) {
222
+ parts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
223
+ }
224
+ if (block.content) {
225
+ parts.push(block.content.map(renderContentBlock).join("\n"));
226
+ }
227
+ return parts.join("\n");
228
+ };
229
+ const renderSectionSidebar = (block) => {
230
+ const color = resolveAccent(block.color);
231
+ const chars = block.label
232
+ .split("")
233
+ .map((ch) => escapeHtml(ch))
234
+ .join("<br>");
235
+ const sidebar = `<div class="w-[48px] shrink-0 rounded-l bg-${c(color)} flex items-center justify-center"><span class="text-sm font-bold text-white font-body leading-snug text-center">${chars}</span></div>`;
236
+ return `<div class="flex rounded overflow-hidden bg-d-card">
237
+ ${sidebar}
238
+ <div class="flex-1 space-y-2 p-3">${renderSectionContent(block)}</div>
239
+ </div>`;
240
+ };
241
+ const renderSectionDefault = (block) => {
242
+ const color = resolveAccent(block.color);
243
+ const badge = `<span class="min-w-[80px] px-3 py-1 rounded text-sm font-bold text-white bg-${c(color)} shrink-0">${renderInlineMarkup(block.label)}</span>`;
244
+ return `<div class="flex gap-4 items-start">
245
+ ${badge}
246
+ <div class="flex-1 space-y-2">${renderSectionContent(block)}</div>
247
+ </div>`;
248
+ };
249
+ const renderSection = (block) => {
250
+ return block.sidebar ? renderSectionSidebar(block) : renderSectionDefault(block);
251
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { generateSlideHTML } from "./render.js";
2
+ export type { ResolvedBranding } from "./render.js";
3
+ export { renderSlideContent } from "./layouts/index.js";
4
+ export { renderContentBlock, renderContentBlocks } from "./blocks.js";
5
+ export { escapeHtml } from "./utils.js";
6
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, slideBrandingLogoSchema, slideBrandingSchema, } from "./schema.js";
7
+ export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, ContentBlock, ImageRefBlock, ChartBlock, MermaidBlock, AccentColorKey, TitleSlide, ColumnsSlide, ComparisonSlide, GridSlide, BigQuoteSlide, StatsSlide, TimelineSlide, SplitSlide, MatrixSlide, TableSlide, FunnelSlide, Card, CalloutBar, SlideStyle, SlideBrandingLogo, SlideBranding, } from "./schema.js";
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Public API for @mulmocast/slide
2
+ // Self-contained slide DSL: SlideLayout JSON → Tailwind-based HTML string.
3
+ export { generateSlideHTML } from "./render.js";
4
+ export { renderSlideContent } from "./layouts/index.js";
5
+ export { renderContentBlock, renderContentBlocks } from "./blocks.js";
6
+ export { escapeHtml } from "./utils.js";
7
+ // Schemas
8
+ export { mulmoSlideMediaSchema, slideLayoutSchema, slideThemeSchema, contentBlockSchema, imageRefBlockSchema, chartBlockSchema, mermaidBlockSchema, accentColorKeySchema, slideBrandingLogoSchema, slideBrandingSchema, } 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 { renderInlineMarkup, accentBar, resolveAccent } from "../utils.js";
2
+ export const layoutBigQuote = (data) => {
3
+ const accent = resolveAccent(data.accentColor);
4
+ const parts = [];
5
+ parts.push(`<div class="flex flex-col items-center justify-center h-full px-20">`);
6
+ parts.push(` ${accentBar(accent, "w-24 mb-8")}`);
7
+ parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed">`);
8
+ parts.push(` &ldquo;${renderInlineMarkup(data.quote)}&rdquo;`);
9
+ parts.push(` </blockquote>`);
10
+ parts.push(` ${accentBar(accent, "w-24 mt-8 mb-6")}`);
11
+ if (data.author) {
12
+ parts.push(` <p class="text-lg text-d-muted font-body">${renderInlineMarkup(data.author)}</p>`);
13
+ }
14
+ if (data.role) {
15
+ parts.push(` <p class="text-sm text-d-dim font-body mt-1">${renderInlineMarkup(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,53 @@
1
+ import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
2
+ import { renderCardContentBlocks } from "../blocks.js";
3
+ const buildColumnCard = (col) => {
4
+ const accent = resolveAccent(col.accentColor);
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">${renderInlineMarkup(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">${renderInlineMarkup(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">${renderInlineMarkup(col.label)}</p>`);
21
+ }
22
+ inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1">${renderInlineMarkup(col.title)}</h3>`);
23
+ }
24
+ if (col.content) {
25
+ const centerCls = col.icon ? "text-center" : "";
26
+ inner.push(`<div class="mt-4 space-y-4 flex-1 min-h-0 overflow-auto flex flex-col ${centerCls}">`);
27
+ inner.push(renderCardContentBlocks(col.content));
28
+ inner.push(`</div>`);
29
+ }
30
+ if (col.footer) {
31
+ inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(col.footer)}</p>`);
32
+ }
33
+ return cardWrap(accent, inner.join("\n"), "flex-1");
34
+ };
35
+ export const layoutColumns = (data) => {
36
+ const cols = data.columns || [];
37
+ const parts = [slideHeader(data)];
38
+ const colElements = [];
39
+ cols.forEach((col, i) => {
40
+ colElements.push(buildColumnCard(col));
41
+ if (data.showArrows && i < cols.length - 1) {
42
+ colElements.push(`<div class="flex items-center shrink-0"><span class="text-2xl text-d-dim">\u25B6</span></div>`);
43
+ }
44
+ });
45
+ parts.push(`<div class="flex gap-4 px-12 mt-5 flex-1 min-h-0 items-start">`);
46
+ parts.push(colElements.join("\n"));
47
+ parts.push(`</div>`);
48
+ parts.push(renderOptionalCallout(data.callout));
49
+ if (data.bottomText) {
50
+ parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${renderInlineMarkup(data.bottomText)}</p>`);
51
+ }
52
+ return parts.join("\n");
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { ComparisonSlide } from "../schema.js";
2
+ export declare const layoutComparison: (data: ComparisonSlide) => string;
@@ -0,0 +1,25 @@
1
+ import { renderInlineMarkup, c, cardWrap, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
2
+ import { renderContentBlocks } from "../blocks.js";
3
+ const buildPanel = (panel) => {
4
+ const accent = resolveAccent(panel.accentColor);
5
+ const inner = [];
6
+ inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${renderInlineMarkup(panel.title)}</h3>`);
7
+ if (panel.content) {
8
+ inner.push(`<div class="mt-5 space-y-4 flex-1 min-h-0 overflow-auto flex flex-col">`);
9
+ inner.push(renderContentBlocks(panel.content));
10
+ inner.push(`</div>`);
11
+ }
12
+ if (panel.footer) {
13
+ inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(panel.footer)}</p>`);
14
+ }
15
+ return cardWrap(accent, inner.join("\n"), "flex-1");
16
+ };
17
+ export const layoutComparison = (data) => {
18
+ const parts = [slideHeader(data)];
19
+ parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1 min-h-0 items-start">`);
20
+ parts.push(buildPanel(data.left));
21
+ parts.push(buildPanel(data.right));
22
+ parts.push(`</div>`);
23
+ parts.push(renderOptionalCallout(data.callout));
24
+ return parts.join("\n");
25
+ };
@@ -0,0 +1,2 @@
1
+ import type { FunnelSlide } from "../schema.js";
2
+ export declare const layoutFunnel: (data: FunnelSlide) => string;
@@ -0,0 +1,25 @@
1
+ import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor } 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 = resolveItemColor(stage.color, data.accentColor);
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">${renderInlineMarkup(stage.label)}</span>`);
13
+ if (stage.description) {
14
+ parts.push(` <span class="text-sm text-white/70 font-body">${renderInlineMarkup(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">${renderInlineMarkup(stage.value)}</span>`);
19
+ }
20
+ parts.push(`</div>`);
21
+ });
22
+ parts.push(`</div>`);
23
+ parts.push(renderOptionalCallout(data.callout));
24
+ return parts.join("\n");
25
+ };
@@ -0,0 +1,2 @@
1
+ import type { GridSlide } from "../schema.js";
2
+ export declare const layoutGrid: (data: GridSlide) => string;
@@ -0,0 +1,38 @@
1
+ import { renderInlineMarkup, cardWrap, numBadge, iconSquare, slideHeader, resolveAccent } from "../utils.js";
2
+ import { renderCardContentBlocks } from "../blocks.js";
3
+ export const layoutGrid = (data) => {
4
+ const nCols = data.gridColumns || 3;
5
+ const parts = [slideHeader(data)];
6
+ parts.push(`<div class="grid grid-cols-${nCols} gap-4 px-12 mt-5 flex-1 min-h-0 overflow-hidden content-center">`);
7
+ (data.items || []).forEach((item) => {
8
+ const itemAccent = resolveAccent(item.accentColor);
9
+ const inner = [];
10
+ if (item.icon) {
11
+ inner.push(`<div class="flex flex-col items-center mb-2">`);
12
+ inner.push(` ${iconSquare(item.icon, itemAccent)}`);
13
+ inner.push(`</div>`);
14
+ inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${renderInlineMarkup(item.title)}</h3>`);
15
+ }
16
+ else if (item.num != null) {
17
+ inner.push(`<div class="flex items-center gap-3">`);
18
+ inner.push(` ${numBadge(item.num, itemAccent)}`);
19
+ inner.push(` <h3 class="text-sm font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
20
+ inner.push(`</div>`);
21
+ }
22
+ else {
23
+ inner.push(`<h3 class="text-lg font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
24
+ }
25
+ if (item.description) {
26
+ inner.push(`<p class="text-sm text-d-muted font-body mt-3">${renderInlineMarkup(item.description)}</p>`);
27
+ }
28
+ if (item.content) {
29
+ inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(item.content)}</div>`);
30
+ }
31
+ parts.push(cardWrap(itemAccent, inner.join("\n")));
32
+ });
33
+ parts.push(`</div>`);
34
+ if (data.footer) {
35
+ parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3">${renderInlineMarkup(data.footer)}</p>`);
36
+ }
37
+ return parts.join("\n");
38
+ };
@@ -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,46 @@
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 { layoutWaterfall } from "./waterfall.js";
13
+ import { escapeHtml } from "../utils.js";
14
+ /** Render the inner content of a slide (without the wrapper div) */
15
+ export const renderSlideContent = (slide) => {
16
+ switch (slide.layout) {
17
+ case "title":
18
+ return layoutTitle(slide);
19
+ case "columns":
20
+ return layoutColumns(slide);
21
+ case "comparison":
22
+ return layoutComparison(slide);
23
+ case "grid":
24
+ return layoutGrid(slide);
25
+ case "bigQuote":
26
+ return layoutBigQuote(slide);
27
+ case "stats":
28
+ return layoutStats(slide);
29
+ case "timeline":
30
+ return layoutTimeline(slide);
31
+ case "split":
32
+ return layoutSplit(slide);
33
+ case "matrix":
34
+ return layoutMatrix(slide);
35
+ case "table":
36
+ return layoutTable(slide);
37
+ case "funnel":
38
+ return layoutFunnel(slide);
39
+ case "waterfall":
40
+ return layoutWaterfall(slide);
41
+ default: {
42
+ const _exhaustive = slide;
43
+ return `<p class="text-white p-8">Unknown layout: ${escapeHtml(String(_exhaustive.layout))}</p>`;
44
+ }
45
+ }
46
+ };
@@ -0,0 +1,2 @@
1
+ import type { MatrixSlide } from "../schema.js";
2
+ export declare const layoutMatrix: (data: MatrixSlide) => string;