@mulmocast/deck 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -1
- package/lib/blocks.js +46 -9
- package/lib/index.d.ts +1 -1
- package/lib/layouts/comparison.js +18 -2
- package/lib/layouts/grid.js +4 -1
- package/lib/layouts/title.js +12 -1
- package/lib/render.js +15 -2
- package/lib/schema.d.ts +2454 -0
- package/lib/schema.js +47 -0
- package/lib/utils.d.ts +13 -1
- package/lib/utils.js +29 -6
- package/package.json +1 -1
package/lib/schema.js
CHANGED
|
@@ -28,6 +28,8 @@ export const slideThemeFontsSchema = z.object({
|
|
|
28
28
|
/** Optional secondary font for numbers / English labels (e.g. "Outfit"). When set, registers a `font-accent` Tailwind utility. */
|
|
29
29
|
accent: z.string().optional(),
|
|
30
30
|
});
|
|
31
|
+
/** Card visual style. "glass" = transparent gradient bg + subtle border (matches reveal.js polished decks); "solid" = opaque card (default). */
|
|
32
|
+
export const slideCardStyleSchema = z.enum(["glass", "solid"]);
|
|
31
33
|
export const slideThemeSchema = z.object({
|
|
32
34
|
colors: slideThemeColorsSchema,
|
|
33
35
|
fonts: slideThemeFontsSchema,
|
|
@@ -35,10 +37,20 @@ export const slideThemeSchema = z.object({
|
|
|
35
37
|
bgGradient: z.string().optional(),
|
|
36
38
|
/** Optional CSS gradient injected as `<style>` to paint `h1.font-title.font-bold` with `background-clip: text`. */
|
|
37
39
|
titleGradient: z.string().optional(),
|
|
40
|
+
/** Default card style for the entire deck. Per-slide layouts can still override individual cards. */
|
|
41
|
+
cardStyle: slideCardStyleSchema.optional(),
|
|
38
42
|
});
|
|
39
43
|
// ═══════════════════════════════════════════════════════════
|
|
40
44
|
// Content Blocks — the atoms of slide content
|
|
41
45
|
// ═══════════════════════════════════════════════════════════
|
|
46
|
+
/**
|
|
47
|
+
* Abstract text-size variant. Maps to a theme-aware (size, weight, color) tuple,
|
|
48
|
+
* so authors don't have to hand-pick pixel sizes per slide.
|
|
49
|
+
* lead = muted intro paragraph (slightly larger, dim)
|
|
50
|
+
* big = emphasized body (larger, full text color)
|
|
51
|
+
* sub = card / footnote body (smaller, dim)
|
|
52
|
+
*/
|
|
53
|
+
export const textSizeSchema = z.enum(["lead", "big", "sub"]);
|
|
42
54
|
export const textBlockSchema = z.object({
|
|
43
55
|
type: z.literal("text"),
|
|
44
56
|
value: z.string(),
|
|
@@ -47,6 +59,7 @@ export const textBlockSchema = z.object({
|
|
|
47
59
|
dim: z.boolean().optional(),
|
|
48
60
|
fontSize: z.number().optional(),
|
|
49
61
|
color: accentColorKeySchema.optional(),
|
|
62
|
+
size: textSizeSchema.optional(),
|
|
50
63
|
});
|
|
51
64
|
/** Sub-bullet item: plain string or object with text */
|
|
52
65
|
const subBulletItemSchema = z.union([z.string(), z.object({ text: z.string() })]);
|
|
@@ -60,6 +73,8 @@ export const bulletItemSchema = z.union([
|
|
|
60
73
|
items: z.array(subBulletItemSchema).optional(),
|
|
61
74
|
/** Optional status icon shown in place of the default bullet marker ("ok" → ✓, "no" → ✕, "warn" → ⚠). */
|
|
62
75
|
icon: bulletIconSchema.optional(),
|
|
76
|
+
/** Per-item size variant (overrides the block-level size). */
|
|
77
|
+
size: textSizeSchema.optional(),
|
|
63
78
|
}),
|
|
64
79
|
]);
|
|
65
80
|
export const bulletsBlockSchema = z.object({
|
|
@@ -67,6 +82,8 @@ export const bulletsBlockSchema = z.object({
|
|
|
67
82
|
items: z.array(bulletItemSchema),
|
|
68
83
|
ordered: z.boolean().optional(),
|
|
69
84
|
icon: z.string().optional(),
|
|
85
|
+
/** Block-level size variant — applied to every item that doesn't set its own. */
|
|
86
|
+
size: textSizeSchema.optional(),
|
|
70
87
|
});
|
|
71
88
|
export const codeBlockSchema = z.object({
|
|
72
89
|
type: z.literal("code"),
|
|
@@ -79,6 +96,8 @@ export const calloutBlockSchema = z.object({
|
|
|
79
96
|
label: z.string().optional(),
|
|
80
97
|
color: accentColorKeySchema.optional(),
|
|
81
98
|
style: z.enum(["quote", "info", "warning"]).optional(),
|
|
99
|
+
/** Optional size variant for the callout body text. */
|
|
100
|
+
size: textSizeSchema.optional(),
|
|
82
101
|
});
|
|
83
102
|
export const metricBlockSchema = z.object({
|
|
84
103
|
type: z.literal("metric"),
|
|
@@ -91,6 +110,15 @@ export const dividerBlockSchema = z.object({
|
|
|
91
110
|
type: z.literal("divider"),
|
|
92
111
|
color: accentColorKeySchema.optional(),
|
|
93
112
|
});
|
|
113
|
+
/**
|
|
114
|
+
* Small uppercase accent label, intended for use INSIDE cards (matches reveal.js .tag).
|
|
115
|
+
* Distinct from the slide-level `eyebrow`: this is per-block and sits above an h3 / title in a card.
|
|
116
|
+
*/
|
|
117
|
+
export const tagBlockSchema = z.object({
|
|
118
|
+
type: z.literal("tag"),
|
|
119
|
+
text: z.string(),
|
|
120
|
+
color: accentColorKeySchema.optional(),
|
|
121
|
+
});
|
|
94
122
|
export const imageBlockSchema = z.object({
|
|
95
123
|
type: z.literal("image"),
|
|
96
124
|
src: z.string(),
|
|
@@ -143,6 +171,7 @@ const baseBlockSchemas = [
|
|
|
143
171
|
chartBlockSchema,
|
|
144
172
|
mermaidBlockSchema,
|
|
145
173
|
tableBlockSchema,
|
|
174
|
+
tagBlockSchema,
|
|
146
175
|
];
|
|
147
176
|
/** All content block types except section (used inside section to prevent recursion) */
|
|
148
177
|
const nonSectionContentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas]);
|
|
@@ -195,12 +224,24 @@ export const eyebrowSchema = z.object({
|
|
|
195
224
|
/** Color token (e.g. "primary", "amber", "success"). Falls back to slide.accentColor / theme primary when absent. */
|
|
196
225
|
color: accentColorKeySchema.optional(),
|
|
197
226
|
});
|
|
227
|
+
/** Slide content density. "compact" shrinks body / bullet / callout text and tightens padding (~85% scale). */
|
|
228
|
+
export const slideDensitySchema = z.enum(["compact", "default"]);
|
|
229
|
+
/** Slide title (h2) size override. "small" tightens for content-heavy slides, "hero" enlarges for closing/section slides. */
|
|
230
|
+
export const slideTitleSizeSchema = z.enum(["small", "default", "large", "hero"]);
|
|
231
|
+
/** Slide subtitle size variant. Defaults to body (15px); "big" matches reveal.js .big.muted ≈ 22px. */
|
|
232
|
+
export const slideSubtitleSizeSchema = z.enum(["default", "big", "lead"]);
|
|
198
233
|
/** Common slide properties shared across all layouts */
|
|
199
234
|
const slideBaseFields = {
|
|
200
235
|
accentColor: accentColorKeySchema.optional(),
|
|
201
236
|
style: slideStyleSchema.optional(),
|
|
202
237
|
/** Optional eyebrow (small uppercase pill) shown at the top of the slide above the header/title. */
|
|
203
238
|
eyebrow: eyebrowSchema.optional(),
|
|
239
|
+
/** Per-slide density. "compact" reduces text size and padding for slides with a lot of content. */
|
|
240
|
+
density: slideDensitySchema.optional(),
|
|
241
|
+
/** Optional override for the slide title (h2) size. Affects layouts that go through slideHeader / centeredSlideHeader. */
|
|
242
|
+
titleSize: slideTitleSizeSchema.optional(),
|
|
243
|
+
/** Optional override for the slide subtitle size. Defaults to body (15px). */
|
|
244
|
+
subtitleSize: slideSubtitleSizeSchema.optional(),
|
|
204
245
|
};
|
|
205
246
|
// ═══════════════════════════════════════════════════════════
|
|
206
247
|
// Layouts
|
|
@@ -234,6 +275,10 @@ export const comparisonPanelSchema = z.object({
|
|
|
234
275
|
accentColor: accentColorKeySchema.optional(),
|
|
235
276
|
content: z.array(contentBlockSchema).optional(),
|
|
236
277
|
footer: z.string().optional(),
|
|
278
|
+
/** Optional column ratio (numeric, used as the panel's flex-grow). Default = 1 on both sides (50/50). */
|
|
279
|
+
ratio: z.number().positive().optional(),
|
|
280
|
+
/** When true, drop the card chrome (background, top accent bar, padding) and render content directly on the slide. */
|
|
281
|
+
cardless: z.boolean().optional(),
|
|
237
282
|
});
|
|
238
283
|
export const comparisonSlideSchema = z.object({
|
|
239
284
|
layout: z.literal("comparison"),
|
|
@@ -253,6 +298,8 @@ export const gridItemSchema = z.object({
|
|
|
253
298
|
num: z.number().optional(),
|
|
254
299
|
icon: z.string().optional(),
|
|
255
300
|
content: z.array(contentBlockSchema).optional(),
|
|
301
|
+
/** Optional column span (1-4) for asymmetric grids (e.g. one wide item across the row). Default = 1. */
|
|
302
|
+
span: z.number().int().min(1).max(4).optional(),
|
|
256
303
|
});
|
|
257
304
|
export const gridSlideSchema = z.object({
|
|
258
305
|
layout: z.literal("grid"),
|
package/lib/utils.d.ts
CHANGED
|
@@ -4,9 +4,15 @@ export declare const escapeHtml: (s: string) => string;
|
|
|
4
4
|
/** Escape HTML and convert newlines to <br> */
|
|
5
5
|
export declare const nl2br: (s: string) => string;
|
|
6
6
|
/**
|
|
7
|
-
* Render inline markup: escape HTML first, then parse **bold
|
|
7
|
+
* Render inline markup: escape HTML first, then parse **bold**, *emphasis*, and {color:text}.
|
|
8
8
|
* Also converts newlines to <br>.
|
|
9
9
|
* Safe: escapeHtml runs before any markup parsing, so XSS is impossible.
|
|
10
|
+
*
|
|
11
|
+
* **bold** → <strong>...</strong> (kept as-is for back-compat)
|
|
12
|
+
* *emphasis* → <em>...</em> (rendered in warning color via CSS; mimics reveal.js amber <em>)
|
|
13
|
+
* {color:text} → <span class="text-d-color">...</span>
|
|
14
|
+
*
|
|
15
|
+
* Bold is parsed first; the single-asterisk pass only matches what's left, so "**x**" doesn't double-fire.
|
|
10
16
|
*/
|
|
11
17
|
export declare const renderInlineMarkup: (s: string) => string;
|
|
12
18
|
/** Sanitize a hex color value (hex digits only) */
|
|
@@ -69,6 +75,8 @@ export declare const renderHeaderText: (data: {
|
|
|
69
75
|
label: string;
|
|
70
76
|
color?: string;
|
|
71
77
|
};
|
|
78
|
+
titleSize?: "small" | "default" | "large" | "hero";
|
|
79
|
+
subtitleSize?: "default" | "big" | "lead";
|
|
72
80
|
}) => string;
|
|
73
81
|
/** Render the common slide header (accent bar + title + subtitle, plus optional eyebrow pill) */
|
|
74
82
|
export declare const slideHeader: (data: {
|
|
@@ -80,6 +88,8 @@ export declare const slideHeader: (data: {
|
|
|
80
88
|
label: string;
|
|
81
89
|
color?: string;
|
|
82
90
|
};
|
|
91
|
+
titleSize?: "small" | "default" | "large" | "hero";
|
|
92
|
+
subtitleSize?: "default" | "big" | "lead";
|
|
83
93
|
}) => string;
|
|
84
94
|
/** Render accent bar + vertically-centered wrapper with header text (used by stats, timeline) */
|
|
85
95
|
export declare const centeredSlideHeader: (data: {
|
|
@@ -91,6 +101,8 @@ export declare const centeredSlideHeader: (data: {
|
|
|
91
101
|
label: string;
|
|
92
102
|
color?: string;
|
|
93
103
|
};
|
|
104
|
+
titleSize?: "small" | "default" | "large" | "hero";
|
|
105
|
+
subtitleSize?: "default" | "big" | "lead";
|
|
94
106
|
}) => string;
|
|
95
107
|
/** Generate a unique ID with the given prefix (e.g. "chart-0", "mermaid-1") */
|
|
96
108
|
export declare const generateSlideId: (prefix: string) => string;
|
package/lib/utils.js
CHANGED
|
@@ -14,14 +14,22 @@ export const nl2br = (s) => {
|
|
|
14
14
|
/** Valid accent color keys for inline markup */
|
|
15
15
|
const inlineColorKeys = new Set(["primary", "accent", "success", "warning", "danger", "info", "highlight"]);
|
|
16
16
|
/**
|
|
17
|
-
* Render inline markup: escape HTML first, then parse **bold
|
|
17
|
+
* Render inline markup: escape HTML first, then parse **bold**, *emphasis*, and {color:text}.
|
|
18
18
|
* Also converts newlines to <br>.
|
|
19
19
|
* Safe: escapeHtml runs before any markup parsing, so XSS is impossible.
|
|
20
|
+
*
|
|
21
|
+
* **bold** → <strong>...</strong> (kept as-is for back-compat)
|
|
22
|
+
* *emphasis* → <em>...</em> (rendered in warning color via CSS; mimics reveal.js amber <em>)
|
|
23
|
+
* {color:text} → <span class="text-d-color">...</span>
|
|
24
|
+
*
|
|
25
|
+
* Bold is parsed first; the single-asterisk pass only matches what's left, so "**x**" doesn't double-fire.
|
|
20
26
|
*/
|
|
21
27
|
export const renderInlineMarkup = (s) => {
|
|
22
28
|
let result = escapeHtml(s);
|
|
23
|
-
// **
|
|
29
|
+
// Bold MUST run before emphasis so **x** doesn't get eaten by the single-* pass.
|
|
24
30
|
result = result.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
31
|
+
// Single-* emphasis. The negative-lookbehind/lookahead keeps it from biting into surviving "**" inside <strong>.
|
|
32
|
+
result = result.replace(/(?<![*\w])\*(?!\s)([^*\n]+?)(?<!\s)\*(?!\w)/g, '<em class="text-d-warning not-italic font-bold">$1</em>');
|
|
25
33
|
// {color:text} → <span class="text-d-color">text</span>
|
|
26
34
|
result = result.replace(/\{([a-z]+):(.+?)\}/g, (_match, color, text) => {
|
|
27
35
|
if (inlineColorKeys.has(color)) {
|
|
@@ -181,26 +189,41 @@ export const renderNumLabel = (label, colorKey) => {
|
|
|
181
189
|
const color = c(colorKey ?? "primary");
|
|
182
190
|
return `<span class="font-accent font-extrabold text-${color} mr-2">${renderInlineMarkup(label)}</span>`;
|
|
183
191
|
};
|
|
192
|
+
/** h2 font-size by titleSize variant — kept in one place so it's easy to keep proportional to subtitle / body. */
|
|
193
|
+
const TITLE_SIZE_CLS = {
|
|
194
|
+
small: "text-[34px]",
|
|
195
|
+
default: "text-[42px]",
|
|
196
|
+
large: "text-[52px]",
|
|
197
|
+
hero: "text-[64px]",
|
|
198
|
+
};
|
|
199
|
+
/** Subtitle font-size by variant. Default keeps the original 15px; bigger variants align with the reveal.js .big/.lead helpers. */
|
|
200
|
+
const SUBTITLE_SIZE_CLS = {
|
|
201
|
+
default: "text-[15px]",
|
|
202
|
+
lead: "text-[17px]",
|
|
203
|
+
big: "text-[22px]",
|
|
204
|
+
};
|
|
184
205
|
/** Render header text elements (stepLabel + title + subtitle) without wrapping div */
|
|
185
206
|
export const renderHeaderText = (data) => {
|
|
186
207
|
const accent = resolveAccent(data.accentColor);
|
|
187
208
|
const lines = [];
|
|
188
209
|
const eyebrowHtml = renderEyebrow(data.eyebrow, accent);
|
|
189
210
|
if (eyebrowHtml)
|
|
190
|
-
lines.push(`<div class="mb-
|
|
211
|
+
lines.push(`<div class="mb-3">${eyebrowHtml}</div>`);
|
|
191
212
|
if (data.stepLabel) {
|
|
192
213
|
lines.push(`<p class="text-sm font-bold text-${c(accent)} font-body">${renderInlineMarkup(data.stepLabel)}</p>`);
|
|
193
214
|
}
|
|
194
|
-
|
|
215
|
+
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>`);
|
|
195
217
|
if (data.subtitle) {
|
|
196
|
-
|
|
218
|
+
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>`);
|
|
197
220
|
}
|
|
198
221
|
return lines.join("\n");
|
|
199
222
|
};
|
|
200
223
|
/** Render the common slide header (accent bar + title + subtitle, plus optional eyebrow pill) */
|
|
201
224
|
export const slideHeader = (data) => {
|
|
202
225
|
const accent = resolveAccent(data.accentColor);
|
|
203
|
-
return [accentBar(accent), `<div class="px-12 pt-
|
|
226
|
+
return [accentBar(accent), `<div class="px-12 pt-8 shrink-0">`, renderHeaderText(data), `</div>`].join("\n");
|
|
204
227
|
};
|
|
205
228
|
/** Render accent bar + vertically-centered wrapper with header text (used by stats, timeline) */
|
|
206
229
|
export const centeredSlideHeader = (data) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmocast/deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|