The column sat in the middle, so it moved as the window was resized and the
start of every line landed somewhere new. It is against the margin now.
The toolbar counts pages in the words the PDF reader uses, with a hairline where
each ends. Pages are drawn, never stored: the note stays one document, Markdown
has no page, and a check asserts none reaches the stored body. The current page
is the one under the middle of the window — measuring from the top claimed "page
1" while you stood at the foot of a two-page note, because a window is shorter
than a page.
The typeface is a per-account setting rather than a per-browser one, because a
choice about how your own notes are set should follow you to the phone. The
server takes serif, sans or mono and drops anything else; that column is written
by a browser, so it is an input like any other.
Alignment is a small extension of our own — one attribute, a parse rule, a render
rule — so the schema every stored note is validated against stays in files this
project owns. Markdown has no alignment: what CommonMark allows is embedded HTML,
so non-left blocks travel as `<div align="…">`, the one form most readers still
honour, and the importer puts it back on the paragraphs. Left is never written.
The full run caught a crash I had put in: PdfWorkspace used `noteFont` without
taking it as a prop, which the build cannot see and the type checker does not
reach in .jsx. It killed the notes panel beside a PDF, and `offline` failed with
it because the whole app was broken.
224 unit tests, 263 browser checks across twelve suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was a page-shaped box that was not page-coloured: the same fill as everything
around it with a hairline border, so it read as an empty panel. Now it is warm
off-white in daylight and a raised surface at night — deliberately not a white
rectangle, since a page that glows brighter than the room is what makes an app
unusable in the evening.
The line ran to 84 characters, past the width at which the eye reliably finds the
start of the next line, and that was the single biggest reason the surface read
badly. Measured, not guessed: 620px of sheet with a 56px margin gives about 73.
It is centred now — it used to sit 28px from the left and stop short of the
right — with almost square corners instead of a dialog's 14px radius.
Blocks on the sheet are tinted from paper rather than from the app background,
or they read as sitting underneath it.
Five German strings survived the earlier pass, "Alle Notizen exportieren" among
them: that sweep filtered on umlauts, so sentences without one went straight
through. Swept again by word.
218 unit tests, 244 browser checks across eleven suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interface had drifted into two languages — the frame in English, everything
added later in German — which is worse than either, since the same idea appeared
under two names depending on when that part was written. Sorting by title still
uses the German collation, so Übung 2 comes before Übung 10: that is about the
notes, not the chrome.
Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ →
drawings/). The importer reads both spellings, and both the old and new marker
line for a drawing — archives exported before this are on disk, and an import
that quietly dropped their drawings would be the worst kind of tidy-up.
Making a note, uploading a PDF and importing Markdown were three buttons side by
side, two of them icons you had to hover to identify. They are the same act —
putting something new in this notebook — so they are now one + menu.
The search box moves out of the sidebar and into the note list, labelled with the
notebook it narrows. Nothing changed about what it does: it has always searched
only the notebook you are in, but from the sidebar it read as searching
everything.
A separator no longer carries a sentence explaining the order the sort menu
already names. Renaming joins removing on the right-click menu — a left-click on
the label started an edit, so the row could not be touched without falling into
one.
218 unit tests, 238 browser checks across eleven suites. Two of those caught real
breakage from this change rather than needing a translation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel beside a PDF is for notes about the document; a note that belongs to
one paragraph is a different thing, and one that has to begin "on page 14, third
paragraph" has already lost its place. Click Randnotiz, click the passage, write.
Pins ride in the same per-page overlay as the drawings, in PDF points, so one
stays on its sentence at every zoom and in the exported file. Living outside the
note body has consequences that each needed handling: nothing else would index
them for search, so the viewer feeds them in with their page numbers; clearing a
page's ink must not take them; and "is this overlay empty?" was answered by
counting strokes — in the client *and* independently in the server, so an
overlay holding only pinned notes was stored as NULL and lost every note on it.
They are stamped into the annotated export as a numbered marker plus a box of
text, on whichever side of the pin has room. Standard PDF fonts are Latin-1, so
characters outside it are replaced rather than failing the whole export.
The Notizen control moves into the viewer's toolbar. A second row for one button
cost reading height on every screen and put one of the document's controls
somewhere none of the others were. On a phone, where the viewer is not on screen
while the notes are, the way back sits in the panel.
Renaming a separator no longer shifts the row: the two rules always share what is
left over equally and the middle keeps one width in both states.
215 unit tests, 206 browser checks across eleven suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The paper measurably shrank on switching to Draw — 733px to 460px on the same
screen. The editor was a flex item at its default `flex: 0 1 auto`, so its width
was its widest row's: the toolbar. Swapping a dozen formatting buttons for six
drawing ones collapsed it. It now fills the pane, so the width is a property of
the window.
Each notebook remembers how its notes are ordered, on the server rather than per
device. The manual order has to survive being looked away from, so `reindex`
renumbers every note in the notebook rather than the filtered view, and dragging
is switched off while a search narrows the list. Reordering has its own route:
`PUT /notes/:id` would bump `updated_at` and snapshot a version, so a drag would
move a note to the top of "last changed" and spend a slot of its history.
Markdown and our own ZIP archives come back as notes, with their images
re-uploaded as attachments. A drawing returns as the PNG it was exported as —
vectors are not recoverable from pixels — and the import says so rather than
leaving it to be discovered. Writing the parser turned up a real bug in the
exporter: blocks were emitted with no blank line between them, so a list after a
list became one list and a quote after an item was read as part of it.
Creating or importing from "All Notes" now asks which notebook instead of
silently taking the first one. The web-link prompt is a real dialog: a bare
domain becomes https, javascript: is refused with a reason. The PDF viewer's
hide-drawing control is a labelled button rather than a filled-or-hollow dot,
and the export icon points down into a tray like every other download.
186 unit tests, 179 browser checks across eleven suites, including a new
`offline` suite that loads the app cold with the network off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage seven, and the last of the PDF work. A PDF note now offers the
document with your marks stamped in, the untouched original, or just the
notes written beside it.
pdf.js only reads, so writing needs pdf-lib — behind a dynamic import
like pdf.js, so the bundle everyone downloads carries no PDF writer for
the sake of people who never export one. Main bundle grew 6 kB; the
180 kB writer arrives only when used.
The coordinate decision from stage four is what makes this a translation
rather than a reconstruction: strokes were stored in page points from the
start, the only space a PDF understands, so nothing has to be guessed
back from screen pixels. Only the origin moves, PDF putting it at the
bottom-left.
The stored file is never modified — pdf-lib builds a new document — so a
clean copy stays available however often it has been drawn on. The suite
re-fetches the stored bytes after exporting and compares them, and checks
the plain export is byte-identical to what was imported.
Eraser strokes are dropped. On screen they remove ink by drawing in
destination-out; a PDF content stream has no equivalent, and stamping
them would put grey scribbles exactly where something had been rubbed
out.
The suite renders both files with pdf.js and counts dark pixels, 1633
against 2557. A larger file only proves bytes were added; this proves ink
can be seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two questions instead of one, because the answers depend on each other:
Markdown holds text only, a PNG holds the drawing only, PDF holds both.
Listing every format flat would have offered combinations that cannot
exist. A note without a drawing skips the first step rather than showing
it mostly greyed out.
While testing that, the drawings turned out never to have made it into
any export at all — not the ZIP, not the PDF. strokeDocOf() *builds* a
stroke document from a bare array, and it was being handed one that was
already a document, so the strokes ended up nested one level too deep and
every length check read undefined. Nothing complained; the export simply
came out without the drawing. It went unnoticed because the notes the
earlier suite exported had none.
Now uses docStrokes(), and the suite draws on a note before exporting it,
checks the PNG's magic bytes, and checks it is 159 KB rather than an empty
white page — a check on the file merely existing would have passed against
the broken version too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reason to build this is not a missing feature — it is that trusting a
program you maintain alone with years of notes should not mean the notes
can only ever be read by that program.
Reachable from a note's toolbar, a notebook's context menu, and Account →
Alle Notizen exportieren.
Markdown carries over everything with an equivalent: headings, lists,
quotes, code, tables, and task state as `- [x]`. A link to another note in
the same export becomes a relative link to its file; a link to a note
outside it degrades to the plain title rather than leaving a dead
reference. Notes with images or a drawing arrive as a ZIP so the files
travel along, and a multi-note export gets a README index.
PDF goes through the browser's print dialog rather than a bundled PDF
library — the browser already has a good one, and it is the only format
where drawings sit inside the document. Markdown gets them as a PNG
beside the text, with a line saying so, because dropping them silently
would be the wrong kind of quiet.
Two decisions worth recording:
- the ZIP writer is eighty lines here rather than a dependency. Note text
is small and images are already compressed, so compression bought
nothing, and stored entries are a handful of fixed-layout records.
- Markdown escaping only covers what changes meaning mid-line, plus a
leading list or heading marker. Escaping every character Markdown ever
uses turns a sentence into `Hallo\. Alles gut\!`, which defeats the
point of choosing a readable format.
42 unit tests on the serialiser and the archive format, plus a browser
suite that unpacks a real download and reads the bytes back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>