@mulmocast/deck 0.5.1 → 0.7.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 +41 -28
- package/lib/layouts/big_quote.js +4 -4
- package/lib/layouts/columns.js +12 -11
- package/lib/layouts/comparison.js +7 -7
- package/lib/layouts/funnel.js +5 -4
- package/lib/layouts/grid.js +11 -9
- package/lib/layouts/manifesto.js +7 -7
- package/lib/layouts/matrix.js +6 -5
- package/lib/layouts/split.js +9 -9
- package/lib/layouts/stats.js +8 -7
- package/lib/layouts/timeline.js +7 -6
- package/lib/layouts/title.js +5 -5
- package/lib/layouts/waterfall.js +2 -2
- package/lib/utils.d.ts +25 -5
- package/lib/utils.js +32 -12
- 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, di } 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,12 @@ 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
|
+
// The bullet `<li>` itself carries `data-mulmo-item-path` so editors can drag-reorder; the inner
|
|
195
|
+
// span keeps `data-mulmo-path` for the text edit target.
|
|
196
|
+
const itemRoot = path ? `${path}.items[${i}]` : "";
|
|
197
|
+
return ` <li class="flex flex-col gap-1${itemCls}"${di(itemRoot)}><div class="flex gap-2">${markerHtml}<span${dp(itemPath)}>${renderInlineMarkup(text)}</span></div>${subHtml}</li>`;
|
|
187
198
|
})
|
|
188
199
|
.join("\n");
|
|
189
200
|
return `<${tag} class="space-y-2 ${blockStyle.fontCls} ${blockStyle.colorCls} font-body">\n${items}\n</${tag}>`;
|
|
@@ -191,7 +202,7 @@ const renderBullets = (block) => {
|
|
|
191
202
|
const renderCode = (block) => {
|
|
192
203
|
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
204
|
};
|
|
194
|
-
const renderCallout = (block) => {
|
|
205
|
+
const renderCallout = (block, path = "") => {
|
|
195
206
|
const isQuote = block.style === "quote";
|
|
196
207
|
const resolveBorderCls = (style) => {
|
|
197
208
|
if (style === "warning")
|
|
@@ -205,18 +216,20 @@ const renderCallout = (block) => {
|
|
|
205
216
|
// Pre-Phase-4 default was `text-sm` (~14px). Map to the size variants only when explicitly requested.
|
|
206
217
|
const sizeStyle = block.size ? TEXT_SIZE_STYLES[block.size] : { fontCls: "text-sm", colorCls: "text-d-muted" };
|
|
207
218
|
const textCls = isQuote ? `italic ${sizeStyle.colorCls}` : sizeStyle.colorCls;
|
|
219
|
+
const dpLabel = dp(path ? `${path}.label` : "");
|
|
220
|
+
const dpText = dp(path ? `${path}.text` : "");
|
|
208
221
|
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>`;
|
|
222
|
+
? `<span class="font-bold text-${c(block.color || "warning")}"${dpLabel}>${renderInlineMarkup(block.label)}:</span> <span class="${textCls}"${dpText}>${renderInlineMarkup(block.text)}</span>`
|
|
223
|
+
: `<span class="${textCls}"${dpText}>${renderInlineMarkup(block.text)}</span>`;
|
|
211
224
|
return `<div class="${bg} ${borderCls} p-3 rounded ${sizeStyle.fontCls} font-body">${content}</div>`;
|
|
212
225
|
};
|
|
213
|
-
const renderMetric = (block) => {
|
|
226
|
+
const renderMetric = (block, path = "") => {
|
|
214
227
|
const lines = [];
|
|
215
228
|
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>`);
|
|
229
|
+
lines.push(` <p class="text-4xl font-bold text-${c(resolveAccent(block.color))}"${dp(path ? `${path}.value` : "")}>${renderInlineMarkup(block.value)}</p>`);
|
|
230
|
+
lines.push(` <p class="text-sm text-d-dim mt-1"${dp(path ? `${path}.label` : "")}>${renderInlineMarkup(block.label)}</p>`);
|
|
218
231
|
if (block.change) {
|
|
219
|
-
lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1">${escapeHtml(block.change)}</p>`);
|
|
232
|
+
lines.push(` <p class="text-sm font-bold text-${c(resolveChangeColor(block.change))} mt-1"${dp(path ? `${path}.change` : "")}>${escapeHtml(block.change)}</p>`);
|
|
220
233
|
}
|
|
221
234
|
lines.push(`</div>`);
|
|
222
235
|
return lines.join("\n");
|
|
@@ -269,7 +282,7 @@ const renderSectionContent = (block) => {
|
|
|
269
282
|
parts.push(`<p class="text-[15px] text-d-muted font-body">${renderInlineMarkup(block.text)}</p>`);
|
|
270
283
|
}
|
|
271
284
|
if (block.content) {
|
|
272
|
-
parts.push(block.content.map(renderContentBlock).join("\n"));
|
|
285
|
+
parts.push(block.content.map((b) => renderContentBlock(b)).join("\n"));
|
|
273
286
|
}
|
|
274
287
|
return parts.join("\n");
|
|
275
288
|
};
|
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,44 +1,45 @@
|
|
|
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
|
-
return cardWrap(accent, inner.join("\n"), "flex-1");
|
|
35
|
+
return cardWrap(accent, inner.join("\n"), "flex-1", basePath);
|
|
35
36
|
};
|
|
36
37
|
export const layoutColumns = (data) => {
|
|
37
38
|
const cols = data.columns || [];
|
|
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,41 +1,43 @@
|
|
|
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" };
|
|
33
35
|
const spanCls = item.span && item.span > 1 ? SPAN_CLS[item.span] || "" : "";
|
|
34
|
-
parts.push(cardWrap(itemAccent, inner.join("\n"), spanCls));
|
|
36
|
+
parts.push(cardWrap(itemAccent, inner.join("\n"), spanCls, base));
|
|
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, di } from "../utils.js";
|
|
2
|
+
const buildManifestoCard = (line, slideAccent, basePath) => {
|
|
3
3
|
const color = resolveItemColor(line.accentColor, slideAccent);
|
|
4
4
|
const parts = [];
|
|
5
|
-
parts.push(`<div class="relative bg-d-card rounded-lg shadow-md overflow-hidden flex flex-col">`);
|
|
5
|
+
parts.push(`<div class="relative bg-d-card rounded-lg shadow-md overflow-hidden flex flex-col"${di(basePath)}>`);
|
|
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, di } 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
|
-
|
|
10
|
+
const base = `stats[${i}]`;
|
|
11
|
+
parts.push(`<div class="flex-1 bg-d-card rounded-lg shadow-lg p-10 text-center"${di(base)}>`);
|
|
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, di } 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
|
-
|
|
16
|
+
const base = `items[${i}]`;
|
|
17
|
+
parts.push(`<div class="flex-1 flex flex-col items-center text-center relative z-10"${di(base)}>`);
|
|
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,26 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Emit a `data-mulmo-item-path="..."` attribute (with leading space) for a drag-reorderable
|
|
14
|
+
* list-item container (e.g. a `<li>` in a bullets block, a stats card, a timeline step, a
|
|
15
|
+
* manifesto line, a columns card, a grid item). The path identifies the item ROOT in the
|
|
16
|
+
* SlideLayout JSON — siblings (items in the same parent array) share the same parent prefix.
|
|
17
|
+
*
|
|
18
|
+
* Distinct from `data-mulmo-path` (which points at a leaf editable text). This attribute is
|
|
19
|
+
* used by editor UIs to wire drag-and-drop reorder.
|
|
20
|
+
*/
|
|
21
|
+
export declare const di: (path: string) => string;
|
|
22
|
+
/** Compose a child path: `dpJoin("columns[0]", "title")` → `columns[0].title`. */
|
|
23
|
+
export declare const dpJoin: (base: string, segment: string) => string;
|
|
4
24
|
/** Escape HTML and convert newlines to <br> */
|
|
5
25
|
export declare const nl2br: (s: string) => string;
|
|
6
26
|
/**
|
|
@@ -43,8 +63,8 @@ export declare const isSafeCssBackground: (s: string) => boolean;
|
|
|
43
63
|
export declare const numBadge: (num: number, colorKey: string) => string;
|
|
44
64
|
/** Render an icon in a square container */
|
|
45
65
|
export declare const iconSquare: (icon: string, colorKey: string) => string;
|
|
46
|
-
/** Render a card wrapper with accent top bar */
|
|
47
|
-
export declare const cardWrap: (accentColor: string, innerHtml: string, extraClass?: string) => string;
|
|
66
|
+
/** Render a card wrapper with accent top bar. Pass `itemPath` to emit a drag-reorder anchor. */
|
|
67
|
+
export declare const cardWrap: (accentColor: string, innerHtml: string, extraClass?: string, itemPath?: string) => string;
|
|
48
68
|
/** Render a callout bar at the bottom of a slide */
|
|
49
69
|
export declare const renderCalloutBar: (obj: {
|
|
50
70
|
text: string;
|
|
@@ -60,9 +80,9 @@ export declare const renderCalloutBar: (obj: {
|
|
|
60
80
|
export declare const renderEyebrow: (eyebrow: {
|
|
61
81
|
label: string;
|
|
62
82
|
color?: string;
|
|
63
|
-
} | undefined, defaultColor?: string) => string;
|
|
83
|
+
} | undefined, defaultColor?: string, basePath?: string) => string;
|
|
64
84
|
/** 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;
|
|
85
|
+
export declare const renderChipRow: (chips: string[] | undefined, basePath?: string) => string;
|
|
66
86
|
/** Render an accent-colored typographic prefix (e.g. "01") to be placed before a card / stat title. */
|
|
67
87
|
export declare const renderNumLabel: (label: string | undefined, colorKey?: string) => string;
|
|
68
88
|
/** Render header text elements (stepLabel + title + subtitle) without wrapping div */
|
|
@@ -77,7 +97,7 @@ export declare const renderHeaderText: (data: {
|
|
|
77
97
|
};
|
|
78
98
|
titleSize?: "small" | "default" | "large" | "hero";
|
|
79
99
|
subtitleSize?: "default" | "big" | "lead";
|
|
80
|
-
}) => string;
|
|
100
|
+
}, basePath?: string) => string;
|
|
81
101
|
/** Render the common slide header (accent bar + title + subtitle, plus optional eyebrow pill) */
|
|
82
102
|
export declare const slideHeader: (data: {
|
|
83
103
|
accentColor?: string;
|
package/lib/utils.js
CHANGED
|
@@ -7,6 +7,26 @@ 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
|
+
/**
|
|
19
|
+
* Emit a `data-mulmo-item-path="..."` attribute (with leading space) for a drag-reorderable
|
|
20
|
+
* list-item container (e.g. a `<li>` in a bullets block, a stats card, a timeline step, a
|
|
21
|
+
* manifesto line, a columns card, a grid item). The path identifies the item ROOT in the
|
|
22
|
+
* SlideLayout JSON — siblings (items in the same parent array) share the same parent prefix.
|
|
23
|
+
*
|
|
24
|
+
* Distinct from `data-mulmo-path` (which points at a leaf editable text). This attribute is
|
|
25
|
+
* used by editor UIs to wire drag-and-drop reorder.
|
|
26
|
+
*/
|
|
27
|
+
export const di = (path) => (path ? ` data-mulmo-item-path="${escapeHtml(path)}"` : "");
|
|
28
|
+
/** Compose a child path: `dpJoin("columns[0]", "title")` → `columns[0].title`. */
|
|
29
|
+
export const dpJoin = (base, segment) => (base ? `${base}.${segment}` : segment);
|
|
10
30
|
/** Escape HTML and convert newlines to <br> */
|
|
11
31
|
export const nl2br = (s) => {
|
|
12
32
|
return escapeHtml(s).replace(/\n/g, "<br>");
|
|
@@ -141,9 +161,9 @@ export const iconSquare = (icon, colorKey) => {
|
|
|
141
161
|
<span class="text-2xl font-mono font-bold text-${c(colorKey)}">${escapeHtml(icon)}</span>
|
|
142
162
|
</div>`;
|
|
143
163
|
};
|
|
144
|
-
/** Render a card wrapper with accent top bar */
|
|
145
|
-
export const cardWrap = (accentColor, innerHtml, extraClass) => {
|
|
146
|
-
return `<div class="bg-d-card rounded-lg shadow-lg overflow-hidden flex flex-col min-h-0 ${sanitizeCssClass(extraClass || "")}">
|
|
164
|
+
/** Render a card wrapper with accent top bar. Pass `itemPath` to emit a drag-reorder anchor. */
|
|
165
|
+
export const cardWrap = (accentColor, innerHtml, extraClass, itemPath) => {
|
|
166
|
+
return `<div class="bg-d-card rounded-lg shadow-lg overflow-hidden flex flex-col min-h-0 ${sanitizeCssClass(extraClass || "")}"${itemPath ? ` data-mulmo-item-path="${escapeHtml(itemPath)}"` : ""}>
|
|
147
167
|
${accentBar(accentColor)}
|
|
148
168
|
<div class="p-5 flex flex-col flex-1 min-h-0 overflow-hidden">
|
|
149
169
|
${innerHtml}
|
|
@@ -167,18 +187,18 @@ export const renderCalloutBar = (obj) => {
|
|
|
167
187
|
* Render an eyebrow pill (small uppercase letter-spaced category label).
|
|
168
188
|
* Renders nothing when `eyebrow` is undefined, so callers can pass through unconditionally.
|
|
169
189
|
*/
|
|
170
|
-
export const renderEyebrow = (eyebrow, defaultColor) => {
|
|
190
|
+
export const renderEyebrow = (eyebrow, defaultColor, basePath = "") => {
|
|
171
191
|
if (!eyebrow)
|
|
172
192
|
return "";
|
|
173
193
|
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>`;
|
|
194
|
+
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
195
|
};
|
|
176
196
|
/** Render a chip-row (small bordered pill badges, e.g. tags below a title). Empty / undefined input renders nothing. */
|
|
177
|
-
export const renderChipRow = (chips) => {
|
|
197
|
+
export const renderChipRow = (chips, basePath = "") => {
|
|
178
198
|
if (!chips || chips.length === 0)
|
|
179
199
|
return "";
|
|
180
200
|
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>`)
|
|
201
|
+
.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
202
|
.join("");
|
|
183
203
|
return `<div class="flex gap-2 flex-wrap mt-4">${items}</div>`;
|
|
184
204
|
};
|
|
@@ -203,20 +223,20 @@ const SUBTITLE_SIZE_CLS = {
|
|
|
203
223
|
big: "text-[22px]",
|
|
204
224
|
};
|
|
205
225
|
/** Render header text elements (stepLabel + title + subtitle) without wrapping div */
|
|
206
|
-
export const renderHeaderText = (data) => {
|
|
226
|
+
export const renderHeaderText = (data, basePath = "") => {
|
|
207
227
|
const accent = resolveAccent(data.accentColor);
|
|
208
228
|
const lines = [];
|
|
209
|
-
const eyebrowHtml = renderEyebrow(data.eyebrow, accent);
|
|
229
|
+
const eyebrowHtml = renderEyebrow(data.eyebrow, accent, basePath);
|
|
210
230
|
if (eyebrowHtml)
|
|
211
231
|
lines.push(`<div class="mb-3">${eyebrowHtml}</div>`);
|
|
212
232
|
if (data.stepLabel) {
|
|
213
|
-
lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
|
|
233
|
+
lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body"${dp(dpJoin(basePath, "stepLabel"))}>${renderInlineMarkup(data.stepLabel)}</p>`);
|
|
214
234
|
}
|
|
215
235
|
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>`);
|
|
236
|
+
lines.push(`<h2 class="${titleCls} leading-tight font-title font-bold text-d-text"${dp(dpJoin(basePath, "title"))}>${renderInlineMarkup(data.title)}</h2>`);
|
|
217
237
|
if (data.subtitle) {
|
|
218
238
|
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>`);
|
|
239
|
+
lines.push(`<p class="${subtitleCls} text-d-dim mt-2 font-body"${dp(dpJoin(basePath, "subtitle"))}>${renderInlineMarkup(data.subtitle)}</p>`);
|
|
220
240
|
}
|
|
221
241
|
return lines.join("\n");
|
|
222
242
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmocast/deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|