@mulmocast/deck 0.1.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 ADDED
@@ -0,0 +1,434 @@
1
+ import { z } from "zod";
2
+ // ═══════════════════════════════════════════════════════════
3
+ // Foundation: Colors & Typography
4
+ // ═══════════════════════════════════════════════════════════
5
+ /** 6-digit hex without '#' prefix, e.g. "3B82F6" */
6
+ const hexColorSchema = z.string().regex(/^[0-9A-Fa-f]{6}$/);
7
+ /** Semantic accent colors usable on cards, badges, borders, etc. */
8
+ export const accentColorKeySchema = z.enum(["primary", "accent", "success", "warning", "danger", "info", "highlight"]);
9
+ export const slideThemeColorsSchema = z.object({
10
+ bg: hexColorSchema,
11
+ bgCard: hexColorSchema,
12
+ bgCardAlt: hexColorSchema,
13
+ text: hexColorSchema,
14
+ textMuted: hexColorSchema,
15
+ textDim: hexColorSchema,
16
+ primary: hexColorSchema,
17
+ accent: hexColorSchema,
18
+ success: hexColorSchema,
19
+ warning: hexColorSchema,
20
+ danger: hexColorSchema,
21
+ info: hexColorSchema,
22
+ highlight: hexColorSchema,
23
+ });
24
+ export const slideThemeFontsSchema = z.object({
25
+ title: z.string(),
26
+ body: z.string(),
27
+ mono: z.string(),
28
+ });
29
+ export const slideThemeSchema = z.object({
30
+ colors: slideThemeColorsSchema,
31
+ fonts: slideThemeFontsSchema,
32
+ });
33
+ // ═══════════════════════════════════════════════════════════
34
+ // Content Blocks — the atoms of slide content
35
+ // ═══════════════════════════════════════════════════════════
36
+ export const textBlockSchema = z.object({
37
+ type: z.literal("text"),
38
+ value: z.string(),
39
+ align: z.enum(["left", "center", "right"]).optional(),
40
+ bold: z.boolean().optional(),
41
+ dim: z.boolean().optional(),
42
+ fontSize: z.number().optional(),
43
+ color: accentColorKeySchema.optional(),
44
+ });
45
+ /** Sub-bullet item: plain string or object with text */
46
+ const subBulletItemSchema = z.union([z.string(), z.object({ text: z.string() })]);
47
+ /** Bullet item: plain string or object with text and optional sub-items (2 levels max) */
48
+ export const bulletItemSchema = z.union([
49
+ z.string(),
50
+ z.object({
51
+ text: z.string(),
52
+ items: z.array(subBulletItemSchema).optional(),
53
+ }),
54
+ ]);
55
+ export const bulletsBlockSchema = z.object({
56
+ type: z.literal("bullets"),
57
+ items: z.array(bulletItemSchema),
58
+ ordered: z.boolean().optional(),
59
+ icon: z.string().optional(),
60
+ });
61
+ export const codeBlockSchema = z.object({
62
+ type: z.literal("code"),
63
+ code: z.string(),
64
+ language: z.string().optional(),
65
+ });
66
+ export const calloutBlockSchema = z.object({
67
+ type: z.literal("callout"),
68
+ text: z.string(),
69
+ label: z.string().optional(),
70
+ color: accentColorKeySchema.optional(),
71
+ style: z.enum(["quote", "info", "warning"]).optional(),
72
+ });
73
+ export const metricBlockSchema = z.object({
74
+ type: z.literal("metric"),
75
+ value: z.string(),
76
+ label: z.string(),
77
+ color: accentColorKeySchema.optional(),
78
+ change: z.string().optional(),
79
+ });
80
+ export const dividerBlockSchema = z.object({
81
+ type: z.literal("divider"),
82
+ color: accentColorKeySchema.optional(),
83
+ });
84
+ export const imageBlockSchema = z.object({
85
+ type: z.literal("image"),
86
+ src: z.string(),
87
+ alt: z.string().optional(),
88
+ fit: z.enum(["contain", "cover"]).optional(),
89
+ });
90
+ export const imageRefBlockSchema = z.object({
91
+ type: z.literal("imageRef"),
92
+ ref: z.string(),
93
+ alt: z.string().optional(),
94
+ fit: z.enum(["contain", "cover"]).optional(),
95
+ });
96
+ export const chartBlockSchema = z.object({
97
+ type: z.literal("chart"),
98
+ chartData: z.record(z.string(), z.unknown()),
99
+ title: z.string().optional(),
100
+ });
101
+ export const mermaidBlockSchema = z.object({
102
+ type: z.literal("mermaid"),
103
+ code: z.string(),
104
+ title: z.string().optional(),
105
+ });
106
+ export const tableCellValueSchema = z.union([
107
+ z.string(),
108
+ z.object({
109
+ text: z.string(),
110
+ color: accentColorKeySchema.optional(),
111
+ bold: z.boolean().optional(),
112
+ badge: z.boolean().optional(),
113
+ }),
114
+ ]);
115
+ export const tableBlockSchema = z.object({
116
+ type: z.literal("table"),
117
+ title: z.string().optional(),
118
+ headers: z.array(z.string()).optional(),
119
+ rows: z.array(z.array(tableCellValueSchema)),
120
+ rowHeaders: z.boolean().optional(),
121
+ striped: z.boolean().optional(),
122
+ });
123
+ /** Block schemas shared between contentBlockSchema and nonSectionContentBlockSchema */
124
+ const baseBlockSchemas = [
125
+ textBlockSchema,
126
+ bulletsBlockSchema,
127
+ codeBlockSchema,
128
+ calloutBlockSchema,
129
+ metricBlockSchema,
130
+ dividerBlockSchema,
131
+ imageBlockSchema,
132
+ imageRefBlockSchema,
133
+ chartBlockSchema,
134
+ mermaidBlockSchema,
135
+ tableBlockSchema,
136
+ ];
137
+ /** All content block types except section (used inside section to prevent recursion) */
138
+ const nonSectionContentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas]);
139
+ export const sectionBlockSchema = z.object({
140
+ type: z.literal("section"),
141
+ label: z.string(),
142
+ color: accentColorKeySchema.optional(),
143
+ content: z.array(nonSectionContentBlockSchema).optional(),
144
+ text: z.string().optional(),
145
+ sidebar: z.boolean().optional(),
146
+ });
147
+ export const contentBlockSchema = z.discriminatedUnion("type", [...baseBlockSchemas, sectionBlockSchema]);
148
+ // ═══════════════════════════════════════════════════════════
149
+ // Shared Components
150
+ // ═══════════════════════════════════════════════════════════
151
+ /** Bottom-of-slide callout bar */
152
+ export const calloutBarSchema = z.object({
153
+ text: z.string(),
154
+ label: z.string().optional(),
155
+ color: accentColorKeySchema.optional(),
156
+ align: z.enum(["left", "center"]).optional(),
157
+ leftBar: z.boolean().optional(),
158
+ });
159
+ /** Reusable card definition — used by columns, grid */
160
+ export const cardSchema = z.object({
161
+ title: z.string(),
162
+ accentColor: accentColorKeySchema.optional(),
163
+ content: z.array(contentBlockSchema).optional(),
164
+ footer: z.string().optional(),
165
+ label: z.string().optional(),
166
+ num: z.number().optional(),
167
+ icon: z.string().optional(),
168
+ });
169
+ // ═══════════════════════════════════════════════════════════
170
+ // Slide-level styling — orthogonal to layout
171
+ // ═══════════════════════════════════════════════════════════
172
+ export const slideStyleSchema = z.object({
173
+ bgColor: z.string().optional(),
174
+ decorations: z.boolean().optional(),
175
+ bgOpacity: z.number().optional(),
176
+ footer: z.string().optional(),
177
+ });
178
+ /** Common slide properties shared across all layouts */
179
+ const slideBaseFields = {
180
+ accentColor: accentColorKeySchema.optional(),
181
+ style: slideStyleSchema.optional(),
182
+ };
183
+ // ═══════════════════════════════════════════════════════════
184
+ // Layouts
185
+ // ═══════════════════════════════════════════════════════════
186
+ // ─── title ───
187
+ export const titleSlideSchema = z.object({
188
+ layout: z.literal("title"),
189
+ ...slideBaseFields,
190
+ title: z.string(),
191
+ subtitle: z.string().optional(),
192
+ author: z.string().optional(),
193
+ note: z.string().optional(),
194
+ });
195
+ // ─── columns ───
196
+ export const columnsSlideSchema = z.object({
197
+ layout: z.literal("columns"),
198
+ ...slideBaseFields,
199
+ title: z.string(),
200
+ stepLabel: z.string().optional(),
201
+ subtitle: z.string().optional(),
202
+ columns: z.array(cardSchema),
203
+ showArrows: z.boolean().optional(),
204
+ callout: calloutBarSchema.optional(),
205
+ bottomText: z.string().optional(),
206
+ });
207
+ // ─── comparison ───
208
+ export const comparisonPanelSchema = z.object({
209
+ title: z.string(),
210
+ accentColor: accentColorKeySchema.optional(),
211
+ content: z.array(contentBlockSchema).optional(),
212
+ footer: z.string().optional(),
213
+ });
214
+ export const comparisonSlideSchema = z.object({
215
+ layout: z.literal("comparison"),
216
+ ...slideBaseFields,
217
+ title: z.string(),
218
+ stepLabel: z.string().optional(),
219
+ subtitle: z.string().optional(),
220
+ left: comparisonPanelSchema,
221
+ right: comparisonPanelSchema,
222
+ callout: calloutBarSchema.optional(),
223
+ });
224
+ // ─── grid ───
225
+ export const gridItemSchema = z.object({
226
+ title: z.string(),
227
+ description: z.string().optional(),
228
+ accentColor: accentColorKeySchema.optional(),
229
+ num: z.number().optional(),
230
+ icon: z.string().optional(),
231
+ content: z.array(contentBlockSchema).optional(),
232
+ });
233
+ export const gridSlideSchema = z.object({
234
+ layout: z.literal("grid"),
235
+ ...slideBaseFields,
236
+ title: z.string(),
237
+ subtitle: z.string().optional(),
238
+ gridColumns: z.number().optional(),
239
+ items: z.array(gridItemSchema),
240
+ footer: z.string().optional(),
241
+ });
242
+ // ─── bigQuote ───
243
+ export const bigQuoteSlideSchema = z.object({
244
+ layout: z.literal("bigQuote"),
245
+ ...slideBaseFields,
246
+ quote: z.string(),
247
+ author: z.string().optional(),
248
+ role: z.string().optional(),
249
+ });
250
+ // ─── stats ───
251
+ export const statItemSchema = z.object({
252
+ value: z.string(),
253
+ label: z.string(),
254
+ color: accentColorKeySchema.optional(),
255
+ change: z.string().optional(),
256
+ });
257
+ export const statsSlideSchema = z.object({
258
+ layout: z.literal("stats"),
259
+ ...slideBaseFields,
260
+ title: z.string(),
261
+ stepLabel: z.string().optional(),
262
+ subtitle: z.string().optional(),
263
+ stats: z.array(statItemSchema),
264
+ callout: calloutBarSchema.optional(),
265
+ });
266
+ // ─── timeline ───
267
+ export const timelineItemSchema = z.object({
268
+ date: z.string(),
269
+ title: z.string(),
270
+ description: z.string().optional(),
271
+ color: accentColorKeySchema.optional(),
272
+ done: z.boolean().optional(),
273
+ });
274
+ export const timelineSlideSchema = z.object({
275
+ layout: z.literal("timeline"),
276
+ ...slideBaseFields,
277
+ title: z.string(),
278
+ stepLabel: z.string().optional(),
279
+ subtitle: z.string().optional(),
280
+ items: z.array(timelineItemSchema),
281
+ });
282
+ // ─── split ───
283
+ export const splitPanelSchema = z.object({
284
+ title: z.string().optional(),
285
+ subtitle: z.string().optional(),
286
+ label: z.string().optional(),
287
+ labelBadge: z.boolean().optional(),
288
+ accentColor: accentColorKeySchema.optional(),
289
+ content: z.array(contentBlockSchema).optional(),
290
+ dark: z.boolean().optional(),
291
+ ratio: z.number().optional(),
292
+ valign: z.enum(["top", "center", "bottom"]).optional(),
293
+ });
294
+ export const splitSlideSchema = z.object({
295
+ layout: z.literal("split"),
296
+ ...slideBaseFields,
297
+ left: splitPanelSchema.optional(),
298
+ right: splitPanelSchema.optional(),
299
+ });
300
+ // ─── matrix ───
301
+ export const matrixCellSchema = z.object({
302
+ label: z.string(),
303
+ items: z.array(z.string()).optional(),
304
+ content: z.array(contentBlockSchema).optional(),
305
+ accentColor: accentColorKeySchema.optional(),
306
+ });
307
+ export const matrixSlideSchema = z.object({
308
+ layout: z.literal("matrix"),
309
+ ...slideBaseFields,
310
+ title: z.string(),
311
+ stepLabel: z.string().optional(),
312
+ subtitle: z.string().optional(),
313
+ rows: z.number().optional(),
314
+ cols: z.number().optional(),
315
+ xAxis: z
316
+ .object({
317
+ low: z.string().optional(),
318
+ high: z.string().optional(),
319
+ label: z.string().optional(),
320
+ })
321
+ .optional(),
322
+ yAxis: z
323
+ .object({
324
+ low: z.string().optional(),
325
+ high: z.string().optional(),
326
+ label: z.string().optional(),
327
+ })
328
+ .optional(),
329
+ cells: z.array(matrixCellSchema),
330
+ });
331
+ // ─── table ───
332
+ export const tableSlideSchema = z.object({
333
+ layout: z.literal("table"),
334
+ ...slideBaseFields,
335
+ title: z.string(),
336
+ stepLabel: z.string().optional(),
337
+ subtitle: z.string().optional(),
338
+ headers: z.array(z.string()),
339
+ rows: z.array(z.array(tableCellValueSchema)),
340
+ rowHeaders: z.boolean().optional(),
341
+ striped: z.boolean().optional(),
342
+ callout: calloutBarSchema.optional(),
343
+ });
344
+ // ─── waterfall ───
345
+ export const waterfallItemSchema = z.object({
346
+ label: z.string(),
347
+ value: z.number(),
348
+ isTotal: z.boolean().optional(),
349
+ color: accentColorKeySchema.optional(),
350
+ });
351
+ export const waterfallSlideSchema = z.object({
352
+ layout: z.literal("waterfall"),
353
+ ...slideBaseFields,
354
+ title: z.string(),
355
+ stepLabel: z.string().optional(),
356
+ subtitle: z.string().optional(),
357
+ items: z.array(waterfallItemSchema),
358
+ unit: z.string().optional(),
359
+ callout: calloutBarSchema.optional(),
360
+ });
361
+ // ─── funnel ───
362
+ export const funnelStageSchema = z.object({
363
+ label: z.string(),
364
+ value: z.string().optional(),
365
+ description: z.string().optional(),
366
+ color: accentColorKeySchema.optional(),
367
+ });
368
+ export const funnelSlideSchema = z.object({
369
+ layout: z.literal("funnel"),
370
+ ...slideBaseFields,
371
+ title: z.string(),
372
+ stepLabel: z.string().optional(),
373
+ subtitle: z.string().optional(),
374
+ stages: z.array(funnelStageSchema),
375
+ callout: calloutBarSchema.optional(),
376
+ });
377
+ // ═══════════════════════════════════════════════════════════
378
+ // Branding — logo & background image overlay
379
+ // ═══════════════════════════════════════════════════════════
380
+ /**
381
+ * Media source for branding assets (self-contained definition to avoid
382
+ * circular dependency with src/types/schema.ts).
383
+ */
384
+ const slideMediaSourceSchema = z.discriminatedUnion("kind", [
385
+ z.object({ kind: z.literal("url"), url: z.url() }).strict(),
386
+ z.object({ kind: z.literal("base64"), data: z.string().min(1) }).strict(),
387
+ z.object({ kind: z.literal("path"), path: z.string().min(1) }).strict(),
388
+ ]);
389
+ const slideBackgroundImageSourceSchema = z.object({
390
+ source: slideMediaSourceSchema,
391
+ size: z.enum(["cover", "contain", "fill", "auto"]).optional(),
392
+ opacity: z.number().min(0).max(1).optional(),
393
+ bgOpacity: z.number().min(0).max(1).optional(),
394
+ });
395
+ export const slideBrandingLogoSchema = z
396
+ .object({
397
+ source: slideMediaSourceSchema,
398
+ position: z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).default("top-right"),
399
+ width: z.number().positive().default(120),
400
+ })
401
+ .strict();
402
+ export const slideBrandingSchema = z
403
+ .object({
404
+ logo: slideBrandingLogoSchema.optional(),
405
+ backgroundImage: slideBackgroundImageSourceSchema.optional(),
406
+ })
407
+ .strict();
408
+ // ═══════════════════════════════════════════════════════════
409
+ // Slide Union & Media Schema
410
+ // ═══════════════════════════════════════════════════════════
411
+ export const slideLayoutSchema = z.discriminatedUnion("layout", [
412
+ titleSlideSchema,
413
+ columnsSlideSchema,
414
+ comparisonSlideSchema,
415
+ gridSlideSchema,
416
+ bigQuoteSlideSchema,
417
+ statsSlideSchema,
418
+ timelineSlideSchema,
419
+ splitSlideSchema,
420
+ matrixSlideSchema,
421
+ tableSlideSchema,
422
+ funnelSlideSchema,
423
+ waterfallSlideSchema,
424
+ ]);
425
+ /** Media schema registered in mulmoImageAssetSchema */
426
+ export const mulmoSlideMediaSchema = z
427
+ .object({
428
+ type: z.literal("slide"),
429
+ theme: slideThemeSchema.optional(),
430
+ slide: slideLayoutSchema,
431
+ reference: z.string().optional(),
432
+ branding: slideBrandingSchema.nullable().optional(),
433
+ })
434
+ .strict();
package/lib/utils.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { SlideTheme, SlideLayout, CalloutBar } from "./schema.js";
2
+ /** Escape HTML special characters */
3
+ export declare const escapeHtml: (s: string) => string;
4
+ /** Escape HTML and convert newlines to <br> */
5
+ export declare const nl2br: (s: string) => string;
6
+ /**
7
+ * Render inline markup: escape HTML first, then parse **bold** and {color:text}.
8
+ * Also converts newlines to <br>.
9
+ * Safe: escapeHtml runs before any markup parsing, so XSS is impossible.
10
+ */
11
+ export declare const renderInlineMarkup: (s: string) => string;
12
+ /** Sanitize a hex color value (hex digits only) */
13
+ export declare const sanitizeHex: (s: string) => string;
14
+ /** Accent color key → Tailwind class segment: "primary" → "d-primary" */
15
+ export declare const c: (key: string) => string;
16
+ /** Resolve accent color key with "primary" as fallback */
17
+ export declare const resolveAccent: (color: string | undefined) => string;
18
+ /** Resolve item-level color with slide-level fallback then "primary" */
19
+ export declare const resolveItemColor: (itemColor: string | undefined, slideAccent: string | undefined) => string;
20
+ /** Render a horizontal accent bar (3px full-width). Pass extraClass for width/margin variants. */
21
+ export declare const accentBar: (colorKey: string, extraClass?: string) => string;
22
+ /** Render an optional block title (chart, mermaid, table) */
23
+ export declare const blockTitle: (title: string | undefined) => string;
24
+ /** Resolve change indicator color: "success" for positive (+), "danger" for negative */
25
+ export declare const resolveChangeColor: (change: string) => string;
26
+ /** Render the optional callout bar at the bottom of a slide, or empty string */
27
+ export declare const renderOptionalCallout: (callout: CalloutBar | undefined) => string;
28
+ /** Build the Tailwind config JSON string for theme colors and fonts */
29
+ export declare const buildTailwindConfig: (theme: SlideTheme) => string;
30
+ /** Render a numbered circle badge */
31
+ export declare const numBadge: (num: number, colorKey: string) => string;
32
+ /** Render an icon in a square container */
33
+ export declare const iconSquare: (icon: string, colorKey: string) => string;
34
+ /** Render a card wrapper with accent top bar */
35
+ export declare const cardWrap: (accentColor: string, innerHtml: string, extraClass?: string) => string;
36
+ /** Render a callout bar at the bottom of a slide */
37
+ export declare const renderCalloutBar: (obj: {
38
+ text: string;
39
+ label?: string;
40
+ color?: string;
41
+ align?: string;
42
+ leftBar?: boolean;
43
+ }) => string;
44
+ /** Render header text elements (stepLabel + title + subtitle) without wrapping div */
45
+ export declare const renderHeaderText: (data: {
46
+ accentColor?: string;
47
+ stepLabel?: string;
48
+ title: string;
49
+ subtitle?: string;
50
+ }) => string;
51
+ /** Render the common slide header (accent bar + title + subtitle) */
52
+ export declare const slideHeader: (data: {
53
+ accentColor?: string;
54
+ stepLabel?: string;
55
+ title: string;
56
+ subtitle?: string;
57
+ }) => string;
58
+ /** Render accent bar + vertically-centered wrapper with header text (used by stats, timeline) */
59
+ export declare const centeredSlideHeader: (data: {
60
+ accentColor?: string;
61
+ stepLabel?: string;
62
+ title: string;
63
+ subtitle?: string;
64
+ }) => string;
65
+ /** Generate a unique ID with the given prefix (e.g. "chart-0", "mermaid-1") */
66
+ export declare const generateSlideId: (prefix: string) => string;
67
+ /** Reset the ID counter (for testing) */
68
+ export declare const resetSlideIdCounter: () => void;
69
+ type BlockTypeFlags = {
70
+ hasChart: boolean;
71
+ hasMermaid: boolean;
72
+ chartPlugins: string[];
73
+ };
74
+ /** Detect whether chart or mermaid content blocks exist in a slide */
75
+ export declare const detectBlockTypes: (slide: SlideLayout) => BlockTypeFlags;
76
+ export {};