Documentation
TangentFlow is a browser-based document builder that generates professional PDFs with mathematically precise text layout. It combines Pretext for text measurement and pdf-lib for PDF generation.
Everything runs client-side in your browser. No server, no dependencies, no data leaves your machine.
Quick Start
- Open the Document Builder
- Choose a template (Report, Invoice, Resume, etc.) or start from scratch
- Add content blocks using the + buttons in the block bar
- Click any block to expand its inline editor
- The canvas preview updates in real-time as you edit
- Click Download PDF to export
Architecture
TangentFlow uses a three-layer architecture:
Your content (blocks)
|
Pretext (measures text, calculates line breaks)
|
Flow Engine (paginates, places elements, handles page breaks)
|
Renderer (Canvas for preview, pdf-lib for export)
The Flow Engine converts your block list into a sequence of draw commands (text, rect, line, image) positioned in PDF coordinate space. These commands are rendered identically to both the canvas preview and the PDF output, ensuring what you see is exactly what you get.
Image
Upload a PNG or JPEG image. Configure width (in points) and alignment (left, center, right). Images maintain their aspect ratio and are embedded directly in the PDF.
{
"type": "image",
"src": "data:image/png;base64,...",
"width": 200,
"align": "center"
}
Heading
Three heading levels with decreasing font sizes. Headings render in bold and are used by the flow engine to add appropriate spacing.
| Level | Font Size | Use Case |
|---|---|---|
| H1 | 22pt | Document title |
| H2 | 16pt | Section heading |
| H3 | 13pt | Subsection heading |
{
"type": "heading",
"text": "Quarterly Report",
"level": 1
}
Paragraph
Body text with automatic Pretext-powered line wrapping. Supports hard line breaks with \n and inline formatting (see Inline Formatting).
{
"type": "paragraph",
"text": "This has **bold**, *italic*, and __underlined__ text."
}
Bullet List
Unordered list with bullet markers. Each item is independently wrapped by Pretext. Items are separated by newlines.
{
"type": "bullet-list",
"items": "First item\nSecond item with longer text that wraps\nThird item"
}
Numbered List
Ordered list with auto-incrementing numbers. Same wrapping behavior as bullet lists.
{
"type": "numbered-list",
"items": "First step\nSecond step\nThird step"
}
Quote / Callout
A block quote with a colored left bar. Useful for callouts, notes, or attributed quotations. The bar color is controlled by the Quote Bar color in Document Style.
{
"type": "quote",
"text": "The only way to do great work is to love what you do.",
"attribution": "Steve Jobs"
}
Table
The most powerful block type. Tables feature:
- Per-cell text wrapping — every cell's content is measured by Pretext and wrapped within its column
- Auto-sized columns — column widths are calculated based on content, distributed proportionally
- Dynamic row heights — rows expand to fit their tallest cell
- Header repeat — when a table spans multiple pages, headers re-render automatically
- Zebra striping — alternating row backgrounds for readability
The inline editor shows a compact 3-row preview. Click Expand Editor to open the full spreadsheet-like modal with keyboard navigation (Tab, Enter, Arrow keys).
{
"type": "table",
"headers": "Name, Description, Price",
"rows": "Widget, A useful widget for all purposes, $9.99\nGadget, An advanced gadget with features, $19.99"
}
Key-Value
Displays label-value pairs in a clean list format with separator lines. Ideal for metadata, contact info, document properties, or billing details.
{
"type": "key-value",
"items": "Client: Acme Corp\nProject: Website Redesign\nDeadline: April 30 2026\nStatus: In Progress"
}
Two Column
Side-by-side text blocks, each independently wrapped to half the content width. Useful for comparisons, contact details, signature areas, or compact layouts.
{
"type": "two-column",
"left": "Left column content here.",
"right": "Right column content here."
}
Stat Row
Horizontal row of metric cards. Each card displays a label above and a bold value below, rendered inside a rounded rectangle. Auto-sizes to fit the available width.
{
"type": "stat-row",
"items": "Revenue: $36.5M, Growth: +23%, Customers: 2847"
}
Divider
A horizontal line spanning the content width. Color is controlled by the Divider color setting.
{ "type": "divider" }
Spacer
Adds vertical spacing between blocks. Adjustable height via a slider (4-80px).
{
"type": "spacer",
"height": 20
}
Page Break
Forces all subsequent content onto a new page. Useful for separating sections in multi-page documents.
{ "type": "page-break" }
Nested Lists
Create hierarchical lists using nested arrays. Each sub-array becomes an indented level beneath its parent item. Nesting works for both bullet and numbered lists.
// Builder API
doc.bulletList([
"Top-level item",
["Sub-item A", "Sub-item B"],
"Another top-level item",
["Nested under second", ["Deeply nested"]]
])
// JSON Schema
{
"type": "bullet-list",
"items": [
"Top-level item",
["Sub-item A", "Sub-item B"],
"Another top-level item",
["Nested under second", ["Deeply nested"]]
]
}
Each nesting level indents by 18pt and uses a smaller bullet marker (disc, circle, dash). Numbered lists restart their counter at each nesting level.
Multi-Column
Flow text across multiple columns with automatic balancing. Unlike the fixed two-column block, multi-column supports 2-4 columns and text flows naturally from one column to the next.
// Builder API
doc.multiColumn({
columns: 3,
gap: 24,
text: "Long body text that flows across three columns..."
})
// JSON Schema
{
"type": "multi-column",
"columns": 3,
"gap": 24,
"text": "Long body text that flows across three columns..."
}
The gap property (in points) controls spacing between columns. Defaults to 20pt if omitted. Text is measured by Pretext and distributed evenly across all columns.
Image Captions
Add captions below images. The caption text renders in a smaller, muted font beneath the image and supports inline formatting.
// Builder API
doc.image({
src: "data:image/png;base64,...",
width: 300,
align: "center",
caption: "Figure 1: Quarterly revenue growth"
})
// JSON Schema
{
"type": "image",
"src": "data:image/png;base64,...",
"width": 300,
"align": "center",
"caption": "Figure 1: Quarterly revenue growth"
}
Captions inherit the Muted Text color and render at 9pt. They are center-aligned relative to the image regardless of the image's own alignment setting.
Inline Formatting
Paragraphs support markdown-like inline formatting that renders with the correct fonts in both the canvas preview and the exported PDF:
| Syntax | Result | PDF Font |
|---|---|---|
**bold text** | bold text | Helvetica Bold |
*italic text* | italic text | Helvetica Oblique |
__underlined__ | underlined | Helvetica + underline |
[link](url) | link | Accent color + underline + clickable |
Formatting can be mixed within a single paragraph. Line-breaking is handled by Pretext on the plain text, then formatting is overlaid per character range.
Header & Footer
Configure repeating headers and footers that appear on every page of the document.
| Element | Description |
|---|---|
| Header Logo | Upload a PNG/JPEG image displayed at the top-left of every page |
| Header Left | Text next to the logo (e.g. company name) |
| Header Right | Right-aligned text (e.g. document title, date) |
| Footer Left | Left-aligned text (e.g. "Confidential") |
| Footer Right | Auto page numbers ("Page 1 of 3"), custom text, or none |
The content area automatically shrinks to accommodate header and footer space.
Watermark
Add a diagonal text watermark across every page. Configure:
- Text — e.g. "DRAFT", "CONFIDENTIAL", "SAMPLE"
- Color — any color via the color picker
- Opacity — 0-100% via the slider
The watermark renders behind all content in both the preview and the exported PDF.
Document Colors
Nine customizable color channels control the entire document palette:
| Color | Affects |
|---|---|
| Heading | H1/H2/H3 text, table header text, stat card values |
| Body Text | Paragraphs, list items, table cells, two-column text, quote text |
| Accent | Link text color |
| Table Header | Background fill of table header rows |
| Table Stripe | Background fill of alternating table rows |
| Divider | Horizontal rules, table borders, key-value separators |
| Quote Bar | Left accent bar on quote/callout blocks |
| Stat Card BG | Background fill of stat row cards |
| Muted Text | Bullet markers, labels, attribution, page numbers |
PDF Metadata
Set document properties that are embedded in the PDF file:
- Title — appears in PDF reader title bars and search indexes
- Author — document author name
- Subject — document subject or description
Additionally, Creator and Producer are automatically set to "TangentFlow".
Page Setup
| Option | Values |
|---|---|
| Size | A4 (595 x 842pt), Letter (612 x 792pt), Legal (612 x 1008pt) |
| Orientation | Portrait or Landscape (swaps width/height) |
| Margin | Narrow (40pt), Normal (60pt), Wide (80pt) |
Table Options
v0.3.0 adds fine-grained control over table column widths, cell alignment, and border rendering.
Column Widths
Override the automatic column sizing with explicit proportional or fixed widths:
// Builder API
doc.table({
headers: "Name, Description, Price",
rows: "Widget, A useful widget, $9.99",
colWidths: [100, "auto", 80]
})
// JSON Schema
{
"type": "table",
"headers": "Name, Description, Price",
"rows": "Widget, A useful widget, $9.99",
"colWidths": [100, "auto", 80]
}
Values can be numbers (fixed width in points) or "auto" to fill remaining space. If omitted, all columns auto-size based on content.
Cell Alignment
Set horizontal text alignment per column:
// Builder API
doc.table({
headers: "Item, Qty, Price",
rows: "Widget, 5, $49.95",
align: ["left", "center", "right"]
})
// JSON Schema
{
"type": "table",
"headers": "Item, Qty, Price",
"rows": "Widget, 5, $49.95",
"align": ["left", "center", "right"]
}
Accepted values are "left", "center", and "right". If fewer entries than columns are provided, remaining columns default to "left".
Table Borders
Control which borders are drawn around cells:
// Builder API
doc.table({
headers: "A, B, C",
rows: "1, 2, 3",
borders: "all"
})
// JSON Schema
{
"type": "table",
"headers": "A, B, C",
"rows": "1, 2, 3",
"borders": "all"
}
Supported values:
| Value | Description |
|---|---|
"horizontal" | Row separators only (default) |
"all" | Full grid with row and column borders |
"outer" | Border around the entire table only |
"none" | No borders at all |
Inline Colors
Apply color to inline text spans using the {#hex|text} syntax. Works in paragraphs, list items, headings, and table cells.
// Builder API
doc.paragraph("Status: {#e74c3c|Overdue} — needs attention")
// JSON Schema
{
"type": "paragraph",
"text": "Status: {#e74c3c|Overdue} — needs attention"
}
The hex value must be a 6-digit color code preceded by #. Inline colors can be combined with bold, italic, and underline formatting.
Superscript & Subscript
Render superscript and subscript text using caret and tilde syntax:
// Builder API
doc.paragraph("E = mc^{2}")
doc.paragraph("H~{2}O is water")
// JSON Schema
{
"type": "paragraph",
"text": "E = mc^{2}"
}
{
"type": "paragraph",
"text": "H~{2}O is water"
}
| Syntax | Result | Description |
|---|---|---|
^{text} | superscript | Renders at 70% font size, raised above the baseline |
~{text} | subscript | Renders at 70% font size, lowered below the baseline |
Table of Contents
Automatically generate a table of contents from all heading blocks in the document. The TOC includes heading text, dot leaders, and page numbers.
// Builder API
doc.tableOfContents({
title: "Contents",
maxLevel: 2
})
// JSON Schema
{
"type": "toc",
"title": "Contents",
"maxLevel": 2
}
The maxLevel property controls which heading levels are included (1 = H1 only, 2 = H1 and H2, 3 = all). Defaults to 3. The TOC block should typically be placed near the beginning of the document. Page numbers are calculated during the flow engine pass and update automatically.
Bookmarks / Outline
When building a PDF, the build() method now returns an outline tree alongside the PDF bytes. This outline provides the bookmark structure for PDF readers' navigation panels.
// Builder API
const { pdfBytes, outline } = await doc.build()
// The outline array looks like:
[
{ title: "Introduction", page: 0, children: [] },
{ title: "Chapter 1", page: 1, children: [
{ title: "Section 1.1", page: 1, children: [] },
{ title: "Section 1.2", page: 2, children: [] }
]},
{ title: "Chapter 2", page: 4, children: [] }
]
// JSON Schema — outline is auto-generated from headings,
// no schema property needed. Use metadata to control:
{
"metadata": {
"outline": true
}
}
The outline is built from heading blocks: H1 becomes a top-level bookmark, H2 nests under the preceding H1, and H3 nests under the preceding H2. Set metadata.outline to false to disable.
Footnotes
Add footnotes to any text content using bracket syntax. Footnotes are collected and rendered at the bottom of the page where they are referenced.
// Builder API
doc.paragraph("TangentFlow uses Pretext[^1] for text measurement.")
doc.footnote(1, "Pretext is a text layout library by Cheng Lou.")
// JSON Schema
{
"type": "paragraph",
"text": "TangentFlow uses Pretext[^1] for text measurement."
}
{
"type": "footnote",
"id": 1,
"text": "Pretext is a text layout library by Cheng Lou."
}
Footnote markers render as superscript numbers in the body text. The footnote text appears at the bottom of the page, separated by a short rule. If a footnote's reference and content appear on different pages, the footnote is placed on the page containing the reference.
Page Break Control
Use the breakBefore property on any block to force it onto a new page without inserting a separate page-break block. This is useful for ensuring sections always start on a fresh page.
// Builder API
doc.heading("Chapter 2", { level: 1, breakBefore: true })
// JSON Schema
{
"type": "heading",
"text": "Chapter 2",
"level": 1,
"breakBefore": true
}
When breakBefore is true, the flow engine inserts a page break immediately before laying out the block. This differs from the page-break block type in that it is attached to a content block rather than being a standalone element. Any block type supports breakBefore.
JSON Schema
Every TangentFlow document is represented as a JSON array of blocks. This schema is what the future @tangentflow/core npm library will accept as input.
{
"page": {
"size": "a4",
"orientation": "portrait",
"margin": 60
},
"style": {
"heading": "#1a1820",
"body": "#404050",
"accent": "#5c7a64",
"tableHeader": "#ebebf0",
"tableStripe": "#f8f8fa",
"divider": "#cccccc",
"quoteBar": "#5c7a64",
"statBg": "#f0f0f5",
"muted": "#666670"
},
"metadata": {
"title": "Document Title",
"author": "Author Name",
"subject": "Subject"
},
"watermark": {
"text": "DRAFT",
"color": "#cccccc",
"opacity": 0.15
},
"headerFooter": {
"logoSrc": "data:image/png;base64,...",
"headerLeft": "Company Name",
"headerRight": "Document Title",
"footerLeft": "Confidential",
"footerRightMode": "page-number"
},
"blocks": [
{ "type": "heading", "text": "Title", "level": 1 },
{ "type": "paragraph", "text": "Body text with **bold** and *italic*." },
{ "type": "table", "headers": "A, B, C", "rows": "1, 2, 3\n4, 5, 6" }
]
}
Pretext API
TangentFlow uses two core functions from @chenglou/pretext:
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
// Phase 1: Prepare (measure glyphs, segment text)
const prepared = prepareWithSegments(text, '11px Helvetica')
// Phase 2: Layout (calculate line breaks)
const result = layoutWithLines(prepared, maxWidth, lineHeight)
// Result:
// {
// lineCount: 3,
// height: 49.5,
// lines: [
// { text: 'First line of text', width: 142.5, start: {...}, end: {...} },
// { text: 'second line wraps here', width: 168.3, start: {...}, end: {...} },
// { text: 'final line.', width: 72.1, start: {...}, end: {...} }
// ]
// }
The two-phase design means you can re-layout at different widths (e.g. when columns resize) without re-measuring the text. Measurement is the expensive operation (~19ms for 500 texts); layout is pure arithmetic (~0.09ms).
pdf-lib Integration
The flow engine produces an array of draw commands that map directly to pdf-lib operations:
// Draw commands produced by the flow engine:
{ type: 'text', text: 'Hello', x: 60, y: 780, fontSize: 11,
fontKey: 'regular', color: [0.25, 0.25, 0.3] }
{ type: 'rect', x: 60, y: 700, w: 475, h: 24,
color: [0.92, 0.92, 0.95] }
{ type: 'line', x1: 60, y1: 700, x2: 535, y2: 700,
color: [0.85, 0.85, 0.88] }
{ type: 'image', src: 'data:...', x: 60, y: 600,
w: 200, h: 150 }
// These are rendered identically to:
// 1. Canvas (preview) — using ctx.fillText, ctx.fillRect, etc.
// 2. pdf-lib (export) — using page.drawText, page.drawRectangle, etc.
This architecture ensures pixel-perfect consistency between the preview and the exported PDF.
Built-in Templates
TangentFlow includes 9 pre-built templates that showcase different block combinations:
| Template | Blocks Used | Best For |
|---|---|---|
| Report | Headings, paragraphs, tables, bullet & numbered lists, quotes, two-column, stat cards | Quarterly reports, performance reviews |
| Invoice | Stat row, key-value, tables, quotes | Billing, invoices, payment receipts |
| Catalog | Multiple tables with long descriptions | Product listings, inventory sheets |
| Resume | Key-value, bullet lists, two-column, dividers | CVs, professional profiles |
| Proposal | Numbered lists, tables, stat cards, quotes, two-column signatures | Project proposals, SOWs, bids |
| Meeting Notes | Two-column attendees, numbered agenda, action items table, quotes | Meeting minutes, standup notes |
| Receipt | Stat row, key-value, tables, two-column payment details | Transaction receipts, order confirmations |
| NDA | Paragraphs, bullet & numbered lists, two-column signature blocks | Legal agreements, contracts |
| Multilingual | Paragraphs with mixed scripts | Testing Pretext's Unicode support |
Ready to build your first document?
Launch TangentFlow