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), optionalnotes. - AGB / Terms pages — validity, payment terms, confidentiality, jurisdiction, optional extra paragraphs (
terms.extrarendered 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>.pdfin 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-liband 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: avoidCSS - No split cards —
.screenshot,.callout,.totals,.recurring,.payment-summaryusebreak-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 toSieif 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,vatAmountDisplayare computed bybuild.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 inbuild.mjsensures 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 insideterms.extraif you have multiple paragraphs — Chrome can then break cleanly between them. Don't use<br/><br/>. - TODO marker:
templates/style.csshas a comment next to the.brandplaceholder. If a real psquared logo SVG is dropped atassets/logo.svg, swap the.brandelement in each template to<img src="assets/logo.svg" alt="psquared" class="brand-img" />.