Files
balam/.claude/skills/proposal-pdf/SKILL.md
T
JohannVelazquez 72b7602e8e Bitácora 8–30 jun: contrato firmado, plan de actividades y skill proposal-pdf
- Comunicaciones (REGISTRO #15–#21): respuesta v1.1 (10-jun), luz verde (16-jun),
  contrato (envío 25-jun, ajustes + firma 26-jun), arranque con Erika y kickoff 1-jul
- Evidencia en fuentes/ (correos 8/10/16-jun, WhatsApp Paola y Erika)
- Contratos (sin firmar y firmado) en propuesta/
- Plan de actividades Etapa 0–3 + guion del kickoff (planeacion/)
- Skill proposal-pdf instalado (.claude/skills) + Propuesta-Balam.pdf regenerada
- README/PENDIENTES al día; limpieza de archivos sueltos; .gitignore (locks de Office)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:42:45 -06:00

229 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: proposal-pdf
description: >-
Build polished, print-optimized PDF proposals, quotes, and reports with an
editorial design system (navy + terracotta, serif display headings, full-bleed
cover, auto-numbered table of contents, stamped confidential footers). Use this
skill WHENEVER the user wants a professional/branded PDF deliverable from
content — e.g. "make this proposal a PDF", "generate a commercial proposal /
cotización / propuesta", "turn this into a nice client-facing PDF", "build a
quote/report PDF", or any request for a high-quality multi-page PDF document
that needs a cover, table of contents, tables, or page numbers. Prefer this
over ad-hoc HTML-to-PDF or reportlab-from-scratch whenever presentation quality
matters. Works in Spanish or English.
---
# proposal-pdf
Produces a refined, print-ready PDF from hand-authored HTML using headless
Chromium, with an automatically numbered table of contents and stamped footers.
This is the same pipeline used to build real commercial proposals — it favors
typographic quality over speed.
## Why this approach
Tools like `reportlab`-from-scratch or `pandoc` give you a document but not a
*designed* one. This skill instead has you author one print-optimized HTML file
against a ready-made design system, then renders it with Chromium (which has the
best CSS print engine available). Two things that are otherwise painful are
handled for you:
- **Full-bleed cover** — a dark cover that runs edge-to-edge, via `@page:first { margin:0 }`.
- **Real TOC page numbers** — resolved in a second render pass by searching the
rendered PDF, so they're always correct regardless of how content paginates.
## Setup (once)
```bash
pip install playwright pdfplumber pypdf reportlab
python -m playwright install chromium
```
### Fonts — bundled and embedded (no system install needed)
The design uses **Caladea** (serif display), **Carlito** (sans body) and
**DejaVu Sans Mono** (code). These ship with the skill in `assets/fonts/`, and
`template.html` embeds them with `@font-face` rules that point at a `fonts/`
folder **next to the HTML**. The output therefore does not depend on which fonts
happen to be installed on the build machine. Rules of the road:
- When you copy `template.html` to your working file, **also copy `assets/fonts/`
next to it** so the `url("fonts/…")` paths resolve. `build_pdf.py` renders each
document from its own folder, so relative paths (fonts, logos, images) just work.
- If the fonts are missing at build time, Chromium silently falls back to system
fonts (Cambria/Calibri/…) — it looks *close* but isn't identical. After building,
`build_pdf.py` inspects the finished PDF and **prints a warning** if Caladea or
Carlito didn't embed, so that regression can't ship unnoticed.
- Caladea/Carlito are *metric-compatible* with Cambria/Calibri: swapping them
changes the glyph shapes but **not** the pagination. So if a rebuild has a
different page count than an older PDF, the **content** changed — not the fonts.
The footer text is stamped separately with a TTF set in the config
(`fonts.footer_ttf`); if that path doesn't exist it falls back to Helvetica.
### En este proyecto (Windows / Balam)
El skill ya está instalado en `.claude/skills/proposal-pdf/`. Para usarlo aquí:
```powershell
# 1) Dependencias (una vez)
pip install playwright pdfplumber pypdf reportlab
python -m playwright install chromium
# 2) Construir (desde la raíz del repo)
python .claude/skills/proposal-pdf/scripts/build_pdf.py <ruta>/pdf.config.json --check # valida
python .claude/skills/proposal-pdf/scripts/build_pdf.py <ruta>/pdf.config.json # construye
```
- **Fuentes:** las fuentes del diseño (Caladea/Carlito/DejaVu Sans Mono) van **embebidas** vía `@font-face` desde `propuesta/fonts/`, así que el PDF sale idéntico sin depender de lo que tenga instalado Windows. No instales nada. Para el **texto del footer** (que se estampa aparte con reportlab) sí se usa una TTF del sistema: `fonts.footer_ttf` apunta a `C:\Windows\Fonts\calibri.ttf`. Si al reconstruir ves el aviso `⚠ design font(s) missing`, es que la carpeta `fonts/` no quedó junto al HTML.
- **Verificación visual:** en este entorno **no hay `pdftoppm`/poppler**, así que el paso "verify visually" se hace con el helper incluido, que usa `pypdfium2` (ya viene con `pdfplumber`):
```powershell
python .claude/skills/proposal-pdf/scripts/render_check.py <ruta>/Salida.pdf 1 2 3 # rasteriza páginas a PNG
```
Luego abre los PNG (o pídeme que los lea). Siempre revisa: portada full-bleed, números de TOC presentes y plausibles, tablas que no se desborden, y que el footer aparezca en el cuerpo pero **no** en la portada.
- **Consola UTF-8:** el script ya fuerza salida UTF-8 (la consola de Windows es cp1252 y truena con los glifos ✓/⚠). No necesitas hacer nada.
## Files in this skill
```
proposal-pdf/
├── SKILL.md
├── scripts/
│ └── build_pdf.py # the render → number → stamp engine
├── assets/
│ ├── template.html # the design system + every component, with examples
│ ├── fonts/ # bundled design fonts (Caladea, Carlito, DejaVu Sans Mono)
│ ├── pdf.config.example.json # the full config schema (documented)
│ └── test.config.json # smoke test for the template
└── examples/ # a second, complete worked example
├── example-a4.html # A4 + cover logo + a long page-spanning table
└── example-a4.config.json # shows A4, "X / N" footer, skip_pages
```
Smoke-test the install end-to-end:
```bash
python3 scripts/build_pdf.py assets/test.config.json # Letter template
python3 scripts/build_pdf.py examples/example-a4.config.json # A4 worked example
```
## Workflow
### 1. Author the content as HTML
Copy `assets/template.html` to a working file (e.g. `proposal.html`) and replace
the example content with the real content. **Keep the CSS and the component
markup/classes** — that's the design system. The big in-file comment and the
component cheat-sheet at the top of the template tell you which class does what.
Common building blocks: numbered section headers (`.sec-head` + `.sec-num`),
tables with `td.k` / `td.num` / `tr.total` / `tr.sub-total` / `td.cov`, accent
panels (`.callout`, `.callout.cool`), a pipeline diagram (`.flow` + `.chip`),
headline stat cards (`.synthesis` + `.stat`), and phase blocks (`.etapa`).
For each section that appears in the table of contents:
- put a `{{PG_<key>}}` token in its TOC row (already wired in the template), and
- remember a **unique phrase from that section's body** (see step 2).
Appendices/new-page sections get the `.break` class to start on a fresh page.
### 2. Write the config
Copy `assets/pdf.config.example.json` to `pdf.config.json` (next to your HTML)
and fill it in. The important part is the `toc` map: each key matches a
`{{PG_<key>}}` token, and its value is a phrase the engine will search for to
find that section's page.
> **Anchor rules — read this, it prevents the two failure modes:**
> 1. **Use BODY text, never the section title.** Titles also render in the TOC,
> so a title would match the TOC page first and give the wrong number.
> 2. **Don't start the anchor on the first letter of a drop-cap paragraph.** The
> CSS drop cap splits the first letter into its own glyph, so the extracted
> text reads `"T his…"`. Start the anchor a word or two in
> (e.g. `"opening paragraph…"`, not `"This opening paragraph…"`).
>
> Good anchor: the first ~610 words of the first normal paragraph after the
> heading. Keep it distinctive.
### 3. Build
```bash
python scripts/build_pdf.py pdf.config.json --check # validate config + env first (optional)
python scripts/build_pdf.py pdf.config.json # build
```
`--check` runs a **preflight**: confirms the deps and Chromium are installed, the
input HTML exists, and — importantly — that every `{{PG_*}}` token in the HTML
has a matching anchor in `toc` (and warns about anchors with no token). Fix any
reported issue before building.
The build renders once with tokens blanked, prints the resolved page numbers,
fills the TOC, re-renders, stamps footers, sets metadata, and writes the PDF. If
an anchor can't be found it stops and tells you exactly which one. If an anchor
matches more than one page it warns and uses the first — make it more specific if
that's wrong.
### 4. ALWAYS verify visually
Rendering bugs are visual, so look at the result. Rasterize a few pages and
inspect them (don't just trust that it ran):
```bash
pdftoppm -png -r 80 -f 1 -l 1 Output.pdf check_cover # full-bleed cover
pdftoppm -png -r 80 -f 2 -l 2 Output.pdf check_toc # TOC page numbers filled
# …and a content page or two
```
Check: the cover fills the page edge-to-edge, the TOC numbers are present and
plausible, tables don't overflow, and the footer shows on body pages but not the
cover. Re-author and rebuild as needed — iteration is normal.
## Customizing
- **Colors / fonts:** edit the `:root` CSS variables in your HTML (`--ink`,
`--accent`, `--cream`, the font stacks). One accent color used sparingly reads
as more premium than many.
- **Page size:** set `"page_size"` to `"Letter"`, `"Legal"`, `"A4"`, `"A3"`, or a
custom `{"width_mm":210,"height_mm":297}`. The engine derives the footer
geometry automatically — you do **not** edit the script. For A4/A3 also change
`@page { size:Letter }` to the matching size in the HTML CSS (there's a comment
there). See `examples/example-a4.html` for a full A4 document.
- **Cover logo:** drop a `<div class="logo">` into the cover `.top` — either a
text mark (`<span class="txt">NAME</span>`) or an image
(`<img class="invert" src="logo.png">`; `.invert` whitens a dark logo on the
navy cover). Example in `examples/example-a4.html`.
- **Footer:** all in `footer` — `text`, `rule` (hairline on/off), `font_size_pt`,
`margin_mm` (distance from the bottom), `color` ([r,g,b] 01),
`page_number_format` (`"{page}"`, `"{page} / {pages}"`, `"Página {page}"`),
`skip_first_page`, and `skip_pages` (extra 1-based pages to skip, e.g. the TOC).
- **No cover / no TOC:** a doc without `{{PG_*}}` tokens just skips the numbering
pass (leave `toc` empty `{}`). Set `footer.skip_first_page` to `false` if
there's no cover.
- **Long tables & page breaks:** handled for you — table headers repeat on every
page a table spans, rows/callouts/cards never split, and headings won't be
stranded at a page bottom. Wrap anything else you want kept together in
`class="keep"`; force a new page with `class="break"`.
## Design principles baked in (keep these)
- One serif for display (cover, section numbers, headings), one sans for body,
one mono for code. Don't add more families.
- A single accent color (terracotta) for rules, section numbers, and the one
callout per section that matters. Everything else is navy/ink and warm neutrals.
- Generous hairlines and uppercase micro-labels instead of heavy boxes.
- Numeric table columns are right-aligned with tabular figures (`.num`); the
total row is the only emphasized row.
- Keep prose tight. The format rewards restraint.
## Troubleshooting
- **"Could not locate these TOC anchors"** → the phrase isn't body text on that
page, or it starts on a drop-cap letter, or whitespace differs. Pick a longer
distinctive body phrase. (Whitespace is normalized automatically.)
- **Cover not full-bleed** → confirm `@page:first { margin:0 }` is present and
the build runs with Chromium margins at 0 (it does by default). The cover
`.cover` element is sized to the full page (216mm × 279mm for Letter).
- **Emoji not in color** (✅⚠️❌ in coverage tables) → install a color emoji font
(`fonts-noto-color-emoji` on Debian/Ubuntu). Chromium uses it automatically.
- **Footer font looks wrong** → set a valid `fonts.footer_ttf`; otherwise it
uses Helvetica.
- **Playwright errors about browsers** → run `python -m playwright install chromium`.