Add the Marble and Highlighter templates, completing the 13 reference templates
Marble uses a flat-to-gradient band in place of the marble photo and bundles Lustria (OFL). Both logo slots are opt-in. Quirk 13 documents a fontkit glyph-cache issue that the tests work around with warm-up renders; the engine fix follows separately.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import type { Sides } from "../../engine/geometry";
|
||||
import { defineRoles, type TypeToken } from "../../fonts/roles";
|
||||
import type { GapRange } from "../neutral/variant";
|
||||
|
||||
const L = "Lustria" as const;
|
||||
const N = "Now" as const;
|
||||
|
||||
export const INK = "#000000";
|
||||
/** Hairlines around the parties band. */
|
||||
export const RULE = "#E7E1E6";
|
||||
/** Zebra rows. */
|
||||
export const ZEBRA = "#F7F7F8";
|
||||
/** The flat/gradient stand-in for the marble photograph: a light warm grey with a soft sheen. */
|
||||
export const BAND_FROM = "#F4F3F5";
|
||||
export const BAND_TO = "#ECEAEE";
|
||||
|
||||
/**
|
||||
* Marble's type (docs/templates/01-marble.md): Lustria in tracked caps for the title, headings, table heads and the
|
||||
* amount due; "Now" (drawn in Jost, the bundled look-alike) for everything else. Jost sets about 12% narrower than Now,
|
||||
* so every Now role carries the tracking that restores the reference widths (0.10 to 0.13 em on top of the design's
|
||||
* 0.04 em). Lustria ascent 0.955, descent 0.327; Jost 1.07 / 0.375.
|
||||
*/
|
||||
export const MARBLE_ROLES = defineRoles({
|
||||
/** "INVOICE" / "TAX INVOICE": 25.18 pt, tracked 0.192 em, shrunk to the room the stack has. */
|
||||
title: { face: L, weight: 400, size: 25.18, leading: 25.2, tracking: 0.192, caps: true, minSize: 13 },
|
||||
/** The vendor's name at the header's left (the design leaves it empty). */
|
||||
brand: { face: L, weight: 400, size: 12.65, leading: 18.2, tracking: 0.11, caps: true, minSize: 9 },
|
||||
meta: { face: N, weight: 400, size: 11.55, leading: 17.5, tracking: 0.105, minSize: 8 },
|
||||
/** BILL TO:, PAYABLE TO:, SUPPLIER:. */
|
||||
heading: { face: L, weight: 400, size: 12.65, leading: 18.2, tracking: 0.11, caps: true },
|
||||
body: { face: N, weight: 400, size: 11.55, leading: 18.2, tracking: 0.105, minSize: 8 },
|
||||
tableHead: { face: L, weight: 400, size: 11.68, leading: 16, tracking: 0.192, caps: true },
|
||||
tableBody: { face: N, weight: 400, size: 10.57, leading: 17.4, tracking: 0.102 },
|
||||
tableSub: { face: N, weight: 400, size: 8.5, leading: 11.5, tracking: 0.1 },
|
||||
totalLabel: { face: L, weight: 400, size: 9, leading: 16, tracking: 0.192, caps: true },
|
||||
totalValue: { face: N, weight: 400, size: 10.57, leading: 16, tracking: 0.102 },
|
||||
/** AMOUNT DUE: 14.6 pt, tracked 0.192 em. */
|
||||
due: { face: L, weight: 400, size: 14.6, leading: 20, tracking: 0.192, caps: true, minSize: 10 },
|
||||
small: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13 },
|
||||
smallLabel: { face: L, weight: 400, size: 9, leading: 15.3, tracking: 0.192, caps: true },
|
||||
signName: { face: N, weight: 400, size: 10.57, leading: 15, tracking: 0.102 },
|
||||
signDesignation: { face: N, weight: 400, size: 9.6, leading: 14, tracking: 0.13 },
|
||||
fine: { face: N, weight: 400, size: 8, leading: 11, tracking: 0.1 },
|
||||
/** The footer band's contact lines. */
|
||||
contact: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13, minSize: 7 },
|
||||
footer: { face: N, weight: 400, size: 7 },
|
||||
});
|
||||
|
||||
export type MarbleRoles = typeof MARBLE_ROLES;
|
||||
|
||||
/**
|
||||
* Reference A4 numbers: the rules and zebra bands start at x 31.5 and end at 549.8 (the text is inset 7 and 9.5 pt),
|
||||
* the right-hand column ends at 549.0; y values are the design's distances turned into flow gaps.
|
||||
*/
|
||||
export interface MarbleVariant {
|
||||
roles: MarbleRoles;
|
||||
ink: string;
|
||||
margins: Sides;
|
||||
marginFloor: Sides;
|
||||
/** Text inset inside the rules, left (parties) and right. */
|
||||
inset: { left: number; right: number };
|
||||
/** The top band's height and the bottom band's (A4 and Letter share the bottom band; the footer text is capped by lines). */
|
||||
bands: { top: number; bottom: number };
|
||||
head: {
|
||||
/** Title line box below the header's top (the logo slot's top). */
|
||||
titleTop: number;
|
||||
/** Title box bottom to the first line of the stack. */
|
||||
titleGap: number;
|
||||
/** The widest share of the content width the right-hand stack may take. */
|
||||
maxShare: number;
|
||||
minStackW: number;
|
||||
/** Vendor name top below the header's top when there is no logo; below the logo box otherwise. */
|
||||
nameTop: number;
|
||||
nameGap: number;
|
||||
minNameW: number;
|
||||
/** Space between the left column and the stack at its narrowest. */
|
||||
colGap: number;
|
||||
/** The logo slot's box height. */
|
||||
logoH: number;
|
||||
};
|
||||
rule: number;
|
||||
parties: { colA: number; colB: number; headingGap: number };
|
||||
gaps: {
|
||||
/** Header bottom to the first hairline. */
|
||||
headRule: GapRange;
|
||||
/** Hairline to the first heading, and the last info line to the second hairline. */
|
||||
infoTop: GapRange;
|
||||
infoBottom: GapRange;
|
||||
/** Second hairline to the table heading. */
|
||||
band: GapRange;
|
||||
/** Last table row to the totals. */
|
||||
tail: GapRange;
|
||||
/** The totals (or the notes) to the signature band. */
|
||||
close: GapRange;
|
||||
};
|
||||
table: {
|
||||
/** Heading box bottom to the first zebra band. */
|
||||
headGap: number;
|
||||
descInset: number;
|
||||
headDescInset: number;
|
||||
cols: { qty: number; rate: number; amount: number };
|
||||
numPad: number;
|
||||
/** Right padding of the rate column (the unit is part of the rate, so it gets less than the amounts). */
|
||||
ratePad: number;
|
||||
padY: (rows: number) => number;
|
||||
padYMin: number;
|
||||
};
|
||||
totals: { boxW: number; rowGap: number; dueGap: number; rightInset: number; leftGap: number; wordsMax: number };
|
||||
closing: { signW: number; gap: number; notesBesideMax: number; signatureImageH: number };
|
||||
footer: {
|
||||
/** Footer text left edge, from the page's left edge (A4 reference: x 40.7) and the second column's left (x 197.8). */
|
||||
col1: number;
|
||||
col2: number;
|
||||
colGap: number;
|
||||
col1MaxW: number;
|
||||
/** Band top to the first line box. */
|
||||
top: number;
|
||||
/** Lines the first column may take (name plus address) on A4 and Letter. */
|
||||
maxLines: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const MARBLE_VARIANT: MarbleVariant = {
|
||||
roles: MARBLE_ROLES,
|
||||
ink: INK,
|
||||
margins: { top: 86, right: 45.8, bottom: 24, left: 31.5 },
|
||||
marginFloor: { top: 86, right: 31.5, bottom: 24, left: 31.5 },
|
||||
inset: { left: 7, right: 0.8 },
|
||||
bands: { top: 64, bottom: 130 },
|
||||
head: { titleTop: 14.85, titleGap: 3.7, maxShare: 0.7, minStackW: 170, nameTop: 26.8, nameGap: 6, minNameW: 90, colGap: 24, logoH: 50 },
|
||||
rule: 0.75,
|
||||
parties: { colA: 172.9, colB: 168.6, headingGap: 2.6 },
|
||||
gaps: {
|
||||
headRule: { nominal: 36.8, min: 14 },
|
||||
infoTop: { nominal: 16.05, min: 10 },
|
||||
infoBottom: { nominal: 11.56, min: 8 },
|
||||
band: { nominal: 50.2, min: 22 },
|
||||
tail: { nominal: 14, min: 6 },
|
||||
close: { nominal: 14, min: 8 },
|
||||
},
|
||||
table: {
|
||||
headGap: 8.85,
|
||||
descInset: 9.5,
|
||||
headDescInset: 7.3,
|
||||
cols: { qty: 84, rate: 72.9, amount: 74.9 },
|
||||
numPad: 9.5,
|
||||
ratePad: 5,
|
||||
padY: (rows) => (rows <= 8 ? 7.3 : rows <= 14 ? 6 : 4.5),
|
||||
padYMin: 3.5,
|
||||
},
|
||||
totals: { boxW: 230, rowGap: 0, dueGap: 10, rightInset: 14.1, leftGap: 16, wordsMax: 280 },
|
||||
closing: { signW: 170, gap: 24, notesBesideMax: 90, signatureImageH: 40 },
|
||||
footer: { col1: 40.7, col2: 197.8, colGap: 24, col1MaxW: 190, top: 24.7, maxLines: 5 },
|
||||
};
|
||||
|
||||
export const leading = (t: TypeToken) => t.leading as number;
|
||||
Reference in New Issue
Block a user