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>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-17 11:45:16 +02:00
1 parent 2ffa357dfc
commit a9391cdcf2
15 files changed
+724 -189

No files matched your search

+17 -8
View File
@@ -249,25 +249,34 @@
align-items: flex-start;
gap: 8px;
}
/* The box sits in the middle of the *first line* beside it, not at a fixed
nudge from the top. `margin-top: 0.35em` was measured once against one font at
one size; changing the note's typeface, or writing a task that wraps, left the
box floating above the text it belongs to. Giving the label the height of one
line and centring inside it holds for every size. */
/* The box is centred on the *letters* of the first line beside it — not on that
line's box, and not at a fixed nudge from the top.
`margin-top: 0.35em` was measured once against one font at one size. Centring
in the full line was the next attempt and is still not it: a line box is
taller than the letters in it by the depth of a descender, so its middle sits
below the middle of a capital. Measured, that left the box's top level with
the capitals and its bottom 4px past the baseline — a square hanging off the
line. Two tenths of an em off the line's height is where the capitals'
middle actually is, for every face on offer here. */
.note-editor .ProseMirror ul[data-type="taskList"] li > label {
flex: 0 0 auto;
display: flex;
align-items: center;
height: calc(1em * var(--ne-line));
height: calc(1em * var(--ne-line) - 0.3em);
user-select: none;
}
.note-editor .ProseMirror ul[data-type="taskList"] li > div {
flex: 1 1 auto;
}
/* Sized in em, so the box stays the size of a capital whatever the note's type
size is. At 15px it was a 15px square against a 10px letter, which read as a
box the text was standing next to rather than part of the same line. */
.note-editor .ProseMirror ul[data-type="taskList"] input[type="checkbox"] {
accent-color: var(--ne-accent);
width: 15px;
height: 15px;
width: 0.9em;
height: 0.9em;
margin: 0;
cursor: pointer;
}
.note-editor .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div {
+4 -1
View File
@@ -162,7 +162,10 @@ function renderSuggestionList() {
const pick = (i) => {
const item = items[i];
if (item && command) command({ id: item.id, label: item.label, kind: item.kind });
// The mark travels with the choice. Dropped here, every new link was born
// wearing the default sheet of paper — the menu showed the note's own mark
// and the link that came out of it did not.
if (item && command) command({ id: item.id, label: item.label, kind: item.kind, icon: item.icon || null });
};
const place = (rect) => {
+54 -23
View File
@@ -133,29 +133,60 @@ export function EditorSurface({ editor, dark, font, minHeight, onOpenNote, resol
* node — the link survived on screen and vanished on the next reload. */
useEffect(() => {
if (!editor || editor.isDestroyed || !resolveLinkLabel) return;
const edits = [];
editor.state.doc.descendants((node, pos) => {
if (node.type.name !== "noteLink") return;
const label = resolveLinkLabel(node.attrs.noteId);
const missing = label === null;
const nextLabel = missing ? node.attrs.label : label;
// The mark travels the same way, and for the same reason: it belongs to
// the note, so it has to follow when the note's changes.
const icon = (resolveLinkIcon ? resolveLinkIcon(node.attrs.noteId) : node.attrs.icon) ?? null;
if (
nextLabel !== node.attrs.label ||
missing !== !!node.attrs.missing ||
icon !== (node.attrs.icon ?? null)
) {
edits.push([pos, { ...node.attrs, label: nextLabel, missing, icon }]);
}
});
if (edits.length === 0) return;
const tr = editor.state.tr;
for (const [pos, attrs] of edits) tr.setNodeMarkup(pos, undefined, attrs);
tr.setMeta(LABEL_REFRESH, true);
tr.setMeta("addToHistory", false); // undo should not step through renames
editor.view.dispatch(tr);
const refresh = () => {
if (editor.isDestroyed) return;
const edits = [];
editor.state.doc.descendants((node, pos) => {
if (node.type.name !== "noteLink") return;
const label = resolveLinkLabel(node.attrs.noteId);
const missing = label === null;
const nextLabel = missing ? node.attrs.label : label;
// The mark travels the same way, and for the same reason: it belongs to
// the note, so it has to follow when the note's changes.
const icon = (resolveLinkIcon ? resolveLinkIcon(node.attrs.noteId) : node.attrs.icon) ?? null;
if (
nextLabel !== node.attrs.label ||
missing !== !!node.attrs.missing ||
icon !== (node.attrs.icon ?? null)
) {
edits.push([pos, { ...node.attrs, label: nextLabel, missing, icon }]);
}
});
if (edits.length === 0) return;
const tr = editor.state.tr;
for (const [pos, attrs] of edits) tr.setNodeMarkup(pos, undefined, attrs);
tr.setMeta(LABEL_REFRESH, true);
tr.setMeta("addToHistory", false); // undo should not step through renames
editor.view.dispatch(tr);
};
refresh();
/* And again whenever the document changes.
*
* Running once was not enough, and the gap was the ordinary case: this
* effect runs while the editor is still empty, and the note's content is
* poured in afterwards — deliberately without an update event, so the
* editor is not marked dirty by its own loading. Every link therefore kept
* whatever it was saved with, which is why a reference always showed the
* sheet of paper however its target was marked.
*
* Deferred by a frame rather than dispatched from inside the handler:
* ProseMirror is still finishing the transaction that brought us here. */
let frame = 0;
const schedule = () => {
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
refresh();
});
};
editor.on("transaction", schedule);
return () => {
if (frame) cancelAnimationFrame(frame);
editor.off("transaction", schedule);
};
}, [editor, resolveLinkLabel, resolveLinkIcon]);
const handleClick = useCallback(