Add the Northline template family: Purple Pop and Citrus Split
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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { View } from "@react-pdf/renderer";
|
||||
import type { ReactNode } from "react";
|
||||
import { colsDetailed, type ColSpec } from "../engine/columns";
|
||||
import { colsDetailed, pinned, type ColSpec } from "../engine/columns";
|
||||
import { PlanError } from "../engine/errors";
|
||||
import { TABLE_HEAD_ID, TABLE_ROW_PREFIX } from "../engine/ids";
|
||||
import { drawnText, type Measurer } from "../engine/measure";
|
||||
@@ -37,6 +37,8 @@ export interface TableLook {
|
||||
headBorder?: number;
|
||||
/** Extra right padding per column, pt (e.g. keeps description text off the next column). Counted in widths. */
|
||||
padRight?: number[];
|
||||
/** Extra left padding per column, pt (a header or body text that starts inside a filled cell). Counted in widths. */
|
||||
padLeft?: number[];
|
||||
faceOverrides?: Readonly<Record<string, string>>;
|
||||
/** Height of the `headerTail` TableRows draws under the header row (a rule and its spacing); counted in the header height. */
|
||||
headerTailH?: number;
|
||||
@@ -108,7 +110,7 @@ export function planItemsTable(input: ItemsTablePlanInput): ItemsTablePlan {
|
||||
for (const r of rows) if (r.length !== columns.length) throw new PlanError(`row has ${r.length} cells for ${columns.length} columns`, "invalid-plan");
|
||||
|
||||
const { widths } = colsDetailed(width, columns, gap);
|
||||
const inner = widths.map((w, i) => Math.max(0, w - 2 * padX - (input.padRight?.[i] ?? 0)));
|
||||
const inner = widths.map((w, i) => Math.max(0, w - 2 * padX - (input.padRight?.[i] ?? 0) - (input.padLeft?.[i] ?? 0)));
|
||||
const heightOf = (role: RoleName | TypeToken, cells: TableCellValue[], py = padY, bd = border) => {
|
||||
const hs = cells.map((c, i) => cellHeight(measurer, role, c, inner[i], input));
|
||||
return { lines: Math.max(...hs.map((h) => h.lines)), height: Math.max(...hs.map((h) => h.height)) + 2 * py + bd };
|
||||
@@ -144,10 +146,13 @@ export interface TableRowsProps extends TableLook {
|
||||
/** Content after the last row, inside the table container (the totals). */
|
||||
tail?: ReactNode;
|
||||
headerStyle?: Record<string, unknown>;
|
||||
rowStyle?: Record<string, unknown>;
|
||||
/** Style of each body row; a function gets the row index (alternating fills). */
|
||||
rowStyle?: Record<string, unknown> | ((index: number) => Record<string, unknown>);
|
||||
/** Style of one header cell by column (a filled cell beside an unfilled one). The cell is as tall as the header row only while the header is one line. */
|
||||
headCellStyle?: (column: number) => Record<string, unknown> | undefined;
|
||||
borderColor?: string;
|
||||
/** Text colour of the header cells. */
|
||||
headColor?: string;
|
||||
/** Text colour of the header cells, or per column. */
|
||||
headColor?: string | ((column: number) => string | undefined);
|
||||
/**
|
||||
* Drawn under the header row inside the same fixed group, so it repeats with the header on every page
|
||||
* (a rule and the gap below it: nothing under a repeated header may depend on a first-page-only margin).
|
||||
@@ -167,6 +172,12 @@ export interface TableRowsProps extends TableLook {
|
||||
* width. The header group is fixed (it repeats), so the bleed passes the safe-zone audit.
|
||||
*/
|
||||
bleed?: { left: number; width: number; bg: string };
|
||||
/**
|
||||
* Rounded "pill" rows (Purple Pop): `width` is the pill's outer width, the cells are laid out `rowInset` inside
|
||||
* it on both sides, and the pill itself (rowStyle / headerStyle carry its fill and radius) moves `indent` right
|
||||
* of the container's left edge. Plan with planItemsTable at width - 2 * rowInset.
|
||||
*/
|
||||
pill?: { indent: number; inset: number };
|
||||
}
|
||||
|
||||
/** Extra right padding per column that stands in for a grid's vertical rules, for planItemsTable's `padRight`. */
|
||||
@@ -179,19 +190,20 @@ export function gridPadRight(columns: number, ruleWidth: number): number[] {
|
||||
* `wrap={false}` row per item. The container has no padding or border; those go on the rows, because
|
||||
* the repeated header is positioned against the container's top.
|
||||
*/
|
||||
export function TableRows({ columns, gap, width, header, rows, roles, align = [], tailH, tail, headerStyle, rowStyle, borderColor, headColor, headerTail, grid, bleed, ...look }: TableRowsProps) {
|
||||
export function TableRows({ columns, gap, width, header, rows, roles, align = [], tailH, tail, headerStyle, rowStyle, headCellStyle, borderColor, headColor, headerTail, grid, bleed, pill, ...look }: TableRowsProps) {
|
||||
const padX = look.padX ?? 0;
|
||||
const padY = look.padY ?? 4;
|
||||
const headPadY = look.headPadY ?? padY;
|
||||
const border = look.border ?? 0;
|
||||
const headBorder = look.headBorder ?? border;
|
||||
const { widths } = colsDetailed(width, columns, gap);
|
||||
const { widths } = colsDetailed(width - 2 * (pill?.inset ?? 0), columns, gap);
|
||||
const cellStyle = (extra?: Record<string, unknown>, bd = border) => ({
|
||||
...(bd ? { borderBottomWidth: bd, borderBottomColor: borderColor ?? "#000000", borderBottomStyle: "solid" } : {}),
|
||||
...extra,
|
||||
});
|
||||
const cells = (role: RoleName | TypeToken, values: TableCellValue[], color?: string, isHeader = false) =>
|
||||
const cells = (role: RoleName | TypeToken, values: TableCellValue[], colorOf?: string | ((column: number) => string | undefined), isHeader = false) =>
|
||||
values.map((v, i) => {
|
||||
const color = typeof colorOf === "function" ? colorOf(i) : colorOf;
|
||||
const edge = grid?.width ?? 0;
|
||||
const last = i === values.length - 1;
|
||||
const inner = isHeader ? (grid?.headerColor ?? grid?.color) : grid?.color;
|
||||
@@ -200,11 +212,11 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
return (
|
||||
<Cell
|
||||
key={i}
|
||||
padding={{ top: isHeader ? headPadY : padY, bottom: isHeader ? headPadY : padY, left: padX, right: padX + (look.padRight?.[i] ?? 0) }}
|
||||
padding={{ top: isHeader ? headPadY : padY, bottom: isHeader ? headPadY : padY, left: padX + (look.padLeft?.[i] ?? 0), right: padX + (look.padRight?.[i] ?? 0) }}
|
||||
borderLeft={grid && i === 0 ? edge : 0}
|
||||
borderRight={edge}
|
||||
style={
|
||||
grid
|
||||
style={{
|
||||
...(grid
|
||||
? {
|
||||
flexGrow: 1,
|
||||
flexBasis: "auto",
|
||||
@@ -212,11 +224,12 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
borderRightColor: last ? outer : inner,
|
||||
...(isHeader ? { borderTopWidth: edge, borderTopColor: outer } : {}),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: {}),
|
||||
...(isHeader ? headCellStyle?.(i) : {}),
|
||||
}}
|
||||
>
|
||||
{cellParts(v).map((part, li) => (
|
||||
<Txt key={li} role={partRole(part, role)} color={color} width={Math.max(0, widths[i] - 2 * padX - (look.padRight?.[i] ?? 0) - ruleW)} align={align[i]} faceOverrides={look.faceOverrides}>
|
||||
<Txt key={li} role={partRole(part, role)} color={color} width={Math.max(0, widths[i] - 2 * padX - (look.padRight?.[i] ?? 0) - (look.padLeft?.[i] ?? 0) - ruleW)} align={align[i]} faceOverrides={look.faceOverrides}>
|
||||
{partText(part)}
|
||||
</Txt>
|
||||
))}
|
||||
@@ -224,6 +237,21 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
);
|
||||
});
|
||||
const rowAlign = grid ? ("stretch" as const) : undefined;
|
||||
const inset = pill?.inset ?? 0;
|
||||
const cellsWidth = width - 2 * inset;
|
||||
/** One table line: a plain Row, or for pills an outer box (the fill) around a Row of the inner width. */
|
||||
const line = (key: string | number, id: string, flags: ReturnType<typeof pdfFlags>, style: Record<string, unknown>, children: ReactNode) =>
|
||||
pill ? (
|
||||
<View key={key} id={id} {...flags} style={{ ...pinned(width), marginLeft: pill.indent, paddingLeft: inset, paddingRight: inset, ...style } as never}>
|
||||
<Row widths={columns} gap={gap} width={cellsWidth} align={rowAlign}>
|
||||
{children}
|
||||
</Row>
|
||||
</View>
|
||||
) : (
|
||||
<Row key={key} id={id} widths={columns} gap={gap} width={width} align={rowAlign} {...flags} style={style}>
|
||||
{children}
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<AvailableWidthProvider width={width}>
|
||||
@@ -233,30 +261,21 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
{...pdfFlags({ fixed: true })}
|
||||
style={bleed ? { marginLeft: -bleed.left, width: bleed.width, minWidth: bleed.width, maxWidth: bleed.width, paddingLeft: bleed.left, backgroundColor: bleed.bg } : undefined}
|
||||
>
|
||||
<Row id={TABLE_HEAD_ID} widths={columns} gap={gap} width={width} align={rowAlign} style={cellStyle(headerStyle, headBorder)}>
|
||||
{cells(roles.head, header, headColor, true)}
|
||||
</Row>
|
||||
{line("head", TABLE_HEAD_ID, {}, cellStyle(headerStyle, headBorder), cells(roles.head, header, headColor, true))}
|
||||
{headerTail}
|
||||
</View>
|
||||
) : (
|
||||
<Row id={TABLE_HEAD_ID} widths={columns} gap={gap} width={width} align={rowAlign} fixed style={cellStyle(headerStyle, headBorder)}>
|
||||
{cells(roles.head, header, headColor, true)}
|
||||
</Row>
|
||||
line("head", TABLE_HEAD_ID, pdfFlags({ fixed: true }), cellStyle(headerStyle, headBorder), cells(roles.head, header, headColor, true))
|
||||
)}
|
||||
{rows.map((r, i) =>
|
||||
line(
|
||||
i,
|
||||
`${TABLE_ROW_PREFIX}${i}`,
|
||||
pdfFlags({ wrap: false, minPresenceAhead: i === rows.length - 1 ? tailH : undefined }),
|
||||
cellStyle(typeof rowStyle === "function" ? rowStyle(i) : rowStyle),
|
||||
cells(roles.body, r),
|
||||
),
|
||||
)}
|
||||
{rows.map((r, i) => (
|
||||
<Row
|
||||
key={i}
|
||||
id={`${TABLE_ROW_PREFIX}${i}`}
|
||||
widths={columns}
|
||||
gap={gap}
|
||||
width={width}
|
||||
align={rowAlign}
|
||||
{...pdfFlags({ wrap: false, minPresenceAhead: i === rows.length - 1 ? tailH : undefined })}
|
||||
style={cellStyle(rowStyle)}
|
||||
>
|
||||
{cells(roles.body, r)}
|
||||
</Row>
|
||||
))}
|
||||
{tail}
|
||||
</View>
|
||||
</AvailableWidthProvider>
|
||||
|
||||
Reference in New Issue
Block a user