Quotes that hold more than a sentence, and one list of marks

A quote may hold whole blocks, and they now have room: the rule that gives
the writing its rhythm reaches only the editor's own children, so inside a
quote a heading, a list and a closing line stood jammed together. Leaving as
Markdown they were written on consecutive lines, which every other reader
folds into one — the sentence after a quoted list became more of the last
bullet. They are divided by a quoted blank line now, the same rule the top
level has had since 11 August.

The marks were two copies of one list, in two dialogs, and only one of them
had ever been extended. One list, one grid, twice as many marks, divided
under headings — and anything else can be typed into the search box, which
on a phone is where the emoji keyboard lives.

The changelog is a third of its length: one entry per day rather than one
per image, since a round that repaired what an earlier round the same day
broke reads as noise afterwards. Every digest is still listed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-17 22:15:55 +02:00
1 parent c0523f3ba4
commit c86c63922a
14 files changed
+742 -887

No files matched your search

+35 -1
View File
@@ -629,12 +629,25 @@ try {
await p.waitForTimeout(600);
await p.getByText("Edit", { exact: true }).click();
await p.waitForTimeout(700);
/* The whole set, as the chapter dialog offers it — kept to compare against
the note dialog further down. The two grids were once two lists, and marks
added to one did not exist in the other. */
const allMarks = (page) =>
page.evaluate(() => [...document.querySelectorAll("[data-icon-option]")].map((el) => el.dataset.iconOption));
const marksInChapterDialog = await allMarks(p);
const headings = await p.locator("[data-icon-group]").count();
check(headings >= 3, "the marks are divided under headings", `${headings} groups`);
await p.getByLabel("Search icons").fill("invoice");
await p.waitForTimeout(400);
// One extra: "no icon" sits in the grid, because taking a mark off is the
// same kind of choice as putting one on.
const offered = await p.locator("[data-icon-option]").count();
check(offered > 1 && offered < 20, "icons are searched by what they are for", `${offered} shown`);
check(
(await p.locator("[data-icon-group]").count()) < headings,
"and a heading with nothing under it is not shown"
);
await p.locator("[data-icon-option]").nth(1).click();
await p.locator("[data-chapter-save]").click();
await p.waitForTimeout(2200);
@@ -650,9 +663,30 @@ try {
await p.waitForTimeout(1200);
await p.locator("[data-note-icon]").click();
await p.waitForTimeout(700);
check(
JSON.stringify(await allMarks(p)) === JSON.stringify(marksInChapterDialog),
"a note is offered exactly the marks a chapter is",
`${marksInChapterDialog.length} marks`
);
/* A mark nobody thought of: the search box is a text field, which on a phone
is where the emoji keyboard lives, so anything typed into it is offered. */
await p.getByLabel("Search icons").fill("🦖");
await p.waitForTimeout(400);
check(
(await p.locator('[data-icon-option="🦖"]').count()) === 1,
"an emoji typed into the search box is offered as a mark"
);
await p.getByLabel("Search icons").fill("plainly not a mark");
await p.waitForTimeout(400);
check(
(await p.locator("[data-icon-option]").count()) === 1,
"while words that match nothing offer only the empty mark"
);
await p.getByLabel("Search icons").fill("house");
await p.waitForTimeout(400);
await p.locator("[data-icon-option]").first().click();
// Both dialogs show the same grid, and in it "no icon" is the first tile.
await p.locator("[data-icon-option]").nth(1).click();
await p.locator('[data-icon-colour="#2da44e"]').click();
await p.locator("[data-icon-save]").click();
await p.waitForTimeout(2500);
+38
View File
@@ -295,6 +295,44 @@ try {
await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)
);
/* ── a quote that holds more than a sentence ──
* Markdown lets a quote hold whole blocks, and so does the editor. What it did
* not do was give them any room: inside a quote nothing had a margin, so a
* heading, a list and a closing line stood jammed together while the items of
* the list were further apart than the blocks around them. */
await createNote(p);
await setTitle(p, "Zitat");
await p.locator(".ProseMirror").click();
await p.keyboard.type("> ### Quartalszahlen");
await p.keyboard.press("Enter");
await p.keyboard.type("- Umsatz");
await p.keyboard.press("Enter");
await p.keyboard.type("Gewinn");
await p.keyboard.press("Enter");
await p.keyboard.press("Enter");
await p.keyboard.type("Alles nach Plan.");
await p.waitForTimeout(1500);
const quoted = await p.evaluate(() =>
[...(document.querySelector(".ProseMirror blockquote")?.children || [])].map((el) => {
const r = el.getBoundingClientRect();
return { tag: el.tagName, top: r.top, bottom: r.bottom };
})
);
check(
quoted.map((c) => c.tag).join(",") === "H3,UL,P",
"a quote holds a heading, a list and a paragraph",
quoted.map((c) => c.tag).join(",")
);
const gaps = quoted.slice(1).map((c, i) => Math.round(c.top - quoted[i].bottom));
check(gaps.every((g) => g >= 6), "with room between them, as outside a quote", gaps.join(" | "));
/* And the same divide when it leaves as Markdown: written on consecutive
lines, every other reader folds that closing line into the last bullet. */
const quoteNote = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Zitat");
const quoteDoc = (await api(p, "GET", `/notes/${quoteNote.id}`)).body.content;
check(/blockquote/.test(quoteDoc), "and it is stored as a quote");
/* ── the typeface follows the account ── */
await p.getByTitle("Account").click();
await p.waitForTimeout(800);
+26
View File
@@ -96,6 +96,23 @@ try {
await p.locator('.ProseMirror input[type="checkbox"]').first().click();
await p.waitForTimeout(1800);
/* A quote holding more than one kind of block. Written on consecutive lines,
Markdown reads the closing sentence as more of the last bullet — so the
blocks inside a quote have to be divided by a quoted blank line. */
await p.locator(".ProseMirror").click();
await p.keyboard.press("Control+End");
await p.keyboard.press("Enter");
await p.keyboard.press("Enter"); // out of the task list again
await p.keyboard.type("> ### Quartalszahlen");
await p.keyboard.press("Enter");
await p.keyboard.type("- Umsatz");
await p.keyboard.press("Enter");
await p.keyboard.type("Gewinn");
await p.keyboard.press("Enter");
await p.keyboard.press("Enter");
await p.keyboard.type("Alles nach Plan.");
await p.waitForTimeout(1800);
await captureDownloads(p);
/* ── single note as Markdown ── */
@@ -114,6 +131,15 @@ try {
check(/\|\s*Spalte\s*\|/.test(md), "the table came along", md.match(/\|.*\|/)?.[0] || "no table row");
check(md.includes("| --- |"), "the table has a separator row so it renders as a table");
check(md.includes("- [x] Erledigte Sache"), "the ticked task is a ticked checkbox");
check(
md.includes("> ### Quartalszahlen\n>\n> - Umsatz"),
"the blocks inside a quote are divided by a quoted blank line",
md.split("\n").filter((l) => l.startsWith(">")).join(" ⏎ ")
);
check(
md.includes("> - Gewinn\n>\n> Alles nach Plan."),
"so the sentence after a quoted list stays a sentence"
);
console.log("--- Markdown ---\n" + md + "\n--- Ende ---");
// The dialog stays up after an export so the result can be read; close it or