72b7602e8e
- 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>
229 lines
12 KiB
Markdown
229 lines
12 KiB
Markdown
---
|
||
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 ~6–10 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] 0–1),
|
||
`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`.
|