Write on paper: a sheet per page, with a gap between them

The writing surface was a column of text in an empty pane. A sheet was
drawn behind it, but only once you started drawing — so writing looked
like text floating in the middle of nothing, and the space either side of
the column read as a void rather than as a margin.

Now every page is a sheet of its own, centred, with a gap after it: the
same way the PDF reader draws its pages, because a page in a note and a
page in a document should read as the same kind of thing.

The gap is the part that takes work. The browser knows nothing about the
sheets and lays the text out as one flow, putting a line straight across
the gap and cutting it in half. So editor/pagination.js measures the flow
and pushes any block that would straddle a gap onto the next page, with a
spacer of exactly the right height in front of it. A paragraph moves down
entire, which is what a page break means on paper. A block taller than a
page is left to run over, since splitting one would mean laying out its
insides ourselves.

The spacers are decorations, not content: they never enter the document,
never reach the server and never appear in an export. Two checks hold
that down — one asks whether any block sits in a gap, the other reads the
stored body back and looks for them there.

Two things that are easy to get wrong. The spacer's height is not the
distance the text moves: margins that used to collapse into each other no
longer do with something between them, so the layout gains that much for
free and the spacer is made exactly that much shorter. And heights change
with no transaction to announce it — a webfont landing, an image
arriving, the window resizing — which a ResizeObserver on the editor
element catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-11 20:17:28 +02:00
1 parent 3ce867c6d0
commit 97a3764d92
9 files changed
+492 -47

No files matched your search

+44 -6
View File
@@ -391,13 +391,51 @@ own notes are set should follow you to the phone rather than be made again on
every device. The server accepts those three values and drops anything else —
that column is written by a browser, so it is an input like any other.
**You write on paper.** Each page is a sheet of its own, centred, with a gap
after it — the same way the PDF reader draws its pages, because a page in a note
and a page in a document should read as the same kind of thing. It also answers
what a bare column of text could not: the space either side of the writing is the
sheet's margin, rather than an unexplained void.
The sheet used to appear only once you started drawing, which made the writing
mode look like text floating in an empty pane.
**Pages are counted, not stored.** The toolbar says *Page 2 of 4* in the same
words the PDF reader uses, and a dashed rule marks where each one ends. A page is
the same 3:4 the canvas uses, so "page 2" means the same thing writing or
drawing. The note itself stays one continuous document — Markdown has no page,
and a test asserts none ever reaches the stored body. The current page is the one
under the *middle* of the window: measuring from the top said "page 1" while you
stood at the foot of a two-page note, because a window is shorter than a page.
words the PDF reader uses. A page is the same 3:4 the canvas uses, so "page 2"
means the same thing writing or drawing. The note itself stays one continuous
document — Markdown has no page, and a test asserts none ever reaches the stored
body. The current page is the one under the *middle* of the window: measuring
from the top said "page 1" while you stood at the foot of a two-page note,
because a window is shorter than a page.
### Holding the gap open
The browser knows nothing about the sheets. Left alone it lays the text out as
one continuous flow and puts a line straight across the gap, cut in half by it.
So [`editor/pagination.js`](frontend/src/editor/pagination.js) measures the flow
and pushes any block that would straddle a gap onto the next page, by inserting a
spacer of exactly the right height in front of it. The spacers are **decorations,
not content**: they never enter the document, never reach the server and never
appear in an export. Two tests hold that down — one asks whether any block sits
in a gap, the other reads the stored body back and looks for them.
The unit is a whole block. A paragraph that would be split moves down entire,
which is what a page break means on paper. A single block taller than a page — a
long table, a tall image — is left where it is and runs over the gap: splitting
one would mean laying out its insides ourselves.
Two details that are easy to get wrong and were:
- **The spacer's height is not the distance the text moves.** Margins that used
to collapse into each other no longer do once something sits between them, so
the layout gains `min(marginBottom, marginTop)` for free and the spacer is made
exactly that much shorter. The spacer carries the intended shift in a data
attribute, because the next measurement has to subtract it again to recover
where the text would sit with no spacers at all.
- **Heights change without a transaction to announce it** — a webfont finishing
loading, an image arriving, the window resizing. A `ResizeObserver` on the
editor element catches all three.
Each page can be drawn on — see [One drawing, as many pages as the
note](#one-drawing-as-many-pages-as-the-note).