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

  1. Open the Document Builder
  2. Choose a template (Report, Invoice, Resume, etc.) or start from scratch
  3. Add content blocks using the + buttons in the block bar
  4. Click any block to expand its inline editor
  5. The canvas preview updates in real-time as you edit
  6. 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.

LevelFont SizeUse Case
H122ptDocument title
H216ptSection heading
H313ptSubsection 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:

SyntaxResultPDF Font
**bold text**bold textHelvetica Bold
*italic text*italic textHelvetica Oblique
__underlined__underlinedHelvetica + underline
[link](url)linkAccent 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.

Configure repeating headers and footers that appear on every page of the document.

ElementDescription
Header LogoUpload a PNG/JPEG image displayed at the top-left of every page
Header LeftText next to the logo (e.g. company name)
Header RightRight-aligned text (e.g. document title, date)
Footer LeftLeft-aligned text (e.g. "Confidential")
Footer RightAuto 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:

ColorAffects
HeadingH1/H2/H3 text, table header text, stat card values
Body TextParagraphs, list items, table cells, two-column text, quote text
AccentLink text color
Table HeaderBackground fill of table header rows
Table StripeBackground fill of alternating table rows
DividerHorizontal rules, table borders, key-value separators
Quote BarLeft accent bar on quote/callout blocks
Stat Card BGBackground fill of stat row cards
Muted TextBullet 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

OptionValues
SizeA4 (595 x 842pt), Letter (612 x 792pt), Legal (612 x 1008pt)
OrientationPortrait or Landscape (swaps width/height)
MarginNarrow (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:

ValueDescription
"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"
}
SyntaxResultDescription
^{text}superscriptRenders at 70% font size, raised above the baseline
~{text}subscriptRenders 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:

TemplateBlocks UsedBest For
ReportHeadings, paragraphs, tables, bullet & numbered lists, quotes, two-column, stat cardsQuarterly reports, performance reviews
InvoiceStat row, key-value, tables, quotesBilling, invoices, payment receipts
CatalogMultiple tables with long descriptionsProduct listings, inventory sheets
ResumeKey-value, bullet lists, two-column, dividersCVs, professional profiles
ProposalNumbered lists, tables, stat cards, quotes, two-column signaturesProject proposals, SOWs, bids
Meeting NotesTwo-column attendees, numbered agenda, action items table, quotesMeeting minutes, standup notes
ReceiptStat row, key-value, tables, two-column payment detailsTransaction receipts, order confirmations
NDAParagraphs, bullet & numbered lists, two-column signature blocksLegal agreements, contracts
MultilingualParagraphs with mixed scriptsTesting Pretext's Unicode support

Ready to build your first document?

Launch TangentFlow