Add typography roles, text primitives, fontkit measurer and layout lint
Txt always sets an absolute line height; RenderTxt never does. Row pins every cell and stacks when the fill column would be too narrow. IdText and FitText shrink then chunk or truncate. The fontkit measurer matches react-pdf text widths exactly. A TypeScript-AST lint test enforces the layout rules (no lineHeight outside Txt, no non-literal fixed/wrap/minPresenceAhead, flexGrow needs flexBasis, no stray absolute positioning).
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* These values are the Classic template's; other templates will pass their own token objects.
|
||||
*/
|
||||
export type FaceKey = "IBM Plex Sans" | "IBM Plex Mono";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Licensed template faces (Now, Gotham, Open Sauce One, ...) map to the OFL look-alike that is
|
||||
* actually bundled. Empty until those families are registered.
|
||||
*/
|
||||
export const SUBSTITUTES: Readonly<Record<string, string>> = {};
|
||||
|
||||
/** 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));
|
||||
}
|
||||
Reference in New Issue
Block a user