Files
Voiced/src/pdf/fonts/roles.ts
T
xavierk 5ea62a9be8 Add the Northline template family: Purple Pop and Citrus Split
Purple Pop (cream page, purple pills, asterisk and dome decor, opt-in
logo) and Citrus Split (grey split panel, halftone chevrons, icon
contacts, logo in the measured slot) are rebuilt from the Canva PDFs on
the engine with A4/Letter support, GST slots and zero audit errors. Adds
static DM Sans, Jost and Poppins 800 as look-alikes for the commercial
fonts. The picker now lists ten templates.
2026-10-04 09:56:56 +05:30

93 lines
3.9 KiB
TypeScript

/**
* Typography roles. `leading` is an absolute line height in pt: Txt turns it into `lineHeight: "Npt"`
* on the same node as fontSize. Roles used by render-prop text (page numbers) have no leading, because
* react-pdf multiplies a render-prop lineHeight again on every relayout.
*
* TYPE_ROLES holds Classic's values. Every other template declares its own role table (see
* defineRoles) from the faces below and passes those tokens wherever a role is accepted.
*/
export type FaceKey =
| "IBM Plex Sans"
| "IBM Plex Mono"
| "Montserrat"
| "Open Sans"
| "Poppins"
| "Inter"
| "DM Sans"
| "Jost"
// Licensed faces of the Canva designs; SUBSTITUTES maps each to the bundled look-alike.
| "Now"
| "Gotham"
| "Open Sauce One";
export interface TypeToken {
face: FaceKey;
weight: 300 | 400 | 500 | 600 | 700 | 800;
italic?: boolean;
size: number;
/** pt, absolute. Optional only for tokens used by RenderTxt. */
leading?: number;
/** em; applied as letterSpacing = tracking * size. */
tracking?: number;
caps?: boolean;
/** Intent only: textkit exposes no OpenType feature switch, so this does not change rendering yet. */
tnum?: boolean;
/** Smallest size IdText/FitText may shrink to. */
minSize?: number;
}
const SANS: FaceKey = "IBM Plex Sans";
export const TYPE_ROLES = {
display: { face: SANS, weight: 600, size: 26, leading: 30 },
title: { face: SANS, weight: 600, size: 18, leading: 22, tracking: 1.5 / 18, caps: true },
h1: { face: SANS, weight: 600, size: 18, leading: 22 },
h2: { face: SANS, weight: 600, size: 13, leading: 17 },
body: { face: SANS, weight: 400, size: 9.5, leading: 13.8 },
bodyStrong: { face: SANS, weight: 600, size: 9.5, leading: 13.8 },
small: { face: SANS, weight: 400, size: 8.5, leading: 12 },
fine: { face: SANS, weight: 400, size: 7.5, leading: 10.5 },
tableHead: { face: SANS, weight: 400, size: 7.5, leading: 10, tracking: 0.8 / 7.5, caps: true },
tableBody: { face: SANS, weight: 400, size: 9.5, leading: 13.8 },
money: { face: SANS, weight: 400, size: 9.5, leading: 13.8, tnum: true },
moneyStrong: { face: SANS, weight: 600, size: 9.5, leading: 13.8, tnum: true },
footer: { face: SANS, weight: 400, size: 7.5 },
label: { face: SANS, weight: 400, size: 7.5, leading: 10, tracking: 1 / 7.5, caps: true },
} as const satisfies Record<string, TypeToken>;
export type RoleName = keyof typeof TYPE_ROLES;
/** A template's own role table: names to tokens. `satisfies`-friendly, so each template keeps its literal keys. */
export type RoleTable<K extends string = string> = Record<K, TypeToken>;
/** Checks a role table against TypeToken and keeps its literal keys: `export const ROLES = defineRoles({ ... })`. */
export function defineRoles<const T extends Record<string, TypeToken>>(table: T): T {
return table;
}
/**
* Licensed template faces (Now, Gotham, Open Sauce One, ...) map to the OFL look-alike that is
* actually bundled. A user-imported font replaces the licensed name through `faceOverrides`.
* Gotham runs about 4% narrower than Montserrat, so tokens that use it carry sizes already scaled by
* GOTHAM_SIZE (see templates/northline); a real Gotham import would need them back at 1.
*/
export const SUBSTITUTES: Readonly<Record<string, string>> = {
Now: "Jost",
Gotham: "Montserrat",
"Open Sauce One": "Poppins",
};
/** The family name to register/look up for a face: user override first, then substitute, then itself. */
export function resolveFace(face: string, overrides?: Readonly<Record<string, string>>): string {
return overrides?.[face] ?? SUBSTITUTES[face] ?? face;
}
export function roleToken(role: RoleName | TypeToken): TypeToken {
return typeof role === "string" ? TYPE_ROLES[role] : role;
}
/** Smallest shrink size for a token: its minSize, else 0.9x the size but never below 7 pt (or the size itself). */
export function minSizeOf(token: TypeToken): number {
return token.minSize ?? Math.min(token.size, Math.max(7, token.size * 0.9));
}