// Minimal HTML invoice template — a clean, professional layout on a white // "paper" surface. This is the single source of layout for the on-screen // preview in both apps and is authored to render identically through a future // HTML -> PDF pipeline. import { sanitizeHexColor } from "../../utils/color"; import { cleanLineDescription, formatInvoiceCurrency, formatInvoiceDate, formatInvoiceDateShort, formatLineQuantity, hasInvoiceFooter, InvoiceFrame, lineQuantityHeader, } from "./shared"; import type { InvoiceTemplateProps } from "./types"; const MINIMAL_CSS = ` .scribe-invoice.t-minimal .si-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; margin-bottom: var(--inv-gap); } .scribe-invoice.t-minimal .si-title { margin: 0 0 4px; font-size: 28px; font-weight: 700; letter-spacing: 0.02em; color: var(--inv-primary); } .scribe-invoice.t-minimal .si-number { color: var(--inv-muted); font-size: 14px; } .scribe-invoice.t-minimal .si-company { text-align: right; font-size: 13px; color: var(--inv-muted); line-height: 1.5; } .scribe-invoice.t-minimal .si-company strong { display: block; margin-bottom: 2px; color: var(--inv-muted-strong); font-size: 14px; } .scribe-invoice.t-minimal .si-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: var(--inv-gap); } .scribe-invoice.t-minimal .si-label { margin: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--inv-muted); } .scribe-invoice.t-minimal .si-client-name { font-weight: 600; color: var(--inv-text); } .scribe-invoice.t-minimal .si-client-line { font-size: 13px; color: var(--inv-muted); white-space: pre-line; } .scribe-invoice.t-minimal .si-dates { display: grid; gap: 6px; font-size: 13px; } .scribe-invoice.t-minimal .si-dates .si-row { display: flex; justify-content: space-between; gap: 16px; } .scribe-invoice.t-minimal .si-dates .si-row span:first-child { color: var(--inv-muted); } .scribe-invoice.t-minimal .si-items { margin-bottom: 24px; border: 1px solid var(--inv-border); border-radius: 10px; overflow: hidden; } .scribe-invoice.t-minimal thead { background: var(--inv-subtle); } .scribe-invoice.t-minimal th { padding: 12px 16px; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--inv-muted); text-align: right; } .scribe-invoice.t-minimal th:first-child, .scribe-invoice.t-minimal td:first-child { text-align: left; } .scribe-invoice.t-minimal td { padding: 12px 16px; font-size: 13px; text-align: right; border-top: 1px solid var(--inv-border); } .scribe-invoice.t-minimal tbody tr:first-child td { border-top: 0; } .scribe-invoice.t-minimal tr { break-inside: avoid; } .scribe-invoice.t-minimal .si-desc { display: flex; align-items: center; gap: 8px; } .scribe-invoice.t-minimal .si-dot { width: 8px; height: 8px; border-radius: 9999px; flex: 0 0 auto; } .scribe-invoice.t-minimal .si-amount { font-weight: 600; } .scribe-invoice.t-minimal .si-totals { display: flex; justify-content: flex-end; break-inside: avoid; } .scribe-invoice.t-minimal .si-totals-inner { width: 260px; display: grid; gap: 8px; } .scribe-invoice.t-minimal .si-totals .si-row { display: flex; justify-content: space-between; font-size: 13px; } .scribe-invoice.t-minimal .si-rule { height: 1px; background: var(--inv-border); margin: 4px 0; } .scribe-invoice.t-minimal .si-grand { font-size: 18px; font-weight: 700; } .scribe-invoice.t-minimal .si-grand span:last-child { color: var(--inv-primary); } .scribe-invoice.t-minimal .si-notes { margin-top: var(--inv-gap); padding-top: 24px; border-top: 1px solid var(--inv-border); display: grid; gap: 16px; } .scribe-invoice.t-minimal .si-notes p { font-size: 13px; white-space: pre-line; } .scribe-invoice.t-minimal .si-pay-methods { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } .scribe-invoice.t-minimal .si-pay-method strong { display: block; margin-bottom: 4px; font-size: 13px; } .scribe-invoice.t-minimal .si-pay-detail { font-size: 12px; color: var(--inv-muted); } .scribe-invoice.t-minimal .si-footer { margin-top: 24px; text-align: center; font-size: 13px; color: var(--inv-accent); } @media print { .scribe-invoice.t-minimal .si-notes { margin-top: 16px; padding-top: 12px; gap: 10px; } .scribe-invoice.t-minimal .si-notes .si-label { margin-bottom: 4px; } .scribe-invoice.t-minimal .si-pay-methods { gap: 10px; } .scribe-invoice.t-minimal .si-footer { margin-top: 12px; break-inside: avoid; } } `; export function MinimalInvoiceTemplate({ invoice, config, pageSize = "letter", className, }: InvoiceTemplateProps) { const showFooter = hasInvoiceFooter(invoice, config); const paymentText = invoice.paymentNotes || config.paymentInstructions; const paymentMethods = config.paymentMethods ?? []; return ( {/* Header: title left, company right */}

INVOICE

{invoice.invoiceNumber}

{config.showCompanyDetails && (
{config.companyName} {config.companyEmail &&
{config.companyEmail}
} {config.companyAddress && (
{config.companyAddress}
)} {config.companyPhone &&
{config.companyPhone}
}
)}
{/* Bill To / dates */}

Bill To

{invoice.clientName ? (

{invoice.clientName}

{invoice.clientEmail && (

{invoice.clientEmail}

)} {invoice.clientAddress && (

{invoice.clientAddress}

)}
) : (

No client selected

)}
Issue Date {formatInvoiceDate(invoice.issueDate)}
{invoice.dueDate && (
Due Date {formatInvoiceDate(invoice.dueDate)}
)} {invoice.periodStart && invoice.periodEnd && (
Period {formatInvoiceDateShort(invoice.periodStart)} -{" "} {formatInvoiceDate(invoice.periodEnd)}
)}
{/* Line items */}
{config.showHours && ( )} {config.showRate && } {invoice.lineItems.map((item, index) => { // Untrusted (projects.color) — strict-hex sanitize before it // reaches the style prop so a poisoned value can't inject CSS into // the PDF render. A non-hex value drops the dot entirely. const dotColor = sanitizeHexColor(item.projectColor); return ( {config.showHours && } {config.showRate && ( )} ); })}
Description{lineQuantityHeader(invoice.lineItems)}RateAmount
{dotColor && ( )} {cleanLineDescription(item.description)}
{formatLineQuantity(item)} {formatInvoiceCurrency(item.rate, invoice.currency)} {formatInvoiceCurrency(item.amount, invoice.currency)}
{/* Totals */}
Subtotal {formatInvoiceCurrency(invoice.subtotal, invoice.currency)}
{invoice.taxRate > 0 && (
Tax ({(invoice.taxRate / 100).toFixed(1)}%) {formatInvoiceCurrency(invoice.taxAmount, invoice.currency)}
)}
Total {formatInvoiceCurrency(invoice.total, invoice.currency)}
{/* Notes / payment */} {showFooter && (
{invoice.notes && (

Notes

{invoice.notes}

)} {paymentText && (

Payment Instructions

{paymentText}

)} {paymentMethods.length > 0 && (

Payment

{paymentMethods.map((method) => (
{method.label} {Object.entries(method.details).map(([key, value]) => (
{key ? `${key}: ${value}` : value}
))}
))}
)} {config.footerText && (

{config.footerText}

)}
)} ); }