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"; import { BAND_ID, DECOR_ID, PAGE_LABEL_ID } from "../engine/ids"; 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". */ role?: RoleName | TypeToken; color?: string; /** Continuation band for pages 2+ (shown only when pageNumber > 1). */ band?: { left?: string; right?: string }; /** Page-space top of the band. Default: the top of the reserve, pad.top - continuationReserve (>= 0). */ bandTop?: number; /** Every-page decor in page space. */ decor?: DecorItem[]; /** Footer row offset from the page bottom; default: the bottom margin (padding minus the footer reserve). */ bottom?: number; /** Colour of the continuation band's text; default `color`. */ bandColor?: string; /** Page-space left edge and width of the continuation band; default: the frame's content box. */ bandBox?: { left: number; width: number }; } /** * 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}`; } /** * Fixed chrome for every page. Put it AFTER the flow as the last child of the Page: with the legacy * paginator a fixed node placed after the flow still repeats on every page. * * Everything is a page-level absolute anchored to the page edges, so none of it takes part in the flow: * - footer: a row in the bottom margin, between the content bottom (page height - pad.bottom) and the * physical margin. `frame.footerReserve` must cover chromeHeight(role); both texts are render-prop * Text in an AUTO-height box (a fixed height smaller than the line draws nothing). * - continuation band: only on pages 2+. On page 1 its render prop returns "" (0 lines, expected). * - decor: fixed absolute boxes in page space, flagged with DECOR_ID. * * The page label has a pinned width taken from the widest label ("Page 99 of 99"), never from the * current pass, because `totalPages` is undefined on the first pass and `subPageNumber` in the split pass. */ export function PageChrome({ frame, footerLeft, footerLeftWidth, footerCenter, pageLabel = true, role = "footer", color, band, bandTop, decor = [], bottom, bandBox, bandColor, }: PageChromeProps) { const { pad } = frame; const need = chromeHeight(role); 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"); } const labelWidth = Math.ceil(measureToken(getMeasurer(), role, "Page 99 of 99")) + 2; const marginBottom = bottom ?? pad.bottom - frame.footerReserve; const bandLeftEdge = bandBox?.left ?? pad.left; const bandWidth = bandBox?.width ?? frame.contentWidth; const bandRightWidth = band?.right !== undefined ? Math.ceil(measureToken(getMeasurer(), role, band.right)) + 2 : 0; const top = bandTop ?? Math.max(0, pad.top - frame.continuationReserve); return ( <> {decor.map((d, i) => ( {d.children} ))} {band && (band.left !== undefined || band.right !== undefined) && ( {band.left !== undefined && ( (pageNumber > 1 ? (band.left ?? "") : "")} /> )} {band.right !== undefined && ( (pageNumber > 1 ? (band.right ?? "") : "")} /> )} )} {showFooter && ( {footerLeft !== undefined && (footerLeftWidth !== undefined ? ( {footerLeft} ) : ( footerLeft} /> ))} {footerCenter !== undefined && footerCenter} />} {footerCenter === undefined && footerLeftWidth !== undefined && } {pageLabel && ( pageLabelText(pageNumber, totalPages)} /> )} )} ); }