Crimson Grid, Tangerine Ledger, Slate Band and Teal Swoosh are rebuilt from the reference PDFs with grid and band tables that survive page breaks, full-bleed fixed bands, pennant tabs, gradient corner swooshes with keep-outs, Carbon icon circles, and the user's logo in the measured slot (knockout or white chip on the dark Slate band). Adds static Poppins 500-700 and Inter fonts. Thumbnails regenerated for all eight templates; the item-count sweeps run in their own test file.
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
import { Page, View } from "@react-pdf/renderer";
|
|
import type { ReactNode } from "react";
|
|
import type { PageFrame } from "./engine/geometry";
|
|
import { chromeHeight } from "./engine/metrics";
|
|
import type { RenderModel } from "./model/build";
|
|
import type { TemplateDefinition } from "./templates/contract";
|
|
import { AvailableWidthProvider } from "./blocks/AvailableWidth";
|
|
import type { DecorItem } from "./blocks/FirstPageHeader";
|
|
import { PageChrome } from "./blocks/PageChrome";
|
|
|
|
/** Height the footer strip takes inside the bottom padding: the text line, a 6 pt gap and the rule. */
|
|
export const FOOTER_RESERVE = 24;
|
|
/** Room above the content on pages 2+ for the continuation band, inside the top margin. */
|
|
export const CONTINUATION_RESERVE = 18;
|
|
|
|
export interface InvoicePageProps {
|
|
frame: PageFrame;
|
|
model: RenderModel;
|
|
template: TemplateDefinition;
|
|
children?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* The only `<Page>` in the app: size and padding come from the frame (no page-size or margin literal
|
|
* anywhere else), the flow is the template's, and the chrome (footer, continuation band) is added after
|
|
* it as fixed page-level boxes. No lineHeight here: it would reach the render-prop page-number text.
|
|
*/
|
|
export function InvoicePage({ frame, model, template, children }: InvoicePageProps) {
|
|
const { pad } = frame;
|
|
const { chrome, decor } = template;
|
|
const marginBottom = pad.bottom - frame.footerReserve;
|
|
const ruleY = frame.page.height - marginBottom - chromeHeight(chrome.role ?? "footer") - 6.6;
|
|
const bandLeft = `${model.docTitle} ${model.meta.number} · ${model.meta.date}`;
|
|
const items: DecorItem[] = [...(decor.everyPage?.(frame, model) ?? [])];
|
|
if (chrome.rule) {
|
|
items.push({ x: pad.left, y: ruleY, width: frame.contentWidth, height: 0.6, children: <View style={{ height: 0.6, backgroundColor: chrome.rule }} /> });
|
|
}
|
|
// The footer sits inside the template's text column, which may be narrower than the page content width.
|
|
const inset = chrome.inset ?? { left: 0, right: 0 };
|
|
const chromeFrame: PageFrame = {
|
|
...frame,
|
|
pad: { ...pad, left: pad.left + inset.left, right: pad.right + inset.right },
|
|
contentWidth: frame.contentWidth - inset.left - inset.right,
|
|
};
|
|
const bandInset = chrome.bandInset ?? inset;
|
|
const bandBox = { left: pad.left + bandInset.left, width: frame.contentWidth - bandInset.left - bandInset.right };
|
|
return (
|
|
<Page
|
|
size={frame.page.size}
|
|
style={{
|
|
paddingTop: pad.top,
|
|
paddingRight: pad.right,
|
|
paddingBottom: pad.bottom,
|
|
paddingLeft: pad.left,
|
|
color: decor.ink,
|
|
...(decor.background ? { backgroundColor: decor.background } : {}),
|
|
}}
|
|
>
|
|
<AvailableWidthProvider width={frame.contentWidth}>{children}</AvailableWidthProvider>
|
|
<PageChrome
|
|
frame={chromeFrame}
|
|
footerLeft={chrome.footerName === false ? undefined : model.supplier.name}
|
|
footerLeftWidth={chrome.leftWidth}
|
|
footerCenter={model.footerText}
|
|
role={chrome.role}
|
|
color={chrome.color}
|
|
band={{ left: bandLeft, ...(chrome.bandRight === false ? {} : { right: "continued" }) }}
|
|
bandBox={bandBox}
|
|
bandColor={chrome.bandColor}
|
|
bottom={chrome.bottom}
|
|
decor={items}
|
|
/>
|
|
</Page>
|
|
);
|
|
}
|