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 Canva 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 490c5b9009
commit 3f7c12b7dd
65 changed files with 3278 additions and 110 deletions
+26 -9
View File
@@ -73,14 +73,24 @@ function checkRendered(r: Rendered, label: string) {
const titleInk = textLinesUnder(title);
for (const line of textLinesUnder(supplier)) for (const t of titleInk) expect(hits(line, t), ctx("a supplier line meets the title text")).toBe(false);
// Header columns (the old fixed 339.5 / 362.5 bounds, derived from the frame so Letter is checked too):
// supplier text stays inside its column and every meta line starts at or right of the meta column.
// Classic's header columns (the old fixed 339.5 / 362.5 bounds, derived from the frame so Letter is checked too):
// supplier text stays inside its column and every meta line starts at or right of the meta column. The
// Neutral templates have no shared header row; the box checks above and the audit cover them.
const frame = frameForPrefs(r.prefs);
const [supplierW] = headerCols(frame.contentWidth);
const supplierRight = frame.pad.left + supplierW;
const metaLeft = supplierRight + HEADER_GAP;
for (const line of textLinesUnder(supplier)) expect(line.x + line.w, ctx(`supplier line [${line.x}..${line.x + line.w}] past its column edge ${supplierRight}`)).toBeLessThanOrEqual(supplierRight + 0.5);
for (const line of textLinesUnder(meta)) expect(line.x, ctx(`meta line at ${line.x} left of the meta column ${metaLeft}`)).toBeGreaterThanOrEqual(metaLeft - 0.5);
if (r.prefs.templateId === "classic") {
const [supplierW] = headerCols(frame.contentWidth);
const supplierRight = frame.pad.left + supplierW;
const metaLeft = supplierRight + HEADER_GAP;
for (const line of textLinesUnder(supplier)) expect(line.x + line.w, ctx(`supplier line [${line.x}..${line.x + line.w}] past its column edge ${supplierRight}`)).toBeLessThanOrEqual(supplierRight + 0.5);
for (const line of textLinesUnder(meta)) expect(line.x, ctx(`meta line at ${line.x} left of the meta column ${metaLeft}`)).toBeGreaterThanOrEqual(metaLeft - 0.5);
} else {
// Supplier lines stay inside the text column; the meta block never reaches left of the client column's end.
const margin = { left: frame.pad.left, right: frame.page.width - frame.pad.right };
for (const line of textLinesUnder(supplier)) {
expect(line.x, ctx("supplier line inside the page margins")).toBeGreaterThanOrEqual(margin.left - 0.5);
expect(line.x + line.w, ctx("supplier line inside the page margins")).toBeLessThanOrEqual(margin.right + 0.5);
}
}
// Header repeats on every continuation page that has rows; no page shows a header without rows.
layout.forEach((page, i) => {
@@ -110,8 +120,15 @@ function checkRendered(r: Rendered, label: string) {
expect(r.pdf.raw, ctx("no Helvetica")).not.toContain("/BaseFont /Helvetica");
expect(text, ctx("rupee extracts as U+20B9, never a superscript one")).not.toContain("¹");
expect(text).toContain("₹");
for (const t of r.pdf.pages.flatMap((p) => p.items)) {
if (/\d\.\d\d$/.test(t.str.trim())) expect(t.str, ctx(`amount "${t.str}" carries the rupee sign`)).toContain("₹");
// A face without the rupee sign (Open Sans) draws it from the fallback font, which extraction reports as
// its own item right before the digits, so the sign counts when it ends the previous item.
for (const page of r.pdf.pages) {
page.items.forEach((t, i) => {
if (/\d\.\d\d$/.test(t.str.trim())) {
const carried = t.str.includes("₹") || (i > 0 && page.items[i - 1].str.trim().endsWith("₹"));
expect(carried, ctx(`amount "${t.str}" carries the rupee sign`)).toBe(true);
}
});
}
const flat = squash(text);
expect(flat, ctx("invoice number")).toContain(squash(model.meta.number));