Write pinned notes in the margin, and give the phone a sheet instead

A note about a passage belongs beside the passage, not on top of it. Only a
numbered marker stays on the page now, with a hairline out to a card in the
margin, level with its anchor. Cards keep one height so two notes close together
can be laid out by pushing the second down; the one being written in grows and
rises above the rest.

A phone has no margin to write in — a page narrow enough to read leaves no white
space beside it, and taking a column would make the document unreadable. There
the note comes up from the bottom instead: over the page, only while open, within
reach of a thumb, with the passage still visible above it.

Reserving the column exposed a loop worth remembering. A flex item is as wide as
its content unless told otherwise, so the column made the viewer wider, which
made there be room for the column: the page ended up at 254px inside a 420px
phone while the layout insisted it had 611px. The viewer and the editor pane now
say minWidth: 0, and the toolbar scrolls rather than cutting its last controls
off on a narrow screen.

Splitting each row into page and margin also broke which pages counted as
visible — the check read data-pdf-page off the row, found nothing, and cancelled
every text layer before it rendered. It reads the row's own attribute now.

215 unit tests, 219 browser checks across eleven suites, including the phone
layout at 420px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-10 14:46:56 +02:00
1 parent 6438648313
commit 011ba82a94
7 files changed
+653 -208

No files matched your search

+115 -5
View File
@@ -480,11 +480,26 @@ try {
await p.mouse.click(page1.x + page1.width * 0.4, page1.y + page1.height * 0.3);
await p.waitForTimeout(800);
check((await p.locator("[data-pin]").count()) === 1, "clicking the page leaves a pin");
const bubble = p.getByLabel("Pinned note");
check(await bubble.isVisible(), "and opens straight into writing");
check((await p.locator("[data-pin]").count()) === 1, "clicking the page leaves a marker on the spot");
check(
(await p.locator("[data-pdf-margin]").count()) > 0,
"and the note itself belongs in the margin beside the page, not on it"
);
const field = p.getByLabel("Pinned note");
check(await field.isVisible(), "which opens straight into writing");
await bubble.fill("Drehmoment beachten");
/* The card has to be level with the passage — that is the whole point of a
margin note, and the reason it is not simply a list under the document. */
const levels = await p.evaluate(() => {
const marker = document.querySelector("[data-pin]").getBoundingClientRect();
const card = document.querySelector("[data-pin-card]").getBoundingClientRect();
const page = document.querySelector('[data-pdf-page="1"]').getBoundingClientRect();
return { gap: Math.abs(marker.top - card.top), pageRight: page.right, cardLeft: card.left };
});
check(levels.gap < 40, "the card sits level with its marker", `${Math.round(levels.gap)}px apart`);
check(levels.cardLeft >= levels.pageRight - 2, "and beside the page rather than over it");
await field.fill("Drehmoment beachten");
await p.getByRole("button", { name: "Fertig" }).click();
await p.waitForTimeout(2500);
@@ -539,11 +554,17 @@ try {
await p.waitForTimeout(4000);
check((await p.locator("[data-pin]").count()) === 1, "the pin survives a reload");
check(
(await p.locator("[data-pin-card]").innerText()).includes("Drehmoment"),
"with the text showing in the margin without being opened first",
await p.locator("[data-pin-card]").innerText()
);
await p.locator("[data-pin]").click();
await p.waitForTimeout(600);
check(
(await p.getByLabel("Pinned note").inputValue()) === "Drehmoment beachten",
"with the text that was written into it"
"and the same text when opened for editing"
);
/* Clearing a page's ink must not take its pinned notes: same page, different
@@ -610,6 +631,95 @@ try {
/* ── on a phone there is no margin to write in ── */
// A page narrow enough to read leaves no white space beside it, so taking a
// column for notes would make the document itself unreadable. The note comes
// up from the bottom instead — over the page, but only while it is open.
await p.locator("[data-pdf-pin-mode]").click();
await p.waitForTimeout(400);
const pageM = await p.locator('[data-pdf-page="1"]').boundingBox();
await p.mouse.click(pageM.x + pageM.width * 0.5, pageM.y + pageM.height * 0.4);
await p.waitForTimeout(800);
await p.getByLabel("Pinned note").fill("Am Rand notiert");
await p.getByRole("button", { name: "Fertig" }).click();
await p.waitForTimeout(2500);
const wideMargins = await p.locator("[data-pdf-margin]").count();
check(wideMargins > 0, "on a wide screen the note is in the margin", String(wideMargins));
const pageWithMargin = (await p.locator('[data-pdf-page="1"]').boundingBox()).width;
await p.setViewportSize({ width: 420, height: 820 });
await p.waitForTimeout(2000);
// Below the phone breakpoint the list and the editor take turns, so the note
// has to be opened again to be looking at the document at all.
// Shrinking a desktop window past the breakpoint leaves the sidebar and its
// backdrop up, which no real phone ever sees. Dismissed here so the rest of
// the section is testing the document and not an overlay.
if ((await p.locator("[data-pdf-page]").count()) === 0) {
await p.evaluate(() => document.querySelector("[data-note-row]")?.click());
await p.waitForTimeout(4500);
}
await p.mouse.click(410, 800);
await p.waitForTimeout(800);
check(
(await p.locator("[data-pdf-margin]").count()) === 0,
"on a narrow one the margin is gone",
String(await p.locator("[data-pdf-margin]").count())
);
check((await p.locator("[data-pin]").count()) > 0, "but the markers are still on their spots");
// The widest page should fill what the screen has, rather than share it with
// a column. (Page 1 is portrait in a document whose widest page is landscape,
// so it is legitimately narrower — page 2 is the one that sets the scale.)
void pageWithMargin;
const widest = (await p.locator('[data-pdf-page="2"]').boundingBox()).width;
check(
widest > 420 - 60,
"and the page gets the width the column was taking",
`${Math.round(widest)}px of 420`
);
/* Reserving a column made the viewer wider, which made there be room for the
column — the page ended up at 254px inside a 420px phone while the layout
insisted it had 611px. A flex item is as wide as its content unless told
otherwise. */
const fits = await p.evaluate(() => {
let el = document.querySelector("[data-pdf-page]")?.parentElement;
while (el && getComputedStyle(el).overflow !== "auto") el = el.parentElement;
return { scroller: el?.clientWidth ?? 0, window: window.innerWidth };
});
check(
fits.scroller <= fits.window,
"and the document area does not push itself past the window",
`${fits.scroller} vs ${fits.window}`
);
const marker = p.locator("[data-pin]").first();
const markerText = await marker.getAttribute("title");
await marker.scrollIntoViewIfNeeded();
await marker.click();
await p.waitForTimeout(800);
check((await p.locator("[data-pin-sheet]").count()) === 1, "tapping the marker opens a sheet");
check(
(await p.getByLabel("Pinned note").inputValue()) === markerText,
"with the note that was written on the desktop",
markerText
);
const sheet = await p.locator("[data-pin-sheet]").boundingBox();
check(
sheet.y + sheet.height > 700,
"at the bottom, where a thumb reaches",
`top ${Math.round(sheet.y)} of 820`
);
await p.getByRole("button", { name: "Fertig" }).click();
await p.waitForTimeout(800);
check((await p.locator("[data-pin-sheet]").count()) === 0, "and closes again");
await p.setViewportSize({ width: 1400, height: 950 });
await p.waitForTimeout(2000);
/* ── the notes panel lives in the same toolbar now ── */
check(
(await p.locator("[data-pdf-toggle-notes]").count()) === 1,