Create a psquared Client Offer (PDF)
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
create-offer started.
Checking environment...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
What this skill produces
A multi-page A4 PDF with psquared branding:
- Title page — edge-to-edge lavender gradient, brand mark top-left, offer reference top-right, eyebrow + client + headline + subtitle, contact blocks for both parties.
- Body pages — project description (Worum es geht / Ausgangslage / Unsere Lösung / Was du bekommst / Vorgehen + custom sections), optional embedded screenshots with captions. Pagination + brand footer on every page after the title.
- Angebot (pricing) page — itemized table, optional bundle discount, USt., total, optional recurring fee box, optional
paymentSummary (e.g. 50/50 schedule), optional notes.
- AGB / Terms pages — validity, payment terms, confidentiality, jurisdiction, optional extra paragraphs (
terms.extra rendered as raw HTML so it supports its own <p> tags), signature block with both parties.
Brand color is #6b46c1 (purple) with #9f7aea light tint accents. Inter font fallback chain.
The reference example is examples/sanacom-example.json — use it as your starting template when copy-pasting structure for a new offer.
Parameters
/create-offer [path-to-config.json] [output.pdf]
- path-to-config.json — optional. JSON file describing the offer (see
examples/sanacom-example.json). If omitted, this skill will interview the user and write the file itself before building.
- output.pdf — optional. Defaults to
./offer-<client-slug>.pdf in the cwd.
STEP 0 — Verify Setup
cd /Users/<you>/Documents/psquared/psquared-skills/skills/create-offer
bash setup.sh
setup.sh is idempotent — verifies Node >= 18, runs npm install (Playwright + pdf-lib) if missing, ensures Playwright Chromium is downloaded.
Announce: Setup OK. Playwright + Chromium + pdf-lib ready.
STEP 1 — Gather Inputs (only if no config file was provided)
If the user passed a JSON path, skip this step and jump to STEP 2.
Otherwise interview the user. Ask in batches, not one question at a time. The minimum required fields are:
| Field |
Example |
client.name |
"sanacom Unternehmensberatung e.U." (legal entity!) |
client.contactPerson |
"Mag. Daniela Gruber" |
client.address |
"Kirchenwagnerweg 6b, 5071 Wals bei Salzburg" |
client.email |
"office@sanacom.at" |
offer.title |
Short product name, e.g. "Sanacom Categorizer" |
offer.subtitle |
One-sentence what-it-does |
offer.eyebrow |
"Angebot" (default) |
offer.referenceNumber |
"YYYY-MMDD-XXX" pattern |
offer.date |
German format: "25. Mai 2026" |
offer.validUntil |
Default: 30 days from today |
body.intro, body.problem, body.solution |
One paragraph each. Use du form unless the user is clearly a stranger / large enterprise. |
body.deliverables |
Array of HTML strings — <strong> allowed |
body.sections[] |
Optional extra sections like "Vorgehen", "Was wir brauchen" with body (intro line) + bullets |
body.screenshots[] |
Optional. { src: "assets/x.png", caption: "..." } — drop image files into assets/ first |
angebot.items |
Array of { position, title, description, qty, unitPrice }. unit is optional (omit it to show just the qty number). |
angebot.paymentSummary |
Optional short string (HTML) shown directly under the totals — good for "50 % bei Auftragserteilung…" |
angebot.recurring |
Optional { label, amount, per, description } |
terms.paymentTerms |
One paragraph describing the payment schedule |
terms.extra |
Optional raw HTML — use multiple <p> tags here for clean page breaks |
For provider, the canonical psquared defaults are:
"provider": {
"name": "psquared GmbH",
"contactPerson": "Martin Pammesberger",
"address": "Dametzstraße 2-4, 4020 Linz, Österreich",
"email": "martin.pammesberger@psquared.dev",
"website": "psquared.dev",
"jurisdiction": "Linz"
}
(Both Martin and Manuel are Geschäftsführer and can sign — pick one as contactPerson for the title page and signature line.)
Write the gathered config to a temp file (e.g. /tmp/create-offer-<slug>.json) and proceed.
STEP 2 — Build the PDF
cd /Users/<you>/Documents/psquared/psquared-skills/skills/create-offer
node build.mjs <path-to-config.json> <output.pdf>
The build does:
- Load + enrich the JSON (computes line totals, USt., discount, formats EUR).
- Render the full HTML (all pages) once in Chromium via Playwright.
- Pass 1: print page 1 only,
margin: 0 → edge-to-edge title PDF.
- Pass 2: print pages 2+,
margin: 40/25/30/25 mm, displayHeaderFooter: true → body PDF with brand footer + Seite X / Y.
- Merge the two PDFs with
pdf-lib and write the output.
If build.mjs errors out:
- "Input file not found" → check the path.
- Playwright complains about a missing browser → re-run
bash setup.sh.
- Token like
{{foo.bar}} shows up in the PDF → the field is missing from the JSON. Fill it in or remove the template reference.
STEP 3 — Verify
After build.mjs finishes, always sanity-check the PDF visually. Common things to look for:
- Title page edge-to-edge gradient (no white border around it)
- Body pages have proper top/side padding (~22mm visible content offset)
- Footer "Seite X / Y" present on every page after the title
- No orphan headings (a heading on one page, content on the next) — covered by
break-after: avoid CSS
- No split cards —
.screenshot, .callout, .totals, .recurring, .payment-summary use break-inside: avoid
- Item table descriptions are short enough that totals + recurring fit on the same page as the table
open "<output.pdf>" # macOS visual check
mdls -name kMDItemNumberOfPages "<output.pdf>"
STEP 4 — Report
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Offer PDF created.
Client: [client.name]
Title: [offer.title]
Reference: [offer.referenceNumber or n/a]
Total (brutto): [angebot.totalDisplay]
Recurring: [angebot.recurring.amountDisplay / per] (if any)
PDF: [absolute path]
Pages: [count]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
File layout
skills/create-offer/
├── SKILL.md # this file
├── README.md # gotchas + extension notes
├── package.json # pins playwright + pdf-lib
├── setup.sh # idempotent installer
├── build.mjs # two-pass renderer + pdf-lib merge
├── .gitignore # node_modules, output.pdf, *.log
├── templates/
│ ├── style.css # shared psquared branding + print CSS
│ ├── title.html # edge-to-edge title page
│ ├── body.html # project description + screenshots
│ ├── angebot.html # pricing table + recurring + payment summary
│ └── terms.html # AGB sections + signature block
├── examples/
│ └── sanacom-example.json # canonical reference — copy this as a starting template
└── assets/ # drop screenshots / logo.svg here
Important reminders for the LLM filling in the template
- Don't ask 12 micro-questions during STEP 1. Batch them into 2–3 messages max.
- Default to
du-form in body copy (intro, problem, solution, deliverables). Only switch to Sie if the client is clearly formal (large enterprise, unknown contact).
- AGB stays formal ("der Auftraggeber", "der Auftragnehmer") regardless — that's the legal convention.
- Currency is Euro net in line items. The build computes USt. automatically. Don't hardcode brutto values.
hasDiscount, subtotalDisplay, totalDisplay, vatAmountDisplay are computed by build.mjs. Don't put them in the input JSON.
- Screenshots must be PNG/JPG dropped into
assets/. Reference as "src": "assets/foo.png". The <base> tag in build.mjs ensures the relative path resolves.
- Keep item descriptions short (≤ 2 lines) — long descriptions push the recurring/totals onto a second page. The Sanacom example shows the maximum length that still fits on one page.
- Use
<p> tags inside terms.extra if you have multiple paragraphs — Chrome can then break cleanly between them. Don't use <br/><br/>.
- TODO marker:
templates/style.css has a comment next to the .brand placeholder. If a real psquared logo SVG is dropped at assets/logo.svg, swap the .brand element in each template to <img src="assets/logo.svg" alt="psquared" class="brand-img" />.
1---2name: create-offer3description: Generate a polished psquared client offer as a multi-page PDF (title, project description, screenshots, Angebot/pricing, AGB). Walks the user through gathering inputs (or accepts a JSON config), renders branded HTML templates with Playwright in two passes (title page edge-to-edge + body pages with margins and pagination), then merges with pdf-lib.4---56# Create a psquared Client Offer (PDF)78> **Announce:**9> ```10> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━11> create-offer started.12> Checking environment...13> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━14> ```1516---1718## What this skill produces1920A multi-page A4 PDF with psquared branding:21221. **Title page** — edge-to-edge lavender gradient, brand mark top-left, offer reference top-right, eyebrow + client + headline + subtitle, contact blocks for both parties.232. **Body pages** — project description (Worum es geht / Ausgangslage / Unsere Lösung / Was du bekommst / Vorgehen + custom sections), optional embedded screenshots with captions. Pagination + brand footer on every page after the title.243. **Angebot (pricing) page** — itemized table, optional bundle discount, USt., total, optional recurring fee box, optional `paymentSummary` (e.g. 50/50 schedule), optional `notes`.254. **AGB / Terms pages** — validity, payment terms, confidentiality, jurisdiction, optional extra paragraphs (`terms.extra` rendered as raw HTML so it supports its own `<p>` tags), signature block with both parties.2627Brand color is `#6b46c1` (purple) with `#9f7aea` light tint accents. Inter font fallback chain.2829The reference example is `examples/sanacom-example.json` — use it as your starting template when copy-pasting structure for a new offer.3031---3233## Parameters3435`/create-offer [path-to-config.json] [output.pdf]`3637- **path-to-config.json** — optional. JSON file describing the offer (see `examples/sanacom-example.json`). If omitted, this skill will interview the user and write the file itself before building.38- **output.pdf** — optional. Defaults to `./offer-<client-slug>.pdf` in the cwd.3940---4142## STEP 0 — Verify Setup4344```bash45cd /Users/<you>/Documents/psquared/psquared-skills/skills/create-offer46bash setup.sh47```4849`setup.sh` is idempotent — verifies Node >= 18, runs `npm install` (Playwright + pdf-lib) if missing, ensures Playwright Chromium is downloaded.5051> **Announce:** `Setup OK. Playwright + Chromium + pdf-lib ready.`5253---5455## STEP 1 — Gather Inputs (only if no config file was provided)5657If the user passed a JSON path, **skip this step** and jump to STEP 2.5859Otherwise interview the user. Ask in batches, not one question at a time. The minimum required fields are:6061| Field | Example |62|-------|---------|63| `client.name` | "sanacom Unternehmensberatung e.U." (legal entity!) |64| `client.contactPerson` | "Mag. Daniela Gruber" |65| `client.address` | "Kirchenwagnerweg 6b, 5071 Wals bei Salzburg" |66| `client.email` | "office@sanacom.at" |67| `offer.title` | Short product name, e.g. "Sanacom Categorizer" |68| `offer.subtitle` | One-sentence what-it-does |69| `offer.eyebrow` | "Angebot" (default) |70| `offer.referenceNumber` | "YYYY-MMDD-XXX" pattern |71| `offer.date` | German format: "25. Mai 2026" |72| `offer.validUntil` | Default: 30 days from today |73| `body.intro`, `body.problem`, `body.solution` | One paragraph each. **Use `du` form unless the user is clearly a stranger / large enterprise.** |74| `body.deliverables` | Array of HTML strings — `<strong>` allowed |75| `body.sections[]` | Optional extra sections like "Vorgehen", "Was wir brauchen" with `body` (intro line) + `bullets` |76| `body.screenshots[]` | Optional. `{ src: "assets/x.png", caption: "..." }` — drop image files into `assets/` first |77| `angebot.items` | Array of `{ position, title, description, qty, unitPrice }`. `unit` is optional (omit it to show just the qty number). |78| `angebot.paymentSummary` | Optional short string (HTML) shown directly under the totals — good for "50 % bei Auftragserteilung…" |79| `angebot.recurring` | Optional `{ label, amount, per, description }` |80| `terms.paymentTerms` | One paragraph describing the payment schedule |81| `terms.extra` | Optional raw HTML — use multiple `<p>` tags here for clean page breaks |8283For `provider`, the canonical psquared defaults are:8485```json86"provider": {87 "name": "psquared GmbH",88 "contactPerson": "Martin Pammesberger",89 "address": "Dametzstraße 2-4, 4020 Linz, Österreich",90 "email": "martin.pammesberger@psquared.dev",91 "website": "psquared.dev",92 "jurisdiction": "Linz"93}94```9596(Both Martin and Manuel are Geschäftsführer and can sign — pick one as `contactPerson` for the title page and signature line.)9798Write the gathered config to a temp file (e.g. `/tmp/create-offer-<slug>.json`) and proceed.99100---101102## STEP 2 — Build the PDF103104```bash105cd /Users/<you>/Documents/psquared/psquared-skills/skills/create-offer106node build.mjs <path-to-config.json> <output.pdf>107```108109The build does:1101111. Load + enrich the JSON (computes line totals, USt., discount, formats EUR).1122. Render the full HTML (all pages) once in Chromium via Playwright.1133. **Pass 1**: print page 1 only, `margin: 0` → edge-to-edge title PDF.1144. **Pass 2**: print pages 2+, `margin: 40/25/30/25 mm`, `displayHeaderFooter: true` → body PDF with brand footer + `Seite X / Y`.1155. Merge the two PDFs with `pdf-lib` and write the output.116117If `build.mjs` errors out:118- "Input file not found" → check the path.119- Playwright complains about a missing browser → re-run `bash setup.sh`.120- Token like `{{foo.bar}}` shows up in the PDF → the field is missing from the JSON. Fill it in or remove the template reference.121122---123124## STEP 3 — Verify125126After `build.mjs` finishes, **always** sanity-check the PDF visually. Common things to look for:127128- Title page edge-to-edge gradient (no white border around it)129- Body pages have proper top/side padding (~22mm visible content offset)130- Footer "Seite X / Y" present on every page after the title131- No orphan headings (a heading on one page, content on the next) — covered by `break-after: avoid` CSS132- No split cards — `.screenshot`, `.callout`, `.totals`, `.recurring`, `.payment-summary` use `break-inside: avoid`133- Item table descriptions are short enough that totals + recurring fit on the same page as the table134135```bash136open "<output.pdf>" # macOS visual check137mdls -name kMDItemNumberOfPages "<output.pdf>"138```139140---141142## STEP 4 — Report143144> **Announce:**145> ```146> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━147> Offer PDF created.148>149> Client: [client.name]150> Title: [offer.title]151> Reference: [offer.referenceNumber or n/a]152> Total (brutto): [angebot.totalDisplay]153> Recurring: [angebot.recurring.amountDisplay / per] (if any)154>155> PDF: [absolute path]156> Pages: [count]157> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━158> ```159160---161162## File layout163164```165skills/create-offer/166├── SKILL.md # this file167├── README.md # gotchas + extension notes168├── package.json # pins playwright + pdf-lib169├── setup.sh # idempotent installer170├── build.mjs # two-pass renderer + pdf-lib merge171├── .gitignore # node_modules, output.pdf, *.log172├── templates/173│ ├── style.css # shared psquared branding + print CSS174│ ├── title.html # edge-to-edge title page175│ ├── body.html # project description + screenshots176│ ├── angebot.html # pricing table + recurring + payment summary177│ └── terms.html # AGB sections + signature block178├── examples/179│ └── sanacom-example.json # canonical reference — copy this as a starting template180└── assets/ # drop screenshots / logo.svg here181```182183---184185## Important reminders for the LLM filling in the template186187- **Don't ask 12 micro-questions** during STEP 1. Batch them into 2–3 messages max.188- **Default to `du`-form** in body copy (intro, problem, solution, deliverables). Only switch to `Sie` if the client is clearly formal (large enterprise, unknown contact).189- **AGB stays formal** ("der Auftraggeber", "der Auftragnehmer") regardless — that's the legal convention.190- **Currency is Euro net** in line items. The build computes USt. automatically. Don't hardcode brutto values.191- **`hasDiscount`, `subtotalDisplay`, `totalDisplay`, `vatAmountDisplay`** are computed by `build.mjs`. Don't put them in the input JSON.192- **Screenshots** must be PNG/JPG dropped into `assets/`. Reference as `"src": "assets/foo.png"`. The `<base>` tag in `build.mjs` ensures the relative path resolves.193- **Keep item descriptions short** (≤ 2 lines) — long descriptions push the recurring/totals onto a second page. The Sanacom example shows the maximum length that still fits on one page.194- **Use `<p>` tags inside `terms.extra`** if you have multiple paragraphs — Chrome can then break cleanly between them. Don't use `<br/><br/>`.195- **TODO marker**: `templates/style.css` has a comment next to the `.brand` placeholder. If a real psquared logo SVG is dropped at `assets/logo.svg`, swap the `.brand` element in each template to `<img src="assets/logo.svg" alt="psquared" class="brand-img" />`.