Fix a permanent black line in the note list, and add separators

The drop indicator was a `borderTop` set in the same style object as a `border`
shorthand. Clearing the longhand resets its width and colour to the defaults
while the shorthand's `solid` stays, so a row kept a 2px black line for good —
measured at rest, before any drag. It is now its own element, which also stops
the list twitching as a row grows and shrinks by a pixel.

That element then turned out to be a dead strip of its own: it pushes the row
down as it appears, so the pointer ended up over the line rather than over the
row and releasing there dropped on nothing. The drag handlers moved to the whole
row and the indicator no longer takes pointer events. A strip below the last row
means "put it at the end", which otherwise needs a drop onto the final note.

Separators are lines drawn between notes, optionally named, stored on the
notebook beside its sort mode. They drag like notes do, and their position is
"how many notes come before me", so they hold their place as the notes around
them are renamed or edited. Shown only in the manual order — in "newest first"
the notes would slide past the line on every edit.

"All Notes" and "Starred" no longer offer a hand-made order: they are questions
asked of every notebook, so there is nothing for an arrangement to belong to.

199 unit tests, 190 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 01:45:38 +02:00
1 parent 0a0864c58b
commit 2cee00e38e
10 files changed
+731 -125

No files matched your search

+94 -9
View File
@@ -79,16 +79,24 @@ try {
const beforeDrag = await titles();
check(beforeDrag.length === 3, "manual order starts from the notes it has", beforeDrag.join(", "));
// Drag the last note onto the first.
// Drag the last note onto the first. The moves are stepped: a single jump can
// land before the drag has started, and the drop indicator shifts the rows as
// it appears, so the pointer has to arrive gradually.
const drag = async (fromLoc, toLoc) => {
const from = await fromLoc.boundingBox();
const to = await toLoc.boundingBox();
await p.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
await p.mouse.down();
await p.mouse.move(from.x + from.width / 2, from.y + from.height / 2 - 12, { steps: 4 });
await p.mouse.move(to.x + to.width / 2, to.y + to.height / 2, { steps: 12 });
await p.waitForTimeout(300);
await p.mouse.move(to.x + to.width / 2, to.y + to.height / 2 + 2, { steps: 4 });
await p.mouse.up();
await p.waitForTimeout(2000);
};
const rows = p.locator("[data-note-row]");
const last = rows.nth(2);
const first = rows.nth(0);
await last.hover();
await p.mouse.down();
await first.hover();
await first.hover(); // a second move, so the drop target registers
await p.mouse.up();
await p.waitForTimeout(1500);
await drag(rows.nth(2), rows.nth(0));
const arranged = await titles();
check(
@@ -122,6 +130,67 @@ try {
await sortBy("manual");
await p.waitForTimeout(800);
/* ── the drop indicator ── */
// It used to be a border on the row, set beside a `border` shorthand — which
// left a permanent 2px black line behind once it was cleared.
const strayLines = await p.locator("[data-drop-line]").count();
check(strayLines === 0, "no drop indicator is showing while nothing is being dragged", String(strayLines));
const borders = await p
.locator("[data-note-row]")
.evaluateAll((rows) => rows.map((r) => getComputedStyle(r).borderTopWidth));
check(
borders.every((b) => b === "1px"),
"and no row has grown a stray top border",
borders.join(", ")
);
/* ── separators ── */
await p.locator("[data-add-separator]").click();
await p.waitForTimeout(1800);
check((await p.locator("[data-separator]").count()) === 1, "a separator can be added");
const label = p.getByLabel("Separator label");
await p.locator("[data-separator] button").first().click();
await p.waitForTimeout(300);
await label.fill("Wichtig");
await label.press("Enter");
await p.waitForTimeout(1800);
check(
/wichtig/i.test(await p.locator("[data-separator]").innerText()),
"and named",
await p.locator("[data-separator]").innerText()
);
// It belongs to the notebook, so it has to survive a reload like the order does.
await p.reload({ waitUntil: "networkidle" });
await p.waitForTimeout(2500);
await p.locator("button").filter({ hasText: "Ordnung" }).first().click();
await p.waitForTimeout(1800);
check(
/wichtig/i.test(await p.locator("[data-separator]").innerText()),
"a separator survives a reload, because it belongs to the notebook"
);
const withSeparator = await titles();
check(
withSeparator.join(",") === arranged.join(","),
"and adding one did not disturb the arrangement",
`${arranged.join(",")} vs ${withSeparator.join(",")}`
);
// Separators only mean something in a hand-made order; in "newest first" the
// notes would slide past the line on every edit.
await sortBy("updated");
check((await p.locator("[data-separator]").count()) === 0, "it is hidden in the other orders");
await sortBy("manual");
await p.waitForTimeout(800);
check((await p.locator("[data-separator]").count()) === 1, "and back on the way in");
await p.locator("[data-separator] button[aria-label=\"Remove separator\"]").click();
await p.waitForTimeout(1800);
check((await p.locator("[data-separator]").count()) === 0, "a separator can be removed again");
/* ── exporting a note and importing it back ── */
await p.locator("[data-note-row]").filter({ hasText: "Beta" }).first().click();
await p.waitForTimeout(1200);
@@ -238,6 +307,22 @@ try {
await p.waitForTimeout(800);
await p.locator("button").filter({ hasText: "All Notes" }).first().click();
await p.waitForTimeout(900);
// "All Notes" is a question asked of every notebook, not a place — there is
// nothing for a hand-made arrangement to belong to.
await p.locator("[data-sort-button]").click();
await p.waitForTimeout(400);
check(
(await p.locator('[data-sort-option="manual"]').count()) === 0,
"no hand-made order is offered outside a notebook"
);
check(
(await p.locator('[data-sort-option="updated"]').count()) === 1,
"but the derived orders still are"
);
await p.keyboard.press("Escape");
await p.locator("h2").first().click();
await p.waitForTimeout(500);
await p.getByTitle("New Note").click();
await p.waitForTimeout(900);