@mulmocast/deck 0.3.0 → 0.5.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/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,15 +59,22 @@ 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() })]);
53
- /** Bullet item: plain string or object with text and optional sub-items (2 levels max) */
66
+ /** Status-icon variants for bullets (renders / / in the accent color of the variant). */
67
+ export const bulletIconSchema = z.enum(["ok", "no", "warn"]);
68
+ /** Bullet item: plain string, or object with text + optional sub-items + optional status icon. */
54
69
  export const bulletItemSchema = z.union([
55
70
  z.string(),
56
71
  z.object({
57
72
  text: z.string(),
58
73
  items: z.array(subBulletItemSchema).optional(),
74
+ /** Optional status icon shown in place of the default bullet marker ("ok" → ✓, "no" → ✕, "warn" → ⚠). */
75
+ icon: bulletIconSchema.optional(),
76
+ /** Per-item size variant (overrides the block-level size). */
77
+ size: textSizeSchema.optional(),
59
78
  }),
60
79
  ]);
61
80
  export const bulletsBlockSchema = z.object({
@@ -63,6 +82,8 @@ export const bulletsBlockSchema = z.object({
63
82
  items: z.array(bulletItemSchema),
64
83
  ordered: z.boolean().optional(),
65
84
  icon: z.string().optional(),
85
+ /** Block-level size variant — applied to every item that doesn't set its own. */
86
+ size: textSizeSchema.optional(),
66
87
  });
67
88
  export const codeBlockSchema = z.object({
68
89
  type: z.literal("code"),
@@ -75,6 +96,8 @@ export const calloutBlockSchema = z.object({
75
96
  label: z.string().optional(),
76
97
  color: accentColorKeySchema.optional(),
77
98
  style: z.enum(["quote", "info", "warning"]).optional(),
99
+ /** Optional size variant for the callout body text. */
100
+ size: textSizeSchema.optional(),
78
101
  });
79
102
  export const metricBlockSchema = z.object({
80
103
  type: z.literal("metric"),
@@ -87,6 +110,15 @@ export const dividerBlockSchema = z.object({
87
110
  type: z.literal("divider"),
88
111
  color: accentColorKeySchema.optional(),
89
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
+ });
90
122
  export const imageBlockSchema = z.object({
91
123
  type: z.literal("image"),
92
124
  src: z.string(),
@@ -139,6 +171,7 @@ const baseBlockSchemas = [
139
171
  chartBlockSchema,
140
172
  mermaidBlockSchema,
141
173
  tableBlockSchema,
174
+ tagBlockSchema,
142
175
  ];
143
176
  /** All content block types except section (used inside section to prevent recursion) */
144
177
  const nonSectionContentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas]);
@@ -191,12 +224,24 @@ export const eyebrowSchema = z.object({
191
224
  /** Color token (e.g. "primary", "amber", "success"). Falls back to slide.accentColor / theme primary when absent. */
192
225
  color: accentColorKeySchema.optional(),
193
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"]);
194
233
  /** Common slide properties shared across all layouts */
195
234
  const slideBaseFields = {
196
235
  accentColor: accentColorKeySchema.optional(),
197
236
  style: slideStyleSchema.optional(),
198
237
  /** Optional eyebrow (small uppercase pill) shown at the top of the slide above the header/title. */
199
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(),
200
245
  };
201
246
  // ═══════════════════════════════════════════════════════════
202
247
  // Layouts
@@ -230,6 +275,10 @@ export const comparisonPanelSchema = z.object({
230
275
  accentColor: accentColorKeySchema.optional(),
231
276
  content: z.array(contentBlockSchema).optional(),
232
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(),
233
282
  });
234
283
  export const comparisonSlideSchema = z.object({
235
284
  layout: z.literal("comparison"),
@@ -249,6 +298,8 @@ export const gridItemSchema = z.object({
249
298
  num: z.number().optional(),
250
299
  icon: z.string().optional(),
251
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(),
252
303
  });
253
304
  export const gridSlideSchema = z.object({
254
305
  layout: z.literal("grid"),
@@ -292,6 +343,8 @@ export const timelineItemSchema = z.object({
292
343
  description: z.string().optional(),
293
344
  color: accentColorKeySchema.optional(),
294
345
  done: z.boolean().optional(),
346
+ /** Optional emphasis flag — when true the step is rendered with a stronger accent border and tinted background. */
347
+ hot: z.boolean().optional(),
295
348
  });
296
349
  export const timelineSlideSchema = z.object({
297
350
  layout: z.literal("timeline"),
@@ -396,6 +449,29 @@ export const funnelSlideSchema = z.object({
396
449
  stages: z.array(funnelStageSchema),
397
450
  callout: calloutBarSchema.optional(),
398
451
  });
452
+ // ─── manifesto ───
453
+ /** A single line in a manifesto / creed grid. */
454
+ export const manifestoLineSchema = z.object({
455
+ title: z.string(),
456
+ description: z.string().optional(),
457
+ /** Accent color for the line's left-border highlight. Falls back to slide.accentColor / "primary". */
458
+ accentColor: accentColorKeySchema.optional(),
459
+ });
460
+ /**
461
+ * Grid of short bordered cards, each with a colored left accent — useful for manifestos,
462
+ * principles, "what we believe" lists, etc.
463
+ */
464
+ export const manifestoSlideSchema = z.object({
465
+ layout: z.literal("manifesto"),
466
+ ...slideBaseFields,
467
+ title: z.string(),
468
+ stepLabel: z.string().optional(),
469
+ subtitle: z.string().optional(),
470
+ items: z.array(manifestoLineSchema),
471
+ /** Number of grid columns (default: 2). */
472
+ columns: z.number().int().min(1).max(4).optional(),
473
+ callout: calloutBarSchema.optional(),
474
+ });
399
475
  // ═══════════════════════════════════════════════════════════
400
476
  // Branding — logo & background image overlay
401
477
  // ═══════════════════════════════════════════════════════════
@@ -443,6 +519,7 @@ export const slideLayoutSchema = z.discriminatedUnion("layout", [
443
519
  tableSlideSchema,
444
520
  funnelSlideSchema,
445
521
  waterfallSlideSchema,
522
+ manifestoSlideSchema,
446
523
  ]);
447
524
  /** Media schema registered in mulmoImageAssetSchema */
448
525
  export const mulmoSlideMediaSchema = z
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** and {color:text}.
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** and {color:text}.
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
- // **bold** <strong>bold</strong>
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-2">${eyebrowHtml}</div>`);
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
- lines.push(`<h2 class="text-[42px] leading-tight font-title font-bold text-d-text">${renderInlineMarkup(data.title)}</h2>`);
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
- lines.push(`<p class="text-[15px] text-d-dim mt-2 font-body">${renderInlineMarkup(data.subtitle)}</p>`);
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-5 shrink-0">`, renderHeaderText(data), `</div>`].join("\n");
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.0",
3
+ "version": "0.5.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",