/** * 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; 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 = Record; /** Checks a role table against TypeToken and keeps its literal keys: `export const ROLES = defineRoles({ ... })`. */ export function defineRoles>(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> = { 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>): 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)); }