@heylemon/lemonade 0.1.6 → 0.1.7

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.
@@ -1,218 +1,451 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * Pro Slides — Presentation Creation Engine
4
+ * Takes a JSON slide spec and produces a polished .pptx file.
5
+ *
6
+ * Usage: node create_pptx.js spec.json output.pptx
7
+ *
8
+ * JSON spec format: see references/spec-format.md
9
+ */
10
+
2
11
  import pptxgen from "pptxgenjs";
3
12
  import fs from "node:fs";
4
13
 
14
+ // ═══════════════════════════════════════════
15
+ // DEFAULT THEME
16
+ // ═══════════════════════════════════════════
5
17
  const DEFAULT_THEME = {
6
- primary: "1E2761",
7
- secondary: "CADCFC",
8
- accent: "F5A623",
9
- text_dark: "1E293B",
10
- text_light: "FFFFFF",
11
- text_muted: "64748B",
12
- bg_light: "F8FAFC",
13
- bg_dark: "1E2761",
14
- font_heading: "Arial Black",
15
- font_body: "Arial",
16
- chart_colors: ["2E86AB", "F5A623", "E74C3C", "2ECC71", "9B59B6", "1ABC9C"],
18
+ primary: "1E2761", // Deep navy (backgrounds, title slides)
19
+ secondary: "CADCFC", // Ice blue (accents, highlights)
20
+ accent: "F5A623", // Gold (callouts, key numbers)
21
+ text_dark: "1E293B", // Near-black for body text
22
+ text_light: "FFFFFF", // White text on dark backgrounds
23
+ text_muted: "64748B", // Slate gray for captions
24
+ bg_light: "F8FAFC", // Off-white slide background
25
+ bg_dark: "1E2761", // Dark slide background
26
+ font_heading: "Arial Black",
27
+ font_body: "Arial",
28
+ chart_colors: ["2E86AB", "F5A623", "E74C3C", "2ECC71", "9B59B6", "1ABC9C"],
17
29
  };
18
30
 
31
+ // Typography scale
19
32
  const TYPE = {
20
- title: 36,
21
- subtitle: 18,
22
- h1: 28,
23
- h2: 20,
24
- body: 14,
25
- caption: 10,
26
- stat: 60,
27
- stat_label: 12,
33
+ title: 36,
34
+ subtitle: 18,
35
+ h1: 28,
36
+ h2: 20,
37
+ body: 14,
38
+ caption: 10,
39
+ stat: 60,
40
+ stat_label: 12,
28
41
  };
29
42
 
30
43
  function buildPresentation(spec) {
31
- const theme = { ...DEFAULT_THEME, ...(spec.theme || {}) };
32
- const pres = new pptxgen();
33
- pres.layout = "LAYOUT_16x9";
34
- pres.author = spec.author || "Lemon AI";
35
- pres.title = spec.title || "Presentation";
36
-
37
- const W = 10;
38
- const H = 5.625;
39
- const M = 0.5;
40
- const CW = W - 2 * M;
41
- const totalSlides = spec.slides.length;
42
-
43
- const addSlideNumber = (slide, slideNum) => {
44
- slide.addText(`${slideNum} / ${totalSlides}`, {
45
- x: W - 1.5,
46
- y: H - 0.4,
47
- w: 1,
48
- h: 0.3,
49
- fontSize: 8,
50
- color: theme.text_muted,
51
- align: "right",
52
- fontFace: theme.font_body,
44
+ const theme = { ...DEFAULT_THEME, ...(spec.theme || {}) };
45
+ const pres = new pptxgen();
46
+ pres.layout = "LAYOUT_16x9";
47
+ pres.author = spec.author || "Lemon AI";
48
+ pres.title = spec.title || "Presentation";
49
+
50
+ const W = 10; // Slide width (inches)
51
+ const H = 5.625; // Slide height (inches)
52
+ const M = 0.5; // Margin
53
+ const CW = W - 2 * M; // Content width
54
+
55
+ // ── Helper: fresh shadow object (PptxGenJS mutates objects) ──
56
+ const cardShadow = () => ({
57
+ type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.1
53
58
  });
54
- };
55
-
56
- spec.slides.forEach((slideDef, idx) => {
57
- const slide = pres.addSlide();
58
- const type = slideDef.type || "content";
59
- const slideNum = idx + 1;
60
-
61
- if (type === "title") {
62
- slide.background = { color: theme.bg_dark };
63
- slide.addText(slideDef.title || "Presentation Title", {
64
- x: M,
65
- y: H * 0.3,
66
- w: CW,
67
- h: 1.5,
68
- fontSize: TYPE.title,
69
- fontFace: theme.font_heading,
70
- color: theme.text_light,
71
- bold: true,
72
- });
73
- if (slideDef.subtitle) {
74
- slide.addText(slideDef.subtitle, {
75
- x: M,
76
- y: H * 0.3 + 1.5,
77
- w: CW,
78
- h: 0.6,
79
- fontSize: TYPE.subtitle,
80
- fontFace: theme.font_body,
81
- color: theme.secondary,
82
- });
83
- }
84
- } else if (type === "section") {
85
- slide.background = { color: theme.primary };
86
- slide.addText(slideDef.title || "Section", {
87
- x: M,
88
- y: H * 0.35,
89
- w: CW,
90
- h: 1,
91
- fontSize: TYPE.h1,
92
- fontFace: theme.font_heading,
93
- color: theme.text_light,
94
- bold: true,
95
- });
96
- } else if (type === "stats") {
97
- slide.background = { color: theme.bg_light };
98
- if (slideDef.title) {
99
- slide.addText(slideDef.title, {
100
- x: M,
101
- y: M,
102
- w: CW,
103
- h: 0.6,
104
- fontSize: TYPE.h2,
105
- fontFace: theme.font_heading,
106
- color: theme.text_dark,
107
- bold: true,
108
- });
109
- }
110
- const stats = slideDef.stats || [];
111
- const count = Math.max(1, stats.length);
112
- const gap = 0.3;
113
- const cardW = (CW - gap * (count - 1)) / count;
114
- const yStart = 1.4;
115
- stats.forEach((stat, i) => {
116
- const x = M + i * (cardW + gap);
117
- slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
118
- x,
119
- y: yStart,
120
- w: cardW,
121
- h: 2.5,
122
- fill: { color: "FFFFFF" },
59
+
60
+ // ── Helper: add slide number ──
61
+ function addSlideNumber(slide, slideNum, total) {
62
+ slide.addText(`${slideNum} / ${total}`, {
63
+ x: W - 1.5, y: H - 0.4, w: 1, h: 0.3,
64
+ fontSize: 8, color: theme.text_muted, align: "right",
65
+ fontFace: theme.font_body,
123
66
  });
124
- slide.addText(stat.value || "0", {
125
- x,
126
- y: yStart + 0.4,
127
- w: cardW,
128
- h: 1,
129
- fontSize: TYPE.stat,
130
- fontFace: theme.font_heading,
131
- color: theme.chart_colors[i % theme.chart_colors.length],
132
- bold: true,
133
- align: "center",
67
+ }
68
+
69
+ // ── Count total slides for numbering ──
70
+ const totalSlides = spec.slides.length;
71
+
72
+ // ── Process each slide ──
73
+ spec.slides.forEach((slideDef, idx) => {
74
+ const slide = pres.addSlide();
75
+ const type = slideDef.type || "content";
76
+ const slideNum = idx + 1;
77
+
78
+ switch (type) {
79
+ case "title":
80
+ buildTitleSlide(slide, slideDef, theme, W, H, M, CW, pres);
81
+ break;
82
+ case "section":
83
+ buildSectionSlide(slide, slideDef, theme, W, H, M, CW);
84
+ break;
85
+ case "content":
86
+ buildContentSlide(slide, slideDef, theme, W, H, M, CW, pres, cardShadow);
87
+ addSlideNumber(slide, slideNum, totalSlides);
88
+ break;
89
+ case "two_column":
90
+ buildTwoColumnSlide(slide, slideDef, theme, W, H, M, CW, pres, cardShadow);
91
+ addSlideNumber(slide, slideNum, totalSlides);
92
+ break;
93
+ case "stats":
94
+ buildStatsSlide(slide, slideDef, theme, W, H, M, CW, pres);
95
+ addSlideNumber(slide, slideNum, totalSlides);
96
+ break;
97
+ case "comparison":
98
+ buildComparisonSlide(slide, slideDef, theme, W, H, M, CW, pres, cardShadow);
99
+ addSlideNumber(slide, slideNum, totalSlides);
100
+ break;
101
+ case "closing":
102
+ buildClosingSlide(slide, slideDef, theme, W, H, M, CW);
103
+ break;
104
+ default:
105
+ buildContentSlide(slide, slideDef, theme, W, H, M, CW, pres, cardShadow);
106
+ addSlideNumber(slide, slideNum, totalSlides);
107
+ }
108
+
109
+ // Speaker notes
110
+ if (slideDef.notes) {
111
+ slide.addNotes(slideDef.notes);
112
+ }
113
+ });
114
+
115
+ return pres;
116
+ }
117
+
118
+ // ═══════════════════════════════════════════
119
+ // SLIDE BUILDERS
120
+ // ═══════════════════════════════════════════
121
+
122
+ function buildTitleSlide(slide, def, theme, W, H, M, CW, pres) {
123
+ slide.background = { color: theme.bg_dark };
124
+
125
+ // Accent bar at top
126
+ slide.addShape(pres.shapes.RECTANGLE, {
127
+ x: 0, y: 0, w: W, h: 0.06, fill: { color: theme.accent }
128
+ });
129
+
130
+ // Title
131
+ slide.addText(def.title || "Presentation Title", {
132
+ x: M, y: H * 0.3, w: CW, h: 1.5,
133
+ fontSize: TYPE.title, fontFace: theme.font_heading,
134
+ color: theme.text_light, bold: true, align: "left", valign: "middle",
135
+ });
136
+
137
+ // Subtitle
138
+ if (def.subtitle) {
139
+ slide.addText(def.subtitle, {
140
+ x: M, y: H * 0.3 + 1.5, w: CW, h: 0.6,
141
+ fontSize: TYPE.subtitle, fontFace: theme.font_body,
142
+ color: theme.secondary, align: "left",
134
143
  });
135
- slide.addText(stat.label || "", {
136
- x: x + 0.2,
137
- y: yStart + 1.5,
138
- w: cardW - 0.4,
139
- h: 0.8,
140
- fontSize: TYPE.stat_label,
141
- fontFace: theme.font_body,
142
- color: theme.text_muted,
143
- align: "center",
144
- });
145
- });
146
- addSlideNumber(slide, slideNum);
147
- } else {
148
- slide.background = { color: theme.bg_light };
149
- let y = M;
150
- if (slideDef.title) {
151
- slide.addText(slideDef.title, {
152
- x: M,
153
- y,
154
- w: CW,
155
- h: 0.6,
156
- fontSize: TYPE.h2,
157
- fontFace: theme.font_heading,
158
- color: theme.text_dark,
159
- bold: true,
160
- });
161
- y += 0.9;
162
- }
163
- if (slideDef.content) {
164
- const text = Array.isArray(slideDef.content)
165
- ? slideDef.content.join("\n\n")
166
- : slideDef.content;
144
+ }
145
+
146
+ // Author / date at bottom
147
+ const meta = [def.author, def.date].filter(Boolean).join(" · ");
148
+ if (meta) {
149
+ slide.addText(meta, {
150
+ x: M, y: H - 0.8, w: CW, h: 0.4,
151
+ fontSize: TYPE.caption, fontFace: theme.font_body,
152
+ color: theme.text_muted, align: "left",
153
+ });
154
+ }
155
+ }
156
+
157
+ function buildSectionSlide(slide, def, theme, W, H, M, CW) {
158
+ slide.background = { color: theme.primary };
159
+
160
+ slide.addText(def.title || "Section", {
161
+ x: M, y: H * 0.35, w: CW, h: 1,
162
+ fontSize: TYPE.h1, fontFace: theme.font_heading,
163
+ color: theme.text_light, bold: true, align: "left",
164
+ });
165
+
166
+ if (def.subtitle) {
167
+ slide.addText(def.subtitle, {
168
+ x: M, y: H * 0.35 + 1.1, w: CW * 0.7, h: 0.8,
169
+ fontSize: TYPE.body, fontFace: theme.font_body,
170
+ color: theme.secondary, align: "left",
171
+ });
172
+ }
173
+ }
174
+
175
+ function buildContentSlide(slide, def, theme, W, H, M, CW, pres, cardShadow) {
176
+ slide.background = { color: theme.bg_light };
177
+
178
+ let yPos = M;
179
+
180
+ // Title
181
+ if (def.title) {
182
+ slide.addText(def.title, {
183
+ x: M, y: yPos, w: CW, h: 0.6, margin: 0,
184
+ fontSize: TYPE.h2, fontFace: theme.font_heading,
185
+ color: theme.text_dark, bold: true, align: "left",
186
+ });
187
+ // Accent underline
188
+ slide.addShape(pres.shapes.RECTANGLE, {
189
+ x: M, y: yPos + 0.6, w: 0.8, h: 0.04, fill: { color: theme.accent }
190
+ });
191
+ yPos += 0.9;
192
+ }
193
+
194
+ // Body text
195
+ if (def.content) {
196
+ const text = Array.isArray(def.content) ? def.content.join("\n\n") : def.content;
167
197
  slide.addText(text, {
168
- x: M,
169
- y,
170
- w: CW,
171
- h: 1.4,
172
- fontSize: TYPE.body,
173
- fontFace: theme.font_body,
174
- color: theme.text_dark,
175
- });
176
- y += 1.5;
177
- }
178
- if (slideDef.items?.length) {
179
- slide.addText(
180
- slideDef.items.map((item, i) => ({
198
+ x: M, y: yPos, w: CW, h: 1.2,
199
+ fontSize: TYPE.body, fontFace: theme.font_body,
200
+ color: theme.text_dark, align: "left", valign: "top",
201
+ lineSpacingMultiple: 1.3,
202
+ });
203
+ yPos += 1.3;
204
+ }
205
+
206
+ // Bullet items
207
+ if (def.items && def.items.length > 0) {
208
+ const bulletText = def.items.map((item, i) => ({
181
209
  text: item,
182
- options: { bullet: true, breakLine: i < slideDef.items.length - 1 },
183
- })),
184
- {
185
- x: M + 0.1,
186
- y,
187
- w: CW - 0.2,
188
- h: H - y - 0.5,
189
- fontSize: TYPE.body,
210
+ options: { bullet: true, breakLine: i < def.items.length - 1 }
211
+ }));
212
+ slide.addText(bulletText, {
213
+ x: M + 0.2, y: yPos, w: CW - 0.4, h: H - yPos - 0.6,
214
+ fontSize: TYPE.body, fontFace: theme.font_body,
215
+ color: theme.text_dark, valign: "top",
216
+ paraSpaceAfter: 6,
217
+ });
218
+ }
219
+
220
+ // Table
221
+ if (def.table) {
222
+ const { headers, rows } = def.table;
223
+ const tableData = [];
224
+ if (headers) {
225
+ tableData.push(headers.map(h => ({
226
+ text: h, options: { bold: true, color: theme.text_light, fill: { color: theme.primary }, fontSize: 11 }
227
+ })));
228
+ }
229
+ rows.forEach((row, ri) => {
230
+ tableData.push(row.map(cell => ({
231
+ text: String(cell), options: {
232
+ fontSize: 11,
233
+ fill: { color: ri % 2 === 0 ? "F1F5F9" : "FFFFFF" }
234
+ }
235
+ })));
236
+ });
237
+ const colW = new Array(headers ? headers.length : rows[0].length).fill(CW / (headers ? headers.length : rows[0].length));
238
+ slide.addTable(tableData, {
239
+ x: M, y: yPos, w: CW, colW,
240
+ border: { pt: 0.5, color: "E2E8F0" },
190
241
  fontFace: theme.font_body,
191
- color: theme.text_dark,
192
- },
193
- );
194
- }
195
- addSlideNumber(slide, slideNum);
242
+ });
243
+ }
244
+ }
245
+
246
+ function buildTwoColumnSlide(slide, def, theme, W, H, M, CW, pres, cardShadow) {
247
+ slide.background = { color: theme.bg_light };
248
+
249
+ // Title
250
+ if (def.title) {
251
+ slide.addText(def.title, {
252
+ x: M, y: M, w: CW, h: 0.6, margin: 0,
253
+ fontSize: TYPE.h2, fontFace: theme.font_heading,
254
+ color: theme.text_dark, bold: true,
255
+ });
256
+ slide.addShape(pres.shapes.RECTANGLE, {
257
+ x: M, y: M + 0.6, w: 0.8, h: 0.04, fill: { color: theme.accent }
258
+ });
196
259
  }
197
260
 
198
- if (slideDef.notes) {
199
- slide.addNotes(slideDef.notes);
261
+ const colWidth = (CW - 0.3) / 2;
262
+ const yStart = 1.2;
263
+
264
+ // Left column
265
+ if (def.left) {
266
+ const leftContent = Array.isArray(def.left) ? def.left : [def.left];
267
+ slide.addText(leftContent.map((item, i) => ({
268
+ text: item,
269
+ options: { bullet: def.left_bullets !== false, breakLine: i < leftContent.length - 1 }
270
+ })), {
271
+ x: M, y: yStart, w: colWidth, h: H - yStart - 0.5,
272
+ fontSize: TYPE.body, fontFace: theme.font_body,
273
+ color: theme.text_dark, valign: "top", paraSpaceAfter: 8,
274
+ });
275
+ }
276
+
277
+ // Right column
278
+ if (def.right) {
279
+ const rightContent = Array.isArray(def.right) ? def.right : [def.right];
280
+ slide.addText(rightContent.map((item, i) => ({
281
+ text: item,
282
+ options: { bullet: def.right_bullets !== false, breakLine: i < rightContent.length - 1 }
283
+ })), {
284
+ x: M + colWidth + 0.3, y: yStart, w: colWidth, h: H - yStart - 0.5,
285
+ fontSize: TYPE.body, fontFace: theme.font_body,
286
+ color: theme.text_dark, valign: "top", paraSpaceAfter: 8,
287
+ });
200
288
  }
201
- });
289
+ }
290
+
291
+ function buildStatsSlide(slide, def, theme, W, H, M, CW, pres) {
292
+ slide.background = { color: theme.bg_light };
293
+
294
+ // Title
295
+ if (def.title) {
296
+ slide.addText(def.title, {
297
+ x: M, y: M, w: CW, h: 0.6, margin: 0,
298
+ fontSize: TYPE.h2, fontFace: theme.font_heading,
299
+ color: theme.text_dark, bold: true,
300
+ });
301
+ }
302
+
303
+ const stats = def.stats || [];
304
+ const count = stats.length;
305
+ const gap = 0.3;
306
+ const cardW = (CW - gap * (count - 1)) / count;
307
+ const yStart = 1.4;
308
+
309
+ stats.forEach((stat, i) => {
310
+ const xPos = M + i * (cardW + gap);
202
311
 
203
- return pres;
312
+ // Card background
313
+ slide.addShape(pres.shapes.RECTANGLE, {
314
+ x: xPos, y: yStart, w: cardW, h: 2.5,
315
+ fill: { color: "FFFFFF" },
316
+ shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.08 },
317
+ });
318
+
319
+ // Accent bar at top of card
320
+ slide.addShape(pres.shapes.RECTANGLE, {
321
+ x: xPos, y: yStart, w: cardW, h: 0.06,
322
+ fill: { color: theme.chart_colors[i % theme.chart_colors.length] }
323
+ });
324
+
325
+ // Big number
326
+ slide.addText(stat.value || "0", {
327
+ x: xPos, y: yStart + 0.4, w: cardW, h: 1,
328
+ fontSize: TYPE.stat, fontFace: theme.font_heading,
329
+ color: theme.chart_colors[i % theme.chart_colors.length],
330
+ bold: true, align: "center", valign: "middle",
331
+ });
332
+
333
+ // Label
334
+ slide.addText(stat.label || "", {
335
+ x: xPos + 0.2, y: yStart + 1.5, w: cardW - 0.4, h: 0.8,
336
+ fontSize: TYPE.stat_label, fontFace: theme.font_body,
337
+ color: theme.text_muted, align: "center", valign: "top",
338
+ });
339
+ });
204
340
  }
205
341
 
342
+ function buildComparisonSlide(slide, def, theme, W, H, M, CW, pres, cardShadow) {
343
+ slide.background = { color: theme.bg_light };
344
+
345
+ if (def.title) {
346
+ slide.addText(def.title, {
347
+ x: M, y: M, w: CW, h: 0.6, margin: 0,
348
+ fontSize: TYPE.h2, fontFace: theme.font_heading,
349
+ color: theme.text_dark, bold: true,
350
+ });
351
+ }
352
+
353
+ const colWidth = (CW - 0.4) / 2;
354
+ const yStart = 1.2;
355
+ const colH = H - yStart - 0.5;
356
+
357
+ // Left column (option A)
358
+ slide.addShape(pres.shapes.RECTANGLE, {
359
+ x: M, y: yStart, w: colWidth, h: colH,
360
+ fill: { color: "FFFFFF" },
361
+ shadow: { type: "outer", color: "000000", blur: 4, offset: 1, angle: 135, opacity: 0.08 },
362
+ });
363
+ slide.addShape(pres.shapes.RECTANGLE, {
364
+ x: M, y: yStart, w: colWidth, h: 0.06, fill: { color: theme.chart_colors[0] }
365
+ });
366
+ slide.addText(def.option_a_title || "Option A", {
367
+ x: M + 0.2, y: yStart + 0.2, w: colWidth - 0.4, h: 0.5,
368
+ fontSize: TYPE.h2 - 2, fontFace: theme.font_heading, color: theme.text_dark, bold: true,
369
+ });
370
+ if (def.option_a) {
371
+ slide.addText(def.option_a.map((item, i) => ({
372
+ text: item, options: { bullet: true, breakLine: i < def.option_a.length - 1 }
373
+ })), {
374
+ x: M + 0.2, y: yStart + 0.8, w: colWidth - 0.4, h: colH - 1,
375
+ fontSize: TYPE.body - 1, fontFace: theme.font_body, color: theme.text_dark,
376
+ paraSpaceAfter: 6, valign: "top",
377
+ });
378
+ }
379
+
380
+ // Right column (option B)
381
+ const xRight = M + colWidth + 0.4;
382
+ slide.addShape(pres.shapes.RECTANGLE, {
383
+ x: xRight, y: yStart, w: colWidth, h: colH,
384
+ fill: { color: "FFFFFF" },
385
+ shadow: { type: "outer", color: "000000", blur: 4, offset: 1, angle: 135, opacity: 0.08 },
386
+ });
387
+ slide.addShape(pres.shapes.RECTANGLE, {
388
+ x: xRight, y: yStart, w: colWidth, h: 0.06, fill: { color: theme.chart_colors[1] }
389
+ });
390
+ slide.addText(def.option_b_title || "Option B", {
391
+ x: xRight + 0.2, y: yStart + 0.2, w: colWidth - 0.4, h: 0.5,
392
+ fontSize: TYPE.h2 - 2, fontFace: theme.font_heading, color: theme.text_dark, bold: true,
393
+ });
394
+ if (def.option_b) {
395
+ slide.addText(def.option_b.map((item, i) => ({
396
+ text: item, options: { bullet: true, breakLine: i < def.option_b.length - 1 }
397
+ })), {
398
+ x: xRight + 0.2, y: yStart + 0.8, w: colWidth - 0.4, h: colH - 1,
399
+ fontSize: TYPE.body - 1, fontFace: theme.font_body, color: theme.text_dark,
400
+ paraSpaceAfter: 6, valign: "top",
401
+ });
402
+ }
403
+ }
404
+
405
+ function buildClosingSlide(slide, def, theme, W, H, M, CW) {
406
+ slide.background = { color: theme.bg_dark };
407
+
408
+ slide.addText(def.title || "Thank You", {
409
+ x: M, y: H * 0.3, w: CW, h: 1,
410
+ fontSize: TYPE.h1, fontFace: theme.font_heading,
411
+ color: theme.text_light, bold: true, align: "center",
412
+ });
413
+
414
+ if (def.subtitle) {
415
+ slide.addText(def.subtitle, {
416
+ x: M, y: H * 0.3 + 1.2, w: CW, h: 0.6,
417
+ fontSize: TYPE.body, fontFace: theme.font_body,
418
+ color: theme.secondary, align: "center",
419
+ });
420
+ }
421
+
422
+ if (def.contact) {
423
+ slide.addText(def.contact, {
424
+ x: M, y: H - 1, w: CW, h: 0.4,
425
+ fontSize: TYPE.caption, fontFace: theme.font_body,
426
+ color: theme.text_muted, align: "center",
427
+ });
428
+ }
429
+ }
430
+
431
+ // ═══════════════════════════════════════════
432
+ // MAIN
433
+ // ═══════════════════════════════════════════
434
+
206
435
  if (process.argv.length < 4) {
207
- console.log("Usage: node create_pptx.js spec.json output.pptx");
208
- process.exit(1);
436
+ console.log("Usage: node create_pptx.js spec.json output.pptx");
437
+ process.exit(1);
209
438
  }
210
439
 
211
440
  const specPath = process.argv[2];
212
441
  const outputPath = process.argv[3];
442
+
213
443
  const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
214
444
  const pres = buildPresentation(spec);
445
+
215
446
  pres.writeFile({ fileName: outputPath }).then(() => {
216
- console.log(`Presentation created: ${outputPath}`);
217
- console.log(` Slides: ${spec.slides.length}`);
447
+ console.log(`Presentation created: ${outputPath}`);
448
+ console.log(` Title: ${spec.title || "Untitled"}`);
449
+ console.log(` Slides: ${spec.slides.length}`);
450
+ console.log(` Theme: ${spec.theme ? "custom" : "default (Midnight Executive)"}`);
218
451
  });