Commit Graph
85 Commits
Author SHA1 Message Date
marcandClaude Opus 5 641acaae40 Three things that only showed on screen
Grey letters ran down the bar while dragging a column edge. The editor marks
the column under the pointer by putting a small div in each of its cells;
drawn along the cell's right edge, that absolutely positioned child sent
Chromium down another painting path and the cell's own text came out a second
time, squeezed into the two pixels under the bar, one letter to a line. It
appeared with the bar and went with it, which named the culprit -- measured,
there was no text there at all, only three empty markers, and a forced repaint
did not clear it. The marker is display:none now and only says which cells the
column is made of; the line is an inset shadow on those cells, matched with
:has(). Two suspects were tried and reverted first: the table as its own paint
region, and separate borders. Neither changed anything, so neither stayed.

Opening a note flashed a long narrow page. The drawing stage starts at the
canonical page, 1600px tall, and the measurement that fits it to the pane ran
as an ordinary effect -- a frame too late. useLayoutEffect runs between the DOM
being built and the browser painting it, which is exactly the gap that flash
lived in. The test samples the sheet's height every frame across a note switch,
because one frame is all it was.

The mark above a note did not stand in the same column as the tools below it.
Vertically it was already exact -- measured, symbol centre 20, cap centre 20 --
but it began six pixels right of the first tool, and the title thirty. The
mark's box is only as wide as the mark now, so the mark lands on that column,
and the title follows nine pixels closer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:14:38 +02:00
marcandClaude Opus 5 1c0f2184e6 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:45:55 +02:00
marcandClaude Opus 5 3b7958b713 A chapter with an order of its own
A chapter orders its own notes now, chosen in the same dialog as its name,
mark and colour -- one decision about one thing. The notebook's setting
governs the notebook's own list; a chapter is a place someone made and
filled, so a reference chapter can stay alphabetical while the notebook
around it stays as it was arranged. It starts hand-made, and a chapter put
in one of the other orders loses its lines and its handles exactly as the
notebook does: neither means anything against an order the app is choosing.

A line still would not go under another line, and the comparator was only
half of it. The write was then thrown away by the "did anything change?"
test, which sorted the separators before comparing them -- to ignore the
other chapters' lines riding along in the same field -- and a sorted
comparison cannot see a sequence. Two lines in one gap carry the same
position, so swapping them changes nothing else. It compares this list's own
lines, in order.

An open chapter swelled while rearranging: the strips at the two ends of its
list stood open for the whole of the mode, adding half a finger of empty
space inside every chapter. They are thin now, and only there while
something is actually being carried -- which is also why the suite presses
first and aims afterwards.

Server: sort_mode on chapters, validated against the same five modes as a
notebook's, and set outside the COALESCE so null means the hand-made order
rather than "leave it alone".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:45:05 +02:00
marcandClaude Opus 5 ea1bca5d25 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:37:29 +02:00
marcandClaude Opus 5 f01522aa6f A chapter is a list too, and deleting stops moving things
Deleting a note moved everything else. A row leaving is the same problem as
a row arriving, in reverse: a line's position counts the rows above it, so a
note deleted above a line left the line counting rows that were no longer
there, and everything under it appeared to jump up past it. withoutRow
renumbers the list a row leaves, and every place one leaves says so -- the
bin, a move, a chapter being removed. Arrivals land at the top: an old
position belongs to an arrangement that has moved on.

A line could not be dragged under another line. Two lines in the same gap
carry the same position, and the comparator claimed "a separator comes
first" without asking what the other one was -- both directions at once for
that case. A comparator that contradicts itself has no defined result, so
the sort returned them in whichever order it happened to walk.

A chapter is always in the user's own order and can be arranged like any
other list: its notes wear handles under the same switch as the list beside
them and are dragged among themselves, strips at both ends included. The two
lists keep apart by scope -- a chapter unfolded under its row holds rows
inside a row of the notebook's list, so the nearest row above the pointer
cannot say which list a drag belongs to. The sort menu is gone inside a
chapter: the mode belongs to the notebook, and a menu there would offer a
choice that cannot be honoured.

The chapters suite no longer assumes the first chapter row is the one that
matters. A chapter is a row in the arrangement and can be anywhere in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:37:02 +02:00
marcandClaude Opus 5 312c4e190e Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:46:28 +02:00
marcandClaude Opus 5 a9391cdcf2 One direction to drop in, and a new note that stays on top
Every new note pulled the arrangement apart. A note was made with no
position and left to the tie-break, which put it near the top by age --
which is not the same thing. A separator's position counts the rows above
it, so a row appearing without a renumber left every line one row too
high. placeOnTop builds the order from the list *without* the new note and
puts it in front: measuring the room after the furniture has moved would
write the misreading back as the truth.

A drop now lands the row under the one it was released on, and only there.
Dropping above a row and dropping below the row before it are the same move
described twice, so every gap could be aimed at from two sides. The strip
above the first row is the one place a drop under something cannot reach.

A chapter could only be dropped into, never dragged past: taking the whole
row as a place quietly took reordering away. The bottom edge of its header
is the gap now, the rest of the row is still the chapter.

A link showed the sheet of paper whatever it pointed at. The @ menu dropped
the mark on the way into the link, and the refresh that keeps a link current
ran once, while the editor was still empty -- content is poured in afterwards
without an update event, so nothing ever looked again.

The mark in the title bar and the box in a task list were each centred on a
line box rather than on the letters in it, which is not the same as being on
one line. The mark shares the title's baseline; the box is centred on the
capitals and sized in em.

Move to... is divided under headings that are deliberately not shaped like
entries, and each row carries the mark its chapter or notebook wears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:45:16 +02:00
marcandClaude Opus 5 2ffa357dfc Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:23:56 +02:00
marcandClaude Opus 5 d68b0775c2 One dialog per thing, and rows dragged into a chapter
A separator had three menu entries — rename, colour, line — plus an input
that appeared in the row itself: three shapes of interaction for one small
thing, and none of them showed the result. Choosing "dotted" from a list
of words is a guess. It is one Edit now, and the dialog draws the line as
it will look with every shape drawn beside its name, from the very rules
the list draws with: a sample that can disagree with the result is worse
than none.

A chapter goes the same way — name, mark and colour in one dialog, the
same one whether it is being made or changed, with the row drawn at the
top. Asked from three entries, getting a new chapter to look right took
three visits. The name dialog it used is gone.

A row is put into a chapter by dragging it onto it: the chapter lights
up and takes what is dropped rather than growing a line above it, since
a chapter is a place and not a neighbour. Notes and separators both, and
the "+ Separator" inside a chapter goes with it — a second way to make
the same thing in a second place is a second thing to keep straight.

A link now wears the mark of the note it points at, in the text and in
the @ menu. It travels when the note's mark changes, as the label
already did.

Two alignment faults: a task list's checkbox was nudged down by a fixed
0.35em measured once against one font, so it floated above the line it
belonged to — it is centred in a box one line tall now. And the title
bar's mark was spaced like the buttons at the far end and grew with its
contents, so picking an emoji shifted the title sideways.

Fourteen suites green: 374 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:23:26 +02:00
marcandClaude Opus 5 59844046c4 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:23:48 +02:00
marcandClaude Opus 5 f1d3e38f8f Marks, lines, and a table that breaks
A long table lay straight across the gap between two sheets with a row
cut in half, and a shorter one jumped to the next sheet whole leaving
the rest of the first blank. Tables are broken between their rows now,
which is where a table breaks on paper. The spacer there is a tr with
one borderless cell: a div inside a tbody is not something the browser
keeps. Its own spacer rows are subtracted again on the next measurement
and counted into the shift every block below inherits — miss that and
everything under a broken table is measured a page too high.

A dialog closed in the middle of an edit if a selection was dragged past
its edge: the click lands on the nearest common ancestor of press and
release, which is the backdrop, and every dialog read that as "clicked
beside me". They remember where the press began now. All ten.

A note and a chapter can wear a mark of their own beside the name,
chosen from a short list searched by what a thing is for rather than
from the system emoji keyboard — a different thing on every machine, and
three thousand pictures for a decision that is "which of these is the
invoice notebook". A separator takes its own colour and one of five
shapes, and a chapter can be divided like the notebook it sits in: both
places a chapter is shown draw it with one component now, so it cannot
be divisible in the column and not in the unfolded block.

Also: a chapter's colour could not be cleared, because the update rode
on COALESCE and null there means "leave it alone". A table's header row
is legible on the dark theme. Three entries in the table's own menu were
still German. Opening a notebook opens its first note, on a wide screen
only — on a phone the list is the screen.

Fourteen suites green: 364 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:23:16 +02:00
marcandClaude Opus 5 0c312e53c2 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 21:45:36 +02:00
marcandClaude Opus 5 c96885d5b3 A notebook opens in its own order
The hand-made order is where a notebook's own arrangement lives:
separators show, rows can be put where they belong, nothing slides about
by itself. Starting in "last changed" made both of those look broken
until the sort menu was found — pressing "+ Separator" there drew a line
the order cannot display, so the button appeared dead. A notebook nobody
has arranged still reads newest-first, because untouched rows all carry
position 0, so nothing is lost by starting there. The views keep "last
changed": they are questions asked of every notebook at once and have
nowhere to keep an arrangement. The button that draws a line is now only
offered in the order that can show one.

Removing a chapter left you stuck in the confirmation. Every destructive
action goes through one helper that takes the question down again; this
one set the dialog directly and bypassed it, so the chapter was removed
and the prompt stayed over the app. Closing now happens where the dialog
is rendered, so it happens whoever asked and however they asked.

The suites did not catch that, and the reason is worth keeping: they
read the list straight through the overlay, which is in the DOM the
whole time, and passed while the screen was in fact stuck. Both now
assert the question is gone afterwards.

The separator is no wider than its name. It sat in a fixed 132px slot —
chosen so the row would not jump when renaming began — with a short name
floating in a gap it did not fill.

Thirteen suites green: 345 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 21:45:03 +02:00
marcandClaude Opus 5 cc0b58dd2f Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:29:35 +02:00
marcandClaude Opus 5 165ccf3cf7 A chapter you can see, in the colour you chose
A chapter did not show until the notebook had a note in it. "Nothing
here" was asked of the notes alone, so a notebook whose only contents
were chapters showed the empty-list panel with the chapters hidden
behind it — making a chapter in a fresh notebook appeared to do nothing
at all. It is asked of every row now, and a notebook with chapters but
no loose notes says so in one line rather than looking like a list that
failed to load.

The colour is chosen while the chapter is being made, in the dialog that
asks for its name. It is what tells one chapter from the next, and
having to right-click a fresh one to say which it is meant two visits
for one decision. Renaming and recolouring afterwards are one entry in
its menu and one write; the server already took the colour on create,
the client simply never sent it.

The word "here" is gone. The line along the top of the pane says which
of the two places you are in, and saying it twice was noise.

Thirteen suites green: 338 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:28:57 +02:00
marcandClaude Opus 5 ebf1f1bc62 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:14:04 +02:00
marcandClaude Opus 5 b2a5f051fe A filed note stays filed, and the handles are asked for
Three things that all came from chapters being thought through too late.

A note put into a chapter came back out of it a few seconds later. The
server had it right the whole time; every sync rebuilds the local copy
from the server's answer, and the function doing that never carried
chapter_id — so each sync handed back a note belonging to no chapter and
the notebook took it in again. The bin's query left the column out too,
which would have unfiled anything restored from it.

A separator drawn in a notebook vanished when a chapter was opened.
"Which chapter is open" and "which list is on screen" were one value.
Opened beside or below, the list still shows the notebook — but
everything around it had switched to the chapter, so the notebook's own
separators were filtered away and a drag among its rows was measured
against the chapter's notes, found nothing, and did nothing.

And where a new note lands is now visible and yours to choose: pressing
in either place makes it the one being worked in, marked with a line
along its top and the word "here", and the + button names it.

Rearranging becomes a mode, switched on beside the sort menu — until
then no row wears a grip and none can be dragged. Filing a note is back
in the row's own menu, where starring and exporting already are; the
button that had appeared in every row is gone with its dialog. The
account panel closes with Escape.

Thirteen suites green: 335 browser checks, 241 unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:59:29 +02:00
marcandClaude Opus 5 4bb86bc5ac Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:20:35 +02:00
marcandClaude Opus 5 d74fea4905 Chapters as rows, in two views, in their own colours
Three things Marc asked for, and one of them turned out to be the hinge
the other two hang from.

**A chapter is a row like any other.** Same grip, same drag, same place
in a hand-made arrangement as a note or a separator. That needed notes
and chapters numbered from *one* counter: numbered separately they are
two sequences that cannot be interleaved again, and a chapter dragged
between two notes springs back to the top. Positions therefore have to be
named rather than read off an array index, so POST /notes/reorder now
takes `positions: [{id, order}]` beside the older `ids`, and chapters
have a reorder route of their own.

**Two ways for a chapter to open**, chosen per account beside the
typeface, for the same reason that one lives there: it is a choice about
how your own notes are laid out and it should follow you to the phone.
Beside the list, its notes in a further column the way OneNote lays out
pages — the notebook's own list carrying on to the left, chapter rows and
all. Or unfolded under the chapter's own row, indented against a rule in
its colour. A phone has room for neither and fills the list instead, with
a way back, which is what it did before there was a choice.

**Its own colour**, from eight named ones rather than the full palette:
the point of chapters is that they are different places, and twenty
near-identical blues do not help anyone tell them apart.

The colour is validated on the way in — it is written by a browser, so
only a plain #rrggbb gets through and anything else becomes no colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:20:14 +02:00
marcandClaude Opus 5 6cad632a0c Record the digest of the image that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:44:13 +02:00
marcandClaude Opus 5 632ea5dcd9 Move a note in one press, and stop Draw inventing a page
Moving lived under a right-click, behind a submenu: two steps before the
list of places appeared at all, and on a phone a long press followed by a
flyout. There is a button on the row now, and it opens one screen with
the chapters of this notebook and the other notebooks together, marking
where the note already is.

The spare sheet in Draw mode was drawn as solid paper, so pressing Draw
on a one-page note made a second page appear from nowhere while the count
still said "of 1". It exists for a good reason — pages otherwise come
only from how far the writing runs, so drawing on page two would mean
writing two pages first — but it should look like the offer it is. Dashed
outline, half opacity, and it says what it is for. It counts once
something is on it.

And the two buttons above the list did not match: the one I added last
round was larger and bolder than the one it sat beside, and the older
one's target was nineteen pixels tall, which a finger misses. Both are
one shape now. A separator's label matches a chapter's name in size for
the same reason — two type treatments in one list is noise, and the
difference was not saying anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:43:51 +02:00
marcandClaude Opus 5 9d5ac1db27 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:04:39 +02:00
marcandClaude Opus 5 1757ec9af3 Chapters: folders inside a notebook
A note is in exactly one chapter, or in none — which means the notebook
itself. Chapters sit above the loose notes; tapping one opens its own
list and the menu button becomes the way back out, which on a phone is
the only way, since the list fills the screen.

A chapter is not a wastebasket. Removing one never removes what it held:
the notes come back to the notebook, where they can be seen and filed
again. That is ON DELETE SET NULL, and it is also written out in the
delete route, because it is the promise the whole thing rests on and
should be visible where it happens rather than inferred from a schema.
The dialog says so too, with the number of notes affected, and the suite
removes a chapter holding three notes and asks the server for all three.

Three rules keep the two levels honest. A note filed into a chapter
leaves the notebook's own list, or filing it would appear to do nothing.
A chapter belongs to its notebook, so the server refuses to file a note
into a chapter of a book it is not in, and moving a note to another
notebook clears its chapter. And separators belong to the list they were
drawn in: a separator's order counts the notes before it, which means
nothing across lists.

Two bugs found while building it, both a day old and both affecting the
desktop rather than the phone they were written for:

- A submenu was clipped away. The context menu had been given
  `overflow-y: auto`, which cuts off anything flying out sideways — so
  the notebooks under "Move to..." were invisible and every click on them
  landed on the note list behind. The side was inverted too:
  `right: 100%` places a panel on the left.
- A separator was a one-pixel hairline in the border colour with faint
  grey text on near-black. Present, and on a phone effectively invisible.

And one test that never measured what it claimed. It asked whether a
dragged note had become "the most recently changed" by reading the order
on screen — but three notes made seconds apart share an `updated_at`
second, so that order is a tie broken arbitrarily. It passed on luck for
weeks and fell over on a run a fraction faster. It now compares the
timestamps themselves, before and after the drag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:04:18 +02:00
marcandClaude Opus 5 e037f01dc2 Let a notebook's design be its own
Two things were drawn on every cover regardless of what was chosen, and
between them they are most of why a shelf of notebooks looked alike.

A 45-degree stripe texture was baked into the cover at 1.5% white. It was
meant to be imperceptible; on the darker gradients it bands visibly, and
since covers gained patterns of their own it was a second pattern
fighting the first. Removed.

And the same pair of rings sat in the middle of every one. The mark
belongs to the design now — a ring, a square, a diamond, a dot, a cross,
an arc, three bars — and the designs whose pattern already fills the face
wear nothing at all. Drawn with borders rather than an image, so each
takes its cover's own accent and stays sharp at any size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:44:15 +02:00
marcandClaude Opus 5 83b76b95a0 Make a separator legible on a phone
It was a one-pixel hairline in the border colour with faint grey text on
near-black: present, and on a small screen in daylight effectively
invisible. Marc reported it as missing, which it may as well have been.

Two-pixel rules in a colour that carries, a brighter and slightly larger
label, and room above it so it reads as the end of what came before
rather than as decoration on the note below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:41:17 +02:00
marcandClaude Opus 5 7cb26898aa Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 02:20:55 +02:00
marcandClaude Opus 5 8ca1ed6e06 Choose a cover in two parts, and let the app be installed
A cover is a colour *and* a pattern now, chosen in turn. Offering every
combination as its own entry meant fifteen colours times a dozen patterns
in a grid nobody could take in — and in practice only the handful someone
had thought to write out. Each pattern is previewed over the colour in
hand, so what is on screen is the cover as it will be. `cover_pattern` is
its own column: they are two decisions, and folding them into one number
would mean renumbering every cover the day a pattern is added.

For one day the patterns *were* twelve appended entries. Those indices
are read back as the colour-and-pattern pair they were drawn as, so
nothing chosen then looks different now.

And three separate reasons the install button never appeared, none of
them in anyone's configuration:

- `beforeinstallprompt` fires once, early — usually before React has
  mounted — and is never fired again. A listener added from inside a
  component simply missed it. It is caught at the top of index.html now.
- A `<link rel="manifest">` is fetched with credentials omitted unless it
  says otherwise. Behind a reverse proxy that authenticates in front of
  the app, that one request comes back as the proxy's login page and the
  browser reads it as an invalid manifest — while the app itself works
  perfectly, because its own requests carry the cookie.
- The service worker cached whatever came back, so a lapsed session could
  be stored as the application itself. The app would then "work offline"
  by showing a login screen it could never get past, curable only by
  clearing the site's data. Only successful, unredirected responses are
  cached now, and the cache name is bumped to drop anything already
  stored under the old rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 02:20:37 +02:00
marcandClaude Opus 5 0d8d1180de Record the digest of the image that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:16:59 +02:00
marcandClaude Opus 5 608da16ac2 Make the phone work: touch reordering, reachable menus, and nothing lost offline
Four things that were built for a mouse on a desk and quietly did nothing
useful on a phone. The last one is the serious one.

**A note made offline lost everything written into it.** Its create
reached the server, the server named it something else, and every edit
still queued behind it went on addressing the name only that device ever
knew — 404, five retries, thrown away. The note arrived empty and
untitled, with nothing to say that anything had been lost. The database
was repointed at the new id all along; the queue was not. It is now, by
replacing the id across the whole queued payload rather than in a list of
named fields, because a list of fields is a list to forget to add to. The
same held for a notebook made offline and everything filed into it.

**The order of notes could not be changed on a phone**, because the list
went through the HTML5 drag API, which mobile browsers never fire from
touch. Pointer events instead, which mouse, pen and finger all produce. A
mouse still drags a row from anywhere; a finger drags from a grip,
because pressing the row has to go on scrolling and holding it has to go
on opening the menu. The row under the finger is found with
elementFromPoint — a finger raises no pointerover on anything it did not
press.

**"Move to..." opened off the side of the screen**, and could not be
opened at all without a hover. It is a tap now, unfolding in place where
there is no room beside the menu and flipping sides where there is. The
menu itself is placed by measuring rather than by assuming it is 220px
wide, its rows are tall enough to hit with a thumb, and Escape closes it.

**Installing said nothing when it could not be done.** The entry appeared
only when the browser had offered a prompt, so where installing was
impossible there was no button and no reason. It is always there now and
names the cause — most usefully that a browser will only install a site
served over HTTPS, which is also why the service worker would not have
registered, so one cause was showing up as two mysteries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:16:41 +02:00
marcandClaude Opus 5 6eecf57247 Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:29:32 +02:00
marcandClaude Opus 5 9e4677c70b Patterns on the covers, a search box that is simply there, roomier margin notes
Four small things about the shape of the app.

Covers can be patterns as well as colours — ruled paper, graph paper,
pinstripe, bands, dots, cross-hatch, linen, washes, a ledger — drawn
entirely with CSS gradient layers. No image files, nothing to upload,
crisp at any size, and offline like everything else. A cover is stored as
an index into that list, so the new ones are appended and never inserted:
renumbering would silently repaint every notebook anyone already has.

Two things the patterns taught. One idea per cover: two crossed diagonals
meant as a chevron came out as noisy plaid at the size a cover is
actually read, and were replaced with plain bands. And the grid has to
scroll — with 27 covers the dialog ran past the bottom of a laptop screen
and took the Create button with it, which a check now holds at 1440x800.

The search box in a notebook is simply there now, and the button that
used to open it is gone. A magnifying glass cost a click and having to
remember that searching was possible at all; a search box is one line and
says for itself what it does.

Margin notes went from 208x74 to 288x112 — from three short lines to
about five. Three lines is a caption, not a remark on a passage, and the
space beside the page was sitting mostly empty.

And the notebook edit dialog was still part German, and would not say
what the server's default number of versions actually is. It names it
now, read from the server rather than written into the interface: it is
set with MAX_VERSIONS_PER_NOTE, and a hardcoded "50" would quietly start
lying the day that changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:29:00 +02:00
marcandClaude Opus 5 635a9632d1 Record the digest of the image that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:27:11 +02:00
marcandClaude Opus 5 d82e72d358 Keep the gap blank, and give the writing more of the page
Two things wrong with the paper.

"Start writing..." was printed at the foot of every page that had a break
after it. The placeholder extension styles `[contenteditable]:empty::before`,
and the spacer holding a gap open is exactly that: empty, and not
editable. It inherited a placeholder meant for an empty note.

And the column was too narrow for the sheet it now sits on — about 73
characters, which left 117px of blank paper down each edge and made the
writing look lost. Widened to about 80, a little short of where the eye
stops reliably finding the start of the next line, which leaves roughly
8% of the sheet as margin: a document's margin rather than a picture
frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:26:51 +02:00
marcandClaude Opus 5 816cc8e68b Record the digest of the image that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:17:55 +02:00
marcandClaude Opus 5 97a3764d92 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>
2026-08-11 20:17:28 +02:00
marcandClaude Opus 5 3ce867c6d0 Stop a drawing from making its note unwritable
The layer showing saved ink lies over the text. The canvas itself is deaf
to the pointer, but the box I wrapped it in yesterday was not — so on any
note carrying a drawing, every click meant for the text was swallowed
before it arrived, and the note simply could not be typed in.

Introduced with per-page drawing, where each page's ink got a wrapper to
be positioned by. The suite now clicks into the text of a note that has a
drawing and types, and asks what actually answers over the writing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:50:32 +02:00
marcandClaude Opus 5 fcdd29ba2b Record the digests of the images that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:27:18 +02:00
marcandClaude Opus 5 230e55903e Make build.sh executable
It has always been run as ./build.sh, which the file's own header says,
and the bit for it was never set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:26:51 +02:00
marcandClaude Opus 5 8e38404a69 Read a PDF where it is imported, and keep the text with the file
A PDF was searchable on a device once that device had opened it, because
the extracted text lived in that browser's IndexedDB and nowhere else. So
a document imported on the desktop was invisible to a search on the
phone — which is exactly the case that matters for filing invoices, since
those are imported and then never opened again.

The reading still never happens on the server. It has no PDF library and
is not getting one: the bytes arrive encrypted and are stored that way.
The device that imports the file reads it there and then, and the result
is stored beside the file in attachments.text_index, encrypted at rest
like everything else and opaque to the server. Every other device asks
for it after login and can then find a document it has never downloaded.

Not storing it was the earlier decision, on the reasoning that text
derivable from bytes the server already holds should not be kept twice.
True, and useless. A few hundred kilobytes against a file measured in
megabytes is a trade worth making.

A `null` answer is not an error — it means nobody has read this file yet.
Documents extracted before this existed are sent up the next time they
are opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:26:07 +02:00
marcandClaude Opus 5 228552d453 Let a drawing run over as many pages as the note
The ink was stuck on page one while the writing carried on without it,
which stopped making sense the moment a note could run to four pages.
Every page is exported anyway, so every page can be drawn on.

Pages are numbered the way the PDF reader numbers them, and only pages
that carry ink are stored, so the keys are the drawn pages. While drawing
there is always one blank page past the end: pages otherwise come from
how far the writing runs, and to draw on page three you would first have
had to write three pages.

A drawing that lives entirely on page one is still written in the old
single-page shape. Nothing requires that — it means an ordinary note is
stored exactly as it always was, and a device running an older build
still shows it.

The bin clears the page being looked at. Undo and redo work on the
drawing as a whole, because one Undo button that means four different
things depending on where you scrolled is not a button anyone can use.

And a real bug found on the way out: the exporter has written
drawings/<note>.png since the interface was put into English, while the
importer was still looking for zeichnungen/<note>.png. A drawing exported
by this very app came back from its own archive as nothing at all. Both
names are read now, and so are the further pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:25:56 +02:00
marcandClaude Opus 5 b8c5c42217 Record the digest of the image that went out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:41:43 +02:00
marcandClaude Opus 5 73cc28696f Write down the standing decisions, and start a changelog
The rules this project is worked under — Docker for the backend, straight
to main, no backup feature because the host is backed up, the encryption
stays, no public sign-up — lived only in a conversation. They belong in
the repository, where they survive it.

The changelog answers a question the commit messages cannot: what is
actually running on the server. It carries the image digests, read back
from the registry rather than taken from the push output.

The readme described the note column as left-aligned. It is centred
again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:38:58 +02:00
marcandClaude Opus 5 dcc42bb8e0 Centre the writing column again
Pinning it to the left edge put the start of every line in the same
place, which was the point, but on a wide window it left the entire
right half of the pane as one dead block. Centred, the same leftover
space reads as two margins instead.

The page surface that tried to give that block a meaning is reverted in
the commit before this one: it made the whole sheet sit hard against the
left edge, which was worse than the problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:36:54 +02:00
marc 558c15f2c9 Revert "Give the writing a page to sit on, so the empty half means something"
This reverts commit 7910179f29.
2026-08-11 18:34:50 +02:00
marcandClaude Opus 5 7910179f29 Give the writing a page to sit on, so the empty half means something
The text was capped at a readable width inside a much wider pane, which
left a large void on the right with nothing to explain it. The page is
now a drawn surface: the writing sits on it with a margin, and the space
that was empty is that margin.

Page boundaries used to be a hairline. They are a dashed band now, with
the next page's number in the margin beside it, so a boundary reads as
one page ending rather than as a rule in the text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:21:26 +02:00
marcandClaude Opus 5 fed53772aa Set the note left, count its pages, choose its typeface, align its text
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>
2026-08-11 17:17:13 +02:00
marcandClaude Opus 5 830af6532a Take the paper back out, and quiet the surfaces around it
The sheet was not what it wanted to be, so writing is plain text again: about 73
characters to the line, centred, no border and no shadow. The fixed page belongs
to the canvas — a stroke has to land in the same place on every screen, which
needs one agreed page — so it appears exactly when something is on it or about
to be: in Draw, or when the note already has a drawing. The cap lives in the
stylesheet rather than on a container, so it holds on a page as well as off one.

Emoji are gone from the note rows and the sidebar. They are drawn by the
operating system, so they looked different on every machine and were loud enough
that a row's picture caught the eye before its title did. Rows are tighter by
three pixels of padding.

The trash button no longer stands permanently in the danger colour beside the
star, which made the most destructive control the loudest thing on screen. It
sits behind a divider and turns red under the pointer, when it is about to be
used.

On a phone the toolbar's fourteen controls scrolled sideways, so the ones at the
end could only be reached by dragging the bar and nothing said they were there.
Nine remain, plus a menu holding the headings, quote, code and undo.

A new `sync` suite proves what was assumed: a PDF written on one device — the
document, the drawing and the pinned notes — reaches a second one. Two browser
contexts are two devices.

Both search suites read a row's title as the second line of its text, which only
worked while the icon was itself a text emoji; they read it by name now.

218 unit tests, 249 browser checks across twelve suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:57:22 +02:00
marcandClaude Opus 5 c12ffa250e Make the writing surface a sheet of paper
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>
2026-08-11 12:08:54 +02:00
marcandClaude Opus 5 11f37b7921 Let each visibility switch hide only its own thing
The margin notes hung off both switches: `hidden: hideInk || !showPins`, so
turning off the drawing took the markers and their column with it — one switch
turning off both of the things the pair exists to separate.

The checks look at what is actually visible — the ink layer's opacity, how many
markers are on the page, whether the column is there — and test both directions
separately, which is what the old code failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:49:47 +02:00
marcandClaude Opus 5 52bd2b8f3d Put the interface into English, and give the notebook one way in
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>
2026-08-11 02:17:22 +02:00