tavant-docs-mcp 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/assets/bg-agenda-data.jpeg +0 -0
  3. package/assets/bg-breaker-brain.jpeg +0 -0
  4. package/assets/bg-breaker-cloud.jpeg +0 -0
  5. package/assets/bg-breaker-lines.jpeg +0 -0
  6. package/assets/bg-thankyou.jpeg +0 -0
  7. package/assets/bg-title-tech.jpeg +0 -0
  8. package/assets/cr-image1.png +0 -0
  9. package/assets/decor-cubes.png +0 -0
  10. package/assets/footer-bar.png +0 -0
  11. package/assets/tavant-logo-orange.png +0 -0
  12. package/assets/tavant-logo-small.png +0 -0
  13. package/assets/tavant-logo-white-sm.png +0 -0
  14. package/assets/tavant-logo-white.png +0 -0
  15. package/assets/tavant-template.potx +0 -0
  16. package/brand.js +21 -0
  17. package/index.js +172 -0
  18. package/knowledge/tavant-company.md +181 -0
  19. package/knowledge/tavant-template.md +61 -0
  20. package/package.json +32 -0
  21. package/templates/contract/builders.js +317 -0
  22. package/templates/contract/register.js +213 -0
  23. package/templates/contract/sections.js +73 -0
  24. package/templates/cr/builders.js +286 -0
  25. package/templates/cr/register.js +189 -0
  26. package/templates/cr/sections.js +55 -0
  27. package/templates/msa/builders.js +480 -0
  28. package/templates/msa/register.js +185 -0
  29. package/templates/msa/sections.js +86 -0
  30. package/templates/nda/builders.js +277 -0
  31. package/templates/nda/register.js +185 -0
  32. package/templates/nda/sections.js +73 -0
  33. package/templates/pptx/builders.js +712 -0
  34. package/templates/pptx/layouts.js +168 -0
  35. package/templates/pptx/register.js +363 -0
  36. package/templates/sow/builders.js +294 -0
  37. package/templates/sow/register.js +183 -0
  38. package/templates/sow/sections.js +76 -0
  39. package/test-custom-slide.js +79 -0
  40. package/test-e2e.js +190 -0
  41. package/test-msa.js +48 -0
  42. package/test-nda-cr.js +88 -0
  43. package/test-pptx.js +93 -0
@@ -0,0 +1,712 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const BRAND = require("../../brand");
4
+
5
+ const ASSETS = path.join(__dirname, "..", "..", "assets");
6
+ const img = (name) => path.join(ASSETS, name);
7
+
8
+ // ─── Common elements matching the real template ─────────────────────────
9
+ // Footer: black bar with orange triangle at bottom + "Tavant & Customer Confidential" + slide number
10
+ function addWhiteSlideChrome(slide, pptx) {
11
+ // Footer bar image (black bar with orange triangle)
12
+ if (fs.existsSync(img("footer-bar.png"))) {
13
+ slide.addImage({ path: img("footer-bar.png"), x: 0, y: 6.83, w: 13.33, h: 0.67 });
14
+ }
15
+ // Tavant logo — LEFT bottom (on footer bar)
16
+ if (fs.existsSync(img("tavant-logo-small.png"))) {
17
+ slide.addImage({ path: img("tavant-logo-small.png"), x: 0.29, y: 7.07, w: 1.38, h: 0.38 });
18
+ }
19
+ // Confidential footer text — black, single line
20
+ slide.addText("Tavant & Customer Confidential", {
21
+ x: 9.5, y: 7.15, w: 3.0, h: 0.18,
22
+ fontSize: 10.7, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
23
+ });
24
+ }
25
+
26
+ function addDarkSlideChrome(slide, pptx) {
27
+ if (fs.existsSync(img("footer-bar.png"))) {
28
+ slide.addImage({ path: img("footer-bar.png"), x: 0, y: 6.83, w: 13.33, h: 0.67 });
29
+ }
30
+ // Tavant logo — LEFT bottom (on footer bar)
31
+ if (fs.existsSync(img("tavant-logo-small.png"))) {
32
+ slide.addImage({ path: img("tavant-logo-small.png"), x: 0.29, y: 7.07, w: 1.38, h: 0.38 });
33
+ }
34
+ slide.addText("Tavant & Customer Confidential", {
35
+ x: 9.5, y: 7.15, w: 3.0, h: 0.18,
36
+ fontSize: 10.7, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
37
+ });
38
+ }
39
+
40
+ // Standard title bar (used on most content slides)
41
+ function addTitle(slide, title, opts = {}) {
42
+ slide.addText(title || "", {
43
+ x: opts.x || 0.36, y: opts.y || 0.37, w: opts.w || 12.62, h: 0.39,
44
+ fontSize: 24, bold: true, color: BRAND.colors.black, fontFace: BRAND.font,
45
+ ...opts,
46
+ });
47
+ }
48
+
49
+ function addSubtitle(slide, subtitle, opts = {}) {
50
+ if (!subtitle) return;
51
+ slide.addText(subtitle, {
52
+ x: opts.x || 0.36, y: opts.y || 0.78, w: opts.w || 12.62, h: 0.41,
53
+ fontSize: 18, color: "F77A33", fontFace: BRAND.font,
54
+ ...opts,
55
+ });
56
+ }
57
+
58
+ // ─── Slide Builders ─────────────────────────────────────────────────────
59
+ const slideBuilders = {
60
+
61
+ // ── SLIDE 1: Title Cover ──────────────────────────────────────────────
62
+ title_cover(pptx, data) {
63
+ const slide = pptx.addSlide();
64
+ slide.background = { color: BRAND.colors.black };
65
+ // Background tech imagery (right side)
66
+ if (fs.existsSync(img("bg-title-tech.jpeg"))) {
67
+ slide.addImage({ path: img("bg-title-tech.jpeg"), x: 0, y: 0, w: 13.33, h: 7.50 });
68
+ }
69
+ // Orange Tavant logo (large)
70
+ if (fs.existsSync(img("tavant-logo-orange.png"))) {
71
+ slide.addImage({ path: img("tavant-logo-orange.png"), x: 0.46, y: 5.73, w: 2.0, h: 0.49 });
72
+ }
73
+ // Tagline
74
+ slide.addText("Unlocking the power of AI. Accelerating digital transformation.", {
75
+ x: 0.46, y: 6.30, w: 5.0, h: 0.30,
76
+ fontSize: 10, color: BRAND.colors.orange, fontFace: BRAND.font,
77
+ });
78
+ // Title (up to 3 lines)
79
+ slide.addText(data.title || "PRESENTATION TITLE", {
80
+ x: 0.49, y: 1.91, w: 6.02, h: 1.75,
81
+ fontSize: 40, bold: true, color: BRAND.colors.black, fontFace: BRAND.font,
82
+ });
83
+ // Subtitle
84
+ if (data.subtitle) {
85
+ slide.addText(data.subtitle, {
86
+ x: 0.49, y: 3.91, w: 6.02, h: 0.50,
87
+ fontSize: 18, color: "F77A33", fontFace: BRAND.font,
88
+ });
89
+ }
90
+ // Date
91
+ if (data.date) {
92
+ slide.addText(data.date, {
93
+ x: 0.49, y: 4.67, w: 6.02, h: 0.39,
94
+ fontSize: 18, color: BRAND.colors.black, fontFace: BRAND.font,
95
+ });
96
+ }
97
+ },
98
+
99
+ // ── SLIDE 2: Agenda ───────────────────────────────────────────────────
100
+ agenda(pptx, data) {
101
+ const slide = pptx.addSlide();
102
+ slide.background = { color: "222222" };
103
+ // Background data imagery
104
+ if (fs.existsSync(img("bg-agenda-data.jpeg"))) {
105
+ slide.addImage({ path: img("bg-agenda-data.jpeg"), x: 5.0, y: 0, w: 8.33, h: 7.50 });
106
+ }
107
+ // Decorative cubes top-right
108
+ if (fs.existsSync(img("decor-cubes.png"))) {
109
+ slide.addImage({ path: img("decor-cubes.png"), x: 11.5, y: 0, w: 1.83, h: 1.50 });
110
+ }
111
+ // Tavant logo
112
+ if (fs.existsSync(img("tavant-logo-small.png"))) {
113
+ slide.addImage({ path: img("tavant-logo-small.png"), x: 12.63, y: 7.02, w: 0.28, h: 0.28 });
114
+ }
115
+ // Footer bar
116
+ if (fs.existsSync(img("footer-bar.png"))) {
117
+ slide.addImage({ path: img("footer-bar.png"), x: 0, y: 6.83, w: 13.33, h: 0.67 });
118
+ }
119
+ slide.addText("Tavant & Customer Confidential", {
120
+ x: 9.5, y: 7.15, w: 3.0, h: 0.18,
121
+ fontSize: 10.7, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
122
+ });
123
+ // AGENDA title
124
+ slide.addText("AGENDA", {
125
+ x: 0.40, y: 0.39, w: 3.09, h: 0.79,
126
+ fontSize: 24, bold: true, color: BRAND.colors.orange, fontFace: BRAND.font,
127
+ });
128
+ // Numbered items
129
+ const items = data.items || [];
130
+ const itemTexts = items.map((item, i) => ({
131
+ text: `${String(i + 1).padStart(2, "0")} ${item}`,
132
+ options: {
133
+ fontSize: 20, bold: true, color: BRAND.colors.orange, fontFace: BRAND.font,
134
+ paraSpaceAfter: 14, bullet: false,
135
+ },
136
+ }));
137
+ slide.addText(itemTexts, {
138
+ x: 0.40, y: 1.31, w: 6.87, h: 5.28, valign: "top",
139
+ });
140
+ },
141
+
142
+ // ── SLIDES 3-5: Breaker slides ────────────────────────────────────────
143
+ breaker_ai(pptx, data) { buildBreaker(pptx, data, "bg-breaker-brain.jpeg"); },
144
+ breaker_cloud(pptx, data) { buildBreaker(pptx, data, "bg-breaker-cloud.jpeg"); },
145
+ breaker_abstract(pptx, data) { buildBreaker(pptx, data, "bg-breaker-lines.jpeg"); },
146
+
147
+ // ── SLIDE 6: Blank ────────────────────────────────────────────────────
148
+ blank(pptx, data) {
149
+ const slide = pptx.addSlide();
150
+ slide.background = { color: BRAND.colors.white };
151
+ addWhiteSlideChrome(slide, pptx);
152
+ },
153
+
154
+ // ── SLIDE 7: Title Only (White) ───────────────────────────────────────
155
+ title_only(pptx, data) {
156
+ const slide = pptx.addSlide();
157
+ slide.background = { color: BRAND.colors.white };
158
+ addTitle(slide, data.title);
159
+ addWhiteSlideChrome(slide, pptx);
160
+ },
161
+
162
+ // ── SLIDE 8: Title Only (Dark) ────────────────────────────────────────
163
+ title_only_dark(pptx, data) {
164
+ const slide = pptx.addSlide();
165
+ slide.background = { color: "222222" };
166
+ addTitle(slide, data.title, { color: BRAND.colors.white });
167
+ addDarkSlideChrome(slide, pptx);
168
+ },
169
+
170
+ // ── SLIDE 9: Title + Content (Dark) ───────────────────────────────────
171
+ content_dark(pptx, data) {
172
+ const slide = pptx.addSlide();
173
+ slide.background = { color: "222222" };
174
+ addTitle(slide, data.title, { color: BRAND.colors.white });
175
+ if (data.body) {
176
+ const bodyItems = Array.isArray(data.body)
177
+ ? data.body.map(b => ({
178
+ text: b,
179
+ options: { fontSize: 18, color: "FF8909", fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
180
+ }))
181
+ : [{ text: data.body, options: { fontSize: 18, color: "FF8909", fontFace: BRAND.font } }];
182
+ slide.addText(bodyItems, { x: 0.36, y: 1.07, w: 12.62, h: 5.58, valign: "top" });
183
+ }
184
+ addDarkSlideChrome(slide, pptx);
185
+ },
186
+
187
+ // ── SLIDE 10: Title + Content (White) ─────────────────────────────────
188
+ content(pptx, data) {
189
+ const slide = pptx.addSlide();
190
+ slide.background = { color: BRAND.colors.white };
191
+ addTitle(slide, data.title);
192
+ if (data.body) {
193
+ const bodyItems = Array.isArray(data.body)
194
+ ? data.body.map(b => ({
195
+ text: b,
196
+ options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
197
+ }))
198
+ : [{ text: data.body, options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font } }];
199
+ slide.addText(bodyItems, { x: 0.36, y: 1.07, w: 12.62, h: 5.58, valign: "top" });
200
+ }
201
+ addWhiteSlideChrome(slide, pptx);
202
+ },
203
+
204
+ // ── SLIDE 11: Title + Subtitle ────────────────────────────────────────
205
+ title_subtitle(pptx, data) {
206
+ const slide = pptx.addSlide();
207
+ slide.background = { color: BRAND.colors.white };
208
+ addTitle(slide, data.title);
209
+ addSubtitle(slide, data.subtitle);
210
+ addWhiteSlideChrome(slide, pptx);
211
+ },
212
+
213
+ // ── SLIDE 12: Title + 2-Column Content ────────────────────────────────
214
+ two_column(pptx, data) {
215
+ const slide = pptx.addSlide();
216
+ slide.background = { color: BRAND.colors.white };
217
+ addTitle(slide, data.title);
218
+ addSubtitle(slide, data.subtitle);
219
+
220
+ const buildCol = (content) => {
221
+ if (!content) return [];
222
+ if (Array.isArray(content)) {
223
+ return content.map(b => ({
224
+ text: b,
225
+ options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
226
+ }));
227
+ }
228
+ return [{ text: content, options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font } }];
229
+ };
230
+
231
+ slide.addText(buildCol(data.left_content), { x: 0.35, y: 1.39, w: 6.18, h: 5.27, valign: "top" });
232
+ slide.addText(buildCol(data.right_content), { x: 6.80, y: 1.39, w: 6.18, h: 5.27, valign: "top" });
233
+ addWhiteSlideChrome(slide, pptx);
234
+ },
235
+
236
+ // ── SLIDE 13: Title + Subtitle + Content ──────────────────────────────
237
+ title_subtitle_content(pptx, data) {
238
+ const slide = pptx.addSlide();
239
+ slide.background = { color: BRAND.colors.white };
240
+ addTitle(slide, data.title);
241
+ addSubtitle(slide, data.subtitle);
242
+ if (data.body) {
243
+ const bodyItems = Array.isArray(data.body)
244
+ ? data.body.map(b => ({
245
+ text: b,
246
+ options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
247
+ }))
248
+ : [{ text: data.body, options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font } }];
249
+ slide.addText(bodyItems, { x: 0.35, y: 1.38, w: 12.63, h: 5.30, valign: "top" });
250
+ }
251
+ addWhiteSlideChrome(slide, pptx);
252
+ },
253
+
254
+ // ── SLIDE 14: Multi-Case Study (4 columns, black bg) ─────────────────
255
+ multi_case_study(pptx, data) {
256
+ const slide = pptx.addSlide();
257
+ slide.background = { color: BRAND.colors.black };
258
+ addTitle(slide, data.title, { color: BRAND.colors.white });
259
+ addSubtitle(slide, data.subtitle);
260
+
261
+ const columns = data.columns || [];
262
+ const xPositions = [0.64, 3.76, 6.88, 10.01];
263
+ columns.slice(0, 4).forEach((col, i) => {
264
+ const x = xPositions[i];
265
+ // White card background
266
+ slide.addShape(pptx.ShapeType.rect, {
267
+ x: x - 0.13, y: 1.46, w: 2.95, h: 5.07,
268
+ fill: { color: BRAND.colors.white }, rectRadius: 0.05,
269
+ });
270
+ // Orange accent line
271
+ slide.addShape(pptx.ShapeType.rect, {
272
+ x: x + 1.20, y: 5.02, w: 0.07, h: 0.50,
273
+ fill: { color: BRAND.colors.orange },
274
+ });
275
+ const title = typeof col === "string" ? col : col.title || "";
276
+ const desc = typeof col === "string" ? "" : col.description || "";
277
+ // Text content
278
+ const textItems = [];
279
+ if (title) {
280
+ textItems.push({ text: title, options: { fontSize: 14, bold: true, color: BRAND.colors.black, fontFace: BRAND.font, paraSpaceAfter: 6 } });
281
+ }
282
+ if (desc) {
283
+ textItems.push({ text: desc, options: { fontSize: 12, color: BRAND.colors.darkGray, fontFace: BRAND.font } });
284
+ }
285
+ if (textItems.length) {
286
+ slide.addText(textItems, { x, y: 3.00, w: 2.69, h: 2.59, valign: "top" });
287
+ }
288
+ });
289
+ addDarkSlideChrome(slide, pptx);
290
+ },
291
+
292
+ // ── SLIDE 15: Image + Content A (image right) ────────────────────────
293
+ image_content_a(pptx, data) {
294
+ const slide = pptx.addSlide();
295
+ slide.background = { color: BRAND.colors.white };
296
+ addTitle(slide, data.title, { w: 7.16 });
297
+ addSubtitle(slide, data.subtitle, { w: 7.16 });
298
+ // Image placeholder on right
299
+ slide.addShape(pptx.ShapeType.rect, {
300
+ x: 6.86, y: 1.31, w: 5.72, h: 4.80,
301
+ fill: { color: "E8E8E8" }, rectRadius: 0.05,
302
+ });
303
+ if (data.image_description) {
304
+ slide.addText(data.image_description, {
305
+ x: 7.1, y: 3.0, w: 5.2, h: 1.0,
306
+ fontSize: 12, italic: true, color: BRAND.colors.mediumGray, fontFace: BRAND.font, align: "center",
307
+ });
308
+ }
309
+ // Content left
310
+ if (data.body) {
311
+ const bodyItems = Array.isArray(data.body)
312
+ ? data.body.map(b => ({
313
+ text: b,
314
+ options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
315
+ }))
316
+ : [{ text: data.body, options: { fontSize: 18, color: BRAND.colors.darkGray, fontFace: BRAND.font } }];
317
+ slide.addText(bodyItems, { x: 0.39, y: 1.64, w: 6.13, h: 5.01, valign: "top" });
318
+ }
319
+ addWhiteSlideChrome(slide, pptx);
320
+ },
321
+
322
+ // ── SLIDE 16: Image + Content B (image left, black bg) ───────────────
323
+ image_content_b(pptx, data) {
324
+ const slide = pptx.addSlide();
325
+ slide.background = { color: BRAND.colors.black };
326
+ addTitle(slide, data.title, { color: BRAND.colors.white });
327
+ addSubtitle(slide, data.subtitle);
328
+ // Image placeholder left
329
+ slide.addShape(pptx.ShapeType.rect, {
330
+ x: 0, y: 1.31, w: 4.60, h: 5.52,
331
+ fill: { color: "333333" },
332
+ });
333
+ // Two topic boxes on right
334
+ const topics = [data.topic_1, data.topic_2].filter(Boolean);
335
+ const topicX = [5.12, 9.33];
336
+ topics.forEach((topic, i) => {
337
+ const t = typeof topic === "string" ? { title: topic } : topic;
338
+ const items = [];
339
+ if (t.title) items.push({ text: `0${i + 1}. ${t.title}`, options: { fontSize: 16, bold: true, color: BRAND.colors.black, fontFace: BRAND.font, paraSpaceAfter: 4 } });
340
+ if (t.description) items.push({ text: t.description, options: { fontSize: 12, color: BRAND.colors.darkGray, fontFace: BRAND.font } });
341
+ slide.addShape(pptx.ShapeType.rect, {
342
+ x: topicX[i], y: 2.09, w: 3.59, h: 1.51,
343
+ fill: { color: BRAND.colors.white }, rectRadius: 0.05,
344
+ });
345
+ if (items.length) {
346
+ slide.addText(items, { x: topicX[i] + 0.15, y: 2.15, w: 3.29, h: 1.40, valign: "top" });
347
+ }
348
+ });
349
+ // Body content below
350
+ if (data.body) {
351
+ slide.addText(data.body, {
352
+ x: 4.86, y: 4.07, w: 8.10, h: 2.81,
353
+ fontSize: 16, color: BRAND.colors.orange, fontFace: BRAND.font, valign: "top",
354
+ });
355
+ }
356
+ addDarkSlideChrome(slide, pptx);
357
+ },
358
+
359
+ // ── SLIDE 17: Image + Content Grid (2x3) ─────────────────────────────
360
+ image_grid(pptx, data) {
361
+ const slide = pptx.addSlide();
362
+ slide.background = { color: BRAND.colors.white };
363
+ // Image area at top
364
+ slide.addShape(pptx.ShapeType.rect, {
365
+ x: 0, y: 0, w: 13.33, h: 3.07,
366
+ fill: { color: "E8E8E8" },
367
+ });
368
+ // Title overlaid on image
369
+ slide.addText(data.title || "", {
370
+ x: 1.33, y: 0.68, w: 10.67, h: 0.57,
371
+ fontSize: 40, color: BRAND.colors.white, fontFace: BRAND.font,
372
+ });
373
+ if (data.subtitle) {
374
+ slide.addText(data.subtitle, {
375
+ x: 1.30, y: 1.66, w: 10.68, h: 0.43,
376
+ fontSize: 18, color: BRAND.colors.orange, fontFace: BRAND.font,
377
+ });
378
+ }
379
+ // 2x3 grid
380
+ const items = data.grid_items || [];
381
+ const positions = [
382
+ { x: 0.86, y: 3.40 }, { x: 5.06, y: 3.40 }, { x: 9.26, y: 3.40 },
383
+ { x: 0.86, y: 5.16 }, { x: 5.06, y: 5.16 }, { x: 9.26, y: 5.16 },
384
+ ];
385
+ items.slice(0, 6).forEach((item, i) => {
386
+ const pos = positions[i];
387
+ const t = typeof item === "string" ? { title: item } : item;
388
+ const texts = [];
389
+ if (t.title) texts.push({ text: t.title, options: { fontSize: 16, bold: true, color: BRAND.colors.black, fontFace: BRAND.font, paraSpaceAfter: 4 } });
390
+ if (t.description) texts.push({ text: t.description, options: { fontSize: 12, color: BRAND.colors.darkGray, fontFace: BRAND.font } });
391
+ if (texts.length) {
392
+ slide.addText(texts, { x: pos.x, y: pos.y, w: 3.38, h: 1.35, valign: "top" });
393
+ }
394
+ });
395
+ addWhiteSlideChrome(slide, pptx);
396
+ },
397
+
398
+ // ── SLIDE 18: 3-Column with Images (black bg) ────────────────────────
399
+ three_column_images(pptx, data) {
400
+ const slide = pptx.addSlide();
401
+ slide.background = { color: BRAND.colors.black };
402
+ addTitle(slide, data.title, { color: BRAND.colors.white });
403
+ addSubtitle(slide, data.subtitle);
404
+
405
+ const columns = data.columns || [];
406
+ const xPositions = [0.54, 4.81, 9.07];
407
+ columns.slice(0, 3).forEach((col, i) => {
408
+ const x = xPositions[i];
409
+ // Image placeholder
410
+ slide.addShape(pptx.ShapeType.rect, {
411
+ x, y: 1.92, w: 3.71, h: 1.94,
412
+ fill: { color: "333333" }, rectRadius: 0.05,
413
+ });
414
+ // White card below
415
+ slide.addShape(pptx.ShapeType.rect, {
416
+ x: x - 0.09, y: 4.04, w: 3.90, h: 2.26,
417
+ fill: { color: BRAND.colors.white }, rectRadius: 0.05,
418
+ });
419
+ // Orange vertical accent
420
+ slide.addShape(pptx.ShapeType.rect, {
421
+ x: x + 1.83, y: 2.12, w: 0.06, h: 3.90,
422
+ fill: { color: BRAND.colors.orange },
423
+ });
424
+ const t = typeof col === "string" ? { title: col } : col;
425
+ const texts = [];
426
+ if (t.title) texts.push({ text: `0${i + 1}. ${t.title}`, options: { fontSize: 20, bold: true, color: BRAND.colors.black, fontFace: BRAND.font, paraSpaceAfter: 6 } });
427
+ if (t.description) texts.push({ text: t.description, options: { fontSize: 14, color: BRAND.colors.darkGray, fontFace: BRAND.font } });
428
+ if (texts.length) {
429
+ slide.addText(texts, { x: x + 0.14, y: 4.28, w: 3.48, h: 1.79, valign: "top" });
430
+ }
431
+ });
432
+ addDarkSlideChrome(slide, pptx);
433
+ },
434
+
435
+ // ── SLIDE 19: Content + Chart ─────────────────────────────────────────
436
+ chart(pptx, data) {
437
+ const slide = pptx.addSlide();
438
+ slide.background = { color: "77787B" };
439
+ addTitle(slide, data.title, { color: BRAND.colors.white });
440
+ addSubtitle(slide, data.subtitle);
441
+
442
+ // Content area left
443
+ if (data.body) {
444
+ const bodyItems = Array.isArray(data.body)
445
+ ? data.body.map(b => ({
446
+ text: b,
447
+ options: { fontSize: 18, color: BRAND.colors.white, fontFace: BRAND.font, bullet: { type: "bullet", color: "FF8909" }, paraSpaceAfter: 8 },
448
+ }))
449
+ : [{ text: data.body, options: { fontSize: 18, color: BRAND.colors.white, fontFace: BRAND.font } }];
450
+ slide.addText(bodyItems, { x: 0.36, y: 1.59, w: 5.00, h: 3.46, valign: "top" });
451
+ }
452
+
453
+ // Key takeaway box
454
+ if (data.takeaway) {
455
+ slide.addShape(pptx.ShapeType.rect, {
456
+ x: 0.26, y: 5.36, w: 4.52, h: 1.32,
457
+ fill: { color: BRAND.colors.orange }, rectRadius: 0.08,
458
+ });
459
+ slide.addText(data.takeaway, {
460
+ x: 0.36, y: 5.40, w: 4.32, h: 1.24,
461
+ fontSize: 14, color: BRAND.colors.white, fontFace: BRAND.font, valign: "middle",
462
+ });
463
+ }
464
+
465
+ // Chart area right
466
+ const chartData = data.chart_data || { labels: ["Q1", "Q2", "Q3", "Q4"], values: [30, 45, 60, 80] };
467
+ slide.addShape(pptx.ShapeType.rect, {
468
+ x: 5.82, y: 1.45, w: 7.51, h: 5.32,
469
+ fill: { color: BRAND.colors.white }, rectRadius: 0.05,
470
+ });
471
+ slide.addChart(pptx.ChartType.bar, [{
472
+ name: chartData.series_name || "Series 1",
473
+ labels: chartData.labels,
474
+ values: chartData.values,
475
+ }], {
476
+ x: 6.28, y: 1.61, w: 6.69, h: 4.98,
477
+ showValue: true, chartColors: [BRAND.colors.orange],
478
+ catAxisFontSize: 11, valAxisFontSize: 10, dataLabelFontSize: 10,
479
+ });
480
+ addDarkSlideChrome(slide, pptx);
481
+ },
482
+
483
+ // ── SLIDE 20: Timeline (Vertical / KPI-style) ────────────────────────
484
+ timeline_vertical(pptx, data) {
485
+ const slide = pptx.addSlide();
486
+ slide.background = { color: BRAND.colors.black };
487
+ addTitle(slide, data.title, { w: 10.18, color: BRAND.colors.white });
488
+ addSubtitle(slide, data.subtitle, { w: 10.18 });
489
+
490
+ // Body text area
491
+ if (data.body) {
492
+ slide.addText(data.body, {
493
+ x: 0.36, y: 1.58, w: 10.18, h: 1.77,
494
+ fontSize: 16, color: BRAND.colors.mediumGray, fontFace: BRAND.font, valign: "top",
495
+ });
496
+ }
497
+
498
+ // 3 KPI/stat blocks
499
+ const blocks = data.blocks || [];
500
+ const blockPositions = [
501
+ { x: 0.35, y: 3.54, color: BRAND.colors.white },
502
+ { x: 3.36, y: 3.54, color: "F77A33" },
503
+ { x: 6.36, y: 3.54, color: BRAND.colors.white },
504
+ ];
505
+ blocks.slice(0, 3).forEach((block, i) => {
506
+ const pos = blockPositions[i];
507
+ const bgColor = i === 1 ? BRAND.colors.orange : BRAND.colors.white;
508
+ const textColor = i === 1 ? BRAND.colors.white : BRAND.colors.black;
509
+ slide.addShape(pptx.ShapeType.rect, {
510
+ x: pos.x, y: pos.y, w: 2.80, h: 2.84,
511
+ fill: { color: bgColor }, rectRadius: 0.05,
512
+ });
513
+ const t = typeof block === "string" ? { title: block } : block;
514
+ const texts = [];
515
+ if (t.label) texts.push({ text: t.label, options: { fontSize: 14, bold: true, color: textColor, fontFace: BRAND.font, paraSpaceAfter: 4 } });
516
+ if (t.value) texts.push({ text: t.value, options: { fontSize: 32, bold: true, color: i === 1 ? BRAND.colors.white : BRAND.colors.orange, fontFace: BRAND.font, paraSpaceAfter: 6 } });
517
+ if (t.description) texts.push({ text: t.description, options: { fontSize: 12, color: textColor === BRAND.colors.white ? "DDDDDD" : BRAND.colors.mediumGray, fontFace: BRAND.font } });
518
+ if (texts.length) {
519
+ slide.addText(texts, { x: pos.x + 0.15, y: pos.y + 0.15, w: 2.50, h: 2.54, valign: "top" });
520
+ }
521
+ });
522
+
523
+ // Year highlight on right
524
+ if (data.year_highlight) {
525
+ slide.addText(data.year_highlight, {
526
+ x: 10.29, y: 4.23, w: 3.05, h: 0.62,
527
+ fontSize: 32, bold: true, color: BRAND.colors.orange, fontFace: BRAND.font,
528
+ });
529
+ }
530
+ addDarkSlideChrome(slide, pptx);
531
+ },
532
+
533
+ // ── SLIDE 21: Timeline (Horizontal) ───────────────────────────────────
534
+ timeline_horizontal(pptx, data) {
535
+ const slide = pptx.addSlide();
536
+ slide.background = { color: "F26F26" };
537
+ addTitle(slide, data.title, { color: BRAND.colors.white });
538
+ addSubtitle(slide, data.subtitle, { color: BRAND.colors.white });
539
+
540
+ // Horizontal line
541
+ slide.addShape(pptx.ShapeType.rect, {
542
+ x: 0.5, y: 2.27, w: 12.33, h: 0.05,
543
+ fill: { color: BRAND.colors.white },
544
+ });
545
+
546
+ const milestones = data.milestones || [];
547
+ const count = Math.min(milestones.length, 8);
548
+ const dateXPositions = [0.75, 2.64, 4.34, 6.03, 7.73, 9.43, 11.13, 11.08];
549
+ const dotXPositions = [1.40, 3.10, 4.80, 6.50, 8.20, 9.90, 11.60, 11.60];
550
+
551
+ milestones.slice(0, 8).forEach((ms, i) => {
552
+ const date = typeof ms === "string" ? ms : ms.date || "";
553
+ const label = typeof ms === "string" ? "" : ms.label || ms.description || "";
554
+ const isHighlight = i % 2 === 0;
555
+
556
+ // Date label
557
+ slide.addText(date, {
558
+ x: dateXPositions[i] || (0.75 + i * 1.70), y: 1.61, w: 1.26, h: 0.64,
559
+ fontSize: 18, color: BRAND.colors.white, fontFace: BRAND.font,
560
+ });
561
+
562
+ // Dot on timeline
563
+ const dotColor = isHighlight ? "005CB9" : BRAND.colors.orange;
564
+ slide.addShape(pptx.ShapeType.ellipse, {
565
+ x: dotXPositions[i] || (1.40 + i * 1.70), y: 2.13, w: 0.33, h: 0.33,
566
+ fill: { color: dotColor },
567
+ });
568
+
569
+ // Content block (alternating above/below line)
570
+ if (label) {
571
+ const yPos = isHighlight ? 2.96 : 4.70;
572
+ const textColor = isHighlight ? BRAND.colors.white : "F77A33";
573
+ slide.addText(label, {
574
+ x: (0.23 + i * 1.70), y: yPos, w: 2.42, h: 1.53,
575
+ fontSize: 14, bold: true, color: textColor, fontFace: BRAND.font, valign: "top",
576
+ });
577
+ }
578
+ });
579
+ addDarkSlideChrome(slide, pptx);
580
+ },
581
+
582
+ // ── SLIDE 22: Multi-Quote / Testimonials ──────────────────────────────
583
+ multi_quote(pptx, data) {
584
+ const slide = pptx.addSlide();
585
+ slide.background = { color: BRAND.colors.black };
586
+ addTitle(slide, data.title, { x: 2.16, w: 10.87, color: BRAND.colors.white });
587
+ addSubtitle(slide, data.subtitle, { x: 2.16, w: 10.87 });
588
+
589
+ const quotes = data.quotes || [];
590
+ const yPositions = [1.67, 3.44, 5.20];
591
+ quotes.slice(0, 3).forEach((quote, i) => {
592
+ const y = yPositions[i];
593
+ const q = typeof quote === "string" ? { text: quote } : quote;
594
+ // Logo/image placeholder on left
595
+ slide.addShape(pptx.ShapeType.rect, {
596
+ x: 0.09, y: y - 0.07, w: 2.88, h: 1.17,
597
+ fill: { color: i === 1 ? BRAND.colors.white : "333333" }, rectRadius: 0.05,
598
+ });
599
+ if (q.company) {
600
+ slide.addText(q.company, {
601
+ x: 0.20, y: y, w: 2.66, h: 1.01,
602
+ fontSize: 12, color: i === 1 ? BRAND.colors.black : BRAND.colors.white, fontFace: BRAND.font,
603
+ align: "center", valign: "middle",
604
+ });
605
+ }
606
+ // Quote text on right
607
+ const texts = [];
608
+ if (q.title || q.author) {
609
+ texts.push({ text: `${q.title || q.author || ""}`, options: { fontSize: 16, bold: true, color: i === 1 ? BRAND.colors.white : BRAND.colors.black, fontFace: BRAND.font, paraSpaceAfter: 4 } });
610
+ }
611
+ const quoteText = q.text || q.quote || "";
612
+ if (quoteText) {
613
+ texts.push({ text: quoteText, options: { fontSize: 12, color: i === 1 ? "DDDDDD" : BRAND.colors.darkGray, fontFace: BRAND.font } });
614
+ }
615
+ // Card background for rows 0 and 2
616
+ if (i !== 1) {
617
+ slide.addShape(pptx.ShapeType.rect, {
618
+ x: 3.50, y: y - 0.07, w: 9.40, h: 1.17,
619
+ fill: { color: BRAND.colors.white }, rectRadius: 0.05,
620
+ });
621
+ }
622
+ if (texts.length) {
623
+ slide.addText(texts, {
624
+ x: 3.60, y: y, w: 9.29, h: 1.01, valign: "middle",
625
+ });
626
+ }
627
+ });
628
+ addDarkSlideChrome(slide, pptx);
629
+ },
630
+
631
+ // ── SLIDE 23: Thank You ───────────────────────────────────────────────
632
+ thank_you(pptx, data) {
633
+ const slide = pptx.addSlide();
634
+ slide.background = { color: BRAND.colors.black };
635
+ // Background imagery
636
+ if (fs.existsSync(img("bg-thankyou.jpeg"))) {
637
+ slide.addImage({ path: img("bg-thankyou.jpeg"), x: 0, y: 0, w: 13.33, h: 7.50 });
638
+ }
639
+ // Orange Tavant logo
640
+ if (fs.existsSync(img("tavant-logo-orange.png"))) {
641
+ slide.addImage({ path: img("tavant-logo-orange.png"), x: 0.46, y: 0.40, w: 2.5, h: 0.65 });
642
+ }
643
+ // THANK YOU text
644
+ slide.addText("THANK YOU", {
645
+ x: 3.51, y: 1.62, w: 6.31, h: 1.65,
646
+ fontSize: 60, bold: true, color: BRAND.colors.white, fontFace: BRAND.font,
647
+ });
648
+ // Office locations
649
+ slide.addText("Santa Clara | New York | Dallas | Mexico | Bangalore | Hyderabad | Noida | Pune", {
650
+ x: 0, y: 5.85, w: 13.30, h: 0.77,
651
+ fontSize: 14, color: BRAND.colors.orange, fontFace: BRAND.font, align: "center",
652
+ });
653
+ // Contact info
654
+ const contactParts = [];
655
+ if (data.contact_phone) contactParts.push(data.contact_phone);
656
+ if (data.contact_email) contactParts.push(data.contact_email || "hello@tavant.com");
657
+ if (data.contact_website) contactParts.push(data.contact_website || "www.tavant.com");
658
+ if (contactParts.length === 0) {
659
+ contactParts.push("+1-866-9-TAVANT", "hello@tavant.com", "www.tavant.com");
660
+ }
661
+ slide.addText(contactParts.join(" | "), {
662
+ x: 2.0, y: 6.24, w: 9.33, h: 0.40,
663
+ fontSize: 14, color: BRAND.colors.white, fontFace: BRAND.font, align: "center",
664
+ });
665
+ // Footer
666
+ slide.addText("Tavant & Customer Confidential", {
667
+ x: 5.61, y: 7.12, w: 3.0, h: 0.18,
668
+ fontSize: 10.67, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
669
+ });
670
+ slide.addText("Tavant Proprietary & Confidential", {
671
+ x: 9.5, y: 7.12, w: 3.0, h: 0.18,
672
+ fontSize: 10.67, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
673
+ });
674
+ },
675
+ };
676
+
677
+ // ─── Shared breaker builder ─────────────────────────────────────────────
678
+ function buildBreaker(pptx, data, bgImage) {
679
+ const slide = pptx.addSlide();
680
+ slide.background = { color: BRAND.colors.black };
681
+ // Full-bleed background
682
+ if (fs.existsSync(img(bgImage))) {
683
+ slide.addImage({ path: img(bgImage), x: 0, y: 0, w: 13.33, h: 7.50 });
684
+ }
685
+ // Footer bar
686
+ if (fs.existsSync(img("footer-bar.png"))) {
687
+ slide.addImage({ path: img("footer-bar.png"), x: 0, y: 6.83, w: 13.33, h: 0.67 });
688
+ }
689
+ // Tavant logo
690
+ if (fs.existsSync(img("tavant-logo-small.png"))) {
691
+ slide.addImage({ path: img("tavant-logo-small.png"), x: 12.63, y: 7.02, w: 0.28, h: 0.28 });
692
+ }
693
+ slide.addText("Tavant & Customer Confidential", {
694
+ x: 9.5, y: 7.15, w: 3.0, h: 0.18,
695
+ fontSize: 10.7, color: BRAND.colors.black, fontFace: BRAND.font, wrap: false,
696
+ });
697
+ // Title
698
+ slide.addText(data.title || "SECTION TITLE", {
699
+ x: 0.44, y: 2.78, w: 5.84, h: 0.47,
700
+ fontSize: 24, bold: true, color: "F77A33", fontFace: BRAND.font,
701
+ });
702
+ // Key points below title
703
+ if (data.key_points && data.key_points.length > 0) {
704
+ const items = data.key_points.map(p => ({
705
+ text: p,
706
+ options: { fontSize: 16, color: BRAND.colors.orange, fontFace: BRAND.font, bullet: { type: "bullet", color: BRAND.colors.orange }, paraSpaceAfter: 6 },
707
+ }));
708
+ slide.addText(items, { x: 0.44, y: 3.59, w: 4.18, h: 1.50, valign: "top" });
709
+ }
710
+ }
711
+
712
+ module.exports = slideBuilders;