@formepdf/hono 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +53 -0
- package/dist/templates/index.d.ts +5 -0
- package/dist/templates/index.js +18 -0
- package/dist/templates/invoice.d.ts +1 -0
- package/dist/templates/invoice.js +14 -0
- package/dist/templates/letter.d.ts +1 -0
- package/dist/templates/letter.js +5 -0
- package/dist/templates/receipt.d.ts +1 -0
- package/dist/templates/receipt.js +9 -0
- package/dist/templates/report.d.ts +1 -0
- package/dist/templates/report.js +179 -0
- package/dist/templates/shipping-label.d.ts +1 -0
- package/dist/templates/shipping-label.js +5 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @formepdf/hono
|
|
2
|
+
|
|
3
|
+
PDF generation for Hono. Middleware or standalone helpers. Runs on Cloudflare Workers, Deno, Bun, and Node.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @formepdf/hono @formepdf/react @formepdf/core hono
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Hono } from 'hono';
|
|
15
|
+
import { formePdf } from '@formepdf/hono';
|
|
16
|
+
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.use(formePdf());
|
|
19
|
+
|
|
20
|
+
app.get('/invoice/:id', async (c) => {
|
|
21
|
+
const invoice = await db.invoices.findById(c.req.param('id'));
|
|
22
|
+
|
|
23
|
+
return c.pdf('invoice', {
|
|
24
|
+
invoiceNumber: invoice.number,
|
|
25
|
+
date: invoice.date,
|
|
26
|
+
dueDate: invoice.dueDate,
|
|
27
|
+
company: { name: 'Acme Corp', initials: 'AC', address: '123 Main St', cityStateZip: 'San Francisco, CA 94105', email: 'billing@acme.com' },
|
|
28
|
+
billTo: invoice.customer,
|
|
29
|
+
shipTo: invoice.shipping,
|
|
30
|
+
items: invoice.lineItems,
|
|
31
|
+
taxRate: 0.08,
|
|
32
|
+
paymentTerms: 'Net 30',
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export default app;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Without Middleware
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { pdfResponse } from '@formepdf/hono';
|
|
43
|
+
|
|
44
|
+
app.get('/invoice/:id', async (c) => {
|
|
45
|
+
const data = await fetchInvoiceData(c.req.param('id'));
|
|
46
|
+
return pdfResponse('invoice', data);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Custom Templates
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { MyReport } from './templates/report';
|
|
54
|
+
|
|
55
|
+
app.get('/report', async (c) => {
|
|
56
|
+
const data = await fetchReportData();
|
|
57
|
+
|
|
58
|
+
return c.pdf(() => MyReport(data), {
|
|
59
|
+
filename: 'quarterly-report.pdf',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Download vs Preview
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Opens in browser PDF viewer (default)
|
|
68
|
+
return c.pdf('invoice', data);
|
|
69
|
+
|
|
70
|
+
// Forces download dialog
|
|
71
|
+
return c.pdf('invoice', data, { download: true });
|
|
72
|
+
|
|
73
|
+
// Custom filename + download
|
|
74
|
+
return c.pdf('invoice', data, { filename: 'invoice-001.pdf', download: true });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Cloudflare Workers
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// src/index.ts
|
|
81
|
+
import { Hono } from 'hono';
|
|
82
|
+
import { formePdf } from '@formepdf/hono';
|
|
83
|
+
|
|
84
|
+
const app = new Hono();
|
|
85
|
+
app.use(formePdf());
|
|
86
|
+
|
|
87
|
+
app.get('/invoice', async (c) => {
|
|
88
|
+
return c.pdf('invoice', {
|
|
89
|
+
invoiceNumber: 'INV-001',
|
|
90
|
+
date: 'February 25, 2026',
|
|
91
|
+
dueDate: 'March 27, 2026',
|
|
92
|
+
company: { name: 'Acme Corp', initials: 'AC', address: '123 Main St', cityStateZip: 'San Francisco, CA 94105', email: 'billing@acme.com' },
|
|
93
|
+
billTo: { name: 'Jane Smith', company: 'Smith Co', address: '456 Oak Ave', cityStateZip: 'Portland, OR 97201', email: 'jane@smith.co' },
|
|
94
|
+
shipTo: { name: 'Jane Smith', address: '456 Oak Ave', cityStateZip: 'Portland, OR 97201' },
|
|
95
|
+
items: [
|
|
96
|
+
{ description: 'Consulting', quantity: 10, unitPrice: 150 },
|
|
97
|
+
],
|
|
98
|
+
taxRate: 0.08,
|
|
99
|
+
paymentTerms: 'Net 30',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export default app;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
# wrangler.toml
|
|
108
|
+
name = "pdf-api"
|
|
109
|
+
compatibility_date = "2024-01-01"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
wrangler deploy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
PDF API running at the edge. No servers, no Puppeteer, no headless Chrome.
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
### `formePdf(options?)`
|
|
121
|
+
|
|
122
|
+
Returns Hono middleware that adds `c.pdf()` to the context.
|
|
123
|
+
|
|
124
|
+
| Option | Type | Default | Description |
|
|
125
|
+
|--------|------|---------|-------------|
|
|
126
|
+
| `defaultDownload` | `boolean` | `false` | Default disposition for all responses |
|
|
127
|
+
|
|
128
|
+
### `c.pdf(template, data, options?)`
|
|
129
|
+
|
|
130
|
+
Render a built-in template and return as Response.
|
|
131
|
+
|
|
132
|
+
### `c.pdf(renderFn, options?)`
|
|
133
|
+
|
|
134
|
+
Render a custom template and return as Response.
|
|
135
|
+
|
|
136
|
+
### `pdfResponse(template, data, options?)`
|
|
137
|
+
|
|
138
|
+
Standalone function. Returns a Response with the PDF. No middleware required.
|
|
139
|
+
|
|
140
|
+
### Options
|
|
141
|
+
|
|
142
|
+
| Option | Type | Default | Description |
|
|
143
|
+
|--------|------|---------|-------------|
|
|
144
|
+
| `filename` | `string` | `{template}.pdf` | Filename in Content-Disposition |
|
|
145
|
+
| `download` | `boolean` | `false` | Force download vs browser preview |
|
|
146
|
+
|
|
147
|
+
## Built-in Templates
|
|
148
|
+
|
|
149
|
+
| Template | Description |
|
|
150
|
+
|----------|-------------|
|
|
151
|
+
| `invoice` | Line items, tax, totals, company/customer info |
|
|
152
|
+
| `receipt` | Payment confirmation, items, total, payment method |
|
|
153
|
+
| `report` | Multi-section document with title, headings, body text |
|
|
154
|
+
| `letter` | Business letter with letterhead, date, recipient, body |
|
|
155
|
+
| `shipping-label` | From/to addresses, 4x6 label format |
|
|
156
|
+
|
|
157
|
+
## Why Hono + Forme
|
|
158
|
+
|
|
159
|
+
Forme's engine is Rust compiled to WASM. It runs anywhere JavaScript runs, including edge runtimes where Puppeteer can't. If you're building an API on Cloudflare Workers and need PDF generation, Forme is one of the only options.
|
|
160
|
+
|
|
161
|
+
## Links
|
|
162
|
+
|
|
163
|
+
- [Forme](https://github.com/formepdf/forme) -- PDF generation with JSX
|
|
164
|
+
- [Docs](https://docs.formepdf.com)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listTemplates } from './templates/index.js';
|
|
2
|
+
import type { MiddlewareHandler } from 'hono';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
interface PdfOptions {
|
|
5
|
+
filename?: string;
|
|
6
|
+
download?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface FormePdfOptions {
|
|
9
|
+
defaultDownload?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function pdfResponse(templateOrRenderFn: string | (() => ReactElement), dataOrOptions?: Record<string, any> | PdfOptions, maybeOptions?: PdfOptions): Promise<Response>;
|
|
12
|
+
declare module 'hono' {
|
|
13
|
+
interface Context {
|
|
14
|
+
pdf: (templateOrRenderFn: string | (() => ReactElement), dataOrOptions?: Record<string, any> | PdfOptions, maybeOptions?: PdfOptions) => Promise<Response>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export declare function formePdf(opts?: FormePdfOptions): MiddlewareHandler;
|
|
18
|
+
export { listTemplates };
|
|
19
|
+
export type { PdfOptions, FormePdfOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { renderDocument } from '@formepdf/core';
|
|
2
|
+
import { getTemplate, listTemplates } from './templates/index.js';
|
|
3
|
+
// --- Standalone pdfResponse (no middleware needed) ---
|
|
4
|
+
export async function pdfResponse(templateOrRenderFn, dataOrOptions, maybeOptions) {
|
|
5
|
+
let element;
|
|
6
|
+
let options;
|
|
7
|
+
if (typeof templateOrRenderFn === 'string') {
|
|
8
|
+
const template = templateOrRenderFn;
|
|
9
|
+
const data = dataOrOptions || {};
|
|
10
|
+
options = maybeOptions || {};
|
|
11
|
+
const templateFn = getTemplate(template);
|
|
12
|
+
if (!templateFn) {
|
|
13
|
+
return Response.json({ error: `Unknown template: "${template}"` }, { status: 400 });
|
|
14
|
+
}
|
|
15
|
+
element = templateFn(data);
|
|
16
|
+
options.filename = options.filename || `${template}.pdf`;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const renderFn = templateOrRenderFn;
|
|
20
|
+
options = dataOrOptions || {};
|
|
21
|
+
element = renderFn();
|
|
22
|
+
options.filename = options.filename || 'document.pdf';
|
|
23
|
+
}
|
|
24
|
+
const pdfBytes = await renderDocument(element);
|
|
25
|
+
const disposition = (options.download ?? false) ? 'attachment' : 'inline';
|
|
26
|
+
return new Response(pdfBytes, {
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/pdf',
|
|
29
|
+
'Content-Disposition': `${disposition}; filename="${options.filename}"`,
|
|
30
|
+
'Content-Length': String(pdfBytes.byteLength),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function formePdf(opts) {
|
|
35
|
+
const defaultDownload = opts?.defaultDownload ?? false;
|
|
36
|
+
return async (c, next) => {
|
|
37
|
+
c.pdf = async function (templateOrRenderFn, dataOrOptions, maybeOptions) {
|
|
38
|
+
let options;
|
|
39
|
+
if (typeof templateOrRenderFn === 'string') {
|
|
40
|
+
options = maybeOptions || {};
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
options = dataOrOptions || {};
|
|
44
|
+
}
|
|
45
|
+
if (options.download === undefined) {
|
|
46
|
+
options.download = defaultDownload;
|
|
47
|
+
}
|
|
48
|
+
return pdfResponse(templateOrRenderFn, dataOrOptions, typeof templateOrRenderFn === 'string' ? options : undefined);
|
|
49
|
+
};
|
|
50
|
+
await next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export { listTemplates };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Invoice from './invoice.js';
|
|
2
|
+
import Receipt from './receipt.js';
|
|
3
|
+
import Report from './report.js';
|
|
4
|
+
import ShippingLabel from './shipping-label.js';
|
|
5
|
+
import Letter from './letter.js';
|
|
6
|
+
const templates = {
|
|
7
|
+
invoice: Invoice,
|
|
8
|
+
receipt: Receipt,
|
|
9
|
+
report: Report,
|
|
10
|
+
'shipping-label': ShippingLabel,
|
|
11
|
+
letter: Letter,
|
|
12
|
+
};
|
|
13
|
+
export function getTemplate(name) {
|
|
14
|
+
return templates[name] || null;
|
|
15
|
+
}
|
|
16
|
+
export function listTemplates() {
|
|
17
|
+
return Object.keys(templates).map(name => ({ name }));
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Invoice(data: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text, Table, Row, Cell, Fixed } from '@formepdf/react';
|
|
3
|
+
export default function Invoice(data) {
|
|
4
|
+
const items = data.items || [];
|
|
5
|
+
const subtotal = items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
|
|
6
|
+
const tax = subtotal * (data.taxRate || 0);
|
|
7
|
+
const total = subtotal + tax;
|
|
8
|
+
return (_jsx(Document, { title: `Invoice ${data.invoiceNumber}`, author: data.company.name, children: _jsxs(Page, { size: "Letter", margin: 48, children: [_jsx(Fixed, { position: "footer", children: _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 8, borderTopWidth: 1, borderColor: '#e2e8f0' }, children: [_jsx(Text, { style: { fontSize: 8, color: '#94a3b8' }, children: data.company.name }), _jsxs(Text, { style: { fontSize: 8, color: '#94a3b8' }, children: ["Page ", '{{pageNumber}}', " of ", '{{totalPages}}'] })] }) }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 32 }, children: [_jsxs(View, { children: [_jsx(View, { style: { width: 48, height: 48, backgroundColor: '#2563eb', borderRadius: 8, marginBottom: 12, justifyContent: 'center', alignItems: 'center' }, children: _jsx(Text, { style: { fontSize: 18, fontWeight: 700, color: '#ffffff', textAlign: 'center', lineHeight: 1.2 }, children: data.company.initials }) }), _jsx(Text, { style: { fontSize: 16, fontWeight: 700, color: '#1e293b' }, children: data.company.name }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 4 }, children: data.company.address }), _jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.company.cityStateZip }), _jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.company.email })] }), _jsxs(View, { style: { alignItems: 'flex-end' }, children: [_jsx(Text, { style: { fontSize: 32, fontWeight: 700, color: '#2563eb' }, children: "INVOICE" }), _jsxs(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 8 }, children: ["Invoice No: ", data.invoiceNumber] }), _jsxs(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: ["Date: ", data.date] }), _jsxs(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: ["Due: ", data.dueDate] })] })] }), _jsxs(View, { style: { flexDirection: 'row', gap: 32, marginBottom: 24 }, children: [_jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#2563eb', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 }, children: "Bill To" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#1e293b' }, children: data.billTo.name }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.billTo.company }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.billTo.address }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.billTo.cityStateZip }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.billTo.email })] }), _jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#2563eb', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 }, children: "Ship To" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#1e293b' }, children: data.shipTo.name }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.shipTo.address }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.shipTo.cityStateZip })] })] }), _jsxs(Table, { columns: [
|
|
9
|
+
{ width: { fraction: 0.45 } },
|
|
10
|
+
{ width: { fraction: 0.15 } },
|
|
11
|
+
{ width: { fraction: 0.2 } },
|
|
12
|
+
{ width: { fraction: 0.2 } }
|
|
13
|
+
], children: [_jsxs(Row, { header: true, style: { backgroundColor: '#2563eb' }, children: [_jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff' }, children: "Description" }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff', textAlign: 'center' }, children: "Qty" }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff', textAlign: 'right' }, children: "Unit Price" }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff', textAlign: 'right' }, children: "Amount" }) })] }), items.map((item, i) => (_jsxs(Row, { style: { backgroundColor: i % 2 === 0 ? '#ffffff' : '#f8fafc' }, children: [_jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, color: '#1e293b' }, children: item.description }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsx(Text, { style: { fontSize: 9, color: '#475569', textAlign: 'center' }, children: item.quantity }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsxs(Text, { style: { fontSize: 9, color: '#475569', textAlign: 'right' }, children: ["$", item.unitPrice.toFixed(2)] }) }), _jsx(Cell, { style: { padding: 10 }, children: _jsxs(Text, { style: { fontSize: 9, color: '#1e293b', textAlign: 'right' }, children: ["$", (item.quantity * item.unitPrice).toFixed(2)] }) })] }, i)))] }), _jsx(View, { style: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 16 }, children: _jsxs(View, { style: { width: 200 }, children: [_jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', padding: 8 }, children: [_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: "Subtotal" }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["$", subtotal.toFixed(2)] })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', padding: 8 }, children: [_jsxs(Text, { style: { fontSize: 9, color: '#64748b' }, children: ["Tax (", (data.taxRate * 100).toFixed(0), "%)"] }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["$", tax.toFixed(2)] })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', padding: 12, backgroundColor: '#2563eb', borderRadius: 4, marginTop: 4 }, children: [_jsx(Text, { style: { fontSize: 11, fontWeight: 700, color: '#ffffff' }, children: "Total Due" }), _jsxs(Text, { style: { fontSize: 11, fontWeight: 700, color: '#ffffff' }, children: ["$", total.toFixed(2)] })] })] }) }), _jsxs(View, { style: { marginTop: 32, padding: 16, backgroundColor: '#f8fafc', borderRadius: 4 }, children: [_jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#1e293b', marginBottom: 8 }, children: "Payment Terms" }), _jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.paymentTerms })] }), data.notes && (_jsxs(View, { style: { marginTop: 16 }, children: [_jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#1e293b', marginBottom: 4 }, children: "Notes" }), _jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.notes })] }))] }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Letter(data: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text } from '@formepdf/react';
|
|
3
|
+
export default function Letter(data) {
|
|
4
|
+
return (_jsx(Document, { title: `Letter to ${data.recipient.name}`, author: data.sender.name, children: _jsxs(Page, { size: "Letter", margin: { top: 72, right: 72, bottom: 72, left: 72 }, children: [_jsxs(View, { style: { marginBottom: 32 }, children: [_jsx(Text, { style: { fontSize: 16, fontWeight: 700, color: '#1e293b' }, children: data.sender.company }), _jsx(View, { style: { borderTopWidth: 2, borderColor: '#2563eb', marginTop: 8, marginBottom: 12 } }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between' }, children: [_jsxs(View, { children: [_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.sender.address }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.sender.cityStateZip })] }), _jsxs(View, { style: { alignItems: 'flex-end' }, children: [data.sender.phone && (_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.sender.phone })), data.sender.email && (_jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.sender.email }))] })] })] }), _jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginBottom: 24 }, children: data.date }), _jsxs(View, { style: { marginBottom: 24 }, children: [_jsx(Text, { style: { fontSize: 10, color: '#1e293b' }, children: data.recipient.name }), data.recipient.title && (_jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginTop: 2 }, children: data.recipient.title })), data.recipient.company && (_jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginTop: 2 }, children: data.recipient.company })), _jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginTop: 2 }, children: data.recipient.address }), _jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginTop: 2 }, children: data.recipient.cityStateZip })] }), _jsx(Text, { style: { fontSize: 10, color: '#1e293b', marginBottom: 16 }, children: data.salutation }), data.body.map((paragraph, i) => (_jsx(Text, { style: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 }, children: paragraph }, i))), _jsxs(View, { style: { marginTop: 16 }, children: [_jsx(Text, { style: { fontSize: 10, color: '#1e293b' }, children: data.closing }), _jsx(View, { style: { height: 48 } }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#1e293b' }, children: data.signatureName }), data.signatureTitle && (_jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.signatureTitle }))] })] }) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Receipt(data: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text } from '@formepdf/react';
|
|
3
|
+
export default function Receipt(data) {
|
|
4
|
+
const items = data.items || [];
|
|
5
|
+
const subtotal = items.reduce((sum, item) => sum + item.price * (item.quantity || 1), 0);
|
|
6
|
+
const tax = subtotal * (data.taxRate || 0);
|
|
7
|
+
const total = subtotal + tax;
|
|
8
|
+
return (_jsx(Document, { title: `Receipt ${data.receiptNumber}`, author: data.store.name, children: _jsxs(Page, { size: "Letter", margin: { top: 72, right: 120, bottom: 72, left: 120 }, children: [_jsxs(View, { style: { alignItems: 'center', marginBottom: 24 }, children: [_jsx(Text, { style: { fontSize: 20, fontWeight: 700, color: '#1e293b' }, children: data.store.name }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 4 }, children: data.store.address }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.store.cityStateZip }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 2 }, children: data.store.phone })] }), _jsx(View, { style: { borderTopWidth: 1, borderColor: '#e2e8f0', marginBottom: 16 } }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16 }, children: [_jsxs(Text, { style: { fontSize: 9, color: '#64748b' }, children: ["Receipt #", data.receiptNumber] }), _jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: data.date })] }), items.map((item, i) => (_jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 6, paddingBottom: 6 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 8, flexGrow: 1 }, children: [_jsx(Text, { style: { fontSize: 9, color: '#1e293b' }, children: item.name }), (item.quantity || 1) > 1 && (_jsxs(Text, { style: { fontSize: 9, color: '#94a3b8' }, children: ["x", item.quantity] }))] }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["$", (item.price * (item.quantity || 1)).toFixed(2)] })] }, i))), _jsx(View, { style: { borderTopWidth: 1, borderColor: '#e2e8f0', marginTop: 12, marginBottom: 12 } }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 4, paddingBottom: 4 }, children: [_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: "Subtotal" }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["$", subtotal.toFixed(2)] })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 4, paddingBottom: 4 }, children: [_jsxs(Text, { style: { fontSize: 9, color: '#64748b' }, children: ["Tax (", (data.taxRate * 100).toFixed(1), "%)"] }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["$", tax.toFixed(2)] })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 12, paddingBottom: 12, borderTopWidth: 2, borderColor: '#1e293b', marginTop: 4 }, children: [_jsx(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b' }, children: "Total" }), _jsxs(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b' }, children: ["$", total.toFixed(2)] })] }), _jsxs(View, { style: { marginTop: 16, paddingTop: 12, paddingBottom: 12, borderTopWidth: 1, borderColor: '#e2e8f0' }, children: [_jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between' }, children: [_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: "Payment Method" }), _jsx(Text, { style: { fontSize: 9, color: '#1e293b' }, children: data.paymentMethod })] }), data.cardLastFour && (_jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 }, children: [_jsx(Text, { style: { fontSize: 9, color: '#64748b' }, children: "Card" }), _jsxs(Text, { style: { fontSize: 9, color: '#1e293b' }, children: ["****", data.cardLastFour] })] }))] }), _jsxs(View, { style: { alignItems: 'center', marginTop: 32 }, children: [_jsx(Text, { style: { fontSize: 10, color: '#64748b' }, children: "Thank you for your purchase!" }), _jsx(Text, { style: { fontSize: 8, color: '#94a3b8', marginTop: 8 }, children: data.store.website })] })] }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Report(data: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text, Svg, Table, Row, Cell, Fixed, PageBreak, StyleSheet } from '@formepdf/react';
|
|
3
|
+
const styles = StyleSheet.create({
|
|
4
|
+
sectionTitle: { fontSize: 20, fontWeight: 700, color: '#0f172a', marginBottom: 12 },
|
|
5
|
+
bodyText: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 },
|
|
6
|
+
introText: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 16 },
|
|
7
|
+
headerFooterText: { fontSize: 8, color: '#94a3b8' },
|
|
8
|
+
headerBar: { flexDirection: 'row', justifyContent: 'space-between', paddingBottom: 8, borderBottomWidth: 1, borderColor: '#e2e8f0', marginBottom: 16 },
|
|
9
|
+
footerBar: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 8, borderTopWidth: 1, borderColor: '#e2e8f0' },
|
|
10
|
+
tableHeaderCell: { padding: 8 },
|
|
11
|
+
tableHeaderText: { fontSize: 9, fontWeight: 700, color: '#ffffff', textAlign: 'right' },
|
|
12
|
+
tableCell: { padding: 8 },
|
|
13
|
+
tableCellText: { fontSize: 9, color: '#334155', textAlign: 'right' },
|
|
14
|
+
chartTitle: { fontSize: 10, fontWeight: 700, color: '#334155', marginBottom: 6 },
|
|
15
|
+
recCard: { flexDirection: 'row', gap: 24, marginBottom: 16, padding: 16, backgroundColor: '#f8fafc', borderRadius: 4, borderLeftWidth: 3, borderColor: '#0f172a' },
|
|
16
|
+
recBadge: { width: 24, height: 24, backgroundColor: '#0f172a', borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
|
17
|
+
recBadgeText: { fontSize: 10, fontWeight: 700, color: '#ffffff', lineHeight: 1.2 },
|
|
18
|
+
recTitle: { fontSize: 11, fontWeight: 700, color: '#0f172a', marginBottom: 4 },
|
|
19
|
+
recBody: { fontSize: 9, color: '#475569', lineHeight: 1.5 },
|
|
20
|
+
recLabel: { fontSize: 8, fontWeight: 700, color: '#64748b' },
|
|
21
|
+
recValue: { fontSize: 8, color: '#334155' },
|
|
22
|
+
});
|
|
23
|
+
// ── Chart SVG generators ─────────────────────────────────────────────
|
|
24
|
+
const CHART_COLORS = ['#3b82f6', '#0f172a', '#64748b', '#94a3b8', '#cbd5e1'];
|
|
25
|
+
function renderBarChart(tableData) {
|
|
26
|
+
const totals = tableData.map((r) => ({
|
|
27
|
+
label: r.region,
|
|
28
|
+
value: parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
29
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')),
|
|
30
|
+
}));
|
|
31
|
+
const maxVal = Math.max(...totals.map(t => t.value));
|
|
32
|
+
const w = 230, h = 150;
|
|
33
|
+
const barAreaTop = 8, barAreaBottom = h - 20;
|
|
34
|
+
const barAreaH = barAreaBottom - barAreaTop;
|
|
35
|
+
const barW = Math.min(32, (w - 20) / totals.length - 8);
|
|
36
|
+
const gap = (w - barW * totals.length) / (totals.length + 1);
|
|
37
|
+
let svg = '';
|
|
38
|
+
// Grid lines
|
|
39
|
+
for (let i = 0; i <= 4; i++) {
|
|
40
|
+
const y = barAreaTop + (barAreaH / 4) * i;
|
|
41
|
+
svg += `<line x1="0" y1="${y}" x2="${w}" y2="${y}" stroke="#e2e8f0" stroke-width="0.5"/>`;
|
|
42
|
+
}
|
|
43
|
+
// Bars and labels
|
|
44
|
+
totals.forEach((t, i) => {
|
|
45
|
+
const x = gap + i * (barW + gap);
|
|
46
|
+
const barH = (t.value / maxVal) * barAreaH;
|
|
47
|
+
const y = barAreaBottom - barH;
|
|
48
|
+
svg += `<rect x="${x}" y="${y}" width="${barW}" height="${barH}" fill="${CHART_COLORS[i % CHART_COLORS.length]}" rx="2"/>`;
|
|
49
|
+
// Region label below bar
|
|
50
|
+
const labelX = x + barW / 2;
|
|
51
|
+
svg += `<rect x="${labelX - 2}" y="${barAreaBottom + 4}" width="4" height="4" fill="${CHART_COLORS[i % CHART_COLORS.length]}" rx="1"/>`;
|
|
52
|
+
});
|
|
53
|
+
return svg;
|
|
54
|
+
}
|
|
55
|
+
/** Approximate a circular arc as cubic bezier segments. */
|
|
56
|
+
function arcPath(cx, cy, r, startAngle, endAngle) {
|
|
57
|
+
let path = '';
|
|
58
|
+
const step = Math.PI / 2; // max 90 degrees per segment
|
|
59
|
+
let a1 = startAngle;
|
|
60
|
+
while (a1 < endAngle - 0.001) {
|
|
61
|
+
const a2 = Math.min(a1 + step, endAngle);
|
|
62
|
+
const alpha = a2 - a1;
|
|
63
|
+
const k = (4 / 3) * Math.tan(alpha / 4);
|
|
64
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
|
|
65
|
+
const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
|
|
66
|
+
const cp1x = x1 - k * r * Math.sin(a1), cp1y = y1 + k * r * Math.cos(a1);
|
|
67
|
+
const cp2x = x2 + k * r * Math.sin(a2), cp2y = y2 - k * r * Math.cos(a2);
|
|
68
|
+
if (a1 === startAngle) {
|
|
69
|
+
path += `M ${f(x1)} ${f(y1)} `;
|
|
70
|
+
}
|
|
71
|
+
path += `C ${f(cp1x)} ${f(cp1y)} ${f(cp2x)} ${f(cp2y)} ${f(x2)} ${f(y2)} `;
|
|
72
|
+
a1 = a2;
|
|
73
|
+
}
|
|
74
|
+
return path;
|
|
75
|
+
}
|
|
76
|
+
function f(n) { return n.toFixed(2); }
|
|
77
|
+
function renderDonutChart(tableData) {
|
|
78
|
+
const totals = tableData.map((r) => ({
|
|
79
|
+
label: r.region,
|
|
80
|
+
value: parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
81
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')),
|
|
82
|
+
}));
|
|
83
|
+
const sum = totals.reduce((s, t) => s + t.value, 0);
|
|
84
|
+
const cx = 55, cy = 75, r = 48, innerR = 28;
|
|
85
|
+
let svg = '';
|
|
86
|
+
let angle = -Math.PI / 2; // start at top
|
|
87
|
+
totals.forEach((t, i) => {
|
|
88
|
+
const sliceAngle = (t.value / sum) * Math.PI * 2;
|
|
89
|
+
if (sliceAngle < 0.01) {
|
|
90
|
+
angle += sliceAngle;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const endAngle = angle + sliceAngle;
|
|
94
|
+
const ix2 = cx + innerR * Math.cos(endAngle), iy2 = cy + innerR * Math.sin(endAngle);
|
|
95
|
+
const innerPath = arcPathReverse(cx, cy, innerR, angle, endAngle);
|
|
96
|
+
const fullD = arcPath(cx, cy, r, angle, endAngle) + `L ${f(ix2)} ${f(iy2)} ` + innerPath + 'Z';
|
|
97
|
+
svg += `<path d="${fullD}" fill="${CHART_COLORS[i % CHART_COLORS.length]}"/>`;
|
|
98
|
+
angle = endAngle;
|
|
99
|
+
});
|
|
100
|
+
return svg;
|
|
101
|
+
}
|
|
102
|
+
/** Reverse arc: draws from endAngle back to startAngle using line-to after the first point. */
|
|
103
|
+
function arcPathReverse(cx, cy, r, startAngle, endAngle) {
|
|
104
|
+
const step = Math.PI / 2;
|
|
105
|
+
const segments = [];
|
|
106
|
+
let a = startAngle;
|
|
107
|
+
while (a < endAngle - 0.001) {
|
|
108
|
+
const a2 = Math.min(a + step, endAngle);
|
|
109
|
+
segments.push({ a1: a, a2 });
|
|
110
|
+
a = a2;
|
|
111
|
+
}
|
|
112
|
+
// Reverse and draw each segment backwards
|
|
113
|
+
let path = '';
|
|
114
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
115
|
+
const { a1, a2 } = segments[i];
|
|
116
|
+
const alpha = a2 - a1;
|
|
117
|
+
const k = (4 / 3) * Math.tan(alpha / 4);
|
|
118
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
|
|
119
|
+
const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
|
|
120
|
+
const cp1x = x1 - k * r * Math.sin(a1), cp1y = y1 + k * r * Math.cos(a1);
|
|
121
|
+
const cp2x = x2 + k * r * Math.sin(a2), cp2y = y2 - k * r * Math.cos(a2);
|
|
122
|
+
// Draw from (x2,y2) to (x1,y1) with swapped control points
|
|
123
|
+
path += `C ${f(cp2x)} ${f(cp2y)} ${f(cp1x)} ${f(cp1y)} ${f(x1)} ${f(y1)} `;
|
|
124
|
+
}
|
|
125
|
+
return path;
|
|
126
|
+
}
|
|
127
|
+
function renderLineChart(tableData) {
|
|
128
|
+
const quarters = ['q1', 'q2', 'q3', 'q4'];
|
|
129
|
+
const qTotals = quarters.map(q => tableData.reduce((sum, r) => sum + parseFloat(r[q].replace(/[$,]/g, '')), 0));
|
|
130
|
+
const minVal = Math.min(...qTotals) * 0.9;
|
|
131
|
+
const maxVal = Math.max(...qTotals) * 1.05;
|
|
132
|
+
const w = 484, h = 140;
|
|
133
|
+
const padL = 16, padR = 16, padT = 12, padB = 24;
|
|
134
|
+
const plotW = w - padL - padR, plotH = h - padT - padB;
|
|
135
|
+
let svg = '';
|
|
136
|
+
// Grid lines
|
|
137
|
+
for (let i = 0; i <= 4; i++) {
|
|
138
|
+
const y = padT + (plotH / 4) * i;
|
|
139
|
+
svg += `<line x1="${padL}" y1="${y}" x2="${w - padR}" y2="${y}" stroke="#e2e8f0" stroke-width="0.5"/>`;
|
|
140
|
+
}
|
|
141
|
+
// Plot points
|
|
142
|
+
const points = qTotals.map((v, i) => ({
|
|
143
|
+
x: padL + (plotW / (quarters.length - 1)) * i,
|
|
144
|
+
y: padT + plotH - ((v - minVal) / (maxVal - minVal)) * plotH,
|
|
145
|
+
}));
|
|
146
|
+
// Fill area under the line
|
|
147
|
+
const areaPath = `M ${f(points[0].x)} ${f(padT + plotH)} L ${points.map(p => `${f(p.x)} ${f(p.y)}`).join(' L ')} L ${f(points[points.length - 1].x)} ${f(padT + plotH)} Z`;
|
|
148
|
+
svg += `<path d="${areaPath}" fill="#3b82f6" fill-opacity="0.08"/>`;
|
|
149
|
+
// Line
|
|
150
|
+
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${f(p.x)} ${f(p.y)}`).join(' ');
|
|
151
|
+
svg += `<path d="${linePath}" fill="none" stroke="#3b82f6" stroke-width="2"/>`;
|
|
152
|
+
// Dots
|
|
153
|
+
points.forEach(p => {
|
|
154
|
+
svg += `<circle cx="${f(p.x)}" cy="${f(p.y)}" r="3.5" fill="#ffffff" stroke="#3b82f6" stroke-width="2"/>`;
|
|
155
|
+
});
|
|
156
|
+
// Quarter markers on x-axis
|
|
157
|
+
points.forEach((p) => {
|
|
158
|
+
svg += `<line x1="${f(p.x)}" y1="${f(padT + plotH)}" x2="${f(p.x)}" y2="${f(padT + plotH + 4)}" stroke="#94a3b8" stroke-width="1"/>`;
|
|
159
|
+
});
|
|
160
|
+
return svg;
|
|
161
|
+
}
|
|
162
|
+
// ── Template ─────────────────────────────────────────────────────────
|
|
163
|
+
export default function Report(data) {
|
|
164
|
+
const tableData = data.sections[1].tableData || [];
|
|
165
|
+
return (_jsxs(Document, { title: data.title, author: data.author, children: [_jsx(Page, { size: "Letter", margin: 72, children: _jsxs(View, { style: { flexGrow: 1, justifyContent: 'center' }, children: [_jsxs(View, { style: { backgroundColor: '#0f172a', padding: 32, borderRadius: 4, marginBottom: 32 }, children: [_jsx(Text, { style: { fontSize: 32, fontWeight: 700, color: '#ffffff' }, children: data.title }), _jsx(Text, { style: { fontSize: 14, color: '#94a3b8', marginTop: 12 }, children: data.subtitle })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }, children: [_jsxs(View, { children: [_jsx(Text, { style: { fontSize: 10, color: '#64748b' }, children: "Prepared by" }), _jsx(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b', marginTop: 4 }, children: data.author }), _jsx(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: data.department })] }), _jsxs(View, { style: { alignItems: 'flex-end' }, children: [_jsx(Text, { style: { fontSize: 10, color: '#64748b' }, children: "Date" }), _jsx(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b', marginTop: 4 }, children: data.date }), _jsx(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: data.classification })] })] })] }) }), _jsxs(Page, { size: "Letter", margin: 54, children: [_jsx(Fixed, { position: "header", children: _jsxs(View, { style: styles.headerBar, children: [_jsx(Text, { style: styles.headerFooterText, children: data.company }), _jsx(Text, { style: styles.headerFooterText, children: data.title })] }) }), _jsx(Fixed, { position: "footer", children: _jsxs(View, { style: styles.footerBar, children: [_jsx(Text, { style: styles.headerFooterText, children: data.classification }), _jsxs(Text, { style: styles.headerFooterText, children: ["Page ", '{{pageNumber}}', " of ", '{{totalPages}}'] })] }) }), _jsx(Text, { style: styles.sectionTitle, children: "Table of Contents" }), data.sections.map((section, i) => (_jsx(View, { href: `#${section.title}`, style: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 6, borderBottomWidth: 1, borderColor: '#f1f5f9' }, children: _jsxs(Text, { style: { fontSize: 10, color: '#2563eb', textDecoration: 'underline' }, children: [i + 1, ". ", section.title] }) }, i))), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[0].title, style: styles.sectionTitle, children: ["1. ", data.sections[0].title] }), (data.sections[0].paragraphs || []).map((p, i) => (_jsx(Text, { style: styles.bodyText, children: p }, i))), data.keyMetrics && (_jsx(View, { style: { flexDirection: 'row', gap: 12, marginTop: 8, marginBottom: 24 }, children: data.keyMetrics.map((metric, i) => (_jsxs(View, { style: { flexGrow: 1, padding: 16, backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0' }, children: [_jsx(Text, { style: { fontSize: 20, fontWeight: 700, color: '#0f172a' }, children: metric.value }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 4 }, children: metric.label })] }, i))) })), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[1].title, style: styles.sectionTitle, children: ["2. ", data.sections[1].title] }), _jsx(Text, { style: styles.introText, children: data.sections[1].intro }), _jsxs(Table, { columns: [
|
|
166
|
+
{ width: { fraction: 0.28 } },
|
|
167
|
+
{ width: { fraction: 0.18 } },
|
|
168
|
+
{ width: { fraction: 0.18 } },
|
|
169
|
+
{ width: { fraction: 0.18 } },
|
|
170
|
+
{ width: { fraction: 0.18 } }
|
|
171
|
+
], children: [_jsxs(Row, { header: true, style: { backgroundColor: '#0f172a' }, children: [_jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff' }, children: "Region" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q1" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q2" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q3" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q4" }) })] }), tableData.map((row, i) => (_jsxs(Row, { style: { backgroundColor: i % 2 === 0 ? '#ffffff' : '#f8fafc' }, children: [_jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: { fontSize: 9, color: '#334155', fontWeight: 700 }, children: row.region }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q1 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q2 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q3 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q4 }) })] }, i)))] }), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[2].title, style: styles.sectionTitle, children: ["3. ", data.sections[2].title] }), _jsx(Text, { style: styles.introText, children: data.sections[2].intro }), _jsxs(View, { style: { flexDirection: 'row', gap: 16, marginBottom: 24 }, children: [_jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: styles.chartTitle, children: "Revenue by Region" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', padding: 8 }, children: [_jsx(Svg, { width: 230, height: 150, viewBox: "0 0 230 150", content: renderBarChart(tableData) }), _jsx(View, { style: { gap: 3, marginTop: 8 }, children: tableData.map((row, i) => (_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 4 }, children: [_jsx(View, { style: { width: 8, height: 8, borderRadius: 2, backgroundColor: CHART_COLORS[i % CHART_COLORS.length] } }), _jsx(Text, { style: { fontSize: 7, color: '#64748b' }, children: row.region })] }, i))) })] })] }), _jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: styles.chartTitle, children: "Market Share" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', padding: 8 }, children: [_jsx(View, { style: { alignItems: 'center' }, children: _jsx(Svg, { width: 110, height: 150, viewBox: "0 0 110 150", content: renderDonutChart(tableData) }) }), _jsx(View, { style: { gap: 3, marginTop: 8 }, children: tableData.map((row, i) => {
|
|
172
|
+
const total = tableData.reduce((s, r) => s + parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
173
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')), 0);
|
|
174
|
+
const rowTotal = parseFloat(row.q1.replace(/[$,]/g, '')) + parseFloat(row.q2.replace(/[$,]/g, ''))
|
|
175
|
+
+ parseFloat(row.q3.replace(/[$,]/g, '')) + parseFloat(row.q4.replace(/[$,]/g, ''));
|
|
176
|
+
const pct = ((rowTotal / total) * 100).toFixed(0);
|
|
177
|
+
return (_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 3 }, children: [_jsx(View, { style: { width: 8, height: 8, borderRadius: 2, backgroundColor: CHART_COLORS[i % CHART_COLORS.length] } }), _jsxs(Text, { style: { fontSize: 7, color: '#64748b' }, children: [row.region, " ", pct, "%"] })] }, i));
|
|
178
|
+
}) })] })] })] }), _jsx(Text, { style: styles.chartTitle, children: "Quarterly Growth Trend" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', paddingVertical: 12, marginBottom: 24 }, children: [_jsx(Svg, { width: 484, height: 140, viewBox: "0 0 484 140", content: renderLineChart(tableData) }), _jsx(View, { style: { position: 'relative', height: 14, marginTop: 4 }, children: ['Q1', 'Q2', 'Q3', 'Q4'].map((q, i) => (_jsx(Text, { style: { position: 'absolute', left: 16 + (452 / 3) * i - 12, width: 24, fontSize: 8, color: '#64748b', textAlign: 'center' }, children: q }, i))) })] }), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[3].title, style: styles.sectionTitle, children: ["4. ", data.sections[3].title] }), _jsx(Text, { style: styles.introText, children: data.sections[3].intro }), (data.sections[3].items || []).map((item, i) => (_jsxs(View, { style: styles.recCard, children: [_jsx(View, { style: styles.recBadge, children: _jsx(Text, { style: styles.recBadgeText, children: i + 1 }) }), _jsxs(View, { style: { flexGrow: 1, flexShrink: 1 }, children: [_jsx(Text, { style: styles.recTitle, children: item.title }), _jsx(Text, { style: styles.recBody, children: item.description }), _jsxs(View, { style: { flexDirection: 'row', gap: 16, marginTop: 8 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 4 }, children: [_jsx(Text, { style: styles.recLabel, children: "Priority:" }), _jsx(Text, { style: styles.recValue, children: item.priority })] }), _jsxs(View, { style: { flexDirection: 'row', gap: 4 }, children: [_jsx(Text, { style: styles.recLabel, children: "Timeline:" }), _jsx(Text, { style: styles.recValue, children: item.timeline })] })] })] })] }, i)))] })] }));
|
|
179
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ShippingLabel(data: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text } from '@formepdf/react';
|
|
3
|
+
export default function ShippingLabel(data) {
|
|
4
|
+
return (_jsx(Document, { title: `Shipping Label - ${data.tracking}`, children: _jsxs(Page, { size: { width: 288, height: 432 }, margin: 16, children: [_jsxs(View, { style: { marginBottom: 12, padding: 8 }, children: [_jsx(Text, { style: { fontSize: 7, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4 }, children: "From" }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.name }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.address }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.cityStateZip })] }), _jsx(View, { style: { borderTopWidth: 2, borderColor: '#0f172a', marginBottom: 12 } }), _jsxs(View, { style: { padding: 12, marginBottom: 12 }, children: [_jsx(Text, { style: { fontSize: 7, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 }, children: "To" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a' }, children: data.to.name }), _jsx(Text, { style: { fontSize: 10, color: '#0f172a', marginTop: 4 }, children: data.to.address }), data.to.address2 && (_jsx(Text, { style: { fontSize: 10, color: '#0f172a' }, children: data.to.address2 })), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.to.cityStateZip })] }), _jsxs(View, { style: { marginBottom: 8, padding: 8 }, children: [_jsx(View, { style: { alignItems: 'center', marginBottom: 8 }, children: _jsxs(View, { style: { flexDirection: 'row', gap: 2 }, children: [_jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } })] }) }), _jsx(Text, { style: { fontSize: 8, fontWeight: 700, color: '#0f172a', letterSpacing: 2, textAlign: 'center' }, children: data.tracking })] }), _jsx(View, { style: { borderTopWidth: 1, borderColor: '#cbd5e1', marginBottom: 8 } }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', padding: 8 }, children: [_jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Weight" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.weight })] }), _jsxs(View, { style: { flex: 2, justifyContent: 'center', alignItems: 'center' }, children: [_jsx(Text, { style: { width: '100%', textAlign: 'center', fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Dimensions" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.dimensions })] }), _jsxs(View, { style: { flex: 1, justifyContent: 'center', alignItems: 'flex-end' }, children: [_jsx(Text, { style: { width: '100%', textAlign: 'right', fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Service" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.service })] })] }), data.stamps && data.stamps.length > 0 && (_jsx(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8, padding: 8 }, children: data.stamps.map((stamp, i) => (_jsx(View, { style: { flex: 1, textAlign: 'center', paddingVertical: 6, paddingHorizontal: 12, borderWidth: 2, borderColor: '#dc2626', borderRadius: 2 }, children: _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#dc2626', textTransform: 'uppercase' }, children: stamp }) }, i))) }))] }) }));
|
|
5
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formepdf/hono",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "PDF middleware and responses for Hono — works on Workers, Deno, Bun, Node",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@formepdf/react": "0.5.0",
|
|
16
|
+
"@formepdf/core": "0.5.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"hono": ">=4.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"react": "^19.0.0",
|
|
24
|
+
"hono": "^4.0.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/formepdf/forme",
|
|
31
|
+
"directory": "packages/hono"
|
|
32
|
+
}
|
|
33
|
+
}
|