Rebuild the Classic template on the engine with A4/Letter and a template harness

Classic now renders through buildRenderModel, computeFrame, the template
and renderCore, with S.No and HSN/SAC columns, GST slots only through
checked Slot components, taxable value, reverse-charge line, cancelled tag,
continuation band and Page n of N. Letter absorbs its height deficit in
elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item
sweep and asserts zero audit errors, repeated headers, no split blocks,
rupee extraction and deterministic output. The legacy InvoiceDocument is
removed.
This commit is contained in:
2026-10-04 05:31:14 +05:30
parent e2032a3f9c
commit fa4d711dc0
40 changed files with 2185 additions and 832 deletions
+33 -2
View File
@@ -1,4 +1,5 @@
import { View } from "@react-pdf/renderer";
import { FILL } from "../engine/columns";
import { chromeHeight } from "../engine/metrics";
import { getMeasurer, measureToken } from "../engine/measure";
import type { PageFrame } from "../engine/geometry";
@@ -7,12 +8,21 @@ import { roleToken, type RoleName, type TypeToken } from "../fonts/roles";
import { PlanError } from "../engine/errors";
import type { DecorItem } from "./FirstPageHeader";
import { pdfFlags } from "./pdfProps";
import { FitText } from "./FitText";
import { RenderTxt } from "./RenderTxt";
export interface PageChromeProps {
frame: PageFrame;
/** Left footer text (e.g. the invoice number); omit for none. */
footerLeft?: string;
/**
* Pins the left text to this width (pt) and fits it to one line (shrunk, then cut with an ellipsis).
* A render-prop Text cannot be clamped, so a long supplier name there would wrap and push the footer
* up into the content. Without it the left text takes the remaining width.
*/
footerLeftWidth?: number;
/** Centre text between the left text and the page label. */
footerCenter?: string;
/** Show "Page n of N" at the right of the footer. Default true. */
pageLabel?: boolean;
/** Footer and band type; render-prop roles (no leading). Default "footer". */
@@ -26,6 +36,16 @@ export interface PageChromeProps {
decor?: DecorItem[];
}
/**
* FitText is static text and needs a leading; the footer role has none (render-prop text), so the
* fitted left text borrows 1.4x the size, which stays within the auto-height line that chromeHeight
* reserves for the same role.
*/
function fineFooter(role: RoleName | TypeToken): TypeToken {
const t = roleToken(role);
return { ...t, leading: Math.round(t.size * 1.4 * 10) / 10 };
}
/** "Page 3 of 5"; before the total is known (first layout pass) just "Page 3". */
export function pageLabelText(pageNumber: number, totalPages: number | undefined): string {
return typeof totalPages === "number" && totalPages > 0 ? `Page ${pageNumber} of ${totalPages}` : `Page ${pageNumber}`;
@@ -48,6 +68,8 @@ export function pageLabelText(pageNumber: number, totalPages: number | undefined
export function PageChrome({
frame,
footerLeft,
footerLeftWidth,
footerCenter,
pageLabel = true,
role = "footer",
color,
@@ -57,7 +79,7 @@ export function PageChrome({
}: PageChromeProps) {
const { pad } = frame;
const need = chromeHeight(role);
const showFooter = pageLabel || footerLeft !== undefined;
const showFooter = pageLabel || footerLeft !== undefined || footerCenter !== undefined;
if (showFooter && frame.footerReserve < need) {
throw new PlanError(`footerReserve ${frame.footerReserve} pt is under the ${need} pt a "${roleToken(role).size} pt" footer line needs`, "chrome-reserve");
}
@@ -105,7 +127,16 @@ export function PageChrome({
style={{ position: "absolute", left: pad.left, bottom: marginBottom, width: frame.contentWidth, flexDirection: "row", justifyContent: "flex-end" }}
{...pdfFlags({ fixed: true })}
>
{footerLeft !== undefined && <RenderTxt role={role} color={color} grow render={() => footerLeft} />}
{footerLeft !== undefined &&
(footerLeftWidth !== undefined ? (
<FitText role={fineFooter(role)} width={footerLeftWidth} color={color}>
{footerLeft}
</FitText>
) : (
<RenderTxt role={role} color={color} grow render={() => footerLeft} />
))}
{footerCenter !== undefined && <RenderTxt role={role} color={color} align="center" grow render={() => footerCenter} />}
{footerCenter === undefined && footerLeftWidth !== undefined && <View style={FILL as never} />}
{pageLabel && (
<RenderTxt
id={PAGE_LABEL_ID}