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). A real Now imported by the user replaces Jost, so those roles take the design's 0.04 em back through * ownFont. 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, ownFont: { tracking: 0.04 }, 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, ownFont: { tracking: 0.04 }, 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, ownFont: { tracking: 0.04 } }, tableSub: { face: N, weight: 400, size: 8.5, leading: 11.5, tracking: 0.1, ownFont: { tracking: 0.04 } }, 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, ownFont: { tracking: 0.04 } }, /** 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, ownFont: { tracking: 0.04 } }, 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, ownFont: { tracking: 0.04 } }, signDesignation: { face: N, weight: 400, size: 9.6, leading: 14, tracking: 0.13, ownFont: { tracking: 0.04 } }, fine: { face: N, weight: 400, size: 8, leading: 11, tracking: 0.1, ownFont: { tracking: 0.04 } }, /** The footer band's contact lines. */ contact: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13, ownFont: { tracking: 0.04 }, 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;