Core concepts
The document model, where a style actually comes from, and why the canvas and the PDF can never disagree.
Rows, columns, blocks
Three levels. That is the entire layout model, and it does not get more complicated later.
A row spans the printable width, and its columns split that width. To put a logo left and an address right, you make one row with two columns.
Column.block, singular.
Not a list. To stack two things vertically you use two rows, not two blocks in one
column. This is the single thing people get wrong when they build a document in code.
width is a string — but the editor offers
1, 2 or 3 equal shares. Four-, six- and twelve-column rows and asymmetric splits are planned;
do not hand-craft JSON that relies on them yet.
Header, footer and cover page
These are not special block slots — they are the same rows-and-columns structure again. That is deliberate: a header with a logo left and document details right is authored exactly the way the body is, with the same blocks and the same drag-and-drop.
They collapse to null when emptied, so an empty band never reserves space on the
page. Whether they print is a page setting, not a document-structure question — see
showHeader and headerPages below.
What a block is
content is what the block says. style, layoutId and
overrides are how it looks. Applying a design changes presentation only — the
words your user typed are never touched. That guarantee is why a user can try 14 designs on a
paragraph without fear.
Page behaviour
Optional per-block hints controlling how it meets a page break. Absent means "flow normally",
and only flags set to true are stored, so saved JSON stays small.
keepTogether | Never split this block across two pages. |
keepWithNext | Keep it on the same page as the block after it — a heading with its paragraph. |
pageBreakBefore / pageBreakAfter | Force a new page either side. |
avoidBreakInside | Prefer not to split, but allow it rather than leave a large gap. |
These live in a small "Page behaviour" group in the inspector rather than in the main list, because most blocks never need them.
Where a style comes from
When a block is blue, four different things could have made it blue. They are stacked, and the one nearest the top wins.
Think of it like getting dressed. The document sets the dress code, the block type has its usual outfit, a design changes that outfit, and the user can still change one button by hand. The hand change is the one you end up seeing.
The document says text is dark grey.
A heading block normally uses the accent colour, so it is blue.
The user picks the "Dark band" design — now white on navy.
Then the user hand-picks yellow in the Style tab.
You see yellow. And if they switch design again, it stays yellow — a hand change outranks a design.
In code, that stack is one merge:
Two details for later: merging goes into nested values, so setting only the top padding leaves the other three sides inherited. Lists and single values replace completely. And blocks receive the finished style — they never work it out themselves.
Design tokens
Document-wide defaults. Change one and every block that has not overridden it follows — the fastest way to make a document feel like a brand.
| Token | What it does |
|---|---|
primaryColor | Accent — headings, rules, table header bands. |
secondaryColor | Supporting accent. |
textColor | Body text. |
fontFamily | Default typeface. A block only stores a font when it deliberately differs. |
baseFontSize | The size everything else is relative to. |
padding | Default inner spacing, as a four-sided object. |
spacing | The rhythm between rows. |
locale | BCP 47 tag — how dates, numbers and money are written. |
currency | ISO 4217 code. Stored as a code, never a symbol. |
currencyDisplay | symbol, narrowSymbol, code or name. |
minimumFractionDigitsmaximumFractionDigits | Override the currency's normal decimal rules. |
useGrouping | Thousands separators on or off. |
Page settings
| Setting | Values |
|---|---|
size | A4, Letter or Legal. |
orientation | portrait | landscape |
margin | Physical margins in millimetres — this is what the PDF uses. |
showHeader / showFooter / showCoverPage | Whether each band prints at all. |
headerPages | every | except-first | first-only |
showPageNumbers | With pageNumberPosition (header/footer), pageNumberAlign (start/center/end) and pageNumberFormat. |
pageNumberFormat | A template — 'Page {page} of {pages}'. |
backgroundColor / backgroundImagePath | One visual layer: the colour paints under the image. |
backgroundPages / watermarkPages | all | cover | body |
How pages are decided
The canvas measures each rendered row in the browser and feeds those real numbers to a pure pagination function. The chrome-free preview calls the same function with the same measurements.
The practical consequence: pagination depends on a browser actually laying the elements out. That is why the builder is client-side only, and why server-side rendering it does not work.
Versioning & migration
Every saved document carries a number called version. It is worth understanding,
because two of the rules below look arbitrary until you know what it is for.
The format has changed twice
| Version | What that shape looked like |
|---|---|
| 1 | The original. header, footer and coverPage each held one single block. |
| 2 | Those became regions with rows and columns — so a header could finally have a logo on the left and document details on the right, instead of just one thing. |
| 3 today | headerFirstPageOnly (a yes/no) became headerPages: 'every' | 'except-first' | 'first-only', because two options were not enough. backgroundPages and watermarkPages were added. |
What happens when an old document loads
They open an invoice they made a year ago.
It opens. Everything is where they left it.
They never learn any of this happened — which is the point.
The two rules, and why they exist
version by hand.
Change a 1 to a 3 and the package believes the document is already
up to date, so it skips both upgrade steps. You are left with a version-1
header being read by version-3 code — and the header breaks or disappears. The number is a
statement of fact, not a setting.
headerFirstPageOnly is gone, so version 3
cannot work out whether the header should print on the first page. Save the whole object,
unknown fields and all.
If you want to run it yourself
Rarely needed — the builder does this on load — but it is available:
A document with no version field at all is treated as version 1, not as broken —
so documents from the earliest days still open.