@heylemon/lemonade 0.1.7 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.7",
3
- "commit": "d184adc6a2e41a87e3e128818c8fb4691ed284fa",
4
- "builtAt": "2026-02-21T17:43:41.892Z"
2
+ "version": "0.1.8",
3
+ "commit": "afe9f4d0fa565323c39787bae2854ba5905b5a21",
4
+ "builtAt": "2026-02-21T18:20:37.765Z"
5
5
  }
@@ -1 +1 @@
1
- 3c0c2fd2a7ea5c555231422230609ed18d2ba0fc3505ef930a0a92cb6a91760e
1
+ 38125fabd1c2129cedf329e3c20fd69940046e0859a30f6906f3ff0f944a5aaa
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heylemon/lemonade",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "AI gateway CLI for Lemon - local AI assistant with integrations",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
@@ -366,7 +366,7 @@ Even if the code is perfect, content matters. Follow these principles:
366
366
  ### Callouts
367
367
  - Use callouts for key decisions, critical warnings, or bottom-line takeaways.
368
368
  - Maximum one per major section — if everything is highlighted, nothing stands out.
369
- - Format as a highlighted paragraph with accent left border and light background.
369
+ - **Always use paragraph borders** (left border + shading), never narrow table cells as accent stripes. Narrow table cells break in Apple Pages. See "Cross-Platform Compatibility" section.
370
370
 
371
371
  ---
372
372
 
@@ -428,7 +428,23 @@ The validator checks:
428
428
  - Empty headings
429
429
  - Manual bullet characters (should use List APIs)
430
430
 
431
- ### 2. Visual Check (PDF Conversion)
431
+ ### 2. Fix Tables for Cross-Platform (MANDATORY if document contains tables)
432
+
433
+ docx-js has a bug where table grid definitions (`tblGrid`) don't match cell widths. This causes tables to render as 1-character-wide columns in Apple Pages, Google Docs, and LibreOffice. Run this fix on every document that contains tables:
434
+
435
+ ```bash
436
+ python scripts/fix_tables.py report.docx
437
+ ```
438
+
439
+ This script:
440
+ - Reads each table's first-row cell widths (`tcW`)
441
+ - Patches the `tblGrid` column definitions to match
442
+ - Adds `tblLayout type="fixed"` for consistent rendering
443
+ - Overwrites the file in place (or pass a second arg for a new file)
444
+
445
+ **Always run this after generating any document with tables.** Without it, tables will look correct in Word but break in Pages and Google Docs.
446
+
447
+ ### 3. Visual Check (PDF Conversion)
432
448
 
433
449
  ```bash
434
450
  # Convert to PDF
@@ -440,13 +456,14 @@ pdftoppm -jpeg -r 150 report.pdf page
440
456
  # View: page-1.jpg, page-2.jpg, etc.
441
457
  ```
442
458
 
443
- ### 3. Fix & Re-validate
459
+ ### 4. Fix & Re-validate
444
460
 
445
461
  If the validator reports issues or the PDF looks off:
446
462
  1. Fix the docx-js code
447
463
  2. Re-run to regenerate .docx
448
- 3. Re-validate with `validate.py`
449
- 4. Re-convert to PDF if visual issues
464
+ 3. Run `fix_tables.py` again (step 2)
465
+ 4. Re-validate with `validate.py`
466
+ 5. Re-convert to PDF if visual issues
450
467
 
451
468
  Iterate until both validation passes and visual output looks polished.
452
469
 
@@ -591,6 +608,85 @@ sudo apt-get install libreoffice poppler-utils # soffice, pdftoppm
591
608
 
592
609
  ---
593
610
 
611
+ ## Cross-Platform Compatibility (Pages, Google Docs, LibreOffice)
612
+
613
+ Documents must look correct in Apple Pages, Google Docs, and LibreOffice — not just Microsoft Word. These apps interpret .docx features differently and many advanced Word features break silently.
614
+
615
+ ### Features That Break in Pages
616
+
617
+ **Narrow decorative table cells (< 200 DXA):**
618
+ Pages collapses very narrow cells, causing adjacent text to render vertically (one character per line). This is the most common Pages rendering bug.
619
+
620
+ - ❌ **Never:** Use a 120 DXA cell as a colored accent stripe next to a content cell
621
+ - ❌ **Never:** Use single-cell tables as horizontal rule/divider decorations
622
+ - ✅ **Instead:** Use paragraph borders for accent effects:
623
+
624
+ ```javascript
625
+ // Callout box — cross-platform safe
626
+ // Uses left border on the paragraph itself, not a narrow table cell
627
+ new Paragraph({
628
+ spacing: { before: 200, after: 200 },
629
+ indent: { left: 360 },
630
+ border: {
631
+ left: { style: BorderStyle.SINGLE, size: 12, color: "2E86AB", space: 10 }
632
+ },
633
+ shading: { fill: "E8F4F8", type: ShadingType.CLEAR },
634
+ children: [
635
+ new TextRun({ text: "Decision needed: ", bold: true, size: 22, color: "1B3A5C" }),
636
+ new TextRun({ text: "Approve the $400K budget by February 28.", size: 22, color: "2C3E50" })
637
+ ]
638
+ })
639
+ ```
640
+
641
+ **Decorative tables as horizontal lines:**
642
+ ```javascript
643
+ // ❌ BREAKS in Pages — tiny table used as accent line
644
+ new Table({
645
+ width: { size: 2880, type: WidthType.DXA },
646
+ rows: [new TableRow({ children: [new TableCell({
647
+ width: { size: 2880, type: WidthType.DXA },
648
+ shading: { fill: "2E86AB", type: ShadingType.CLEAR },
649
+ children: [new Paragraph({ children: [new TextRun({ text: " ", size: 4 })] })]
650
+ })] })]
651
+ })
652
+
653
+ // ✅ WORKS everywhere — paragraph bottom border as accent line
654
+ new Paragraph({
655
+ spacing: { after: 200 },
656
+ border: {
657
+ bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E86AB", space: 8 }
658
+ },
659
+ children: [] // Empty paragraph with just a bottom border
660
+ })
661
+ ```
662
+
663
+ ### Other Cross-Platform Pitfalls
664
+
665
+ - **Text boxes / floating elements**: Pages and Google Docs don't support them — content disappears or overlaps
666
+ - **Custom XML / content controls**: Ignored by non-Word apps
667
+ - **Complex nested tables**: Keep nesting to 1 level max; Pages handles deeply nested tables poorly
668
+ - **Tab stops for alignment**: Use tables instead — tab rendering varies between apps
669
+ - **Form fields**: Only work in Word
670
+ - **Watermarks**: Render differently or not at all in Pages
671
+ - **SmartArt / ActiveX**: Not supported outside Word
672
+
673
+ ### Safe Cross-Platform Features
674
+
675
+ These features render consistently across all apps:
676
+ - Simple tables with explicit cell widths (minimum 400 DXA per cell)
677
+ - Paragraph borders (left, bottom, top — great for callouts and dividers)
678
+ - Paragraph shading/highlighting
679
+ - Bold, italic, underline, font color, font size
680
+ - Heading styles (Heading 1-6)
681
+ - Bullet and numbered lists
682
+ - Page breaks
683
+ - Headers and footers with page numbers
684
+ - Images (inline, not floating)
685
+
686
+ ### The 400 DXA Rule
687
+
688
+ **Never create a table cell narrower than 400 DXA (roughly 0.28 inches).** Cells below this width are rendered unpredictably in Pages and LibreOffice. If you need a thin accent stripe, use a paragraph left border instead.
689
+
594
690
  ## Troubleshooting
595
691
 
596
692
  **Document won't open in Word:** XML malformed. Check for unclosed tags, invalid characters. Run `validate.py` to check structure.
@@ -603,6 +699,8 @@ sudo apt-get install libreoffice poppler-utils # soffice, pdftoppm
603
699
 
604
700
  **TOC not generating:** docx-js doesn't support field codes natively. Use placeholder text or manually update in Word after opening.
605
701
 
702
+ **Text renders vertically in Pages:** A table cell is too narrow (< 200 DXA). Replace narrow decorative cells with paragraph borders. See "Cross-Platform Compatibility" section above.
703
+
606
704
  ---
607
705
 
608
706
  ## Example: Simple Report
@@ -0,0 +1,118 @@
1
+ """
2
+ Post-process a docx-js generated .docx file to fix tblGrid values for Pages compatibility.
3
+
4
+ docx-js generates <w:gridCol w:w="100"/> for all columns regardless of actual cell widths.
5
+ Pages relies on tblGrid for layout, so tables render as 1-char-wide columns.
6
+ This script reads the tcW values from each table's first row and patches tblGrid to match.
7
+ """
8
+ import sys
9
+ import zipfile
10
+ import os
11
+ import re
12
+ import shutil
13
+ from lxml import etree
14
+
15
+ WORD_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
16
+ NSMAP = {'w': WORD_NS}
17
+
18
+ def fix_tables(xml_content):
19
+ """Fix tblGrid values to match first-row cell widths."""
20
+ root = etree.fromstring(xml_content)
21
+ tables = root.findall('.//w:tbl', NSMAP)
22
+ fixes = 0
23
+
24
+ for tbl in tables:
25
+ grid = tbl.find('w:tblGrid', NSMAP)
26
+ if grid is None:
27
+ continue
28
+
29
+ # Get first row's cell widths
30
+ first_row = tbl.find('w:tr', NSMAP)
31
+ if first_row is None:
32
+ continue
33
+
34
+ cells = first_row.findall('w:tc', NSMAP)
35
+ cell_widths = []
36
+ for tc in cells:
37
+ tcPr = tc.find('w:tcPr', NSMAP)
38
+ if tcPr is not None:
39
+ tcW = tcPr.find('w:tcW', NSMAP)
40
+ if tcW is not None:
41
+ w_val = tcW.get(f'{{{WORD_NS}}}w')
42
+ w_type = tcW.get(f'{{{WORD_NS}}}type', 'dxa')
43
+ if w_type == 'dxa' and w_val:
44
+ try:
45
+ cell_widths.append(int(w_val))
46
+ except ValueError:
47
+ cell_widths.append(2340) # default fallback
48
+ else:
49
+ cell_widths.append(2340)
50
+ else:
51
+ cell_widths.append(2340)
52
+ else:
53
+ cell_widths.append(2340)
54
+
55
+ # Check if grid needs fixing (all gridCol are 100 = docx-js default)
56
+ grid_cols = grid.findall('w:gridCol', NSMAP)
57
+ all_100 = all(gc.get(f'{{{WORD_NS}}}w') == '100' for gc in grid_cols)
58
+
59
+ if all_100 and len(cell_widths) == len(grid_cols):
60
+ for gc, width in zip(grid_cols, cell_widths):
61
+ gc.set(f'{{{WORD_NS}}}w', str(width))
62
+ fixes += 1
63
+
64
+ # Also ensure tblLayout is set to fixed for Pages
65
+ tblPr = tbl.find('w:tblPr', NSMAP)
66
+ if tblPr is not None:
67
+ layout = tblPr.find('w:tblLayout', NSMAP)
68
+ if layout is None:
69
+ layout = etree.SubElement(tblPr, f'{{{WORD_NS}}}tblLayout')
70
+ layout.set(f'{{{WORD_NS}}}type', 'fixed')
71
+
72
+ return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True), fixes
73
+
74
+ def fix_docx(input_path, output_path=None):
75
+ """Fix a .docx file's table grids for Pages compatibility."""
76
+ if output_path is None:
77
+ output_path = input_path
78
+
79
+ tmp_dir = input_path + '_tmp'
80
+ os.makedirs(tmp_dir, exist_ok=True)
81
+
82
+ try:
83
+ # Extract
84
+ with zipfile.ZipFile(input_path, 'r') as z:
85
+ z.extractall(tmp_dir)
86
+
87
+ # Fix document.xml
88
+ doc_path = os.path.join(tmp_dir, 'word', 'document.xml')
89
+ with open(doc_path, 'rb') as f:
90
+ xml_content = f.read()
91
+
92
+ fixed_xml, num_fixes = fix_tables(xml_content)
93
+
94
+ with open(doc_path, 'wb') as f:
95
+ f.write(fixed_xml)
96
+
97
+ # Repack
98
+ with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zout:
99
+ for root_dir, dirs, files in os.walk(tmp_dir):
100
+ for file in files:
101
+ file_path = os.path.join(root_dir, file)
102
+ arcname = os.path.relpath(file_path, tmp_dir)
103
+ zout.write(file_path, arcname)
104
+
105
+ print(f"Fixed {num_fixes} table(s) in {output_path}")
106
+ return num_fixes
107
+
108
+ finally:
109
+ shutil.rmtree(tmp_dir, ignore_errors=True)
110
+
111
+ if __name__ == '__main__':
112
+ if len(sys.argv) < 2:
113
+ print("Usage: python fix_docx_tables.py <input.docx> [output.docx]")
114
+ sys.exit(1)
115
+
116
+ input_file = sys.argv[1]
117
+ output_file = sys.argv[2] if len(sys.argv) > 2 else input_file
118
+ fix_docx(input_file, output_file)
@@ -96,6 +96,57 @@ Use one font pairing for the entire deck. Consistency.
96
96
  - Never center body text (left-align only)
97
97
  - Use `paraSpaceAfter: 6` for bullet spacing, never `lineSpacing`
98
98
 
99
+ ### Slide Boundary Rules (CRITICAL — Content Overflow)
100
+
101
+ **Nothing may extend beyond the slide edges.** Content that overflows is invisible — it gets cut off and looks broken in presentation mode.
102
+
103
+ Standard 16:9 slide dimensions: **10" wide × 5.625" tall**
104
+
105
+ Every element must satisfy these constraints:
106
+ - `x + w ≤ 10` (right edge)
107
+ - `y + h ≤ 5.625` (bottom edge)
108
+ - `x ≥ 0` and `y ≥ 0` (top-left origin)
109
+
110
+ **Before placing any element, calculate remaining space:**
111
+ ```
112
+ available_height = 5.625 - current_y - 0.5 // 0.5" bottom margin
113
+ ```
114
+
115
+ If content doesn't fit, you MUST do one of these (in order of preference):
116
+ 1. **Split across two slides** — A second slide is always better than overflow
117
+ 2. **Reduce font sizes** — Body → 12pt, stats → 48pt instead of 60pt, title → 18pt
118
+ 3. **Remove items** — Fewer bullets, fewer cards. Less is more
119
+ 4. **Tighten spacing** — Reduce gaps between elements to 0.2" minimum
120
+
121
+ **Safe content zones (with 0.5" margins):**
122
+ ```
123
+ Title area: y = 0.5, h = 0.6
124
+ Content start: y = 1.2
125
+ Content end: y = 5.1 (0.5" bottom margin)
126
+ Usable content height: 3.9 inches — plan ALL layouts within this
127
+ ```
128
+
129
+ **Layout height budgets — know these before coding:**
130
+
131
+ | Layout Type | Title | Content Area | Max Items |
132
+ |---|---|---|---|
133
+ | Bullet list | 0.7" | 3.9" | 8 bullets max (0.35" each) |
134
+ | 2-column bullets | 0.7" | 3.9" | 5 bullets per column |
135
+ | Stat cards (1 row) | 0.7" | 1.8" for cards | 3-4 cards |
136
+ | Stat cards (2 rows) | 0.7" | 3.4" for cards | 6-8 cards |
137
+ | Comparison (side-by-side) | 0.7" | 3.2" max per card | 4 bullets per card |
138
+ | Table | 0.7" | 3.9" | 7 rows max (0.4" per row) |
139
+ | Title + subtitle + cards | 1.2" | 3.4" | 3 cards |
140
+
141
+ **Common overflow traps:**
142
+ - **Comparison layouts** — Two side-by-side cards are taller than you think. Budget 2.5-3" max per card, not 3.5". If each card has a title + 5 bullets, it needs ~3" minimum — split to two slides
143
+ - **Bullet lists with 6+ items** — Each bullet needs ~0.35". Six bullets = 2.1". Add title + spacing = 3.5"+
144
+ - **Stat cards stacked in 2 rows** — stat (60pt) + label (12pt) + padding = ~1.5" per row. Two rows + title = 4" before you add anything else
145
+ - **Tables with 5+ rows** — Header + 5 data rows = ~2.4". Add title + margins = 3.5"
146
+ - **"Just one more element"** — Adding a footnote, source line, or page number below a full slide. Check remaining space first
147
+
148
+ **Validation rule: After writing code for any slide, mentally verify that the lowest element's `y + h` does not exceed 5.5 inches (leaving 0.125" safety margin). If it does, refactor before moving to the next slide.**
149
+
99
150
  ### What to Avoid
100
151
 
101
152
  - **Don't repeat the same layout** — Two content slides in a row can feel monotonous. Vary between text-heavy, stat cards, comparisons, and two-column layouts.
@@ -104,6 +155,7 @@ Use one font pairing for the entire deck. Consistency.
104
155
  - **Don't default to blue** — Blue is overdone. It's the safe choice. Pick a palette that matches the content's emotional tone.
105
156
  - **Never add accent lines under titles** — This is the hallmark of AI-generated slides. If you add an underline, make it purposeful (color bar on stat cards) not decorative.
106
157
  - **Don't create text-only slides** — Every slide needs at least one visual anchor: a stat card, an accent color, an icon, or a highlighted quote.
158
+ - **Never let content overflow the slide** — If `y + h > 5.625`, content is cut off. Always calculate remaining space before placing elements. See "Slide Boundary Rules" above.
107
159
 
108
160
  ## QA Process
109
161
 
@@ -29,16 +29,28 @@ Key point: Always create a fresh `PptxGenJS()` instance per presentation. Don't
29
29
  Standard 16:9 layout is 10 inches wide by 5.625 inches tall. All positioning is in inches.
30
30
 
31
31
  ```javascript
32
- // Slide area
33
- const W = 10; // Width
34
- const H = 5.625; // Height
32
+ // Slide area — HARD LIMITS (nothing may exceed these)
33
+ const W = 10; // Width (inches)
34
+ const H = 5.625; // Height (inches)
35
35
  const M = 0.5; // Margin (standard)
36
- const contentWidth = W - 2 * M; // 9 inches
36
+ const contentWidth = W - 2 * M; // 9 inches usable
37
37
 
38
38
  // Common positioning
39
- const titleY = M; // Top margin
40
- const contentStartY = M + 0.8; // After title
41
- const bottomY = H - 0.8; // Near bottom for footer
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
+ }
42
54
  ```
43
55
 
44
56
  ## Text & Formatting
@@ -622,3 +634,33 @@ slide.addText([
622
634
 
623
635
  pres.writeFile({ fileName: "output.pptx" });
624
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"
@@ -8,8 +8,8 @@
8
8
  * JSON spec format: see references/spec-format.md
9
9
  */
10
10
 
11
- import pptxgen from "pptxgenjs";
12
- import fs from "node:fs";
11
+ const pptxgen = require("pptxgenjs");
12
+ const fs = require("fs");
13
13
 
14
14
  // ═══════════════════════════════════════════
15
15
  // DEFAULT THEME