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>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-09 01:38:03 +02:00
1 parent 3acb8b66a4
commit 0a0864c58b
27 files changed
+2962 -86

No files matched your search

+70 -2
View File
@@ -19,6 +19,8 @@ A self-hosted note-taking application with rich text editing, freehand drawing,
- **Star/Favorite** — Quick access to important notes
- **PDF Reading & Annotation** — Import a PDF, read it, select its text, draw on it, write notes beside it, and export it with or without your marks
- **Export** — Any note, notebook or the whole account as Markdown or PDF, entirely in the browser
- **Import** — Markdown files, or a ZIP this app exported, come back as notes with their images
- **Sorting** — Each notebook remembers how its notes are ordered, including an arrangement made by hand
- **Dark & Light Mode** — Full theme support with refined color schemes
### Sync & Storage
@@ -302,7 +304,7 @@ failure distinguished by comparing an error's `.message` to a magic string.
Failures now carry a typed `SyncError.code`; `.message` still holds the same
string, so the remaining JavaScript callers are unaffected.
The 81 unit tests concentrate on the same places, and several exist because the
The 186 unit tests concentrate on the same places, and several exist because the
bug they cover actually happened:
- a queue entry that fails must stay queued, with an attempt counter
@@ -334,6 +336,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` | 24 | a hand-made order survives a search; a note exported and imported comes back whole |
| `offline` | 12 | a cold page load with the network off still opens notes and searches them |
Each suite creates a throwaway account through the admin API and deletes it
afterwards, so runs leave the database as they found it. They need an admin to
@@ -354,7 +358,7 @@ needs. Everything else lives behind a hook:
| `useSearchIndex` | the local index and the background backfill that fills it |
| `useResponsive` | phone vs desktop, sidebar and which pane a phone shows |
| `useNavigationHistory` | mapping the current location onto browser history |
| `useNotebookDnd` | drag-to-reorder, shared by sidebar and dashboard |
| `useNotebookDnd` | drag-to-reorder, shared by the sidebar, the dashboard and the note list |
| `usePwaInstall` | the browser's install offer |
`useNoteData` deliberately has no confirmation prompts in it: whether to ask
@@ -703,6 +707,70 @@ rows behind.
---
## Sorting a notebook
Each notebook remembers how its notes are ordered, and remembers it on the
server rather than per device — the order is a property of the notebook, and
finding one sorted differently on the phone than on the desktop is its own small
annoyance. A reference book can stay alphabetical while a journal stays
newest-first.
| Mode | Order |
| --- | --- |
| `manual` | the user's own arrangement, dragged into place |
| `updated` | most recently changed first *(the default)* |
| `oldest` | least recently changed first |
| `created` | most recently created first |
| `title` | A–Z, case- and umlaut-aware, with numbers read as numbers |
**The manual order has to survive being looked away from.** Searching imposes
its own order — relevance — and switching back has to show exactly what was
arranged. That is only keepable if the arrangement lives in the notes and is
never re-derived from what is on screen, so two rules hold it up:
- `reindex` renumbers **every note in the notebook**, taken from the store, not
the list being displayed. Renumbering a filtered subset would give a hidden
note the same position as a visible one and quietly scramble the rest.
- Dragging is switched off while a search is narrowing the list, and the list
says so rather than silently ignoring the drag.
Reordering also has its own route, `POST /api/notes/reorder`. Going through
`PUT /api/notes/:id` would bump `updated_at` and snapshot a version — so
dragging a note would move it to the top of "last changed" and spend a slot of
its history on a change that is not to the note at all.
---
## Import
The other half of the promise that notes are not hostage to this program. A
`.md` file becomes a note; a `.zip` this app exported comes back nearly whole —
text, tables, task boxes, and the images, re-uploaded as attachments so they are
encrypted at rest again like anything else.
The parser is a deliberate subset: what the exporter emits, plus what a
hand-written note is likely to use. It is not CommonMark and does not try to be.
A full parser is thousands of lines and a permanent maintenance surface, and the
failure mode here is mild — an unrecognised construct arrives as the literal
text that was written, which is still the user's words in the user's order.
Two things do not come back, and the import says so afterwards rather than
leaving them to be discovered:
- **A drawing returns as a picture, not as strokes.** It was exported as a PNG,
and vectors cannot be recovered from pixels. It stays visible, printable and
exportable, but the pen can no longer edit it.
- **An image the archive does not contain cannot be restored.** The place it sat
in the note is kept, so what used to be there is at least recorded.
Writing the exporter also turned up a real bug in it: blocks were emitted
without a blank line between them, so a list written straight after another list
became more items of the first, and a quote or code fence on the next line was
read as part of the item above. That is not one reader's quirk — it is what the
format says — so the file rendered wrongly everywhere, not only here.
---
## Export
Notes should not be hostage to this program, so everything can leave it — from a