Let a margin remark become a note, and lead to it ever after

Some remarks turn into a page of their own. "Notiz anlegen" on a margin card
makes one, titles it from what was written, and goes there; the pin keeps its id
so the passage leads to that note afterwards. The note is created with a link
back to the document, so the connection reads both ways and the document lists it
among its backlinks — a note born from a passage should not be a note with
nothing on it saying which passage. An existing note can be linked instead, and a
link can be let go of without touching the note. Both are offered on the phone
sheet too.

Two older bugs stood in the way and are fixed:

`openLinkTarget` finds a note's notebook by looking it up in the notes list, and
a note created in the same tick is not in the copy that callback closed over — so
jumping to a fresh note went nowhere at all. Creating one now navigates by the
notebook it already knows.

A drawing save waits 700ms for the next stroke, and leaving the document inside
that window dropped it. That is precisely what turning a remark into a note does,
so the link would have been lost on the way out. The viewer flushes a pending
write when it unmounts, which also covers drawing and navigating away quickly.

215 unit tests, 229 browser checks across eleven suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-10 16:40:38 +02:00
1 parent 011ba82a94
commit 502aecfc92
10 files changed
+541 -12

No files matched your search

+64 -2
View File
@@ -34,6 +34,7 @@ import { EditorPane } from "./features/notes/EditorPane.jsx";
import { HistoryModal } from "./features/notes/HistoryModal.jsx";
import { DrawingLayer } from "./features/drawing/DrawingLayer.jsx";
import { CANON_W, CANON_H, PREVIEW_DRAWING_W } from "./features/drawing/strokes.js";
import { serializeNoteContent } from "./editor/content";
/** Views that are not a notebook and therefore cannot hold a new note. */
const VIRTUAL_FOLDERS = ["all", "starred", "trash", "dashboard"];
@@ -279,9 +280,70 @@ export default function App() {
[notes, openFolder, openNote]
);
/**
* Turns a remark in a PDF's margin into a note of its own.
*
* The new note opens with a link back to the document it came from, so the
* connection reads in both directions: the pin leads here, and this note shows
* up in the document's backlinks. Without that a note born from a passage
* would have nothing on it saying which passage.
*/
const createNoteForPin = useCallback(
async (source, title) => {
const folderId = source.folderId || notebooks[0]?.id;
if (!folderId) return null;
const note = await data.createNote(folderId);
const doc = {
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Zu " },
{
type: "noteLink",
attrs: { noteId: source.id, label: source.title || "Dokument", kind: "note" },
},
],
},
{ type: "paragraph" },
],
};
data.updateNote(note.id, {
title,
content: serializeNoteContent(doc),
links: [source.id],
attachments: [],
});
return { ...note, title };
},
[data, notebooks]
);
/**
* Opens a note that was created a moment ago.
*
* `openLinkTarget` looks the target up in the notes list to find its notebook,
* and a note created in the same tick is not in the copy that callback closed
* over — so it went nowhere at all. Here the notebook is already known.
*/
const openCreatedNote = useCallback(
(id, folderId) => {
if (folderId) setActiveFolder(folderId);
openNote(id);
},
[openNote]
);
const linkProps = useMemo(
() => ({ openLinkTarget, searchLinkTargets, resolveLinkLabel }),
[openLinkTarget, searchLinkTargets, resolveLinkLabel]
() => ({
openLinkTarget,
searchLinkTargets,
resolveLinkLabel,
createNoteForPin,
openCreatedNote,
}),
[openLinkTarget, searchLinkTargets, resolveLinkLabel, createNoteForPin, openCreatedNote]
);
/* ── Actions ── */