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)}
/>
)}
)}
>
);
}