Link the margin notes from the notes, and let them be hidden

The linking went the wrong way round. A remark in the margin is short by design;
when there is more to say about that passage it belongs in the notes beside the
document — and it has to be possible to say *which* passage. So the pins are
offered to the editor's `@` menu exactly like notes and notebooks are, labelled
with their page, and following one scrolls the document to the spot and opens the
remark. Pins are unhidden first: a link leading to something invisible would read
as broken.

A pin link is the same node with kind "pin", and is deliberately kept out of the
links reported to the server. Its target is a marker inside this note's own
overlay, so no note has that id, and reporting it would fill the backlink table
with rows matching nothing.

They can also be hidden altogether. Reading a document as it was printed is a
normal thing to want; the toolbar takes the markers and the column away together
and the page takes the width back.

The other direction — making a note out of a remark — is removed, along with its
picker and the pin's noteId: 357 lines fewer than before. The unmount flush it
turned up stays, since a drawing save was genuinely being dropped by navigating
away inside its debounce.

218 unit tests, 231 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 20:00:46 +02:00
1 parent 502aecfc92
commit 4c1b632029
13 files changed
+239 -484

No files matched your search

+2 -64
View File
@@ -34,7 +34,6 @@ 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"];
@@ -280,70 +279,9 @@ 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,
createNoteForPin,
openCreatedNote,
}),
[openLinkTarget, searchLinkTargets, resolveLinkLabel, createNoteForPin, openCreatedNote]
() => ({ openLinkTarget, searchLinkTargets, resolveLinkLabel }),
[openLinkTarget, searchLinkTargets, resolveLinkLabel]
);
/* ── Actions ── */