From f2b9455f2818e30aa25e31a666265952f1f8f52b Mon Sep 17 00:00:00 2001 From: yeabwang Date: Mon, 13 Jul 2026 11:13:22 +0800 Subject: [PATCH 1/2] feat(demo): add printable invoice template --- packages/demo/src/hooks/useRenderTemplate.ts | 2 +- .../demo/src/sections/TemplateShowcase.tsx | 9 +- .../demo/src/templates/InvoiceReceipt.tsx | 351 ++++++++++++++++++ packages/demo/src/templates/index.ts | 17 +- 4 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 packages/demo/src/templates/InvoiceReceipt.tsx diff --git a/packages/demo/src/hooks/useRenderTemplate.ts b/packages/demo/src/hooks/useRenderTemplate.ts index afffae97..bbca3942 100644 --- a/packages/demo/src/hooks/useRenderTemplate.ts +++ b/packages/demo/src/hooks/useRenderTemplate.ts @@ -23,7 +23,7 @@ export function useRenderTemplate(templateId: string): RenderResult { let plainText = ""; try { - const parts = renderToHtmlParts(element, { mode: "email" }); + const parts = renderToHtmlParts(element, { mode: entry.mode ?? "email" }); head = parts.head; html = parts.body; } catch (e) { diff --git a/packages/demo/src/sections/TemplateShowcase.tsx b/packages/demo/src/sections/TemplateShowcase.tsx index c850b537..e422575d 100644 --- a/packages/demo/src/sections/TemplateShowcase.tsx +++ b/packages/demo/src/sections/TemplateShowcase.tsx @@ -22,6 +22,13 @@ const categoryLabels: Record = { transactional: "Transactional", notification: "Notification", security: "Security", + document: "Document", +}; + +const modeLabels: Record = { + email: "Email", + web: "Web", + document: "Document", }; export default function TemplateShowcase() { @@ -215,7 +222,7 @@ export default function TemplateShowcase() { {selected.description} - Rendering as Email mode + Rendering as {modeLabels[selected.mode ?? "email"]} mode diff --git a/packages/demo/src/templates/InvoiceReceipt.tsx b/packages/demo/src/templates/InvoiceReceipt.tsx new file mode 100644 index 00000000..333ab0d0 --- /dev/null +++ b/packages/demo/src/templates/InvoiceReceipt.tsx @@ -0,0 +1,351 @@ +import type { ReactElement } from "react"; +import type { TableValues } from "@unlayer/react-elements"; +import { + Document, + Row, + Column, + Paragraph, + Heading, + Divider, + Image, + Table, + ColumnLayouts, +} from "@unlayer/react-elements"; + +// Print-ready invoice built with . renderToHtml() returns a complete +// print/PDF-friendly HTML document that converts to PDF with Puppeteer: +// +// import { renderToHtml } from "@unlayer/react-elements"; +// import puppeteer from "puppeteer"; +// +// const html = renderToHtml(InvoiceReceipt(), { title: "Invoice INV-2041" }); +// const browser = await puppeteer.launch(); +// const page = await browser.newPage(); +// await page.setContent(html, { waitUntil: "networkidle0" }); +// await page.pdf({ path: "invoice.pdf", format: "A4", printBackground: true }); +// await browser.close(); +// +// Or with Playwright: chromium.launch() → page.setContent(html) → page.pdf(). + +const sansFont = { + label: "Sans Serif", + value: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif", +}; + +// Line items via the `values` escape hatch: the shorthand headers/data API +// can't right-align individual columns. Typed Partial — the exporter merges +// defaults for anything omitted. +const lineItemsTable: Partial = { + columns: 4, + rows: 3, + enableHeader: true, + headerBackgroundColor: "#f6f8fa", + headerColor: "#687385", + headerFontSize: "11px", + headerFontWeight: 600, + headerPadding: "10px 12px", + headerFontFamily: sansFont, + contentColor: "#1a1f36", + contentFontSize: "13px", + contentPadding: "12px 12px", + contentFontFamily: sansFont, + stripedRows: false, + border: { + borderTopWidth: "0px", + borderTopStyle: "solid", + borderTopColor: "#e3e8ee", + borderLeftWidth: "0px", + borderLeftStyle: "solid", + borderLeftColor: "#e3e8ee", + borderRightWidth: "0px", + borderRightStyle: "solid", + borderRightColor: "#e3e8ee", + borderBottomWidth: "1px", + borderBottomStyle: "solid", + borderBottomColor: "#e3e8ee", + }, + table: { + headers: [ + { + height: 0, + cells: [ + { text: "DESCRIPTION", width: 46, textAlign: "left" }, + { text: "QTY", width: 14, textAlign: "right" }, + { text: "UNIT PRICE", width: 20, textAlign: "right" }, + { text: "AMOUNT", width: 20, textAlign: "right" }, + ], + }, + ], + rows: [ + { + height: 0, + cells: [ + { text: "Design system audit", width: 46, textAlign: "left" }, + { text: "1", width: 14, textAlign: "right" }, + { text: "$2,400.00", width: 20, textAlign: "right" }, + { text: "$2,400.00", width: 20, textAlign: "right" }, + ], + }, + { + height: 0, + cells: [ + { text: "Component library development", width: 46, textAlign: "left" }, + { text: "40", width: 14, textAlign: "right" }, + { text: "$120.00", width: 20, textAlign: "right" }, + { text: "$4,800.00", width: 20, textAlign: "right" }, + ], + }, + { + height: 0, + cells: [ + { text: "Accessibility review", width: 46, textAlign: "left" }, + { text: "1", width: 14, textAlign: "right" }, + { text: "$950.00", width: 20, textAlign: "right" }, + { text: "$950.00", width: 20, textAlign: "right" }, + ], + }, + ], + footers: [], + }, +}; + +export default function InvoiceReceipt(): ReactElement { + return ( + + {/* Header — logo and invoice label */} + + + + + + + + + + + + {/* Full-bleed accent bar */} + + + + + + + {/* Bill-to and invoice details */} + + + + + + + + + + + + + + + + {/* Line items */} + + + + + + + {/* Totals — tinted band */} + + + + + + + + + + + {/* Payment details */} + + + + + + + + + {/* Footer band */} + + + + + + + + ); +} diff --git a/packages/demo/src/templates/index.ts b/packages/demo/src/templates/index.ts index 120c32fc..a987bbf5 100644 --- a/packages/demo/src/templates/index.ts +++ b/packages/demo/src/templates/index.ts @@ -12,6 +12,7 @@ import ShippingUpdate from "./ShippingUpdate"; import ProductLaunch from "./ProductLaunch"; import VerificationCode from "./VerificationCode"; import UsageReport from "./UsageReport"; +import InvoiceReceipt from "./InvoiceReceipt"; // Source code via Vite ?raw imports import welcomeSource from "./WelcomeEmail.tsx?raw"; @@ -25,16 +26,19 @@ import shippingSource from "./ShippingUpdate.tsx?raw"; import productLaunchSource from "./ProductLaunch.tsx?raw"; import verificationSource from "./VerificationCode.tsx?raw"; import usageReportSource from "./UsageReport.tsx?raw"; +import invoiceSource from "./InvoiceReceipt.tsx?raw"; export interface TemplateEntry { id: string; name: string; description: string; - category: "onboarding" | "transactional" | "notification" | "security" | "marketing" | "ecommerce" | "newsletter" | "saas"; + category: "onboarding" | "transactional" | "notification" | "security" | "marketing" | "ecommerce" | "newsletter" | "saas" | "document"; inspiration: string; colorAccent: string; component: () => ReactElement; sourceCode: string; + /** Render mode for previews — default email */ + mode?: "email" | "web" | "document"; } export const templates: TemplateEntry[] = [ @@ -148,6 +152,17 @@ export const templates: TemplateEntry[] = [ component: UsageReport, sourceCode: usageReportSource, }, + { + id: "invoice-receipt", + name: "Invoice / Receipt", + description: "Stripe-inspired print-ready invoice for PDF generation", + category: "document", + inspiration: "Stripe Invoicing", + colorAccent: "#635bff", + component: InvoiceReceipt, + sourceCode: invoiceSource, + mode: "document", + }, ]; export const templateMap = Object.fromEntries( From b689c8ec3e9cc7914b8a12a425217a23a5d46f4c Mon Sep 17 00:00:00 2001 From: yeabwang Date: Tue, 14 Jul 2026 13:57:53 +0800 Subject: [PATCH 2/2] fix: removing page break styles causing trailing page breaks on A4 --- packages/demo/src/templates/InvoiceReceipt.tsx | 2 +- .../react/src/__snapshots__/golden-template.test.tsx.snap | 5 +---- packages/react/src/components/Body.test.tsx | 4 ++-- packages/react/src/components/Body.tsx | 8 +++++++- .../src/components/__snapshots__/snapshots.test.tsx.snap | 6 +----- packages/react/src/components/wrappers.test.tsx | 4 ++-- packages/react/src/utils/render-to-html.test.tsx | 2 ++ 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/demo/src/templates/InvoiceReceipt.tsx b/packages/demo/src/templates/InvoiceReceipt.tsx index 333ab0d0..2d4bfb17 100644 --- a/packages/demo/src/templates/InvoiceReceipt.tsx +++ b/packages/demo/src/templates/InvoiceReceipt.tsx @@ -326,7 +326,7 @@ export default function InvoiceReceipt(): ReactElement { {/* Footer band */} - + snapshot locks the full document HTML - +