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>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-12 13:04:18 +02:00
1 parent e037f01dc2
commit 1757ec9af3
15 files changed
+1078 -78

No files matched your search

+36 -1
View File
@@ -431,7 +431,8 @@ docker compose -f docker-compose.dev.yml up -d --build # must be running
| `attachments-lifecycle` | 10 | a picture is kept while any version shows it, and freed once none does |
| `conflict` | 10 | a late write wins but the state it buries survives as a version |
| `export` | 21 | the downloaded bytes are real Markdown, ZIP and PNG, unpacked and read back |
| `organise` | 50 | a hand-made order survives a search; separators hold their place; a note exported and imported comes back whole |
| `organise` | 52 | a hand-made order survives a search; separators hold their place; a note exported and imported comes back whole |
| `chapters` | 13 | a note filed into a chapter leaves the notebook's list, and removing the chapter gives every note back |
| `offline` | 17 | a cold page load with the network off still opens notes and searches them — and a note made, written and drawn on offline arrives whole once there is a connection |
| `sync` | 7 | a PDF written on one device — document, drawing and pinned notes — reaches a second one, which can find it by its contents without opening it |
@@ -940,6 +941,40 @@ rows behind.
---
## Chapters
Folders inside a notebook. A note is in exactly one chapter, or in none — which
means the notebook itself. Chapters are listed above the loose notes; tapping one
opens its own list, and the menu button becomes a 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` on `notes.chapter_id`, and it is also written out explicitly
in the delete route — it is the promise the whole feature rests on, and it should
be visible where it happens rather than inferred from a schema. The dialog says
so too, with the number of notes affected.
Three rules that keep the two levels honest:
- **A note filed into a chapter leaves the notebook's own list.** Showing it in
both would mean filing it appeared to do nothing.
- **A chapter belongs to its notebook.** Filing a note into a chapter of a book
it is not in is refused by the server; moving a note to another notebook clears
its chapter, since the old one is in the book it just left.
- **Separators belong to the list they were drawn in.** A separator's `order`
counts the notes before it, which only means anything inside one list, so each
carries the chapter it belongs to — `null` for the notebook itself. The
notebook still stores one list of them, and a change made in one chapter writes
the others back untouched.
A note made while a chapter is open is filed there without being moved. A chapter
is named when it is created, unlike a separator: a separator is a line and reads
perfectly well unnamed, while an unnamed chapter is a row nobody can tell from
the next one.
---
## Sorting a notebook
Each notebook remembers how its notes are ordered, and remembers it on the