Commit Graph
6 Commits
Author SHA1 Message Date
marcandClaude Opus 5 011ba82a94 Write pinned notes in the margin, and give the phone a sheet instead
A note about a passage belongs beside the passage, not on top of it. Only a
numbered marker stays on the page now, with a hairline out to a card in the
margin, level with its anchor. Cards keep one height so two notes close together
can be laid out by pushing the second down; the one being written in grows and
rises above the rest.

A phone has no margin to write in — a page narrow enough to read leaves no white
space beside it, and taking a column would make the document unreadable. There
the note comes up from the bottom instead: over the page, only while open, within
reach of a thumb, with the passage still visible above it.

Reserving the column exposed a loop worth remembering. A flex item is as wide as
its content unless told otherwise, so the column made the viewer wider, which
made there be room for the column: the page ended up at 254px inside a 420px
phone while the layout insisted it had 611px. The viewer and the editor pane now
say minWidth: 0, and the toolbar scrolls rather than cutting its last controls
off on a narrow screen.

Splitting each row into page and margin also broke which pages counted as
visible — the check read data-pdf-page off the row, found nothing, and cancelled
every text layer before it rendered. It reads the row's own attribute now.

215 unit tests, 219 browser checks across eleven suites, including the phone
layout at 420px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:46:56 +02:00
marcandClaude Opus 5 6438648313 Pin notes to passages in a PDF, and put the notes control with the rest
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>
2026-08-10 13:08:20 +02:00
marcandClaude Opus 5 0a0864c58b Sort a notebook, import Markdown back, and stop the page shrinking in Draw
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>
2026-08-09 01:38:03 +02:00
marcandClaude Opus 5 b12b0d6142 Select text in a PDF, and find PDFs by their contents
Stage five. pdf.js draws pages as pictures, so selecting means laying
transparent spans over them exactly where each word was painted. The
layer carries the same scale as the canvas — they drift apart otherwise
and a selection lands on the wrong words. While drawing it stops taking
pointer events entirely, or a stroke would come out as a selection.

The text is also extracted and handed to the index that already holds the
notes, so one search box finds a sentence whether it was typed or
imported. Extraction runs after the document is on screen and yields
between pages: it must never stand between the reader and the first page.

The extracted text is deliberately not sent to the server. It is
derivable from a file the server already holds, so storing it again costs
space and a second thing to encrypt for nothing gained. The price, stated
plainly in the readme, is that a PDF becomes searchable on a device once
that device has opened it — the same shape as the limitation notes have.

Text runs are joined on pdf.js's hasEOL rather than concatenated. Without
it the last word of a line glues to the first of the next and matches
nothing when searched for.

The minimal text-layer CSS is written out rather than importing
pdfjs-dist/web/pdf_viewer.css, which carries the whole reference viewer's
toolbars and dialogs.

Suite now checks the layer sits over the page rather than stacked in a
corner, that the text is transparent, that dragging selects real words,
that a word from inside the file finds the note and one that is not in it
finds nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 21:12:06 +02:00
marcandClaude Opus 5 1be3a9b9ba Draw on PDF pages
Stage four. Strokes are kept per page in that page's own coordinate
space — PDF points at scale 1 — not in the fixed 1200x1600 space a note's
drawing uses. A PDF cannot use one space: every page has its own size,
and a stroke belongs to the page it was drawn on.

That is what keeps a mark on the word it was drawn over at any zoom and
on any screen, and it is the only form that can be stamped back into the
file in stage seven, where page points are the only coordinates that mean
anything. Measured rather than assumed: zooming moves a stroke by 0.001
of the page.

Pen sizes are scaled by the page width. The sizes are calibrated for the
1200-unit note page, so on a 595-point A4 page the same number would have
drawn twice as thick.

Reading and drawing are separate modes. While reading the ink layer takes
no pointer events, so scrolling and selecting text reach the page beneath
it; while drawing, a finger still scrolls and only a pen or mouse marks,
because a touch-drag on a phone means scroll far more often than it means
draw. The drawing can be hidden without being erased, for reading the
original clean.

The server accepted only the single-page shape; it now takes either and
counts strokes across pages for its limit.

Carried over from notes: the save token guard, so the six-second poller
adopting a response issued before the local save cannot undo what was
just drawn. The suite waits nine seconds and checks the ink is still
there — that bug has cost strokes twice before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:54:10 +02:00
marcandClaude Opus 5 2986bcb620 Read imported PDFs in the app
Stage three: the viewer. Drawing on pages comes next.

pdf.js stays out of the bundle everyone downloads — it is larger than the
rest of the application put together, so it is behind a dynamic import and
a session that never opens a PDF never fetches it. Main bundle grew by
8 kB for the viewer shell; the 162 kB library arrives only when needed.
The worker is bundled rather than pulled from a CDN, because a viewer
that stops working during someone else's outage is not one you can rely
on offline.

Two things that only showed up once it ran in a browser:

- pdf.js 6 calls Uint8Array.prototype.toHex, which only very recent
  browsers have. Anything older failed with "n.toHex is not a function",
  which says nothing about the cause. Now on the legacy build, which
  compiles those away — a little larger, and the difference between
  working everywhere and working on this year's browsers.
- nginx has no MIME type for .mjs and served the worker as
  octet-stream, which a browser refuses to run as a module.

Pages are laid out at their true proportions before anything is drawn, so
the scrollbar is honest immediately and a document mixing portrait and
landscape keeps both — the first version scaled every page to the widest
one, which the suite caught by checking the two aspect ratios differ.

Bytes come from the authenticated attachment route and are cached in
IndexedDB. Content-addressed storage means a cached copy can never be
stale, so a document opened once needs no connection again. Verified with
the network switched off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:36:04 +02:00