@mulmocast/deck 0.5.0 → 0.6.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/lib/blocks.d.ts +9 -4
- package/lib/blocks.js +38 -28
- package/lib/index.d.ts +1 -1
- package/lib/layouts/big_quote.js +4 -4
- package/lib/layouts/columns.js +11 -10
- package/lib/layouts/comparison.js +7 -7
- package/lib/layouts/funnel.js +5 -4
- package/lib/layouts/grid.js +10 -8
- package/lib/layouts/manifesto.js +6 -6
- package/lib/layouts/matrix.js +6 -5
- package/lib/layouts/split.js +9 -9
- package/lib/layouts/stats.js +7 -6
- package/lib/layouts/timeline.js +6 -5
- package/lib/layouts/title.js +5 -5
- package/lib/layouts/waterfall.js +2 -2
- package/lib/utils.d.ts +13 -3
- package/lib/utils.js +19 -9
- package/package.json +1 -1
package/lib/blocks.d.ts
CHANGED
|
@@ -5,9 +5,14 @@ export declare const resolveCellColor: (cellObj: {
|
|
|
5
5
|
export declare const renderBadge: (text: string, color: string) => string;
|
|
6
6
|
export declare const renderCellValue: (cell: TableCellValue, isRowHeader: boolean) => string;
|
|
7
7
|
export declare const renderTableCore: (headers: string[] | undefined, rows: TableCellValue[][], rowHeaders?: boolean, striped?: boolean) => string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Render a single content block to HTML.
|
|
10
|
+
* @param path - JSON path of this block in the source SlideLayout, used to emit `data-mulmo-path`
|
|
11
|
+
* attributes on every editable text node. Optional for back-compat — empty paths
|
|
12
|
+
* just suppress the attribute.
|
|
13
|
+
*/
|
|
14
|
+
export declare const renderContentBlock: (block: ContentBlock, path?: string) => string;
|
|
10
15
|
/** Render an array of content blocks to HTML */
|
|
11
|
-
export declare const renderContentBlocks: (blocks: ContentBlock[]) => string;
|
|
16
|
+
export declare const renderContentBlocks: (blocks: ContentBlock[], basePath?: string) => string;
|
|
12
17
|
/** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
|
|
13
|
-
export declare const renderCardContentBlocks: (blocks: ContentBlock[]) => string;
|
|
18
|
+
export declare const renderCardContentBlocks: (blocks: ContentBlock[], basePath?: string) => string;
|
package/lib/blocks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml, c, generateSlideId, renderInlineMarkup, blockTitle, resolveChangeColor, resolveAccent } from "./utils.js";
|
|
1
|
+
import { escapeHtml, c, generateSlideId, renderInlineMarkup, blockTitle, resolveChangeColor, resolveAccent, dp } from "./utils.js";
|
|
2
2
|
/**
|
|
3
3
|
* Map a TextSize variant to its Tailwind classes.
|
|
4
4
|
* default — body / muted (the original 0.1.x behavior; emitted only when neither size nor numeric fontSize is set).
|
|
@@ -62,19 +62,24 @@ export const renderTableCore = (headers, rows, rowHeaders, striped) => {
|
|
|
62
62
|
const renderTableBlock = (block) => {
|
|
63
63
|
return `<div class="overflow-auto">${blockTitle(block.title)}${renderTableCore(block.headers, block.rows, block.rowHeaders, block.striped)}</div>`;
|
|
64
64
|
};
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Render a single content block to HTML.
|
|
67
|
+
* @param path - JSON path of this block in the source SlideLayout, used to emit `data-mulmo-path`
|
|
68
|
+
* attributes on every editable text node. Optional for back-compat — empty paths
|
|
69
|
+
* just suppress the attribute.
|
|
70
|
+
*/
|
|
71
|
+
export const renderContentBlock = (block, path = "") => {
|
|
67
72
|
switch (block.type) {
|
|
68
73
|
case "text":
|
|
69
|
-
return renderText(block);
|
|
74
|
+
return renderText(block, path);
|
|
70
75
|
case "bullets":
|
|
71
|
-
return renderBullets(block);
|
|
76
|
+
return renderBullets(block, path);
|
|
72
77
|
case "code":
|
|
73
78
|
return renderCode(block);
|
|
74
79
|
case "callout":
|
|
75
|
-
return renderCallout(block);
|
|
80
|
+
return renderCallout(block, path);
|
|
76
81
|
case "metric":
|
|
77
|
-
return renderMetric(block);
|
|
82
|
+
return renderMetric(block, path);
|
|
78
83
|
case "divider":
|
|
79
84
|
return renderDivider(block);
|
|
80
85
|
case "image":
|
|
@@ -90,28 +95,29 @@ export const renderContentBlock = (block) => {
|
|
|
90
95
|
case "table":
|
|
91
96
|
return renderTableBlock(block);
|
|
92
97
|
case "tag":
|
|
93
|
-
return renderTag(block);
|
|
98
|
+
return renderTag(block, path);
|
|
94
99
|
default:
|
|
95
100
|
return `<p class="text-sm text-d-muted font-body">[unknown block type]</p>`;
|
|
96
101
|
}
|
|
97
102
|
};
|
|
98
103
|
/** Render a card-internal accent tag (small uppercase label, sits above an h3). Matches reveal.js .tag. */
|
|
99
|
-
const renderTag = (block) => {
|
|
104
|
+
const renderTag = (block, path = "") => {
|
|
100
105
|
const color = c(block.color || "primary");
|
|
101
|
-
return `<span class="text-xs font-bold uppercase tracking-[0.12em] text-${color} font-accent">${renderInlineMarkup(block.text)}</span>`;
|
|
106
|
+
return `<span class="text-xs font-bold uppercase tracking-[0.12em] text-${color} font-accent"${dp(path ? `${path}.text` : "")}>${renderInlineMarkup(block.text)}</span>`;
|
|
102
107
|
};
|
|
103
108
|
/** Render an array of content blocks to HTML */
|
|
104
|
-
export const renderContentBlocks = (blocks) => {
|
|
105
|
-
return blocks.map(renderContentBlock).join("\n");
|
|
109
|
+
export const renderContentBlocks = (blocks, basePath = "") => {
|
|
110
|
+
return blocks.map((b, i) => renderContentBlock(b, basePath ? `${basePath}[${i}]` : `content[${i}]`)).join("\n");
|
|
106
111
|
};
|
|
107
112
|
/** Render content blocks with fixed aspect-ratio container for image blocks (used in card layouts) */
|
|
108
|
-
export const renderCardContentBlocks = (blocks) => {
|
|
113
|
+
export const renderCardContentBlocks = (blocks, basePath = "") => {
|
|
109
114
|
return blocks
|
|
110
|
-
.map((block) => {
|
|
115
|
+
.map((block, i) => {
|
|
116
|
+
const p = basePath ? `${basePath}[${i}]` : `content[${i}]`;
|
|
111
117
|
if (block.type === "image") {
|
|
112
|
-
return `<div class="aspect-video shrink-0 overflow-hidden">${renderContentBlock(block)}</div>`;
|
|
118
|
+
return `<div class="aspect-video shrink-0 overflow-hidden">${renderContentBlock(block, p)}</div>`;
|
|
113
119
|
}
|
|
114
|
-
return renderContentBlock(block);
|
|
120
|
+
return renderContentBlock(block, p);
|
|
115
121
|
})
|
|
116
122
|
.join("\n");
|
|
117
123
|
};
|
|
@@ -130,7 +136,7 @@ const resolveAlign = (align) => {
|
|
|
130
136
|
return "text-right";
|
|
131
137
|
return "";
|
|
132
138
|
};
|
|
133
|
-
const renderText = (block) => {
|
|
139
|
+
const renderText = (block, path = "") => {
|
|
134
140
|
// Resolution order for size: explicit numeric `fontSize` (legacy) wins over new `size` variant.
|
|
135
141
|
const legacyXl = block.fontSize !== undefined && block.fontSize >= 18;
|
|
136
142
|
const style = TEXT_SIZE_STYLES[block.size ?? "default"];
|
|
@@ -139,7 +145,7 @@ const renderText = (block) => {
|
|
|
139
145
|
const colorCls = explicitColor ?? style.colorCls ?? "text-d-muted";
|
|
140
146
|
const bold = block.bold ? "font-bold" : "";
|
|
141
147
|
const alignCls = resolveAlign(block.align);
|
|
142
|
-
return `<p class="${sizeCls} ${colorCls} ${bold} ${alignCls} font-body leading-relaxed">${renderInlineMarkup(block.value)}</p>`;
|
|
148
|
+
return `<p class="${sizeCls} ${colorCls} ${bold} ${alignCls} font-body leading-relaxed"${dp(path ? `${path}.value` : "")}>${renderInlineMarkup(block.value)}</p>`;
|
|
143
149
|
};
|
|
144
150
|
/** Extract text from a bullet item (string or object) */
|
|
145
151
|
const bulletItemText = (item) => {
|
|
@@ -168,7 +174,7 @@ const resolveBulletSize = (block, item) => {
|
|
|
168
174
|
return item.size;
|
|
169
175
|
return block.size ?? "default";
|
|
170
176
|
};
|
|
171
|
-
const renderBullets = (block) => {
|
|
177
|
+
const renderBullets = (block, path = "") => {
|
|
172
178
|
const tag = block.ordered ? "ol" : "ul";
|
|
173
179
|
const blockStyle = TEXT_SIZE_STYLES[block.size ?? "default"];
|
|
174
180
|
const items = block.items
|
|
@@ -183,7 +189,9 @@ const renderBullets = (block) => {
|
|
|
183
189
|
const itemStyle = TEXT_SIZE_STYLES[resolveBulletSize(block, item)];
|
|
184
190
|
// Emit per-item classes only when they differ from the block-level style so output stays compact in the common case.
|
|
185
191
|
const itemCls = itemStyle.fontCls === blockStyle.fontCls && itemStyle.colorCls === blockStyle.colorCls ? "" : ` ${itemStyle.fontCls} ${itemStyle.colorCls}`;
|
|
186
|
-
|
|
192
|
+
// For string items, the editable target is `path.items[i]`. For object items, it's `path.items[i].text`.
|
|
193
|
+
const itemPath = path ? (typeof item === "string" ? `${path}.items[${i}]` : `${path}.items[${i}].text`) : "";
|
|
194
|
+
return ` <li class="flex flex-col gap-1${itemCls}"><div class="flex gap-2">${markerHtml}<span${dp(itemPath)}>${renderInlineMarkup(text)}</span></div>${subHtml}</li>`;
|
|
187
195
|
})
|
|
188
196
|
.join("\n");
|
|
189
197
|
return `<${tag} class="space-y-2 ${blockStyle.fontCls} ${blockStyle.colorCls} font-body">\n${items}\n</${tag}>`;
|
|
@@ -191,7 +199,7 @@ const renderBullets = (block) => {
|
|
|
191
199
|
const renderCode = (block) => {
|
|
192
200
|
return `<pre class="bg-[#0D1117] p-4 rounded text-sm font-mono text-d-dim leading-relaxed whitespace-pre-wrap">${escapeHtml(block.code)}</pre>`;
|
|
193
201
|
};
|
|
194
|
-
const renderCallout = (block) => {
|
|
202
|
+
const renderCallout = (block, path = "") => {
|
|
195
203
|
const isQuote = block.style === "quote";
|
|
196
204
|
const resolveBorderCls = (style) => {
|
|
197
205
|
if (style === "warning")
|
|
@@ -205,18 +213,20 @@ const renderCallout = (block) => {
|
|
|
205
213
|
// Pre-Phase-4 default was `text-sm` (~14px). Map to the size variants only when explicitly requested.
|
|
206
214
|
const sizeStyle = block.size ? TEXT_SIZE_STYLES[block.size] : { fontCls: "text-sm", colorCls: "text-d-muted" };
|
|
207
215
|
const textCls = isQuote ? `italic ${sizeStyle.colorCls}` : sizeStyle.colorCls;
|
|
216
|
+
const dpLabel = dp(path ? `${path}.label` : "");
|
|
217
|
+
const dpText = dp(path ? `${path}.text` : "");
|
|
208
218
|
const content = block.label
|
|
209
|
-
? `<span class="font-bold text-${c(block.color || "warning")}">${renderInlineMarkup(block.label)}:</span> <span class="${textCls}">${renderInlineMarkup(block.text)}</span>`
|
|
210
|
-
: `<span class="${textCls}">${renderInlineMarkup(block.text)}</span>`;
|
|
219
|
+
? `<span class="font-bold text-${c(block.color || "warning")}"${dpLabel}>${renderInlineMarkup(block.label)}:</span> <span class="${textCls}"${dpText}>${renderInlineMarkup(block.text)}</span>`
|
|
220
|
+
: `<span class="${textCls}"${dpText}>${renderInlineMarkup(block.text)}</span>`;
|
|
211
221
|
return `<div class="${bg} ${borderCls} p-3 rounded ${sizeStyle.fontCls} font-body">${content}</div>`;
|
|
212
222
|
};
|
|
213
|
-
const renderMetric = (block) => {
|
|
223
|
+
const renderMetric = (block, path = "") => {
|
|
214
224
|
const lines = [];
|
|
215
225
|
lines.push(`<div class="text-center">`);
|
|
216
|
-
lines.push(` <p class="text-4xl font-bold text-${c(resolveAccent(block.color))}">${renderInlineMarkup(block.value)}</p>`);
|
|
217
|
-
lines.push(` <p class="text-sm text-d-dim mt-1">${renderInlineMarkup(block.label)}</p>`);
|
|
226
|
+
lines.push(` <p class="text-4xl font-bold text-${c(resolveAccent(block.color))}"${dp(path ? `${path}.value` : "")}>${renderInlineMarkup(block.value)}</p>`);
|
|
227
|
+
lines.push(` <p class="text-sm text-d-dim mt-1"${dp(path ? `${path}.label` : "")}>${renderInlineMarkup(block.label)}</p>`);
|
|
218
228
|
if (block.change) {
|
|
219
|
-
lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1">${escapeHtml(block.change)}</p>`);
|
|
229
|
+
lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1"${dp(path ? `${path}.change` : "")}>${escapeHtml(block.change)}</p>`);
|
|
220
230
|
}
|
|
221
231
|
lines.push(`</div>`);
|
|
222
232
|
return lines.join("\n");
|
|
@@ -269,7 +279,7 @@ const renderSectionContent = (block) => {
|
|
|
269
279
|
parts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
|
|
270
280
|
}
|
|
271
281
|
if (block.content) {
|
|
272
|
-
parts.push(block.content.map(renderContentBlock).join("\n"));
|
|
282
|
+
parts.push(block.content.map((b) => renderContentBlock(b)).join("\n"));
|
|
273
283
|
}
|
|
274
284
|
return parts.join("\n");
|
|
275
285
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { renderSlideContent } from "./layouts/index.js";
|
|
|
4
4
|
export { renderContentBlock, renderContentBlocks } from "./blocks.js";
|
|
5
5
|
export { escapeHtml, resetSlideIdCounter, renderInlineMarkup } from "./utils.js";
|
|
6
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,
|
|
7
|
+
export type { MulmoSlideMedia, SlideLayout, SlideTheme, SlideThemeColors, SlideThemeFonts, AccentColorKey, ContentBlock, TextBlock, BulletsBlock, BulletItem, CodeBlock, CalloutBlock, MetricBlock, DividerBlock, ImageBlock, ImageRefBlock, ChartBlock, MermaidBlock, SectionBlock, TableBlock, TagBlock, TableCellValue, TitleSlide, ColumnsSlide, ComparisonSlide, ComparisonPanel, GridSlide, GridItem, BigQuoteSlide, StatsSlide, StatItem, TimelineSlide, TimelineItem, SplitSlide, SplitPanel, MatrixSlide, MatrixCell, TableSlide, FunnelSlide, FunnelStage, WaterfallSlide, WaterfallItem, ManifestoSlide, ManifestoLine, Card, CalloutBar, SlideStyle, SlideBrandingLogo, SlideBranding, BulletIcon, TextSize, SlideDensity, SlideTitleSize, SlideSubtitleSize, SlideCardStyle, } from "./schema.js";
|
package/lib/layouts/big_quote.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, accentBar, resolveAccent, renderEyebrow } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, accentBar, resolveAccent, renderEyebrow, dp } from "../utils.js";
|
|
2
2
|
export const layoutBigQuote = (data) => {
|
|
3
3
|
const accent = resolveAccent(data.accentColor);
|
|
4
4
|
const eyebrowHtml = renderEyebrow(data.eyebrow, accent);
|
|
@@ -7,15 +7,15 @@ export const layoutBigQuote = (data) => {
|
|
|
7
7
|
if (eyebrowHtml)
|
|
8
8
|
parts.push(` <div class="mb-6">${eyebrowHtml}</div>`);
|
|
9
9
|
parts.push(` ${accentBar(accent, "w-24 mb-8")}`);
|
|
10
|
-
parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed">`);
|
|
10
|
+
parts.push(` <blockquote class="text-[32px] text-d-text font-title italic text-center leading-relaxed"${dp("quote")}>`);
|
|
11
11
|
parts.push(` “${renderInlineMarkup(data.quote)}”`);
|
|
12
12
|
parts.push(` </blockquote>`);
|
|
13
13
|
parts.push(` ${accentBar(accent, "w-24 mt-8 mb-6")}`);
|
|
14
14
|
if (data.author) {
|
|
15
|
-
parts.push(` <p class="text-lg text-d-muted font-body">${renderInlineMarkup(data.author)}</p>`);
|
|
15
|
+
parts.push(` <p class="text-lg text-d-muted font-body"${dp("author")}>${renderInlineMarkup(data.author)}</p>`);
|
|
16
16
|
}
|
|
17
17
|
if (data.role) {
|
|
18
|
-
parts.push(` <p class="text-sm text-d-dim font-body mt-1">${renderInlineMarkup(data.role)}</p>`);
|
|
18
|
+
parts.push(` <p class="text-sm text-d-dim font-body mt-1"${dp("role")}>${renderInlineMarkup(data.role)}</p>`);
|
|
19
19
|
}
|
|
20
20
|
parts.push(`</div>`);
|
|
21
21
|
return parts.join("\n");
|
package/lib/layouts/columns.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderOptionalCallout, resolveAccent, renderNumLabel } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, cardWrap, numBadge, iconSquare, slideHeader, renderOptionalCallout, resolveAccent, renderNumLabel, dp } from "../utils.js";
|
|
2
2
|
import { renderCardContentBlocks } from "../blocks.js";
|
|
3
|
-
const buildColumnCard = (col) => {
|
|
3
|
+
const buildColumnCard = (col, basePath) => {
|
|
4
4
|
const accent = resolveAccent(col.accentColor);
|
|
5
5
|
const inner = [];
|
|
6
|
+
const dpTitle = dp(`${basePath}.title`);
|
|
6
7
|
if (col.icon) {
|
|
7
8
|
inner.push(`<div class="flex flex-col items-center mb-3">`);
|
|
8
9
|
inner.push(` ${iconSquare(col.icon, accent)}`);
|
|
9
10
|
inner.push(`</div>`);
|
|
10
|
-
inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${renderInlineMarkup(col.title)}</h3>`);
|
|
11
|
+
inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body"${dpTitle}>${renderInlineMarkup(col.title)}</h3>`);
|
|
11
12
|
}
|
|
12
13
|
else if (col.num != null) {
|
|
13
14
|
inner.push(`<div class="flex items-center gap-3 mb-1">`);
|
|
14
15
|
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(` <h3 class="text-lg font-bold text-d-text font-body"${dpTitle}>${renderInlineMarkup(col.title)}</h3>`);
|
|
16
17
|
inner.push(`</div>`);
|
|
17
18
|
}
|
|
18
19
|
else {
|
|
19
20
|
if (col.label) {
|
|
20
|
-
inner.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(col.label)}</p>`);
|
|
21
|
+
inner.push(`<p class="text-sm font-bold text-${c(accent)} font-body"${dp(`${basePath}.label`)}>${renderInlineMarkup(col.label)}</p>`);
|
|
21
22
|
}
|
|
22
23
|
const numPrefix = renderNumLabel(col.numLabel, accent);
|
|
23
|
-
inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1">${numPrefix}${renderInlineMarkup(col.title)}</h3>`);
|
|
24
|
+
inner.push(`<h3 class="text-2xl font-title font-bold text-d-text mt-1"${dpTitle}>${numPrefix}${renderInlineMarkup(col.title)}</h3>`);
|
|
24
25
|
}
|
|
25
26
|
if (col.content) {
|
|
26
27
|
const centerCls = col.icon ? "text-center" : "";
|
|
27
28
|
inner.push(`<div class="mt-4 space-y-4 flex-1 min-h-0 overflow-auto flex flex-col ${centerCls}">`);
|
|
28
|
-
inner.push(renderCardContentBlocks(col.content));
|
|
29
|
+
inner.push(renderCardContentBlocks(col.content, `${basePath}.content`));
|
|
29
30
|
inner.push(`</div>`);
|
|
30
31
|
}
|
|
31
32
|
if (col.footer) {
|
|
32
|
-
inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(col.footer)}</p>`);
|
|
33
|
+
inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3"${dp(`${basePath}.footer`)}>${renderInlineMarkup(col.footer)}</p>`);
|
|
33
34
|
}
|
|
34
35
|
return cardWrap(accent, inner.join("\n"), "flex-1");
|
|
35
36
|
};
|
|
@@ -38,7 +39,7 @@ export const layoutColumns = (data) => {
|
|
|
38
39
|
const parts = [slideHeader(data)];
|
|
39
40
|
const colElements = [];
|
|
40
41
|
cols.forEach((col, i) => {
|
|
41
|
-
colElements.push(buildColumnCard(col));
|
|
42
|
+
colElements.push(buildColumnCard(col, `columns[${i}]`));
|
|
42
43
|
if (data.showArrows && i < cols.length - 1) {
|
|
43
44
|
colElements.push(`<div class="flex items-center shrink-0"><span class="text-2xl text-d-dim">\u25B6</span></div>`);
|
|
44
45
|
}
|
|
@@ -48,7 +49,7 @@ export const layoutColumns = (data) => {
|
|
|
48
49
|
parts.push(`</div>`);
|
|
49
50
|
parts.push(renderOptionalCallout(data.callout));
|
|
50
51
|
if (data.bottomText) {
|
|
51
|
-
parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4">${renderInlineMarkup(data.bottomText)}</p>`);
|
|
52
|
+
parts.push(`<p class="text-center text-sm text-d-dim font-body pb-4"${dp("bottomText")}>${renderInlineMarkup(data.bottomText)}</p>`);
|
|
52
53
|
}
|
|
53
54
|
return parts.join("\n");
|
|
54
55
|
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, accentBar, slideHeader, renderOptionalCallout, resolveAccent } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, accentBar, slideHeader, renderOptionalCallout, resolveAccent, dp } from "../utils.js";
|
|
2
2
|
import { renderContentBlocks } from "../blocks.js";
|
|
3
|
-
const buildPanel = (panel) => {
|
|
3
|
+
const buildPanel = (panel, basePath) => {
|
|
4
4
|
const accent = resolveAccent(panel.accentColor);
|
|
5
5
|
const inner = [];
|
|
6
|
-
inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body">${renderInlineMarkup(panel.title)}</h3>`);
|
|
6
|
+
inner.push(`<h3 class="text-xl font-bold text-${c(accent)} font-body"${dp(`${basePath}.title`)}>${renderInlineMarkup(panel.title)}</h3>`);
|
|
7
7
|
if (panel.content) {
|
|
8
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));
|
|
9
|
+
inner.push(renderContentBlocks(panel.content, `${basePath}.content`));
|
|
10
10
|
inner.push(`</div>`);
|
|
11
11
|
}
|
|
12
12
|
if (panel.footer) {
|
|
13
|
-
inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3">${renderInlineMarkup(panel.footer)}</p>`);
|
|
13
|
+
inner.push(`<p class="text-sm text-d-dim font-body mt-auto pt-3"${dp(`${basePath}.footer`)}>${renderInlineMarkup(panel.footer)}</p>`);
|
|
14
14
|
}
|
|
15
15
|
// Tailwind's arbitrary `flex-[1.5]` works at runtime but stops short of clean class sanitization;
|
|
16
16
|
// emitting `flex-grow` inline keeps the output predictable and avoids depending on JIT mode.
|
|
@@ -33,8 +33,8 @@ ${inner.join("\n")}
|
|
|
33
33
|
export const layoutComparison = (data) => {
|
|
34
34
|
const parts = [slideHeader(data)];
|
|
35
35
|
parts.push(`<div class="flex gap-5 px-12 mt-5 flex-1 min-h-0 items-start">`);
|
|
36
|
-
parts.push(buildPanel(data.left));
|
|
37
|
-
parts.push(buildPanel(data.right));
|
|
36
|
+
parts.push(buildPanel(data.left, "left"));
|
|
37
|
+
parts.push(buildPanel(data.right, "right"));
|
|
38
38
|
parts.push(`</div>`);
|
|
39
39
|
parts.push(renderOptionalCallout(data.callout));
|
|
40
40
|
return parts.join("\n");
|
package/lib/layouts/funnel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor, dp } from "../utils.js";
|
|
2
2
|
export const layoutFunnel = (data) => {
|
|
3
3
|
const parts = [slideHeader(data)];
|
|
4
4
|
const stages = data.stages || [];
|
|
@@ -7,15 +7,16 @@ export const layoutFunnel = (data) => {
|
|
|
7
7
|
stages.forEach((stage, i) => {
|
|
8
8
|
const color = resolveItemColor(stage.color, data.accentColor);
|
|
9
9
|
const widthPct = 100 - (i / Math.max(total - 1, 1)) * 55;
|
|
10
|
+
const base = `stages[${i}]`;
|
|
10
11
|
parts.push(`<div class="bg-${c(color)} rounded-lg flex items-center justify-between px-6 py-4" style="width: ${widthPct}%">`);
|
|
11
12
|
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
|
+
parts.push(` <span class="text-base font-bold text-white font-body"${dp(`${base}.label`)}>${renderInlineMarkup(stage.label)}</span>`);
|
|
13
14
|
if (stage.description) {
|
|
14
|
-
parts.push(` <span class="text-sm text-white/70 font-body">${renderInlineMarkup(stage.description)}</span>`);
|
|
15
|
+
parts.push(` <span class="text-sm text-white/70 font-body"${dp(`${base}.description`)}>${renderInlineMarkup(stage.description)}</span>`);
|
|
15
16
|
}
|
|
16
17
|
parts.push(` </div>`);
|
|
17
18
|
if (stage.value) {
|
|
18
|
-
parts.push(` <span class="text-lg font-bold text-white font-body">${renderInlineMarkup(stage.value)}</span>`);
|
|
19
|
+
parts.push(` <span class="text-lg font-bold text-white font-body"${dp(`${base}.value`)}>${renderInlineMarkup(stage.value)}</span>`);
|
|
19
20
|
}
|
|
20
21
|
parts.push(`</div>`);
|
|
21
22
|
});
|
package/lib/layouts/grid.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import { renderInlineMarkup, cardWrap, numBadge, iconSquare, slideHeader, resolveAccent } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, cardWrap, numBadge, iconSquare, slideHeader, resolveAccent, dp } from "../utils.js";
|
|
2
2
|
import { renderCardContentBlocks } from "../blocks.js";
|
|
3
3
|
export const layoutGrid = (data) => {
|
|
4
4
|
const nCols = data.gridColumns || 3;
|
|
5
5
|
const parts = [slideHeader(data)];
|
|
6
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) => {
|
|
7
|
+
(data.items || []).forEach((item, i) => {
|
|
8
8
|
const itemAccent = resolveAccent(item.accentColor);
|
|
9
9
|
const inner = [];
|
|
10
|
+
const base = `items[${i}]`;
|
|
11
|
+
const dpTitle = dp(`${base}.title`);
|
|
10
12
|
if (item.icon) {
|
|
11
13
|
inner.push(`<div class="flex flex-col items-center mb-2">`);
|
|
12
14
|
inner.push(` ${iconSquare(item.icon, itemAccent)}`);
|
|
13
15
|
inner.push(`</div>`);
|
|
14
|
-
inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body">${renderInlineMarkup(item.title)}</h3>`);
|
|
16
|
+
inner.push(`<h3 class="text-lg font-bold text-d-text text-center font-body"${dpTitle}>${renderInlineMarkup(item.title)}</h3>`);
|
|
15
17
|
}
|
|
16
18
|
else if (item.num != null) {
|
|
17
19
|
inner.push(`<div class="flex items-center gap-3">`);
|
|
18
20
|
inner.push(` ${numBadge(item.num, itemAccent)}`);
|
|
19
|
-
inner.push(` <h3 class="text-sm font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
|
|
21
|
+
inner.push(` <h3 class="text-sm font-bold text-d-text font-body"${dpTitle}>${renderInlineMarkup(item.title)}</h3>`);
|
|
20
22
|
inner.push(`</div>`);
|
|
21
23
|
}
|
|
22
24
|
else {
|
|
23
|
-
inner.push(`<h3 class="text-lg font-bold text-d-text font-body">${renderInlineMarkup(item.title)}</h3>`);
|
|
25
|
+
inner.push(`<h3 class="text-lg font-bold text-d-text font-body"${dpTitle}>${renderInlineMarkup(item.title)}</h3>`);
|
|
24
26
|
}
|
|
25
27
|
if (item.description) {
|
|
26
|
-
inner.push(`<p class="text-sm text-d-muted font-body mt-3">${renderInlineMarkup(item.description)}</p>`);
|
|
28
|
+
inner.push(`<p class="text-sm text-d-muted font-body mt-3"${dp(`${base}.description`)}>${renderInlineMarkup(item.description)}</p>`);
|
|
27
29
|
}
|
|
28
30
|
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>`);
|
|
31
|
+
inner.push(`<div class="mt-3 space-y-3 flex-1 min-h-0 overflow-hidden flex flex-col">${renderCardContentBlocks(item.content, `${base}.content`)}</div>`);
|
|
30
32
|
}
|
|
31
33
|
// Asymmetric grids: items can span multiple columns. Class names are mapped explicitly so the JIT compiler keeps them.
|
|
32
34
|
const SPAN_CLS = { 1: "", 2: "col-span-2", 3: "col-span-3", 4: "col-span-4" };
|
|
@@ -35,7 +37,7 @@ export const layoutGrid = (data) => {
|
|
|
35
37
|
});
|
|
36
38
|
parts.push(`</div>`);
|
|
37
39
|
if (data.footer) {
|
|
38
|
-
parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3">${renderInlineMarkup(data.footer)}</p>`);
|
|
40
|
+
parts.push(`<p class="text-xs text-d-dim font-body px-12 pb-3"${dp("footer")}>${renderInlineMarkup(data.footer)}</p>`);
|
|
39
41
|
}
|
|
40
42
|
return parts.join("\n");
|
|
41
43
|
};
|
package/lib/layouts/manifesto.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor } from "../utils.js";
|
|
2
|
-
const buildManifestoCard = (line, slideAccent) => {
|
|
1
|
+
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, resolveItemColor, dp } from "../utils.js";
|
|
2
|
+
const buildManifestoCard = (line, slideAccent, basePath) => {
|
|
3
3
|
const color = resolveItemColor(line.accentColor, slideAccent);
|
|
4
4
|
const parts = [];
|
|
5
5
|
parts.push(`<div class="relative bg-d-card rounded-lg shadow-md overflow-hidden flex flex-col">`);
|
|
6
6
|
parts.push(` <div class="absolute left-0 top-0 bottom-0 w-1 bg-${c(color)}"></div>`);
|
|
7
7
|
parts.push(` <div class="px-5 py-4 pl-6 flex-1">`);
|
|
8
|
-
parts.push(` <h3 class="text-lg font-bold text-d-text font-body leading-snug">${renderInlineMarkup(line.title)}</h3>`);
|
|
8
|
+
parts.push(` <h3 class="text-lg font-bold text-d-text font-body leading-snug"${dp(`${basePath}.title`)}>${renderInlineMarkup(line.title)}</h3>`);
|
|
9
9
|
if (line.description) {
|
|
10
|
-
parts.push(` <p class="text-sm text-d-muted font-body mt-1 leading-relaxed">${renderInlineMarkup(line.description)}</p>`);
|
|
10
|
+
parts.push(` <p class="text-sm text-d-muted font-body mt-1 leading-relaxed"${dp(`${basePath}.description`)}>${renderInlineMarkup(line.description)}</p>`);
|
|
11
11
|
}
|
|
12
12
|
parts.push(` </div>`);
|
|
13
13
|
parts.push(`</div>`);
|
|
@@ -30,8 +30,8 @@ export const layoutManifesto = (data) => {
|
|
|
30
30
|
const items = data.items || [];
|
|
31
31
|
const parts = [slideHeader(data)];
|
|
32
32
|
parts.push(`<div class="grid ${gridColsClass(cols)} gap-4 px-12 mt-5 flex-1 min-h-0 content-start">`);
|
|
33
|
-
items.forEach((line) => {
|
|
34
|
-
parts.push(buildManifestoCard(line, data.accentColor));
|
|
33
|
+
items.forEach((line, i) => {
|
|
34
|
+
parts.push(buildManifestoCard(line, data.accentColor, `items[${i}]`));
|
|
35
35
|
});
|
|
36
36
|
parts.push(`</div>`);
|
|
37
37
|
parts.push(renderOptionalCallout(data.callout));
|
package/lib/layouts/matrix.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, cardWrap, slideHeader, resolveAccent } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, cardWrap, slideHeader, resolveAccent, dp } from "../utils.js";
|
|
2
2
|
import { renderContentBlocks } from "../blocks.js";
|
|
3
3
|
export const layoutMatrix = (data) => {
|
|
4
4
|
const parts = [slideHeader(data)];
|
|
@@ -22,17 +22,18 @@ export const layoutMatrix = (data) => {
|
|
|
22
22
|
const idx = r * cols + ci;
|
|
23
23
|
const cell = cells[idx] || { label: "" };
|
|
24
24
|
const accent = resolveAccent(cell.accentColor);
|
|
25
|
+
const base = `cells[${idx}]`;
|
|
25
26
|
const inner = [];
|
|
26
|
-
inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body">${renderInlineMarkup(cell.label)}</h3>`);
|
|
27
|
+
inner.push(`<h3 class="text-lg font-bold text-${c(accent)} font-body"${dp(`${base}.label`)}>${renderInlineMarkup(cell.label)}</h3>`);
|
|
27
28
|
if (cell.items) {
|
|
28
29
|
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>${renderInlineMarkup(item)}</span></li>`);
|
|
30
|
+
cell.items.forEach((item, ii) => {
|
|
31
|
+
inner.push(` <li class="flex gap-2"><span class="text-d-dim shrink-0">•</span><span${dp(`${base}.items[${ii}]`)}>${renderInlineMarkup(item)}</span></li>`);
|
|
31
32
|
});
|
|
32
33
|
inner.push(`</ul>`);
|
|
33
34
|
}
|
|
34
35
|
if (cell.content) {
|
|
35
|
-
inner.push(`<div class="mt-2 space-y-2">${renderContentBlocks(cell.content)}</div>`);
|
|
36
|
+
inner.push(`<div class="mt-2 space-y-2">${renderContentBlocks(cell.content, `${base}.content`)}</div>`);
|
|
36
37
|
}
|
|
37
38
|
parts.push(cardWrap(accent, inner.join("\n"), "flex-1"));
|
|
38
39
|
});
|
package/lib/layouts/split.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, accentBar, resolveAccent } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, accentBar, resolveAccent, dp } from "../utils.js";
|
|
2
2
|
import { renderContentBlocks } from "../blocks.js";
|
|
3
3
|
const resolveValign = (valign) => {
|
|
4
4
|
if (valign === "top")
|
|
@@ -7,7 +7,7 @@ const resolveValign = (valign) => {
|
|
|
7
7
|
return "justify-end";
|
|
8
8
|
return "justify-center";
|
|
9
9
|
};
|
|
10
|
-
const buildSplitPanel = (panel, fallbackAccent, ratio) => {
|
|
10
|
+
const buildSplitPanel = (panel, fallbackAccent, ratio, basePath) => {
|
|
11
11
|
const accent = panel.accentColor || fallbackAccent;
|
|
12
12
|
const bg = panel.dark ? "bg-d-card" : "";
|
|
13
13
|
const vCls = resolveValign(panel.valign);
|
|
@@ -15,20 +15,20 @@ const buildSplitPanel = (panel, fallbackAccent, ratio) => {
|
|
|
15
15
|
lines.push(`<div class="${bg} flex flex-col ${vCls} px-10 py-8" style="flex: ${ratio}">`);
|
|
16
16
|
if (panel.label) {
|
|
17
17
|
if (panel.labelBadge) {
|
|
18
|
-
lines.push(` <span class="inline-block self-start px-6 py-2.5 rounded-lg bg-${c(accent)} text-lg font-bold text-white font-title mb-4">${renderInlineMarkup(panel.label)}</span>`);
|
|
18
|
+
lines.push(` <span class="inline-block self-start px-6 py-2.5 rounded-lg bg-${c(accent)} text-lg font-bold text-white font-title mb-4"${dp(`${basePath}.label`)}>${renderInlineMarkup(panel.label)}</span>`);
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
|
-
lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body mb-2">${renderInlineMarkup(panel.label)}</p>`);
|
|
21
|
+
lines.push(` <p class="text-sm font-bold text-${c(accent)} font-body mb-2"${dp(`${basePath}.label`)}>${renderInlineMarkup(panel.label)}</p>`);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
if (panel.title) {
|
|
25
|
-
lines.push(` <h2 class="text-[36px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(panel.title)}</h2>`);
|
|
25
|
+
lines.push(` <h2 class="text-[36px] leading-tight font-title font-bold text-d-text"${dp(`${basePath}.title`)}>${renderInlineMarkup(panel.title)}</h2>`);
|
|
26
26
|
}
|
|
27
27
|
if (panel.subtitle) {
|
|
28
|
-
lines.push(` <p class="text-base text-d-dim font-body mt-3">${renderInlineMarkup(panel.subtitle)}</p>`);
|
|
28
|
+
lines.push(` <p class="text-base text-d-dim font-body mt-3"${dp(`${basePath}.subtitle`)}>${renderInlineMarkup(panel.subtitle)}</p>`);
|
|
29
29
|
}
|
|
30
30
|
if (panel.content) {
|
|
31
|
-
lines.push(` <div class="mt-6 space-y-3">${renderContentBlocks(panel.content)}</div>`);
|
|
31
|
+
lines.push(` <div class="mt-6 space-y-3">${renderContentBlocks(panel.content, `${basePath}.content`)}</div>`);
|
|
32
32
|
}
|
|
33
33
|
lines.push(`</div>`);
|
|
34
34
|
return lines.join("\n");
|
|
@@ -41,10 +41,10 @@ export const layoutSplit = (data) => {
|
|
|
41
41
|
const rightRatio = data.right?.ratio || 50;
|
|
42
42
|
parts.push(`<div class="flex h-full">`);
|
|
43
43
|
if (data.left) {
|
|
44
|
-
parts.push(buildSplitPanel(data.left, accent, leftRatio));
|
|
44
|
+
parts.push(buildSplitPanel(data.left, accent, leftRatio, "left"));
|
|
45
45
|
}
|
|
46
46
|
if (data.right) {
|
|
47
|
-
parts.push(buildSplitPanel(data.right, accent, rightRatio));
|
|
47
|
+
parts.push(buildSplitPanel(data.right, accent, rightRatio, "right"));
|
|
48
48
|
}
|
|
49
49
|
parts.push(`</div>`);
|
|
50
50
|
return parts.join("\n");
|
package/lib/layouts/stats.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, resolveItemColor, resolveChangeColor, centeredSlideHeader, renderOptionalCallout } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, resolveItemColor, resolveChangeColor, centeredSlideHeader, renderOptionalCallout, dp } from "../utils.js";
|
|
2
2
|
export const layoutStats = (data) => {
|
|
3
3
|
const stats = data.stats || [];
|
|
4
4
|
const parts = [];
|
|
5
5
|
parts.push(centeredSlideHeader(data));
|
|
6
6
|
// Stats cards
|
|
7
7
|
parts.push(`<div class="flex gap-6 mt-10">`);
|
|
8
|
-
stats.forEach((stat) => {
|
|
8
|
+
stats.forEach((stat, i) => {
|
|
9
9
|
const color = resolveItemColor(stat.color, data.accentColor);
|
|
10
|
+
const base = `stats[${i}]`;
|
|
10
11
|
parts.push(`<div class="flex-1 bg-d-card rounded-lg shadow-lg p-10 text-center">`);
|
|
11
12
|
parts.push(` <div class="h-[3px] bg-${c(color)} rounded-full w-12 mx-auto mb-6"></div>`);
|
|
12
13
|
if (stat.numLabel) {
|
|
13
|
-
parts.push(` <p class="font-accent font-extrabold text-${c(color)} text-sm tracking-wider mb-2">${renderInlineMarkup(stat.numLabel)}</p>`);
|
|
14
|
+
parts.push(` <p class="font-accent font-extrabold text-${c(color)} text-sm tracking-wider mb-2"${dp(`${base}.numLabel`)}>${renderInlineMarkup(stat.numLabel)}</p>`);
|
|
14
15
|
}
|
|
15
|
-
parts.push(` <p class="text-[52px] font-bold text-${c(color)} font-body leading-none">${renderInlineMarkup(stat.value)}</p>`);
|
|
16
|
-
parts.push(` <p class="text-lg text-d-muted font-body mt-4">${renderInlineMarkup(stat.label)}</p>`);
|
|
16
|
+
parts.push(` <p class="text-[52px] font-bold text-${c(color)} font-body leading-none"${dp(`${base}.value`)}>${renderInlineMarkup(stat.value)}</p>`);
|
|
17
|
+
parts.push(` <p class="text-lg text-d-muted font-body mt-4"${dp(`${base}.label`)}>${renderInlineMarkup(stat.label)}</p>`);
|
|
17
18
|
if (stat.change) {
|
|
18
|
-
parts.push(` <p class="text-base font-bold text-${c(resolveChangeColor(stat.change))} font-body mt-3">${renderInlineMarkup(stat.change)}</p>`);
|
|
19
|
+
parts.push(` <p class="text-base font-bold text-${c(resolveChangeColor(stat.change))} font-body mt-3"${dp(`${base}.change`)}>${renderInlineMarkup(stat.change)}</p>`);
|
|
19
20
|
}
|
|
20
21
|
parts.push(`</div>`);
|
|
21
22
|
});
|
package/lib/layouts/timeline.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, resolveItemColor, centeredSlideHeader } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, resolveItemColor, centeredSlideHeader, dp } from "../utils.js";
|
|
2
2
|
export const layoutTimeline = (data) => {
|
|
3
3
|
const parts = [];
|
|
4
4
|
const items = data.items || [];
|
|
@@ -6,21 +6,22 @@ export const layoutTimeline = (data) => {
|
|
|
6
6
|
// Timeline items
|
|
7
7
|
parts.push(`<div class="flex items-start mt-10 relative">`);
|
|
8
8
|
parts.push(`<div class="absolute left-4 right-4 top-[52px] h-[2px] bg-d-alt"></div>`);
|
|
9
|
-
items.forEach((item) => {
|
|
9
|
+
items.forEach((item, i) => {
|
|
10
10
|
const baseColor = resolveItemColor(item.color, data.accentColor);
|
|
11
11
|
// 'hot' items are emphasized — keep the configured color when set, else fall back to warning (amber) for visibility.
|
|
12
12
|
const color = item.hot ? item.color || "warning" : baseColor;
|
|
13
13
|
const dotBorder = item.done ? `bg-${c(color)}` : `bg-d-alt`;
|
|
14
14
|
const dotInner = item.done ? "bg-d-text" : `bg-${c(color)}`;
|
|
15
15
|
const hotRing = item.hot ? ` ring-2 ring-${c(color)} ring-offset-2 ring-offset-d-bg` : "";
|
|
16
|
+
const base = `items[${i}]`;
|
|
16
17
|
parts.push(`<div class="flex-1 flex flex-col items-center text-center relative z-10">`);
|
|
17
18
|
parts.push(` <div class="w-10 h-10 rounded-full ${dotBorder} flex items-center justify-center shadow-lg${hotRing}">`);
|
|
18
19
|
parts.push(` <div class="w-4 h-4 rounded-full ${dotInner}"></div>`);
|
|
19
20
|
parts.push(` </div>`);
|
|
20
|
-
parts.push(` <p class="text-sm font-bold text-${c(color)} font-body mt-4">${renderInlineMarkup(item.date)}</p>`);
|
|
21
|
-
parts.push(` <p class="text-base font-bold text-d-text font-body mt-2">${renderInlineMarkup(item.title)}</p>`);
|
|
21
|
+
parts.push(` <p class="text-sm font-bold text-${c(color)} font-body mt-4"${dp(`${base}.date`)}>${renderInlineMarkup(item.date)}</p>`);
|
|
22
|
+
parts.push(` <p class="text-base font-bold text-d-text font-body mt-2"${dp(`${base}.title`)}>${renderInlineMarkup(item.title)}</p>`);
|
|
22
23
|
if (item.description) {
|
|
23
|
-
parts.push(` <p class="text-sm text-d-muted font-body mt-1 px-3">${renderInlineMarkup(item.description)}</p>`);
|
|
24
|
+
parts.push(` <p class="text-sm text-d-muted font-body mt-1 px-3"${dp(`${base}.description`)}>${renderInlineMarkup(item.description)}</p>`);
|
|
24
25
|
}
|
|
25
26
|
parts.push(`</div>`);
|
|
26
27
|
});
|
package/lib/layouts/title.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, accentBar, renderEyebrow, renderChipRow, resolveAccent } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, accentBar, renderEyebrow, renderChipRow, resolveAccent, dp } from "../utils.js";
|
|
2
2
|
/**
|
|
3
3
|
* h1 font-size for the title layout, keyed by titleSize variant.
|
|
4
4
|
* Tuned to match reveal.js scale (base 30px × multiplier): default ≈ 2em, hero ≈ 2.5em.
|
|
@@ -20,11 +20,11 @@ export const layoutTitle = (data) => {
|
|
|
20
20
|
`<div class="absolute -bottom-12 -left-16 w-[280px] h-[280px] rounded-full bg-d-accent opacity-10"></div>`,
|
|
21
21
|
`<div class="flex flex-col justify-center h-full px-16 relative z-10">`,
|
|
22
22
|
eyebrowHtml ? ` <div class="mb-4">${eyebrowHtml}</div>` : "",
|
|
23
|
-
` <h1 class="${titleCls} leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h1>`,
|
|
24
|
-
data.subtitle ? ` <p class="text-2xl text-d-muted mt-6 font-body">${renderInlineMarkup(data.subtitle)}</p>` : "",
|
|
25
|
-
data.author ? ` <p class="text-lg text-d-dim mt-10 font-body">${renderInlineMarkup(data.author)}</p>` : "",
|
|
23
|
+
` <h1 class="${titleCls} leading-tight font-title font-bold text-d-text"${dp("title")}>${renderInlineMarkup(data.title)}</h1>`,
|
|
24
|
+
data.subtitle ? ` <p class="text-2xl text-d-muted mt-6 font-body"${dp("subtitle")}>${renderInlineMarkup(data.subtitle)}</p>` : "",
|
|
25
|
+
data.author ? ` <p class="text-lg text-d-dim mt-10 font-body"${dp("author")}>${renderInlineMarkup(data.author)}</p>` : "",
|
|
26
26
|
data.note
|
|
27
|
-
? ` <div class="bg-d-card px-4 py-2 mt-6 inline-block rounded"><p class="text-sm text-d-dim font-body">${renderInlineMarkup(data.note)}</p></div>`
|
|
27
|
+
? ` <div class="bg-d-card px-4 py-2 mt-6 inline-block rounded"><p class="text-sm text-d-dim font-body"${dp("note")}>${renderInlineMarkup(data.note)}</p></div>`
|
|
28
28
|
: "",
|
|
29
29
|
chipsHtml,
|
|
30
30
|
`</div>`,
|
package/lib/layouts/waterfall.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout } from "../utils.js";
|
|
1
|
+
import { renderInlineMarkup, c, slideHeader, renderOptionalCallout, dp } from "../utils.js";
|
|
2
2
|
/** Height of the chart area as percentage of available space */
|
|
3
3
|
const CHART_HEIGHT_PCT = 75;
|
|
4
4
|
export const layoutWaterfall = (data) => {
|
|
@@ -25,7 +25,7 @@ export const layoutWaterfall = (data) => {
|
|
|
25
25
|
// Bar (absolute positioned from bottom)
|
|
26
26
|
parts.push(` <div class="absolute left-1 right-1 bg-${c(color)} rounded-t" style="bottom: ${bottomPct}%; height: ${heightPct}%"></div>`);
|
|
27
27
|
// Bottom label
|
|
28
|
-
parts.push(` <p class="absolute bottom-0 w-full text-xs text-d-muted font-body text-center" style="transform: translateY(100%)">${renderInlineMarkup(item.label)}</p>`);
|
|
28
|
+
parts.push(` <p class="absolute bottom-0 w-full text-xs text-d-muted font-body text-center" style="transform: translateY(100%)"${dp(`items[${i}].label`)}>${renderInlineMarkup(item.label)}</p>`);
|
|
29
29
|
parts.push(`</div>`);
|
|
30
30
|
});
|
|
31
31
|
parts.push(`</div>`);
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { SlideTheme, SlideLayout, CalloutBar } from "./schema.js";
|
|
2
2
|
/** Escape HTML special characters */
|
|
3
3
|
export declare const escapeHtml: (s: string) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Emit a `data-mulmo-path="..."` attribute (with leading space) for an editable text node.
|
|
6
|
+
* Consumers like @mulmocast/deck-web use this to map clicks in the rendered HTML back to
|
|
7
|
+
* the source SlideLayout JSON path (e.g. `title`, `stats[0].value`, `columns[0].content[1].text`).
|
|
8
|
+
*
|
|
9
|
+
* Empty path → empty string, so callers can pass `dp(path)` unconditionally.
|
|
10
|
+
*/
|
|
11
|
+
export declare const dp: (path: string) => string;
|
|
12
|
+
/** Compose a child path: `dpJoin("columns[0]", "title")` → `columns[0].title`. */
|
|
13
|
+
export declare const dpJoin: (base: string, segment: string) => string;
|
|
4
14
|
/** Escape HTML and convert newlines to <br> */
|
|
5
15
|
export declare const nl2br: (s: string) => string;
|
|
6
16
|
/**
|
|
@@ -60,9 +70,9 @@ export declare const renderCalloutBar: (obj: {
|
|
|
60
70
|
export declare const renderEyebrow: (eyebrow: {
|
|
61
71
|
label: string;
|
|
62
72
|
color?: string;
|
|
63
|
-
} | undefined, defaultColor?: string) => string;
|
|
73
|
+
} | undefined, defaultColor?: string, basePath?: string) => string;
|
|
64
74
|
/** Render a chip-row (small bordered pill badges, e.g. tags below a title). Empty / undefined input renders nothing. */
|
|
65
|
-
export declare const renderChipRow: (chips: string[] | undefined) => string;
|
|
75
|
+
export declare const renderChipRow: (chips: string[] | undefined, basePath?: string) => string;
|
|
66
76
|
/** Render an accent-colored typographic prefix (e.g. "01") to be placed before a card / stat title. */
|
|
67
77
|
export declare const renderNumLabel: (label: string | undefined, colorKey?: string) => string;
|
|
68
78
|
/** Render header text elements (stepLabel + title + subtitle) without wrapping div */
|
|
@@ -77,7 +87,7 @@ export declare const renderHeaderText: (data: {
|
|
|
77
87
|
};
|
|
78
88
|
titleSize?: "small" | "default" | "large" | "hero";
|
|
79
89
|
subtitleSize?: "default" | "big" | "lead";
|
|
80
|
-
}) => string;
|
|
90
|
+
}, basePath?: string) => string;
|
|
81
91
|
/** Render the common slide header (accent bar + title + subtitle, plus optional eyebrow pill) */
|
|
82
92
|
export declare const slideHeader: (data: {
|
|
83
93
|
accentColor?: string;
|
package/lib/utils.js
CHANGED
|
@@ -7,6 +7,16 @@ export const escapeHtml = (s) => {
|
|
|
7
7
|
.replace(/"/g, """)
|
|
8
8
|
.replace(/'/g, "'");
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Emit a `data-mulmo-path="..."` attribute (with leading space) for an editable text node.
|
|
12
|
+
* Consumers like @mulmocast/deck-web use this to map clicks in the rendered HTML back to
|
|
13
|
+
* the source SlideLayout JSON path (e.g. `title`, `stats[0].value`, `columns[0].content[1].text`).
|
|
14
|
+
*
|
|
15
|
+
* Empty path → empty string, so callers can pass `dp(path)` unconditionally.
|
|
16
|
+
*/
|
|
17
|
+
export const dp = (path) => (path ? ` data-mulmo-path="${escapeHtml(path)}"` : "");
|
|
18
|
+
/** Compose a child path: `dpJoin("columns[0]", "title")` → `columns[0].title`. */
|
|
19
|
+
export const dpJoin = (base, segment) => (base ? `${base}.${segment}` : segment);
|
|
10
20
|
/** Escape HTML and convert newlines to <br> */
|
|
11
21
|
export const nl2br = (s) => {
|
|
12
22
|
return escapeHtml(s).replace(/\n/g, "<br>");
|
|
@@ -167,18 +177,18 @@ export const renderCalloutBar = (obj) => {
|
|
|
167
177
|
* Render an eyebrow pill (small uppercase letter-spaced category label).
|
|
168
178
|
* Renders nothing when `eyebrow` is undefined, so callers can pass through unconditionally.
|
|
169
179
|
*/
|
|
170
|
-
export const renderEyebrow = (eyebrow, defaultColor) => {
|
|
180
|
+
export const renderEyebrow = (eyebrow, defaultColor, basePath = "") => {
|
|
171
181
|
if (!eyebrow)
|
|
172
182
|
return "";
|
|
173
183
|
const color = c(eyebrow.color ?? defaultColor ?? "primary");
|
|
174
|
-
return `<span class="inline-flex items-center gap-2 font-accent font-extrabold uppercase tracking-[0.16em] text-[12px] px-3 py-1 rounded-full border border-d-textDim/30 bg-${color}/10 text-${color}">${renderInlineMarkup(eyebrow.label)}</span>`;
|
|
184
|
+
return `<span class="inline-flex items-center gap-2 font-accent font-extrabold uppercase tracking-[0.16em] text-[12px] px-3 py-1 rounded-full border border-d-textDim/30 bg-${color}/10 text-${color}"${dp(dpJoin(basePath, "eyebrow.label"))}>${renderInlineMarkup(eyebrow.label)}</span>`;
|
|
175
185
|
};
|
|
176
186
|
/** Render a chip-row (small bordered pill badges, e.g. tags below a title). Empty / undefined input renders nothing. */
|
|
177
|
-
export const renderChipRow = (chips) => {
|
|
187
|
+
export const renderChipRow = (chips, basePath = "") => {
|
|
178
188
|
if (!chips || chips.length === 0)
|
|
179
189
|
return "";
|
|
180
190
|
const items = chips
|
|
181
|
-
.map((label) => `<span class="text-sm px-3 py-1.5 rounded-full border border-d-textDim/30 bg-d-card/40 text-d-text">${renderInlineMarkup(label)}</span>`)
|
|
191
|
+
.map((label, i) => `<span class="text-sm px-3 py-1.5 rounded-full border border-d-textDim/30 bg-d-card/40 text-d-text"${dp(dpJoin(basePath, `chips[${i}]`))}>${renderInlineMarkup(label)}</span>`)
|
|
182
192
|
.join("");
|
|
183
193
|
return `<div class="flex gap-2 flex-wrap mt-4">${items}</div>`;
|
|
184
194
|
};
|
|
@@ -203,20 +213,20 @@ const SUBTITLE_SIZE_CLS = {
|
|
|
203
213
|
big: "text-[22px]",
|
|
204
214
|
};
|
|
205
215
|
/** Render header text elements (stepLabel + title + subtitle) without wrapping div */
|
|
206
|
-
export const renderHeaderText = (data) => {
|
|
216
|
+
export const renderHeaderText = (data, basePath = "") => {
|
|
207
217
|
const accent = resolveAccent(data.accentColor);
|
|
208
218
|
const lines = [];
|
|
209
|
-
const eyebrowHtml = renderEyebrow(data.eyebrow, accent);
|
|
219
|
+
const eyebrowHtml = renderEyebrow(data.eyebrow, accent, basePath);
|
|
210
220
|
if (eyebrowHtml)
|
|
211
221
|
lines.push(`<div class="mb-3">${eyebrowHtml}</div>`);
|
|
212
222
|
if (data.stepLabel) {
|
|
213
|
-
lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
|
|
223
|
+
lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body"${dp(dpJoin(basePath, "stepLabel"))}>${renderInlineMarkup(data.stepLabel)}</p>`);
|
|
214
224
|
}
|
|
215
225
|
const titleCls = TITLE_SIZE_CLS[data.titleSize ?? "default"];
|
|
216
|
-
lines.push(`<h2 class="${titleCls} leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
|
|
226
|
+
lines.push(`<h2 class="${titleCls} leading-tight font-title font-bold text-d-text"${dp(dpJoin(basePath, "title"))}>${renderInlineMarkup(data.title)}</h2>`);
|
|
217
227
|
if (data.subtitle) {
|
|
218
228
|
const subtitleCls = SUBTITLE_SIZE_CLS[data.subtitleSize ?? "default"];
|
|
219
|
-
lines.push(`<p class="${subtitleCls} text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
|
|
229
|
+
lines.push(`<p class="${subtitleCls} text-d-dim mt-2 font-body"${dp(dpJoin(basePath, "subtitle"))}>${renderInlineMarkup(data.subtitle)}</p>`);
|
|
220
230
|
}
|
|
221
231
|
return lines.join("\n");
|
|
222
232
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmocast/deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MulmoCast deck DSL: JSON-described semantic slide layouts (stats, comparison, timeline, ...) rendered to Tailwind-based HTML",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|