Add the Marble and Highlighter templates, completing the 13 Canva 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,575 @@
|
||||
import { Image, View } from "@react-pdf/renderer";
|
||||
import type { ReactNode } from "react";
|
||||
import { colsDetailed, pinned } from "../../engine/columns";
|
||||
import { CLOSING_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, TOTALS_ID } from "../../engine/ids";
|
||||
import { AvailableWidthProvider } from "../../blocks/AvailableWidth";
|
||||
import { BottomSpacer } from "../../blocks/BottomSpacer";
|
||||
import { FirstPageHeader } from "../../blocks/FirstPageHeader";
|
||||
import { FitText } from "../../blocks/FitText";
|
||||
import { IdText } from "../../blocks/IdText";
|
||||
import { Cell, Row } from "../../blocks/Row";
|
||||
import { SafeTxt } from "../../blocks/SafeTxt";
|
||||
import { pdfFlags } from "../../blocks/pdfProps";
|
||||
import { Slot } from "../../blocks/slots";
|
||||
import { TableGuard } from "../../blocks/TableGuard";
|
||||
import { Txt } from "../../blocks/Txt";
|
||||
import { ContactIcon } from "../../decor/CircledIcon";
|
||||
import { META_ID, SUPPLIER_ID, TITLE_ID } from "../classic/ids";
|
||||
import type { PlacedLogo, TemplateProps } from "../contract";
|
||||
import { includesName } from "../logoName";
|
||||
import { CANCELLED_INK, CANCELLED_SENTENCE } from "../neutral/plan";
|
||||
import type { CellPart, TableCellValue } from "../../blocks/ItemsTable";
|
||||
import { LABEL_GAP, NOTES_FLOW_GAP, paymentLines, planHighlighter, REVERSE_GAP, signatureOptions, taxLabel, type HighlighterPlan } from "./plan";
|
||||
import { BLACK, INDIGO, YELLOW, type HighlighterVariant } from "./tokens";
|
||||
|
||||
/** Layout-tree ids of the blocks that are Highlighter's own (the harness ids are in classic/ids.ts). */
|
||||
export const PAY_ID = "highlighter-pay";
|
||||
export const PAD_PREFIX = "highlighter-pad-";
|
||||
|
||||
/** A block that starts with less than a line of room left is cut into zero-height lines: it starts on the next page instead. */
|
||||
const NOTES_HOLD = 70;
|
||||
|
||||
interface Ctx {
|
||||
model: TemplateProps["model"];
|
||||
plan: HighlighterPlan;
|
||||
v: HighlighterVariant;
|
||||
logo: PlacedLogo | null;
|
||||
}
|
||||
|
||||
function Spacer({ h }: { h: number }) {
|
||||
return h > 0 ? <View style={{ height: h }} /> : null;
|
||||
}
|
||||
|
||||
// --- header -----------------------------------------------------------------------------------------------------
|
||||
|
||||
function Title({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const h = plan.head;
|
||||
return (
|
||||
<View style={{ ...pinned(h.titleW), marginLeft: v.inset } as never}>
|
||||
<FitText id={TITLE_ID} role={{ ...v.roles.title, size: h.titleSize }} color={v.ink} width={h.titleW}>
|
||||
{model.docTitle}
|
||||
</FitText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/** The vendor at the top right: the logo (right-aligned at the slot's top), the name, GSTIN and PAN. */
|
||||
function Vendor({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v, logo } = ctx;
|
||||
const h = plan.head;
|
||||
const r = v.roles;
|
||||
const s = model.supplier;
|
||||
return (
|
||||
<View id={SUPPLIER_ID} style={pinned(h.stackW) as never}>
|
||||
<AvailableWidthProvider width={h.stackW}>
|
||||
{logo ? <Image src={logo.dataUri} style={{ ...pinned(logo.w), height: logo.h, marginLeft: Math.max(0, h.stackW - logo.w), objectFit: "contain" } as never} /> : null}
|
||||
{h.showName ? (
|
||||
<SafeTxt role={r.vendorName} color={v.ink} align="right">
|
||||
{s.name}
|
||||
</SafeTxt>
|
||||
) : null}
|
||||
<Slot.Gstin model={model} party="supplier" role={r.vendorId} color={v.ink} align="right" />
|
||||
{s.pan ? (
|
||||
<IdText role={r.vendorId} color={v.ink} align="right" fill>
|
||||
{`PAN: ${s.pan}`}
|
||||
</IdText>
|
||||
) : null}
|
||||
{model.cancelled ? (
|
||||
<Txt role={r.vendorName} color={CANCELLED_INK} width={h.stackW} align="right">
|
||||
CANCELLED
|
||||
</Txt>
|
||||
) : null}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderRow({ ctx }: { ctx: Ctx }) {
|
||||
const { plan, v } = ctx;
|
||||
return (
|
||||
<Row widths={[{ fill: true }, { fixed: plan.head.stackW + v.rightInset }]} width={plan.W} style={{ minHeight: plan.head.h }}>
|
||||
<Title ctx={ctx} />
|
||||
<Vendor ctx={ctx} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
// --- fields and payment -----------------------------------------------------------------------------------------
|
||||
|
||||
function Field({ ctx, index }: { ctx: Ctx; index: number }) {
|
||||
const { plan, v } = ctx;
|
||||
const f = v.fields;
|
||||
const spec = plan.fields.specs[index];
|
||||
const labelW = plan.fields.labelW[index];
|
||||
const valueW = f.w - labelW - f.labelGap;
|
||||
return (
|
||||
<View style={{ marginTop: index > 0 ? f.rowGap : 0 }}>
|
||||
<Row widths={[{ fixed: labelW }, { fixed: valueW }]} gap={f.labelGap} width={f.w}>
|
||||
<Txt role={v.roles.label} color={v.ink} width={labelW}>
|
||||
{spec.label}
|
||||
</Txt>
|
||||
<View style={{ ...pinned(valueW), borderBottomWidth: v.hair, borderBottomColor: INDIGO } as never}>
|
||||
<AvailableWidthProvider width={valueW - f.valuePad}>
|
||||
<View style={{ marginLeft: f.valuePad }}>
|
||||
<SafeTxt role={v.roles.value} color={v.ink}>
|
||||
{spec.value}
|
||||
</SafeTxt>
|
||||
</View>
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
</Row>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Fields({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const r = v.roles;
|
||||
const c = model.client;
|
||||
const f = v.fields;
|
||||
return (
|
||||
<View id={META_ID} style={pinned(f.w) as never}>
|
||||
{plan.fields.specs.map((_, i) => (
|
||||
<Field key={i} ctx={ctx} index={i} />
|
||||
))}
|
||||
{plan.fields.subH > 0 ? (
|
||||
<View style={{ ...pinned(f.w), marginTop: f.subGap } as never}>
|
||||
<AvailableWidthProvider width={f.w}>
|
||||
{c.addressLines.map((line, i) => (
|
||||
<SafeTxt key={i} role={r.sub} color={v.ink}>
|
||||
{line}
|
||||
</SafeTxt>
|
||||
))}
|
||||
<Slot.Gstin model={model} party="client" role={r.sub} color={v.ink} />
|
||||
<Slot.PlaceOfSupply model={model} role={r.sub} color={v.ink} />
|
||||
{c.poNumber ? <SafeTxt role={r.sub} color={v.ink}>{`PO: ${c.poNumber}`}</SafeTxt> : null}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Payment({ ctx }: { ctx: Ctx }) {
|
||||
const { model, v } = ctx;
|
||||
const r = v.roles;
|
||||
const lines = paymentLines(model);
|
||||
if (lines.length === 0) return <View />;
|
||||
const w = v.pay.w;
|
||||
return (
|
||||
<View id={PAY_ID} style={{ ...pinned(w), marginTop: v.pay.top } as never}>
|
||||
<AvailableWidthProvider width={w}>
|
||||
<Txt role={r.payHead} color={v.ink} width={w}>
|
||||
Payment Method:
|
||||
</Txt>
|
||||
{lines.map((line, i) => (
|
||||
<SafeTxt key={i} role={r.payLine} color={v.ink}>
|
||||
{line}
|
||||
</SafeTxt>
|
||||
))}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// --- table ------------------------------------------------------------------------------------------------------
|
||||
|
||||
const partsOf = (value: TableCellValue): CellPart[] => (Array.isArray(value) ? (value.length ? value : [""]) : [value]);
|
||||
|
||||
/** One table line inside the 3 pt frame: the frame's sides are the line's own borders, the dividers are its cells'. */
|
||||
function Line({
|
||||
ctx,
|
||||
id,
|
||||
last,
|
||||
flags,
|
||||
minH,
|
||||
children,
|
||||
}: {
|
||||
ctx: Ctx;
|
||||
id: string;
|
||||
last: boolean;
|
||||
flags: ReturnType<typeof pdfFlags>;
|
||||
minH?: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { plan, v } = ctx;
|
||||
return (
|
||||
<View
|
||||
id={id}
|
||||
{...flags}
|
||||
style={
|
||||
{
|
||||
...pinned(plan.W),
|
||||
borderLeftWidth: v.frame,
|
||||
borderRightWidth: v.frame,
|
||||
borderLeftColor: v.ink,
|
||||
borderRightColor: v.ink,
|
||||
borderBottomWidth: last ? v.frame : v.hair,
|
||||
borderBottomColor: last ? v.ink : INDIGO,
|
||||
} as never
|
||||
}
|
||||
>
|
||||
<Row widths={plan.columns} width={plan.innerW} align="stretch" style={minH ? { minHeight: minH } : undefined}>
|
||||
{children}
|
||||
</Row>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function colInner(ctx: Ctx, i: number): number {
|
||||
const { plan, v } = ctx;
|
||||
const w = plan.table.widths[i];
|
||||
const last = i === plan.columns.length - 1;
|
||||
return Math.max(0, w - plan.padLeft[i] - (plan.padRight[i] - (last ? 0 : v.hair)) - (last ? 0 : v.hair));
|
||||
}
|
||||
|
||||
function BodyCell({ ctx, i, children }: { ctx: Ctx; i: number; children?: ReactNode }) {
|
||||
const { plan, v } = ctx;
|
||||
const last = i === plan.columns.length - 1;
|
||||
return (
|
||||
<Cell
|
||||
padding={{ top: plan.padY, bottom: plan.padY, left: plan.padLeft[i], right: plan.padRight[i] - (last ? 0 : v.hair) }}
|
||||
borderRight={last ? 0 : v.hair}
|
||||
style={{ flexGrow: 1, flexBasis: "auto", ...(last ? {} : { borderRightColor: INDIGO }) }}
|
||||
>
|
||||
{children}
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
function DataRow({ ctx, index, lastRow }: { ctx: Ctx; index: number; lastRow: boolean }) {
|
||||
const { plan, v } = ctx;
|
||||
const r = v.roles;
|
||||
const cells = plan.rows[index];
|
||||
return (
|
||||
<Line ctx={ctx} id={`${TABLE_ROW_PREFIX}${index}`} last={lastRow} flags={pdfFlags({ wrap: false, minPresenceAhead: lastRow ? plan.tailH + (v.frame - v.hair) : undefined })}>
|
||||
{cells.map((value, i) => (
|
||||
<BodyCell key={i} ctx={ctx} i={i}>
|
||||
{partsOf(value).map((part, li) => {
|
||||
const text = typeof part === "string" ? part : part.text;
|
||||
const role = typeof part === "string" ? r.tableBody : (part.role as typeof r.tableBody);
|
||||
return i === 0 && typeof part === "string" ? (
|
||||
<SafeTxt key={li} role={role} color={v.ink}>
|
||||
{text}
|
||||
</SafeTxt>
|
||||
) : (
|
||||
<Txt key={li} role={role} color={v.ink} width={colInner(ctx, i)} align={plan.align[i]}>
|
||||
{text}
|
||||
</Txt>
|
||||
);
|
||||
})}
|
||||
</BodyCell>
|
||||
))}
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
|
||||
function PadRow({ ctx, index, lastRow }: { ctx: Ctx; index: number; lastRow: boolean }) {
|
||||
const { plan, v } = ctx;
|
||||
return (
|
||||
<Line ctx={ctx} id={`${PAD_PREFIX}${index}`} last={lastRow} flags={pdfFlags({ wrap: false, minPresenceAhead: lastRow ? plan.tailH + (v.frame - v.hair) : undefined })} minH={plan.padRowH - v.hair}>
|
||||
{plan.columns.map((_, i) => (
|
||||
<BodyCell key={i} ctx={ctx} i={i} />
|
||||
))}
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
|
||||
/** The header group, repeated on every page: the yellow bar, the gap, and the body's 3 pt top rule. */
|
||||
function HeaderGroup({ ctx }: { ctx: Ctx }) {
|
||||
const { plan, v } = ctx;
|
||||
const t = v.table;
|
||||
const r = v.roles;
|
||||
const fr = v.frame;
|
||||
return (
|
||||
<View {...pdfFlags({ fixed: true })} style={pinned(plan.W) as never}>
|
||||
<View
|
||||
id={TABLE_HEAD_ID}
|
||||
style={
|
||||
{
|
||||
...pinned(plan.W),
|
||||
backgroundColor: YELLOW,
|
||||
borderWidth: fr,
|
||||
borderColor: v.ink,
|
||||
} as never
|
||||
}
|
||||
>
|
||||
<Row widths={plan.columns} width={plan.innerW} align="stretch">
|
||||
{plan.headerCells.map((text, i) => {
|
||||
const last = i === plan.columns.length - 1;
|
||||
return (
|
||||
<Cell
|
||||
key={i}
|
||||
padding={{ top: t.barPadTop, bottom: t.barPadTop, left: 0, right: 0 }}
|
||||
borderRight={last ? 0 : v.hair}
|
||||
style={{ flexGrow: 1, flexBasis: "auto", ...(last ? {} : { borderRightColor: INDIGO }) }}
|
||||
>
|
||||
<Txt role={r.tableHead} color={v.ink} width={Math.max(0, plan.table.widths[i] - (last ? 0 : v.hair))} align="center">
|
||||
{text}
|
||||
</Txt>
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</View>
|
||||
<Spacer h={t.bodyGap} />
|
||||
<View style={{ ...pinned(plan.W), height: fr, backgroundColor: v.ink } as never} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Table({ ctx }: { ctx: Ctx }) {
|
||||
const { plan } = ctx;
|
||||
const n = plan.rows.length;
|
||||
return (
|
||||
<AvailableWidthProvider width={plan.W}>
|
||||
<View style={pinned(plan.W) as never}>
|
||||
<HeaderGroup ctx={ctx} />
|
||||
{plan.rows.map((_, i) => (
|
||||
<DataRow key={i} ctx={ctx} index={i} lastRow={i === n - 1 && plan.padRows === 0} />
|
||||
))}
|
||||
{Array.from({ length: plan.padRows }, (_, i) => (
|
||||
<PadRow key={i} ctx={ctx} index={i} lastRow={i === plan.padRows - 1} />
|
||||
))}
|
||||
<Tail ctx={ctx} />
|
||||
</View>
|
||||
</AvailableWidthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// --- totals -----------------------------------------------------------------------------------------------------
|
||||
|
||||
function TotalRow({ ctx, label, amount, total }: { ctx: Ctx; label: string; amount: string; total?: boolean }) {
|
||||
const { plan, v } = ctx;
|
||||
const t = plan.tail;
|
||||
const r = v.roles;
|
||||
const o = v.totals;
|
||||
const pad = { paddingTop: o.padY, paddingBottom: o.padY };
|
||||
return (
|
||||
<View style={{ flexDirection: "row", ...pinned(t.boxW - 2 * v.frame), ...(total ? {} : { borderBottomWidth: v.hair, borderBottomColor: INDIGO }) } as never}>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
...pinned(t.labelW),
|
||||
...pad,
|
||||
paddingLeft: o.labelPad,
|
||||
borderRightWidth: v.frame,
|
||||
borderRightColor: v.ink,
|
||||
...(total ? { backgroundColor: YELLOW } : {}),
|
||||
} as never
|
||||
}
|
||||
>
|
||||
<Txt role={r.totalLabel} color={BLACK} width={t.labelW - v.frame - o.labelPad}>
|
||||
{label}
|
||||
</Txt>
|
||||
</View>
|
||||
<View style={{ ...pinned(t.valueW), ...pad, paddingRight: o.valuePad } as never}>
|
||||
<Txt role={total ? r.totalStrong : r.totalValue} color={v.ink} width={t.valueW - o.valuePad} align="right">
|
||||
{amount}
|
||||
</Txt>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalsBox({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const t = plan.tail;
|
||||
return (
|
||||
<View style={{ ...pinned(t.boxW), borderWidth: v.frame, borderColor: v.ink } as never}>
|
||||
<AvailableWidthProvider width={t.boxW - 2 * v.frame}>
|
||||
{t.stack.map((row, i) => (
|
||||
<TotalRow key={i} ctx={ctx} label={row.label} amount={row.amount} />
|
||||
))}
|
||||
<Slot.TaxRows model={model} role={v.roles.totalLabel} color={v.ink} amountWidth={t.valueW} format={taxLabel} renderRow={(row) => <TotalRow ctx={ctx} label={row.label} amount={row.amount} />} />
|
||||
<TotalRow ctx={ctx} label="total :" amount={model.totals.totalText} total />
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Tail({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const t = plan.tail;
|
||||
const r = v.roles;
|
||||
const o = v.totals;
|
||||
return (
|
||||
<View id={TOTALS_ID} wrap={false} style={pinned(plan.W) as never}>
|
||||
<Spacer h={o.gap} />
|
||||
<Row widths={[{ fill: true }, { fixed: t.boxW }]} width={plan.W}>
|
||||
<View style={{ ...pinned(t.wordsW), marginLeft: v.inset, paddingTop: t.leftH ? o.wordsTop : 0 } as never}>
|
||||
<AvailableWidthProvider width={t.wordsW}>
|
||||
<Slot.AmountInWords model={model} role={r.words} labelRole={r.wordsLabel} color={v.ink} />
|
||||
{t.reverseH > 0 ? <Slot.ReverseCharge model={model} role={r.words} color={v.ink} style={{ marginTop: t.wordsH > 0 ? REVERSE_GAP : 0 }} /> : null}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
<TotalsBox ctx={ctx} />
|
||||
</Row>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// --- closing ----------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Notes in flow mode (too long for the ruled lines): a label held with the first line, every line unbreakable. */
|
||||
function NotesFlow({ ctx, width }: { ctx: Ctx; width: number }) {
|
||||
const { plan, v } = ctx;
|
||||
const r = v.roles;
|
||||
const lines = plan.closing.notes;
|
||||
if (lines.length === 0) return null;
|
||||
return (
|
||||
<View style={pinned(width) as never}>
|
||||
<AvailableWidthProvider width={width}>
|
||||
<View wrap={false}>
|
||||
<Txt role={r.wordsLabel} color={v.ink} width={width} style={{ marginBottom: LABEL_GAP }}>
|
||||
Notes:
|
||||
</Txt>
|
||||
<SafeTxt role={r.note} color={v.ink}>
|
||||
{lines[0]}
|
||||
</SafeTxt>
|
||||
</View>
|
||||
{lines.slice(1).map((line, i) => (
|
||||
<SafeTxt key={i} role={r.note} color={v.ink} wrap={false}>
|
||||
{line}
|
||||
</SafeTxt>
|
||||
))}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SignOff({ ctx }: { ctx: Ctx }) {
|
||||
const { plan, v } = ctx;
|
||||
const r = v.roles;
|
||||
const cl = v.closing;
|
||||
const c = plan.closing;
|
||||
const w = c.leftW - v.inset;
|
||||
return (
|
||||
<View style={{ ...pinned(c.leftW), paddingLeft: v.inset, paddingTop: cl.leftTop } as never}>
|
||||
<AvailableWidthProvider width={w}>
|
||||
<Txt role={r.thanks} color={v.ink} width={w}>
|
||||
THANK YOU
|
||||
</Txt>
|
||||
<View style={{ marginTop: c.contacts.length ? cl.thanksGap : 0 }}>
|
||||
{c.contacts.map((x, i) => (
|
||||
<View key={i} style={{ marginTop: i > 0 ? cl.contactGap : 0 }}>
|
||||
<Row widths={[{ fixed: cl.iconSize }, { fill: true }]} gap={cl.iconGap} width={w}>
|
||||
<View style={{ marginTop: 0.6 }}>
|
||||
<ContactIcon name={x.icon} size={cl.iconSize} color={v.ink} />
|
||||
</View>
|
||||
{x.icon === "location" ? (
|
||||
<SafeTxt role={r.contact} color={BLACK}>
|
||||
{x.text}
|
||||
</SafeTxt>
|
||||
) : (
|
||||
// A phone number or e-mail is an identifier: one fitted line (shrunk, then split at "@ . / -"), never a wrapped one.
|
||||
<IdText role={r.contact} color={BLACK}>
|
||||
{x.text}
|
||||
</IdText>
|
||||
)}
|
||||
</Row>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/** The design's ruled lines: the notes written on them (a line per wrapped line), then the signature on the last one. */
|
||||
function Ruled({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const r = v.roles;
|
||||
const cl = v.closing;
|
||||
const c = plan.closing;
|
||||
const opts = signatureOptions(v);
|
||||
return (
|
||||
<View style={pinned(cl.ruledW) as never}>
|
||||
<AvailableWidthProvider width={cl.ruledW}>
|
||||
{Array.from({ length: c.ruledRows }, (_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={
|
||||
{
|
||||
...pinned(cl.ruledW),
|
||||
minHeight: cl.ruledH,
|
||||
paddingTop: cl.ruledH - v.hair - (r.note.leading as number),
|
||||
borderBottomWidth: v.hair,
|
||||
borderBottomColor: INDIGO,
|
||||
} as never
|
||||
}
|
||||
>
|
||||
{!c.notesFlow && c.ruled[i] !== undefined ? (
|
||||
<FitText role={r.note} color={v.ink} width={cl.ruledW - 2}>
|
||||
{c.ruled[i]}
|
||||
</FitText>
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
<Slot.Signatory model={model} width={cl.ruledW} {...opts} />
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Closing({ ctx }: { ctx: Ctx }) {
|
||||
const { model, plan, v } = ctx;
|
||||
const cl = v.closing;
|
||||
const c = plan.closing;
|
||||
return (
|
||||
<>
|
||||
<BottomSpacer closingH={plan.closingH} />
|
||||
<View id={CLOSING_ID} wrap={false} style={pinned(plan.W) as never}>
|
||||
<AvailableWidthProvider width={plan.W}>
|
||||
<Spacer h={plan.gaps.close} />
|
||||
<Row widths={[{ fixed: c.leftW }, { fixed: cl.ruledW }]} gap={cl.colGap} width={plan.W - v.rightInset}>
|
||||
<SignOff ctx={ctx} />
|
||||
<Ruled ctx={ctx} />
|
||||
</Row>
|
||||
{model.cancelled ? (
|
||||
<Txt role={v.roles.note} color={CANCELLED_INK} width={plan.W} style={{ marginTop: 10, marginLeft: v.inset }}>
|
||||
{CANCELLED_SENTENCE}
|
||||
</Txt>
|
||||
) : null}
|
||||
</AvailableWidthProvider>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// --- the layout -------------------------------------------------------------------------------------------------
|
||||
|
||||
export function HighlighterBody({ model, frame, logo, prefs, variant: v }: TemplateProps & { variant: HighlighterVariant }) {
|
||||
const plan = planHighlighter(model, frame, v, logo, includesName({ model, prefs }));
|
||||
const ctx: Ctx = { model, plan, v, logo };
|
||||
const notesFlow: ReactNode = !plan.closing.notesFlow ? null : (
|
||||
<>
|
||||
<TableGuard {...{ minPresenceAhead: NOTES_FLOW_GAP + NOTES_HOLD }} />
|
||||
<View style={{ marginTop: NOTES_FLOW_GAP, marginLeft: v.inset }}>
|
||||
<NotesFlow ctx={ctx} width={plan.W - v.inset - v.rightInset} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FirstPageHeader pad={frame.pad}>
|
||||
<HeaderRow ctx={ctx} />
|
||||
<Spacer h={v.fields.top} />
|
||||
<Row widths={[{ fixed: plan.fields.leftW }, { fixed: v.pay.w + v.rightInset }]} width={plan.W}>
|
||||
<View style={{ ...pinned(plan.fields.leftW), paddingLeft: v.inset } as never}>
|
||||
<Fields ctx={ctx} />
|
||||
</View>
|
||||
<Payment ctx={ctx} />
|
||||
</Row>
|
||||
</FirstPageHeader>
|
||||
|
||||
<Spacer h={plan.gaps.bar} />
|
||||
|
||||
<TableGuard {...{ minPresenceAhead: plan.guardH }} />
|
||||
<Table ctx={ctx} />
|
||||
{notesFlow}
|
||||
<Closing ctx={ctx} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user