Vendor PacketLast updated on
Last updated on
A packet of three documents — the purchase order composed live, a filled W-9 painted from its PDF layer, and the vendor's certificate of insurance as an annex, whose bytes it ships.
vendor-packet is the one block here that is not a single document: it is
three, one of each kind a packet can hold — a live composition, a filled
registry PDF form, and an uploaded annex — bundled so a buyer can send a new
supplier everything they need to sign and certify in one scroll.
Preview
Installation
npx shadcn@4 add @paradoc/vendor-packetOr, with the Paradoc CLI, which writes the namespace into components.json for you:
npx paradoc-cli add vendor-packetInstalls to artifacts/paradoc/vendor-packet.artifact.ts, artifacts/paradoc/vendor-packet.annex.ts, artifacts/paradoc/vendor-packet.data.ts, and components/paradoc/vendor-packet.tsx and brings along bundle, pages, part, pdf-pages, purchase-order.
Usage
Composition
components/paradoc/vendor-packet.tsx
/** @jsxRuntime classic */import React from "react";import { Bundle } from "@/components/paradoc/bundle";import { markDocumentRoot, type PagePlan } from "@paradoc/react";import { PdfPages, type PdfPaintReport } from "@/components/paradoc/pdf-pages";import { Pages } from "@/components/paradoc/pages";import { Part } from "@/components/paradoc/part";import { PurchaseOrderDocument, purchaseOrderFurniture } from "@/components/paradoc/purchase-order";import { purchaseOrderData as defaultPurchaseOrderData, type PurchaseOrderData,} from "@/artifacts/paradoc/purchase-order.data";import { VENDOR_PACKET_ANNEX_FILENAME, VENDOR_PACKET_KEYS,} from "../../artifacts/paradoc/vendor-packet.artifact";/** * The vendor packet on screen. * * **Sample material, not a stable API**, for the reason `./examples` gives. * * Three documents in one scroll, one of each kind, each numbering its own * pages: the purchase order paginated live from its tree, the filled W-9 * painted from the PDF its official layer produced, and the certificate of * insurance painted from the bytes the vendor supplied. This is the whole of * what a packet preview is; everything specific to it is the labels. * * The PDFs arrive as bytes rather than being produced here, because both are * produced outside the browser: the W-9 by filling its layer, the certificate * by whoever issued it. Passing the seal's `parts` in as well labels each * document with the packet pages it occupies, so what a reader sees on screen * and what the signature map says are visibly the same pages. *//** Where one part sits in the packet, as core's bundle seal reports it. */export interface PacketPlacement { /** The bundle content key. */ key: string; /** 1-based packet page the part's first page is. Zero for an attached part. */ firstPage: number; /** Pages the part contributes. Zero for an attached part. */ pageCount: number; /** True when the part is carried beside the packet rather than merged into it. */ attached?: boolean;}export interface VendorPacketDocumentProps { /** The purchase order's data. Defaults to the sample's own. */ purchaseOrderData?: PurchaseOrderData; /** The filled W-9, as its PDF layer rendered it. */ taxpayerPdf: Uint8Array; /** The certificate of insurance, as the vendor supplied it. */ insurancePdf: Uint8Array; /** Where each part sits in the packet, when the caller has sealed it. */ placements?: readonly PacketPlacement[]; /** * The `packetHash` the placements were computed for, and the packet on screen. * * They are one value here because the packet on screen is the packet that was * sealed. A host that lets a document change under a stale seal passes the * two separately to `Part`, and each part says its pages are pending until * the reseal lands. */ packetHash?: string; /** Where pdf.js loads its worker from. See `paintPdfPages`. */ workerSrc?: string; /** Where pdf.js loads the standard fourteen fonts from. See `paintPdfPages`. */ standardFontDataUrl?: string; /** Where pdf.js loads its CMaps from. See `paintPdfPages`. */ cMapUrl?: string; /** How long each PDF part may take to paint before it becomes an attachment. */ timeoutMs?: number; /** Called as each PDF part settles, painted or attached. */ onPaint?: (key: string, report: PdfPaintReport) => void; /** Called with the purchase order's page plan. */ onPaginate?: (plan: PagePlan) => void;}/** The packet, on screen. */export function VendorPacketDocument({ purchaseOrderData = defaultPurchaseOrderData, taxpayerPdf, insurancePdf, placements, packetHash, workerSrc, standardFontDataUrl, cMapUrl, timeoutMs, onPaint, onPaginate,}: VendorPacketDocumentProps) { const placed = (key: string) => placements?.find((placement) => placement.key === key); /** Everything a part needs to say where it sits, or nothing when unsealed. */ const placement = (key: string) => ({ firstPage: placed(key)?.firstPage, pageCount: placed(key)?.pageCount, attached: placed(key)?.attached, placedFor: packetHash, packetHash, }); return ( <Bundle id="vendor-packet" className="flex flex-col gap-10"> <Part id={VENDOR_PACKET_KEYS.purchaseOrder} kind="composition" label="Purchase order · composed live" {...placement(VENDOR_PACKET_KEYS.purchaseOrder)} > <Pages furniture={purchaseOrderFurniture} onPaginate={onPaginate}> <PurchaseOrderDocument data={purchaseOrderData} /> </Pages> </Part> <Part id={VENDOR_PACKET_KEYS.taxpayer} kind="form" label="Form W-9 · filled through its PDF layer" {...placement(VENDOR_PACKET_KEYS.taxpayer)} > <PdfPages bytes={taxpayerPdf} filename="w-9.pdf" workerSrc={workerSrc} standardFontDataUrl={standardFontDataUrl} cMapUrl={cMapUrl} timeoutMs={timeoutMs} onPaint={(report) => onPaint?.(VENDOR_PACKET_KEYS.taxpayer, report)} /> </Part> <Part id={VENDOR_PACKET_KEYS.insurance} kind="annex" label="Certificate of insurance · annex" {...placement(VENDOR_PACKET_KEYS.insurance)} > <PdfPages bytes={insurancePdf} filename={VENDOR_PACKET_ANNEX_FILENAME} workerSrc={workerSrc} standardFontDataUrl={standardFontDataUrl} cMapUrl={cMapUrl} timeoutMs={timeoutMs} onPaint={(report) => onPaint?.(VENDOR_PACKET_KEYS.insurance, report)} /> </Part> </Bundle> );}markDocumentRoot(VendorPacketDocument);export default VendorPacketDocument;Sample data
artifacts/paradoc/vendor-packet.data.ts
import { purchaseOrderData, type PurchaseOrderData } from "@/artifacts/paradoc/purchase-order.data";import { vendorPacketAnnexBytes } from "./vendor-packet.annex";/** * Sample data for the vendor packet. * * **Sample material, not a stable API**, for the reason `./examples` gives. * * A packet is three documents and its sample is three things, one per part: the * purchase order's own data, the taxpayer's values for the W-9, and the annex * bytes. The three are gathered here so the block installs a packet a consumer * can render without inventing any of them. * * The W-9 itself is not here, and that is deliberate: this data module carries * values only. The block preview imports `@paradoc/essentials`; other consumers * may resolve the packet's registry part through their own application wiring. * So this carries the taxpayer's values and nothing else. A consumer holds them * against the artifact: * * ```ts * import { w9 } from "@paradoc/essentials"; * * const parsed = w9.safeParseData(vendorPacketTaxpayerData); * const draft = w9 * .fill(parsed.data) * .addSigner("taxpayer-signer", { person: { name: "Dana Whitfield" } }) * .addSignatory("taxpayer", "taxpayer-0", { signerId: "taxpayer-signer" }); * * const pdf = await draft.render({ layer: "pdf" }); * ``` * * No resolver is passed: `@paradoc/essentials` binds each artifact's bundled * resolver when it constructs the form, and every instance derived from it — * including the one every mutator above returns — carries it. *//** * The taxpayer's values for the packet's W-9. * * Untyped against the artifact on purpose: typing it would mean importing the * artifact, and this module does not. The shape is the payload `safeParseData` * takes, and the values are the supplier the purchase order names, so the two * parts of the packet describe one company. * * The party carries `firstName`/`lastName` alongside `name`: the format * package infers a party's identity (person vs. organization) from which of * those disambiguating keys are present on the record itself, and a bare * `name` alone answers neither, so rendering the taxpayer's identity — filled * PDF included — would otherwise fail as ambiguous. */export const vendorPacketTaxpayerData = { parties: { taxpayer: { id: "taxpayer-0", name: "Dana Whitfield", firstName: "Dana", lastName: "Whitfield" }, }, fields: { taxClassification: "partnership", businessName: "Northgate Systems, LLC", ein: "47-2938471", mailingAddress: { line1: "1400 Rio Grande Street", line2: "Suite 220", locality: "Austin", region: "TX", postalCode: "78701", country: "US", }, },} as const;/** The person who signs the W-9 for the taxpayer party. */export const VENDOR_PACKET_TAXPAYER_SIGNER = { signerId: "taxpayer-signer", partyId: "taxpayer-0", person: { name: "Dana Whitfield" },} as const;/** The whole packet's sample, one entry per part. */export interface VendorPacketData { /** The purchase order's data, as its own block ships it. */ purchaseOrder: PurchaseOrderData; /** The taxpayer's values for the W-9. */ taxpayer: typeof vendorPacketTaxpayerData; /** The certificate of insurance, as bytes the packet carries. */ insurance: Uint8Array;}/** The vendor packet's sample. */export const vendorPacketData: VendorPacketData = { purchaseOrder: purchaseOrderData, taxpayer: vendorPacketTaxpayerData, insurance: vendorPacketAnnexBytes,};export { vendorPacketAnnexBytes };Composition
The packet's root is a Bundle, and each of its three
documents is a Part: the boundary that gives every
document its own page numbers and, once the packet is sealed, says where in
the packet those pages land.
The first part is the purchase order
composed live — the very artifact that block installs, not a copy of it, so
one edit to the order is one edit for both blocks. It wraps in its own
Pages, because a composition's pages come from a plan
this packet computes, unlike the two PDF parts beside it.
The second part is the IRS W-9, filled through its official PDF layer and
painted by PdfPages. A PDF already has its own
pages at its own paper size, so PdfPages paints exactly those pages rather
than planning new ones — the W-9 keeps its own paper even inside a packet
branded to another. Filling it needs @paradoc/essentials, which the block
declares as a dependency even though nothing it ships imports it: the
packet's sample carries the taxpayer's values, not the form, so a consumer
does the filling themselves:
import { w9 } from "@paradoc/essentials";
import { vendorPacketData } from "@/artifacts/paradoc/vendor-packet.data";
const parsed = w9.safeParseData(vendorPacketData.taxpayer);
if (!parsed.success) throw new Error("Invalid taxpayer data");
const draft = w9.fill(parsed.data);
const taxpayerPdf = await draft.render({ layer: "pdf" });No resolver is passed: @paradoc/essentials binds each artifact's bundled
resolver when it constructs the form, and every instance derived from it,
including draft, carries it.
The third part is the certificate of insurance, also painted by PdfPages.
Its readable PDF bytes are merged into the packet's pages; an annex is carried
as a true attachment only when its bytes cannot be read as a PDF. It is not
the caller's to supply: the block installs it as vendor-packet.annex.ts, a
module of bytes, because a block installed into a browser project has no engine
to draw one with.
Variants
A block's Variants section shows an existing alternate sample-data scenario, never a new prop configuration. Both of this block's parts that take a live sample — the purchase order and the taxpayer's W-9 values — ship exactly one sample-data scenario today, and the certificate of insurance ships one annex. There is no second scenario to show, so rather than invent one, this variant documents that and renders the same real packet again.
Standard packet
The block's one real sample scenario, shown again for its own subheading.