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>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
---
|
||||
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`.
|
||||
Reference in New Issue
Block a user