import type { ComponentType } from "react"; import { effectiveMargins, type MarginPreset, type PageFrame, type Sides } from "../engine/geometry"; import type { Rect } from "../engine/layoutTree"; import type { SlotLogoPlacement } from "../engine/logo"; import type { RoleName, RoleTable, TypeToken } from "../fonts/roles"; import type { DecorItem } from "../blocks/FirstPageHeader"; import type { RenderModel } from "../model/build"; import type { RenderPrefsV1 } from "../model/prefs"; import type { TemplateSlot } from "./slots.types"; /** A logo after fitting and the stack-or-side-by-side decision (engine/logo.ts). */ export interface PlacedLogo { dataUri: string; w: number; h: number; stacked: boolean; /** Set for slot-based templates: where the reference placeholder was and how the logo was fitted to it. */ placement?: SlotLogoPlacement; /** Where the fitted logo sits inside the slot's target box (pt from its top-left), so a flow layout can reproduce the alignment. */ slotOffset?: { x: number; y: number }; } /** What the page itself carries besides the flow: the paper, the ink and every-page art. */ export interface TemplateDecor { /** Page fill (the Page backgroundColor), e.g. Serenity's paper. Omitted: white. */ background?: string; /** Default text colour of the page. */ ink: string; /** Fixed every-page decor in page space; must stay clear of the content column. */ everyPage?: (frame: PageFrame) => DecorItem[]; /** Page-space boxes the flow must stay out of (audited as `keepout`). */ keepOuts?: (frame: PageFrame) => Rect[]; } /** Footer and page-label chrome of a template. */ export interface TemplateChrome { color: string; /** Hairline above the footer (Classic); null for none. */ rule: string | null; /** Footer type (a render-prop role: no leading). Default: the "footer" role. */ role?: RoleName | TypeToken; /** The footer text starts and ends this far inside the content column. */ inset?: { left: number; right: number }; /** Width the left footer text (supplier name) is fitted to. */ leftWidth: number; /** Height reserved at the foot of every page for the footer line; default FOOTER_RESERVE (it includes Classic's rule). */ reserve?: number; } export interface TemplateProps { model: RenderModel; frame: PageFrame; logo: PlacedLogo | null; prefs: RenderPrefsV1; } export interface TemplateDefinition { id: string; name: string; family: string; /** Static PNG for the template picker, served from /public (written by scripts/thumbnails.mjs). */ thumbnail: string; /** Shown on the picker tile. */ badge?: "Matches your original format"; /** The template's own type roles (Classic's are TYPE_ROLES). */ roles: RoleTable; decor: TemplateDecor; chrome: TemplateChrome; /** Bump when the layout changes in a way that alters pages; it goes into the PDF Creator string. */ version: number; /** The template's own physical margins (the "template" margin preset). */ margins: Sides; /** No margin preset may go below this, per side; the printer-safe floor still applies on top. */ marginFloor: Sides; logo: { /** The largest box the logo may fill, pt. */ box: { w: number; h: number }; /** The logo moves above the text when the text beside it would be narrower than this. */ stackBelowTextWidth: number; defaultOn: boolean; /** Width of the column the logo shares with the supplier text, for this frame. */ slotWidth: (frame: PageFrame) => number; /** * The measured reference placeholder (or opt-in area) the user's logo replaces, from slotFor(id). * Null for Classic, which fits its logo with computeClassicLogoPlacement. */ slot: TemplateSlot | null; /** The template has no placeholder: the logo is shown only when the user turns it on (prefs.logo.show). */ optIn?: boolean; /** Room the layout can really give the logo; the slot's target box shrinks to it. */ available?: (frame: PageFrame) => { w: number; h: number }; }; Layout: ComponentType; } /** Physical margins for a template under a margin preset. */ export function marginsFor(template: TemplateDefinition, preset: MarginPreset): Sides { return effectiveMargins(preset === "template" ? template.margins : preset, template.marginFloor); }