{
  "markdown": "# @boxpdf/writer\n\nA box-layout DSL over [pdf-lib](https://pdf-lib.js.org/). Implemented in portable JavaScript, it runs in Node 20+, Cloudflare Workers, Deno, and browsers.\n\nLive gallery: <https://earonesty.github.io/boxpdf/>\n\n```ts\nimport { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from \"@boxpdf/writer\";\n\nconst bytes = await flowToPdf(async (pdf) => {\n  const { font, bold } = await standardFonts(pdf);\n  const theme = cleanTheme({ font, bold });\n\n  return [\n    vstack({ gap: 8 },\n      text(\"Receipt #18472\", theme.type.h1),\n      text(\"May 14, 2026\", theme.type.caption)\n    ),\n    hline(theme.hr),\n    hstack({ gap: 16, justify: \"between\", width: 515 },\n      text(\"Wool socks\", theme.type.body),\n      text(\"$28.00\", { ...theme.type.body, font: bold, align: \"right\", width: 80 })\n    )\n  ];\n});\n```\n\n`flowToPdf` owns the document lifecycle and returns the saved bytes. `standardFonts` embeds the built-in Helvetica family (regular, bold, italic, bold-italic) in one call.\n\n<details>\n<summary>Prefer to manage the document yourself? The explicit path still works.</summary>\n\n```ts\nimport { PDFDocument, StandardFonts } from \"pdf-lib\";\nimport { cleanTheme, renderFlow, text, vstack } from \"@boxpdf/writer\";\n\nconst pdf  = await PDFDocument.create();\nconst font = await pdf.embedFont(StandardFonts.Helvetica);\nconst bold = await pdf.embedFont(StandardFonts.HelveticaBold);\nconst theme = cleanTheme(font, bold);\n\nawait renderFlow(pdf, [\n  vstack({ gap: 8 },\n    text(\"Receipt #18472\", theme.type.h1),\n    text(\"May 14, 2026\", theme.type.caption)\n  )\n]);\n\nconst bytes = await pdf.save();\n```\n\n`renderFlow(pdf, nodes, options)` paginates into a document you own and returns `{ pages }` — reach for it when you need multiple render passes, the page objects, or custom `save()` options. `boxpdf` re-exports `PDFDocument` and `StandardFonts` for this explicit lifecycle.\n\n</details>\n\n## Install\n\n```sh\nnpm install @boxpdf/writer pdf-lib\n```\n\n`pdf-lib` is a peer dependency.\n\n### Legacy package name\n\nThe original `boxpdf` package remains supported and is published from the same build at the same\nversion. Existing imports and the `boxpdf` CLI continue to work unchanged:\n\n```sh\nnpm install boxpdf pdf-lib\n```\n\nNew projects should use `@boxpdf/writer`. Both package names expose the same API, and both provide\nthe `boxpdf` command.\n\n## What it does\n\n- Declarative layout primitives: `vstack`, `hstack`, `text`, `image`, `hline`, `vline`, `spacer`, `flex`, `keepTogether`, `link`, `svgPath`, `table`.\n- Layout-aware AcroForm fields: text, checkbox, radio, dropdown, option-list, and push-button widgets.\n- Padding, margin, background, background images, borders, borderRadius, overflow clipping, flex-grow, flex-shrink, justify, align.\n- Rich paragraphs with mixed inline runs, inline replaced nodes, hard breaks, hanging indents, and optional paragraph floats.\n- Word wrapping with `maxLines` truncation, optional `breakWords`, and no-wrap control.\n- Themes: `cleanTheme`, `stripeTheme`, `editorialTheme`, `brutalistTheme`.\n- Multi-page flow with per-page headers and footers, stack fragmentation, and table row fragmentation.\n- Streaming generation for memory-bounded output.\n- PDF link annotations, text decorations, document metadata.\n- ~7 KB minified core. Custom fonts pull in `@pdf-lib/fontkit` only when you call `loadFont` or `embedInter`.\n\n## Templates\n\nFiles in [`templates/`](./templates) cover receipts, boarding passes, resumes, order confirmations, and certificates. Each is a single file.\n\nScaffold one into your app with the CLI:\n\n```sh\nnpx boxpdf init receipt --out src/pdf/receipt.ts\nnpx boxpdf list\n```\n\nThe CLI also ships a resource-only MCP server for agents:\n\n```sh\nclaude mcp add boxpdf -- npx -y boxpdf mcp\n```\n\n## Themes\n\n```ts\nimport { cleanTheme, editorialTheme, standardFonts } from \"@boxpdf/writer\";\n\nconst theme = cleanTheme(await standardFonts(pdf));            // Helvetica\nconst serif = editorialTheme(await standardFonts(pdf, \"times\")); // serif + italic slot\n```\n\nEvery theme factory accepts either a `{ font, bold, italic? }` object — which is exactly what `standardFonts(pdf)` and `embedInter(pdf)` return — or the legacy positional fonts:\n\n```ts\ncleanTheme({ font, bold })            // or cleanTheme(font, bold)\nstripeTheme({ font, bold })\neditorialTheme({ font, bold, italic }) // or editorialTheme(font, bold, italic)\nbrutalistTheme({ font, bold })         // courier regular + bold\n```\n\n`standardFonts(pdf, family)` takes `\"helvetica\"` (default), `\"times\"`, or `\"courier\"` and returns `{ font, bold, italic, boldItalic }`. Every theme exposes the same shape: `colors`, `spacing`, `radii`, `type`, `card`, `hr`.\n\n## API\n\n### Containers\n\n- `vstack(style, ...children)`. Vertical layout.\n- `hstack(style, ...children)`. Horizontal layout.\n- `keepTogether({ gap?, margin? }, ...children)`. Paginates atomically.\n\nContainer `style`:\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `width` / `height` | number | Fixed dimensions; otherwise size to content. |\n| `padding` / `margin` | number \\| `{ top, right, bottom, left }` | Shorthand or per-side. |\n| `background` | RGB | Solid fill. |\n| `backgroundImage` | `{ image, width, height, offsetX?, offsetY?, repeat? }` | Image painted behind children and clipped to the box. |\n| `border` | `{ color, width }` | 1pt+ stroke around the box. |\n| `borderSides` | `{ top?, right?, bottom?, left? }` | Per-side strokes using `{ color, width }`. |\n| `borderRadius` | number | Corner radius. |\n| `overflow` | `\"visible\"` \\| `\"hidden\"` | Clips stack children and absolute descendants to the box rectangle. |\n| `position` | `\"relative\"` \\| `\"absolute\"` | CSS-like positioning for boxes. |\n| `top` / `right` / `bottom` / `left` | number | Absolute offsets in points. |\n| `zIndex` | number | Paint order for positioned boxes; higher values render later. |\n| `rotate` | number | Clockwise paint rotation in degrees around the box center; layout is unchanged. |\n| `transform` | `BoxTransform[]` | Ordered paint transforms: `translate`, `scale`, `rotate`, `skew`, and `matrix`. |\n| `transformOrigin` | `{ x, y }` | Pivot using `{ length, percent }` components; defaults to the box center. |\n| `grow` | number | Flex grow weight along the parent's main axis. |\n| `shrink` | number | Flex shrink weight. |\n| `breakInside` | `\"auto\"` \\| `\"avoid\"` | Fragmentation hint under `renderFlow`; `avoid` keeps the box atomic. |\n| `gap` | number | Spacing between children. |\n| `justify` | `\"start\"` \\| `\"center\"` \\| `\"end\"` \\| `\"between\"` \\| `\"around\"` \\| `\"evenly\"` | Main-axis distribution. |\n| `align` | `\"start\"` \\| `\"center\"` \\| `\"end\"` \\| `\"stretch\"` \\| `\"baseline\"` | Cross-axis alignment. `baseline` is intended for `hstack` rows. |\n\n### Leaves\n\n- `text(content, { size, font, color?, align?, width?, lineHeight?, maxLines?, underline?, strikethrough?, margin? })`. Word-wraps when `width` is set. Truncates with ellipsis when `maxLines` is set. Default `lineHeight` uses the font's full height, including descenders.\n- `paragraph({ width?, align?, lineHeight?, margin?, paddingLeft?, textIndent?, wrap?, floats? }, ...runs)`. Mixed inline text runs and atomic inline nodes that wrap together as one paragraph. Use `run(text, style)`, `linkRun(text, style, href)`, and `inlineNode(node, { verticalAlign?, href? })`. Newlines in runs create hard breaks; `wrap: false` disables soft wrapping.\n- `image(pdfImage, { width, height, margin? })`. Takes an already-embedded `PDFImage`.\n- `imageFit(pdfImage, { width, height, fit?, margin? })`. Draws an image centered in a fixed rectangle, scaled to contain (default) or cover with clipping.\n- `spacer(size, { grow? })` / `flex(weight = 1)`. Fixed or growing gap.\n- `hline({ color, thickness?, width?, margin? })`.\n- `vline({ color, thickness?, height?, margin? })`.\n- `link({ href }, child)`. Wraps a child and registers a PDF Link annotation over its rendered bounding box.\n- `table({ columns, rows, ... })`. Fixed / auto / fractional columns with header/footer rows, dividers, styled cells, and row-level page fragmentation under `renderFlow`. Cells can be plain nodes or `{ content, colSpan?, padding?, background?, border?, borderSides?, borderRadius?, align?, valign? }`.\n\n### AcroForm fields\n\nForm widgets are atomic layout nodes, so they work inside stacks, tables, pagination, and streamed documents without manual page coordinates.\n\n```ts\nimport {\n  checkbox,\n  dropdown,\n  flowToPdf,\n  standardFonts,\n  text,\n  textField,\n  vstack\n} from \"@boxpdf/writer\";\n\nconst bytes = await flowToPdf(async (pdf) => {\n  const { font } = await standardFonts(pdf);\n  return [\n    vstack({ gap: 10 },\n      text(\"Registration\", { size: 18, font }),\n      textField({\n        name: \"person.name\",\n        width: 260,\n        height: 26,\n        font,\n        fontSize: 11,\n        required: true\n      }),\n      dropdown({\n        name: \"person.state\",\n        width: 140,\n        height: 26,\n        font,\n        options: [\"CA\", \"NY\", \"WA\"]\n      }),\n      checkbox({\n        name: \"terms.accepted\",\n        width: 16,\n        height: 16,\n        required: true\n      })\n    )\n  ];\n});\n```\n\n- `textField({ name, width, height, ... })`. Supports an initial `value`, `multiline`, `password`, `maxLength`, `combed`, alignment, and shared field flags and appearance options. Password text fields are non-exportable by default unless `exported: true` is explicitly set.\n- `checkbox({ name, width, height, checked? })`.\n- `radioOption({ name, option, width, height, selected? })`. Nodes with the same name form one radio group.\n- `dropdown({ name, options, width, height, selected?, editable?, sorted? })`. Kept single-select for consistent viewer behavior.\n- `optionList({ name, options, width, height, selected?, multiselect?, sorted? })`.\n- `button({ name, label, width, height, ... })`. Creates a portable push-button widget and appearance; BoxPDF does not attach PDF JavaScript or submit actions. Standard SubmitForm actions are reader-dependent and browser viewers may block submissions from local PDFs by origin policy.\n\nAll fields accept `margin`, `alignSelf`, `readOnly`, `required`, `exported`, `hidden`, `backgroundColor`, `borderColor`, and `borderWidth`. Text-bearing fields also accept `font`, `fontSize`, and `textColor`. Field names are document-global. Reusing a name adds another widget for the same logical field; reusing it for a different field type throws. Password text fields default to `exported: false`, so mark `exported: true` to permit submission/export intentionally. Forms work with ordinary, streamed, and encrypted output; use the encryption `fillForms` permission to control whether conforming readers allow changes.\nFor shared logical fields, text, dropdown, and option-list initialization state (`value`, `options`, and initial `selected`) is fixed by the first node. Radio-group flags (`offToggleable`, `mutuallyExclusive`) are also first-node-only. `selected: true` on a `radioOption` marks that option, while `selected: false` does not clear any existing selection.\n\nUse `getFormValues(pdf)`, `setFormValues(pdf, values)`, and `flattenForm(pdf)` when working with a caller-owned `PDFDocument`. When updating non-WinAnsi text, pass the embedded font as `{ font }` to `setFormValues` or `flattenForm` so pdf-lib regenerates the appearances with that font.\n\nAcroForm widgets are PDF annotations rather than page drawing operations. They therefore cannot be placed inside transformed BoxPDF ancestors; BoxPDF throws instead of emitting a misplaced widget. XFA, signature fields, PDF JavaScript, and cryptographic signing are outside the core form layer.\n\n### Rendering\n\n- `flowToPdf(build, options?)`. The shortest path to bytes. Creates a `PDFDocument`, hands it to your `build(pdf)` callback (embed fonts/images there and return the top-level nodes), paginates with `renderFlow`, and returns the saved `Uint8Array`. Same `options` as `renderFlow`.\n- `renderFlow(pdf, nodes[], options)`. Paginates a sequence of top-level children. Top-level `vstack` nodes may fragment between children; `table()` fragments between rows and repeats headers on continuation pages. Use `keepTogether()` or `breakInside: \"avoid\"` for atomic blocks. Options: `size`, `margin`, `header?`, `footer?`, `reserveBottom?`, `title?`, `author?`, `subject?`, `keywords?`, `creator?`, `producer?`, `debug?`, `warnings?`, `profile?`. Headers and footers receive `{ pageNumber, totalPages }`. Defaults to LETTER (612×792). Pass `{ size: PageSizes.A4 }` for A4. When a top-level child's measured width exceeds the page content area, boxpdf emits a `console.warn`. Suppress with `warnings: false`.\n- `savePdf(pdf, options?)`. Save a caller-owned document, optionally with password encryption. Calling `pdf.save()` directly always remains pdf-lib's unencrypted behavior.\n- `streamFlow(pdf, writable, asyncIterable, options)`. Incremental page-by-page rendering. Memory stays bounded regardless of page count. Writes PDF bytes to a `WritableStream<Uint8Array>` as each page closes. See the Streaming section below for the contract.\n- `renderToPdf(node, options)`. One-page convenience.\n- `pageInner(size, margin)` / `pageContent(size, margin)`. Compute the inner content width or rectangle of a page.\n- `render(node, page, x, yTop, parentWidth)`. Draws a subtree at a known position on an existing `PDFPage`.\n- `measure(node, parentWidth)`. Computes intrinsic size independently of rendering.\n\nPass `{ debug: true }` to outline content boxes in red and margin boxes in orange.\n\n### Helpers\n\n- `standardFonts(pdf, family?)`. Embed a built-in pdf-lib family (`\"helvetica\"` default, `\"times\"`, `\"courier\"`) and get `{ font, bold, italic, boldItalic }` back — ready to drop into any theme. These use compact PDF standard-font references.\n- `loadFont(pdf, source, options?)`. Embed a TTF from URL, bytes, base64, or data URL.\n- `loadImage(pdf, source)`. Embed a PNG or JPEG (auto-detected).\n- `aspectRatio(ratio, { width })` / `aspectRatio(ratio, { height })`. Derive the missing dimension for fixed-ratio boxes or images.\n- `formatCurrency(n, { currency, locale })`. `Intl.NumberFormat` wrapper.\n- `defineStyles({ ... })`. Typed identity for reusable style bundles.\n- `hex(\"#1f8a4d\")` / `rgb255(31, 138, 77)`. Color builders.\n\n## Loading fonts\n\nThree options.\n\n**Bundled bytes via the CLI.** Recommended for production.\n\n```sh\nnpx boxpdf font add ./Acme-Regular.ttf=regular ./Acme-Bold.ttf=bold \\\n  --out src/fonts/acme.ts\n```\n\nGenerates `src/fonts/acme.ts` with `export const` base64 strings. Then:\n\n```ts\nimport { loadFont } from \"@boxpdf/writer\";\nimport { regular, bold } from \"./fonts/acme.js\";\n\nconst font = await loadFont(pdf, regular);\nconst acmeBold = await loadFont(pdf, bold);\n```\n\nBytes ship inside your bundle for immediate local loading.\n\n**The built-in Inter weights.**\n\n```ts\nimport { loadFont } from \"@boxpdf/writer\";\nimport { inter, interBold } from \"@boxpdf/writer/inter\";\n\nconst font = await loadFont(pdf, inter);\nconst bold = await loadFont(pdf, interBold);\n```\n\n`boxpdf/inter` re-exports the same Inter subset as raw base64 strings (`inter`, `interBold`, `interItalic`) and as `embedInter(pdf, { italic?, tabularFigures? })`.\n\nImporting `boxpdf/inter` loads ~325 KB of font bytes plus `@pdf-lib/fontkit`. Core-only imports stay on the smaller core bundle.\n\n```ts\nimport { embedInter } from \"@boxpdf/writer/inter\";\n\nconst { font, bold } = await embedInter(pdf);\nconst theme = cleanTheme(font, bold);\n```\n\nPass `{ tabularFigures: true }` to also get tabular-numeral variants for money columns:\n\n```ts\nconst { font, bold, tabularFont, tabularBold } = await embedInter(pdf, {\n  tabularFigures: true\n});\n\ntext(formatCurrency(amount), { size: 12, font: tabularBold, align: \"right\" });\n```\n\n**Fetch from a URL.**\n\n```ts\nconst brand = await loadFont(pdf, \"https://example.com/Acme-Regular.ttf\");\n```\n\nThe full TTF gets fetched and subsetted at embed time. On Cloudflare Workers with a warm cache this is fast (~5-15 ms). On a cold cache or in Node you pay the full fetch each time.\n\n`loadFont` accepts the same `{ subset?: boolean; features?: { tnum: true } }` options regardless of the source. Use `features: { tnum: true }` to enable tabular numerals.\n\n## Password encryption\n\nBoxPDF can write PDF 2.0 password-encrypted output using the Standard Security\nHandler revision 6 and AES-256. Encryption uses the runtime's Web Crypto\nimplementation and adds no crypto dependency to browser bundles. The\nimplementation is loaded as a separate chunk only when encryption is requested.\n\n```ts\nconst bytes = await flowToPdf(\n  async (pdf) => {\n    const { font } = await standardFonts(pdf);\n    return [text(\"Confidential\", { font, size: 18 })];\n  },\n  {\n    encryption: {\n      password: \"document-open-password\",\n      ownerPassword: \"administrative-password\",\n      permissions: {\n        printing: \"lowResolution\",\n        copying: false,\n        modify: false\n      }\n    }\n  }\n);\n```\n\nFor a caller-owned document, save through `savePdf`:\n\n```ts\nimport { PDFDocument, renderFlow, savePdf } from \"@boxpdf/writer\";\n\nconst pdf = await PDFDocument.create();\nawait renderFlow(pdf, nodes);\nconst bytes = await savePdf(pdf, {\n  encryption: { password: \"open me\" }\n});\n```\n\n`password` is required and cannot prepare to an empty value. `ownerPassword` is\noptional; when omitted, BoxPDF generates and discards a random internal owner\ncredential. Passwords use SASLprep and may contain Unicode, with a maximum of\n127 UTF-8 bytes after preparation. Available permissions are `printing`,\n`modify`, `copying`, `annotate`, `fillForms`, and `assemble`.\n\nPDF permissions are advisory viewer settings, not DRM. Send the password by a\ndifferent channel from the PDF. Encryption cannot be combined with PDF/A, and\nBoxPDF does not decrypt input PDFs or preserve existing signatures. Saving the\nsame document again creates fresh keys, salts, file identifiers, and IVs.\n\n## Streaming output\n\nFor long-running document generation, use `streamFlow` instead of `renderFlow`. It emits PDF bytes to a `WritableStream<Uint8Array>` as each page closes. Peak heap is bounded at `O(shared resources + one page in flight)` regardless of total page count.\n\n```ts\nimport { PDFDocument, StandardFonts } from \"pdf-lib\";\nimport { streamFlow, text, cleanTheme } from \"@boxpdf/writer\";\n\nconst pdf = await PDFDocument.create();\nconst font = await pdf.embedFont(StandardFonts.Helvetica);\nconst bold = await pdf.embedFont(StandardFonts.HelveticaBold);\n\nconst { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();\nstreamFlow(pdf, writable, generate(font, bold)).catch(console.error);\n\nreturn new Response(readable, {\n  headers: { \"content-type\": \"application/pdf\" }\n});\n\nasync function* generate(font, bold) {\n  for await (const order of fetchOrders()) {\n    yield buildOrderRow(font, bold, order);\n  }\n}\n```\n\nFor Node, adapt a `stream.Writable`:\n\n```ts\nimport { createWriteStream } from \"node:fs\";\nimport { streamFlow, nodeAdapter } from \"@boxpdf/writer\";\n\nconst out = nodeAdapter(createWriteStream(\"./report.pdf\"));\nawait streamFlow(pdf, out, nodes, {\n  encryption: { password: \"open me\" }\n});\n```\n\nIf one logical stack or table is too large to construct at once, emit bounded\npieces with `flowContinuation`. Adjacent pieces with the same id are paginated\nas though they were one node, including stack gaps, decoration, table headers,\nand row dividers:\n\n```ts\nfor (let offset = 0; offset < rows.length; offset += 100) {\n  const final = offset + 100 >= rows.length;\n  yield flowContinuation(\n    table({ columns, header, rows: rows.slice(offset, offset + 100) }),\n    \"orders\",\n    final\n  );\n}\n```\n\nContinuation fragments must be consecutive and the last one must set\n`final: true`; `streamFlow` rejects interrupted or unfinished sequences instead\nof silently producing an incomplete layout.\n\n### Contract\n\n1. All `embedFont` / `embedJpg` / `embedPng` calls must complete before `streamFlow`. Embedding mid-stream throws.\n2. The iterable is consumed one node at a time. Pass a generator.\n3. `streamFlow` takes exclusive ownership of the writable, closing it on success and aborting it on failure.\n4. Streaming headers and footers receive `ctx.pageNumber`. Use `renderFlow` for headers or footers that display \"Page X of Y\"; accessing `ctx.totalPages` during streaming throws.\n5. Output is 0-5% larger than `renderFlow`'s default `save()`.\n\n### Memory bench\n\nPeak heap during render. Each measurement runs in its own subprocess. 50 lines of text per page. `@react-pdf/renderer` included for shape comparison.\n\n| Pages | streamFlow peak | renderFlow peak | @react-pdf peak | Output |\n| ---:  | ---:            | ---:            | ---:            | ---:   |\n|    50 |     12.8 MB     |     31.7 MB     |    160.8 MB     |  70 KB |\n|   250 |     15.4 MB     |     91.1 MB     |    643.1 MB     | 347 KB |\n|   500 |     18.7 MB     |    120.8 MB     |  1,219.9 MB     | 693 KB |\n|  1000 |     25.4 MB     |    219.6 MB     |  2,292.6 MB     | 1.4 MB |\n\nstreamFlow holds peak heap roughly flat (12 → 25 MB across a 100× workload increase). renderFlow scales roughly linearly with page count. `@react-pdf/renderer` adds ~2.3 MB per page in this workload and peaks at 2.3 GB by 1000 pages. See `docs/design/streaming.md` for the design and the chart.\n\nThe continuation path has its own heap-capped subprocess check. Unlike the\nolder comparison above, it constructs every fragment lazily and forces a GC\nafter output to distinguish V8's allocation high-water mark from the retained\nlive set:\n\n| Continuation fragments | Output pages | Sampled peak heap | Retained heap after GC | Output |\n| ---: | ---: | ---: | ---: | ---: |\n| 100 | 81 | ~58 MB | ~16 MB | 129 KB |\n| 1000 | 810 | ~125 MB | ~26 MB | 1.3 MB |\n\nBoth runs complete with `--max-old-space-size=128`. Across the 10× workload,\nthe retained heap grows by about 10 MB, primarily from the final page tree and xref\nindex; rendered page content and continuation input are released incrementally.\nReproduce it with `pnpm memory:check:continuation`.\n\n## Cloudflare Workers\n\nBoth the core and the `boxpdf/inter` subpath run on Workers without `nodejs_compat`.\n\n```ts\nimport { Hono } from \"hono\";\nimport { cleanTheme, flowToPdf, standardFonts, text } from \"@boxpdf/writer\";\n\nconst app = new Hono();\n\napp.get(\"/receipt.pdf\", async (c) => {\n  const bytes = await flowToPdf(async (pdf) => {\n    const t = cleanTheme(await standardFonts(pdf));\n    return [\n      text(\"Thanks!\", t.type.h1),\n      text(\"This PDF was generated at the edge.\", t.type.body)\n    ];\n  });\n  return new Response(bytes, { headers: { \"content-type\": \"application/pdf\" } });\n});\n\nexport default app;\n```\n\n## Examples\n\nRunnable scripts in [`examples/`](./examples):\n\n- `receipt.ts`. Single-page receipt with totals.\n- `itinerary.ts`. Two-band travel itinerary.\n- `invoice.ts`. Multi-page invoice with running header and footer plus `keepTogether`.\n- `debug.ts`. Layout with `{ debug: true }`.\n- `themes-showcase.ts`. The same receipt rendered in all four themes.\n- `inter-showcase.ts`. Clean theme rendered with Inter.\n- `flex-shrink.ts`. Three URL-overflow behaviors side by side.\n- `hanging-indent.ts`. Paragraph `paddingLeft` plus negative `textIndent` for list markers.\n- `overflow-clipping.ts`. Clipped cards with absolute overlays and background images.\n\n## Flex-shrink\n\nOpt-in via `shrink: number` on any child of an `hstack` or `vstack`. When the sum of children's intrinsic main-axis sizes exceeds the parent's available space, items with `shrink > 0` give up shares proportional to `shrink × baseSize`. Items with `shrink = 0` (the default) are frozen.\n\n```ts\nhstack(\n  { width: 360, gap: 16 },\n  text(\"Customer:\", { size: 11, font: bold }),\n  text(\"Mr. Algernon Hephaestus Constantine Pemberton-Smythe III\", {\n    size: 11, font, shrink: 1\n  })\n)\n```\n\nBehavior:\n\n- A text child's minimum width equals its widest whitespace-separated word. Wrapping occurs at whitespace boundaries.\n- A single-token string (URL, hash, slug) preserves its intrinsic width and visibly overflows its slot. Two opt-ins lower the floor:\n  - `maxLines: N`. The engine ellipsizes overflow. The text shrinks to its slot and trims with `…`.\n  - `breakWords: true`. CSS `overflow-wrap: break-word`. Hard-breaks at character boundaries.\n- When shrunk text rewraps to more lines, the container's intrinsic height grows accordingly.\n- When one item hits its min-word floor, its remaining shrink weight redistributes to siblings.\n- Works on `vstack` too when the parent has a fixed `height` smaller than the sum of children.\n- `link` forwards its child's shrink weight, so linked text shrinks and re-wraps like bare text.\n\nSee `examples/flex-shrink.ts`.\n\n## Absolute positioning\n\nBoxes can use a small CSS-like positioning model:\n\n```ts\nvstack(\n  { width: 240, height: 120, position: \"relative\", padding: 16 },\n  text(\"Receipt\", { size: 18, font: bold }),\n  hstack(\n    { position: \"absolute\", top: 12, right: 12, width: 70 },\n    text(\"PAID\", { size: 14, font: bold, align: \"center\", width: 70 })\n  )\n)\n```\n\nBehavior:\n\n- Any positioned box establishes the containing block for absolute descendant boxes.\n- `position: \"absolute\"` removes a `vstack` or `hstack` from normal stack flow.\n- Absolute boxes render after normal children, so they can be used for stamps, badges, overlays, and watermarks.\n- `top`, `right`, `bottom`, and `left` are point offsets from the nearest positioned ancestor, falling back to the current `render()` root.\n- If both `left` and `right` are set and `width` is omitted, the box stretches to the remaining width. `top` plus `bottom` does the same for height.\n- Absolute siblings render by `zIndex` from low to high. Boxes with the same `zIndex` keep document order.\n- Parent measurement, gaps, flex grow/shrink, and pagination ignore absolute boxes. Give the containing box a fixed `width` and `height` when you need stable placement.\n\n## Limitations\n\n- Positioning supports relative containing boxes, out-of-flow absolute boxes, point offsets, `zIndex`, and stretch from paired edges.\n- Font shaping follows pdf-lib and fontkit support. Complex Indic, Arabic, and Thai scripts require a HarfBuzz-based stack; available HarfBuzz stacks currently target runtimes beyond Cloudflare Workers.\n- `streamFlow` supports incremental generation. PDF linearization (reordering the byte stream so byte 1 is page 1) remains a separate post-process.\n\n## License\n\nMIT © Erik Aronesty\n",
  "bytes": 26392,
  "sha": "8e192b32173c32395df3b348ea9d6ba0951efcd89011310be31fc5ca61ddfef7",
  "repo_slug": "earonesty/boxpdf",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_earonesty_boxpdf_04544730/readme"
}