@heylemon/lemonade 0.1.6 → 0.1.8

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.
@@ -0,0 +1,666 @@
1
+ # PptxGenJS API Tutorial
2
+
3
+ PptxGenJS is a JavaScript library for generating PowerPoint presentations programmatically. This guide covers the most practical patterns for building professional slides.
4
+
5
+ ## Setup & Basic Structure
6
+
7
+ Create a new presentation and configure it:
8
+
9
+ ```javascript
10
+ const PptxGenJS = require("pptxgenjs");
11
+ const pres = new PptxGenJS();
12
+
13
+ pres.layout = "LAYOUT_16x9";
14
+ pres.author = "Your Name";
15
+ pres.title = "Presentation Title";
16
+ pres.subject = "Subtitle or description";
17
+
18
+ // Add slides
19
+ const slide1 = pres.addSlide();
20
+
21
+ // Save to file
22
+ pres.writeFile({ fileName: "output.pptx" });
23
+ ```
24
+
25
+ Key point: Always create a fresh `PptxGenJS()` instance per presentation. Don't reuse across multiple presentations.
26
+
27
+ ## Layout Dimensions
28
+
29
+ Standard 16:9 layout is 10 inches wide by 5.625 inches tall. All positioning is in inches.
30
+
31
+ ```javascript
32
+ // Slide area — HARD LIMITS (nothing may exceed these)
33
+ const W = 10; // Width (inches)
34
+ const H = 5.625; // Height (inches)
35
+ const M = 0.5; // Margin (standard)
36
+ const contentWidth = W - 2 * M; // 9 inches usable
37
+
38
+ // Common positioning
39
+ const titleY = M; // Top margin
40
+ const contentStartY = M + 0.8; // After title (y = 1.3)
41
+ const maxContentY = H - M; // 5.125 — absolute bottom of content
42
+ const contentHeight = maxContentY - contentStartY; // ~3.8" available
43
+
44
+ // BOUNDARY CHECK — use before placing every element:
45
+ // if (elementY + elementH > H) → content will overflow, reduce or split slide
46
+ ```
47
+
48
+ **CRITICAL: Every element must satisfy `y + h ≤ 5.625`.** If an element's bottom edge exceeds the slide height, it will be cut off in presentation mode. Always calculate remaining space before placing elements:
49
+ ```javascript
50
+ const remainingHeight = H - currentY - M; // available space below current position
51
+ if (cardHeight > remainingHeight) {
52
+ // Split to next slide or reduce content
53
+ }
54
+ ```
55
+
56
+ ## Text & Formatting
57
+
58
+ ### Basic Text
59
+
60
+ ```javascript
61
+ slide.addText("Hello World", {
62
+ x: 0.5, y: 1, w: 9, h: 0.5,
63
+ fontSize: 16,
64
+ fontFace: "Arial",
65
+ color: "000000",
66
+ bold: true,
67
+ align: "left",
68
+ valign: "top"
69
+ });
70
+ ```
71
+
72
+ ### Multi-line Text with Rich Formatting
73
+
74
+ Use an array to mix formatted segments:
75
+
76
+ ```javascript
77
+ slide.addText([
78
+ { text: "This is bold", options: { bold: true } },
79
+ { text: " and this is ", options: {} },
80
+ { text: "italic", options: { italic: true } }
81
+ ], {
82
+ x: 0.5, y: 2, w: 9, h: 1,
83
+ fontSize: 14,
84
+ fontFace: "Arial",
85
+ color: "1E293B",
86
+ align: "left"
87
+ });
88
+ ```
89
+
90
+ ### Bullets & Line Breaks
91
+
92
+ Always use `bullet: true` with `breakLine: true` between items:
93
+
94
+ ```javascript
95
+ slide.addText([
96
+ { text: "First point", options: { bullet: true, breakLine: true } },
97
+ { text: "Second point", options: { bullet: true, breakLine: true } },
98
+ { text: "Third point", options: { bullet: true, breakLine: false } } // Last item
99
+ ], {
100
+ x: 0.7, y: 2, w: 8.6, h: 2,
101
+ fontSize: 14,
102
+ fontFace: "Arial",
103
+ color: "1E293B",
104
+ paraSpaceAfter: 6, // Space after each paragraph
105
+ valign: "top"
106
+ });
107
+ ```
108
+
109
+ **Never use Unicode bullets** like `•` or `◦`. Always use `bullet: true`.
110
+
111
+ ### Text Block with Padding
112
+
113
+ Use `margin` to add internal padding:
114
+
115
+ ```javascript
116
+ slide.addText("This text has padding", {
117
+ x: 1, y: 1, w: 8, h: 2,
118
+ fontSize: 14,
119
+ margin: [0.2, 0.3, 0.2, 0.3], // [top, right, bottom, left]
120
+ align: "left"
121
+ });
122
+ ```
123
+
124
+ ## Shapes
125
+
126
+ ### Rectangle
127
+
128
+ ```javascript
129
+ slide.addShape(pres.shapes.RECTANGLE, {
130
+ x: 1, y: 1, w: 4, h: 2,
131
+ fill: { color: "E74C3C" },
132
+ line: { color: "C0392B", width: 2 }, // Border
133
+ shadow: {
134
+ type: "outer",
135
+ color: "000000",
136
+ blur: 6,
137
+ offset: 2,
138
+ angle: 135,
139
+ opacity: 0.1
140
+ }
141
+ });
142
+ ```
143
+
144
+ ### Oval / Circle
145
+
146
+ ```javascript
147
+ slide.addShape(pres.shapes.OVAL, {
148
+ x: 1, y: 1, w: 1.5, h: 1.5, // Width and height equal = circle
149
+ fill: { color: "F5A623" }
150
+ });
151
+ ```
152
+
153
+ ### Rounded Rectangle
154
+
155
+ ```javascript
156
+ slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
157
+ x: 1, y: 1, w: 4, h: 2,
158
+ fill: { color: "FFFFFF" },
159
+ rectRadius: 0.1, // Corner radius in inches
160
+ shadow: { type: "outer", color: "000000", blur: 4, offset: 1, angle: 135, opacity: 0.08 }
161
+ });
162
+ ```
163
+
164
+ ### Line
165
+
166
+ ```javascript
167
+ slide.addShape(pres.shapes.LINE, {
168
+ x: 1, y: 2, w: 4, h: 0, // Height 0 for horizontal line
169
+ line: { color: "F5A623", width: 3 }
170
+ });
171
+ ```
172
+
173
+ ### Shape Properties Reference
174
+
175
+ | Property | Type | Notes |
176
+ |---|---|---|
177
+ | `x, y, w, h` | number | Position (inches) and size |
178
+ | `fill` | object | `{ color: "HEX" }` or `{ type: "solid", color: "HEX", transparency: 0 }` |
179
+ | `line` | object | `{ color: "HEX", width: 2, dashType: "solid" \| "dash" }` |
180
+ | `shadow` | object | `{ type: "outer", color, blur, offset, angle, opacity }` |
181
+ | `transparency` | number | 0-100. Use `fill.transparency`, not color string. |
182
+ | `rotate` | number | Degrees (0-360) |
183
+
184
+ ## Images
185
+
186
+ ### From File Path
187
+
188
+ ```javascript
189
+ slide.addImage({
190
+ path: "/path/to/image.jpg",
191
+ x: 5, y: 1, w: 4, h: 2.5
192
+ });
193
+ ```
194
+
195
+ ### From URL
196
+
197
+ ```javascript
198
+ slide.addImage({
199
+ url: "https://example.com/logo.png",
200
+ x: 5, y: 1, w: 4, h: 2.5
201
+ });
202
+ ```
203
+
204
+ ### From Base64
205
+
206
+ ```javascript
207
+ const base64Data = "data:image/png;base64,iVBORw0KGgo...";
208
+ slide.addImage({
209
+ data: base64Data,
210
+ x: 5, y: 1, w: 4, h: 2.5
211
+ });
212
+ ```
213
+
214
+ ### Sizing Modes
215
+
216
+ ```javascript
217
+ slide.addImage({
218
+ path: "/image.jpg",
219
+ x: 1, y: 1, w: 4, h: 2.5,
220
+ sizing: {
221
+ type: "cover", // "cover" = crop to fill, "contain" = fit inside
222
+ w: 4,
223
+ h: 2.5
224
+ }
225
+ });
226
+ ```
227
+
228
+ ## Icons
229
+
230
+ Convert React Icons to base64 PNG and embed:
231
+
232
+ ```javascript
233
+ // 1. Generate SVG from react-icons, save as .svg
234
+ // 2. Convert to PNG with sharp: sharp('icon.svg').png().toFile('icon.png')
235
+ // 3. Convert PNG to base64
236
+ const fs = require('fs');
237
+ const iconBase64 = fs.readFileSync('icon.png', 'base64');
238
+ const iconDataUri = `data:image/png;base64,${iconBase64}`;
239
+
240
+ // 4. Add to slide
241
+ slide.addShape(pres.shapes.OVAL, {
242
+ x: 1, y: 1, w: 0.4, h: 0.4,
243
+ fill: { color: "F5A623" }
244
+ });
245
+ slide.addImage({
246
+ data: iconDataUri,
247
+ x: 1.05, y: 1.05, w: 0.3, h: 0.3
248
+ });
249
+ ```
250
+
251
+ ## Slide Backgrounds
252
+
253
+ ### Solid Color
254
+
255
+ ```javascript
256
+ const slide = pres.addSlide();
257
+ slide.background = { color: "1E2761" };
258
+ ```
259
+
260
+ ### Gradient
261
+
262
+ ```javascript
263
+ slide.background = {
264
+ type: "solid",
265
+ fill: "F8FAFC"
266
+ };
267
+ ```
268
+
269
+ ## Tables
270
+
271
+ ### Basic Table
272
+
273
+ ```javascript
274
+ const tableData = [
275
+ [
276
+ { text: "Header A", options: { bold: true, color: "FFFFFF", fill: { color: "1E2761" } } },
277
+ { text: "Header B", options: { bold: true, color: "FFFFFF", fill: { color: "1E2761" } } }
278
+ ],
279
+ [
280
+ { text: "Row 1, Col A", options: {} },
281
+ { text: "Row 1, Col B", options: { fill: { color: "F8FAFC" } } }
282
+ ],
283
+ [
284
+ { text: "Row 2, Col A", options: { fill: { color: "F8FAFC" } } },
285
+ { text: "Row 2, Col B", options: {} }
286
+ ]
287
+ ];
288
+
289
+ slide.addTable(tableData, {
290
+ x: 0.5, y: 2, w: 9, h: 2,
291
+ colW: [4.5, 4.5], // Column widths
292
+ border: { pt: 1, color: "E2E8F0" },
293
+ fontFace: "Arial",
294
+ fontSize: 12
295
+ });
296
+ ```
297
+
298
+ ### Table with Merged Cells
299
+
300
+ ```javascript
301
+ const tableData = [
302
+ [
303
+ { text: "Merged", options: {}, colspan: 2 }, // Spans 2 columns
304
+ { text: "Normal", options: {} }
305
+ ]
306
+ ];
307
+ ```
308
+
309
+ ## Charts
310
+
311
+ ### Bar Chart
312
+
313
+ ```javascript
314
+ const barData = [
315
+ {
316
+ name: "Series 1",
317
+ labels: ["Q1", "Q2", "Q3", "Q4"],
318
+ values: [15, 22, 18, 25]
319
+ },
320
+ {
321
+ name: "Series 2",
322
+ labels: ["Q1", "Q2", "Q3", "Q4"],
323
+ values: [12, 20, 16, 23]
324
+ }
325
+ ];
326
+
327
+ slide.addChart(pres.ChartTypes.bar, barData, {
328
+ x: 1, y: 1, w: 8, h: 3,
329
+ chartColors: ["2E86AB", "F5A623"],
330
+ showLegend: true,
331
+ dataLabelPosition: "outEnd" // Outside bars
332
+ });
333
+ ```
334
+
335
+ ### Line Chart
336
+
337
+ ```javascript
338
+ const lineData = [
339
+ {
340
+ name: "Revenue",
341
+ labels: ["Jan", "Feb", "Mar", "Apr"],
342
+ values: [100, 120, 140, 160]
343
+ }
344
+ ];
345
+
346
+ slide.addChart(pres.ChartTypes.line, lineData, {
347
+ x: 1, y: 1, w: 8, h: 3,
348
+ lineSize: 2,
349
+ chartColors: ["F5A623"],
350
+ dataLabelPosition: "outEnd"
351
+ });
352
+ ```
353
+
354
+ ### Pie Chart
355
+
356
+ ```javascript
357
+ const pieData = [
358
+ {
359
+ name: "Category",
360
+ labels: ["Segment A", "Segment B", "Segment C"],
361
+ values: [30, 25, 45]
362
+ }
363
+ ];
364
+
365
+ slide.addChart(pres.ChartTypes.pie, pieData, {
366
+ x: 1, y: 1, w: 4, h: 3,
367
+ chartColors: ["2E86AB", "F5A623", "E74C3C"],
368
+ dataLabelPosition: "ctr" // Center
369
+ });
370
+ ```
371
+
372
+ ### Chart Options
373
+
374
+ | Option | Values | Notes |
375
+ |---|---|---|
376
+ | `chartColors` | Array of hex colors | Controls bar/line/pie segment colors |
377
+ | `dataLabelPosition` | `"outEnd"`, `"ctr"`, `"inEnd"`, `"ctrH"` | Where labels appear |
378
+ | `dataLabelFontSize` | number | Font size of data labels |
379
+ | `gridLines` | object | `{ style: "solid", color: "E0E0E0", size: 1 }` |
380
+ | `showLegend` | boolean | Show legend on chart |
381
+ | `legendPos` | `"b"`, `"r"`, `"t"`, `"l"` | Legend position (bottom, right, top, left) |
382
+
383
+ ## Slide Masters
384
+
385
+ Define a master slide template and reuse it:
386
+
387
+ ```javascript
388
+ // Define master
389
+ pres.defineLayout({ name: "MASTER_CONTENT" }, (slide) => {
390
+ slide.background = { color: "F8FAFC" };
391
+
392
+ // Add a footer
393
+ slide.addText("© 2024 Company", {
394
+ x: 0.5, y: H - 0.4, w: 9, h: 0.3,
395
+ fontSize: 8,
396
+ color: "64748B",
397
+ align: "right"
398
+ });
399
+ });
400
+
401
+ // Use master
402
+ const slide = pres.addSlide("MASTER_CONTENT");
403
+ ```
404
+
405
+ ## Common Pitfalls
406
+
407
+ ### Colors
408
+
409
+ - **NEVER use `#` with hex colors** — `"#FF0000"` corrupts the file. Always use `"FF0000"`.
410
+ - **NEVER encode opacity in hex** — Don't try `"FF0000CC"`. Use `fill: { color: "FF0000", transparency: 50 }` instead.
411
+
412
+ ### Bullets
413
+
414
+ - **Always use `bullet: true`** — Never try to build bullets manually with Unicode bullets.
415
+ - **Always use `breakLine: true` between items** — This is required for proper spacing.
416
+ - **Avoid `lineSpacing` with bullets** — Use `paraSpaceAfter: 6` instead for better control.
417
+
418
+ ### Objects & Mutations
419
+
420
+ - **Create fresh option objects for each call** — PptxGenJS mutates option objects. Reusing the same object across multiple `addShape` or `addText` calls causes unexpected behavior.
421
+
422
+ ```javascript
423
+ // WRONG: Shadow object reused
424
+ const shadow = { type: "outer", color: "000000", blur: 6, offset: 2, opacity: 0.1 };
425
+ slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 2, h: 1, shadow });
426
+ slide.addShape(pres.shapes.RECTANGLE, { x: 4, y: 1, w: 2, h: 1, shadow });
427
+
428
+ // RIGHT: Fresh shadow for each shape
429
+ slide.addShape(pres.shapes.RECTANGLE, {
430
+ x: 1, y: 1, w: 2, h: 1,
431
+ shadow: { type: "outer", color: "000000", blur: 6, offset: 2, opacity: 0.1 }
432
+ });
433
+ slide.addShape(pres.shapes.RECTANGLE, {
434
+ x: 4, y: 1, w: 2, h: 1,
435
+ shadow: { type: "outer", color: "000000", blur: 6, offset: 2, opacity: 0.1 }
436
+ });
437
+ ```
438
+
439
+ ### Shadow Offsets
440
+
441
+ - **Shadow `offset` must be non-negative** — Negative values cause file corruption. Use positive values only.
442
+ - **Typical shadow: blur 6, offset 2, opacity 0.08-0.1** — This creates a subtle drop shadow.
443
+
444
+ ### Rounded Rectangles
445
+
446
+ - **Don't pair rounded rectangles with accent borders** — Rounded rectangles with thick borders (> 2pt) look awkward. Use subtle shadows instead.
447
+
448
+ ## Quick Reference
449
+
450
+ ### Shape Types
451
+ ```javascript
452
+ pres.shapes.RECTANGLE
453
+ pres.shapes.OVAL
454
+ pres.shapes.LINE
455
+ pres.shapes.ROUNDED_RECTANGLE
456
+ pres.shapes.DIAMOND
457
+ pres.shapes.TRIANGLE
458
+ ```
459
+
460
+ ### Chart Types
461
+ ```javascript
462
+ pres.ChartTypes.bar
463
+ pres.ChartTypes.line
464
+ pres.ChartTypes.pie
465
+ pres.ChartTypes.doughnut
466
+ pres.ChartTypes.area
467
+ pres.ChartTypes.scatter
468
+ ```
469
+
470
+ ### Text Alignment
471
+ ```javascript
472
+ align: "left" | "center" | "right"
473
+ valign: "top" | "middle" | "bottom"
474
+ ```
475
+
476
+ ### Common Font Faces
477
+ ```javascript
478
+ "Arial"
479
+ "Arial Black"
480
+ "Calibri"
481
+ "Georgia"
482
+ "Trebuchet MS"
483
+ "Helvetica"
484
+ ```
485
+
486
+ ### Sizing Guidelines
487
+
488
+ | Element | Size (pt) |
489
+ |---|---|
490
+ | Slide title | 20-28 |
491
+ | Section heading | 18 |
492
+ | Body text | 14 |
493
+ | Bullet text | 14 |
494
+ | Captions | 10-11 |
495
+ | Stat numbers | 60 |
496
+ | Stat labels | 12 |
497
+ | Table header | 11-12 |
498
+ | Table body | 11 |
499
+
500
+ ### Spacing Guidelines
501
+
502
+ | Element | Value |
503
+ |---|---|
504
+ | Slide margins | 0.5" |
505
+ | Content block gap | 0.3-0.5" |
506
+ | Line height multiplier | 1.3 |
507
+ | Bullet spacing | `paraSpaceAfter: 6` |
508
+ | Card shadow blur | 6 |
509
+ | Card shadow offset | 2 |
510
+ | Card shadow opacity | 0.08-0.1 |
511
+ | Accent bar height | 0.06" |
512
+
513
+ ## Pattern: Professional Stat Card
514
+
515
+ ```javascript
516
+ // Background card
517
+ slide.addShape(pres.shapes.RECTANGLE, {
518
+ x: 1, y: 1, w: 2.5, h: 2,
519
+ fill: { color: "FFFFFF" },
520
+ shadow: { type: "outer", color: "000000", blur: 6, offset: 2, opacity: 0.08 }
521
+ });
522
+
523
+ // Color bar at top
524
+ slide.addShape(pres.shapes.RECTANGLE, {
525
+ x: 1, y: 1, w: 2.5, h: 0.06,
526
+ fill: { color: "F5A623" }
527
+ });
528
+
529
+ // Large number
530
+ slide.addText("3x", {
531
+ x: 1, y: 1.4, w: 2.5, h: 0.8,
532
+ fontSize: 60,
533
+ fontFace: "Arial Black",
534
+ color: "F5A623",
535
+ bold: true,
536
+ align: "center"
537
+ });
538
+
539
+ // Label
540
+ slide.addText("User Growth", {
541
+ x: 1.2, y: 2.3, w: 2.1, h: 0.5,
542
+ fontSize: 12,
543
+ fontFace: "Arial",
544
+ color: "64748B",
545
+ align: "center"
546
+ });
547
+ ```
548
+
549
+ ## Pattern: Two-Column Layout
550
+
551
+ ```javascript
552
+ const colW = (9 - 0.3) / 2; // Two equal columns
553
+
554
+ // Left column
555
+ slide.addText([
556
+ { text: "Point 1", options: { bullet: true, breakLine: true } },
557
+ { text: "Point 2", options: { bullet: true, breakLine: true } },
558
+ { text: "Point 3", options: { bullet: true, breakLine: false } }
559
+ ], {
560
+ x: 0.5, y: 1.5, w: colW, h: 3,
561
+ fontSize: 14,
562
+ fontFace: "Arial",
563
+ color: "1E293B",
564
+ valign: "top",
565
+ paraSpaceAfter: 6
566
+ });
567
+
568
+ // Right column
569
+ slide.addText([
570
+ { text: "Benefit 1", options: { bullet: true, breakLine: true } },
571
+ { text: "Benefit 2", options: { bullet: true, breakLine: true } },
572
+ { text: "Benefit 3", options: { bullet: true, breakLine: false } }
573
+ ], {
574
+ x: 0.5 + colW + 0.3, y: 1.5, w: colW, h: 3,
575
+ fontSize: 14,
576
+ fontFace: "Arial",
577
+ color: "1E293B",
578
+ valign: "top",
579
+ paraSpaceAfter: 6
580
+ });
581
+ ```
582
+
583
+ ## Example: Complete Slide
584
+
585
+ ```javascript
586
+ const PptxGenJS = require("pptxgenjs");
587
+ const pres = new PptxGenJS();
588
+ pres.layout = "LAYOUT_16x9";
589
+ pres.author = "Jane Doe";
590
+ pres.title = "Q4 Results";
591
+
592
+ const slide = pres.addSlide();
593
+ const W = 10, H = 5.625, M = 0.5, CW = 9;
594
+
595
+ // Background
596
+ slide.background = { color: "F8FAFC" };
597
+
598
+ // Title
599
+ slide.addText("Revenue Beat Target by 15%", {
600
+ x: M, y: M, w: CW, h: 0.6,
601
+ fontSize: 20,
602
+ fontFace: "Arial Black",
603
+ color: "1E293B",
604
+ bold: true
605
+ });
606
+
607
+ // Accent underline
608
+ slide.addShape(pres.shapes.RECTANGLE, {
609
+ x: M, y: M + 0.6, w: 0.8, h: 0.04,
610
+ fill: { color: "F5A623" }
611
+ });
612
+
613
+ // Body text
614
+ slide.addText("We exceeded quarterly targets across all regions.", {
615
+ x: M, y: M + 1, w: CW, h: 0.6,
616
+ fontSize: 14,
617
+ fontFace: "Arial",
618
+ color: "1E293B"
619
+ });
620
+
621
+ // Bullet points
622
+ slide.addText([
623
+ { text: "Sales team landed 3 enterprise deals", options: { bullet: true, breakLine: true } },
624
+ { text: "Product revenue grew 22% MoM", options: { bullet: true, breakLine: true } },
625
+ { text: "Customer retention at 96%", options: { bullet: true, breakLine: false } }
626
+ ], {
627
+ x: M + 0.2, y: M + 1.8, w: CW - 0.4, h: 2,
628
+ fontSize: 14,
629
+ fontFace: "Arial",
630
+ color: "1E293B",
631
+ paraSpaceAfter: 6,
632
+ valign: "top"
633
+ });
634
+
635
+ pres.writeFile({ fileName: "output.pptx" });
636
+ ```
637
+
638
+ ## Common Pitfalls: Stat Cards & Large Numbers
639
+
640
+ **Large numbers overflow their text boxes.** At 48pt+ with bold fonts like Arial Black, values like "12.4K", "$156K", or "$48K" are often wider than narrow card text boxes (2-3 inches). When they overflow, the text wraps and produces broken-looking results — e.g. "12.4" on one line and "K" huge on the next.
641
+
642
+ **Always use `shrinkText: true` on stat values:**
643
+ ```javascript
644
+ slide.addText("$156K", {
645
+ x: cardX, y: cardY + 0.5, w: cardW, h: 1,
646
+ fontSize: 52,
647
+ fontFace: "Arial Black",
648
+ color: accentColor,
649
+ bold: true,
650
+ align: "center",
651
+ valign: "middle",
652
+ shrinkText: true // CRITICAL — auto-scales text to fit the box
653
+ });
654
+ ```
655
+
656
+ This tells PowerPoint to auto-shrink the font size if the text doesn't fit. It's invisible when text fits fine, but prevents wrapping when values are wider than expected.
657
+
658
+ **When to use `shrinkText: true`:**
659
+ - All stat card values (any font ≥ 36pt in a box < 3" wide)
660
+ - Slide titles on section dividers (large font + long titles)
661
+ - Any text box where the content length is unpredictable
662
+
663
+ **Alternative approaches if `shrinkText` isn't sufficient:**
664
+ - Reduce font size for longer values: `fontSize: value.length > 4 ? 40 : 52`
665
+ - Widen cards: use 3 cards per row instead of 4
666
+ - Abbreviate values: "$156K" instead of "$156,000"