@mulmocast/deck 0.4.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,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** 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.4.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",