Files
Voiced/src/pdf/templates/contract.ts
T
xavierk aa7a407944 Add the Neutral template family, template picker and text-only logo allowances
Linea (the vendor's original format.pdf design), Monogram and Serenity are
rebuilt from the reference PDFs as one parametric family on the engine, with
the user's logo in the measured slot, a home for every GST field and
A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and
a generated Ms Madi outline for Serenity's vertical word. Text-only logo
placeholders (Monolith, Tangerine) use a doc-derived allowance box. A
Carbon TemplatePicker with committed thumbnails opens from the page
setup controls.
2026-10-04 07:27:12 +05:30

100 lines
4.2 KiB
TypeScript

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