Add the Neutral template family, template picker and text-only logo allowances

Linea (the vendor's original format.pdf design), Monogram and Serenity are
rebuilt from the reference PDFs as one parametric family on the engine, with
the user's logo in the measured slot, a home for every GST field and
A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and
a generated Ms Madi outline for Serenity's vertical word. Text-only logo
placeholders (Monolith, Tangerine) use a doc-derived allowance box. A
Carbon TemplatePicker with committed thumbnails opens from the page
setup controls.
This commit is contained in:
2026-10-04 07:27:12 +05:30
parent eddf27629f
commit aa7a407944
61 changed files with 3278 additions and 110 deletions
+46
View File
@@ -0,0 +1,46 @@
// Renders every registered template with one fixed sample invoice into public/templates/<id>.png, the
// static thumbnails of the template picker. Run: npm run thumbnails (the PNGs are committed).
// The sample is the harness's tax invoice with The sample vendor's logo; the picker says that thumbnails use sample data.
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { ROOT, loadRenderer, rasterize } from "./templates/lib.mjs";
const OUT = path.join(ROOT, "public", "templates");
const WIDTH = 420;
const A4_WIDTH_PT = 595.28;
const ITEMS = [
["Voice-over narration", 4, 1500],
["Audio mastering", 2, 800],
].map(([description, quantity, rate], i) => ({
id: i + 1,
description,
mode: "rate",
rate,
unit: "hour",
quantity,
amount: 0,
sortOrder: i,
hsnSac: "998397",
}));
const SAMPLE = {
name: "thumbnail",
items: ITEMS,
logo: "real",
signature: false,
// A leaner supplier block keeps the whole design (closing block included) on the thumbnail's one page.
vendor: { vendorEmail: "", vendorPhone: "", vendorPan: "" },
invoice: { poNumber: "", notes: "" },
};
const r = await loadRenderer();
r.ensureSetup();
mkdirSync(OUT, { recursive: true });
for (const id of Object.keys(r.TEMPLATES)) {
const rendered = await r.renderFixture(SAMPLE, r.prefsFor("A4", { templateId: id }));
const canvas = await rasterize(rendered.result.bytes, 1, WIDTH / A4_WIDTH_PT);
const file = path.join(OUT, `${id}.png`);
writeFileSync(file, canvas.toBuffer("image/png"));
console.log(`${id}: ${canvas.width} x ${canvas.height} px, ${rendered.result.pages} page(s), ${rendered.result.issues.length} audit issue(s)`);
}