=
{
a4: { css: "A4", width: "210mm" },
letter: { css: "letter", width: "8.5in" },
};
/**
* Base, print-ready CSS shared by all templates. Scoped under
* `.scribe-invoice` so it never leaks into the host app's styles. Includes
* `@page` sizing, page-break protection, and orphan/widow control so the same
* markup renders cleanly on screen and through a headless-browser PDF.
*/
export function baseInvoiceCss(pageSize: InvoicePageSize): string {
const page = PAGE_DIMENSIONS[pageSize];
return `
.scribe-invoice {
--inv-gap: 32px;
box-sizing: border-box;
width: 100%;
max-width: ${page.width};
margin: 0 auto;
padding: 48px;
background: var(--inv-paper);
color: var(--inv-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08),
0 8px 24px rgba(15, 23, 42, 0.06);
}
.scribe-invoice * { box-sizing: border-box; }
.scribe-invoice p { margin: 0; }
.scribe-invoice table { width: 100%; border-collapse: collapse; }
.scribe-invoice .si-break-avoid { break-inside: avoid; }
.scribe-invoice .si-flow { orphans: 3; widows: 3; }
@page { size: ${page.css}; margin: 0.5in; }
@media print {
.scribe-invoice {
max-width: none;
margin: 0;
padding: 0;
border-radius: 0;
box-shadow: none;
}
.scribe-invoice .si-no-print { display: none !important; }
}
`;
}
// ── Page frame ─────────────────────────────────────────────
export interface InvoiceFrameProps {
config: InvoiceTemplateConfig;
pageSize: InvoicePageSize;
/** Template-specific CSS appended after the shared base CSS. */
css: string;
className?: string;
children: ReactNode;
}
/**
* Root wrapper for every template. Establishes the CSS-variable scope, injects
* the print-ready stylesheet, and renders a paper-like surface that looks the
* same on screen as it will in the future PDF output.
*/
export function InvoiceFrame({
config,
pageSize,
css,
className,
children,
}: InvoiceFrameProps) {
return (
{/* Static, build-time template CSS — no user input is interpolated, so a
plain string child is safe (and avoids dangerouslySetInnerHTML). */}
{children}
);
}
// ── Default config ─────────────────────────────────────────
/**
* A neutral default config for surfaces that render a template without a
* stored one (e.g. the web preview). Mirrors the DB "classic" defaults without
* importing the DB layer, keeping this module free of drizzle/sqlite imports.
*/
export const DEFAULT_HTML_TEMPLATE_CONFIG: InvoiceTemplateConfig = {
companyName: "Your Company",
primaryColor: "#0f172a",
accentColor: "#22d3ee",
textColor: "#374151",
headerStyle: "classic",
showLogo: true,
showCompanyDetails: true,
showHours: true,
showRate: true,
showProjectBreakdown: true,
footerText: "Thank you for your business!",
};
/** True when an invoice has anything to render in its footer/notes region. */
export function hasInvoiceFooter(
invoice: InvoiceTemplateData,
config: InvoiceTemplateConfig,
): boolean {
return Boolean(
invoice.notes ||
invoice.paymentNotes ||
config.footerText ||
config.paymentInstructions ||
(config.paymentMethods && config.paymentMethods.length > 0),
);
}