Chapters as rows, in two views, in their own colours
Three things Marc asked for, and one of them turned out to be the hinge
the other two hang from.
**A chapter is a row like any other.** Same grip, same drag, same place
in a hand-made arrangement as a note or a separator. That needed notes
and chapters numbered from *one* counter: numbered separately they are
two sequences that cannot be interleaved again, and a chapter dragged
between two notes springs back to the top. Positions therefore have to be
named rather than read off an array index, so POST /notes/reorder now
takes `positions: [{id, order}]` beside the older `ids`, and chapters
have a reorder route of their own.
**Two ways for a chapter to open**, chosen per account beside the
typeface, for the same reason that one lives there: it is a choice about
how your own notes are laid out and it should follow you to the phone.
Beside the list, its notes in a further column the way OneNote lays out
pages — the notebook's own list carrying on to the left, chapter rows and
all. Or unfolded under the chapter's own row, indented against a rule in
its colour. A phone has room for neither and fills the list instead, with
a way back, which is what it did before there was a choice.
**Its own colour**, from eight named ones rather than the full palette:
the point of chapters is that they are different places, and twenty
near-identical blues do not help anyone tell them apart.
The colour is validated on the way in — it is written by a browser, so
only a plain #rrggbb gets through and anything else becomes no colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6cad632a0c
commit
d74fea4905
12 files changed
+640
-88
No files matched your search
+69
-7
@@ -75,27 +75,85 @@ try {
|
||||
|
||||
await p.locator("[data-chapter-row]").first().click();
|
||||
await p.waitForTimeout(1200);
|
||||
check((await titles()).join(",") === "Stromrechnung", "and is inside the chapter", (await titles()).join(", "));
|
||||
check((await p.locator("[data-chapter-back]").count()) === 1, "with a way back out");
|
||||
const inChapter = () =>
|
||||
p.locator("[data-chapter-note]").evaluateAll((els) => els.map((e) => e.textContent.trim()));
|
||||
check((await inChapter()).join(",").includes("Stromrechnung"), "and is inside the chapter", (await inChapter()).join(", "));
|
||||
check((await p.locator("[data-chapter-close]").count()) === 1, "with a way to close it again");
|
||||
|
||||
const filed = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Stromrechnung");
|
||||
check(!!filed?.chapter_id, "the server filed it too", String(filed?.chapter_id));
|
||||
|
||||
/* A note made inside a chapter belongs to it, without being moved. */
|
||||
/* A note made while a chapter is open belongs to it, without being moved. */
|
||||
await createNote(p);
|
||||
await setTitle(p, "Wasserrechnung");
|
||||
await p.waitForTimeout(1500);
|
||||
const made = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Wasserrechnung");
|
||||
check(made?.chapter_id === filed?.chapter_id, "a note made inside a chapter is filed there", String(made?.chapter_id));
|
||||
check(made?.chapter_id === filed?.chapter_id, "a note made while it is open is filed there", String(made?.chapter_id));
|
||||
|
||||
await p.locator("[data-chapter-back]").click();
|
||||
await p.locator("[data-chapter-close]").click();
|
||||
await p.waitForTimeout(1200);
|
||||
check(
|
||||
(await titles()).join(",") === "Zählerstand",
|
||||
"and going back shows the notebook again, without the chapter's notes",
|
||||
"and the notebook's own list never held the chapter's notes",
|
||||
(await titles()).join(", ")
|
||||
);
|
||||
|
||||
/* ── the two ways a chapter opens ──
|
||||
* A further column beside the list, the way OneNote lays out its pages, or
|
||||
* unfolded under its own row. Chosen on the account, so the answer follows to
|
||||
* the phone — where there is room for neither and the chapter fills the
|
||||
* screen instead. */
|
||||
await addChapter("Verträge");
|
||||
await p.locator("[data-chapter-row]").first().click();
|
||||
await p.waitForTimeout(1200);
|
||||
check((await p.locator("[data-chapter-column]").count()) === 1, "a chapter opens beside the list by default");
|
||||
check(
|
||||
(await p.locator("[data-chapter-row]").count()) === 2,
|
||||
"and the notebook's own list keeps every chapter row",
|
||||
String(await p.locator("[data-chapter-row]").count())
|
||||
);
|
||||
|
||||
await p.getByTitle("Account").click();
|
||||
await p.waitForTimeout(800);
|
||||
await p.locator('[data-chapter-view="inline"]').click();
|
||||
await p.waitForTimeout(1500);
|
||||
await p.mouse.click(1380, 900); // outside the panel
|
||||
await p.waitForTimeout(1200);
|
||||
await p.locator("[data-chapter-row]").first().click();
|
||||
await p.waitForTimeout(1200);
|
||||
check((await p.locator("[data-chapter-notes]").count()) === 1, "the other setting unfolds it under its row");
|
||||
check((await p.locator("[data-chapter-column]").count()) === 0, "and there is no column then");
|
||||
|
||||
const me = await api(p, "GET", "/auth/me");
|
||||
check(me.body?.preferences?.chapterView === "inline", "the choice is on the account, not in this browser");
|
||||
|
||||
/* ── a chapter is a row like any other ──
|
||||
* Same level as notes and separators, so it can be dragged into place in the
|
||||
* arrangement rather than being stuck above everything. */
|
||||
await p.locator("[data-sort-button]").click();
|
||||
await p.waitForTimeout(300);
|
||||
await p.locator('[data-sort-option="manual"]').click();
|
||||
await p.waitForTimeout(1200);
|
||||
check(
|
||||
(await p.locator("[data-reorder-grip]").count()) >= 2,
|
||||
"a chapter has a grip like a note does",
|
||||
String(await p.locator("[data-reorder-grip]").count())
|
||||
);
|
||||
|
||||
/* ── its own colour ── */
|
||||
await p.locator("[data-chapter-row]").first().click({ button: "right", force: true });
|
||||
await p.waitForTimeout(600);
|
||||
await p.locator("[data-submenu-toggle]").filter({ hasText: "Colour" }).first().click();
|
||||
await p.waitForTimeout(500);
|
||||
await p.locator("[data-submenu] button").filter({ hasText: "Green" }).first().click();
|
||||
await p.waitForTimeout(2000);
|
||||
const coloured = (await api(p, "GET", "/chapters")).body;
|
||||
check(
|
||||
coloured.some((c) => c.color === "#2da44e"),
|
||||
"a chapter can be given its own colour",
|
||||
JSON.stringify(coloured.map((c) => [c.name, c.color]))
|
||||
);
|
||||
|
||||
/* ── a chapter is not a wastebasket ──
|
||||
* Removing one must never remove what it held: the notes come back to the
|
||||
* notebook, where they can be seen and filed again. */
|
||||
@@ -107,7 +165,11 @@ try {
|
||||
await p.getByRole("button", { name: "Remove", exact: true }).click();
|
||||
await p.waitForTimeout(2500);
|
||||
|
||||
check((await p.locator("[data-chapter-row]").count()) === 0, "the chapter is gone");
|
||||
check(
|
||||
(await p.locator("[data-chapter-row]").count()) === 1,
|
||||
"the chapter is gone, and only that one",
|
||||
String(await p.locator("[data-chapter-row]").count())
|
||||
);
|
||||
const after = (await titles()).sort().join(",");
|
||||
check(
|
||||
after === "Stromrechnung,Wasserrechnung,Zählerstand",
|
||||
|
||||
Reference in new issue
Block a user