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.
93 lines
3.9 KiB
TypeScript
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));
|
|
}
|